Skip to content

desktop: the window is shown before the main bundle and layers finish on the same thread #54296

Open

Description

@Te-River

Description

The desktop window is shown before the main process is ready, and the work that follows runs on the same thread, so the window is visible but unresponsive for a moment after it appears.

packages/desktop/src/main/windows/early.ts creates and shows the first window with show: true as soon as Electron is ready, before the rest of the main process has loaded. The main bundle and the Effect layers are then evaluated on the main thread (packages/desktop/src/main/index.ts → desktop.ts), and the renderer's assets are served by a protocol.handle handler that also runs on the main thread (packages/desktop/src/main/windows/protocol.ts).

The layer build does synchronous work on that thread: new DatabaseSync plus migrations (packages/desktop/src/main/storage/database.ts, migration.ts), importLegacyStores reading the whole userData directory (storage/legacy.ts), and an onboarding readDirectory with unbounded concurrency (lifecycle/onboarding.ts). storedBackgroundColor() also resolves a theme palette synchronously inside the BrowserWindow options when nothing is stored (windows/defaults.ts).

Plugins

none

OpenCode version

2.0.26

Steps to reproduce

  1. Start the desktop app.
  2. The window appears, then the UI is briefly unresponsive (spinner on hover).
  3. Profile the main process from the first statement: the interval between the window being shown and the layers being ready is one block of synchronous work.

Screenshot and/or share link

n/a

Operating System

Windows 11

Terminal

n/a (desktop app)

