98% of idle-with-holes windows wait on the expanded critical set; region compile runs 0.26 ms but its gate stays pending 18 ms p50. Broad early admission: near +15.4 pt but p99 13.4 ms (over the VR 11 ms frame); near-only: +9.9 pt but far holes +1.5 pt. Trace instrumentation kept as a patch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
299 lines
16 KiB
Python
299 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""Archived five-pair harness for transient L122 fast-travel admission candidates.
|
|
|
|
The candidate CVar branches were removed after neither design met the quality gate.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import math
|
|
import statistics
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[4]
|
|
HOST = Path("/home/jahni/VFHost")
|
|
EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd")
|
|
SPAWN_TN_M = (19.166015625, -14.2001953125, -753.3740234375)
|
|
SPAWN_FI_M = (25.8369140625, 3.19921875, -737.0)
|
|
DESIGN = "relax_expanded_critical_gate"
|
|
NEAR_ONLY_DESIGN = "near_only_expanded_critical_gate"
|
|
|
|
|
|
def percentile(values: list[float], fraction: float) -> float | None:
|
|
if not values:
|
|
return None
|
|
ordered = sorted(values)
|
|
return ordered[max(0, min(len(ordered) - 1, math.ceil(len(ordered) * fraction) - 1))]
|
|
|
|
|
|
def read_events(path: Path) -> list[dict]:
|
|
rows = []
|
|
if path.is_file():
|
|
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
if not line.startswith("{"):
|
|
continue
|
|
try:
|
|
rows.append(json.loads(line))
|
|
except json.JSONDecodeError:
|
|
continue
|
|
return rows
|
|
|
|
|
|
def run(scenario: str, condition: str, repetition: int, design: str) -> dict:
|
|
candidate = condition == design
|
|
wait_for_critical = 0 if candidate else 1
|
|
near_only_while_critical_pending = int(candidate and design == NEAR_ONLY_DESIGN)
|
|
if scenario == "movement":
|
|
speed_cm_s, start_s, duration_s, exit_s, slot = 3000.0, 6.0, 12.0, 23.0, 0
|
|
spawn = SPAWN_TN_M
|
|
else:
|
|
speed_cm_s, start_s, duration_s, exit_s, slot = 140.0, 20.0, 12.0, 37.0, 1
|
|
spawn = SPAWN_FI_M
|
|
|
|
output_name = scenario + ("-near-only" if design == NEAR_ONLY_DESIGN else "")
|
|
root_out = ROOT / f"Saved/Linux/l122/{output_name}"
|
|
out = root_out / condition / "unreal"
|
|
host_out = HOST / "Plugins/VoxelForge/Saved/Linux/l122" / output_name / condition
|
|
host_saved = HOST / "Saved/Linux/l122"
|
|
label = f"{scenario}-r{repetition}"
|
|
trace = out / f"{label}.trace.jsonl"
|
|
abslog = out / f"{label}.log"
|
|
stdout = out / f"{label}.stdout.log"
|
|
export = host_out / "output" / label
|
|
userdir = host_saved / "user" / scenario / condition / label
|
|
zendata = host_saved / "zen" / scenario / condition / label
|
|
ddc = host_saved / "DDC"
|
|
for directory in (out, export, userdir, zendata, ddc):
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
trace.unlink(missing_ok=True)
|
|
spawn_cm = ",".join(f"{value * 100.0:.3f}" for value in spawn)
|
|
command = [
|
|
str(EDITOR), str(HOST / "VFHost.uproject"), "-nullrhi", "-unattended",
|
|
"-nop4", "-nosplash", "-NoSound", "-corelimit=12", "-NoZenAutoLaunch",
|
|
"-notraceserver", "-nocrashreports", "-DisablePlugins=AndroidFileServer",
|
|
"-ddc=NoZenLocalFallback", f"-userdir={userdir}", f"-ZenDataPath={zendata}",
|
|
f"-LocalDataCachePath={ddc}", "-run=VoxelForgeExplore", "-seed=0",
|
|
"-archetype=TunnelNetwork", f"-slot={slot}", "-modes=export", "-gameconfig=1",
|
|
"-coreworldtest=1", "-ownerfourstrateplan=1", "-corevoxelsizecm=25",
|
|
"-coreislandtest=0", "-corefallbacktest=0", "-coreofftest=0", "-coreplantest=0",
|
|
"-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings", "-corehybridmesher=0",
|
|
f"-out={export}", "-voxel.StreamingCoarseFirst=1",
|
|
"-voxel.StreamingDynamicNearReserve=1",
|
|
"-voxel.StreamingLegacyPendingResultCap=0",
|
|
"-voxel.CoreRegionConcurrentAdmission=1", "-voxel.StreamingResultBacklogCap=256",
|
|
"-voxel.StreamingReadyQueueDepth=8", "-voxel.StreamingBackgroundWorkerCap=1",
|
|
"-voxel.CoreColumnCacheDirectMapped=0", "-voxel.StreamingFarApplySupplement=1",
|
|
f"-voxel.StreamingFastTravelWaitForAllCriticalTiles={wait_for_critical}",
|
|
f"-voxel.StreamingFastTravelNearOnlyWhileCriticalPending={near_only_while_critical_pending}",
|
|
"-voxel.CoreMCGradientMemoization=1", "-voxel.StreamingCollisionLookahead=1",
|
|
"-voxel.TestMove=1", f"-voxel.TestMoveSpeedCmPerSecond={speed_cm_s:.1f}",
|
|
f"-voxel.TestMoveStartSeconds={start_s:.1f}",
|
|
f"-voxel.TestMoveDurationSeconds={duration_s:.1f}",
|
|
f"-voxel.TestExitSeconds={exit_s:.1f}", "-voxel.TestMoveDirection=1,0,0",
|
|
"-voxel.StreamingSchedulerPolicy=3", "-voxel.TestExitOnSteady=0",
|
|
"-coreworldstreamtrace=1", f"-voxel.StartupTraceSpawnCm={spawn_cm}",
|
|
"-voxel.StartupTraceUntilEndPlay=1", "-voxel.StartupTraceThroughCrossing=1",
|
|
"-voxel.StartupTraceMaxTiles=16384", f"-voxel.StartupTraceFile={trace}",
|
|
f"-abslog={abslog}",
|
|
]
|
|
wrapped = ["taskset", "-c", "0-11", "timeout", "1740s", *command]
|
|
started = time.monotonic()
|
|
with stdout.open("w", encoding="utf-8") as stream:
|
|
process = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
|
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
|
events = read_events(trace)
|
|
samples = [event for event in events if event.get("type") == "streaming_sample"]
|
|
moving = [event for event in samples if float(event.get("speed_cm_s", 0.0)) >= 50.0]
|
|
stop = next((event for event in events if event.get("type") == "streaming_test_move_stopped"), {})
|
|
stop_s = float(stop.get("t_s", 0.0))
|
|
move_start = min((float(event.get("t_s", 0.0)) for event in moving), default=float("inf"))
|
|
dwell = [event for event in samples if float(event.get("t_s", 0.0)) >= stop_s
|
|
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
|
backend = next((event for event in events if event.get("type") == "worker_backend_config"), {})
|
|
cap = next((event for event in events if event.get("type") == "streaming_worker_cap_readback"), {})
|
|
if process.returncode or len(moving) < 40 or len(dwell) < 16:
|
|
raise RuntimeError(f"incomplete {scenario} run: {trace}; exit={process.returncode}; "
|
|
f"move={len(moving)} dwell={len(dwell)}")
|
|
if (int(backend.get("streaming_fast_travel_wait_for_all_critical_tiles", -1))
|
|
!= wait_for_critical
|
|
or int(backend.get("streaming_fast_travel_near_only_while_critical_pending", -1))
|
|
!= near_only_while_critical_pending
|
|
or int(backend.get("streaming_far_apply_supplement", -1)) != 1
|
|
or int(cap.get("streaming_background_worker_cap", -1)) != 1
|
|
or int(backend.get("effective_tile_slots", -1)) != 8):
|
|
raise RuntimeError(f"configuration readback mismatch: {backend}; {cap}")
|
|
|
|
moving_scheduler = [event for event in events if event.get("type") == "scheduler_admission"
|
|
and move_start <= float(event.get("t_s", 0.0)) < stop_s
|
|
and event.get("moving", 0)]
|
|
idle = [event for event in moving_scheduler
|
|
if event.get("running_workers", 0) == 0
|
|
and sum(event.get("desired_missing_by_lod", [0] * 9)[:3])
|
|
+ sum(event.get("pending_by_lod", [0] * 9)[:3]) > 0]
|
|
move_rows = []
|
|
for event in moving:
|
|
desired = event.get("desired_by_lod", [0] * 9)
|
|
resident = event.get("resident_by_lod", [0] * 9)
|
|
covered = event.get("covered_by_lod", resident)
|
|
move_rows.append({
|
|
"near_ready_pct": 100.0 * sum(resident[:3]) / max(1, sum(desired[:3])),
|
|
"lod0_ready_pct": 100.0 * resident[0] / max(1, desired[0]),
|
|
"lod1_ready_pct": 100.0 * resident[1] / max(1, desired[1]),
|
|
"lod2_ready_pct": 100.0 * resident[2] / max(1, desired[2]),
|
|
"far_hole_pct": 100.0 * (sum(desired[3:]) - sum(covered[3:]))
|
|
/ max(1, sum(desired[3:])),
|
|
})
|
|
work_frames = [event for event in events if event.get("type") == "game_thread_work_frame"
|
|
and move_start <= float(event.get("t_s", 0.0)) < stop_s]
|
|
tick_values = [float(event.get("tick_ms", 0.0)) for event in work_frames]
|
|
update_values = [float(event.get("update_chunks_ms", 0.0)) for event in work_frames]
|
|
requests = [event for event in events if event.get("type") == "request"
|
|
and move_start <= float(event.get("t_s", 0.0)) < stop_s
|
|
and event.get("level", 9) <= 2]
|
|
|
|
arrival_rows = [event for event in samples if float(event.get("t_s", 0.0)) < start_s
|
|
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
|
|
|
def near_complete(event: dict) -> bool:
|
|
desired = event.get("desired_by_lod", [0] * 9)
|
|
settled = event.get("settled_by_lod", [0] * 9)
|
|
return sum(desired[:3]) > 0 and sum(settled[:3]) >= sum(desired[:3])
|
|
|
|
def whole_complete(event: dict) -> bool:
|
|
desired = event.get("desired_by_lod", [0] * 9)
|
|
settled = event.get("settled_by_lod", [0] * 9)
|
|
return sum(desired) > 0 and sum(settled) >= sum(desired)
|
|
|
|
def far_holes(event: dict) -> float | None:
|
|
desired = event.get("desired_by_lod", [0] * 9)[3:]
|
|
covered = event.get("covered_by_lod", [0] * 9)[3:]
|
|
return (100.0 * (sum(desired) - sum(covered)) / sum(desired)
|
|
if sum(desired) else None)
|
|
|
|
near_settled = next((event for event in arrival_rows if near_complete(event)), None)
|
|
whole_settled = next((event for event in arrival_rows if whole_complete(event)), None)
|
|
far_clear = next((event for event in arrival_rows if far_holes(event) == 0.0), None)
|
|
|
|
def mean_metric(name: str, rows: list[dict]) -> float | None:
|
|
values = [float(row[name]) for row in rows if row.get(name) is not None]
|
|
return statistics.mean(values) if values else None
|
|
|
|
row = {
|
|
"scenario": scenario, "condition": condition, "repetition": repetition,
|
|
"design": design, "speed_mps": speed_cm_s / 100.0,
|
|
"wait_for_all_critical_tiles": wait_for_critical,
|
|
"near_only_while_critical_pending": near_only_while_critical_pending,
|
|
"exit_code": process.returncode, "wall_s": round(time.monotonic() - started, 3),
|
|
"moving_samples": len(moving), "dwell_samples": len(dwell),
|
|
"near_ready_moving_mean_pct": mean_metric("near_ready_pct", move_rows),
|
|
"lod0_ready_moving_mean_pct": mean_metric("lod0_ready_pct", move_rows),
|
|
"lod1_ready_moving_mean_pct": mean_metric("lod1_ready_pct", move_rows),
|
|
"lod2_ready_moving_mean_pct": mean_metric("lod2_ready_pct", move_rows),
|
|
"far_holes_moving_mean_pct": mean_metric("far_hole_pct", move_rows),
|
|
"game_thread_tick_p99_ms": percentile(tick_values, 0.99),
|
|
"game_thread_tick_max_ms": max(tick_values, default=None),
|
|
"stream_update_p99_ms": percentile(update_values, 0.99),
|
|
"stream_update_max_ms": max(update_values, default=None),
|
|
"near_requests_during_move": len(requests),
|
|
"arrival_sample_count": len(arrival_rows),
|
|
"lod02_settled_t_s": float(near_settled["t_s"]) if near_settled else None,
|
|
"whole_view_settled_t_s": float(whole_settled["t_s"]) if whole_settled else None,
|
|
"far_clear_t_s": float(far_clear["t_s"]) if far_clear else None,
|
|
"far_holes_at_pre_move_pct": far_holes(arrival_rows[-1]) if arrival_rows else None,
|
|
"idle_missing_near_windows": len(idle),
|
|
"idle_critical_gate_closed": sum(not event.get("critical_tiles_ready", 1)
|
|
for event in idle),
|
|
"idle_exact_region_declines": sum(
|
|
sum(event.get("region_compile_declines_by_lod", [0] * 9)[:3]) > 0
|
|
for event in idle),
|
|
"idle_far_parent_declines": sum(
|
|
sum(event.get("far_parent_declines_by_lod", [0] * 9)[:3]) > 0
|
|
for event in idle),
|
|
"idle_near_cap_declines": sum(
|
|
any(value in (1, 2) for value in event.get("declined_rule_by_lod", [0] * 9)[:3])
|
|
for event in idle),
|
|
"idle_desired_center_stale": sum(not event.get("desired_center_current", 1)
|
|
for event in idle),
|
|
"trace": str(trace.relative_to(ROOT)), "log": str(abslog.relative_to(ROOT)),
|
|
}
|
|
print("[L122Pair] " + json.dumps(row, sort_keys=True), flush=True)
|
|
return row
|
|
|
|
|
|
def pairs_for(runs: list[dict], scenario: str, design: str) -> list[dict]:
|
|
rows = []
|
|
for repetition in range(1, 6):
|
|
baseline = next(row for row in runs if row["scenario"] == scenario
|
|
and row["condition"] == "baseline" and row["repetition"] == repetition)
|
|
candidate = next(row for row in runs if row["scenario"] == scenario
|
|
and row["condition"] == design and row["repetition"] == repetition)
|
|
rows.append({
|
|
"repetition": repetition,
|
|
"near_ready_delta_pct": candidate.get("near_ready_moving_mean_pct", 0.0)
|
|
- baseline.get("near_ready_moving_mean_pct", 0.0),
|
|
"lod0_ready_delta_pct": candidate.get("lod0_ready_moving_mean_pct", 0.0)
|
|
- baseline.get("lod0_ready_moving_mean_pct", 0.0),
|
|
"lod1_ready_delta_pct": candidate.get("lod1_ready_moving_mean_pct", 0.0)
|
|
- baseline.get("lod1_ready_moving_mean_pct", 0.0),
|
|
"lod2_ready_delta_pct": candidate.get("lod2_ready_moving_mean_pct", 0.0)
|
|
- baseline.get("lod2_ready_moving_mean_pct", 0.0),
|
|
"far_holes_delta_pct": candidate.get("far_holes_moving_mean_pct", 0.0)
|
|
- baseline.get("far_holes_moving_mean_pct", 0.0),
|
|
"tick_p99_delta_ms": candidate.get("game_thread_tick_p99_ms", 0.0)
|
|
- baseline.get("game_thread_tick_p99_ms", 0.0),
|
|
"tick_max_delta_ms": candidate.get("game_thread_tick_max_ms", 0.0)
|
|
- baseline.get("game_thread_tick_max_ms", 0.0),
|
|
})
|
|
return rows
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--scenario", choices=("movement", "arrival"), required=True)
|
|
parser.add_argument("--design", choices=(DESIGN, NEAR_ONLY_DESIGN), default=DESIGN)
|
|
args = parser.parse_args()
|
|
if not EDITOR.is_file():
|
|
raise SystemExit(f"missing Unreal editor: {EDITOR}")
|
|
output_name = args.scenario + ("-near-only" if args.design == NEAR_ONLY_DESIGN else "")
|
|
out = ROOT / f"Saved/Linux/l122/{output_name}"
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
runs = []
|
|
for repetition in range(1, 6):
|
|
order = ("baseline", args.design) if repetition % 2 else (args.design, "baseline")
|
|
for condition in order:
|
|
runs.append(run(args.scenario, condition, repetition, args.design))
|
|
(out / "runs-partial.json").write_text(json.dumps(runs, indent=2) + "\n",
|
|
encoding="utf-8")
|
|
pairs = pairs_for(runs, args.scenario, args.design)
|
|
summary = {
|
|
"design_id": args.design,
|
|
"design": ("Allow fast-travel near LOD0-2 admission after the 3x3 walkable ring is ready "
|
|
"while other expanded critical tiles remain unresident; skip directional sort "
|
|
"and far admission until the critical set is ready."
|
|
if args.design == NEAR_ONLY_DESIGN else
|
|
"Allow fast-travel near/far admissions after the 3x3 walkable ring is ready, "
|
|
"even while other expanded critical tiles remain unresident; critical requests "
|
|
"still submit first."),
|
|
"scenario": args.scenario,
|
|
"runs": runs,
|
|
"pairs": pairs,
|
|
"paired_median_near_ready_delta_pct": statistics.median(
|
|
pair["near_ready_delta_pct"] for pair in pairs),
|
|
"paired_median_far_holes_delta_pct": statistics.median(
|
|
pair["far_holes_delta_pct"] for pair in pairs),
|
|
"paired_median_tick_p99_delta_ms": statistics.median(
|
|
pair["tick_p99_delta_ms"] for pair in pairs),
|
|
"paired_median_tick_max_delta_ms": statistics.median(
|
|
pair["tick_max_delta_ms"] for pair in pairs),
|
|
}
|
|
(out / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
|
|
encoding="utf-8")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|