From 1fe18718f7aa3b0e295b934f2130079635e86780 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 5 Jun 2020 19:49:53 +1000 Subject: [PATCH] add daily count updating to backend --- proto/backend.proto | 26 +++++++++- pylib/anki/rsbackend.py | 1 + pylib/anki/sched.py | 21 +++++--- pylib/anki/schedv2.py | 72 ++++++++++++++++----------- rslib/src/backend/mod.rs | 34 +++++++++++++ rslib/src/decks/mod.rs | 93 ++++++++++++++++++++++++++++++++++- rslib/src/decks/schema11.rs | 4 +- rslib/src/decks/tree.rs | 2 - rslib/src/storage/deck/mod.rs | 15 ++++++ rspy/src/lib.rs | 3 ++ 10 files changed, 231 insertions(+), 40 deletions(-) diff --git a/proto/backend.proto b/proto/backend.proto index ee954826e..b125dede8 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -93,6 +93,9 @@ service BackendService { rpc SchedTimingToday (Empty) returns (SchedTimingTodayOut); rpc StudiedToday (StudiedTodayIn) returns (String); rpc CongratsLearnMessage (CongratsLearnMessageIn) returns (String); + rpc UpdateStats (UpdateStatsIn) returns (Empty); + rpc ExtendLimits (ExtendLimitsIn) returns (Empty); + rpc CountsForDeckToday (DeckID) returns (CountsForDeckTodayOut); // media @@ -250,8 +253,11 @@ message DeckCommon { uint32 last_day_studied = 3; int32 new_studied = 4; int32 review_studied = 5; + int32 milliseconds_studied = 7; + + // previously set in the v1 scheduler, + // but not currently used for anything int32 learning_studied = 6; - int32 secs_studied = 7; bytes other = 255; } @@ -945,3 +951,21 @@ message RemoveNotesIn { message RemoveCardsIn { repeated int64 card_ids = 1; } + +message UpdateStatsIn { + int64 deck_id = 1; + int32 new_delta = 2; + int32 review_delta = 4; + int32 millisecond_delta = 5; +} + +message ExtendLimitsIn { + int64 deck_id = 1; + int32 new_delta = 2; + int32 review_delta = 3; +} + +message CountsForDeckTodayOut { + int32 new = 1; + int32 review = 2; +} diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 7b84f6680..dca6a55af 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -55,6 +55,7 @@ StockNoteType = pb.StockNoteType SyncAuth = pb.SyncAuth SyncOutput = pb.SyncCollectionOut SyncStatus = pb.SyncStatusOut +CountsForDeckToday = pb.CountsForDeckTodayOut try: import orjson diff --git a/pylib/anki/sched.py b/pylib/anki/sched.py index f4ac3248c..9895e87e9 100644 --- a/pylib/anki/sched.py +++ b/pylib/anki/sched.py @@ -51,6 +51,10 @@ class Scheduler(V2): # former is for logging new cards, latter also covers filt. decks card.wasNew = card.type == CARD_TYPE_NEW # type: ignore wasNewQ = card.queue == QUEUE_TYPE_NEW + + new_delta = 0 + review_delta = 0 + if wasNewQ: # came from the new queue, move to learning card.queue = QUEUE_TYPE_LRN @@ -65,17 +69,22 @@ class Scheduler(V2): # reviews get their ivl boosted on first sight card.ivl = self._dynIvlBoost(card) card.odue = self.today + card.ivl - self._updateStats(card, "new") + new_delta = +1 if card.queue in (QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN): self._answerLrnCard(card, ease) - if not wasNewQ: - self._updateStats(card, "lrn") elif card.queue == QUEUE_TYPE_REV: self._answerRevCard(card, ease) - self._updateStats(card, "rev") + review_delta = +1 else: raise Exception("Invalid queue '%s'" % card) - self._updateStats(card, "time", card.timeTaken()) + + self.update_stats( + card.did, + new_delta=new_delta, + review_delta=review_delta, + milliseconds_delta=+card.timeTaken(), + ) + card.mod = intTime() card.usn = self.col.usn() card.flush() @@ -447,7 +456,7 @@ and due <= ? limit ?)""", if d["dyn"]: return self.reportLimit c = self.col.decks.confForDid(d["id"]) - limit = max(0, c["rev"]["perDay"] - self._update_stats(d, "rev", 0)) + limit = max(0, c["rev"]["perDay"] - self.counts_for_deck_today(d["id"]).review) return hooks.scheduler_review_limit_for_single_deck(limit, d) def _revForDeck(self, did: int, lim: int) -> int: # type: ignore[override] diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index d37848eb4..dcbd8c3cf 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -14,7 +14,12 @@ from anki import hooks from anki.cards import Card from anki.consts import * from anki.lang import _ -from anki.rsbackend import DeckTreeNode, FormatTimeSpanContext, SchedTimingToday +from anki.rsbackend import ( + CountsForDeckToday, + DeckTreeNode, + FormatTimeSpanContext, + SchedTimingToday, +) from anki.utils import ids2str, intTime # card types: 0=new, 1=lrn, 2=rev, 3=relrn @@ -82,7 +87,6 @@ class Scheduler: self._answerCard(card, ease) - self._updateStats(card, "time", card.timeTaken()) card.mod = intTime() card.usn = self.col.usn() card.flush() @@ -94,24 +98,32 @@ class Scheduler: card.reps += 1 + new_delta = 0 + review_delta = 0 + if card.queue == QUEUE_TYPE_NEW: # came from the new queue, move to learning card.queue = QUEUE_TYPE_LRN card.type = CARD_TYPE_LRN # init reps to graduation card.left = self._startingLeft(card) - # update daily limit - self._updateStats(card, "new") + new_delta = +1 if card.queue in (QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN): self._answerLrnCard(card, ease) elif card.queue == QUEUE_TYPE_REV: self._answerRevCard(card, ease) - # update daily limit - self._updateStats(card, "rev") + review_delta = +1 else: raise Exception("Invalid queue '%s'" % card) + self.update_stats( + card.did, + new_delta=new_delta, + review_delta=review_delta, + milliseconds_delta=+card.timeTaken(), + ) + # once a card has been answered once, the original due date # no longer applies if card.odue: @@ -187,29 +199,33 @@ order by due""" # Rev/lrn/time daily stats ########################################################################## - def _updateStats(self, card: Card, type: str, cnt: int = 1) -> None: - for g in [self.col.decks.get(card.did)] + self.col.decks.parents(card.did): - self._update_stats(g, type, cnt) - self.col.decks.save(g) + def update_stats( + self, deck_id: int, new_delta=0, review_delta=0, milliseconds_delta=0 + ): + self.col.backend.update_stats( + deck_id=deck_id, + new_delta=new_delta, + review_delta=review_delta, + millisecond_delta=milliseconds_delta, + ) - # resets stat if day has changed, applies delta, and returns modified value - def _update_stats(self, deck: Dict, type: str, delta: int) -> int: - key = type + "Today" - if deck[key][0] != self.today: - deck[key] = [self.today, 0] - deck[key][1] += delta - return deck[key][1] + def counts_for_deck_today(self, deck_id: int) -> CountsForDeckToday: + return self.col.backend.counts_for_deck_today(deck_id) def extendLimits(self, new: int, rev: int) -> None: - cur = self.col.decks.current() - parents = self.col.decks.parents(cur["id"]) - children = [ - self.col.decks.get(did) for did in self.col.decks.child_ids(cur["name"]) - ] - for g in [cur] + parents + children: - self._update_stats(g, "new", -new) - self._update_stats(g, "rev", -rev) - self.col.decks.save(g) + did = self.col.decks.current()["id"] + self.col.backend.extend_limits(deck_id=did, new_delta=new, review_delta=rev) + + # legacy + + def _updateStats(self, card: Card, type: str, cnt: int = 1) -> None: + did = card.did + if type == "new": + self.update_stats(did, new_delta=cnt) + elif type == "rev": + self.update_stats(did, review_delta=cnt) + elif type == "time": + self.update_stats(did, milliseconds_delta=cnt) # Deck list ########################################################################## @@ -373,7 +389,7 @@ select count() from if g["dyn"]: return self.dynReportLimit c = self.col.decks.confForDid(g["id"]) - limit = max(0, c["new"]["perDay"] - self._update_stats(g, "new", 0)) + limit = max(0, c["new"]["perDay"] - self.counts_for_deck_today(g["id"]).new) return hooks.scheduler_new_limit_for_single_deck(limit, g) def totalNewForCurrentDeck(self) -> int: @@ -766,7 +782,7 @@ and due <= ? limit ?)""", return self.dynReportLimit c = self.col.decks.confForDid(d["id"]) - lim = max(0, c["rev"]["perDay"] - self._update_stats(d, "rev", 0)) + lim = max(0, c["rev"]["perDay"] - self.counts_for_deck_today(d["id"]).review) if parentLimit is not None: lim = min(parentLimit, lim) diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index bd83e2f73..5c5ed2280 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -467,6 +467,40 @@ impl BackendService for Backend { Ok(learning_congrats(input.remaining as usize, input.next_due, &self.i18n).into()) } + fn update_stats(&mut self, input: pb::UpdateStatsIn) -> BackendResult { + self.with_col(|col| { + col.transact(None, |col| { + let today = col.current_due_day(0)?; + let usn = col.usn()?; + col.update_deck_stats(today, usn, input).map(Into::into) + }) + }) + } + + fn extend_limits(&mut self, input: pb::ExtendLimitsIn) -> BackendResult { + self.with_col(|col| { + col.transact(None, |col| { + let today = col.current_due_day(0)?; + let usn = col.usn()?; + col.extend_limits( + today, + usn, + input.deck_id.into(), + input.new_delta, + input.review_delta, + ) + .map(Into::into) + }) + }) + } + + fn counts_for_deck_today( + &mut self, + input: pb::DeckId, + ) -> BackendResult { + self.with_col(|col| col.counts_for_deck_today(input.did.into())) + } + // decks //----------------------------------------------- diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index 5fe348ed3..bfc2ea8ff 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -69,6 +69,17 @@ impl Deck { kind: DeckKind::Filtered(filt), } } + + fn reset_stats_if_day_changed(&mut self, today: u32) { + let c = &mut self.common; + if c.last_day_studied != today { + c.new_studied = 0; + c.learning_studied = 0; + c.review_studied = 0; + c.milliseconds_studied = 0; + c.last_day_studied = today; + } + } } impl Deck { @@ -185,7 +196,7 @@ impl From for pb::deck::Kind { } } -fn immediate_parent_name(machine_name: &str) -> Option<&str> { +pub(crate) fn immediate_parent_name(machine_name: &str) -> Option<&str> { machine_name.rsplitn(2, '\x1f').nth(1) } @@ -465,6 +476,86 @@ impl Collection { }) .collect()) } + + /// Apply input delta to deck, and its parents. + /// Caller should ensure transaction. + pub(crate) fn update_deck_stats( + &mut self, + today: u32, + usn: Usn, + input: pb::UpdateStatsIn, + ) -> Result<()> { + let did = input.deck_id.into(); + let mutator = |c: &mut DeckCommon| { + c.new_studied += input.new_delta; + c.review_studied += input.review_delta; + c.milliseconds_studied += input.millisecond_delta; + }; + if let Some(mut deck) = self.storage.get_deck(did)? { + self.update_deck_stats_single(today, usn, &mut deck, mutator)?; + for mut deck in self.storage.parent_decks(&deck)? { + self.update_deck_stats_single(today, usn, &mut deck, mutator)?; + } + } + Ok(()) + } + + /// Modify the deck's limits by adjusting the 'done today' count. + /// Positive values increase the limit, negative value decrease it. + /// Caller should ensure a transaction. + pub(crate) fn extend_limits( + &mut self, + today: u32, + usn: Usn, + did: DeckID, + new_delta: i32, + review_delta: i32, + ) -> Result<()> { + let mutator = |c: &mut DeckCommon| { + c.new_studied -= new_delta; + c.review_studied -= review_delta; + }; + if let Some(mut deck) = self.storage.get_deck(did)? { + self.update_deck_stats_single(today, usn, &mut deck, mutator)?; + for mut deck in self.storage.parent_decks(&deck)? { + self.update_deck_stats_single(today, usn, &mut deck, mutator)?; + } + for mut deck in self.storage.child_decks(&deck)? { + self.update_deck_stats_single(today, usn, &mut deck, mutator)?; + } + } + + Ok(()) + } + + pub(crate) fn counts_for_deck_today( + &mut self, + did: DeckID, + ) -> Result { + let today = self.current_due_day(0)?; + let mut deck = self.storage.get_deck(did)?.ok_or(AnkiError::NotFound)?; + deck.reset_stats_if_day_changed(today); + Ok(pb::CountsForDeckTodayOut { + new: deck.common.new_studied, + review: deck.common.review_studied, + }) + } + + fn update_deck_stats_single( + &mut self, + today: u32, + usn: Usn, + deck: &mut Deck, + mutator: F, + ) -> Result<()> + where + F: FnOnce(&mut DeckCommon), + { + deck.reset_stats_if_day_changed(today); + mutator(&mut deck.common); + deck.set_modified(usn); + self.add_or_update_single_deck(deck, usn) + } } #[cfg(test)] diff --git a/rslib/src/decks/schema11.rs b/rslib/src/decks/schema11.rs index ea55fb743..8f89b4c96 100644 --- a/rslib/src/decks/schema11.rs +++ b/rslib/src/decks/schema11.rs @@ -280,7 +280,7 @@ impl From<&DeckCommonSchema11> for DeckCommon { new_studied: today.new.amount, review_studied: today.rev.amount, learning_studied: today.lrn.amount, - secs_studied: common.today.time.amount, + milliseconds_studied: common.today.time.amount, other, } } @@ -393,7 +393,7 @@ impl From<&Deck> for DeckTodaySchema11 { }, time: TodayAmountSchema11 { day, - amount: c.secs_studied, + amount: c.milliseconds_studied, }, } } diff --git a/rslib/src/decks/tree.rs b/rslib/src/decks/tree.rs index 79932cb75..f98fb6cf6 100644 --- a/rslib/src/decks/tree.rs +++ b/rslib/src/decks/tree.rs @@ -16,8 +16,6 @@ use std::{ }; use unicase::UniCase; -// fixme: handle mixed case of parents - fn deck_names_to_tree(names: Vec<(DeckID, String)>) -> DeckTreeNode { let mut top = DeckTreeNode::default(); let mut it = names.into_iter().peekable(); diff --git a/rslib/src/storage/deck/mod.rs b/rslib/src/storage/deck/mod.rs index ceedeb770..99820d283 100644 --- a/rslib/src/storage/deck/mod.rs +++ b/rslib/src/storage/deck/mod.rs @@ -6,6 +6,7 @@ use crate::{ card::CardID, card::CardQueue, config::SchedulerVersion, + decks::immediate_parent_name, decks::{Deck, DeckCommon, DeckID, DeckKindProto, DeckSchema11, DueCounts}, err::{AnkiError, DBErrorKind, Result}, i18n::{I18n, TR}, @@ -152,6 +153,20 @@ impl SqliteStorage { .collect() } + pub(crate) fn parent_decks(&self, child: &Deck) -> Result> { + let mut decks: Vec = vec![]; + while let Some(parent_name) = + immediate_parent_name(decks.last().map(|d| &d.name).unwrap_or_else(|| &child.name)) + { + if let Some(parent_did) = self.get_deck_id(parent_name)? { + let parent = self.get_deck(parent_did)?.unwrap(); + decks.push(parent); + } + } + + Ok(decks) + } + pub(crate) fn due_counts( &self, sched: SchedulerVersion, diff --git a/rspy/src/lib.rs b/rspy/src/lib.rs index ed176bca1..bdf9c7d8f 100644 --- a/rspy/src/lib.rs +++ b/rspy/src/lib.rs @@ -115,6 +115,9 @@ fn want_release_gil(method: u32) -> bool { BackendMethod::FullDownload => true, BackendMethod::RemoveNotes => true, BackendMethod::RemoveCards => true, + BackendMethod::UpdateStats => true, + BackendMethod::ExtendLimits => true, + BackendMethod::CountsForDeckToday => true, } } else { false