From e5b7ed6cad95fb079f235e410979defddfde1a3a Mon Sep 17 00:00:00 2001 From: Daniel Pechersky Date: Sun, 14 Sep 2025 11:33:59 +0700 Subject: [PATCH 01/14] Simplify update_memory_state --- rslib/src/scheduler/fsrs/memory_state.rs | 167 ++++++++++++----------- 1 file changed, 85 insertions(+), 82 deletions(-) diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 420ead5a3..7eb0c8fbb 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -80,11 +80,7 @@ impl Collection { SearchBuilder::all([search.into(), SearchNode::State(StateKind::New).negated()]); let revlog = self.revlog_for_srs(search)?; let reschedule = req.as_ref().map(|e| e.reschedule).unwrap_or_default(); - let last_revlog_info = if reschedule { - Some(get_last_revlog_info(&revlog)) - } else { - None - }; + let last_revlog_info = reschedule.then(|| get_last_revlog_info(&revlog)); let mut rescheduler = self .get_config_bool(BoolKey::LoadBalancerEnabled) .then(|| Rescheduler::new(self)) @@ -101,12 +97,26 @@ impl Collection { )?; let mut progress = self.new_progress_handler::(); progress.update(false, |s| s.total_cards = items.len() as u32)?; + + let Some(req) = &req else { + // clear FSRS data if FSRS is disabled + for (idx, (card_id, _)) in items.into_iter().enumerate() { + progress.update(true, |state| state.current_cards = idx as u32 + 1)?; + let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; + let original = card.clone(); + card.clear_fsrs_data(); + self.update_card_inner(&mut card, original, usn)?; + } + return Ok(()); + }; + let preset_desired_retention = req.preset_desired_retention; + for (idx, (card_id, item)) in items.into_iter().enumerate() { progress.update(true, |state| state.current_cards = idx as u32 + 1)?; let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; let original = card.clone(); - if let Some(req) = &req { - let preset_desired_retention = req.preset_desired_retention; + + 'update_card: { // Store decay and desired retention in the card so that add-ons, card info, // stats and browser search/sorts don't need to access the deck config. // Unlike memory states, scheduler doesn't use decay and dr stored in the card. @@ -117,85 +127,78 @@ impl Collection { .unwrap_or(&preset_desired_retention); card.desired_retention = Some(desired_retention); card.decay = decay; - if let Some(item) = item { - card.set_memory_state(&fsrs, Some(item), historical_retention.unwrap())?; - // if rescheduling - if let Some(reviews) = &last_revlog_info { - // and we have a last review time for the card - if let Some(last_info) = reviews.get(&card.id) { - if let Some(last_review) = &last_info.last_reviewed_at { - let days_elapsed = - timing.next_day_at.elapsed_days_since(*last_review) as i32; - // and the card's not new - if let Some(state) = &card.memory_state { - // or in (re)learning - if card.ctype == CardType::Review { - let deck = self - .get_deck(card.original_or_current_deck_id())? - .or_not_found(card.original_or_current_deck_id())?; - let deckconfig_id = deck.config_id().unwrap(); - // reschedule it - let original_interval = card.interval; - let interval = fsrs.next_interval( - Some(state.stability), - desired_retention, - 0, - ); - card.interval = rescheduler - .as_mut() - .and_then(|r| { - r.find_interval( - interval, - 1, - req.max_interval, - days_elapsed as u32, - deckconfig_id, - get_fuzz_seed(&card, true), - ) - }) - .unwrap_or_else(|| { - with_review_fuzz( - card.get_fuzz_factor(true), - interval, - 1, - req.max_interval, - ) - }); - let due = if card.original_due != 0 { - &mut card.original_due - } else { - &mut card.due - }; - let new_due = (timing.days_elapsed as i32) - - days_elapsed - + card.interval as i32; - if let Some(rescheduler) = &mut rescheduler { - rescheduler.update_due_cnt_per_day( - *due, - new_due, - deckconfig_id, - ); - } - *due = new_due; - // Add a rescheduled revlog entry - self.log_rescheduled_review( - &card, - original_interval, - usn, - )?; - } - } - } - } - } - } else { + let Some(item) = item else { // clear memory states if item is None card.memory_state = None; + break 'update_card; + }; + card.set_memory_state(&fsrs, Some(item), historical_retention.unwrap())?; + + // if rescheduling + let Some(reviews) = &last_revlog_info else { + break 'update_card; + }; + + // and we have a last review time for the card + let Some(last_info) = reviews.get(&card.id) else { + break 'update_card; + }; + let Some(last_review) = &last_info.last_reviewed_at else { + break 'update_card; + }; + + // and the card's not new + let Some(state) = &card.memory_state else { + break 'update_card; + }; + // or in (re)learning + if card.ctype != CardType::Review { + break 'update_card; + }; + + let deck = self + .get_deck(card.original_or_current_deck_id())? + .or_not_found(card.original_or_current_deck_id())?; + let deckconfig_id = deck.config_id().unwrap(); + // reschedule it + let days_elapsed = timing.next_day_at.elapsed_days_since(*last_review) as i32; + let original_interval = card.interval; + let interval = fsrs.next_interval(Some(state.stability), desired_retention, 0); + card.interval = rescheduler + .as_mut() + .and_then(|r| { + r.find_interval( + interval, + 1, + req.max_interval, + days_elapsed as u32, + deckconfig_id, + get_fuzz_seed(&card, true), + ) + }) + .unwrap_or_else(|| { + with_review_fuzz( + card.get_fuzz_factor(true), + interval, + 1, + req.max_interval, + ) + }); + let due = if card.original_due != 0 { + &mut card.original_due + } else { + &mut card.due + }; + let new_due = + (timing.days_elapsed as i32) - days_elapsed + card.interval as i32; + if let Some(rescheduler) = &mut rescheduler { + rescheduler.update_due_cnt_per_day(*due, new_due, deckconfig_id); } - } else { - // clear FSRS data if FSRS is disabled - card.clear_fsrs_data(); + *due = new_due; + // Add a rescheduled revlog entry + self.log_rescheduled_review(&card, original_interval, usn)?; } + self.update_card_inner(&mut card, original, usn)?; } } From a5e9a063e5a1a72ad042b370e16bc3753469edbc Mon Sep 17 00:00:00 2001 From: Daniel Pechersky Date: Sun, 14 Sep 2025 18:43:43 +0700 Subject: [PATCH 02/14] Try speeding up update_memory_state by parallelizing card.set_memory_state --- rslib/src/scheduler/fsrs/memory_state.rs | 66 +++++++++++++++--------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 7eb0c8fbb..3b31c701a 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -10,6 +10,7 @@ use fsrs::FSRS; use fsrs::FSRS5_DEFAULT_DECAY; use fsrs::FSRS6_DEFAULT_DECAY; use itertools::Itertools; +use rayon::iter::{IntoParallelRefMutIterator as _, ParallelIterator as _}; use super::params::ignore_revlogs_before_ms_from_config; use super::rescheduler::Rescheduler; @@ -111,49 +112,60 @@ impl Collection { }; let preset_desired_retention = req.preset_desired_retention; + let mut to_update_memory_state = Vec::new(); for (idx, (card_id, item)) in items.into_iter().enumerate() { progress.update(true, |state| state.current_cards = idx as u32 + 1)?; let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; let original = card.clone(); - 'update_card: { - // Store decay and desired retention in the card so that add-ons, card info, - // stats and browser search/sorts don't need to access the deck config. - // Unlike memory states, scheduler doesn't use decay and dr stored in the card. - let deck_id = card.original_or_current_deck_id(); - let desired_retention = *req - .deck_desired_retention - .get(&deck_id) - .unwrap_or(&preset_desired_retention); - card.desired_retention = Some(desired_retention); - card.decay = decay; - let Some(item) = item else { - // clear memory states if item is None - card.memory_state = None; - break 'update_card; - }; - card.set_memory_state(&fsrs, Some(item), historical_retention.unwrap())?; + // Store decay and desired retention in the card so that add-ons, card info, + // stats and browser search/sorts don't need to access the deck config. + // Unlike memory states, scheduler doesn't use decay and dr stored in the card. + let deck_id = card.original_or_current_deck_id(); + let desired_retention = *req + .deck_desired_retention + .get(&deck_id) + .unwrap_or(&preset_desired_retention); + card.desired_retention = Some(desired_retention); + card.decay = decay; + if let Some(item) = item { + to_update_memory_state.push((card, original, item)); + } else { + // clear memory states if item is None + card.memory_state = None; + self.update_card_inner(&mut card, original, usn)?; + } + } + to_update_memory_state.par_iter_mut().try_for_each_with( + fsrs.clone(), + |fsrs, (card, _, item)| { + card.set_memory_state(fsrs, Some(item.clone()), historical_retention.unwrap()) + }, + )?; + + for (mut card, original, _) in to_update_memory_state { + 'reschedule_card: { // if rescheduling let Some(reviews) = &last_revlog_info else { - break 'update_card; + break 'reschedule_card; }; // and we have a last review time for the card let Some(last_info) = reviews.get(&card.id) else { - break 'update_card; + break 'reschedule_card; }; let Some(last_review) = &last_info.last_reviewed_at else { - break 'update_card; + break 'reschedule_card; }; // and the card's not new let Some(state) = &card.memory_state else { - break 'update_card; + break 'reschedule_card; }; // or in (re)learning if card.ctype != CardType::Review { - break 'update_card; + break 'reschedule_card; }; let deck = self @@ -163,7 +175,12 @@ impl Collection { // reschedule it let days_elapsed = timing.next_day_at.elapsed_days_since(*last_review) as i32; let original_interval = card.interval; - let interval = fsrs.next_interval(Some(state.stability), desired_retention, 0); + let interval = fsrs.next_interval( + Some(state.stability), + card.desired_retention + .expect("We set desired retention above"), + 0, + ); card.interval = rescheduler .as_mut() .and_then(|r| { @@ -198,7 +215,6 @@ impl Collection { // Add a rescheduled revlog entry self.log_rescheduled_review(&card, original_interval, usn)?; } - self.update_card_inner(&mut card, original, usn)?; } } @@ -272,7 +288,7 @@ impl Card { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct FsrsItemForMemoryState { pub item: FSRSItem, /// When revlogs have been truncated, this stores the initial state at first From e7e27825b606baeaa9aa27969d4d88444cce8234 Mon Sep 17 00:00:00 2001 From: Daniel Pechersky Date: Sun, 14 Sep 2025 19:03:36 +0700 Subject: [PATCH 03/14] Satisfy ./ninja check --- CONTRIBUTORS | 1 + rslib/src/scheduler/fsrs/memory_state.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index b03108e16..de57518b2 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -242,6 +242,7 @@ Lee Doughty <32392044+leedoughty@users.noreply.github.com> memchr Max Romanowski Aldlss +Daniel Pechersky ******************** diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 3b31c701a..0016488c7 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -10,7 +10,8 @@ use fsrs::FSRS; use fsrs::FSRS5_DEFAULT_DECAY; use fsrs::FSRS6_DEFAULT_DECAY; use itertools::Itertools; -use rayon::iter::{IntoParallelRefMutIterator as _, ParallelIterator as _}; +use rayon::iter::IntoParallelRefMutIterator as _; +use rayon::iter::ParallelIterator as _; use super::params::ignore_revlogs_before_ms_from_config; use super::rescheduler::Rescheduler; From dda13248728c61604d446b590b79eb6cda56434f Mon Sep 17 00:00:00 2001 From: Daniel Pechersky Date: Mon, 15 Sep 2025 19:02:47 +0700 Subject: [PATCH 04/14] Use fsrs batched function --- Cargo.lock | 5 +- Cargo.toml | 59 ++++++++++++++++++++---- rslib/src/scheduler/fsrs/memory_state.rs | 28 +++++------ 3 files changed, 62 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe88eb3ab..962c70b53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2233,9 +2233,8 @@ dependencies = [ [[package]] name = "fsrs" -version = "5.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04954cc67c3c11ee342a2ee1f5222bf76d73f7772df08d37dc9a6cdd73c467eb" +version = "5.2.0" +source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git#1e271981367454468391f1c686af03a0aa7aab3c" dependencies = [ "burn", "itertools 0.14.0", diff --git a/Cargo.toml b/Cargo.toml index 2e9489cb8..186335001 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,8 +33,8 @@ git = "https://github.com/ankitects/linkcheck.git" rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca" [workspace.dependencies.fsrs] -version = "5.1.0" -# git = "https://github.com/open-spaced-repetition/fsrs-rs.git" +# version = "5.1.0" +git = "https://github.com/open-spaced-repetition/fsrs-rs.git" # path = "../open-spaced-repetition/fsrs-rs" [workspace.dependencies] @@ -63,7 +63,10 @@ bitflags = "2.9.1" blake3 = "1.8.2" bytes = "1.10.1" camino = "1.1.10" -chrono = { version = "0.4.41", default-features = false, features = ["std", "clock"] } +chrono = { version = "0.4.41", default-features = false, features = [ + "std", + "clock", +] } clap = { version = "4.5.40", features = ["derive"] } coarsetime = "0.1.36" convert_case = "0.8.0" @@ -107,12 +110,26 @@ prost-build = "0.13" prost-reflect = "0.14.7" prost-types = "0.13" pulldown-cmark = "0.13.0" -pyo3 = { version = "0.25.1", features = ["extension-module", "abi3", "abi3-py39"] } +pyo3 = { version = "0.25.1", features = [ + "extension-module", + "abi3", + "abi3-py39", +] } rand = "0.9.1" rayon = "1.10.0" regex = "1.11.1" -reqwest = { version = "0.12.20", default-features = false, features = ["json", "socks", "stream", "multipart"] } -rusqlite = { version = "0.36.0", features = ["trace", "functions", "collation", "bundled"] } +reqwest = { version = "0.12.20", default-features = false, features = [ + "json", + "socks", + "stream", + "multipart", +] } +rusqlite = { version = "0.36.0", features = [ + "trace", + "functions", + "collation", + "bundled", +] } rustls-pemfile = "2.2.0" scopeguard = "1.2.0" serde = { version = "1.0.219", features = ["derive"] } @@ -128,10 +145,18 @@ syn = { version = "2.0.103", features = ["parsing", "printing"] } tar = "0.4.44" tempfile = "3.20.0" termcolor = "1.4.1" -tokio = { version = "1.45", features = ["fs", "rt-multi-thread", "macros", "signal"] } +tokio = { version = "1.45", features = [ + "fs", + "rt-multi-thread", + "macros", + "signal", +] } tokio-util = { version = "0.7.15", features = ["io"] } tower-http = { version = "0.6.6", features = ["trace"] } -tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] } +tracing = { version = "0.1.41", features = [ + "max_level_trace", + "release_max_level_debug", +] } tracing-appender = "0.2.3" tracing-subscriber = { version = "0.3.20", features = ["fmt", "env-filter"] } unic-langid = { version = "0.9.6", features = ["macros"] } @@ -141,10 +166,24 @@ walkdir = "2.5.0" which = "8.0.0" widestring = "1.1.0" winapi = { version = "0.3", features = ["wincon", "winreg"] } -windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams", "Win32_System_Console", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_Foundation", "Win32_UI_Shell", "Wdk_System_SystemServices"] } +windows = { version = "0.61.3", features = [ + "Media_SpeechSynthesis", + "Media_Core", + "Foundation_Collections", + "Storage_Streams", + "Win32_System_Console", + "Win32_System_Registry", + "Win32_System_SystemInformation", + "Win32_Foundation", + "Win32_UI_Shell", + "Wdk_System_SystemServices", +] } wiremock = "0.6.3" xz2 = "0.1.7" -zip = { version = "4.1.0", default-features = false, features = ["deflate", "time"] } +zip = { version = "4.1.0", default-features = false, features = [ + "deflate", + "time", +] } zstd = { version = "0.13.3", features = ["zstdmt"] } # Apply mild optimizations to our dependencies in dev mode, which among other things diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 0016488c7..6fc2913c1 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -10,8 +10,6 @@ use fsrs::FSRS; use fsrs::FSRS5_DEFAULT_DECAY; use fsrs::FSRS6_DEFAULT_DECAY; use itertools::Itertools; -use rayon::iter::IntoParallelRefMutIterator as _; -use rayon::iter::ParallelIterator as _; use super::params::ignore_revlogs_before_ms_from_config; use super::rescheduler::Rescheduler; @@ -113,7 +111,9 @@ impl Collection { }; let preset_desired_retention = req.preset_desired_retention; - let mut to_update_memory_state = Vec::new(); + let mut to_update = Vec::new(); + let mut fsrs_items = Vec::new(); + let mut starting_states = Vec::new(); for (idx, (card_id, item)) in items.into_iter().enumerate() { progress.update(true, |state| state.current_cards = idx as u32 + 1)?; let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; @@ -130,7 +130,9 @@ impl Collection { card.desired_retention = Some(desired_retention); card.decay = decay; if let Some(item) = item { - to_update_memory_state.push((card, original, item)); + to_update.push((card, original)); + fsrs_items.push(item.item); + starting_states.push(item.starting_state); } else { // clear memory states if item is None card.memory_state = None; @@ -138,14 +140,11 @@ impl Collection { } } - to_update_memory_state.par_iter_mut().try_for_each_with( - fsrs.clone(), - |fsrs, (card, _, item)| { - card.set_memory_state(fsrs, Some(item.clone()), historical_retention.unwrap()) - }, - )?; + let memory_states = fsrs.memory_state_batch(fsrs_items, starting_states)?; + + for ((mut card, original), memory_state) in to_update.into_iter().zip(memory_states) { + card.memory_state = Some(memory_state.into()); - for (mut card, original, _) in to_update_memory_state { 'reschedule_card: { // if rescheduling let Some(reviews) = &last_revlog_info else { @@ -159,11 +158,6 @@ impl Collection { let Some(last_review) = &last_info.last_reviewed_at else { break 'reschedule_card; }; - - // and the card's not new - let Some(state) = &card.memory_state else { - break 'reschedule_card; - }; // or in (re)learning if card.ctype != CardType::Review { break 'reschedule_card; @@ -177,7 +171,7 @@ impl Collection { let days_elapsed = timing.next_day_at.elapsed_days_since(*last_review) as i32; let original_interval = card.interval; let interval = fsrs.next_interval( - Some(state.stability), + Some(memory_state.stability), card.desired_retention .expect("We set desired retention above"), 0, From f54e79c737d60b53e0041d61420f23d0e3715dbe Mon Sep 17 00:00:00 2001 From: Daniel Pechersky Date: Mon, 15 Sep 2025 20:59:32 +0700 Subject: [PATCH 05/14] Batch both max # of items processed and max # of items passed to fsrs --- Cargo.lock | 7 + Cargo.toml | 1 + rslib/Cargo.toml | 1 + rslib/src/scheduler/fsrs/memory_state.rs | 225 +++++++++++++---------- 4 files changed, 140 insertions(+), 94 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 962c70b53..333cd6110 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,7 @@ dependencies = [ "once_cell", "pbkdf2", "percent-encoding-iri", + "permutation", "phf 0.11.3", "pin-project", "prettyplease", @@ -4560,6 +4561,12 @@ name = "percent-encoding-iri" version = "2.2.0" source = "git+https://github.com/ankitects/rust-url.git?rev=bb930b8d089f4d30d7d19c12e54e66191de47b88#bb930b8d089f4d30d7d19c12e54e66191de47b88" +[[package]] +name = "permutation" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df202b0b0f5b8e389955afd5f27b007b00fb948162953f1db9c70d2c7e3157d7" + [[package]] name = "pest" version = "2.8.1" diff --git a/Cargo.toml b/Cargo.toml index 186335001..f9ed269aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,6 +102,7 @@ num_cpus = "1.17.0" num_enum = "0.7.3" once_cell = "1.21.3" pbkdf2 = { version = "0.12", features = ["simple"] } +permutation = "0.4.1" phf = { version = "0.11.3", features = ["macros"] } pin-project = "1.1.10" prettyplease = "0.2.34" diff --git a/rslib/Cargo.toml b/rslib/Cargo.toml index 9be9e8d87..efb9b998e 100644 --- a/rslib/Cargo.toml +++ b/rslib/Cargo.toml @@ -76,6 +76,7 @@ num_enum.workspace = true once_cell.workspace = true pbkdf2.workspace = true percent-encoding-iri.workspace = true +permutation.workspace = true phf.workspace = true pin-project.workspace = true prost.workspace = true diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 6fc2913c1..e5393a4d0 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -58,6 +58,18 @@ pub(crate) struct UpdateMemoryStateEntry { pub ignore_before: TimestampMillis, } +trait ChunkIntoVecs { + fn chunk_into_vecs(&mut self, chunk_size: usize) -> impl Iterator>; +} + +impl ChunkIntoVecs for Vec { + fn chunk_into_vecs(&mut self, chunk_size: usize) -> impl Iterator> { + std::iter::from_fn(move || { + (!self.is_empty()).then(|| self.split_off(chunk_size.min(self.len()))) + }) + } +} + impl Collection { /// For each provided set of params, locate cards with the provided search, /// and update their memory state. @@ -68,6 +80,9 @@ impl Collection { &mut self, entries: Vec, ) -> Result<()> { + const ITEM_CHUNK_SIZE: usize = 100_000; + const FSRS_CHUNK_SIZE: usize = 1000; + let timing = self.timing_today()?; let usn = self.usn()?; for UpdateMemoryStateEntry { @@ -88,7 +103,7 @@ impl Collection { let fsrs = FSRS::new(req.as_ref().map(|w| &w.params[..]).or(Some([].as_slice())))?; let decay = req.as_ref().map(|w| get_decay_from_params(&w.params)); let historical_retention = req.as_ref().map(|w| w.historical_retention); - let items = fsrs_items_for_memory_states( + let mut items = fsrs_items_for_memory_states( &fsrs, revlog, timing.next_day_at, @@ -114,103 +129,125 @@ impl Collection { let mut to_update = Vec::new(); let mut fsrs_items = Vec::new(); let mut starting_states = Vec::new(); - for (idx, (card_id, item)) in items.into_iter().enumerate() { - progress.update(true, |state| state.current_cards = idx as u32 + 1)?; - let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; - let original = card.clone(); + for (i, items) in items.chunk_into_vecs(ITEM_CHUNK_SIZE).enumerate() { + progress.update(true, |state| { + let end_of_chunk_index = i * ITEM_CHUNK_SIZE + items.len(); + state.current_cards = end_of_chunk_index as u32 + 1 + })?; + for (card_id, item) in items.into_iter() { + let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; + let original = card.clone(); - // Store decay and desired retention in the card so that add-ons, card info, - // stats and browser search/sorts don't need to access the deck config. - // Unlike memory states, scheduler doesn't use decay and dr stored in the card. - let deck_id = card.original_or_current_deck_id(); - let desired_retention = *req - .deck_desired_retention - .get(&deck_id) - .unwrap_or(&preset_desired_retention); - card.desired_retention = Some(desired_retention); - card.decay = decay; - if let Some(item) = item { - to_update.push((card, original)); - fsrs_items.push(item.item); - starting_states.push(item.starting_state); - } else { - // clear memory states if item is None - card.memory_state = None; - self.update_card_inner(&mut card, original, usn)?; - } - } - - let memory_states = fsrs.memory_state_batch(fsrs_items, starting_states)?; - - for ((mut card, original), memory_state) in to_update.into_iter().zip(memory_states) { - card.memory_state = Some(memory_state.into()); - - 'reschedule_card: { - // if rescheduling - let Some(reviews) = &last_revlog_info else { - break 'reschedule_card; - }; - - // and we have a last review time for the card - let Some(last_info) = reviews.get(&card.id) else { - break 'reschedule_card; - }; - let Some(last_review) = &last_info.last_reviewed_at else { - break 'reschedule_card; - }; - // or in (re)learning - if card.ctype != CardType::Review { - break 'reschedule_card; - }; - - let deck = self - .get_deck(card.original_or_current_deck_id())? - .or_not_found(card.original_or_current_deck_id())?; - let deckconfig_id = deck.config_id().unwrap(); - // reschedule it - let days_elapsed = timing.next_day_at.elapsed_days_since(*last_review) as i32; - let original_interval = card.interval; - let interval = fsrs.next_interval( - Some(memory_state.stability), - card.desired_retention - .expect("We set desired retention above"), - 0, - ); - card.interval = rescheduler - .as_mut() - .and_then(|r| { - r.find_interval( - interval, - 1, - req.max_interval, - days_elapsed as u32, - deckconfig_id, - get_fuzz_seed(&card, true), - ) - }) - .unwrap_or_else(|| { - with_review_fuzz( - card.get_fuzz_factor(true), - interval, - 1, - req.max_interval, - ) - }); - let due = if card.original_due != 0 { - &mut card.original_due + // Store decay and desired retention in the card so that add-ons, card info, + // stats and browser search/sorts don't need to access the deck config. + // Unlike memory states, scheduler doesn't use decay and dr stored in the card. + let deck_id = card.original_or_current_deck_id(); + let desired_retention = *req + .deck_desired_retention + .get(&deck_id) + .unwrap_or(&preset_desired_retention); + card.desired_retention = Some(desired_retention); + card.decay = decay; + if let Some(item) = item { + to_update.push((card, original)); + fsrs_items.push(item.item); + starting_states.push(item.starting_state); } else { - &mut card.due - }; - let new_due = - (timing.days_elapsed as i32) - days_elapsed + card.interval as i32; - if let Some(rescheduler) = &mut rescheduler { - rescheduler.update_due_cnt_per_day(*due, new_due, deckconfig_id); + // clear memory states if item is None + card.memory_state = None; + self.update_card_inner(&mut card, original, usn)?; + } + } + + // fsrs.memory_state_batch is O(nm) where n is the number of cards and m is the max review count between all items. + // Therefore we want to pass batches to fsrs.memory_state_batch where the review count is relatively even. + let mut p = + permutation::sort_unstable_by_key(&fsrs_items, |item| item.reviews.len()); + p.apply_slice_in_place(&mut to_update); + p.apply_slice_in_place(&mut fsrs_items); + p.apply_slice_in_place(&mut starting_states); + + for ((to_update, fsrs_items), starting_states) in to_update + .chunk_into_vecs(FSRS_CHUNK_SIZE) + .zip_eq(fsrs_items.chunk_into_vecs(FSRS_CHUNK_SIZE)) + .zip_eq(starting_states.chunk_into_vecs(FSRS_CHUNK_SIZE)) + { + let memory_states = fsrs.memory_state_batch(fsrs_items, starting_states)?; + + for ((mut card, original), memory_state) in + to_update.into_iter().zip(memory_states) + { + card.memory_state = Some(memory_state.into()); + + 'reschedule_card: { + // if rescheduling + let Some(reviews) = &last_revlog_info else { + break 'reschedule_card; + }; + + // and we have a last review time for the card + let Some(last_info) = reviews.get(&card.id) else { + break 'reschedule_card; + }; + let Some(last_review) = &last_info.last_reviewed_at else { + break 'reschedule_card; + }; + // or in (re)learning + if card.ctype != CardType::Review { + break 'reschedule_card; + }; + + let deck = self + .get_deck(card.original_or_current_deck_id())? + .or_not_found(card.original_or_current_deck_id())?; + let deckconfig_id = deck.config_id().unwrap(); + // reschedule it + let days_elapsed = + timing.next_day_at.elapsed_days_since(*last_review) as i32; + let original_interval = card.interval; + let interval = fsrs.next_interval( + Some(memory_state.stability), + card.desired_retention + .expect("We set desired retention above"), + 0, + ); + card.interval = rescheduler + .as_mut() + .and_then(|r| { + r.find_interval( + interval, + 1, + req.max_interval, + days_elapsed as u32, + deckconfig_id, + get_fuzz_seed(&card, true), + ) + }) + .unwrap_or_else(|| { + with_review_fuzz( + card.get_fuzz_factor(true), + interval, + 1, + req.max_interval, + ) + }); + let due = if card.original_due != 0 { + &mut card.original_due + } else { + &mut card.due + }; + let new_due = + (timing.days_elapsed as i32) - days_elapsed + card.interval as i32; + if let Some(rescheduler) = &mut rescheduler { + rescheduler.update_due_cnt_per_day(*due, new_due, deckconfig_id); + } + *due = new_due; + // Add a rescheduled revlog entry + self.log_rescheduled_review(&card, original_interval, usn)?; + } + self.update_card_inner(&mut card, original, usn)?; } - *due = new_due; - // Add a rescheduled revlog entry - self.log_rescheduled_review(&card, original_interval, usn)?; } - self.update_card_inner(&mut card, original, usn)?; } } Ok(()) From 4670e1efc3e223adde126dc4fdff9946a6f2abf5 Mon Sep 17 00:00:00 2001 From: Daniel Pechersky Date: Tue, 16 Sep 2025 01:37:45 +0700 Subject: [PATCH 06/14] Cleanup --- rslib/src/scheduler/fsrs/memory_state.rs | 295 +++++++++++++---------- 1 file changed, 165 insertions(+), 130 deletions(-) diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index e5393a4d0..199c05581 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -13,8 +13,6 @@ use itertools::Itertools; use super::params::ignore_revlogs_before_ms_from_config; use super::rescheduler::Rescheduler; -use crate::card::CardType; -use crate::prelude::*; use crate::revlog::RevlogEntry; use crate::scheduler::answering::get_fuzz_seed; use crate::scheduler::fsrs::params::reviews_for_fsrs; @@ -23,6 +21,8 @@ use crate::scheduler::states::fuzz::with_review_fuzz; use crate::search::Negated; use crate::search::SearchNode; use crate::search::StateKind; +use crate::{card::CardType, progress::ThrottlingProgressHandler}; +use crate::{prelude::*, scheduler::timing::SchedTimingToday}; #[derive(Debug, Clone, Copy, Default)] pub struct ComputeMemoryProgress { @@ -80,9 +80,6 @@ impl Collection { &mut self, entries: Vec, ) -> Result<()> { - const ITEM_CHUNK_SIZE: usize = 100_000; - const FSRS_CHUNK_SIZE: usize = 1000; - let timing = self.timing_today()?; let usn = self.usn()?; for UpdateMemoryStateEntry { @@ -94,159 +91,197 @@ impl Collection { let search = SearchBuilder::all([search.into(), SearchNode::State(StateKind::New).negated()]); let revlog = self.revlog_for_srs(search)?; - let reschedule = req.as_ref().map(|e| e.reschedule).unwrap_or_default(); + let reschedule = req.as_ref().map(|e| e.reschedule).unwrap_or(false); let last_revlog_info = reschedule.then(|| get_last_revlog_info(&revlog)); - let mut rescheduler = self + let rescheduler = self .get_config_bool(BoolKey::LoadBalancerEnabled) .then(|| Rescheduler::new(self)) .transpose()?; let fsrs = FSRS::new(req.as_ref().map(|w| &w.params[..]).or(Some([].as_slice())))?; - let decay = req.as_ref().map(|w| get_decay_from_params(&w.params)); let historical_retention = req.as_ref().map(|w| w.historical_retention); - let mut items = fsrs_items_for_memory_states( + let items = fsrs_items_for_memory_states( &fsrs, revlog, timing.next_day_at, historical_retention.unwrap_or(0.9), ignore_before, )?; + let mut progress = self.new_progress_handler::(); progress.update(false, |s| s.total_cards = items.len() as u32)?; - let Some(req) = &req else { // clear FSRS data if FSRS is disabled - for (idx, (card_id, _)) in items.into_iter().enumerate() { - progress.update(true, |state| state.current_cards = idx as u32 + 1)?; - let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; - let original = card.clone(); - card.clear_fsrs_data(); - self.update_card_inner(&mut card, original, usn)?; - } + self.clear_fsrs_data_for_cards( + items.into_iter().map(|(card_id, _)| card_id), + usn, + &mut progress, + )?; return Ok(()); }; - let preset_desired_retention = req.preset_desired_retention; + let last_revlog_info = last_revlog_info.unwrap(); - let mut to_update = Vec::new(); - let mut fsrs_items = Vec::new(); - let mut starting_states = Vec::new(); - for (i, items) in items.chunk_into_vecs(ITEM_CHUNK_SIZE).enumerate() { - progress.update(true, |state| { - let end_of_chunk_index = i * ITEM_CHUNK_SIZE + items.len(); - state.current_cards = end_of_chunk_index as u32 + 1 - })?; - for (card_id, item) in items.into_iter() { - let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; - let original = card.clone(); + self.update_memory_state_for_items( + items, + last_revlog_info, + req, + &fsrs, + rescheduler, + &mut progress, + timing, + usn, + )?; + } + Ok(()) + } - // Store decay and desired retention in the card so that add-ons, card info, - // stats and browser search/sorts don't need to access the deck config. - // Unlike memory states, scheduler doesn't use decay and dr stored in the card. - let deck_id = card.original_or_current_deck_id(); - let desired_retention = *req - .deck_desired_retention - .get(&deck_id) - .unwrap_or(&preset_desired_retention); - card.desired_retention = Some(desired_retention); - card.decay = decay; - if let Some(item) = item { - to_update.push((card, original)); - fsrs_items.push(item.item); - starting_states.push(item.starting_state); - } else { - // clear memory states if item is None - card.memory_state = None; - self.update_card_inner(&mut card, original, usn)?; - } + fn clear_fsrs_data_for_cards( + &mut self, + cards: impl Iterator, + usn: Usn, + progress: &mut ThrottlingProgressHandler, + ) -> Result<()> { + for (idx, card_id) in cards.enumerate() { + progress.update(true, |state| state.current_cards = idx as u32 + 1)?; + let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; + let original = card.clone(); + card.clear_fsrs_data(); + self.update_card_inner(&mut card, original, usn)?; + } + Ok(()) + } + + #[expect(clippy::too_many_arguments)] + fn update_memory_state_for_items( + &mut self, + mut items: Vec<(CardId, Option)>, + last_revlog_info: HashMap, + + req: &UpdateMemoryStateRequest, + fsrs: &FSRS, + mut rescheduler: Option, + progress: &mut ThrottlingProgressHandler, + timing: SchedTimingToday, + + usn: Usn, + ) -> Result<()> { + const ITEM_CHUNK_SIZE: usize = 100_000; + const FSRS_CHUNK_SIZE: usize = 1000; + + let decay = get_decay_from_params(&req.params); + + let mut to_update = Vec::new(); + let mut fsrs_items = Vec::new(); + let mut starting_states = Vec::new(); + for (i, items) in items.chunk_into_vecs(ITEM_CHUNK_SIZE).enumerate() { + progress.update(true, |state| { + let end_of_chunk_index = i * ITEM_CHUNK_SIZE + items.len(); + state.current_cards = end_of_chunk_index as u32 + 1 + })?; + for (card_id, item) in items.into_iter() { + let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; + let original = card.clone(); + + // Store decay and desired retention in the card so that add-ons, card info, + // stats and browser search/sorts don't need to access the deck config. + // Unlike memory states, scheduler doesn't use decay and dr stored in the card. + let deck_id = card.original_or_current_deck_id(); + let desired_retention = *req + .deck_desired_retention + .get(&deck_id) + .unwrap_or(&req.preset_desired_retention); + card.desired_retention = Some(desired_retention); + card.decay = Some(decay); + if let Some(item) = item { + to_update.push((card, original)); + fsrs_items.push(item.item); + starting_states.push(item.starting_state); + } else { + // clear memory states if item is None + card.memory_state = None; + self.update_card_inner(&mut card, original, usn)?; } + } - // fsrs.memory_state_batch is O(nm) where n is the number of cards and m is the max review count between all items. - // Therefore we want to pass batches to fsrs.memory_state_batch where the review count is relatively even. - let mut p = - permutation::sort_unstable_by_key(&fsrs_items, |item| item.reviews.len()); - p.apply_slice_in_place(&mut to_update); - p.apply_slice_in_place(&mut fsrs_items); - p.apply_slice_in_place(&mut starting_states); + // fsrs.memory_state_batch is O(nm) where n is the number of cards and m is the max review count between all items. + // Therefore we want to pass batches to fsrs.memory_state_batch where the review count is relatively even. + let mut p = permutation::sort_unstable_by_key(&fsrs_items, |item| item.reviews.len()); + p.apply_slice_in_place(&mut to_update); + p.apply_slice_in_place(&mut fsrs_items); + p.apply_slice_in_place(&mut starting_states); - for ((to_update, fsrs_items), starting_states) in to_update - .chunk_into_vecs(FSRS_CHUNK_SIZE) - .zip_eq(fsrs_items.chunk_into_vecs(FSRS_CHUNK_SIZE)) - .zip_eq(starting_states.chunk_into_vecs(FSRS_CHUNK_SIZE)) + for ((to_update, fsrs_items), starting_states) in to_update + .chunk_into_vecs(FSRS_CHUNK_SIZE) + .zip_eq(fsrs_items.chunk_into_vecs(FSRS_CHUNK_SIZE)) + .zip_eq(starting_states.chunk_into_vecs(FSRS_CHUNK_SIZE)) + { + let memory_states = fsrs.memory_state_batch(fsrs_items, starting_states)?; + + for ((mut card, original), memory_state) in to_update.into_iter().zip(memory_states) { - let memory_states = fsrs.memory_state_batch(fsrs_items, starting_states)?; + card.memory_state = Some(memory_state.into()); - for ((mut card, original), memory_state) in - to_update.into_iter().zip(memory_states) - { - card.memory_state = Some(memory_state.into()); + 'reschedule_card: { + // we have a last review time for the card + let Some(last_info) = last_revlog_info.get(&card.id) else { + break 'reschedule_card; + }; + let Some(last_review) = &last_info.last_reviewed_at else { + break 'reschedule_card; + }; + // the card isn't in (re)learning + if card.ctype != CardType::Review { + break 'reschedule_card; + }; - 'reschedule_card: { - // if rescheduling - let Some(reviews) = &last_revlog_info else { - break 'reschedule_card; - }; - - // and we have a last review time for the card - let Some(last_info) = reviews.get(&card.id) else { - break 'reschedule_card; - }; - let Some(last_review) = &last_info.last_reviewed_at else { - break 'reschedule_card; - }; - // or in (re)learning - if card.ctype != CardType::Review { - break 'reschedule_card; - }; - - let deck = self - .get_deck(card.original_or_current_deck_id())? - .or_not_found(card.original_or_current_deck_id())?; - let deckconfig_id = deck.config_id().unwrap(); - // reschedule it - let days_elapsed = - timing.next_day_at.elapsed_days_since(*last_review) as i32; - let original_interval = card.interval; - let interval = fsrs.next_interval( - Some(memory_state.stability), - card.desired_retention - .expect("We set desired retention above"), - 0, - ); - card.interval = rescheduler - .as_mut() - .and_then(|r| { - r.find_interval( - interval, - 1, - req.max_interval, - days_elapsed as u32, - deckconfig_id, - get_fuzz_seed(&card, true), - ) - }) - .unwrap_or_else(|| { - with_review_fuzz( - card.get_fuzz_factor(true), - interval, - 1, - req.max_interval, - ) - }); - let due = if card.original_due != 0 { - &mut card.original_due - } else { - &mut card.due - }; - let new_due = - (timing.days_elapsed as i32) - days_elapsed + card.interval as i32; - if let Some(rescheduler) = &mut rescheduler { - rescheduler.update_due_cnt_per_day(*due, new_due, deckconfig_id); - } - *due = new_due; - // Add a rescheduled revlog entry - self.log_rescheduled_review(&card, original_interval, usn)?; + let deck = self + .get_deck(card.original_or_current_deck_id())? + .or_not_found(card.original_or_current_deck_id())?; + let deckconfig_id = deck.config_id().unwrap(); + // reschedule it + let days_elapsed = + timing.next_day_at.elapsed_days_since(*last_review) as i32; + let original_interval = card.interval; + let interval = fsrs.next_interval( + Some(memory_state.stability), + card.desired_retention + .expect("We set desired retention above"), + 0, + ); + card.interval = rescheduler + .as_mut() + .and_then(|r| { + r.find_interval( + interval, + 1, + req.max_interval, + days_elapsed as u32, + deckconfig_id, + get_fuzz_seed(&card, true), + ) + }) + .unwrap_or_else(|| { + with_review_fuzz( + card.get_fuzz_factor(true), + interval, + 1, + req.max_interval, + ) + }); + let due = if card.original_due != 0 { + &mut card.original_due + } else { + &mut card.due + }; + let new_due = + (timing.days_elapsed as i32) - days_elapsed + card.interval as i32; + if let Some(rescheduler) = &mut rescheduler { + rescheduler.update_due_cnt_per_day(*due, new_due, deckconfig_id); } - self.update_card_inner(&mut card, original, usn)?; + *due = new_due; + // Add a rescheduled revlog entry + self.log_rescheduled_review(&card, original_interval, usn)?; } + self.update_card_inner(&mut card, original, usn)?; } } } From a051f0f3ee171f917dcc48d8366e97c4257501af Mon Sep 17 00:00:00 2001 From: Daniel Pechersky Date: Tue, 16 Sep 2025 02:07:29 +0700 Subject: [PATCH 07/14] More cleanup --- rslib/src/scheduler/fsrs/memory_state.rs | 118 ++++++++++++++--------- 1 file changed, 75 insertions(+), 43 deletions(-) diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 199c05581..97101465f 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -9,20 +9,21 @@ use fsrs::MemoryState; use fsrs::FSRS; use fsrs::FSRS5_DEFAULT_DECAY; use fsrs::FSRS6_DEFAULT_DECAY; -use itertools::Itertools; +use itertools::{Either, Itertools}; use super::params::ignore_revlogs_before_ms_from_config; use super::rescheduler::Rescheduler; +use crate::card::CardType; +use crate::prelude::*; use crate::revlog::RevlogEntry; use crate::scheduler::answering::get_fuzz_seed; use crate::scheduler::fsrs::params::reviews_for_fsrs; use crate::scheduler::fsrs::params::Params; use crate::scheduler::states::fuzz::with_review_fuzz; +use crate::scheduler::timing::SchedTimingToday; use crate::search::Negated; use crate::search::SearchNode; use crate::search::StateKind; -use crate::{card::CardType, progress::ThrottlingProgressHandler}; -use crate::{prelude::*, scheduler::timing::SchedTimingToday}; #[derive(Debug, Clone, Copy, Default)] pub struct ComputeMemoryProgress { @@ -107,27 +108,53 @@ impl Collection { ignore_before, )?; - let mut progress = self.new_progress_handler::(); - progress.update(false, |s| s.total_cards = items.len() as u32)?; let Some(req) = &req else { // clear FSRS data if FSRS is disabled - self.clear_fsrs_data_for_cards( - items.into_iter().map(|(card_id, _)| card_id), - usn, - &mut progress, - )?; + self.clear_fsrs_data_for_cards(items.into_iter().map(|(card_id, _)| card_id), usn)?; return Ok(()); }; let last_revlog_info = last_revlog_info.unwrap(); - self.update_memory_state_for_items( + let (items, cards_without_items): (Vec<(CardId, FsrsItemForMemoryState)>, Vec) = + items.into_iter().partition_map(|(card_id, item)| { + if let Some(item) = item { + Either::Left((card_id, item)) + } else { + Either::Right(card_id) + } + }); + + let decay = get_decay_from_params(&req.params); + + // Store decay and desired retention in the card so that add-ons, card info, + // stats and browser search/sorts don't need to access the deck config. + // Unlike memory states, scheduler doesn't use decay and dr stored in the card. + let set_decay_and_desired_retention = move |card: &mut Card| { + let deck_id = card.original_or_current_deck_id(); + + let desired_retention = *req + .deck_desired_retention + .get(&deck_id) + .unwrap_or(&req.preset_desired_retention); + + card.desired_retention = Some(desired_retention); + card.decay = Some(decay); + }; + + self.update_memory_state_for_itemless_cards( + cards_without_items, + set_decay_and_desired_retention, + usn, + )?; + + self.update_memory_state_for_cards_with_items( items, last_revlog_info, req, &fsrs, rescheduler, - &mut progress, timing, + set_decay_and_desired_retention, usn, )?; } @@ -136,10 +163,11 @@ impl Collection { fn clear_fsrs_data_for_cards( &mut self, - cards: impl Iterator, + cards: impl ExactSizeIterator, usn: Usn, - progress: &mut ThrottlingProgressHandler, ) -> Result<()> { + let mut progress = self.new_progress_handler::(); + progress.update(false, |s| s.total_cards = cards.len() as u32)?; for (idx, card_id) in cards.enumerate() { progress.update(true, |state| state.current_cards = idx as u32 + 1)?; let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; @@ -150,56 +178,59 @@ impl Collection { Ok(()) } - #[expect(clippy::too_many_arguments)] - fn update_memory_state_for_items( + fn update_memory_state_for_itemless_cards( &mut self, - mut items: Vec<(CardId, Option)>, + cards: Vec, + mut set_decay_and_desired_retention: impl FnMut(&mut Card), + usn: Usn, + ) -> Result<()> { + let mut progress = self.new_progress_handler::(); + progress.update(false, |s| s.total_cards = cards.len() as u32)?; + for (idx, card_id) in cards.into_iter().enumerate() { + progress.update(true, |state| state.current_cards = idx as u32 + 1)?; + let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; + let original = card.clone(); + set_decay_and_desired_retention(&mut card); + card.memory_state = None; + self.update_card_inner(&mut card, original, usn)?; + } + Ok(()) + } + + #[expect(clippy::too_many_arguments)] + fn update_memory_state_for_cards_with_items( + &mut self, + mut items: Vec<(CardId, FsrsItemForMemoryState)>, last_revlog_info: HashMap, req: &UpdateMemoryStateRequest, fsrs: &FSRS, mut rescheduler: Option, - progress: &mut ThrottlingProgressHandler, timing: SchedTimingToday, + mut set_decay_and_desired_retention: impl FnMut(&mut Card), usn: Usn, ) -> Result<()> { const ITEM_CHUNK_SIZE: usize = 100_000; const FSRS_CHUNK_SIZE: usize = 1000; - let decay = get_decay_from_params(&req.params); - let mut to_update = Vec::new(); let mut fsrs_items = Vec::new(); let mut starting_states = Vec::new(); + + let mut progress = self.new_progress_handler::(); + progress.update(false, |s| s.total_cards = items.len() as u32)?; for (i, items) in items.chunk_into_vecs(ITEM_CHUNK_SIZE).enumerate() { progress.update(true, |state| { let end_of_chunk_index = i * ITEM_CHUNK_SIZE + items.len(); state.current_cards = end_of_chunk_index as u32 + 1 })?; for (card_id, item) in items.into_iter() { - let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; - let original = card.clone(); + let card = self.storage.get_card(card_id)?.or_not_found(card_id)?; - // Store decay and desired retention in the card so that add-ons, card info, - // stats and browser search/sorts don't need to access the deck config. - // Unlike memory states, scheduler doesn't use decay and dr stored in the card. - let deck_id = card.original_or_current_deck_id(); - let desired_retention = *req - .deck_desired_retention - .get(&deck_id) - .unwrap_or(&req.preset_desired_retention); - card.desired_retention = Some(desired_retention); - card.decay = Some(decay); - if let Some(item) = item { - to_update.push((card, original)); - fsrs_items.push(item.item); - starting_states.push(item.starting_state); - } else { - // clear memory states if item is None - card.memory_state = None; - self.update_card_inner(&mut card, original, usn)?; - } + to_update.push(card); + fsrs_items.push(item.item); + starting_states.push(item.starting_state); } // fsrs.memory_state_batch is O(nm) where n is the number of cards and m is the max review count between all items. @@ -216,8 +247,9 @@ impl Collection { { let memory_states = fsrs.memory_state_batch(fsrs_items, starting_states)?; - for ((mut card, original), memory_state) in to_update.into_iter().zip(memory_states) - { + for (mut card, memory_state) in to_update.into_iter().zip(memory_states) { + let original = card.clone(); + set_decay_and_desired_retention(&mut card); card.memory_state = Some(memory_state.into()); 'reschedule_card: { From 0f94caaa7e8b58f72ad8708b6a347452a27defc8 Mon Sep 17 00:00:00 2001 From: Daniel Pechersky Date: Tue, 16 Sep 2025 02:49:01 +0700 Subject: [PATCH 08/14] Even more cleanup --- rslib/src/scheduler/fsrs/memory_state.rs | 187 ++++++++++++----------- 1 file changed, 96 insertions(+), 91 deletions(-) diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 97101465f..f4f343b29 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -20,7 +20,6 @@ use crate::scheduler::answering::get_fuzz_seed; use crate::scheduler::fsrs::params::reviews_for_fsrs; use crate::scheduler::fsrs::params::Params; use crate::scheduler::states::fuzz::with_review_fuzz; -use crate::scheduler::timing::SchedTimingToday; use crate::search::Negated; use crate::search::SearchNode; use crate::search::StateKind; @@ -92,28 +91,30 @@ impl Collection { let search = SearchBuilder::all([search.into(), SearchNode::State(StateKind::New).negated()]); let revlog = self.revlog_for_srs(search)?; - let reschedule = req.as_ref().map(|e| e.reschedule).unwrap_or(false); - let last_revlog_info = reschedule.then(|| get_last_revlog_info(&revlog)); - let rescheduler = self - .get_config_bool(BoolKey::LoadBalancerEnabled) - .then(|| Rescheduler::new(self)) - .transpose()?; - let fsrs = FSRS::new(req.as_ref().map(|w| &w.params[..]).or(Some([].as_slice())))?; - let historical_retention = req.as_ref().map(|w| w.historical_retention); - let items = fsrs_items_for_memory_states( - &fsrs, - revlog, - timing.next_day_at, - historical_retention.unwrap_or(0.9), - ignore_before, - )?; let Some(req) = &req else { + let items = fsrs_items_for_memory_states( + &FSRS::new(Some([].as_slice()))?, + revlog, + timing.next_day_at, + 0.9, + ignore_before, + )?; // clear FSRS data if FSRS is disabled self.clear_fsrs_data_for_cards(items.into_iter().map(|(card_id, _)| card_id), usn)?; return Ok(()); }; - let last_revlog_info = last_revlog_info.unwrap(); + + let fsrs = FSRS::new(Some(&req.params[..]))?; + let last_revlog_info = req.reschedule.then(|| get_last_revlog_info(&revlog)); + + let items = fsrs_items_for_memory_states( + &fsrs, + revlog, + timing.next_day_at, + req.historical_retention, + ignore_before, + )?; let (items, cards_without_items): (Vec<(CardId, FsrsItemForMemoryState)>, Vec) = items.into_iter().partition_map(|(card_id, item)| { @@ -147,14 +148,85 @@ impl Collection { usn, )?; + let mut rescheduler = self + .get_config_bool(BoolKey::LoadBalancerEnabled) + .then(|| Rescheduler::new(self)) + .transpose()?; + + let reschedule = move |card: &mut Card, + collection: &mut Self, + fsrs: &FSRS| + -> Result<()> { + // we are rescheduling + let Some(last_revlog_info) = &last_revlog_info else { + return Ok(()); + }; + + // we have a last review time for the card + let Some(last_info) = last_revlog_info.get(&card.id) else { + return Ok(()); + }; + let Some(last_review) = &last_info.last_reviewed_at else { + return Ok(()); + }; + // the card isn't in (re)learning + if card.ctype != CardType::Review { + return Ok(()); + }; + + let deck = collection + .get_deck(card.original_or_current_deck_id())? + .or_not_found(card.original_or_current_deck_id())?; + let deckconfig_id = deck.config_id().unwrap(); + // reschedule it + let days_elapsed = timing.next_day_at.elapsed_days_since(*last_review) as i32; + let original_interval = card.interval; + let interval = fsrs.next_interval( + Some( + card.memory_state + .expect("We set it before this function is called") + .stability, + ), + card.desired_retention + .expect("We set it before this function is called"), + 0, + ); + card.interval = rescheduler + .as_mut() + .and_then(|r| { + r.find_interval( + interval, + 1, + req.max_interval, + days_elapsed as u32, + deckconfig_id, + get_fuzz_seed(card, true), + ) + }) + .unwrap_or_else(|| { + with_review_fuzz(card.get_fuzz_factor(true), interval, 1, req.max_interval) + }); + let due = if card.original_due != 0 { + &mut card.original_due + } else { + &mut card.due + }; + let new_due = (timing.days_elapsed as i32) - days_elapsed + card.interval as i32; + if let Some(rescheduler) = &mut rescheduler { + rescheduler.update_due_cnt_per_day(*due, new_due, deckconfig_id); + } + *due = new_due; + // Add a rescheduled revlog entry + collection.log_rescheduled_review(card, original_interval, usn)?; + + Ok(()) + }; + self.update_memory_state_for_cards_with_items( items, - last_revlog_info, - req, &fsrs, - rescheduler, - timing, set_decay_and_desired_retention, + reschedule, usn, )?; } @@ -197,18 +269,12 @@ impl Collection { Ok(()) } - #[expect(clippy::too_many_arguments)] fn update_memory_state_for_cards_with_items( &mut self, mut items: Vec<(CardId, FsrsItemForMemoryState)>, - last_revlog_info: HashMap, - - req: &UpdateMemoryStateRequest, fsrs: &FSRS, - mut rescheduler: Option, - timing: SchedTimingToday, mut set_decay_and_desired_retention: impl FnMut(&mut Card), - + mut maybe_reschedule_card: impl FnMut(&mut Card, &mut Self, &FSRS) -> Result<()>, usn: Usn, ) -> Result<()> { const ITEM_CHUNK_SIZE: usize = 100_000; @@ -247,72 +313,11 @@ impl Collection { { let memory_states = fsrs.memory_state_batch(fsrs_items, starting_states)?; - for (mut card, memory_state) in to_update.into_iter().zip(memory_states) { + for (mut card, memory_state) in to_update.into_iter().zip_eq(memory_states) { let original = card.clone(); set_decay_and_desired_retention(&mut card); card.memory_state = Some(memory_state.into()); - - 'reschedule_card: { - // we have a last review time for the card - let Some(last_info) = last_revlog_info.get(&card.id) else { - break 'reschedule_card; - }; - let Some(last_review) = &last_info.last_reviewed_at else { - break 'reschedule_card; - }; - // the card isn't in (re)learning - if card.ctype != CardType::Review { - break 'reschedule_card; - }; - - let deck = self - .get_deck(card.original_or_current_deck_id())? - .or_not_found(card.original_or_current_deck_id())?; - let deckconfig_id = deck.config_id().unwrap(); - // reschedule it - let days_elapsed = - timing.next_day_at.elapsed_days_since(*last_review) as i32; - let original_interval = card.interval; - let interval = fsrs.next_interval( - Some(memory_state.stability), - card.desired_retention - .expect("We set desired retention above"), - 0, - ); - card.interval = rescheduler - .as_mut() - .and_then(|r| { - r.find_interval( - interval, - 1, - req.max_interval, - days_elapsed as u32, - deckconfig_id, - get_fuzz_seed(&card, true), - ) - }) - .unwrap_or_else(|| { - with_review_fuzz( - card.get_fuzz_factor(true), - interval, - 1, - req.max_interval, - ) - }); - let due = if card.original_due != 0 { - &mut card.original_due - } else { - &mut card.due - }; - let new_due = - (timing.days_elapsed as i32) - days_elapsed + card.interval as i32; - if let Some(rescheduler) = &mut rescheduler { - rescheduler.update_due_cnt_per_day(*due, new_due, deckconfig_id); - } - *due = new_due; - // Add a rescheduled revlog entry - self.log_rescheduled_review(&card, original_interval, usn)?; - } + maybe_reschedule_card(&mut card, self, fsrs)?; self.update_card_inner(&mut card, original, usn)?; } } From 77dacfb808e1ab0dd0a409071a7cdd23c3b38080 Mon Sep 17 00:00:00 2001 From: Daniel Pechersky Date: Tue, 16 Sep 2025 03:19:47 +0700 Subject: [PATCH 09/14] Fix bug --- rslib/src/scheduler/fsrs/memory_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index f4f343b29..30a944347 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -102,7 +102,7 @@ impl Collection { )?; // clear FSRS data if FSRS is disabled self.clear_fsrs_data_for_cards(items.into_iter().map(|(card_id, _)| card_id), usn)?; - return Ok(()); + continue; }; let fsrs = FSRS::new(Some(&req.params[..]))?; From 39e8711996ae33533a56449c3489d91c750daaa0 Mon Sep 17 00:00:00 2001 From: Daniel Pechersky Date: Tue, 16 Sep 2025 03:18:43 +0700 Subject: [PATCH 10/14] Smoke test --- rslib/src/scheduler/fsrs/memory_state.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 30a944347..c969f4c50 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -627,4 +627,27 @@ mod tests { ); Ok(()) } + + mod update_memory_state { + use crate::collection::CollectionBuilder; + + use super::*; + + #[test] + fn smoke() { + let mut collection = CollectionBuilder::default().build().unwrap(); + let entry = UpdateMemoryStateEntry { + req: None, + search: SearchNode::WholeCollection, + ignore_before: TimestampMillis(0), + }; + + collection + .transact(Op::UpdateDeckConfig, |collection| { + collection.update_memory_state(vec![entry]).unwrap(); + Ok(()) + }) + .unwrap(); + } + } } From f2d6c30036870cf4772044103b02055d3bc2badc Mon Sep 17 00:00:00 2001 From: Daniel Pechersky Date: Tue, 16 Sep 2025 13:28:41 +0700 Subject: [PATCH 11/14] Incorporate L-M's suggestions --- rslib/src/scheduler/fsrs/memory_state.rs | 51 +++++++++++++++--------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index c969f4c50..9044c6602 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -65,7 +65,7 @@ trait ChunkIntoVecs { impl ChunkIntoVecs for Vec { fn chunk_into_vecs(&mut self, chunk_size: usize) -> impl Iterator> { std::iter::from_fn(move || { - (!self.is_empty()).then(|| self.split_off(chunk_size.min(self.len()))) + (!self.is_empty()).then(|| self.drain(..chunk_size.min(self.len())).collect()) }) } } @@ -100,8 +100,15 @@ impl Collection { 0.9, ignore_before, )?; + + let on_updated_card = self.create_progress_closure(items.len())?; + // clear FSRS data if FSRS is disabled - self.clear_fsrs_data_for_cards(items.into_iter().map(|(card_id, _)| card_id), usn)?; + self.clear_fsrs_data_for_cards( + items.into_iter().map(|(card_id, _)| card_id), + usn, + on_updated_card, + )?; continue; }; @@ -116,6 +123,8 @@ impl Collection { ignore_before, )?; + let mut on_updated_card = self.create_progress_closure(items.len())?; + let (items, cards_without_items): (Vec<(CardId, FsrsItemForMemoryState)>, Vec) = items.into_iter().partition_map(|(card_id, item)| { if let Some(item) = item { @@ -146,6 +155,7 @@ impl Collection { cards_without_items, set_decay_and_desired_retention, usn, + &mut on_updated_card, )?; let mut rescheduler = self @@ -228,24 +238,34 @@ impl Collection { set_decay_and_desired_retention, reschedule, usn, + on_updated_card, )?; } Ok(()) } + fn create_progress_closure(&self, item_count: usize) -> Result Result<()>> { + let mut progress = self.new_progress_handler::(); + progress.update(false, |s| { + s.total_cards = item_count as u32; + s.current_cards = 1; + })?; + let on_updated_card = move || progress.update(true, |p| p.current_cards += 1); + Ok(on_updated_card) + } + fn clear_fsrs_data_for_cards( &mut self, - cards: impl ExactSizeIterator, + cards: impl Iterator, usn: Usn, + mut on_updated_card: impl FnMut() -> Result<()>, ) -> Result<()> { - let mut progress = self.new_progress_handler::(); - progress.update(false, |s| s.total_cards = cards.len() as u32)?; - for (idx, card_id) in cards.enumerate() { - progress.update(true, |state| state.current_cards = idx as u32 + 1)?; + for card_id in cards { let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; let original = card.clone(); card.clear_fsrs_data(); self.update_card_inner(&mut card, original, usn)?; + on_updated_card()? } Ok(()) } @@ -255,16 +275,15 @@ impl Collection { cards: Vec, mut set_decay_and_desired_retention: impl FnMut(&mut Card), usn: Usn, + mut on_updated_card: impl FnMut() -> Result<()>, ) -> Result<()> { - let mut progress = self.new_progress_handler::(); - progress.update(false, |s| s.total_cards = cards.len() as u32)?; - for (idx, card_id) in cards.into_iter().enumerate() { - progress.update(true, |state| state.current_cards = idx as u32 + 1)?; + for card_id in cards { let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; let original = card.clone(); set_decay_and_desired_retention(&mut card); card.memory_state = None; self.update_card_inner(&mut card, original, usn)?; + on_updated_card()?; } Ok(()) } @@ -276,6 +295,7 @@ impl Collection { mut set_decay_and_desired_retention: impl FnMut(&mut Card), mut maybe_reschedule_card: impl FnMut(&mut Card, &mut Self, &FSRS) -> Result<()>, usn: Usn, + mut on_updated_card: impl FnMut() -> Result<()>, ) -> Result<()> { const ITEM_CHUNK_SIZE: usize = 100_000; const FSRS_CHUNK_SIZE: usize = 1000; @@ -284,13 +304,7 @@ impl Collection { let mut fsrs_items = Vec::new(); let mut starting_states = Vec::new(); - let mut progress = self.new_progress_handler::(); - progress.update(false, |s| s.total_cards = items.len() as u32)?; - for (i, items) in items.chunk_into_vecs(ITEM_CHUNK_SIZE).enumerate() { - progress.update(true, |state| { - let end_of_chunk_index = i * ITEM_CHUNK_SIZE + items.len(); - state.current_cards = end_of_chunk_index as u32 + 1 - })?; + for items in items.chunk_into_vecs(ITEM_CHUNK_SIZE) { for (card_id, item) in items.into_iter() { let card = self.storage.get_card(card_id)?.or_not_found(card_id)?; @@ -319,6 +333,7 @@ impl Collection { card.memory_state = Some(memory_state.into()); maybe_reschedule_card(&mut card, self, fsrs)?; self.update_card_inner(&mut card, original, usn)?; + on_updated_card()?; } } } From 1514b997856d491cae83767e9690b43ef08951aa Mon Sep 17 00:00:00 2001 From: Daniel Pechersky Date: Tue, 16 Sep 2025 16:17:06 +0700 Subject: [PATCH 12/14] Remove overall item batching (still keeping FSRS batching) --- rslib/src/scheduler/fsrs/memory_state.rs | 57 +++++++++++------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 9044c6602..ec7a1577d 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -290,51 +290,48 @@ impl Collection { fn update_memory_state_for_cards_with_items( &mut self, - mut items: Vec<(CardId, FsrsItemForMemoryState)>, + items: Vec<(CardId, FsrsItemForMemoryState)>, fsrs: &FSRS, mut set_decay_and_desired_retention: impl FnMut(&mut Card), mut maybe_reschedule_card: impl FnMut(&mut Card, &mut Self, &FSRS) -> Result<()>, usn: Usn, mut on_updated_card: impl FnMut() -> Result<()>, ) -> Result<()> { - const ITEM_CHUNK_SIZE: usize = 100_000; - const FSRS_CHUNK_SIZE: usize = 1000; + const FSRS_BATCH_SIZE: usize = 1000; let mut to_update = Vec::new(); let mut fsrs_items = Vec::new(); let mut starting_states = Vec::new(); - for items in items.chunk_into_vecs(ITEM_CHUNK_SIZE) { - for (card_id, item) in items.into_iter() { - let card = self.storage.get_card(card_id)?.or_not_found(card_id)?; + for (card_id, item) in items.into_iter() { + let card = self.storage.get_card(card_id)?.or_not_found(card_id)?; - to_update.push(card); - fsrs_items.push(item.item); - starting_states.push(item.starting_state); - } + to_update.push(card); + fsrs_items.push(item.item); + starting_states.push(item.starting_state); + } - // fsrs.memory_state_batch is O(nm) where n is the number of cards and m is the max review count between all items. - // Therefore we want to pass batches to fsrs.memory_state_batch where the review count is relatively even. - let mut p = permutation::sort_unstable_by_key(&fsrs_items, |item| item.reviews.len()); - p.apply_slice_in_place(&mut to_update); - p.apply_slice_in_place(&mut fsrs_items); - p.apply_slice_in_place(&mut starting_states); + // fsrs.memory_state_batch is O(nm) where n is the number of cards and m is the max review count between all items. + // Therefore we want to pass batches to fsrs.memory_state_batch where the review count is relatively even. + let mut p = permutation::sort_unstable_by_key(&fsrs_items, |item| item.reviews.len()); + p.apply_slice_in_place(&mut to_update); + p.apply_slice_in_place(&mut fsrs_items); + p.apply_slice_in_place(&mut starting_states); - for ((to_update, fsrs_items), starting_states) in to_update - .chunk_into_vecs(FSRS_CHUNK_SIZE) - .zip_eq(fsrs_items.chunk_into_vecs(FSRS_CHUNK_SIZE)) - .zip_eq(starting_states.chunk_into_vecs(FSRS_CHUNK_SIZE)) - { - let memory_states = fsrs.memory_state_batch(fsrs_items, starting_states)?; + for ((to_update, fsrs_items), starting_states) in to_update + .chunk_into_vecs(FSRS_BATCH_SIZE) + .zip_eq(fsrs_items.chunk_into_vecs(FSRS_BATCH_SIZE)) + .zip_eq(starting_states.chunk_into_vecs(FSRS_BATCH_SIZE)) + { + let memory_states = fsrs.memory_state_batch(fsrs_items, starting_states)?; - for (mut card, memory_state) in to_update.into_iter().zip_eq(memory_states) { - let original = card.clone(); - set_decay_and_desired_retention(&mut card); - card.memory_state = Some(memory_state.into()); - maybe_reschedule_card(&mut card, self, fsrs)?; - self.update_card_inner(&mut card, original, usn)?; - on_updated_card()?; - } + for (mut card, memory_state) in to_update.into_iter().zip_eq(memory_states) { + let original = card.clone(); + set_decay_and_desired_retention(&mut card); + card.memory_state = Some(memory_state.into()); + maybe_reschedule_card(&mut card, self, fsrs)?; + self.update_card_inner(&mut card, original, usn)?; + on_updated_card()?; } } Ok(()) From 46d291bd24c5a967aa50f34d11f88ddb7813859b Mon Sep 17 00:00:00 2001 From: Daniel Pechersky Date: Tue, 16 Sep 2025 16:35:37 +0700 Subject: [PATCH 13/14] Minor improvement --- rslib/src/scheduler/fsrs/memory_state.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index ec7a1577d..c213e2ec0 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -304,9 +304,7 @@ impl Collection { let mut starting_states = Vec::new(); for (card_id, item) in items.into_iter() { - let card = self.storage.get_card(card_id)?.or_not_found(card_id)?; - - to_update.push(card); + to_update.push(card_id); fsrs_items.push(item.item); starting_states.push(item.starting_state); } @@ -325,7 +323,8 @@ impl Collection { { let memory_states = fsrs.memory_state_batch(fsrs_items, starting_states)?; - for (mut card, memory_state) in to_update.into_iter().zip_eq(memory_states) { + for (card_id, memory_state) in to_update.into_iter().zip_eq(memory_states) { + let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; let original = card.clone(); set_decay_and_desired_retention(&mut card); card.memory_state = Some(memory_state.into()); From 2eab86a633f5984758f3c0a6698905add9923db0 Mon Sep 17 00:00:00 2001 From: Daniel Pechersky Date: Tue, 16 Sep 2025 16:31:05 +0700 Subject: [PATCH 14/14] Pass some lints --- Cargo.toml | 55 ++++-------------------- rslib/src/scheduler/fsrs/memory_state.rs | 11 ++--- 2 files changed, 14 insertions(+), 52 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f9ed269aa..35511e64f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,10 +63,7 @@ bitflags = "2.9.1" blake3 = "1.8.2" bytes = "1.10.1" camino = "1.1.10" -chrono = { version = "0.4.41", default-features = false, features = [ - "std", - "clock", -] } +chrono = { version = "0.4.41", default-features = false, features = ["std", "clock"] } clap = { version = "4.5.40", features = ["derive"] } coarsetime = "0.1.36" convert_case = "0.8.0" @@ -111,26 +108,12 @@ prost-build = "0.13" prost-reflect = "0.14.7" prost-types = "0.13" pulldown-cmark = "0.13.0" -pyo3 = { version = "0.25.1", features = [ - "extension-module", - "abi3", - "abi3-py39", -] } +pyo3 = { version = "0.25.1", features = ["extension-module", "abi3", "abi3-py39"] } rand = "0.9.1" rayon = "1.10.0" regex = "1.11.1" -reqwest = { version = "0.12.20", default-features = false, features = [ - "json", - "socks", - "stream", - "multipart", -] } -rusqlite = { version = "0.36.0", features = [ - "trace", - "functions", - "collation", - "bundled", -] } +reqwest = { version = "0.12.20", default-features = false, features = ["json", "socks", "stream", "multipart"] } +rusqlite = { version = "0.36.0", features = ["trace", "functions", "collation", "bundled"] } rustls-pemfile = "2.2.0" scopeguard = "1.2.0" serde = { version = "1.0.219", features = ["derive"] } @@ -146,18 +129,10 @@ syn = { version = "2.0.103", features = ["parsing", "printing"] } tar = "0.4.44" tempfile = "3.20.0" termcolor = "1.4.1" -tokio = { version = "1.45", features = [ - "fs", - "rt-multi-thread", - "macros", - "signal", -] } +tokio = { version = "1.45", features = ["fs", "rt-multi-thread", "macros", "signal"] } tokio-util = { version = "0.7.15", features = ["io"] } tower-http = { version = "0.6.6", features = ["trace"] } -tracing = { version = "0.1.41", features = [ - "max_level_trace", - "release_max_level_debug", -] } +tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] } tracing-appender = "0.2.3" tracing-subscriber = { version = "0.3.20", features = ["fmt", "env-filter"] } unic-langid = { version = "0.9.6", features = ["macros"] } @@ -167,24 +142,10 @@ walkdir = "2.5.0" which = "8.0.0" widestring = "1.1.0" winapi = { version = "0.3", features = ["wincon", "winreg"] } -windows = { version = "0.61.3", features = [ - "Media_SpeechSynthesis", - "Media_Core", - "Foundation_Collections", - "Storage_Streams", - "Win32_System_Console", - "Win32_System_Registry", - "Win32_System_SystemInformation", - "Win32_Foundation", - "Win32_UI_Shell", - "Wdk_System_SystemServices", -] } +windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams", "Win32_System_Console", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_Foundation", "Win32_UI_Shell", "Wdk_System_SystemServices"] } wiremock = "0.6.3" xz2 = "0.1.7" -zip = { version = "4.1.0", default-features = false, features = [ - "deflate", - "time", -] } +zip = { version = "4.1.0", default-features = false, features = ["deflate", "time"] } zstd = { version = "0.13.3", features = ["zstdmt"] } # Apply mild optimizations to our dependencies in dev mode, which among other things diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index c213e2ec0..a65c1c79f 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -9,7 +9,8 @@ use fsrs::MemoryState; use fsrs::FSRS; use fsrs::FSRS5_DEFAULT_DECAY; use fsrs::FSRS6_DEFAULT_DECAY; -use itertools::{Either, Itertools}; +use itertools::Either; +use itertools::Itertools; use super::params::ignore_revlogs_before_ms_from_config; use super::rescheduler::Rescheduler; @@ -309,8 +310,9 @@ impl Collection { starting_states.push(item.starting_state); } - // fsrs.memory_state_batch is O(nm) where n is the number of cards and m is the max review count between all items. - // Therefore we want to pass batches to fsrs.memory_state_batch where the review count is relatively even. + // fsrs.memory_state_batch is O(nm) where n is the number of cards and m is the + // max review count between all items. Therefore we want to pass batches + // to fsrs.memory_state_batch where the review count is relatively even. let mut p = permutation::sort_unstable_by_key(&fsrs_items, |item| item.reviews.len()); p.apply_slice_in_place(&mut to_update); p.apply_slice_in_place(&mut fsrs_items); @@ -640,9 +642,8 @@ mod tests { } mod update_memory_state { - use crate::collection::CollectionBuilder; - use super::*; + use crate::collection::CollectionBuilder; #[test] fn smoke() {