Add Milestone 4 adapter benchmark gate
This commit is contained in:
parent
ecbf94d14a
commit
e525efd2fc
2 changed files with 509 additions and 2 deletions
13
Makefile
13
Makefile
|
|
@ -5,7 +5,7 @@ NPM := npm
|
||||||
PYTHONPYCACHEPREFIX := /tmp/docforge-quality-pycache
|
PYTHONPYCACHEPREFIX := /tmp/docforge-quality-pycache
|
||||||
PYTEST_BASETEMP := /tmp/docforge-quality-pytest
|
PYTEST_BASETEMP := /tmp/docforge-quality-pytest
|
||||||
|
|
||||||
.PHONY: accessibility benchmark benchmark-m1 benchmark-m1-smoke benchmark-m2 benchmark-m2-smoke benchmark-m3 benchmark-m3-full benchmark-m3-smoke benchmark-smoke build compile contract dependencies format-check gate lint lock test type
|
.PHONY: accessibility benchmark benchmark-m1 benchmark-m1-smoke benchmark-m2 benchmark-m2-smoke benchmark-m3 benchmark-m3-full benchmark-m3-smoke benchmark-m4 benchmark-m4-full benchmark-m4-smoke benchmark-smoke build compile contract dependencies format-check gate lint lock test type
|
||||||
|
|
||||||
accessibility:
|
accessibility:
|
||||||
$(NPM) run test:accessibility
|
$(NPM) run test:accessibility
|
||||||
|
|
@ -89,4 +89,13 @@ benchmark-m3:
|
||||||
|
|
||||||
benchmark-m3-full: benchmark-m3
|
benchmark-m3-full: benchmark-m3
|
||||||
|
|
||||||
gate: format-check lint type compile contract test accessibility lock dependencies build benchmark-smoke benchmark-m1-smoke benchmark-m2-smoke benchmark-m3-smoke
|
benchmark-m4-smoke:
|
||||||
|
$(PYTHON) tools/milestone4_benchmark.py --mode smoke \
|
||||||
|
--output /tmp/docforge-milestone4-smoke.json > /dev/null
|
||||||
|
|
||||||
|
benchmark-m4:
|
||||||
|
$(PYTHON) tools/milestone4_benchmark.py --mode full
|
||||||
|
|
||||||
|
benchmark-m4-full: benchmark-m4
|
||||||
|
|
||||||
|
gate: format-check lint type compile contract test accessibility lock dependencies build benchmark-smoke benchmark-m1-smoke benchmark-m2-smoke benchmark-m3-smoke benchmark-m4-smoke
|
||||||
|
|
|
||||||
498
tools/milestone4_benchmark.py
Normal file
498
tools/milestone4_benchmark.py
Normal file
|
|
@ -0,0 +1,498 @@
|
||||||
|
"""Milestone 4 Python adapter SDK, incremental, and recovery benchmark gates."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import ast
|
||||||
|
import gc
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import platform
|
||||||
|
import resource
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import tracemalloc
|
||||||
|
from collections.abc import Callable, Generator, Mapping
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import cast
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from docforge.adapter_sdk import (
|
||||||
|
AdapterAssembly,
|
||||||
|
AdapterProject,
|
||||||
|
AdapterSource,
|
||||||
|
AdapterSourceProjection,
|
||||||
|
)
|
||||||
|
from docforge.adapters.python import PythonReferenceAdapter
|
||||||
|
from docforge.index import ProjectIndex
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
FULL_SOURCE_COUNT = 334
|
||||||
|
SMOKE_SOURCE_COUNT = 12
|
||||||
|
NODES_PER_SOURCE = 3
|
||||||
|
DEFAULT_FULL_SAMPLES = 3
|
||||||
|
DEFAULT_SMOKE_SAMPLES = 1
|
||||||
|
|
||||||
|
# These are regression tripwires, not performance targets. They deliberately
|
||||||
|
# leave several times the Milestone 3 1,000-node in-process allowances for the
|
||||||
|
# additional AST, Logic, extraction-cache, and SQLite work in this gate.
|
||||||
|
MAX_TRACED_PEAK_BYTES = 256 * 1024 * 1024
|
||||||
|
MAX_PROCESS_PEAK_BYTES = 512 * 1024 * 1024
|
||||||
|
MAX_RESPONSE_BYTES = 256 * 1024
|
||||||
|
COLD_LIMIT_MS = 30_000.0
|
||||||
|
WARM_LIMIT_MS = 20_000.0
|
||||||
|
EQUIVALENCE_LIMIT_MS = 30_000.0
|
||||||
|
RECOVERY_LIMIT_MS = 30_000.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class _InstrumentedResult:
|
||||||
|
value: Mapping[str, object]
|
||||||
|
ast_parse_calls: int
|
||||||
|
extraction_calls: int
|
||||||
|
|
||||||
|
|
||||||
|
class _RecordingPythonAdapter(PythonReferenceAdapter):
|
||||||
|
"""Count extraction entry points without weakening the production adapter."""
|
||||||
|
|
||||||
|
def __init__(self, root: Path) -> None:
|
||||||
|
super().__init__(
|
||||||
|
root,
|
||||||
|
source_roots=("src",),
|
||||||
|
project_id="milestone4-python-benchmark",
|
||||||
|
title="Milestone 4 Python adapter benchmark",
|
||||||
|
)
|
||||||
|
self.extraction_calls = 0
|
||||||
|
|
||||||
|
def extract_source(self, source: AdapterSource) -> AdapterSourceProjection:
|
||||||
|
self.extraction_calls += 1
|
||||||
|
return super().extract_source(source)
|
||||||
|
|
||||||
|
|
||||||
|
def _parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=("Gate DocForge2 Milestone 4 adapter behavior on a disposable Python project.")
|
||||||
|
)
|
||||||
|
parser.add_argument("--mode", choices=("smoke", "full"), default="full")
|
||||||
|
parser.add_argument("--sources", type=int)
|
||||||
|
parser.add_argument("--samples", type=int)
|
||||||
|
parser.add_argument("--output", type=Path)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def encode_report(value: object) -> str:
|
||||||
|
"""Serialize benchmark evidence in one deterministic JSON representation."""
|
||||||
|
|
||||||
|
return json.dumps(value, sort_keys=True, indent=2, ensure_ascii=False) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _compact_json(value: object) -> bytes:
|
||||||
|
return json.dumps(
|
||||||
|
value,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
ensure_ascii=False,
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(value: object) -> str:
|
||||||
|
return hashlib.sha256(_compact_json(value)).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _git(arguments: list[str]) -> str:
|
||||||
|
return subprocess.run(
|
||||||
|
["git", *arguments],
|
||||||
|
cwd=ROOT,
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
).stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def write_python_fixture(root: Path, source_count: int) -> None:
|
||||||
|
"""Write a deterministic project with three primary nodes per source."""
|
||||||
|
|
||||||
|
if source_count < 2:
|
||||||
|
raise ValueError("The Milestone 4 fixture requires at least two sources")
|
||||||
|
package = root / "src" / "benchmark"
|
||||||
|
package.mkdir(parents=True)
|
||||||
|
for index in range(source_count):
|
||||||
|
dependency = ""
|
||||||
|
call = f"value = {index}"
|
||||||
|
if index:
|
||||||
|
dependency = f"from benchmark.module_{index - 1:04d} import compute_{index - 1:04d}\n\n"
|
||||||
|
call = f"value = compute_{index - 1:04d}(value)"
|
||||||
|
source = (
|
||||||
|
f'"""Synthetic adapter benchmark module {index:04d}."""\n\n'
|
||||||
|
f"{dependency}"
|
||||||
|
f"def compute_{index:04d}(value: int) -> int:\n"
|
||||||
|
f' """Return one deterministic branch result."""\n'
|
||||||
|
f" {call}\n"
|
||||||
|
" if value % 2:\n"
|
||||||
|
f" return value + {index + 1}\n"
|
||||||
|
" return value\n"
|
||||||
|
)
|
||||||
|
(package / f"module_{index:04d}.py").write_text(source, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _count_ast_parses() -> Generator[mock.MagicMock]:
|
||||||
|
with mock.patch.object(ast, "parse", wraps=ast.parse) as parsed:
|
||||||
|
yield parsed
|
||||||
|
|
||||||
|
|
||||||
|
def _instrument(
|
||||||
|
adapter: _RecordingPythonAdapter,
|
||||||
|
operation: Callable[[], Mapping[str, object]],
|
||||||
|
) -> _InstrumentedResult:
|
||||||
|
adapter.extraction_calls = 0
|
||||||
|
with _count_ast_parses() as counter:
|
||||||
|
value = operation()
|
||||||
|
return _InstrumentedResult(
|
||||||
|
value=value,
|
||||||
|
ast_parse_calls=counter.call_count,
|
||||||
|
extraction_calls=adapter.extraction_calls,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_summary(sample: _InstrumentedResult) -> dict[str, object]:
|
||||||
|
value = sample.value
|
||||||
|
build = cast(Mapping[str, object], value["build"])
|
||||||
|
return {
|
||||||
|
"status": value["status"],
|
||||||
|
"revision": value["revision"],
|
||||||
|
"source_hash": value["source_hash"],
|
||||||
|
"node_hash": value["node_hash"],
|
||||||
|
"node_count": value["node_count"],
|
||||||
|
"edge_hash": value["edge_hash"],
|
||||||
|
"edge_count": value["edge_count"],
|
||||||
|
"logic_hash": value["logic_hash"],
|
||||||
|
"logic_projection_count": value["logic_projection_count"],
|
||||||
|
"logic_node_count": value["logic_node_count"],
|
||||||
|
"logic_edge_count": value["logic_edge_count"],
|
||||||
|
"cache_hits": build["cache_hits"],
|
||||||
|
"reparsed_sources": build["reparsed_sources"],
|
||||||
|
"invalidated_sources": build["invalidated_sources"],
|
||||||
|
"deleted_sources": build["deleted_sources"],
|
||||||
|
"total_sources": build["total_sources"],
|
||||||
|
"ast_parse_calls": sample.ast_parse_calls,
|
||||||
|
"extraction_calls": sample.extraction_calls,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _equivalence_summary(sample: _InstrumentedResult) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
**sample.value,
|
||||||
|
"ast_parse_calls": sample.ast_parse_calls,
|
||||||
|
"extraction_calls": sample.extraction_calls,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _synchronization_summary(sample: _InstrumentedResult) -> dict[str, object]:
|
||||||
|
synchronization = cast(Mapping[str, object], sample.value["synchronization"])
|
||||||
|
build = cast(Mapping[str, object], synchronization["build"])
|
||||||
|
return {
|
||||||
|
"status": sample.value["status"],
|
||||||
|
"revision": sample.value["revision"],
|
||||||
|
"source_hash": sample.value["source_hash"],
|
||||||
|
"node_hash": sample.value["node_hash"],
|
||||||
|
"node_count": sample.value["node_count"],
|
||||||
|
"edge_hash": sample.value["edge_hash"],
|
||||||
|
"edge_count": sample.value["edge_count"],
|
||||||
|
"logic_hash": sample.value["logic_hash"],
|
||||||
|
"logic_projection_count": sample.value["logic_projection_count"],
|
||||||
|
"action": synchronization["action"],
|
||||||
|
"initial_error_code": cast(Mapping[str, object], synchronization["initial_error"])["code"],
|
||||||
|
"cache_hits": build["cache_hits"],
|
||||||
|
"reparsed_sources": build["reparsed_sources"],
|
||||||
|
"ast_parse_calls": sample.ast_parse_calls,
|
||||||
|
"extraction_calls": sample.extraction_calls,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _measure(
|
||||||
|
operation: Callable[[], _InstrumentedResult],
|
||||||
|
*,
|
||||||
|
samples: int,
|
||||||
|
p95_limit_ms: float,
|
||||||
|
summary: Callable[[_InstrumentedResult], Mapping[str, object]],
|
||||||
|
) -> tuple[dict[str, object], _InstrumentedResult]:
|
||||||
|
durations: list[float] = []
|
||||||
|
traced_peaks: list[int] = []
|
||||||
|
response_sizes: list[int] = []
|
||||||
|
stable_summary: dict[str, object] | None = None
|
||||||
|
last: _InstrumentedResult | None = None
|
||||||
|
for _ in range(samples):
|
||||||
|
gc.collect()
|
||||||
|
tracemalloc.start()
|
||||||
|
started = time.perf_counter_ns()
|
||||||
|
try:
|
||||||
|
current = operation()
|
||||||
|
elapsed_ms = (time.perf_counter_ns() - started) / 1_000_000
|
||||||
|
_, traced_peak = tracemalloc.get_traced_memory()
|
||||||
|
finally:
|
||||||
|
tracemalloc.stop()
|
||||||
|
current_summary = dict(summary(current))
|
||||||
|
if stable_summary is None:
|
||||||
|
stable_summary = current_summary
|
||||||
|
elif current_summary != stable_summary:
|
||||||
|
raise RuntimeError("Milestone 4 operation changed its deterministic result")
|
||||||
|
response_bytes = len(_compact_json(current.value))
|
||||||
|
if response_bytes > MAX_RESPONSE_BYTES:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Milestone 4 response exceeded its fixed byte boundary: "
|
||||||
|
f"{response_bytes} > {MAX_RESPONSE_BYTES}"
|
||||||
|
)
|
||||||
|
if traced_peak > MAX_TRACED_PEAK_BYTES:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Milestone 4 operation exceeded its traced-memory boundary: "
|
||||||
|
f"{traced_peak} > {MAX_TRACED_PEAK_BYTES}"
|
||||||
|
)
|
||||||
|
durations.append(elapsed_ms)
|
||||||
|
traced_peaks.append(traced_peak)
|
||||||
|
response_sizes.append(response_bytes)
|
||||||
|
last = current
|
||||||
|
ordered = sorted(durations)
|
||||||
|
p95_index = max(0, math.ceil(len(ordered) * 0.95) - 1)
|
||||||
|
p95 = ordered[p95_index]
|
||||||
|
if p95 > p95_limit_ms:
|
||||||
|
raise RuntimeError(f"Milestone 4 operation p95 {p95:.3f} ms exceeds {p95_limit_ms:.3f} ms")
|
||||||
|
assert stable_summary is not None
|
||||||
|
assert last is not None
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
"samples": samples,
|
||||||
|
"median_ms": round(statistics.median(ordered), 3),
|
||||||
|
"p95_ms": round(p95, 3),
|
||||||
|
"min_ms": round(ordered[0], 3),
|
||||||
|
"max_ms": round(ordered[-1], 3),
|
||||||
|
"p95_limit_ms": p95_limit_ms,
|
||||||
|
"maximum_response_bytes": max(response_sizes),
|
||||||
|
"response_limit_bytes": MAX_RESPONSE_BYTES,
|
||||||
|
"maximum_traced_peak_bytes": max(traced_peaks),
|
||||||
|
"traced_peak_limit_bytes": MAX_TRACED_PEAK_BYTES,
|
||||||
|
"stable_result": stable_summary,
|
||||||
|
},
|
||||||
|
last,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _semantic_assembly_hash(assembly: AdapterAssembly) -> str:
|
||||||
|
projection = assembly.projection
|
||||||
|
return _sha256(
|
||||||
|
{
|
||||||
|
"project_id": projection.project_id,
|
||||||
|
"title": projection.title,
|
||||||
|
"adapter_id": projection.adapter_id,
|
||||||
|
"adapter_version": projection.adapter_version,
|
||||||
|
"revision": projection.revision,
|
||||||
|
"source_hash": projection.source_hash,
|
||||||
|
"nodes": [item.as_dict() for item in projection.nodes],
|
||||||
|
"edges": [item.as_dict() for item in projection.edges],
|
||||||
|
"logic": [item.as_dict() for item in assembly.logic],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _benchmark(root: Path, source_count: int, samples: int) -> dict[str, object]:
|
||||||
|
write_python_fixture(root, source_count)
|
||||||
|
adapter = _RecordingPythonAdapter(root)
|
||||||
|
cache_root = root / ".docforge" / "benchmark"
|
||||||
|
project = AdapterProject(adapter, cache_root=cache_root)
|
||||||
|
index = ProjectIndex(project)
|
||||||
|
|
||||||
|
cold, cold_sample = _measure(
|
||||||
|
lambda: _instrument(adapter, index.build),
|
||||||
|
samples=1,
|
||||||
|
p95_limit_ms=COLD_LIMIT_MS,
|
||||||
|
summary=_build_summary,
|
||||||
|
)
|
||||||
|
cold_summary = _build_summary(cold_sample)
|
||||||
|
expected_nodes = source_count * NODES_PER_SOURCE
|
||||||
|
if (
|
||||||
|
cold_summary["node_count"] != expected_nodes
|
||||||
|
or cold_summary["logic_projection_count"] != source_count
|
||||||
|
or cold_summary["reparsed_sources"] != source_count
|
||||||
|
or cast(int, cold_summary["ast_parse_calls"]) < source_count
|
||||||
|
or cold_summary["extraction_calls"] != source_count
|
||||||
|
):
|
||||||
|
raise RuntimeError("Cold adapter build did not cover the complete synthetic fixture")
|
||||||
|
|
||||||
|
warm, warm_sample = _measure(
|
||||||
|
lambda: _instrument(adapter, index.build),
|
||||||
|
samples=samples,
|
||||||
|
p95_limit_ms=WARM_LIMIT_MS,
|
||||||
|
summary=_build_summary,
|
||||||
|
)
|
||||||
|
warm_summary = _build_summary(warm_sample)
|
||||||
|
if (
|
||||||
|
warm_summary["cache_hits"] != source_count
|
||||||
|
or warm_summary["reparsed_sources"] != 0
|
||||||
|
or warm_summary["ast_parse_calls"] != 0
|
||||||
|
or warm_summary["extraction_calls"] != 0
|
||||||
|
):
|
||||||
|
raise RuntimeError("Warm adapter build performed forbidden parsing or extraction")
|
||||||
|
|
||||||
|
equivalence, equivalence_sample = _measure(
|
||||||
|
lambda: _instrument(adapter, project.verify_incremental_equivalence),
|
||||||
|
samples=1,
|
||||||
|
p95_limit_ms=EQUIVALENCE_LIMIT_MS,
|
||||||
|
summary=_equivalence_summary,
|
||||||
|
)
|
||||||
|
equivalence_summary = _equivalence_summary(equivalence_sample)
|
||||||
|
if (
|
||||||
|
equivalence_summary["status"] != "ok"
|
||||||
|
or equivalence_summary["node_count"] != expected_nodes
|
||||||
|
or equivalence_summary["logic_projection_count"] != source_count
|
||||||
|
):
|
||||||
|
raise RuntimeError("Full and incremental graph plus Logic output was not equivalent")
|
||||||
|
complete_assembly = adapter.load_complete_assembly()
|
||||||
|
assembly_hash = _semantic_assembly_hash(complete_assembly)
|
||||||
|
|
||||||
|
extraction_cache = cache_root / "extractions.json"
|
||||||
|
extraction_cache.write_bytes(b"{broken")
|
||||||
|
cache_recovery, cache_recovery_sample = _measure(
|
||||||
|
lambda: _instrument(adapter, index.build),
|
||||||
|
samples=1,
|
||||||
|
p95_limit_ms=RECOVERY_LIMIT_MS,
|
||||||
|
summary=_build_summary,
|
||||||
|
)
|
||||||
|
cache_recovery_summary = _build_summary(cache_recovery_sample)
|
||||||
|
if (
|
||||||
|
cache_recovery_summary["reparsed_sources"] != source_count
|
||||||
|
or cast(int, cache_recovery_summary["ast_parse_calls"]) < source_count
|
||||||
|
or cache_recovery_summary["node_hash"] != cold_summary["node_hash"]
|
||||||
|
or cache_recovery_summary["logic_hash"] != cold_summary["logic_hash"]
|
||||||
|
):
|
||||||
|
raise RuntimeError("Corrupt extraction-cache recovery changed adapter output")
|
||||||
|
|
||||||
|
index.path.write_bytes(b"not-a-sqlite-index")
|
||||||
|
index_recovery, index_recovery_sample = _measure(
|
||||||
|
lambda: _instrument(adapter, index.synchronize),
|
||||||
|
samples=1,
|
||||||
|
p95_limit_ms=RECOVERY_LIMIT_MS,
|
||||||
|
summary=_synchronization_summary,
|
||||||
|
)
|
||||||
|
index_recovery_summary = _synchronization_summary(index_recovery_sample)
|
||||||
|
if (
|
||||||
|
index_recovery_summary["action"] != "rebuilt"
|
||||||
|
or index_recovery_summary["cache_hits"] != source_count
|
||||||
|
or index_recovery_summary["reparsed_sources"] != 0
|
||||||
|
or index_recovery_summary["ast_parse_calls"] != 0
|
||||||
|
or index_recovery_summary["extraction_calls"] != 0
|
||||||
|
or index_recovery_summary["node_hash"] != cold_summary["node_hash"]
|
||||||
|
or index_recovery_summary["logic_hash"] != cold_summary["logic_hash"]
|
||||||
|
):
|
||||||
|
raise RuntimeError("Corrupt derived-index recovery changed adapter output")
|
||||||
|
|
||||||
|
evidence = {
|
||||||
|
"source_count": source_count,
|
||||||
|
"node_count": expected_nodes,
|
||||||
|
"edge_count": cold_summary["edge_count"],
|
||||||
|
"logic_projection_count": source_count,
|
||||||
|
"logic_node_count": cold_summary["logic_node_count"],
|
||||||
|
"logic_edge_count": cold_summary["logic_edge_count"],
|
||||||
|
"source_hash": cold_summary["source_hash"],
|
||||||
|
"node_hash": cold_summary["node_hash"],
|
||||||
|
"edge_hash": cold_summary["edge_hash"],
|
||||||
|
"logic_hash": cold_summary["logic_hash"],
|
||||||
|
"complete_assembly_hash": assembly_hash,
|
||||||
|
"full_incremental_graph_and_logic_exact": True,
|
||||||
|
"warm_zero_ast_parse": True,
|
||||||
|
"warm_zero_source_extraction": True,
|
||||||
|
"cache_recovery_exact": True,
|
||||||
|
"index_recovery_exact": True,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"fixture": {
|
||||||
|
"kind": "synthetic_python_reference_adapter",
|
||||||
|
"source_count": source_count,
|
||||||
|
"nodes_per_source": NODES_PER_SOURCE,
|
||||||
|
"expected_node_count": expected_nodes,
|
||||||
|
},
|
||||||
|
"operations": {
|
||||||
|
"cold_incremental_build": cold,
|
||||||
|
"warm_incremental_build": warm,
|
||||||
|
"full_incremental_equivalence": equivalence,
|
||||||
|
"corrupt_extraction_cache_recovery": cache_recovery,
|
||||||
|
"corrupt_index_recovery": index_recovery,
|
||||||
|
},
|
||||||
|
"evidence": evidence,
|
||||||
|
"evidence_sha256": _sha256(evidence),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
arguments = _parser().parse_args()
|
||||||
|
default_sources = FULL_SOURCE_COUNT if arguments.mode == "full" else SMOKE_SOURCE_COUNT
|
||||||
|
default_samples = DEFAULT_FULL_SAMPLES if arguments.mode == "full" else DEFAULT_SMOKE_SAMPLES
|
||||||
|
source_count = default_sources if arguments.sources is None else arguments.sources
|
||||||
|
samples = default_samples if arguments.samples is None else arguments.samples
|
||||||
|
if not 2 <= source_count <= FULL_SOURCE_COUNT:
|
||||||
|
raise SystemExit(f"--sources must be between 2 and {FULL_SOURCE_COUNT}")
|
||||||
|
if arguments.mode == "full" and source_count != FULL_SOURCE_COUNT:
|
||||||
|
raise SystemExit(f"--mode full requires exactly {FULL_SOURCE_COUNT} sources")
|
||||||
|
if samples < 1:
|
||||||
|
raise SystemExit("--samples must be positive")
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="docforge-milestone4-") as directory:
|
||||||
|
measurement = _benchmark(Path(directory).resolve(), source_count, samples)
|
||||||
|
|
||||||
|
process_peak_bytes = int(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) * 1024
|
||||||
|
if process_peak_bytes > MAX_PROCESS_PEAK_BYTES:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Milestone 4 process peak exceeded its memory boundary: "
|
||||||
|
f"{process_peak_bytes} > {MAX_PROCESS_PEAK_BYTES}"
|
||||||
|
)
|
||||||
|
status = _git(["status", "--porcelain"])
|
||||||
|
report: dict[str, object] = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"benchmark": "docforge2_milestone4",
|
||||||
|
"mode": arguments.mode,
|
||||||
|
"source": {
|
||||||
|
"revision": _git(["rev-parse", "HEAD"]),
|
||||||
|
"dirty": bool(status),
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"platform": platform.platform(),
|
||||||
|
"machine": platform.machine(),
|
||||||
|
"python": platform.python_version(),
|
||||||
|
"implementation": platform.python_implementation(),
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"clock": "time.perf_counter_ns",
|
||||||
|
"in_process_peak_memory": "tracemalloc per measured invocation",
|
||||||
|
"process_peak_memory": "resource.getrusage(RUSAGE_SELF).ru_maxrss",
|
||||||
|
"response_size": "UTF-8 bytes of compact sorted JSON",
|
||||||
|
"ast_instrumentation": "temporary counter around stdlib ast.parse",
|
||||||
|
"extraction_instrumentation": "reference adapter extract_source entry counter",
|
||||||
|
"samples": samples,
|
||||||
|
"full_mode_source_requirement": FULL_SOURCE_COUNT,
|
||||||
|
"threshold_basis": (
|
||||||
|
"Regression tripwires leave several times the Milestone 3 1,000-node "
|
||||||
|
"allowances for AST, Logic, extraction-cache, and SQLite work."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
**measurement,
|
||||||
|
"memory": {
|
||||||
|
"process_peak_bytes": process_peak_bytes,
|
||||||
|
"process_peak_limit_bytes": MAX_PROCESS_PEAK_BYTES,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
encoded = encode_report(report)
|
||||||
|
if arguments.output is not None:
|
||||||
|
output = arguments.output.resolve()
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
output.write_text(encoded, encoding="utf-8")
|
||||||
|
sys.stdout.write(encoded)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Loading…
Add table
Add a link
Reference in a new issue