//! Render-path benchmarks. //! //! docengine sits on the MNW per-request render path (creator descriptions, //! forum posts) and the startup doc-load path (site-docs). These establish a //! baseline to measure any perf refactor against — parser reuse, `Arc` //! sharing, or a directive pass that avoids re-scanning the whole HTML. //! //! Run with: `cargo bench --features full`. use std::collections::HashMap; use std::hint::black_box; use std::path::PathBuf; use criterion::{Criterion, criterion_group, criterion_main}; use docengine::{ DocLoader, DocLoaderConfig, QuoteAuthor, Renderer, post_process_directives, post_process_quotes, }; /// A ~2 KB markdown description, the shape of a typical creator project blurb or /// a short forum post: headings, emphasis, a link, a list, a code span, a table. fn description_2kb() -> String { let block = "\ ## What this is A small, focused tool for **doing one thing well**. It stays out of your way and does not phone home. See the [guide](/docs/guide/overview) for the full tour, or jump straight to `quickstart` below. - Fast, native builds — no runtime to install - Full export, no lock-in - Works offline first, syncs when you want it to | Tier | Price | Files | |------|-------|-------| | Basic | $16 | text | | Big Files | $36 | large | > A short aside about why this exists and who it is for. "; // The block is ~520 bytes; four copies lands near 2 KB. let mut s = String::with_capacity(2200); for _ in 0..4 { s.push_str(block); } s } /// A ~20 KB long-form doc page: many sections, alert directives, and images — /// the heavy end of what a single site-docs page renders to. fn doc_page_20kb() -> String { let section = "\ ## Section heading Longer explanatory prose that runs a few sentences so the parser has real text to walk, not just markup. It references [another page](/docs/guide/other) and uses `inline code` plus **bold** and *italic* spans throughout the paragraph. ![a diagram](https://cdn.makenot.work/img/diagram.png) > [!NOTE] > A callout that the directives post-processor will rewrite into a styled alert > block. These are common in the developer docs. > [!WARNING] > A second callout of a different type, to exercise the type-matching arm. 1. First ordered step with some detail 2. Second step that continues the thought 3. Third step to close it out "; // The section is ~640 bytes; ~32 copies lands near 20 KB. let mut s = String::with_capacity(21_000); for _ in 0..32 { s.push_str(section); } s } /// Locate the live site-docs corpus relative to this crate. Returns `None` when /// the MNW server tree is not checked out alongside docengine (e.g. the crate /// was vendored standalone), so the load benchmark self-skips instead of failing. fn site_docs_path() -> Option { let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../server/site-docs/public"); p.is_dir().then_some(p) } fn server_doc_config() -> DocLoaderConfig { // Mirrors the sections the MNW server registers in main.rs. pre_process is // left None: assumption substitution is a separate cost, benched elsewhere. DocLoaderConfig { sections: vec![ ("about".to_string(), "About".to_string()), ("guide".to_string(), "Guide".to_string()), ("developer".to_string(), "Developer".to_string()), ("legal".to_string(), "Legal".to_string()), ("support".to_string(), "Support".to_string()), ("tech".to_string(), "Tech".to_string()), ], link_prefix: "/docs".to_string(), unpublished_pattern: Some("unpublished/".to_string()), examples_path: None, pre_process: None, } } // (1) Renderer::render across the four presets on a ~2 KB description. fn bench_presets(c: &mut Criterion) { let input = description_2kb(); let mut group = c.benchmark_group("render_presets_2kb"); for (name, renderer) in [ ("permissive", Renderer::permissive()), ("standard", Renderer::standard()), ("strict", Renderer::strict()), ("sanitize_only", Renderer::sanitize_only()), ] { group.bench_function(name, |b| { b.iter(|| renderer.render(black_box(&input))); }); } group.finish(); } // (2) render on a large ~20 KB doc page with directives + images (permissive, // the preset site-docs pages render under). fn bench_large_page(c: &mut Criterion) { let input = doc_page_20kb(); c.bench_function("render_permissive_20kb_page", |b| { let renderer = Renderer::permissive(); b.iter(|| renderer.render(black_box(&input))); }); } // (3) DocLoader::load startup on the current server/site-docs corpus. fn bench_doc_loader(c: &mut Criterion) { let Some(path) = site_docs_path() else { eprintln!("skipping doc_loader_load: site-docs corpus not found alongside crate"); return; }; c.bench_function("doc_loader_load_site_docs", |b| { b.iter(|| DocLoader::load(black_box(&path), &server_doc_config())); }); } // (4) post_process_directives + post_process_quotes, separately, on // already-rendered HTML (both run after render() on the request path). fn bench_post_process(c: &mut Criterion) { // Directives operate on rendered HTML, so render the alert-heavy page first. let directive_html = Renderer::permissive().render(&doc_page_20kb()); c.bench_function("post_process_directives_20kb", |b| { b.iter(|| post_process_directives(black_box(&directive_html))); }); // Quotes replace [quote:UUID:HASH] markers with author attribution. Build a // page carrying several markers and a matching author map. let id = uuid::Uuid::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef); let marker = format!("[quote:{id}:0123abcd]"); let mut quote_html = String::with_capacity(4096); for _ in 0..16 { quote_html.push_str("

Some preceding paragraph text for context.

\n"); quote_html.push_str("

"); quote_html.push_str(&marker); quote_html.push_str("

\n"); } let mut authors = HashMap::new(); authors.insert( id, QuoteAuthor { username: "creator".to_string(), display_name: "A Creator".to_string(), is_removed: false, }, ); c.bench_function("post_process_quotes_16_markers", |b| { b.iter(|| post_process_quotes(black_box("e_html), black_box(&authors))); }); } criterion_group!( benches, bench_presets, bench_large_page, bench_doc_loader, bench_post_process ); criterion_main!(benches);