- Export commandlet writes the live Core adapter's world_assembly + resolved layout pair; the headless owner path imports that pair (manifest/layout equal, layout_chunk_width_q validated). owner_world.json re-exported from the owner's staged assets on the Linux host. - Adapter SW route: Core clearance/radius floors, 0.5 m support search, outward-facing tie-break; passage mouth/mid radius floors match Core (36/24 owner voxels). - FI in Unreal now uses the legacy (compiled analytic) generator unless a plan is imported: the auto native-FI plan failed the passage audit on the owner plan (1843 blocked samples, 945 open-air violations). Owner decides whether native FI returns. - In-engine spawn traces per strate (3 each): SW GenerateTile 42.7 worker-s vs 20.1 headless for equal tile counts; LOD0 p95 45-169 ms in UE. - Fast tier green; all three passages pass on the shared plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
125 lines
6.1 KiB
Python
125 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Summarize L70 Unreal spawn traces beside exact-plan headless arrivals."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import statistics
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[4]
|
|
SAVED = ROOT / "Saved/Linux/l70"
|
|
TRACES = SAVED / "traces"
|
|
HEADLESS = SAVED / "headless-arrivals"
|
|
OUT = SAVED / "arrival-metrics.json"
|
|
|
|
|
|
def parse_trace(strate: int, trial: int) -> dict:
|
|
path = TRACES / f"strate{strate}-trial{trial}.trace.jsonl"
|
|
events = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines()
|
|
if line.strip()]
|
|
spawn = next(row for row in events if row.get("type") == "trace_spawn_location")
|
|
ready_gate = next(row for row in events if row.get("type") == "walkable_gate"
|
|
and row.get("ready") == 1)
|
|
ring = next(row for row in events if row.get("type") == "lod0_ring_ready")
|
|
full = next(row for row in events if row.get("type") == "stream_trace_milestone"
|
|
and row.get("name") == "initial_set_ready")
|
|
summary = next(row for row in events if row.get("type") == "summary")
|
|
tiles = [row for row in events if row.get("type") == "tile"]
|
|
lod0 = summary["tile_percentiles"]["lod0"]["worker_task_s"]
|
|
return {
|
|
"label": f"strate{strate}-trial{trial}",
|
|
"strate": strate,
|
|
"trial": trial,
|
|
"walkable_ring_s": ready_gate["t_s"] - spawn["t_s"],
|
|
"walkable_ready_tiles": ready_gate.get("ready_tiles"),
|
|
"walkable_required_tiles": ready_gate.get("required_tiles"),
|
|
"lod0_ring_s": ring["t_s"] - spawn["t_s"],
|
|
"lod0_tiles": ring.get("lod0_tiles"),
|
|
"initial_set_s": full["t_s"] - spawn["t_s"],
|
|
"initial_set_tiles": full.get("loaded"),
|
|
"worker_seconds": sum(row.get("worker_task_s", 0.0) for row in tiles),
|
|
"generate_tile_seconds": sum(row.get("core_generate_s", 0.0) for row in tiles),
|
|
"mesh_copy_seconds": sum(row.get("mesh_copy_s", 0.0) for row in tiles),
|
|
"rmc_seconds": sum(row.get("rmc_stream_build_s", 0.0) for row in tiles),
|
|
"lod0_worker_p50_s": lod0["p50"],
|
|
"lod0_worker_p95_s": lod0["p95"],
|
|
"tile_events": len(tiles),
|
|
"summary_tiles": summary["tile_percentiles"]["all"]["count"],
|
|
}
|
|
|
|
|
|
def parse_headless(strate: int) -> dict:
|
|
path = HEADLESS / f"strate{strate}.log"
|
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
milestone = re.search(r"\[PipelineMilestone\] first_tile_ready=([0-9.]+)s", text)
|
|
stream = re.search(r"\[StreamPipeline\] ([^\n]+)", text)
|
|
if not milestone or not stream:
|
|
raise ValueError(f"missing pipeline timings in {path}")
|
|
fields = dict(re.findall(r"([A-Za-z0-9_]+)=([^ ]+)", stream.group(1)))
|
|
worker_rows = re.findall(
|
|
r"\[WorldAssemblerLODWorker\] lod=(\d+) tiles=(\d+) worker_sum_s=([0-9.]+)"
|
|
r".*?mc_exclusive_ms=([0-9.]+).*?mesh_assembly_ms=([0-9.]+)", text)
|
|
return {
|
|
"strate": strate,
|
|
"first_tile_s": float(milestone.group(1)),
|
|
"neighborhood_ready_s": float(fields["neighborhood_ready"].rstrip("s")),
|
|
"full_detail_ready_s": float(fields["full_detail_ready"].rstrip("s")),
|
|
"whole_view_ready_s": float(fields["whole_view_ready"].rstrip("s")),
|
|
"neighborhood_tiles": fields["neighborhood_tiles"],
|
|
"full_detail_tiles": fields["full_detail_tiles"],
|
|
"whole_tiles": fields["whole_tiles"],
|
|
"worker_seconds": sum(float(row[2]) for row in worker_rows),
|
|
"worker_lods": len(worker_rows),
|
|
"mc_exclusive_seconds": sum(float(row[3]) for row in worker_rows) / 1000.0,
|
|
"mesh_assembly_seconds": sum(float(row[4]) for row in worker_rows) / 1000.0,
|
|
}
|
|
|
|
|
|
def median(rows: list[dict], key: str) -> float:
|
|
return statistics.median(float(row[key]) for row in rows)
|
|
|
|
|
|
def main() -> int:
|
|
results = []
|
|
for strate in range(4):
|
|
trials = [parse_trace(strate, trial) for trial in range(1, 4)]
|
|
headless = parse_headless(strate)
|
|
engine = {key: median(trials, key) for key in (
|
|
"walkable_ring_s", "lod0_ring_s", "initial_set_s", "worker_seconds",
|
|
"generate_tile_seconds", "mesh_copy_seconds", "rmc_seconds",
|
|
"lod0_worker_p50_s", "lod0_worker_p95_s")}
|
|
engine.update({"walkable_ready_tiles": trials[0]["walkable_ready_tiles"],
|
|
"walkable_required_tiles": trials[0]["walkable_required_tiles"],
|
|
"lod0_tiles": trials[0]["lod0_tiles"],
|
|
"initial_set_tiles": trials[0]["initial_set_tiles"],
|
|
"trace_tile_events": trials[0]["tile_events"],
|
|
"trace_summary_tiles": trials[0]["summary_tiles"]})
|
|
row = {"strate": strate, "engine_median": engine, "engine_trials": trials,
|
|
"headless": headless,
|
|
"initial_set_time_gap_percent":
|
|
100.0 * (engine["initial_set_s"] / headless["whole_view_ready_s"] - 1.0),
|
|
"worker_sum_gap_percent":
|
|
100.0 * (engine["worker_seconds"] / headless["worker_seconds"] - 1.0)}
|
|
results.append(row)
|
|
print(
|
|
f"strate={strate} walk={engine['walkable_ring_s']:.3f}s "
|
|
f"lod0={engine['lod0_ring_s']:.3f}s/{engine['lod0_tiles']} tiles "
|
|
f"full=UE {engine['initial_set_s']:.3f}s/{engine['initial_set_tiles']} "
|
|
f"vs headless {headless['whole_view_ready_s']:.3f}s/{headless['whole_tiles']} "
|
|
f"workers=UE {engine['worker_seconds']:.3f}s/{engine['trace_tile_events']} "
|
|
f"vs headless {headless['worker_seconds']:.3f}s "
|
|
f"LOD0 p50/p95={engine['lod0_worker_p50_s']:.6f}/{engine['lod0_worker_p95_s']:.6f}s "
|
|
f"GenerateTile/mesh-copy/RMC={engine['generate_tile_seconds']:.3f}/"
|
|
f"{engine['mesh_copy_seconds']:.3f}/{engine['rmc_seconds']:.3f}s "
|
|
f"gaps={row['initial_set_time_gap_percent']:+.1f}% time,"
|
|
f"{row['worker_sum_gap_percent']:+.1f}% worker")
|
|
OUT.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8")
|
|
print(f"[L70ArrivalMetrics] wrote {OUT}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|