perf(ssh): visibility-gate and back off the remote port scanner (#7610)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson 2026-07-07 21:42:12 -07:00 committed by GitHub
parent a09a00f066
commit c0091c07c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 410 additions and 12 deletions

View File

@ -111,6 +111,7 @@ import {
import { createMainWindow, loadMainWindow } from './window/createMainWindow'
import { createSystemTray, destroySystemTray } from './tray/system-tray'
import { focusExistingMainWindow } from './window/focus-existing-window'
import { notifyMainWindowBecameVisible } from './window/main-window-visibility'
import { CodexAccountService } from './codex-accounts/service'
import { CodexRuntimeHomeService } from './codex-accounts/runtime-home-service'
import {
@ -986,6 +987,11 @@ function openMainWindow(): BrowserWindow {
window.on('restore', resumeSyntheticTitleSpinnerTimer)
window.on('hide', stopSyntheticTitleSpinnerTimer)
window.on('minimize', stopSyntheticTitleSpinnerTimer)
// Why: visibility-gated main-process pollers (SSH port scanner) park while
// hidden and rely on this signal to resume; re-wired per window because
// macOS dock re-activation recreates the BrowserWindow.
window.on('show', notifyMainWindowBecameVisible)
window.on('restore', notifyMainWindowBecameVisible)
agentHookServer.setListener(
({
paneKey,

View File

@ -0,0 +1,261 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
PortScanner,
SSH_PORT_SCAN_BASE_INTERVAL_MS,
SSH_PORT_SCAN_MAX_INTERVAL_MS,
type PortScannerWindowVisibility
} from './ssh-port-scanner'
import type { SshChannelMultiplexer } from './ssh-channel-multiplexer'
import type { DetectedPort } from '../../shared/ssh-types'
type VisibilityHarness = {
visibility: PortScannerWindowVisibility
setVisible: (visible: boolean) => void
listenerCount: () => number
}
function createVisibilityHarness(initiallyVisible: boolean): VisibilityHarness {
let visible = initiallyVisible
const listeners = new Set<() => void>()
return {
visibility: {
isWindowVisible: () => visible,
onWindowBecameVisible: (listener) => {
listeners.add(listener)
return () => {
listeners.delete(listener)
}
}
},
setVisible: (next) => {
const wasVisible = visible
visible = next
if (!wasVisible && next) {
for (const listener of Array.from(listeners)) {
listener()
}
}
},
listenerCount: () => listeners.size
}
}
function port(portNumber: number): DetectedPort {
return { port: portNumber, host: '127.0.0.1', pid: 100 + portNumber, processName: 'node' }
}
function createMux(ports: () => DetectedPort[]): {
mux: SshChannelMultiplexer
request: ReturnType<typeof vi.fn>
} {
const request = vi.fn(async () => ({ ports: ports(), platform: 'linux' }))
return { mux: { request } as unknown as SshChannelMultiplexer, request }
}
const BASE = SSH_PORT_SCAN_BASE_INTERVAL_MS
const MAX = SSH_PORT_SCAN_MAX_INTERVAL_MS
describe('PortScanner', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('sends zero ports.detect requests while the window is hidden, then scans immediately on show', async () => {
const harness = createVisibilityHarness(false)
const { mux, request } = createMux(() => [port(3000)])
const scanner = new PortScanner(harness.visibility)
scanner.startScanning('t1', mux, vi.fn())
await vi.advanceTimersByTimeAsync(10 * 60_000)
expect(request).not.toHaveBeenCalled()
harness.setVisible(true)
await vi.advanceTimersByTimeAsync(0)
expect(request).toHaveBeenCalledTimes(1)
scanner.dispose()
})
it('polls at the base cadence while visible when results keep changing', async () => {
const harness = createVisibilityHarness(true)
let next = 3000
const { mux, request } = createMux(() => [port(next++)])
const scanner = new PortScanner(harness.visibility)
scanner.startScanning('t1', mux, vi.fn())
await vi.advanceTimersByTimeAsync(0)
expect(request).toHaveBeenCalledTimes(1)
// 5 simulated base intervals -> 5 more scans (no backoff while changing).
await vi.advanceTimersByTimeAsync(5 * BASE)
expect(request).toHaveBeenCalledTimes(6)
scanner.dispose()
})
it('doubles the interval up to the cap while unchanged and resets to base on a change', async () => {
const harness = createVisibilityHarness(true)
let ports = [port(3000)]
const { mux, request } = createMux(() => ports)
const onChanged = vi.fn()
const scanner = new PortScanner(harness.visibility)
scanner.startScanning('t1', mux, onChanged)
// t=0: first scan is a change (empty -> {3000}) and stays at base.
await vi.advanceTimersByTimeAsync(0)
expect(request).toHaveBeenCalledTimes(1)
expect(onChanged).toHaveBeenCalledTimes(1)
// t=12s: unchanged -> next wait doubles to 24s.
await vi.advanceTimersByTimeAsync(BASE)
expect(request).toHaveBeenCalledTimes(2)
// t=24s: nothing (waiting until t=36s).
await vi.advanceTimersByTimeAsync(BASE)
expect(request).toHaveBeenCalledTimes(2)
// t=36s: unchanged -> next wait caps at 30s (not 48s).
await vi.advanceTimersByTimeAsync(BASE)
expect(request).toHaveBeenCalledTimes(3)
// t=65.999s: still waiting.
await vi.advanceTimersByTimeAsync(MAX - 1)
expect(request).toHaveBeenCalledTimes(3)
// t=66s: unchanged -> next wait remains capped at 30s.
await vi.advanceTimersByTimeAsync(1)
expect(request).toHaveBeenCalledTimes(4)
await vi.advanceTimersByTimeAsync(MAX - 1)
expect(request).toHaveBeenCalledTimes(4)
await vi.advanceTimersByTimeAsync(1)
expect(request).toHaveBeenCalledTimes(5)
expect(onChanged).toHaveBeenCalledTimes(1)
// A changed result resets the cadence back to base.
ports = [port(3000), port(4000)]
await vi.advanceTimersByTimeAsync(MAX)
expect(request).toHaveBeenCalledTimes(6)
expect(onChanged).toHaveBeenCalledTimes(2)
await vi.advanceTimersByTimeAsync(BASE)
expect(request).toHaveBeenCalledTimes(7)
expect(scanner.getDetectedPorts('t1').map((p) => p.port)).toEqual([3000, 4000])
scanner.dispose()
})
it('resets backoff and scans immediately when scanning restarts (reconnect/session-ready)', async () => {
const harness = createVisibilityHarness(true)
const { mux, request } = createMux(() => [port(3000)])
const scanner = new PortScanner(harness.visibility)
const onChanged = vi.fn()
scanner.startScanning('t1', mux, onChanged)
// Back off: scans at t=0, t=12s, t=36s -> interval now 48s.
await vi.advanceTimersByTimeAsync(3 * BASE)
expect(request).toHaveBeenCalledTimes(3)
// Reconnect paths call startScanning again: immediate scan, base cadence.
scanner.startScanning('t1', mux, onChanged)
await vi.advanceTimersByTimeAsync(0)
expect(request).toHaveBeenCalledTimes(4)
await vi.advanceTimersByTimeAsync(BASE)
expect(request).toHaveBeenCalledTimes(5)
scanner.dispose()
})
it('parks when the window hides mid-run and resumes with an immediate scan on show', async () => {
const harness = createVisibilityHarness(true)
let next = 3000
const { mux, request } = createMux(() => [port(next++)])
const scanner = new PortScanner(harness.visibility)
scanner.startScanning('t1', mux, vi.fn())
await vi.advanceTimersByTimeAsync(BASE)
expect(request).toHaveBeenCalledTimes(2)
harness.setVisible(false)
await vi.advanceTimersByTimeAsync(30 * 60_000)
expect(request).toHaveBeenCalledTimes(2)
harness.setVisible(true)
await vi.advanceTimersByTimeAsync(0)
expect(request).toHaveBeenCalledTimes(3)
await vi.advanceTimersByTimeAsync(BASE)
expect(request).toHaveBeenCalledTimes(4)
scanner.dispose()
})
it('never overlaps a slow in-flight request and resumes the chain after it settles', async () => {
const harness = createVisibilityHarness(true)
let resolveFirst: ((value: { ports: DetectedPort[]; platform: string }) => void) | null = null
const request = vi.fn(
() =>
new Promise<{ ports: DetectedPort[]; platform: string }>((resolve) => {
resolveFirst = resolve
})
)
const scanner = new PortScanner(harness.visibility)
scanner.startScanning('t1', { request } as unknown as SshChannelMultiplexer, vi.fn())
expect(request).toHaveBeenCalledTimes(1)
// The chain waits for the in-flight request instead of stacking more.
await vi.advanceTimersByTimeAsync(10 * BASE)
expect(request).toHaveBeenCalledTimes(1)
resolveFirst!({ ports: [port(3000)], platform: 'linux' })
await vi.advanceTimersByTimeAsync(BASE)
expect(request).toHaveBeenCalledTimes(2)
scanner.dispose()
})
it('stopScanning halts polling and detaches the visibility listener', async () => {
const harness = createVisibilityHarness(true)
const { mux, request } = createMux(() => [port(3000)])
const scanner = new PortScanner(harness.visibility)
scanner.startScanning('t1', mux, vi.fn())
await vi.advanceTimersByTimeAsync(0)
expect(request).toHaveBeenCalledTimes(1)
expect(harness.listenerCount()).toBe(1)
scanner.stopScanning('t1')
expect(harness.listenerCount()).toBe(0)
await vi.advanceTimersByTimeAsync(10 * 60_000)
harness.setVisible(false)
harness.setVisible(true)
await vi.advanceTimersByTimeAsync(10 * 60_000)
expect(request).toHaveBeenCalledTimes(1)
expect(scanner.getDetectedPorts('t1')).toEqual([])
})
it('keeps targets independent: stopping one host leaves the other scanning', async () => {
const harness = createVisibilityHarness(true)
let nextA = 3000
let nextB = 4000
const a = createMux(() => [port(nextA++)])
const b = createMux(() => [port(nextB++)])
const scanner = new PortScanner(harness.visibility)
scanner.startScanning('host-a', a.mux, vi.fn())
scanner.startScanning('host-b', b.mux, vi.fn())
await vi.advanceTimersByTimeAsync(0)
expect(a.request).toHaveBeenCalledTimes(1)
expect(b.request).toHaveBeenCalledTimes(1)
scanner.stopScanning('host-a')
await vi.advanceTimersByTimeAsync(BASE)
expect(a.request).toHaveBeenCalledTimes(1)
expect(b.request).toHaveBeenCalledTimes(2)
scanner.dispose()
await vi.advanceTimersByTimeAsync(10 * BASE)
expect(b.request).toHaveBeenCalledTimes(2)
expect(harness.listenerCount()).toBe(0)
})
})

View File

@ -1,10 +1,30 @@
import type { SshChannelMultiplexer } from './ssh-channel-multiplexer'
import type { DetectedPort } from '../../shared/ssh-types'
const POLL_INTERVAL_MS = 3_000
// Why: every tick walks /proc/*/fd on the remote relay, so cadence is remote
// CPU, not just a local timer. 12s cuts steady-state request volume 4x vs the
// old 3s while keeping new-port detection well under the 30s workspace-scanner
// cadence users already accept.
export const SSH_PORT_SCAN_BASE_INTERVAL_MS = 12_000
// Why: idle backoff cap — an unchanged port set doubles the interval up to
// this bound, so a quiet remote costs two scans per minute instead of twenty
// without exceeding the existing workspace-port scanner's visible cadence.
export const SSH_PORT_SCAN_MAX_INTERVAL_MS = 30_000
export type PortScannerWindowVisibility = {
isWindowVisible: () => boolean
// Fires when a hidden/minimized window is shown again; returns unsubscribe.
onWindowBecameVisible: (listener: () => void) => () => void
}
type ScanHandle = {
timer: ReturnType<typeof setInterval>
timer: ReturnType<typeof setTimeout> | null
intervalMs: number
// Why: while the window is hidden the scan chain is parked outright — no
// timer wakeups, no remote requests — and the visibility listener resumes
// it with an immediate scan so ports opened while hidden surface at once.
parkedWhileHidden: boolean
unsubscribeVisibility: () => void
// Why: keyed by "host:port" (not just port) so that host-distinct listeners
// on the same port (e.g. 127.0.0.1:3000 + 0.0.0.0:3000) are tracked separately.
previousPorts: Map<string, DetectedPort>
@ -17,6 +37,8 @@ type ScanHandle = {
export class PortScanner {
private handles = new Map<string, ScanHandle>()
constructor(private visibility: PortScannerWindowVisibility) {}
startScanning(
targetId: string,
mux: SshChannelMultiplexer,
@ -25,14 +47,18 @@ export class PortScanner {
this.stopScanning(targetId)
const handle: ScanHandle = {
timer: null!,
timer: null,
intervalMs: SSH_PORT_SCAN_BASE_INTERVAL_MS,
parkedWhileHidden: false,
unsubscribeVisibility: () => {},
previousPorts: new Map(),
initialPorts: null
}
const isCurrent = (): boolean => this.handles.get(targetId) === handle
// Why: guard against overlapping scans. On slow remotes, /proc/*/fd walks
// can take longer than POLL_INTERVAL_MS. Without this guard, setInterval
// would stack up concurrent requests on the shared SSH multiplexer.
// Why: guard against overlapping scans. The timer chain only reschedules
// after a poll completes, but the visibility-resume path can race a poll
// still in flight on a slow remote.
let polling = false
const poll = async (): Promise<void> => {
if (polling) {
@ -45,7 +71,7 @@ export class PortScanner {
platform: string
}
if (!this.handles.has(targetId)) {
if (!isCurrent()) {
return
}
@ -60,7 +86,10 @@ export class PortScanner {
if (!portsEqual(handle.previousPorts, currentPorts)) {
handle.previousPorts = currentPorts
handle.intervalMs = SSH_PORT_SCAN_BASE_INTERVAL_MS
onChanged(targetId, result.ports, result.platform)
} else {
handle.intervalMs = Math.min(handle.intervalMs * 2, SSH_PORT_SCAN_MAX_INTERVAL_MS)
}
} catch {
// Relay disconnected or request timed out — retry on next interval
@ -69,10 +98,32 @@ export class PortScanner {
}
}
handle.timer = setInterval(() => void poll(), POLL_INTERVAL_MS)
this.handles.set(targetId, handle)
const tick = async (): Promise<void> => {
handle.timer = null
if (!isCurrent()) {
return
}
if (!this.visibility.isWindowVisible()) {
handle.parkedWhileHidden = true
return
}
await poll()
if (!isCurrent()) {
return
}
handle.timer = setTimeout(() => void tick(), handle.intervalMs)
}
void poll()
handle.unsubscribeVisibility = this.visibility.onWindowBecameVisible(() => {
if (!isCurrent() || !handle.parkedWhileHidden) {
return
}
handle.parkedWhileHidden = false
void tick()
})
this.handles.set(targetId, handle)
void tick()
}
getDetectedPorts(targetId: string): DetectedPort[] {
@ -88,7 +139,10 @@ export class PortScanner {
if (!handle) {
return
}
clearInterval(handle.timer)
if (handle.timer) {
clearTimeout(handle.timer)
}
handle.unsubscribeVisibility()
this.handles.delete(targetId)
}

View File

@ -118,6 +118,10 @@ function createMockDeps() {
} as unknown as SshPortForwardManager
const mockWindow = {
isDestroyed: () => false,
// Why: the port scanner visibility-gates its ticks; a visible mock window
// keeps establish-path tests exercising the scan-on-ready behavior.
isVisible: () => true,
isMinimized: () => false,
webContents: { send: vi.fn() }
}
const getMainWindow = vi.fn().mockReturnValue(mockWindow)

View File

@ -52,6 +52,7 @@ import {
import { registerSshGitProvider, unregisterSshGitProvider } from '../providers/ssh-git-dispatch'
import { notifyRemoteWorkspaceHandlers } from '../ipc/remote-workspace-events'
import { PortScanner } from './ssh-port-scanner'
import { isMainWindowVisible, onMainWindowBecameVisible } from '../window/main-window-visibility'
import type { SshPortForwardManager } from './ssh-port-forward'
import type { SshConnection } from './ssh-connection'
import { joinRemotePath, isWindowsRemoteHost, type RemoteHostPlatform } from './ssh-remote-platform'
@ -982,7 +983,13 @@ export class SshRelaySession {
if (!this.mux || this.isDisposed()) {
return
}
const scanner = new PortScanner()
// Why: each scan walks /proc/*/fd on the remote host, so the scanner skips
// ticks entirely while no window can show the results (hidden to tray or
// minimized overnight) and rescans immediately when the window returns.
const scanner = new PortScanner({
isWindowVisible: () => isMainWindowVisible(this.getMainWindow()),
onWindowBecameVisible: onMainWindowBecameVisible
})
this.portScanner = scanner
// Why: capture the scanner instance so that a late ports.detect callback
// from a previous relay session (before reconnect replaced it) is silently

View File

@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest'
import { isMainWindowVisible } from './main-window-visibility'
describe('main window visibility helpers', () => {
it('treats a minimal alive window double as visible', () => {
expect(isMainWindowVisible({ isDestroyed: () => false })).toBe(true)
})
it('parks work when the real window is hidden, minimized, destroyed, or missing', () => {
expect(
isMainWindowVisible({
isDestroyed: () => false,
isVisible: () => false,
isMinimized: () => false
})
).toBe(false)
expect(
isMainWindowVisible({
isDestroyed: () => false,
isVisible: () => true,
isMinimized: () => true
})
).toBe(false)
expect(isMainWindowVisible({ isDestroyed: () => true })).toBe(false)
expect(isMainWindowVisible(null)).toBe(false)
})
})

View File

@ -0,0 +1,38 @@
// Why: BrowserWindow instances are recreated on macOS dock re-activation, so
// long-lived main-process services (e.g. the SSH port scanner) subscribe to
// this process-global signal instead of a specific window instance; index.ts
// re-wires each new window's show/restore events into notifyMainWindowBecameVisible.
type MainWindowBecameVisibleListener = () => void
type MainWindowVisibilityState = {
isDestroyed: () => boolean
isVisible?: () => boolean
isMinimized?: () => boolean
}
const listeners = new Set<MainWindowBecameVisibleListener>()
export function notifyMainWindowBecameVisible(): void {
for (const listener of Array.from(listeners)) {
listener()
}
}
export function onMainWindowBecameVisible(listener: MainWindowBecameVisibleListener): () => void {
listeners.add(listener)
return () => {
listeners.delete(listener)
}
}
export function isMainWindowVisible(window: MainWindowVisibilityState | null): boolean {
if (window === null || window.isDestroyed()) {
return false
}
// Why: production BrowserWindow exposes both APIs, but older main-process
// tests use minimal window doubles that should stay visible by default.
const isVisible = window.isVisible?.() ?? true
const isMinimized = window.isMinimized?.() ?? false
return isVisible && !isMinimized
}