brick/tests/benchmarks/runner.py
trvon 48436f35b7
feat: resolve C/C++ and Rust calls across files and typed receivers
C++ calls now resolve through the include closure: header prototypes pair
with definitions in other translation units, lookup walks class (including
in-repository bases), namespace, and `using` scopes outward, and static or
anonymous-namespace definitions stay file-local. Member calls resolve only
through explicitly declared receiver types (parameters, locals, fields,
this, smart pointers, aliases); template parameters stay unknown. Calls
hidden by the most vexing parse are recovered, casts are no longer calls,
reference-returning and conversion-operator declarations are named
correctly, and benign `= {}` default-argument recoveries no longer drop
prototypes or flag files.

Rust method calls resolve through a crate-wide impl index for self, typed
parameters and lets, struct fields, Self-returning constructors, dyn
traits, and Type::/Self:: associated calls, with foreign imports and
generics never guessed. Calls inside expression macros are re-parsed.

Every unresolved call records source.unresolved-reason, weaker resolutions
record source.resolution/source.extraction, and metrics.resolution reports
per-language call-site coverage. Declarations gain facts consumed by
caller-status queries (qualified names, linkage, virtual, trait impls,
attributes, name references, parse-incomplete). C++/Rust External targets
include the reason in their identity; out-of-line C++ methods keep their
IDs.

Linear node/edge scans and per-node ancestor walks are replaced with
indexes, cutting a YAMS scan from 575 s to 23 s.

New labeled cpp_xtu and rust_methods corpora run three times in the
quality gate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 15:11:04 -06:00

