"""Markdown reporting for AcTrail performance benchmarks."""
from __future__ import annotations
import datetime as dt
import platform
import random
import statistics
import sys
from dataclasses import dataclass
from pathlib import Path
from lib.statistics import distribution_stats, quantile_sorted
@dataclass(frozen=True)
class BenchRun:
case: str
mode: str
iteration: int
outer_wall_ms: float
task_runtime_ms: float
trace_id: int | None
summary: str
diagnostics: str
workload_result: dict[str, object]
def write_report(
repo: Path,
output: Path,
cases: list[str],
modes: list[str],
warmups: int,
repetitions: int,
overhead_threshold_percent: float,
alpha: float,
bootstrap_resamples: int,
permutation_resamples: int,
max_exact_permutations: int,
random_seed: int,
runs: list[BenchRun],
) -> None:
output_path = resolve(repo, output)
output_path.parent.mkdir(parents=True, exist_ok=True)
lines = [
"# AcTrail Performance Benchmark",
"",
f"- Generated: {dt.datetime.now().isoformat(timespec='seconds')}",
f"- Host: `{platform.platform()}`",
f"- Python: `{sys.version.split()[0]}`",
f"- Cases: `{', '.join(cases)}`",
f"- Modes: `{', '.join(modes)}`",
f"- Warmups: `{warmups}`",
f"- Repetitions: `{repetitions}`",
f"- Distribution overhead threshold: `{overhead_threshold_percent:.3f}%`",
f"- Alpha: `{alpha:.3f}`",
f"- Bootstrap resamples: `{bootstrap_resamples}`",
f"- Permutation resamples: `{permutation_resamples}`",
"",
"## Task Runtime Summary",
"",
"| Case | Mode | Runs | Median task ms | P95 task ms | Median Task Overhead vs Baseline |",
"| --- | --- | ---: | ---: | ---: | ---: |",
]
append_task_summary_rows(lines, cases, modes, runs)
append_outer_wall_rows(lines, cases, modes, runs)
append_distribution_rows(
lines,
cases,
modes,
overhead_threshold_percent,
alpha,
bootstrap_resamples,
permutation_resamples,
max_exact_permutations,
random_seed,
runs,
)
append_raw_runs(lines, runs)
output_path.write_text("\n".join(lines), encoding="utf-8")
def append_task_summary_rows(
lines: list[str],
cases: list[str],
modes: list[str],
runs: list[BenchRun],
) -> None:
for case in cases:
baseline = median_for(runs, case, "baseline", "task")
for mode in modes:
values = runtime_values(runs, case, mode, "task")
if not values:
continue
overhead = "n/a"
if baseline is not None and mode != "baseline":
overhead = f"{((statistics.median(values) - baseline) / baseline) * 100:.2f}%"
lines.append(
"| "
+ " | ".join(
[
case,
mode,
str(len(values)),
f"{statistics.median(values):.3f}",
f"{percentile(values, 95):.3f}",
overhead,
]
)
+ " |"
)
def append_outer_wall_rows(
lines: list[str],
cases: list[str],
modes: list[str],
runs: list[BenchRun],
) -> None:
lines.extend(
[
"",
"## Outer Command Wall-Clock",
"",
"| Case | Mode | Runs | Median outer ms | Median outer-task ms |",
"| --- | --- | ---: | ---: | ---: |",
]
)
for case in cases:
for mode in modes:
outer_values = runtime_values(runs, case, mode, "outer")
task_values = runtime_values(runs, case, mode, "task")
if not outer_values or not task_values:
continue
gaps = [
run.outer_wall_ms - run.task_runtime_ms
for run in runs
if run.case == case and run.mode == mode
]
lines.append(
"| "
+ " | ".join(
[
case,
mode,
str(len(outer_values)),
f"{statistics.median(outer_values):.3f}",
f"{statistics.median(gaps):.3f}",
]
)
+ " |"
)
def append_distribution_rows(
lines: list[str],
cases: list[str],
modes: list[str],
overhead_threshold_percent: float,
alpha: float,
bootstrap_resamples: int,
permutation_resamples: int,
max_exact_permutations: int,
random_seed: int,
runs: list[BenchRun],
) -> None:
lines.extend(
[
"",
"## Distribution-Level 5% Overhead Assessment",
"",
"| Case | Mode | Baseline n | Observed n | HL Overhead | CI | KS p(same distribution) | MW p(> threshold) | Decision |",
"| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |",
]
)
rng = random.Random(random_seed)
for case in cases:
baseline = runtime_values(runs, case, "baseline", "task")
if not baseline:
continue
for mode in modes:
if mode == "baseline":
continue
observed = runtime_values(runs, case, mode, "task")
if not observed:
continue
stats = distribution_stats(
baseline,
observed,
overhead_threshold_percent,
alpha,
bootstrap_resamples,
permutation_resamples,
max_exact_permutations,
rng,
)
lines.append(
"| "
+ " | ".join(
[
case,
mode,
str(len(baseline)),
str(len(observed)),
f"{stats.hl_overhead_percent:.2f}%",
f"[{stats.ci_low_percent:.2f}%, {stats.ci_high_percent:.2f}%]",
f"{stats.ks_p_value:.4f}",
f"{stats.mw_p_value:.4f}",
stats.decision,
]
)
+ " |"
)
def append_raw_runs(lines: list[str], runs: list[BenchRun]) -> None:
lines.extend(["", "## Raw Runs", ""])
for run in runs:
trace = f"trace-{run.trace_id}" if run.trace_id is not None else "n/a"
lines.append(
f"- case={run.case} mode={run.mode} iteration={run.iteration} "
f"task_runtime_ms={run.task_runtime_ms:.3f} "
f"outer_wall_ms={run.outer_wall_ms:.3f} trace={trace} "
f"diagnostics={diagnostic_kinds(run.diagnostics)} workload={run.workload_result}"
)
lines.extend(
[
"",
"## Interpretation Notes",
"",
"- `baseline` runs the workload without AcTrail.",
"- `daemon-idle` keeps `actraild` running but does not attach the workload.",
"- `observed-ebpf-core` uses `actrailctl launch` with eBPF process/file/network observation.",
"- `observed-ebpf-payload` adds stdio payload, socket plaintext payload, HTTP/1.1 semantics, and resource sampling; it does not enable seccomp exec/fork or TLS LLM payload capture.",
"- `observed-seccomp-agent` enables process/network observation plus seccomp exec/fork/clone observation and agent invocation semantics; it is the mode intended for real external LLM CLI wall-clock tests.",
"- `task_runtime_ms` is measured by `tests/performance/lib/timing_wrapper.py` immediately around the target workload/CLI command in both baseline and observed modes.",
"- `outer_wall_ms` includes benchmark runner and `actrailctl launch` wrapper time. It is reported separately and is not used for the 5% overhead assessment.",
"- `HL Overhead` is the Hodges-Lehmann estimator over all observed/baseline pairwise runtime ratios, so it compares distributions rather than matching iteration numbers.",
"- `CI` is a bootstrap confidence interval for the distribution-level pairwise ratio overhead.",
"- `KS p(same distribution)` is a two-sample permutation Kolmogorov-Smirnov p-value. A large p-value does not prove equality; it only means this sample is insufficient to reject same-distribution behavior.",
"- `MW p(> threshold)` is a one-sided Mann-Whitney permutation p-value after scaling the baseline sample by the configured threshold.",
"- A benchmark result is invalid if any run fails, times out, or produces a non-BootstrapGap degraded trace.",
"",
]
)
def diagnostic_kinds(diagnostics: str) -> str:
kinds: list[str] = []
for line in diagnostics.splitlines():
if line.startswith("diag-"):
fields = line.split()
if len(fields) >= 3:
kinds.append(fields[2])
return ",".join(kinds) if kinds else "none"
def runtime_values(runs: list[BenchRun], case: str, mode: str, kind: str) -> list[float]:
if kind == "task":
return [run.task_runtime_ms for run in runs if run.case == case and run.mode == mode]
if kind == "outer":
return [run.outer_wall_ms for run in runs if run.case == case and run.mode == mode]
raise RuntimeError(f"unknown runtime kind {kind}")
def median_for(runs: list[BenchRun], case: str, mode: str, kind: str) -> float | None:
values = runtime_values(runs, case, mode, kind)
if not values:
return None
return statistics.median(values)
def percentile(values: list[float], percentile_value: float) -> float:
return quantile_sorted(sorted(values), percentile_value / 100.0)
def resolve(repo: Path, path: Path) -> Path:
if path.is_absolute():
return path
return repo / path