-
Notifications
You must be signed in to change notification settings - Fork 182
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
feat: blazingly fast initial accounts load #7822
Conversation
src/components/ManageAccountsDrawer/components/ImportAccounts.tsx
Outdated
Show resolved
Hide resolved
{ forceRefetch: true }, | ||
), | ||
) | ||
dispatch(portfolioApi.endpoints.getAccount.initiate({ accountId, upsertOnFetch: true })) |
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.
No forceRefetch here. If this is the first call, this will fetch, else leverage RTK caching to avoid spew spew
35f1742
to
150ae0f
Compare
// Only fetch and upsert Tx history once all are loaded, otherwise big main thread rug | ||
const { getAllTxHistory } = txHistoryApi.endpoints | ||
|
||
await Promise.all( | ||
requestedAccountIds.map(requestedAccountId => | ||
dispatch(getAllTxHistory.initiate(requestedAccountId)), | ||
), | ||
) |
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.
Confirmed Tx history is still fetched on account management new accounts toggle
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.
really nice, so much better for readability than relying on separate useEffect
(walletId, walletById, enabledAccountIds): AccountId[] => { | ||
const walletAccountIds = (walletId && walletById[walletId]) ?? [] | ||
return walletAccountIds.filter(accountId => (enabledAccountIds ?? []).includes(accountId)) | ||
}, | ||
) | ||
|
||
export const selectWalletAccountIds = createDeepEqualOutputSelector( |
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.
The same as above, but not necessarily enabled (yet)
@@ -38,23 +38,32 @@ export const selectWalletEnabledAccountIds = createDeepEqualOutputSelector( | |||
}, | |||
) | |||
|
|||
export const selectWalletAccountIds = createDeepEqualOutputSelector( | |||
export const selectEnabledWalletAccountIds = createDeepEqualOutputSelector( |
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.
enabled vs. enabledWallet
above, meaning this selects enabled
AccountIds which are in portfolio.wallet.byId.
In comparison, the above selects enabledAccountIds[walletId]
, but these may not be present in portfolio.wallet.byId
(yet)
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.
you are the 🐐 of naming sir
// Use the `prepareAutoBatched` utility to automatically | ||
// add the `action.meta[SHOULD_AUTOBATCH]` field the enhancer needs | ||
prepare: prepareAutoBatched<Portfolio>(), | ||
upsertPortfolio: (draftState, { payload }: { payload: Portfolio }) => { |
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.
Removed prepare
and consequently reducer
, which isn't possible without prepare
(at least at types-level)
dispatch(txHistory.actions.setAccountIdErrored(accountId)) | ||
} | ||
} | ||
getAllTxHistory: build.query<null, AccountId>({ |
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.
Review me without whitespaces, the only changes here is this fetches a single accountId
, which ensures proper caching across runs.
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.
thank you for this tip!
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.
Code review done, looking excellent. Will test after requested changes actioned
@@ -206,95 +206,91 @@ export const txHistory = createSlice({ | |||
}, | |||
}) | |||
|
|||
// Exported as a paranoia to ensure module-time evaluation as a singleton | |||
export const requestQueue = new PQueue({ concurrency: 2 }) |
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.
[suggestion]: this shouldnt be necessary and there isnt really a use case for exposing the internals of the query. It actually does make sense to break it out like you have though, so this change should mostly stay except for the export
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.
Might as well remove the paranoia, indeed! 5a3b1ae
dispatch(txHistory.actions.setAccountIdErrored(accountId)) | ||
} | ||
} | ||
getAllTxHistory: build.query<null, AccountId>({ |
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.
thank you for this tip!
@@ -38,23 +38,32 @@ export const selectWalletEnabledAccountIds = createDeepEqualOutputSelector( | |||
}, | |||
) | |||
|
|||
export const selectWalletAccountIds = createDeepEqualOutputSelector( | |||
export const selectEnabledWalletAccountIds = createDeepEqualOutputSelector( |
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.
you are the 🐐 of naming sir
src/state/slices/common-selectors.ts
Outdated
selectWalletId, | ||
(state: ReduxState) => state.portfolio.wallet.byId, | ||
(walletId, walletById): AccountId[] => { | ||
const walletAccountIds = (walletId && walletById[walletId]) ?? [] |
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.
[100% non-blocking]: just found it interesting that optional chaining applies here and can be concise-er
const walletAccountIds = (walletId && walletById[walletId]) ?? [] | |
const walletAccountIds = walletById?.[walletId] ?? [] |
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.
// For others, no need to use chunks, but for the sake of consistency, we keep the same structure | ||
const accountNumberAccountIdChunks = ( | ||
_accountIds: AccountId[], | ||
): Record<ChainId, AccountId[]> => { |
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.
[suggestion]: Rename this to accountNumberAccountIdsByChainId
or similar and update the rest of the vernacular to match.
These aren't really chunks - chunking typically refers to batching an array into multiple smaller arrays.
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.
Ab-solutely 💯: 5c094cc
if (isUtxoChainId(chainId)) { | ||
acc[chainId] = [_accountId] | ||
return acc | ||
} |
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.
[suggestion]: This path shouldnt be required if there arent multiple utxo account ids for a given chainId. I.e if there is no collision on chainId we can rely on the flow below this statement to correctly group accountIds by chainId - even if there is only 1 per chainId.
If the intent is to "overwrite" i.e "last write wins" then leave as-is
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.
You are absolutely right! Reading this again with fresh eyes, this looks blatantly wrong. A UTXO ChainId may and will most likely contain multiple AccountIds (one per scriptType), and we need them all in one "chunk" (terminology removed btw!) to detect activity across scriptTypes, that effectively voided it.
5c094cc
if (accountNumber > 0 && !accountNumberHasChainActivity) { | ||
chainIdsWithActivity = chainIdsWithActivity.filter(_chainId => _chainId !== chainId) | ||
delete accountMetadataByAccountId[accountId] | ||
} else { | ||
// unique set to handle utxo chains with multiple account types per account | ||
chainIdsWithActivity = Array.from(new Set([...chainIdsWithActivity, chainId])) | ||
|
||
dispatch(portfolio.actions.upsertPortfolio(account)) | ||
const chainIdAccountMetadata = Object.entries(accountMetadataByAccountId).reduce( | ||
(acc, [accountId, metadata]) => { | ||
const { chainId: _chainId } = fromAccountId(accountId) | ||
if (chainId === _chainId) { | ||
acc[accountId] = metadata | ||
} | ||
return acc | ||
}, | ||
{} as AccountMetadataById, | ||
) | ||
dispatch( | ||
portfolio.actions.upsertAccountMetadata({ | ||
accountMetadataByAccountId: chainIdAccountMetadata, | ||
walletId, | ||
}), | ||
) | ||
for (const accountId of Object.keys(accountMetadataByAccountId)) { | ||
dispatch(portfolio.actions.enableAccountId(accountId)) | ||
} | ||
} | ||
}) | ||
|
||
return results | ||
}) | ||
|
||
chainIds = chainIdsWithActivity | ||
} | ||
await Promise.allSettled(accountNumberAccountIdsPromises) | ||
|
||
dispatch( | ||
portfolio.actions.upsertAccountMetadata({ | ||
accountMetadataByAccountId, | ||
walletId: await wallet.getDeviceID(), | ||
}), | ||
) | ||
|
||
for (const accountId of Object.keys(accountMetadataByAccountId)) { | ||
dispatch(portfolio.actions.enableAccountId(accountId)) | ||
chainIds = chainIdsWithActivity |
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.
[suggestion]: For both performance and readability, chainIdsWithActivity
would be much better suited as a const chainIdsWithActivity: Set<ChainId> = new Set()
instead of a let chainIdsWithActivity: string[] = []
. This way, above you can mutate the set rather than reassigning a new array at every step:
chainIdsWithActivity.delete(chainId)
chainIdsWithActivity.add(chainId)
instead of
chainIdsWithActivity = chainIdsWithActivity.filter(_chainId => _chainId !== chainId)
chainIdsWithActivity = Array.from(new Set([...chainIdsWithActivity, chainId]))
and then at the end you can assign chainIds = Array.from(chainIdsWithActivity)
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.
Much cleaner indeed! Went with keeping everything as a set and doing in-place Array.from()
for the one place where chainIds
is required by another API (deriveAccountIdsAndMetadata
) to be an array:
81b6e9b and confirmed we're still happy! https://jam.dev/c/6625fa90-5f30-4cd1-9acb-b46a9ad82dad
const chainIdAccountMetadata = Object.entries(accountMetadataByAccountId).reduce( | ||
(acc, [accountId, metadata]) => { | ||
const { chainId: _chainId } = fromAccountId(accountId) | ||
if (chainId === _chainId) { | ||
acc[accountId] = metadata | ||
} | ||
return acc | ||
}, | ||
{} as AccountMetadataById, | ||
) |
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.
[preferably blocking]: Untested but if possible this would be nicer:
const chainIdAccountMetadata = Object.entries(accountMetadataByAccountId).reduce( | |
(acc, [accountId, metadata]) => { | |
const { chainId: _chainId } = fromAccountId(accountId) | |
if (chainId === _chainId) { | |
acc[accountId] = metadata | |
} | |
return acc | |
}, | |
{} as AccountMetadataById, | |
) | |
const chainIdAccountMetadata = { | |
[chainId]: accountMetadataByAccountId[toAccountId({ chainId, account })] | |
} |
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.
Left as-is as this will lose the intent of this (keeping only the AccountIds - note plural here - for the current ChainId) and won't generalize to UTXOs (this would only keep the last AccountId for UTXO ChainIds)
// Only fetch and upsert Tx history once all are loaded, otherwise big main thread rug | ||
const { getAllTxHistory } = txHistoryApi.endpoints | ||
|
||
await Promise.all( | ||
requestedAccountIds.map(requestedAccountId => | ||
dispatch(getAllTxHistory.initiate(requestedAccountId)), | ||
), | ||
) |
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.
really nice, so much better for readability than relying on separate useEffect
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.
Actually awesome. Loading accounts feels much faster and everything seems to function as intended. Fresh load, cached load, managing accounts, balances, account details all look good.
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.
Description
This PR reads like a
mental breakdown of trying to optimize the initial load of accountsrecipe, rather than a definite solution. It is by no means final and discussions / blockers / inputs are more than welcome 🙏🏽.It consists of many optimizations, some of them we may want to keep, some of them we may not. In fact, you can see some of them being ditched as part of progression here.
Here is a summary of the different paths taken in this PR, and their rationale:
87a2daf - while we originally thought that XHRs were the bottleneck, side-effects (upsertion/loading state detection) really is. In fact, not having a queue means we are spammy, but we do fetch faster.reintroduced in 10189e6 - there was a bug in my initial implementation which rugged multi-account. As we fetch more account numbers, things were very, very spewy and eventually result in failures/account degradation.44630f9 - removeskept back just in case as this seems to make things somehow safer, will take another look on a granular PRupsertOnFetch
option inupsertPortfolio
.<AppContext />
was now the only one consuming it, for legacy reasons which we handle somewhere else now: https://github.com/shapeshift/web/pull/3294/files#diff-bf8aa83a0397b2b1f8bbe9f8e94af3e311f0046d1445385a49c06a2221a7b602R134isPortfolioLoaded
heuristics to assume loaded when there is at least one AccountId for that walletId in the store, vs. at least one enabled AccountId for that WalletId previously. The rationale being the whole enabled logic is currently done as a side effect much, much later on (when all finish fetching). Which was one of the main reasons behind the slowness previously - accounts were fetched, upsertions were done, but the final metadata upsertion (which is where we set AccountIds as enabled) is done after all are loaded, so AccountIds being present in the slice didn't cut it, they were present early on, but not enabled until much, much later on.upsertOnFetch
for perf. ReasonsWhere to go from here
If we're happy with the changes here and decide to go with it, an issue/follow-up should be created to handle incremental loads with an ACC broadly like this:
Issue (if applicable)
Risk
High - touches accounts/meta fetching
Testing
Engineering
Operations
Screenshots (if applicable)
https://jam.dev/c/edaa2299-f301-4e7e-8478-edbd1dbea9d2
https://jam.dev/c/75f15c70-7829-4562-8ef0-14982cfaf353