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
88 changes: 44 additions & 44 deletions README.md

Large diffs are not rendered by default.

19 changes: 18 additions & 1 deletion perf-testing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ The runner never upgrades dependencies implicitly. Reports record the actual ins

`--patches off|on|both` defaults to `off`, preserving the original plain-update benchmark. `benchmark:immer:check` checks both patch modes and both freeze modes (all selected scenarios with all four libraries, each in the modes it supports). `--library all` (the default) measures candidate, pinned v1, Immer, and the hand-written reducer; `--library both` retains the original candidate/Immer comparison. `--patches on` measures patch generation with automatic freezing off and on; `--patches both` additionally measures the plain-update baseline in the same processes. The JSON schema is version 2, with `enablePatches` on each trial and summary; the summarizer still reads the original version 1 archives as patches off.

### Replacement returns with default freeze settings

The [replacement-return comparison](./reports/SUMMARY.md#replacement-returns-with-rawreturn-and-default-freeze-settings) pairs Mutative's `return-replace-raw` with auto-freeze off and Immer's `return-replace` with auto-freeze on, both with patches off. `rawReturn()` explicitly promises that the whole returned value contains no drafts. Immer receives an ordinary replacement value and pre-frozen inputs and payloads; the first freeze of external data is outside timing.

To reproduce this comparison and its ordinary-return controls, build once and run the three sizes sequentially:

```sh
pnpm build
pnpm benchmark:immer:build
for rows in 100 1000 10000; do
node --expose-gc --enable-source-maps perf-testing/dist/immutability-benchmarks.mjs --check --library both --freeze both --patches off --filter '^return-replace(-raw)?$' --array-size "$rows"
node perf-testing/run-benchmarks.mjs --isolate --runs 3 --library both --freeze both --patches off --filter '^return-replace(-raw)?$' --array-size "$rows" --output "perf-testing/results/return-defaults-$rows.json"
done
```

Each generated report includes both scenarios with matched freeze modes. For the comparison across default settings, select `summary`'s `return-replace-raw` entry with `autoFreeze: false` and `enablePatches: false`, then read `libraries.mutative.medianMeanNs`; select `return-replace` with `autoFreeze: true` and `enablePatches: false`, then read `libraries.immer.medianMeanNs`. Divide Immer's time by Mutative's for the speed ratio, and calculate `100 * (1 - Mutative / Immer)` for the percentage of time saved. The reports' existing `immerOverMutative` fields compare the same scenario and freeze mode and therefore describe a different comparison.

## Workloads and units

| Group | Scenarios | Reducer calls per iteration |
Expand Down Expand Up @@ -113,7 +130,7 @@ The `search-*` scenarios find the last row of an array of nested rows and update
- The hand-written reducer is the reference implementation of every scenario: spreads, `slice`, `map`, and new `Map` and `Set` copies, with plain loops for the values that recipes read through drafts. It runs with freeze and patches off only, since it neither freezes nor produces patches. It is an ordinary reducer, not the fastest possible code.
- Immer needs `enableMapSet()` for Map and Set values. Once loaded, Immer calls the plugin while finalizing every draft: `small-object-update` measured about 4% slower with it (0.67 µs instead of 0.64 µs), 100-row reads and updates unchanged. Map and Set scenarios therefore run in a process of their own within each run, and memory workers load the plugin only for them, so other scenarios measure Immer without it. Reports record `immerMapSetEnabled`.
- Immer drafts the class instances through a static `immerable` property; Mutative through its `mark` option, which returns `immutable` for those classes and is passed to `apply` as well. Mutative copies such an instance property by property with its descriptors; Immer 11 copies it with `Object.assign` onto its prototype, since its `useStrictShallowCopy` option is off by default.
- Objects with hundreds of properties are sensitive to the object shapes V8 keeps from the code that ran earlier in the same process. Inserting a property into the 1,000-property object took the hand-written reducer 1.7 µs when it ran after the libraries and 130–133 µs when it ran first; Immer took 2.7 µs after the hand-written reducer and 152–155 µs after Mutative. Alternating the library order between runs does not cancel such effects. `--isolate` gives each scenario and library a process of its own, so every measurement follows only its own validation and priming. Code that runs beside a library changes its speed in other scenarios too: with all libraries and scenarios in one process, every library ran slower, by different amounts, so the summary reports isolated results for every scenario and keeps the shared-process runs as a control.
- Objects with hundreds of properties are sensitive to the object shapes V8 keeps from the code that ran earlier in the same process. Inserting a property into the 1,000-property object took the hand-written reducer 1.7 µs when it ran after the libraries and 124–130 µs when it ran first; Immer took 2.7–2.8 µs after the hand-written reducer and 146–148 µs after Mutative. Alternating the library order between runs does not cancel such effects. `--isolate` gives each scenario and library a process of its own, so every measurement follows only its own validation and priming. Code that runs beside a library changes its speed in other scenarios too: with all libraries and scenarios in one process, every library ran slower, by different amounts, so the summary reports isolated results for every scenario and keeps the shared-process runs as a control.
- Auto-freeze **off** means unfrozen inputs and payloads for both libraries.
- Auto-freeze **on** means deeply pre-frozen inputs and payloads, and actual output freezing enabled for both libraries. This measures ongoing immutable state updates; it excludes the first full-graph freeze of cold external data. Map and Set inputs are frozen like Immer's results: replaced mutators and `Object.freeze`. The candidate freezes collection instances too. The pinned Mutative v1 baseline only replaces their mutators, and the checks accept either form.
- Every trial owns its input graph. Each timed sample starts at the same immutable base. Reuse and mixed scenarios evolve the returned state only within that sample; state does not grow across measurement samples.
Expand Down
2 changes: 1 addition & 1 deletion perf-testing/reports/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Performance evidence and archives

This directory contains two maintained documents: this archive index and the [current performance summary](./SUMMARY.md). The summary records the October 6 and 7 cross-library measurements of `main`, with the Set scenarios measured again on October 8 for [PR #195](https://github.com/unadlib/mutative/pull/195), and its [history](./SUMMARY.md#history) summarizes earlier batches, including the October 5 base/head matrix of PR #184, whose paired CI measurements are uploaded by the [PR checks](https://github.com/unadlib/mutative/pull/184/checks). Benchmark source, tests, pinned versions, [`budgets.json`](../budgets.json), and the [build-size baseline](../../scripts/build-size-baseline.json) remain versioned. Historical measurements are not inputs to CI: the performance jobs measure their actual base/head checkouts and upload their own results.
This directory contains two maintained documents: this archive index and the [current performance summary](./SUMMARY.md). The summary records the October 9 and 10 cross-library measurements of `main` at `ce0c25e`, and its [history](./SUMMARY.md#history) summarizes earlier batches, including the October 6 and 7 batch with the cells that PRs #195, #196 and #198 measured again or added, and the October 5 base/head matrix of PR #184, whose paired CI measurements are uploaded by the [PR checks](https://github.com/unadlib/mutative/pull/184/checks). Benchmark source, tests, pinned versions, [`budgets.json`](../budgets.json), and the [build-size baseline](../../scripts/build-size-baseline.json) remain versioned. Historical measurements are not inputs to CI: the performance jobs measure their actual base/head checkouts and upload their own results.

## Retention policy

Expand Down
Loading
Loading