Skip to content

perf(electron-service): batch updateAllMocks into a single CDP round-trip#292

Open
goosewobbler wants to merge 5 commits into
mainfrom
perf/electron-batch-update-mocks
Open

perf(electron-service): batch updateAllMocks into a single CDP round-trip#292
goosewobbler wants to merge 5 commits into
mainfrom
perf/electron-batch-update-mocks

Conversation

@goosewobbler
Copy link
Copy Markdown
Contributor

Summary

  • Closes perf(electron-service): batch updateAllMocks into a single CDP round-trip #268 — element-command overrides previously paid N CDP round-trips per click/setValue/etc (one per registered mock). The scheduler now performs at most one native (browser.electron.execute) and one browser-mode (browser.execute) round-trip per batch regardless of mock count.
  • Each mock variant (api / prototype / constructor / browser-mode) exposes a JSON-serialisable __accessor and a local __applyCalls(data) method. The batched scheduler reads all mocks in one execute and dispatches slices to each mock's __applyCalls with no further I/O.
  • Per-mock mock.update() is unchanged for explicit user calls and withImplementation.

Commit layout

  1. feat(native-types) — adds ElectronMockReadAccessor plus the two new optional hooks on ElectronMockInstance (types only).
  2. refactor(electron-service) — wires __accessor + __applyCalls onto every mock variant; behavior-preserving (update() now delegates its apply phase to the same helper).
  3. perf(electron-service) — rewrites MockUpdateScheduler#runOnce to split by accessor kind and batch each subset; updates one existing scheduler test and adds three new ones.

Test plan

  • pnpm --filter @wdio/electron-service test — 510 tests pass (was 507 before the refactor; 3 new + 1 updated).
  • pnpm --filter @wdio/electron-service lint — clean (pre-existing warnings only).
  • pnpm --filter @wdio/electron-service typecheck and pnpm exec turbo run typecheck — all 21 tasks pass.
  • E2E acceptance: should trigger mock updates when DOM interactions occur in e2e/test/electron/mocking.spec.ts — needs a built app to run; please verify in CI.

New unit tests

  • should batch updates for ≥2 mocks into a single browser.electron.execute call — covers all three native accessor kinds (api, prototype, constructor) and asserts exactly one CDP call.
  • should batch updates for ≥2 browser-mode mocks into a single browser.execute call — registers two browser-mode mocks, asserts exactly one browser.execute call, and verifies the payload round-trips through parseCallData.
  • should split native and browser-mode mocks across exactly two round-trips — proves the routing split fires both transports exactly once when both kinds are registered.

The existing concurrent-scheduler and recovery tests still pass via a small legacy fallback that calls mock.update() for any mock without an accessor — kept narrowly so plain test doubles ({ update: vi.fn() }) continue to work without restructuring.

Sibling work (follow-up)

The issue notes that @wdio/tauri-service has the same per-mock-update cost. Not included here; should be filed/folded as a follow-up once this pattern lands.

🤖 Generated with Claude Code

goosewobbler and others added 3 commits May 22, 2026 20:40
Adds two optional hooks to ElectronMockInstance — __accessor (JSON-serialisable
descriptor of how to reach the inner mock) and __applyCalls (local diff-apply,
no I/O). The new ElectronMockReadAccessor type discriminates on api / prototype
/ constructor / browser. Used by electron-service to batch every mock's call-data
read into a single CDP round-trip; see #268.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… mock

Each mock variant (api, prototype, constructor, browser-mode) now exposes the
new accessor descriptor and a local diff-apply method. The existing per-mock
update() API is preserved verbatim and now delegates its apply phase to the
same helper, so explicit user calls still work end-to-end. No behavior change
yet — the scheduler refactor in the next commit is what actually batches reads.

Refs #268.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…trip

The scheduler now splits registered mocks by accessor kind and routes each
subset through one round-trip:

- Native mocks (api / prototype / constructor) ride a single
  browser.electron.execute() whose inner script walks every accessor and
  returns {[mockId]: {calls, results}}.
- Browser-mode mocks ride one browser.execute() against window.__wdio_mocks__,
  with raw payloads post-processed via interceptor.parseCallData() to
  reconstruct Error markers.

Each mock's __applyCalls() distributes its slice locally with no further I/O.
Mocks without an accessor (test doubles) fall through to the per-mock
update() path. Per-mock mock.update() is unchanged for explicit user calls.

Tests:
- Updated "should update mocks after overridden element command executes" to
  assert __applyCalls is invoked (the new sync path).
- Added "should batch updates for ≥2 mocks into a single
  browser.electron.execute call" covering all three native accessor kinds.
- Added "should batch updates for ≥2 browser-mode mocks into a single
  browser.execute call" with parseCallData round-trip assertion.
- Added "should split native and browser-mode mocks across exactly two
  round-trips" proving the routing split fires both transports exactly once.

Closes #268.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

Release Preview — no release

No bump label detected.
Reason: No release labels found (need bump:* or release:stable)
Note: Add bump:patch, bump:minor, or bump:major to trigger a release.


Updated automatically by ReleaseKit

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 22, 2026

Greptile Summary

