Files
orca/src/main/memory/windows-process-sample-parsing.test.ts
T
Neil 64c992cd56 fix(memory): report the Windows number that predicts paging, not just resident pages (#16211) (#16589)
* fix(memory): report Windows commit charge, not just working set (#16211)

On Windows the per-process figure was working set — resident pages only.
An agent whose pages Windows has trimmed to the pagefile shrinks its
working set while still holding the commit that pushes the host into
paging, so Resource Manager and `orca diagnostics memory` understated an
owned tree by 10-40x (9 codex.exe: 1.4 GB working set, 13.4 GB private)
and could not warn before the host was already thrashing.

Add committed private bytes as a second, separately-labelled quantity
rather than redefining the existing one:

- CIM sweep gains one property (PageFileUsage, UInt32 KB); the typeperf
  fallback gains one counter (\Process(*)\Private Bytes). Both ride the
  sweep that already runs.
- MemorySnapshot gains optional `privateMemory` per app/worktree/session
  plus `processCommitMetric` and `totalPrivateMemory`. Rule 1 additive
  optional fields: old clients ignore them, and absence reads as "not
  measured", never as zero — Unix hosts and older hosts send nothing.
- `totalMemory` and `processMemoryMetric` keep their exact meaning, so
  the "shared pages may repeat" copy stays true; the working-set copy now
  also says paged-out memory is not counted.
- Resource Manager shows "Σ Private" beside "Σ WS", and tints the badge
  yellow/red once tracked commit passes 60/80% of physical RAM — the same
  thresholds `usageTextColorClass` already uses for host usage. Tint and
  tooltip only; no toast, and the badge number is unchanged.

The parsers move to windows-process-sample-parsing.ts and the Windows
sweep tests to their own file to stay under max-lines.

Not migrating the collector to windows-process-table.ts: the native
snapshot exposes no commit figure and no CPU times, and truncates
WorkingSetSize through a DWORD. Documented in the enumeration reference.

* fix(memory): derive the typeperf field cap from the counter list

The fallback parser's 8192-field cap was sized for three `\Process(*)`
counters. Adding `Private Bytes` cut the parsable process count from ~2730
to ~2047, and overrun is a blackout (`parseTypeperfCsvLine` returns `[]`, so
the whole sweep reports nothing) rather than a truncation. The counter list
now lives beside the decoder that reads those names back out of the PDH
header, and the cap is derived from it.

Also collapses the four spellings of "omit privateMemory when unmeasured"
in collector.ts onto one `commitField` helper, drops the unread parameter
and the never-rendered `columnLabel` from `getResourceCommitMetricCopy`,
folds `getCommitPressurePercent` into the only function that called it, and
reverts unrelated Prettier churn in the Windows enumeration doc.

The commit tint's doc comment no longer claims to predict host paging: it
measures Orca's own share of physical RAM. Host commit charge / commit
limit stays a follow-up (#16211).
2026-08-26 15:43:02 -07:00

141 lines
6.1 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
async function loadWindowsProcessSampleParsing() {
vi.resetModules()
return await import('./windows-process-sample-parsing')
}
describe('parseWindowsProcessOutput', () => {
it('parses tab-delimited CIM process rows', async () => {
const { parseWindowsProcessOutput } = await loadWindowsProcessSampleParsing()
expect(parseWindowsProcessOutput('100\t1\t2048\r\n200\t100\t1024')).toEqual([
{ pid: 100, ppid: 1, cpu: 0, memory: 2048 },
{ pid: 200, ppid: 100, cpu: 0, memory: 1024 }
])
})
it('skips malformed rows and clamps invalid memory to zero', async () => {
const { parseWindowsProcessOutput } = await loadWindowsProcessSampleParsing()
expect(
parseWindowsProcessOutput(
[
'garbage',
'abc\t1\t100',
'10\txyz\t100',
'0\t0\t100',
'-5\t0\t100',
'30\t-1\t100',
'20\t1\t-50'
].join('\n')
)
).toEqual([{ pid: 20, ppid: 1, cpu: 0, memory: 0 }])
})
it('reads PageFileUsage kilobytes into committed private bytes', async () => {
const { parseWindowsProcessOutput } = await loadWindowsProcessSampleParsing()
// 5,600,000 KB of commit behind a 96 MB working set is the reported shape.
expect(
parseWindowsProcessOutput('100\t1\t100663296\t0\t0\t638830000000000000\t5600000')
).toEqual([{ pid: 100, ppid: 1, cpu: 0, memory: 100663296, privateMemory: 5600000 * 1024 }])
})
it('leaves committed bytes absent when the host omits PageFileUsage', async () => {
const { parseWindowsProcessOutput } = await loadWindowsProcessSampleParsing()
const [row] = parseWindowsProcessOutput('100\t1\t2048\t0\t0\t638830000000000000')
expect(row.privateMemory).toBeUndefined()
expect(parseWindowsProcessOutput('100\t1\t2048\t0\t0\t1\t')[0].privateMemory).toBeUndefined()
})
it('keeps a zero PageFileUsage distinct from an unreported one', async () => {
const { parseWindowsProcessOutput } = await loadWindowsProcessSampleParsing()
expect(parseWindowsProcessOutput('4\t0\t2048\t0\t0\t1\t0')[0].privateMemory).toBe(0)
})
it('preserves empty CIM field positions instead of shifting CPU ticks into memory', async () => {
const { parseWindowsProcessOutput } = await loadWindowsProcessSampleParsing()
expect(parseWindowsProcessOutput('100\t1\t\t200\t300\t638830000000000000')).toEqual([
{ pid: 100, ppid: 1, cpu: 0, memory: 0 }
])
})
})
describe('parseTypeperfProcessOutput', () => {
it('joins PID, parent PID, and working-set counters by process instance', async () => {
const { parseTypeperfProcessOutput } = await loadWindowsProcessSampleParsing()
const stdout = [
'"(PDH-CSV 4.0)","\\\\HOST\\Process(node)\\ID Process","\\\\HOST\\Process(node#1)\\ID Process","\\\\HOST\\Process(node)\\Creating Process ID","\\\\HOST\\Process(node#1)\\Creating Process ID","\\\\HOST\\Process(node)\\Working Set","\\\\HOST\\Process(node#1)\\Working Set"',
'"07/15/2026 01:44:54.514","100.000000","200.000000","1.000000","100.000000","2048.000000","4096.000000"'
].join('\r\n')
expect(parseTypeperfProcessOutput(stdout)).toEqual([
{ pid: 100, ppid: 1, cpu: 0, memory: 2048 },
{ pid: 200, ppid: 100, cpu: 0, memory: 4096 }
])
})
it('joins the Private Bytes counter onto the same process instance', async () => {
const { parseTypeperfProcessOutput } = await loadWindowsProcessSampleParsing()
const stdout = [
'"(PDH-CSV 4.0)","\\\\HOST\\Process(codex)\\ID Process","\\\\HOST\\Process(codex)\\Creating Process ID","\\\\HOST\\Process(codex)\\Working Set","\\\\HOST\\Process(codex)\\Private Bytes"',
'"07/15/2026 01:44:54.514","100.000000","1.000000","100663296.000000","5734400000.000000"'
].join('\r\n')
expect(parseTypeperfProcessOutput(stdout)).toEqual([
{ pid: 100, ppid: 1, cpu: 0, memory: 100663296, privateMemory: 5734400000 }
])
})
it('leaves committed bytes absent when the Private Bytes counter is missing', async () => {
const { parseTypeperfProcessOutput } = await loadWindowsProcessSampleParsing()
const stdout = [
'"(PDH-CSV 4.0)","\\\\HOST\\Process(codex)\\ID Process","\\\\HOST\\Process(codex)\\Creating Process ID","\\\\HOST\\Process(codex)\\Working Set"',
'"time","100.000000","1.000000","2048.000000"'
].join('\r\n')
expect(parseTypeperfProcessOutput(stdout)[0].privateMemory).toBeUndefined()
})
it('still parses a busy host once a fourth counter widens every sample line', async () => {
const { parseTypeperfProcessOutput } = await loadWindowsProcessSampleParsing()
// 2100 processes x 4 counters overruns a fixed 8192-field cap; the reported
// MCP fan-out host runs well past 2048 processes.
const instanceCount = 2100
const headers = ['"(PDH-CSV 4.0)"']
const values = ['"time"']
for (let index = 0; index < instanceCount; index += 1) {
for (const counter of ['ID Process', 'Creating Process ID', 'Working Set', 'Private Bytes']) {
headers.push(`"\\\\HOST\\Process(node#${index})\\${counter}"`)
}
values.push(`"${1000 + index}"`, '"1"', '"2048"', '"4096"')
}
const stdout = [headers.join(','), values.join(',')].join('\r\n')
const rows = parseTypeperfProcessOutput(stdout)
expect(rows).toHaveLength(instanceCount)
expect(rows[instanceCount - 1]).toEqual({
pid: 1000 + instanceCount - 1,
ppid: 1,
cpu: 0,
memory: 2048,
privateMemory: 4096
})
})
it('ignores aggregate and incomplete rows and clamps invalid memory', async () => {
const { parseTypeperfProcessOutput } = await loadWindowsProcessSampleParsing()
const stdout = [
'"(PDH-CSV 4.0)","\\\\HOST\\Process(_Total)\\ID Process","\\\\HOST\\Process(cmd)\\ID Process","\\\\HOST\\Process(orphan)\\ID Process","\\\\HOST\\Process(_Total)\\Creating Process ID","\\\\HOST\\Process(cmd)\\Creating Process ID","\\\\HOST\\Process(_Total)\\Working Set","\\\\HOST\\Process(cmd)\\Working Set"',
'"time","0.000000","100.000000","200.000000","0.000000","1.000000","999999.000000","-1.000000"'
].join('\r\n')
expect(parseTypeperfProcessOutput(stdout)).toEqual([{ pid: 100, ppid: 1, cpu: 0, memory: 0 }])
})
})