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

perf: optimize reactive state handlers #69

Merged
merged 6 commits into from
Nov 1, 2021
Merged

Conversation

nolanlawson
Copy link
Contributor

This is a performance optimization for two reasons:

  1. We avoid creating an extra object ({ readOnly: ..., reactive: ... }). Instead, we just keep track of the readOnly and reactive functions separately.
  2. We avoid calling Object.defineProperty when the getter is called (since we don't have an object, it's no longer needed).

In the perf benchmarks, this is a 1-7% improvement and no regression:

Screen Shot 2021-10-29 at 10 20 03 AM

This benchmark uses this PR on top of v1.0.1 (to avoid including changes in v1.1.3), with Tachometer set to 1% horizon, 20 min timeout, and 100 min samples, on a MacBook Pro running Chrome 95.

@@ -72,7 +66,8 @@ export class ReactiveMembrane {
valueObserved: ReactiveMembraneAccessCallback = defaultValueObserved;
valueIsObservable: ReactiveMembraneObservableCallback = defaultValueIsObservable;
tagPropertyKey: ProxyPropertyKey | undefined;
private objectGraph: WeakMap<any, ReactiveState> = new WeakMap();
private readOnlyObjectGraph: WeakMap<any, any> = new WeakMap();
private reactiveObjectGraph: WeakMap<any, any> = new WeakMap();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having an extra object here doesn't seem to be a big deal. We only ever create one ReactiveMembrane object, so this amounts to one extra WeakMap.

// when trying to extract the writable version of a readonly
// we return the readonly.
return o.readOnly === value ? value : o.reactive;
return readOnly === value ? value : this.getReactiveHandler(unwrappedValue, distorted, false);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a bit worried that this might actually be a perf regression (due to calling WeakMap.get() twice instead of once), but in our perf benchmarks at least, it doesn't seem to have a negative impact. I'm guessing this code path isn't hit very frequently.

Copy link
Contributor

@jodarove jodarove Oct 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this code path is only hit for @track fields, and we do not have any perf test with @track.

@nolanlawson This makes me wonder if we could remove the creation of the readOnly proxy in this routine? my mental model is: this check is in place for when we want to getProxy of a readonly proxy, therefore it should already be in the readOnlyObjectGraph, and if the value is not in the readOnlyObjectGraph, then we should create a this.getReactiveHandler(unwrappedValue, distorted, false).

Even if is not visible in the perf tests (because all of their values are @api/readonly), for cases in which the values are set to a writable proxy (ex: data from lds) or @track foo = {...}, it will be noticeable, since we won't create a read-only proxy when we shouldn't (saves mem + processing time)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the details! I'll add a benchmark for @track. I think it's important to test this use case.

As for not creating the extra readOnly, your explanation makes sense to me. We should be able to validate it with a @track benchmark.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jodarove Made the change you recommended, and added a couple tests for safety. I'll re-run the perf tests with @track included.

Copy link
Contributor

@jodarove jodarove left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, we can remove the (unnecessary) creation of the readOnly handler in the getProxy routine by checking if the distorted value is in the readOnlyObjectGraph and matches value. When it is not, we should return the writable proxy.

@nolanlawson
Copy link
Contributor Author

@jodarove I re-ran the benchmark with the change you recommended, using a slight tweak to the perf tests to cover @track. Looks like it's an even bigger perf improvement than before!

Screen Shot 2021-10-29 at 2 23 37 PM

I wouldn't worry about that 1% regression; it's only a 0.4ms difference so probably just noise.

@nolanlawson
Copy link
Contributor Author

Ran on this new benchmark (salesforce/lwc#2560), it also shows an improvement there (10%):

Remote Desktop Picture October 29, 2021 at 4 04 35 PM PDT

Copy link
Contributor

@jodarove jodarove left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is great!

let reactiveState = objectGraph.get(distortedValue);
if (reactiveState) {
return reactiveState;
private getReactiveHandler(value: any, distortedValue: any, readOnly: boolean): any {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid boolean arguments, instead just pass the corresponding map, that should also make the code more readable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or make it two methods

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I rather prefer two methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can make it two methods. It just requires duplicating ~5 lines of code, which is no big deal.

Copy link
Contributor

@caridy caridy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@nolanlawson nolanlawson merged commit cb575cd into master Nov 1, 2021
@ravijayaramappa ravijayaramappa deleted the nolan/reactive-state branch November 2, 2021 21:12
const handler = new ReadOnlyHandler(this, distortedValue);
proxy = new Proxy(createShadowTarget(distortedValue), handler);
registerProxy(proxy, value);
this.readOnlyObjectGraph.set(distortedValue, proxy);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any specific reason to use the distortedValue as the key instead of the value?

IIRC, the original implementation does not have an invariant that two values cannot produce the same distorted value.
nvm, I see the original implementation also maintained a map of distorted value -> ReactiveState. So you are not introducing any new behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ravijayaramappa Correct, yeah, it's the same as before.

I'm not really sure how the distortion works, though. In LWC, we don't do any distortion, and the tests in this repo don't do a lot of distortion either. Arguably we could release a breaking change for this library to just get rid of the distortion entirely and use the raw value everywhere (assuming there aren't any major consumers of this library that are relying on the distortion).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with removing distortions.

@nolanlawson nolanlawson mentioned this pull request Nov 3, 2021
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.

None yet

4 participants