User report: the graph "disappeared". Reproduced, and the cause was not the
graph at all — the panel's main thread was blocked hard enough that the browser
stopped responding to clicks.
Root cause, found by measuring rather than guessing:
rAF 2 FPS, setTimeout(0) lag 1353ms, with the graph rendering correctly the
whole time. The scene was repainting continuously at 60fps even though it is
static between the 10s data refreshes. Each repaint cost ~305ms on this
host's software GL, so the main thread never got a free slot.
Fixes:
- Render on demand. The ticker is registered but not started; it runs only while
an attack pulse is in flight or the user is dragging, and stops once the scene
is quiet. applyView() (the single funnel for pan/zoom/drag/refit) repaints
synchronously. Result: 2 FPS -> 73 FPS, 1353ms -> 2ms lag. That is faster than
a blank page in the same harness (29 FPS), which confirms the ticker was the
cost, not the scene complexity.
- Stop rebuilding Pixi objects every refresh. _redrawEdges destroyed and
recreated ~70 Graphics + ~36 Text each cycle; every new Text allocates a
canvas, rasterises glyphs and uploads a texture. Edges are now kept per
from->to key and only their geometry is redrawn; a label's text is re-set only
when the string actually changes. Same for node titles/subtitles.
- Bind tooltip handlers once, at node creation. Re-binding inside the refresh
loop added a pointerover listener every 10s, so a single hover after an hour
fired thousands of handlers.
Two correctness bugs fixed while in there:
- init() race. The Topology tab button calls showView('topo') AND loadTopo(), and
showView() itself calls loadTopo(), so two ran concurrently. The re-entry guard
tested `topoGraph && topoGraph.app`, but topoGraph was assigned BEFORE the
awaited init(), so app was still null and a second renderer was built. Both
cleared host.innerHTML and appended their own canvas, so the last init to
finish won the DOM while the bridge still pointed at the other — a live canvas
that was no longer on the page. Now guarded by a single-flight promise, and
the instance is published only after init() resolves.
- Dropped the dead topoLoaded flag left over from the SVG renderer.
Hierarchical drag: dragging a team now carries its 16 challenge nodes with it.
The parent->children index is built from the edge list, the same source the
lines are drawn from, so the drag hierarchy cannot disagree with the picture;
challenges missing from the edge list still attach by id prefix. Children
translate rigidly (verified: 0.000px deviation across all 16).
Verified from a cold panel restart: all 5 topology tests pass, 73 FPS, 2ms
lag, 37 nodes drawn, graph framed at 4 viewport widths, no page errors, and
platform SLA unregressed at 32/32.
39 lines
1.0 KiB
Bash
39 lines
1.0 KiB
Bash
#!/usr/bin/env bash
|
|
# Cold-start verification: restart the panel, run the whole topology suite, and
|
|
# confirm the platform SLA is unaffected. This is the check to run after any
|
|
# change to the topology renderer.
|
|
set -u
|
|
export PLAYWRIGHT_BROWSERS_PATH=/root/.cache/ms-playwright
|
|
|
|
echo "=== restart panel ==="
|
|
systemctl restart gemastik-panel
|
|
sleep 7
|
|
echo "gemastik-panel: $(systemctl is-active gemastik-panel)"
|
|
|
|
echo
|
|
echo "=== topology test suite ==="
|
|
bash /opt/gemastik18-final/panel/run_topo_tests.sh
|
|
suite=$?
|
|
|
|
echo
|
|
echo "=== platform SLA ==="
|
|
curl -sS "http://127.0.0.1:18081/api/public/scoreboard" | python3 -c "
|
|
import json, sys
|
|
d = json.load(sys.stdin)
|
|
ta = tt = 0
|
|
for t in d.get('teams', []):
|
|
a, n = t.get('alive', 0), t.get('total', 0)
|
|
ta += a; tt += n
|
|
print(f\" team {t.get('label')}: SLA {a}/{n}\")
|
|
print(f' TOTAL: {ta}/{tt}' + (f' ({100*ta/tt:.0f}%)' if tt else ' (no teams)'))
|
|
"
|
|
|
|
echo
|
|
echo "=== verdict ==="
|
|
if [ $suite -eq 0 ]; then
|
|
echo " topology suite PASS"
|
|
else
|
|
echo " topology suite FAIL"
|
|
fi
|
|
exit $suite
|