137 lines
4.6 KiB
TypeScript
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'
|
|
}
|