Skip to main content

max / makenotwork

6.5 KB · 187 lines History Blame Raw
1 //! Render-path benchmarks.
2 //!
3 //! docengine sits on the MNW per-request render path (creator descriptions,
4 //! forum posts) and the startup doc-load path (site-docs). These establish a
5 //! baseline to measure any perf refactor against — parser reuse, `Arc<str>`
6 //! sharing, or a directive pass that avoids re-scanning the whole HTML.
7 //!
8 //! Run with: `cargo bench --features full`.
9
10 use std::collections::HashMap;
11 use std::hint::black_box;
12 use std::path::PathBuf;
13
14 use criterion::{Criterion, criterion_group, criterion_main};
15
16 use docengine::{
17 DocLoader, DocLoaderConfig, QuoteAuthor, Renderer, post_process_directives, post_process_quotes,
18 };
19
20 /// A ~2 KB markdown description, the shape of a typical creator project blurb or
21 /// a short forum post: headings, emphasis, a link, a list, a code span, a table.
22 fn description_2kb() -> String {
23 let block = "\
24 ## What this is
25
26 A small, focused tool for **doing one thing well**. It stays out of your way
27 and does not phone home. See the [guide](/docs/guide/overview) for the full
28 tour, or jump straight to `quickstart` below.
29
30 - Fast, native builds — no runtime to install
31 - Full export, no lock-in
32 - Works offline first, syncs when you want it to
33
34 | Tier | Price | Files |
35 |------|-------|-------|
36 | Basic | $16 | text |
37 | Big Files | $36 | large |
38
39 > A short aside about why this exists and who it is for.
40 ";
41 // The block is ~520 bytes; four copies lands near 2 KB.
42 let mut s = String::with_capacity(2200);
43 for _ in 0..4 {
44 s.push_str(block);
45 }
46 s
47 }
48
49 /// A ~20 KB long-form doc page: many sections, alert directives, and images —
50 /// the heavy end of what a single site-docs page renders to.
51 fn doc_page_20kb() -> String {
52 let section = "\
53 ## Section heading
54
55 Longer explanatory prose that runs a few sentences so the parser has real text
56 to walk, not just markup. It references [another page](/docs/guide/other) and
57 uses `inline code` plus **bold** and *italic* spans throughout the paragraph.
58
59 ![a diagram](https://cdn.makenot.work/img/diagram.png)
60
61 > [!NOTE]
62 > A callout that the directives post-processor will rewrite into a styled alert
63 > block. These are common in the developer docs.
64
65 > [!WARNING]
66 > A second callout of a different type, to exercise the type-matching arm.
67
68 1. First ordered step with some detail
69 2. Second step that continues the thought
70 3. Third step to close it out
71 ";
72 // The section is ~640 bytes; ~32 copies lands near 20 KB.
73 let mut s = String::with_capacity(21_000);
74 for _ in 0..32 {
75 s.push_str(section);
76 }
77 s
78 }
79
80 /// Locate the live site-docs corpus relative to this crate. Returns `None` when
81 /// the MNW server tree is not checked out alongside docengine (e.g. the crate
82 /// was vendored standalone), so the load benchmark self-skips instead of failing.
83 fn site_docs_path() -> Option<PathBuf> {
84 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../server/site-docs/public");
85 p.is_dir().then_some(p)
86 }
87
88 fn server_doc_config() -> DocLoaderConfig {
89 // Mirrors the sections the MNW server registers in main.rs. pre_process is
90 // left None: assumption substitution is a separate cost, benched elsewhere.
91 DocLoaderConfig {
92 sections: vec![
93 ("about".to_string(), "About".to_string()),
94 ("guide".to_string(), "Guide".to_string()),
95 ("developer".to_string(), "Developer".to_string()),
96 ("legal".to_string(), "Legal".to_string()),
97 ("support".to_string(), "Support".to_string()),
98 ("tech".to_string(), "Tech".to_string()),
99 ],
100 link_prefix: "/docs".to_string(),
101 unpublished_pattern: Some("unpublished/".to_string()),
102 examples_path: None,
103 pre_process: None,
104 }
105 }
106
107 // (1) Renderer::render across the four presets on a ~2 KB description.
108 fn bench_presets(c: &mut Criterion) {
109 let input = description_2kb();
110 let mut group = c.benchmark_group("render_presets_2kb");
111 for (name, renderer) in [
112 ("permissive", Renderer::permissive()),
113 ("standard", Renderer::standard()),
114 ("strict", Renderer::strict()),
115 ("sanitize_only", Renderer::sanitize_only()),
116 ] {
117 group.bench_function(name, |b| {
118 b.iter(|| renderer.render(black_box(&input)));
119 });
120 }
121 group.finish();
122 }
123
124 // (2) render on a large ~20 KB doc page with directives + images (permissive,
125 // the preset site-docs pages render under).
126 fn bench_large_page(c: &mut Criterion) {
127 let input = doc_page_20kb();
128 c.bench_function("render_permissive_20kb_page", |b| {
129 let renderer = Renderer::permissive();
130 b.iter(|| renderer.render(black_box(&input)));
131 });
132 }
133
134 // (3) DocLoader::load startup on the current server/site-docs corpus.
135 fn bench_doc_loader(c: &mut Criterion) {
136 let Some(path) = site_docs_path() else {
137 eprintln!("skipping doc_loader_load: site-docs corpus not found alongside crate");
138 return;
139 };
140 c.bench_function("doc_loader_load_site_docs", |b| {
141 b.iter(|| DocLoader::load(black_box(&path), &server_doc_config()));
142 });
143 }
144
145 // (4) post_process_directives + post_process_quotes, separately, on
146 // already-rendered HTML (both run after render() on the request path).
147 fn bench_post_process(c: &mut Criterion) {
148 // Directives operate on rendered HTML, so render the alert-heavy page first.
149 let directive_html = Renderer::permissive().render(&doc_page_20kb());
150 c.bench_function("post_process_directives_20kb", |b| {
151 b.iter(|| post_process_directives(black_box(&directive_html)));
152 });
153
154 // Quotes replace [quote:UUID:HASH] markers with author attribution. Build a
155 // page carrying several markers and a matching author map.
156 let id = uuid::Uuid::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
157 let marker = format!("[quote:{id}:0123abcd]");
158 let mut quote_html = String::with_capacity(4096);
159 for _ in 0..16 {
160 quote_html.push_str("<p>Some preceding paragraph text for context.</p>\n");
161 quote_html.push_str("<blockquote><p>");
162 quote_html.push_str(&marker);
163 quote_html.push_str("</p></blockquote>\n");
164 }
165 let mut authors = HashMap::new();
166 authors.insert(
167 id,
168 QuoteAuthor {
169 username: "creator".to_string(),
170 display_name: "A Creator".to_string(),
171 is_removed: false,
172 },
173 );
174 c.bench_function("post_process_quotes_16_markers", |b| {
175 b.iter(|| post_process_quotes(black_box(&quote_html), black_box(&authors)));
176 });
177 }
178
179 criterion_group!(
180 benches,
181 bench_presets,
182 bench_large_page,
183 bench_doc_loader,
184 bench_post_process
185 );
186 criterion_main!(benches);
187