diff --git a/api/src/unraid-api/graph/resolvers/info/cpu/cpu.model.ts b/api/src/unraid-api/graph/resolvers/info/cpu/cpu.model.ts index 6a03d1002..cb3414d6a 100644 --- a/api/src/unraid-api/graph/resolvers/info/cpu/cpu.model.ts +++ b/api/src/unraid-api/graph/resolvers/info/cpu/cpu.model.ts @@ -27,6 +27,16 @@ export class CpuLoad { description: 'The percentage of time the CPU spent servicing hardware interrupts.', }) 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 }) diff --git a/api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts b/api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts new file mode 100644 index 000000000..e2cc3d352 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts @@ -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, + }) + ); + }); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts b/api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts index 0ae43debe..e6095b06d 100644 --- a/api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts +++ b/api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts @@ -37,6 +37,8 @@ export class CpuService { percentNice: cpu.loadNice, percentIdle: cpu.loadIdle, percentIrq: cpu.loadIrq, + percentGuest: cpu.loadGuest || 0, + percentSteal: cpu.loadSteal || 0, })), }; } diff --git a/api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.spec.ts index af674c24e..f757eb5c5 100644 --- a/api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.spec.ts +++ b/api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.spec.ts @@ -32,6 +32,8 @@ describe('MetricsResolver', () => { loadNice: 0, loadIdle: 70.0, loadIrq: 0, + loadGuest: 0, + loadSteal: 0, }, { load: 21.0, @@ -40,6 +42,8 @@ describe('MetricsResolver', () => { loadNice: 0, loadIdle: 79.0, loadIrq: 0, + loadGuest: 0, + loadSteal: 0, }, ], }),