test: add Criterion bench suite for PPE hot-path costs - #35
test: add Criterion bench suite for PPE hot-path costs#35abdallahsamabd wants to merge 1 commit into
Conversation
praxis-bot
left a comment
There was a problem hiding this comment.
Criterion Bench Suite for PPE Hot Path
PR adds an ppe-benches crate covering plugin dispatch, full-decision latency, throughput, per-PDP cost, and memory/session-taint growth. Suite design is solid: setup runs outside timed loops, fixtures mirror production wiring, CI compiles the suite via clippy --all-targets without wall-clock gating, and docs/benchmarks.md records the decision rationale and baseline numbers.
Findings
| # | Severity | File | Issue |
|---|---|---|---|
| 1 | Large | benches/memory.rs:77 |
session_append_one accumulates state across Criterion iterations |
| 2 | Medium | benches/pdp_cost.rs:98 |
PDP evaluate results not checked for expected decision outcome |
| let store = Arc::clone(&store); | ||
| b.to_async(&rt).iter(|| async { | ||
| store | ||
| .append_labels("sess-taint", &["EXTRA".to_owned()]) |
There was a problem hiding this comment.
[Large] session_append_one appends the same "EXTRA" label to the same session key across all Criterion iterations without resetting the store. Two failure modes depending on append_labels semantics:
- Deduplicates: every iteration after the first measures duplicate-detection cost, not actual append cost. The benchmark is really measuring "attempt to add a label that already exists."
- Does not deduplicate: the label set grows unboundedly across iterations, making measurements non-stationary -- later iterations are slower than earlier ones.
Either way, the benchmark does not hold label count constant at n_labels as the BenchmarkId parameter implies.
Fix: use a unique label per iteration (e.g., AtomicU64 counter in the closure to generate format!("X{}", counter.fetch_add(1, Ordering::Relaxed))) so each iteration appends a genuinely new label to the baseline set. Alternatively, document the intentional drift and rename the benchmark to reflect what it actually measures.
There was a problem hiding this comment.
Fixed by appending a unique label per iteration (AtomicU64) so each sample is a genuine append on top of the seeded baseline.
| let d = cedar | ||
| .evaluate(black_box(&cedar_args), black_box(&bag)) | ||
| .await | ||
| .expect("cedar eval"); |
There was a problem hiding this comment.
[Medium] .expect("cedar eval") only asserts Ok(...), not the decision outcome. If reader_bag() does not correctly populate the attributes Cedar needs (e.g., if the "role.reader" bag key does not map to principal.roles.contains("reader") in the Cedar entity model), the benchmark silently times the deny path instead of the allow path.
invoke_once in lib.rs guards against exactly this -- its doc says "Criterion must not silently time a deny path when the harness expected allow." The same principle applies here. The same concern applies to cel_evaluate and opa_evaluate below.
Fix: assert the decision is allow, at minimum once during setup before the timed loop begins. A single assert! outside the bench_function closure catches bag misconfiguration before baselines go stale.
There was a problem hiding this comment.
Added a one-shot Allow assert in setup (outside the timed loop) for Cedar, CEL, and OPA before Criterion starts measuring.
Signed-off-by: Abdallah Samara <abdallahsamabd@gmail.com>
a1307c7 to
53499e9
Compare
araujof
left a comment
There was a problem hiding this comment.
Thanks for this PR!
The suite builds and its smoke tests pass. A few notes to address missing measurements and some other nits:
- Criterion does not report p95 or p99 by default. The docs only record means, so the requested p50/p95/p99 results are not covered.
- No CPU profile was captured; the document gives an expected flamegraph instead.
session_append_onekeeps adding labels to the same store, so the 8/64/512 starting sizes are quickly swamped. It also reportsn_labelselements per iteration even though only one label is appended.- The memory results do not show per-decision allocation or footprint growth with policy size.
Please address these before merging.
|
Hi @abdallahsamabd, Thank you for the contribution. Here is my review on this PR: Finding 1:
|
terylt
left a comment
There was a problem hiding this comment.
Overall, nice work! See my comments in the previous message for suggested changes.
Summary
Adds a Criterion benchmark suite for the Praxis Policy Engine hot path (#19).
ppe-benchesunderbenches/covering:plugin_only/cedar_only/plugin_then_cedarmode: concurrentevaluateonlydhat-heapprofilemake benchruns the suite on demand (not part ofmake ci)docs/benchmarks.mdSetup (YAML load, Cedar compile) stays outside Criterion iters so timings reflect
PolicyEngine::invoke_named/PdpResolver::evaluate/SessionStore, not load cost.CI policy: do not gate PRs on wall-clock benches. Clippy/
make cistill compile all[[bench]]targets so the suite cannot bitrot.Test plan
cargo bench -p ppe-benches --no-runcargo bench -p ppe-benches -- --testcargo clippy -p ppe-benches --all-targets -- -D warningsmake bench(or targeted:--bench full_decision,--bench pdp_cost)cargo bench -p ppe-benches --features dhat-heap --bench memorydocs/benchmarks.mdmatches local hardware / re-run if publishing release numbers