//! Times the engine's stages -- HTML parsing, parsing plus styling (`Document::parse`), //! box-tree construction, and layout -- over the 227-case recipe corpus and, with //! `--page `, over one real page. The first measurement `roadmap.md` -> "Substrate //! rework" item 4 asks for before any incremental restyle or layout cache is built. //! //! cargo run --release -p webengine --example bench -- [--page ] [--rounds ] //! //! Each document is run `rounds` times (default 20) and its fastest round kept, which //! is the least noisy estimate of what the code costs; the corpus figures are the sum of //! those minima over every case. Styling is `Document::parse` minus parsing alone, since //! the two run as one call. use std::time::{Duration, Instant}; use anyhow::{bail, Context, Result}; use dom::Document; use layout::boxtree::BoxTree; use layout::fragment; const VIEWPORT: (f64, f64) = (800.0, 600.0); #[derive(Default, Clone, Copy)] struct Stages { parse: Duration, parse_and_style: Duration, box_tree: Duration, layout: Duration, } impl Stages { fn min(self, other: Stages) -> Stages { Stages { parse: self.parse.min(other.parse), parse_and_style: self.parse_and_style.min(other.parse_and_style), box_tree: self.box_tree.min(other.box_tree), layout: self.layout.min(other.layout), } } fn add(self, other: Stages) -> Stages { Stages { parse: self.parse + other.parse, parse_and_style: self.parse_and_style + other.parse_and_style, box_tree: self.box_tree + other.box_tree, layout: self.layout + other.layout, } } } fn once(html: &str) -> Stages { let start = Instant::now(); let nodes = dom::parse::parse_html(html); let parse = start.elapsed(); drop(nodes); let start = Instant::now(); let document = Document::parse_with_viewport(html, VIEWPORT); let parse_and_style = start.elapsed(); let start = Instant::now(); let tree = BoxTree::build(&document); let box_tree = start.elapsed(); let start = Instant::now(); let root = fragment::layout(&document, &tree, VIEWPORT); let layout = start.elapsed(); drop(root); Stages { parse, parse_and_style, box_tree, layout } } fn fastest(html: &str, rounds: usize) -> Stages { let max = Duration::MAX; (0..rounds).fold(Stages { parse: max, parse_and_style: max, box_tree: max, layout: max }, |best, _| { best.min(once(html)) }) } fn report(label: &str, documents: usize, s: Stages) { let style = s.parse_and_style.saturating_sub(s.parse); let total = s.parse_and_style + s.box_tree + s.layout; let ms = |d: Duration| d.as_secs_f64() * 1e3; println!( "{label:<22} {documents:>4} doc(s) parse {:>8.3} ms style {:>8.3} ms box tree {:>8.3} ms layout {:>8.3} ms total {:>8.3} ms", ms(s.parse), ms(style), ms(s.box_tree), ms(s.layout), ms(total), ); } fn main() -> Result<()> { let mut page = None; let mut rounds = 20usize; let mut args = std::env::args().skip(1); while let Some(arg) = args.next() { match arg.as_str() { "--page" => page = Some(args.next().context("--page needs a path")?), "--rounds" => rounds = args.next().context("--rounds needs a value")?.parse()?, other => bail!("unknown argument: {other}"), } } let mut cases = harness::cases::enumerated(); cases.extend(harness::cases::random(0xC0FFEE, 200)); let corpus = cases.iter().fold(Stages::default(), |sum, case| sum.add(fastest(&case.html, rounds))); report("recipe corpus", cases.len(), corpus); if let Some(path) = page { let html = std::fs::read_to_string(&path).with_context(|| format!("reading {path}"))?; let document = Document::parse_with_viewport(&html, VIEWPORT); let elements = document.elements_in_document_order().len(); drop(document); println!("page: {path} ({} bytes, {elements} elements)", html.len()); report("page", 1, fastest(&html, rounds)); } Ok(()) }