Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

load balancer! #3230

Merged
merged 65 commits into from
Aug 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
b1836ed
start of load balancer
jakeprobst Jun 10, 2024
5bb8959
add configuration options; option to load balance per deck
jakeprobst Jun 11, 2024
ac12e13
formatting
jakeprobst Jun 11, 2024
9c96fe5
clippy
jakeprobst Jun 11, 2024
25a8549
add myself to contributors
jakeprobst Jun 11, 2024
cd5e0df
cleanup
jakeprobst Jun 11, 2024
9595f6c
cargo fmt
jakeprobst Jun 11, 2024
63fa18b
copyright header on load_balancer.rs
jakeprobst Jun 11, 2024
a2bad0a
remove extra space
jakeprobst Jun 11, 2024
d53e248
more formatting
jakeprobst Jun 11, 2024
be92048
python formatting
jakeprobst Jun 11, 2024
8e28b71
ignore this being None
jakeprobst Jun 11, 2024
370eef6
only calculate notes on each day if we are trying to avoid siblings
jakeprobst Jun 13, 2024
cc3e991
don't fuzz intervals if the load balancer is enabled
jakeprobst Jun 13, 2024
98b3e79
force generator to eval so this actually happens
jakeprobst Jun 14, 2024
4972ef0
load balance instead of fuzzing, rather than in addition to
jakeprobst Jun 14, 2024
672021d
use builtin fuzz_bounds rather than reinvent something new
jakeprobst Jun 14, 2024
901bec7
print some debug info on how its load balancing
jakeprobst Jun 14, 2024
d16d4e3
clippy
jakeprobst Jun 14, 2024
e3a2d85
more accurately load balance only when we want to fuzz
jakeprobst Jun 14, 2024
7ff398a
incorrectly doublechecking the presence of the load balancer
jakeprobst Jun 14, 2024
47feb8e
more printfs for debugging
jakeprobst Jun 14, 2024
e80f90d
avoid siblings -> disperse siblings
jakeprobst Jun 14, 2024
ec6e9c0
load balance learning graduating intervals
jakeprobst Jun 25, 2024
a539954
load balancer: respect min/max intervals; graduating easy should be a…
jakeprobst Jun 27, 2024
b02cf01
filter out after-days under minimum interval
jakeprobst Jun 27, 2024
31691ed
this is an inclusive check
jakeprobst Jun 27, 2024
54521fb
switch load balancer to caching instead of on the fly calculation
jakeprobst Jul 12, 2024
267e255
Merge branch 'main' into load_balancer
jakeprobst Jul 12, 2024
7a6b42c
handle case where load balancer would balance outside of its bounds
jakeprobst Jul 12, 2024
8b93c39
disable lb when unselecting it in preferences
jakeprobst Jul 12, 2024
f3fd530
call load_balancer in StateContext::with_review_fuzz instead of next to
jakeprobst Jul 12, 2024
daeb154
rebuild load balancer when card queue is rebuilt
jakeprobst Jul 13, 2024
f9a0073
remove now-unused configuration options
jakeprobst Jul 13, 2024
17569e7
add note option to notetype to enable/disable sibling dispersion
jakeprobst Jul 13, 2024
d52afcd
add options to exclude decks from load balancing
jakeprobst Jul 14, 2024
de017e1
theres a lint checking that the link actually exists so I guess I'll …
jakeprobst Jul 14, 2024
033d757
Merge branch 'main' into load_balancer
jakeprobst Jul 22, 2024
4081f22
how did I even update this
jakeprobst Jul 22, 2024
291d8d1
move load balancer to cardqueue
jakeprobst Jul 23, 2024
7441395
remove per-deck balancing options
jakeprobst Jul 24, 2024
3395349
improve determining whether to disperse siblings when load balancing
jakeprobst Jul 24, 2024
580ea5e
don't recalculate notes on days every time
jakeprobst Jul 24, 2024
4ae66ae
Merge branch 'main' into load_balancer
dae Aug 5, 2024
ed97e8d
remove debug code
jakeprobst Aug 5, 2024
aff9087
remove all configuration; load balancer enabled by default; disperse …
jakeprobst Aug 5, 2024
6b8d9f8
didn't fully remove caring about decks from load balancer sql query
jakeprobst Aug 5, 2024
50e99fe
load balancer should only count cards in the same preset
jakeprobst Aug 5, 2024
0281aff
fuzz interval if its outside of load balancer's range
jakeprobst Aug 5, 2024
6a2b5b9
also check minimum when bailing out of load balancer
jakeprobst Aug 5, 2024
b7d0e59
cleanup; make tests happy
jakeprobst Aug 5, 2024
a3104ba
experimental weight-based load balance fuzzing
jakeprobst Aug 8, 2024
b4207e2
take into account interval when weighting as it seems to help
jakeprobst Aug 10, 2024
bcd56a1
if theres no cards the interval weight is just 1.0
jakeprobst Aug 13, 2024
72f9b69
make load balancer disableable through debug console
jakeprobst Aug 14, 2024
73f38d4
remove debug prints
jakeprobst Aug 14, 2024
bcfcfc9
typo
jakeprobst Aug 14, 2024
1d8882a
remove debugging print
jakeprobst Aug 17, 2024
f98210f
explain a bit how load balancer works
jakeprobst Aug 17, 2024
ad6d242
properly balance per preset
jakeprobst Aug 17, 2024
af45ea3
use inclusive range rather than +1
jakeprobst Aug 17, 2024
9e13e10
Merge remote-tracking branch 'upstream/main' into load_balancer
jakeprobst Aug 17, 2024
2742d75
-1 type cast
jakeprobst Aug 17, 2024
32840e1
move type hint somewhere less ugly; fix comment typo
jakeprobst Aug 17, 2024
edbe52c
Reuse existing deck list from parent function
dae Aug 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ James Elmore <[email protected]>
Ian Samir Yep Manzano <https://github.com/isym444>
David Culley <[email protected]>
Rastislav Kish <[email protected]>
jake <[email protected]>
Expertium <https://github.com/Expertium>
Christian Donat <https://github.com/cdonat2>
Asuka Minato <https://asukaminato.eu.org>
Expand Down
2 changes: 2 additions & 0 deletions proto/anki/config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ message ConfigKey {
RANDOM_ORDER_REPOSITION = 23;
SHIFT_POSITION_OF_EXISTING_CARDS = 24;
RENDER_LATEX = 25;
LOAD_BALANCER_ENABLED = 26;
}
enum String {
SET_DUE_BROWSER = 0;
Expand Down Expand Up @@ -115,6 +116,7 @@ message Preferences {
bool show_remaining_due_counts = 3;
bool show_intervals_on_buttons = 4;
uint32 time_limit_secs = 5;
bool load_balancer_enabled = 6;
}
message Editing {
bool adding_defaults_to_current_deck = 1;
Expand Down
10 changes: 10 additions & 0 deletions pylib/anki/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,16 @@ def set_aux_template_config(
)
return self.set_config(key, value, undoable=undoable)

def _get_enable_load_balancer(self) -> bool:
return self.get_config_bool(Config.Bool.LOAD_BALANCER_ENABLED)

def _set_enable_load_balancer(self, value: bool) -> None:
self.set_config_bool(Config.Bool.LOAD_BALANCER_ENABLED, value)
Comment on lines +975 to +979
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this change, I get these two type errors from mypy:

pylib/anki/collection.py:976: error: "type[Bool]" has no attribute "LOAD_BALANCER_ENABLED"  [attr-defined]
pylib/anki/collection.py:979: error: "type[Bool]" has no attribute "LOAD_BALANCER_ENABLED"  [attr-defined]

Do you get them too @jakeprobst? Or is this a misconfiguration on my personal system?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

regen your protos (probably)

Copy link
Contributor

@davidculley davidculley Aug 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do I do that?

Edit: I found out how. In case someone else discovers this comment, I documented how you generate the required Python code from the *.proto files in #3390.


load_balancer_enabled = property(
fget=_get_enable_load_balancer, fset=_set_enable_load_balancer
)

# Stats
##########################################################################

Expand Down
1 change: 1 addition & 0 deletions rslib/src/backend/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ impl From<BoolKeyProto> for BoolKey {
BoolKeyProto::RandomOrderReposition => BoolKey::RandomOrderReposition,
BoolKeyProto::ShiftPositionOfExistingCards => BoolKey::ShiftPositionOfExistingCards,
BoolKeyProto::RenderLatex => BoolKey::RenderLatex,
BoolKeyProto::LoadBalancerEnabled => BoolKey::LoadBalancerEnabled,
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions rslib/src/config/bool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub enum BoolKey {
WithScheduling,
WithDeckConfigs,
Fsrs,
LoadBalancerEnabled,
#[strum(to_string = "normalize_note_text")]
NormalizeNoteText,
#[strum(to_string = "dayLearnFirst")]
Expand Down Expand Up @@ -73,6 +74,7 @@ impl Collection {
| BoolKey::CardCountsSeparateInactive
| BoolKey::RestorePositionBrowser
| BoolKey::RestorePositionReviewer
| BoolKey::LoadBalancerEnabled
| BoolKey::NormalizeNoteText => self.get_config_optional(key).unwrap_or(true),

// other options default to false
Expand Down
3 changes: 3 additions & 0 deletions rslib/src/preferences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ impl Collection {
show_intervals_on_buttons: self
.get_config_bool(BoolKey::ShowIntervalsAboveAnswerButtons),
time_limit_secs: self.get_answer_time_limit_secs(),
load_balancer_enabled: self.get_config_bool(BoolKey::LoadBalancerEnabled),
})
}

Expand All @@ -117,6 +118,8 @@ impl Collection {
s.show_intervals_on_buttons,
)?;
self.set_answer_time_limit_secs(s.time_limit_secs)?;
self.set_config_bool_inner(BoolKey::LoadBalancerEnabled, s.load_balancer_enabled)?;

Ok(())
}

Expand Down
53 changes: 51 additions & 2 deletions rslib/src/scheduler/answering/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use revlog::RevlogEntryPartial;

use super::fsrs::weights::ignore_revlogs_before_ms_from_config;
use super::queue::BuryMode;
use super::states::load_balancer::LoadBalancerContext;
use super::states::steps::LearningSteps;
use super::states::CardState;
use super::states::FilteredState;
Expand All @@ -26,6 +27,7 @@ use super::timespan::answer_button_time_collapsible;
use super::timing::SchedTimingToday;
use crate::card::CardQueue;
use crate::card::CardType;
use crate::config::BoolKey;
use crate::deckconfig::DeckConfig;
use crate::deckconfig::LeechAction;
use crate::decks::Deck;
Expand Down Expand Up @@ -77,7 +79,10 @@ impl CardStateUpdater {
/// Returns information required when transitioning from one card state to
/// another with `next_states()`. This separate structure decouples the
/// state handling code from the rest of the Anki codebase.
pub(crate) fn state_context(&self) -> StateContext<'_> {
pub(crate) fn state_context<'a>(
&'a self,
load_balancer: Option<LoadBalancerContext<'a>>,
) -> StateContext<'a> {
StateContext {
fuzz_factor: get_fuzz_factor(self.fuzz_seed),
steps: self.learn_steps(),
Expand All @@ -89,6 +94,8 @@ impl CardStateUpdater {
interval_multiplier: self.config.inner.interval_multiplier,
maximum_review_interval: self.config.inner.maximum_review_interval,
leech_threshold: self.config.inner.leech_threshold,
load_balancer: load_balancer
.map(|load_balancer| load_balancer.set_fuzz_seed(self.fuzz_seed)),
relearn_steps: self.relearn_steps(),
lapse_multiplier: self.config.inner.lapse_multiplier,
minimum_lapse_interval: self.config.inner.minimum_lapse_interval,
Expand Down Expand Up @@ -215,9 +222,36 @@ impl Collection {
/// Return the next states that will be applied for each answer button.
pub fn get_scheduling_states(&mut self, cid: CardId) -> Result<SchedulingStates> {
let card = self.storage.get_card(cid)?.or_not_found(cid)?;
let deck = self.get_deck(card.deck_id)?.or_not_found(card.deck_id)?;

let note_id = deck
.config_id()
.map(|deck_config_id| self.get_deck_config(deck_config_id, false))
.transpose()?
.flatten()
.map(|deck_config| deck_config.inner.bury_reviews)
.unwrap_or(false)
.then_some(card.note_id);

let ctx = self.card_state_updater(card)?;
let current = ctx.current_card_state();
let state_ctx = ctx.state_context();

let load_balancer = self
.get_config_bool(BoolKey::LoadBalancerEnabled)
.then(|| {
let deckconfig_id = deck.config_id();

self.state.card_queues.as_ref().and_then(|card_queues| {
Some(
card_queues
.load_balancer
.review_context(note_id, deckconfig_id?),
)
})
})
.flatten();

let state_ctx = ctx.state_context(load_balancer);
Ok(current.next_states(&state_ctx))
}

Expand Down Expand Up @@ -305,11 +339,26 @@ impl Collection {
card.custom_data = data;
card.validate_custom_data()?;
}

self.update_card_inner(&mut card, original, usn)?;
if answer.new_state.leeched() {
self.add_leech_tag(card.note_id)?;
}

if card.queue == CardQueue::Review {
let deck = self.get_deck(card.deck_id)?;
if let Some(card_queues) = self.state.card_queues.as_mut() {
if let Some(deckconfig_id) = deck.and_then(|deck| deck.config_id()) {
card_queues.load_balancer.add_card(
card.id,
card.note_id,
deckconfig_id,
card.interval,
)
}
}
}

self.update_queues_after_answering_card(
&card,
timing,
Expand Down
12 changes: 11 additions & 1 deletion rslib/src/scheduler/queue/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::deckconfig::ReviewCardOrder;
use crate::deckconfig::ReviewMix;
use crate::decks::limits::LimitTreeMap;
use crate::prelude::*;
use crate::scheduler::states::load_balancer::LoadBalancer;
use crate::scheduler::timing::SchedTimingToday;

/// Temporary holder for review cards that will be built into a queue.
Expand Down Expand Up @@ -99,13 +100,14 @@ pub(super) struct QueueSortOptions {
pub(super) new_review_mix: ReviewMix,
}

#[derive(Debug, Clone)]
#[derive(Debug)]
pub(super) struct QueueBuilder {
pub(super) new: Vec<NewCard>,
pub(super) review: Vec<DueCard>,
pub(super) learning: Vec<DueCard>,
pub(super) day_learning: Vec<DueCard>,
limits: LimitTreeMap,
load_balancer: LoadBalancer,
context: Context,
}

Expand Down Expand Up @@ -144,12 +146,19 @@ impl QueueBuilder {
let sort_options = sort_options(&root_deck, &config_map);
let deck_map = col.storage.get_decks_map()?;

let did_to_dcid = deck_map
.values()
.filter_map(|deck| Some((deck.id, deck.config_id()?)))
.collect::<HashMap<_, _>>();
let load_balancer = LoadBalancer::new(timing.days_elapsed, did_to_dcid, &col.storage)?;

Ok(QueueBuilder {
new: Vec::new(),
review: Vec::new(),
learning: Vec::new(),
day_learning: Vec::new(),
limits,
load_balancer,
context: Context {
timing,
config_map,
Expand Down Expand Up @@ -201,6 +210,7 @@ impl QueueBuilder {
learn_ahead_secs,
current_day: self.context.timing.days_elapsed,
build_time: TimestampMillis::now(),
load_balancer: self.load_balancer,
current_learning_cutoff: now,
}
}
Expand Down
2 changes: 2 additions & 0 deletions rslib/src/scheduler/queue/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use self::undo::QueueUpdate;
use super::states::SchedulingStates;
use super::timing::SchedTimingToday;
use crate::prelude::*;
use crate::scheduler::states::load_balancer::LoadBalancer;
use crate::timestamp::TimestampSecs;

#[derive(Debug)]
Expand All @@ -37,6 +38,7 @@ pub(crate) struct CardQueues {
/// counts are zero. Ensures we don't show a newly-due learning card after a
/// user returns from editing a review card.
current_learning_cutoff: TimestampSecs,
pub(crate) load_balancer: LoadBalancer,
}

#[derive(Debug, Copy, Clone)]
Expand Down
12 changes: 12 additions & 0 deletions rslib/src/scheduler/queue/undo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ impl Collection {
}
queues.push_undo_entry(update.entry);
}

if let Some(card_queues) = self.state.card_queues.as_mut() {
match &update.entry {
QueueEntry::IntradayLearning(entry) => {
card_queues.load_balancer.remove_card(entry.id);
}
QueueEntry::Main(entry) => {
card_queues.load_balancer.remove_card(entry.id);
}
}
}

self.save_undo(UndoableQueueChange::CardAnswerUndone(update));

Ok(())
Expand Down
7 changes: 5 additions & 2 deletions rslib/src/scheduler/states/fuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ static FUZZ_RANGES: [FuzzRange; 3] = [
impl<'a> StateContext<'a> {
/// Apply fuzz, respecting the passed bounds.
pub(crate) fn with_review_fuzz(&self, interval: f32, minimum: u32, maximum: u32) -> u32 {
with_review_fuzz(self.fuzz_factor, interval, minimum, maximum)
self.load_balancer
.as_ref()
.and_then(|load_balancer| load_balancer.find_interval(interval, minimum, maximum))
.unwrap_or_else(|| with_review_fuzz(self.fuzz_factor, interval, minimum, maximum))
}
}

Expand Down Expand Up @@ -74,7 +77,7 @@ pub(crate) fn with_review_fuzz(
/// Return the bounds of the fuzz range, respecting `minimum` and `maximum`.
/// Ensure the upper bound is larger than the lower bound, if `maximum` allows
/// it and it is larger than 1.
fn constrained_fuzz_bounds(interval: f32, minimum: u32, maximum: u32) -> (u32, u32) {
pub(crate) fn constrained_fuzz_bounds(interval: f32, minimum: u32, maximum: u32) -> (u32, u32) {
let minimum = minimum.min(maximum);
let interval = interval.clamp(minimum as f32, maximum as f32);
let (mut lower, mut upper) = fuzz_bounds(interval);
Expand Down
Loading