Repository navigation
[Bug]: Preview tabs in WSL environments are laggy since #15328 (no native webview, CPU-rendered stream) #17090
Description
Activity
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
usernsfor that binary, becausekernel.apparmor_restrict_unprivileged_userns=1gives 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:
- 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.
- 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.
- 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.
Same regression here on a standalone Linux server (Ubuntu 24.04, AMD Radeon 780M iGPU,
t3 serveas 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-codesargs: ["--disable-gpu", "--force-device-scale-factor=2"]. Playwright adds--enable-unsafe-swiftshader, so WebGL runs on SwiftShader (--use-angle=swiftshader-webglin 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_BROWSERis gone, so there's no knob for flags or the executable. - The viewer stream is also capped independently of rendering: each
Page.screencastFrameAckis delayed untilarrivedAt + 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 tostartScreencast(.5)atSCREENCAST_MOTION_QUALITY50.
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-eglAMD Radeon 780M (radeonsi) 60.7 fps (vsync) --ignore-gpu-blocklist --use-angle=vulkan --enable-features=Vulkan,DefaultANGLEVulkan,VulkanFromANGLEAMD Radeon 780M (RADV) 60.5 fps --use-angle=glor--enable-gpualoneno 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-shellis 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 uset3 browser setup's AppArmor profile, note that its path glob only matches thechrome-headless-shellfilename.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-pipefds 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 configurableSCREENCAST_ACK_PACE_MSwould make it unnecessary.Suggestions
- 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. - Make
SCREENCAST_ACK_PACE_MS, stream quality, and device scale factor configurable, or derive DPR from the viewer. - An env var for extra Chrome args or the executable path would remove the need for wrappers.
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_MScomment expects (measured with arequestAnimationFramepage, Chrome 155, frames counted at the panel's WebSocket):- Frames in flight.
Page.startScreencastdefaults tomaxFramesInFlight: 3, and acks wait 100 ms, so the panel got ~25 fps while the page rendered 58. PassingmaxFramesInFlight: 10in the viewer'sstartScreencastgave 58–60 fps, with the pacing and the viewer's WebSocket backpressure unchanged. Acking early instead is a trap: Chromium'sscreencastFrameAckdecrements in-flight per screencast session, not per frame, so a duplicate ack lifts the cap for good. --force-device-scale-factor=2on 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, sincecaptureViewportassumesRENDER_SCALE2. Choosing the scale from the viewer'sdevicePixelRatiomight fit both cases.- 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
maxFramesInFlightchange alone looks low-risk for upstream.- Frames in flight.
Before submitting
Area
apps/desktop
Steps to reproduce
127.0.0.1.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:
chrome-headless-shellinside 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>.<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/desktopBrowserControlFdinDesktopBackendConfiguration.ts).resolveWslStartConfigdoesn't set them, andDesktopBackendManager.tsnotes thatwsl.exedrops additional fds. So a WSL environment always takes the streaming path. In my setup, the WSL server runs as thet3 servicesystemd user service rather than being spawned by the desktop, which leads to the same result.--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.T3CODE_SERVER_BROWSERand the desktop automation path, so users can't switch back.I ruled out host load. With memory freed and the service restarted,
t3 serveis 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 thet3 servicebackground 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-webglBefore the restart, the server log also showed repeated
event loop stalled for 2500-7000 mswarnings whilet3 servewas 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
Related: #15328