Files
owniverse/tests/benchmark_dynamic_cadence.gd
Kirill 273093bd9e Разделить частоту обновления часов и расчёта симуляции
Счётчик времени UI обновляется до 10 Гц по запрошенному времени, а тяжёлые циклы по-прежнему запускаются с адаптивным интервалом и обрабатывают весь накопленный промежуток. Обновить тесты и описание модели.
2026-10-17 23:41:18 +03:00

122 lines
4.8 KiB
GDScript

extends SceneTree
## Paired, read-only throughput benchmark for adaptive 5/4/3/2 Hz cadence.
const E = preload("res://world/owniverse/math/exact.gd")
const SAVE_PATHS: Array[String] = [
"res://tests/artifacts/all_objects_speed6.adventure.json",
"res://tests/artifacts/many_objects_fragmented_stars_cycle.adventure.json"
]
const RESULT_PATH := "res://tests/artifacts/dynamic_cadence_results.json"
var cycles: int = 40
var repeats: int = 2
func _initialize() -> void:
call_deferred("run")
func restore_json_integers(value):
if value is float and value == floorf(value) and absf(value) <= 9007199254740991:
return int(value)
if value is Dictionary:
for key in value: value[key] = restore_json_integers(value[key])
elif value is Array:
for i in value.size(): value[i] = restore_json_integers(value[i])
return value
func create_world(initial: Dictionary, use_adaptive_cadence: bool):
var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
world.state = initial.duplicate(true)
world.speed = 6
world.adaptive_cadence = use_adaptive_cadence
world.frame_step_seconds = 0.2
world.cadence_level = 0
return world
func dispose(world) -> void:
for entity in world.catalog.entities.values(): entity.free()
world.free()
func measure(initial: Dictionary, adaptive: bool, save_name: String, repeat_index: int) -> Dictionary:
var world = create_world(initial, adaptive)
var elapsed_usec: int = 0
var requested_seconds: float = 0.0
var step_counts: Dictionary = {}
var samples: Array[float] = []
var initial_clock: Variant = world.state.time.datetime
for i in cycles:
var step: float = world.frame_step_seconds
step_counts[str(step)] = int(step_counts.get(str(step), 0)) + 1
requested_seconds += step
var started: int = Time.get_ticks_usec()
world.evolute(step)
var usec: int = Time.get_ticks_usec() - started
elapsed_usec += usec
samples.append(float(usec) / 1000.0)
var applied_years: Variant = E.subtract(world.state.time.datetime, initial_clock)
var expected_years: float = requested_seconds * 100000.0
assert(absf(E.approx(applied_years) - expected_years) <= maxf(1.0, expected_years * 1e-10), "Clock advance differs from accumulated cadence interval")
var validation: String = root.get_node("SaveManager").validate_data({
"format_version": 6,
"app_version": "0.7.0",
"ui": {"current_page": 0},
"state": world.state
})
assert(validation.is_empty(), validation)
samples.sort()
var result: Dictionary = {
"save": save_name,
"mode": "adaptive_5_4_3_2" if adaptive else "fixed_5",
"repeat": repeat_index,
"cycles": cycles,
"mean_ms_per_cycle": float(elapsed_usec) / (cycles * 1000.0),
"p95_ms": samples[int(ceil(samples.size() * 0.95)) - 1],
"max_ms": samples.back(),
"cpu_cycles_per_second": float(cycles) * 1000000.0 / elapsed_usec,
"simulated_seconds_per_cpu_second": requested_seconds * 1000000.0 / elapsed_usec,
"requested_simulation_seconds": requested_seconds,
"applied_game_years": E.fixed(applied_years),
"cadence_steps": step_counts,
"final_cadence_hz": 1.0 / world.frame_step_seconds,
"final_adaptive_quality": world.adaptive_quality
}
print("CADENCE_RESULT ", JSON.stringify(result))
dispose(world)
return result
func run() -> void:
for arg in OS.get_cmdline_user_args():
if arg.begins_with("cycles="): cycles = maxi(1, arg.trim_prefix("cycles=").to_int())
if arg.begins_with("repeats="): repeats = maxi(1, arg.trim_prefix("repeats=").to_int())
var inputs: Dictionary = {}
for path in SAVE_PATHS:
var parsed = JSON.parse_string(FileAccess.get_file_as_string(path))
assert(parsed is Dictionary and parsed.get("format_version") == 6, "Invalid benchmark save: " + path)
var payload: Dictionary = E.decode(restore_json_integers(parsed))
var validation: String = root.get_node("SaveManager").validate_data(payload)
assert(validation.is_empty(), validation)
inputs[path.get_file()] = payload.state
var results: Array[Dictionary] = []
for save_name in inputs:
var initial: Dictionary = inputs[save_name]
# Alternate order to limit machine warm-up and thermal bias.
for repeat_index in repeats:
if repeat_index % 2 == 0:
results.append(measure(initial, false, save_name, repeat_index))
results.append(measure(initial, true, save_name, repeat_index))
else:
results.append(measure(initial, true, save_name, repeat_index))
results.append(measure(initial, false, save_name, repeat_index))
var output := FileAccess.open(RESULT_PATH, FileAccess.WRITE)
assert(output != null)
output.store_string(JSON.stringify({
"machine": OS.get_model_name(),
"godot": Engine.get_version_info(),
"speed": 6,
"cycles_per_run": cycles,
"repeats": repeats,
"input_saves_unchanged": true,
"results": results
}, "\t", true, true))
output.close()
print("Wrote paired cadence results: ", RESULT_PATH)
quit()