//! 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(())
}