gongxiangp2/lib/system-performance.ts

137 lines
4.6 KiB
TypeScript

import { readFile } from 'fs/promises'
import os from 'os'
export type MemorySource = 'container' | 'host'
export interface ProcessMemoryStats {
usedBytes: number
totalBytes: number
percentage: number
source: MemorySource
hostUsedBytes: number
hostTotalBytes: number
hostPercentage: number
}
const CGROUP_V2_CURRENT = '/sys/fs/cgroup/memory.current'
const CGROUP_V2_MAX = '/sys/fs/cgroup/memory.max'
const CGROUP_V1_USAGE = '/sys/fs/cgroup/memory/memory.usage_in_bytes'
const CGROUP_V1_LIMIT = '/sys/fs/cgroup/memory/memory.limit_in_bytes'
const CGROUP_UNLIMITED = BigInt(1) << BigInt(60)
type FileReader = (path: string) => Promise<string>
async function readNumericFile(read: FileReader, path: string): Promise<bigint | null> {
try {
const raw = (await read(path)).trim()
if (!raw || raw === 'max') return null
const value = BigInt(raw)
return value >= BigInt(0) ? value : null
} catch {
return null
}
}
function toFiniteNumber(value: bigint): number | null {
if (value > BigInt(Number.MAX_SAFE_INTEGER)) return null
const asNumber = Number(value)
return Number.isFinite(asNumber) ? asNumber : null
}
export function resolveProcessMemory(input: {
containerUsedBytes?: number | null
containerLimitBytes?: number | null
hostTotalBytes: number
hostFreeBytes: number
}): ProcessMemoryStats {
const hostTotal = Math.max(0, input.hostTotalBytes)
const hostFree = Math.max(0, Math.min(input.hostFreeBytes, hostTotal))
const hostUsed = Math.max(0, hostTotal - hostFree)
const containerUsed = input.containerUsedBytes
const containerLimit = input.containerLimitBytes
const hasContainerLimit =
typeof containerUsed === 'number' &&
Number.isFinite(containerUsed) &&
containerUsed >= 0 &&
typeof containerLimit === 'number' &&
Number.isFinite(containerLimit) &&
containerLimit > 0 &&
containerLimit < hostTotal
const hostPercentage = hostTotal > 0 ? Math.round((hostUsed / hostTotal) * 100) : 0
if (hasContainerLimit) {
return {
usedBytes: Math.min(containerUsed, containerLimit),
totalBytes: containerLimit,
percentage: Math.round((Math.min(containerUsed, containerLimit) / containerLimit) * 100),
source: 'container',
hostUsedBytes: hostUsed,
hostTotalBytes: hostTotal,
hostPercentage,
}
}
return {
usedBytes: hostUsed,
totalBytes: hostTotal,
percentage: hostPercentage,
source: 'host',
hostUsedBytes: hostUsed,
hostTotalBytes: hostTotal,
hostPercentage,
}
}
export async function readProcessMemory(options?: {
readFile?: FileReader
totalmem?: () => number
freemem?: () => number
}): Promise<ProcessMemoryStats> {
const read = options?.readFile ?? ((path: string) => readFile(path, 'utf8'))
const hostTotalBytes = options?.totalmem?.() ?? os.totalmem()
const hostFreeBytes = options?.freemem?.() ?? os.freemem()
const v2Used = await readNumericFile(read, CGROUP_V2_CURRENT)
const v2Max = await readNumericFile(read, CGROUP_V2_MAX)
const v1Used = v2Used == null ? await readNumericFile(read, CGROUP_V1_USAGE) : null
const v1Limit = v2Max == null ? await readNumericFile(read, CGROUP_V1_LIMIT) : null
const rawUsed = v2Used ?? v1Used
const rawLimit = v2Max ?? v1Limit
const usableLimit = rawLimit != null && rawLimit < CGROUP_UNLIMITED ? rawLimit : null
return resolveProcessMemory({
containerUsedBytes: rawUsed != null ? toFiniteNumber(rawUsed) : null,
containerLimitBytes: usableLimit != null ? toFiniteNumber(usableLimit) : null,
hostTotalBytes,
hostFreeBytes,
})
}
export function classifyUploadSession(input: {
progress: number
inactiveMs: number
activeThresholdMs: number
maxShowThresholdMs: number
}): 'processing' | 'waiting' | 'hidden' {
if (input.progress >= 100) return 'hidden'
if (!Number.isFinite(input.inactiveMs) || input.inactiveMs >= input.maxShowThresholdMs) return 'hidden'
return input.inactiveMs < input.activeThresholdMs ? 'processing' : 'waiting'
}
export function classifyTranscodeMaterial(input: {
status: string | null | undefined
leaseExpiresAt?: Date | string | null
now?: Date
}): 'processing' | 'waiting' | 'hidden' {
const status = String(input.status || '').trim().toLowerCase()
if (status !== 'pending' && status !== 'processing') return 'hidden'
const now = input.now ?? new Date()
const expiresAt = input.leaseExpiresAt ? new Date(input.leaseExpiresAt) : null
const leaseValid = Boolean(expiresAt && !Number.isNaN(expiresAt.getTime()) && expiresAt.getTime() > now.getTime())
if (status === 'processing' && leaseValid) return 'processing'
return 'waiting'
}