Skip to content

feat(rule): gate sync rule-result resolution behind runTypeASync flag#235

Open
chikara1608 wants to merge 1 commit into
mainfrom
feat/run-type-a-sync-ff
Open

feat(rule): gate sync rule-result resolution behind runTypeASync flag#235
chikara1608 wants to merge 1 commit into
mainfrom
feat/run-type-a-sync-ff

Conversation

@chikara1608

Copy link
Copy Markdown
Collaborator

What

Gates per-rule result resolution on a new runTypeASync flag (set into axe._cache by a11y-engine-core from the a11yCoreConfig.runTypeASync config field):

  • Flag ONresolve(ruleResult) synchronously.
  • Flag OFF (default) → original setTimeout(0) defer (Deque #1172), unchanged behaviour, plus lightweight yield-latency instrumentation.

Why

The setTimeout(0) wrapper around resolve(ruleResult) creates one macrotask transition per rule (~335). On large DOMs with advance ON, these interleave with concurrent DevTools IPC (resource.getContent), inflating the per-rule resolve from ~8ms to ~180ms and axe.run from ~14s to ~85s (Savvas Realize investigation). Resolving synchronously eliminates the contention.

We are shipping the fix behind a flag, default OFF, and want to measure which other customers exhibit the same pathology before enabling it for them.

Instrumentation (OFF path only)

Accumulates the schedule-to-fire delay of each resolve yield onto axe._typeASyncYield { totalMs, count, maxMs }. a11y-engine-core reads this back after axe.run and surfaces it in Type A telemetry. totalMs ≈ the wall-clock the async resolve costs = the exact time the flag would recover.

Cost / safety

  • Adds no new macrotask — only times the setTimeout that already existed on main. Event-loop behaviour on the OFF path is byte-for-byte identical to today.
  • ~2 performance.now() + a few arithmetic ops per rule ≈ ~1ms per scan; scales with rule count, not DOM size.
  • No per-rule allocations (three scalars on one object, reset per run) — no GC pressure.
  • Fully wrapped in try/catch: instrumentation can never break rule resolution.

Tagging

All changes carry // [a11y-core]: per submodule convention.

Test plan

  • build_axe.sh compiles clean; runTypeASync + _typeASyncYield present in built axe.js.
  • Flag ON: rules resolve synchronously, type_a_sync_yield_count reports 0.
  • Flag OFF: type_a_sync_yield_total_ms populated on a large advance-ON scan.

Paired with a11y-engine PR (flag extraction + telemetry surfacing + submodule bump).

🤖 Generated with Claude Code

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>
@chikara1608 chikara1608 force-pushed the feat/run-type-a-sync-ff branch from 351ca61 to e023225 Compare June 4, 2026 05:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants