Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export default defineConfig([
autoFix: true,
cspell: {
words: [
'loong',
'riscv',
'evanwashere',
'fastly',
'IsHTMLDDA',
Expand Down
128 changes: 128 additions & 0 deletions src/platform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { GetPlatformMetricsOptions, Machine, OS, PlatformMetrics } from './types.js'
import { runtime as jsRuntime, type JSRuntime } from './utils.js'

const loadNodeOS = async (jsRuntime: JSRuntime, g: typeof globalThis = globalThis) => {
return ['bun', 'deno', 'node'].includes(jsRuntime)
? await import('node:os')
: {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
cpus: typeof g.navigator?.hardwareConcurrency === 'number'
? () => {
return Array
.from(
{ length: (g.navigator as unknown as { hardwareConcurrency: number }).hardwareConcurrency },
() => ({ model: 'unknown', speed: -1 })
)
}
: () => ([]),
freemem: () => -1,
getPriority: () => -1,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/no-deprecated
machine: typeof g.navigator?.platform === 'string'
? () => normalizeMachine(g.navigator.platform.split(' ')[1]) // eslint-disable-line @typescript-eslint/no-deprecated
: () => 'unknown',
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/no-deprecated
platform: typeof g.navigator?.platform === 'string'
? () => normalizeMachine(g.navigator.platform.split(' ')[0]) // eslint-disable-line @typescript-eslint/no-deprecated
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The platform function should call normalizeOSType() instead of normalizeMachine() since it's meant to return the OS type, not the machine architecture.

Copilot uses AI. Check for mistakes.
: () => 'unknown',
release: () => 'unknown',
totalmem: typeof (g as unknown as { navigator?: { deviceMemory: number } }).navigator?.deviceMemory === 'number'
? () => (g as unknown as { navigator: { deviceMemory: number } }).navigator.deviceMemory * 2 ** 30
: () => -1,
}
}

/* eslint-disable */
const machineLookup: { [key: string]: Machine } = {
// @ts-ignore __proto__ makes the object null-prototyped and sets it in dictionary mode
__proto__: null,
ia32: "x32",
amd64: "x64",
x86_64: "x64",
}
/* eslint-enable */

/**
* @param machine - a value to normalize
* @returns normalized architecture
*/
export function normalizeMachine (machine?: unknown): Machine {
return typeof machine !== 'string' || machine.length === 0
? 'unknown'
: ((machine = machine.toLowerCase()) && (machineLookup[machine as Machine] ?? machine)) as Machine
}

const osLookup: Record<Lowercase<string>, OS> = {
// @ts-expect-error __proto__ makes the object null-prototyped and sets it in dictionary mode
__proto__: null,
windows: 'win32',
}

let cachedPlatformMetrics: null | PlatformMetrics = null

/**
* @param opts - Options object
* @returns platform metrics
*/
export async function getPlatformMetrics (opts: GetPlatformMetricsOptions = {}): Promise<PlatformMetrics> {
const {
g = globalThis,
runtime = jsRuntime,
useCache = true
} = opts
if (useCache && cachedPlatformMetrics !== null) {
return cachedPlatformMetrics
}
const userAgent = (g as unknown as { navigator?: { userAgent: string } }).navigator?.userAgent ?? ''

let cpuCores = -1
let cpuModel = 'unknown'
let cpuSpeed = -1
let osKernel = 'unknown'
let osType: OS = 'unknown'
let cpuMachine: Machine = 'unknown'
let priority: null | number = -1
let memoryTotal = -1
let memoryFree = -1

const nodeOs = await loadNodeOS(runtime, g)

try {
osType = normalizeOSType(nodeOs.platform())
cpuMachine = normalizeMachine(nodeOs.machine())
osKernel = nodeOs.release()
memoryTotal = nodeOs.totalmem()
memoryFree = nodeOs.freemem()
priority = nodeOs.getPriority()

cpuCores = nodeOs.cpus().length
if (cpuCores > 0) {
cpuModel = (nodeOs as unknown as { cpus: () => [{ model: string }, ...{ model: string }[]] }).cpus()[0].model
cpuSpeed = (nodeOs as unknown as { cpus: () => [{ speed: number }, ...{ speed: number }[]] }).cpus()[0].speed
}
} catch { /* ignore */ }

return (cachedPlatformMetrics = {
cpuCores,
cpuMachine,
cpuModel,
cpuSpeed,
memoryFree,
memoryTotal,
osKernel,
osType,
priority,
runtime,
userAgent
})
}

/**
* @param os - a value to normalize
* @returns normalized OS
*/
export function normalizeOSType (os?: unknown): OS {
return typeof os !== 'string' || os.length === 0
? 'unknown'
: ((os = os.toLowerCase()) && (osLookup[os as OS] ?? os)) as OS
}
65 changes: 65 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,12 @@ export interface FnReturnedObject {
overriddenDuration?: number
}

export interface GetPlatformMetricsOptions {
g?: typeof globalThis;
runtime?: JSRuntime;
useCache?: boolean;
}

/**
* The hook function signature.
* If warmup is enabled, the hook will be called twice, once for the warmup and once for the run.
Expand All @@ -243,6 +249,34 @@ export type Hook = (
mode?: 'run' | 'warmup'
) => Promise<void> | void

export type Machine = (Lowercase<string> & Record<never, never>) | (
| 'arm64'
| 'arm'
| 'i686'
Copy link

Copilot AI Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Machine type includes 'i686' but there's no test coverage for this architecture in test/platform-normalize-arch.test.ts. Consider adding a test case to verify how 'i686' is normalized.

Copilot uses AI. Check for mistakes.
| 'ia32'
Copy link

Copilot AI Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Machine type includes 'ia32' as a valid value, but the machineLookup in src/platform.ts maps 'ia32' to 'x32' (line 37). This means 'ia32' will never be returned by normalizeMachine(). Consider removing 'ia32' from the union type since it's normalized to 'x32'.

Suggested change
| 'ia32'

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The Machine type includes 'ia32' but the normalization logic maps 'ia32' to 'x32'. Consider whether 'ia32' should be in the union type if it's always normalized to 'x32'.

Suggested change
| 'ia32'

Copilot uses AI. Check for mistakes.
| 'loong64'
| 'mips64'
Copy link

Copilot AI Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Machine type includes 'mips64' but the test file only covers 'mips' and 'mipsel'. While 'mips64' may be a valid architecture, consider adding test coverage for it in test/platform-normalize-arch.test.ts to ensure it's handled correctly.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The Machine type includes 'mips64' but there's no corresponding entry in the test file test/platform-normalize-arch.test.ts (which tests 'mips' and 'mipsel'). Consider adding test coverage for 'mips64' if it's a supported architecture.

Copilot uses AI. Check for mistakes.
| 'mips'
| 'ppc64'
| 'riscv64'
| 's390x'
| 'x86_64')
Comment on lines +262 to +263
Copy link

Copilot AI Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Machine type includes 'x86_64' as a valid value, but the machineLookup in src/platform.ts maps 'x86_64' to 'x64' (line 39). This means 'x86_64' will never be returned by normalizeMachine(). Consider removing 'x86_64' from the union type since it's normalized to 'x64', or document why it's included.

Suggested change
| 's390x'
| 'x86_64')
| 's390x')

Copilot uses AI. Check for mistakes.

Comment on lines +262 to +264
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The Machine type includes 'x86_64' but the normalization logic maps it to 'x64'. Consider whether 'x86_64' should be in the union type if it's always normalized to 'x64'.

Suggested change
| 's390x'
| 'x86_64')
| 's390x')

Copilot uses AI. Check for mistakes.
export type OS = (Lowercase<string> & Record<never, never>) | (
| 'aix'
| 'android'
| 'cygwin'
| 'darwin'
| 'freebsd'
| 'haiku'
| 'linux'
| 'netbsd'
| 'openbsd'
| 'sunos'
| 'win32')

export type PlatformMetrics = PlatformMetricsBase | PlatformMetricsBrowser | PlatformMetricsNodeLike

// @types/node doesn't have these types globally, and we don't want to bring "dom" lib for everyone
export type RemoveEventListenerOptionsArgument = Parameters<
typeof EventTarget.prototype.removeEventListener
Expand Down Expand Up @@ -451,3 +485,34 @@ export interface TaskResultWithStatistics {
type Concurrency = 'bench' | 'task' | null

type NowFn = () => number

interface PlatformMetricsBase {
cpuMachine: Machine;
memoryFree: number;
memoryTotal: number;
osType: OS;
runtime: Omit<JSRuntime, 'browser' | 'bun' | 'deno' | 'node'>;
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Omit excludes all known runtime values ('browser', 'bun', 'deno', 'node'), which would result in never. This makes PlatformMetricsBase unusable since the runtime field would have no valid values.

Suggested change
runtime: Omit<JSRuntime, 'browser' | 'bun' | 'deno' | 'node'>;
runtime: JSRuntime;

Copilot uses AI. Check for mistakes.
userAgent: string;
}

interface PlatformMetricsBrowser {
cpuMachine: Machine;
memoryFree: number;
memoryTotal: number;
osType: OS;
runtime: Extract<JSRuntime, 'browser'>;
userAgent: string;
}

interface PlatformMetricsNodeLike {
cpuCores: number;
cpuMachine: Machine;
cpuModel: string;
cpuSpeed: number;
memoryFree: number;
memoryTotal: number;
osKernel: string;
osType: OS;
priority: null | number;
runtime: Extract<JSRuntime, 'bun' | 'deno' | 'node'>
}
9 changes: 9 additions & 0 deletions test/platform-metrics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { expect, test } from 'vitest'

import { getPlatformMetrics } from '../src/platform'

test('platform metrics', async () => {
const metrics = await getPlatformMetrics({ useCache: false })
expect(metrics).toHaveProperty('osType')
expect(metrics).toHaveProperty('cpuMachine')
})
36 changes: 36 additions & 0 deletions test/platform-normalize-arch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { expect, test } from 'vitest'

import { normalizeMachine } from '../src/platform'

test('normalizeArch with non string value returns unknown', () => {
expect(normalizeMachine(undefined)).toBe('unknown')
expect(normalizeMachine(123)).toBe('unknown')
expect(normalizeMachine(null)).toBe('unknown')
expect(normalizeMachine({})).toBe('unknown')
expect(normalizeMachine([])).toBe('unknown')
})

test('normalizeArch', () => {
expect(normalizeMachine('arm')).toBe('arm')
expect(normalizeMachine('arm64')).toBe('arm64')
expect(normalizeMachine('ia32')).toBe('x32')
expect(normalizeMachine('loong64')).toBe('loong64')
expect(normalizeMachine('mips')).toBe('mips')
expect(normalizeMachine('mipsel')).toBe('mipsel')
expect(normalizeMachine('ppc64')).toBe('ppc64')
expect(normalizeMachine('riscv64')).toBe('riscv64')
expect(normalizeMachine('s390x')).toBe('s390x')
expect(normalizeMachine('x64')).toBe('x64')
})

test('normalizeArch with alternative values', () => {
expect(normalizeMachine('ia32')).toBe('x32')
Copy link

Copilot AI Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test assertion duplicates the test on line 16 in the same file. The 'normalizeArch' test already checks that 'ia32' normalizes to 'x32'. Consider removing this duplicate assertion.

Suggested change
expect(normalizeMachine('ia32')).toBe('x32')

Copilot uses AI. Check for mistakes.
expect(normalizeMachine('amd64')).toBe('x64')
expect(normalizeMachine('x86')).toBe('x86')
expect(normalizeMachine('x86_64')).toBe('x64')
})

test('normalizeArch returns lowercase', () => {
expect(normalizeMachine('ARM')).toBe('arm')
expect(normalizeMachine('AARCH64')).toBe('aarch64')
})
37 changes: 37 additions & 0 deletions test/platform-normalize-os.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect, test } from 'vitest'

import { normalizeOSType } from '../src/platform'

test('normalizeOS with non string value returns unknown', () => {
expect(normalizeOSType(undefined)).toBe('unknown')
expect(normalizeOSType(123)).toBe('unknown')
expect(normalizeOSType(null)).toBe('unknown')
expect(normalizeOSType({})).toBe('unknown')
expect(normalizeOSType([])).toBe('unknown')
})

test('normalizeOS defaults provided by node', () => {
expect(normalizeOSType('aix')).toBe('aix')
expect(normalizeOSType('android')).toBe('android')
expect(normalizeOSType('darwin')).toBe('darwin')
expect(normalizeOSType('freebsd')).toBe('freebsd')
expect(normalizeOSType('haiku')).toBe('haiku')
expect(normalizeOSType('linux')).toBe('linux')
expect(normalizeOSType('openbsd')).toBe('openbsd')
expect(normalizeOSType('sunos')).toBe('sunos')
expect(normalizeOSType('win32')).toBe('win32')
expect(normalizeOSType('cygwin')).toBe('cygwin')
expect(normalizeOSType('netbsd')).toBe('netbsd')
})

test('normalizeOS returns lowercase', () => {
expect(normalizeOSType('Linux')).toBe('linux')
expect(normalizeOSType('SunOS')).toBe('sunos')
})

test('normalizeOS with alternative Windows values', () => {
expect(normalizeOSType('Windows')).toBe('win32')
expect(normalizeOSType('Win16')).toBe('win16')
expect(normalizeOSType('Win32')).toBe('win32')
expect(normalizeOSType('WinCE')).toBe('wince')
})
Loading