Files
orca/config/scripts/idle-cpu-process-sampling.test.mjs
T
NeilandClaude cc0217fe96 test(ci): bound the Windows OEM process-read test's cold PowerShell start (#28564)
* test(ci): give the Windows OEM process-read test the PowerShell cold-start watchdog

The runner's first Windows PowerShell 5.1 start has reached 48 s on packaged CI; the
test's 30 s cap failed package (windows) 7 times in ~1,000 runs. Match the 75 s
watchdog nsis-process-check already uses for the same cold start.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(ci): keep the OEM marker alive past a cold read, and bound the PowerShell read

A blocking spawnSync can't be interrupted by the test timeout, and the marker child
exited at 30 s, so a 48 s cold read could miss it. The reader now has its own 60 s bound.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(bench): keep idle-CPU cleanup running when the final process read fails

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(bench): kill the app tree when the final process read fails

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-14 08:13:53 -07:00

106 lines
3.6 KiB
JavaScript

import { spawn } from 'node:child_process'
import { describe, expect, it } from 'vitest'
import {
cimCpuSeconds,
readProcessRows,
sampleProcessTreeUntilWorkloadsComplete
} from './idle-cpu-process-sampling.mjs'
function createClock(workloadCompletesAt) {
let currentMs = 0
let resolveWorkload
const workloadPromise = new Promise((resolve) => {
resolveWorkload = resolve
})
const wait = async (durationMs) => {
currentMs += durationMs
if (currentMs >= workloadCompletesAt) {
resolveWorkload('complete')
}
await Promise.resolve()
}
const readRows = () => [
{
pid: 10,
ppid: 0,
percentCpu: 0,
rssBytes: 1_024,
cpuTimeSeconds: currentMs / 2_000,
command: 'electron'
}
]
return { now: () => currentMs, readRows, wait, workloadPromise }
}
describe('Windows CIM CPU time mapping', () => {
it('converts the 100ns user and kernel times to a summed second count', () => {
// 10_000_000 and 25_000_000 units of 100ns are 1s and 2.5s.
expect(cimCpuSeconds({ UserModeTime: 10_000_000, KernelModeTime: 25_000_000 })).toBe(3.5)
})
it('maps a missing or non-numeric time to null instead of a kernel-only reading', () => {
expect(cimCpuSeconds({ UserModeTime: null, KernelModeTime: 10_000_000 })).toBeNull()
expect(cimCpuSeconds({ KernelModeTime: 10_000_000 })).toBeNull()
expect(cimCpuSeconds({ UserModeTime: '10000000', KernelModeTime: 10_000_000 })).toBeNull()
expect(cimCpuSeconds({ UserModeTime: Number.NaN, KernelModeTime: 10_000_000 })).toBeNull()
expect(
cimCpuSeconds({ UserModeTime: Number.POSITIVE_INFINITY, KernelModeTime: 10_000_000 })
).toBeNull()
})
})
describe.skipIf(process.platform !== 'win32')('Windows process table read', () => {
// Not a latency budget: this is often the runner's first Windows PowerShell 5.1 start, which
// takes 10-19 s on packaged CI and has reached 48 s. Above the reader's own 60 s bound.
it('parses a command line whose characters the OEM code page maps to control bytes', () => {
const marker = 'idle-cpu-oem • → ♪'
const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 60_000)', marker], {
stdio: 'ignore'
})
try {
const row = readProcessRows().find((candidate) => candidate.pid === child.pid)
expect(row?.command).toContain(marker)
expect(row?.cpuTimeSeconds).toEqual(expect.any(Number))
} finally {
child.kill()
}
}, 75_000)
})
describe('idle CPU process sampling window', () => {
it('extends through a slow workload and captures a final CPU delta', async () => {
const clock = createClock(40)
const result = await sampleProcessTreeUntilWorkloadsComplete({
rootPid: 10,
requestedDurationMs: 20,
intervalMs: 10,
maxWorkloadOverrunMs: 100,
...clock
})
expect(result.workloadResult).toBe('complete')
expect(result.samples.map((sample) => sample.at)).toEqual([10, 20, 30, 40])
expect(result.samplingWindow).toEqual({
requestedDurationMs: 20,
measuredDurationMs: 40,
maxWorkloadOverrunMs: 100,
extendedForWorkload: true,
workloadSettledElapsedMs: 40,
workloadSettledBeforeStop: true
})
})
it('invalidates a run that reaches the workload overrun guard', async () => {
const clock = createClock(Infinity)
await expect(
sampleProcessTreeUntilWorkloadsComplete({
rootPid: 10,
requestedDurationMs: 20,
intervalMs: 10,
maxWorkloadOverrunMs: 20,
...clock
})
).rejects.toThrow('exceeded the 20ms sampling overrun limit')
})
})