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

View file

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

View file

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

View file

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

View file

@ -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<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> {
// convert string map to &str
let fields: HashMap<_, _> = input

View file

@ -16,6 +16,7 @@ use crate::{
types::Usn,
};
mod schema11;
mod tree;
pub use schema11::DeckSchema11;
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(())
}
}