From 65534cfd1581e1896991e6fc268a0c4f334b5f57 Mon Sep 17 00:00:00 2001 From: Takeshi Date: Mon, 19 Oct 2026 20:29:04 +0900 Subject: [PATCH] P06-01: Show A06 reserve capacity in HUD, add new-PID and perf evidence Co-Authored-By: Claude Opus 5.5 --- Makefile | 2 + .../execution/p06-01-a06/perf-r01.json | 78 ++++++++++++ .../execution/p06-01-a06/perf_probe.gd | 55 +++++++++ scenes/main/main.gd | 4 +- tests/s2/test_mana_reserve_new_pid.gd | 111 ++++++++++++++++++ 5 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p06-01-a06/perf-r01.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p06-01-a06/perf_probe.gd create mode 100644 tests/s2/test_mana_reserve_new_pid.gd diff --git a/Makefile b/Makefile index 6a9a092..f572da3 100644 --- a/Makefile +++ b/Makefile @@ -65,6 +65,8 @@ test-s2: $(GODOT) --headless --path . --script res://tests/s2/test_mana_network_service.gd $(GODOT) --headless --path . --script res://tests/s2/test_mana_reserve_cycle.gd $(GODOT) --headless --path . --script res://tests/s2/test_runtime_mana_reserve.gd + $(GODOT) --headless --path . --script res://tests/s2/test_mana_reserve_new_pid.gd -- writer + $(GODOT) --headless --path . --script res://tests/s2/test_mana_reserve_new_pid.gd -- reader $(GODOT) --headless --path . --script res://tests/s2/test_runtime_operations_cycle.gd test-s3: diff --git a/docs/plans/original-completion-2026-10-02/execution/p06-01-a06/perf-r01.json b/docs/plans/original-completion-2026-10-02/execution/p06-01-a06/perf-r01.json new file mode 100644 index 0000000..5ff176f --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p06-01-a06/perf-r01.json @@ -0,0 +1,78 @@ +{ + "probe": "perf_probe.gd", + "baseline_source": "c366012 (autoload/simulation_runtime.gd, scripts/mana/mana_network_service.gd swapped in place)", + "candidate_source": "working tree after ae0f998 + HUD change", + "runs": [ + { + "side": "BASE", + "minutes": 1440, + "save_ms_avg": 18.752, + "save_ms_max": 20.915, + "snapshot_ms_avg": 0.38877, + "tick_day_ms_avg": 1005.434, + "tick_day_ms_max": 1022.47, + "trials": 5 + }, + { + "side": "CAND", + "minutes": 1440, + "save_ms_avg": 19.2478, + "save_ms_max": 21.626, + "snapshot_ms_avg": 0.39343, + "tick_day_ms_avg": 1013.9146, + "tick_day_ms_max": 1037.407, + "trials": 5 + }, + { + "side": "BASE", + "minutes": 1440, + "save_ms_avg": 19.9272, + "save_ms_max": 22.122, + "snapshot_ms_avg": 0.39169, + "tick_day_ms_avg": 1010.7658, + "tick_day_ms_max": 1021.417, + "trials": 5 + }, + { + "side": "CAND", + "minutes": 1440, + "save_ms_avg": 16.6394, + "save_ms_max": 20.276, + "snapshot_ms_avg": 0.39521, + "tick_day_ms_avg": 1016.2724, + "tick_day_ms_max": 1036.077, + "trials": 5 + }, + { + "side": "BASE", + "minutes": 1440, + "save_ms_avg": 18.8624, + "save_ms_max": 21.534, + "snapshot_ms_avg": 0.38799, + "tick_day_ms_avg": 1007.718, + "tick_day_ms_max": 1034.979, + "trials": 5 + }, + { + "side": "CAND", + "minutes": 1440, + "save_ms_avg": 15.2772, + "save_ms_max": 17.564, + "snapshot_ms_avg": 0.40309, + "tick_day_ms_avg": 1012.192, + "tick_day_ms_max": 1053.282, + "trials": 5 + } + ], + "base_mean": { + "tick_day_ms_avg": 1007.973, + "snapshot_ms_avg": 0.389, + "save_ms_avg": 19.181 + }, + "cand_mean": { + "tick_day_ms_avg": 1014.126, + "snapshot_ms_avg": 0.397, + "save_ms_avg": 17.055 + }, + "scope": "Ordinary new-game Runtime only, 1440 minutes x5 trials x3 runs, snapshot x20, disk save. Not rendering, Operations-heavy fixtures, or Windows." +} \ No newline at end of file diff --git a/docs/plans/original-completion-2026-10-02/execution/p06-01-a06/perf_probe.gd b/docs/plans/original-completion-2026-10-02/execution/p06-01-a06/perf_probe.gd new file mode 100644 index 0000000..a0ccb70 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p06-01-a06/perf_probe.gd @@ -0,0 +1,55 @@ +extends SceneTree + +## P06-01 A06 performance regression probe. Same script runs on the baseline +## and candidate checkouts; prints one JSON line. Measures ordinary Runtime +## minute ticks, full snapshots and disk saves; not rendering or Windows. +const TRIALS := 5 +const MINUTES := 1440 +const SNAPSHOTS := 20 + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + var tick_ms: Array = [] + var snapshot_ms: Array = [] + var save_ms: Array = [] + for trial: int in TRIALS: + var clock: Variant = load("res://autoload/game_clock.gd").new() + var state: Variant = load("res://autoload/game_state.gd").new() + var runtime: Variant = load("res://autoload/simulation_runtime.gd").new() + clock.reset(1, 6, 0) + state.reset() + runtime.initialize(clock, state, 5505 + trial) + clock.set_paused(true) + var start := Time.get_ticks_usec() + clock.advance_minutes(MINUTES) + tick_ms.append((Time.get_ticks_usec() - start) / 1000.0) + start = Time.get_ticks_usec() + var snapshot: Dictionary + for i: int in SNAPSHOTS: + snapshot = runtime.create_snapshot() + snapshot_ms.append((Time.get_ticks_usec() - start) / 1000.0 / SNAPSHOTS) + var path := "user://saves/p06_01_perf_probe_%d" % trial + start = Time.get_ticks_usec() + var saved: Dictionary = SaveService.save_snapshot(path, snapshot) + save_ms.append((Time.get_ticks_usec() - start) / 1000.0) + SaveService.delete_save(path) + if not saved[&"ok"]: + push_error("save failed") + runtime.free(); clock.free(); state.free() + print("P06_01_PERF=" + JSON.stringify({ + "trials": TRIALS, "minutes": MINUTES, + "tick_day_ms_avg": _avg(tick_ms), "tick_day_ms_max": tick_ms.max(), + "snapshot_ms_avg": _avg(snapshot_ms), "save_ms_avg": _avg(save_ms), "save_ms_max": save_ms.max(), + })) + quit(0) + + +func _avg(values: Array) -> float: + var total := 0.0 + for value: float in values: + total += value + return total / values.size() diff --git a/scenes/main/main.gd b/scenes/main/main.gd index fff37f0..d304cc9 100644 --- a/scenes/main/main.gd +++ b/scenes/main/main.gd @@ -2793,6 +2793,7 @@ func _phase_name(phase: GameClock.Phase) -> String: return "夜" +const MANA_RESERVE := preload("res://scripts/mana/mana_reserve_cycle.gd") const RESOURCE_TITLES := {&"gold": "所持金", &"mana": "マナ", &"materials": "資材", &"faith": "信仰"} func _resource_title(resource_name: StringName) -> String: @@ -2805,8 +2806,9 @@ func _on_resource_changed(resource_name: StringName, value: int) -> void: var label := resource_labels[resource_name] as Label label.text = "%s %d" % [_resource_title(resource_name), value] if resource_name == &"mana": + label.text += "/%d" % MANA_RESERVE.CAPACITY label.mouse_filter = Control.MOUSE_FILTER_STOP - label.tooltip_text = "残量0で全館停止。接客・調理・搬入・エレベーターと新規入館が停止します。\n残量と配電が回復すると再開します。破壊・摘発による休止は解除されません。" + label.tooltip_text = "世界樹が%d分ごとに%dマナを生成し、施設の維持マナを先に支払って残りを最大%dまで蓄えます。\n残量0で全館停止。接客・調理・搬入・エレベーターと新規入館が停止します。\n残量と配電が回復すると再開します。破壊・摘発による休止は解除されません。" % [MANA_RESERVE.INTERVAL_MINUTES, MANA_RESERVE.WORLD_TREE_GENERATION, MANA_RESERVE.CAPACITY] if value <= 0: label.text += "・停止" label.add_theme_color_override("font_color", Color("ffb095")) diff --git a/tests/s2/test_mana_reserve_new_pid.gd b/tests/s2/test_mana_reserve_new_pid.gd new file mode 100644 index 0000000..0f9661e --- /dev/null +++ b/tests/s2/test_mana_reserve_new_pid.gd @@ -0,0 +1,111 @@ +extends SceneTree + +## A06 reserve across a real process boundary. Run "-- writer" first, then +## "-- reader" in a new Godot process; the reader must reproduce the writer's +## continuation exactly from the disk save, and migrate a legacy save. +const Cycle = preload("res://scripts/mana/mana_reserve_cycle.gd") +const CURRENT_SAVE := "user://saves/a06_mana_new_pid_current" +const LEGACY_SAVE := "user://saves/a06_mana_new_pid_legacy" +const EXPECTED_PATH := "user://saves/a06_mana_new_pid_expected.json" +const CONTINUE_MINUTES := 75 +var _checks := 0 +var _failures := 0 + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + var mode := "writer" if not "reader" in OS.get_cmdline_user_args() else "reader" + print("S2: A06 Mana reserve new-PID %s (pid %d)" % [mode, OS.get_process_id()]) + if mode == "writer": + _write() + else: + _read() + print("%s: %d checks" % ["PASS" if _failures == 0 else "FAIL", _checks]) + quit(_failures) + + +func _new_runtime() -> Array: + var clock: Variant = load("res://autoload/game_clock.gd").new() + var state: Variant = load("res://autoload/game_state.gd").new() + var runtime: Variant = load("res://autoload/simulation_runtime.gd").new() + clock.reset(1, 6, 0) + state.reset() + runtime.initialize(clock, state, 4404) + clock.set_paused(true) + return [clock, state, runtime] + + +func _free(nodes: Array) -> void: + for node: Variant in nodes: + node.free() + + +func _write() -> void: + var nodes := _new_runtime() + var clock: Variant = nodes[0] + var state: Variant = nodes[1] + var runtime: Variant = nodes[2] + # Stop mid-interval so the reader must not settle the pending boundary early. + clock.advance_minutes(43) + var snapshot: Dictionary = runtime.create_snapshot() + _expect(SaveService.save_snapshot(CURRENT_SAVE, snapshot)[&"ok"], "writer saves current A06 reserve") + var legacy: Dictionary = snapshot.duplicate(true) + legacy[&"systems"][&"runtime"].erase(&"mana_reserve") + legacy[&"game_state"][&"resources"][&"mana"] = 0 + _expect(SaveService.save_snapshot(LEGACY_SAVE, legacy)[&"ok"], "writer saves legacy zero-reserve save") + var saved_mana: int = state.get_resource(&"mana") + clock.advance_minutes(CONTINUE_MINUTES) + var expected := { + "writer_pid": OS.get_process_id(), + "saved_minute": int(snapshot[&"clock"][&"absolute_minute"]), + "saved_mana": saved_mana, + "mana": state.get_resource(&"mana"), + "reserve": var_to_str(runtime.mana_reserve.create_snapshot()), + "digest": SimulationDigest.compute(runtime.create_snapshot()), + } + var file := FileAccess.open(EXPECTED_PATH, FileAccess.WRITE) + _expect(file != null, "writer records continuation") + if file != null: + file.store_string(JSON.stringify(expected)) + file.close() + _free(nodes) + + +func _read() -> void: + var expected: Variant = JSON.parse_string(FileAccess.get_file_as_string(EXPECTED_PATH)) + _expect(expected is Dictionary, "reader finds writer record") + if not expected is Dictionary: + return + _expect(int(expected["writer_pid"]) != OS.get_process_id(), "reader runs in a different process") + var nodes := _new_runtime() + var clock: Variant = nodes[0] + var state: Variant = nodes[1] + var runtime: Variant = nodes[2] + var loaded := SaveService.load_snapshot(CURRENT_SAVE) + _expect(loaded[&"ok"] and runtime.restore_snapshot(loaded[&"snapshot"]), "reader restores current save from disk") + _expect(clock.absolute_minute == int(expected["saved_minute"]) and state.get_resource(&"mana") == int(expected["saved_mana"]), "restored minute and reserve match the save") + clock.advance_minutes(CONTINUE_MINUTES) + _expect(state.get_resource(&"mana") == int(expected["mana"]), "continued reserve matches the writer") + _expect(var_to_str(runtime.mana_reserve.create_snapshot()) == String(expected["reserve"]), "continued reserve receipt matches the writer") + _expect(SimulationDigest.compute(runtime.create_snapshot()) == String(expected["digest"]), "whole continued state digest matches the writer") + + var legacy := SaveService.load_snapshot(LEGACY_SAVE) + _expect(legacy[&"ok"] and runtime.restore_snapshot(legacy[&"snapshot"]), "reader migrates legacy save from disk") + _expect(state.get_resource(&"mana") == 0 and runtime.mana_reserve.get_last_flow().is_empty(), "legacy explicit 0 stays stopped") + _expect(runtime.create_snapshot()[&"systems"][&"runtime"].has(&"mana_reserve"), "migrated state saves with the A06 reserve") + for path: String in [CURRENT_SAVE, LEGACY_SAVE]: + SaveService.delete_save(path) + DirAccess.remove_absolute(ProjectSettings.globalize_path(EXPECTED_PATH)) + _free(nodes) + + +func _expect(condition: bool, label: String) -> void: + _checks += 1 + if condition: + print(" ✓ %s" % label) + else: + _failures += 1 + push_error(" ✗ %s" % label)