This PR replaces the per-mock update() call-per-element-command pattern with a batched scheduler that collapses all native mocks into a single browser.electron.execute round-trip and all browser-mode mocks into a single browser.execute round-trip, regardless of how many mocks are registered.

  • Mock variants (api, prototype, constructor, browser) each expose a JSON-serialisable __accessor and a synchronous __applyCalls closure; the scheduler reads all state in one call and dispatches slices locally with no further I/O.
  • Legacy compatibility is preserved via a narrow fallback that calls mock.update() for objects without an accessor, keeping existing test doubles working without restructuring.
  • Four new unit tests verify the single-round-trip invariant for native and browser-mode batches, the mixed-transport split, and per-reader failure isolation.

Confidence Score: 5/5

Safe to merge — the batch path is well-tested with four new unit tests covering all transport variants and failure isolation, and the legacy fallback keeps existing doubles working without changes.

The refactor is behaviour-preserving: mock.update() delegates to the same applyCalls closure the scheduler uses, so per-mock and batched paths are always in sync. The one asymmetry found (missing ?? fallback before parseCallData in the browser-mode loop) is guarded at runtime because parseCallData already accepts null from the existing per-mock path.

packages/electron-service/src/service.ts — specifically the batchUpdateBrowserModeMocks post-execute loop, which lacks the explicit null-fallback that its native counterpart uses.

Important Files Changed

Filename Overview
packages/electron-service/src/service.ts Core scheduler rewrite: splits mocks into native/browser/legacy buckets and batches each in a single round-trip. Minor asymmetry — batchUpdateBrowserModeMocks lacks the ?? fallback that batchUpdateNativeMocks uses before calling parseCallData.
packages/electron-service/src/mock.ts Extracts applyCalls closure shared by scheduler batch path and mock.update(); adds __accessor/__applyCalls to both the inner mock and the wrapper mock. Behaviour-preserving refactor.
packages/electron-service/src/classMock.ts Extracts applyConstructorCalls/applyCalls closures for prototype and constructor mocks; both __accessor and __applyCalls are wired consistently alongside mock.update() delegation.
packages/electron-service/test/service.spec.ts Adds four new scheduler tests covering native-batch, browser-mode batch, mixed-transport split, and per-reader failure isolation; updates the existing element-override test to use the new accessor pattern.
packages/native-types/src/electron.ts Adds ElectronMockReadAccessor union type and optional __accessor/__applyCalls fields to ElectronMockInstance; clearly documented as service-internal.
packages/electron-service/src/mockFactory.ts Exports MockApplyData interface and re-exports ElectronMockReadAccessor as MockReadAccessor alias for use across service modules. Pure type additions.
packages/native-types/src/index.ts Adds ElectronMockReadAccessor to the public re-export list. Trivial barrel update.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Element command triggered\nclick / setValue / etc.] --> B[MockUpdateScheduler#runOnce]
    B --> C{Classify each mock\nby __accessor.kind}
    C -->|kind = api/prototype/constructor| D[native bucket]
    C -->|kind = browser| E[browserMode bucket]
    C -->|no __accessor| F[legacy bucket]

    D --> G[batchUpdateNativeMocks\nbrowser.electron.execute — 1 CDP call]
    E --> H[batchUpdateBrowserModeMocks\nbrowser.execute — 1 renderer call]
    F --> I[per-mock m.update\none call per legacy mock]

    G --> J[Inner Electron script\ndiscriminates api/prototype/constructor\nreturns Record<mockId, slice>]
    J --> K[Outer loop: __applyCalls per mock\nno further I/O]

    H --> L[Inner browser script\nnew Function per channel read\nreturns Record<mockId, raw>]
    L --> M[parseCallData per slice\n__applyCalls per mock]

    G & H & I --> N[Promise.all resolves\nAll mocks synced]
Loading

Fix All in Claude Code Fix All in Cursor

Reviews (3): Last reviewed commit: "refactor(electron-service): isolate per-..." | Re-trigger Greptile

Comment thread packages/electron-service/src/service.ts Outdated
Comment thread packages/electron-service/src/service.ts Outdated
Comment thread packages/electron-service/src/service.ts Outdated
goosewobbler and others added 2 commits May 22, 2026 21:15
- Browser-mode batch (P2 #1 + #2): switch batchUpdateBrowserModeMocks from a
  string-interpolated wrapper script to browser.execute(fn, ids, scripts).
  Null-byte mockStore keys now ride WebDriver's serialisation boundary, and
  the per-channel read scripts are sourced from
  browserInterceptor.buildCallDataReadScript() — same contract per-mock
  update() uses, so Error serialisation can't drift between the two paths.

- Native batch (P2 #3): inner script's per-mock try/catch now emits an
  __error marker on the slice. The outer code log.warns it before forwarding
  to __applyCalls so the scheduler doesn't stall, but vacuous test passes
  from silently-zeroed call data are surfaced.

- New unit test asserts the __error marker propagates from the native batch
  inner script through to __applyCalls.

Refs #268.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…de batch

The browser-mode batch wrapper's `new Function(...)()` invocation was missing
the per-reader try/catch that batchUpdateNativeMocks already has — a single
malformed channel script or runtime throw would tank the whole batch instead
of being scoped to one mock.

Each reader is now wrapped in its own try/catch that emits the same
{ calls: [], results: [], invocationCallOrder: [], __error } shape used by the
native batch. The outer loop log.warns __error before forwarding the slice to
__applyCalls, preserving observability without stalling the scheduler.

Added a test asserting one bad reader doesn't prevent healthy mocks from
receiving their slices.

Refs #268.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

perf(electron-service): batch updateAllMocks into a single CDP round-trip

1 participant