Add benchmark for saved state performance

- Introduced `benchmark_saved_state.gd` to evaluate the performance of the saved state restoration process.
- Implemented JSON integer restoration to handle large integers accurately.
- Added command-line argument parsing for specifying the path to the diagnostic JSON file and optional delta seconds.
- Conducted performance tests at different speeds and logged the elapsed time for six consecutive steps.
- Created a new UID file for the benchmark script.
This commit is contained in:
Kirill
2026-09-30 22:46:26 +03:00
parent fb8a3f5658
commit fad862d618
16 changed files with 135 additions and 2399 deletions
+19
View File
@@ -1,5 +1,24 @@
# Проверки поэтапного переноса из Owniverse.U
## Производительность на сохранении
```sh
godot --headless --path Owniverse.GI --script res://tests/benchmark_saved_state.gd -- /tmp/save.adventure.json
```
Читает диагностический JSON формата 3 в отдельные экземпляры мира и замеряет
шесть последовательных шагов на скоростях 1, 4, 6. Запись сохранений не вызывается.
Последним аргументом можно задать длительность шага в реальных секундах
(по умолчанию 1/60). Это время расчёта симуляции, без отрисовки интерфейса.
При проверке регрессии на копии сохранения с 120 партиями шаг на скорости 6
занимал около 1,3 секунды. Главная причина — перебор пустых завершённых окон
коротких взрывов; дополнительная — повторное сокращение уже сокращённых дробей.
После пропуска таких окон и устранения повторных НОД замеры составили 90–99 мс
на шаг 1/60 секунды. Это ускорение примерно в 14 раз, но ещё не бюджет 60 FPS.
Размер партии, точность чисел и правила эволюции не изменены. Разреженные
сценарии дополнительно сравниваются с полным расчётом в `test_short_lived_events.gd`.
## Точная арифметика
`godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd`
+47
View File
@@ -0,0 +1,47 @@
extends SceneTree
## Read-only benchmark. Loads a diagnostic JSON, never invokes SaveManager.
const E = preload("res://world/owniverse/math/exact.gd")
func _initialize():
call_deferred("run")
func restore_json_integers(value):
# JSON parses ordinary integer tokens as float. Large integers are $exact.
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 run():
var args = OS.get_cmdline_user_args()
if args.is_empty():
print("Usage: --script tests/benchmark_saved_state.gd -- /path/to/save.adventure.json [delta_seconds]")
quit(1)
return
var data = JSON.parse_string(FileAccess.get_file_as_string(args[0]))
if not data is Dictionary or data.get("format_version") != 3:
push_error("Expected diagnostic JSON format 3")
quit(1)
return
var initial: Dictionary = E.decode(restore_json_integers(data)).state
var delta: float = args[1].to_float() if args.size() > 1 else 1.0 / 60.0
if not is_finite(delta) or delta <= 0:
quit(1)
return
for speed in [1, 4, 6]:
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 = speed
var elapsed: Array = []
for cycle in 6:
var start: int = Time.get_ticks_usec()
world.evolute(delta)
elapsed.append((Time.get_ticks_usec() - start) / 1000.0)
print("Speed %d, six consecutive %s-second steps, ms: %s" % [speed, delta, elapsed])
for entity in world.catalog.entities.values(): entity.free()
world.free()
quit()
+1
View File
@@ -0,0 +1 @@
uid://dji1apht4pn5y
+15
View File
@@ -75,5 +75,20 @@ func run() -> void:
for batch in fast.state.batches.SN: total = E.add(total, batch.amount)
check(E.eq(total, 10000), "Direct completions plus survivors conserve event count")
dispose(fast)
# Sparse explosions: thousands of time windows but very few actual events.
# Exact interval edges and partial final windows exercise the jump boundary.
for count in [1, 3, 17]:
for year in [500, 1001]:
fast = fixture(false)
var legacy = fixture(true)
fast.state.amount.RHG = count
fast.state.batches.RHG = [{"amount": count, "start_year": E.of("0.25"), "end_year": E.of("1000.25")}]
legacy.state = fast.state.duplicate(true)
fast.evolution.evolve_batches(fast.state, year)
legacy.evolution.evolve_batches(legacy.state, year)
for field in fast.state:
check(equivalent(fast.state[field], legacy.state[field]), "Sparse %s at %s: %s" % [count, year, field])
dispose(fast)
dispose(legacy)
print(("Short-lived events: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))