feat(graph): Phase 4 — Barnes-Hut, cache, lazy loading
Some checks failed
CI / lint (push) Successful in 13s
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
CI / security (push) Has been cancelled

Performance optimizations for large vaults:
- Barnes-Hut quadtree repulsion (O(n log n) for >200 nodes)
- Naive O(n²) preserved for small graphs (<200 nodes)
- Graph cache: reuse data when same (vault, path, depth, scope, tag)
- Cache key displayed in info bar: '(cache)' label
This commit is contained in:
Bruno Charest 2026-05-28 14:52:31 -04:00
parent 8c95899456
commit e9e954f36b

View File

@ -81,10 +81,32 @@ export const GraphViewManager = {
safeCreateIcons();
},
_cache: {},
_cacheKey: '',
_getCacheKey() {
return `${this._vault}|${this._path}|${this._depth}|${this._scope}|${this._searchTerm}`;
},
async _fetchAndRender() {
const info = document.getElementById('graph-info');
if (!info) return;
const cacheKey = this._getCacheKey();
// Use cache if same parameters
if (this._cache[cacheKey]) {
const cached = this._cache[cacheKey];
this._nodes = cached.nodes;
this._edges = cached.edges;
this._scope = cached.scope;
const scopeLabel = this._scope === 'full' ? 'Vault complet' : 'Dossier';
info.textContent = `${this._nodes.length} nœuds, ${this._edges.length} liens · ${scopeLabel} · prof=${this._depth} (cache)`;
this._initLayout();
this._startRender();
return;
}
const params = new URLSearchParams();
if (this._path) params.set('path', this._path);
params.set('depth', String(this._depth));
@ -99,9 +121,16 @@ export const GraphViewManager = {
this._edges = data.edges || [];
this._scope = data.scope || 'directory';
// Cache the result
this._cache[cacheKey] = {
nodes: this._nodes,
edges: this._edges,
scope: this._scope,
};
this._cacheKey = cacheKey;
const scopeLabel = this._scope === 'full' ? 'Vault complet' : 'Dossier';
const depthLabel = `prof=${this._depth}`;
info.textContent = `${this._nodes.length} nœuds, ${this._edges.length} liens · ${scopeLabel} · ${depthLabel}`;
info.textContent = `${this._nodes.length} nœuds, ${this._edges.length} liens · ${scopeLabel} · prof=${this._depth}`;
this._initLayout();
this._startRender();
} catch (err) {
@ -204,6 +233,7 @@ export const GraphViewManager = {
const cx = this._width / 2;
const cy = this._height / 2;
// Spring forces (edges) — unchanged
for (const edge of this._edges) {
const a = positions[edge.source];
const b = positions[edge.target];
@ -223,25 +253,14 @@ export const GraphViewManager = {
b.vy -= fy;
}
for (const n1 of this._nodes) {
for (const n2 of this._nodes) {
if (n1.id === n2.id) continue;
const a = positions[n1.id];
const b = positions[n2.id];
if (!a || !b) continue;
const dx = b.x - a.x;
const dy = b.y - a.y;
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
const force = 2000 / (dist * dist);
const fx = (dx / dist) * force;
const fy = (dy / dist) * force;
a.vx -= fx;
a.vy -= fy;
}
// Repulsion: Barnes-Hut for >200 nodes, naive O(n²) otherwise
if (this._nodes.length > 200) {
this._barnesHutRepulsion(positions);
} else {
this._naiveRepulsion(positions);
}
// Center gravity
for (const node of this._nodes) {
const p = positions[node.id];
if (!p) continue;
@ -249,6 +268,7 @@ export const GraphViewManager = {
p.vy += (cy - p.y) * 0.001;
}
// Apply velocities with damping
for (const node of this._nodes) {
const p = positions[node.id];
if (!p) continue;
@ -259,6 +279,121 @@ export const GraphViewManager = {
}
},
_naiveRepulsion(positions) {
for (const n1 of this._nodes) {
for (const n2 of this._nodes) {
if (n1.id === n2.id) continue;
const a = positions[n1.id];
const b = positions[n2.id];
if (!a || !b) continue;
const dx = b.x - a.x;
const dy = b.y - a.y;
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
const force = 2000 / (dist * dist);
const fx = (dx / dist) * force;
const fy = (dy / dist) * force;
a.vx -= fx;
a.vy -= fy;
}
}
},
// Barnes-Hut quadtree for O(n log n) repulsion
_barnesHutRepulsion(positions) {
// Build quadtree
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const node of this._nodes) {
const p = positions[node.id];
if (!p) continue;
if (p.x < minX) minX = p.x;
if (p.y < minY) minY = p.y;
if (p.x > maxX) maxX = p.x;
if (p.y > maxY) maxY = p.y;
}
const root = this._buildQuadTree(positions, minX, minY, maxX, maxY);
// Apply forces from quadtree
const theta = 0.9; // Barnes-Hut opening angle
for (const node of this._nodes) {
const p = positions[node.id];
if (!p) continue;
this._applyQuadTreeForce(root, p, theta);
}
},
_buildQuadTree(positions, x0, y0, x1, y1) {
const cx = (x0 + x1) / 2;
const cy = (y0 + y1) / 2;
const cell = { x0, y0, x1, y1, cx, cy, mass: 0, mx: 0, my: 0, children: null };
// Collect nodes in this cell
const contained = [];
for (const node of this._nodes) {
const p = positions[node.id];
if (p && p.x >= x0 && p.x < x1 && p.y >= y0 && p.y < y1) {
contained.push(p);
}
}
if (contained.length === 0) return cell;
if (contained.length === 1) {
cell.mass = 1;
cell.mx = contained[0].x;
cell.my = contained[0].y;
return cell;
}
// Subdivide
const midX = cx;
const midY = cy;
cell.children = [
this._buildQuadTree(positions, x0, y0, midX, midY), // NW
this._buildQuadTree(positions, midX, y0, x1, midY), // NE
this._buildQuadTree(positions, x0, midY, midX, y1), // SW
this._buildQuadTree(positions, midX, midY, x1, y1), // SE
];
// Compute center of mass
let totalMass = 0;
let sumX = 0, sumY = 0;
for (const child of cell.children) {
if (child.mass > 0) {
totalMass += child.mass;
sumX += child.mx * child.mass;
sumY += child.my * child.mass;
}
}
cell.mass = totalMass;
cell.mx = sumX / totalMass;
cell.my = sumY / totalMass;
return cell;
},
_applyQuadTreeForce(cell, p, theta) {
if (cell.mass === 0) return;
const dx = cell.mx - p.x;
const dy = cell.my - p.y;
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
const size = cell.x1 - cell.x0;
// If cell is far enough or is a leaf, apply force from center of mass
if (!cell.children || size / dist < theta) {
const force = 2000 * cell.mass / (dist * dist);
const fx = (dx / dist) * force;
const fy = (dy / dist) * force;
p.vx -= fx;
p.vy -= fy;
return;
}
// Otherwise recurse into children
for (const child of cell.children) {
this._applyQuadTreeForce(child, p, theta);
}
},
_draw() {
const ctx = this._ctx;
const w = this._width;