perf: reuse port scan worktree attribution (#4274)

This commit is contained in:
Neil 2026-05-31 10:42:03 -07:00 committed by GitHub
parent 0cf4f6010e
commit 57caa94116
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 111 additions and 20 deletions

View File

@ -1,4 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import path from 'path'
import {
attributePortToWorkspace,
isContainerProcess,
@ -142,6 +143,62 @@ describe('container process classification', () => {
})
})
describe('scanWorkspacePorts attribution work', () => {
afterEach(() => {
vi.restoreAllMocks()
execFileMock.mockReset()
})
it('normalizes worktree paths once per scan instead of once per port phase', async () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
const resolveSpy = vi.spyOn(path, 'resolve')
const invokeCallback = (callback: unknown, stdout: string): void => {
if (typeof callback !== 'function') {
throw new Error('missing execFile callback')
}
const execCallback = callback as (error: Error | null, stdout: string) => void
execCallback(null, stdout)
}
execFileMock.mockImplementation(
(command: string, args: string[], _options: unknown, callback: unknown) => {
if (command === 'lsof' && args.includes('-iTCP')) {
invokeCallback(
callback,
['p123', 'cnode', 'n127.0.0.1:3000', 'p124', 'cnode', 'n127.0.0.1:3001'].join('\n')
)
} else if (command === 'lsof') {
invokeCallback(
callback,
['p123', 'n/repo/service', 'p124', 'n/repo/worktrees/feature/app'].join('\n')
)
} else if (command === 'ps') {
invokeCallback(
callback,
[
'123 node /repo/service/server.js',
'124 node /repo/worktrees/feature/app/server.js'
].join('\n')
)
} else {
invokeCallback(callback, '')
}
return { kill: vi.fn() }
}
)
const scan = await scanWorkspacePorts(worktrees, {
lookup: () => undefined,
reconcileScan: vi.fn()
})
expect(scan.ports.filter((port) => port.kind === 'workspace')).toHaveLength(2)
const worktreePathResolveCalls = resolveSpy.mock.calls.filter(
([input]) => input === '/repo' || input === '/repo/worktrees/feature'
)
expect(worktreePathResolveCalls).toHaveLength(worktrees.length)
})
})
describe('scanWorkspacePorts command timeout', () => {
afterEach(() => {
vi.useRealTimers()

View File

@ -31,15 +31,21 @@ type ProcessMetadata = {
cwd?: string
}
type NormalizedWorkspacePortProbe = {
worktree: WorkspacePortProbe
normalizedPath: string
}
export async function scanWorkspacePorts(
worktrees: WorkspacePortProbe[],
urlWatcher: Pick<AdvertisedUrlWatcher, 'lookup' | 'reconcileScan'> = advertisedUrlWatcher
): Promise<WorkspacePortScanResult> {
try {
const rawPorts = await scanPlatformListeningPorts()
reconcileAdvertisedUrls(rawPorts, worktrees, urlWatcher)
const normalizedWorktrees = normalizeWorkspacePortProbes(worktrees)
reconcileAdvertisedUrls(rawPorts, normalizedWorktrees, urlWatcher)
const ports = rawPorts
.map((port) => enrichPort(port, worktrees, urlWatcher))
.map((port) => enrichPort(port, normalizedWorktrees, urlWatcher))
.sort(compareWorkspacePorts)
.slice(0, MAX_PORTS)
return { platform: process.platform, scannedAt: Date.now(), ports }
@ -57,17 +63,31 @@ export async function scanWorkspacePorts(
export function attributePortToWorkspace(
port: Pick<RawListeningPort, 'cwd' | 'commandLine'>,
worktrees: WorkspacePortProbe[]
): WorkspacePortOwner | undefined {
return attributePortToNormalizedWorkspaces(port, normalizeWorkspacePortProbes(worktrees))
}
function normalizeWorkspacePortProbes(
worktrees: readonly WorkspacePortProbe[]
): NormalizedWorkspacePortProbe[] {
return worktrees.map((worktree) => ({
worktree,
normalizedPath: normalizeComparablePath(worktree.path)
}))
}
function attributePortToNormalizedWorkspaces(
port: Pick<RawListeningPort, 'cwd' | 'commandLine'>,
worktrees: readonly NormalizedWorkspacePortProbe[]
): WorkspacePortOwner | undefined {
const cwd = port.cwd ? normalizeComparablePath(port.cwd) : null
const commandLine = port.commandLine ? normalizeComparableText(port.commandLine) : null
const cwdMatches = cwd
? worktrees
.map((worktree) => ({ worktree, normalizedPath: normalizeComparablePath(worktree.path) }))
.filter(({ normalizedPath }) => isSameOrDescendant(cwd, normalizedPath))
: []
const cwdMatch = pickDeepestMatch(cwdMatches)
const cwdMatch = cwd
? pickDeepestMatching(worktrees, ({ normalizedPath }) =>
isSameOrDescendant(cwd, normalizedPath)
)
: undefined
if (cwdMatch) {
return toOwner(cwdMatch.worktree, 'cwd')
}
@ -76,10 +96,9 @@ export function attributePortToWorkspace(
return undefined
}
const commandMatches = worktrees
.map((worktree) => ({ worktree, normalizedPath: normalizeComparablePath(worktree.path) }))
.filter(({ normalizedPath }) => includesPathBoundary(commandLine, normalizedPath))
const commandMatch = pickDeepestMatch(commandMatches)
const commandMatch = pickDeepestMatching(worktrees, ({ normalizedPath }) =>
includesPathBoundary(commandLine, normalizedPath)
)
return commandMatch ? toOwner(commandMatch.worktree, 'command') : undefined
}
@ -220,6 +239,9 @@ async function readProcNet(
async function mapLinuxInodesToPids(inodes: Set<number>): Promise<Map<number, number>> {
const result = new Map<number, number>()
if (inodes.size === 0) {
return result
}
let pids: string[]
try {
pids = (await readdir('/proc')).filter((entry) => /^\d+$/.test(entry))
@ -393,10 +415,10 @@ async function readTextIfAvailable(filePath: string): Promise<string | undefined
function enrichPort(
port: RawListeningPort,
worktrees: WorkspacePortProbe[],
worktrees: readonly NormalizedWorkspacePortProbe[],
urlWatcher: Pick<AdvertisedUrlWatcher, 'lookup'>
): WorkspacePort {
const owner = attributePortToWorkspace(port, worktrees)
const owner = attributePortToNormalizedWorkspaces(port, worktrees)
const base = {
id: `${port.host}:${port.port}:${port.pid ?? 'unknown'}`,
bindHost: port.host,
@ -428,15 +450,15 @@ function enrichPort(
function reconcileAdvertisedUrls(
ports: RawListeningPort[],
worktrees: WorkspacePortProbe[],
worktrees: readonly NormalizedWorkspacePortProbe[],
urlWatcher: Pick<AdvertisedUrlWatcher, 'reconcileScan'>
): void {
const observationsByWorktree = new Map<string, { port: number; pid?: number }[]>()
for (const worktree of worktrees) {
observationsByWorktree.set(worktree.id, [])
observationsByWorktree.set(worktree.worktree.id, [])
}
for (const port of ports) {
const owner = attributePortToWorkspace(port, worktrees)
const owner = attributePortToNormalizedWorkspaces(port, worktrees)
if (!owner) {
continue
}
@ -485,8 +507,20 @@ function toOwner(
}
}
function pickDeepestMatch<T extends { normalizedPath: string }>(matches: T[]): T | undefined {
return matches.sort((a, b) => b.normalizedPath.length - a.normalizedPath.length)[0]
function pickDeepestMatching<T extends { normalizedPath: string }>(
candidates: readonly T[],
predicate: (candidate: T) => boolean
): T | undefined {
let best: T | undefined
for (const candidate of candidates) {
if (!predicate(candidate)) {
continue
}
if (!best || candidate.normalizedPath.length > best.normalizedPath.length) {
best = candidate
}
}
return best
}
function isSameOrDescendant(candidate: string, parent: string): boolean {