| … | … | @@ -0,0 +1,200 @@ |
| 1 | extends 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 | |
| 17 | const ShapeFold := preload("res://scenes/maps/shapes/param/shape_fold.gd") |
| 18 | const OpFold := preload("res://scenes/maps/op_fold.gd") |
| 19 | const ModFold := preload("res://scenes/maps/mod_fold.gd") |
| 20 | const CollectionFold := preload("res://scenes/maps/collection_fold.gd") |
| 21 | |
| 22 | var persist: Node |
| 23 | var level: Node |
| 24 | var map: Node3D |
| 25 | |
| 26 | |
| 27 | func _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 | |
| 71 | func _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 | |
| 78 | func _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 | |
| 87 | func _med(ts: Array) -> float: |
| 88 | ts.sort() |
| 89 | return ts[ts.size() / 2] |
| 90 | |
| 91 | |
| 92 | func _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)]) |