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

Feature: Onyx Cache #76

Merged
merged 40 commits into from
Jun 7, 2021
Merged

Conversation

kidroca
Copy link
Contributor

@kidroca kidroca commented May 25, 2021

@marcaaron, @tgolen

Details

Added a new file and defined OnyxCache class to encapsulate cache related methods and have some reuse
Update Onyx methods to use cache
Updated Onyx methods to deal with concurrent method calls while data is still being read
Added tests

Related Issues

GH_LINK
Fixes #63
Expensify/App#2762

Automated Tests

Added test annotated with GIVEN, WHEN, THEN. They should be pretty self explanatory
Covering OnyxCache and the integration in Onyx and AsyncStorage calls

Linked PRs

Copy link
Contributor Author

@kidroca kidroca left a comment

Choose a reason for hiding this comment

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

There are still some things to complete
Like clearing cached keys when there are no subscribers for a given key
Some existing tests started to fail after the update, I'll take a look tomorrow

Copy link
Contributor

@marcaaron marcaaron left a comment

Choose a reason for hiding this comment

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

Just leaving some initial comments since it seems like you are still working through stuff.

Copy link
Contributor Author

@kidroca kidroca left a comment

Choose a reason for hiding this comment

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

Thanks for the review @marcaaron
Added some replies

@kidroca kidroca force-pushed the kidroca/onyx-cache branch from 61c0c82 to 5435063 Compare May 27, 2021 18:32
@kidroca
Copy link
Contributor Author

kidroca commented May 27, 2021

Updated
I've left some threads open for potential discussions
The only thing remaining otherwise is a 26 of the test I've written for the cache use async/await syntax I find the tests more readable that way, but can convert them to promise chains if you want to

@kidroca
Copy link
Contributor Author

kidroca commented Jun 3, 2021

Addressed the outstanding comment by explicitly converting the set to array

Copy link
Contributor

@Jag96 Jag96 left a comment

Choose a reason for hiding this comment

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

LGTM, will leave open to let @Julesssss get his review in

Copy link
Contributor

@Jag96 Jag96 left a comment

Choose a reason for hiding this comment

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

Re-tested this today on the latest E.cash main on an account that has an IOU and I'm getting this error on both web and desktop after signing in (iOS/Android work fine):

TypeError: Currency code is required with currency style.
    at new NumberFormat (<anonymous>)
    at oO (app-6bf1371e025ae4112ce5.bundle.js:78)
    at Object.numberFormat (app-6bf1371e025ae4112ce5.bundle.js:78)
    at IOUBadge (app-6bf1371e025ae4112ce5.bundle.js:78)
    at Yi (app-6bf1371e025ae4112ce5.bundle.js:50)
    at gs (app-6bf1371e025ae4112ce5.bundle.js:50)
    at lc (app-6bf1371e025ae4112ce5.bundle.js:50)
    at sc (app-6bf1371e025ae4112ce5.bundle.js:50)
    at Zs (app-6bf1371e025ae4112ce5.bundle.js:50)
    at app-6bf1371e025ae4112ce5.bundle.js:50

Uncaught (in promise) TypeError: Currency code is required with currency style.
    at new NumberFormat (<anonymous>)
    at oO (app-6bf1371e025ae4112ce5.bundle.js:78)
    at Object.numberFormat (app-6bf1371e025ae4112ce5.bundle.js:78)
    at IOUBadge (app-6bf1371e025ae4112ce5.bundle.js:78)
    at Yi (app-6bf1371e025ae4112ce5.bundle.js:50)
    at gs (app-6bf1371e025ae4112ce5.bundle.js:50)
    at lc (app-6bf1371e025ae4112ce5.bundle.js:50)
    at sc (app-6bf1371e025ae4112ce5.bundle.js:50)
    at Zs (app-6bf1371e025ae4112ce5.bundle.js:50)
    at app-6bf1371e025ae4112ce5.bundle.js:50

