Files
feedBack/tests/js/highway_pause_throttle.test.js
T
byrongamatos 4a1b055afd fix(highway): the paused-frame throttle was throttling the whole venue
Pausing the song dropped the venue, the crowd and the stage to ~10 fps —
"everything around the highway drops fps by a lot".

draw() caps paused frames to one per _PAUSED_FRAME_INTERVAL_MS (100ms), on an
assumption stated plainly in highway-constants.js: a heavy WebGL renderer "does
a full render every frame even while paused. That is pure waste." That was true
when a paused chart was a still picture.

The venue broke the assumption. Its video backdrop keeps playing and its crowd
reacts on a clock of their own, and BOTH are drawn into the same canvas as the
notes — so a throttle aimed at static notes throttled the entire room. The
scene only got a texture upload 10 times a second while the transport sat
paused.

Renderers can now declare that their picture is not static while the chart
clock is stopped: an optional needsContinuousFrames(). The throttle is skipped
only when it returns exactly true, and the probe fails closed — a renderer that
doesn't implement it, or one that throws, keeps the throttle unchanged. So the
GPU saving that motivated #654 survives everywhere it was actually valid.

highway_3d implements it and claims continuous frames ONLY while a crowd video
is genuinely rolling (bound, unpaused, not ended, readyState >= 2). With no
venue pack — the common case — the paused scene really is static, so it keeps
the throttle and the GPU still idles.

Tests extend tests/js/highway_pause_throttle.test.js, which guards this code
path source-level (the draw loop owns the rAF + WebGL lifecycle and is
deliberately not reproduced in a vm — see the file header). The new guards pin
that the capability GATES the early return rather than merely being called near
it, that the probe fails closed on absent/non-function/throwing/truthy-but-not-
true, and that the 3D renderer keys off the real video elements and can still
return false. All 3 fail against the pre-fix source.

eslint 0 errors; JS 1202/1202; pytest 2597 passed.
2026-07-14 22:03:50 +02:00

127 lines
6.5 KiB
JavaScript

