2026-07-29 12:38:25 -04:00
|
|
|
"""Milestone 3 projection, worker, fragment, and publication benchmark gates."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
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, Mapping, Sequence
|
|
|
|
|
from dataclasses import dataclass, replace
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import cast
|
|
|
|
|
|
|
|
|
|
from milestone0_baseline import write_synthetic_project
|
|
|
|
|
|
|
|
|
|
from docforge.graph_projection import (
|
|
|
|
|
GraphViewRequestV1,
|
|
|
|
|
build_graph_projection_package,
|
|
|
|
|
build_graph_view_plan,
|
|
|
|
|
)
|
|
|
|
|
from docforge.graph_rendering import GraphRenderService
|
|
|
|
|
from docforge.manual_projection import (
|
|
|
|
|
build_manual_projection_package,
|
|
|
|
|
build_manual_render_plan,
|
|
|
|
|
)
|
|
|
|
|
from docforge.models import ProjectSnapshot
|
|
|
|
|
from docforge.project import Project
|
|
|
|
|
from docforge.projection_contract import (
|
|
|
|
|
MAX_PACKAGE_BYTES,
|
|
|
|
|
MAX_PLAN_BYTES,
|
|
|
|
|
MAX_RECEIPT_BYTES,
|
|
|
|
|
GraphViewPlanV1,
|
|
|
|
|
ManualRenderPlanV1,
|
|
|
|
|
ProjectionPackageV1,
|
|
|
|
|
ProjectionRenderResult,
|
|
|
|
|
canonical_projection_bytes,
|
|
|
|
|
projection_hash,
|
|
|
|
|
)
|
|
|
|
|
from docforge.projection_fragments import (
|
|
|
|
|
FragmentKey,
|
|
|
|
|
FragmentRecord,
|
|
|
|
|
ProjectionFragmentCache,
|
|
|
|
|
fragment_semantic_hash,
|
|
|
|
|
)
|
|
|
|
|
from docforge.projection_worker import (
|
|
|
|
|
MAX_WORKER_ARTIFACT_BYTES,
|
|
|
|
|
render_projection_in_worker,
|
|
|
|
|
)
|
|
|
|
|
from docforge.render_contract import GenericHtmlRenderer, PreparedRender
|
|
|
|
|
from docforge.rendering import RenderService
|
|
|
|
|
from docforge.telemetry import COUNTER_NAMES, request
|
|
|
|
|
from docforge_renderers.graph import PortableGraphHtmlRenderer
|
|
|
|
|
from docforge_renderers.manual import ManualHtmlRenderer
|
|
|
|
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
FULL_NODE_COUNT = 1_000
|
|
|
|
|
SMOKE_NODE_COUNT = 25
|
|
|
|
|
DEFAULT_FULL_SAMPLES = 3
|
|
|
|
|
DEFAULT_SMOKE_SAMPLES = 1
|
|
|
|
|
MAX_STATUS_RESPONSE_BYTES = 256_000
|
|
|
|
|
MAX_TRACED_PEAK_BYTES = 256 * 1024 * 1024
|
|
|
|
|
MAX_CHILD_PEAK_BYTES = 256 * 1024 * 1024
|
|
|
|
|
|
|
|
|
|
ZERO_WORK_COUNTERS = tuple(
|
|
|
|
|
counter for counter in COUNTER_NAMES if counter != "source_generation_checks"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class _ProjectionSample:
|
|
|
|
|
plan_id: str
|
|
|
|
|
package_id: str
|
|
|
|
|
artifact: bytes
|
|
|
|
|
receipt: Mapping[str, object]
|
|
|
|
|
plan_bytes: int
|
|
|
|
|
package_bytes: int
|
|
|
|
|
child_peak_memory_bytes: int | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class _StatusSample:
|
|
|
|
|
response: Mapping[str, object]
|
|
|
|
|
counters: Mapping[str, object]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class _FragmentSweep:
|
|
|
|
|
fragment_count: int
|
|
|
|
|
aggregate_content_bytes: int
|
|
|
|
|
ordered_record_hash: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parser() -> argparse.ArgumentParser:
|
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
|
description="Gate DocForge2 Milestone 3 projection behavior on a disposable project."
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument("--mode", choices=("smoke", "full"), default="full")
|
|
|
|
|
parser.add_argument("--nodes", type=int)
|
|
|
|
|
parser.add_argument("--samples", type=int)
|
|
|
|
|
parser.add_argument("--output", type=Path)
|
|
|
|
|
return parser
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def encode_report(value: object) -> str:
|
|
|
|
|
"""Serialize one report deterministically for files, CI logs, and comparisons."""
|
|
|
|
|
|
|
|
|
|
return json.dumps(value, sort_keys=True, indent=2, ensure_ascii=False) + "\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _git(arguments: list[str]) -> str:
|
|
|
|
|
return subprocess.run(
|
|
|
|
|
["git", *arguments],
|
|
|
|
|
cwd=ROOT,
|
|
|
|
|
check=True,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
).stdout.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _compact_size(value: object) -> int:
|
|
|
|
|
return len(canonical_projection_bytes(value))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sha256(content: bytes) -> str:
|
|
|
|
|
return hashlib.sha256(content).hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _prepare_fixture(root: Path, node_count: int) -> None:
|
|
|
|
|
write_synthetic_project(root, node_count)
|
|
|
|
|
descriptor = root / ".docforge" / "project.toml"
|
|
|
|
|
maximum_edges = node_count - 1
|
|
|
|
|
maximum_work = max(100, node_count * 4)
|
|
|
|
|
original = descriptor.read_text(encoding="utf-8")
|
|
|
|
|
original = original.replace(
|
|
|
|
|
"max_render_bytes = 20000000",
|
|
|
|
|
"max_render_bytes = 4000000",
|
|
|
|
|
)
|
|
|
|
|
descriptor.write_text(
|
|
|
|
|
original
|
|
|
|
|
+ f"""
|
|
|
|
|
|
|
|
|
|
[graph_render]
|
|
|
|
|
output_root = ".docforge/portable-graph"
|
|
|
|
|
|
|
|
|
|
[[graph_render.views]]
|
|
|
|
|
id = "architecture"
|
|
|
|
|
renderer = "portable_graph_html"
|
|
|
|
|
output = "architecture.html"
|
|
|
|
|
title = "Synthetic architecture"
|
|
|
|
|
query = "synthetic measurement"
|
|
|
|
|
initial_mode = "web"
|
|
|
|
|
depth = 1
|
|
|
|
|
max_nodes = {node_count}
|
|
|
|
|
max_edges = {maximum_edges}
|
|
|
|
|
max_work = {maximum_work}
|
|
|
|
|
families = ["guide"]
|
|
|
|
|
relations = ["depends_on"]
|
|
|
|
|
authorities = []
|
|
|
|
|
statuses = ["active"]
|
|
|
|
|
tags = []
|
|
|
|
|
include_logic = false
|
|
|
|
|
""",
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _manual_package(
|
|
|
|
|
snapshot: ProjectSnapshot,
|
|
|
|
|
) -> tuple[ManualRenderPlanV1, ProjectionPackageV1, str]:
|
|
|
|
|
descriptor = snapshot.descriptor
|
|
|
|
|
render = descriptor.render
|
|
|
|
|
if render is None:
|
|
|
|
|
raise RuntimeError("Milestone 3 fixture has no manual render configuration")
|
|
|
|
|
view = render.views[0]
|
|
|
|
|
renderer_version = GenericHtmlRenderer(incremental=False).renderer_version
|
|
|
|
|
plan = build_manual_render_plan(snapshot, view, changeset_hash=None)
|
|
|
|
|
package = build_manual_projection_package(
|
|
|
|
|
plan,
|
|
|
|
|
view.template_path.read_bytes(),
|
|
|
|
|
renderer_id=ManualHtmlRenderer.renderer_id,
|
|
|
|
|
renderer_version=renderer_version,
|
|
|
|
|
max_output_bytes=descriptor.limits.max_render_bytes,
|
|
|
|
|
)
|
|
|
|
|
return plan, package, renderer_version
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _graph_package(
|
|
|
|
|
snapshot: ProjectSnapshot,
|
|
|
|
|
node_count: int,
|
|
|
|
|
) -> tuple[GraphViewPlanV1, ProjectionPackageV1]:
|
|
|
|
|
descriptor = snapshot.descriptor
|
|
|
|
|
plan = build_graph_view_plan(
|
|
|
|
|
snapshot,
|
|
|
|
|
GraphViewRequestV1(
|
|
|
|
|
view_id="architecture",
|
|
|
|
|
title="Synthetic architecture",
|
|
|
|
|
query="synthetic measurement",
|
|
|
|
|
initial_mode="web",
|
|
|
|
|
depth=1,
|
|
|
|
|
max_nodes=node_count,
|
|
|
|
|
max_edges=node_count - 1,
|
|
|
|
|
max_work=max(100, node_count * 4),
|
|
|
|
|
families=("guide",),
|
|
|
|
|
relations=("depends_on",),
|
|
|
|
|
statuses=("active",),
|
|
|
|
|
),
|
|
|
|
|
False,
|
|
|
|
|
)
|
|
|
|
|
package = build_graph_projection_package(
|
|
|
|
|
plan,
|
|
|
|
|
renderer_id=PortableGraphHtmlRenderer.renderer_id,
|
|
|
|
|
renderer_version=PortableGraphHtmlRenderer.renderer_version,
|
|
|
|
|
max_output_bytes=descriptor.limits.max_render_bytes,
|
|
|
|
|
)
|
|
|
|
|
return plan, package
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _projection_sample(
|
|
|
|
|
plan: ManualRenderPlanV1 | GraphViewPlanV1,
|
|
|
|
|
package: ProjectionPackageV1,
|
|
|
|
|
result: ProjectionRenderResult,
|
|
|
|
|
) -> _ProjectionSample:
|
|
|
|
|
if len(result.artifacts) != 1:
|
|
|
|
|
raise RuntimeError("Projection benchmark expected exactly one artifact")
|
|
|
|
|
artifact = result.artifacts[0].content
|
|
|
|
|
peak = result.receipt.document.get("peak_memory_bytes")
|
|
|
|
|
if peak is not None and (type(peak) is not int or peak < 1):
|
|
|
|
|
raise RuntimeError("Detached worker did not report valid peak memory")
|
|
|
|
|
return _ProjectionSample(
|
|
|
|
|
plan_id=plan.plan_id,
|
|
|
|
|
package_id=package.package_id,
|
|
|
|
|
artifact=artifact,
|
|
|
|
|
receipt=result.receipt.as_dict(),
|
|
|
|
|
plan_bytes=_compact_size(plan.as_dict()),
|
|
|
|
|
package_bytes=_compact_size(package.as_dict()),
|
|
|
|
|
child_peak_memory_bytes=peak,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _projection_summary(sample: _ProjectionSample) -> dict[str, object]:
|
|
|
|
|
return {
|
|
|
|
|
"plan_id": sample.plan_id,
|
|
|
|
|
"package_id": sample.package_id,
|
|
|
|
|
"artifact_sha256": _sha256(sample.artifact),
|
|
|
|
|
"artifact_bytes": len(sample.artifact),
|
|
|
|
|
"plan_bytes": sample.plan_bytes,
|
|
|
|
|
"package_bytes": sample.package_bytes,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _measure(
|
|
|
|
|
operation: Callable[[], object],
|
|
|
|
|
*,
|
|
|
|
|
samples: int,
|
|
|
|
|
p95_limit_ms: float,
|
|
|
|
|
response_limit_bytes: int,
|
|
|
|
|
summary: Callable[[object], Mapping[str, object]],
|
|
|
|
|
response_size: Callable[[object], int],
|
|
|
|
|
warmups: int = 0,
|
|
|
|
|
) -> tuple[dict[str, object], object]:
|
|
|
|
|
for _ in range(warmups):
|
|
|
|
|
operation()
|
|
|
|
|
durations: list[float] = []
|
|
|
|
|
traced_peaks: list[int] = []
|
|
|
|
|
response_sizes: list[int] = []
|
|
|
|
|
stable_summary: Mapping[str, object] | None = None
|
|
|
|
|
last: object = None
|
|
|
|
|
for _ in range(samples):
|
|
|
|
|
gc.collect()
|
|
|
|
|
tracemalloc.start()
|
|
|
|
|
started = time.perf_counter_ns()
|
|
|
|
|
try:
|
|
|
|
|
value = operation()
|
|
|
|
|
elapsed_ms = (time.perf_counter_ns() - started) / 1_000_000
|
|
|
|
|
_, traced_peak = tracemalloc.get_traced_memory()
|
|
|
|
|
finally:
|
|
|
|
|
tracemalloc.stop()
|
|
|
|
|
current_summary = dict(summary(value))
|
|
|
|
|
if stable_summary is None:
|
|
|
|
|
stable_summary = current_summary
|
|
|
|
|
elif current_summary != stable_summary:
|
|
|
|
|
raise RuntimeError("Milestone 3 operation changed deterministic result across samples")
|
|
|
|
|
current_response_size = response_size(value)
|
|
|
|
|
if current_response_size > response_limit_bytes:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"Milestone 3 response exceeded its fixed benchmark boundary: "
|
|
|
|
|
f"{current_response_size} > {response_limit_bytes}"
|
|
|
|
|
)
|
|
|
|
|
if traced_peak > MAX_TRACED_PEAK_BYTES:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"Milestone 3 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(current_response_size)
|
|
|
|
|
last = value
|
|
|
|
|
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 3 operation p95 {p95:.3f} ms exceeds {p95_limit_ms:.3f} ms")
|
|
|
|
|
assert stable_summary 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": response_limit_bytes,
|
|
|
|
|
"maximum_traced_peak_bytes": max(traced_peaks),
|
|
|
|
|
"traced_peak_limit_bytes": MAX_TRACED_PEAK_BYTES,
|
|
|
|
|
"stable_result": dict(stable_summary),
|
|
|
|
|
},
|
|
|
|
|
last,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _projection_measurement(
|
|
|
|
|
operation: Callable[[], _ProjectionSample],
|
|
|
|
|
*,
|
|
|
|
|
samples: int,
|
|
|
|
|
p95_limit_ms: float,
|
|
|
|
|
) -> tuple[dict[str, object], _ProjectionSample]:
|
|
|
|
|
child_peaks: list[int] = []
|
|
|
|
|
|
|
|
|
|
def observed_operation() -> _ProjectionSample:
|
|
|
|
|
sample = operation()
|
|
|
|
|
if sample.child_peak_memory_bytes is not None:
|
|
|
|
|
if sample.child_peak_memory_bytes > MAX_CHILD_PEAK_BYTES:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"Detached worker peak memory "
|
|
|
|
|
f"{sample.child_peak_memory_bytes} exceeds "
|
|
|
|
|
f"{MAX_CHILD_PEAK_BYTES} bytes"
|
|
|
|
|
)
|
|
|
|
|
child_peaks.append(sample.child_peak_memory_bytes)
|
|
|
|
|
return sample
|
|
|
|
|
|
|
|
|
|
measurement, value = _measure(
|
|
|
|
|
observed_operation,
|
|
|
|
|
samples=samples,
|
|
|
|
|
p95_limit_ms=p95_limit_ms,
|
|
|
|
|
response_limit_bytes=MAX_RECEIPT_BYTES,
|
|
|
|
|
summary=lambda item: _projection_summary(cast(_ProjectionSample, item)),
|
|
|
|
|
response_size=lambda item: _compact_size(cast(_ProjectionSample, item).receipt),
|
|
|
|
|
)
|
|
|
|
|
sample = cast(_ProjectionSample, value)
|
|
|
|
|
if sample.plan_bytes > MAX_PLAN_BYTES or sample.package_bytes > MAX_PACKAGE_BYTES:
|
|
|
|
|
raise RuntimeError("Projection plan or package exceeded its protocol boundary")
|
|
|
|
|
if child_peaks:
|
|
|
|
|
measurement["maximum_child_peak_bytes"] = max(child_peaks)
|
|
|
|
|
measurement["child_peak_limit_bytes"] = MAX_CHILD_PEAK_BYTES
|
|
|
|
|
return measurement, sample
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _profiled_status(operation: Callable[[], Mapping[str, object]]) -> _StatusSample:
|
|
|
|
|
with request("benchmark.m3", enabled=True) as collector:
|
|
|
|
|
response = operation()
|
|
|
|
|
if collector is None:
|
|
|
|
|
raise RuntimeError("Milestone 3 telemetry collector was not created")
|
|
|
|
|
counters = collector.as_dict(outcome="ok")["counters"]
|
|
|
|
|
typed_counters = cast(Mapping[str, object], counters)
|
|
|
|
|
for counter in ZERO_WORK_COUNTERS:
|
|
|
|
|
if typed_counters[counter] != 0:
|
|
|
|
|
raise RuntimeError(f"Receipt-only status performed forbidden work: {counter}")
|
|
|
|
|
if typed_counters["source_generation_checks"] != 2:
|
|
|
|
|
raise RuntimeError("Receipt-only status did not perform its two race-safe source checks")
|
|
|
|
|
if response.get("status") != "ok" or response.get("state") != "current":
|
|
|
|
|
raise RuntimeError("Receipt-only status did not report a current publication")
|
|
|
|
|
return _StatusSample(response=response, counters=typed_counters)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _status_summary(value: object) -> Mapping[str, object]:
|
|
|
|
|
sample = cast(_StatusSample, value)
|
|
|
|
|
return {
|
|
|
|
|
"response": dict(sample.response),
|
|
|
|
|
"counters": dict(sample.counters),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fragment_summary(value: object) -> Mapping[str, object]:
|
|
|
|
|
sample = cast(_FragmentSweep, value)
|
|
|
|
|
return {
|
|
|
|
|
"fragment_count": sample.fragment_count,
|
|
|
|
|
"aggregate_content_bytes": sample.aggregate_content_bytes,
|
|
|
|
|
"ordered_record_hash": sample.ordered_record_hash,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fragment_sweep(records: Sequence[FragmentRecord]) -> _FragmentSweep:
|
|
|
|
|
return _FragmentSweep(
|
|
|
|
|
fragment_count=len(records),
|
|
|
|
|
aggregate_content_bytes=sum(record.byte_count for record in records),
|
|
|
|
|
ordered_record_hash=projection_hash([record.record_id for record in records]),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _prepared_summary(value: object) -> Mapping[str, object]:
|
|
|
|
|
prepared = cast(PreparedRender, value)
|
|
|
|
|
return {
|
|
|
|
|
"render_identity": prepared.render_identity,
|
|
|
|
|
"output_sha256": prepared.output_hash,
|
|
|
|
|
"output_bytes": len(prepared.output),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 13:00:35 -04:00
|
|
|
def _prepared_child_peak(prepared: PreparedRender) -> int:
|
|
|
|
|
receipt = prepared.projection_receipt
|
|
|
|
|
if not isinstance(receipt, Mapping):
|
|
|
|
|
raise RuntimeError("Production detached render did not return a projection receipt")
|
|
|
|
|
peak = receipt.get("peak_memory_bytes")
|
|
|
|
|
if type(peak) is not int or peak < 1:
|
|
|
|
|
raise RuntimeError("Production detached render did not report valid child peak memory")
|
|
|
|
|
if peak > MAX_CHILD_PEAK_BYTES:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
f"Production detached worker peak memory {peak} exceeds {MAX_CHILD_PEAK_BYTES} bytes"
|
|
|
|
|
)
|
|
|
|
|
return peak
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 12:38:25 -04:00
|
|
|
def _prepared_response_size(value: object) -> int:
|
|
|
|
|
prepared = cast(PreparedRender, value)
|
|
|
|
|
return _compact_size(prepared.projection_receipt)
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 13:00:35 -04:00
|
|
|
def _prepared_measurement(
|
|
|
|
|
operation: Callable[[], PreparedRender],
|
|
|
|
|
*,
|
|
|
|
|
samples: int,
|
|
|
|
|
p95_limit_ms: float,
|
|
|
|
|
) -> tuple[dict[str, object], PreparedRender]:
|
|
|
|
|
child_peaks: list[int] = []
|
|
|
|
|
|
|
|
|
|
def observed_operation() -> PreparedRender:
|
|
|
|
|
prepared = operation()
|
|
|
|
|
child_peaks.append(_prepared_child_peak(prepared))
|
|
|
|
|
return prepared
|
|
|
|
|
|
|
|
|
|
measurement, value = _measure(
|
|
|
|
|
observed_operation,
|
|
|
|
|
samples=samples,
|
|
|
|
|
p95_limit_ms=p95_limit_ms,
|
|
|
|
|
response_limit_bytes=MAX_RECEIPT_BYTES,
|
|
|
|
|
summary=_prepared_summary,
|
|
|
|
|
response_size=_prepared_response_size,
|
|
|
|
|
)
|
|
|
|
|
measurement["maximum_child_peak_bytes"] = max(child_peaks)
|
|
|
|
|
measurement["child_peak_limit_bytes"] = MAX_CHILD_PEAK_BYTES
|
|
|
|
|
return measurement, cast(PreparedRender, value)
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 12:38:25 -04:00
|
|
|
def _benchmark(root: Path, node_count: int, samples: int) -> dict[str, object]:
|
|
|
|
|
project = Project.open(root)
|
|
|
|
|
snapshot = project.load()
|
|
|
|
|
if len(snapshot.nodes) != node_count or len(snapshot.edges) != node_count - 1:
|
|
|
|
|
raise RuntimeError("Milestone 3 fixture does not have full synthetic coverage")
|
|
|
|
|
|
|
|
|
|
operations: dict[str, object] = {}
|
|
|
|
|
|
|
|
|
|
def manual_full() -> _ProjectionSample:
|
|
|
|
|
plan, package, renderer_version = _manual_package(snapshot)
|
|
|
|
|
result = ManualHtmlRenderer(renderer_version).render(package)
|
|
|
|
|
return _projection_sample(plan, package, result)
|
|
|
|
|
|
|
|
|
|
operations["manual_full_render"], manual_result = _projection_measurement(
|
|
|
|
|
manual_full,
|
|
|
|
|
samples=samples,
|
|
|
|
|
p95_limit_ms=15_000,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def graph_full() -> _ProjectionSample:
|
|
|
|
|
plan, package = _graph_package(snapshot, node_count)
|
|
|
|
|
result = PortableGraphHtmlRenderer().render(package)
|
|
|
|
|
return _projection_sample(plan, package, result)
|
|
|
|
|
|
|
|
|
|
operations["portable_graph_full_render"], graph_result = _projection_measurement(
|
|
|
|
|
graph_full,
|
|
|
|
|
samples=samples,
|
|
|
|
|
p95_limit_ms=10_000,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
manual_plan, manual_package, manual_renderer_version = _manual_package(snapshot)
|
|
|
|
|
graph_plan, graph_package = _graph_package(snapshot, node_count)
|
|
|
|
|
graph_diagnostics = cast(Mapping[str, object], graph_plan.document["diagnostics"])
|
|
|
|
|
manual_pages = cast(list[dict[str, object]], manual_plan.document["pages"])
|
|
|
|
|
if (
|
|
|
|
|
len(manual_pages) != node_count
|
|
|
|
|
or graph_diagnostics["returned_nodes"] != node_count
|
|
|
|
|
or graph_diagnostics["returned_edges"] != node_count - 1
|
|
|
|
|
):
|
|
|
|
|
raise RuntimeError("Projection plans did not retain every synthetic node and edge")
|
|
|
|
|
|
|
|
|
|
manual_worker_measurement, manual_worker = _projection_measurement(
|
|
|
|
|
lambda: _projection_sample(
|
|
|
|
|
manual_plan,
|
|
|
|
|
manual_package,
|
|
|
|
|
render_projection_in_worker(manual_package),
|
|
|
|
|
),
|
|
|
|
|
samples=samples,
|
|
|
|
|
p95_limit_ms=20_000,
|
|
|
|
|
)
|
|
|
|
|
operations["manual_detached_worker"] = manual_worker_measurement
|
|
|
|
|
manual_worker_peak = cast(
|
|
|
|
|
int,
|
|
|
|
|
manual_worker_measurement["maximum_child_peak_bytes"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
graph_worker_measurement, graph_worker = _projection_measurement(
|
|
|
|
|
lambda: _projection_sample(
|
|
|
|
|
graph_plan,
|
|
|
|
|
graph_package,
|
|
|
|
|
render_projection_in_worker(graph_package),
|
|
|
|
|
),
|
|
|
|
|
samples=samples,
|
|
|
|
|
p95_limit_ms=20_000,
|
|
|
|
|
)
|
|
|
|
|
operations["portable_graph_detached_worker"] = graph_worker_measurement
|
|
|
|
|
graph_worker_peak = cast(
|
|
|
|
|
int,
|
|
|
|
|
graph_worker_measurement["maximum_child_peak_bytes"],
|
|
|
|
|
)
|
|
|
|
|
if (
|
|
|
|
|
manual_worker.artifact != manual_result.artifact
|
|
|
|
|
or graph_worker.artifact != graph_result.artifact
|
|
|
|
|
):
|
|
|
|
|
raise RuntimeError("Detached worker output is not byte-equivalent to in-process output")
|
|
|
|
|
|
|
|
|
|
manual_renderer = ManualHtmlRenderer(manual_renderer_version)
|
|
|
|
|
fragment_records = [
|
|
|
|
|
FragmentRecord.create(
|
|
|
|
|
FragmentKey.create(
|
|
|
|
|
projection_kind="manual",
|
|
|
|
|
renderer_id=ManualHtmlRenderer.renderer_id,
|
|
|
|
|
renderer_version=manual_renderer_version,
|
|
|
|
|
component_version=GenericHtmlRenderer.page_component_version,
|
|
|
|
|
semantic_input_hash=fragment_semantic_hash(page),
|
|
|
|
|
),
|
|
|
|
|
manual_renderer.render_page_fragment(page).encode("utf-8"),
|
|
|
|
|
)
|
|
|
|
|
for page in manual_pages
|
|
|
|
|
]
|
|
|
|
|
fragment_cache = ProjectionFragmentCache(
|
|
|
|
|
root,
|
|
|
|
|
root / ".docforge" / "milestone3-fragment-cache",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def fragment_misses() -> _FragmentSweep:
|
|
|
|
|
if any(fragment_cache.get(record.key) is not None for record in fragment_records):
|
|
|
|
|
raise RuntimeError("Cold fragment-cache lookup unexpectedly hit")
|
|
|
|
|
return _fragment_sweep(fragment_records)
|
|
|
|
|
|
|
|
|
|
operations["fragment_cache_miss_sweep"], _ = _measure(
|
|
|
|
|
fragment_misses,
|
|
|
|
|
samples=samples,
|
|
|
|
|
p95_limit_ms=5_000,
|
|
|
|
|
response_limit_bytes=32_768,
|
|
|
|
|
summary=_fragment_summary,
|
|
|
|
|
response_size=lambda item: _compact_size(_fragment_summary(item)),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def fragment_puts() -> _FragmentSweep:
|
|
|
|
|
published: list[FragmentRecord] = []
|
|
|
|
|
for record in fragment_records:
|
|
|
|
|
stored = fragment_cache.put(record.key, record.content)
|
|
|
|
|
if stored != record:
|
|
|
|
|
raise RuntimeError("Fragment cache did not publish an exact record")
|
|
|
|
|
assert stored is not None
|
|
|
|
|
published.append(stored)
|
|
|
|
|
return _fragment_sweep(published)
|
|
|
|
|
|
|
|
|
|
operations["fragment_cache_put_sweep"], _ = _measure(
|
|
|
|
|
fragment_puts,
|
|
|
|
|
samples=1,
|
|
|
|
|
p95_limit_ms=10_000,
|
|
|
|
|
response_limit_bytes=32_768,
|
|
|
|
|
summary=_fragment_summary,
|
|
|
|
|
response_size=lambda item: _compact_size(_fragment_summary(item)),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def fragment_hits() -> _FragmentSweep:
|
|
|
|
|
loaded: list[FragmentRecord] = []
|
|
|
|
|
for expected in fragment_records:
|
|
|
|
|
record = fragment_cache.get(expected.key)
|
|
|
|
|
if record != expected:
|
|
|
|
|
raise RuntimeError("Fragment cache hit was not byte-exact")
|
|
|
|
|
assert record is not None
|
|
|
|
|
loaded.append(record)
|
|
|
|
|
return _fragment_sweep(loaded)
|
|
|
|
|
|
|
|
|
|
operations["fragment_cache_hit_sweep"], _ = _measure(
|
|
|
|
|
fragment_hits,
|
|
|
|
|
samples=samples,
|
|
|
|
|
p95_limit_ms=5_000,
|
|
|
|
|
response_limit_bytes=32_768,
|
|
|
|
|
summary=_fragment_summary,
|
|
|
|
|
response_size=lambda item: _compact_size(_fragment_summary(item)),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
render_config = snapshot.descriptor.render
|
|
|
|
|
assert render_config is not None
|
|
|
|
|
incremental_package = build_manual_projection_package(
|
|
|
|
|
manual_plan,
|
|
|
|
|
render_config.views[0].template_path.read_bytes(),
|
|
|
|
|
renderer_id=ManualHtmlRenderer.renderer_id,
|
|
|
|
|
renderer_version=manual_renderer_version,
|
|
|
|
|
max_output_bytes=snapshot.descriptor.limits.max_render_bytes,
|
|
|
|
|
fragment_records=[record.as_dict() for record in fragment_records],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def fragment_equivalence() -> _ProjectionSample:
|
|
|
|
|
sample = _projection_sample(
|
|
|
|
|
manual_plan,
|
|
|
|
|
incremental_package,
|
|
|
|
|
ManualHtmlRenderer(manual_renderer_version).render(incremental_package),
|
|
|
|
|
)
|
|
|
|
|
if sample.artifact != manual_result.artifact:
|
|
|
|
|
raise RuntimeError("Fragment-assisted manual output is not byte-equivalent")
|
|
|
|
|
return sample
|
|
|
|
|
|
|
|
|
|
operations["fragment_assisted_equivalence"], fragment_result = _projection_measurement(
|
|
|
|
|
fragment_equivalence,
|
|
|
|
|
samples=samples,
|
|
|
|
|
p95_limit_ms=15_000,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
render_config = snapshot.descriptor.render
|
|
|
|
|
assert render_config is not None
|
|
|
|
|
manual_view = render_config.views[0]
|
|
|
|
|
template_bytes = manual_view.template_path.read_bytes()
|
|
|
|
|
production_renderer = GenericHtmlRenderer()
|
2026-07-29 13:00:35 -04:00
|
|
|
operations["manual_incremental_cold"], cold_prepared = _prepared_measurement(
|
2026-07-29 12:38:25 -04:00
|
|
|
lambda: production_renderer.prepare(
|
|
|
|
|
snapshot,
|
|
|
|
|
manual_view,
|
|
|
|
|
template_bytes,
|
|
|
|
|
changeset_hash=None,
|
|
|
|
|
),
|
|
|
|
|
samples=1,
|
|
|
|
|
p95_limit_ms=20_000,
|
|
|
|
|
)
|
2026-07-29 13:00:35 -04:00
|
|
|
operations["manual_incremental_warm"], warm_prepared = _prepared_measurement(
|
2026-07-29 12:38:25 -04:00
|
|
|
lambda: production_renderer.prepare(
|
|
|
|
|
snapshot,
|
|
|
|
|
manual_view,
|
|
|
|
|
template_bytes,
|
|
|
|
|
changeset_hash=None,
|
|
|
|
|
),
|
|
|
|
|
samples=samples,
|
|
|
|
|
p95_limit_ms=20_000,
|
|
|
|
|
)
|
2026-07-29 13:00:35 -04:00
|
|
|
operations["manual_forced_full"], forced_prepared = _prepared_measurement(
|
2026-07-29 12:38:25 -04:00
|
|
|
lambda: GenericHtmlRenderer(incremental=False).prepare(
|
|
|
|
|
snapshot,
|
|
|
|
|
manual_view,
|
|
|
|
|
template_bytes,
|
|
|
|
|
changeset_hash=None,
|
|
|
|
|
),
|
|
|
|
|
samples=samples,
|
|
|
|
|
p95_limit_ms=20_000,
|
|
|
|
|
)
|
2026-07-29 13:00:35 -04:00
|
|
|
production_values = (cold_prepared, warm_prepared, forced_prepared)
|
2026-07-29 12:38:25 -04:00
|
|
|
if len({value.output for value in production_values}) != 1:
|
|
|
|
|
raise RuntimeError("Production cold, warm, and forced-full manual output differs")
|
|
|
|
|
|
|
|
|
|
changed_node = replace(
|
|
|
|
|
snapshot.nodes[0],
|
|
|
|
|
content=snapshot.nodes[0].content + "\nChanged projection content.\n",
|
|
|
|
|
content_hash=_sha256(
|
|
|
|
|
(snapshot.nodes[0].content + "\nChanged projection content.\n").encode()
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
added_node = replace(
|
|
|
|
|
snapshot.nodes[-1],
|
|
|
|
|
node_id="guide.synthetic-added",
|
|
|
|
|
title="Synthetic added",
|
|
|
|
|
source_path="docs/content/synthetic-added.md",
|
|
|
|
|
content_hash=_sha256(b"synthetic-added"),
|
|
|
|
|
)
|
|
|
|
|
variants = {
|
|
|
|
|
"change": replace(
|
|
|
|
|
snapshot,
|
|
|
|
|
nodes=(changed_node, *snapshot.nodes[1:]),
|
|
|
|
|
source_hash=_sha256(b"manual-change-variant"),
|
|
|
|
|
),
|
|
|
|
|
"add": replace(
|
|
|
|
|
snapshot,
|
|
|
|
|
nodes=(*snapshot.nodes, added_node),
|
|
|
|
|
source_hash=_sha256(b"manual-add-variant"),
|
|
|
|
|
),
|
|
|
|
|
"delete": replace(
|
|
|
|
|
snapshot,
|
|
|
|
|
nodes=snapshot.nodes[:-1],
|
|
|
|
|
edges=tuple(
|
|
|
|
|
edge
|
|
|
|
|
for edge in snapshot.edges
|
|
|
|
|
if edge.source_id != snapshot.nodes[-1].node_id
|
|
|
|
|
and edge.target_id != snapshot.nodes[-1].node_id
|
|
|
|
|
),
|
|
|
|
|
source_hash=_sha256(b"manual-delete-variant"),
|
|
|
|
|
),
|
|
|
|
|
"reorder": replace(
|
|
|
|
|
snapshot,
|
|
|
|
|
nodes=tuple(reversed(snapshot.nodes)),
|
|
|
|
|
source_hash=_sha256(b"manual-reorder-variant"),
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
variant_equivalence: dict[str, bool] = {}
|
2026-07-29 13:00:35 -04:00
|
|
|
variant_child_peaks: list[int] = []
|
2026-07-29 12:38:25 -04:00
|
|
|
for name, variant in variants.items():
|
|
|
|
|
incremental = GenericHtmlRenderer().prepare(
|
|
|
|
|
variant,
|
|
|
|
|
manual_view,
|
|
|
|
|
template_bytes,
|
|
|
|
|
changeset_hash=None,
|
|
|
|
|
)
|
|
|
|
|
full = GenericHtmlRenderer(incremental=False).prepare(
|
|
|
|
|
variant,
|
|
|
|
|
manual_view,
|
|
|
|
|
template_bytes,
|
|
|
|
|
changeset_hash=None,
|
|
|
|
|
)
|
2026-07-29 13:00:35 -04:00
|
|
|
variant_child_peaks.extend(
|
|
|
|
|
(
|
|
|
|
|
_prepared_child_peak(incremental),
|
|
|
|
|
_prepared_child_peak(full),
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-07-29 12:38:25 -04:00
|
|
|
variant_equivalence[name] = incremental.output == full.output
|
|
|
|
|
if not all(variant_equivalence.values()):
|
|
|
|
|
raise RuntimeError("Production incremental mutation output differs from forced full")
|
|
|
|
|
|
|
|
|
|
manual_service = RenderService(project)
|
|
|
|
|
graph_service = GraphRenderService(project)
|
|
|
|
|
manual_service.render("manual")
|
|
|
|
|
graph_service.render("architecture")
|
|
|
|
|
operations["manual_status_no_work"], manual_status = _measure(
|
|
|
|
|
lambda: _profiled_status(lambda: manual_service.status("manual")),
|
|
|
|
|
samples=samples,
|
|
|
|
|
p95_limit_ms=500,
|
|
|
|
|
response_limit_bytes=MAX_STATUS_RESPONSE_BYTES,
|
|
|
|
|
summary=_status_summary,
|
|
|
|
|
response_size=lambda item: _compact_size(cast(_StatusSample, item).response),
|
|
|
|
|
)
|
|
|
|
|
operations["portable_graph_status_no_work"], graph_status = _measure(
|
|
|
|
|
lambda: _profiled_status(lambda: graph_service.status("architecture")),
|
|
|
|
|
samples=samples,
|
|
|
|
|
p95_limit_ms=500,
|
|
|
|
|
response_limit_bytes=MAX_STATUS_RESPONSE_BYTES,
|
|
|
|
|
summary=_status_summary,
|
|
|
|
|
response_size=lambda item: _compact_size(cast(_StatusSample, item).response),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"fixture": {
|
|
|
|
|
"kind": "synthetic_generic_projection",
|
|
|
|
|
"node_count": node_count,
|
|
|
|
|
"edge_count": node_count - 1,
|
|
|
|
|
"manual_page_count": len(manual_pages),
|
|
|
|
|
"portable_graph_node_count": graph_diagnostics["returned_nodes"],
|
|
|
|
|
"portable_graph_edge_count": graph_diagnostics["returned_edges"],
|
|
|
|
|
"full_coverage": (
|
|
|
|
|
len(manual_pages) == node_count
|
|
|
|
|
and graph_diagnostics["returned_nodes"] == node_count
|
|
|
|
|
and graph_diagnostics["returned_edges"] == node_count - 1
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
"operations": operations,
|
|
|
|
|
"equivalence": {
|
|
|
|
|
"manual_in_process_vs_detached": manual_worker.artifact == manual_result.artifact,
|
|
|
|
|
"portable_graph_in_process_vs_detached": graph_worker.artifact == graph_result.artifact,
|
|
|
|
|
"manual_full_vs_fragment_assisted": fragment_result.artifact == manual_result.artifact,
|
|
|
|
|
"manual_production_cold_warm_full": len({value.output for value in production_values})
|
|
|
|
|
== 1,
|
|
|
|
|
"manual_production_variants": variant_equivalence,
|
|
|
|
|
},
|
|
|
|
|
"sizes": {
|
|
|
|
|
"manual": {
|
|
|
|
|
**_projection_summary(manual_result),
|
|
|
|
|
"receipt_bytes": _compact_size(manual_result.receipt),
|
|
|
|
|
},
|
|
|
|
|
"portable_graph": {
|
|
|
|
|
**_projection_summary(graph_result),
|
|
|
|
|
"receipt_bytes": _compact_size(graph_result.receipt),
|
|
|
|
|
},
|
|
|
|
|
"fragment_assisted_manual": {
|
|
|
|
|
**_projection_summary(fragment_result),
|
|
|
|
|
"receipt_bytes": _compact_size(fragment_result.receipt),
|
|
|
|
|
"fragment_count": len(fragment_records),
|
|
|
|
|
"aggregate_fragment_content_bytes": sum(
|
|
|
|
|
record.byte_count for record in fragment_records
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
"manual_status_response_bytes": _compact_size(
|
|
|
|
|
cast(_StatusSample, manual_status).response
|
|
|
|
|
),
|
|
|
|
|
"portable_graph_status_response_bytes": _compact_size(
|
|
|
|
|
cast(_StatusSample, graph_status).response
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
"memory": {
|
|
|
|
|
"process_peak_rss_kib": int(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss),
|
|
|
|
|
"manual_worker_peak_bytes": manual_worker_peak,
|
2026-07-29 13:00:35 -04:00
|
|
|
"manual_production_worker_peak_bytes": max(
|
|
|
|
|
cast(
|
|
|
|
|
int,
|
|
|
|
|
operations["manual_incremental_cold"]["maximum_child_peak_bytes"],
|
|
|
|
|
),
|
|
|
|
|
cast(
|
|
|
|
|
int,
|
|
|
|
|
operations["manual_incremental_warm"]["maximum_child_peak_bytes"],
|
|
|
|
|
),
|
|
|
|
|
cast(
|
|
|
|
|
int,
|
|
|
|
|
operations["manual_forced_full"]["maximum_child_peak_bytes"],
|
|
|
|
|
),
|
|
|
|
|
*variant_child_peaks,
|
|
|
|
|
),
|
2026-07-29 12:38:25 -04:00
|
|
|
"portable_graph_worker_peak_bytes": graph_worker_peak,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
arguments = _parser().parse_args()
|
|
|
|
|
default_nodes = FULL_NODE_COUNT if arguments.mode == "full" else SMOKE_NODE_COUNT
|
|
|
|
|
default_samples = DEFAULT_FULL_SAMPLES if arguments.mode == "full" else DEFAULT_SMOKE_SAMPLES
|
|
|
|
|
node_count = default_nodes if arguments.nodes is None else arguments.nodes
|
|
|
|
|
samples = default_samples if arguments.samples is None else arguments.samples
|
|
|
|
|
if not 2 <= node_count <= FULL_NODE_COUNT:
|
|
|
|
|
raise SystemExit("--nodes must be between 2 and 1000")
|
|
|
|
|
if arguments.mode == "full" and node_count != FULL_NODE_COUNT:
|
|
|
|
|
raise SystemExit("--mode full requires exactly 1000 nodes")
|
|
|
|
|
if samples < 1:
|
|
|
|
|
raise SystemExit("--samples must be positive")
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix="docforge-milestone3-") as directory:
|
|
|
|
|
benchmark_root = Path(directory).resolve()
|
|
|
|
|
_prepare_fixture(benchmark_root, node_count)
|
|
|
|
|
measurement = _benchmark(benchmark_root, node_count, samples)
|
|
|
|
|
|
|
|
|
|
status = _git(["status", "--porcelain"])
|
|
|
|
|
report: dict[str, object] = {
|
|
|
|
|
"schema_version": 1,
|
|
|
|
|
"benchmark": "docforge2_milestone3",
|
|
|
|
|
"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",
|
|
|
|
|
"detached_peak_memory": "worker receipt resource peak RSS",
|
|
|
|
|
"process_peak_memory": "resource.getrusage(RUSAGE_SELF).ru_maxrss",
|
|
|
|
|
"response_size": "UTF-8 bytes of canonical compact sorted JSON",
|
|
|
|
|
"samples": samples,
|
|
|
|
|
"full_mode_node_requirement": FULL_NODE_COUNT,
|
|
|
|
|
"determinism": (
|
|
|
|
|
"stable semantic summaries must match across samples; report JSON uses sorted keys"
|
|
|
|
|
),
|
|
|
|
|
"maximum_worker_artifact_bytes": MAX_WORKER_ARTIFACT_BYTES,
|
|
|
|
|
},
|
|
|
|
|
**measurement,
|
|
|
|
|
}
|
|
|
|
|
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())
|