blog.git

blog

~ Personal research notes. blog.sqzer.com
git clone https://github.com/debuman/blog.git
commit d19bd8b59ac60796a90ed3432d63b30edb54a50f
parent 591166a63d287a4e6f262bed9aef61fd29bb3d05
Author: sqzer <110329244+sqzer-x@users.noreply.github.com>
Date: Fri, 25 Sep 2026 03:42:44 +0900

fix: let render-diagrams exit as soon as it is done

Both Promise.race timeouts left their timers pending, so Node stayed
alive until the 120 s render budget ran out after every successful
render. Rendering one diagram took 121 s of wall time for 1 s of work.
The timers are now cleared when the race settles, and the success path
exits explicitly like every other path does, because Chrome on Windows
can hold its pipes open after it is killed. One diagram now takes 1.2 s,
and the rendered SVG is byte-identical.

Diffstat:
Mscripts/render-diagrams.mjs | 28++++++++++++++++++++++------
1 file changed, 22 insertion(+), 6 deletion(-)

diff --git a/scripts/render-diagrams.mjs b/scripts/render-diagrams.mjs
@@ -197,6 +197,17 @@ async function connect(wsUrl) {
   };
 }
 
+/**
+ * `promise`, or a rejection after `ms`. The timer is cleared either way: a bare
+ * Promise.race leaves it pending, and Node then stays alive until it fires, which held
+ * every successful render open for the full two-minute render budget.
+ */
+function within(promise, ms, message) {
+  let timer;
+  const guard = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), ms); });
+  return Promise.race([promise, guard]).finally(() => clearTimeout(timer));
+}
+
 const started = Date.now();
 // A throwaway profile per run, so a Chrome the author already has open cannot be joined
 // (which would hand back a browser whose fonts and flags are not the ones set here).
@@ -224,7 +235,7 @@ const fail = (message) => {
 const watchdog = setTimeout(() => fail('the browser stopped responding'), RENDER_TIMEOUT_MS * 2);
 child.on('error', (error) => fail(`could not start ${browser} — ${error.message}`));
 
-const wsUrl = await Promise.race([
+const wsUrl = await within(
   new Promise((resolve) => {
     let buffered = '';
     child.stderr.on('data', (chunk) => {
@@ -233,8 +244,9 @@ const wsUrl = await Promise.race([
       if (match) resolve(match[0]);
     });
   }),
-  new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 30_000)),
-]).catch(() => fail(`${browser} started but never announced a debugging endpoint`));
+  30_000,
+  'timeout',
+).catch(() => fail(`${browser} started but never announced a debugging endpoint`));
 
 const origin = wsUrl.replace('ws://', 'http://').replace(/\/devtools.*/, '');
 const targets = await (await fetch(`${origin}/json/list`)).json();
@@ -287,10 +299,11 @@ const expression = `(async () => {
   return JSON.stringify(out);
 })()`;
 
-const result = await Promise.race([
+const result = await within(
   page.send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }),
-  new Promise((_, reject) => setTimeout(() => reject(new Error('render timed out')), RENDER_TIMEOUT_MS)),
-]).catch((error) => fail(error.message));
+  RENDER_TIMEOUT_MS,
+  'render timed out',
+).catch((error) => fail(error.message));
 
 if (result.exceptionDetails) fail(`render failed: ${JSON.stringify(result.exceptionDetails).slice(0, 800)}`);
 
@@ -336,6 +349,9 @@ console.log(
 if (process.env.CI) {
   console.log('  note: these were rendered on CI, so the committed cache is behind — run `npm run diagrams` and commit.');
 }
+// Explicit, like every other exit path in this file. On Windows a Chrome that is slow to
+// die can hold the stderr pipe or the CDP socket open for minutes after the work is done.
+process.exit(0);
 
 /**
  * Astro's content layer caches rendered Markdown keyed on the *Markdown* file's digest, and