Activity

  1. opencode-agent commented on Oct 10, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed write-up and the pointers into the code. I checked the paths you mention. windows/early.ts shows the first window (show: true) before the rest of the main process has loaded, and the storage, legacy-import and onboarding work then runs synchronously on the main thread while the layers are built. So the order of events you describe matches the code.

    This sandbox can't run the desktop app on a real display (let alone on Windows) or profile its main process, so a maintainer with the desktop environment will need to measure how long the unresponsive period lasts.

    Related issues and PRs, most relevant first:

    If you have a main-process profile or timings (e.g. how many ms between the window appearing and the UI responding), please attach them here. They would help.

  2. Te-River commented on Oct 10, 2026

    @Te-River
    Author

    Here are the timings you asked for, from the repo's own dev startup benchmark (bun run bench:devex), reading the main-process marks (lifecycle/marks.ts).

    Machine (so the numbers are comparable): AMD Ryzen 5 5600U (6C/12T), 13.8 GB RAM, AMD Radeon integrated graphics (driver 30.0.13044.0) plus a virtual display adapter, 1920×1080 @60Hz, Windows 11 Pro for Workstations build 26300, balanced power plan. Electron 44.4.5, HEAD 4b6fea2875.

    Window appears → layers ready: 280ms (median of 4 runs; range 241-285ms). Split:

    phase ms
    main bundle evaluation 152
    storage 72
    logging 34
    onboarding 4
    theme resolution 1.3

    Inside storage: sqlite open + migrate 29ms, legacy store import 21ms.

    Two assumptions from the issue did not survive measurement: theme resolution is 1.3ms (not "tens of milliseconds"), and onboarding is 4ms.

    Window appears → Home interactive is ≈12.1-15.4s in dev, but that is dominated by the dev-mode renderer (9.0-14.1s) and the isolated dev service (5.9-14.1s), not by the main thread. The benchmark's own visibleWindowToHome (11798-15129ms, median 12045) has the same problem: its windowVisible milestone is the adoption log after the layers, so the 280ms block sits outside it.

    #54317 forks the legacy store import off that block: the storage interval goes from 72ms to 42-47ms. The early snapshot already handles the pre-import state (storage/window-snapshot.ts, covered by window-snapshot.test.ts), so the renderer's first read is unaffected.

    The remaining 152ms is the main bundle evaluation, which is the larger half of the block.

  3. richardding commented on Oct 14, 2026

    @richardding

    macOS data point for this issue — renderer stalls on oc:// assets while the main process reports ready

    • Platform: macOS 27 (Darwin 27.0.0), Apple Silicon
    • OpenCode Desktop 2.0.28 (prod channel, ai.opencode.desktop)

    Same signature as the issue: after a clean quit (proc_exit confirmed in the unified log), relaunches from the Dock show the window but it never paints — the Dock stays in the "opening" state indefinitely.

    What the logs show (~/Library/Application Support/ai.opencode.desktop/logs/<ts>/):

    1. Main process is fast and healthy. Lifecycle marks: window → layers ready in ~176 ms (window: 1791964123017, layers: 1791964123193). Background service probe succeeds (probed: true, HTTP 200 in ~10 ms against 127.0.0.1:49374).

    2. The renderer stalls on bundled oc:// assets anyway. From renderer.log of a stalled launch:

      • 15:49:12.437 — Slow network is detected. … Fallback font will be used while loading: oc://renderer/assets/Inter.ttf
      • 15:49:29.352 — same message for JetBrainsMonoNerdFontMono-Regular.woff2 (16.9 s later)
      • then the log ends; the window never rendered.

      So on macOS the oc:// handler starvation lasts far longer than the ~200 ms layers phase — 17 s+ between two local font fetches, well after layers ready was logged.

    3. Every launch (including fully healthy sessions) logs failed to start net log … net::ERR_INSUFFICIENT_RESOURCES in main.log.

    4. The legacy-store import path from refactor(desktop): fork the legacy store import off the first window's path #54317 is active on this machine: imported legacy store files { imported: 18 }.

    5. No Crashpad reports, no .hang/.spin entries in DiagnosticReports for any of these launches.

    One side observation: while the app was stuck in this state, repeated Dock clicks each spawned a new main process (3 instances in 24 minutes, each creating its own log folder) instead of activating/refusing — a single-instance guard might limit the damage.

    Happy to attach the full log folders, or a sample of a stalled renderer/GPU process next time it reproduces, if useful.

  4. Te-River commented on Oct 14, 2026

    @Te-River
    Author

    Thanks — this is a different mechanism from the one the issue describes, and it is worth separating.

    What we measured on Windows is a main-thread block: the window is shown before the main bundle and the layers finish, and that block is ~280ms (bundle 152ms, storage 72ms, logging 34ms). #54317 forks the legacy store import off it, taking storage from 72ms to 42-47ms.

    Your logs show the opposite shape: the main process is fast (window → layers in ~176ms) and the renderer stalls anyway, on oc:// assets — 17s+ between two local font fetches, well after layers ready. That is the oc:// handler starving the renderer, not the main thread. So #54317 will not fix your case; it only shortens a block that is already short on your machine.

    Two things in your report look like their own issues:

    • oc:// asset starvation. A local protocol handler taking 17s between two bundled fonts, with net::ERR_INSUFFICIENT_RESOURCES on every launch, is a renderer-side stall independent of the main process. The net log failure on healthy launches too suggests the handler is resource-starved rather than slow per request.
    • No single-instance guard. Three main processes in 24 minutes from repeated Dock clicks, each with its own log folder, means a stuck launch can multiply. That is a separate, cheap fix.

    If you can attach a sample of a stalled renderer next time it reproduces, that would settle which of the two is the stall. Happy to file both if you would rather not.

  5. richardding commented on Oct 15, 2026

    @richardding

    Follow-up with the requested renderer sample, captured live today on macOS (arm64), OpenCode desktop 2.0.29, single launch, during a reproduced renderer stall.

    Method: sample <renderer-pid> 8 while the stall was active; ps snapshot at the same moment.

    Reproduction is deterministic on our machine, not intermittent (2.0.29): 1) run the desktop app normally, 2) Cmd+Q, 3) the Dock icon remains afterwards (app not fully gone), 4) a single click on the Dock icon → window shows, renderer stalls. Every time, in our experience. The attached sample was captured during one such stall (single Dock click, no re-clicks).

    One more observation that localizes the primary defect to the quit path: after Cmd+Q the app neither terminates nor enters its background-service state (the Dock icon keeps showing it as running, not "run in background"). If we pkill -f OpenCode at that moment, the Dock switches to the normal "run in background" state and the next Dock click launches the GUI cleanly. Our reading: the post-quit half-dead instance is what the next Dock click reconnects to, and that is where the renderer stall starts — the hanging quit is upstream of the stall.

    Factual contents, interpretation left to you:

    • Renderer main thread: 6120/6120 hits (100% of the 8s window) in v8::internal::OptimizingCompileTaskExecutor::WaitUntilCompilationJobsDoneForIsolate → mach_msg2_trap — blocked for the entire window.
    • No V8 compilation thread is present anywhere in the 8s sample: the main thread waits on compilation jobs while no worker thread is observable running them.
    • Other renderer threads (Compositor, Chrome_ChildIOThread, ThreadPool workers) could not be unwound past _pthread_start by sample; raw file attached so you can dig deeper.
    • Full sample + ps snapshot: https://gist.github.com/richardding/94b037afe00441b1dd0972978e75cf7f

    On the two spin-offs: please file both if that's easier on your side — the single-instance guard matches our earlier observation (three main processes from repeated Dock clicks during one stuck launch). For tracking: #54317 is still open as of today, and this stall reproduces on shipped 2.0.29, consistent with your read that it addresses a different mechanism.

    Happy to capture more (net log, longer sample, spindump) on the next reproduction.

  6. Te-River commented on Oct 17, 2026

    @Te-River
    Author

    Thanks — the sample is the useful part, and your quit-path reading holds up.

    What it rules out: in this capture the renderer is not waiting on oc:// at all. 100% of the 8s window is OptimizingCompileTaskExecutor::WaitUntilCompilationJobsDoneForIsolate → mach_msg2_trap, and no V8 compilation thread is present in the sample — the main thread waits on compilation jobs that nothing is running. That is a different stall from the font fetches you saw earlier (those were 17s apart, but they were progressing).

    Filed both spin-offs, as you asked:

    So the three mechanisms now have separate homes: this issue (the main-thread block), #56394 (the quit path and the renderer stall), and the oc:// starvation — still only described here; say the word and it gets its own.

    If you capture a longer sample or a spindump on the next reproduction, #56394 is the place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions