fix(og): aggressive GC to prevent OOM in constrained containers

Full OG regeneration (2,350 images) was OOM-killed because WASM native
memory from Satori/Resvg accumulated between GC cycles. The previous
GC interval of 50 images allowed ~2.5-5 GB of native allocations before
reclamation. Reduce to every 5 images to keep peak RSS under ~400 MB.

Also reduce --max-old-space-size from 768 to 512 MB (V8 heap only uses
~22 MB) and add peak RSS tracking to the completion log.

Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
This commit is contained in:
Ricardo
2026-03-09 17:25:05 +01:00
parent bfd885cbc9
commit 6dd8f03214
2 changed files with 14 additions and 6 deletions

View File

@@ -1123,7 +1123,7 @@ export default function (eleventyConfig) {
const siteName = process.env.SITE_NAME || "My IndieWeb Blog"; const siteName = process.env.SITE_NAME || "My IndieWeb Blog";
try { try {
execFileSync(process.execPath, [ execFileSync(process.execPath, [
"--max-old-space-size=768", "--max-old-space-size=512",
"--expose-gc", "--expose-gc",
resolve(__dirname, "lib", "og-cli.js"), resolve(__dirname, "lib", "og-cli.js"),
contentDir, contentDir,

View File

@@ -298,8 +298,13 @@ export async function generateOgImages(contentDir, cacheDir, siteName) {
let skipped = 0; let skipped = 0;
const newManifest = {}; const newManifest = {};
const SAVE_INTERVAL = 10; const SAVE_INTERVAL = 10;
const GC_INTERVAL = 50; // GC every 5 images to keep WASM native memory bounded.
// Satori (Yoga WASM) + Resvg (Rust WASM) allocate ~50-100 MB native memory
// per image that V8 doesn't track. Without aggressive GC, native memory
// grows unbounded and OOM-kills the process in constrained containers.
const GC_INTERVAL = 5;
const hasGC = typeof global.gc === "function"; const hasGC = typeof global.gc === "function";
let peakRss = 0;
for (const filePath of mdFiles) { for (const filePath of mdFiles) {
const raw = readFileSync(filePath, "utf8"); const raw = readFileSync(filePath, "utf8");
@@ -333,25 +338,28 @@ export async function generateOgImages(contentDir, cacheDir, siteName) {
newManifest[slug] = { title: slug, hash }; newManifest[slug] = { title: slug, hash };
generated++; generated++;
// Save manifest periodically to preserve progress // Save manifest periodically to preserve progress on OOM kill
if (generated % SAVE_INTERVAL === 0) { if (generated % SAVE_INTERVAL === 0) {
writeFileSync(manifestPath, JSON.stringify(newManifest, null, 2)); writeFileSync(manifestPath, JSON.stringify(newManifest, null, 2));
} }
// Force GC to reclaim Satori/Resvg WASM native memory. // Force GC to reclaim Satori/Resvg WASM native memory.
// V8 doesn't track native heap (Satori Yoga WASM + Resvg Rust WASM), // V8 doesn't track native heap (Satori Yoga WASM + Resvg Rust WASM),
// so without periodic GC the JS wrappers accumulate and native memory // so without frequent GC the JS wrappers accumulate and native memory
// grows unbounded. With --expose-gc this keeps peak RSS under control. // grows unbounded. Every 5 images keeps peak RSS under ~400 MB.
if (hasGC && generated % GC_INTERVAL === 0) { if (hasGC && generated % GC_INTERVAL === 0) {
global.gc(); global.gc();
const rss = process.memoryUsage().rss;
if (rss > peakRss) peakRss = rss;
} }
} }
if (hasGC) global.gc(); if (hasGC) global.gc();
writeFileSync(manifestPath, JSON.stringify(newManifest, null, 2)); writeFileSync(manifestPath, JSON.stringify(newManifest, null, 2));
const mem = process.memoryUsage(); const mem = process.memoryUsage();
if (mem.rss > peakRss) peakRss = mem.rss;
console.log( console.log(
`[og] Generated ${generated} images, skipped ${skipped} (cached or have photos)` + `[og] Generated ${generated} images, skipped ${skipped} (cached or have photos)` +
` | RSS: ${(mem.rss / 1024 / 1024).toFixed(0)} MB, heap: ${(mem.heapUsed / 1024 / 1024).toFixed(0)} MB`, ` | RSS: ${(mem.rss / 1024 / 1024).toFixed(0)} MB, peak: ${(peakRss / 1024 / 1024).toFixed(0)} MB, heap: ${(mem.heapUsed / 1024 / 1024).toFixed(0)} MB`,
); );
} }