Skip to content

[Bug]: Preview tabs in WSL environments are laggy since #15328 (no native webview, CPU-rendered stream) #17090

Description

@paulcatamio

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. On Windows, run the T3 desktop app with a T3 server running inside WSL2. The desktop app connects to it over 127.0.0.1.
  2. Open a thread in the WSL environment.
  3. Open a preview tab, for example a local dev server, and scroll or interact with it.

Expected behavior

The preview feels like it did before #15328: rendered natively on Windows, with smooth scrolling and immediate input.

Actual behavior

Since #15328 ("run the browser on the environment server"), WSL environment preview tabs are noticeably laggy. Scrolling stutters and input feels delayed.

What I found:

  • The tab runs in chrome-headless-shell inside WSL, under ~/.t3/tools/chrome-headless-shell/linux64/154.0.8037.92/. It reaches the desktop app as streamed JPEG frames instead of a native <webview>.
  • The native <webview> path only applies to the server the desktop launched as its local primary. It depends on the browser bootstrap fds 6 and 7 (desktopBrowserFd / desktopBrowserControlFd in DesktopBackendConfiguration.ts). resolveWslStartConfig doesn't set them, and DesktopBackendManager.ts notes that wsl.exe drops additional fds. So a WSL environment always takes the streaming path. In my setup, the WSL server runs as the t3 service systemd user service rather than being spawned by the desktop, which leads to the same result.
  • Chrome launches with --disable-gpu --enable-unsafe-swiftshader --force-device-scale-factor=2. Rendering happens on the CPU at 2x pixel density, even though WSL2 exposes the GPU through /dev/dxg.
  • feat(preview): run the browser on the environment server #15328 removed T3CODE_SERVER_BROWSER and the desktop automation path, so users can't switch back.

I ruled out host load. With memory freed and the service restarted, t3 serve is under 400 MB, swap is near zero, and there are no event loop stalls. The streaming path is still what WSL tabs use.

Impact

Major degradation or frequent failure

Version or commit

0.0.46-nightly.20261008.2801

Environment

Windows 10.0.26340, WSL2 Ubuntu 24.04.4 (kernel 6.18.33.2-microsoft-standard-WSL2), 16 cores / 16 GB, networkingMode=mirrored. T3 server running in WSL as the t3 service background service. Desktop app on Windows.

Logs or stack traces

# Chrome launch flags for a WSL preview tab (trimmed)
chrome-headless-shell ... --enable-unsafe-swiftshader --headless --disable-gpu --force-device-scale-factor=2 \
  --user-data-dir=/tmp/playwright_chromiumdev_profile-XXXX --remote-debugging-pipe --no-startup-window
chrome-headless-shell --type=gpu-process ... --ozone-platform=headless --use-angle=swiftshader-webgl

Before the restart, the server log also showed repeated event loop stalled for 2500-7000 ms warnings while t3 serve was at about 6.7 GB, including about 2.4 GB in swap, after about 40 minutes of uptime. Other workloads were also using memory on that host, so I'm noting this as a possible separate issue, not the cause.

Workaround

Open the dev server URL in a normal Windows browser, since WSL forwards localhost. That loses agent visibility of the tab. Preview tabs in a Windows-native environment are still rendered natively.

Possible fixes

  • Bridge the desktop browser channel to WSL backends over loopback or a socket instead of fds 6 and 7, so the desktop can render WSL environment tabs natively again.
  • Or, failing that, use GPU rendering or 1x scale for headless tabs when the viewer is local.

Related: #15328

