Files
90-RED 3474927aee v1.3.0 — Web Worker routing, profiler disk export, stretch extraction
- Web Worker routing — A* search runs in a background Worker thread,
  keeping the UI responsive during large batch routes
- Worker watchdog — auto-falls back to main-thread routing on any failure
- Worker held-pause — drag-pause re-routes also run in worker
- stretch.js — extracted sticky-path logic, shared by main thread and worker
- Profiler disk export — POST /linkrouter/write_log endpoint saves reports
- Chunked worker processing — 8ms macrotask slices for cancel responsiveness
- New settings: Background worker routing, Held-pause in worker
2026-07-19 06:36:24 +08:00

144 lines
5.4 KiB
JavaScript

// Pure policy helpers for Adaptive drag mode.
export const DRAG_MODE_RANK = {
none: 0,
"freeze-others": 1,
"freeze-others-strict": 2,
"hide-self": 3,
};
export const LIVE_AVOIDANCE_LIMIT = 15;
export const HEAVY_WORKFLOW_NODE_LIMIT = 250;
export const HEAVY_WORKFLOW_LINK_LIMIT = 300;
function modeAtRank(rank) {
return Object.keys(DRAG_MODE_RANK).find((key) => DRAG_MODE_RANK[key] === rank) || "none";
}
export function updateRouteCost(previous, sampleMs, weight = 0.3) {
const sample = Math.max(0, +sampleMs || 0);
if (!Number.isFinite(previous)) return sample;
return previous * (1 - weight) + sample * weight;
}
export function chooseAdaptiveDragMode(activeLinks, predictedMs, currentMode = null) {
const count = Math.max(0, activeLinks | 0);
const cost = Math.max(0, +predictedMs || 0);
let candidate;
if (count <= 2) candidate = "none";
else if (count <= LIVE_AVOIDANCE_LIMIT) candidate = "freeze-others";
else if (count <= 30) candidate = "freeze-others-strict";
else candidate = "hide-self";
// Up to the synchronous-update limit, measured cost may freeze unaffected
// lines but must never disable live collision avoidance.
if (count <= LIVE_AVOIDANCE_LIMIT && cost > 70 && DRAG_MODE_RANK[candidate] < 1)
candidate = "freeze-others";
else if (count > LIVE_AVOIDANCE_LIMIT) {
if (cost > 180) candidate = "hide-self";
else if (cost > 100 && DRAG_MODE_RANK[candidate] < 2)
candidate = "freeze-others-strict";
}
const maxRank = count <= LIVE_AVOIDANCE_LIMIT
? DRAG_MODE_RANK["freeze-others"]
: count <= 30
? DRAG_MODE_RANK["freeze-others-strict"]
: DRAG_MODE_RANK["hide-self"];
const currentRank = Math.min(DRAG_MODE_RANK[currentMode] ?? -1, maxRank);
if (currentRank > DRAG_MODE_RANK[candidate]) return modeAtRank(currentRank);
return candidate;
}
// Pick the presentation mode once from links directly attached to the moved
// node(s). Collision density may change with drag direction, but must not make
// self-links suddenly disappear midway through the same gesture.
export function lockAdaptiveDragMode(currentMode, directLinks, _predictedMs) {
if (currentMode && currentMode !== "adaptive") return currentMode;
// The four presentation levels are count-defined. Historical timing may
// throttle collision work, but must not change visibility semantics.
return chooseAdaptiveDragMode(directLinks, 0);
}
// Mode 2 keeps a bounded amount of live collision avoidance. Direct links
// consume the same synchronous-work allowance; overflow collisions remain
// frozen until the held-pause queue or release pass processes them.
export function liveCollisionBudget(effectiveMode, directLinks) {
if (effectiveMode === "none") return Infinity;
if (effectiveMode !== "freeze-others") return 0;
return Math.max(0, LIVE_AVOIDANCE_LIMIT - Math.max(0, directLinks | 0));
}
// Large workflows pay a substantial OVG build cost even before connector A*
// begins. Lock them into the deferred drag scheduler for the whole gesture;
// unlike collision-count promotion, this decision cannot change by direction.
export function shouldUseHeavyWorkflowDrag(nodeCount, linkCount) {
return (
Math.max(0, nodeCount | 0) >= HEAVY_WORKFLOW_NODE_LIMIT ||
Math.max(0, linkCount | 0) >= HEAVY_WORKFLOW_LINK_LIMIT
);
}
export function shouldActivateHeavyDrag(dragMode, nodeCount, linkCount) {
if (dragMode === "heavy-deferred") return true;
if (dragMode !== "adaptive") return false;
return shouldUseHeavyWorkflowDrag(nodeCount, linkCount);
}
export function shouldStartHeavyDrag(
dragMode,
nodeCount,
linkCount,
pointerGeometryDragging,
) {
return !!(
pointerGeometryDragging &&
shouldActivateHeavyDrag(dragMode, nodeCount, linkCount)
);
}
export function shouldDeferHeavyGraphBuild(
heavyDrag,
pointerDown,
pauseActive,
hasStableRoutes,
) {
return !!(heavyDrag && pointerDown && !pauseActive && hasStableRoutes);
}
export function escalateDragModeAfterFrame(currentMode, connectorMs, activeLinks = Infinity) {
const mode = currentMode && currentMode !== "adaptive" ? currentMode : "none";
const elapsed = Math.max(0, +connectorMs || 0);
const n = Number(activeLinks);
const count = Number.isFinite(n) ? Math.max(0, Math.floor(n)) : Infinity;
if (count <= LIVE_AVOIDANCE_LIMIT) {
if (DRAG_MODE_RANK[mode] > DRAG_MODE_RANK["freeze-others"])
return "freeze-others";
if (elapsed > 45 && mode === "none") return "freeze-others";
return mode;
}
if (elapsed > 90) return "hide-self";
if (elapsed <= 45) return mode;
const rank = Math.min(3, (DRAG_MODE_RANK[mode] ?? 0) + 1);
return modeAtRank(rank);
}
// Mid-gesture escalation may only raise the presentation level, never lower
// it: re-showing self links halfway through the same drag would flicker.
// escalateDragModeAfterFrame alone can also downgrade (e.g. hide-self back to
// freeze-others when the active set shrinks); this wrapper filters that out.
export function escalateLockedDragMode(lockedMode, connectorMs, activeLinks = Infinity) {
if (!lockedMode || lockedMode === "adaptive") return lockedMode;
const next = escalateDragModeAfterFrame(lockedMode, connectorMs, activeLinks);
return DRAG_MODE_RANK[next] > DRAG_MODE_RANK[lockedMode] ? next : lockedMode;
}
export function shouldInvalidateAfterDrag(flags, collidesWithFinalNode) {
return !!(
flags?.affected ||
flags?.draggedHidden ||
flags?.sticky ||
(flags?.frozen && collidesWithFinalNode)
);
}