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, || {
50            wysiwyg::build(&nodes, &src, None, false, &HashMap::new(), None)
51                .rows
52                .len()
53        });
54        {
55            // The incremental path with a warm cache and nothing changed: the
56            // floor cost the block cache adds even on a pure repaint — hash every
57            // block, clone every reused row, recollect stops. No subtree is
58            // marshalled (every block hits). The real keystroke win shows up in
59            // "Doc::insert + rebuild" below, which re-marshals only the edited
60            // block and reuses the rest.
61            let mut cache = wysiwyg::BlockCache::default();
62            let top = ed.child_spans(None).unwrap();
63            let _ = wysiwyg::build_cached(
64                &top,
65                &src,
66                None,
67                false,
68                &HashMap::new(),
69                None,
70                &mut cache,
71                |id| ed.subtree(twig::NodeId(id)).unwrap_or_default(),
72            );
73            time("wysiwyg::build_cached (all reused)", 5, || {
74                let top = ed.child_spans(None).unwrap();
75                wysiwyg::build_cached(
76                    &top,
77                    &src,
78                    None,
79                    false,
80                    &HashMap::new(),
81                    None,
82                    &mut cache,
83                    |id| ed.subtree(twig::NodeId(id)).unwrap_or_default(),
84                )
85                .rows
86                .len()
87            });
88        }
89
90        println!("  -- claimed hot, actually noise --");
91        time("twig source_str() (full copy)", 5, || {
92            ed.source_str().unwrap().len()
93        });
94        let clean = src.clone();
95        time("dirty compare (full cmp)", 5, || src == clean);
96
97        println!("  -- what the GUI adds on a cache miss --");
98        time("clone every row's glyphs", 5, || {
99            map.rows
100                .iter()
101                .map(|r| r.glyphs.clone())
102                .collect::<Vec<_>>()
103                .len()
104        });
105        time("hash every glyph (cache key?)", 5, || {
106            let mut n = 0u64;
107            for r in &map.rows {
108                let mut h = std::collections::hash_map::DefaultHasher::new();
109                for g in &r.glyphs {
110                    g.ch.hash(&mut h);
111                }
112                n ^= h.finish();
113            }
114            n
115        });
116
117        println!("  -- the whole path, as a frontend calls it --");
118        let mut p = std::env::temp_dir();
119        p.push(format!("leaf_bench_{kb}.md"));
120        std::fs::write(&p, &src).unwrap();
121        let mut d = Doc::open(p).unwrap();
122        d.view = View::Wysiwyg;
123        d.place_caret(src.len() / 2, false);
124        d.build_visual_unwrapped();
125        time("build_visual (cached: a repaint)", 200, || {
126            d.build_visual_unwrapped()
127        });
128        time("Doc::insert + rebuild (a keystroke)", 5, || {
129            d.insert("x");
130            d.build_visual_unwrapped();
131        });
132        println!();
133    }
134}