-
Notifications
You must be signed in to change notification settings - Fork 27
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
Conversation
@@ -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(); |
There was a problem hiding this comment.
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
.
src/reactive-membrane.ts
Outdated
// 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
f692aa1
to
d64791f
Compare
@jodarove I re-ran the benchmark with the change you recommended, using a slight tweak to the perf tests to cover I wouldn't worry about that 1% regression; it's only a 0.4ms difference so probably just noise. |
Ran on this new benchmark (salesforce/lwc#2560), it also shows an improvement there (10%): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is great!
src/reactive-membrane.ts
Outdated
let reactiveState = objectGraph.get(distortedValue); | ||
if (reactiveState) { | ||
return reactiveState; | ||
private getReactiveHandler(value: any, distortedValue: any, readOnly: boolean): any { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
const handler = new ReadOnlyHandler(this, distortedValue); | ||
proxy = new Proxy(createShadowTarget(distortedValue), handler); | ||
registerProxy(proxy, value); | ||
this.readOnlyObjectGraph.set(distortedValue, proxy); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
This is a performance optimization for two reasons:
{ readOnly: ..., reactive: ... }
). Instead, we just keep track of thereadOnly
andreactive
functions separately.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:
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.