Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
- On macOS or Linux, open a terminal in any thread so the scan isn't skipped as having nothing to find (perf(server): skip idle port discovery on Linux and macOS #20975).
- Open a thread whose details panel shows its Servers section, or open an empty preview.
- Hide the window, switch to another app, or move the browser tab to the background. Leave it there.
- Watch the server's process activity or trace file.
lsof -iTCP -sTCP:LISTEN -P -n -F pcnkeeps running every 3 seconds, andPortDiscovery.pollTickspans keep arriving at the same rate.
Expected behavior
While no client is in the foreground, the server stops the 3-second port poll, the way other background work already checks BackgroundPolicy. VcsStatusBroadcaster and UsageLimitSources call shouldRunScopeWork, and the pull request sweep in ThreadPullRequestService calls shouldRunOpportunisticWork. When a client returns to the foreground, the server list catches up on the next tick.
Actual behavior
The poll loop in apps/server/src/preview/PortScanner.ts (pollIfRetained, repeated on Schedule.spaced(POLL_INTERVAL)) checks only retainCount > 0 and never consults BackgroundPolicy. The web and desktop clients hold subscribeDiscoveredLocalServers whenever the thread details panel shows its Servers section or the preview empty state is mounted. A hidden window or a background tab keeps the scan running for as long as it stays open.
Numbers from one real install, over a 112-minute trace window:
- 2,483
lsofspawns, with a median of 101 ms each, for 243 s of total process time. - A flat 290 spawns per 15 minutes for the whole window. At that rate the poll runs about 28,800 times a day while the panel stays mounted.
- The six
PortDiscovery.*span names added about 5.5 MB of trace data.
Impact
Minor bug or occasional failure
Version or commit
main @ 5b9731c
Environment
macOS. Linux runs the same loop with lsof, or with the /proc scan when lsof is missing.
Possible fix
Skip the scheduled tick unless backgroundPolicy.shouldRunOpportunisticWork is true, the same check the pull request sweep uses. The initial scan on subscribe and the refresh after previewStopLocalServer would stay unconditional, so opening the panel or stopping a server still shows fresh results right away. The change is server-only. Web and desktop already report foreground leases, and mobile doesn't use this subscription.
A few behavior questions for a maintainer:
shouldRunOpportunisticWorkalso returns false while the host is locked, suspended, under thermal pressure, or in low power mode on the Balanced profile. The Balanced description already says it pauses probes in low power mode, but a focused user in Low Power Mode would stop seeing new servers until they leave it. Gating onactiveForegroundLeaseCount > 0from the policy snapshot would avoid that.- On Balanced, a window that is visible but unfocused, with no input for 45 seconds, doesn't hold a foreground lease. A server list watched on a second monitor would stop updating until the user interacts with it again.
- The Performance profile promises scoped probes while any subscribed client stays connected. Port discovery has no background scope, so it would follow the foreground rule in every profile unless it gets a scope of its own, which needs client changes.
Related work
- perf(server): skip idle port discovery on Linux and macOS #20975 skips the scan when there are no terminals, configured URLs, or possible agent servers. fix(server): preview discovery stops sending HTTP and TLS to agent-started ports #27893 removed HTTP probing. On
t3 servesendsGET /every ~16 seconds to every port opened by a process an agent started #29087, discovery was confirmed to still run thelsofor/procscan. None of these cover a mounted panel in a background window. - perf: faster, lighter desktop startup and streaming #28991 replaces this poll loop with a polling fiber per retention, so a fix here would conflict with it. The gate fits in either version of the loop. I haven't opened a PR for that reason.
- fix(web): show running servers on thread rows #24606 and feat(web): optionally show running ports beside sidebar threads #29669 show ports on sidebar thread rows through
useThreadDiscoveredPorts, which holds this subscription whenever the sidebar shows. Either one would make the background cost apply to most users all day.
Activity