🐛 Guard renders against heavy graphs; add ?safe escape hatch

A heavy file could freeze the tab on load-and-render despite the animation gate: the render guard counted nodes only, and the edge-bundling plugin is iteration-heavy in edges. Guard now also trips on edges (8000), edge bundling only activates at <= 300 edges, and /dbg/graph?safe disables auto-render entirely (counts + "Render anyway"), so a page that hung can always be re-entered with the session intact.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
This commit is contained in:
Álvaro Tejero Cantero 2026-07-17 00:33:21 +02:00
parent 82746bbf06
commit dd5c9b0cba
No known key found for this signature in database

View File

@ -570,9 +570,16 @@ Graph Console
// Legend glyph characters mirroring the G6 node types above.
const GLYPH_CHARS = { diamond: "◆", rect: "■", hexagon: "⬢",
circle: "●", triangle: "▲", star: "★" };
// Above this many nodes the view is not rendered automatically; the
// "Render anyway" button forces it (~1.5 s per 2k nodes, measured).
// Above this many nodes/edges the view is not rendered automatically;
// the "Render anyway" button forces it (~1.5 s per 2k nodes, measured;
// edges gate the guard too since plugin/combo cost scales with them).
// Escape hatch: /dbg/graph?safe disables auto-render entirely, so a
// page that hung on render can always be re-entered.
const RENDER_GUARD_NODES = 4000;
const RENDER_GUARD_EDGES = 8000;
// Edge bundling is iteration-heavy in the number of edges — a
// page-freezing cost at scale; bundle only small graphs.
const BUNDLE_MAX_EDGES = 300;
// G6's entrance/update animation is nice didactics on small graphs but
// the performance killer at scale (>2 min at 1700 nodes vs ~1.5 s off);
// keep it only below this node count.
@ -910,7 +917,7 @@ Graph Console
// empty canvas clears. Runs alongside the node:click inspector.
{ type: "click-select", degree: 1,
state: "selected", neighborState: "active", unselectedState: "inactive" }],
plugins: [{key: "edge-bundling", type: "edge-bundling"},{
plugins: [{
key: "toolbar",
type: "toolbar",
position: "top-left",
@ -936,6 +943,9 @@ Graph Console
}]
};
if (layoutCfg) opts.layout = layoutCfg;
if (g6data.edges.length <= BUNDLE_MAX_EDGES) {
opts.plugins.unshift({ key: "edge-bundling", type: "edge-bundling" });
}
g6graph = new G6.Graph(opts);
// Re-attached on every instance creation (layout/animation switches
// destroy and recreate the graph).
@ -1070,9 +1080,14 @@ Graph Console
? " — query filter: " + data.nodes.length + " of "
+ lastGraphData.nodes.length + " nodes"
: "";
if (shown.nodes.length > RENDER_GUARD_NODES && !renderForced) {
const safeMode = new URLSearchParams(location.search).has("safe");
if ((safeMode
|| shown.nodes.length > RENDER_GUARD_NODES
|| shown.edges.length > RENDER_GUARD_EDGES) && !renderForced) {
graphViewStatus.textContent =
graphStatusText(data) + filterNote + " — too large to render automatically";
graphStatusText(data) + filterNote
+ (safeMode ? " — safe mode (?safe): auto-render off"
: " — too large to render automatically");
if (anywayBtn) anywayBtn.style.display = "inline";
return;
}