This results in a white screen on Web and Desktop. I tested this on dev after running npx react-native-clean-project and wiping the node_modules folder, and I also confirmed it happens when running npm run desktop-build as well. Here's the steps I did to test this, just to make sure I'm not missing anything:

  1. Run npx react-native-clean-project and wipe node_modules folder
  2. Update package.json react-native-onyx commit hash to 77b07e10dfd10831380c197a132a032e812b31f9
  3. Run npm install then npm run web and npm run desktop and npm run desktop-build
  4. Sign into an account that has an IOU
  5. See the above errors in the console and a white screen
  6. Clear changes in package.json so the react-native-onyx hash is back to d0a5d12c08f2f029e3038fb3516a93a9403e9746
  7. Run npx react-native-clean-project and wipe node_modules folder
  8. Run npm install then npm run web and npm run desktop and npm run desktop-build
  9. Confirm there is no white screen and the error doesn't appear after signing in

I chatted w/ @roryabraham and he is also going to test this to confirm

@Jag96
Copy link
Contributor

Jag96 commented Jun 4, 2021

It looks like setting a default value for currency here fixes the issue, going to create an E.cash PR.

@Jag96
Copy link
Contributor

Jag96 commented Jun 4, 2021

The e.cash PR has been merged, going to test again on the main branch to confirm everything is working

@quinthar
Copy link

quinthar commented Jun 4, 2021 via email

Copy link
Contributor

@roryabraham roryabraham left a comment

Choose a reason for hiding this comment

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

Spent a while testing this on iOS, web, desktop, and I couldn't find any regressions or obvious errors. Seems to work well.

Copy link
Contributor

@Jag96 Jag96 left a comment

Choose a reason for hiding this comment

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

This looks good to me as well, tested on all platforms and it's working great! I'll let @marcaaron have a final review/merge for this one

Copy link
Contributor

@Julesssss Julesssss left a comment

Choose a reason for hiding this comment

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

Sorry for the late review! I ran this locally and while it was tricky to do a visual comparison between dev and prod builds the improvement is noticeable.

