Skip to content

Use WeakMap for listener wrappers#170

Closed
pedronfigueiredo wants to merge 1 commit into
LavaMoat:mainfrom
pedronfigueiredo:pnf/listener-wrapper-memory-retention
Closed

Use WeakMap for listener wrappers#170
pedronfigueiredo wants to merge 1 commit into
LavaMoat:mainfrom
pedronfigueiredo:pnf/listener-wrapper-memory-retention

Conversation

@pedronfigueiredo

@pedronfigueiredo pedronfigueiredo commented May 15, 2026

Copy link
Copy Markdown

Summary

  • capture the pristine WeakMap constructor in Snow natives
  • switch the protected listener wrapper cache from Map to WeakMap
  • preserve native pass-through behavior for null and other non-object listener values
  • add listener coverage for duplicate handlers, removal, object handleEvent listeners, nullable listeners, and options behavior
  • rebuild both distributed bundles: snow.js and snow.prod.js

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.

@pedronfigueiredo pedronfigueiredo changed the title [codex] Use WeakMap for listener wrappers Use WeakMap for listener wrappers May 15, 2026
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 -->
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.

1 participant