Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Poor Performance of Classes vs Plain JavaScript Objects in Immer 10.0.2 vs 6.0.3 #1071

Closed
bradedelman opened this issue Sep 18, 2023 · 16 comments

Comments

@bradedelman
Copy link

bradedelman commented Sep 18, 2023

🐛 Bug Report

This is sample that illustrates a problem we are having with our real use of Immer, but is simplified for this issue report.

We have a piece of data with 12 simple numeric fields.
We have a version of it that is a plain JavaScript object.
We have a version of it that is a JavaScript class.

In Immer 6.0.2, mutating with produce is about the same speed with both the Class Instance and Plain JS Object.
In Immer 10.0.2, mutating with produce, the class instance is about 5x slower than the Plain JS Object.

Note that in Immer 9.0.21, plain objects are slower than 6.0.2, but about the same speed as Class Instances, so it seems this is an active area of development. It's nice that 10.0.2 is faster than 6.0.2 for Plain JS Object. However, the perf issue that remains for class instances is significant.

We are targeting 120,000 object mutations per second (on top of other work) and that 5x multiple has a meaningful, negative performance impact. (We have 2000 objects and there's a target 60 FPS update).

The class is very simple here. Nothing fancy. No getters, etc.

class Foo {
	[immer.immerable] = true;	
  	a = 1;
  	b = 2;
  	c = 3;
  	d = 4;
  	e = 5;
  	f = 6;
  	g = 7;
  	h = 8;
  	i = 9;
  	j = 10;
  	k = 11;
  	l = 12;
}

vs an instance of this plain JS Object:

{
  "a": 1,
  "b": 2,
  "c": 3,
  "d": 4,
  "e": 5,
  "f": 6,
  "g": 7,
  "h": 8,
  "i": 9,
  "j": 10,
  "k": 11,
  "l": 12
}

Link to repro

(be sure to view the JS Fiddle Console to see the console output)

Here's our sample code running JS Fiddle with Immer 6.0.3:
https://jsfiddle.net/csz37eb0/1/

"class", 232.2000002861023
"object", 221
"x slower with class", 1.0506787343262547

Here's our sample code running JS Fiddle with Immer 10.0.2:
https://jsfiddle.net/bop856g1/2/

"class", 489.5
"object", 93.7000002861023
"x slower with class", 5.2241195144649675

To Reproduce

Sample code provided in the JS Fiddles.

Observed behavior

mutating a simple class instance is 5x slower than equivalent plain JS Object.
5x is significant. Also, Immer 6.0.3 had them about the same speed.

Expected behavior

Same or almost same speed for a simple class like this.

Environment

  • Immer version 10.0.2
  • Mac M1
  • Chrome Version 116.0.5845.187.
@bradedelman bradedelman changed the title Very Poor Performance of Classes vs Plain JavaScript Objects in Immer 10.0.2 vs 6.0.3 Poor Performance of Classes vs Plain JavaScript Objects in Immer 10.0.2 vs 6.0.3 Sep 18, 2023
@mweststrate
Copy link
Collaborator

mweststrate commented Sep 19, 2023

This likely relates to the setUseStrictShallowCopy(true); has been defaulted back to false in 10.0 for plain objects. Possibly it should be possible to enable loose mode for classes as well, but for classes it is a bit more dangerous than plain objects.

@mweststrate
Copy link
Collaborator

Closing for inactivity

@bradedelman
Copy link
Author

inactivity? I mean, who is supposed to work on this? I reported the issue, and it's still an issue. I'd like to see this worked on :-)

@mweststrate
Copy link
Collaborator

There was no response to the follow-up question I asked.

And if you really want to play that card; no one in particular is is supposed to work on this, the software is provided as-is and I owe my time to no one. Sorry for being salty; and maybe you didn't mean it that way, but entitlement of users to a specific issue to be worked on after you have already given your work and time away for free is really frustrating as a maintainer. I mean, in the end you are probably on someones payroll to make whatever product you are working on faster, I'm not. So feel free to investigate the difference and put up a PR. "I'd like to see this worked on" is something you might be able to bring up as a convincing motivation at your job, but not in an open source community.

@bradedelman
Copy link
Author

I'm not trying to be snarky or "play a card"! What was the question asked? The only comment I see

"This likely relates to the setUseStrictShallowCopy(true); has been defaulted back to false in 10.0 for plain objects. Possibly it should be possible to enable loose mode for classes as well, but for classes it is a bit more dangerous than plain objects."

doesn't read like a question.

