From dce3de477d0df34ef32218d67015ec3b21f60558 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Sat, 27 Sep 2025 07:57:09 +0700 Subject: [PATCH] Add benchmarks for error conversion hot paths --- CHANGELOG.md | 9 ++ Cargo.lock | 247 ++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 7 +- README.md | 27 ++++- README.template.md | 23 ++++ benches/error_paths.rs | 101 +++++++++++++++++ 6 files changed, 410 insertions(+), 4 deletions(-) create mode 100644 benches/error_paths.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index cf9c07f..45e96b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.24.2] - 2025-10-18 + +### Added +- Introduced a Criterion benchmark (`benches/error_paths.rs`) covering + `Context::into_error` redaction scenarios and `ProblemJson::from_app_error` + conversions to track serialization hot paths. +- Documented the benchmarking workflow in the README and exposed the suite via + `cargo bench --bench error_paths` with the default harness disabled. + ## [0.24.1] - 2025-10-17 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index e4f108c..9a2c043 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -229,6 +229,18 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + [[package]] name = "anyhow" version = "1.0.100" @@ -405,6 +417,12 @@ dependencies = [ "bytes", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.39" @@ -439,6 +457,58 @@ dependencies = [ "windows-link", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + [[package]] name = "combine" version = "4.6.7" @@ -553,6 +623,61 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -1107,6 +1232,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1151,6 +1286,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1517,6 +1658,26 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1665,12 +1826,13 @@ dependencies = [ [[package]] name = "masterror" -version = "0.24.1" +version = "0.24.2" dependencies = [ "actix-web", "anyhow", "axum", "config", + "criterion", "http 1.3.1", "itoa", "js-sys", @@ -1902,6 +2064,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "openssl-probe" version = "0.1.6" @@ -2071,6 +2239,34 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -2270,6 +2466,26 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "rc-box" version = "1.3.0" @@ -2559,6 +2775,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.28" @@ -3297,6 +3522,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.10.0" @@ -3742,6 +3977,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 801b6f2..ca7a6c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.24.1" +version = "0.24.2" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" @@ -134,6 +134,7 @@ tonic = { version = "0.14", optional = true } [dev-dependencies] anyhow = { version = "1", default-features = false, features = ["std"] } +criterion = "0.5" serde_json = "1" tokio = { version = "1", features = [ "macros", @@ -146,6 +147,10 @@ toml = "0.9" tempfile = "3" tracing-subscriber = { version = "0.3", features = ["registry"] } +[[bench]] +name = "error_paths" +harness = false + [build-dependencies] serde = { version = "1", features = ["derive"] } toml = "0.9" diff --git a/README.md b/README.md index dcfe183..8a5ca8c 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,9 @@ The build script keeps the full feature snippet below in sync with ~~~toml [dependencies] -masterror = { version = "0.24.1", default-features = false } +masterror = { version = "0.24.2", default-features = false } # or with features: -# masterror = { version = "0.24.1", features = [ +# masterror = { version = "0.24.2", features = [ # "std", "axum", "actix", "openapi", # "serde_json", "tracing", "metrics", "backtrace", # "sqlx", "sqlx-migrate", "reqwest", "redis", @@ -88,6 +88,29 @@ masterror = { version = "0.24.1", default-features = false } --- +### Benchmarks + +Criterion benchmarks cover the hottest conversion paths so regressions are +visible before shipping. Run them locally with: + +~~~sh +cargo bench --bench error_paths +~~~ + +The suite emits two groups: + +- `context_into_error/*` promotes a dummy source error with representative + metadata (strings, counters, durations, IPs) through `Context::into_error` in + both redacted and non-redacted modes. +- `problem_json_from_app_error/*` consumes the resulting `AppError` values to + build RFC 7807 payloads via `ProblemJson::from_app_error`, showing how message + redaction and field policies impact serialization. + +Adjust Criterion CLI flags (for example `--sample-size 200`) after `--` to trade +throughput for tighter confidence intervals when investigating changes. + +--- +
Quick start diff --git a/README.template.md b/README.template.md index 03bbd4f..cf34c7f 100644 --- a/README.template.md +++ b/README.template.md @@ -83,6 +83,29 @@ masterror = { version = "{{CRATE_VERSION}}", default-features = false } --- +### Benchmarks + +Criterion benchmarks cover the hottest conversion paths so regressions are +visible before shipping. Run them locally with: + +~~~sh +cargo bench --bench error_paths +~~~ + +The suite emits two groups: + +- `context_into_error/*` promotes a dummy source error with representative + metadata (strings, counters, durations, IPs) through `Context::into_error` in + both redacted and non-redacted modes. +- `problem_json_from_app_error/*` consumes the resulting `AppError` values to + build RFC 7807 payloads via `ProblemJson::from_app_error`, showing how message + redaction and field policies impact serialization. + +Adjust Criterion CLI flags (for example `--sample-size 200`) after `--` to trade +throughput for tighter confidence intervals when investigating changes. + +--- +
Quick start diff --git a/benches/error_paths.rs b/benches/error_paths.rs new file mode 100644 index 0000000..be8e1e4 --- /dev/null +++ b/benches/error_paths.rs @@ -0,0 +1,101 @@ +use core::{ + net::{IpAddr, Ipv4Addr}, + time::Duration +}; +use std::fmt::{Display, Formatter, Result as FmtResult}; + +use criterion::{BatchSize, Criterion, black_box, criterion_group, criterion_main}; +use masterror::{AppError, AppErrorKind, Context, FieldRedaction, ProblemJson, ResultExt, field}; + +#[derive(Debug)] +struct DummyError; + +impl Display for DummyError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + f.write_str("dummy") + } +} + +impl std::error::Error for DummyError {} + +fn context_into_error(c: &mut Criterion) { + let mut group = c.benchmark_group("context_into_error"); + + group.bench_function("non_redacted", |b| { + b.iter(|| { + let context = build_context(false); + let err = promote_error(context); + black_box(err) + }); + }); + + group.bench_function("redacted", |b| { + b.iter(|| { + let context = build_context(true); + let err = promote_error(context); + black_box(err) + }); + }); + + group.finish(); +} + +fn problem_json_from_app_error(c: &mut Criterion) { + let mut group = c.benchmark_group("problem_json_from_app_error"); + + group.bench_function("non_redacted", |b| { + b.iter_batched( + || promote_error(build_context(false)), + |error| { + let problem = ProblemJson::from_app_error(error); + black_box(problem); + }, + BatchSize::SmallInput + ); + }); + + group.bench_function("redacted", |b| { + b.iter_batched( + || promote_error(build_context(true)), + |error| { + let problem = ProblemJson::from_app_error(error); + black_box(problem); + }, + BatchSize::SmallInput + ); + }); + + group.finish(); +} + +fn build_context(redacted: bool) -> Context { + let mut context = Context::new(AppErrorKind::Service) + .with(field::str("operation", "sync_job")) + .with(field::u64("attempt", 3)) + .with(field::duration("elapsed", Duration::from_millis(275))) + .with(field::bool("idempotent", true)) + .with(field::ip("peer", IpAddr::from(Ipv4Addr::LOCALHOST))); + + if redacted { + context = context + .with(field::str("token", "secret-token")) + .redact_field("token", FieldRedaction::Hash) + .redact(true) + .track_caller(); + } else { + context = context.with(field::str("token", "secret-token")); + } + + context +} + +fn promote_error(context: Context) -> AppError { + let failing: Result<(), DummyError> = Err(DummyError); + match failing.ctx(|| context) { + Err(err) => err, + Ok(_) => AppError::internal("benchmark expected error") + } +} + +criterion_group!(benches, context_into_error, problem_json_from_app_error); +criterion_main!(benches);