use compmd::context::RenderContext;
use compmd::expand::Options;
use compmd::host::{LoadedRelease, Release};
use compmd::processor::Expanded;
use compmd::{Ambient, ParseOptions, Processor, TextEdit};
use criterion::{Criterion, SamplingMode, criterion_group, criterion_main};
use std::collections::HashMap;
use std::hint::black_box;
use std::path::Path;
use std::rc::Rc;
use std::time::{Duration, Instant};
const SAMPLES: usize = 1_000;
const KEYSTROKE: Duration = Duration::from_millis(1);
fn fixture(path: &str) -> String {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../wasm/bench/fixtures/notes");
std::fs::read_to_string(root.join(path)).expect("a fixture")
}
fn packs() -> HashMap<String, Vec<LoadedRelease>> {
let files = ["index.comp.md", "package.json"]
.into_iter()
.map(|file| (file.to_string(), fixture(&format!("preview-pack/{file}"))))
.collect();
HashMap::from([(
"@preview/charts".to_string(),
vec![LoadedRelease {
release: Release {
version: "1.0.0".parse().expect("a version"),
integrity: None,
broken: None,
},
files,
}],
)])
}
fn documents() -> [(&'static str, String); 2] {
let notes = fixture("notes.comp.md");
let (frontmatter, body) = notes.split_at(notes.find("\n# ").expect("a heading") + 1);
let definition = "\n[ref]: https://example.com\n";
[
("52 KB", format!("{notes}{definition}")),
(
"500 KB",
format!("{frontmatter}{}{definition}", body.repeat(10)),
),
]
}
struct Class {
name: &'static str,
edit: TextEdit,
undo: TextEdit,
target: Option<Duration>,
}
fn insertion(name: &'static str, target: Option<Duration>, at: usize, text: &str) -> Class {
Class {
name,
target,
edit: TextEdit {
start: at,
end: at,
text: text.into(),
},
undo: TextEdit {
start: at,
end: at + text.chars().count(),
text: String::new(),
},
}
}
fn classes(text: &str) -> [Class; 4] {
let chars: Vec<char> = text.chars().collect();
let middle = chars.len() / 2;
let sentence_end = middle
+ chars[middle..]
.iter()
.position(|c| *c == '.')
.expect("a sentence");
let label = chars.len() - "ref]: https://example.com\n".chars().count();
[
insertion("keystroke", Some(KEYSTROKE), sentence_end, "x"),
insertion("blank line", Some(KEYSTROKE), sentence_end + 1, "\n\n"),
insertion("label", None, label, "x"),
insertion(
"frontmatter",
None,
"---\nmd:\n version: 1".chars().count(),
"\n title: x",
),
]
}
fn measured(
c: &mut Criterion,
group: &str,
name: &str,
flat: bool,
target: Option<Duration>,
mut op: impl FnMut(),
) {
let mut samples = Vec::new();
let mut group = c.benchmark_group(group);
if flat {
group.sampling_mode(SamplingMode::Flat).sample_size(10);
}
group.bench_function(name, |b| {
b.iter_custom(|iters| {
let mut total = Duration::ZERO;
for _ in 0..iters {
let started = Instant::now();
op();
let elapsed = started.elapsed();
samples.push(elapsed);
total += elapsed;
}
total
});
});
group.finish();
if samples.is_empty() {
return;
}
let recent = if flat {
&samples[..]
} else {
&samples[samples.len().saturating_sub(SAMPLES)..]
};
let mut sorted = recent.to_vec();
sorted.sort();
let at = |share: usize| sorted[(sorted.len() - 1) * share / 100];
let (p50, p95) = (at(50), at(95));
println!(
"{name}: p50 {:.3} ms, p95 {:.3} ms{}",
p50.as_secs_f64() * 1000.0,
p95.as_secs_f64() * 1000.0,
target.map_or_else(String::new, |target| format!(
", target {} ms: {}",
target.as_millis(),
if p95 <= target { "met" } else { "missed" }
))
);
}
fn edits(c: &mut Criterion) {
let packs = Rc::new(packs());
let ambient = Ambient {
packs: vec!["@preview/charts".into()],
report: false,
};
let context = RenderContext {
now: 0,
tz: "UTC".into(),
time: Rc::new(()),
inputs: serde_json::Map::default(),
};
for (size, text) in documents() {
let files = Rc::new(HashMap::from([("notes.comp.md".to_string(), text.clone())]));
let process = || {
Processor::new("notes.comp.md", text.clone())
.parse(&ParseOptions::default())
.resolve(Rc::clone(&files), Rc::clone(&packs), &ambient)
.validate(&[])
.load(context.clone(), (), ())
.expand(&Options::default())
};
measured(c, "whole", size, true, None, || {
black_box(process());
});
for class in classes(&text) {
let mut processor: Option<Processor<Expanded>> = Some(process());
let mut undo = false;
measured(
c,
&format!("edit {size}"),
class.name,
false,
class.target,
|| {
let edit = if undo { &class.undo } else { &class.edit };
undo = !undo;
processor = processor.take().map(|processor| processor.edit(edit));
},
);
}
}
}
criterion_group! {
name = benches;
config = Criterion::default().measurement_time(Duration::from_secs(10));
targets = edits
}
criterion_main!(benches);