I spent quite a lot of time tracking down and writing the examples that show the performance issue. If there's a way I can help, I'm open to that, though I was hoping that the developers who had done the work and had the context might be able to improve it, given it was faster in the past.

Thanks.

@mweststrate
Copy link
Collaborator

Ok that is good to know.

The question was basically whether setUseStrictShallowCopy was used or not. 10.0.4 might expose different perf characteristics btw, as the iteration mechanism slightly changed (didn't find a difference in my tests). Over time we did change the iteration method a few times, and the bottleneck is probably getOwnPropertyDescriptors right now, which is slow but also the one that doesn't miss any props, nor finds anything that it shouldn't (properties coming from a prototype etc). So I'd expect that a large deal of the speed loss is the cost of being correct in the corner cases (which is also why the option was added to disable parts of that)

@bradedelman
Copy link
Author

bradedelman commented Mar 11, 2024

You can see the code from the tests in the jsfiddle's linked in the original issue report.
setUseStrictShallowCopy(true);
was not used.

If I add that to the version of the test that used Immer 10.0.2, indeed it is faster. 1.21x slower with class vs. 5.33x slower without it. So indeed, that is the lion's share of the difference.

And yes, do think I saw getOwnPropertyDescriptors show up in my profiling.

Any advice on how to determine whether our use of Immer will be impacted by setUseStrictShallowCopy(true);?

You do explain it somewhat above, but what I'd be curious for is how to most easily determine whether our logic is dependent on the factors you mention.

@mweststrate mweststrate reopened this Mar 11, 2024
@mweststrate
Copy link
Collaborator

This solution might be worth further investigation https://jsfiddle.net/ht9xm2zf/, see #1107. Didn't think through yet what it'll break though.

@bradedelman
Copy link
Author

bradedelman commented Mar 11, 2024

right, that's what I had just tried. and yes, it makes it faster.
however, to consider it, I'll have to "think through yet what it'll break".
since we ran ok with immer 6.0.2, does that suggest we are likely not going to break?

also, does the change impact non-classes?

@mweststrate
Copy link
Collaborator

Our suite of exotic scenarios grew vastly over the years, so things that might not have been working correctly in 6, but were irrelevant for your case, might now fail simply because they are tested, where they weren't in the past. E.g. own, string based property is like 98% of the cases that Immer needs to handle. But the remaining 2% is a state explosion of string / number / symbol * enumerable / non-enumerable * own / proto * value / getter / setter / getter + setter configurations that all have impact on reflection. So that is 48 scenarios that need to be work correctly, multiplied by the flags Immer itself offers. And not all of them were considered in the past.

@bradedelman
Copy link
Author

ok, thanks. we are going to test with setUseStrictShallowCopy(true); and I suspect that will address it for us.
sorry that I did not understand that you were asking us to try that last September. I thought you were leaving an "internal comment".
I think given the information provided and our initial resaults/understanding, it's ok if you prefer to close the issue. I guess the alternative would be to provide a way to "test if this object requires the more complex logic" so that there was a clear(er) criteria for evaluating use of setUseStrictShallowCopy.

@mweststrate
Copy link
Collaborator

Did you try the fiddle I linked above (note: with useStrictShallowCopy(false))? It does restore the same perf characteristics as before, it might be worth validating that in your real project (version 10.0.4-beta). So that does link interesting to explore further and rollout. I'll need some time to properly create that in a backward compatible manner etc.

Screenshot 2024-03-12 at 20 10 20

@bradedelman
Copy link
Author

yes, I tried it and commented yesterday (above) that indeed it restored the performance.

@fantasticsoul

This comment was marked as spam.

@mweststrate

This comment was marked as off-topic.

mweststrate added a commit that referenced this issue Apr 27, 2024
…1071

Immer 10.x solved slow iteration for plain JS objects. This update applies the same handling to class instances. In cases this makes class instance handling 3 times faster. Note that this slightly modifies the behavior of Immer with classes in obscure corner cases, in ways that match current documentation, but do not match previous behavior. If you run into issues with this release icmw. class instances, use `setUseStrictShallowCopy("class_only")` to revert to the old behavior. For more details see https://immerjs.github.io/immer/complex-objects#semantics-in-detail
@mweststrate
Copy link
Collaborator

mweststrate commented Apr 27, 2024

Perf improvements have landed as https://github.com/immerjs/immer/releases/tag/v10.1.0! Thanks for surfacing this @bradedelman (despite my lacklustre response)!

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

No branches or pull requests

3 participants