test(pin-trace): guard the scaling the wire index buys, not a stopwatch
The cost test asserted milliseconds per pin, and a deploy gate that happened to be running two test shards read 8.19 where an idle box reads 1.96 — so it failed the deploy over the machine's mood rather than over the code. Worse, the number it watched was the wrong one. Tracing a pin on a shared ground rail costs O(node) because the answer is a property of the whole node: a canvas with more legs on that rail HAS a bigger node, and a higher per-pin cost, by design. The hazard the index actually removes is different — the first cut re-scanned every wire on the canvas at every frame, so circuitry that had nothing to do with the traced net still slowed it down. So the test now grows the canvas with UNRELATED clusters (their own breadboard, their own rail — a board pad is where a walk stops, so they share no node) and asserts that tracing the same little net does not get slower. Best-of-seven, so it measures the code and not a scheduling hiccup. Indexed it reads ~0.6-0.9x across an eightfold canvas; with the cache disabled it reads 7.8x and fails, which is the negative control this threshold was set from.
This commit is contained in:
parent
1041301969
commit
54d9b90bdd
|
|
@ -186,71 +186,100 @@ describe('nets whose answer is not a board pin', () => {
|
|||
});
|
||||
|
||||
describe('cost of walking the net', () => {
|
||||
// Walking the whole node is not free, and the shape that hurts is the one
|
||||
// every real canvas has: a ground rail with dozens of legs on it. This is a
|
||||
// guard rail, not a benchmark — it exists so a future change that turns the
|
||||
// walk quadratic again shows up here instead of as "the canvas feels slow".
|
||||
it('stays well under a millisecond per pin on a 300-wire canvas', () => {
|
||||
// What is worth guarding, and what is not.
|
||||
//
|
||||
// Tracing a pin on a shared ground rail costs O(node): the answer is a
|
||||
// property of the whole node, so the whole node gets collected. A canvas
|
||||
// with more legs on that rail has a bigger node and a higher per-pin cost —
|
||||
// by design, not a defect, and an absolute millisecond ceiling on it just
|
||||
// fails whenever the machine is busy (it failed a deploy gate that happened
|
||||
// to be running two test shards).
|
||||
//
|
||||
// The real hazard is different: the first cut of this walk re-scanned EVERY
|
||||
// wire on the canvas at every frame, so wires that had nothing to do with
|
||||
// the traced net still made it slower. The per-array wire index fixed that,
|
||||
// and this is the test for it — grow the canvas with UNRELATED circuitry and
|
||||
// the cost of tracing the same little net must not move. A ratio says so
|
||||
// regardless of how loaded the machine is.
|
||||
const BB = 'bb';
|
||||
|
||||
/** One traced cluster (fixed size) plus `noise` legs of unrelated circuitry
|
||||
* on a second breadboard, which shares no node with it. */
|
||||
const buildCanvas = (noise: number) => {
|
||||
useSimulatorStore.setState({ boards: [], components: [], wires: [] } as never);
|
||||
useSimulatorStore.getState().addBoard('arduino-uno' as never, 0, 0, BOARD);
|
||||
const comps: unknown[] = [{ id: 'bb', metadataId: 'breadboard', x: 0, y: 0, properties: {} }];
|
||||
const wires: unknown[] = [wire('gnd', [BOARD, 'GND'], ['bb', 'bn.1'])];
|
||||
const N = 100;
|
||||
for (let i = 0; i < N; i++) {
|
||||
const comps: unknown[] = [
|
||||
{ id: BB, metadataId: 'breadboard', x: 0, y: 0, properties: {} },
|
||||
{ id: 'bb2', metadataId: 'breadboard', x: 0, y: 0, properties: {} },
|
||||
];
|
||||
const wires: unknown[] = [wire('gnd', [BOARD, 'GND'], [BB, 'bn.1'])];
|
||||
const TRACED = 20;
|
||||
for (let i = 0; i < TRACED; i++) {
|
||||
comps.push({ id: `led${i}`, metadataId: 'led', x: 0, y: 0, properties: {} });
|
||||
comps.push({ id: `r${i}`, metadataId: 'resistor-1k', x: 0, y: 0, properties: { value: '1000' } });
|
||||
wires.push(wire(`w${i}a`, [`led${i}`, 'C'], [`r${i}`, '1']));
|
||||
wires.push(wire(`w${i}b`, [`r${i}`, '2'], ['bb', `bn.${(i % 28) + 2}`]));
|
||||
wires.push(wire(`w${i}b`, [`r${i}`, '2'], [BB, `bn.${(i % 28) + 2}`]));
|
||||
wires.push(wire(`w${i}c`, [`led${i}`, 'A'], [BOARD, String((i % 12) + 2)]));
|
||||
}
|
||||
const s = useSimulatorStore.getState();
|
||||
s.setComponents(comps as never);
|
||||
s.setWires(wires as never);
|
||||
const state = useSimulatorStore.getState();
|
||||
|
||||
// The anodes go straight to a GPIO — the common case, and the one the
|
||||
// early exit is for.
|
||||
let t0 = performance.now();
|
||||
for (let i = 0; i < N; i++) {
|
||||
expect(traceDetailed(state as never, `led${i}`, 'A', 0).arduinoPin).toBe((i % 12) + 2);
|
||||
// Noise: its own rail, its own parts. A board pad is where a walk stops, so
|
||||
// none of this belongs to the node the assertions below trace.
|
||||
wires.push(wire('gnd2', [BOARD, 'GND.1'], ['bb2', 'bn.1']));
|
||||
for (let i = 0; i < noise; i++) {
|
||||
comps.push({ id: `nled${i}`, metadataId: 'led', x: 0, y: 0, properties: {} });
|
||||
comps.push({ id: `nr${i}`, metadataId: 'resistor-1k', x: 0, y: 0, properties: { value: '1000' } });
|
||||
wires.push(wire(`n${i}a`, [`nled${i}`, 'C'], [`nr${i}`, '1']));
|
||||
wires.push(wire(`n${i}b`, [`nr${i}`, '2'], ['bb2', `bn.${(i % 28) + 2}`]));
|
||||
wires.push(wire(`n${i}c`, [`nled${i}`, 'A'], [BOARD, String((i % 12) + 2)]));
|
||||
}
|
||||
const perDriven = (performance.now() - t0) / N;
|
||||
const st = useSimulatorStore.getState();
|
||||
st.setComponents(comps as never);
|
||||
st.setWires(wires as never);
|
||||
return { state: useSimulatorStore.getState(), traced: TRACED };
|
||||
};
|
||||
|
||||
// The cathodes all land on the same rail — the worst case, where the whole
|
||||
// node has to be collected before "ground" is certain.
|
||||
t0 = performance.now();
|
||||
for (let i = 0; i < N; i++) {
|
||||
expect(traceDetailed(state as never, `led${i}`, 'C', 0).arduinoPin).toBe(-1);
|
||||
/** BEST per-pin cost of tracing the traced cluster's cathodes (the rail case,
|
||||
* where the whole node is collected). The minimum of several passes on
|
||||
* purpose: the fastest pass is the one that was not interrupted, so it
|
||||
* measures the code rather than the machine's mood. */
|
||||
const costPerPin = (noise: number): number => {
|
||||
const { state, traced } = buildCanvas(noise);
|
||||
let best = Infinity;
|
||||
for (let r = 0; r < 7; r++) {
|
||||
const t0 = performance.now();
|
||||
for (let i = 0; i < traced; i++) {
|
||||
expect(traceDetailed(state as never, `led${i}`, 'C', 0).arduinoPin).toBe(-1);
|
||||
}
|
||||
best = Math.min(best, (performance.now() - t0) / traced);
|
||||
}
|
||||
const perRail = (performance.now() - t0) / N;
|
||||
return best;
|
||||
};
|
||||
|
||||
it('unrelated wires elsewhere on the canvas do not slow a trace down', () => {
|
||||
const quiet = costPerPin(20); // ~122 wires
|
||||
const busy = costPerPin(320); // ~1022 wires, same little net under test
|
||||
const growth = busy / quiet;
|
||||
console.log(
|
||||
`[pin-trace] 301 wires: ${perDriven.toFixed(3)} ms/pin driven, ` +
|
||||
`${perRail.toFixed(3)} ms/pin on the shared rail`,
|
||||
`[pin-trace] ${quiet.toFixed(3)} ms/pin with 122 wires, ` +
|
||||
`${busy.toFixed(3)} with 1022 — growth ${growth.toFixed(2)}x`,
|
||||
);
|
||||
expect(perDriven).toBeLessThan(3);
|
||||
expect(perRail).toBeLessThan(8);
|
||||
// Indexed: ~1x, the node is identical. Re-scanning every wire per frame:
|
||||
// ~8x, since the canvas is eight times bigger. 3.0 separates them with
|
||||
// room for timer noise.
|
||||
expect(growth).toBeLessThan(3.0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('a net shared by two boards', () => {
|
||||
it('answers each board its own pad', () => {
|
||||
// One sensor line wired to a GPIO on BOTH boards. Whoever pre-registers
|
||||
// the sensor for board B must learn B's pad, whatever order the wires
|
||||
// were drawn in — the walk must not stop at A's pad and report "not on B".
|
||||
useSimulatorStore.setState({ boards: [], components: [], wires: [] } as never);
|
||||
useSimulatorStore.getState().addBoard('arduino-uno' as never, 0, 0, 'uno-a');
|
||||
useSimulatorStore.getState().addBoard('esp32' as never, 0, 0, 'esp-b');
|
||||
const s = useSimulatorStore.getState();
|
||||
s.setComponents([]);
|
||||
s.setWires([
|
||||
wire('x1', ['sensor', 'ECHO'], ['uno-a', '7']),
|
||||
wire('x2', ['sensor', 'ECHO'], ['esp-b', '19']),
|
||||
] as never);
|
||||
const state = useSimulatorStore.getState();
|
||||
expect(traceBoardGpio(state as never, 'sensor', 'ECHO', 'uno-a')).toBe(7);
|
||||
expect(traceBoardGpio(state as never, 'sensor', 'ECHO', 'esp-b')).toBe(19);
|
||||
// And a board the net never reaches is still "not here".
|
||||
expect(traceBoardGpio(state as never, 'sensor', 'ECHO', 'nope')).toBeNull();
|
||||
it('a pin wired straight to a GPIO short-circuits, far cheaper than the rail', () => {
|
||||
const { state, traced } = buildCanvas(120);
|
||||
let driven = Infinity;
|
||||
for (let r = 0; r < 7; r++) {
|
||||
const t0 = performance.now();
|
||||
for (let i = 0; i < traced; i++) {
|
||||
expect(traceDetailed(state as never, `led${i}`, 'A', 0).arduinoPin).toBe((i % 12) + 2);
|
||||
}
|
||||
driven = Math.min(driven, (performance.now() - t0) / traced);
|
||||
}
|
||||
// The early exit on the first driven pad is what keeps the common case
|
||||
// cheap; if it ever stops firing, this collapses towards the rail cost.
|
||||
expect(driven).toBeLessThan(costPerPin(120));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue