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

Revert "Revert "Debounce dispatched state diffs"" #3579

Merged
merged 9 commits into from
Aug 15, 2023
Merged

Conversation

hyphenized
Copy link
Contributor

@hyphenized hyphenized commented Jul 29, 2023

Reimplements debounced/aggregated diffs dispatched on store updates and fixes awaiting of dispatched thunks from the UI.

Added some logic to track pending store updates on the dispatch responder. This is done so that async thunk responses are postponed until state changes are committed to the UI's proxied store. This fixes cases where thunks cause unexpected behaviour by resolving too early, before the aggregated updates have been sent to the UI.

This relies on the subscribed callback from webext-redux for diffing to have executed before the dispatched thunk has finished executing in the background script.

To Test

A consistent way to reproduce this issue is to attempt to initiate a transfer through the Send Asset page and then reject it.

Internally the route is meant to change once the thunk completes. Thus, if we change the route before the transaction state has been cleared by the thunk completion, we are incorrectly sent back to the sign transaction screen. This PR also addresses similar issues.

Latest build: extension-builds-3579 (as of Tue, 15 Aug 2023 19:39:44 GMT).

@hyphenized hyphenized requested a review from a team July 29, 2023 18:49
Tracks pending store updates to delay async thunk responses until state
changes are commited to the UI proxied store. This fixes cases where
thunks resolve too early i.e. before the aggregated updates have been sent
to the UI.

This relies on the subscribed callback from webext-redux for diffing to have
executed before the dispatched thunk has finished executing in the
background script.
This fixes an issue that caused additional delay on thunks that only return a
value, but, still rely on other async operations.
Copy link
Contributor

@Shadowfiend Shadowfiend left a comment

Choose a reason for hiding this comment

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

Solution feels strong. Left a couple of notes on how we might tighten it up just a touch. I wish we could go ahead and do our own fork of webext-redux already, but I just remembered when I tried to do this I had to face down some dependency stuff because the package hasn't seen a lot of updates the last couple of years… For another time.

Comment on lines 523 to 528
pendingUpdates.forEach((resolve) => resolve())

// Clear all currently tracked updates
while (pendingUpdates.length) {
pendingUpdates.pop()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

If I'm not mistaken and the below remark about not needing an array doesn't apply, I think this can be rewritten as:

Suggested change
pendingUpdates.forEach((resolve) => resolve())
// Clear all currently tracked updates
while (pendingUpdates.length) {
pendingUpdates.pop()
}
pendingUpdates
.splice(0)
.forEach((resolve) => resolve())

// by this time, all pending updates should've been tracked.
// since we're dealing with a thunk, we need to wait for
// the store to be updated
if (isThunk) await storeUpdateLock
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we suffer if we do this for all dispatches?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point! We don't because this is only relevant to dispatch responses

background/main.ts Show resolved Hide resolved
return diffWrapper === undefined ? [] : [diffWrapper]
diffStrategy: (oldObj, newObj, forceUpdate) => {
storeUpdateLock = new Promise((resolve) => {
pendingUpdates.push(resolve)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need this to be an array? It feels like storeUpdateLock can be one Promise<void> | undefined with one associated unlocker function:

    // let storeUpdateLock: Promise<void> | undefined
    // let unlockStoreUpdate: (_: void) => void | undefined

    if (storeUpdateLock === undefined) {
      // Set read lock so others can block on it.
      storeUpdateLock = new Promise((resolve) => unlockStoreUpdate = resolve)
   }

   queueUpdate(oldObj, newObj, forceUpdate)

   // Return no diffs.......
   return []

Then queueUpdate, when it's called debounced, can just resolve (and clear) the one promise and its resolver, and everyone who has blocked in the meantime will be awaiting that one promise. I might not be playing it all out correctly in my brain though, I'm not at 100%.

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 think this could work but it might cause unexpected behaviour if a thunk performs a dispatch and takes longer than 30ms to complete 🤔

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 wondering if that's functionally different than the current version. In both cases, storeUpdateLock's value becomes relevant once the thunk is resolved, and in both cases the storeUpdateLock promise is resolved as soon as the diff starts running. My read is that means the array and non-array versions would be identical in behavior, but these are def complicated interactions to reason about so I could totally be wrong.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ahhh I maybe see what you're saying. I think this change from array of promises to single promise would be functionally identical, but the Promise.all approach in the previous comment would not be, because using Promise.all reads the lock before the thunk has completed, while the behavior we want here is to let the thunk finish, then wait until the latest debounced state update is pushed before responding with the thunk outcome.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup they are identical, refactored the array out in favour of a single lock (forgot to submit these comments 🤦‍♂️ )

@Shadowfiend
Copy link
Contributor

None of the above seem blocking, so I'm going to give this a test whirl and plan to merge it for next week's release whether or not we get a chance to work on them.

Only the last dispatched update from the thunk is needed to confirm that the
store has been synced on the UI side and it's now safe to complete the thunk.
Fixes a bunch of errors caused from re-rendering accounts with same-symbol
assets.
Copy link
Contributor

@Shadowfiend Shadowfiend left a comment

Choose a reason for hiding this comment

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

Looks like when we force update after the thunk is done, during dApp signing, we can try to push something through a disconnected port (since the popover has closed out). Shouldn't be a huge issue in practice, but might be nice to catch it and log it at a lower level than error.

Screenshot 2023-08-15 at 15 22 47

Not blocking, wallet seems to be behaving correctly across the board, tried dApps and swaps as well as settings toggles and the mentioned send/reject flow. Let's roll it!

Comment on lines +539 to +546
if (!storeUpdateLock) {
storeUpdateLock = new Promise((resolve) => {
releaseLock = () => {
resolve()
storeUpdateLock = null
}
})
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Golfing at this point, but:

Suggested change
if (!storeUpdateLock) {
storeUpdateLock = new Promise((resolve) => {
releaseLock = () => {
resolve()
storeUpdateLock = null
}
})
}
storeUpdateLock ??= new Promise((resolve) => {
releaseLock = () => {
resolve()
storeUpdateLock = null
}
}

@Shadowfiend Shadowfiend enabled auto-merge August 15, 2023 19:30
@Shadowfiend Shadowfiend merged commit 4685cb3 into main Aug 15, 2023
@Shadowfiend Shadowfiend deleted the wait-aggregation branch August 15, 2023 19:34
@Shadowfiend Shadowfiend mentioned this pull request Aug 15, 2023
Shadowfiend added a commit that referenced this pull request Aug 17, 2023
## Highlights

- Further performance improvements.
- Groundwork to rename backend dApp connection things (for the provider)
to Taho naming.

## What's Changed
* v0.46.1 by @Shadowfiend in
#3598
* Updates to release checklist: Removed Mintkudos test by @andreachapman
in #3600
* Revert "Revert "Debounce dispatched state diffs"" by @hyphenized in
#3579
* Underbridge: Drop updated dApp connections feature flag, further Taho
renames by @Shadowfiend in
#3595


**Full Changelog**:
v0.46.1...v0.47.0

Latest build:
[extension-builds-3601](https://github.com/tahowallet/extension/suites/15160003504/artifacts/866327987)
(as of Wed, 16 Aug 2023 19:09:51 GMT).
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.

2 participants