Skip to content

Commit 351ca61

Browse files
chikara1608claude
andcommitted
feat(rule): gate sync rule-result resolution behind runTypeASync flag
Resolving each rule's result via setTimeout(0) (Deque #1172) creates one macrotask transition per rule (~335). On large DOMs with advance ON these interleave with DevTools IPC (resource.getContent), ballooning the per-rule resolve from ~8ms to ~180ms and inflating axe.run from ~14s to ~85s. When axe._cache 'runTypeASync' is set (by a11y-engine-core run.js from the a11yCoreConfig flag), resolve synchronously to eliminate the contention. The flag defaults OFF, preserving the original deferred behaviour. On the OFF path, instrument the schedule-to-fire delay of each yield and accumulate {totalMs,count,maxMs} onto axe._typeASyncYield so every scan reports how much wall-clock the async resolve costs — the exact time the flag would recover. Read back into Type A telemetry by a11y-engine-core. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0371a36 commit 351ca61

1 file changed

Lines changed: 42 additions & 4 deletions

File tree

lib/core/base/rule.js

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -310,11 +310,49 @@ Rule.prototype.run = function run(context, options = {}, resolve, reject) {
310310
if (options.performanceTimer) {
311311
this._logRulePerformance();
312312
}
313-
// Defer the rule's execution to prevent "unresponsive script" warnings.
314-
// See https://github.com/dequelabs/axe-core/pull/1172 for discussion and details.
315-
setTimeout(() => {
313+
// [a11y-core]: gate rule-result resolution on the runTypeASync feature
314+
// flag (set into axe._cache by a11y-engine-core run.js). The original
315+
// setTimeout(0) defer (Deque #1172) creates one macrotask transition per
316+
// rule (~335) that, on large DOMs with advance ON, interleaves with
317+
// DevTools IPC (resource.getContent) and inflated axe.run from ~14s to
318+
// ~85s. Resolving synchronously eliminates that contention.
319+
if (cache.get('runTypeASync')) {
316320
resolve(ruleResult);
317-
}, 0);
321+
} else {
322+
// [a11y-core]: original deferred path retained as the default (avoids
323+
// "unresponsive script" warnings, Deque #1172). Instrument the
324+
// schedule-to-fire delay of this yield so each scan reports how much
325+
// wall-clock the async resolve costs — this is the exact time the
326+
// runTypeASync flag would recover. Accumulate onto the axe global, not
327+
// axe._cache, which is cleared on teardown before resolve fires.
328+
let scheduledAt;
329+
try {
330+
scheduledAt = window.performance.now();
331+
} catch {
332+
scheduledAt = undefined;
333+
}
334+
setTimeout(() => {
335+
try {
336+
if (scheduledAt !== undefined) {
337+
const yieldDelay = window.performance.now() - scheduledAt;
338+
const yieldStats = axe._typeASyncYield || {
339+
totalMs: 0,
340+
count: 0,
341+
maxMs: 0
342+
};
343+
yieldStats.totalMs += yieldDelay;
344+
yieldStats.count += 1;
345+
if (yieldDelay > yieldStats.maxMs) {
346+
yieldStats.maxMs = yieldDelay;
347+
}
348+
axe._typeASyncYield = yieldStats;
349+
}
350+
} catch {
351+
// instrumentation must never break rule resolution
352+
}
353+
resolve(ruleResult);
354+
}, 0);
355+
}
318356
}).catch(error => {
319357
if (options.performanceTimer) {
320358
this._logRulePerformance();

0 commit comments

Comments
 (0)