From a1e53866177a688c0d7d601ab7145e36fb47b2f9 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 12 Aug 2026 12:52:37 -0400 Subject: [PATCH] Back the coverage accumulator off on large apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The periodic takePreciseCoverage() that guards against Chrome being killed mid-collection ran on a fixed 100ms timer. That assumes the call is cheap, and it isn't: V8 serialises every loaded script, so the cost scales with the app. Measured on a mid-size Ember app, with only a handful of unit tests running: calls=24 mean=35.2ms median=2ms p90=98ms max=188ms (29% over 50ms) Against a 100ms interval that leaves the browser doing little except producing coverage, and testem runs N of these per machine. On a large suite it pushed tests past their own timeouts — an app with ~32 browsers per CI machine saw 141 tests time out that otherwise pass. Schedule the next take off how long the last one actually took, at roughly a 1/10 duty cycle, never sooner than the configured floor. Small apps keep the current cadence, large apps back off on their own rather than needing every consumer to rediscover this and hand-tune it. Adds chrome.cacheInterval (default 100) for that floor. --- src/testem/middleware.js | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/testem/middleware.js b/src/testem/middleware.js index ac7856a..ed50884 100644 --- a/src/testem/middleware.js +++ b/src/testem/middleware.js @@ -183,6 +183,7 @@ export function middleware(options = {}) { connectionTimeout = 30_000, remoteDebuggingPort = 9222, userDataDir = os.tmpdir(), + cacheInterval = 100, stragglerTimeout = 30_000, } = chrome || {}; const normalizedReporters = normalizeReporters(reporters); @@ -286,7 +287,28 @@ export function middleware(options = {}) { * ms of the run. Which functions survived was a timing race that played out * differently per OS — issue #22.) */ - const CACHE_INTERVAL = 100; // ms + const CACHE_INTERVAL = cacheInterval; + + /** + * How much of a browser's time this accumulator is allowed to consume. + * + * A fixed short interval quietly assumes takePreciseCoverage is cheap. It is + * not: V8 serialises every script currently loaded, so the cost scales with + * the app. Measured on a mid-size Ember app it was mean 35ms / p90 98ms / + * max 188ms per call — against a 100ms interval, i.e. the browser was doing + * little else, and with N browsers on one machine that is enough to push + * tests past their own timeouts. + * + * So the next take is scheduled off how long the last one actually took, at + * roughly a 1/(FACTOR+1) duty cycle, and never sooner than cacheInterval. + * Small apps keep the old cadence; large apps back off on their own instead + * of needing every consumer to discover this and tune it by hand. + */ + const CACHE_DUTY_FACTOR = 9; + + function nextCacheDelay(lastTakeMs) { + return Math.max(CACHE_INTERVAL, lastTakeMs * CACHE_DUTY_FACTOR); + } function mergeIntoCache(state, result) { state.coverageCache = state.coverageCache @@ -306,9 +328,15 @@ export function middleware(options = {}) { const session = state.cdpClient; if (!session || state.coverageFinalized) return; + + let elapsed = 0; + try { + const startedAt = Date.now(); const { result } = await session.Profiler.takePreciseCoverage(); + elapsed = Date.now() - startedAt; + // Discard the delta if the session changed while the call was in flight // (e.g. the pre-reload tab's stale data arriving after the fresh // coverage tab attached and reset the cache) or if the final result was @@ -321,7 +349,10 @@ export function middleware(options = {}) { } // Schedule the next refresh only if this browser's session is still alive. if (state.cdpClient && !state.coverageFinalized) { - state.cacheTimerHandle = setTimeout(() => refreshCoverageCache(state), CACHE_INTERVAL); + state.cacheTimerHandle = setTimeout( + () => refreshCoverageCache(state), + nextCacheDelay(elapsed), + ); } }