Page MenuHomePhabricator

[lib/web/native] Add IntegrityStore
ClosedPublic

Authored by michal on Sep 22 2023, 3:44 AM.
Tags
None
Referenced Files
F3349423: D9265.id31806.diff
Fri, Nov 22, 5:58 PM
F3349375: D9265.id31634.diff
Fri, Nov 22, 5:47 PM
Unknown Object (File)
Wed, Nov 13, 2:10 AM
Unknown Object (File)
Tue, Nov 12, 1:05 PM
Unknown Object (File)
Mon, Nov 11, 12:16 PM
Unknown Object (File)
Sun, Nov 3, 3:27 AM
Unknown Object (File)
Thu, Oct 31, 1:04 PM
Unknown Object (File)
Thu, Oct 31, 1:04 PM
Subscribers

Details

Summary

Part of ENG-4959
Suggested in D9255, which was a previous attempt at storing hashes in redux

We want to store thread hashes in redux so that we don't have to calculate all of them whenever the state check is triggered. Instead we keep them in sync using thread ops. Additionally the hashes will be now generated based on the client schema so we convert them on the server instead of the client.

On login/register/web page load instead of handling all of the thread updates at once we convert them in batches of 50, one every 0.5s.

Test Plan

Test:

  • opening the web page
  • logging in on web and mobile

Check that there are UPDATE_INTEGIRTY_STORE actions with batches and the last one with that sets threadHashingComplete to true

Performance testing:
I've let the integrity handler calculate hashes continuously and checked the behaviour of web app and the native app on iPhone X and some old Android. Didn't see any noticeable performance difference when navigating between threads etc.

Diff Detail

Repository
rCOMM Comm
Lint
No Lint Coverage
Unit
No Test Coverage

Event Timeline

iOS emulator takes about 1ms per one thread and the speed should be O(n). In the case from the linear task where the state check took 53s, there was about 2000 threads, so logging in would take additional 2s

This is concerning. As an approximation of the size of my ThreadStore, I ran this query on my production keyserver, and got 6977 results. Based on 1ms per thread, it looks like this would take 7 seconds for me.

I don't think we can move forward with a solution that does all of this processing on login. I also agree that doing it during the state check doesn't make sense either. Instead, for a fresh log-in I think we should process it in 100ms batches (maybe one batch every 5s) until everything is ready. If the keyserver asks for a state check before then, we should have some mechanism to wait until all of the batches are complete.

I also tested another idea: let's do all "batch hashing" (where we hash all items of a store) on the keyserver side. So during login the keyserver would send client all thread hashes. The client would only update the hashes during thread ops. That means we should optimise the keyserver-side calculations, so we should do the schema conversion on the client. I tested it and got:

  • Do the hashing (without conversion) on the keyserver during login => I got ~0.5s for 7300 threads (not sure how that would translate to the actual keyserver though)
  • From previous testing: updating one hash with conversion on native should take about ~10ms, but it should be a few threads at most at one time

@ashoat any thoughts?

Separately if we decide to to go with "staggered hashing" I have question regarding:

If the keyserver asks for a state check before then, we should have some mechanism to wait until all of the batches are complete.

Do you mean just blocking until all hashes are complete (like it is currently) or something smarter (e.g. we send a request to the server to delay the state check)?

Do you mean just blocking until all hashes are complete (like it is currently) or something smarter (e.g. we send a request to the server to delay the state check)?

I wasn't thinking of anything smarter. Just want to make sure that the state check on the keyserver side doesn't time out due to delays on the client.

I also tested another idea: let's do all "batch hashing" (where we hash all items of a store) on the keyserver side. So during login the keyserver would send client all thread hashes. The client would only update the hashes during thread ops. That means we should optimise the keyserver-side calculations, so we should do the schema conversion on the client. I tested it and got:

  • Do the hashing (without conversion) on the keyserver during login => I got ~0.5s for 7300 threads (not sure how that would translate to the actual keyserver though)
  • From previous testing: updating one hash with conversion on native should take about ~10ms, but it should be a few threads at most at one time

@ashoat any thoughts?

The downside there would be increasing the size of that initial payload... it's already quite expensive, which makes login on native and loading the website take very long.

I'm open to it but I figure doing it on the client doesn't need to be an issue if we make sure to space it out properly. Also curious for @atul's thoughts given he's spent so much time on performance recently.

michal edited the test plan for this revision. (Show Details)

Spacing out hashes calculations on web page load and login/register.

