mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* fix(bench): make the idle-CPU benchmark run and report CPU on Windows Three independent Windows defects in the idle-CPU benchmark (its report is what Windows performance work would cite): - `buildAppIfNeeded` spawned `npx`, which is `npx.cmd` on Windows, so execFileSync failed with ENOENT before any sampling. Build through `config/scripts/run-electron-vite-build.mjs` instead — the repo's own Windows-safe wrapper, which also runs the boot-graph verification. - `readWindowsProcesses` returned `cpuTimeSeconds: null`, so the sampler's per-process CPU delta (idle-cpu-process-sampling.mjs:198) fell back to the hardcoded `percentCpu: 0` and every CPU KPI in the report was zero. Read `UserModeTime`/`KernelModeTime` from `Win32_Process` (100 ns units) and keep `null` when a host does not return them. Measured on Windows, same options before and after the CPU fix: | class | mean CPU before | mean CPU after | | --- | ---: | ---: | | renderer | 0 | 2.20 | | gpu | 0 | 2.08 | | utility | 0 | 0.11 | | agent-or-node | 0 | 0.06 | | total | 0 | 4.51 (p95 8.40) | RSS is unchanged (934 MiB -> 937 MiB), so only the CPU reporting changed. Refs #25235 * fix(bench): reject a missing Win32_Process time instead of reading it as zero Review follow-up (CodeRabbit). `Number(null)` is `0`, which is also a real counter value, so an absent `UserModeTime`/`KernelModeTime` passed the finite check and the sampler would report a kernel-only delta instead of taking the fallback. Require an actual number from the CIM payload. Verified on Windows that the real payload still reports CPU once the check is tight: total mean 4.40%, p95 14.48%, renderer 3.51%. * test(bench): cover the Windows CIM CPU time mapping The mapping was only reachable through Get-CimInstance, so no test ran it. Lift it into cimCpuSeconds and assert the 100ns-to-seconds sum plus the missing / string / non-finite cases that must stay null. * fix(bench): read the Windows process table as UTF-8 so JSON.parse survives PowerShell writes to a redirected stdout in the OEM code page (437 here), which best-fits characters such as U+2022 and U+2192 in any running process's command line to raw 0x07 / 0x1A bytes. ConvertTo-Json had left them unescaped because they are not control characters in UTF-16, so the sampler's JSON.parse failed with "Bad control character in string literal" whenever such a process happened to be alive -- hence intermittent. Set [Console]::OutputEncoding to UTF-8, as the app's own CIM scan does, and cover it with a Windows-only test that fails without the fix. Fixes #25236 * fix(bench): pass the local runtime to fetchRepos in the idle-CPU fixture fetchRepos now requires a runtimeEnvironmentId, so the fixture's bare call threw "Cannot read properties of undefined (reading 'runtimeEnvironmentId')" before sampling on every platform. null selects this app's own catalog, as the e2e helpers already do. * ci(bench): run the Windows process-table test in the Windows lane The UTF-8 regression test self-skips off win32, so it must be registered in both Windows lists or it runs on no machine. --------- Co-authored-by: Paolo <paovalletta@hotmail.it> Co-authored-by: Orca Worker <orca-worker@localhost>
104 lines
3.4 KiB
JavaScript
104 lines
3.4 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', () => {
|
|
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', 'setTimeout(() => {}, 30_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()
|
|
}
|
|
}, 30_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')
|
|
})
|
|
})
|