OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / bench_bots.gd

84 lines · gd ref:
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])