flash

Prepared hint models

AppMonitor prepares complete hint models for the focused app. Each model carries the app’s dirty token, configuration revision, and completion time. Lookup and publication check those identities; invalid or stale models are never served. Activation can run a complete walk on demand. Background work never publishes a partial walk or changes provider ordering.

PreparedModelScheduler owns refresh and maintenance tickets on the main thread. It takes uptime explicitly, so its timing policy is tested with a fake clock. One refresh wake coalesces a burst; further events extend its deadline. An earlier focus, configuration, or user-action request replaces a throttled wake with a new ticket. Later noisy events cannot demote that request. Each callback must consume its own ticket, so a cancelled or replaced callback cannot clear or execute newly scheduled work for the same PID.

AX and queued refreshes debounce for 80 ms and observe a 2.5-second minimum interval between automatic walks. Maintenance has a separate identity and bypasses that minimum interval: it wakes 250 ms before the model’s freshness ceiling, then uses the same 80-ms debounce. Each model carries its own ceiling: it starts at 1.5 s and doubles (up to 30 s) every time a maintenance walk reproduces the previous model unchanged — same dirty token, configuration revision, and target geometry/role digest — and resets to 1.5 s on any other outcome. A maintenance wake is skipped outright after 60 s without keyboard or pointer input; the model then expires and the next activation walks on demand.

Maintenance is the one self-renewing wake — each stored model arms the next — so it is a deadline registration on the shared PollScheduler (core:prepared_model_maintenance) rather than a timer of its own: it coalesces with the process’s other wake-ups and is held while the displays sleep or the session is locked. Only the frontmost app’s model is stored, so one registration serves, and a cancelled maintenance releases it. Its .normal priority allows 100 ms of slack, inside the 250-ms lead, so a late wake still starts before the ceiling. Debounce and readiness wakes re-arm themselves too — later events extend a debounce, each readiness step arms the next — so they ride the same clock at .normal, one registration per app and kind (core:prepared_model_refresh:<pid>, core:prepared_model_readiness:<pid>) that cancelling the app’s refresh work releases; an activation that arrives first walks on demand rather than waiting on them. The lead allows eligible cheap walks to finish before the current model expires. A newer completed model replaces the prior maintenance ticket even when its dirty token and configuration revision are unchanged.

AX observer sources are hosted on a dedicated AXObserverThread; callbacks enqueue and the main thread drains each burst in one hop, bumping every dirty token in arrival order. Observer registration, its retry ladder, focused-window re-observation and teardown run on a serial queue of their own, so a walk, a readiness probe or a commit on the AX queue never waits behind an app slow to answer a registration; a multi-app screen-scope walk steps one app per turn and asks main between apps whether its activation still stands. AX storms and automatic walks taking at least 50 ms suppress AX, queued, and maintenance warming. Focus and configuration requests can reassess those apps; explicit activation remains available and complete. A degenerate walk (see below) is fast because there was nothing to read, so it never lifts the slow-walk suppression. Suppression, invalidation, termination, and shutdown revoke the applicable tickets. No timer is a second source of model validity: maintenance also rechecks focus, dirty token, and configuration before requesting a refresh.

Windows that are covered

A walk first computes the focused window’s visible region from the window list (WindowSnapshot). When nothing of the window is visible — another process’s floating window, the Dock or a lock screen covers all of it — the walk is occluded and reads nothing. An occluded walk is never cached: whether the window is covered is window-list state that no AX event reports, so nothing would invalidate an empty model once the covering window left. It evicts the current model instead (that model no longer describes the screen) and arms no maintenance; nor does it count toward the slow-walk backoff, the empty-walk gate or a readiness rewalk. An activation that finds no model recomputes the visible region and walks on demand, so the same window list always gives the same hints, and a window still fully covered at activation stays silent. A partly covered window is the same window-list state: every stored model carries the visible region its walk saw — the window’s frame and the fragments left of it — and a lookup recomputes the region from the activation’s own window-list read, through the builder the walk used, and treats a difference as a miss. A notification banner, a floating player or a panel moving over or off the window therefore costs one walk, never a stale set of hints drawn over the cover or missing from what it uncovered.

Trees that are not built yet

Chromium and Flutter build their accessibility tree asynchronously after the enhanced-UI wake, and Gecko once its tree is read (AppTraits.buildsAccessibilityTreeAsynchronously). A walk that runs first finds an empty or half-built tree in a few milliseconds and yields a degenerate result: empty, or below a tenth of the app’s last healthy count (discoveryLooksDegenerate). The healthy count is recorded from activations and from stored background walks that are not degenerate, and forgotten when the app quits.

Readiness is decided by a bounded probe, AccessibilityReadiness: one batched read (role and children) per element, breadth-first under the walked window, at most 32 elements, descending only into containers. A web area with children is ready and an empty one is not; without a web area in reach, a window with more than five direct children, or a tree larger than the probe reads, is ready. The probe runs on the AX queue (Gecko inside GeckoAccessibility.withTree) and never decides the hints; it only decides when the one walk that follows is worth doing. Waits between probes follow ReadinessLadder: 50, 100, 200, 400 ms, then a final 750 ms after which the walk runs regardless, 1.5 s at most. Every wait is a deadline on the shared PollScheduler (.system while an activation waits on it, .normal for a background ladder); nothing sleeps on the main thread or the AX queue.

Gecko discards its tree whenever its accessibility mode is switched off, so switching it off after every operation makes walks and probes wake a tree that may not be built yet — the Firefox background walks that come back empty in 1–5 ms. The mode Flash turns on therefore stays on while the app is focused (GeckoAccessibility; see runtime ownership): one wake per focus builds the tree, the ladder’s probes watch that same tree fill, and later walks read it built, the two Gecko scopes of one walk (the walked window’s frame, then the tree) included. Focus leaving the app, a Space change and every window move switch it off, so the first walk after one of those can still find the tree unbuilt, and is repaired as above.

A readiness step has its own ticket kind beside refresh and maintenance. A probe holds the ticket while it runs, and its verdict applies only if the hold survives: focus leaving the app, a walk starting, termination, a newer ladder or cancelRefreshWork revoke it.

Apps whose own tree has nothing

A volatile provider (tmux) owns a terminal’s hints; the prepared model is its Accessibility fallback. When the provider declines and the app’s tree has never produced targets, a degenerate activation walk is the answer: it is not repeated, and the activation ends silently. After five consecutive empty automatic walks of an app whose provider plan includes a volatile provider, the app gets no automatic walks at all — focus, AX events, maintenance, readiness — until an activation finds targets in its tree, the configuration changes, or it quits (EmptyBackgroundWalkGate, logged once as [ax] model_refresh_gated). The judgement is the walks, never the bundle identifier: terminals that expose real Accessibility targets keep warming.