Skip to content

Keep the polling filmstrip from queueing in front of the test - #567

Open
vishnuv688 wants to merge 11 commits into
mainfrom
perf/561-filmstrip-poll-queue
Open

vishnuv688 wants to merge 11 commits into
mainfrom
perf/561-filmstrip-poll-queue

Conversation

@vishnuv688

@vishnuv688 vishnuv688 commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

What & why

Closes #561. Part of the #516 follow-ups.

With no CDP screencast (Appium, Safari), the filmstrip recorder polls a screenshot every 200 ms, and a driver serves one session's requests one at a time, so polls queued in front of the test's commands and per-action captures. Measured in #516: a per-action screenshot took 600-665 ms on the Android emulator with the poller running, against 229 ms with no recorder.

Lossless changes, default on:

  • Skip a poll while the driver is busy. One gate in core (driver-activity.ts) is held by each adapter's command hook and by the per-action capture; the recorder skips a tick while it is held. Waits (whose polls signal on their own) and host-only commands (pause, debug, ...) do not hold it. It replaces the old input-only gate, which covered a subset of commands, so the chromedriver click race stays covered.
  • A bounded gap. Once the newest frame is three intervals old, the recorder polls anyway during an ordinary command, so the longest gap is about three intervals plus one screenshot. It never polls during a click, typing (including WebdriverIO's nested elementClick/elementSendKeys/performActions, Selenium's actions().perform()) or a per-action capture, whose own screenshot is the frame.
  • Reuse capture screenshots as frames. Each per-action capture offers its screenshot to the polling recorder as a frame, and the next tick is skipped; stale or duplicate shots are dropped, and maxBufferFrames and thinning apply as usual.
  • A recorder line in the timing report: with DEVTOOLS_CAPTURE_TIMING=1, the report prints screenshots taken, ticks skipped (and why) and capture screenshots reused as frames.

The polling interval and the filmstrip default are unchanged; lossy options stay out of scope.

Type of change

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Polish (an improvement to an existing feature)
  • Breaking change (existing behavior changes for users)
  • Documentation
  • Internal (build, CI, dependencies, tooling)

Packages touched

  • shared (types and contracts)
  • core (framework-agnostic capture/reporting)
  • elements (published element/snapshot API — @wdio/elements)
  • service (WebdriverIO adapter)
  • nightwatch-devtools (Nightwatch adapter)
  • selenium-devtools (Selenium adapter)
  • selenium-devtools-py (Selenium Python adapter)
  • backend (server)
  • app (UI)
  • script (page-injected runtime)
  • trace (Trace mode)

Capability matrix

  • packages/conformance/src/features.ts updated, and tables regenerated with pnpm docs:capabilities, if this adds or changes user-facing behaviour

No capability change: the filmstrip reasons ("other browsers poll", "poll every 200 ms") still hold.

Notes for reviewers

  • Why core: all three JS adapters poll through core's ScreencastRecorderBase; the adapters only signal command start and end from the hooks they already have. Python has no polling recorder.
  • Nightwatch opens a window only for commands that call back (input, mapped, or a known read); browser.element() and custom commands get none, so they cannot hold the gate for its 3 s bound.
  • Residuals: one gate per process (a process driving several sessions shares it, and frame reuse turns off while more than one recorder records); a poll already sent when a command starts still runs first; after a failed assert, Nightwatch's aborted queue strands the input windows of commands enqueued after it for up to 3 s.
  • Two independent reviews; every finding is addressed.

Testing

  • pnpm build, pnpm typecheck, pnpm test (4090 passed, 1 skipped), pnpm test:ui, pnpm lint, pnpm docs:capabilities --check are green. New tests fail without the change.
  • Measured against main on the same machine, trace mode with DEVTOOLS_CAPTURE_TIMING=1:
Run Per-action screenshot median Capture total Run
Desktop Safari, Selenium example (2 runs) 153 / 84 ms → 71 / 67 ms 2.0 / 1.2 s → 1.0 / 1.0 s 30.1 / 25.8 s → 25.1 / 22.9 s
iOS simulator, Selenium mobile (2 runs) 437 / 389 ms → 452 / 390 ms 3.6 / 3.0 s → 3.2 / 2.7 s 34.3 / 30.3 s → 32.6 / 29.2 s
iOS simulator, Nightwatch mobile 442 → 391 ms 3.3 → 2.5 s 27.2 → 24.9 s

Filmstrip frames in the traces are equal or more (Nightwatch 7 → 9, Selenium 4 → 5). The iOS simulator gain is small because its polls are fast (about 120 ms).

Needs checking: the Android emulator, where #516 measured the 600-665 ms: DEVTOOLS_MODE=trace DEVTOOLS_CAPTURE_TIMING=1 pnpm demo:selenium:mobile and pnpm demo:nightwatch:mobile, comparing the per-capture screenshot median and the filmstrip recorder: line.

Screenshots / recordings

None.

🤖 Generated with Claude Code

vishnuv688 and others added 2 commits November 10, 2026 17:09
Without a CDP screencast a driver serves the 200 ms screenshot polls and the
test's own requests one at a time, so the polls sat in front of commands and
per-action captures (#516: a per-action screenshot 600-665 ms on Android with
the poller running, 229 ms without). The recorder now skips a tick while a
command or a capture holds the driver, through one bounded gate in core that
the adapters' command hooks and captureActionSnapshot open; it replaces the
input-only gate, whose click race it covers. Waits and host-side commands open
nothing, since the driver is idle between their polls. Each capture's
screenshot is kept as a filmstrip frame and the poll that would repeat it is
skipped. The recorder's own screenshots and skipped ticks are counted in the
DEVTOOLS_CAPTURE_TIMING report so this cost stays visible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vishnuv688 vishnuv688 self-assigned this Nov 10, 2026
@vercel

vercel Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
devtools Ready Ready Preview Nov 10, 2026 12:46pm UTC

Request Review

vishnuv688 and others added 9 commits November 10, 2026 17:30
A tick skipped for a busy driver was dropped, not deferred, so a run of
back-to-back commands that take no capture (reads, lookups, wait polls, or any
live-mode run on Safari or Appium) could leave the filmstrip without a frame for
as long as the run lasted. Once the newest frame is three intervals old the
recorder polls anyway, still one shot at a time. An input-dispatching command
is never overridden, since a poll inside its dispatch makes the click activate
nothing, so the gate tells input windows apart again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#afterCommand returns before closing the window while #injecting, so an
injection call from a re-arm (reloadSession, a context change) held the gate
to its 3 s bound and replaced the user command's single-field window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The window closes from the callback the proxy appends, so a wrapped function
that never calls it (a builder or scoped API such as actions() or element(),
or a function-style custom command that never calls done) held the
filmstrip's gate for its full 3 s bound on every call. The window now opens
only for an input command, a command the trace vocabulary maps, or a known
driver read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recorder counters are only drained when a trace is exported, so a live-mode
run with DEVTOOLS_CAPTURE_TIMING on kept every shot's duration for the life of
the process. The count and total stay exact; the median reads a ring of the
newest 2000 shots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gap cap counted from the newest frame, usually the previous capture's, so
a 1.4 to 2.4 s native capture crossed it partway and the capped poll queued
behind the capture's own screenshot, adding nothing and holding the driver in
front of the next command. A capture window is now never overridden; its own
screenshot is the frame. WDIO runs its hooks for the protocol commands nested
in click and setValue, whose single window then dropped the input mark, so
elementClick, elementSendKeys, elementClear, performActions and releaseActions
count as input, as do Nightwatch's updateValue, setPassword, clickAndHold and
releaseMouseButton. Selenium's actions().perform() goes through execute(), so
it opens its input window itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In Nightwatch v3 browser.element() builds a scoped element and never calls the
appended callback, yet it is listed among the read-only commands, so it held
the gate to its bound on every call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A capped poll still waits behind the busy command and takes its own
screenshot time, so the longest gap is about three intervals plus one
screenshot, and captures as well as input commands are never overridden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

Native: stop the polling filmstrip from queueing in front of per-action capture

1 participant