Skip to content

Commit 144e96b

Browse files
authored
test(runtime-metrics): validate CPU percentage against process usage (#9289)
Hardcoded CPU bounds fail when scheduler contention shifts system time, while wide static tolerances can hide conversion errors. Bracket the collector's reads with independent process CPU and monotonic-clock samples to keep the real native and fallback paths while deriving the accepted range from the observed interval.
1 parent 8339a6f commit 144e96b

1 file changed

Lines changed: 53 additions & 65 deletions

File tree

packages/dd-trace/test/runtime_metrics.spec.js

Lines changed: 53 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -625,81 +625,69 @@ NATIVE_METRICS_VARIANTS.forEach((nativeMetrics) => {
625625
})
626626

627627
describe('CPU Usage Calculations', () => {
628-
it('should report CPU percentages within valid ranges', () => {
629-
const startCpuUsage = process.cpuUsage()
630-
const startTime = Date.now()
631-
const startPerformanceNow = performance.now()
628+
it('should report CPU percentages matching real process usage', () => {
629+
const outerStartCpuUsage = process.cpuUsage()
630+
const outerStartTime = performance.now()
631+
clock.tick(10000)
632+
client.gauge.resetHistory()
633+
const innerStartTime = performance.now()
634+
const innerStartCpuUsage = process.cpuUsage()
635+
632636
let iterations = 0
633-
let ticks = 0
634-
while (Date.now() - startTime < 100) {
635-
iterations++
636-
if (iterations % 1000000 === 0) {
637-
clock.tick(1)
638-
ticks++
637+
let userCpuUsage = 0
638+
while (userCpuUsage < 100_000) {
639+
if (++iterations % 1_000_000 === 0) {
640+
userCpuUsage = process.cpuUsage(innerStartCpuUsage).user
639641
}
640642
}
641-
const cpuUsage = process.cpuUsage()
642-
const cpuUsageStub = sinon.stub(process, 'cpuUsage').returns(cpuUsage)
643-
const performanceNowStub = sinon.stub(performance, 'now').returns(startPerformanceNow + 10000)
644-
clock.tick(10000 - ticks)
645-
performanceNowStub.restore()
646-
cpuUsageStub.restore()
647643

648-
const timeDivisor = 100_000 // Microseconds * 100 for percent
644+
const innerEndCpuUsage = process.cpuUsage()
645+
const innerEndTime = performance.now()
646+
clock.tick(10000)
647+
const outerEndTime = performance.now()
648+
const outerEndCpuUsage = process.cpuUsage()
649649

650-
const cpuMetrics = new Map([[
651-
'runtime.node.cpu.user',
652-
Number(((cpuUsage.user - startCpuUsage.user) / timeDivisor).toFixed(2)),
653-
], [
650+
const cpuCalls = client.gauge.getCalls().filter(call => call.args[0].startsWith('runtime.node.cpu.'))
651+
const cpuMetrics = new Map(cpuCalls.map(call => [call.args[0], call.args[1]]))
652+
assert.deepStrictEqual([...cpuMetrics.keys()].sort(), [
654653
'runtime.node.cpu.system',
655-
Number(((cpuUsage.system - startCpuUsage.system) / timeDivisor).toFixed(2)),
656-
], [
657654
'runtime.node.cpu.total',
658-
Number((
659-
((cpuUsage.user - startCpuUsage.user) + (cpuUsage.system - startCpuUsage.system)) / timeDivisor
660-
).toFixed(2)),
661-
]])
662-
663-
let userPercent = 0
664-
let systemPercent = 0
665-
let totalPercent = 0
666-
667-
for (const call of client.gauge.getCalls()) {
668-
const metric = call.args[0]
669-
const expected = cpuMetrics.get(metric)
670-
cpuMetrics.delete(metric)
671-
if (expected !== undefined) {
672-
const stringValue = call.args[1]
673-
assert.match(stringValue, /^\d+(\.\d{1,2})?$/)
674-
const number = Number(stringValue)
675-
if (metric === 'runtime.node.cpu.user') {
676-
assert(
677-
number >= 1,
678-
`${metric} sanity check failed (increase CPU load above with more ticks): ${number}`
679-
)
680-
userPercent = number
681-
}
682-
if (metric === 'runtime.node.cpu.system') {
683-
assert(number >= 0 && number <= 5, `${metric} sanity check failed: ${number}`)
684-
systemPercent = number
685-
}
686-
if (metric === 'runtime.node.cpu.total') {
687-
assert(
688-
// Subtracting 0.1 for time-window/baseline alignment numbers and due to rounding issues.
689-
number >= expected - 0.1 && number <= expected + 1,
690-
`${metric} sanity check failed (increase CPU load above with more ticks): ${number} ${expected}`
691-
)
692-
totalPercent = number
693-
}
694-
const epsilon = os.platform() === 'win32' ? 1.5 : 0.5
695-
assert(number - expected < epsilon, `${metric} sanity check failed: ${number} ${expected}`)
696-
}
655+
'runtime.node.cpu.user',
656+
])
657+
assert.strictEqual(cpuCalls.length, cpuMetrics.size, 'CPU metrics should be reported exactly once')
658+
for (const value of cpuMetrics.values()) {
659+
assert.match(value, /^\d+\.\d{2}$/)
697660
}
698661

699-
assert.strictEqual(cpuMetrics.size, 0, `All CPU metrics should be matched, missing ${[...cpuMetrics.keys()]}`)
700-
662+
const userPercent = Number(cpuMetrics.get('runtime.node.cpu.user'))
663+
const systemPercent = Number(cpuMetrics.get('runtime.node.cpu.system'))
664+
const totalPercent = Number(cpuMetrics.get('runtime.node.cpu.total'))
701665
const totalDiff = Math.abs(totalPercent - userPercent - systemPercent)
702-
assert(totalDiff <= 0.03, `Total CPU percentage sanity check failed: ${totalDiff} > 0.03`)
666+
assert(totalDiff <= 0.02, `Total CPU percentage sanity check failed: ${totalDiff} > 0.02`)
667+
668+
// The collector reads its counters between the outer and inner samples on each side.
669+
const minimumElapsedTime = innerEndTime - innerStartTime
670+
const maximumElapsedTime = outerEndTime - outerStartTime
671+
const minimumUserPercent = (innerEndCpuUsage.user - innerStartCpuUsage.user) / (maximumElapsedTime * 10)
672+
const maximumUserPercent = (outerEndCpuUsage.user - outerStartCpuUsage.user) / (minimumElapsedTime * 10)
673+
const minimumSystemPercent = (innerEndCpuUsage.system - innerStartCpuUsage.system) / (maximumElapsedTime * 10)
674+
const maximumSystemPercent = (outerEndCpuUsage.system - outerStartCpuUsage.system) / (minimumElapsedTime * 10)
675+
const minimumTotalPercent = minimumUserPercent + minimumSystemPercent
676+
const maximumTotalPercent = maximumUserPercent + maximumSystemPercent
677+
678+
assert(
679+
userPercent >= minimumUserPercent - 0.01 && userPercent <= maximumUserPercent + 0.01,
680+
`Expected real user CPU percentage ${minimumUserPercent} <= ${userPercent} <= ${maximumUserPercent}`
681+
)
682+
assert(
683+
systemPercent >= minimumSystemPercent - 0.01 && systemPercent <= maximumSystemPercent + 0.01,
684+
`Expected real system CPU percentage ${minimumSystemPercent} <= ${systemPercent} <= ${maximumSystemPercent}`
685+
)
686+
687+
assert(
688+
totalPercent >= minimumTotalPercent - 0.01 && totalPercent <= maximumTotalPercent + 0.01,
689+
`Expected real total CPU percentage ${minimumTotalPercent} <= ${totalPercent} <= ${maximumTotalPercent}`
690+
)
703691
})
704692
})
705693

0 commit comments

Comments
 (0)