Skip to content
Open
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
329 changes: 327 additions & 2 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ members = [
# why it is not a builtin.
"reference/plugins/pii-scanner",
"reference/plugins/audit-logger",
# Criterion suite for issue #19. Unpublished; excluded from default-members
# so everyday `cargo build` stays lean. `cargo bench -p ppe-benches` /
# `make bench` still pick it up, and `clippy --all-targets` compiles it.
"crates/ppe-benches",
]

# The session store pulls a redis client and a TLS stack, which slows a default
Expand Down Expand Up @@ -133,6 +137,10 @@ praxis-policy-pdp-cel = { path = "builtins/pdps/cel",
praxis-policy-pdp-opa = { path = "builtins/pdps/opa", version = "0.1.0" }
praxis-policy-session-valkey = { path = "builtins/session/valkey", version = "0.1.0" }

# Criterion is only consumed by the unpublished `ppe-benches` crate (issue #19).
# Kept in the workspace table so version bumps stay one-line.
criterion = { version = "0.7", features = ["html_reports", "async_tokio"] }

# Cross-crate inlining and dead-code elimination, one codegen unit for
# optimization quality.
#
Expand Down
30 changes: 30 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ help:
@echo "Test:"
@echo " test Run all workspace tests"
@echo ""
@echo "Benchmarks (on demand — not part of make ci; see docs/benchmarks.md):"
@echo " bench Criterion suite (ppe-benches / issue #19)"
@echo " bench-percentiles p50/p95/p99 from target/criterion samples"
@echo " bench-heap dhat per-decision + policy-size footprint"
@echo ""
@echo "Supply chain & coverage:"
@echo " audit cargo deny check (advisories, licenses, bans, sources)"
@echo " coverage Coverage summary, gated at COVERAGE_FLOOR percent"
Expand All @@ -55,6 +60,8 @@ help:
@echo ""
@echo "CI:"
@echo " ci What CI runs: lint + test"
@echo " (benches compile via clippy --all-targets;"
@echo " make bench is on-demand — docs/benchmarks.md)"
@echo ""
@echo "Release:"
@echo " release-dry Preview a release (no changes)"
Expand Down Expand Up @@ -149,6 +156,29 @@ test:
@$(CARGO) test --workspace
@$(CARGO) test --workspace --all-features

# =============================================================================
# Benchmarks (issue #19) — on demand, never part of `make ci`
# =============================================================================
#
# Wall-clock benches do not gate PRs: CI runners are noisy and a flaky
# p99 gate would train people to ignore failures. Clippy --all-targets
# still *compiles* the suite so bitrot is caught. See docs/benchmarks.md.

.PHONY: bench
bench:
@echo "Criterion suite (ppe-benches) — on demand, not a CI gate ..."
@$(CARGO) bench -p ppe-benches
@echo "HTML reports under target/criterion/; write-up in docs/benchmarks.md"

.PHONY: bench-percentiles
bench-percentiles:
@python3 tools/bench_percentiles.py

.PHONY: bench-heap
bench-heap:
@echo "dhat heap_profile (per-decision + policy-size) ..."
@$(CARGO) bench -p ppe-benches --features dhat-heap --bench heap_profile

# =============================================================================
# Supply chain & coverage
# =============================================================================
Expand Down
72 changes: 72 additions & 0 deletions crates/ppe-benches/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2026 Praxis Contributors

# ppe-benches — Criterion microbenchmarks for the Praxis Policy Engine
# (https://github.com/praxis-proxy/policy/issues/19).
#
# Unpublished on purpose: measurement harness, not a library.
#
# Run:
# make bench
# cargo bench -p ppe-benches --bench full_decision
#
# Profiles (on demand, not part of `make ci`):
# cargo flamegraph -p ppe-benches --bench full_decision
# cargo bench -p ppe-benches --features dhat-heap --bench memory

[package]
name = "ppe-benches"
description = "Criterion benchmarks for the Praxis Policy Engine (issue #19)."
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
rust-version.workspace = true
publish = false

[dependencies]
praxis-policy-core = { workspace = true }
praxis-policy-apl-core = { workspace = true }
praxis-policy-apl-runtime = { workspace = true }
praxis-policy-pdp-cedar-direct = { workspace = true }
praxis-policy-pdp-cel = { workspace = true }
praxis-policy-pdp-opa = { workspace = true }
async-trait = { workspace = true }
serde_yaml = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread", "sync", "time"] }
dhat = { version = "0.3", optional = true }

[dev-dependencies]
criterion = { workspace = true }

[features]
dhat-heap = ["dep:dhat"]

[lints]
workspace = true

[[bench]]
name = "hook_overhead"
harness = false

[[bench]]
name = "full_decision"
harness = false

[[bench]]
name = "throughput"
harness = false

[[bench]]
name = "pdp_cost"
harness = false

[[bench]]
name = "memory"
harness = false

[[bench]]
name = "heap_profile"
harness = false
required-features = ["dhat-heap"]
65 changes: 65 additions & 0 deletions crates/ppe-benches/benches/full_decision.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Praxis Contributors

//! Full-decision latency (issue #19 — Time: full decision + eval vs dispatch).
//!
//! | Fixture | Path |
//! |----------------------|-------------------------------------------|
//! | `plugin_only` | APL route → `plugin(noop)` |
//! | `cedar_only` | APL route → `cedar:` PDP step |
//! | `plugin_then_cedar` | APL → plugin then Cedar (operator shape) |
//!
//! Flamegraph: `cargo flamegraph -p ppe-benches --bench full_decision`

#![allow(
missing_docs,
clippy::expect_used,
clippy::unwrap_used,
clippy::panic,
reason = "benchmark harness — Criterion macros + fixture expects"
)]

