Skip to content

test: paired hot-path benchmark for createSelector - #770

Open
veksa wants to merge 1 commit into
reduxjs:masterfrom
veksa:bench/create-selector-hot-path
Open

test: paired hot-path benchmark for createSelector#770
veksa wants to merge 1 commit into
reduxjs:masterfrom
veksa:bench/create-selector-hot-path

Conversation

@veksa

@veksa veksa commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Our benchmarks run on tinybench, which is good at "which memoizer is faster" but can't answer "did this commit make createSelector 6% slower". That's the number I kept wanting while poking at the hot path, so this adds a harness for it: yarn bench:hot-path.

It builds src twice — the working tree and a baseline git ref — and measures both in the same process, back to back. That part turned out to matter: when I ran them as two processes, a 10% swing got credited to a commit that hadn't touched the code being measured. Rounds are interleaved across cases, the statistic is the minimum, and the noise floor is measured per case (rounds split in half, gap between their minima) rather than assumed.

Every case also carries a recomputation counter checked against expectation, so a variant that looks fast because it quietly skipped work can't pass as a win. It caught three bugs in my own cases while I was writing them.

Identical code on both sides, as a sanity check:

case base current change
1 input, (state) 12.0 12.1 1.0% noise
3 inputs, (state, props) 218.3 218.4 0.1% noise
1 input, (state, props) 179.1 180.6 0.8% noise
nested 13.3 13.1 −1.5% noise

So about ±1.5% on a quiet machine. On a busy one the gate widens on its own and refuses to call anything a result, which is the failure mode I wanted.

Where we actually are (ns/call, production):

case reselect hand-written
1 input, (state) 12.0 4.6
1 input, (state, props) 179.1 11.2
same, argsMemoize: lruMemoize 78.7

The gap between those first two rows is the interesting bit. A slice selector called with just the state hits the argument cache on all but the tick's first call. A parametric one never hits it — new state every tick, different props every call — so it allocates a cache node and does a WeakMap.set per call, and ends up 16x above a hand-written keyed memo.

yarn bench:hot-path:dev runs the same thing with the dev checks left in, since they sit in the hot path and a production build can't show what they cost.

Two things I left out on purpose. There's no allocation column: heapUsed deltas measure what the collector hasn't freed yet, and on a change that removed two allocations per call it went up. And the clock covers the state-producing step as well as the calls, which dilutes every difference — excluding it looked obviously right and reported one case 22% slower that the same code had just measured 5% faster, because GC pauses land wherever the allocation threshold falls.

bench/profile.mjs is in here too. It's what showed me the array allocation and arity dispatch I'd assumed were the problem were 1.3% of the call.

@netlify

netlify Bot commented Aug 7, 2026

Copy link
Copy Markdown

Deploy Preview for reselect-docs canceled.

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

@codesandbox-ci

codesandbox-ci Bot commented Aug 7, 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