1use leaf_core::{Doc, View, source, 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 let mut ed = Editor::new_str(&src, Format::Markdown).unwrap();
57 time("twig nodes() FFI marshal", 5, || ed.nodes().unwrap().len());
58 time("wysiwyg::build", 5, || {
59 wysiwyg::build(&nodes, &src, None, false, &HashMap::new(), None)
60 .rows
61 .len()
62 });
63 time("source::build", 5, || {
68 source::build(&nodes, &src).runs().len()
69 });
70 {
71 let mut cache = wysiwyg::BlockCache::default();
78 let top = ed.child_spans(None).unwrap();
79 let _ = wysiwyg::build_cached(
80 &top,
81 &src,
82 None,
83 false,
84 &HashMap::new(),
85 None,
86 &mut cache,
87 |id| ed.subtree(twig::NodeId(id)).unwrap_or_default(),
88 );
89 time("wysiwyg::build_cached (all reused)", 5, || {
90 let top = ed.child_spans(None).unwrap();
91 wysiwyg::build_cached(
92 &top,
93 &src,
94 None,
95 false,
96 &HashMap::new(),
97 None,
98 &mut cache,
99 |id| ed.subtree(twig::NodeId(id)).unwrap_or_default(),
100 )
101 .rows
102 .len()
103 });
104 }
105
106 println!(" -- claimed hot, actually noise --");
107 time("twig source_str() (full copy)", 5, || {
108 ed.source_str().unwrap().len()
109 });
110 let clean = src.clone();
111 time("dirty compare (full cmp)", 5, || src == clean);
112
113 println!(" -- what the GUI adds on a cache miss --");
114 time("clone every row's glyphs", 5, || {
115 map.rows
116 .iter()
117 .map(|r| r.glyphs.clone())
118 .collect::<Vec<_>>()
119 .len()
120 });
121 time("hash every glyph (cache key?)", 5, || {
122 let mut n = 0u64;
123 for r in &map.rows {
124 let mut h = std::collections::hash_map::DefaultHasher::new();
125 for g in &r.glyphs {
126 g.ch.hash(&mut h);
127 }
128 n ^= h.finish();
129 }
130 n
131 });
132
133 println!(" -- the whole path, as a frontend calls it --");
134 let mut p = std::env::temp_dir();
135 p.push(format!("leaf_bench_{kb}.md"));
136 std::fs::write(&p, &src).unwrap();
137 let mut d = Doc::open(p).unwrap();
138 d.view = View::Wysiwyg;
139 d.place_caret(src.len() / 2, false);
140 d.build_visual_unwrapped();
141 time("build_visual (cached: a repaint)", 200, || {
142 d.build_visual_unwrapped()
143 });
144 time("Doc::insert + rebuild (a keystroke)", 5, || {
145 d.insert("x");
146 d.build_visual_unwrapped();
147 });
148 println!();
149 }
150}