122 lines
4.8 KiB
GDScript
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()
|