Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 38 additions & 30 deletions packages/spec/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,55 +180,63 @@ const swapServerOnlyGrammarArm: Plugin = {
* it, and the pass's non-heap overhead measures ~250 MB, so the bound is
* ~6.4 GB inside an 8 GB container.
*
* WHY THE PASS IS THIS HEAVY: ONE `ts.Program` PER ENTRY. tsup 8.5.1 runs the
* pass through its bundled rollup-plugin-dts 6.1.1, whose `createPrograms`
* groups entries by a directory key. tsup always passes the tsconfig path, and
* on that path every entry after the first is keyed by its OWN directory (the
* same code is in rollup-plugin-dts 6.5.1). So each entry above gets its own
* program, which parses, binds and emits its whole reachable graph again: the
* peak grows with entries × graph, not with the graph. ⇒ Every entry added to
* `entries` adds a program to this pass.
* ONE `ts.Program` FOR ALL ENTRIES — held by a patch, not by tsup. tsup 8.5.1
* runs this pass through its bundled rollup-plugin-dts 6.1.1, whose
* `createPrograms` groups entries by a directory key. tsup always passes the
* tsconfig path, and on that path every entry after the first was keyed by its
* OWN directory (the same code is in rollup-plugin-dts 6.5.1). So each entry
* above got its own program, which parsed, bound and emitted its whole
* reachable graph again: the peak grew with entries × graph, not with the
* graph. `patches/tsup@8.5.1.patch` (wired in `pnpm-workspace.yaml`'s
* `patchedDependencies`) keys every entry by the tsconfig's directory, so the
* 18 entries share one program. The patch names the exact tsup version, and
* `pnpm install` refuses a patch that matches no installed package, so a tsup
* bump cannot drop it silently. ⇒ On a tsup bump, re-derive the patch or
* retire it, then re-measure this table. If this pass ever shows 18 programs
* again, the entries are back to one program each.
*
* `noCheck`: rollup-plugin-dts forces `noEmitOnError`, which made every one of
* those programs also semantically CHECK each file it emitted — a type check
* `noCheck`: rollup-plugin-dts forces `noEmitOnError`, which makes the program
* also semantically CHECK each file it emits — a type check
* the `typecheck` script (`tsc --noEmit` over this same tsconfig, run by the
* required `TypeScript Type Check` job) already performs once. `noCheck` drops
* only that duplicate. Syntactic, option, global and declaration diagnostics
* still fail the pass, so a declaration that cannot be emitted still stops the
* build; a plain type error in `src/` is `typecheck`'s to report, not this
* pass's.
*
* Measured on 8cdbe0c6e5's source, DTS pass alone at the 6144 ceiling, inside a
* Measured on 8113763026's source, DTS pass alone at the 6144 ceiling, inside a
* cgroup capped at 8192 MB. Live heap is the largest heap V8 kept after a
* mark-compact (`--trace-gc`); peak RSS is the peak anonymous RSS of the whole
* process tree:
* process tree; wall times are shared-box readings:
*
* pass live heap peak RSS wall
* duplicate check on (before) 5658 MB 6177 MB 181-194s
* noCheck (this config) 5083 MB 5889 MB 134s
* one program, grouping patched 1379 MB 3749 MB 53s
* `tsc --noEmit`, whole package 1103 MB 1149 MB 18s
* pass (both with noCheck) programs live heap peak RSS wall
* one program per entry (unpatched) 18 4997 MB 5694 MB 153s
* one program (patched, this tree) 1 882 MB 3537 MB 41s
*
* The third row was measured with the grouping patched in a copy of tsup
* outside this tree. It shows what cutting the program count is worth.
* The patched run did only 4 mark-compacts, so its live-heap figure is a
* lower bound; its largest heap before a mark-compact was 1677 MB. The whole
* `build` script went 189s → 87s. For scale, `tsc --noEmit` over the whole
* package peaked at 1103 MB live on 8cdbe0c6e5.
*
* NOT BYTE-STABLE: this pass does not emit the same bytes twice. TypeScript
* prints union members, and the members of object types derived from them, in
* type-creation order, and that order follows emit order. Three runs of the
* same commit gave three different trees, and rollup's content-hashed chunk
* names moved with them. Once union and property-signature order are
* normalised, all three runs and the `noCheck` run are the same tree (128
* files, 30389539 bytes). ⇒ Compare two declaration trees in such an
* order-insensitive form, never by a byte digest.
* type-creation order, and that order follows emit order. Three runs of one
* commit gave three different trees, and rollup's content-hashed chunk names
* moved with them. The one-program pass also reorders top-level statements
* and `import`/`export` specifiers, and names one shared chunk after a
* different module (`data-engine` → `analytics.zod`). Normalise all of those
* (sort union and property-only type-literal members, statements and
* specifiers; strip chunk hashes; pair chunks by content), and the unpatched
* and patched trees are the same tree: 130 files, 0 differing. ⇒ Compare two
* declaration trees in that order-insensitive form, never by a byte digest.
*
* If this pass starts failing with `ERR_WORKER_OUT_OF_MEMORY`, the live type
* graph has outgrown 6144 — that is a loud, actionable failure and the point of
* the ceiling. ⛔ Do not "fix" it by raising the number past what the build
* container has; that trades this error back for the silent exit 137. Cut the
* program count, shrink the graph, or split the pass across entries. Cutting
* the count moves statement order and one chunk name beyond the noise above. A
* split redraws the shared chunks. Either one changes what publishes, so it
* needs that reviewed first.
* container has; that trades this error back for the silent exit 137. First
* check that the pass still builds ONE program (above). Then shrink the graph,
* or split the pass across entries. A split redraws the shared chunks and
* changes what publishes, so it needs that reviewed first.
*/
const isDts = process.env.BUILD_DTS === 'true';

Expand Down
16 changes: 16 additions & 0 deletions patches/tsup@8.5.1.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
diff --git a/dist/rollup.js b/dist/rollup.js
index e128b61b9558318b9f86dc11d72c31f09a8eb7db..35f85e39c2af6c645fb2d3da115742df445d2343 100644
--- a/dist/rollup.js
+++ b/dist/rollup.js
@@ -4915,6 +4915,11 @@ var require_rollup_plugin_dts = _chunkTWFEYLU4js.__commonJS.call(void 0, {
} else {
logCache("HIT", cacheKey);
}
+ if (overrideConfigPath) {
+ // objectstack patch: a cache HIT kept dirname(input), so each entry
+ // got its own ts.Program. One tsconfig for all entries => one key.
+ dirName = path__namespace.dirname(path__namespace.resolve(process.cwd(), overrideConfigPath));
+ }
const { fileNames, options, errors } = configByPath.get(cacheKey);
dtsFiles = fileNames.filter((name) => DTS_EXTENSIONS.test(name));
if (errors.length) {
27 changes: 16 additions & 11 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ onlyBuiltDependencies:
- msw
- sharp

# Patched dependencies. Written with `pnpm patch` / `pnpm patch-commit`, never
# by hand. `patch-commit` writes this key into package.json's `pnpm` field;
# move it here, beside `overrides` and `onlyBuiltDependencies`, then run
# `pnpm install`.
# - tsup@8.5.1: its bundled rollup-plugin-dts gave every DTS entry its own
# ts.Program. tsup always passes the tsconfig path, and on that path a
# config-cache hit keyed each later entry by its own directory. The patch
# keys every entry by the tsconfig's directory, so all entries share one
# program. The same code is in rollup-plugin-dts 6.5.1. Twelve packages
# here have more than one DTS entry; @objectstack/spec had 18 programs
# and now has one, and its DTS pass went from 4997 MB to 882 MB of live
# heap. Every one of the twelve emits the same types as before.
# The key names the exact version. When the installed tsup is any other
# version, the patch goes unused and `pnpm install` fails
# (ERR_PNPM_UNUSED_PATCH), so a tsup upgrade cannot drop the patch
# silently. Re-derive the patch, or retire it, on every tsup bump.
patchedDependencies:
tsup@8.5.1: patches/tsup@8.5.1.patch

# Transitive-dependency version pins. pnpm v10 reads `overrides` from THIS file
# — the `pnpm.overrides` block in package.json is silently ignored — so all
# pins must live here (previously orphaned in package.json: minimatch, tar).
Expand Down
Loading