// Source-level guards for the playback-aware paused-render throttle
// (feedBack#654). The createHighway closure owns the rAF loop + WebGL
// context lifecycle that's too heavy to reproduce in a vm sandbox, so —
// like highway_visibility.test.js — these checks lock in the wiring.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const highwayJs = path.join(__dirname, '..', '..', 'static', 'highway.js');
// Brace-balanced extraction (shared shape with highway_visibility.test.js)
// so a future edit that grows the loop body doesn't get truncated.
function extractBlock(src, signature) {
const start = src.indexOf(signature);
assert.ok(start !== -1, `signature '${signature}' not found`);
const openBrace = src.indexOf('{', start);
assert.ok(openBrace !== -1, `opening brace after '${signature}' not found`);
let depth = 1;
let i = openBrace + 1;
while (i < src.length && depth > 0) {
const ch = src[i];
if (ch === '{') depth++;
else if (ch === '}') depth--;
i++;
}
assert.ok(depth === 0, `unbalanced braces after '${signature}'`);
return src.slice(start, i);
}
// R3c: highway.js is being carved into modules, so its source is no longer ONE file. Read the
// whole set. Re-pinning these assertions at whichever file currently holds a constant just
// means they break again on the next carve — and worse, a source-shape assertion that silently
// stops finding its target is indistinguishable from one that passes.
function highwaySources() {
const root = path.join(__dirname, '..', '..');
const jsDir = path.join(root, 'static', 'js');
const parts = [fs.readFileSync(path.join(root, 'static', 'highway.js'), 'utf8')];
for (const f of fs.readdirSync(jsDir).sort()) {
if (f.startsWith('highway-') && f.endsWith('.js')) {
parts.push(fs.readFileSync(path.join(jsDir, f), 'utf8'));
}
}
return parts.join('\n');
}
test('highway declares the paused-render throttle state', () => {
const src = highwaySources();
assert.match(src, /(?:export\s+)?const\s+_PAUSED_FRAME_INTERVAL_MS\s*=\s*\d+/, 'missing _PAUSED_FRAME_INTERVAL_MS cap');
assert.match(src, /hwState\._lastPausedDrawAt\s*=\s*0/, 'missing _lastPausedDrawAt accumulator');
});
test('draw() throttles full renders while the audio clock is stalled', () => {
const src = highwaySources();
const fn = extractBlock(src, 'function draw()');
// Reuse getTime()'s pause signal rather than inventing a parallel one.
assert.match(fn, /_chartLastAdvanceAt/, 'throttle must key off _chartLastAdvanceAt (the advance timestamp)');
assert.match(fn, /_CHART_MAX_INTERP_MS/, 'throttle must reuse the _CHART_MAX_INTERP_MS pause threshold');
assert.match(fn, /_PAUSED_FRAME_INTERVAL_MS/, 'throttle must cap paused draws to _PAUSED_FRAME_INTERVAL_MS');
assert.match(fn, /hwState\._lastPausedDrawAt\s*=\s*_nowP/, 'throttle must record the last paused draw time');
});
test('throttle runs after the ready gate, before bundle/draw', () => {
const src = highwaySources();
const fn = extractBlock(src, 'function draw()');
// Regex landmarks (not exact-string indexOf) so harmless spacing /
// semicolon changes don't break the ordering guard — matches the
// search-based style of the other highway source-guard tests.
const readyIdx = fn.search(/if\s*\(\s*!hwState\.ready\s*\)\s*return;/);
const throttleIdx = fn.search(/_PAUSED_FRAME_INTERVAL_MS/);
const drawIdx = fn.search(/_renderer\.draw\s*\(/);
assert.ok(readyIdx !== -1, 'ready gate not found');
assert.ok(throttleIdx !== -1, 'throttle not found');
assert.ok(drawIdx !== -1, '_renderer.draw call not found');
assert.ok(readyIdx < throttleIdx, 'throttle must come after the ready gate');
assert.ok(throttleIdx < drawIdx, 'throttle must come before the renderer draw');
});
// ── The throttle must not starve a renderer that animates on its own clock ──
//
// The throttle assumes a paused chart is a still picture, so re-rendering it is
// waste. That stopped being true when the venue landed: the 3D highway draws the
// venue's VIDEO backdrop and its reactive crowd into the same canvas as the
// notes, so capping paused frames capped the whole room — pausing the song
// dropped the venue to ~10 fps ("everything around the highway drops fps").
//
// Renderers now opt out via an optional needsContinuousFrames(). Absent or
// throwing must mean false, so every other renderer keeps the throttle.
test('paused throttle defers to a renderer that needs continuous frames', () => {
const src = highwaySources();
const fn = extractBlock(src, 'function draw()');
assert.match(fn, /_rendererNeedsContinuousFrames\s*\(\s*\)/,
'the paused throttle must consult the renderer capability');
// The capability must GATE the early-return, not merely be called near it:
// the throttle only applies when the renderer does NOT need every frame.
assert.match(
fn,
/!\s*_rendererNeedsContinuousFrames\s*\(\s*\)[\s\S]{0,160}_PAUSED_FRAME_INTERVAL_MS[\s\S]{0,40}return;/,
'throttle must be skipped when the renderer needs continuous frames',
);
});
test('the capability probe fails closed (absent / non-function / throwing)', () => {
const src = highwaySources();
const fn = extractBlock(src, 'function _rendererNeedsContinuousFrames()');
assert.match(fn, /typeof\s+r\.needsContinuousFrames\s*!==\s*'function'[\s\S]{0,40}return false/,
'a renderer without the method must keep the throttle');
assert.match(fn, /catch[\s\S]{0,40}return false/,
'a throwing renderer must keep the throttle, not crash the draw loop');
assert.match(fn, /===\s*true/,
'only an explicit true opts out — a truthy accident must not disable the throttle');
});
test('3D highway claims continuous frames only while a crowd video is rolling', () => {
const h3d = fs.readFileSync(
path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js'), 'utf8');
const fn = extractBlock(h3d, 'needsContinuousFrames()');
assert.match(fn, /_venueCrowdVideos/, 'must key off the actual crowd video elements');
assert.match(fn, /\.paused/, 'a paused video is a still frame — throttle should still apply');
// With no venue pack (the common case) the paused scene really is static and
// the GPU saving must survive: the method has to be able to return false.
assert.match(fn, /return false;/, 'must fall through to false with no live video');
});