Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

markerikson
Copy link
Collaborator

Stacked on top of #1164 (perf tweaks) and #1183 (finalization callbacks).

This PR:

  • adds a set of interception overrides for both mutating and non-mutating array methods that operate directly on the underlying copy, in order to avoid creating proxies every time we read and write to an array index.

Implementation

We have a known list of both mutating and non-mutating array methods in proxy.ts. We check field access in the get trap, and if it's one of the known array methods, track which method to indicate we're in an array operation.

We then manually implement the operation by calling the method on the underlying state.copy_, or in some cases reimplementing some of the behavior. For example, filter does a manual iteration and calls the predicate with the existing values to avoid accessing the draft array and creating proxies for every accessed field. But, for consistency and further updates, it does then do result.push(state.draft[i]) every time the predicate returns true. That means we avoid some proxy creation overhead and only create proxies for values included in the result array. For methods like sort and reverse, we mutate the copy_ array, and manually set the assigned_ indices, avoiding any access to the drafted array and thus no proxy creation for fields.

This gives a significant speedup for almost all array benchmark scenarios.

Note that for filter / find etc, we're now passing likely-raw values into the callbacks. If the user were to mutate those, they'd be really mutated and we wouldn't prevent or pick up on that. Since those are semantically read-only operations and you shouldn't be mutating in those callbacks in the first place, this seems like a reasonable tradeoff to me.

I've opted to not override map / flatMap / reduce / reduceRight. Those can return arbitrary values, and also added noticeably more bundle size from implementation. I decided it wasn't worth the complexity to optimize those methods.

Performance

Running the perf benchmarks with this PR, I get:

┌─────────────────────┬──────────────┬──────────────┬─────────────┐
│ Scenario            │ immer10      │ immer10Perf  │ Improvement │
├─────────────────────┼──────────────┼──────────────┼─────────────┤
│ reverse-array       │      117.5µs │       14.4µs │      +87.7% │
│ sortById-reverse    │      128.0µs │       17.7µs │      +86.2% │
│ remove-high         │      119.6µs │       16.9µs │      +85.8% │
│ remove              │      128.4µs │       20.9µs │      +83.7% │
│ update-high         │       89.7µs │       14.7µs │      +83.7% │
│ filter              │       49.1µs │       11.7µs │      +76.1% │
│ remove-reuse        │        4.1ms │        1.2ms │      +69.5% │
│ remove-high-reuse   │        3.9ms │        1.2ms │      +69.4% │
│ update-reuse        │        3.5ms │        1.3ms │      +63.6% │
│ mixed-sequence      │        3.5ms │        1.3ms │      +61.8% │
│ update-high-reuse   │        3.3ms │        1.4ms │      +59.3% │
│ concat              │      117.1µs │       51.2µs │      +56.3% │
│ rtkq-sequence       │       12.5ms │        7.0ms │      +44.1% │
│ updateLargeObject   │      235.7µs │      132.1µs │      +44.0% │
│ update-multiple     │       47.1µs │       27.9µs │      +40.8% │
│ updateLargeObject-r │        9.6ms │        6.0ms │      +37.6% │
│ add                 │       24.4µs │       17.0µs │      +30.4% │
│ update              │       22.7µs │       18.6µs │      +18.1% │
│ mapNested           │      120.0µs │      128.8µs │       -7.3% │
└─────────────────────┴──────────────┴──────────────┴─────────────┘

✓ immer10Perf shows an average 57.4% performance improvement over immer10

That's a very sizeable 30%+ increase over #1183 , which is not surprising given that all the array behaviors suddenly have significantly less overhead.

Bundle Size

Per #1183, the series of PRs does increase bundle size noticeably:

Eyeballing bundle sizes, this PR increases the immer.production.js minified bundle size by another 1-1.5K on top of #1183, from ~14K to ~15K. If I build a Vite app and measure using Sonda, actual size in a built app appears to have grown from:

As I said, I'm very sensitive to bundle size changes. I have been assuming we would probably want to make this PR into a new plugin, so that users can opt-in to whether they want the extra bundle size to gain better array perf.

Right now the plugin system only has methods for loading and getting a plugin. We'd probably want to add a util to see if a plugin is currently loaded, roughly like const isPluginLoaded = (name: string) => !!plugins[name]. That way we can do a quick check inside of get. Alternately, we could have proxy.ts expose a setter to indicate this plugin is available, and have the plugin logic update that flag when this plugin is loaded.

@coveralls
Copy link

Pull Request Test Coverage Report for Build 18533775572

Details

  • 186 of 199 (93.47%) changed or added relevant lines in 2 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+2.6%) to 47.787%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/core/proxy.ts 184 188 97.87%
src/utils/common.ts 2 11 18.18%
Totals Coverage Status
Change from base Build 18515105801: 2.6%
Covered Lines: 1670
Relevant Lines: 4185

💛 - Coveralls

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.

2 participants