{"version":3,"sources":["../src/cpu.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { existsSync, readFileSync } from 'node:fs';\nimport os from 'node:os';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport { CGroupV1CpuMonitor, CGroupV2CpuMonitor, DefaultCpuMonitor, getCpuMonitor } from './cpu.js';\n\nvi.mock('node:fs', () => ({\n  existsSync: vi.fn(() => false),\n  readFileSync: vi.fn(() => ''),\n}));\n\nconst mockExistsSync = vi.mocked(existsSync);\nconst mockReadFileSync = vi.mocked(readFileSync);\n\ndescribe('cpu', () => {\n  beforeEach(() => {\n    vi.clearAllMocks();\n    delete process.env.NUM_CPUS;\n  });\n\n  afterEach(() => {\n    delete process.env.NUM_CPUS;\n  });\n\n  describe('getCpuMonitor', () => {\n    it('returns CGroupV2CpuMonitor when /sys/fs/cgroup/cpu.stat exists', () => {\n      mockExistsSync.mockImplementation((p) => p === '/sys/fs/cgroup/cpu.stat');\n      const monitor = getCpuMonitor();\n      expect(monitor).toBeInstanceOf(CGroupV2CpuMonitor);\n    });\n\n    it('returns CGroupV1CpuMonitor when cgroup v1 paths exist', () => {\n      mockExistsSync.mockImplementation((p) => p === '/sys/fs/cgroup/cpuacct/cpuacct.usage');\n      const monitor = getCpuMonitor();\n      expect(monitor).toBeInstanceOf(CGroupV1CpuMonitor);\n    });\n\n    it('returns DefaultCpuMonitor when no cgroup paths exist', () => {\n      mockExistsSync.mockReturnValue(false);\n      const monitor = getCpuMonitor();\n      expect(monitor).toBeInstanceOf(DefaultCpuMonitor);\n    });\n  });\n\n  describe('DefaultCpuMonitor', () => {\n    it('returns os.cpus().length for cpuCount', () => {\n      const monitor = new DefaultCpuMonitor();\n      expect(monitor.cpuCount()).toBe(os.cpus().length);\n    });\n\n    it('respects NUM_CPUS env var', () => {\n      process.env.NUM_CPUS = '4.5';\n      const monitor = new DefaultCpuMonitor();\n      expect(monitor.cpuCount()).toBe(4.5);\n    });\n\n    it('ignores invalid NUM_CPUS', () => {\n      process.env.NUM_CPUS = 'notanumber';\n      const monitor = new DefaultCpuMonitor();\n      expect(monitor.cpuCount()).toBe(os.cpus().length);\n    });\n\n    it('cpuPercent returns value in [0, 1]', async () => {\n      const monitor = new DefaultCpuMonitor();\n      const result = await monitor.cpuPercent(50);\n      expect(result).toBeGreaterThanOrEqual(0);\n      expect(result).toBeLessThanOrEqual(1);\n    }, 10_000);\n  });\n\n  describe('CGroupV2CpuMonitor', () => {\n    it('returns quota/period for cpuCount', () => {\n      mockReadFileSync.mockImplementation((p) => {\n        if (String(p) === '/sys/fs/cgroup/cpu.max') return '200000 100000';\n        return '';\n      });\n      const monitor = new CGroupV2CpuMonitor();\n      expect(monitor.cpuCount()).toBe(2);\n    });\n\n    it('falls back to os.cpus().length when quota is max', () => {\n      mockReadFileSync.mockImplementation((p) => {\n        if (String(p) === '/sys/fs/cgroup/cpu.max') return 'max 100000';\n        return '';\n      });\n      const monitor = new CGroupV2CpuMonitor();\n      expect(monitor.cpuCount()).toBe(os.cpus().length);\n    });\n\n    it('handles missing cpu.max gracefully', () => {\n      mockReadFileSync.mockImplementation(() => {\n        throw new Error('ENOENT');\n      });\n      const monitor = new CGroupV2CpuMonitor();\n      expect(monitor.cpuCount()).toBe(os.cpus().length);\n    });\n\n    it('respects NUM_CPUS env var', () => {\n      process.env.NUM_CPUS = '3';\n      const monitor = new CGroupV2CpuMonitor();\n      expect(monitor.cpuCount()).toBe(3);\n    });\n\n    it('cpuPercent computes correct value from usage_usec deltas', async () => {\n      let callCount = 0;\n      mockReadFileSync.mockImplementation((p) => {\n        if (String(p) === '/sys/fs/cgroup/cpu.stat') {\n          callCount++;\n          // Two reads: 1,000,000 usec apart => 1s of CPU usage over the interval\n          return callCount <= 1\n            ? 'usage_usec 1000000\\nuser_usec 800000\\nsystem_usec 200000'\n            : 'usage_usec 2000000\\nuser_usec 1600000\\nsystem_usec 400000';\n        }\n        if (String(p) === '/sys/fs/cgroup/cpu.max') return '200000 100000';\n        return '';\n      });\n      const monitor = new CGroupV2CpuMonitor();\n      // interval=100ms, 2 cpus, 1s of usage => 1/(0.1*2) = 5, clamped to 1\n      const result = await monitor.cpuPercent(100);\n      expect(result).toBe(1);\n    }, 10_000);\n\n    it('cpuPercent returns fractional load', async () => {\n      let callCount = 0;\n      mockReadFileSync.mockImplementation((p) => {\n        if (String(p) === '/sys/fs/cgroup/cpu.stat') {\n          callCount++;\n          // 50,000 usec delta => 0.05s of CPU over 0.1s on 2 cpus => 0.05/(0.1*2) = 0.25\n          return callCount <= 1 ? 'usage_usec 1000000\\n' : 'usage_usec 1050000\\n';\n        }\n        if (String(p) === '/sys/fs/cgroup/cpu.max') return '200000 100000';\n        return '';\n      });\n      const monitor = new CGroupV2CpuMonitor();\n      const result = await monitor.cpuPercent(100);\n      expect(result).toBeCloseTo(0.25, 1);\n    }, 10_000);\n\n    it('throws when usage_usec is missing from cpu.stat', async () => {\n      mockReadFileSync.mockImplementation((p) => {\n        if (String(p) === '/sys/fs/cgroup/cpu.stat') return 'user_usec 800000\\nsystem_usec 200000';\n        return '';\n      });\n      const monitor = new CGroupV2CpuMonitor();\n      await expect(() => monitor.cpuPercent(50)).rejects.toThrow('Failed to read CPU usage');\n    });\n  });\n\n  describe('CGroupV1CpuMonitor', () => {\n    it('returns quota/period for cpuCount', () => {\n      mockReadFileSync.mockImplementation((p) => {\n        if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_quota_us') return '200000';\n        if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_period_us') return '100000';\n        return '';\n      });\n      const monitor = new CGroupV1CpuMonitor();\n      expect(monitor.cpuCount()).toBe(2);\n    });\n\n    it('defaults to 2.0 when quota is -1', () => {\n      mockReadFileSync.mockImplementation((p) => {\n        if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_quota_us') return '-1';\n        if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_period_us') return '100000';\n        return '';\n      });\n      const monitor = new CGroupV1CpuMonitor();\n      expect(monitor.cpuCount()).toBe(2.0);\n    });\n\n    it('defaults to 2.0 when quota file is unreadable', () => {\n      mockReadFileSync.mockImplementation(() => {\n        throw new Error('ENOENT');\n      });\n      const monitor = new CGroupV1CpuMonitor();\n      expect(monitor.cpuCount()).toBe(2.0);\n    });\n\n    it('returns fractional cpuCount for sub-CPU limits', () => {\n      mockReadFileSync.mockImplementation((p) => {\n        if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_quota_us') return '50000';\n        if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_period_us') return '100000';\n        return '';\n      });\n      const monitor = new CGroupV1CpuMonitor();\n      expect(monitor.cpuCount()).toBe(0.5);\n    });\n\n    it('respects NUM_CPUS env var', () => {\n      process.env.NUM_CPUS = '8';\n      const monitor = new CGroupV1CpuMonitor();\n      expect(monitor.cpuCount()).toBe(8);\n    });\n\n    it('cpuPercent computes correct value from nanosecond deltas', async () => {\n      let callCount = 0;\n      mockReadFileSync.mockImplementation((p) => {\n        if (String(p) === '/sys/fs/cgroup/cpuacct/cpuacct.usage') {\n          callCount++;\n          // 100_000_000 ns delta = 0.1s CPU over 0.1s interval on 2 cpus => 0.1/(0.1*2) = 0.5\n          return callCount <= 1 ? '1000000000' : '1100000000';\n        }\n        if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_quota_us') return '200000';\n        if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_period_us') return '100000';\n        return '';\n      });\n      const monitor = new CGroupV1CpuMonitor();\n      const result = await monitor.cpuPercent(100);\n      expect(result).toBeCloseTo(0.5, 1);\n    }, 10_000);\n\n    it('clamps cpuPercent output to [0, 1]', async () => {\n      let callCount = 0;\n      mockReadFileSync.mockImplementation((p) => {\n        if (String(p) === '/sys/fs/cgroup/cpuacct/cpuacct.usage') {\n          callCount++;\n          // Huge delta => would exceed 1.0 without clamping\n          return callCount <= 1 ? '0' : '10000000000';\n        }\n        if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_quota_us') return '100000';\n        if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_period_us') return '100000';\n        return '';\n      });\n      const monitor = new CGroupV1CpuMonitor();\n      const result = await monitor.cpuPercent(100);\n      expect(result).toBeLessThanOrEqual(1);\n      expect(result).toBeGreaterThanOrEqual(0);\n    }, 10_000);\n\n    it('throws when cpuacct.usage is unreadable', async () => {\n      mockReadFileSync.mockImplementation(() => {\n        throw new Error('ENOENT');\n      });\n      const monitor = new CGroupV1CpuMonitor();\n      await expect(() => monitor.cpuPercent(50)).rejects.toThrow('Failed to read cpuacct.usage');\n    });\n  });\n});\n"],"mappings":"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","names":["os"]}