@memlab/lens 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/dist/index.js +112 -0
- package/explainer.md +54 -0
- package/package.json +34 -0
- package/playwright.config.ts +21 -0
- package/src/config/config.ts +32 -0
- package/src/core/dom-observer.ts +189 -0
- package/src/core/event-listener-tracker.ts +171 -0
- package/src/core/react-fiber-analysis.ts +123 -0
- package/src/core/react-memory-scan.ts +366 -0
- package/src/core/types.ts +180 -0
- package/src/core/valid-component-name.ts +17 -0
- package/src/extensions/basic-extension.ts +41 -0
- package/src/extensions/dom-visualization-extension.ts +42 -0
- package/src/index.ts +16 -0
- package/src/memlens.lib.js.flow +75 -0
- package/src/memlens.lib.ts +22 -0
- package/src/memlens.run.ts +21 -0
- package/src/tests/bundle/lib.bundle.test.ts +31 -0
- package/src/tests/bundle/run.bundle.start.head.test.ts +48 -0
- package/src/tests/bundle/run.bundle.start.test.ts +48 -0
- package/src/tests/bundle/run.bundle.test.ts +51 -0
- package/src/tests/fiber/dev.fiber.complex.dev.test.ts +92 -0
- package/src/tests/fiber/dev.fiber.complex.list.dev.test.ts +118 -0
- package/src/tests/fiber/dev.fiber.complex.prod.test.ts +92 -0
- package/src/tests/fiber/dev.fiber.name.dev.test.ts +77 -0
- package/src/tests/fiber/dev.fiber.name.prod.test.ts +79 -0
- package/src/tests/lib/babel.prod.js +4 -0
- package/src/tests/lib/react-dom-v18.dev.js +29926 -0
- package/src/tests/lib/react-dom-v18.prod.js +269 -0
- package/src/tests/lib/react-v18.dev.js +3345 -0
- package/src/tests/lib/react-v18.prod.js +33 -0
- package/src/tests/manual/playwright-open-manual.js +40 -0
- package/src/tests/manual/todo-list/todo-with-run.bundle.html +80 -0
- package/src/tests/utils/test-utils.ts +28 -0
- package/src/utils/intersection-observer.ts +65 -0
- package/src/utils/react-fiber-utils.ts +212 -0
- package/src/utils/utils.ts +201 -0
- package/src/utils/weak-ref-utils.ts +308 -0
- package/src/visual/components/component-stack-panel.ts +85 -0
- package/src/visual/components/control-widget.ts +96 -0
- package/src/visual/components/overlay-rectangle.ts +167 -0
- package/src/visual/components/status-text.ts +53 -0
- package/src/visual/components/toggle-button.ts +57 -0
- package/src/visual/components/visual-overlay.ts +19 -0
- package/src/visual/dom-element-visualizer-interactive.ts +358 -0
- package/src/visual/dom-element-visualizer.ts +130 -0
- package/src/visual/visual-utils.ts +89 -0
- package/tsconfig.json +18 -0
- package/webpack.config.js +105 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# MemLens
|
|
2
|
+
|
|
3
|
+
## What is this?
|
|
4
|
+
|
|
5
|
+
MemLens is a debugging tool that helps identify memory leaks in React applications. It tracks:
|
|
6
|
+
|
|
7
|
+
- Detached DOM elements that are no longer connected to the document but still held in memory
|
|
8
|
+
- Unmounted React Fiber nodes that haven't been properly cleaned up
|
|
9
|
+
- Memory usage patterns and growth over time
|
|
10
|
+
|
|
11
|
+
The tool provides:
|
|
12
|
+
|
|
13
|
+
1. Real-time memory monitoring through the browser console
|
|
14
|
+
2. Visual representation of problematic DOM elements and React components
|
|
15
|
+
3. Memory usage statistics and trends
|
|
16
|
+
|
|
17
|
+
This can help developers identify:
|
|
18
|
+
- Components that aren't properly cleaned up
|
|
19
|
+
|
|
20
|
+
## How to Build?
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
webpack
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## How to Test?
|
|
27
|
+
|
|
28
|
+
1. Install Playwright
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx playwright install
|
|
32
|
+
npx playwright install-deps
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
2. Run the test
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm run test:e2e
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
3. Test manually by copying and pasting the content of `dist/run.bundle.js`
|
|
42
|
+
into web console
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## TODO List
|
|
46
|
+
* Note that document.querySelectorAll('*') only captures DOM elements on the DOM tree
|
|
47
|
+
* So the DOM tree scanning and component name analysis must be done in a frequent interval (every 10s or so)
|
|
48
|
+
* Extensible framework for tracking additional metadata
|
|
49
|
+
* Auto diffing and counting detached Element and unmounted Fiber nodes
|
|
50
|
+
* being able to summarize the leaked components that was not reported (this can reduce the report overhead)
|
|
51
|
+
* Improve the DOM visualizer - only visualize the common ancestors of the detached DOM elements and unmounted fiber nodes
|
|
52
|
+
* Improving the scanning efficiency so that the overhead is minimal in production environment
|
|
53
|
+
* Improve the fiber tree traversal efficiency (there are redundant traversals right now)
|
|
54
|
+
* Not only keep track of detached DOM elements, but also keep track of unmounted fiber nodes in WeakMap and WeakSet
|
|
55
|
+
* DOM visualizer is leaking canvas DOM elements after each scan
|
|
56
|
+
* Monitor event listener leaks?
|
|
57
|
+
* Real-time memory usage graphs
|
|
58
|
+
* Real-time component count and other react memory scan obtained stats graphs
|
|
59
|
+
* Component re-render heat maps
|
|
60
|
+
* Interactive component tree navigation
|
|
61
|
+
* Browser extension integration
|
|
62
|
+
* centralized config file
|
|
63
|
+
* centralized exception handling
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/******/ (() => { // webpackBootstrap
|
|
2
|
+
/******/ "use strict";
|
|
3
|
+
/******/ var __webpack_modules__ = ({
|
|
4
|
+
|
|
5
|
+
/***/ 3:
|
|
6
|
+
/***/ ((module) => {
|
|
7
|
+
|
|
8
|
+
module.exports = require("path");
|
|
9
|
+
|
|
10
|
+
/***/ }),
|
|
11
|
+
|
|
12
|
+
/***/ 383:
|
|
13
|
+
/***/ ((module) => {
|
|
14
|
+
|
|
15
|
+
module.exports = require("fs");
|
|
16
|
+
|
|
17
|
+
/***/ }),
|
|
18
|
+
|
|
19
|
+
/***/ 783:
|
|
20
|
+
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
26
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
27
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
28
|
+
}
|
|
29
|
+
Object.defineProperty(o, k2, desc);
|
|
30
|
+
}) : (function(o, m, k, k2) {
|
|
31
|
+
if (k2 === undefined) k2 = k;
|
|
32
|
+
o[k2] = m[k];
|
|
33
|
+
}));
|
|
34
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
35
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
36
|
+
}) : function(o, v) {
|
|
37
|
+
o["default"] = v;
|
|
38
|
+
});
|
|
39
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
40
|
+
var ownKeys = function(o) {
|
|
41
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
42
|
+
var ar = [];
|
|
43
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
44
|
+
return ar;
|
|
45
|
+
};
|
|
46
|
+
return ownKeys(o);
|
|
47
|
+
};
|
|
48
|
+
return function (mod) {
|
|
49
|
+
if (mod && mod.__esModule) return mod;
|
|
50
|
+
var result = {};
|
|
51
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
52
|
+
__setModuleDefault(result, mod);
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
})();
|
|
56
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
57
|
+
exports.getBundleContent = getBundleContent;
|
|
58
|
+
/**
|
|
59
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
60
|
+
*
|
|
61
|
+
* This source code is licensed under the MIT license found in the
|
|
62
|
+
* LICENSE file in the root directory of this source tree.
|
|
63
|
+
*
|
|
64
|
+
* @format
|
|
65
|
+
* @oncall memory_lab
|
|
66
|
+
*/
|
|
67
|
+
const fs = __importStar(__webpack_require__(383));
|
|
68
|
+
const path = __importStar(__webpack_require__(3));
|
|
69
|
+
function getBundleContent() {
|
|
70
|
+
const bundlePath = path.join(__dirname, 'memlens.run.bundle.min.js');
|
|
71
|
+
return fs.readFileSync(bundlePath, 'utf-8');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
/***/ })
|
|
76
|
+
|
|
77
|
+
/******/ });
|
|
78
|
+
/************************************************************************/
|
|
79
|
+
/******/ // The module cache
|
|
80
|
+
/******/ var __webpack_module_cache__ = {};
|
|
81
|
+
/******/
|
|
82
|
+
/******/ // The require function
|
|
83
|
+
/******/ function __webpack_require__(moduleId) {
|
|
84
|
+
/******/ // Check if module is in cache
|
|
85
|
+
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
|
86
|
+
/******/ if (cachedModule !== undefined) {
|
|
87
|
+
/******/ return cachedModule.exports;
|
|
88
|
+
/******/ }
|
|
89
|
+
/******/ // Create a new module (and put it into the cache)
|
|
90
|
+
/******/ var module = __webpack_module_cache__[moduleId] = {
|
|
91
|
+
/******/ // no module.id needed
|
|
92
|
+
/******/ // no module.loaded needed
|
|
93
|
+
/******/ exports: {}
|
|
94
|
+
/******/ };
|
|
95
|
+
/******/
|
|
96
|
+
/******/ // Execute the module function
|
|
97
|
+
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
|
98
|
+
/******/
|
|
99
|
+
/******/ // Return the exports of the module
|
|
100
|
+
/******/ return module.exports;
|
|
101
|
+
/******/ }
|
|
102
|
+
/******/
|
|
103
|
+
/************************************************************************/
|
|
104
|
+
/******/
|
|
105
|
+
/******/ // startup
|
|
106
|
+
/******/ // Load entry module and return exports
|
|
107
|
+
/******/ // This entry module is referenced by other modules so it can't be inlined
|
|
108
|
+
/******/ var __webpack_exports__ = __webpack_require__(783);
|
|
109
|
+
/******/ module.exports = __webpack_exports__;
|
|
110
|
+
/******/
|
|
111
|
+
/******/ })()
|
|
112
|
+
;
|
package/explainer.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
### Purpose and Core Functionality
|
|
4
|
+
This project is a specialized production memory diagnostic collection and local dev debugging tool designed to detect and visualize memory leaks in React applications, with a particular focus on identifying detached components that should have been garbage collected but remain in memory.
|
|
5
|
+
|
|
6
|
+
### How It Works
|
|
7
|
+
|
|
8
|
+
#### Detection System
|
|
9
|
+
The module operates through multiple layers of monitoring:
|
|
10
|
+
* **DOM Scanning**: Continuously scans the Document Object Model to create a comprehensive map of all elements present in the application.
|
|
11
|
+
* **React Fiber Analysis**: Specifically tracks React's internal fiber tree structure, which represents the component hierarchy and their relationships.
|
|
12
|
+
* **Memory Leak Detection**: Identifies components that are no longer attached to the main DOM tree but still persist in memory, indicating potential memory leaks.
|
|
13
|
+
|
|
14
|
+
#### Monitoring Process
|
|
15
|
+
The system employs a dual-monitoring approach:
|
|
16
|
+
* **Periodic Scanning**: Runs regular scans at configurable intervals to check for detached components.
|
|
17
|
+
* **Real-time Observation**: Uses mutation observers to detect DOM changes as they happen, ensuring immediate detection of potential issues.
|
|
18
|
+
|
|
19
|
+
#### Visualization System
|
|
20
|
+
The visualization component provides real-time feedback through:
|
|
21
|
+
* **Overlay Layer**: Creates a transparent canvas overlay that sits above the application.
|
|
22
|
+
* **Visual Indicators**: Draws highlighting boxes around problematic components.
|
|
23
|
+
* **Component Information**: Displays relevant information about detected issues, including component names and their locations.
|
|
24
|
+
|
|
25
|
+
### Key Features
|
|
26
|
+
|
|
27
|
+
#### Memory Safety
|
|
28
|
+
- Implements memory-safe tracking mechanisms using weak references
|
|
29
|
+
- Avoids creating memory leaks while detecting them
|
|
30
|
+
- Ensures the debugging tool itself doesn't impact application performance
|
|
31
|
+
|
|
32
|
+
#### Non-Intrusive Design
|
|
33
|
+
- Operates without interfering with the application's normal functionality
|
|
34
|
+
- Uses a transparent overlay that doesn't block user interactions
|
|
35
|
+
- Can be easily enabled or disabled as needed
|
|
36
|
+
|
|
37
|
+
#### Developer Tools Integration
|
|
38
|
+
- Provides detailed debugging information in development mode
|
|
39
|
+
- Offers subscription capabilities for external monitoring tools
|
|
40
|
+
- Maintains detailed statistics about detected issues
|
|
41
|
+
|
|
42
|
+
### Use Cases
|
|
43
|
+
|
|
44
|
+
* **Production Performance Monitoring**
|
|
45
|
+
- Monitoring application memory usage patterns
|
|
46
|
+
- Detecting gradual memory leaks in long-running applications
|
|
47
|
+
- Identifying patterns in component detachment
|
|
48
|
+
|
|
49
|
+
* **Development Debugging**
|
|
50
|
+
- Identifying component cleanup issues during development
|
|
51
|
+
- Tracking down memory leaks in complex component hierarchies
|
|
52
|
+
- Visualizing component lifecycle issues
|
|
53
|
+
|
|
54
|
+
This tool serves as an essential debugging aid for React developers, helping them maintain efficient and leak-free applications while providing valuable insights into component behavior and memory management patterns.
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@memlab/lens",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "MemLens is a tool for inspecting memory leaks in browser",
|
|
6
|
+
"author": "Liang Gong <lgong@meta.com>",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "webpack",
|
|
14
|
+
"rebuild": "webpack",
|
|
15
|
+
"reset-build": "rm package-lock.json; rm -rf node_modules; npm i; webpack",
|
|
16
|
+
"test": "playwright test --output=out",
|
|
17
|
+
"test:manual": "node src/tests/manual/playwright-open-manual.js todo-list/todo-with-run.bundle.html",
|
|
18
|
+
"test:e2e": "playwright test --output=out",
|
|
19
|
+
"test:rebuild": "webpack && playwright test --output=out",
|
|
20
|
+
"build-pkg": "webpack",
|
|
21
|
+
"publish-patch": "npm publish",
|
|
22
|
+
"clean-pkg": "rm -rf ./dist && rm -rf ./node_modules && rm -f ./tsconfig.tsbuildinfo && rm -rf out"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@playwright/test": "^1.49.1",
|
|
26
|
+
"@types/react": "^18.3.12",
|
|
27
|
+
"@types/react-reconciler": "^0.28.8",
|
|
28
|
+
"react-reconciler": "^0.31.0",
|
|
29
|
+
"ts-loader": "^9.5.1",
|
|
30
|
+
"typescript": "^5.7.2",
|
|
31
|
+
"webpack": "^5.97.1",
|
|
32
|
+
"webpack-cli": "^5.1.4"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
* @oncall memory_lab
|
|
9
|
+
*/
|
|
10
|
+
import process from 'process';
|
|
11
|
+
import {PlaywrightTestConfig} from '@playwright/test';
|
|
12
|
+
|
|
13
|
+
const config: PlaywrightTestConfig = {
|
|
14
|
+
testDir: './src/tests',
|
|
15
|
+
use: {
|
|
16
|
+
// Serve files from the root directory
|
|
17
|
+
baseURL: `file://${process.cwd()}`,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default config;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
* @oncall memory_lab
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {AnyValue, Config} from '../core/types';
|
|
12
|
+
|
|
13
|
+
// Performance Configuration
|
|
14
|
+
export const performanceConfig = {
|
|
15
|
+
scanIntervalMs: 1000,
|
|
16
|
+
maxComponentStackDepth: 100,
|
|
17
|
+
memoryMeasurementIntervalMs: 5000,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Feature Flags
|
|
21
|
+
export const featureFlags = {
|
|
22
|
+
enableMutationObserver: true,
|
|
23
|
+
enableMemoryTracking: true,
|
|
24
|
+
enableComponentStack: true,
|
|
25
|
+
enableConsoleLogs: (window as AnyValue)?.TEST_MEMORY_SCAN,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// overall Config
|
|
29
|
+
export const config: Config = {
|
|
30
|
+
performance: performanceConfig,
|
|
31
|
+
features: featureFlags,
|
|
32
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
* @oncall memory_lab
|
|
9
|
+
*/
|
|
10
|
+
import {isWeakAPINative} from '../utils/weak-ref-utils';
|
|
11
|
+
import {isVisualizerElement} from '../visual/visual-utils';
|
|
12
|
+
import {DOMElementStats, DOMObserveCallback, Optional} from './types';
|
|
13
|
+
|
|
14
|
+
// if false, we only capture the top-most element of a removed subtree
|
|
15
|
+
const CAPTURE_ALL_REMOVED_ELEMENTS = true;
|
|
16
|
+
const IS_WEAK_REF_NATIVE = isWeakAPINative();
|
|
17
|
+
const IS_MUTATION_OBSERVER_SUPPORTED = window.MutationObserver !== undefined;
|
|
18
|
+
|
|
19
|
+
export class DOMObserver {
|
|
20
|
+
#elementCount: number;
|
|
21
|
+
#detachedElementCount: number;
|
|
22
|
+
#mutationObserver: Optional<MutationObserver>;
|
|
23
|
+
#trackedElements: Array<WeakRef<Element>>;
|
|
24
|
+
#trackedElementSet: WeakSet<Element>;
|
|
25
|
+
#consolidateInterval: Optional<number>;
|
|
26
|
+
#observeCallback: Array<DOMObserveCallback>;
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
this.#elementCount = 0;
|
|
30
|
+
this.#detachedElementCount = 0;
|
|
31
|
+
this.#trackedElements = [];
|
|
32
|
+
this.#trackedElementSet = new WeakSet();
|
|
33
|
+
this.#observeCallback = [];
|
|
34
|
+
this.startMonitoring();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
register(callback: DOMObserveCallback): void {
|
|
38
|
+
this.#observeCallback.push(callback);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
startMonitoring(): void {
|
|
42
|
+
if (!IS_WEAK_REF_NATIVE || !IS_MUTATION_OBSERVER_SUPPORTED) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (this.#mutationObserver != null) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.#mutationObserver = new MutationObserver(mutationsList => {
|
|
49
|
+
let newlyAdded: Array<WeakRef<Element>> = [];
|
|
50
|
+
const updateCallback = (node: Node) => {
|
|
51
|
+
if (node == null) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (node.nodeType != Node.ELEMENT_NODE) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const element = node as Element;
|
|
58
|
+
if (isVisualizerElement(element)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (CAPTURE_ALL_REMOVED_ELEMENTS) {
|
|
62
|
+
const diff = this.#gatherAllElementsInRemovedSubtree(element);
|
|
63
|
+
newlyAdded = [...newlyAdded, ...diff];
|
|
64
|
+
} else if (!this.#trackedElementSet.has(element)) {
|
|
65
|
+
const ref = new WeakRef(element);
|
|
66
|
+
this.#trackedElements.push(ref);
|
|
67
|
+
this.#trackedElementSet.add(element);
|
|
68
|
+
newlyAdded.push(ref);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
mutationsList.forEach(mutation => {
|
|
73
|
+
mutation.addedNodes.forEach(updateCallback);
|
|
74
|
+
mutation.removedNodes.forEach(updateCallback);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.#observeCallback.forEach(cb => cb(newlyAdded));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const waitForBodyAndObserve = () => {
|
|
81
|
+
if (document.body) {
|
|
82
|
+
// observe changes in DOM tree
|
|
83
|
+
this.#mutationObserver?.observe(document.body, {
|
|
84
|
+
childList: true, // Detect direct additions/removals
|
|
85
|
+
subtree: true, // Observe all descendants
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
setTimeout(waitForBodyAndObserve, 0);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
waitForBodyAndObserve();
|
|
93
|
+
|
|
94
|
+
// starts consolidating removedElements weak references;
|
|
95
|
+
this.#consolidateInterval = window.setInterval(() => {
|
|
96
|
+
this.#consolidateElementRefs();
|
|
97
|
+
}, 3000);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
stopMonitoring(): void {
|
|
101
|
+
if (this.#mutationObserver != null) {
|
|
102
|
+
this.#mutationObserver.disconnect();
|
|
103
|
+
this.#mutationObserver = null;
|
|
104
|
+
}
|
|
105
|
+
if (this.#consolidateInterval != null) {
|
|
106
|
+
window.clearInterval(this.#consolidateInterval);
|
|
107
|
+
this.#consolidateInterval = null;
|
|
108
|
+
}
|
|
109
|
+
// TODO: clean up memory
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#consolidateElementRefs(): void {
|
|
113
|
+
const consolidatedList = [];
|
|
114
|
+
const trackedElements = new Set<Element>();
|
|
115
|
+
for (const ref of this.#trackedElements) {
|
|
116
|
+
const element = ref.deref();
|
|
117
|
+
if (element != null && !trackedElements.has(element)) {
|
|
118
|
+
consolidatedList.push(ref);
|
|
119
|
+
trackedElements.add(element);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
this.#trackedElements = consolidatedList;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#getTotalDOMElementCount(): number {
|
|
126
|
+
return (this.#elementCount =
|
|
127
|
+
document?.getElementsByTagName('*')?.length ?? 0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#getDetachedElementCount(): number {
|
|
131
|
+
let count = 0;
|
|
132
|
+
for (const ref of this.#trackedElements) {
|
|
133
|
+
const element = ref.deref();
|
|
134
|
+
if (element && element.isConnected === false) {
|
|
135
|
+
++count;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return (this.#detachedElementCount = count);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#gatherAllElementsInRemovedSubtree(node: Node): Array<WeakRef<Element>> {
|
|
142
|
+
const queue = [node];
|
|
143
|
+
const visited = new Set<Node>();
|
|
144
|
+
const newlyAdded: Array<WeakRef<Element>> = [];
|
|
145
|
+
while (queue.length > 0) {
|
|
146
|
+
const current = queue.pop();
|
|
147
|
+
if (current == null || visited.has(current)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (current?.nodeType !== Node.ELEMENT_NODE) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const element = current as Element;
|
|
154
|
+
if (isVisualizerElement(element)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
visited.add(element);
|
|
158
|
+
if (!this.#trackedElementSet.has(element)) {
|
|
159
|
+
const ref = new WeakRef(element);
|
|
160
|
+
this.#trackedElements.push(ref);
|
|
161
|
+
this.#trackedElementSet.add(element);
|
|
162
|
+
newlyAdded.push(ref);
|
|
163
|
+
}
|
|
164
|
+
const list = element.childNodes;
|
|
165
|
+
for (let i = 0; i < list.length; ++i) {
|
|
166
|
+
queue.push(list[i]);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return newlyAdded;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getDOMElements(): Array<WeakRef<Element>> {
|
|
173
|
+
return [...this.#trackedElements];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getStats(): DOMElementStats {
|
|
177
|
+
try {
|
|
178
|
+
this.#elementCount = this.#getTotalDOMElementCount();
|
|
179
|
+
this.#detachedElementCount = this.#getDetachedElementCount();
|
|
180
|
+
} catch (ex) {
|
|
181
|
+
// do nothing
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
elements: this.#elementCount,
|
|
186
|
+
detachedElements: this.#detachedElementCount,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
* @oncall memory_lab
|
|
9
|
+
*/
|
|
10
|
+
import type {Fiber} from 'react-reconciler';
|
|
11
|
+
import {getFiberNodeFromElement} from '../utils/react-fiber-utils';
|
|
12
|
+
import {WeakMapPlus} from '../utils/weak-ref-utils';
|
|
13
|
+
|
|
14
|
+
type EventListenerEntry = {
|
|
15
|
+
type: string;
|
|
16
|
+
cb: WeakRef<EventListenerOrEventListenerObject>;
|
|
17
|
+
options?: boolean | AddEventListenerOptions;
|
|
18
|
+
fiber?: WeakRef<Fiber>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type DetachedListenerGroup = {
|
|
22
|
+
type: string;
|
|
23
|
+
count: number;
|
|
24
|
+
entries: WeakRef<EventListenerEntry>[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class EventListenerTracker {
|
|
28
|
+
private static instance: EventListenerTracker | null = null;
|
|
29
|
+
#listenerMap: WeakMapPlus<EventTarget, EventListenerEntry[]>;
|
|
30
|
+
#detachedListeners: Map<string, DetachedListenerGroup[]>;
|
|
31
|
+
#originalAddEventListener: typeof EventTarget.prototype.addEventListener;
|
|
32
|
+
#originalRemoveEventListener: typeof EventTarget.prototype.removeEventListener;
|
|
33
|
+
|
|
34
|
+
private constructor() {
|
|
35
|
+
this.#listenerMap = new WeakMapPlus({fallback: 'noop', cleanupMs: 100});
|
|
36
|
+
this.#detachedListeners = new Map();
|
|
37
|
+
this.#originalAddEventListener = EventTarget.prototype.addEventListener;
|
|
38
|
+
this.#originalRemoveEventListener =
|
|
39
|
+
EventTarget.prototype.removeEventListener;
|
|
40
|
+
this.#patchEventListeners();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static getInstance(): EventListenerTracker {
|
|
44
|
+
if (!EventListenerTracker.instance) {
|
|
45
|
+
EventListenerTracker.instance = new EventListenerTracker();
|
|
46
|
+
}
|
|
47
|
+
return EventListenerTracker.instance;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#patchEventListeners(): void {
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
52
|
+
const self = this;
|
|
53
|
+
EventTarget.prototype.addEventListener = function (
|
|
54
|
+
type: string,
|
|
55
|
+
listener: EventListenerOrEventListenerObject,
|
|
56
|
+
options?: boolean | AddEventListenerOptions,
|
|
57
|
+
) {
|
|
58
|
+
self.#originalAddEventListener.call(this, type, listener, options);
|
|
59
|
+
if (this instanceof Element) {
|
|
60
|
+
const fiber = getFiberNodeFromElement(this);
|
|
61
|
+
const entry: EventListenerEntry = {
|
|
62
|
+
type,
|
|
63
|
+
cb: new WeakRef(listener),
|
|
64
|
+
options,
|
|
65
|
+
fiber: fiber ? new WeakRef(fiber) : undefined,
|
|
66
|
+
};
|
|
67
|
+
const listeners = self.#listenerMap.get(this) ?? [];
|
|
68
|
+
listeners.push(entry);
|
|
69
|
+
self.#listenerMap.set(this, listeners);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
EventTarget.prototype.removeEventListener = function (
|
|
74
|
+
type: string,
|
|
75
|
+
listener: EventListenerOrEventListenerObject,
|
|
76
|
+
options?: boolean | EventListenerOptions,
|
|
77
|
+
) {
|
|
78
|
+
self.#originalRemoveEventListener.call(this, type, listener, options);
|
|
79
|
+
if (this instanceof Element) {
|
|
80
|
+
const listeners = self.#listenerMap.get(this);
|
|
81
|
+
if (listeners) {
|
|
82
|
+
const index = listeners.findIndex(
|
|
83
|
+
entry =>
|
|
84
|
+
entry.type === type &&
|
|
85
|
+
entry.cb.deref() === listener &&
|
|
86
|
+
entry.options === options,
|
|
87
|
+
);
|
|
88
|
+
if (index !== -1) {
|
|
89
|
+
listeners.splice(index, 1);
|
|
90
|
+
}
|
|
91
|
+
if (listeners.length === 0) {
|
|
92
|
+
self.#listenerMap.delete(this);
|
|
93
|
+
} else {
|
|
94
|
+
self.#listenerMap.set(this, listeners);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#unpatchEventListeners(): void {
|
|
102
|
+
EventTarget.prototype.addEventListener = this.#originalAddEventListener;
|
|
103
|
+
EventTarget.prototype.removeEventListener =
|
|
104
|
+
this.#originalRemoveEventListener;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
addListener(
|
|
108
|
+
el: EventTarget,
|
|
109
|
+
type: string,
|
|
110
|
+
cb: EventListenerOrEventListenerObject,
|
|
111
|
+
options?: boolean | AddEventListenerOptions,
|
|
112
|
+
): void {
|
|
113
|
+
el.addEventListener(type, cb, options);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
removeListener(
|
|
117
|
+
el: EventTarget,
|
|
118
|
+
type: string,
|
|
119
|
+
cb: EventListenerOrEventListenerObject,
|
|
120
|
+
options?: boolean | AddEventListenerOptions,
|
|
121
|
+
): void {
|
|
122
|
+
el.removeEventListener(type, cb, options);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
scan(
|
|
126
|
+
getComponentName: (elRef: WeakRef<Element>) => string,
|
|
127
|
+
): Map<string, DetachedListenerGroup[]> {
|
|
128
|
+
const detachedListeners = new Map<string, DetachedListenerGroup[]>();
|
|
129
|
+
|
|
130
|
+
for (const [el, listeners] of this.#listenerMap.entries()) {
|
|
131
|
+
if (el instanceof Element && !el.isConnected) {
|
|
132
|
+
for (const listener of listeners) {
|
|
133
|
+
// Skip if the callback has been garbage collected
|
|
134
|
+
if (!listener.cb.deref()) continue;
|
|
135
|
+
|
|
136
|
+
const componentName = getComponentName(new WeakRef(el));
|
|
137
|
+
if (!detachedListeners.has(componentName)) {
|
|
138
|
+
detachedListeners.set(componentName, []);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const groups = detachedListeners.get(componentName);
|
|
142
|
+
let group = groups?.find(g => g.type === listener.type);
|
|
143
|
+
if (!group) {
|
|
144
|
+
group = {
|
|
145
|
+
type: listener.type,
|
|
146
|
+
count: 0,
|
|
147
|
+
entries: [],
|
|
148
|
+
};
|
|
149
|
+
groups?.push(group);
|
|
150
|
+
}
|
|
151
|
+
group.count++;
|
|
152
|
+
group.entries.push(new WeakRef(listener));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.#detachedListeners = detachedListeners;
|
|
158
|
+
return detachedListeners;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getDetachedListeners(): Map<string, DetachedListenerGroup[]> {
|
|
162
|
+
return this.#detachedListeners;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
destroy(): void {
|
|
166
|
+
this.#unpatchEventListeners();
|
|
167
|
+
this.#listenerMap.destroy();
|
|
168
|
+
this.#detachedListeners.clear();
|
|
169
|
+
EventListenerTracker.instance = null;
|
|
170
|
+
}
|
|
171
|
+
}
|