OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_keys_with_prefix.gd

74 lines · gd ref:
extends SceneTree
## GATE — GameState.keys_with_prefix() is LINEAR (2026-10-05: "delete is extremely
## slow" — 11 s hitches). It deduplicated with `k not in result` on an Array, a linear
## search per key: quadratic in the journal size (measured 1.9 / 11.3 / 42 ms at
## 500 / 1000 / 2000 records), and a delete calls it several times.
##
##   1. same answer: every matching key once, realtime ones first, nothing else
##   2. scaling: 4000 keys take < 8x the time of 1000 (linear ~4x, quadratic ~16x) —
##      a RATIO, so a loaded CI machine does not make it flaky
##
## Run: "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_keys_with_prefix.gd

var ok := true
var _ran := false


func _p(c: bool, m: String) -> void:
	print(("ok   " if c else "FAIL: ") + m)
	if not c:
		ok = false


func _process(_d: float) -> bool:
	if _ran:
		return true
	_ran = true
	var gs: Node = root.get_node_or_null("GameState")
	_p(gs != null, "GameState autoload present")
	if gs != null:
		_run(gs)
	print("GATE keys_with_prefix: %s" % ("PASS" if ok else "FAIL"))
	quit(0 if ok else 1)
	return true


func _fill(gs: Node, n: int) -> void:
	gs._realtime.clear()
	gs._durable.clear()
	for i in n:
		gs._durable["world:level:a:op:%d" % i] = i
	for i in n / 4:
		gs._realtime["world:level:a:op:%d" % i] = i          # overlaps the durable ones
		gs._realtime["world:level:rt:%d" % i] = i            # realtime only
	gs._durable["presence:x"] = 1                            # another prefix


func _time(gs: Node) -> float:
	var best := INF
	for i in 3:
		var t0 := Time.get_ticks_usec()
		gs.keys_with_prefix("world:level:")
		best = minf(best, float(Time.get_ticks_usec() - t0))
	return best


func _run(gs: Node) -> void:
	_fill(gs, 40)
	var got: Array = gs.keys_with_prefix("world:level:")
	var uniq := {}
	for k: String in got:
		uniq[k] = true
	_p(got.size() == uniq.size(), "1. every key once (%d keys, %d unique)" % [got.size(), uniq.size()])
	_p(got.size() == 40 + 10, "1. all the matching keys (%d, want 50)" % got.size())
	_p(not got.has("presence:x"), "1. nothing outside the prefix")
	_p(String(got[0]).begins_with("world:level:") and gs._realtime.has(String(got[0])), "1. realtime keys first, as before")

	_fill(gs, 1000)
	var t1 := _time(gs)
	_fill(gs, 4000)
	var t4 := _time(gs)
	_p(t4 < 8.0 * t1, "2. 4x the keys costs %.1fx the time (want < 8; quadratic ~16)" % (t4 / maxf(t1, 1.0)))
	gs._realtime.clear()
	gs._durable.clear()