diff --git a/src/relay/port-scan-handler.test.ts b/src/relay/port-scan-handler.test.ts index fc0002dcb5e..799663071d3 100644 --- a/src/relay/port-scan-handler.test.ts +++ b/src/relay/port-scan-handler.test.ts @@ -119,6 +119,31 @@ function mockLinuxProcScan({ }) } +describe('PortScanHandler Linux walk bounds', () => { + it('stops walking procfs once every listening socket has an owner', async () => { + // Why: this scan repeats for the life of the session, and its unit cost was O(all host + // processes x all fds) regardless of how few sockets it was resolving. On a busy remote the + // process count only climbs, so the scan got permanently more expensive -- the shape behind + // "SSH degrades the longer Orca stays open". One listener means one readlink, not 100,000. + mockLinuxProcScan({ pidCount: 1_000, fdCount: 100 }) + + await capturePortDetectHandler()({}, requestContext()) + + expect(readlinkMock).toHaveBeenCalledTimes(1) + }) + + it('still walks the whole table when a socket has no reachable owner', async () => { + // The inverse: an unattributable inode (another user's process) must not make the scan give up + // early on sockets it could still attribute. + mockLinuxProcScan({ pidCount: 3, fdCount: 2 }) + readlinkMock.mockImplementation(() => Promise.resolve('socket:[99999]')) + + await capturePortDetectHandler()({}, requestContext()) + + expect(readlinkMock).toHaveBeenCalledTimes(6) + }) +}) + describe('PortScanHandler Linux cancellation', () => { it('does not touch procfs for an already-cancelled request', async () => { const controller = new AbortController() diff --git a/src/relay/port-scan-handler.ts b/src/relay/port-scan-handler.ts index 40150133a04..a9fa7351122 100644 --- a/src/relay/port-scan-handler.ts +++ b/src/relay/port-scan-handler.ts @@ -161,6 +161,13 @@ export class PortScanHandler { for (const pidStr of pids) { signal?.throwIfAborted() + // Why: every remaining pid costs a readdir plus one readlink per fd, and this scan repeats for + // the life of the session. Without this the walk was O(all host processes x all fds) even once + // every listener was already attributed, so its cost grew with the remote's process count and + // never came back down — the shape behind "SSH gets slower the longer Orca stays open". + if (result.size === inodes.size) { + return result + } const fdDir = `/proc/${pidStr}/fd` let fds: string[] try { @@ -192,6 +199,9 @@ export class PortScanHandler { const inode = Number.parseInt(match[1], 10) if (inodes.has(inode)) { result.set(inode, pid) + if (result.size === inodes.size) { + return result + } } } }