181 lines
6.0 KiB
JavaScript
181 lines
6.0 KiB
JavaScript
/**
|
|||
|
|
* 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" : "")
|
||
|
|
);
|
||
|
|
}
|