P06-01: Show A06 reserve capacity in HUD, add new-PID and perf evidence
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ae0f998ff4
commit
65534cfd15
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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()
|
||||
+3
-1
@@ -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"))
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user