/** * Cross-platform host profile for build / migrate / seed / Vite. * Auto-detects OS, RAM, and CPU; caps parallelism so rebuilds do not pin 100% forever. * * Override anytime: * BUILD_PROFILE=low|medium|high * BUILD_NODE_HEAP_MB / BUILD_GO_P / BUILD_GO_MAXPROCS / BUILD_GO_MEMLIMIT * BUILD_LOW_MEM=0|1 */ import os from "node:os"; export function envInt(name, fallback, env = process.env) { const raw = env[name]; if (raw == null || raw === "") return fallback; const n = Number.parseInt(raw, 10); return Number.isFinite(n) && n > 0 ? n : fallback; } export function envFlag(name, env = process.env) { const raw = env[name]; if (raw == null || raw === "") return null; if (/^(1|true|yes|on)$/i.test(raw)) return true; if (/^(0|false|no|off)$/i.test(raw)) return false; return null; } /** Total / free RAM in MiB (Node os.*; works on Windows, Linux, macOS). */ export function hostRamMiB() { const total = Math.floor(os.totalmem() / (1024 * 1024)); const free = Math.floor(os.freemem() / (1024 * 1024)); return { totalMiB: Number.isFinite(total) && total > 0 ? total : null, freeMiB: Number.isFinite(free) && free >= 0 ? free : null, }; } export function hostCpuCount() { const n = os.cpus()?.length ?? 1; return Number.isFinite(n) && n > 0 ? n : 1; } /** * Pick a resource tier. * - low: ≤5 GiB RAM or ≤2 CPUs (typical 4GiB VPS) * - medium: ≤12 GiB * - high: everything else (dev workstations) */ export function detectTier(env = process.env) { const forced = String(env.BUILD_PROFILE || "").toLowerCase(); if (forced === "low" || forced === "medium" || forced === "high") return forced; const { totalMiB } = hostRamMiB(); const cpus = hostCpuCount(); if (totalMiB != null && totalMiB <= 5120) return "low"; if (cpus <= 2) return "low"; if (totalMiB != null && totalMiB <= 12288) return "medium"; return "high"; } /** * Leave CPU headroom for OS / Postgres / shell so rebuilds do not pin 100%. * low → 1 · medium → ≤4 and keep 1 free · high → ≤75% and keep ≥2 free when possible */ function cappedProcs(cpus, tier) { if (tier === "low") return 1; if (tier === "medium") return Math.max(1, Math.min(cpus - 1, 4)); if (cpus <= 4) return Math.max(1, cpus - 1); const keepFree = Math.max(2, Math.ceil(cpus * 0.25)); return Math.max(1, cpus - keepFree); } export function detectHostProfile(env = process.env) { const platform = process.platform; // win32 | linux | darwin | … const { totalMiB, freeMiB } = hostRamMiB(); const cpus = hostCpuCount(); const tier = detectTier(env); let nodeHeapMb; let goMaxProcs; let goP; let goMemLimit; // string or null (unset = Go default) let goGc; // string or null let uvThreadpool; let lowMem; if (tier === "low") { // Keep Node well under total RAM; leave room for Postgres + Go. const budget = totalMiB != null ? Math.floor(totalMiB * 0.35) : 1536; nodeHeapMb = Math.min(1536, Math.max(768, budget)); goMaxProcs = 1; goP = 1; goMemLimit = "768MiB"; goGc = "50"; uvThreadpool = 2; lowMem = true; } else if (tier === "medium") { nodeHeapMb = Math.min(3072, totalMiB != null ? Math.floor(totalMiB * 0.3) : 3072); goMaxProcs = cappedProcs(cpus, tier); goP = goMaxProcs; goMemLimit = "2048MiB"; goGc = null; uvThreadpool = 4; lowMem = false; } else { nodeHeapMb = Math.min(6144, totalMiB != null ? Math.floor(totalMiB * 0.25) : 4096); goMaxProcs = cappedProcs(cpus, tier); goP = goMaxProcs; goMemLimit = null; goGc = null; uvThreadpool = Math.min(8, Math.max(4, goMaxProcs)); lowMem = false; } // Explicit env always wins. nodeHeapMb = envInt("BUILD_NODE_HEAP_MB", nodeHeapMb, env); goMaxProcs = envInt("BUILD_GO_MAXPROCS", goMaxProcs, env); goP = envInt("BUILD_GO_P", goP, env); if (env.BUILD_GO_MEMLIMIT) goMemLimit = env.BUILD_GO_MEMLIMIT; const lowMemFlag = envFlag("BUILD_LOW_MEM", env); if (lowMemFlag != null) lowMem = lowMemFlag; return { platform, tier, totalMiB, freeMiB, cpus, nodeHeapMb, goMaxProcs, goP, goMemLimit, goGc, uvThreadpool, lowMem, }; } /** Merge --max-old-space-size into NODE_OPTIONS without duplicating the flag. */ export function withNodeHeap(baseEnv = process.env, heapMb, uvThreadpool) { const profile = heapMb == null || uvThreadpool == null ? detectHostProfile(baseEnv) : null; const heap = heapMb ?? profile.nodeHeapMb; const uv = uvThreadpool ?? profile.uvThreadpool; const out = { ...baseEnv }; const flag = `--max-old-space-size=${heap}`; const prev = out.NODE_OPTIONS ?? ""; if (/\b--max-old-space-size=\d+\b/.test(prev)) { out.NODE_OPTIONS = prev.replace(/\b--max-old-space-size=\d+\b/g, flag); } else { out.NODE_OPTIONS = prev ? `${prev} ${flag}` : flag; } if (!out.UV_THREADPOOL_SIZE) out.UV_THREADPOOL_SIZE = String(uv); // Tell Vite whether to use low-mem knobs (only set when unset). if (out.BUILD_LOW_MEM == null || out.BUILD_LOW_MEM === "") { out.BUILD_LOW_MEM = (profile ?? detectHostProfile(baseEnv)).lowMem ? "1" : "0"; } return out; } /** Apply Go concurrency / soft memory caps from the detected (or overridden) profile. */ export function withGoLowMem(baseEnv = process.env, profile = detectHostProfile(baseEnv)) { const out = { ...baseEnv }; if (!out.GOMAXPROCS) out.GOMAXPROCS = String(profile.goMaxProcs); if (profile.goMemLimit && !out.GOMEMLIMIT) out.GOMEMLIMIT = profile.goMemLimit; if (profile.goGc && !out.GOGC) out.GOGC = profile.goGc; return out; } export function formatHostNote(profile = detectHostProfile()) { const ram = profile.totalMiB != null ? `${profile.totalMiB}MiB total` + (profile.freeMiB != null ? `, ~${profile.freeMiB}MiB free` : "") : "RAM unknown"; return ( `${profile.platform} tier=${profile.tier} cpus=${profile.cpus} (${ram}) → ` + `nodeHeap=${profile.nodeHeapMb} go -p=${profile.goP} GOMAXPROCS=${profile.goMaxProcs}` + (profile.goMemLimit ? ` GOMEMLIMIT=${profile.goMemLimit}` : "") + (profile.lowMem ? " lowMem" : "") ); }