extends Node
## BENCHMARK (not a gate) — what pathwalker BOTS cost the peer that runs their brains
## (hyper-games #114 §7). The setup is gate_nav_end_to_end's, which is known to work:
## map_flat under a node named "Game" with a declared current_map (the path bots use to
## find a nav provider), the map's own DCNavVolume, and real bots/pathwalker.gd brains in
## WANDER mode — each picks a standable destination, routes to it (nav_find_path, the
## bot.nav counter) and walks it, forever.
##
## For each count N: N brains ticked at 60 Hz (the physics rate) for SAMPLE ticks; the
## brain CPU is timed here around _bot_tick, the route queries by PerfMetrics' bot.nav.
## Env: BENCH_COUNTS "0,10,100,500,1000" · BENCH_SAMPLE ticks per point (default 1200)
## Run: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . \
## res://scenes/test/bench_bots.tscn
## Output: one "BENCH bots ..." line per count.
class StubGame extends Node:
var current_map: Node = null
var _counts: Array = [0, 10, 100, 500, 1000]
var _sample := 1200
const DT := 1.0 / 60.0
func _ready() -> void:
if OS.get_environment("BENCH_COUNTS") != "":
_counts = []
for s: String in OS.get_environment("BENCH_COUNTS").split(","):
_counts.append(int(s))
if OS.get_environment("BENCH_SAMPLE") != "":
_sample = int(OS.get_environment("BENCH_SAMPLE"))
var game := StubGame.new()
game.name = "Game"
get_tree().root.add_child.call_deferred(game)
await get_tree().process_frame
var map: Node = (load("res://scenes/maps/map_flat.tscn") as PackedScene).instantiate()
game.add_child(map)
game.current_map = map
for i in 10:
await get_tree().process_frame
var chunks: int = int(map.call("nav_rebuild")) if map.has_method("nav_rebuild") else 0
var nav: Node = map.call("get_nav_volume") if map.has_method("get_nav_volume") else null
print("BENCH bots start: map_flat nav chunks=%d sample=%d ticks @60 Hz" % [chunks, _sample])
if nav == null or chunks <= 0:
print("BENCH bots ABORT: no nav volume on map_flat")
get_tree().quit(1)
return
var brain: GDScript = load("res://bots/pathwalker.gd")
for n: int in _counts:
_point(brain, nav, n)
await get_tree().process_frame
get_tree().quit(0)
func _point(brain: GDScript, nav: Node, n: int) -> void:
var bots: Array = []
for i in n:
var b: Object = brain.new()
b.set("bot_id", "bench%d" % i)
b.set("wander", true)
b.set("wander_pause", 0.0)
b.set("speed", 8.0)
b.set("_pos", nav.call("random_standable_point"))
b.set("_have_pos", true)
bots.append(b)
for t in 60: # warm-up: first routes, first legs
for b: Object in bots:
b.call("_bot_tick", DT)
SimStats.reset()
var t0 := Time.get_ticks_usec()
for t in _sample:
for b: Object in bots:
b.call("_bot_tick", DT)
var us := Time.get_ticks_usec() - t0
var snap: Dictionary = SimStats.snapshot()
var navk: Dictionary = snap.get("bot.nav", {})
var sim_s := float(_sample) * DT # simulated seconds
var legs := 0
for b: Object in bots:
legs += int(b.get("_legs"))
print("BENCH bots n=%d ticks=%d us_per_bot_tick=%.2f bot_ms_per_sim_s=%.2f nav_queries=%d nav_us_per_query=%.1f nav_ms_per_sim_s=%.2f legs=%d wall_s=%.2f" % [
n, n * _sample, float(us) / maxf(float(n * _sample), 1.0), float(us) / 1000.0 / sim_s,
int(navk.get("queries", 0)), float(navk.get("usec", 0)) / maxf(float(navk.get("queries", 0)), 1.0),
float(navk.get("usec", 0)) / 1000.0 / sim_s, legs, float(us) / 1e6])