-
-
Notifications
You must be signed in to change notification settings - Fork 267
perf(multichain-account-service)!: avoid excessive state syncing #6654
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
base: main
Are you sure you want to change the base?
Conversation
…to wallets and groups
…tead of getAccountByAddress which iterates through the whole of internal accounts in the AccountsController
| accountsList, | ||
| ); | ||
| // we cast here because we know that the accounts are BIP-44 compatible | ||
| return internalAccounts as Bip44Account<KeyringAccount>[]; |
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.
Although the getAccounts's return type is (InternalAccount | undefined)[], we're sure to get back all the accounts we want since the accounts list will never be stale.
…accountAdded and accountRemoved handling, it is dead code
| MultichainAccountWallet<Bip44Account<KeyringAccount>> | ||
| >; | ||
|
|
||
| readonly #accountIdToContext: Map< |
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.
Decided to get rid of this mapping since it was only being used for handling the accountRemoved and accountAdded events, removing this gets rid of a large loop in the init function as well. If there's a particular need for this data at the client level, we can always add this back in.
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.
We'll have to add this back, this is now being used on the extension:
…handle createNewVaultAndKeychain and createNewVaultAndRestore code paths
…s, remove redundant state assignment, use assert to ensure wallet existence after creation
…ble with new changes
packages/multichain-account-service/src/providers/BaseBip44AccountProvider.ts
Show resolved
Hide resolved
|
@metamaskbot publish-preview |
|
Preview builds have been published. See these instructions for more information about preview builds. Expand for full list of packages and versions. |
|
@metamaskbot publish-preview |
|
Preview builds have been published. See these instructions for more information about preview builds. Expand for full list of packages and versions. |
|
@metamaskbot publish-preview |
|
Preview builds have been published. See these instructions for more information about preview builds. Expand for full list of packages and versions. |
|
@metamaskbot publish-preview |
|
Preview builds have been published. See these instructions for more information about preview builds. Expand for full list of packages and versions. |
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.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
| for (const account of accounts) { | ||
| this.accounts.add(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.
Provider accounts set not cleared on re-initialization
Medium Severity
The init() method in BaseBip44AccountProvider only adds account IDs to the internal accounts Set without ever clearing it. When MultichainAccountService.init() is called multiple times (e.g., during app restart or unlock cycles while keeping the same service instance), providers accumulate stale account IDs from previous initializations. Since providers are created once in the constructor and reused, the accounts set grows unbounded.
This could cause getAccounts() to request accounts that no longer exist in the AccountsController, and the as unknown as Account[] type assertion masks any undefined entries in the returned array.
Additional Locations (1)
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.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
| for (const account of accounts) { | ||
| this.accounts.add(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.
Provider accounts set accumulates without clearing on reinit
Medium Severity
The init method in BaseBip44AccountProvider only adds account IDs to the internal accounts Set without clearing it first. When MultichainAccountService.init() is called multiple times (e.g., during wallet lock/unlock cycles), #wallets is cleared but providers are reused with the same instances. This causes account IDs from previous initializations to accumulate in the provider's Set. If an account were ever removed between init calls, the stale ID would remain, potentially causing getAccounts() to include undefined entries when those IDs are fetched from AccountsController.
Additional Locations (1)
| this.#providerToAccounts.set(provider, accounts); | ||
| for (const accountId of accounts) { | ||
| this.#accountToProvider.set(accountId, provider); | ||
| } |
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.
Redundant map updates in group alignment method
Low Severity
The alignAccounts method updates the #providerToAccounts and #accountToProvider maps twice: first inline during the Promise.allSettled callback (lines 292-295), and then again via this.update(groupState) at line 331. The update method calls #setState which clears entries for each provider via #clearAccountToProviderState and re-sets the same values. This is redundant work that adds complexity without changing behavior.
Additional Locations (1)
packages/multichain-account-service/src/MultichainAccountService.ts
Outdated
Show resolved
Hide resolved
packages/multichain-account-service/src/MultichainAccountService.ts
Outdated
Show resolved
Hide resolved
packages/multichain-account-service/src/MultichainAccountService.ts
Outdated
Show resolved
Hide resolved
|
@metamaskbot publish-preview |
|
Preview builds have been published. See these instructions for more information about preview builds. Expand for full list of packages and versions. |
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.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
| for (const account of accounts) { | ||
| this.accounts.add(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.
Provider state not cleared on re-initialization
Medium Severity
The init method in BaseBip44AccountProvider only adds account IDs to the internal accounts Set without clearing it first. The service's init method calls this.#wallets.clear() before re-initializing, but providers are constructed once and reused. When init is called multiple times (e.g., on unlock), stale account IDs persist in the provider's Set. Since AccountsController:getAccounts returns (InternalAccount | undefined)[] for missing IDs, and getAccounts() casts this without filtering, the returned array may contain undefined values, causing potential runtime errors downstream.
Additional Locations (1)
| this.#providerToAccounts.set(provider, accounts); | ||
| for (const accountId of accounts) { | ||
| this.#accountToProvider.set(accountId, provider); | ||
| } |
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.
Redundant state setting in alignAccounts method
Low Severity
In alignAccounts, the #providerToAccounts and #accountToProvider maps are set twice: first inside the Promise.allSettled callback (lines 292-295), then again via this.update(groupState) at line 331. The update method calls #setState which clears and re-sets the same data. The first setting inside the callback is redundant since update will overwrite it anyway.
Explanation
MultichainAccountServicecreateMultichainAccountWallet) that handles import/restore/new vault.ServiceStateindex in one pass and passes state slices to wallets/groups (cuts repeated controller scans/calls).initpath and removed deadaccountIdToContextmapping.MultichainAccountWalletinitnow consumes a pre-sliced wallet state (entropySource → groups → providerName → ids) instead of querying providers.MultichainAccountGroupinitregisters account IDs per provider and fills reverse maps; callsprovider.addAccounts(ids)to keep providers in sync.getAccountIds()for direct access to underlying IDs.BaseBip44AccountProvideraddAccounts(ids: string[]), enabling providers to track their own account ID lists.getAccounts()paths rely on known IDs (plural lookups) rather than scanning the full controller list.EvmAccountProvidergetAccount(s)) for create/discover (removesPerformance Analysis
When fully aligned$g = n / p$ .$g = max(f(p))$ , where $f(p)$ is the number of accounts associated with a provider.
When accounts are not fully aligned then
Consider two scenarios:
General formulas
For Scenario 2, the formulas are as follows:
Before this refactor, the number of loops can be represented$n * p * (1 + w + g)$ , which with $p = 4$ , becomes $n^2 + 4n(1 + w)$ .
Before this refactor, the number of controller calls can be represented as$1 + w + g$ , which with $p = 4$ , becomes $1 + w + n/4$ .
After this refactor, the number of loops can be represented by$n * p$ , which with $p = 4$ , becomes $4n$ .
After this refactor, the number of calls is just$1$ .
For Scenario 1, the formulas are entirely dependent on the breakdown of the number of accounts each provider has amongst the$n$ accounts, let's consider a scenario where Solana has $n/2$ , Ethereum has $n/8$ , Bitcoin has $n/4$ and Tron has $n/8$ , the formulas would be as follows:
Before this refactor, the number of loops in the alignment process can be represented as$(p * g) + (n * e)$ , which with $p=4$ and $g = n/2$ , becomes $2n + 3n^2/8$ . Therefore the number of loops for initialization + alignment in this scenario with $p = 4$ and $g = n/2$ , becomes $(19/8)n^2 + (4w + 6)n$ .
Before this refactor, the number of controller calls in the alignment process can be represented as$e$ , which becomes $3n/8$ . Therefore the number of controller calls for initialization + alignment in this scenario with $p = 4$ , becomes $1 + w + 5n/8$ .
After this refactor, the number of loops in the alignment process can be represented as$p * g$ , which becomes $2n$ . Therefore, the number of loops for initialization + alignment in this scenario with $p = 4$ and $g = n/2$ , becomes $6n$ .
After this refactor, the number of controller calls in the alignment process can be represented as$e$ which becomes $3n/8$ . Therefore, the number of controller calls for initialization + alignment in this scenario with $p = 4$ and $g = n/2$ , becomes $1 + 3n/8$ .
In short, previous
initperformance for loops and controller calls was quadratic and linear, respectively. After, it is linear and constant.Performance Charts
Below are charts that show performance (loops and controller calls)$n = 0$ -> $n = 256$ for Scenario 1 and 2 with $w = 2$ , respectively:
References
MultichainAccountServicechanges metamask-extension#38265multichain-account-servicemetamask-mobile#25040Checklist
Note
Major performance/architecture refactor across multichain account service.
ServiceStatebuilt once, passed toMultichainAccountWallet.initandMultichainAccountGroup.init/update; removes per-provider scanning and legacy reverse mapscreateMultichainAccountWalletsupportingimport/create/restore; addsremoveMultichainAccountWalletactionalignAccounts; improved partial‑failure aggregation/warnings; provider enable/disable respected viaAccountProviderWrapperBaseBip44AccountProvidernow tracks account IDs internally (init, ID set),getAccounts/getAccountuse controller ID lookups instead of global scansEvmAccountProvideruses deterministic IDs (getUUIDFromAddressOfNormalAccount) andgetAccount(s); similar ID tracking added to BTC/TRX/SOL providersWritten by Cursor Bugbot for commit 91e7f90. This will update automatically on new commits. Configure here.