perf: stop recomputing control bounds in overlap tests
compileGeometry took 1575 ms on a 41-road area and grew superlinearly — 6x the time for 2x the roads — while feature count grew linearly. Profiling put 84% of it in three adjacent lines: `bounds` alone was 821 ms, `ringsOverlap` 258 ms, and the intermediate arrays cost another 163 ms of GC. The callers walk every lane in 0.25 m steps and test each step's rectangle against every control feature, which is millions of calls on a small area. Each one recomputed the control's bounding box from scratch through four `Math.min(...ring.map(...))` spreads, allocating four arrays per call for a box that never changes. Control rings are the same array objects for the whole compile, so their bounds are now computed once into a WeakMap, candidate bounds are taken in a single allocation-free pass, and the cheap rejection moved up into `ringsOverlapControl` so the exact test only runs for boxes that actually touch. 1575 ms -> 104 ms, and the per-road cost is flat instead of rising, so this now extends to city-scale areas rather than degrading quadratically. Output is unchanged, which the fixture baselines prove. Found because a preview felt slow to drag. The measurement mattered more than the reading did: the nested `model.roads.find()` calls that looked like the culprit were not, and building the scoped-compile architecture first would have left this untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1621,19 +1621,55 @@ function directionArrowFeatures(road, lane, controlFeatures, diagnostics) {
|
|||||||
return features;
|
return features;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Control rings are the same array objects across the whole compile, while the
|
||||||
|
// candidate rings are built fresh, so only the controls are worth caching. Their
|
||||||
|
// bounds used to be recomputed inside every comparison: the callers walk each lane
|
||||||
|
// in 0.25 m steps and test the step's rectangle against every control, which on a
|
||||||
|
// 41-road area is millions of calls. Recomputing four `Math.min(...ring.map(...))`
|
||||||
|
// spreads per call made `bounds` alone 821 ms of a 1575 ms compile, plus 163 ms of
|
||||||
|
// GC from the intermediate arrays.
|
||||||
|
const controlRingBounds = new WeakMap();
|
||||||
|
|
||||||
|
function ringBounds(ring) {
|
||||||
|
let minX = Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
for (let index = 0; index < ring.length; index += 1) {
|
||||||
|
const x = ring[index][0];
|
||||||
|
const y = ring[index][1];
|
||||||
|
if (x < minX) minX = x;
|
||||||
|
if (x > maxX) maxX = x;
|
||||||
|
if (y < minY) minY = y;
|
||||||
|
if (y > maxY) maxY = y;
|
||||||
|
}
|
||||||
|
return [minX, minY, maxX, maxY];
|
||||||
|
}
|
||||||
|
|
||||||
|
function controlBounds(ring) {
|
||||||
|
let box = controlRingBounds.get(ring);
|
||||||
|
if (!box) {
|
||||||
|
box = ringBounds(ring);
|
||||||
|
controlRingBounds.set(ring, box);
|
||||||
|
}
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
function boundsDisjoint(a, b) {
|
||||||
|
return a[0] > b[2] || a[2] < b[0] || a[1] > b[3] || a[3] < b[1];
|
||||||
|
}
|
||||||
|
|
||||||
function ringsOverlapControl(rings, controls) {
|
function ringsOverlapControl(rings, controls) {
|
||||||
return rings.some((ring) => controls.some((feature) => ringsOverlap(ring, feature.geometry.coordinates[0])));
|
return rings.some((ring) => {
|
||||||
|
const box = ringBounds(ring);
|
||||||
|
return controls.some((feature) => {
|
||||||
|
const other = feature.geometry.coordinates[0];
|
||||||
|
// Cheap rejection first; the exact test only runs for boxes that touch.
|
||||||
|
return !boundsDisjoint(box, controlBounds(other)) && ringsOverlap(ring, other);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
function ringsOverlap(first, second) {
|
function ringsOverlap(first, second) {
|
||||||
const bounds = (ring) => [
|
|
||||||
Math.min(...ring.map((point) => point[0])),
|
|
||||||
Math.min(...ring.map((point) => point[1])),
|
|
||||||
Math.max(...ring.map((point) => point[0])),
|
|
||||||
Math.max(...ring.map((point) => point[1])),
|
|
||||||
];
|
|
||||||
const a = bounds(first);
|
|
||||||
const b = bounds(second);
|
|
||||||
if (a[0] > b[2] || a[2] < b[0] || a[1] > b[3] || a[3] < b[1]) return false;
|
|
||||||
if (first.some((point) => pointInPolygon(point, second)) || second.some((point) => pointInPolygon(point, first)))
|
if (first.some((point) => pointInPolygon(point, second)) || second.some((point) => pointInPolygon(point, first)))
|
||||||
return true;
|
return true;
|
||||||
return first
|
return first
|
||||||
|
|||||||
Reference in New Issue
Block a user