Repository navigation
perf: change the copy of a Set draft directly, and keep the changes of a Set draft that left its key - #195
Merged
Conversation
|
Coverage after merging perf/set-drafts into main will be
Coverage Report
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Coverage after merging perf/set-drafts into main will be
Coverage Report
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
…ubclass overrides
|
Coverage after merging perf/set-drafts into main will be
Coverage Report
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
…s add() must store
… without searching them first
…s drafts that keep their order
… the rebuild and subclass changes
|
Coverage after merging perf/set-drafts into main will be
Coverage Report
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Coverage after merging perf/set-drafts into main will be
Coverage Report
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This was referenced Oct 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #168.
Set drafts were one of the few places where Immer was faster: without auto-freeze or patches, adding or deleting an item was 5–12% slower, and the performance page listed it among the 9 cases that Immer won. The first change or iteration of a Set draft built a Map of all its items (
new Map(original.entries()), 542 µs at 10,000 items), every change went through that Map, and finalization cleared the copy and added every item back from it (408 µs), even when no item had to be replaced. The copy made at the first change was discarded.finalizeNodeskipped such a draft, and only a draft found at its key rebuilt its copy. A Set draft that left its key now finalizes its items at its own entry, before the new place takes its value. The change in 2 depends on it: without it, a draft added to a moved Set would stay in the next state as a revoked proxy.add,deleteandclearchange the copy, which holds the items in order, andhas,sizeand the iterators read it.setMapmaps only the original items that an iterator drafted, to their drafts, and the objects that the recipe added, drafts among them, to themselves. Finalization rebuilds the Set in order only once one of those items has a different final value. Patch paths under the items of an unchanged Set take their positions from the original Set, which no rebuild changes.haslooks a value up before it inspects it, so a value that is an item, even a draft that its producer revoked, is found as onmain.main. Only a draft of another producer, which may change after the Set's producer finished, rebuilds it again when its own producer finalizes it; for that, the first rebuild keeps the items in order onsetMap, so drafts keep the size they have onmain.add,has,deleteandclear, as Map drafts do. The copy holds the original items in their order, whatever the subclass's constructor oradddoes with them, and the draft,current(), patch paths and the rebuild iterate the items as a Set holds them, so patches replay onto the right items also when the subclass's constructor orvalues()orders them otherwise. As a Map subclass'ssetalready must onmain, a Set subclass'saddmust store the value that it receives:mainpassed the items of a changed Set throughaddonly after the recipe, so anaddthat stores a copy could receive drafts there, while here it leaves the drafts in the next state. The migration guide notes both.createimport and from 8.3 to 8.4 kB for all ESM exports, which measure 8.52, 7.50 (7,495 bytes) and 8.37 kB. Againstmain, in Brotli bytes: the production builds grow by 133 (CommonJS, 8,201 to 8,334, of which 11 for fix 1), 138 (ESM) and 140 (UMD), the consumer bundles by 138–148, and the development builds by 183–200.Results
Mutative alone, the source of
mainate6af4b0and of this branch in alternating order, each scenario in processes of its own, three runs, Apple M1 Max, Node 24.16.0, µs per update:mainmainAll 32 Set cells were faster, 0.28 of the time of
mainon geometric mean. With every library in processes of its own, the change is faster than Immer in every Set cell, 2.9–51.1 times (set-addat 10,000 items: 34.1 µs against 929 µs), and faster than the hand-written reducer inset-addat 1,000 and 10,000 items.set-addat 10,000 items allocates 325 KiB per update, against 2,878 KiB for Mutative 1.3.0 (Immer 1,607 KiB). With these cells in place of the earlier ones, the published comparison becomes 548 / 12 / 6 of 566 against Immer (geometric mean 3.62, was 536 / 21 / 9 and 3.37) and 142 / 0 / 3 of 145 with each library's defaults (6.62, was 5.98); Immer is faster in 6 cases instead of 9.Iterating every item of a Set of objects and changing one, which the suite does not measure, takes 0.96–0.99 of the time of
mainat 100 items and 1.01–1.05 at 1,000 and 10,000 items (2,945 µs against 2,827 µs at 10,000): the rebuild looks each item up in the map of drafted items, wheremainwalked its map of all items. Reading every item takes 0.91–1.00 of the time. With patches, changing 100 items of an unchanged Set of 1,000 objects takes 106 µs, against 291 µs onmain(Mutative alone, medians in one process), as the patch path of each item counts its position without copying the Set.Adding 50 changed drafts to a Set of numbers rebuilt the Set once per draft on
main; it is now rebuilt once, whether the drafts were created before or after the Set draft: 111 µs instead of 1,901 µs at 1,000 numbers and 684 µs instead of 22,846 µs at 10,000 (Mutative alone, medians in one process). A draft takes 331.8 bytes, as onmain.Correctness
mainplus fix 1 only. The first ran 40,000 random Set recipes in production builds and 10,000 in development builds:has,size,add,deleteandclearwith primitives, items, drafts from other paths and objects holding drafts, every iteration form, partial iteration, changes during iteration, nested Sets, Maps and arrays,current(),union(), Sets moved, shared and detached, auto-freeze, patches with array and string paths, strict mode and marks. It compared the reads, errors, results with their sharing of base objects, freezing, patches and their replay, and the untouched base: 0 differences. The second ran 16,000 recipes in production and 8,000 in development builds with nested producers, among them outer recipes that go on changing a Set after a nested producer finalized a draft that the Set holds or a place that holds the Set, Set subclasses, returned values,rawReturn(), strict mode withunsafe(), marks, frozen bases andapply()on drafts, warnings included: 0 differences.mainitself, the fuzzers differ in 252 of the 40,000 recipes of the first and 1,471 of the 16,000 of the second, the same counts as betweenmainandmainplus fix 1: the recipes wheremainloses the changes of a moved Set draft.values()orders the items otherwise replay their patches onto the right items in both directions, with auto-freeze and with array and string paths, as onmain; a subclass whoseaddstores a copy of what it receives fails as described in item 5.main: the 7 for moved Set drafts, the 2 that check that adding, deleting and reading items neither index nor rebuild the Set, the one that checks that added drafts rebuild a Set once, and the one for a subclass'saddandhasduring the recipe. The other 8 guard the order of changed items, drafts of an outer producer that a Set holds, changes that an outer recipe makes after a nested producer finalized a draft that a Set holds or a place that holds the Set, the positions of patch paths after an early rebuild and in Set subclasses that order their items otherwise, an item that is a revoked draft, and a nested producer that copies a draft of a Set subclass.Not covered
set-addwith patches at 10,000 items takes 452 µs, against 33.2 µs without (Immer 1,347 µs). The membership changes recorded inassignedMapcould produce those patches without the full comparison.set-updateat 10,000 items takes 544 µs, against 411 µs for the hand-written reducer. The patch path of each changed item of an unchanged Set still counts its position in the original Set.main(see Results). When no item was added or deleted, walking the map of drafted items alongside the original Set instead of looking each item up would remove that, for about 44 more bytes.addstores something other than the value it receives cannot receive drafts (item 5). Restoring the behavior ofmainfor subclasses, with the methods of a Set during the recipe and the subclass's own methods only when the Set is rebuilt, would take about 125 more bytes.createimport measures 7,495 of its 7,500 bytes, so the next change to it is likely to raise that cap.