add daily count updating to backend

This commit is contained in:
Damien Elmes 2020-06-05 19:49:53 +10:00
parent fee6cdff22
commit 1fe18718f7
10 changed files with 231 additions and 40 deletions

View file

@ -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;
}

View file

@ -55,6 +55,7 @@ StockNoteType = pb.StockNoteType
SyncAuth = pb.SyncAuth
SyncOutput = pb.SyncCollectionOut
SyncStatus = pb.SyncStatusOut
CountsForDeckToday = pb.CountsForDeckTodayOut
try:
import orjson

View file

@ -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]

View file

@ -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)

View file

@ -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<Empty> {
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<Empty> {
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<pb::CountsForDeckTodayOut> {
self.with_col(|col| col.counts_for_deck_today(input.did.into()))
}
// decks
//-----------------------------------------------

View file

@ -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<DeckKind> 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<pb::CountsForDeckTodayOut> {
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<F>(
&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)]

View file

@ -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,
},
}
}

View file

@ -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();

View file

@ -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<Vec<Deck>> {
let mut decks: Vec<Deck> = 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,

View file

@ -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