mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
getProcessTableSnapshot deduped the ps fork (#6288/#6667) but cached only the raw stdout string on POSIX, so every concurrent agent pane re-ran parsePsRows over the identical output within each 500ms TTL window — O(M*P) redundant tokenization + row allocation. The Windows reader already caches parsed rows; this makes the POSIX default reader do the same by parsing inside the deduped scan and returning ProcessTableRow[]. Collapses the duplicate parsePsRows in the main and relay foreground resolvers into one shared parseProcessTableRows. Co-authored-by: Orca <help@stably.ai>
166 lines
5.1 KiB
TypeScript
166 lines
5.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { createProcessTableSnapshotReader, parseProcessTableRows } from './process-table-snapshot'
|
|
|
|
function deferred<T>(): {
|
|
promise: Promise<T>
|
|
resolve: (v: T) => void
|
|
reject: (e: unknown) => void
|
|
} {
|
|
let resolve!: (v: T) => void
|
|
let reject!: (e: unknown) => void
|
|
const promise = new Promise<T>((res, rej) => {
|
|
resolve = res
|
|
reject = rej
|
|
})
|
|
return { promise, resolve, reject }
|
|
}
|
|
|
|
describe('process-table-snapshot reader', () => {
|
|
it('collapses concurrent calls into a single ps scan', async () => {
|
|
let scans = 0
|
|
const gate = deferred<string>()
|
|
const reader = createProcessTableSnapshotReader({
|
|
runPs: () => {
|
|
scans += 1
|
|
return gate.promise
|
|
},
|
|
now: () => 0
|
|
})
|
|
|
|
const a = reader.getSnapshot()
|
|
const b = reader.getSnapshot()
|
|
const c = reader.getSnapshot()
|
|
gate.resolve('ps-output')
|
|
|
|
expect(await a).toBe('ps-output')
|
|
expect(await b).toBe('ps-output')
|
|
expect(await c).toBe('ps-output')
|
|
// Why: the in-flight promise is shared, so a burst of panes inspecting at
|
|
// once forks `ps` exactly once.
|
|
expect(scans).toBe(1)
|
|
})
|
|
|
|
it('reuses the cached snapshot within the TTL window', async () => {
|
|
let scans = 0
|
|
let clock = 0
|
|
const reader = createProcessTableSnapshotReader({
|
|
runPs: () => {
|
|
scans += 1
|
|
return Promise.resolve(`scan-${scans}`)
|
|
},
|
|
now: () => clock,
|
|
ttlMs: 500
|
|
})
|
|
|
|
expect(await reader.getSnapshot()).toBe('scan-1')
|
|
clock = 499
|
|
expect(await reader.getSnapshot()).toBe('scan-1')
|
|
expect(scans).toBe(1)
|
|
})
|
|
|
|
it('rescans once the TTL expires', async () => {
|
|
let scans = 0
|
|
let clock = 0
|
|
const reader = createProcessTableSnapshotReader({
|
|
runPs: () => {
|
|
scans += 1
|
|
return Promise.resolve(`scan-${scans}`)
|
|
},
|
|
now: () => clock,
|
|
ttlMs: 500
|
|
})
|
|
|
|
expect(await reader.getSnapshot()).toBe('scan-1')
|
|
clock = 500
|
|
expect(await reader.getSnapshot()).toBe('scan-2')
|
|
expect(scans).toBe(2)
|
|
})
|
|
|
|
it('stamps capture time after the scan resolves so a slow ps cannot serve a stale snapshot', async () => {
|
|
let scans = 0
|
|
let clock = 0
|
|
const gate = deferred<string>()
|
|
const reader = createProcessTableSnapshotReader({
|
|
runPs: () => {
|
|
scans += 1
|
|
return scans === 1 ? gate.promise : Promise.resolve(`scan-${scans}`)
|
|
},
|
|
now: () => clock,
|
|
ttlMs: 500
|
|
})
|
|
|
|
const first = reader.getSnapshot()
|
|
// The scan takes 600ms of wall clock to return — longer than the TTL.
|
|
clock = 600
|
|
gate.resolve('scan-1')
|
|
expect(await first).toBe('scan-1')
|
|
|
|
// capturedAt is stamped at now()=600, so a call at 900 is still within TTL.
|
|
clock = 900
|
|
expect(await reader.getSnapshot()).toBe('scan-1')
|
|
expect(scans).toBe(1)
|
|
})
|
|
|
|
it('does not cache failures and retries on the next call', async () => {
|
|
let scans = 0
|
|
const reader = createProcessTableSnapshotReader({
|
|
runPs: () => {
|
|
scans += 1
|
|
if (scans === 1) {
|
|
return Promise.reject(new Error('ps timed out'))
|
|
}
|
|
return Promise.resolve('recovered')
|
|
},
|
|
now: () => 0
|
|
})
|
|
|
|
await expect(reader.getSnapshot()).rejects.toThrow('ps timed out')
|
|
// Why: a transient ps failure must not poison the cache — the next
|
|
// inspection re-scans rather than returning a cached error.
|
|
expect(await reader.getSnapshot()).toBe('recovered')
|
|
expect(scans).toBe(2)
|
|
})
|
|
|
|
it('shares one parsed-rows array across a burst so panes do not each re-parse', async () => {
|
|
// Mirrors the POSIX default reader: runPs parses inside the deduped scan, so
|
|
// every caller in the TTL window gets the SAME ProcessTableRow[] instance
|
|
// instead of re-tokenizing identical stdout per pane.
|
|
let parses = 0
|
|
const gate = deferred<ReturnType<typeof parseProcessTableRows>>()
|
|
const reader = createProcessTableSnapshotReader<ReturnType<typeof parseProcessTableRows>>({
|
|
runPs: () => {
|
|
parses += 1
|
|
return gate.promise
|
|
},
|
|
now: () => 0
|
|
})
|
|
|
|
const a = reader.getSnapshot()
|
|
const b = reader.getSnapshot()
|
|
gate.resolve(parseProcessTableRows('100 1 Ss+ /bin/zsh'))
|
|
|
|
const rowsA = await a
|
|
const rowsB = await b
|
|
expect(parses).toBe(1)
|
|
// Reference identity: the burst reuses one parse, not one-per-caller.
|
|
expect(rowsA).toBe(rowsB)
|
|
})
|
|
})
|
|
|
|
describe('parseProcessTableRows', () => {
|
|
it('parses pid/ppid/stat and keeps the full command (including spaces)', () => {
|
|
const rows = parseProcessTableRows(
|
|
['501 1 S /bin/zsh', '600 501 S+ node /path/bin/codex --flag'].join('\n')
|
|
)
|
|
expect(rows).toEqual([
|
|
{ pid: 501, ppid: 1, stat: 'S', command: '/bin/zsh' },
|
|
{ pid: 600, ppid: 501, stat: 'S+', command: 'node /path/bin/codex --flag' }
|
|
])
|
|
})
|
|
|
|
it('tolerates CRLF and skips header/blank/non-matching lines', () => {
|
|
const rows = parseProcessTableRows(' PID PPID STAT COMMAND\r\n42 1 Ss /sbin/launchd\r\n\r\n')
|
|
expect(rows).toEqual([{ pid: 42, ppid: 1, stat: 'Ss', command: '/sbin/launchd' }])
|
|
})
|
|
})
|