OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps commits / 1c0d06d

#127: _probe_fold_split — where a full refold's time goes, stage by stage
A probe (not a gate): partition / ShapeFold.expand / apply_journal / rebuild (+ DCLevel.get_stats phases) / the folded listeners, median of N; PROBE_CASE bundle20 | bundle40 | spheres20 (PROBE_N spheres at 32 x 16); PROBE_DUMP writes high-plane brushes' planes for a standalone libdc benchmark. Findings on #127. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1c0d06d0abbb4b796e80c33366e43de596daa3e8
by plankatron <fleeky@gmail.com> · 2026-10-19T16:45:02.000Z

Changed files (2) +206 -0

scenes/test/_probe_fold_split.gd +200 -0
……@@ -0,0 +1,200 @@
1extends Node
2## PROBE (not a gate) — WHERE A FULL REFOLD'S TIME GOES (hyper-games #127). One room per
3## process, PROBE_CASE:
4## bundle20 a 20-cable bundle (as _probe_cable_fold_time)
5## bundle40 the same with 40 cables — the RULER CHECK: the cable-attributable stages
6## should about double (a ruler that does not scale is not measuring cables)
7## spheres20 20 spheres at 32 x 16 (the user's "high poly faceted spheres")
8## Then the brush half of LevelPersistence.refold, STAGE BY STAGE as refold runs it
9## (records partition -> ShapeFold.expand -> level.apply_journal -> level.rebuild), each
10## the median of 3, plus refold() itself the same way for the total. No ops / mods in
11## these rooms, so those stages are empty (checked).
12## DIGEST: a hash of every live brush's planes after the fold — the correctness golden for
13## a change that must not alter geometry (same digest before and after it).
14##
15## RUN: PROBE_CASE=bundle20 CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . res://scenes/test/_probe_fold_split.tscn
16
17const ShapeFold := preload("res://scenes/maps/shapes/param/shape_fold.gd")
18const OpFold := preload("res://scenes/maps/op_fold.gd")
19const ModFold := preload("res://scenes/maps/mod_fold.gd")
20const CollectionFold := preload("res://scenes/maps/collection_fold.gd")
21
22var persist: Node
23var level: Node
24var map: Node3D
25
26
27func _ready() -> void:
28 var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
29 gs.route("world:level:*", 1, "peer")
30 var keet: Node = get_tree().root.get_node_or_null("/root/KeetService")
31 if keet != null:
32 keet.current_member_id = "probe"
33 get_tree().root.get_node("/root/GameMode").set_mode("build")
34 map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
35 add_child(map)
36 for i in 6:
37 await get_tree().process_frame
38 persist = map.get("_persist")
39 level = map.get("level")
40 var case := OS.get_environment("PROBE_CASE") if OS.get_environment("PROBE_CASE") != "" else "bundle20"
41 match case:
42 "bundle20", "bundle40":
43 _bundle(20 if case == "bundle20" else 40)
44 "spheres20":
45 _spheres(int(OS.get_environment("PROBE_N")) if OS.get_environment("PROBE_N") != "" else 20)
46 _:
47 print("PROBE unknown case %s" % case)
48 get_tree().quit(1)
49 return
50 print("PROBE step: room built %d ms" % Time.get_ticks_msec())
51 persist.refold()
52 # PROBE_DUMP=<file>: write the planes of every brush with > 64 planes (nx ny nz d per
53 # line, a blank line between brushes) — the input for a standalone libdc benchmark.
54 if OS.get_environment("PROBE_DUMP") != "":
55 var fo := FileAccess.open(OS.get_environment("PROBE_DUMP"), FileAccess.WRITE)
56 for b: int in (level.get_live_brush_ids() as PackedInt32Array):
57 var pl: PackedFloat32Array = level.get_brush(b)["planes"]
58 if pl.size() / 4 > 64:
59 for i in pl.size() / 4:
60 fo.store_line("%.9f %.9f %.9f %.9f" % [pl[i * 4], pl[i * 4 + 1], pl[i * 4 + 2], pl[i * 4 + 3]])
61 fo.store_line("")
62 fo.close()
63 print("PROBE dumped to %s" % OS.get_environment("PROBE_DUMP"))
64 get_tree().quit(0)
65 return
66 print("PROBE step: first refold done %d ms" % Time.get_ticks_msec())
67 _split(case)
68 get_tree().quit(0)
69
70
71func _bundle(n: int) -> void:
72 var A := {"pts": PackedVector3Array([Vector3(400, 0, 300), Vector3(400, 0, 302), Vector3(400, 2, 302), Vector3(400, 2, 300)]), "n": Vector3.RIGHT}
73 var B := {"pts": PackedVector3Array([Vector3(410, 0, 300), Vector3(410, 2, 300), Vector3(410, 2, 302), Vector3(410, 0, 302)]), "n": Vector3.LEFT}
74 map.cable_bundle([A, B], {"count": n, "random": true, "seed": 1, "tension": 1.0, "jitter": 0.3, "slack": 1.0},
75 {"radius": 0.03})
76
77
78func _spheres(n: int) -> void:
79 persist.begin_gesture("spheres")
80 for k in n:
81 persist.record("shape.create", {"type": "sphere", "gv": 1, "params": {"segments": 32, "rings": 16},
82 "frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, Vector3(300 + (k % 5) * 6, 0, 300 + (k / 5) * 6))),
83 "size": [4, 4, 4], "material_id": 2, "level_id": 0})
84 persist.end_gesture()
85
86
87func _med(ts: Array) -> float:
88 ts.sort()
89 return ts[ts.size() / 2]
90
91
92func _split(case: String) -> void:
93 var part := []
94 var ex := []
95 var ap := []
96 var rb := []
97 var tot := []
98 var brush_recs: Array = []
99 var extra := 0
100 var reps := int(OS.get_environment("PROBE_REPS")) if OS.get_environment("PROBE_REPS") != "" else 3
101 var phases := {}
102 for rep in reps:
103 var t0 := Time.get_ticks_usec()
104 brush_recs = []
105 var shape_recs: Array = []
106 var curve_recs: Array = []
107 extra = 0
108 for r: Dictionary in persist.records(false):
109 var rk := String(r.get("kind", ""))
110 if LevelPersistence.CurveFold.handles(rk):
111 curve_recs.append(r)
112 elif CollectionFold.handles(rk):
113 continue
114 elif ShapeFold.handles(rk):
115 shape_recs.append(r)
116 elif OpFold.handles(rk) or ModFold.handles(rk):
117 extra += 1
118 else:
119 brush_recs.append(r)
120 print("PROBE step: rep %d partitioned %d ms" % [rep, Time.get_ticks_msec()])
121 var curves := LevelPersistence.CurveFold.live(curve_recs)
122 part.append(float(Time.get_ticks_usec() - t0) / 1000.0)
123 t0 = Time.get_ticks_usec()
124 brush_recs.append_array(ShapeFold.expand(shape_recs, {}, curves))
125 ex.append(float(Time.get_ticks_usec() - t0) / 1000.0)
126 t0 = Time.get_ticks_usec()
127 print("PROBE step: rep %d expanded %d ms" % [rep, Time.get_ticks_msec()])
128 level.apply_journal(brush_recs)
129 print("PROBE step: rep %d applied %d ms" % [rep, Time.get_ticks_msec()])
130 ap.append(float(Time.get_ticks_usec() - t0) / 1000.0)
131 t0 = Time.get_ticks_usec()
132 level.rebuild()
133 rb.append(float(Time.get_ticks_usec() - t0) / 1000.0)
134 var st: Dictionary = level.get_stats()
135 for k: String in ["ms_split", "ms_clip", "ms_mesh", "ms_upload", "ms_collide", "ms_pocket", "build_ms"]:
136 (phases.get_or_add(k, []) as Array).append(float(st.get(k, -1.0)))
137 for rep2 in reps:
138 persist.set("_brush_ver_folded", -1) # force the brush half
139 persist.set("_ix_tail_ok", false)
140 var t1 := Time.get_ticks_usec()
141 persist.refold()
142 tot.append(float(Time.get_ticks_usec() - t1) / 1000.0)
143 var ids: PackedInt32Array = level.get_live_brush_ids()
144 var planes := 0
145 var h := HashingContext.new()
146 h.start(HashingContext.HASH_SHA256)
147 var keyed: Array = []
148 for b: int in ids:
149 keyed.append([String(level.get_brush_key(b)), b])
150 keyed.sort_custom(func(x: Array, y: Array) -> bool: return String(x[0]) < String(y[0]))
151 for kb: Array in keyed:
152 var pl: PackedFloat32Array = level.get_brush(int(kb[1]))["planes"]
153 planes += pl.size() / 4
154 h.update(String(kb[0]).to_utf8_buffer())
155 h.update(pl.to_byte_array())
156 print("PROBE %s: %d live brushes, %d planes, %d brush records, ops/mods %d" % [case, ids.size(), planes, brush_recs.size(), extra])
157 print("PROBE %s: partition %.1f | ShapeFold.expand %.1f | apply_journal %.1f | rebuild %.1f | (sum %.1f) | refold() %.1f ms"
158 % [case, _med(part), _med(ex), _med(ap), _med(rb), _med(part) + _med(ex) + _med(ap) + _med(rb), _med(tot)])
159 var ph := []
160 for k2: String in phases:
161 ph.append("%s %.1f" % [k2, _med(phases[k2])])
162 print("PROBE %s: rebuild phases (get_stats, median): %s" % [case, ", ".join(ph)])
163 # THE REST OF refold(): the objects half, and the `folded` listeners (the editor's
164 # outliner, curve view, panels … redraw on every fold).
165 var ob := []
166 var em := []
167 var lf: Dictionary = persist.get("_last_fold")
168 for rep3 in reps:
169 var t2 := Time.get_ticks_usec()
170 var objs: Object = persist.get("objects")
171 if objs != null:
172 objs.apply_journal(persist.call("_object_records"))
173 ob.append(float(Time.get_ticks_usec() - t2) / 1000.0)
174 t2 = Time.get_ticks_usec()
175 persist.folded.emit(lf)
176 em.append(float(Time.get_ticks_usec() - t2) / 1000.0)
177 var lis: Array = []
178 for c: Dictionary in persist.folded.get_connections():
179 lis.append("%s.%s" % [str((c["callable"] as Callable).get_object()).get_slice(":", 0), (c["callable"] as Callable).get_method()])
180 print("PROBE %s: objects.apply_journal %.1f | folded.emit %.1f ms | listeners %s" % [case, _med(ob), _med(em), lis])
181 var parts := {}
182 for rep4 in reps:
183 var cs: Array = [["brush_view", Callable(map, "_refresh_brush_view")], ["reselect", Callable(map, "_reselect_shape")],
184 ["curves", Callable(map, "_refresh_curves")]]
185 var ol: Variant = map.get("_outliner")
186 if ol != null and is_instance_valid(ol):
187 cs.append(["outliner", Callable(ol, "refresh")])
188 for c2: Dictionary in persist.folded.get_connections():
189 var cb: Callable = c2["callable"]
190 if cb.get_method() == "_on_folded":
191 cs.append(["history", cb.bind(lf)])
192 for pr: Array in cs:
193 var t3 := Time.get_ticks_usec()
194 (pr[1] as Callable).call()
195 (parts.get_or_add(String(pr[0]), []) as Array).append(float(Time.get_ticks_usec() - t3) / 1000.0)
196 var pl2 := []
197 for k3: String in parts:
198 pl2.append("%s %.1f" % [k3, _med(parts[k3])])
199 print("PROBE %s: folded listener parts (median): %s" % [case, ", ".join(pl2)])
200 print("PROBE %s: digest %s" % [case, h.finish().hex_encode().substr(0, 16)])
scenes/test/_probe_fold_split.tscn +6 -0
……@@ -0,0 +1,6 @@
1[gd_scene load_steps=2 format=3]
2
3[ext_resource type="Script" path="res://scenes/test/_probe_fold_split.gd" id="1"]
4
5[node name="FoldSplitProbe" type="Node"]
6script = ExtResource("1")