tomek requested changes to this revision.Oct 3 2023, 2:52 AM

Looks great, but there are a couple of issues.

lib/components/integrity-handler.react.js
11–24 ↗(On Diff #31531)

Maybe we can create a function that splits array into chunks inside lib/utils/array? And then call it with Object.keys(threadInfos) here

37 ↗(On Diff #31531)

Do we really need to clear this timeout?

If it is called initially, with threadHashingComplete = false, there shouldn't be any timeout.
If it is called when threadHashingComplete becomes true, there also shouldn't be a timeout, because when dispatching { threadHashingComplete: true } we're early returning.

We could also consider clearing the timeout when we unmount the component.

40 ↗(On Diff #31531)

We should add a comment explaining why we should skip the deps.

lib/reducers/integrity-reducer.js
23–26 ↗(On Diff #31531)

We probably should also clear on logout, delete account, etc.

54 ↗(On Diff #31531)

There is a subtle issue here: if we ever decide that updateIntegrityStoreActionType should generate a thread op, we would return here instead of processing it. Not sure how likely it is, but it could be really hard to catch this issue. Maybe we should return the newState only if there are no ops?

lib/types/redux-types.js
1225–1227 ↗(On Diff #31531)

Why these are mutable?

native/redux/default-state.js
89 ↗(On Diff #31531)

It is safer to also add a migration so that, in the future, we won't have an issue like https://linear.app/comm/issue/ENG-4932/redux-persist-migrations-fail-on-native-cannot-read-property-links-of

This revision now requires changes to proceed.Oct 3 2023, 2:52 AM

Added comments and migrations. Extracted array chunking logic to utils. Fix issues with new action and types.

Instead of threadHashingComplete we now have threadHashingStatus with three states: starting, running and completed. This fixes an issue where previously fullStateSyncActionType wouldn't start a new hashing process if one was already in progress (because threadHashingComplete would still be false). Additionaly now thread operation remove_all sets the status to completed. On setting the status to completed we cleanup remaining timeout and batches so when e.g. logout happens we clear any data remaining

lib/reducers/integrity-reducer.js
23–26 ↗(On Diff #31531)

This is handled by "remove_all" thread ops. Actions specified here are only the ones which we special case to hash asynchronously because there is potentially a large amount of threads.

tomek requested changes to this revision.Oct 4 2023, 6:11 AM
tomek added inline comments.
lib/components/integrity-handler.react.js
12 ↗(On Diff #31629)

Should we keep it as it was (500ms)?

35–40 ↗(On Diff #31629)

Now we can remove this hack, and add a dependency on threadInfos. In running state the effect will simply do nothing.

60 ↗(On Diff #31629)

Should we clearTimeout as a returned value from this effect?

61–63 ↗(On Diff #31629)

We probably don't need this

lib/utils/array.js
29 ↗(On Diff #31629)

According to Flow docs https://flow.org/en/docs/types/arrays/#toc-array-type-shorthand-syntax we should prefer Array syntax

This revision now requires changes to proceed.Oct 4 2023, 6:11 AM

Change dependencies arrays, fix interval, clearTimeout as effect cleanup, fix array type.

lib/components/integrity-handler.react.js
12 ↗(On Diff #31629)

Sorry, I changed it for testing and forgot to change it back.

This revision is now accepted and ready to land.Oct 4 2023, 7:30 AM

Fixed an issue where the integrity handler would run before the threads were fetched from db.

Is this new IntegrityStore persisted via redux-persist? If so, it's very unfortunate that we're adding more O(n) data to redux-persist... this decision has set back our efforts to move all O(n) data to SQLite.

When we know we're making an effort to achieve a long-term goal, we should always try to move that goal forward incrementally in all of our work.

Moving the goal *backwards* for expediency is a recipe for never achieving that goal.

At the very least, the following needs to be done:

  1. Update @kamil's Comm Storage document to reflect the addition of new O(n) data to redux-persist
  2. Create a follow-up task to address this

Apologies if I'm misintepreting here, or if I've previously indicated that using redux-persist here is acceptable.

  1. I've updated the notion doc
  2. Created tasks here: ENG-5235 (web), ENG-5234 (native) as children of "Migrate data to SQLite"

Separately: while integrityStore shouldn't be a big problem for performance as it stores just a string + number for each thread, maybe it would be a good idea to add a code comment with this idea ("store O(n) data in sqlite")? Currently it's only stated in notion doc and some linear tasks which aren't really discoverable.