mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* 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>
106 lines
3.6 KiB
JavaScript
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')
|
|
})
|
|
})
|