From 7ece64fa8b04633df3913e2b4ac346243bb126e5 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 4 Sep 2026 04:48:53 -0700 Subject: [PATCH] perf(ports): serve unchanged macOS listeners from remembered metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workspace port scan shells out three times every 30s while the window is visible. Profiling an idle app showed the cycle costs 0.67s of CPU — about 2.2% of a core sustained, roughly 65% of Orca's total idle CPU — and two of those three commands only re-derive the command line and cwd of listeners that have not changed since the last scan. Reuse the metadata already remembered for a listener when this scan's free lsof -F c process name still matches, so a recycled pid re-probes instead of inheriting a dead process's attribution. In steady state that drops the cycle to the single listening scan. --- .../local-workspace-platform-port-scanner.ts | 13 ++-- .../ports/local-workspace-port-scan-state.ts | 37 ++++++++++++ .../local-workspace-port-scanner.test.ts | 59 ++++++++++++++++++- 3 files changed, 104 insertions(+), 5 deletions(-) diff --git a/src/main/ports/local-workspace-platform-port-scanner.ts b/src/main/ports/local-workspace-platform-port-scanner.ts index 7e3b9941617..ba12b226d0c 100644 --- a/src/main/ports/local-workspace-platform-port-scanner.ts +++ b/src/main/ports/local-workspace-platform-port-scanner.ts @@ -3,6 +3,7 @@ import { getProcessOutputFields } from '../../shared/process-output-field-scanne import { readWindowsProcessTable } from '../windows/windows-process-table' import { runPortScanCommand } from './port-scan-command-client' import { + partitionListenersNeedingMetadata, recallListenerMetadata, rememberListenerMetadata, shouldSkipMetadataCommands, @@ -124,11 +125,15 @@ async function scanDarwinLsofPorts( if (shouldSkipMetadataCommands(spawnMs, options)) { return { ports, metadataAvailable: false } } - const metadata = await loadDarwinProcessMetadata( - new Set(ports.flatMap((p) => (p.pid ? [p.pid] : []))) - ) + // Why: on a quiet machine the same servers keep listening, so the two metadata commands — the + // expensive half of the scan — would re-derive answers the last scan already has. + const { hydrated, pidsNeedingMetadata } = partitionListenersNeedingMetadata(ports) + if (pidsNeedingMetadata.size === 0) { + return { ports: hydrated, metadataAvailable: true } + } + const metadata = await loadDarwinProcessMetadata(pidsNeedingMetadata) return { - ports: ports.map((port) => ({ ...metadata.get(port.pid ?? -1), ...port })), + ports: hydrated.map((port) => ({ ...metadata.get(port.pid ?? -1), ...port })), metadataAvailable: true } } diff --git a/src/main/ports/local-workspace-port-scan-state.ts b/src/main/ports/local-workspace-port-scan-state.ts index 582e8b9731e..e4833b45449 100644 --- a/src/main/ports/local-workspace-port-scan-state.ts +++ b/src/main/ports/local-workspace-port-scan-state.ts @@ -85,6 +85,43 @@ export function rememberListenerMetadata(ports: readonly RawListeningPort[]): vo ) } +/** + * Split listeners into those a previous scan already resolved and the pids still needing a probe. + * + * Why: the metadata commands are the expensive half of a macOS scan, and a listener that is still + * the same process on the same address has the same command line and cwd it had 30s ago. The + * remembered entry is only trusted when the process name from this scan's free `lsof -F c` field + * still matches, so a recycled pid re-probes instead of inheriting the dead process's metadata. + */ +export function partitionListenersNeedingMetadata(ports: readonly RawListeningPort[]): { + hydrated: RawListeningPort[] + pidsNeedingMetadata: Set +} { + const hydrated: RawListeningPort[] = [] + const pidsNeedingMetadata = new Set() + for (const port of ports) { + const remembered = lastListenerMetadata.get(listenerMetadataKey(port)) + // Why require commandLine: a probe that returned nothing must not be cached as an answer. + if ( + remembered?.commandLine !== undefined && + remembered.processName === port.processName && + port.pid !== undefined + ) { + hydrated.push({ + ...port, + commandLine: port.commandLine ?? remembered.commandLine, + cwd: port.cwd ?? remembered.cwd + }) + continue + } + hydrated.push(port) + if (port.pid !== undefined) { + pidsNeedingMetadata.add(port.pid) + } + } + return { hydrated, pidsNeedingMetadata } +} + export function recallListenerMetadata(port: RawListeningPort): RawListeningPort { const remembered = lastListenerMetadata.get(listenerMetadataKey(port)) if (!remembered) { diff --git a/src/main/ports/local-workspace-port-scanner.test.ts b/src/main/ports/local-workspace-port-scanner.test.ts index be37ad60eb3..be2421a5d49 100644 --- a/src/main/ports/local-workspace-port-scanner.test.ts +++ b/src/main/ports/local-workspace-port-scanner.test.ts @@ -387,7 +387,19 @@ describe('scanWorkspacePorts with delayed process creation', () => { it('does not let a required-metadata scan reset the background skip parity', async () => { vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin') - mockStalledDarwinScan() + // Why a fresh pid each cycle: a listener the previous scan already resolved is served from the + // remembered metadata, so a stable pid would hide whether this scan skipped the probe or not. + let listenerPid = 123 + runPortScanCommandMock.mockImplementation(async (command: string, args: string[]) => { + if (command === 'lsof' && args.includes('-iTCP')) { + listenerPid += 1 + return { stdout: `p${listenerPid}\ncnode\nn127.0.0.1:5173`, spawnMs: 4_200 } + } + if (command === 'lsof') { + return { stdout: [`p${listenerPid}`, 'n/repo'].join('\n'), spawnMs: 4_200 } + } + return { stdout: `${listenerPid} node /repo/server.js`, spawnMs: 4_200 } + }) await scanWorkspacePorts(worktrees, urlWatcherStub()) await scanWorkspacePorts(worktrees, urlWatcherStub(), { requireMetadata: true }) @@ -397,6 +409,51 @@ describe('scanWorkspacePorts with delayed process creation', () => { expect(runPortScanCommandMock).toHaveBeenCalledTimes(7) }) + it('serves an unchanged listener from remembered metadata instead of re-probing', async () => { + vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin') + runPortScanCommandMock.mockImplementation(async (command: string, args: string[]) => { + if (command === 'lsof' && args.includes('-iTCP')) { + return { stdout: LSOF_LISTEN_OUTPUT, spawnMs: 5 } + } + if (command === 'lsof') { + return { stdout: ['p123', 'n/repo'].join('\n'), spawnMs: 5 } + } + return { stdout: '123 node /repo/server.js', spawnMs: 5 } + }) + + const first = await scanWorkspacePorts(worktrees, urlWatcherStub()) + expect(runPortScanCommandMock).toHaveBeenCalledTimes(3) + + const second = await scanWorkspacePorts(worktrees, urlWatcherStub()) + + // Only the listening scan itself runs; the two metadata commands are served from the cache. + expect(runPortScanCommandMock).toHaveBeenCalledTimes(4) + expect(second.ports).toEqual(first.ports) + }) + + it('re-probes when a recycled pid is running a different process', async () => { + vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin') + let processName = 'node' + runPortScanCommandMock.mockImplementation(async (command: string, args: string[]) => { + if (command === 'lsof' && args.includes('-iTCP')) { + return { stdout: `p123\nc${processName}\nn127.0.0.1:5173`, spawnMs: 5 } + } + if (command === 'lsof') { + return { stdout: ['p123', 'n/repo'].join('\n'), spawnMs: 5 } + } + return { stdout: `123 ${processName} /repo/server.js`, spawnMs: 5 } + }) + + await scanWorkspacePorts(worktrees, urlWatcherStub()) + expect(runPortScanCommandMock).toHaveBeenCalledTimes(3) + + // Same pid and address, different process: the remembered metadata must not be reused. + processName = 'python3' + await scanWorkspacePorts(worktrees, urlWatcherStub()) + + expect(runPortScanCommandMock).toHaveBeenCalledTimes(6) + }) + // Regression for #11161 review: without carry-forward the panel moves every // workspace port into External on each skipped cycle. it('carries the previous cycle attribution through a skipped scan', async () => {