speed up browser load by rendering deck tree in Rust and skipping counts

This commit is contained in:
Damien Elmes 2020-05-03 08:43:22 +10:00
parent 5cefece264
commit a88bc1e836
7 changed files with 153 additions and 30 deletions

View file

@ -30,7 +30,7 @@ message I18nBackendInit {
message BackendInput { message BackendInput {
oneof value { oneof value {
SchedTimingTodayIn sched_timing_today = 17; SchedTimingTodayIn sched_timing_today = 17;
Empty deck_tree = 18; DeckTreeIn deck_tree = 18;
SearchCardsIn search_cards = 19; SearchCardsIn search_cards = 19;
SearchNotesIn search_notes = 20; SearchNotesIn search_notes = 20;
RenderCardIn render_card = 21; RenderCardIn render_card = 21;
@ -108,7 +108,7 @@ message BackendOutput {
AllStockNotetypesOut all_stock_notetypes = 60; AllStockNotetypesOut all_stock_notetypes = 60;
// fallible commands // fallible commands
DeckTreeOut deck_tree = 18; DeckTreeNode deck_tree = 18;
SearchCardsOut search_cards = 19; SearchCardsOut search_cards = 19;
SearchNotesOut search_notes = 20; SearchNotesOut search_notes = 20;
RenderCardOut render_card = 21; RenderCardOut render_card = 21;
@ -238,19 +238,20 @@ message SchedTimingTodayOut {
int64 next_day_at = 2; int64 next_day_at = 2;
} }
message DeckTreeOut { message DeckTreeIn {
DeckTreeNode top = 1; bool include_counts = 1;
} }
message DeckTreeNode { message DeckTreeNode {
// the components of a deck, split on :: int64 deck_id = 1;
repeated string names = 1; string name = 2;
int64 deck_id = 2; repeated DeckTreeNode children = 3;
uint32 review_count = 3; uint32 level = 4;
uint32 learn_count = 4; bool collapsed = 5;
uint32 new_count = 5;
repeated DeckTreeNode children = 6; uint32 review_count = 6;
bool collapsed = 7; uint32 learn_count = 7;
uint32 new_count = 8;
} }
message RenderCardIn { message RenderCardIn {
@ -705,3 +706,4 @@ message AddOrUpdateDeckLegacyIn {
bytes deck = 1; bytes deck = 1;
bool preserve_usn_and_mtime = 2; bool preserve_usn_and_mtime = 2;
} }

View file

@ -219,6 +219,9 @@ class DeckManager:
except anki.rsbackend.ExistsError: except anki.rsbackend.ExistsError:
raise DeckRenameError("deck already exists") 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: def all(self, force_default: bool = True) -> List:
"""A list of all decks. """A list of all decks.

View file

@ -49,6 +49,7 @@ BackendCard = pb.Card
BackendNote = pb.Note BackendNote = pb.Note
TagUsnTuple = pb.TagUsnTuple TagUsnTuple = pb.TagUsnTuple
NoteType = pb.NoteType NoteType = pb.NoteType
DeckTreeNode = pb.DeckTreeNode
try: try:
import orjson import orjson
@ -729,6 +730,11 @@ class RustBackend:
def remove_deck(self, did: int) -> None: def remove_deck(self, did: int) -> None:
self._run_command(pb.BackendInput(remove_deck=did)) 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( def translate_string_in(
key: TR, **kwargs: Union[str, int, float] key: TR, **kwargs: Union[str, int, float]

View file

@ -24,7 +24,7 @@ from anki.decks import DeckManager
from anki.lang import _, ngettext from anki.lang import _, ngettext
from anki.models import NoteType from anki.models import NoteType
from anki.notes import Note 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 anki.utils import htmlToTextLine, ids2str, intTime, isMac, isWin
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.editor import Editor from aqt.editor import Editor
@ -1147,32 +1147,28 @@ QTableView {{ gridline-color: {grid} }}
root.addChild(item) root.addChild(item)
def _decksTree(self, root) -> None: def _decksTree(self, root) -> None:
assert self.col tree = self.col.decks.deck_tree()
grps = self.col.sched.deckDueTree()
def fillGroups(root, grps, head=""): def fillGroups(root, nodes: List[DeckTreeNode], head=""):
for g in grps: for node in nodes:
baseName = g[0] if node.deck_id == 1 and not node.children:
did = g[1]
children = g[5]
if str(did) == "1" and not children:
if not self.mw.col.decks.should_default_be_displayed( if not self.mw.col.decks.should_default_be_displayed(
force_default=False, assume_no_child=True force_default=False, assume_no_child=True
): ):
continue continue
item = SidebarItem( item = SidebarItem(
baseName, node.name,
":/icons/deck.svg", ":/icons/deck.svg",
lambda baseName=baseName: self.setFilter("deck", head + baseName), lambda baseName=node.name: self.setFilter("deck", head + baseName),
lambda expanded, did=did: self.mw.col.decks.collapseBrowser(did), lambda expanded, did=node.deck_id: self.mw.col.decks.collapseBrowser(did),
not self.mw.col.decks.get(did).get("browserCollapsed", False), not self.mw.col.decks.get(node.deck_id).get("browserCollapsed", False),
) )
root.addChild(item) root.addChild(item)
newhead = head + baseName + "::" newhead = head + node.name + "::"
fillGroups(item, children, newhead) fillGroups(item, node.children, newhead)
fillGroups(root, grps) fillGroups(root, tree.children)
def _modelTree(self, root) -> None: def _modelTree(self, root) -> None:
assert self.col assert self.col

View file

@ -217,7 +217,7 @@ impl Backend {
Value::SchedTimingToday(input) => { Value::SchedTimingToday(input) => {
OValue::SchedTimingToday(self.sched_timing_today(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::RenderCard(input) => OValue::RenderCard(self.render_template(input)?),
Value::LocalMinutesWest(stamp) => { Value::LocalMinutesWest(stamp) => {
OValue::LocalMinutesWest(local_minutes_west_for_stamp(stamp)) OValue::LocalMinutesWest(local_minutes_west_for_stamp(stamp))
@ -436,6 +436,16 @@ impl Backend {
} }
} }
fn deck_tree(&self, input: pb::DeckTreeIn) -> Result<pb::DeckTreeNode> {
self.with_col(|col| {
if input.include_counts {
todo!()
} else {
col.deck_tree()
}
})
}
fn render_template(&self, input: pb::RenderCardIn) -> Result<pb::RenderCardOut> { fn render_template(&self, input: pb::RenderCardIn) -> Result<pb::RenderCardOut> {
// convert string map to &str // convert string map to &str
let fields: HashMap<_, _> = input let fields: HashMap<_, _> = input

View file

@ -16,6 +16,7 @@ use crate::{
types::Usn, types::Usn,
}; };
mod schema11; mod schema11;
mod tree;
pub use schema11::DeckSchema11; pub use schema11::DeckSchema11;
use std::{borrow::Cow, sync::Arc}; use std::{borrow::Cow, sync::Arc};

105
rslib/src/decks/tree.rs Normal file
View file

@ -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<impl Iterator<Item = (DeckID, String)>>,
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<DeckTreeNode> {
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(())
}
}