extends Node
## GATE: the hitch logger catches a long frame and puts the time in the right
## phase (hyper-games #93 §2 -- ashatron's 210-613 ms frames).
##
## "$G" --path . --rendering-driver vulkan res://scenes/test/gate_hitch_log.tscn
##
## Needs a window: the draw phase is timed from RenderingServer's frame_pre_draw /
## frame_post_draw, which a headless run never emits.
##
## After a 3 s baseline it injects, one frame each, a known 150 ms stall:
## 1. in a script's _process -> a hitch with process_ms >= 140
## 2. in a _physics_process -> a hitch with physics_ms >= 140
## 3. in a frame_pre_draw handler -> a hitch with draw_ms >= 140
## 4. 400 nodes added + 120 ms in _process -> nodes_added >= 400
## 5. in a call_deferred -> process_ms >= 140 (deferred calls run
## in SceneTree::process, outside _process)
## 6. in a coroutine resumed on process_frame -> process_ms >= 140 (how a map
## change from a script's await lands)
## and checks that the ordinary frames in between were NOT logged as hitches.
## Writes to a scratch log, never the player's user://hitches.log.
const HitchLog := preload("res://scenes/game/hitch_log.gd")
const STALL_MS := 150
var _log: Node = null
var _stall_process := false
var _stall_physics := false
var _stall_draw := false
var _burst := false
var _fails := 0
func _ready() -> void:
_run.call_deferred()
func _process(_delta: float) -> void:
if _stall_process:
_stall_process = false
OS.delay_msec(STALL_MS)
if _burst:
_burst = false
var holder := Node.new()
holder.name = "BurstHolder"
add_child(holder)
for i in 400:
holder.add_child(Node.new())
OS.delay_msec(120)
func _physics_process(_delta: float) -> void:
if _stall_physics:
_stall_physics = false
OS.delay_msec(STALL_MS)
func _stall_deferred() -> void:
OS.delay_msec(STALL_MS)
func _on_pre_draw() -> void:
if _stall_draw:
_stall_draw = false
OS.delay_msec(STALL_MS)
func _frames(n: int) -> void:
for i in n:
await get_tree().process_frame
func _expect(label: String, field: String, minimum: float) -> void:
await _frames(20)
var hs: Array = _log.call("get_hitches")
if hs.is_empty():
print("[GATE-HITCH] FAIL %s: no hitch logged" % label)
_fails += 1
return
var h: Dictionary = hs[-1]
var v := float(h.get(field, -1.0))
var ok := v >= minimum
print("[GATE-HITCH] %s %s: %s=%.1f (wall %.1f, phys %.1f proc %.1f draw %.1f other %.1f)" % [
"PASS" if ok else "FAIL", label, field, v, float(h.get("wall_ms", -1)),
float(h.get("physics_ms", -1)), float(h.get("process_ms", -1)),
float(h.get("draw_ms", -1)), float(h.get("other_ms", -1))])
if not ok:
_fails += 1
func _run() -> void:
_log = HitchLog.new()
_log.set("log_path", "user://gate_hitch_log.jsonl")
get_tree().root.add_child(_log)
# Connected AFTER the logger's own handler, so the stall lands inside its
# pre_draw -> post_draw window.
RenderingServer.frame_pre_draw.connect(_on_pre_draw)
await _frames(180)
var baseline := (_log.call("get_hitches") as Array).size()
print("[GATE-HITCH] baseline hitches: %d" % baseline)
_stall_process = true
await _expect("process stall", "process_ms", 140.0)
_stall_physics = true
await _expect("physics stall", "physics_ms", 140.0)
_stall_draw = true
await _expect("draw stall", "draw_ms", 140.0)
_burst = true
await _expect("node burst", "nodes_added", 400.0)
_stall_deferred.call_deferred()
await _expect("deferred-call stall", "process_ms", 140.0)
await get_tree().process_frame
OS.delay_msec(STALL_MS)
await _expect("process_frame coroutine stall", "process_ms", 140.0)
var total := (_log.call("get_hitches") as Array).size()
if total - baseline != 6:
print("[GATE-HITCH] FAIL: expected exactly 6 new hitches, got %d" % (total - baseline))
_fails += 1
print("RESULT: %s" % ("PASS" if _fails == 0 else "FAIL"))
get_tree().quit(0 if _fails == 0 else 1)