use std::hint::black_box;
use std::time::Duration;

use criterion::{Criterion, criterion_group, criterion_main};
use ppe_benches::{
YAML_CEDAR_ONLY, YAML_PLUGIN_ONLY, YAML_PLUGIN_THEN_CEDAR, engine_from_yaml, extensions_reader,
invoke_once,
};
use tokio::runtime::Runtime;

fn full_decision(c: &mut Criterion) {
let rt = Runtime::new().expect("tokio runtime");
let mut group = c.benchmark_group("full_decision");
group
.warm_up_time(Duration::from_secs(1))
.measurement_time(Duration::from_secs(5))
.sample_size(80);

let (plugin_only, _) = rt.block_on(engine_from_yaml(YAML_PLUGIN_ONLY, None));
group.bench_function("plugin_only", |b| {
b.to_async(&rt).iter(|| async {
black_box(invoke_once(&plugin_only, extensions_reader()).await);
});
});

let (cedar_only, _) = rt.block_on(engine_from_yaml(YAML_CEDAR_ONLY, None));
group.bench_function("cedar_only", |b| {
b.to_async(&rt).iter(|| async {
black_box(invoke_once(&cedar_only, extensions_reader()).await);
});
});

let (full, _) = rt.block_on(engine_from_yaml(YAML_PLUGIN_THEN_CEDAR, None));
group.bench_function("plugin_then_cedar", |b| {
b.to_async(&rt).iter(|| async {
black_box(invoke_once(&full, extensions_reader()).await);
});
});

group.finish();
}

criterion_group!(benches, full_decision);
criterion_main!(benches);
76 changes: 76 additions & 0 deletions crates/ppe-benches/benches/heap_profile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Praxis Contributors

//! Isolated heap measurements (issue #19 — Memory).
//!
//! Unlike the Criterion `memory` target, this binary runs a **fixed** number of
//! decisions under `dhat` so totals are attributable:
//!
//! - **per-decision** — `total_bytes / N` after N hot-path invokes (setup outside)
//! - **policy-size footprint** — `max_bytes` after load + one decide for 1/10/50
//! Cedar policies
//!
//! ```bash
//! cargo bench -p ppe-benches --features dhat-heap --bench heap_profile
//! ```

#![allow(
missing_docs,
clippy::expect_used,
clippy::unwrap_used,
clippy::print_stdout,
clippy::print_stderr,
reason = "heap profile harness — prints findings for docs/benchmarks.md"
)]

#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;

use std::sync::Arc;

use ppe_benches::{
YAML_PLUGIN_THEN_CEDAR, engine_from_yaml, extensions_reader, extensions_with_session,
invoke_once, yaml_cedar_policy_count,
};
use tokio::runtime::Runtime;

