P28-01-COLLECTOR: 現HEADの性能基準:windowed x1/x3・Operations ON collector

This commit is contained in:
Agent Control Runner
2026-10-20 22:37:35 +09:00
parent 3ac3243dfb
commit a09681e287
4 changed files with 368 additions and 13 deletions
@@ -6,6 +6,7 @@ import json
import math import math
from pathlib import Path from pathlib import Path
import re import re
import struct
PREFIX = "P28_01_PERF_REPORT=" PREFIX = "P28_01_PERF_REPORT="
RAW_SOURCE = "clock.performance_budget (product diagnostic, raw, not a verdict)" RAW_SOURCE = "clock.performance_budget (product diagnostic, raw, not a verdict)"
@@ -35,6 +36,9 @@ FORBIDDEN = ("_toggle_pause", "clear(", "enable_production_", "set_paused(false)
# Only declared collector fields are normalized. Product dictionaries (RNG, # Only declared collector fields are normalized. Product dictionaries (RNG,
# startup, raw reports) retain their original JSON representation. # startup, raw reports) retain their original JSON representation.
INTEGER_FIELDS = { INTEGER_FIELDS = {
"engine_frame_pre", "engine_frame_post", "pre_order", "post_order",
"minute_at_frame_start", "minute_after", "speed_at_frame_start",
"allowed_ticks_observation", "short_of_allowed_frames",
"cores", "memory_bytes", "pid", "seed", "frame", "game_start", "game_end", "game_minute", "cores", "memory_bytes", "pid", "seed", "frame", "game_start", "game_end", "game_minute",
"minutes_advanced", "start_minute", "end_minute", "opening_minute", "actual_minutes", "minutes_advanced", "start_minute", "end_minute", "opening_minute", "actual_minutes",
"warmup_target", "measurement_target", "speed", "speed_after", "backlog", "warmup_target", "measurement_target", "speed", "speed_after", "backlog",
@@ -233,6 +237,9 @@ def interval(report, name):
need(set(observation) == {"frame_wall_us", "sim_us_per_minute", "stall_over_50000_us"}, need(set(observation) == {"frame_wall_us", "sim_us_per_minute", "stall_over_50000_us"},
"frame observation must not contain verdict") "frame observation must not contain verdict")
need(value["frames"] == len(rows), "frame count") need(value["frames"] == len(rows), "frame count")
need(value.get("short_of_allowed_frames") ==
sum(f["minutes_advanced"] < f["allowed_ticks_observation"] for f in rows),
"short-of-allowed observation count")
need(value["actual_minutes"] == sum(f["minutes_advanced"] for f in rows), "false interval duration") need(value["actual_minutes"] == sum(f["minutes_advanced"] for f in rows), "false interval duration")
for field, expected in (("start_minute", rows[0]["game_start"] if rows else None), for field, expected in (("start_minute", rows[0]["game_start"] if rows else None),
("end_minute", rows[-1]["game_end"] if rows else None), ("end_minute", rows[-1]["game_end"] if rows else None),
@@ -325,7 +332,15 @@ def check_report(report):
previous_wall = report["start_wall"] previous_wall = report["start_wall"]
previous_drawing = None previous_drawing = None
previous_counts = None previous_counts = None
keys(report, ("baseline",))
previous_pair = report["baseline"]
check_pair(previous_pair)
need(previous_pair.get("excluded_from_statistics") is True and
previous_pair["minute_after"] == previous and previous_pair.get("wall") == previous_wall,
"missing/exposed baseline")
for index, frame in enumerate(frames): for index, frame in enumerate(frames):
check_pair(frame, previous_pair, speed)
previous_pair = frame
keys(frame, ("frame", "game_start", "game_end", "minutes_advanced", "frame_wall_us", "wall", "wall_start", keys(frame, ("frame", "game_start", "game_end", "minutes_advanced", "frame_wall_us", "wall", "wall_start",
"speed", "paused", "segment", "registered", "active", "drawn", "seed", "rng_streams", "speed", "paused", "segment", "registered", "active", "drawn", "seed", "rng_streams",
"metadata", "input_sha256", "preparation_sha256", "render_cpu_raw_ms", "metadata", "input_sha256", "preparation_sha256", "render_cpu_raw_ms",
@@ -475,18 +490,107 @@ def check_receipt(receipt, directory):
stdout = (directory / row["phase"] / "stdout.log").read_text() stdout = (directory / row["phase"] / "stdout.log").read_text()
printed = [normalize_report(json.loads(x[len(PREFIX):])) for x in stdout.splitlines() if x.startswith(PREFIX)] printed = [normalize_report(json.loads(x[len(PREFIX):])) for x in stdout.splitlines() if x.startswith(PREFIX)]
need(printed == [row["report"]], "raw/report mismatch") need(printed == [row["report"]], "raw/report mismatch")
equal(audio_close(stdout, row["report"]), row["audio_close"], "audio close binding")
need(row["capture"]["actual_captured_pids"] == [row["report"]["pid"]], "PID mismatch") need(row["capture"]["actual_captured_pids"] == [row["report"]["pid"]], "PID mismatch")
check_report(row["report"]) check_report(row["report"])
if row["phase"] in ("R0", "O0"): if row["phase"] in ("R0", "O0"):
equal(row["extrapolation"], extrapolation(row["report"]), "extrapolation") equal(row["extrapolation"], extrapolation(row["report"]), "extrapolation")
def decode_f64(value):
need(isinstance(value, str) and re.fullmatch("[0-9a-f]{16}", value) is not None,
"missing/malformed f64 evidence")
result = struct.unpack("<d", bytes.fromhex(value))[0]
need(math.isfinite(result), "nonfinite f64 evidence")
return result
def check_pair(pair, previous=None, speed=None):
keys(pair, ("engine_frame_pre", "engine_frame_post", "pre_order", "post_order",
"paused_at_frame_start", "speed_at_frame_start", "skip_at_frame_start",
"acc_at_frame_start_f64_hex", "minute_at_frame_start", "process_delta_f64_hex",
"post_delta_f64_hex", "paused_after", "minute_after", "acc_after_f64_hex"))
for key in ("engine_frame_pre", "engine_frame_post", "pre_order", "post_order",
"minute_at_frame_start", "minute_after", "speed_at_frame_start"):
need(type(pair[key]) is int and pair[key] >= 0, "invalid pair integer")
for key in ("paused_at_frame_start", "paused_after", "skip_at_frame_start"):
need(type(pair[key]) is bool, "invalid pair boolean")
need(pair["engine_frame_pre"] == pair["engine_frame_post"] and
pair["post_order"] == pair["pre_order"] + 1, "unpaired frame")
acc = decode_f64(pair["acc_at_frame_start_f64_hex"])
after = decode_f64(pair["acc_after_f64_hex"])
delta = decode_f64(pair["process_delta_f64_hex"])
decode_f64(pair["post_delta_f64_hex"])
need(pair["process_delta_f64_hex"] == pair["post_delta_f64_hex"], "different process deltas")
if previous is None:
# First complete pair is an excluded anchor, never an inferred pre-begin state.
return
need(pair["engine_frame_pre"] == previous["engine_frame_post"] + 1 and
pair["pre_order"] == previous["post_order"] + 1, "frame order gap")
need(pair["minute_at_frame_start"] == previous["minute_after"] and
pair["acc_at_frame_start_f64_hex"] == previous["acc_after_f64_hex"], "pair continuity")
need(pair["speed_at_frame_start"] == speed, "cadence speed")
advanced = pair["minute_after"] - pair["minute_at_frame_start"]
allowed = 0
if not pair["skip_at_frame_start"] and not pair["paused_at_frame_start"] and speed > 0 and delta > 0:
acc += delta * speed
allowed = min(int(acc / (10.0 / 60.0)), 8)
need(0 <= advanced <= allowed, "cadence upper bound")
need(type(pair.get("allowed_ticks_observation")) is int and
pair["allowed_ticks_observation"] == allowed, "allowed tick observation")
for _ in range(advanced):
acc -= 10.0 / 60.0
need(struct.pack("<d", acc).hex() == pair["acc_after_f64_hex"], "accumulator recurrence")
need(advanced == pair["minutes_advanced"] and
pair["game_start"] == pair["minute_at_frame_start"] and
pair["game_end"] == pair["minute_after"] and pair["paused"] is pair["paused_after"],
"pair/frame reconciliation")
def audio_close(stdout, report):
prefix = "P28_01_AUDIO_CLOSE="
rows = [json.loads(line[len(prefix):]) for line in stdout.splitlines() if line.startswith(prefix)]
need(len(rows) == 1, "exactly one AUDIO_CLOSE required")
row = rows[0]
need(type(report.get("main_added")) is bool, "missing main-added observation")
need(type(report.get("audio_owner_present")) is bool, "missing audio owner observation")
if row.get("coordinator") is True:
need(report["main_added"] and report["audio_owner_present"] and row.get("passed") is True and
row.get("deadline_reached") is False and row.get("all_owned_targets_released") is True,
"audio close drain failed")
need(type(row.get("waited_actual_us")) in (int, float) and
math.isfinite(row["waited_actual_us"]) and row["waited_actual_us"] >= 0,
"audio close wait")
else:
need(not report["main_added"] or not report["audio_owner_present"], "owner drain bypassed")
need(row == {"coordinator": False, "reason":
"no_audio_owner" if report["main_added"] else "main_not_added"},
"invalid no-owner receipt")
return row
def check_probe(text): def check_probe(text):
# The SceneTree bootstrap name is required; all other initialization is forbidden. # The SceneTree bootstrap name is required; all other initialization is forbidden.
body = text.replace("func _initialize()", "func BOOTSTRAP()") body = text.replace("func _initialize()", "func BOOTSTRAP()")
for token in FORBIDDEN: for token in FORBIDDEN:
need(token not in body, "forbidden probe token: " + token) need(token not in body, "forbidden probe token: " + token)
need('main.call("_set_speed", speed)' in body, "missing UI speed path") need('main.call("_set_speed", speed)' in body, "missing UI speed path")
close_branch = '''\tif not is_instance_valid(main) or not main.is_inside_tree():
\t\temit_audio_close({"coordinator": false, "reason": "main_not_added"})
\t\tquit(0)
\telif not root.has_meta(UIAudioOwnerShutdownScript.OWNER_META):
\t\temit_audio_close({"coordinator": false, "reason": "no_audio_owner"})
\t\tquit(0)
\telse:
\t\tmain.ui_audio_window_close_drained.connect(emit_audio_close, CONNECT_ONE_SHOT)
\t\troot.close_requested.emit()'''
need(close_branch in body, "missing product close branch")
need(re.search(r"\bquit\s*\(", body.replace(close_branch, "")) is None,
"quit outside no-main/no-owner branches")
need("tests/helpers/ui_audio_fixture_cleanup" not in body, "fixture close forbidden")
for token in ("pre_observer.process_priority = -1000000", "observer.process_priority = 100000",
"PackedFloat64Array([value]).to_byte_array().hex_encode()", "anchor_baseline()"):
need(token in body, "missing passive pair observation: " + token)
def main(): def main():
@@ -13,7 +13,7 @@ import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
sys.dont_write_bytecode = True sys.dont_write_bytecode = True
from check_p28_01_report import check_report, check_receipt, check_probe, normalize_report, check_admission, R02_FIELDS, VERDICT_SCOPE, CAP_SOURCE from check_p28_01_report import check_report, check_receipt, check_probe, normalize_report, check_admission, audio_close, R02_FIELDS, VERDICT_SCOPE, CAP_SOURCE
HERE = Path(__file__).resolve().parent HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[5] ROOT = HERE.parents[5]
FIXTURE_SHA = "eb4f770071eb07cd7e136fe54c3f78768cc8de7a4264e62a9af1f06a8f90f9bd" FIXTURE_SHA = "eb4f770071eb07cd7e136fe54c3f78768cc8de7a4264e62a9af1f06a8f90f9bd"
@@ -152,6 +152,11 @@ def bound_report(folder, row):
require(len(printed) == 1, "exactly one raw report required") require(len(printed) == 1, "exactly one raw report required")
report = normalize_report(json.loads(printed[0])) report = normalize_report(json.loads(printed[0]))
require(row["capture"]["actual_captured_pids"] == [report["pid"]], "PID binding") require(row["capture"]["actual_captured_pids"] == [report["pid"]], "PID binding")
try:
row["audio_close"] = audio_close((folder / "stdout.log").read_text(), report)
except (ValueError, KeyError, TypeError):
row.update(passed=False, stop_reason="engine_failure")
raise
return report return report
@@ -1,14 +1,28 @@
extends SceneTree extends SceneTree
const UIAudioOwnerShutdownScript = preload("res://scripts/ui/ui_audio_owner_shutdown.gd")
## External, passive observer of the default autoload world. ## External, passive observer of the default autoload world.
## Raw per-frame lines survive an owned timeout before the final report. ## Raw per-frame lines survive an owned timeout before the final report.
class PreObserver:
extends Node
var owner_probe: Variant
func _process(delta: float) -> void:
owner_probe.pre_sample(delta)
class Observer: class Observer:
extends Node extends Node
var owner_probe: Variant var owner_probe: Variant
func _process(_delta: float) -> void: func _process(delta: float) -> void:
var observer_started_us := Time.get_ticks_usec() var observer_started_us := Time.get_ticks_usec()
owner_probe.sample(observer_started_us) owner_probe.sample(observer_started_us, delta)
var pending_pair := {}
var baseline := {}
var last_pair := {}
var finished := false
var audio_emitted := false
var observation_order := 0
var pre_cost_us := 0
var clock: Variant var clock: Variant
var runtime: Variant var runtime: Variant
var main: Variant var main: Variant
@@ -253,7 +267,7 @@ func begin() -> void:
speed = int(OS.get_environment("THE_TREE_P28_SPEED")) speed = int(OS.get_environment("THE_TREE_P28_SPEED"))
warmup = int(OS.get_environment("THE_TREE_P28_WARMUP")) warmup = int(OS.get_environment("THE_TREE_P28_WARMUP"))
target = int(OS.get_environment("THE_TREE_P28_MINUTES")) target = int(OS.get_environment("THE_TREE_P28_MINUTES"))
report = {"schema": "p28-01-v1", "pid": OS.get_process_id(), report = {"schema": "p28-01-v1", "pid": OS.get_process_id(), "main_added": false,
"phase": OS.get_environment("THE_TREE_P28_PHASE"), "input": input, "phase": OS.get_environment("THE_TREE_P28_PHASE"), "input": input,
"metadata": JSON.parse_string(OS.get_environment("THE_TREE_P28_META")), "metadata": JSON.parse_string(OS.get_environment("THE_TREE_P28_META")),
"input_sha256": null, "input_sha256_reason": "fresh_autoload_initialization", "input_sha256": null, "input_sha256_reason": "fresh_autoload_initialization",
@@ -301,6 +315,7 @@ func begin() -> void:
var opening_minute: int = clock.absolute_minute var opening_minute: int = clock.absolute_minute
main = load("res://scenes/main/main.tscn").instantiate() main = load("res://scenes/main/main.tscn").instantiate()
root.add_child(main) root.add_child(main)
report["main_added"] = true
report["startup"] = {"pause_fresh_opening": {"return": null, "reason": "void_function", report["startup"] = {"pause_fresh_opening": {"return": null, "reason": "void_function",
"paused_before": paused_before, "paused_after": clock.paused}, "paused_before": paused_before, "paused_after": clock.paused},
"save_policy": {"return": null, "reason": "return_discarded_by_main(main.gd:222)"}, "save_policy": {"return": null, "reason": "return_discarded_by_main(main.gd:222)"},
@@ -333,6 +348,10 @@ func begin() -> void:
runtime.save_operation_completed.connect(on_save) runtime.save_operation_completed.connect(on_save)
RenderingServer.frame_pre_draw.connect(on_pre_draw) RenderingServer.frame_pre_draw.connect(on_pre_draw)
RenderingServer.frame_post_draw.connect(on_post_draw) RenderingServer.frame_post_draw.connect(on_post_draw)
var pre_observer := PreObserver.new()
pre_observer.owner_probe = self
pre_observer.process_priority = -1000000
root.add_child(pre_observer)
var observer := Observer.new() var observer := Observer.new()
observer.owner_probe = self observer.owner_probe = self
observer.process_priority = 100000 observer.process_priority = 100000
@@ -346,6 +365,9 @@ func begin() -> void:
if clock.paused or clock.speed != speed: if clock.paused or clock.speed != speed:
finish("input_shortfall") finish("input_shortfall")
return return
ready_to_sample = true
func anchor_baseline() -> void:
start_minute = clock.absolute_minute start_minute = clock.absolute_minute
measurement_start = start_minute if warmup == 0 else -1 measurement_start = start_minute if warmup == 0 else -1
previous_minute = start_minute previous_minute = start_minute
@@ -363,7 +385,12 @@ func begin() -> void:
report["start_minute"] = start_minute report["start_minute"] = start_minute
report["start_wall"] = Time.get_unix_time_from_system() report["start_wall"] = Time.get_unix_time_from_system()
previous_wall = report["start_wall"] previous_wall = report["start_wall"]
ready_to_sample = true baseline["wall"] = report.start_wall
baseline["excluded_from_statistics"] = true
report["baseline"] = baseline
cached_registered = registered()
cached_active = active_counts()
cached_rng = rng_state()
func on_overload(backlog: int) -> void: func on_overload(backlog: int) -> void:
overloads.append({"backlog": backlog, "game_minute": clock.absolute_minute, "frame": frames.size()}) overloads.append({"backlog": backlog, "game_minute": clock.absolute_minute, "frame": frames.size()})
@@ -373,7 +400,7 @@ func on_checkpoint(_presentation: Dictionary) -> void:
"wall": Time.get_unix_time_from_system(), "cause": "production_checkpoint_presentation_requested"}) "wall": Time.get_unix_time_from_system(), "cause": "production_checkpoint_presentation_requested"})
func on_tick(minute: int) -> void: func on_tick(minute: int) -> void:
if not ready_to_sample: if not ready_to_sample or baseline.is_empty():
return return
var tick_us := Time.get_ticks_usec() var tick_us := Time.get_ticks_usec()
var row := {"game_minute": minute, "wall": Time.get_unix_time_from_system(), var row := {"game_minute": minute, "wall": Time.get_unix_time_from_system(),
@@ -383,9 +410,61 @@ func on_tick(minute: int) -> void:
previous_tick_us = tick_us previous_tick_us = tick_us
print("P28_01_MINUTE=" + JSON.stringify(row)) print("P28_01_MINUTE=" + JSON.stringify(row))
func sample(observer_started_us: int) -> void: func f64(value: float) -> String:
return PackedFloat64Array([value]).to_byte_array().hex_encode()
func pre_sample(delta: float) -> void:
if not ready_to_sample: if not ready_to_sample:
return return
var began := Time.get_ticks_usec()
if not pending_pair.is_empty():
report["frame_order_evidence"] = pending_pair
finish("frame_order_unverified")
return
observation_order += 1
pending_pair = {"engine_frame_pre": Engine.get_process_frames(), "pre_order": observation_order,
"paused_at_frame_start": clock.paused, "speed_at_frame_start": clock.speed,
"skip_at_frame_start": clock.get("_skip_next_process"),
"acc_at_frame_start_f64_hex": f64(clock.get("_accumulator")),
"minute_at_frame_start": clock.absolute_minute, "process_delta_f64_hex": f64(delta)}
var allowed := 0
if not pending_pair.skip_at_frame_start and not clock.paused and clock.speed > 0 and delta > 0:
allowed = mini(int((float(clock.get("_accumulator")) + delta * clock.speed) / (10.0 / 60.0)), 8)
pending_pair["allowed_ticks_observation"] = allowed
pre_cost_us = Time.get_ticks_usec() - began
func sample(observer_started_us: int, delta: float) -> void:
if not ready_to_sample:
return
if pending_pair.is_empty():
if baseline.is_empty():
report["leading_partial_frame"] = {"engine_frame_post": Engine.get_process_frames()}
return
report["frame_order_evidence"] = {"engine_frame_post": Engine.get_process_frames()}
finish("frame_order_unverified")
return
var pair := pending_pair
pending_pair = {}
observation_order += 1
pair.merge({"engine_frame_post": Engine.get_process_frames(), "post_order": observation_order,
"post_delta_f64_hex": f64(delta), "paused_after": clock.paused,
"minute_after": clock.absolute_minute, "acc_after_f64_hex": f64(clock.get("_accumulator"))})
if pair.engine_frame_pre != pair.engine_frame_post or pair.process_delta_f64_hex != pair.post_delta_f64_hex:
report["frame_order_evidence"] = pair
finish("frame_order_unverified")
return
if baseline.is_empty():
baseline = pair
last_pair = pair
anchor_baseline()
baseline["observer_us"] = pre_cost_us + Time.get_ticks_usec() - observer_started_us
return
if pair.engine_frame_pre != last_pair.engine_frame_post + 1 or pair.pre_order != last_pair.post_order + 1 \
or pair.minute_at_frame_start != last_pair.minute_after or pair.acc_at_frame_start_f64_hex != last_pair.acc_after_f64_hex:
report["frame_order_evidence"] = {"previous": last_pair, "current": pair}
finish("frame_order_unverified")
return
last_pair = pair
var current_us := Time.get_ticks_usec() var current_us := Time.get_ticks_usec()
var minute: int = clock.absolute_minute var minute: int = clock.absolute_minute
var elapsed := current_us - previous_us var elapsed := current_us - previous_us
@@ -426,6 +505,7 @@ func sample(observer_started_us: int) -> void:
"render_cpu_raw_ms": RenderingServer.viewport_get_measured_render_time_cpu(root.get_viewport_rid()), "render_cpu_raw_ms": RenderingServer.viewport_get_measured_render_time_cpu(root.get_viewport_rid()),
"render_gpu_raw_ms": RenderingServer.viewport_get_measured_render_time_gpu(root.get_viewport_rid()), "render_gpu_raw_ms": RenderingServer.viewport_get_measured_render_time_gpu(root.get_viewport_rid()),
"time_process_seconds": Performance.get_monitor(Performance.TIME_PROCESS)} "time_process_seconds": Performance.get_monitor(Performance.TIME_PROCESS)}
row.merge(pair)
last_render_serial = render_serial last_render_serial = render_serial
previous_us = current_us previous_us = current_us
previous_wall = row.wall previous_wall = row.wall
@@ -458,7 +538,7 @@ func sample(observer_started_us: int) -> void:
# summary are disclosed separately and are never subtracted from frame wall. # summary are disclosed separately and are never subtracted from frame wall.
var raw_line := JSON.stringify(row) var raw_line := JSON.stringify(row)
print("P28_01_FRAME=" + raw_line) print("P28_01_FRAME=" + raw_line)
row["observer_us"] = Time.get_ticks_usec() - observer_started_us row["observer_us"] = pre_cost_us + Time.get_ticks_usec() - observer_started_us
frames.append(row) frames.append(row)
print("P28_01_OBSERVER=" + JSON.stringify({"frame": row.frame, "observer_us": row.observer_us})) print("P28_01_OBSERVER=" + JSON.stringify({"frame": row.frame, "observer_us": row.observer_us}))
if not reason.is_empty(): if not reason.is_empty():
@@ -486,6 +566,7 @@ func summary(segment: String) -> Dictionary:
var observer: Array = [] var observer: Array = []
var submit: Array = [] var submit: Array = []
var excluded := 0 var excluded := 0
var short_of_allowed := 0
var minutes := 0 var minutes := 0
var cpu_missing := false var cpu_missing := false
var gpu_missing := false var gpu_missing := false
@@ -500,6 +581,8 @@ func summary(segment: String) -> Dictionary:
submit.append(frame.render_submit_wall_us) submit.append(frame.render_submit_wall_us)
process.append(frame.time_process_seconds) process.append(frame.time_process_seconds)
minutes += frame.minutes_advanced minutes += frame.minutes_advanced
if frame.minutes_advanced < frame.allowed_ticks_observation:
short_of_allowed += 1
if frame.frame_wall_us > 50000: if frame.frame_wall_us > 50000:
stalls.append(frame.frame_wall_us) stalls.append(frame.frame_wall_us)
if frame.minutes_advanced > 0: if frame.minutes_advanced > 0:
@@ -520,6 +603,7 @@ func summary(segment: String) -> Dictionary:
var unavailable := "zero_from_api_on_gl_compatibility_unsupported_or_unmeasured" var unavailable := "zero_from_api_on_gl_compatibility_unsupported_or_unmeasured"
var minute_stats := stats(per_minute) var minute_stats := stats(per_minute)
return {"frames": selected.size(), "actual_minutes": minutes, return {"frames": selected.size(), "actual_minutes": minutes,
"short_of_allowed_frames": short_of_allowed,
"start_minute": selected.front().game_start if not selected.is_empty() else null, "start_minute": selected.front().game_start if not selected.is_empty() else null,
"end_minute": selected.back().game_end if not selected.is_empty() else null, "end_minute": selected.back().game_end if not selected.is_empty() else null,
"start_wall": selected.front().wall_start if not selected.is_empty() else null, "start_wall": selected.front().wall_start if not selected.is_empty() else null,
@@ -534,7 +618,13 @@ func summary(segment: String) -> Dictionary:
"time_process_seconds": stats(process), "count_ranges": ranges} "time_process_seconds": stats(process), "count_ranges": ranges}
func finish(reason: String) -> void: func finish(reason: String) -> void:
if finished:
return
finished = true
ready_to_sample = false ready_to_sample = false
if not pending_pair.is_empty():
report["trailing_partial_frame"] = pending_pair
pending_pair = {}
# Early input shortfalls also need observed startup time for stage0 evidence. # Early input shortfalls also need observed startup time for stage0 evidence.
if not report.has("startup_wall_seconds"): if not report.has("startup_wall_seconds"):
report["startup_wall_seconds"] = float(Time.get_ticks_usec()) / 1000000.0 report["startup_wall_seconds"] = float(Time.get_ticks_usec()) / 1000000.0
@@ -544,9 +634,25 @@ func finish(reason: String) -> void:
report["end_wall"] = Time.get_unix_time_from_system() report["end_wall"] = Time.get_unix_time_from_system()
report["kernel_at_phase_end"] = {"source": "clock.performance_budget (product diagnostic, raw, not a verdict)", "data": clock.get_performance_reports()} report["kernel_at_phase_end"] = {"source": "clock.performance_budget (product diagnostic, raw, not a verdict)", "data": clock.get_performance_reports()}
report["budget_at_phase_end"] = {"source": "clock.performance_budget (product diagnostic, raw, not a verdict)", "data": budget_snapshot()} report["budget_at_phase_end"] = {"source": "clock.performance_budget (product diagnostic, raw, not a verdict)", "data": budget_snapshot()}
report["budget_verdict"] = kernel_verdict(report.budget_at_warmup_end.data, report.budget_at_phase_end.data) if reason != "frame_order_unverified":
report["budget_verdict"] = kernel_verdict(report.budget_at_warmup_end.data, report.budget_at_phase_end.data)
report["operations_at_end"] = runtime.get_operations_report() report["operations_at_end"] = runtime.get_operations_report()
report["warmup"] = summary("warmup") report["warmup"] = summary("warmup")
report["measurement"] = summary("measurement") report["measurement"] = summary("measurement")
report["audio_owner_present"] = root.has_meta(UIAudioOwnerShutdownScript.OWNER_META)
print("P28_01_PERF_REPORT=" + JSON.stringify(report)) print("P28_01_PERF_REPORT=" + JSON.stringify(report))
quit(0) if not is_instance_valid(main) or not main.is_inside_tree():
emit_audio_close({"coordinator": false, "reason": "main_not_added"})
quit(0)
elif not root.has_meta(UIAudioOwnerShutdownScript.OWNER_META):
emit_audio_close({"coordinator": false, "reason": "no_audio_owner"})
quit(0)
else:
main.ui_audio_window_close_drained.connect(emit_audio_close, CONNECT_ONE_SHOT)
root.close_requested.emit()
func emit_audio_close(result: Dictionary) -> void:
if audio_emitted:
return
audio_emitted = true
print("P28_01_AUDIO_CLOSE=" + JSON.stringify(result))
@@ -4,6 +4,7 @@ import ast
import copy import copy
import hashlib import hashlib
import json import json
import struct
from pathlib import Path from pathlib import Path
import sys import sys
import tempfile import tempfile
@@ -19,6 +20,30 @@ import collect_p28_01 as collector
HERE = Path(__file__).resolve().parent HERE = Path(__file__).resolve().parent
def f64(value):
return struct.pack("<d", value).hex()
def pair(number, minute, acc, delta=.2, ticks=1, speed=1, paused=False, skip=False,
paused_after=False):
after = acc
allowed = 0
if not skip and not paused and speed > 0 and delta > 0:
after += delta * speed
allowed = min(int(after / (10.0 / 60.0)), 8)
for _ in range(ticks):
after -= 10.0 / 60.0
return {"engine_frame_pre": number, "engine_frame_post": number,
"pre_order": number * 2 + 1, "post_order": number * 2 + 2,
"paused_at_frame_start": paused, "speed_at_frame_start": speed,
"skip_at_frame_start": skip, "acc_at_frame_start_f64_hex": f64(acc),
"minute_at_frame_start": minute, "process_delta_f64_hex": f64(delta),
"post_delta_f64_hex": f64(delta), "paused_after": paused_after,
"minute_after": minute + ticks, "acc_after_f64_hex": f64(after),
"minutes_advanced": ticks, "game_start": minute, "game_end": minute + ticks,
"paused": paused_after, "allowed_ticks_observation": allowed}
def synthetic_guard(runtime=Path("private"), name="runtime"): def synthetic_guard(runtime=Path("private"), name="runtime"):
return {"verified": True, "conditions": [True] * 5, "conditions_checked": 5, return {"verified": True, "conditions": [True] * 5, "conditions_checked": 5,
"actual_user_data_dir": str(runtime), "expected_user_data_dir": str(runtime), "actual_user_data_dir": str(runtime), "expected_user_data_dir": str(runtime),
@@ -121,6 +146,19 @@ def synthetic_report():
for name in checker.RAW_FIELDS: for name in checker.RAW_FIELDS:
report[name] = {"source": checker.RAW_SOURCE, "data": report[name]} report[name] = {"source": checker.RAW_SOURCE, "data": report[name]}
report["probe_deadline"] = {"limit_seconds": 1740, "source": checker.DEADLINE_SOURCE} report["probe_deadline"] = {"limit_seconds": 1740, "source": checker.DEADLINE_SOURCE}
report["main_added"] = True
report["audio_owner_present"] = True
report["baseline"] = pair(0, 360, 0, delta=0, ticks=0, skip=True)
report["baseline"].update(excluded_from_statistics=True, wall=100)
acc = 0.0
for i, frame in enumerate(frames):
evidence = pair(i + 1, frame["game_start"], acc)
frame.update(evidence)
acc = checker.decode_f64(evidence["acc_after_f64_hex"])
for name in ("warmup", "measurement"):
report[name]["short_of_allowed_frames"] = sum(
f["minutes_advanced"] < f["allowed_ticks_observation"]
for f in frames if f["segment"] == name)
return report return report
@@ -131,6 +169,105 @@ def synthetic_admission():
class ReportTests(unittest.TestCase): class ReportTests(unittest.TestCase):
def test_k1_audio_close_receipts(self):
report = {"main_added": True, "audio_owner_present": True}
close = {"coordinator": True, "passed": True, "deadline_reached": False,
"all_owned_targets_released": True, "waited_actual_us": 15000}
def raw(value):
return "P28_01_AUDIO_CLOSE=" + json.dumps(value) + "\n"
self.assertEqual(checker.audio_close(raw(close), report), close)
no_owner = {"coordinator": False, "reason": "no_audio_owner"}
self.assertEqual(checker.audio_close(raw(no_owner),
dict(report, audio_owner_present=False)), no_owner)
for text in ("", raw(close) * 2, raw(dict(close, passed=False)),
raw(dict(close, deadline_reached=True)),
raw(dict(close, all_owned_targets_released=False)),
raw({"coordinator": False, "reason": "main_not_added"}), raw(no_owner)):
with self.subTest(text=text), self.assertRaises(ValueError):
checker.audio_close(text, report)
# Binding uses the actual raw marker, independently of a phase's claimed pass.
with patch.object(Path, "read_text", return_value=checker.PREFIX +
json.dumps({"pid": 123, **report}) + "\n" + raw(close)):
row = {"capture": {"actual_captured_pids": [123]}}
collector.bound_report(Path("synthetic"), row)
self.assertEqual(row["audio_close"], close)
def test_k1_product_close_static(self):
text = (HERE / "measure_p28_01.gd").read_text()
checker.check_probe(text)
owner = text.split("\telse:\n\t\tmain.ui_audio_window_close_drained.connect", 1)[1]
self.assertNotIn("quit(", owner)
for broken in (text + "\nfunc bypass():\n\tquit(0)\n",
text.replace("\t\tquit(0)", "\tquit(0)", 1),
text.replace("root.close_requested.emit()", "quit(0)"),
text + "\n# tests/helpers/ui_audio_fixture_cleanup.gd"):
with self.assertRaises(ValueError):
checker.check_probe(broken)
def test_k2_valid_cadence_boundaries(self):
for options in ({"acc": 2.0, "delta": .016, "ticks": 8},
{"acc": 2.0, "delta": .016, "ticks": 3, "paused_after": True},
{"acc": 2.0, "delta": .016, "ticks": 0, "skip": True},
{"acc": 30.0, "delta": .016, "ticks": 8, "paused_after": True},
{"acc": 30.0, "delta": .016, "ticks": 0, "paused": True,
"paused_after": True}):
baseline = pair(0, 360, options["acc"], delta=0, ticks=0)
current = pair(1, 360, **options)
with self.subTest(options=options):
checker.check_pair(current, baseline, 1)
# A real skip may follow an excluded baseline; don't guess it was consumed.
baseline = pair(0, 360, 0, delta=0, ticks=0)
skipped = pair(1, 360, 0, ticks=0, skip=True)
checker.check_pair(skipped, baseline, 1)
checker.check_pair(pair(2, 360, 0), skipped, 1)
def test_k2_reject_cadence_and_missing_evidence(self):
baseline = pair(0, 360, 0, delta=0, ticks=0)
valid = pair(1, 360, 0)
mutations = [
lambda x: x.update(**pair(1, 360, 0, delta=.016, ticks=1)),
lambda x: x.update(**pair(1, 360, 0, delta=.2, ticks=3)),
lambda x: x.update(**pair(1, 360, 0, delta=2, ticks=9)),
lambda x: x.update(**pair(1, 360, 0, paused=True)),
lambda x: x.update(minute_at_frame_start=361),
lambda x: x.update(acc_at_frame_start_f64_hex=f64(.01)),
lambda x: x.update(engine_frame_post=2),
lambda x: x.update(pre_order=4),
]
for key in ("process_delta_f64_hex", "post_delta_f64_hex",
"acc_at_frame_start_f64_hex", "acc_after_f64_hex", "skip_at_frame_start"):
mutations.append(lambda x, k=key: x.pop(k))
mutations.append(lambda x, k=key: x.update({k: "broken"}))
mutations.append(lambda x: x.update(process_delta_f64_hex=f64(float("nan"))))
for mutate in mutations:
current = copy.deepcopy(valid)
mutate(current)
with self.subTest(current=current), self.assertRaises(ValueError):
checker.check_pair(current, baseline, 1)
baseline = pair(0, 360, 2.0, delta=0, ticks=0)
current = pair(1, 360, 2.0, delta=.016, ticks=8)
bulk = f64((2.0 + .016) - (10.0 / 60.0) * 8)
self.assertNotEqual(bulk, current["acc_after_f64_hex"])
current["acc_after_f64_hex"] = bulk
with self.assertRaises(ValueError):
checker.check_pair(current, baseline, 1)
def test_k3_baseline_and_complete_pairs(self):
checker.check_report(synthetic_report())
for mutate in (
lambda r: r.pop("baseline"),
lambda r: r["baseline"].update(excluded_from_statistics=False),
lambda r: r["baseline"].update(minute_after=359),
lambda r: r["baseline"].update(wall=99),
lambda r: r["frames"][0].update(engine_frame_pre=0, engine_frame_post=0,
pre_order=1, post_order=2),
lambda r: r["frames"][1].pop("post_order"),
lambda r: r["frames"][1].update(acc_at_frame_start_f64_hex=f64(0)),
lambda r: r["frames"][1].update(minute_at_frame_start=360),
):
with self.subTest(mutate=mutate):
self.reject(mutate)
def test_r1_recursive_verdict_scope(self): def test_r1_recursive_verdict_scope(self):
report = synthetic_report() report = synthetic_report()
receipt = {"admission": synthetic_admission(), "phases": [{"report": report, "passed": True}], receipt = {"admission": synthetic_admission(), "phases": [{"report": report, "passed": True}],
@@ -306,10 +443,12 @@ class ReportTests(unittest.TestCase):
def test_receipt_rejects_source_player_and_raw_pid(self): def test_receipt_rejects_source_player_and_raw_pid(self):
r = synthetic_report() r = synthetic_report()
raw = (checker.PREFIX + json.dumps(r) + "\n").encode() close = {"coordinator": True, "passed": True, "deadline_reached": False,
"all_owned_targets_released": True, "waited_actual_us": 15000}
raw = (checker.PREFIX + json.dumps(r) + "\nP28_01_AUDIO_CLOSE=" + json.dumps(close) + "\n").encode()
row = {"phase": "O0", "passed": True, "timeout_seconds": 1800, row = {"phase": "O0", "passed": True, "timeout_seconds": 1800,
"raw": {"stdout.log": {"sha256": hashlib.sha256(raw).hexdigest(), "bytes": len(raw)}}, "raw": {"stdout.log": {"sha256": hashlib.sha256(raw).hexdigest(), "bytes": len(raw)}},
"report": r, "capture": {"actual_captured_pids": [123]}, "report": r, "audio_close": close, "capture": {"actual_captured_pids": [123]},
"extrapolation": collector.extrapolation(r), "extrapolation": collector.extrapolation(r),
"guard": synthetic_guard(), "runtime_profile": "private", "runtime_name": "runtime"} "guard": synthetic_guard(), "runtime_profile": "private", "runtime_name": "runtime"}
receipt = {"admission": synthetic_admission(), "source_before": {"x": "a"}, "source_after": {"x": "a"}, receipt = {"admission": synthetic_admission(), "source_before": {"x": "a"}, "source_after": {"x": "a"},
@@ -475,7 +614,8 @@ class ReportTests(unittest.TestCase):
"time.sleep(0.15)\n" "time.sleep(0.15)\n"
"if mode=='timeout': time.sleep(5)\n" "if mode=='timeout': time.sleep(5)\n"
"for i in range(0 if mode=='missing' else 2 if mode=='duplicate' else 1):\n" "for i in range(0 if mode=='missing' else 2 if mode=='duplicate' else 1):\n"
" print('P28_01_PERF_REPORT='+json.dumps({'pid':os.getpid()+int(mode=='pid')}),flush=True)\n" " print('P28_01_PERF_REPORT='+json.dumps({'pid':os.getpid()+int(mode=='pid'),'main_added':False,'audio_owner_present':False}),flush=True)\n"
"print('P28_01_AUDIO_CLOSE='+json.dumps({'coordinator':False,'reason':'main_not_added'}),flush=True)\n"
"sys.exit(3 if mode=='exit' else 0)\n") "sys.exit(3 if mode=='exit' else 0)\n")
with tempfile.TemporaryDirectory() as temp: with tempfile.TemporaryDirectory() as temp:
base = Path(temp) base = Path(temp)