I had some of the same concerns as @Jag96 mentioned here, but as discussed there are further optimizations that might take care of this. The main one is that even pages without huge lists feel slightly slower to display. For example, the initial delay when tapping an option in the setting page feels like input lag (I know this isn't actually the case, but this might seem like input lag to a user). Also, it would be great to clear up the issue where FlatList items are added one by one.

Anyway, thanks for leading this huge improvement. I look forward to testing on a prod build!

Copy link
Contributor

@marcaaron marcaaron left a comment

Choose a reason for hiding this comment

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

Left a few minor comments (mostly about tests and not blockers - but should be addressed). Still think we should move usages of captureTask and AsyncStorage into the cache itself, but it's not a hill I will die on so I'll let it go for now.

import ViewWithCollections from '../components/ViewWithCollections';

describe('Onyx', () => {
describe('Cache Service', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

NAB, appreciate the thoroughness here, but rather than test the individual cache methods we could have just tested the behavior that we'd expect when Onyx is actually using the cache vs. using the cache in isolation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

NAB, appreciate the thoroughness here, but rather than test the individual cache methods we could have just tested the behavior that we'd expect when Onyx is actually using the cache

Actually we've tested both in isolation here and the behavior that we'd expect when Onyx is using cache (in the next section)

It's much harder to test all the expectations written here in Onyx, you have to

  • setup component(s)
  • configure async blocks and flush promises at specific times
  • not as straightforward if you want to debug a test

The test for Onyx validate complete flows covering multiple cache methods - e.g. a key is not retrieved from storage when a rendered component uses it

/** @type OnyxCache */
let cache;

// Always use a "fresh" instance
Copy link
Contributor

Choose a reason for hiding this comment

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

NAB, Is that what requiring again would do? When would this not be creating a "fresh" instance? Why should we use one instead of just clearing out the cache?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

NAB, Is that what requiring again would do? When would this not be creating a "fresh" instance? Why should we use one instead of just clearing out the cache?

Simply requiring again would not do the trick. It would return the instance that was already created from the first time the script was required somewhere

There's no function that completely clears the cache. You could implemented something here or in the cache file, but if it's only for the test you could just tell jest you want a clean env

@marcaaron marcaaron merged commit 4f341cf into Expensify:master Jun 7, 2021
@marcaaron
Copy link
Contributor

Next we will just need to create a Expensify/Expensify.cash PR referencing this commit 4f341cfca83bcc4f9a8d89b593d81ea78903f04c for react-native-onyx.

Copy link
Contributor Author

@kidroca kidroca left a comment

Choose a reason for hiding this comment

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

Still think we should move usages of captureTask and AsyncStorage into the cache itself, but it's not a hill I will die on so I'll let it go for now.

Sorry I thought that's what you meant by #76 (comment)

Thanks for your thoughts. Let's go ahead and make this change.

I thought it was a reply to

Adding Onyx specifics here makes this util less flexible it should just provide some low level cache handling heplers like hasCacheForKey, get, set, merge and the resolveTask helper for concurrency calls and leave the caller the flexibility to use it however needed. Then it's transparent how items are retrieved in Onyx.js and the cache handling is on the same abstraction level

And so I switched to how Cache was set up initially

Right now we have separation - cache logic in Cache. Onyx logic and flows in Onyx. It's transparent how Onyx uses and updates cache

It would be confusing to me if I read the high level of the code for Onyx.get and see that it just does return cache.get(key). "What happens if there's no cache?" is my first question, then I have to open OnyxCache and have another question "Why does cache knows how to retrieve values from storage?"

From the current code it's clear that it doesn't have to know and works

If it's just a Proxy - the "startTask" implementation where it doesn't know how the task would provide the value is OK, but it made it harder to test and it was confusing for everybody but me

import ViewWithCollections from '../components/ViewWithCollections';

describe('Onyx', () => {
describe('Cache Service', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

NAB, appreciate the thoroughness here, but rather than test the individual cache methods we could have just tested the behavior that we'd expect when Onyx is actually using the cache

Actually we've tested both in isolation here and the behavior that we'd expect when Onyx is using cache (in the next section)

It's much harder to test all the expectations written here in Onyx, you have to

  • setup component(s)
  • configure async blocks and flush promises at specific times
  • not as straightforward if you want to debug a test

The test for Onyx validate complete flows covering multiple cache methods - e.g. a key is not retrieved from storage when a rendered component uses it

/** @type OnyxCache */
let cache;

// Always use a "fresh" instance
Copy link
Contributor Author

Choose a reason for hiding this comment

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

NAB, Is that what requiring again would do? When would this not be creating a "fresh" instance? Why should we use one instead of just clearing out the cache?

Simply requiring again would not do the trick. It would return the instance that was already created from the first time the script was required somewhere

There's no function that completely clears the cache. You could implemented something here or in the cache file, but if it's only for the test you could just tell jest you want a clean env

@kidroca
Copy link
Contributor Author

kidroca commented Jun 7, 2021

Next we will just need to create a Expensify/Expensify.cash PR referencing this commit 4f341cfca83bcc4f9a8d89b593d81ea78903f04c for react-native-onyx.

Should I create this?
I guess yes, and I'll need to provide samples of my tests for all the platforms
I can do this tomorrow

@marcaaron
Copy link
Contributor

Sorry I thought that's what you meant by #76 (comment)

No. I was thanking you for your thoughts and requesting that you take my suggestion anyway.

@kidroca kidroca deleted the kidroca/onyx-cache branch June 8, 2021 12:16
@kidroca kidroca mentioned this pull request Jun 8, 2021
@Julesssss
Copy link
Contributor

Moving this comment here, as the linked issue was locked.


This morning @trjExpensify and I noticed a potential regression with the Onyx changes:

The iou loading state values that we initialize here are returned null here when opening the IOUModal. The data is valid within local storage and this is only reproducible about 1 in 4 attempts.

While the simple solution would be to add a conditional check to confirm the iou object is valid, we need to confirm whether this requires a fix, or is now expected behavior.

CC @kidroca

IOURequestOnyxCrash.mov

@Julesssss
Copy link
Contributor

It looks like the cleanup PR supposedly resolved this issue. But as far as I can tell, these commits are also on the staging build. So it seems to me that the issue is still occurring:

@kidroca
Copy link
Contributor Author

kidroca commented Jun 9, 2021

@Julesssss

This morning @trjExpensify and I noticed a potential regression with the Onyx changes

I'll investigate

// Set our default key states to use when initializing and clearing Onyx data
defaultKeyStates = initialKeyStates;

Are we supposed to put these initialKeyStates in cache like so?

    // Set our default key states to use when initializing and clearing Onyx data
    defaultKeyStates = initialKeyStates;
    cache.merge(initialKeyStates);

I think they automatically go there:

// Initialize all of our keys with data provided
initializeWithDefaultKeyStates();

This function would internally call merge which would call set and this will update the values in cache

Maybe, this happens after enough delay that the initial value is null because it's not yet set ?

Anyway, I'll see what can I find

@trjExpensify
Copy link

Thanks, @kidroca! We're imminently sending a newsletter out to all our users to announce the IOU features and welcome them into E.cash to test out what we’ve been built so far. We shipped the latest prod build off to the app stores last night for review, which was the last item on the list before hitting send.

If this regression makes it to production, it delays our timeline for the announcement, as the IOU feature will be broken on Web/Desktop/mWeb. With that, it would be awesome if we could get a fix up ASAP to minimise the delay. Thanks again!

@Julesssss
Copy link
Contributor

Julesssss commented Jun 9, 2021

Are we supposed to put these initialKeyStates in cache like so?

Yeah, I think that would make sense. Perhaps we should just expect these to be initially null... but I think having the defaults added to cache would be preferable.

@kidroca
Copy link
Contributor Author

kidroca commented Jun 9, 2021

I traced what is causing the issue. It's our cache cleanup logic and a bug in cache.remove

image

  • When you close the IOU modal, since it's the last and only connection that uses the iou key
  • on disconnect it tells cache to remove the iou from cache - because there are no more usages on screen
  • but cache.remove would also remove it from the storageKeys (the bug)
  • the next time someone tries to connect to the iou key (You open the IOU modal)
  • getAllKeys is not returning the iou because it was removed
  • since getAllKeys does not return it connect short-circuits the value to null

getAllKeys()
.then((keys) => {
// Find all the keys matched by the config key
const matchingKeys = _.filter(keys, key => isKeyMatch(mapping.key, key));
// If the key being connected to does not exist, initialize the value with null
if (matchingKeys.length === 0) {
sendDataToConnection(mapping, null);
return;
}

cache.remove should not remove the key from storageKeys so that getAllKeys continues to return correct result

/**
* Remove data from cache
* @param {string} key
*/
remove(key) {
this.storageKeys.delete(key);
delete this.storageMap[key];
}

It should just drop the cached value for that key


@Julesssss I'll open a PR shortly

kidroca added a commit to kidroca/react-native-onyx that referenced this pull request Jun 9, 2021
`OnyxCache.remove` was renamed to `drop` and updated so that it
does not remove the `key` but only the value
This way the result of `getAllKeys` is unaffected

Related to Expensify#76
@kidroca kidroca mentioned this pull request Jun 9, 2021
@kidroca
Copy link
Contributor Author

kidroca commented Jun 9, 2021

@Julesssss pushed a fix here #81

And confirmed it against the main branch of E.cash

Expensify.cash.-.Google.Chrome.2021-06-09.18-19-25.mp4

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.

Improvements for Onyx connect
7 participants