Skip to main content

bench/
bench.rs

1//! Where does leaf's per-keystroke and per-paint time actually go?
2//!
3//! `cargo run --release -p leaf-core --example bench`
4use leaf_core::{Doc, View, wysiwyg};
5use std::collections::HashMap;
6use std::hash::{Hash, Hasher};
7use std::time::Instant;
8use twig::{Editor, Format};
9
10fn body(bytes: usize) -> String {
11    let mut s = String::new();
12    let mut i = 0;
13    while s.len() < bytes {
14        s.push_str(&format!(
15            "## Section {i}\n\nThe quick brown fox jumps over the lazy dog, and \
16             **bold** text with a [link](https://example.dev) and `code` besides. \
17             Another sentence follows to make the paragraph a realistic length.\n\n"
18        ));
19        i += 1;
20    }
21    s
22}
23
24fn time<T>(label: &str, n: usize, mut f: impl FnMut() -> T) -> f64 {
25    let t = Instant::now();
26    for _ in 0..n {
27        std::hint::black_box(f());
28    }
29    let ms = t.elapsed().as_secs_f64() * 1000.0 / n as f64;
30    println!("  {label:<30}{ms:8.2} ms");
31    ms
32}
33
34fn main() {
35    for kb in [10usize, 100, 1000] {
36        let src = body(kb * 1024);
37        println!("=== {} KB ===", src.len() / 1024);
38
39        let mut ed = Editor::new_str(&src, Format::Markdown).unwrap();
40        let nodes = ed.nodes().unwrap();
41        let map = wysiwyg::build(&nodes, &src, None, false, &HashMap::new(), None);
42        println!("  ({} AST nodes, {} map rows)", nodes.len(), map.rows.len());
43
44        println!("  -- per edit (unavoidable today) --");
45        time("twig edit_range (reparse)", 5, || {
46            ed.edit_range(src.len() / 2, src.len() / 2, "x").is_ok()
47        });
48        time("twig nodes() FFI marshal", 5, || ed.nodes().unwrap().len());
49        time("wysiwyg::build", 5, || wysiwyg::build(&nodes, &src, None, false, &HashMap::new(), None).rows.len());
50        {
51            // The incremental path with a warm cache and nothing changed: the
52            // floor cost the block cache adds even on a pure repaint — hash every
53            // block, clone every reused row, recollect stops. No subtree is
54            // marshalled (every block hits). The real keystroke win shows up in
55            // "Doc::insert + rebuild" below, which re-marshals only the edited
56            // block and reuses the rest.
57            let mut cache = wysiwyg::BlockCache::default();
58            let top = ed.child_spans(None).unwrap();
59            let _ = wysiwyg::build_cached(&top, &src, None, false, &HashMap::new(), None, &mut cache, |id| {
60                ed.subtree(twig::NodeId(id)).unwrap_or_default()
61            });
62            time("wysiwyg::build_cached (all reused)", 5, || {
63                let top = ed.child_spans(None).unwrap();
64                wysiwyg::build_cached(&top, &src, None, false, &HashMap::new(), None, &mut cache, |id| {
65                    ed.subtree(twig::NodeId(id)).unwrap_or_default()
66                })
67                .rows
68                .len()
69            });
70        }
71
72        println!("  -- claimed hot, actually noise --");
73        time("twig source_str() (full copy)", 5, || ed.source_str().unwrap().len());
74        let clean = src.clone();
75        time("dirty compare (full cmp)", 5, || src == clean);
76
77        println!("  -- what the GUI adds on a cache miss --");
78        time("clone every row's glyphs", 5, || {
79            map.rows.iter().map(|r| r.glyphs.clone()).collect::<Vec<_>>().len()
80        });
81        time("hash every glyph (cache key?)", 5, || {
82            let mut n = 0u64;
83            for r in &map.rows {
84                let mut h = std::collections::hash_map::DefaultHasher::new();
85                for g in &r.glyphs {
86                    g.ch.hash(&mut h);
87                }
88                n ^= h.finish();
89            }
90            n
91        });
92
93        println!("  -- the whole path, as a frontend calls it --");
94        let mut p = std::env::temp_dir();
95        p.push(format!("leaf_bench_{kb}.md"));
96        std::fs::write(&p, &src).unwrap();
97        let mut d = Doc::open(p).unwrap();
98        d.view = View::Wysiwyg;
99        d.place_caret(src.len() / 2, false);
100        d.build_visual_unwrapped();
101        time("build_visual (cached: a repaint)", 200, || d.build_visual_unwrapped());
102        time("Doc::insert + rebuild (a keystroke)", 5, || {
103            d.insert("x");
104            d.build_visual_unwrapped();
105        });
106        println!();
107    }
108}