Skip to content

test: add benchmarks for cache fill cost and retained cache size - #769

Open
veksa wants to merge 2 commits into
reduxjs:masterfrom
veksa:test/cache-fill-and-memory-benchmarks
Open

test: add benchmarks for cache fill cost and retained cache size#769
veksa wants to merge 2 commits into
reduxjs:masterfrom
veksa:test/cache-fill-and-memory-benchmarks

Conversation

@veksa

@veksa veksa commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

The benchmarks we have call each selector with 30 unique arguments, twice over, so filling the cache and reading it back land in the same number. Nothing measures how a miss gets more expensive as the cache grows, and nothing measures cache size at all — the one attempt at it, describe.skip('weakMapMemoize memory leak') in weakMapMemoize.bench.ts, has been skipped since it was written.

So this adds two benchmark files.

cacheWarmup.bench.ts measures the two paths separately. The cold suite clears the cache in tinybench's beforeEach, which runs outside the timed region, so every measured call is a miss; the warm suite fills the cache once in setup and never clears it. Plus a sweep over 1k/10k/100k keys, and a startup suite that creates 1,000 selectors, calls them for the first time, then calls them warm. Iterations are 30 rather than the 10 the other files use — filling a cache allocates, and GC lands on random iterations.

2,000 unique arguments               cold        warm
  weakMapMemoize                   0.73 ms     0.04 ms
  lruMemoize (maxSize: 1)          0.73 ms     0.81 ms
  lruMemoize (maxSize: 2,000)     58.24 ms    23.77 ms

fill cost as the key count grows   weakMap   lruMemoize
  1,000 keys                       0.33 ms      0.35 ms
  10,000 keys                      5.77 ms      3.87 ms
  100,000 keys                    67.50 ms     36.38 ms

startup
  createSelector, 1,000 instances                0.59 ms
  first call, 1,000 fresh instances              1.36 ms
  steady state call, 1,000 warm instances        0.03 ms

cacheMemory.bench.ts reports retained heap instead of time — the hz/mean columns are not meaningful there, which the file says at the top:

weakMapMemoize, 10,000 entries      2.01 MB   (210 bytes/entry)
lruMemoize maxSize: 1               0.04 MB   (4 bytes/entry)
lruMemoize maxSize: 10,000          2.15 MB   (225 bytes/entry)

object arguments, refs dropped      1.02 MB
object arguments, refs kept        15.56 MB
primitive arguments                14.94 MB   (157 bytes/entry)
  after clearCache()                0.00 MB

bench now runs through node --expose-gc, the same way test already does, since the memory numbers need forced collection. Without the flag they warn and still run.

The second commit adds scripts/weakMapMemoizeHeapSnapshot.mjs (yarn heap-snapshot). The benchmarks above say how much a filled cache retains; this says what is in it. It fills a selector the way #635 describes — one long-lived object argument, one primitive argument that keeps taking new values — then writes a V8 heap snapshot and prints a per-type summary:

$ yarn heap-snapshot --entries=50000
memoize: weakmap   cached entries: 50,000   heapUsed: 25.72 MB

object type                        count   shallow size
object / Object                  101,730        9.23 MB
array / (object elements)         50,109        7.33 MB
object / Array                    50,150        1.53 MB

$ yarn heap-snapshot --entries=50000 --memoize=lru
memoize: lru       cached entries: 50,000   heapUsed:  4.26 MB
(no Object/Array rows in the top twelve)

Two cache nodes per entry, one in the argsMemoize tree and one in the memoize tree, plus the result each of them holds. The written .heapsnapshot loads into Chrome DevTools for retained sizes and retainer chains, which is what the printed shallow sizes cannot give you. Flags: --memoize=weakmap|lru, --entries=N, --out=path; *.heapsnapshot is gitignored.

This commit is separable: drop it and the benchmarks still stand.

Two caveats worth stating up front. The result functions here are deliberately cheap, so the numbers reflect memoization bookkeeping rather than payload — a selector whose result function does real work will look different, and the warm column is where weakMapMemoize pays off. And timings move between runs; the 100,000-key row ranged from 67 ms to 92 ms on the same machine, so compare rows within one run rather than numbers across runs.

Related: #635.

@netlify

netlify Bot commented Aug 4, 2026

Copy link
Copy Markdown

Deploy Preview for reselect-docs canceled.

Name Link
🔨 Latest commit ace5407
🔍 Latest deploy log https://app.netlify.com/projects/reselect-docs/deploys/6a724878436f160008d496fc

@codesandbox-ci

codesandbox-ci Bot commented Aug 4, 2026

Copy link
Copy Markdown

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant