Skip to content

test(browser): wait for the popup fixture's first paint during startup, not in the first test - #30854

Merged
Jinwoo-H merged 6 commits into
mainfrom
fix/popup-compat-frame-capture-flake
Oct 20, 2026
Merged

Jinwoo-H merged 6 commits into
mainfrom
fix/popup-compat-frame-capture-flake

Conversation

@Jinwoo-H

@Jinwoo-H Jinwoo-H commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 0 0 0 0
Prod 1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​13 $\color{#cf222e}{\Huge{\mathbf{−}}}$​14 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1

ELI5

A required Linux check that tests browser popups has been failing at random on unrelated PRs since about 12:30 ET on 2026-10-19. Nothing is wrong with popups. The test starts a hidden copy of the app, and on a brand-new CI machine the very first launch can take several seconds before it can draw anything, and the failing runs suggest it sometimes takes more than 30. The test only allowed 10 seconds for that, and it counted that time against the first test. This PR moves the wait into the setup step, so a slow first launch is waited out once at startup and no test fails for it.

What Changed

src/main/browser/browser-popup-compatibility-cdp-fixture.ts:

  • Before: the fixture declared itself ready once its hidden pages had loaded. The first test then started capturing each page's picture and waited up to 10 seconds (the general poll limit) for the first frame.
  • After: setup starts the picture capture right after it finds each page and waits for the first frame before any test runs. The wait goes through the fixture's existing startup wait loop with a 45-second limit. That is about 1.5 times the worst case seen (about 31 s); it is a safety margin, not a measured ceiling. The setup step's own limit goes from 60 to 90 seconds to fit it. If the limit is hit, the error names the page and carries the app's stderr, like the other startup waits. Each page is still captured once and stopped in teardown, as before.

No product code changes.

Why

Root cause. Temporary timing probes on this PR, run in the real Linux package job and since removed, showed this:

  • Only the first Electron launch on a fresh runner is slow. In that launch, Chromium's GPU process (which does all compositing, even in software mode) did not exist 3.4 s after app-ready and only reported in at about 6.0 s. Every hidden page then drew its first animation frame at the same moment, 6.015 s. Before that, the page ran 0 animation frames in a 500 ms sample. In the seven warm launches on the same machine, the GPU process came up in about 0.15 s and pages ran at 60 fps before capture started.
  • Clearing the Mesa shader cache or the fontconfig cache between launches did not make a launch slow, so the cost is not either of those caches. It is a one-time cost on a machine's first launch.
  • The native compatibility file always runs first in that step, so its first test took that cost. In passing runs that first test takes 3 to 8.6 s, compared with about 2.5 s warm. In every failure the first test fails at exactly 10.0 s and the next test takes another 1.5 to 10 s. So the first frame arrived roughly 12 to 20 s after the first capture started, and in one failing run more than 30 s (three tests timed out in a row). This fits the cold start above, but the probes measured one cold launch at about 6 s. They didn't catch one of the long failures directly. The slow tail existed before this week, and it started going past 10 s often on 2026-10-19 without any related code change.

No commit caused this. The suspected commits (#30537, #30611, #30452, #30652, #30490) don't touch the frame capture, and #30490 landed after the first failure. The frame-capture wait was added in #28879 (2026-10-14).

Alternatives considered:

  • Raise the 10-second poll for every test: this would hide real slowness in every assertion. The cost is a one-time startup cost, so it belongs in setup.
  • Drop the screencast: before [CI] Reduce import, frame-capture, teardown, and shard costs #28879, without it, these files took 86 s and 282 s on CI instead of about 12 s and 25 s.
  • Launch with --disable-gpu or pre-warm the runner in the workflow: these change the probe's environment or add CI steps, and nothing showed they would remove the cold start.

Linked Issue

N/A (maintainer CI fix; no issue)

Visual Proof

N/A: test-only change, nothing users see.

Testing

  • macOS: the native, plain, background and adapter popup Electron files pass locally with ORCA_BACKGROUND_LAUNCH=1 (hidden windows).

  • Linux: the probe commits on this PR measured the cold and warm launches described above. At head 722f2ce, the package job (run 39322204950) passed 4 of 4 attempts: jobs 118302356105, 118316780416, 118325853714 and 118344791129. The first test took 0.44 to 0.5 s instead of 2.5 to 10 s, and the native file's total was 9.9 to 17.5 s because setup now absorbs the cold start.

  • Separate, already present on main and not touched here: browser-popup-adapter-compatibility.electron.test.ts sometimes fails inside a test body (childAt poll). It reproduced 1 in 5 times on unmodified main locally and once on CI (2026-10-19 09:30Z).

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance

Test fixture only. No effect on SSH, mobile or Windows behaviour.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0bb79683-8c18-4644-9e1a-d2fad2370ba9
📥 Commits

Reviewing files that changed from the base of the PR and between 4d1308b and c155064.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

The CDP fixture starts frame capture for each guest page during beforeAll and waits up to 45 seconds for the first frame. It reports a guest-specific error if no frame arrives. The setup timeout increases from 60 to 90 seconds. waitForFixture now accepts a configurable timeout, with a default of 20 seconds.

Priority: ⬇️ Low

Merge Risk: 🟡 Moderate · up to 4d130

Some popup compatibility suites can still time out without identifying the failing page, so the fixture’s shared wait budget should be corrected before merge.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title is concise, specific, and accurately describes the main change: moving the popup fixture's first-paint wait into startup.
Description check Passed The description is complete and relevant. It explains the problem, change, rationale, alternatives, testing, scope, and checklist items. The maintainer exception reasonably supports marking the linked…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description is complete and relevant. It explains the problem, change, rationale, alternatives, testing, scope, and checklist items. The maintainer exception reasonably supports marking the linked issue as N/A.

✨ Finishing Touches: 💡 2
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2e35d28f-da0d-497e-8db2-3104bdf2d850
📥 Commits

Reviewing files that changed from the base of the PR and between bd7633f and b053113.

📒 Files selected for processing: 3
  • .github/workflows/pr.yml
  • src/main/browser/browser-popup-compatibility-cdp-fixture.ts
  • src/main/browser/popup-flake-diagnostic-fixture.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Comment thread .github/workflows/pr.yml Resolved

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1e2201bb-9167-4619-83eb-f6aa7b09dde9
📥 Commits

Reviewing files that changed from the base of the PR and between b053113 and b22804d.

📒 Files selected for processing: 4
  • .github/workflows/pr.yml
  • src/main/browser/browser-popup-compatibility-cdp-fixture.ts
  • src/main/browser/browser-popup-compatibility-electron-fixture.ts
  • src/main/browser/popup-flake-main-diagnostic.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

@Jinwoo-H Jinwoo-H changed the title test(browser): stop the popup compatibility check failing at random on Linux test(browser): wait for the popup fixture's first paint during startup, not in the first test Oct 19, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4504cf7c-8d13-4c20-a3f7-93dd4004bb6c
📥 Commits

Reviewing files that changed from the base of the PR and between b22804d and 4d1308b.

📒 Files selected for processing: 1
  • src/main/browser/browser-popup-compatibility-cdp-fixture.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Comment on lines +212 to +223
// A fresh runner's first launch can take tens of seconds to start Chromium's GPU process, and
// no hidden page paints until it does; charge that to startup, not the first test's poll.
stopFrameCaptures.set(
page,
await startHiddenRendererFrameCapture(page, (hasFrame) =>
waitForFixture(hasFrame, 45_000).catch((error: unknown) => {
throw new Error(`No first frame for ${name}`, { cause: error })
})
)
}
)
}
})
}, 90_000)

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

set -eu
printf '%s\n' '--- fixture setup and helper references ---'
nl -ba src/main/browser/browser-popup-compatibility-cdp-fixture.ts | sed -n '1,260p'
printf '%s\n' '--- four-page callers ---'
nl -ba src/main/browser/browser-popup-background-compatibility.electron.test.ts | sed -n '1,45p'
nl -ba src/main/browser/browser-popup-adapter-compatibility.electron.test.ts | sed -n '1,40p'
printf '%s\n' '--- changed hunk against merge base ---'
git diff --unified=25 c5d03fc475c01fa342d527715a8aa519114b4177 4d1308b1060c1182432daaa180eaf9e4a8978c5f -- src/main/browser/browser-popup-compatibility-cdp-fixture.ts

Repository: stablyai/orca

Length of output: 20815


🏁 Script executed:

set -eu
printf '%s\n' '--- capture helper ---'
nl -ba src/main/browser/hidden-renderer-frame-capture-fixture.ts | sed -n '1,180p'
printf '%s\n' '--- comparable capture usages ---'
rg -n -F --glob '*.ts' -e 'startHiddenRendererFrameCapture' -e 'stopFrameCaptures' src/main/browser || test "$?" -eq 1

Repository: stablyai/orca

Length of output: 2647


Share the setup budget across page captures.

