⚡ Speed up MapArbitrary generate hot path#7023
Conversation
Two focused changes on `MapArbitrary.generate`: - Drop the `[mappedValue, sourceValue]` tuple previously created by `mapperWithCloneIfNeeded`. The only consumer (`valueMapper`) already has access to `v` for the source value, so the tuple is pure per-call allocation pressure and is removed. `mapperWithCloneIfNeeded` now returns `U` directly. - Inline the non-cloneable fast path into `generate()`. Most mapper outputs are primitive (string, number, plain object) and have no `cloneMethod` symbol; we no longer go through the helper-call frame or the cloneable-wiring branches in that case. The general path is preserved for cloneable values and for shrink-stream consumers via a small `bindValueMapper` helper.
|
@fast-check/ava
fast-check
@fast-check/jest
@fast-check/packaged
@fast-check/poisoning
@fast-check/vitest
@fast-check/worker
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7023 +/- ##
=======================================
Coverage 94.82% 94.83%
=======================================
Files 212 212
Lines 5876 5885 +9
Branches 1541 1543 +2
=======================================
+ Hits 5572 5581 +9
Misses 296 296
Partials 8 8
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:
|
Description
Arbitrary.prototype.map(mapper, unmapper)(backed byMapArbitrary) is one of the most heavily-used composition primitives in fast-check.fc.string,fc.json,fc.uuid,fc.emailAddress,fc.webAuthority,fc.mixedCase, plus most user-built arbitraries derived via.map(...)all funnel throughMapArbitrary.generate. Anything we shave there is paid back many times over by everything downstream.What changes
[mappedValue, sourceValue]tuple inmapperWithCloneIfNeeded. The helper used to return a 2-element array so that the cloneable wiring could re-runmapper(sourceValue)on subsequent reads. The only call site (valueMapper) already holds the sourceValue<T>(the variablev) and can produce the source value itself, so the tuple is pure per-call allocation pressure. The helper now returnsUdirectly.generate(). Most mapper outputs are primitives or plain objects with nocloneMethodsymbol; the cloneable wiring is dead code for them. We branch once onmappedValue(and on the sourcehasToBeCloned) and skip the helper-call frame plus the cloneable branches entirely. The general path is preserved for cloneable values and for shrink-stream consumers via a smallbindValueMapperhelper.Background reading
Observable behaviour
No public API change. Same seed → same generated value, same shrink ordering.
canShrinkWithoutContextreturns the same booleans (the unmapper path is untouched).Tests pass unchanged:
test/unit/check/arbitrary/definition/Arbitrary.utest.spec.tstest/unit/check/arbitrary/definition/Arbitrary.itest.spec.tsTotal: 29/29 (the broader
test/unit/check/sweep — 268/268 — also passes).Numbers
Median of 5–7 runs × 2 s, paired against
main. The wins are largest for trivial mappers (where the per-call overhead dominates the mapper cost) and steadily shrink as the mapper itself becomes more expensive — exactly the expected shape:integer().map(x => x + 1)integer().map(String)integer().map(x => x + 1, x => x - 1)(with unmapper)nat().map(x => x*2).map(x => x+1)(chained)array(integer()).map(arr => arr.join(','))shrink first 10Object-out mappers (
tuple(int,int).map(([a,b]) => ({a,b}))) and large-array string mappers come out as flat-to-mildly-positive within the run-to-run noise floor (≈ ±5%), with no observed regression.Re-runnable benchmark:
Ideas considered but dropped
The agent also tried:
canShrinkWithoutContextcheap tests,isSafeContext,.map(f).map(g)at construction,bindValueMapper,all of which were either <3% wins, not on the hot generate path, or risked behaviour change. They were reverted before commit.
Why this is patch-level
No API change. No behaviour change for a given seed. One file touched (
Arbitrary.ts, only theMapArbitraryclass section). The existing 29 tests already pin the contract this PR preserves.Checklist
— Don't delete this checklist and make sure you do the following before opening the PR
pnpm run bumpor by following the instructions from the changeset bot🐛(vitest) Something...) when the change targets a package other thanfast-checkGenerated by Claude Code