Files
Marius CiepluchandClaude Opus 5.5 6b85a60ff1 perf(push): find the Python-side scale walls all at once, and remove them
The loop the maintainer called out: four scale defects in the PHP transfer
replay, found one per two-hour Joern run, all Python bookkeeping between
engine calls. benchmarks/push/python_overhead.py ends that: the real
replay() on the real PMPro pins, with a JoernBackend whose graphs are stubs
and whose queries answer instantly with rows recorded from the real engine.
Engine time is zero, so everything measured is ours, in about a minute,
under the sampling profiler. Its first pass named two more walls:

- Region discovery re-parsed the ruleset for every region: 89,127 TOML
  parses, 275 s of a 339 s discovery, twice per push. The fact, obligation
  and taxonomy loaders now keep the parsed (immutable) result keyed by each
  file's name, size and mtime -- an edit in place or an added file reloads.
  Discovery 339 s -> 7 s; output digest identical on PMPro.
- affected_context decoded every span's and every line anchor's path for
  every context row -- a changed file has an anchor per unchanged line --
  78% of a stubbed push. Decoded once per call, anchors indexed by line and
  bisected, original order kept. A randomised control compares it against
  the row-by-row matching over 200 contexts (>100 attachments).

Result: the whole stubbed PMPro push, 27,542 answered requests and 2,688
outcomes, runs in 58.5 s of Python. What is left is the engine.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-23 19:36:32 +02:00

130 lines
6.1 KiB
Python

"""The push path at full repository scale with an engine that answers instantly: what does the PYTHON cost?
Named by the 2026-09-23 PHP transfer replay, which met four scale defects one two-hour run at a time -- a cache
that rescanned itself on every write, a relationship list searched linearly, a fsync per cache entry, and the
ruleset re-parsed per region (89,127 TOML parses in one discovery). Every one was Python bookkeeping around the
engine, every one grew with the repository, and none could show in a unit test or on a small subject.
This runs the real `replay()` -- snapshots, discovery, partitions, evidence, change context, arbitration,
comparison, reporting, the reuse cache -- on a real checkout, with a `JoernBackend` whose graphs are stubs and
whose queries answer at once with rows recorded from the real engine on the same repository. Engine time is
zero by construction, so everything left is ours. It runs in minutes, with the sampling profiler on, and it
fails loudly if the instrument did not actually exercise the path (no questions, no rows, no stages).
Usage:
python benchmarks/push/python_overhead.py --repository <objects.git> --base <oid> --head <oid> \\
--rows <recorded rows json> [--rows ...] --out <dir> [--deadline 3600]
"""
from __future__ import annotations
import argparse
import json
import time
from collections.abc import Mapping
from pathlib import Path
from typing import Any
from openultrasast import profiling
from openultrasast.config import PushConfig
from openultrasast.cpg.backend import TIMING, CpgResult, JoernBackend
from openultrasast.push.runner import replay
# What each partition's graph holds, so the stub census names exactly the files a real frontend would read.
EXTENSIONS = {"php": (".php",), "javascript": (".js", ".jsx", ".mjs", ".cjs"), "typescript": (".ts", ".tsx"), "python": (".py",)}
class StubEngine(JoernBackend):
"""A backend whose engine is a table lookup: real row shapes, zero engine time."""
def __init__(self, templates: list[list[object]], **kwargs: Any) -> None:
super().__init__(**kwargs)
self.templates = templates
self.asked = 0
self.graphs = 0
def build(self, root: Path, *, language: str = "", exclude: Any = (), execution_budget: Any = None) -> CpgResult | None:
extensions = EXTENSIONS.get(language.lower(), ())
files = sorted(str(p.relative_to(root)) for p in root.rglob("*") if p.is_file() and p.suffix.lower() in extensions)
graph = Path(__import__("tempfile").mkdtemp(prefix="ousast-stub-")) / "cpg.bin"
graph.write_bytes(("stub graph " + language + " " + str(len(files))).encode())
self.graphs += 1
census = {
"files": str(len(files)),
"methods": str(max(1, 10 * len(files))),
"calls": "1",
"file_names": files,
"overlays": "dataflowOss",
}
def run(kind: str, params: Mapping[str, object]) -> object:
if kind == "census":
return dict(census)
answer = batch(kind, {"one": params})
return answer.get("one") if answer else None
def batch(kind: str, requests: Mapping[str, Mapping[str, object]]) -> dict[str, list[object]]:
answers: dict[str, list[object]] = {"__census__": [dict(census)]}
timing = []
for rid in requests:
self.asked += 1
rows = self.templates[self.asked % len(self.templates)] if kind == "taint" else [{"kind": "context_summary"}]
answers[rid] = list(rows)
timing.append({"id": rid, "ms": 1.0})
answers[TIMING] = timing # type: ignore[assignment]
return answers
return CpgResult(cpg_path=graph, run=run, run_batch=batch, run_batch_once=batch, execution_diagnostics=lambda: ())
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repository", type=Path, required=True)
parser.add_argument("--base", required=True)
parser.add_argument("--head", required=True)
parser.add_argument("--rows", type=Path, action="append", required=True, help="recorded engine rows, {id: rows}")
parser.add_argument("--out", type=Path, required=True)
parser.add_argument("--deadline", type=float, default=3600.0)
parser.add_argument("--max-regions", type=int, default=500)
args = parser.parse_args()
args.out.mkdir(parents=True, exist_ok=True)
templates = [rows for path in args.rows for rows in json.loads(path.read_text()).values() if isinstance(rows, list)]
if not templates:
raise SystemExit("no recorded rows: the stub would answer nothing and measure nothing")
print(f"{len(templates)} recorded answers from {len(args.rows)} file(s)", flush=True)
profiling.start(str(args.out / "profile.txt"), flush=10.0)
engine = StubEngine(templates)
started = time.monotonic()
delivery = replay(
args.repository,
base=args.base,
head=args.head,
artifact=args.out / "artifact.json",
config=PushConfig(deadline_seconds=args.deadline, cancellation_allowance_seconds=2.0),
backend=engine,
max_regions=args.max_regions,
cache_dir=args.out / "cache",
)
elapsed = time.monotonic() - started
artifact = json.loads((args.out / "artifact.json").read_text()) if (args.out / "artifact.json").is_file() else {}
timings = {k: round(v, 1) for k, v in artifact.get("timings", {}).items() if k.endswith("_seconds")}
questions = sum(len(s["scan"]["question_outcomes"]) for s in artifact.get("scans", []))
summary = {
"elapsed": round(elapsed, 1),
"graphs": engine.graphs,
"requests_answered": engine.asked,
"questions": questions,
"timings": timings,
}
(args.out / "summary.json").write_text(json.dumps(summary, indent=2) + "\n")
print(json.dumps(summary), flush=True)
print(delivery.terminal if hasattr(delivery, "terminal") else "", flush=True)
if not engine.asked or not questions:
raise SystemExit("the instrument did not exercise the push path: nothing was asked or no question was recorded")
return 0
if __name__ == "__main__":
raise SystemExit(main())