From 6dd8f0321405a930475dd0d79cc7d420f19eb04e Mon Sep 17 00:00:00 2001 From: Ricardo Date: Mon, 9 Mar 2026 17:25:05 +0100 Subject: [PATCH] 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 --- eleventy.config.js | 2 +- lib/og.js | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/eleventy.config.js b/eleventy.config.js index b8431ae..615871e 100644 --- a/eleventy.config.js +++ b/eleventy.config.js @@ -1123,7 +1123,7 @@ export default function (eleventyConfig) { const siteName = process.env.SITE_NAME || "My IndieWeb Blog"; try { execFileSync(process.execPath, [ - "--max-old-space-size=768", + "--max-old-space-size=512", "--expose-gc", resolve(__dirname, "lib", "og-cli.js"), contentDir, diff --git a/lib/og.js b/lib/og.js index 4feb24b..7b9d394 100644 --- a/lib/og.js +++ b/lib/og.js @@ -298,8 +298,13 @@ export async function generateOgImages(contentDir, cacheDir, siteName) { let skipped = 0; const newManifest = {}; 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"; + let peakRss = 0; for (const filePath of mdFiles) { const raw = readFileSync(filePath, "utf8"); @@ -333,25 +338,28 @@ export async function generateOgImages(contentDir, cacheDir, siteName) { newManifest[slug] = { title: slug, hash }; generated++; - // Save manifest periodically to preserve progress + // Save manifest periodically to preserve progress on OOM kill if (generated % SAVE_INTERVAL === 0) { writeFileSync(manifestPath, JSON.stringify(newManifest, null, 2)); } // Force GC to reclaim Satori/Resvg WASM native memory. // V8 doesn't track native heap (Satori Yoga WASM + Resvg Rust WASM), - // so without periodic GC the JS wrappers accumulate and native memory - // grows unbounded. With --expose-gc this keeps peak RSS under control. + // so without frequent GC the JS wrappers accumulate and native memory + // grows unbounded. Every 5 images keeps peak RSS under ~400 MB. if (hasGC && generated % GC_INTERVAL === 0) { global.gc(); + const rss = process.memoryUsage().rss; + if (rss > peakRss) peakRss = rss; } } if (hasGC) global.gc(); writeFileSync(manifestPath, JSON.stringify(newManifest, null, 2)); const mem = process.memoryUsage(); + if (mem.rss > peakRss) peakRss = mem.rss; console.log( `[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`, ); }