480 lines
18 KiB
Python
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Small artifact-first benchmark runner for Brick's labeled seed corpus."""
from __future__ import annotations
import argparse
import datetime as dt
import hashlib
import json
import os
import pathlib
import platform
import re
import statistics
import subprocess
import sys
import time
from collections import Counter
ROOT = pathlib.Path(__file__).resolve().parents[2]
PLANS = ROOT / "tests" / "benchmarks" / "plans"
def git_provenance(path: pathlib.Path) -> dict:
"""Describe a Git worktree and fingerprint all tracked and untracked changes."""
discovered = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
cwd=path,
text=True,
capture_output=True,
check=False,
)
if discovered.returncode != 0:
return {
"root": str(path.resolve()),
"revision": None,
"dirty": None,
"working_tree_fingerprint": None,
}
repository = pathlib.Path(discovered.stdout.strip()).resolve()
revision = subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=repository,
text=True,
capture_output=True,
check=False,
)
status = subprocess.run(
["git", "status", "--porcelain=v1", "--untracked-files=all", "-z"],
cwd=repository,
capture_output=True,
check=False,
)
if revision.returncode != 0 or status.returncode != 0:
return {
"root": str(repository),
"revision": None,
"dirty": None,
"working_tree_fingerprint": None,
}
dirty = bool(status.stdout)
fingerprint = None
if dirty:
diff = subprocess.run(
["git", "diff", "--binary", "HEAD"],
cwd=repository,
capture_output=True,
check=False,
)
untracked = subprocess.run(
["git", "ls-files", "--others", "--exclude-standard", "-z"],
cwd=repository,
capture_output=True,
check=False,
)
if diff.returncode == 0 and untracked.returncode == 0:
digest = hashlib.sha256()
digest.update(b"status\0")
digest.update(status.stdout)
digest.update(b"diff\0")
digest.update(diff.stdout)
for encoded_path in filter(None, untracked.stdout.split(b"\0")):
entry = repository / os.fsdecode(encoded_path)
if entry.is_symlink():
contents = os.fsencode(os.readlink(entry))
elif entry.is_file():
contents = entry.read_bytes()
else:
contents = b""
digest.update(b"untracked\0")
digest.update(encoded_path)
digest.update(b"\0")
digest.update(contents)
fingerprint = digest.hexdigest()
return {
"root": str(repository),
"revision": revision.stdout.strip(),
"dirty": dirty,
"working_tree_fingerprint": fingerprint,
}
def load_plan(name: str) -> dict:
if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]*", name) is None:
raise ValueError(f"invalid benchmark plan name: {name!r}")
path = PLANS / f"{name}.json"
with path.open(encoding="utf-8") as handle:
plan = json.load(handle)
if plan.get("schema_version") != 1 or plan.get("name") != name:
raise ValueError(f"invalid plan identity: {path}")
if int(plan.get("repeats", 0)) < 1:
raise ValueError("repeats must be positive")
return plan
def command_for(
plan: dict, corpus: pathlib.Path, graph: pathlib.Path, html: pathlib.Path
) -> list[str]:
features = plan.get("cargo_features", [])
feature_args = ["--features", ",".join(features)] if features else []
prefix = (
[str(ROOT / "target" / "debug" / "brick")]
if plan.get("direct_binary")
else ["cargo", "run", "--quiet", *feature_args, "--"]
)
return [
*prefix,
"build",
str(corpus),
"--json",
str(graph),
"--html",
str(html),
*plan.get("build_args", []),
]
def fixture_assertions(metrics: dict, expected: dict) -> dict[str, bool]:
assertions = {
"symbols": metrics["symbol_count"] >= expected["min_symbols"],
"dependencies": metrics["dependency_edge_count"]
>= expected["min_dependency_edges"],
}
if "cycles" in expected:
assertions["cycles"] = metrics["cycle_count"] == expected["cycles"]
if "max_cycles" in expected:
assertions["max_cycles"] = metrics["cycle_count"] <= expected["max_cycles"]
for field, value in expected.get("p4", {}).items():
assertions[f"p4.{field}"] = metrics.get("p4", {}).get(field) == value
return assertions
def p4_relationship_quality(model: dict, labels: list[dict]) -> dict:
nodes = {node["id"]: node for node in model["nodes"]}
def node_identity(node_id: str) -> tuple[str | None, str | None, str | None]:
node = nodes.get(node_id, {})
owner = nodes.get(node.get("parent"), {})
return node.get("path"), owner.get("name"), node.get("name")
expected = {
(
label["source_path"],
label["source_owner"],
label["source"],
label["target_path"],
label["target_owner"],
label["target"],
label["relation"],
)
for label in labels
}
relation_kinds = {label["relation"] for label in labels}
observed = set()
for edge in model["edges"]:
raw_relations = edge.get("attributes", {}).get("p4.relation")
edge_relations = (
raw_relations if isinstance(raw_relations, list) else [raw_relations]
)
for relation in edge_relations:
if relation not in relation_kinds:
continue
source = node_identity(edge["source"])
target = node_identity(edge["target"])
if source[2] is not None and target[2] is not None:
observed.add((*source, *target, relation))
true_positives = len(expected & observed)
false_positives = len(observed - expected)
false_negatives = len(expected - observed)
precision = true_positives / len(observed) if observed else float(not expected)
recall = true_positives / len(expected) if expected else float(not observed)
return {
"expected": [list(item) for item in sorted(expected)],
"observed": [list(item) for item in sorted(observed)],
"true_positives": true_positives,
"false_positives": false_positives,
"false_negatives": false_negatives,
"precision": round(precision, 3),
"recall": round(recall, 3),
}
def run_plan(
name: str,
out_dir: pathlib.Path | None = None,
corpus_override: pathlib.Path | None = None,
) -> pathlib.Path:
plan = load_plan(name)
corpus = (corpus_override or ROOT / plan["corpus"]).resolve()
if not corpus.is_dir():
raise ValueError(f"benchmark corpus is missing: {corpus}")
stamp = dt.datetime.now(dt.UTC).strftime("%Y%m%dT%H%M%SZ")
artifact = out_dir or ROOT / "build" / "benchmarks" / name / stamp
artifact.mkdir(parents=True, exist_ok=False)
brick_provenance = git_provenance(ROOT)
corpus_provenance = git_provenance(corpus)
manifest = {
"schema_version": 1,
"started_at": stamp,
"plan": name,
"corpus": str(corpus),
"root": str(ROOT),
"revision": brick_provenance["revision"],
"provenance": {
"brick": brick_provenance,
"corpus": corpus_provenance,
},
"host": {
"platform": platform.platform(),
"python": platform.python_version(),
},
}
if plan.get("direct_binary"):
prepare_command = ["cargo", "build", "--quiet"]
features = plan.get("cargo_features", [])
if features:
prepare_command.extend(["--features", ",".join(features)])
prepared = subprocess.run(
prepare_command,
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
(artifact / "prepare.stdout").write_text(prepared.stdout, encoding="utf-8")
(artifact / "prepare.stderr").write_text(prepared.stderr, encoding="utf-8")
if prepared.returncode:
raise RuntimeError(prepared.stderr or prepared.stdout)
manifest["prepare_command"] = prepare_command
(artifact / "run_manifest.json").write_text(
json.dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
samples = []
reference = None
reference_model = None
for repeat in range(plan["repeats"]):
graph = artifact / f"graph-{repeat}.json"
html = artifact / f"building-{repeat}.html"
started = time.perf_counter()
command = command_for(plan, corpus, graph, html)
rss_path = artifact / f"rss-{repeat}.json"
max_rss_kib = None
if plan.get("measure_rss"):
if not hasattr(os, "wait4"):
raise RuntimeError("RSS measurement requires os.wait4")
stdout_path = artifact / f"command-{repeat}.stdout"
stderr_path = artifact / f"command-{repeat}.stderr"
with stdout_path.open("wb") as stdout, stderr_path.open("wb") as stderr:
process = subprocess.Popen(
command,
cwd=ROOT,
stdout=stdout,
stderr=stderr,
)
_, status, usage = os.wait4(process.pid, 0)
process.returncode = os.waitstatus_to_exitcode(status)
max_rss_kib = int(usage.ru_maxrss)
if sys.platform == "darwin":
max_rss_kib //= 1024
rss_path.write_text(
json.dumps({"max_rss_kib": max_rss_kib}, indent=2) + "\n",
encoding="utf-8",
)
completed = subprocess.CompletedProcess(
command,
process.returncode,
stdout_path.read_text(encoding="utf-8"),
stderr_path.read_text(encoding="utf-8"),
)
else:
completed = subprocess.run(
command,
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
elapsed_ms = (time.perf_counter() - started) * 1000
if completed.returncode:
raise RuntimeError(completed.stderr or completed.stdout)
with graph.open(encoding="utf-8") as handle:
model = json.load(handle)
signature = json.dumps(model, sort_keys=True, separators=(",", ":"))
if reference is None:
reference = signature
reference_model = model
if signature != reference:
raise AssertionError(f"unstable graph identity on repeat {repeat}")
sample = {
"repeat": repeat,
"command": command,
"elapsed_ms": round(elapsed_ms, 3),
"metrics": model["metrics"],
}
if max_rss_kib is not None:
sample["max_rss_kib"] = max_rss_kib
samples.append(sample)
metrics = samples[0]["metrics"]
expected = plan["expect"]
assertions = fixture_assertions(metrics, expected)
label_quality = None
source_quality = None
if plan.get("source_labels"):
if reference_model is None:
raise AssertionError("benchmark produced no reference model")
labels = json.loads((corpus / plan["source_labels"]).read_text())
source_quality = source_language_quality(reference_model, labels)
assertions["source_exact_symbols"] = source_quality["symbols"]["exact"]
assertions["source_exact_relationships"] = source_quality["relationships"]["exact"]
assertions["source_clean_diagnostics"] = not reference_model.get("diagnostics")
if plan.get("p4_relationship_labels"):
if reference_model is None:
raise AssertionError("benchmark produced no reference model")
label_quality = p4_relationship_quality(
reference_model, plan["p4_relationship_labels"]
)
assertions["p4_relationship_precision"] = label_quality["precision"] == 1.0
assertions["p4_relationship_recall"] = label_quality["recall"] == 1.0
if not all(assertions.values()):
raise AssertionError(
f"fixture expectations failed: {assertions}; metrics={metrics}"
)
elapsed = [sample["elapsed_ms"] for sample in samples]
summary = {
"schema_version": 1,
"plan": name,
"repeats": len(samples),
"elapsed_ms_median": round(statistics.median(elapsed), 3),
"elapsed_ms_min": min(elapsed),
"elapsed_ms_max": max(elapsed),
"identity_stable": True,
"full_model_stable": True,
"fixture_assertions": assertions,
"metrics": metrics,
"samples": samples,
}
if label_quality is not None:
summary["p4_relationship_quality"] = label_quality
if source_quality is not None:
summary["source_language_quality"] = source_quality
if plan.get("measure_rss"):
rss = [sample["max_rss_kib"] for sample in samples]
summary["max_rss_kib_median"] = statistics.median(rss)
summary["max_rss_kib_min"] = min(rss)
summary["max_rss_kib_max"] = max(rss)
(artifact / "plan.resolved.json").write_text(
json.dumps(plan, indent=2) + "\n", encoding="utf-8"
)
(artifact / "summary.json").write_text(
json.dumps(summary, indent=2) + "\n", encoding="utf-8"
)
report = (
f"# Brick benchmark: {name}\n\n"
f"- Repeats: {len(samples)}\n"
f"- Median: {summary['elapsed_ms_median']} ms\n"
f"- Range: {summary['elapsed_ms_min']}–{summary['elapsed_ms_max']} ms\n"
f"- Stable full-model JSON: yes\n"
f"- Fixture assertions: {assertions}\n"
f"- Structural score: {metrics['structural_health']['score']}\n"
)
if label_quality is not None:
report += (
f"- P4 relationship precision: {label_quality['precision']}\n"
f"- P4 relationship recall: {label_quality['recall']}\n"
)
if plan.get("measure_rss"):
report += (
f"- Max RSS median: {summary['max_rss_kib_median']} KiB\n"
f"- Max RSS range: {summary['max_rss_kib_min']}–{summary['max_rss_kib_max']} KiB\n"
)
(artifact / "REPORT.md").write_text(report, encoding="utf-8")
return artifact
def source_language_quality(model: dict, labels: dict) -> dict:
"""Compare every declaration and dependency, including clean-negative files."""
nodes = {node["id"]: node for node in model["nodes"]}
def identity(node: dict) -> str:
if node["kind"] == "external":
return f"external:{node.get('language')}:{node['name']}"
names = []
current = node
visited = set()
while current["kind"] in ("class", "function"):
if current["id"] in visited:
raise AssertionError("cyclic declaration ownership")
visited.add(current["id"])
qualifier = current.get("attributes", {}).get("source.qualifier")
names.append(f"{qualifier}::{current['name']}" if qualifier else current["name"])
current = nodes[current["parent"]]
scope = node.get("attributes", {}).get("source.scope")
return "::".join([node["path"], *([f"scope={scope}"] if scope else []), *reversed(names)])
observed_symbols = [(identity(n), n["kind"]) for n in nodes.values() if n["kind"] in ("class", "function")]
observed_relationships = [(identity(nodes[e["source"]]), e["kind"], identity(nodes[e["target"]])) for e in model["edges"] if e["kind"] in ("calls", "imports", "inherits")]
def compare(expected: list, observed: list) -> dict:
expected, observed = Counter(map(tuple, expected)), Counter(map(tuple, observed))
tp = sum((expected & observed).values())
fp, fn = observed - expected, expected - observed
return {
"exact": not fp and not fn,
"true_positives": tp,
"false_positives": list(fp.elements()),
"false_negatives": list(fn.elements()),
"precision": tp / sum(observed.values()) if observed else float(not expected),
"recall": tp / sum(expected.values()) if expected else float(not observed),
}
result = {"symbols": compare(labels["symbols"], observed_symbols), "relationships": compare(labels["relationships"], observed_relationships)}
# Corpus directories are the authored language strata (tsx is reported separately).
prefixes = labels.get("language_prefixes") or {
name: name + "/" for name in sorted({item[0].split("/")[0] for item in labels["symbols"]})
}
result["by_language"] = {
language: {
"symbols": compare([s for s in labels["symbols"] if s[0].startswith(prefix)], [s for s in observed_symbols if s[0].startswith(prefix)]),
"relationships": compare([s for s in labels["relationships"] if s[0].startswith(prefix)], [s for s in observed_relationships if s[0].startswith(prefix)]),
}
for language, prefix in prefixes.items()
}
return result
def main() -> int:
parser = argparse.ArgumentParser()
subcommands = parser.add_subparsers(dest="command", required=True)
subcommands.add_parser("self-test")
run_parser = subcommands.add_parser("run")
run_parser.add_argument("plan")
run_parser.add_argument("--out-dir", type=pathlib.Path)
run_parser.add_argument("--corpus", type=pathlib.Path)
args = parser.parse_args()
if args.command == "self-test":
plan = load_plan("lexical_smoke")
expected = plan["expect"]
if expected["min_symbols"] < 1 or expected["cycles"] < 0:
raise AssertionError("invalid benchmark expectations")
if not (ROOT / plan["corpus"]).is_dir():
raise AssertionError("benchmark corpus is missing")
print("benchmark harness self-test passed")
return 0
artifact = run_plan(args.plan, args.out_dir, args.corpus)
print(f"ARTIFACT={artifact}")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, ValueError, RuntimeError, AssertionError) as error:
print(f"benchmark: {error}", file=sys.stderr)
raise SystemExit(1) from error