Skip to content

Commit b5f1532

Browse files
authored
load balancer: improve sibling dispersal (ankitects#4640)
the load balancer only looks at cards in the same preset when balancing cards. which means that if a sibling is in a different preset, it doesn't get avoided. this is not ideal. so heres a fix. and while I was here I added some extra weighting so it tries to spread siblings out a bit.
1 parent e412ea7 commit b5f1532

File tree

1 file changed

+77
-20
lines changed

1 file changed

+77
-20
lines changed

rslib/src/scheduler/states/load_balancer.rs

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,14 @@ const MAX_LOAD_BALANCE_INTERVAL: usize = 90;
2424
// cache. a flat 10% increase over the max interval should be enough to not have
2525
// problems
2626
const LOAD_BALANCE_DAYS: usize = (MAX_LOAD_BALANCE_INTERVAL as f32 * 1.1) as usize;
27-
const SIBLING_PENALTY: f32 = 0.001;
27+
// when bury siblings is enabled, we try and make it so siblings are not
28+
// scheduled on the same days. a day with a sibling is set to a very low
29+
// (non-zero to make algorithms simpler) weight. to further disperse siblings,
30+
// closer days are given lower weights. days right before/after have a 0.2
31+
// weight modifier, 2 days before/after have a 0.4 weight modifier, etc
32+
const SIBLING_MODIFIER_STEPS: [i32; 11] = [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5];
33+
const SIBLING_MODIFIER_RANGE: [f32; 11] =
34+
[1.0, 0.8, 0.6, 0.4, 0.2, 0.000001, 0.2, 0.4, 0.6, 0.8, 1.0];
2835

2936
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3037
pub enum EasyDay {
@@ -137,7 +144,6 @@ impl LoadBalancer {
137144
HashMap::<_, Vec<_>>::new(),
138145
|mut day_group_by_dcid, (cid, nid, dcid)| {
139146
day_group_by_dcid.entry(dcid).or_default().push((cid, nid));
140-
141147
day_group_by_dcid
142148
},
143149
)
@@ -237,25 +243,23 @@ impl LoadBalancer {
237243
let easy_days_load = self.easy_days_percentages_by_preset.get(&deckconfig_id)?;
238244
let easy_days_modifier =
239245
calculate_easy_days_modifiers(easy_days_load, &weekdays, &review_counts);
240-
241-
let intervals = interval_days
242-
.iter()
246+
let sibling_modifier =
247+
calculate_sibling_modifiers(&self.days_by_preset, before_days, after_days, note_id);
248+
let intervals = review_counts
249+
.into_iter()
250+
.zip(easy_days_modifier)
251+
.zip(sibling_modifier)
243252
.enumerate()
244-
.map(|(interval_index, interval_day)| {
245-
LoadBalancerInterval {
246-
target_interval: interval_index as u32 + before_days,
247-
review_count: review_counts[interval_index],
248-
// if there is a sibling on this day, give it a very low weight
249-
sibling_modifier: note_id
250-
.and_then(|note_id| {
251-
interval_day
252-
.has_sibling(&note_id)
253-
.then_some(SIBLING_PENALTY)
254-
})
255-
.unwrap_or(1.0),
256-
easy_days_modifier: easy_days_modifier[interval_index],
257-
}
258-
});
253+
.map(
254+
|(interval_index, ((review_count, easy_days_modifier), sibling_modifier))| {
255+
LoadBalancerInterval {
256+
target_interval: interval_index as u32 + before_days,
257+
review_count,
258+
sibling_modifier,
259+
easy_days_modifier,
260+
}
261+
},
262+
);
259263

260264
select_weighted_interval(intervals, fuzz_seed)
261265
}
@@ -352,6 +356,59 @@ pub(crate) fn calculate_easy_days_modifiers(
352356
.collect()
353357
}
354358

359+
// gently nudge siblings so they don't clump together
360+
// all deckconfigs need to be searched, rather than just the day, because
361+
// siblings might have different deckconfigs.
362+
// for example, a card is being scheduled and days 2 and 8 have siblings:
363+
// X X
364+
// days: 0 1 2 3 4 5 6 7 8 9 10
365+
// 2 mod: 0.40 0.20 0.00 0.20 0.40 0.60 0.80
366+
// 8 mod: 0.80 0.60 0.40 0.20 0.00 0.20 0.40
367+
// total: 0.40 0.20 0.00 0.20 0.32 0.36 0.32 0.20 0.00 0.20 0.40
368+
// 0.00 is actually a not-actually-zero-but-really small number for ease of
369+
// implementation
370+
fn calculate_sibling_modifiers(
371+
days_by_preset: &HashMap<DeckConfigId, [LoadBalancerDay; LOAD_BALANCE_DAYS]>,
372+
before_days: u32,
373+
after_days: u32,
374+
nid: Option<NoteId>,
375+
) -> Vec<f32> {
376+
let mut modifiers = vec![1.0; after_days as usize - before_days as usize + 1];
377+
378+
if let Some(nid) = nid {
379+
let sibling_days = days_by_preset
380+
.iter()
381+
.flat_map(|(_did, days)| {
382+
days.iter()
383+
.enumerate()
384+
.fold(HashSet::new(), |mut sibling_days, (i, day)| {
385+
if day.has_sibling(&nid) {
386+
sibling_days.insert(i);
387+
}
388+
sibling_days
389+
})
390+
})
391+
.collect::<HashSet<_>>();
392+
393+
for sibling_day in sibling_days {
394+
let sibling_iter = SIBLING_MODIFIER_STEPS
395+
.iter()
396+
.zip(SIBLING_MODIFIER_RANGE.iter());
397+
for (step, sibling_modifier_value) in sibling_iter {
398+
// converts true interval to modifier-space
399+
let target_day = sibling_day as i32 + step - before_days as i32;
400+
if let Ok(target_day) = TryInto::<usize>::try_into(target_day) {
401+
if let Some(day_modifier) = modifiers.get_mut(target_day) {
402+
*day_modifier *= sibling_modifier_value;
403+
}
404+
}
405+
}
406+
}
407+
}
408+
409+
modifiers
410+
}
411+
355412
pub struct LoadBalancerInterval {
356413
pub target_interval: u32,
357414
pub review_count: usize,

0 commit comments

Comments
 (0)