Разделить частоту обновления часов и расчёта симуляции
Счётчик времени UI обновляется до 10 Гц по запрошенному времени, а тяжёлые циклы по-прежнему запускаются с адаптивным интервалом и обрабатывают весь накопленный промежуток. Обновить тесты и описание модели.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user