From a88bc1e8361b416324e2a1a304baea1376e30afb Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 3 May 2020 08:43:22 +1000 Subject: [PATCH] speed up browser load by rendering deck tree in Rust and skipping counts --- proto/backend.proto | 26 +++++----- pylib/anki/decks.py | 3 ++ pylib/anki/rsbackend.py | 6 +++ qt/aqt/browser.py | 30 +++++------ rslib/src/backend/mod.rs | 12 ++++- rslib/src/decks/mod.rs | 1 + rslib/src/decks/tree.rs | 105 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 153 insertions(+), 30 deletions(-) create mode 100644 rslib/src/decks/tree.rs diff --git a/proto/backend.proto b/proto/backend.proto index 6a505c37c..e56d4e6ad 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -30,7 +30,7 @@ message I18nBackendInit { message BackendInput { oneof value { SchedTimingTodayIn sched_timing_today = 17; - Empty deck_tree = 18; + DeckTreeIn deck_tree = 18; SearchCardsIn search_cards = 19; SearchNotesIn search_notes = 20; RenderCardIn render_card = 21; @@ -108,7 +108,7 @@ message BackendOutput { AllStockNotetypesOut all_stock_notetypes = 60; // fallible commands - DeckTreeOut deck_tree = 18; + DeckTreeNode deck_tree = 18; SearchCardsOut search_cards = 19; SearchNotesOut search_notes = 20; RenderCardOut render_card = 21; @@ -238,19 +238,20 @@ message SchedTimingTodayOut { int64 next_day_at = 2; } -message DeckTreeOut { - DeckTreeNode top = 1; +message DeckTreeIn { + bool include_counts = 1; } message DeckTreeNode { - // the components of a deck, split on :: - repeated string names = 1; - int64 deck_id = 2; - uint32 review_count = 3; - uint32 learn_count = 4; - uint32 new_count = 5; - repeated DeckTreeNode children = 6; - bool collapsed = 7; + int64 deck_id = 1; + string name = 2; + repeated DeckTreeNode children = 3; + uint32 level = 4; + bool collapsed = 5; + + uint32 review_count = 6; + uint32 learn_count = 7; + uint32 new_count = 8; } message RenderCardIn { @@ -705,3 +706,4 @@ message AddOrUpdateDeckLegacyIn { bytes deck = 1; bool preserve_usn_and_mtime = 2; } + diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index d1c6ec379..c3656e268 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -219,6 +219,9 @@ class DeckManager: except anki.rsbackend.ExistsError: raise DeckRenameError("deck already exists") + def deck_tree(self) -> pb.DeckTreeNode: + return self.col.backend.deck_tree(include_counts=False) + def all(self, force_default: bool = True) -> List: """A list of all decks. diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 7edf4fe97..34b3286c1 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -49,6 +49,7 @@ BackendCard = pb.Card BackendNote = pb.Note TagUsnTuple = pb.TagUsnTuple NoteType = pb.NoteType +DeckTreeNode = pb.DeckTreeNode try: import orjson @@ -729,6 +730,11 @@ class RustBackend: def remove_deck(self, did: int) -> None: self._run_command(pb.BackendInput(remove_deck=did)) + def deck_tree(self, include_counts: bool) -> DeckTreeNode: + return self._run_command( + pb.BackendInput(deck_tree=pb.DeckTreeIn(include_counts=include_counts)) + ).deck_tree + def translate_string_in( key: TR, **kwargs: Union[str, int, float] diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index fe850796e..52ff71916 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -24,7 +24,7 @@ from anki.decks import DeckManager from anki.lang import _, ngettext from anki.models import NoteType from anki.notes import Note -from anki.rsbackend import TR +from anki.rsbackend import TR, DeckTreeNode from anki.utils import htmlToTextLine, ids2str, intTime, isMac, isWin from aqt import AnkiQt, gui_hooks from aqt.editor import Editor @@ -1147,32 +1147,28 @@ QTableView {{ gridline-color: {grid} }} root.addChild(item) def _decksTree(self, root) -> None: - assert self.col - grps = self.col.sched.deckDueTree() + tree = self.col.decks.deck_tree() - def fillGroups(root, grps, head=""): - for g in grps: - baseName = g[0] - did = g[1] - children = g[5] - if str(did) == "1" and not children: + def fillGroups(root, nodes: List[DeckTreeNode], head=""): + for node in nodes: + if node.deck_id == 1 and not node.children: if not self.mw.col.decks.should_default_be_displayed( force_default=False, assume_no_child=True ): - continue + item = SidebarItem( - baseName, + node.name, ":/icons/deck.svg", - lambda baseName=baseName: self.setFilter("deck", head + baseName), - lambda expanded, did=did: self.mw.col.decks.collapseBrowser(did), - not self.mw.col.decks.get(did).get("browserCollapsed", False), + lambda baseName=node.name: self.setFilter("deck", head + baseName), + lambda expanded, did=node.deck_id: self.mw.col.decks.collapseBrowser(did), + not self.mw.col.decks.get(node.deck_id).get("browserCollapsed", False), ) root.addChild(item) - newhead = head + baseName + "::" - fillGroups(item, children, newhead) + newhead = head + node.name + "::" + fillGroups(item, node.children, newhead) - fillGroups(root, grps) + fillGroups(root, tree.children) def _modelTree(self, root) -> None: assert self.col diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 32e12a64e..f9d05021f 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -217,7 +217,7 @@ impl Backend { Value::SchedTimingToday(input) => { OValue::SchedTimingToday(self.sched_timing_today(input)) } - Value::DeckTree(_) => todo!(), + Value::DeckTree(input) => OValue::DeckTree(self.deck_tree(input)?), Value::RenderCard(input) => OValue::RenderCard(self.render_template(input)?), Value::LocalMinutesWest(stamp) => { OValue::LocalMinutesWest(local_minutes_west_for_stamp(stamp)) @@ -436,6 +436,16 @@ impl Backend { } } + fn deck_tree(&self, input: pb::DeckTreeIn) -> Result { + self.with_col(|col| { + if input.include_counts { + todo!() + } else { + col.deck_tree() + } + }) + } + fn render_template(&self, input: pb::RenderCardIn) -> Result { // convert string map to &str let fields: HashMap<_, _> = input diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index 4f548cb0b..1eb1e6aec 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -16,6 +16,7 @@ use crate::{ types::Usn, }; mod schema11; +mod tree; pub use schema11::DeckSchema11; use std::{borrow::Cow, sync::Arc}; diff --git a/rslib/src/decks/tree.rs b/rslib/src/decks/tree.rs new file mode 100644 index 000000000..dc79029be --- /dev/null +++ b/rslib/src/decks/tree.rs @@ -0,0 +1,105 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::{backend_proto::DeckTreeNode, collection::Collection, decks::DeckID, err::Result}; +use std::iter::Peekable; + +// 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(); + + add_child_nodes(&mut it, &mut top); + + top +} + +fn add_child_nodes( + names: &mut Peekable>, + parent: &mut DeckTreeNode, +) { + while let Some((id, name)) = names.peek() { + let split_name: Vec<_> = name.split("::").collect(); + match split_name.len() as u32 { + l if l <= parent.level => { + // next item is at a higher level + return; + } + l if l == parent.level + 1 => { + // next item is an immediate descendent of parent + parent.children.push(DeckTreeNode { + deck_id: id.0, + name: (*split_name.last().unwrap()).into(), + children: vec![], + level: parent.level + 1, + ..Default::default() + }); + names.next(); + } + _ => { + // next item is at a lower level + if let Some(last_child) = parent.children.last_mut() { + add_child_nodes(names, last_child) + } else { + // immediate parent is missing, skip the deck until a DB check is run + names.next(); + } + } + } + } +} + +impl Collection { + pub fn deck_tree(&self) -> Result { + let names = self.storage.get_all_deck_names()?; + Ok(deck_names_to_tree(names)) + } +} + +#[cfg(test)] +mod test { + use crate::{collection::open_test_collection, err::Result}; + + #[test] + fn wellformed() -> Result<()> { + let mut col = open_test_collection(); + + col.get_or_create_normal_deck("1")?; + col.get_or_create_normal_deck("2")?; + col.get_or_create_normal_deck("2::a")?; + col.get_or_create_normal_deck("2::b")?; + col.get_or_create_normal_deck("2::c")?; + col.get_or_create_normal_deck("2::c::A")?; + col.get_or_create_normal_deck("3")?; + + let tree = col.deck_tree()?; + + // 4 including default + assert_eq!(tree.children.len(), 4); + + assert_eq!(tree.children[1].name, "2"); + assert_eq!(tree.children[1].children[0].name, "a"); + assert_eq!(tree.children[1].children[2].name, "c"); + assert_eq!(tree.children[1].children[2].children[0].name, "A"); + + Ok(()) + } + + #[test] + fn malformed() -> Result<()> { + let mut col = open_test_collection(); + + col.get_or_create_normal_deck("1")?; + col.get_or_create_normal_deck("2::3::4")?; + + // remove the top parent and middle parent + col.storage.remove_deck(col.get_deck_id("2")?.unwrap())?; + col.storage.remove_deck(col.get_deck_id("2::3")?.unwrap())?; + + let tree = col.deck_tree()?; + assert_eq!(tree.children.len(), 2); + + Ok(()) + } +}