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 async function readNumericFile(read: FileReader, path: string): Promise { 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 { 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' }