Skip to content

Commit

Permalink
load balancer! (#3230)
Browse files Browse the repository at this point in the history
* start of load balancer

* add configuration options; option to load balance per deck

* formatting

* clippy

* add myself to contributors

* cleanup

* cargo fmt

* copyright header on load_balancer.rs

* remove extra space

* more formatting

* python formatting

* ignore this being None

only doing this cause python has awful lambdas and can't
loop in a meaningful way without doing this

* only calculate notes on each day if we are trying to avoid siblings

* don't fuzz intervals if the load balancer is enabled

* force generator to eval so this actually happens

* load balance instead of fuzzing, rather than in addition to

* use builtin fuzz_bounds rather than reinvent something new

* print some debug info on how its load balancing

* clippy

* more accurately load balance only when we want to fuzz

* incorrectly doublechecking the presence of the load balancer

* more printfs for debugging

* avoid siblings -> disperse siblings

* load balance learning graduating intervals

* load balancer: respect min/max intervals; graduating easy should be at least +1 good

* filter out after-days under minimum interval

* this is an inclusive check

* switch load balancer to caching instead of on the fly calculation

* handle case where load balancer would balance outside of its bounds

* disable lb when unselecting it in preferences

* call load_balancer in StateContext::with_review_fuzz instead of next to

* rebuild load balancer when card queue is rebuilt

* remove now-unused configuration options

* add note option to notetype to enable/disable sibling dispersion

* add options to exclude decks from load balancing

* theres a lint checking that the link actually exists so I guess I'll add the anchor back in later?

* how did I even update this

* move load balancer to cardqueue

* remove per-deck balancing options

* improve determining whether to disperse siblings when load balancing

* don't recalculate notes on days every time

* remove debug code

* remove all configuration; load balancer enabled by default; disperse siblings if bury_reviews is set

* didn't fully remove caring about decks from load balancer sql query

* load balancer should only count cards in the same preset

* fuzz interval if its outside of load balancer's range

* also check minimum when bailing out of load balancer

* cleanup; make tests happy

* experimental weight-based load balance fuzzing

* take into account interval when weighting as it seems to help

* if theres no cards the interval weight is just 1.0

* make load balancer disableable through debug console

* remove debug prints

* typo

* remove debugging print

* explain a bit how load balancer works

* properly balance per preset

* use inclusive range rather than +1

* -1 type cast

* move type hint somewhere less ugly; fix comment typo

* Reuse existing deck list from parent function (dae)

Minor optimisation
  • Loading branch information
jakeprobst committed Aug 17, 2024
1 parent a87a44d commit c6cb4e4
Show file tree
Hide file tree
Showing 14 changed files with 369 additions and 5 deletions.
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)

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

0 comments on commit c6cb4e4

Please sign in to comment.