#![allow(clippy::doc_markdown, missing_docs)]
mod fixtures;
use std::hint::black_box;
use std::sync::LazyLock;
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use easydoc::prelude::{
CellData, DocxRow, EventSink, HeadingLevel, Paragraph, RowData, Table, ViewMode,
};
use easydoc::{ConverterRegistry, DocumentEvent, EasyDoc};
use easydoc_core::metadata::TableColumn;
#[derive(Debug, Clone)]
struct BenchRow {
id: u64,
name: String,
amount: f64,
active: bool,
}
impl DocxRow for BenchRow {
fn schema() -> &'static [TableColumn] {
static SCHEMA: LazyLock<Vec<TableColumn>> = LazyLock::new(|| {
vec![
TableColumn::new("ID", "id", 0),
TableColumn::new("Name", "name", 1),
TableColumn::new("Amount", "amount", 2),
TableColumn::new("Active", "active", 3),
]
});
&SCHEMA
}
fn from_row(_row: &RowData) -> easydoc_core::Result<Self> {
unimplemented!("not used in benchmarks")
}
fn from_row_with_converters(
_row: &RowData,
_registry: &ConverterRegistry,
) -> easydoc_core::Result<Self> {
unimplemented!("not used in benchmarks")
}
fn to_row(&self) -> easydoc_core::Result<Vec<CellData>> {
Ok(vec![
CellData::new(i64::try_from(self.id).unwrap_or(i64::MAX)),
CellData::new(self.name.clone()),
CellData::new(self.amount),
CellData::new(self.active),
])
}
fn to_row_with_converters(
&self,
_registry: &ConverterRegistry,
) -> easydoc_core::Result<Vec<CellData>> {
self.to_row()
}
}
fn generate_rows(n: usize) -> Vec<BenchRow> {
(0..n as u64)
.map(|i| BenchRow {
id: i,
name: format!("item_{i:06}"),
amount: (i as f64) * 1.23,
active: i % 3 != 0,
})
.collect()
}
struct Fixture {
_dir: tempfile::TempDir,
small_docx: std::path::PathBuf, medium_docx: std::path::PathBuf, large_docx: std::path::PathBuf, }
static FIXTURE: LazyLock<Fixture> = LazyLock::new(|| {
let dir = tempfile::tempdir().expect("failed to create temp dir for bench fixtures");
let small_path = dir.path().join("small.docx");
let small_rows = generate_rows(50);
EasyDoc::document(&small_path)
.title("Benchmark Document")
.add_heading("Introduction", HeadingLevel::H1)
.add_paragraph(Paragraph::new().add_text(
"This is a synthetic document generated for benchmarking purposes.\
It contains multiple paragraphs, headings, and tables to simulate\
real-world document structure.",
))
.add_heading("Data Table", HeadingLevel::H2)
.add_paragraph(Paragraph::new().add_text("Below is a table with 50 rows of sample data."))
.add_table(Table::from_data(&small_rows))
.add_heading("Conclusion", HeadingLevel::H2)
.add_paragraph(Paragraph::new().add_text("End of benchmark document."))
.save()
.expect("failed to create small fixture");
let medium_path = dir.path().join("medium.docx");
let medium_rows = generate_rows(1_000);
EasyDoc::write_table(&medium_path, &medium_rows)
.title("Medium Benchmark Table")
.do_write()
.expect("failed to create medium fixture");
let large_path = dir.path().join("large.docx");
let large_rows = generate_rows(5_000);
EasyDoc::write_table(&large_path, &large_rows)
.title("Large Benchmark Table")
.do_write()
.expect("failed to create large fixture");
Fixture {
_dir: dir,
small_docx: small_path,
medium_docx: medium_path,
large_docx: large_path,
}
});
fn bench_write_throughput(c: &mut Criterion) {
let mut group = c.benchmark_group("write_throughput");
group.sample_size(50);
for &n in &[100, 500, 1_000] {
let rows = generate_rows(n);
group.bench_with_input(BenchmarkId::from_parameter(n), &rows, |b, rows| {
b.iter(|| {
black_box(EasyDoc::write_table_to_bytes(rows).expect("write failed"));
});
});
}
group.finish();
}
fn bench_read_text(c: &mut Criterion) {
let fx = &*FIXTURE;
let mut group = c.benchmark_group("read_text_throughput");
group.sample_size(50);
group.bench_function("small_docx", |b| {
b.iter(|| {
black_box(EasyDoc::read_text(&fx.small_docx).expect("read failed"));
});
});
group.bench_function("medium_docx", |b| {
b.iter(|| {
black_box(EasyDoc::read_text(&fx.medium_docx).expect("read failed"));
});
});
group.bench_function("large_docx", |b| {
b.iter(|| {
black_box(EasyDoc::read_text(&fx.large_docx).expect("read failed"));
});
});
group.finish();
}
fn bench_view_mode(c: &mut Criterion) {
let fx = &*FIXTURE;
let mut group = c.benchmark_group("view_mode_throughput");
group.sample_size(50);
let modes: &[(&str, ViewMode)] = &[
("Plain", ViewMode::Plain),
("Annotated", ViewMode::Annotated),
("Outline_3", ViewMode::Outline { max_level: 3 }),
("Stats", ViewMode::Stats),
];
for (label, mode) in modes {
group.bench_with_input(BenchmarkId::new("small", label), mode, |b, mode| {
b.iter(|| {
black_box(EasyDoc::view_as(&fx.small_docx, mode).expect("view_as failed"));
});
});
}
for (label, mode) in modes {
group.bench_with_input(BenchmarkId::new("medium", label), mode, |b, mode| {
b.iter(|| {
black_box(EasyDoc::view_as(&fx.medium_docx, mode).expect("view_as failed"));
});
});
}
group.finish();
}
struct CountingSink {
count: usize,
}
impl CountingSink {
fn new() -> Self {
Self { count: 0 }
}
}
impl EventSink for CountingSink {
fn on_event(&mut self, _event: &DocumentEvent) -> easydoc_core::Result<()> {
self.count += 1;
Ok(())
}
}
fn bench_stream_vs_oneshot(c: &mut Criterion) {
let fx = &*FIXTURE;
let mut group = c.benchmark_group("stream_vs_oneshot");
group.sample_size(50);
group.bench_function("oneshot_medium", |b| {
b.iter(|| {
black_box(EasyDoc::load(&fx.medium_docx).expect("load failed"));
});
});
group.bench_function("stream_medium", |b| {
b.iter(|| {
let mut sink = CountingSink::new();
EasyDoc::read_events(&fx.medium_docx, &mut sink).expect("read_events failed");
black_box(sink.count);
});
});
group.bench_function("oneshot_large", |b| {
b.iter(|| {
black_box(EasyDoc::load(&fx.large_docx).expect("load failed"));
});
});
group.bench_function("stream_large", |b| {
b.iter(|| {
let mut sink = CountingSink::new();
EasyDoc::read_events(&fx.large_docx, &mut sink).expect("read_events failed");
black_box(sink.count);
});
});
group.finish();
}
fn bench_markdown_throughput(c: &mut Criterion) {
let fx = &*FIXTURE;
let mut group = c.benchmark_group("markdown_throughput");
group.sample_size(50);
group.bench_function("small_docx", |b| {
b.iter(|| {
black_box(EasyDoc::to_markdown(&fx.small_docx).expect("to_markdown failed"));
});
});
group.bench_function("medium_docx", |b| {
b.iter(|| {
black_box(EasyDoc::to_markdown(&fx.medium_docx).expect("to_markdown failed"));
});
});
group.finish();
}
fn bench_fidelity(c: &mut Criterion) {
let fx = fixtures::Fixtures::load();
let mut group = c.benchmark_group("fidelity");
for fixture in fx.all() {
group.throughput(Throughput::Bytes(fixture.original_size));
group.bench_with_input(
BenchmarkId::from_parameter(fixture.name),
fixture,
|b, fix| {
b.iter(|| {
let tmp = fix.write_to_temp();
let result = EasyDoc::view_as(tmp.path(), &ViewMode::Plain).expect("view_as");
assert_eq!(
result, fix.expected_text,
"fidelity regression in fixture '{}'",
fix.name
);
});
},
);
}
group.finish();
}
criterion_group!(
benches,
bench_write_throughput,
bench_read_text,
bench_view_mode,
bench_stream_vs_oneshot,
bench_markdown_throughput,
bench_fidelity,
);
criterion_main!(benches);