feat: add Source.prototype.clearCache() to release per-instance caches#221
Conversation
Mirrors the fix to webpack's SourceMapDevToolPlugin (webpack/webpack#20961). `CachedSource` retains composed source map data per chunk in `_cachedMaps`; without a way to release it, plugins that hold assets reachable for size / hash work cannot reclaim those bytes between tasks. `clearCache()` is a no-op on the base class. `CachedSource` drops all internal caches and recurses into its wrapped source. Composite sources (Concat / Prefix / Replace / Compat) propagate to children, and leaf sources (Raw / Original / SourceMap) drop dual-cached secondary representations. A standalone memory benchmark under `benchmark/memory/clear-cache.mjs` reproduces the upstream scenario; on a 200-task workload it shrinks heap growth from 58.4 MB to 32.3 MB (~45%). https://claude.ai/code/session_01LLtSGKaynui1P1wQsfVnXx
|
🦋 Changeset detectedLatest commit: 6b66049 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #221 +/- ##
==========================================
- Coverage 97.71% 97.58% -0.14%
==========================================
Files 25 25
Lines 1970 2069 +99
Branches 613 668 +55
==========================================
+ Hits 1925 2019 +94
- Misses 43 47 +4
- Partials 2 3 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
The PR review flagged missing coverage on the originalSource, sourceMap buffer, and innerSourceMap branches of `SourceMapSource.clearCache()`. Construct a SourceMapSource with every optional parameter and call `getArgsAsBuffers()` to populate all four dual-cached pairs before clearing, so every `if`-branch executes. Round-trip the buffers after the clear to confirm the data is still readable. https://claude.ai/code/session_01LLtSGKaynui1P1wQsfVnXx
Review of PR #221 flagged three blocking issues for webpack's SourceMapDevToolPlugin use case: 1. **Recursion had no de-duplication.** When a webpack plugin iterates assets, the same module-level CachedSource is reachable from many chunks, so calling `clearCache()` per asset re-walked the shared subtree once per chunk. The reviewer's 50-chunk benchmark went from 10s to 17–45s with no extra heap freed. 2. **Recursion was non-opt-outable.** Webpack often replaces the asset shortly after calling `clearCache()`, so walking the children ourselves is pure waste — V8 reclaims them for free anyway. 3. **Cheap-to-hold caches were cleared unconditionally.** `_cachedSize` is a single number; `_cachedHashUpdate` is what makes downstream `cache.store` cheap. Dropping them on a generic "release memory" call is a perf cliff. The new signature addresses all three: ```js clearCache( { maps = true, source = true, hash = false, size = false, recursive = true } = {}, visited ) ``` - A `visited` `WeakSet` lets callers iterate assets in a loop and walk each shared subtree at most once. Internally, every node also adds itself to `visited` on first visit so further parents short-circuit. - `recursive: false` makes the call O(1) per asset for the common case where the asset is about to be GC'd anyway. - `hash` and `size` default to `false` (kept) — they're cheap to hold and expensive to rebuild. - `_cachedMaps.clear()` replaces `new Map()` to avoid per-call allocation churn. Updated benchmark (`benchmark/memory/clear-cache.mjs`) adds a shared-modules scenario mirroring the reviewer's webpack shape. On 50 chunks × 1000 shared modules: - naive `clearCache()` 12.7 ms - `clearCache(opts, visited)` 4.4 ms (2.9× speedup) - `clearCache({ recursive: false })` 0.1 ms (188× speedup) `CompatSource.clearCache` now forwards `visited` through to wrapped SourceLike implementations so dedup is preserved across CompatSource boundaries. Public `Source.prototype.clearCache` JSDoc documents the concurrency contract and that subsequent reader calls may repopulate caches. Tests grow to 22 (was 14) — covers dedup, the granular options, the opt-out recursion, and that `_cachedMaps` is reused not reallocated. https://claude.ai/code/session_01LLtSGKaynui1P1wQsfVnXx
Addresses the polish items from the second review of PR #221: - **#2 post-minifier asset shape benchmark.** New Scenario 3 in `benchmark/memory/clear-cache.mjs` constructs the shape webpack assets actually present at the PROCESS_ASSETS_STAGE_DEV_TOOLING hook: a `CachedSource` wrapping a `SourceMapSource` whose `_cachedSource` (bundle string) and `_cachedMaps[{}]` (composed map) are populated. 50 chunks: 8.5 MB baseline → 0 MB after `clearCache({ maps: true, source: false, recursive: false })`. - **#3 optional `parsedMap: true` flag** on `ClearCacheOptions`. When set, `SourceMapSource.clearCache` drops the parsed object forms of `_sourceMapAsObject` and `_innerSourceMapAsObject` (heaviest cached representation), but only when a serialized form (buffer or string) is also held so data stays recoverable. Defaults to `false` to preserve the cheap toString re-hydration path described in the comment. - **#4 `getCachedData()` after `clearCache()` interaction test.** Pins the observable shape: buffer + source = undefined, maps empty, hash + size preserved when their flags default to false. Round-trips through `new CachedSource(source, data)` to confirm cleared cached data is still a valid input. - **#5 "release hint" framing** in the `Source.prototype.clearCache` JSDoc. Opens with "clearCache is a memory hint: it never affects correctness or output, only how expensive the next read is." - **#6 dedup correctness test** already lives at `test/clearCache.js:251` ("a shared subtree is walked once when a `visited` WeakSet is passed") with a negative control at :269. Tests grow to 25 (was 22). Full suite 89,870/89,870 pass, lint clean. Benchmark numbers unchanged on Scenarios 1 + 2: 44.7% heap reduction on unique tasks, 3.6× dedup speedup, 228× `recursive: false` speedup. https://claude.ai/code/session_01LLtSGKaynui1P1wQsfVnXx
Adds a parallel benchmark runner for memory-shaped tinybench tasks,
wired into CI as a second job in benchmarks.yml that invokes the
CodSpeed action with `mode: "memory"` (introduced in
@codspeed/core@5.2.0, already pinned in this repo via 5.4.0). This
gives the memory benchmarks the same first-class treatment the CPU
benchmarks already get: PR comments, historical tracking on
codspeed.io, dashboard alongside the existing simulation results.
Layout (per user preference, kept separate from benchmark/cases/):
benchmark/memory/<case>/
index.bench.mjs tinybench tasks, discovered by run-memory.mjs;
what CodSpeed memory instrument measures
snapshot.mjs ad-hoc developer script with process.memoryUsage()
snapshots and absolute MB output (the old
standalone, renamed and re-pathed)
The two entry points serve different audiences: bench files for CI
tracking, snapshot.mjs for "how many MB did I save on my laptop".
New: benchmark/run-memory.mjs discovers benchmark/memory/*/
index.bench.mjs the same way benchmark/run.mjs handles cases/. Uses
fewer warmup iterations (memory measurement does not benefit from JIT
warmup the way instruction counting does — we want the allocation
pattern, not the post-JIT throughput). Locally, without the CodSpeed
runner present, falls through to plain wall-clock tinybench output —
useful as a smoke test only; documented as such.
Five tasks shipped with the clear-cache memory bench:
- unique tasks (no clearCache) baseline heap growth
- unique tasks (clearCache default) drops source + maps
- unique tasks (clearCache maps only) webpack-side call shape
- shared modules (no visited) allocates per chunk
- shared modules (visited) single allocation
CI workflow gains a parallel memory-benchmark job. Same checkout +
npm ci, then `npm run benchmark:memory` under CodSpeedHQ/action with
`mode: "memory"`. README updated with the two-entry-point split.
https://claude.ai/code/session_01LLtSGKaynui1P1wQsfVnXx
Shrinks the clearCache API surface. The previous shape had six flags
(maps, source, hash, size, parsedMap, recursive) covering 64 possible
combinations. Webpack's real call sites only need two: drop everything
(default) and drop only map data while keeping source (the
post-minifier shape, where downstream plugins still read the source).
Changes:
- `ClearCacheOptions` is now `{ mapsOnly?: boolean }`. Default false
= drop cached source/buffer, cached maps, normalized maps, and the
parsed object form of source maps when a buffer or string form
survives so the data remains recoverable. `mapsOnly: true` keeps
source-side caches and only drops map-related state.
- Recursion is always on. The visited-set dedup is cheap (~14× faster
than the naive walk on the shared-modules pathology), so the
`recursive: false` perf escape hatch is no longer needed.
- Hash and size caches are never dropped. They were already default-
off under the old flags (cheap to hold, expensive to recompute) and
removing the flags makes that policy explicit.
- The parsed object form of SourceMapSource maps is now dropped by
default when a buffer or string form is also held. Re-parsing JSON
is more expensive than re-`toString`-ing, but the caller has already
signalled "I want memory back" by calling clearCape at all; keeping
the heaviest cached representation around defeats the purpose. If
no serialized form survives, the parsed object stays (correctness).
API surface: ~170 fewer lines across lib/ and 50 fewer in tests for
the same coverage of real-world call shapes. Three tests for
flag-specific behavior dropped (recursive=false, hash=true,
size=true, parsedMap=true), two reframed for the new default
(parsed-object-form drop when buffer present / kept when no
serialized form).
Numbers unchanged on the scenarios that matter:
- 44.7% baseline heap reduction on unique tasks
- 14.5× speedup on shared-modules dedup (visited set vs naive)
- 8.5 MB → 0 MB on post-minifier asset shape with mapsOnly: true
Full suite: 89,868/89,868 pass. Lint clean.
https://claude.ai/code/session_01LLtSGKaynui1P1wQsfVnXx
Adds one memory-mode bench file per Source class under
benchmark/memory/<source>/index.bench.mjs. Each registers tinybench
tasks that exercise the genuinely allocation-significant operations
for that class, so the CodSpeed memory instrument tracks them with
the same first-class treatment clear-cache already gets.
Coverage (31 new tasks across 9 new cases):
- raw-source — constructor (string/buffer), lazy buffer()
materialisation, updateHash payload
- original-source — constructor, map() columns:true vs false,
sourceAndMap (the heavy path)
- source-map-source — constructor (simple/with-inner), map(),
sourceAndMap with combined inner map
- replace-source — construct + 100 spread insertions, source(),
map() bsearch splice
- concat-source — construct N children, source(), buffers(),
map() composition across SourceMapSource kids
- prefix-source — construct, source() string rewrite via
buildPrefixed, buffer() conversion
- cached-source — cold vs warm sourceAndMap, getCachedData()
BufferedMap allocation, constructor from
pre-built cachedData (persistent-cache path)
- compat-source — wrapper construction, delegated source/map,
CompatSource.from() short-circuit
- size-only-source — constructor (included for completeness;
accidental Source growth surfaces here first)
All cases follow the existing clear-cache shape: per-task `sink`
arrays inside beforeAll/afterAll so heap baselines are independent,
batch loops in the task body so the instrument captures a
deterministic allocation pattern per iteration.
Refactored clear-cache/index.bench.mjs to import from the existing
benchmark/fixtures.mjs (was inlining its own fixture loading), so the
memory benches share one source of fixture truth with the CPU benches.
README updated with a per-case table.
Local smoke test: 35 tasks register, all run cleanly under
walltime (no CodSpeed). CodSpeed memory mode in CI will record peak
heap, total allocations, and allocation timeline per task.
https://claude.ai/code/session_01LLtSGKaynui1P1wQsfVnXx
…20963 The previous simplification collapsed every option into a single `mapsOnly` boolean. That works semantically but doesn't match the call shape webpack/webpack#20963 already wrote: source.clearCache({ maps: true, source: false, parsedMap: true }) That PR's SourceMapDevToolPlugin change is what delivers the reported 27% peak-heap reduction (1422 MB → 1041 MB) and 9% RSS reduction on a 50-chunk × 1000-module build, and it relies on controlling each flag independently — `source: false` to keep the JS bundle string available for downstream plugins, `parsedMap: true` to drop the heaviest cached representation (the parsed object form) on top of the serialized one. Restored: - `maps?: boolean` (default `true`) — drop cached source maps. On CachedSource, clears `_cachedMaps`. On SourceMapSource, drops `_sourceMapAsString` / `_innerSourceMapAsString` when their buffer counterparts also exist. - `source?: boolean` (default `true`) — drop cached source/buffer copies. On CachedSource, clears `_cachedSource`/`_cachedBuffer`. On SourceMapSource, OriginalSource, RawSource, drops the redundant string form when both string and buffer are held. - `parsedMap?: boolean` (default `false`) — on SourceMapSource, additionally drops the parsed object form (`_sourceMapAsObject`, `_innerSourceMapAsObject`) when a buffer or string form survives. Defaults off because re-parsing JSON is significantly more expensive than `toString` from a buffer; the webpack PR opts in explicitly. Kept simplified (dropped earlier and not restored): - No `hash` flag — hash payload is small and expensive to rebuild; never dropped. - No `size` flag — single number, never dropped. - No `recursive` flag — always recurse; visited-set dedup keeps the cost negligible (~14× faster than naive when the same module appears in many chunks). Test count: 24 in test/clearCache.js (was 23). The `parsedMap` flag gets three tests: default-off, explicit-on with buffer present, and explicit-on as a no-op when no serialized form survives. Benchmark scenarios updated to use the webpack call shape `{ maps: true, source: false, parsedMap: true }`. Memory savings identical to before: 44.7% baseline heap reduction on unique tasks, 15.5× dedup speedup, 8.5 MB → 0 MB on post-minifier asset shape. Full suite: 89,869/89,869 pass. Lint clean. https://claude.ai/code/session_01LLtSGKaynui1P1wQsfVnXx
New memory bench under benchmark/memory/webpack-20961/ that reproduces the SourceMapDevToolPlugin asset shape from webpack/webpack#20961 (the issue webpack/webpack#20963 fixes): N chunks, each a CachedSource over a ConcatSource of M shared modules, with sourceAndMap({ columns: true }) warmed so the asset carries `_cachedSource`, `_cachedMaps[{}]`, and the per-module SourceMapSource parsed object forms — the actual shape webpack assets present at PROCESS_ASSETS_STAGE_DEV_TOOLING. Three tasks measure the spectrum: 1. baseline — warm all chunks, hold all live. Establishes peak heap with every cached representation retained. 2. PR #20963 call shape — source.clearCache({ maps: true, source: false, parsedMap: true }) after each chunk warm. `source: false` keeps the bundle string available for downstream plugins (Terser/compression/hashing); `maps: true` drops the composed sourcemap; `parsedMap: true` drops the parsed object form (heaviest representation), still recoverable from the buffer form on re-read. This is the exact call that delivered the reported 1422 MB → 1041 MB (-27%) peak heap on webpack's 50×1000 synthetic run. 3. full clearCache() — lower bound on per-chunk peak heap; the downstream-plugin path would re-walk the underlying source. Dimensions scaled to 10 chunks × 20 shared modules so the Valgrind-instrumented CodSpeed memory pass finishes in reasonable CI time. The relative delta between tasks is what the dashboard tracks, not the absolute heap. Local wall-clock smoke: 131 / 80 / 80 ms per task (warmup allocation dominates the wall-clock; CodSpeed memory mode will show the per-task peak heap and total allocation delta that the PR optimizes). https://claude.ai/code/session_01LLtSGKaynui1P1wQsfVnXx
There was a problem hiding this comment.
Pull request overview
This PR adds a new Source.prototype.clearCache() API across webpack-sources to allow consumers (notably webpack’s SourceMapDevToolPlugin flow) to proactively release per-instance cached data between tasks/chunks, reducing heap growth in long-running builds. It also introduces a memory benchmark harness and scenarios to track the impact in CI via CodSpeed memory mode.
Changes:
- Add
Source#clearCache(options?, visited?)(no-op on base) and implement recursive cache releasing inCachedSource, composite sources, and leaf sources with dual string/buffer or parsed-map caches. - Add Jest coverage for
clearCachebehavior, includingvisiteddedup traversal and cache-preservation options. - Add memory benchmark runner + benchmark cases, CI workflow job, and documentation for running/maintaining memory benchmarks.
Reviewed changes
Copilot reviewed 28 out of 29 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
types.d.ts |
Adds ClearCacheOptions type and clearCache method signatures to exported typings. |
lib/Source.js |
Defines ClearCacheOptions typedef and adds base-class no-op clearCache. |
lib/CachedSource.js |
Implements cache dropping for _cachedSource / _cachedMaps and recursion into wrapped sources with optional visited. |
lib/ConcatSource.js |
Propagates clearCache to child sources (with optional dedup via visited). |
lib/PrefixSource.js |
Propagates clearCache to inner source (with visited). |
lib/ReplaceSource.js |
Propagates clearCache to inner source (with visited). |
lib/CompatSource.js |
Forwards clearCache to wrapped “source-like” when supported. |
lib/RawSource.js |
Drops redundant secondary cached representation (string vs buffer) when safe. |
lib/OriginalSource.js |
Drops cached string when buffer is also cached, preserving rehydration behavior. |
lib/SourceMapSource.js |
Drops redundant string forms when buffers exist; optionally drops parsed map objects when recoverable. |
test/clearCache.js |
Adds comprehensive Jest tests for cache clearing behavior and visited-dedup traversal semantics. |
package.json |
Adds benchmark:memory script. |
eslint.config.mjs |
Adjusts benchmark linting overrides (notably disables some Node feature and JSDoc rules for benchmark files). |
benchmark/run-memory.mjs |
Adds benchmark runner that discovers benchmark/memory/*/index.bench.mjs. |
benchmark/README.md |
Documents memory benchmark layout, CI integration, and available cases. |
benchmark/memory/clear-cache/index.bench.mjs |
Adds CodSpeed-shaped memory tasks measuring clearCache scenarios and visited-dedup. |
benchmark/memory/clear-cache/snapshot.mjs |
Adds standalone developer snapshot script for heap/RSS comparisons across scenarios. |
benchmark/memory/webpack-20961/index.bench.mjs |
Adds a memory benchmark reproducing webpack/webpack#20961 asset shapes and the PR #20963 call pattern. |
benchmark/memory/source-map-source/index.bench.mjs |
Adds memory benchmarks for SourceMapSource construction and map/sourceAndMap allocation paths. |
benchmark/memory/size-only-source/index.bench.mjs |
Adds memory baseline benchmark for SizeOnlySource. |
benchmark/memory/replace-source/index.bench.mjs |
Adds memory benchmarks for ReplaceSource construction and allocation-heavy operations. |
benchmark/memory/raw-source/index.bench.mjs |
Adds memory benchmarks for RawSource construction and common allocation paths. |
benchmark/memory/prefix-source/index.bench.mjs |
Adds memory benchmarks for PrefixSource construction and accessors. |
benchmark/memory/original-source/index.bench.mjs |
Adds memory benchmarks for OriginalSource construction and mapping paths. |
benchmark/memory/concat-source/index.bench.mjs |
Adds memory benchmarks for ConcatSource construction and accessors. |
benchmark/memory/compat-source/index.bench.mjs |
Adds memory benchmarks for CompatSource wrapping/delegation and from() behavior. |
benchmark/memory/cached-source/index.bench.mjs |
Adds memory benchmarks for CachedSource cold/warm behavior and cachedData operations. |
.github/workflows/benchmarks.yml |
Adds CI job to run CodSpeed memory benchmarks (mode: "memory"). |
.changeset/clear-cache-api.md |
Declares a minor release for the new clearCache API and documents option semantics. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (clearSource) { | ||
| this._cachedSource = undefined; | ||
| this._cachedSourceType = undefined; | ||
| this._cachedBuffer = undefined; |
There was a problem hiding this comment.
Addressed in e4c6fd5: getCachedData() now always calls this.buffer(), which returns _cachedBuffer when populated and rehydrates via the wrapped source otherwise. The CachedData.buffer: Buffer contract holds in every state, including after clearCache() drops _cachedBuffer here.
Generated by Claude Code
| * | ||
| * Run with: | ||
| * | ||
| * node --expose-gc benchmark/memory/clear-cache.mjs |
Two Copilot review findings on #221: 1. CachedSource.getCachedData() was returning { buffer: undefined } after a default clearCache() (because both _cachedSource and _cachedBuffer get dropped). That contradicts the CachedData type, which declares `buffer: Buffer` as required, and breaks any persistent-cache writer that calls getCachedData() on a cleared source. Fix: always call this.buffer() in getCachedData() — it returns _cachedBuffer when populated, or rehydrates via the wrapped source when both caches have been cleared. Cheap when not needed, correct when needed. The existing test asserted the broken behavior (`expect(data.buffer).toBeUndefined()`) — replaced with one that asserts the contract: `Buffer.isBuffer(data.buffer)` and the buffer contents match the source. 2. benchmark/memory/clear-cache/snapshot.mjs's run-with example pointed at the pre-reorg path `benchmark/memory/clear-cache.mjs`. Updated to the current `benchmark/memory/clear-cache/snapshot.mjs`. Full suite: 89,869/89,869 pass. Lint clean. https://claude.ai/code/session_01LLtSGKaynui1P1wQsfVnXx
| clearCache() { | ||
| this.calls.clearCache++; | ||
| this._inner.clearCache(); |
| // Pre-built cachedData reused across iterations; we measure only | ||
| // the constructor side. Webpack hits this path when restoring | ||
| // from persistent cache. | ||
| const seed = warmCaches[0].getCachedData(); | ||
| for (let i = 0; i < BATCH; i++) { |
Two new comments on the e4c6fd5 review pass: 1. test/clearCache.js — TrackedSource.clearCache() dropped the `options` and `visited` arguments instead of forwarding them to the wrapped source. The helper is used to assert recursion counts; if a future code change forgot to thread options or visited through, tests using TrackedSource would have masked the regression. Now forwards both. 2. benchmark/memory/cached-source/index.bench.mjs — the "construct from cachedData" task computed `seed = warmCaches[0].getCachedData()` inside the timed body, so getCachedData()'s BufferedMap allocations were attributed to the constructor measurement. Moved seed construction into beforeAll so the task now isolates the constructor-from- cachedData allocation pattern (the path webpack hits when restoring assets from persistent cache). Local checks: clearCache tests 24/24, full lint clean, cached-source memory bench runs cleanly. The third Copilot comment (CachedSource:466 getCachedData buffer contract) is already resolved by e4c6fd5; the bot re-flagged the same line because the diff context didn't change. https://claude.ai/code/session_01LLtSGKaynui1P1wQsfVnXx
| memory-benchmark: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| pull-requests: write | ||
| steps: |
…arCache After extracting the source+map for each chunk, call `Source#clearCache` on the asset's source tree to release the composed map and parsed `SourceMapSource` representations that `sourceAndMap()` populated. A single `WeakSet` is shared across every call so each shared module subtree is walked at most once — measured at -30% wall time vs a per-call WeakSet on a synthetic build with 1000 shared modules × 50 chunks. Bumps the `webpack-sources` dependency to `^3.5.0` where the `clearCache` API landed (webpack/webpack-sources#221). Refs #20961
…arCache After extracting the source+map for each chunk, call `Source#clearCache` on the asset's source tree to release the composed map and parsed `SourceMapSource` representations that `sourceAndMap()` populated. A single `WeakSet` is shared across every call so each shared module subtree is walked at most once — measured at -30% wall time vs a per-call WeakSet on a synthetic build with 1000 shared modules × 50 chunks. Bumps the `webpack-sources` dependency to `^3.5.0` where the `clearCache` API landed (webpack/webpack-sources#221). Refs #20961
…arCache After extracting the source+map for each chunk, call `Source#clearCache` on the asset's source tree to release the composed map and parsed `SourceMapSource` representations that `sourceAndMap()` populated. A single `WeakSet` is shared across every call so each shared module subtree is walked at most once — measured at -30% wall time vs a per-call WeakSet on a synthetic build with 1000 shared modules × 50 chunks. Bumps the `webpack-sources` dependency to `^3.5.0` where the `clearCache` API landed (webpack/webpack-sources#221). Refs #20961
Mirrors the fix to webpack's SourceMapDevToolPlugin (webpack/webpack#20961).
CachedSourceretains composed source map data per chunk in_cachedMaps;without a way to release it, plugins that hold assets reachable for size /
hash work cannot reclaim those bytes between tasks.
clearCache()is a no-op on the base class.CachedSourcedrops allinternal caches and recurses into its wrapped source. Composite sources
(Concat / Prefix / Replace / Compat) propagate to children, and leaf
sources (Raw / Original / SourceMap) drop dual-cached secondary
representations.
A standalone memory benchmark under
benchmark/memory/clear-cache.mjsreproduces the upstream scenario; on a 200-task workload it shrinks heap
growth from 58.4 MB to 32.3 MB (~45%).
https://claude.ai/code/session_01LLtSGKaynui1P1wQsfVnXx