Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ benchmark-results.json
benchmark-report.md
fuzz/corpus
.idea
/=
17 changes: 16 additions & 1 deletion BENCHMARKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ The current matrix covers:
- load is bounded by `STRUCTURED_ZSTD_SILESIA_MAX_FILES` (default `12`) and
`STRUCTURED_ZSTD_SILESIA_MAX_FILE_BYTES` (default `67108864`)

For decompression, each scenario/level pair is benchmarked against two frame sources:

- `rust_stream`: frame produced by `structured-zstd`
- `c_stream`: frame produced by C `zstd`

This keeps Rust-vs-C decoder comparisons symmetric and catches format/interop drift sooner.

Comment thread
polaz marked this conversation as resolved.
The local default for the large scenario is `100 MiB`. In GitHub Actions, when
`STRUCTURED_ZSTD_BENCH_LARGE_BYTES` is unset, `.github/scripts/run-benchmarks.sh` defaults it to
`16 MiB` to keep CI regression runs bounded while still exercising the same code path.
Expand All @@ -35,6 +42,14 @@ Dictionary benchmarks are tracked separately with C FFI `with_dict` vs `without_
dictionary trained from scenario samples. Pure Rust dictionary compression is still pending and is
therefore not part of the pure-Rust-vs-C timing matrix yet.

## Issue #24 Acceptance Mapping

- [x] Criterion benchmarks for compress/decompress at all currently implemented levels
- [x] Comparison against C zstd at same levels
- [x] Flamegraph generation script (`scripts/bench-flamegraph.sh`)
- [x] Small data (`1-10 KiB`) scenarios for CoordiNode-like payloads
- [x] Results documented in `benchmark-report.md`

## Commands

Run the full Criterion matrix:
Expand All @@ -58,7 +73,7 @@ bash scripts/bench-flamegraph.sh
Override the benchmark targeted by the flamegraph script:

```bash
bash scripts/bench-flamegraph.sh decompress/default/decodecorpus-z000033/matrix/pure_rust
bash scripts/bench-flamegraph.sh decompress/default/decodecorpus-z000033/rust_stream/matrix/pure_rust
```

## Outputs
Expand Down
137 changes: 100 additions & 37 deletions zstd/benches/compare_ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,47 +79,110 @@ fn bench_decompress(c: &mut Criterion) {
let emit_reports = emit_reports_enabled();
for scenario in benchmark_scenarios_cached().iter() {
for level in supported_levels() {
let rust_compressed =
structured_zstd::encoding::compress_to_vec(&scenario.bytes[..], level.rust_level);
let ffi_compressed = zstd::encode_all(&scenario.bytes[..], level.ffi_level).unwrap();
let expected_len = scenario.len();
if emit_reports {
emit_memory_report(
scenario,
level,
"decompress",
ffi_compressed.len() + expected_len,
ffi_compressed.len() + expected_len,
);
}
let benchmark_name = format!("decompress/{}/{}/{}", level.name, scenario.id, "matrix");
let mut group = c.benchmark_group(benchmark_name);
configure_group(&mut group, scenario);
group.throughput(Throughput::Bytes(scenario.throughput_bytes()));

group.bench_function("pure_rust", |b| {
let mut target = vec![0u8; expected_len];
let mut decoder = FrameDecoder::new();
b.iter(|| {
let written = decoder.decode_all(&ffi_compressed, &mut target).unwrap();
assert_eq!(written, expected_len);
})
});
bench_decompress_source(
c,
scenario,
level,
"rust_stream",
&rust_compressed,
expected_len,
emit_reports,
);
bench_decompress_source(
c,
scenario,
level,
"c_stream",
&ffi_compressed,
expected_len,
emit_reports,
);
}
}
}

group.bench_function("c_ffi", |b| {
let mut decoder = zstd::bulk::Decompressor::new().unwrap();
let mut output = Vec::with_capacity(expected_len);
b.iter(|| {
output.clear();
let written = decoder
.decompress_to_buffer(&ffi_compressed[..], &mut output)
.unwrap();
assert_eq!(written, expected_len);
assert_eq!(output.len(), expected_len);
})
});
fn bench_decompress_source(
c: &mut Criterion,
scenario: &Scenario,
level: LevelConfig,
source: &'static str,
compressed: &[u8],
expected_len: usize,
emit_reports: bool,
) {
assert_decompress_matches_reference(scenario, compressed, expected_len);

group.finish();
}
if emit_reports {
emit_memory_report(
scenario,
level,
&format!("decompress-{source}"),
compressed.len() + expected_len,
compressed.len() + expected_len,
);
}

let benchmark_name = format!(
"decompress/{}/{}/{}/matrix",
level.name, scenario.id, source
);
let mut group = c.benchmark_group(benchmark_name);
configure_group(&mut group, scenario);
group.throughput(Throughput::Bytes(scenario.throughput_bytes()));

group.bench_function("pure_rust", |b| {
let mut target = vec![0u8; expected_len];
let mut decoder = FrameDecoder::new();
b.iter(|| {
let written = decoder
.decode_all(black_box(compressed), &mut target)
.unwrap();
black_box(&target[..written]);
assert_eq!(written, expected_len);
})
});

group.bench_function("c_ffi", |b| {
let mut decoder = zstd::bulk::Decompressor::new().unwrap();
let mut output = Vec::with_capacity(expected_len);
b.iter(|| {
output.clear();
let written = decoder
.decompress_to_buffer(black_box(compressed), &mut output)
.unwrap();
black_box(output.as_slice());
assert_eq!(written, expected_len);
assert_eq!(output.len(), expected_len);
})
});
Comment thread
polaz marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

group.finish();
}

fn assert_decompress_matches_reference(
scenario: &Scenario,
compressed: &[u8],
expected_len: usize,
) {
let mut rust_target = vec![0u8; expected_len];
let mut rust_decoder = FrameDecoder::new();
let rust_written = rust_decoder
.decode_all(compressed, &mut rust_target)
.unwrap();
assert_eq!(rust_written, expected_len);
assert_eq!(&rust_target[..rust_written], scenario.bytes.as_slice());

let mut ffi_decoder = zstd::bulk::Decompressor::new().unwrap();
let mut ffi_output = Vec::with_capacity(expected_len);
let ffi_written = ffi_decoder
.decompress_to_buffer(compressed, &mut ffi_output)
.unwrap();
assert_eq!(ffi_written, expected_len);
assert_eq!(ffi_output.as_slice(), scenario.bytes.as_slice());
}

fn bench_dictionary(c: &mut Criterion) {
Expand Down Expand Up @@ -214,7 +277,7 @@ fn configure_group<M: criterion::measurement::Measurement>(
fn emit_memory_report(
scenario: &Scenario,
level: LevelConfig,
stage: &'static str,
stage: &str,
rust_buffer_bytes_estimate: usize,
ffi_buffer_bytes_estimate: usize,
) {
Expand Down
Loading