The four-page configurations start captures sequentially. Each capture can wait 45 seconds, while beforeAll has one 90-second timeout. Two slow pages can consume the full setup budget before a later page reaches its own timeout. CI then reports the setup timeout instead of the page-specific No first frame for ${name} error and its stderr cause.

Use concurrent captures and wait for all capture attempts to settle before propagating a failure.

Suggested fix
--- "a/src/main/browser/browser-popup-compatibility-cdp-fixture.ts"
+++ "b/src/main/browser/browser-popup-compatibility-cdp-fixture.ts"
@@ -192,34 +192,43 @@
     ...(nativeControls ? ['local-control', 'route-control'] : []),
     ...(adapterProbe ? ['probe-one', 'probe-two'] : [])
   ]
-  for (const name of names) {
-    await waitForFixture(() =>
-      Boolean(
-        browser
-          ?.contexts()
-          .flatMap((context) => context.pages())
-          .some((page) => page.url() === `${origin}/${name}`)
-      )
-    )
-    const page = browser
-      .contexts()
-      .flatMap((context) => context.pages())
-      .find((candidate) => candidate.url() === `${origin}/${name}`)
-    if (!page) {
-      throw new Error('Missing guest page')
-    }
-    pages.set(name, page)
-    // A fresh runner's first launch can take tens of seconds to start Chromium's GPU process, and
-    // no hidden page paints until it does; charge that to startup, not the first test's poll.
-    stopFrameCaptures.set(
-      page,
-      await startHiddenRendererFrameCapture(page, (hasFrame) =>
-        waitForFixture(hasFrame, 45_000).catch((error: unknown) => {
-          throw new Error(`No first frame for ${name}`, { cause: error })
-        })
-      )
-    )
+  const captureResults = await Promise.allSettled(
+    names.map(async (name) => {
+      await waitForFixture(() =>
+        Boolean(
+          browser
+            ?.contexts()
+            .flatMap((context) => context.pages())
+            .some((page) => page.url() === `${origin}/${name}`)
+        )
+      )
+      const page = browser
+        .contexts()
+        .flatMap((context) => context.pages())
+        .find((candidate) => candidate.url() === `${origin}/${name}`)
+      if (!page) {
+        throw new Error('Missing guest page')
+      }
+      pages.set(name, page)
+      // A fresh runner's first launch can take tens of seconds to start Chromium's GPU process, and
+      // no hidden page paints until it does; charge that to startup, not the first test's poll.
+      const stop = await startHiddenRendererFrameCapture(page, (hasFrame) =>
+        waitForFixture(hasFrame, 45_000).catch((error: unknown) => {
+          throw new Error(`No first frame for ${name}`, { cause: error })
+        })
+      )
+      return [page, stop] as const
+    })
+  )
+  for (const result of captureResults) {
+    if (result.status === 'fulfilled') {
+      stopFrameCaptures.set(...result.value)
+    }
+  }
+  const failedCapture = captureResults.find((result) => result.status === 'rejected')
+  if (failedCapture?.status === 'rejected') {
+    throw failedCapture.reason
   }
 }, 90_000)
 
 afterEach(async () => {

@Jinwoo-H
Jinwoo-H merged commit 449cbb0 into main Oct 20, 2026
33 checks passed
nwparker added a commit that referenced this pull request Oct 20, 2026
…clicking them

Fresh sandboxed iframes run in their own renderer and can finish loading
before the browser routes pointer input to them, so Playwright's click
landed on the parent and no popup opened. Hover-poll until the new frame
sees trusted input before clicking, in one helper shared by the adapter,
native and background popup probes.

Share the cold first-frame budget from #30854 with popup-origin-chrome,
which still waited only 10s for the same one-time GPU startup.
nwparker added a commit that referenced this pull request Oct 20, 2026
…clicking them (#30966)

Fresh sandboxed iframes run in their own renderer and can finish loading
before the browser routes pointer input to them, so Playwright's click
landed on the parent and no popup opened. Hover-poll until the new frame
sees trusted input before clicking, in one helper shared by the adapter,
native and background popup probes.

Share the cold first-frame budget from #30854 with popup-origin-chrome,
which still waited only 10s for the same one-time GPU startup.
34 pushed a commit to 34/orca that referenced this pull request Oct 20, 2026
…p, not in the first test (stablyai#30854)

* test(browser): temporary timing probe for the popup first-frame flake

* test(browser): extend the temporary popup flake probe to cold caches and the main process

* test(browser): wait for the popup fixture's first paint during startup, not in the first test

* test(browser): name the page whose first frame never arrived

---------

Co-authored-by: E2E Test <e2e@test.local>
(cherry picked from commit 449cbb0)
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