Activity

  1. jtksystems commented on Oct 9, 2026

    @jtksystems

    Same regression outside WSL: a remote Linux environment reached through T3 Connect.

    Setup: T3 server on an always-on Ubuntu 26.04 host (Ryzen 7 7735HS, Radeon 680M iGPU, 16 threads, 26 GB), running as a user service. The desktop app runs on a Mac and connects to it through T3 Connect. Before 0.0.46-nightly.20261008, preview tabs for this environment rendered natively on the Mac and were smooth. Since the update, scrolling lags and frames blur while moving, on any site.

    What I see: the same path this issue describes. Because the desktop app didn't launch this server, its tabs never get the native <webview>. They run in ~/.t3/tools/chrome-headless-shell/linux64/154.0.8037.92/ and stream to the Mac:

    chrome-headless-shell ... --headless --force-device-scale-factor=2 ...
    chrome-headless-shell --type=gpu-process ... --ozone-platform=headless --use-angle=swiftshader-webgl --enable-unsafe-swiftshader

    So it renders on the CPU at 2× DPR while the host's GPU (/dev/dri/renderD128) sits idle, and every frame crosses the Connect tunnel. The page itself is fine: in a plain headless Chromium on the same host, the same pages scroll at a steady 16.7 ms per frame. The cost is in the streaming path.

    Also worth noting for Ubuntu 23.10+ hosts: the new engine wouldn't start at all until I added an AppArmor profile allowing userns for that binary, because kernel.apparmor_restrict_unprivileged_userns=1 gives Chrome's "No usable sandbox". T3's error message pointed at the fix, which is good. A narrow profile works and keeps the system-wide restriction on:

    profile t3-chrome-headless-shell /home/*/.t3/tools/chrome-headless-shell/linux64/*/chrome-headless-shell flags=(unconfined) {
      userns,
    }
    

    What would help, in rough order:

    1. A per-environment setting to render preview tabs on the client (the old native path) when the client is a desktop app, for remote and WSL environments alike.
    2. On the streamed path, a configurable or DPR-aware device scale factor instead of a fixed 2, and GPU rendering where the host has one.
    3. Stream quality and frame-rate settings for the streamed path.

    Workaround for now: open the URL in the client's own browser, which loses agent visibility of the tab, as noted above.

  2. UncleLYHME commented on Oct 9, 2026

    @UncleLYHME

    Same regression here on a standalone Linux server (Ubuntu 24.04, AMD Radeon 780M iGPU, t3 serve as a systemd user service, 0.0.46-nightly.20261008.2849), viewed remotely. I dug into the bundled server to see what's configurable and benchmarked the GPU path.

    What's in the code (0.0.46)

    • ServerBrowserContexts.launchOptions() hard-codes args: ["--disable-gpu", "--force-device-scale-factor=2"]. Playwright adds --enable-unsafe-swiftshader, so WebGL runs on SwiftShader (--use-angle=swiftshader-webgl in the GPU process).
    • The HTML render path (launchBrowser) also passes --disable-gpu.
    • The only browser env var left is T3CODE_SERVER_BROWSER_SANDBOX. T3CODE_SERVER_BROWSER is gone, so there's no knob for flags or the executable.
    • The viewer stream is also capped independently of rendering: each Page.screencastFrameAck is delayed until arrivedAt + SCREENCAST_ACK_PACE_MS (100 ms). Chrome allows 2 unacked screencast frames, so the live view tops out around 20 fps (measured 22 fps on a continuously animating page). During scroll it drops to startScreencast(.5) at SCREENCAST_MOTION_QUALITY 50.

    GPU benchmark (same pinned chrome-headless-shell 154.0.8037.92, launched via the bundled playwright-core, 1280×800 @ DPR 2, full-viewport fragment shader)

    Flags WebGL renderer Heavy shader
    T3 default (--disable-gpu) SwiftShader 9.4 fps
    --ignore-gpu-blocklist --use-gl=angle --use-angle=gl-egl AMD Radeon 780M (radeonsi) 60.7 fps (vsync)
    --ignore-gpu-blocklist --use-angle=vulkan --enable-features=Vulkan,DefaultANGLEVulkan,VulkanFromANGLE AMD Radeon 780M (RADV) 60.5 fps
    --use-angle=gl or --enable-gpu alone no WebGL n/a

    So headless shell with ozone headless can use a real GPU through ANGLE (EGL or Vulkan) when the host exposes /dev/dri/renderD128.

    Workaround (no source edits)

    T3 only checks that ~/.t3/tools/chrome-headless-shell/<platform>/<version>/chrome-headless-shell is an executable file, so a wrapper works:

    cd ~/.t3/tools/chrome-headless-shell/linux64/154.0.8037.92
    mv chrome-headless-shell chrome-headless-shell.real
    cat > chrome-headless-shell <<'SH'
    #!/bin/bash
    args=()
    for a in "$@"; do [[ $a == --disable-gpu ]] || args+=("$a"); done
    exec "$(dirname "$0")/chrome-headless-shell.real" \
      --ignore-gpu-blocklist --use-gl=angle --use-angle=gl-egl "${args[@]}"
    SH
    chmod 755 chrome-headless-shell
    # then kill the running chrome-headless-shell; T3 relaunches it on next use

    After a relaunch, a T3 preview tab reports ANGLE (AMD, AMD Radeon 780M Graphics (radeonsi ...), OpenGL ES 3.2). A Chrome version bump installs a new directory and silently drops the wrapper. If you use t3 browser setup's AppArmor profile, note that its path glob only matches the chrome-headless-shell filename.

    This fixes rendering cost (big for WebGL/map pages) but not the ~20 fps stream cap above.

    Edit: the same wrapper can also lift the stream cap without source changes. It runs a small Node relay on the --remote-debugging-pipe fds that acks Chrome's screencast frames itself while fewer than N frames are still awaiting T3's ack, and answers T3's late acks locally, so a slow viewer still applies backpressure. With T3's exact pacing simulated: 22 fps without the relay, 30 / 48 / 59 fps with N = 3 / 6 / 10. Screenshots, evaluate, and T3's scroll-time screencast restarts keep working. Happy to share the script if useful; a configurable SCREENCAST_ACK_PACE_MS would make it unnecessary.

    Suggestions

    1. Detect a usable GPU (or offer an env/setting such as T3CODE_SERVER_BROWSER_GPU=1) and launch with ANGLE EGL/Vulkan instead of --disable-gpu, keeping SwiftShader as the fallback.
    2. Make SCREENCAST_ACK_PACE_MS, stream quality, and device scale factor configurable, or derive DPR from the viewer.
    3. An env var for extra Chrome args or the executable path would remove the need for wrappers.
  3. Luzivog commented on Oct 11, 2026

    @Luzivog

    Streaming quality still matters after #17316, for agent tabs and web/mobile viewers. On a local Linux host with spare CPU, three things kept the server-browser stream far below the ~30 fps the SCREENCAST_ACK_PACE_MS comment expects (measured with a requestAnimationFrame page, Chrome 155, frames counted at the panel's WebSocket):

    1. Frames in flight. Page.startScreencast defaults to maxFramesInFlight: 3, and acks wait 100 ms, so the panel got ~25 fps while the page rendered 58. Passing maxFramesInFlight: 10 in the viewer's startScreencast gave 58–60 fps, with the pacing and the viewer's WebSocket backpressure unchanged. Acking early instead is a trap: Chromium's screencastFrameAck decrements in-flight per screencast session, not per frame, so a duplicate ack lifts the cap for good.
    2. --force-device-scale-factor=2 on 1x displays. The 2x paint dominates for wide panels: 14 fps at a 1400 px wide panel, 57 fps with the flag removed. Agent snapshots need their capture scale adjusted, since captureViewport assumes RENDER_SCALE 2. Choosing the scale from the viewer's devicePixelRatio might fit both cases.
    3. Motion downscale. Half size at quality 50 while scrolling is very visible. Keeping full size at 60 fps cost about 0.6 of a core per viewer on a page that repaints every frame, nothing on a still page.

    The maxFramesInFlight change alone looks low-risk for upstream.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions