Prevent updating values in cache when they do not change#191
Prevent updating values in cache when they do not change#191
Conversation
| for (let i = stateMappingKeys.length; i--;) { | ||
| const subscriber = callbackToStateMapping[stateMappingKeys[i]]; | ||
| if (!subscriber || !isKeyMatch(subscriber.key, key)) { | ||
| if (!subscriber || !isKeyMatch(subscriber.key, key) || (_.isFunction(canUpdateSubscriber) && !canUpdateSubscriber(subscriber))) { |
There was a problem hiding this comment.
NAB: Is this new logic the same thing as using _.result(subscriber, 'canUpdateSubscriber')? https://underscorejs.org/#result
Maybe not... I think I am thinking of this: https://github.com/Expensify/expensify-common/blob/main/lib/Func.jsx#L13-L19 which would be cool if we could just import it without any additional work :D I don't think that's possible though.
There was a problem hiding this comment.
_.result() takes a property name as it's second arg so it's a little different
What you are asking sounds similar to Str.result() and I think would work if we did:
Str.result(canUpdateSubscriber, subscriber)
and would return undefined if the first argument was undefined
| // If the value in the cache is the same as what we have then do not update subscribers unless they | ||
| // have initWithStoredValues: false then they MUST get all updates even if nothing has changed. | ||
| if (!cache.hasValueChanged(key, value)) { | ||
| notifySubscribersOnNextTick(key, value, subscriber => subscriber.initWithStoredValues === false); |
There was a problem hiding this comment.
| notifySubscribersOnNextTick(key, value, subscriber => subscriber.initWithStoredValues === false); | |
| notifySubscribersOnNextTick(key, value, !subscriber.initWithStoredValues); |
Why does the third argument need to be a function? Can it be a simple boolean instead?
There was a problem hiding this comment.
It doesn't have to be a function. I had it as a simple boolean, but feel the function is more intuitive and also puts the context for why we are doing what we are doing where we are doing it. Check this commit to get a feel for what I mean:
lib/OnyxCache.js
Outdated
| */ | ||
| hasValueChanged(key, value) { | ||
| this.addToAccessedKeys(key); | ||
| return !_.isEqual(this.storageMap[key], value); |
There was a problem hiding this comment.
Did you by any chance explore other deep equal solutions? If we're doing this for performance reasons, then maybe we should try to find the fastest one?
There was a problem hiding this comment.
I explored this one dequal and it does seem faster. But decided to use _.isEqual() since we already have the underscore dependency.
There was a problem hiding this comment.
So I went and benchmarked a few of these today and tried to compare what I think are our biggest data types here's what I came back with:
Comparing two sets of very large personalDetails with one user displayName changed
_.isEqual() x 1,248 ops/sec ±0.43% (96 runs sampled)
dequal() x 277 ops/sec ±2.30% (86 runs sampled)
deepEqual() x 1,231 ops/sec ±0.65% (93 runs sampled)
fastDeepEqual() x 1,130 ops/sec ±0.84% (93 runs sampled)
microDiff() x 214 ops/sec ±1.58% (87 runs sampled)
Fastest is _.isEqual()
Comparing two report objects one with a different lastVisitedTimestamp
_.isEqual() x 1,857,706 ops/sec ±0.17% (92 runs sampled)
dequal() x 2,556,428 ops/sec ±0.35% (93 runs sampled)
deepEqual() x 2,457,190 ops/sec ±0.33% (96 runs sampled)
fastDeepEqual() x 1,768,857 ops/sec ±0.09% (99 runs sampled)
microDiff() x 795,347 ops/sec ±0.19% (96 runs sampled)
Fastest is dequal()
Comparing two reportActions objects each with 250 comments and one with an additional comment
_.isEqual() x 258,482 ops/sec ±0.39% (95 runs sampled)
dequal() x 3,394 ops/sec ±0.47% (97 runs sampled)
deepEqual() x 260,473 ops/sec ±0.98% (94 runs sampled)
fastDeepEqual() x 268,251 ops/sec ±0.33% (96 runs sampled)
microDiff() x 1,828 ops/sec ±0.41% (99 runs sampled)
Fastest is fastDeepEqual()
So going off of these numbers it looks to me like:
dequalis trailing hard for very large objects, but best at small stuff_.isEqualseems pretty comparable to other libs at the large stuff
Based on that I think it's not really worth using dequal or any of the others. I'd be more concerned about performance with these really large objects and underscore isn't too far behind the competition.
There was a problem hiding this comment.
deepEqual looks like the best option here, no?
Given this is super low level code that will get called everywhere, I think is worth to add a new dependency for the sake of performance
There was a problem hiding this comment.
Cool I'm good with that. I wasn't sure if there was enough of a meaningful difference between that and _.isEquals()
tests/unit/onyxTest.js
Outdated
| }) | ||
| .then(() => { | ||
| // WHEN merge is called again with an object of equivalent value but not the same reference | ||
| Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_POLICY}${1}`, _.clone(collectionUpdate.test_policy_1)); |
There was a problem hiding this comment.
I think this makes sense, that a different reference wouldn't trigger the callback. I've seen so many bugs in the past where code thinks a variable is one reference, but it ends up being another, and there is stale data... It makes me leary, but until it's an actual problem, I think it's OK.
There was a problem hiding this comment.
Mainly wanted to show here that whether it's a reference or not makes no difference since we are doing a value comparison and not reference comparison. In the previous test, we pass a reference and in the new test we pass a new object. Both have the same value so both should not trigger any data to be set or updates to occur.
I've seen so many bugs in the past where code thinks a variable is one reference, but it ends up being another, and there is stale data... It makes me leary, but until it's an actual problem, I think it's OK.
I don't think we have any problems like this or I have not observed any at least. I am pretty confident we are always creating new object references before saving them to the cache (at least when using the Onyx.merge() method)
e.g.
Line 766 in 0846e16
|
Tested on iOS, Android, and Web and updated the switch_report metrics in the description. Going to take this out of draft. |
lib/OnyxCache.js
Outdated
| */ | ||
| hasValueChanged(key, value) { | ||
| this.addToAccessedKeys(key); | ||
| return !_.isEqual(this.storageMap[key], value); |
There was a problem hiding this comment.
deepEqual looks like the best option here, no?
Given this is super low level code that will get called everywhere, I think is worth to add a new dependency for the sake of performance
|
Updated and using the |
|
re-tested and ready for another review whenever 🙇 |
|
@PauloGasparSv @iwiznia all yours |
|
Looks like @iwiznia is ooo but does not have any unresolved comments so going to pull the merge trigger on this one. |
cc @iwiznia @tgolen
Details
isEqual()whenever writing data - which seems like it would be "inefficient", but the savings from avoiding re-renders appear much greater than any performance hit caused by the value equality check.Related Issues
https://github.com/Expensify/Expensify/issues/230464
Automated Tests
Added some automated tests to show that we will prevent updates when the values don't change.
Linked PRs
TODO, but the changes can be tested against
mainBenchmarks
While timing a
switch_reportevent on Android these are some before and after readingsScreenshots
Android
Before
DEBUG Timing:expensify.cash.switch_report.cold 6078
DEBUG Timing:expensify.cash.switch_report.cold 6699
DEBUG Timing:expensify.cash.switch_report.cold 7158
DEBUG Timing:expensify.cash.switch_report.cold 7359
DEBUG Timing:expensify.cash.switch_report.cold 7606
After
DEBUG Timing:expensify.cash.switch_report.cold 2850
DEBUG Timing:expensify.cash.switch_report.cold 2872
DEBUG Timing:expensify.cash.switch_report.cold 3051
DEBUG Timing:expensify.cash.switch_report.cold 3218
DEBUG Timing:expensify.cash.switch_report.cold 3372
iOS
Before
DEBUG Timing:expensify.cash.switch_report.cold 1708
DEBUG Timing:expensify.cash.switch_report.cold 1724
DEBUG Timing:expensify.cash.switch_report.cold 1739
DEBUG Timing:expensify.cash.switch_report.cold 1631
DEBUG Timing:expensify.cash.switch_report.cold 1656
After
DEBUG Timing:expensify.cash.switch_report.cold 1176
DEBUG Timing:expensify.cash.switch_report.cold 1134
DEBUG Timing:expensify.cash.switch_report.cold 1098
DEBUG Timing:expensify.cash.switch_report.cold 1135
DEBUG Timing:expensify.cash.switch_report.cold 1092
Web
Before
Timing:expensify.cash.switch_report.cold 297
Timing:expensify.cash.switch_report.cold 276
Timing:expensify.cash.switch_report.cold 281
Timing:expensify.cash.switch_report.cold 278
Timing:expensify.cash.switch_report.cold 284
After
Timing:expensify.cash.switch_report.cold 186
Timing:expensify.cash.switch_report.cold 188
Timing:expensify.cash.switch_report.cold 186
Timing:expensify.cash.switch_report.cold 180
Timing:expensify.cash.switch_report.cold 185