diff --git a/benchmarks/locator-resolution/README.md b/benchmarks/locator-resolution/README.md
index 75607e8..445784b 100644
--- a/benchmarks/locator-resolution/README.md
+++ b/benchmarks/locator-resolution/README.md
@@ -24,17 +24,10 @@ role-dependent action and settable probes make that model inaccurate. Native
Slack/Electron IPC measurements require Accessibility permission and belong in
the privileged macOS integration suite.
-Generate the standalone sanitized report from the synthetic and paired live
-evidence:
-
-```bash
-python3 benchmarks/locator-resolution/generate_performance_report.py \
- --synthetic /private/tmp/agent-desktop-locator-synthetic.json \
- --live /private/tmp/agent-desktop-electron-slack-paired-final.json \
- --output /private/tmp/agent-desktop-locator-performance-report.html
-```
-
-Benchmark outputs are generated evidence, not source. Keep them outside the repository.
+This synthetic benchmark JSON is consumed by the repo-wide performance report:
+run `bash scripts/perf-baseline-compare.sh` (optionally with `--apps "Slack,Google
+Chrome"`) to produce `report.html`, covering the HEAD-vs-main fixture A/B,
+optional real-app read-only probes, and this synthetic benchmark.
For privileged Electron/Chromium runs, the macOS adapter first asks whether the
application root exposes `AXManualAccessibility` as settable, reads its current
diff --git a/benchmarks/locator-resolution/generate_performance_report.py b/benchmarks/locator-resolution/generate_performance_report.py
deleted file mode 100644
index db9a353..0000000
--- a/benchmarks/locator-resolution/generate_performance_report.py
+++ /dev/null
@@ -1,336 +0,0 @@
-#!/usr/bin/env python3
-
-import argparse
-import html
-import json
-from pathlib import Path
-
-
-DEFAULT_BASELINE_BYTES = 1_793_312
-DEFAULT_CURRENT_BYTES = 2_162_496
-SCENARIO_LABELS = {
- "deep_anonymous_has_text": "Deep anonymous text",
- "duplicate_button_role_and_name": "Duplicate role + name",
- "electron_dual_identifier_moving_bounds": "Moving Electron bounds",
- "large_nested_channel_has_unread": "Large nested tree",
-}
-
-
-def require(mapping, *keys):
- value = mapping
- for key in keys:
- if not isinstance(value, dict) or key not in value:
- raise ValueError(f"missing required field: {'.'.join(keys)}")
- value = value[key]
- return value
-
-
-def load_json(path):
- with path.open(encoding="utf-8") as source:
- return json.load(source)
-
-
-def fmt(value, digits=1):
- return f"{float(value):,.{digits}f}"
-
-
-def rate(value):
- return float(value) * 100.0
-
-
-def directional_change(value, lower_label, higher_label, unchanged_label):
- value = float(value)
- if value < 0:
- return abs(value), lower_label
- if value > 0:
- return value, higher_label
- return 0.0, unchanged_label
-
-
-def size_change(baseline_bytes, current_bytes):
- delta = current_bytes - baseline_bytes
- percent = abs(delta) / baseline_bytes * 100.0
- if delta < 0:
- return percent, "smaller", f"{delta:,}"
- if delta > 0:
- return percent, "larger", f"+{delta:,}"
- return 0.0, "the same size", "0"
-
-
-def paired_wall_deltas(live):
- baseline = {
- int(sample["pair_index"]): float(sample["wall_ms"])
- for sample in require(live, "runs", "baseline", "samples")
- if sample.get("command_success") and isinstance(sample.get("wall_ms"), (int, float))
- }
- current = {
- int(sample["pair_index"]): float(sample["wall_ms"])
- for sample in require(live, "runs", "current", "samples")
- if sample.get("command_success") and isinstance(sample.get("wall_ms"), (int, float))
- }
- indexes = sorted(baseline.keys() & current.keys())
- return [current[index] - baseline[index] for index in indexes]
-
-
-def svg_grouped_bars(title, baseline, current, unit):
- labels = ("p50", "p95")
- values = [baseline[0], current[0], baseline[1], current[1]]
- maximum = max(values) * 1.18
- bars = []
- for group, label in enumerate(labels):
- group_x = 86 + group * 200
- for offset, (series, value, css_class) in enumerate(
- (("Baseline", baseline[group], "baseline"), ("Current", current[group], "current"))
- ):
- height = 155 * value / maximum
- x = group_x + offset * 66
- y = 193 - height
- bars.append(
- f''
- f'{fmt(value)} {unit}'
- f'{series}'
- )
- bars.append(f'{label}')
- summary = (
- f"{title}. Baseline p50 {fmt(baseline[0])} and p95 {fmt(baseline[1])} {unit}; "
- f"current p50 {fmt(current[0])} and p95 {fmt(current[1])} {unit}."
- )
- return (
- f'"
- )
-
-
-def svg_delta_plot(deltas):
- if not deltas:
- raise ValueError("live report has no comparable successful wall-time pairs")
- ordered = sorted(deltas)
- limit = max(abs(min(ordered)), abs(max(ordered)), 1.0) * 1.1
- zero_y = 132
- scale = 102 / limit
- plot_width = 610
- bar_width = max(3.0, plot_width / len(ordered) - 3.0)
- bars = []
- for index, value in enumerate(ordered):
- x = 49 + index * plot_width / len(ordered)
- height = abs(value) * scale
- y = zero_y if value >= 0 else zero_y - height
- css_class = "slower" if value > 0 else "faster"
- bars.append(
- f''
- f"Pair {index + 1}: {fmt(value)} ms"
- )
- faster = sum(value < 0 for value in ordered)
- summary = (
- f"Ordered current minus baseline wall-time deltas for {len(ordered)} pairs. "
- f"Current was faster in {faster} pairs. Negative values are faster."
- )
- return (
- '"
- )
-
-
-def svg_rate_chart(live):
- metrics = (
- ("Correct", "correct_result_rate"),
- ("Addressable", "addressable_result_rate"),
- ("Exact re-resolution", "exact_reresolution_rate"),
- )
- rows = []
- summaries = []
- for index, (label, key) in enumerate(metrics):
- baseline = rate(require(live, "runs", "baseline", "reliability", key))
- current = rate(require(live, "runs", "current", "reliability", key))
- summaries.append(f"{label}: baseline {fmt(baseline, 0)} percent, current {fmt(current, 0)} percent")
- y = 42 + index * 67
- rows.append(f'{html.escape(label)}')
- for offset, (value, css_class, series) in enumerate(
- ((baseline, "baseline", "Baseline"), (current, "current", "Current"))
- ):
- bar_y = y + offset * 22
- rows.append(
- f''
- f''
- f'{series} {fmt(value, 0)}%'
- )
- summary = "; ".join(summaries)
- return (
- '"
- )
-
-
-def svg_speedups(synthetic):
- speedups = [
- (
- SCENARIO_LABELS.get(item["name"], item["name"].replace("_", " ").title()),
- float(require(item, "comparison", "p50_find_speedup")),
- float(require(item, "comparison", "p95_find_speedup")),
- )
- for item in require(synthetic, "scenarios")
- ]
- maximum = max(max(row[1:]) for row in speedups) * 1.15
- rows = []
- for index, (label, p50, p95) in enumerate(speedups):
- y = 34 + index * 64
- rows.append(f'{html.escape(label)}')
- for offset, (value, css_class, percentile) in enumerate(
- ((p50, "current", "p50"), (p95, "synthetic-p95", "p95"))
- ):
- bar_y = y + offset * 22
- width = value / maximum * 365
- rows.append(
- f''
- f'{percentile} {fmt(value, 2)}×'
- )
- summary = "; ".join(f"{label}: p50 {fmt(p50, 2)} times, p95 {fmt(p95, 2)} times" for label, p50, p95 in speedups)
- return (
- '"
- )
-
-
-def svg_binary_size(baseline_bytes, current_bytes):
- maximum = max(baseline_bytes, current_bytes) * 1.15
- bars = []
- for index, (label, value, css_class) in enumerate(
- (("Baseline", baseline_bytes, "baseline"), ("Current", current_bytes, "current"))
- ):
- width = value / maximum * 470
- y = 45 + index * 62
- bars.append(
- f'{label}'
- f''
- f'{value / 1_000_000:.3f} MB'
- )
- return (
- '"
- )
-
-
-def render_report(synthetic, live, baseline_bytes, current_bytes, current_sha256=None):
- wall = tuple(
- require(live, "runs", name, "metrics", "end_to_end_wall_all_attempts", percentile)
- for name in ("baseline", "current")
- for percentile in ("p50", "p95")
- )
- cpu = tuple(
- require(live, "runs", name, "metrics", "process_cpu_all_attempts", percentile)
- for name in ("baseline", "current")
- for percentile in ("p50", "p95")
- )
- pairs = int(require(live, "paired_comparison", "comparable_successful_pairs"))
- faster_rate = rate(require(live, "paired_comparison", "current_minus_baseline", "current_faster_wall_rate"))
- delta_p50 = require(live, "paired_comparison", "current_minus_baseline", "wall", "p50")
- wall_change, wall_direction = directional_change(
- delta_p50, "reduction", "increase", "change"
- )
- reliability_keys = (
- "correct_result_rate",
- "addressable_result_rate",
- "exact_reresolution_rate",
- )
- current_reliability = min(
- rate(require(live, "runs", "current", "reliability", key))
- for key in reliability_keys
- )
- synthetic_scenarios = require(synthetic, "scenarios")
- synthetic_total = len(synthetic_scenarios)
- legacy_synthetic_correct = sum(
- bool(require(item, "legacy_snapshot", "correct_all_runs"))
- for item in synthetic_scenarios
- )
- current_synthetic_correct = sum(
- bool(require(item, "live_find_selected_refs", "correct_all_runs"))
- and bool(require(item, "live_find_selected_refs", "selected_refs_reresolvable"))
- for item in synthetic_scenarios
- )
- size_percent, size_direction, size_delta = size_change(baseline_bytes, current_bytes)
- captured_at = html.escape(str(live.get("generated_at", "unknown time")))
- measured_sha = str(require(live, "runs", "current", "binary", "sha256"))
- if current_sha256 and measured_sha != current_sha256:
- live_evidence_note = (
- f'
Live evidence was captured at {captured_at} '
- f'against build {html.escape(measured_sha[:12])}. The final build '
- f'{html.escape(current_sha256[:12])} differs and could not be re-probed; '
- 'treat live latency as pre-final evidence.
'
- )
- else:
- live_evidence_note = (
- f'
Live evidence captured at {captured_at} against build '
- f'{html.escape(measured_sha[:12])}.
{svg_grouped_bars("Live process CPU", (cpu[0], cpu[1]), (cpu[2], cpu[3]), "ms")}
-
Paired live wall-time deltas
All {pairs} current-minus-baseline deltas, ordered by value. Negative means current is faster.
{svg_delta_plot(paired_wall_deltas(live))}
-
Live locator reliability
Command exit success alone was 100% for both binaries; correctness, addressability, and exact re-resolution distinguish usable results.
{svg_rate_chart(live)}
-
Synthetic locator speedup
31 measured runs after 5 warmups per deterministic Chromium/Electron fixture. Current find was correct and re-resolvable in {current_synthetic_correct}/{synthetic_total} scenarios; legacy was correct in {legacy_synthetic_correct}/{synthetic_total}. Speed ratios are timing evidence, not a substitute for correctness. These timings exclude native accessibility IPC.
p50 find speedupp95 find speedup
{svg_speedups(synthetic)}
-
Release binary size
Current is {fmt(size_percent, 2)}% {size_direction} ({size_delta} bytes), while remaining far below the 15 MB ceiling.
Live data is one machine, one app state, and one macOS accessibility-permission state; it is not a cross-machine claim.
The live operation was read-only find plus exact ref re-resolution. No click, typing, navigation, or message mutation occurred.
Synthetic data measures platform-neutral locator work on deterministic Electron-shaped fixtures; it does not measure native IPC or whole-command latency.
Binary size is {fmt(size_percent, 2)}% {size_direction}; performance evidence does not erase packaging changes.