mirror of
https://github.com/wisplite/BLE-mesh-sim.git
synced 2026-10-30 13:32:21 -05:00
HUGE performance update! Non-blocking quickPacket, markNeighbors, and massively improved network generator
This commit is contained in:
@@ -74,6 +74,7 @@
|
|||||||
<label for="quickPlace">Quick Place</label>
|
<label for="quickPlace">Quick Place</label>
|
||||||
</div>
|
</div>
|
||||||
<button id="cancelMarkNeighbor">Cancel</button>
|
<button id="cancelMarkNeighbor">Cancel</button>
|
||||||
|
<p id="nodeCount">Node Count: 0</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="centerColumn" style="display: flex; flex-direction: column; align-items: center; justify-content: center; width: 70%; height: 100%; margin: 0 auto;">
|
<div id="centerColumn" style="display: flex; flex-direction: column; align-items: center; justify-content: center; width: 70%; height: 100%; margin: 0 auto;">
|
||||||
<div id="simContainer"></div>
|
<div id="simContainer"></div>
|
||||||
@@ -99,6 +100,8 @@
|
|||||||
<div id="randomGraphModalContent">
|
<div id="randomGraphModalContent">
|
||||||
<label for="randomGraphNodes">Number of Nodes</label>
|
<label for="randomGraphNodes">Number of Nodes</label>
|
||||||
<input type="number" id="randomGraphNodes" value="10" autocomplete="off">
|
<input type="number" id="randomGraphNodes" value="10" autocomplete="off">
|
||||||
|
<label for="randomGraphDensity">Density (nodes per cell)</label>
|
||||||
|
<input type="number" id="randomGraphDensity" value="10" autocomplete="off">
|
||||||
<div id="randomGraphModalButtons" style="display: flex; flex-direction: row; gap: 10px; justify-content: end; align-items: center;">
|
<div id="randomGraphModalButtons" style="display: flex; flex-direction: row; gap: 10px; justify-content: end; align-items: center;">
|
||||||
<button id="createRandomGraphCancel" class="controlButton" onclick="document.getElementById('randomGraphModal').style.display = 'none';">Cancel</button>
|
<button id="createRandomGraphCancel" class="controlButton" onclick="document.getElementById('randomGraphModal').style.display = 'none';">Cancel</button>
|
||||||
<button id="createRandomGraphConfirm" class="controlButton">Create</button>
|
<button id="createRandomGraphConfirm" class="controlButton">Create</button>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ var showUpdates = false;
|
|||||||
// Track all active intervals for marking neighbors, and a cancel flag
|
// Track all active intervals for marking neighbors, and a cancel flag
|
||||||
var markNeighborIntervals = new Set();
|
var markNeighborIntervals = new Set();
|
||||||
var cancelMarkNeighborRequested = false;
|
var cancelMarkNeighborRequested = false;
|
||||||
|
var markNeighborRafs = new Set();
|
||||||
|
|
||||||
function smoothColorTransition(color1, color2, min, max, current) {
|
function smoothColorTransition(color1, color2, min, max, current) {
|
||||||
// Clamp current between min and max
|
// Clamp current between min and max
|
||||||
@@ -121,11 +122,11 @@ onEdgeEngine.setArrivalCallback(async ({ from, to, dot }) => {
|
|||||||
edges.update({id: `${from}->${to}`, color: {color: '#2b7ce9', highlight: '#2b7ce9'}});
|
edges.update({id: `${from}->${to}`, color: {color: '#2b7ce9', highlight: '#2b7ce9'}});
|
||||||
edges.update({id: `${to}->${from}`, color: {color: '#2b7ce9', highlight: '#2b7ce9'}});
|
edges.update({id: `${to}->${from}`, color: {color: '#2b7ce9', highlight: '#2b7ce9'}});
|
||||||
}
|
}
|
||||||
if (nodeTable[to].packetCache.includes(dot.id.split('-')[0])) {
|
if (nodeTable[to].packetCache.has(dot.id.split('-')[0])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// update color of receiving node for visualization
|
// update color of receiving node for visualization
|
||||||
nodeTable[to].packetCache.push(dot.id.split('-')[0]);
|
nodeTable[to].packetCache.add(dot.id.split('-')[0]);
|
||||||
var packetId = dot.id.split('-')[0];
|
var packetId = dot.id.split('-')[0];
|
||||||
var originNode = dot.id.split('-')[3];
|
var originNode = dot.id.split('-')[3];
|
||||||
ttl--;
|
ttl--;
|
||||||
@@ -160,25 +161,43 @@ onEdgeEngine.setArrivalCallback(async ({ from, to, dot }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function quickPacket(startNode, packetInfo, fromNode) {
|
function quickPacketRaf(startNode, packetInfo, fromNode, opts = {}) {
|
||||||
const packetId = packetInfo.split('-')[0];
|
const packetId = packetInfo.split('-')[0];
|
||||||
const ttlStart = parseInt(packetInfo.split('-')[2]);
|
const ttlStart = parseInt(packetInfo.split('-')[2]);
|
||||||
const originNode = packetInfo.split('-')[3];
|
const originNode = packetInfo.split('-')[3];
|
||||||
|
|
||||||
const queue = [{ nodeId: startNode, ttl: ttlStart, from: fromNode }];
|
// Dequeue with head index (avoid Array.shift() O(n))
|
||||||
|
let queue = [{ nodeId: startNode, ttl: ttlStart, from: fromNode }];
|
||||||
|
let head = 0;
|
||||||
|
|
||||||
while (queue.length > 0) {
|
// Tuning knobs
|
||||||
const { nodeId, ttl, from } = queue.shift();
|
const timeBudgetMs = opts.timeBudgetMs ?? 6; // per frame budget
|
||||||
|
const maxPerFrame = opts.maxPerFrame ?? 2000; // hard cap per frame
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
function step() {
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
const t0 = performance.now();
|
||||||
|
let processed = 0;
|
||||||
|
|
||||||
|
while (head < queue.length) {
|
||||||
|
const { nodeId, ttl, from } = queue[head++];
|
||||||
|
|
||||||
if (ttl < 0) {
|
if (ttl < 0) {
|
||||||
|
if (document.getElementById('showTTL').checked) {
|
||||||
markNeighborsAsFailed(nodeId, from, packetId);
|
markNeighborsAsFailed(nodeId, from, packetId);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
if (nodeTable[nodeId]?.packetCache?.includes(packetId)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeTable[nodeId].packetCache.push(packetId);
|
const node = nodeTable[nodeId];
|
||||||
|
if (!node) continue;
|
||||||
|
|
||||||
|
const cache = node.packetCache;
|
||||||
|
if (cache && cache.has(packetId)) continue;
|
||||||
|
|
||||||
|
cache.add(packetId);
|
||||||
|
|
||||||
if (document.getElementById('showTTL').checked) {
|
if (document.getElementById('showTTL').checked) {
|
||||||
nodes.update({id: nodeId, color: {background: smoothColorTransition('#eb4034', '#40eb34', 0, TTL, ttl)}});
|
nodes.update({id: nodeId, color: {background: smoothColorTransition('#eb4034', '#40eb34', 0, TTL, ttl)}});
|
||||||
@@ -190,16 +209,25 @@ function quickPacket(startNode, packetInfo, fromNode) {
|
|||||||
edges.update({id: `${nodeId}->${from}`, color: {color: '#2b7ce9', highlight: '#2b7ce9'}});
|
edges.update({id: `${nodeId}->${from}`, color: {color: '#2b7ce9', highlight: '#2b7ce9'}});
|
||||||
}
|
}
|
||||||
|
|
||||||
const neighborsSnapshot = (nodeTable[nodeId]?.connections?.slice()) || [];
|
// Iterate connections directly (avoid .slice() allocations)
|
||||||
|
const neighbors = node.connections || [];
|
||||||
for (let neighbor of neighborsSnapshot) {
|
for (let i = 0; i < neighbors.length; i++) {
|
||||||
if (neighbor === originNode || neighbor === from) {
|
const neighbor = neighbors[i];
|
||||||
continue;
|
if (neighbor === originNode || neighbor === from) continue;
|
||||||
}
|
|
||||||
queue.push({ nodeId: neighbor, ttl: ttl - 1, from: nodeId });
|
queue.push({ nodeId: neighbor, ttl: ttl - 1, from: nodeId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
processed++;
|
||||||
|
if (processed >= maxPerFrame || (performance.now() - t0) >= timeBudgetMs) {
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
return { cancel: () => { cancelled = true; } };
|
||||||
|
}
|
||||||
|
|
||||||
// This is a modified version of quickPacket that contains some extra logic to save routing tables.
|
// This is a modified version of quickPacket that contains some extra logic to save routing tables.
|
||||||
// It also disables the TTL colors for unreachable nodes to save performance.
|
// It also disables the TTL colors for unreachable nodes to save performance.
|
||||||
@@ -218,11 +246,11 @@ function quickRoute(startNode, packetInfo, fromNode) {
|
|||||||
displayWarningWhenDone = true;
|
displayWarningWhenDone = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (nodeTable[nodeId]?.packetCache?.includes(packetId)) {
|
if (nodeTable[nodeId]?.packetCache?.has(packetId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeTable[nodeId].packetCache.push(packetId);
|
nodeTable[nodeId].packetCache.add(packetId);
|
||||||
nodeTable[nodeId].routingTable[originNode] = from;
|
nodeTable[nodeId].routingTable[originNode] = from;
|
||||||
|
|
||||||
if (document.getElementById('showTTL').checked) {
|
if (document.getElementById('showTTL').checked) {
|
||||||
@@ -318,85 +346,145 @@ async function routePacket(currentNode, goalNode, packetInfo, first=false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const markNeighborsAsFailed = (nodeId, from, packetId, visited = new Set()) => {
|
// RAF-driven neighbor marking system
|
||||||
if (cancelMarkNeighborRequested) {
|
const markNeighborsAsFailed = (startNodeId, from, packetId, visited = new Set(), opts = {}) => {
|
||||||
return;
|
if (cancelMarkNeighborRequested) return;
|
||||||
}
|
if (visited.has(startNodeId)) return;
|
||||||
if (visited.has(nodeId)) {
|
visited.add(startNodeId);
|
||||||
return;
|
|
||||||
}
|
|
||||||
visited.add(nodeId);
|
|
||||||
|
|
||||||
if (warningModalShown) {
|
const timeBudgetMs = opts.timeBudgetMs ?? 3; // tighter budget prevents long frames
|
||||||
// enable walkthrough mode to prevent browser from freezing
|
const maxOpsPerFrame = opts.maxOpsPerFrame ?? 500;
|
||||||
if (markNeighborIntervals.size === 0) {
|
const batchSize = opts.batchSize ?? 256; // batch visual updates
|
||||||
|
|
||||||
|
// show cancel button when we start work
|
||||||
|
if (markNeighborRafs.size === 0 && markNeighborIntervals.size === 0) {
|
||||||
cancelMarkNeighborRequested = false;
|
cancelMarkNeighborRequested = false;
|
||||||
}
|
|
||||||
document.getElementById('cancelMarkNeighbor').style.display = 'block';
|
document.getElementById('cancelMarkNeighbor').style.display = 'block';
|
||||||
const neighborsSnapshot = (nodeTable[nodeId]?.connections?.slice()) || [];
|
}
|
||||||
const neighborsLength = neighborsSnapshot.length;
|
|
||||||
let currentIdx = 0;
|
// BFS queue of per-node neighbor iterators (no .slice())
|
||||||
const intervalId = setInterval(() => {
|
const tasks = [];
|
||||||
|
let head = 0;
|
||||||
|
const pushTask = (id, fromId) => {
|
||||||
|
const n = nodeTable[id];
|
||||||
|
const neighbors = (n && n.connections) ? n.connections : [];
|
||||||
|
tasks.push({ nodeId: id, from: fromId, neighbors, idx: 0 });
|
||||||
|
};
|
||||||
|
pushTask(startNodeId, from);
|
||||||
|
|
||||||
|
// batch updates to reduce DOM churn
|
||||||
|
let nodeBatch = [];
|
||||||
|
let edgeBatch = [];
|
||||||
|
function flushBatches() {
|
||||||
|
if (nodeBatch.length) {
|
||||||
|
nodes.update(nodeBatch);
|
||||||
|
nodeBatch = [];
|
||||||
|
}
|
||||||
|
if (edgeBatch.length) {
|
||||||
|
edges.update(edgeBatch);
|
||||||
|
edgeBatch = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rafId = null;
|
||||||
|
function step() {
|
||||||
if (cancelMarkNeighborRequested) {
|
if (cancelMarkNeighborRequested) {
|
||||||
clearInterval(intervalId);
|
if (rafId != null) {
|
||||||
markNeighborIntervals.delete(intervalId);
|
cancelAnimationFrame(rafId);
|
||||||
if (markNeighborIntervals.size === 0) {
|
markNeighborRafs.delete(rafId);
|
||||||
|
rafId = null;
|
||||||
|
}
|
||||||
|
flushBatches();
|
||||||
|
if (markNeighborRafs.size === 0 && markNeighborIntervals.size === 0) {
|
||||||
document.getElementById('cancelMarkNeighbor').style.display = 'none';
|
document.getElementById('cancelMarkNeighbor').style.display = 'none';
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentIdx < neighborsLength) {
|
|
||||||
const neighbor = neighborsSnapshot[currentIdx];
|
const deadline = performance.now() + timeBudgetMs;
|
||||||
if (!visited.has(neighbor) &&
|
let ops = 0;
|
||||||
!nodeTable[neighbor]?.packetCache?.includes(packetId) &&
|
|
||||||
neighbor !== from) {
|
while (head < tasks.length) {
|
||||||
nodes.update({id: neighbor, color: {background: 'white'}});
|
const task = tasks[head];
|
||||||
edges.update({id: `${nodeId}->${neighbor}`, color: {color: 'white', highlight: '#97c2fc'}});
|
|
||||||
edges.update({id: `${neighbor}->${nodeId}`, color: {color: 'white', highlight: '#97c2fc'}});
|
// process a few neighbors at a time; yield aggressively
|
||||||
markNeighborsAsFailed(neighbor, nodeId, packetId, visited);
|
while (task.idx < task.neighbors.length) {
|
||||||
}
|
const neighbor = task.neighbors[task.idx++];
|
||||||
currentIdx++;
|
|
||||||
} else {
|
if (neighbor === task.from) continue;
|
||||||
clearInterval(intervalId);
|
if (visited.has(neighbor)) continue;
|
||||||
markNeighborIntervals.delete(intervalId);
|
|
||||||
if (markNeighborIntervals.size === 0) {
|
const neighborNode = nodeTable[neighbor];
|
||||||
document.getElementById('cancelMarkNeighbor').style.display = 'none';
|
if (!neighborNode) continue;
|
||||||
}
|
if (neighborNode.packetCache?.has(packetId)) continue;
|
||||||
}
|
|
||||||
}, 1);
|
visited.add(neighbor);
|
||||||
markNeighborIntervals.add(intervalId);
|
|
||||||
|
// enqueue child (BFS)
|
||||||
|
const nextNeighbors = neighborNode.connections || [];
|
||||||
|
tasks.push({ nodeId: neighbor, from: task.nodeId, neighbors: nextNeighbors, idx: 0 });
|
||||||
|
|
||||||
|
// batch visuals
|
||||||
|
nodeBatch.push({ id: neighbor, color: { background: 'white' } });
|
||||||
|
edgeBatch.push({ id: `${task.nodeId}->${neighbor}`, color: { color: 'white', highlight: '#97c2fc' } });
|
||||||
|
edgeBatch.push({ id: `${neighbor}->${task.nodeId}`, color: { color: 'white', highlight: '#97c2fc' } });
|
||||||
|
|
||||||
|
ops++;
|
||||||
|
|
||||||
|
// time/ops-based yield
|
||||||
|
if (ops >= maxOpsPerFrame || performance.now() >= deadline) {
|
||||||
|
flushBatches(); // flush once per frame
|
||||||
|
if (rafId != null) markNeighborRafs.delete(rafId);
|
||||||
|
rafId = requestAnimationFrame(step);
|
||||||
|
markNeighborRafs.add(rafId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const neighborsSnapshot = (nodeTable[nodeId]?.connections?.slice()) || [];
|
// keep batches bounded
|
||||||
for (let neighbor of neighborsSnapshot) {
|
if (nodeBatch.length >= batchSize || edgeBatch.length >= batchSize * 2) {
|
||||||
if (visited.has(neighbor)) {
|
flushBatches();
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (nodeTable[neighbor]?.packetCache?.includes(packetId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (neighbor === from) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
nodes.update({id: neighbor, color: {background: 'white'}});
|
|
||||||
edges.update({id: `${nodeId}->${neighbor}`, color: {color: 'white', highlight: '#97c2fc'}});
|
|
||||||
edges.update({id: `${neighbor}->${nodeId}`, color: {color: 'white', highlight: '#97c2fc'}});
|
|
||||||
markNeighborsAsFailed(neighbor, nodeId, packetId, visited);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// done with this node
|
||||||
|
head++;
|
||||||
|
}
|
||||||
|
|
||||||
function recomputeNodeConnections(nodeId) {
|
// finished
|
||||||
|
flushBatches();
|
||||||
|
if (rafId != null) {
|
||||||
|
markNeighborRafs.delete(rafId);
|
||||||
|
rafId = null;
|
||||||
|
}
|
||||||
|
if (markNeighborRafs.size === 0 && markNeighborIntervals.size === 0) {
|
||||||
|
document.getElementById('cancelMarkNeighbor').style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rafId = requestAnimationFrame(step);
|
||||||
|
markNeighborRafs.add(rafId);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function recomputeNodeConnections(nodeId, dragging=false) {
|
||||||
const p = network.getPosition(nodeId);
|
const p = network.getPosition(nodeId);
|
||||||
const candidates = grid.getNeighborCandidates(p.x, p.y);
|
const candidates = grid.getNeighborCandidates(p.x, p.y);
|
||||||
|
|
||||||
|
// cache node positions now to prevent duplicate work later
|
||||||
|
const posById = new Map();
|
||||||
|
for (let id of candidates) {
|
||||||
|
if (nodeTable[id]) {
|
||||||
|
posById.set(id, network.getPosition(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// check all nearby nodes against all other nearby nodes
|
// check all nearby nodes against all other nearby nodes
|
||||||
for (let a of candidates) {
|
for (let a of candidates) {
|
||||||
if (!nodeTable[a]) {
|
if (!nodeTable[a]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// in case user drags node too fast, drop far connections
|
// in case user drags node too fast, drop far connections
|
||||||
|
if (dragging) {
|
||||||
for (let connection of nodeTable[a].connections) {
|
for (let connection of nodeTable[a].connections) {
|
||||||
const aPos = network.getPosition(a);
|
const aPos = network.getPosition(a);
|
||||||
const bPos = network.getPosition(connection);
|
const bPos = network.getPosition(connection);
|
||||||
@@ -405,6 +493,7 @@ function recomputeNodeConnections(nodeId) {
|
|||||||
dropConnection(a,connection);
|
dropConnection(a,connection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (showUpdates) {
|
if (showUpdates) {
|
||||||
console.log('updating')
|
console.log('updating')
|
||||||
let lastTime;
|
let lastTime;
|
||||||
@@ -439,8 +528,8 @@ function recomputeNodeConnections(nodeId) {
|
|||||||
if (!nodeTable[b]) {
|
if (!nodeTable[b]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const aPos = network.getPosition(a);
|
const aPos = posById.get(a);
|
||||||
const bPos = network.getPosition(b);
|
const bPos = posById.get(b);
|
||||||
const distance = calculateDistance(aPos,bPos);
|
const distance = calculateDistance(aPos,bPos);
|
||||||
if (distance < CONNECTION_DISTANCE) {
|
if (distance < CONNECTION_DISTANCE) {
|
||||||
let connectionScore = 0;
|
let connectionScore = 0;
|
||||||
@@ -509,6 +598,78 @@ function recomputeNodeConnections(nodeId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function computeAllConnectionsFast() {
|
||||||
|
// Cache positions and initialize degree/connection sets
|
||||||
|
const items = nodes.get(); // [{id,x,y,...}]
|
||||||
|
const pos = new Map(items.map(n => [n.id, { x: n.x, y: n.y }]));
|
||||||
|
const ids = items.map(n => n.id);
|
||||||
|
|
||||||
|
for (let id of ids) {
|
||||||
|
if (!nodeTable[id]) {
|
||||||
|
nodeTable[id] = { connections: [], packetCache: new Set(), routingTable: {} };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const degree = new Map(ids.map(id => [id, nodeTable[id].connections.length || 0]));
|
||||||
|
const connSet = new Map(ids.map(id => [id, new Set(nodeTable[id].connections || [])]));
|
||||||
|
|
||||||
|
const newEdges = [];
|
||||||
|
|
||||||
|
// Greedy, capacity-aware linking within neighborhood
|
||||||
|
for (let a of ids) {
|
||||||
|
const ap = pos.get(a);
|
||||||
|
if (!ap) continue;
|
||||||
|
|
||||||
|
const candidates = grid.getNeighborCandidates(ap.x, ap.y);
|
||||||
|
const scored = [];
|
||||||
|
|
||||||
|
for (let b of candidates) {
|
||||||
|
if (b === a) continue;
|
||||||
|
if (!pos.has(b)) continue;
|
||||||
|
if (connSet.get(a).has(b)) continue;
|
||||||
|
|
||||||
|
const bp = pos.get(b);
|
||||||
|
const d = calculateDistance(ap, bp);
|
||||||
|
if (d >= CONNECTION_DISTANCE) continue;
|
||||||
|
|
||||||
|
// Favor closer, low-degree, and isolated nodes
|
||||||
|
const bdeg = degree.get(b) || 0;
|
||||||
|
let score = 100 + distanceToRSSI(d);
|
||||||
|
score += 100 / (bdeg + 1);
|
||||||
|
if (bdeg === 0) score += 100;
|
||||||
|
|
||||||
|
scored.push({ id: b, score });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scored.length === 0) continue;
|
||||||
|
scored.sort((x, y) => y.score - x.score);
|
||||||
|
|
||||||
|
for (let s of scored) {
|
||||||
|
if ((degree.get(a) || 0) >= MAX_CONNECTIONS) break;
|
||||||
|
const b = s.id;
|
||||||
|
if ((degree.get(b) || 0) >= MAX_CONNECTIONS) continue;
|
||||||
|
if (connSet.get(a).has(b)) continue;
|
||||||
|
|
||||||
|
connSet.get(a).add(b);
|
||||||
|
connSet.get(b).add(a);
|
||||||
|
degree.set(a, (degree.get(a) || 0) + 1);
|
||||||
|
degree.set(b, (degree.get(b) || 0) + 1);
|
||||||
|
|
||||||
|
nodeTable[a].connections.push(b);
|
||||||
|
nodeTable[b].connections.push(a);
|
||||||
|
|
||||||
|
newEdges.push({ id: `${a}->${b}`, from: a, to: b });
|
||||||
|
newEdges.push({ id: `${b}->${a}`, from: b, to: a });
|
||||||
|
|
||||||
|
if ((degree.get(a) || 0) >= MAX_CONNECTIONS) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newEdges.length > 0) {
|
||||||
|
edges.add(newEdges);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function dropConnection(nodeA, nodeB) {
|
function dropConnection(nodeA, nodeB) {
|
||||||
edges.remove(`${nodeA}->${nodeB}`);
|
edges.remove(`${nodeA}->${nodeB}`);
|
||||||
edges.remove(`${nodeB}->${nodeA}`);
|
edges.remove(`${nodeB}->${nodeA}`);
|
||||||
@@ -522,36 +683,23 @@ function dropConnection(nodeA, nodeB) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onNodeAdd(nodeId) {
|
function onNodeAdd(nodeId, defer=false) {
|
||||||
// update grid
|
// update grid
|
||||||
const position = network.getPosition(nodeId);
|
const position = network.getPosition(nodeId);
|
||||||
grid.add(nodeId, position.x, position.y);
|
grid.add(nodeId, position.x, position.y);
|
||||||
|
|
||||||
|
if (!defer) {
|
||||||
recomputeNodeConnections(nodeId);
|
recomputeNodeConnections(nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
var nodeLength = Object.keys(nodeTable).length;
|
var nodeLength = Object.keys(nodeTable).length;
|
||||||
if (nodeLength >= 200 && !warningModalShown) {
|
document.getElementById('nodeCount').innerText = 'Node Count: ' + nodeLength;
|
||||||
warningModalShown = true;
|
|
||||||
document.getElementById('warningModal').style.display = 'block';
|
|
||||||
document.getElementById('warningModalMessage').innerHTML = 'Large network detected; simulation speed will be reduced.';
|
|
||||||
document.getElementById('warningModalCustomContent').innerHTML = `<details><summary>What does this mean?</summary>
|
|
||||||
<p>The simulation, by default, computes every node's connections 100 'times' per second. Due to the size of this network, this could cause the browser to freeze. To prevent this, the update interval (found in engine settings) is now locked to a multiple of the number of nodes. Additionally, when computing which nodes weren't reached after a packet is sent, the simulation will now walk through the network in steps rather than computing everything at once.</p>
|
|
||||||
<br>
|
|
||||||
<p>You may also want to consider using the quick packet feature, which will compute the packet propagation instantly rather than animating it. The animated nodes can cause lots of performance issues, especially on non-Chromium browsers.</p>`;
|
|
||||||
document.getElementById('updateInterval').disabled = true;
|
|
||||||
document.getElementById('updateInterval').value = nodeLength*2;
|
|
||||||
UPDATE_INTERVAL = nodeLength*2;
|
|
||||||
document.getElementById('simSpeed').value = 1;
|
|
||||||
onEdgeEngine.setSimulationSpeed(1);
|
|
||||||
} else if (nodeLength >= 200 && warningModalShown) {
|
|
||||||
document.getElementById('updateInterval').value = nodeLength*2;
|
|
||||||
UPDATE_INTERVAL = nodeLength*2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('addNode').addEventListener('click', function() {
|
document.getElementById('addNode').addEventListener('click', function() {
|
||||||
const nodeId = generateRealisticLabel();
|
const nodeId = generateRealisticLabel();
|
||||||
nodes.add({id: nodeId, label: nodeId});
|
nodes.add({id: nodeId, label: nodeId});
|
||||||
nodeTable[nodeId] = {'connections': [], 'packetCache': [], 'routingTable': {}};
|
nodeTable[nodeId] = {'connections': [], 'packetCache': new Set(), 'routingTable': {}};
|
||||||
onNodeAdd(nodeId);
|
onNodeAdd(nodeId);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -568,7 +716,7 @@ network.on('click', function(properties) {
|
|||||||
if (document.getElementById('quickPlace').checked) {
|
if (document.getElementById('quickPlace').checked) {
|
||||||
const nodeId = generateRealisticLabel();
|
const nodeId = generateRealisticLabel();
|
||||||
nodes.add({id: nodeId, label: nodeId, x: properties.pointer.canvas.x, y: properties.pointer.canvas.y});
|
nodes.add({id: nodeId, label: nodeId, x: properties.pointer.canvas.x, y: properties.pointer.canvas.y});
|
||||||
nodeTable[nodeId] = {'connections': [], 'packetCache': [], 'routingTable': {}};
|
nodeTable[nodeId] = {'connections': [], 'packetCache': new Set(), 'routingTable': {}};
|
||||||
onNodeAdd(nodeId);
|
onNodeAdd(nodeId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -584,7 +732,7 @@ network.on('doubleClick', function(properties) {
|
|||||||
}
|
}
|
||||||
const nodeId = generateRealisticLabel();
|
const nodeId = generateRealisticLabel();
|
||||||
nodes.add({id: nodeId, label: nodeId, x: properties.pointer.canvas.x, y: properties.pointer.canvas.y});
|
nodes.add({id: nodeId, label: nodeId, x: properties.pointer.canvas.x, y: properties.pointer.canvas.y});
|
||||||
nodeTable[nodeId] = {'connections': [], 'packetCache': [], 'routingTable': {}};
|
nodeTable[nodeId] = {'connections': [], 'packetCache': new Set(), 'routingTable': {}};
|
||||||
onNodeAdd(nodeId);
|
onNodeAdd(nodeId);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -595,7 +743,7 @@ network.on('dragging', (properties) => {
|
|||||||
const nodeId = properties.nodes[0];
|
const nodeId = properties.nodes[0];
|
||||||
const pos = network.getPosition(nodeId);
|
const pos = network.getPosition(nodeId);
|
||||||
grid.update(nodeId, pos.x, pos.y);
|
grid.update(nodeId, pos.x, pos.y);
|
||||||
recomputeNodeConnections(nodeId);
|
recomputeNodeConnections(nodeId, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
network.on('beforeDrawing', function(ctx) {
|
network.on('beforeDrawing', function(ctx) {
|
||||||
@@ -673,19 +821,22 @@ document.getElementById('sendPacket').addEventListener('click', function() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
onEdgeEngine.createDotNode(movingNode, selectedNode, connection);
|
onEdgeEngine.createDotNode(movingNode, selectedNode, connection);
|
||||||
nodeTable[selectedNode].packetCache.push(packetId);
|
nodeTable[selectedNode].packetCache.add(packetId);
|
||||||
packetShift = generateRealisticLabel();
|
packetShift = generateRealisticLabel();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('resetTTLColors').addEventListener('click', function() {
|
document.getElementById('resetTTLColors').addEventListener('click', function() {
|
||||||
|
let nodeUpdates = [];
|
||||||
|
let edgeUpdates = [];
|
||||||
for (let node of nodes.get()) {
|
for (let node of nodes.get()) {
|
||||||
nodes.update({id: node.id, color: {background: '#97c2fc'}});
|
nodeUpdates.push({id: node.id, color: {background: '#97c2fc'}});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let edge of edges.get()) {
|
for (let edge of edges.get()) {
|
||||||
edges.update({id: edge.id, color: {color: '#2b7ce9', highlight: '#2b7ce9'}});
|
edgeUpdates.push({id: edge.id, color: {color: '#2b7ce9', highlight: '#2b7ce9'}});
|
||||||
}
|
}
|
||||||
|
nodes.update(nodeUpdates);
|
||||||
|
edges.update(edgeUpdates);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('updateInterval').addEventListener('change', function() {
|
document.getElementById('updateInterval').addEventListener('change', function() {
|
||||||
@@ -722,32 +873,79 @@ document.getElementById('createRandomGraph').addEventListener('click', function(
|
|||||||
|
|
||||||
document.getElementById('createRandomGraphConfirm').addEventListener('click', function() {
|
document.getElementById('createRandomGraphConfirm').addEventListener('click', function() {
|
||||||
const nodeCount = parseInt(document.getElementById('randomGraphNodes').value);
|
const nodeCount = parseInt(document.getElementById('randomGraphNodes').value);
|
||||||
network.setOptions({
|
const density = Math.max(1, parseInt(document.getElementById('randomGraphDensity').value) || 1);
|
||||||
nodes: {
|
|
||||||
shape: 'box',
|
|
||||||
physics: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
let nodeCounter = 0;
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
const nodeId = generateRealisticLabel();
|
|
||||||
nodes.add({id: nodeId, label: nodeId, x: Math.random() * 1000, y: Math.random() * 1000});
|
|
||||||
nodeTable[nodeId] = {'connections': [], 'packetCache': [], 'routingTable': {}};
|
|
||||||
onNodeAdd(nodeId);
|
|
||||||
nodeCounter++;
|
|
||||||
if (nodeCounter >= nodeCount) {
|
|
||||||
clearInterval(interval);
|
|
||||||
setTimeout(() => {
|
|
||||||
network.setOptions({
|
|
||||||
nodes: {
|
|
||||||
shape: 'box',
|
|
||||||
physics: false,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
document.getElementById('randomGraphModal').style.display = 'none';
|
document.getElementById('randomGraphModal').style.display = 'none';
|
||||||
|
if (!Number.isFinite(nodeCount) || nodeCount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset existing graph
|
||||||
|
nodes.clear();
|
||||||
|
edges.clear();
|
||||||
|
nodeTable = {};
|
||||||
|
grid = new GridIndex(CONNECTION_DISTANCE);
|
||||||
|
selectedNode = null;
|
||||||
|
|
||||||
|
const cellSize = CONNECTION_DISTANCE;
|
||||||
|
const cellsNeeded = Math.max(1, Math.ceil(nodeCount / density));
|
||||||
|
const cols = Math.ceil(Math.sqrt(cellsNeeded));
|
||||||
|
const rows = Math.ceil(cellsNeeded / cols);
|
||||||
|
|
||||||
|
// Center the layout on the current view
|
||||||
|
const center = network.getViewPosition();
|
||||||
|
|
||||||
|
// Distribute node counts across cells (roughly even, no big clumps)
|
||||||
|
const counts = new Array(cellsNeeded).fill(Math.floor(nodeCount / cellsNeeded));
|
||||||
|
let remainder = nodeCount % cellsNeeded;
|
||||||
|
const idxs = Array.from({ length: cellsNeeded }, (_, i) => i);
|
||||||
|
for (let i = idxs.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[idxs[i], idxs[j]] = [idxs[j], idxs[i]];
|
||||||
|
}
|
||||||
|
for (let i = 0; i < remainder; i++) {
|
||||||
|
counts[idxs[i]]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jitter within each cell so nodes are spread but remain near the cell center
|
||||||
|
const jitter = cellSize * 0.45;
|
||||||
|
|
||||||
|
let cellsPerFrame = 1;
|
||||||
|
let runId;
|
||||||
|
let nodesAdded = 0;
|
||||||
|
let cellsFilled = 0;
|
||||||
|
let lastNodeAdded;
|
||||||
|
function addCells() {
|
||||||
|
const end = Math.min(cellsFilled+cellsPerFrame, cellsNeeded);
|
||||||
|
for (let i = cellsFilled; i < end; i++) {
|
||||||
|
const col = i % cols;
|
||||||
|
const row = Math.floor(i / cols);
|
||||||
|
const cx = center.x + (col - (cols - 1) / 2) * cellSize;
|
||||||
|
const cy = center.y + (row - (rows - 1) / 2) * cellSize;
|
||||||
|
|
||||||
|
for (let k = 0; k < counts[i]; k++) {
|
||||||
|
const x = cx + (Math.random() * 2 - 1) * jitter;
|
||||||
|
const y = cy + (Math.random() * 2 - 1) * jitter;
|
||||||
|
|
||||||
|
const nodeId = generateRealisticLabel();
|
||||||
|
nodes.add({ id: nodeId, label: nodeId, x, y });
|
||||||
|
nodeTable[nodeId] = { 'connections': [], 'packetCache': new Set(), 'routingTable': {} };
|
||||||
|
onNodeAdd(nodeId, true);
|
||||||
|
lastNodeAdded = nodeId;
|
||||||
|
nodesAdded++;
|
||||||
|
}
|
||||||
|
//recomputeNodeConnections(lastNodeAdded);
|
||||||
|
cellsFilled++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cellsFilled < cellsNeeded) {
|
||||||
|
runId = requestAnimationFrame(addCells);
|
||||||
|
} else {
|
||||||
|
cancelAnimationFrame(runId);
|
||||||
|
computeAllConnectionsFast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runId = requestAnimationFrame(addCells);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('darkMode').addEventListener('change', function() {
|
document.getElementById('darkMode').addEventListener('change', function() {
|
||||||
@@ -767,7 +965,7 @@ document.getElementById('quickPacket').addEventListener('click', async function(
|
|||||||
const packetId = generateRealisticLabel();
|
const packetId = generateRealisticLabel();
|
||||||
var packetShift = generateRealisticLabel();
|
var packetShift = generateRealisticLabel();
|
||||||
let ttl = TTL;
|
let ttl = TTL;
|
||||||
quickPacket(selectedNode, `${packetId}-${packetShift}-${ttl}-${selectedNode}`, selectedNode);
|
quickPacketRaf(selectedNode, `${packetId}-${packetShift}-${ttl}-${selectedNode}`, selectedNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('createRoutingTables').addEventListener('click', function() {
|
document.getElementById('createRoutingTables').addEventListener('click', function() {
|
||||||
@@ -921,11 +1119,18 @@ window.onload = function() {
|
|||||||
document.getElementById('cancelMarkNeighbor').addEventListener('click', function() {
|
document.getElementById('cancelMarkNeighbor').addEventListener('click', function() {
|
||||||
consoleLog('Cancelling mark neighbor...');
|
consoleLog('Cancelling mark neighbor...');
|
||||||
cancelMarkNeighborRequested = true;
|
cancelMarkNeighborRequested = true;
|
||||||
// Clear any tracked intervals
|
|
||||||
|
// Clear any tracked intervals (legacy)
|
||||||
for (let id of Array.from(markNeighborIntervals)) {
|
for (let id of Array.from(markNeighborIntervals)) {
|
||||||
clearInterval(id);
|
clearInterval(id);
|
||||||
markNeighborIntervals.delete(id);
|
markNeighborIntervals.delete(id);
|
||||||
}
|
}
|
||||||
|
// Clear any tracked RAFs
|
||||||
|
for (let id of Array.from(markNeighborRafs)) {
|
||||||
|
cancelAnimationFrame(id);
|
||||||
|
markNeighborRafs.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('cancelMarkNeighbor').style.display = 'none';
|
document.getElementById('cancelMarkNeighbor').style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,9 @@ html, body {
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#createRandomGraphCancel {
|
#createRandomGraphCancel {
|
||||||
|
|||||||
@@ -126,6 +126,9 @@ html, body {
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#createRandomGraphConfirm, #createRandomGraphCancel {
|
#createRandomGraphConfirm, #createRandomGraphCancel {
|
||||||
|
|||||||
Reference in New Issue
Block a user