Use WeakMap for listener wrappers#170
Closed
pedronfigueiredo wants to merge 1 commit into
Closed
Conversation
7 tasks
sgextcsi
pushed a commit
to sgextcsi/metamask-extension-ne-regression
that referenced
this pull request
May 21, 2026
## **Description** Fixes a shared Popover listener leak that retained old route trees during send/settings route churn. Popover registered document `keydown` and `click` listeners with capture enabled, but removed them without matching capture options. Since `removeEventListener` matches on event type, callback, and capture flag, cleanup failed and leaked document listeners retained their React closure scope. This PR keeps the change scoped to `ui/components/component-library/popover/popover.tsx` by: - defining one shared capture listener options object - adding document listeners only while the Popover is open and the matching callback exists - removing those listeners with the same callback and capture options - adding a regression test that verifies unmount cleanup uses the same listener and options captured from `addEventListener` ## **Changelog** CHANGELOG entry: null <!-- ## **Related issues** Fixes: --> ## **Manual testing steps** 1. Build the test extension: ```bash source ~/.nvm/nvm.sh && nvm use yarn build:test:webpack ``` 2. The `yarn llm:memory` profiler is available on the separate [profiler PR](MetaMask#42733). With that tooling available and this branch's `dist/chrome` build, run: ```bash yarn llm:memory -- --iterations 50 --flow send-open-back --sample final --probe cdp --wait-after-flow 500 --extension-path dist/chrome yarn llm:memory -- --iterations 100 --flow settings-route --sample final --probe cdp --wait-after-flow 500 --extension-path dist/chrome ``` 3. Expected post-fix profile from the prior investigation with the [Snow WeakMap patch](LavaMoat/snow#170) still applied: | Flow | Before | After | |---|---:|---:| | settings-route 50x | +118.91 MiB | +2.22 MiB rerun, +37 listeners | | settings-route 100x | n/a | -0.34 MiB, +34 listeners | | settings-route 50x paired heap snapshots | n/a | -5.84 MiB | | send-open-back 50x | +111.66 MiB | +0.05 MiB, +307 listeners | <!-- ## **Screenshots/Recordings** If applicable, add screenshots and/or recordings to visualize the before and after of your change. ### **Before** [screenshots/recordings] ### **After** [screenshots/recordings] --> ## **Validation** - `source ~/.nvm/nvm.sh && nvm use && yarn test:unit ui/components/component-library/popover/popover.test.tsx` - `source ~/.nvm/nvm.sh && nvm use && yarn lint:changed:fix` - `source ~/.nvm/nvm.sh && nvm use && yarn webpack:tsc` - `source ~/.nvm/nvm.sh && nvm use && yarn build:test:webpack` ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: scoped to Popover’s document event listener lifecycle and adds a regression test; main risk is behavior change in when listeners are attached (only when open and handlers provided). > > **Overview** > Fixes a Popover document-listener leak by **reusing a shared capture options object** and ensuring `removeEventListener` is called with the *same* callback and capture options used for `addEventListener`. > > Listeners for `keydown` (Esc) and `click` (outside) are now only attached when the popover is open and the corresponding handler prop is provided, and a new unit test asserts the add/remove signatures match on unmount to prevent regressions. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 0ccfb58. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Why
The listener wrapper cache kept original listener functions and wrapper functions alive through a strong Map. Temporary UI listeners that were no longer reachable by application code could still retain related DOM and closure graphs. WeakMap keeps wrapper lookup behavior for live listener objects/functions without making Snow the owner of listener lifetime.