Try speeding up update_memory_state by parallelizing card.set_memory_state

This commit is contained in:
Daniel Pechersky 2025-09-14 18:43:43 +07:00
parent e5b7ed6cad
commit a5e9a063e5

View file

@ -10,6 +10,7 @@ use fsrs::FSRS;
use fsrs::FSRS5_DEFAULT_DECAY; use fsrs::FSRS5_DEFAULT_DECAY;
use fsrs::FSRS6_DEFAULT_DECAY; use fsrs::FSRS6_DEFAULT_DECAY;
use itertools::Itertools; use itertools::Itertools;
use rayon::iter::{IntoParallelRefMutIterator as _, ParallelIterator as _};
use super::params::ignore_revlogs_before_ms_from_config; use super::params::ignore_revlogs_before_ms_from_config;
use super::rescheduler::Rescheduler; use super::rescheduler::Rescheduler;
@ -111,12 +112,12 @@ impl Collection {
}; };
let preset_desired_retention = req.preset_desired_retention; 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() { for (idx, (card_id, item)) in items.into_iter().enumerate() {
progress.update(true, |state| state.current_cards = idx as u32 + 1)?; 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 mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?;
let original = card.clone(); let original = card.clone();
'update_card: {
// Store decay and desired retention in the card so that add-ons, card info, // 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. // 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. // Unlike memory states, scheduler doesn't use decay and dr stored in the card.
@ -127,33 +128,44 @@ impl Collection {
.unwrap_or(&preset_desired_retention); .unwrap_or(&preset_desired_retention);
card.desired_retention = Some(desired_retention); card.desired_retention = Some(desired_retention);
card.decay = decay; card.decay = decay;
let Some(item) = item else { if let Some(item) = item {
to_update_memory_state.push((card, original, item));
} else {
// clear memory states if item is None // clear memory states if item is None
card.memory_state = None; card.memory_state = None;
break 'update_card; self.update_card_inner(&mut card, original, usn)?;
}; }
card.set_memory_state(&fsrs, Some(item), historical_retention.unwrap())?; }
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 // if rescheduling
let Some(reviews) = &last_revlog_info else { let Some(reviews) = &last_revlog_info else {
break 'update_card; break 'reschedule_card;
}; };
// and we have a last review time for the card // and we have a last review time for the card
let Some(last_info) = reviews.get(&card.id) else { 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 { let Some(last_review) = &last_info.last_reviewed_at else {
break 'update_card; break 'reschedule_card;
}; };
// and the card's not new // and the card's not new
let Some(state) = &card.memory_state else { let Some(state) = &card.memory_state else {
break 'update_card; break 'reschedule_card;
}; };
// or in (re)learning // or in (re)learning
if card.ctype != CardType::Review { if card.ctype != CardType::Review {
break 'update_card; break 'reschedule_card;
}; };
let deck = self let deck = self
@ -163,7 +175,12 @@ impl Collection {
// reschedule it // reschedule it
let days_elapsed = timing.next_day_at.elapsed_days_since(*last_review) as i32; let days_elapsed = timing.next_day_at.elapsed_days_since(*last_review) as i32;
let original_interval = card.interval; 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 card.interval = rescheduler
.as_mut() .as_mut()
.and_then(|r| { .and_then(|r| {
@ -198,7 +215,6 @@ impl Collection {
// Add a rescheduled revlog entry // Add a rescheduled revlog entry
self.log_rescheduled_review(&card, original_interval, usn)?; self.log_rescheduled_review(&card, original_interval, usn)?;
} }
self.update_card_inner(&mut card, original, 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(crate) struct FsrsItemForMemoryState {
pub item: FSRSItem, pub item: FSRSItem,
/// When revlogs have been truncated, this stores the initial state at first /// When revlogs have been truncated, this stores the initial state at first