#![expect(
clippy::unwrap_used,
clippy::print_stderr,
clippy::cast_precision_loss,
clippy::format_push_string,
clippy::shadow_unrelated,
reason = "a benchmark: fixed inputs that always parse, a generated document, sizes printed"
)]
use std::hint::black_box;
use criterion::{Criterion, criterion_group, criterion_main};
const TASKS: usize = 4000;
const LONG_STRINGS: usize = 300;
const LONG_STRING_WORDS: usize = 400;
fn tasks() -> String {
let mut out = String::from("{\n \"name\": \"bench\",\n \"tasks\": [\n");
for i in 0..TASKS {
let f = i as f64;
out.push_str(&format!(
" {{ \"id\": {i}, \"name\": \"task_{i}\", \"parts\": [\n {{ \"type\": \"estimate\", \"range\": [{:.3}, {:.3}, {:.3}], \"weights\": [1.0, 0.0, 0.0, 0.0] }}",
f * 0.5,
(f * 0.25).sin(),
-f
));
if i % 4 != 0 {
out.push_str(&format!(
",\n {{ \"type\": \"attachment\", \"file\": \"@file:images/photo_{}.png\", \"owner\": \"@user:alice\" }}",
i % 16
));
}
if i % 3 == 0 {
out.push_str(",\n { \"type\": \"review\", \"status\": \"open\", \"hours\": 2.5 }");
}
out.push_str(if i + 1 < TASKS {
"\n ] },\n"
} else {
"\n ] }\n"
});
}
out.push_str(" ]\n}\n");
out
}
fn long_strings() -> String {
let mut out = String::from("{\n");
for i in 0..LONG_STRINGS {
out.push_str(&format!(" // note {i}\n /* a block comment, {i} */\n"));
out.push_str(&format!(" \"note_{i}\": \""));
for w in 0..LONG_STRING_WORDS {
out.push_str(&format!("word{} naïve — 日本語 ", (i + w) % 97));
}
out.push_str("\",\n");
}
out.push_str("}\n");
out
}
fn bench_long(c: &mut Criterion) {
let text = long_strings();
let value = substrate_data::text::parse_value(&text).unwrap();
let bytes = substrate_data::binary::encode(&value);
eprintln!(
"long strings: text {} bytes, binary {} bytes",
text.len(),
bytes.len()
);
let mut g = c.benchmark_group("parse_long_strings");
g.bench_function("substrate_data text", |b| {
b.iter(|| substrate_data::text::parse_value(black_box(&text)).unwrap());
});
g.bench_function("substrate_data binary", |b| {
b.iter(|| substrate_data::binary::decode(black_box(&bytes)).unwrap());
});
g.bench_function("source text (from bytes)", |b| {
b.iter(|| substrate_data::source::text(black_box(text.as_bytes())).unwrap());
});
g.finish();
}
fn bench(c: &mut Criterion) {
let book = tasks();
let doc_text = book.as_str();
let value = substrate_data::text::parse_value(doc_text).unwrap();
let bytes = substrate_data::binary::encode(&value);
let json: serde_json::Value = serde_json::from_str(doc_text).unwrap();
eprintln!(
"text {} bytes, binary {} bytes",
doc_text.len(),
bytes.len()
);
let mut g = c.benchmark_group("parse");
g.bench_function("serde_json", |b| {
b.iter(|| serde_json::from_str::<serde_json::Value>(black_box(doc_text)).unwrap());
});
g.bench_function("substrate_data text", |b| {
b.iter(|| substrate_data::text::parse_value(black_box(doc_text)).unwrap());
});
g.bench_function("substrate_data text + spans", |b| {
b.iter(|| substrate_data::text::parse(black_box(doc_text)).unwrap());
});
g.bench_function("substrate_data binary", |b| {
b.iter(|| substrate_data::binary::decode(black_box(&bytes)).unwrap());
});
g.finish();
let mut g = c.benchmark_group("write");
g.bench_function("serde_json", |b| {
b.iter(|| serde_json::to_string(black_box(&json)).unwrap());
});
g.bench_function("substrate_data text", |b| {
b.iter(|| substrate_data::text::to_string(black_box(&value)));
});
g.bench_function("substrate_data binary", |b| {
b.iter(|| substrate_data::binary::encode(black_box(&value)));
});
g.finish();
}
criterion_group!(benches, bench, bench_long);
criterion_main!(benches);