fix: add missing CPU guest metrics to CPU responses (#1644)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- New Features
- CPU load metrics now include guest runtime and hypervisor steal time
percentages, exposed as additional fields in CPU load responses
(per‑CPU).
- Tests
- Added comprehensive unit tests for CPU info and load generation,
including edge cases and validation of the new guest and steal metrics.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Eli Bosley
2025-09-03 11:06:51 -04:00
committed by GitHub
parent c42f79d406
commit 99dbad57d5
4 changed files with 262 additions and 0 deletions

View File

@@ -27,6 +27,16 @@ export class CpuLoad {
description: 'The percentage of time the CPU spent servicing hardware interrupts.', description: 'The percentage of time the CPU spent servicing hardware interrupts.',
}) })
percentIrq!: number; percentIrq!: number;
@Field(() => Float, {
description: 'The percentage of time the CPU spent running virtual machines (guest).',
})
percentGuest!: number;
@Field(() => Float, {
description: 'The percentage of CPU time stolen by the hypervisor.',
})
percentSteal!: number;
} }
@ObjectType({ implements: () => Node }) @ObjectType({ implements: () => Node })

View File

@@ -0,0 +1,246 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CpuService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.service.js';
vi.mock('systeminformation', () => ({
cpu: vi.fn().mockResolvedValue({
manufacturer: 'Intel',
brand: 'Core i7-9700K',
vendor: 'Intel',
family: '6',
model: '158',
stepping: '12',
revision: '',
voltage: '1.2V',
speed: 3.6,
speedMin: 800,
speedMax: 4900,
cores: 16,
physicalCores: 8,
processors: 1,
socket: 'LGA1151',
cache: {
l1d: 32768,
l1i: 32768,
l2: 262144,
l3: 12582912,
},
}),
cpuFlags: vi.fn().mockResolvedValue('fpu vme de pse tsc msr pae mce cx8'),
currentLoad: vi.fn().mockResolvedValue({
avgLoad: 2.5,
currentLoad: 25.5,
currentLoadUser: 15.0,
currentLoadSystem: 8.0,
currentLoadNice: 0.5,
currentLoadIdle: 74.5,
currentLoadIrq: 1.0,
currentLoadSteal: 0.2,
currentLoadGuest: 0.3,
rawCurrentLoad: 25500,
rawCurrentLoadUser: 15000,
rawCurrentLoadSystem: 8000,
rawCurrentLoadNice: 500,
rawCurrentLoadIdle: 74500,
rawCurrentLoadIrq: 1000,
rawCurrentLoadSteal: 200,
rawCurrentLoadGuest: 300,
cpus: [
{
load: 30.0,
loadUser: 20.0,
loadSystem: 10.0,
loadNice: 0,
loadIdle: 70.0,
loadIrq: 0,
loadSteal: 0,
loadGuest: 0,
rawLoad: 30000,
rawLoadUser: 20000,
rawLoadSystem: 10000,
rawLoadNice: 0,
rawLoadIdle: 70000,
rawLoadIrq: 0,
rawLoadSteal: 0,
rawLoadGuest: 0,
},
{
load: 21.0,
loadUser: 15.0,
loadSystem: 6.0,
loadNice: 0,
loadIdle: 79.0,
loadIrq: 0,
loadSteal: 0,
loadGuest: 0,
rawLoad: 21000,
rawLoadUser: 15000,
rawLoadSystem: 6000,
rawLoadNice: 0,
rawLoadIdle: 79000,
rawLoadIrq: 0,
rawLoadSteal: 0,
rawLoadGuest: 0,
},
],
}),
}));
describe('CpuService', () => {
let service: CpuService;
beforeEach(() => {
service = new CpuService();
});
describe('generateCpu', () => {
it('should return CPU information with correct structure', async () => {
const result = await service.generateCpu();
expect(result).toEqual({
id: 'info/cpu',
manufacturer: 'Intel',
brand: 'Core i7-9700K',
vendor: 'Intel',
family: '6',
model: '158',
stepping: 12,
revision: '',
voltage: '1.2V',
speed: 3.6,
speedmin: 800,
speedmax: 4900,
cores: 8,
threads: 16,
processors: 1,
socket: 'LGA1151',
cache: {
l1d: 32768,
l1i: 32768,
l2: 262144,
l3: 12582912,
},
flags: ['fpu', 'vme', 'de', 'pse', 'tsc', 'msr', 'pae', 'mce', 'cx8'],
});
});
it('should handle missing speed values', async () => {
const { cpu } = await import('systeminformation');
vi.mocked(cpu).mockResolvedValueOnce({
manufacturer: 'Intel',
brand: 'Core i7-9700K',
vendor: 'Intel',
family: '6',
model: '158',
stepping: '12',
revision: '',
voltage: '1.2V',
speed: 3.6,
cores: 16,
physicalCores: 8,
processors: 1,
socket: 'LGA1151',
cache: { l1d: 32768, l1i: 32768, l2: 262144, l3: 12582912 },
} as any);
const result = await service.generateCpu();
expect(result.speedmin).toBe(-1);
expect(result.speedmax).toBe(-1);
});
it('should handle cpuFlags error gracefully', async () => {
const { cpuFlags } = await import('systeminformation');
vi.mocked(cpuFlags).mockRejectedValueOnce(new Error('flags error'));
const result = await service.generateCpu();
expect(result.flags).toEqual([]);
});
});
describe('generateCpuLoad', () => {
it('should return CPU utilization with all load metrics', async () => {
const result = await service.generateCpuLoad();
expect(result).toEqual({
id: 'info/cpu-load',
percentTotal: 25.5,
cpus: [
{
percentTotal: 30.0,
percentUser: 20.0,
percentSystem: 10.0,
percentNice: 0,
percentIdle: 70.0,
percentIrq: 0,
percentGuest: 0,
percentSteal: 0,
},
{
percentTotal: 21.0,
percentUser: 15.0,
percentSystem: 6.0,
percentNice: 0,
percentIdle: 79.0,
percentIrq: 0,
percentGuest: 0,
percentSteal: 0,
},
],
});
});
it('should include guest and steal metrics when present', async () => {
const { currentLoad } = await import('systeminformation');
vi.mocked(currentLoad).mockResolvedValueOnce({
avgLoad: 2.5,
currentLoad: 25.5,
currentLoadUser: 15.0,
currentLoadSystem: 8.0,
currentLoadNice: 0.5,
currentLoadIdle: 74.5,
currentLoadIrq: 1.0,
currentLoadSteal: 0.2,
currentLoadGuest: 0.3,
rawCurrentLoad: 25500,
rawCurrentLoadUser: 15000,
rawCurrentLoadSystem: 8000,
rawCurrentLoadNice: 500,
rawCurrentLoadIdle: 74500,
rawCurrentLoadIrq: 1000,
rawCurrentLoadSteal: 200,
rawCurrentLoadGuest: 300,
cpus: [
{
load: 30.0,
loadUser: 20.0,
loadSystem: 10.0,
loadNice: 0,
loadIdle: 70.0,
loadIrq: 0,
loadGuest: 2.5,
loadSteal: 1.2,
rawLoad: 30000,
rawLoadUser: 20000,
rawLoadSystem: 10000,
rawLoadNice: 0,
rawLoadIdle: 70000,
rawLoadIrq: 0,
rawLoadGuest: 2500,
rawLoadSteal: 1200,
},
],
});
const result = await service.generateCpuLoad();
expect(result.cpus[0]).toEqual(
expect.objectContaining({
percentGuest: 2.5,
percentSteal: 1.2,
})
);
});
});
});

View File

@@ -37,6 +37,8 @@ export class CpuService {
percentNice: cpu.loadNice, percentNice: cpu.loadNice,
percentIdle: cpu.loadIdle, percentIdle: cpu.loadIdle,
percentIrq: cpu.loadIrq, percentIrq: cpu.loadIrq,
percentGuest: cpu.loadGuest || 0,
percentSteal: cpu.loadSteal || 0,
})), })),
}; };
} }

View File

@@ -32,6 +32,8 @@ describe('MetricsResolver', () => {
loadNice: 0, loadNice: 0,
loadIdle: 70.0, loadIdle: 70.0,
loadIrq: 0, loadIrq: 0,
loadGuest: 0,
loadSteal: 0,
}, },
{ {
load: 21.0, load: 21.0,
@@ -40,6 +42,8 @@ describe('MetricsResolver', () => {
loadNice: 0, loadNice: 0,
loadIdle: 79.0, loadIdle: 79.0,
loadIrq: 0, loadIrq: 0,
loadGuest: 0,
loadSteal: 0,
}, },
], ],
}), }),