Skip to content

[Bug]: Preview tabs from other threads stay alive as hidden, unthrottled webviews until restart (13 tabs, ~2.7 GB) #15579

Description

@davidvornholt

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/desktop

Steps to reproduce

  1. Over a working day, let agents in several threads call preview_open (13 tabs across a few threads here).
  2. Never close those previews by hand. The agents never call preview_close either.
  3. Switch to another thread with the preview panel closed and leave the app running.

Expected behavior

Preview tabs that the user can't see, in threads they're not looking at, should be throttled, frozen or discarded. They should also be closed eventually, for example when the thread is idle, archived or deleted. They shouldn't keep a full renderer running for the rest of the session.

Actual behavior

Nothing ever closes them. The desktop trace log shows 13 createTab and 0 closeTab since start. ElectronBrowserHost mounts a HostedBrowserWebview for every session in every thread. Hidden ones are only moved offscreen with CSS, and there is no backgroundThrottling, freeze or discard. The pages keep running scripts, including the ad and tracker iframes of the visited sites. PreviewManager holds the sessions in memory until an explicit close(), and ResourceCleanupService doesn't close previews.

Measured 2026-10, with the T3 Code window visible on another thread and no preview panel open:

  • 18 renderer processes, about 2.7 GB PSS in total. Most of that is the preview tabs; the main window is one renderer.
  • The main process "Gamepad polling" thread wakes about 245 times a second; the same thread in another Chromium browser here is idle. T3 Code itself doesn't use the Gamepad API, so this is presumably a script on one of the preview pages calling navigator.getGamepads() (fingerprinting scripts do this).

None of this is visible in the UI, so the user can't tell that the previews are still open.

Related: #14966 (sessions outlive a deleted thread), #14730 (inactive macOS previews keep rendering) and #13610 (orphaned hidden tab leaks GPU memory). This issue is the Linux, cross-thread version: threads that are neither deleted nor orphaned.

Impact

Major degradation or frequent failure

Version or commit

v0.0.45 (desktop)

Environment

NixOS, Linux 7.2.8, GNOME (Wayland), Intel Lunar Lake laptop on battery

Workaround

Restart T3 Code (preview sessions aren't persisted), or open each thread and close its previews.

Activity

  1. juliusmarminge commented on Oct 4, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Triage

    Thanks so much for the careful measurements, @davidvornholt, they made this easy to trace. I checked it on current main (eac52f0087) and on v0.0.45 (6c8fed35dd), and the relevant code is the same on both: preview tabs you aren't looking at stay mounted as live Chromium guests for the rest of the session.

    What I found

    • ElectronBrowserHost sits outside the router and mounts a HostedBrowserWebview for every preview session in every thread, not just the thread on screen (apps/web/src/browser/ElectronBrowserHost.tsx). A thread stays in that index as long as it has sessions (syncActivePreviewThread in apps/web/src/previewStateStore.ts).
    • closeTab runs only when the React guest unmounts (apps/web/src/browser/desktopTabLifetime.ts), which happens only after an explicit preview.close from the UI or from preview_close / t3_preview_close. Closing the panel or switching threads doesn't close tabs.
    • On Linux, an inactive guest is parked at (-100000, -100000) with visibility: hidden (apps/web/src/browser/hostedBrowserWebviewStyle.ts). data-preview-rendering="suspended" is only a marker for automation. Nothing calls Chromium freeze or discard, and nothing destroys the guest.
    • The webview preferences are only contextIsolation=false,sandbox=true,nodeIntegration=false (apps/desktop/src/preview/WebviewPreferences.ts). Electron's default background throttling only applies once Chromium considers the page backgrounded, and the guest is temporarily set to setBackgroundThrottling(false) while recording or in picture-in-picture.
    • The repo never calls navigator.getGamepads(), so the Gamepad polling wakeups very likely come from a script inside one of those guests, which fits your theory.
    • ResourceCleanupService only cleans up terminals and attachments, and thread deletion, archive, and settlement don't touch previews (apps/server/src/orchestration-v2/ThreadDeletion.ts).

    How this differs from the related issues you linked:

    Likely fix area

    A few options a maintainer could weigh:

    • Really suspending inactive guests (panel closed, other thread, and not recording, in picture-in-picture, or held by automation). That could mean freezing them, which keeps page state, or discarding the webview and recreating it from the session URL when the thread is shown again, which frees the memory but reloads the page.
    • Keeping recording, picture-in-picture, and automation on the existing renderingActive path.
    • Separately, closing previews when a thread is idle or settled, the way idle terminals already close. That would change current behavior, since tabs are kept today so they're still there when you come back.

    Restarting the app or closing each thread's previews remains the workaround for now.

    A maintainer will decide on the fix direction.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 4, 2026
  3. juliusmarminge commented on Oct 8, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Reopening: #16961 (merge d9f7c0c) still listed Fixes for this issue, but the hidden-tabs commit was reverted before merge (f3bbe7f — Revert "fix(web): hidden browser tabs on macOS stop painting"). main still keeps inactive macOS guests paintable via keepPaintableWhenInactive: isMacPlatform(...), so this is not fixed yet.

  4. TheCommandCat commented on Oct 9, 2026

    @TheCommandCat

    Additional measured incident on macOS with the newer server-hosted browser path. Posting here to avoid duplicating the broader retained-preview report; the headless rendering path may warrant separate tracking. Related: #17090.

    Environment:

    • T3 Code Nightly 0.0.46-nightly.20261009.2861, installed commit 3b6af0bd1600.
    • macOS 26.6, build 25G72, Apple Silicon (Mac17,9), 15 CPU cores, 48 GiB RAM.
    • Managed chrome-headless-shell 154.0.8037.92.

    The user reported mild lag across the system. During read-only inspection:

    • A T3-owned headless browser's graphics process consumed 140.9% CPU in a timed top sample and 153.7% in a separate rolling ps reading (100% = one core).
    • Browser launch flags included --disable-gpu --enable-unsafe-swiftshader --force-device-scale-factor=2 --disable-background-timer-throttling --disable-renderer-backgrounding.
    • Its graphics process used --use-angle=swiftshader-webgl.
    • The browser had been running for approximately 4 hours 42 minutes and was parented by the T3 backend.
    • T3's Electron GPU/interface processes and WindowServer were also busy; total system CPU still had about 60% idle capacity.
    • Memory pressure was normal, swap was zero, data-volume free space was about 846 GiB, and pmset -g therm reported no thermal/performance warning.
    • preview_status and t3_preview_list for the diagnostic thread returned no tabs. These tools are thread-scoped, so this does not establish that tabs in other threads were inactive or had no automation consumers. Their individual visibility was not inspected.

    At the user's request to close previews, SIGTERM was sent only to the identity-verified shared headless browser root process. Its graphics process and all managed headless-browser processes then disappeared; the T3 desktop and backend remained running. A subsequent timed system sample had 72.4% CPU idle (the preceding sample had 74.9%). This was a short live observation, not a controlled benchmark, and no claim is made that all perceived lag was resolved. The browser was stopped at process level because the available normal tab-close tool only covered the diagnostic thread; preview-session metadata cleanup was not verified.

    This supports investigating the CPU cost and lifecycle of retained server-hosted browser tabs on local macOS installations, in addition to the earlier native webview reports. It does not establish a memory leak, an exact reproduction sequence, or that SwiftShader alone caused the system-wide lag. Inactive tabs without automation, recording, or viewing consumers should suspend rendering, and users should be able to discover and close retained tabs across threads.

    Measurements and this comment prepared with Codex on the user's behalf.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions