Skip to content

fix(tracing): Address redundant state captures per action - #377

Open
Winify wants to merge 1 commit into
webdriverio:mainfrom
Winify:fix/redundant-trace-capture
Open

fix(tracing): Address redundant state captures per action#377
Winify wants to merge 1 commit into
webdriverio:mainfrom
Winify:fix/redundant-trace-capture

Conversation

@Winify

@Winify Winify commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

What & why

mode: 'trace' took two DOM captures per action plus a readyState poll hiding the second one's motion. On native Appium each capture is two serial round trips (GET /screenshot ~1.2 s at 1.86 MB, GET /source ~0.09s at 40 KB) — ~1.2 s per action, bracketing #351's 40–60 s/run. This restores the original one-capture design and removes the two patches built on it.

Closes #351

The original approach

One capture per action in beforeCommand, taken before the command is issued — the one moment the driver is idle, so an action's result is the next action's "before" and no row resolves to a state in motion.

Stamped at the previous action's end (Date.now() for the session's first, which makes it the initial frame); afterCommand captures nothing in trace mode, it only drains the collector.

Nothing is waited for, because the gap it needs is the test's own: measured on Appium, a capture at a 0 s gap is 359–476 KB mid-transition against 1,871,924 B settled, and already settled at 0.1 s and 0.25 s. Cost: 1.19 s/action vs 2.41 s; end-to-end 12.4 s vs 19.1 s, live 5.9 s. Only the last action has no successor to hand its result to — hence a settle in exactly one place.

How it regressed

  • 44477f7 — per-action capture born: one capture in afterCommand, fire-and-forget.
  • b3ed046 — getPageSource enters the mobile element path.
  • 6f8cd47 — native guard: skips execute/getUrl/getTitle, so native ≈ one screenshot.
  • d924a02 — capture moved into beforeCommand, stamped at the previous action's end.
  • 06d0ee1 — the doubling: an eager post-action capture added beside the pre-capture, both stamped to one slot, the second discarded by the richer-screenshot merge.
  • 86f5b10 — the patch: waitForActionResult, a readyState poll + 250 ms pause, because that eager capture lands while the screen still moves.

The doubling came first; the wait hid the consequence of the capture it added. The beforeCommand design was never the problem, and the native guard survived throughout — just paid twice. Nuance for review: the second capture was not only waste — the merge keeps the larger screenshot, so it supplied the settled frame whenever a test had a gap.

That accident is why a settle is needed for the last action and nowhere else.

What changed

  • One capture per action; captureActionResult, waitForActionResult, the __wdioSnapMark tag deleted. The last action's capture comes from #finalizePerScenario, named after that action; FINAL_SNAPSHOT_COMMAND (final) is now a shared const reserved for a session that ran no action — skipping it by name would otherwise have dropped the only capture of the last action, i.e. the failing row's own screenshot.
  • Settle gated, not timed: the drain before it anchors each document once, so SessionCapturer.replacedDocumentInLastDrain says whether the last action navigated to an unseen document. No → return; yes → waitUntil(readyState === 'complete'), with the body.childElementCount > 0 clause dropped (empty-bodied is then correct, not a guaranteed 8 s timeout). Native pauses 250 ms.
  • backend nearestFrame preferred minimum absolute distance, so a row without its own capture could replay its successor's state; now latest at-or-before, matching the app's rule.
  • Cross-test stamp borrow: the log is run-long, so the next test's first pre-capture landed on the previous test's last-action slot, where the richer merge could replace it (under reloadSession, the post-reload page). It now stamps Date.now() when the scanned timestamp predates #currentTestStartWallTime (0 without per-test hooks, so standalone is unchanged).
  • Screencast: #pollInFlight keeps one shot outstanding (native 1.2 s against a 200 ms interval stacked ~6 deep; a 15 ms command measured 4.5–7.8 s); #pollGeneration invalidates a shot orphaned by stop(); start() claims the generation before its first await so a mid-first-screenshot stop() cannot have the loop armed underneath it.
  • Native example (wdio.native.conf.ts, pnpm demo:wdio:native) — the platform was previously unmeasurable. No APK; APPIUM_HOST/_PORT/_DEVICE.
  • Cleanups: the capture gate's predicate was written twice, now #isActionCommand; ActionSnapshot.command documented as a label, not a key.

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)

Notes for reviewers

Screenshots / recordings

@greptile-apps

greptile-apps Bot commented Sep 13, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with the revised capture, finalization, screencast, and playback behavior covered by focused tests.

Summary

  • Removes redundant post-command capture and document-tag polling.
  • Prevents snapshots from being merged across test boundaries.
  • Serializes polling screenshots and invalidates frames from stopped screencast generations.
  • Adds native Appium measurement coverage and updates trace behavior tests and documentation.

Diagram

sequenceDiagram
  participant Runner
  participant Service
  participant Driver
  participant Collector
  participant Exporter
  participant Backend

  Runner->>Service: beforeCommand(action N)
  Service->>Driver: capture snapshot
  Driver-->>Service: state left by action N-1
  Service->>Service: stamp at action N-1 end
  Runner->>Driver: execute action N
  Driver-->>Runner: command result
  Runner->>Service: afterCommand(action N)
  Service->>Collector: drain trace data
  Note over Service,Driver: No post-command snapshot

  Runner->>Service: test/session finalization
  Service->>Collector: force final document anchor
  Collector-->>Service: navigation detected?
  opt Last action introduced a document
    Service->>Driver: wait for readyState and paint
  end
  Service->>Driver: capture final action result
  Service->>Exporter: snapshots and trace streams
  Exporter->>Backend: trace artifact
  Backend->>Backend: choose latest frame at-or-before action
Loading

Reviews (1) · Last reviewed commit: "fix(tracing): Address redundant state ca..."

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 mobile trace mode costs 40-60s per run in per-action probes

1 participant