const PER_DECISION_ITERS: usize = 500;

fn main() {
let rt = Runtime::new().expect("tokio runtime");

println!("ppe-benches heap_profile (dhat-heap)");
println!("--- per-decision allocation ---");
{
let _profiler = dhat::Profiler::new_heap();
let (mgr, _) = rt.block_on(engine_from_yaml(YAML_PLUGIN_THEN_CEDAR, None));
let before = dhat::HeapStats::get();
for _ in 0..PER_DECISION_ITERS {
rt.block_on(invoke_once(&mgr, extensions_with_session("bench-sess")));
}
let after = dhat::HeapStats::get();
let delta = after.total_bytes.saturating_sub(before.total_bytes);
let per = delta / PER_DECISION_ITERS as u64;
println!(
"iters={PER_DECISION_ITERS} delta_total_bytes={delta} per_decision_bytes≈{per} peak_max_bytes={}",
after.max_bytes
);
}

println!("--- policy-size footprint (load + 1 decide) ---");
for &n_policies in &[1_usize, 10, 50] {
let _profiler = dhat::Profiler::new_heap();
let yaml = yaml_cedar_policy_count(n_policies);
let (mgr, _) = rt.block_on(engine_from_yaml(&yaml, None));
rt.block_on(invoke_once(&mgr, extensions_reader()));
let stats = dhat::HeapStats::get();
println!(
"policies={n_policies} total_bytes={} max_bytes={} curr_bytes={}",
stats.total_bytes, stats.max_bytes, stats.curr_bytes
);
// Keep engine alive until after stats read.
let _keep = Arc::clone(&mgr);
}

println!("dhat-heap.json written on profiler drop (see docs/benchmarks.md)");
}
74 changes: 74 additions & 0 deletions crates/ppe-benches/benches/hook_overhead.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Praxis Contributors

//! Hook / plugin-dispatch overhead (issue #19 — Time: plugin dispatch).
//!
//! Isolates the executor path with **no APL visitor and no PDP**:
//! `register_handler_for_names` → `invoke_named` → N no-op `HookHandler`s.

#![allow(
missing_docs,
clippy::expect_used,
clippy::unwrap_used,
clippy::panic,
reason = "benchmark harness — Criterion macros + fixture expects"
)]

use std::hint::black_box;
use std::time::Duration;

use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use ppe_benches::{
HOOK_TOOL_PRE, cmf_payload, engine_plugins_only, extensions_reader, invoke_once,
};
use praxis_policy_core::cmf::CmfHook;
use praxis_policy_core::plugin::PluginMode;
use tokio::runtime::Runtime;

fn hook_overhead(c: &mut Criterion) {
let rt = Runtime::new().expect("tokio runtime");
let mut group = c.benchmark_group("hook_overhead");
group
.warm_up_time(Duration::from_secs(1))
.measurement_time(Duration::from_secs(3))
.sample_size(100);

for &n in &[1_usize, 4, 16] {
let mgr = rt.block_on(engine_plugins_only(n, PluginMode::Sequential));
group.throughput(Throughput::Elements(n as u64));
group.bench_with_input(BenchmarkId::new("sequential", n), &n, |b, _| {
b.to_async(&rt).iter(|| async {
black_box(invoke_once(&mgr, extensions_reader()).await);
});
});

let mgr_c = rt.block_on(engine_plugins_only(n, PluginMode::Concurrent));
group.bench_with_input(BenchmarkId::new("concurrent", n), &n, |b, _| {
b.to_async(&rt).iter(|| async {
black_box(invoke_once(&mgr_c, extensions_reader()).await);
});
});
}

// Reset throughput so empty_registry is not reported as Elements(16).
group.throughput(Throughput::Elements(1));
let empty = rt.block_on(engine_plugins_only(0, PluginMode::Sequential));
group.bench_function("empty_registry", |b| {
b.to_async(&rt).iter(|| async {
let (result, _bg) = empty
.invoke_named::<CmfHook>(
HOOK_TOOL_PRE,
cmf_payload("bench"),
extensions_reader(),
None,
)
.await;
black_box(result.continue_processing);
});
});

group.finish();
}

criterion_group!(benches, hook_overhead);
criterion_main!(benches);
Loading
Loading