Skip to content

fix(tauri): pump the macOS main run loop so WebKit schedules work on a headless runner (#540) - #553

Open
goosewobbler wants to merge 4 commits into
mainfrom
fix/tauri-macos26-runloop-pump
Open

fix(tauri): pump the macOS main run loop so WebKit schedules work on a headless runner (#540)#553
goosewobbler wants to merge 4 commits into
mainfrom
fix/tauri-macos26-runloop-pump

Conversation

@goosewobbler

Copy link
Copy Markdown
Contributor

Follow-up to #552, targeting the residual macOS-26.4 DirectEval flake.

Problem

After #552 fixed the root cause (malformed fixture CSP → postMessage IPC → pump stall) and hardened against WebKit code-4, a residual intermittent stall remained on the off-screen headless CI runner. The WebKit unified log on a failing run shows the app's main run loop parked in -[NSApplication run] → _DPSNextEvent → mach_msg, while WebContent's URL-scheme resource requests (page-load subresources) sit unserviced for ~27s — nothing drives the loop when there's no display, so WebKit doesn't promptly schedule its work and the first execute times out.

Fix

Schedule a repeating no-op NSTimer in the main run loop's common modes (macOS-only, once). Each tick wakes the loop, which then drains the pending WebContent scheme-handler requests / eval dispatches — turning a multi-second stall into ≤ one tick. Harmless with a real display (the loop already pumps continuously), which is why this isn't reproducible locally — hence CI is the adjudicator.

Validation

cargo check + local standalone smoke green (no regression). Diagnostics (WebKit-log capture, inherited from #552) are retained so a failure is classifiable. Validating ungated on CI over several runs; will strip diagnostics once the DirectEval macOS legs are consistently green.

Refs #540

🤖 Generated with Claude Code

…a headless runner

Residual #540: even with the CSP fix (custom-protocol IPC working), the
DirectEval legs still occasionally time out on the off-screen CI runner. The
WebKit unified log shows the app's main run loop parked in -[NSApplication run]
while WebContent's URL-scheme resource requests (page-load subresources) sit
unserviced for ~27s — nothing drives the loop when there's no display.

Schedule a repeating no-op NSTimer in the main run loop's common modes so it
keeps waking and drains that pending work each tick. macOS-only, once, harmless
with a real display (the loop already pumps continuously).

Diagnostics kept in to validate; can't reproduce locally (a real display
already pumps the loop), so CI is the adjudicator.

Refs #540

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKc4JHZKXfyqCiiU4C2b7V
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Standing release PR: #456 · 15 packages queued · open 35d 6h 9m · ✅ ready to merge

Release Preview — 17 packages

Note: Labels on this PR are advisory in standing-pr mode. Bumps come from conventional commits in the standing PR; override by editing labels on the standing PR itself. Add release:immediate to bypass the standing PR and release this PR directly.

These changes will be added to the release PR (#456) when merged:

Changelog

@wdio/dioxus-bridge 1.0.0-next.3 → 1.0.0-next.4

Changed

  • Update version to 1.0.0-next.4
@wdio/dioxus-service 1.0.0-next.3 → 1.0.0-next.4

Changed

  • Update version to 1.0.0-next.4
wdio-dioxus-driver 1.0.0-next.3 → 1.0.0-next.4

Changed

  • Update version to 1.0.0-next.4
wdio-dioxus-embedded-driver 1.0.0-next.3 → 1.0.0-next.4

Changed

  • Update version to 1.0.0-next.4
@wdio/tauri-plugin 1.2.0 → 1.2.1

Changed

  • Update version to 1.2.1
@wdio/tauri-service 1.2.0 → 1.2.1

Changed

  • Update version to 1.2.1
tauri-plugin-wdio-webdriver 1.2.0 → 1.2.1

Changed

Fixed

  • reduce the run-loop pump frequency to minimise idle wakeups (tauri)
  • pump the macOS main run loop so WebKit schedules work on a headless runner (tauri)
@wdio/flutter-service 1.0.0-next.1 → 1.0.0-next.2

Changed

  • Update version to 1.0.0-next.2
wdio_flutter N/A → 0.1.1

Changed

  • Update version to 0.1.1
@wdio/electrobun-service 0.1.0 → 0.1.1

Changed

  • Update version to 0.1.1
@wdio/electron-service 10.1.0 → 10.1.1

Changed

  • Update version to 10.1.1
@wdio/native-cdp-bridge 1.0.0 → 1.0.1

Changed

  • Update version to 1.0.1
@wdio/native-core 1.0.0 → 1.0.1

Changed

  • Update version to 1.0.1
@wdio/native-mobile-core 1.0.0 → 1.0.1

Changed

  • Update version to 1.0.1
@wdio/native-spy 1.1.0 → 1.1.1

Changed

  • Update version to 1.1.1
@wdio/native-types 2.4.0 → 2.4.1

Changed

  • Update version to 2.4.1
@wdio/native-utils 2.5.0 → 2.5.1

Changed

  • Update version to 2.5.1
@wdio/react-native-service 1.0.0-next.0 → 1.0.0-next.1

Changed

  • Update version to 1.0.0-next.1

After merge — predicted release

No version escalation — this PR's changes will be included in the queued release without affecting the projected versions.

Package Standing PR This PR After merge
@wdio/dioxus-bridge 1.0.0-next.4 1.0.0-next.4 1.0.0-next.4
@wdio/dioxus-service 1.0.0-next.4 1.0.0-next.4 1.0.0-next.4
@wdio/electrobun-service 0.2.0 0.1.1 0.2.0
@wdio/electron-service 10.2.0 10.1.1 10.2.0
@wdio/native-cdp-bridge 1.1.0 1.0.1 1.1.0
@wdio/native-core 1.1.0 1.0.1 1.1.0
@wdio/native-spy 1.2.0 1.1.1 1.2.0
@wdio/native-types 2.5.0 2.4.1 2.5.0
@wdio/native-utils 2.6.0 2.5.1 2.6.0
@wdio/tauri-plugin 1.3.0 1.2.1 1.3.0
@wdio/tauri-service 1.3.0 1.2.1 1.3.0
tauri-plugin-wdio-webdriver 1.3.0 1.2.1 1.3.0
wdio_flutter 0.2.0 0.1.1 0.2.0
wdio-dioxus-driver 1.0.0-next.4 1.0.0-next.4 1.0.0-next.4
wdio-dioxus-embedded-driver 1.0.0-next.4 1.0.0-next.4 1.0.0-next.4

Updated automatically by ReleaseKit

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR improves macOS DirectEval reliability on off-screen CI runners.

  • Adds a one-time 50 ms timer to the main run loop’s common modes so pending WebKit work is serviced promptly.
  • Records temporary display, activation, and occlusion diagnostics for evaluating a future reliable gate.
  • Enables the required macOS Objective-C framework features.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/tauri-plugin-webdriver/src/platform/macos.rs Adds the correctly retained, main-thread-scheduled run-loop pump and temporary runner-state diagnostics; no new eligible failure remains.
packages/tauri-plugin-webdriver/Cargo.toml Enables the objc2 Foundation and AppKit features required by the new macOS timer and diagnostics.

Reviews (3): Last reviewed commit: "chore(tauri): TEMP diagnostic — log disp..." | Re-trigger Greptile

Comment on lines +118 to +119
let timer = NSTimer::timerWithTimeInterval_repeats_block(0.016, true, &block);
NSRunLoop::mainRunLoop().addTimer_forMode(&timer, NSRunLoopCommonModes);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unconditional lifetime run-loop timer

Every macOS app using the plugin receives a repeating 16 ms common-mode timer with no invalidation or headless-state gate, producing approximately 62.5 avoidable main-thread wakeups per second while the app is idle, minimized, or backgrounded.

Fix in Claude Code Fix in Cursor

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coarsened the interval to 50ms (~20 wakeups/sec, a 3× cut) — the interval only bounds worst-case servicing latency, which just needs to stay well under the command timeout, so it doesn't need to be tight. Kept it unconditional rather than gated: this plugin ships only in WebDriver-automation builds (not production apps), and a reliable headless gate isn't available — the stall is the app being off-screen/inactive on the CI runner rather than "no display attached", so an NSScreen-empty check would misfire.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair point — the WebDriver-only deployment context changes the calculus significantly. In a test harness that's already orchestrating a browser, 20 wakeups/sec is noise, and the risk of a gating heuristic (like NSScreen checks) introducing its own class of flakes outweighs the overhead concern.

50ms is a reasonable interval given the constraint you described — well under typical command timeouts while being 3× cheaper than the original.

The original concern stands for a general-purpose plugin, but the scoping argument holds here.

Shall I remember this as a rule: "this plugin is WebDriver-automation-only, so CI-reliability concerns take precedence over per-process overhead concerns when evaluating tradeoffs"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — that's a good rule to remember: this plugin (@wdio/tauri-plugin-webdriver) ships only in WebDriver-automation builds, so CI/automation reliability takes precedence over per-process runtime overhead when weighing tradeoffs. Noting for future: a display/headless gate is still a desirable refinement here, just deferred until the pump's efficacy is confirmed over multiple runs and a reliable gate signal is found (NSScreen-empty is unreliable because the CI stall is app-inactive/off-screen, not no-display-attached).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule saved. View rule

The nuance about NSScreen-empty being an unreliable gate (app-inactive vs. no-display-attached being distinct conditions) is captured — that distinction is exactly the kind of thing worth preserving so future suggestions don't chase the wrong signal.

goosewobbler and others added 3 commits July 28, 2026 02:32
Greptile P2: the pump timer ran unconditionally at 16ms (~62.5 wakeups/sec).
The interval only bounds worst-case servicing latency (which just needs to be
well under the command timeout), so coarsen it to 50ms — ~20/sec, a 3x cut —
while keeping page-load/eval servicing responsive. The plugin ships only in
WebDriver-automation builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKc4JHZKXfyqCiiU4C2b7V
…540 gate investigation

Logs NSScreen.screens count, NSApplication.isActive, and occlusionState at
pump-schedule time to determine whether a 'no display' gate is viable on the CI
runner (local 26.5.1 reports screens=1 isActive=false, so a screens==0 gate is
likely dead). Behaviour unchanged; to be reverted after the investigation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKc4JHZKXfyqCiiU4C2b7V
… display diag

The display/activation diagnostic answered its question: the CI runner reports
screens=1 isActive=false occlusion=8192 — identical to a local machine with a
display — so a 'no display' (or isActive/occlusion) gate can't distinguish the
stalling-CI case from the working-local one. A display-based gate isn't viable;
reverting that diagnostic (plugin back to the plain 50ms pump).

To characterise the remaining intermittent macOS Tauri *package* failure (not
IPC, not code-4), add the same WebKit-unified-log capture the e2e workflow has,
scoped to macOS + tauri, so the next package failure is classifiable.

Refs #540

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKc4JHZKXfyqCiiU4C2b7V
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.

1 participant