Skip to content

Commit 0d4a6a8

Browse files
os-zhuangclaude
andauthored
build(spec): bound the DTS pass heap ceiling to the build container's memory (#12684)
The DTS pass declared --max-old-space-size=12288 inside an 8 GB Vercel build container. A ceiling above the container's memory does not permit a bigger build: V8 defers major GCs below its ceiling, so the process is SIGKILLed at the container limit before V8 ever reaches it -- exit 137, no diagnostic. Measured inside a cgroup capped at 8192 MB (peak anonymous RSS, whole process tree): 12288 -> 7290 MB; 6144 -> 5794 MB; 5120 -> 5328 MB; 4096 -> ERR_WORKER_OUT_OF_MEMORY. Declaration output is byte-identical across every completing ceiling (122 files, one sha256 over all of them). Claude-Session: https://claude.ai/code/session_01DKWDdUJ2XNRESVVWUvcpnh Co-authored-by: Claude <noreply@anthropic.com>
1 parent e0abc38 commit 0d4a6a8

3 files changed

Lines changed: 80 additions & 2 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
build(spec): bound the DTS pass's heap ceiling to something the build container actually has (#12677)
6+
7+
The DTS pass ran under `NODE_OPTIONS="--max-old-space-size=12288"`. A heap
8+
ceiling is a promise to V8 that the memory exists: below it, V8 defers major GCs
9+
and lets the resident set grow. A ceiling **above** the container's memory does
10+
not permit a bigger build — it converts a recoverable JS heap error into a
11+
kernel SIGKILL, because the process meets the container limit long before V8
12+
considers the ceiling reached, and `exit 137` carries no diagnostic.
13+
14+
That is what took the docs site down: every `objectstack.ai` production deploy
15+
since 2026-08-25 died with `@objectstack/spec:build` exit 137. Vercel builds the
16+
docs app with `turbo run build --filter=@objectstack/docs` inside one
17+
fixed-memory container, and this package is that app's only workspace
18+
dependency — so this pass met the container limit alone.
19+
20+
The ceiling is now `6144`. Measured on this package's DTS pass inside a cgroup
21+
capped at 8192 MB, as peak anonymous RSS of the whole process tree:
22+
23+
| ceiling | result | peak RSS | wall |
24+
|---|---|---|---|
25+
| 12288 (before) | ok, ~0.9 GB spare | 7290 MB | 132s |
26+
| 6144 (after) | ok, ~2.3 GB spare | 5794 MB | 134s |
27+
| 5120 | ok | 5328 MB | 148s |
28+
| 4096 | `ERR_WORKER_OUT_OF_MEMORY` | — | 113s |
29+
30+
No output change: every completing ceiling emitted a byte-identical declaration
31+
tree (122 files, compared as one sha256 over all of them). The number buys
32+
headroom and costs GC time, not build time.
33+
34+
Should the live type graph ever outgrow `6144`, the pass now fails **loud**
35+
with `ERR_WORKER_OUT_OF_MEMORY` instead of being killed silently. Raising the
36+
number past what the build container has would trade that diagnostic back for
37+
`exit 137`.

‎packages/spec/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@
243243
"spec-changes.json"
244244
],
245245
"scripts": {
246-
"build": "pnpm gen:schema && pnpm gen:openapi && tsup && if [ -z \"$OS_SKIP_DTS\" ]; then NODE_OPTIONS=\"--max-old-space-size=12288\" BUILD_DTS=true tsup; fi && node ../../scripts/check-dts-emitted.mjs && node ../../scripts/check-dev-prereqs.mjs --stamp",
246+
"build": "pnpm gen:schema && pnpm gen:openapi && tsup && if [ -z \"$OS_SKIP_DTS\" ]; then NODE_OPTIONS=\"--max-old-space-size=6144\" BUILD_DTS=true tsup; fi && node ../../scripts/check-dts-emitted.mjs && node ../../scripts/check-dev-prereqs.mjs --stamp",
247247
"dev": "tsc --watch",
248248
"clean": "rm -rf dist",
249249
"gen:schema": "OS_EAGER_SCHEMAS=1 tsx scripts/build-schemas.ts",

‎packages/spec/tsup.config.ts‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,48 @@ const swapServerOnlyGrammarArm: Plugin = {
138138
},
139139
};
140140

141-
// Generate DTS separately to avoid memory issues
141+
/**
142+
* Generate DTS separately to avoid memory issues — this pass is by far the
143+
* heaviest thing this package's build does, and `package.json` runs it under an
144+
* explicit `--max-old-space-size` for a reason worth stating where the next
145+
* author will look.
146+
*
147+
* ⚠️ THE HEAP CEILING MUST FIT THE SMALLEST CONTAINER THIS BUILD RUNS IN.
148+
* `--max-old-space-size` is a promise to V8 that the memory is there: below it,
149+
* V8 defers major GCs and lets the resident set grow. A ceiling ABOVE the
150+
* container's memory therefore does not "allow a big build", it converts a
151+
* recoverable JS heap error into a kernel SIGKILL — the process is killed at
152+
* the container limit long before V8 ever considers the ceiling reached, and
153+
* exit 137 carries no diagnostic at all.
154+
*
155+
* That is not hypothetical: at a 12288 ceiling this pass was killed on every
156+
* Vercel docs deploy for two days (`@objectstack/spec:build` exit 137), because
157+
* the docs site is built by `turbo run build --filter=@objectstack/docs` inside
158+
* one fixed-memory build container and this package is its only workspace
159+
* dependency — so this pass meets the container's limit ALONE, with no
160+
* parallelism to cap.
161+
*
162+
* Measured on this package's DTS pass, inside a cgroup capped at 8192 MB
163+
* (peak anonymous RSS of the whole process tree):
164+
*
165+
* ceiling result peak RSS wall
166+
* 12288 ok, but only ~0.9 GB spare 7290 MB 132s
167+
* 6144 ok 5794 MB 134s
168+
* 5120 ok 5328 MB 148s
169+
* 4096 ERR_WORKER_OUT_OF_MEMORY — 113s
170+
*
171+
* 6144 is chosen as the largest ceiling whose WORST case still fits: V8 cannot
172+
* exceed it, and the pass's non-heap overhead measured ~250 MB, so the bound is
173+
* ~6.4 GB inside an 8 GB container. Every completing ceiling emitted a
174+
* byte-identical declaration tree (122 files, one sha256 over all of them), so
175+
* this number buys headroom and costs nothing but GC time.
176+
*
177+
* If this pass starts failing with `ERR_WORKER_OUT_OF_MEMORY`, the live type
178+
* graph has outgrown 6144 — that is a loud, actionable failure and the point of
179+
* the ceiling. ⛔ Do not "fix" it by raising the number past what the build
180+
* container has; that trades this error back for the silent exit 137. Shrink
181+
* the graph, or split the pass across entries.
182+
*/
142183
const isDts = process.env.BUILD_DTS === 'true';
143184

144185
const mainConfig: Options = {

0 commit comments

Comments
 (0)