@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
|
@@ -0,0 +1,123 @@
|
|
|
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 * as utils from '../utils/utils';
|
|
11
|
+
import type {Fiber} from 'react-reconciler';
|
|
12
|
+
import {
|
|
13
|
+
traverseFiber,
|
|
14
|
+
getDisplayNameOfFiberNode,
|
|
15
|
+
getFiberNodeFromElement,
|
|
16
|
+
getTopMostFiberWithChild,
|
|
17
|
+
} from '../utils/react-fiber-utils';
|
|
18
|
+
import {ScanResult, AnyValue} from './types';
|
|
19
|
+
import {isValidComponentName} from './valid-component-name';
|
|
20
|
+
|
|
21
|
+
export default class ReactFiberAnalyzer {
|
|
22
|
+
scan(
|
|
23
|
+
elementWeakRefList: Array<WeakRef<Element>>,
|
|
24
|
+
elementToComponentStack: WeakMap<Element, string[]>,
|
|
25
|
+
): ScanResult {
|
|
26
|
+
const visitedRootFibers = new Set<Fiber>();
|
|
27
|
+
const components = new Set<string>();
|
|
28
|
+
const componentToFiberNodeCount = new Map();
|
|
29
|
+
const detachedComponentToFiberNodeCount = new Map();
|
|
30
|
+
const topDownVisited = new Set();
|
|
31
|
+
const analyzedFibers = new Set();
|
|
32
|
+
const fiberNodes: Array<WeakRef<Fiber>> = [];
|
|
33
|
+
let totalElements = 0;
|
|
34
|
+
let totalDetachedElements = 0;
|
|
35
|
+
|
|
36
|
+
function analyzeFiber(fiberNode: Fiber): void {
|
|
37
|
+
traverseFiber(
|
|
38
|
+
fiberNode,
|
|
39
|
+
(fiberNode: Fiber) => {
|
|
40
|
+
// skip if the fiber node has already been analyzed
|
|
41
|
+
if (analyzedFibers.has(fiberNode)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
analyzedFibers.add(fiberNode);
|
|
45
|
+
|
|
46
|
+
// traverse the fiber tree up to find the component name
|
|
47
|
+
const displayName = getDisplayNameOfFiberNode(fiberNode);
|
|
48
|
+
if (displayName != null && isValidComponentName(displayName)) {
|
|
49
|
+
components.add(displayName);
|
|
50
|
+
utils.addCountbyKey(componentToFiberNodeCount, displayName, 1);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
true,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const weakRef of elementWeakRefList) {
|
|
59
|
+
const elem = weakRef.deref();
|
|
60
|
+
if (elem == null) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// elements stats
|
|
64
|
+
++totalElements;
|
|
65
|
+
// TODO: simplify this logic
|
|
66
|
+
if (!elem.isConnected) {
|
|
67
|
+
if (elementToComponentStack.has(elem)) {
|
|
68
|
+
const componentStack = elementToComponentStack.get(elem) ?? [];
|
|
69
|
+
// set component name
|
|
70
|
+
const component = componentStack[0];
|
|
71
|
+
(elem as AnyValue).__component_name = component;
|
|
72
|
+
utils.addCountbyKey(detachedComponentToFiberNodeCount, component, 1);
|
|
73
|
+
}
|
|
74
|
+
++totalDetachedElements;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// analyze fiber nodes
|
|
78
|
+
const fiberNode = getFiberNodeFromElement(elem);
|
|
79
|
+
if (fiberNode == null) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
analyzeFiber(fiberNode);
|
|
83
|
+
|
|
84
|
+
// try to traverse each fiber node in the entire fiber tree
|
|
85
|
+
const rootFiber = getTopMostFiberWithChild(fiberNode);
|
|
86
|
+
if (rootFiber == null) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (visitedRootFibers.has(rootFiber)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
visitedRootFibers.add(rootFiber);
|
|
93
|
+
|
|
94
|
+
// start traversing fiber tree from the know root host
|
|
95
|
+
traverseFiber(
|
|
96
|
+
rootFiber,
|
|
97
|
+
(node: Fiber) => {
|
|
98
|
+
if (topDownVisited.has(node)) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
topDownVisited.add(node);
|
|
102
|
+
fiberNodes.push(new WeakRef(node));
|
|
103
|
+
analyzeFiber(node);
|
|
104
|
+
},
|
|
105
|
+
false,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
topDownVisited.clear();
|
|
110
|
+
analyzedFibers.clear();
|
|
111
|
+
visitedRootFibers.clear();
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
components,
|
|
115
|
+
componentToFiberNodeCount,
|
|
116
|
+
totalElements,
|
|
117
|
+
totalDetachedElements,
|
|
118
|
+
detachedComponentToFiberNodeCount,
|
|
119
|
+
fiberNodes,
|
|
120
|
+
leakedFibers: [],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
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 type {
|
|
12
|
+
AnalysisResult,
|
|
13
|
+
AnalysisResultCallback,
|
|
14
|
+
BoundingRect,
|
|
15
|
+
CreateOptions,
|
|
16
|
+
DOMElementInfo,
|
|
17
|
+
Optional,
|
|
18
|
+
ScanResult,
|
|
19
|
+
AnyValue,
|
|
20
|
+
EventListenerLeak,
|
|
21
|
+
Nullable,
|
|
22
|
+
} from './types';
|
|
23
|
+
import type {BasicExtension} from '../extensions/basic-extension';
|
|
24
|
+
|
|
25
|
+
import * as utils from '../utils/utils';
|
|
26
|
+
import ReactFiberAnalyzer from './react-fiber-analysis';
|
|
27
|
+
import {
|
|
28
|
+
getFiberNodeFromElement,
|
|
29
|
+
getReactComponentStack,
|
|
30
|
+
} from '../utils/react-fiber-utils';
|
|
31
|
+
import {DOMObserver} from './dom-observer';
|
|
32
|
+
import {config} from '../config/config';
|
|
33
|
+
import {EventListenerTracker} from './event-listener-tracker';
|
|
34
|
+
|
|
35
|
+
export default class ReactMemoryScan {
|
|
36
|
+
static nextElementId = 0;
|
|
37
|
+
#elementWeakRefs: Array<WeakRef<Element>>;
|
|
38
|
+
#isActivated: boolean;
|
|
39
|
+
#intervalId: NodeJS.Timeout;
|
|
40
|
+
#elementToBoundingRects: WeakMap<Element, BoundingRect>;
|
|
41
|
+
#elementToComponentStack: WeakMap<Element, string[]>;
|
|
42
|
+
#knownFiberNodes: Array<WeakRef<Fiber>>;
|
|
43
|
+
#fiberAnalyzer: ReactFiberAnalyzer;
|
|
44
|
+
#isDevMode: boolean;
|
|
45
|
+
#subscribers: Array<AnalysisResultCallback>;
|
|
46
|
+
#extensions: Array<BasicExtension>;
|
|
47
|
+
#scanIntervalMs: number;
|
|
48
|
+
#domObserver: Optional<DOMObserver>;
|
|
49
|
+
#eventListenerTracker: Nullable<EventListenerTracker>;
|
|
50
|
+
|
|
51
|
+
constructor(options: CreateOptions = {}) {
|
|
52
|
+
this.#elementWeakRefs = [];
|
|
53
|
+
this.#isActivated = false;
|
|
54
|
+
this.#elementToBoundingRects = new WeakMap();
|
|
55
|
+
this.#elementToComponentStack = new WeakMap();
|
|
56
|
+
this.#knownFiberNodes = [];
|
|
57
|
+
this.#eventListenerTracker = options.trackEventListenerLeaks
|
|
58
|
+
? EventListenerTracker.getInstance()
|
|
59
|
+
: null;
|
|
60
|
+
|
|
61
|
+
this.#fiberAnalyzer = new ReactFiberAnalyzer();
|
|
62
|
+
this.#intervalId = 0 as unknown as NodeJS.Timeout;
|
|
63
|
+
this.#isDevMode = options.isDevMode ?? false;
|
|
64
|
+
this.#subscribers = options.subscribers ?? [];
|
|
65
|
+
this.#extensions = options.extensions ?? [];
|
|
66
|
+
this.#scanIntervalMs =
|
|
67
|
+
options.scanIntervalMs ?? config.performance.scanIntervalMs;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#log(...args: AnyValue[]): void {
|
|
71
|
+
if (this.#isDevMode && config.features.enableConsoleLogs) {
|
|
72
|
+
utils.consoleLog(...args);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
subscribe(callback: AnalysisResultCallback): () => void {
|
|
77
|
+
this.#subscribers.push(callback);
|
|
78
|
+
return () => this.unsubscribe(callback);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
unsubscribe(callback: AnalysisResultCallback): void {
|
|
82
|
+
this.#subscribers = this.#subscribers.filter(cb => cb !== callback);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#notifySubscribers(result: AnalysisResult): void {
|
|
86
|
+
for (const subscriber of this.#subscribers) {
|
|
87
|
+
subscriber(result);
|
|
88
|
+
}
|
|
89
|
+
const duration = result.end - result.start;
|
|
90
|
+
this.#log('duration: ', `${duration} ms`);
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
92
|
+
const {scanner, leakedFibers, fiberNodes, ...rest} = result;
|
|
93
|
+
this.#log(rest);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
registerExtension(extension: BasicExtension): () => void {
|
|
97
|
+
this.#extensions.push(extension);
|
|
98
|
+
return () => this.unregisterExtension(extension);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
unregisterExtension(extension: BasicExtension): void {
|
|
102
|
+
this.#extensions = this.#extensions.filter(e => e !== extension);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#notifyExtensionsBeforeScan(): void {
|
|
106
|
+
for (const extension of this.#extensions) {
|
|
107
|
+
extension?.beforeScan();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#notifyExtensionsAfterScan(result: AnalysisResult): void {
|
|
112
|
+
for (const extension of this.#extensions) {
|
|
113
|
+
extension?.afterScan(result);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
start() {
|
|
118
|
+
this.#isActivated = true;
|
|
119
|
+
this.#intervalId = setInterval(
|
|
120
|
+
this.#scanCycle.bind(this),
|
|
121
|
+
this.#scanIntervalMs,
|
|
122
|
+
);
|
|
123
|
+
if (config.features.enableMutationObserver) {
|
|
124
|
+
if (this.#domObserver == null) {
|
|
125
|
+
this.#domObserver = new DOMObserver();
|
|
126
|
+
// NOTE: do not update the fiber or component information here
|
|
127
|
+
// with this.#domObserver.register as those elements in the delta
|
|
128
|
+
// list may be unmounted or just attached and their shape and
|
|
129
|
+
// component info is not correct or not set yet
|
|
130
|
+
}
|
|
131
|
+
this.#domObserver.startMonitoring();
|
|
132
|
+
}
|
|
133
|
+
console.log('[Memory] Tracking React and DOM memory...');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#scanCycle() {
|
|
137
|
+
if (!this.#isActivated) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this.#notifyExtensionsBeforeScan();
|
|
141
|
+
const start = performance.now();
|
|
142
|
+
const stats = this.scan();
|
|
143
|
+
const end = performance.now();
|
|
144
|
+
|
|
145
|
+
// inform subscribers and extensions
|
|
146
|
+
const analysiResult = {
|
|
147
|
+
...stats,
|
|
148
|
+
start,
|
|
149
|
+
end,
|
|
150
|
+
scanner: this,
|
|
151
|
+
};
|
|
152
|
+
this.#notifySubscribers(analysiResult);
|
|
153
|
+
this.#notifyExtensionsAfterScan(analysiResult);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
pause() {
|
|
157
|
+
this.#isActivated = false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
stop() {
|
|
161
|
+
this.#isActivated = false;
|
|
162
|
+
clearInterval(this.#intervalId);
|
|
163
|
+
this.#elementWeakRefs = [];
|
|
164
|
+
this.#domObserver?.stopMonitoring();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
recordBoundingRectangles(elementRefs: Array<WeakRef<Element>>) {
|
|
168
|
+
for (const elemRef of elementRefs) {
|
|
169
|
+
const element = elemRef.deref();
|
|
170
|
+
if (element == null || this.#elementToBoundingRects.has(element)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const rect = utils.getBoundingClientRect(element);
|
|
174
|
+
if (rect != null) {
|
|
175
|
+
this.#elementToBoundingRects.set(element, rect);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getDetachedDOMInfo(): Array<DOMElementInfo> {
|
|
181
|
+
const detachedDOMElements = [];
|
|
182
|
+
for (const weakRef of this.#elementWeakRefs) {
|
|
183
|
+
const element = weakRef.deref();
|
|
184
|
+
if (element == null || element.isConnected) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
// add a unique id to that detach dom element
|
|
188
|
+
const elem = element as AnyValue;
|
|
189
|
+
if (elem.detachedElementId == null) {
|
|
190
|
+
const elementId = ReactMemoryScan.nextElementId++;
|
|
191
|
+
elem.detachedElementIdStr = `memory-id-${elementId}@`;
|
|
192
|
+
elem.detachedElementId = elementId;
|
|
193
|
+
}
|
|
194
|
+
const componentStack = this.#elementToComponentStack.get(element) ?? [];
|
|
195
|
+
detachedDOMElements.push({
|
|
196
|
+
element: weakRef,
|
|
197
|
+
boundingRect: this.#elementToBoundingRects.get(element),
|
|
198
|
+
componentStack,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return detachedDOMElements;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
isDevMode(): boolean {
|
|
205
|
+
return this.#isDevMode;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#updateElementToComponentInfo(elements: Array<WeakRef<Element>>): void {
|
|
209
|
+
for (const elemRef of elements) {
|
|
210
|
+
const element = elemRef.deref();
|
|
211
|
+
if (element == null || this.#elementToComponentStack.has(element)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const fiberNode = getFiberNodeFromElement(element);
|
|
215
|
+
if (fiberNode == null) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
this.#elementToComponentStack.set(
|
|
219
|
+
element,
|
|
220
|
+
getReactComponentStack(fiberNode),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
getCachedComponentName(elementRef: WeakRef<Element>): string {
|
|
226
|
+
const FALLBACK_NAME = '<Unknown>';
|
|
227
|
+
const element = elementRef.deref();
|
|
228
|
+
if (element == null) {
|
|
229
|
+
return FALLBACK_NAME;
|
|
230
|
+
}
|
|
231
|
+
const componentStack = this.#elementToComponentStack.get(element);
|
|
232
|
+
if (componentStack == null) {
|
|
233
|
+
return FALLBACK_NAME;
|
|
234
|
+
}
|
|
235
|
+
return componentStack[0] ?? FALLBACK_NAME;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
updateFiberNodes(fiberNodes: Array<WeakRef<Fiber>>): Array<WeakRef<Fiber>> {
|
|
239
|
+
const knownFiberSet = new WeakSet<Fiber>();
|
|
240
|
+
for (const fiberNode of this.#knownFiberNodes) {
|
|
241
|
+
const fiber = fiberNode.deref();
|
|
242
|
+
if (fiber != null) {
|
|
243
|
+
knownFiberSet.add(fiber);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const newFiberSet = new WeakSet<Fiber>();
|
|
247
|
+
for (const fiberNode of fiberNodes) {
|
|
248
|
+
const fiber = fiberNode.deref();
|
|
249
|
+
if (fiber != null) {
|
|
250
|
+
newFiberSet.add(fiber);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const leakedFibers: Array<WeakRef<Fiber>> = [];
|
|
254
|
+
const newExistingFibers: Array<WeakRef<Fiber>> = [];
|
|
255
|
+
// clean up and compact the existing fiber node lists
|
|
256
|
+
for (const fiberRef of this.#knownFiberNodes) {
|
|
257
|
+
const fiber = fiberRef.deref();
|
|
258
|
+
if (fiber == null) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (!newFiberSet.has(fiber)) {
|
|
262
|
+
leakedFibers.push(fiberRef);
|
|
263
|
+
} else {
|
|
264
|
+
newExistingFibers.push(fiberRef);
|
|
265
|
+
if (fiber.return == null) {
|
|
266
|
+
leakedFibers.push(fiberRef);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// add new fibers to the existing list
|
|
271
|
+
for (const fiberRef of fiberNodes) {
|
|
272
|
+
const fiber = fiberRef.deref();
|
|
273
|
+
if (fiber == null) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (!knownFiberSet.has(fiber)) {
|
|
277
|
+
newExistingFibers.push(fiberRef);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
this.#knownFiberNodes = newExistingFibers;
|
|
281
|
+
this.#log('known fibers: ', this.#knownFiberNodes.length);
|
|
282
|
+
this.#log('leaked fibers: ', leakedFibers.length);
|
|
283
|
+
return leakedFibers;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
packLeakedFibers(leakedFibers: Array<WeakRef<Fiber>>): Array<LeakedFiber> {
|
|
287
|
+
const ret = [];
|
|
288
|
+
for (const leakedFiber of leakedFibers) {
|
|
289
|
+
ret.push(new LeakedFiber(leakedFiber));
|
|
290
|
+
}
|
|
291
|
+
return ret;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#getTrackedDOMRefs(): Array<WeakRef<Element>> {
|
|
295
|
+
if (this.#domObserver == null) {
|
|
296
|
+
return utils.getDOMElements();
|
|
297
|
+
}
|
|
298
|
+
return [...utils.getDOMElements(), ...this.#domObserver.getDOMElements()];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
#runGC(): void {
|
|
302
|
+
if ((window as AnyValue)?.gc != null) {
|
|
303
|
+
(window as AnyValue).gc();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#scanEventListenerLeaks(): EventListenerLeak[] {
|
|
308
|
+
if (this.#eventListenerTracker == null) {
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
// Scan for event listener leaks
|
|
312
|
+
const detachedListeners = this.#eventListenerTracker.scan(
|
|
313
|
+
this.getCachedComponentName.bind(this),
|
|
314
|
+
);
|
|
315
|
+
const eventListenerLeaks: EventListenerLeak[] = [];
|
|
316
|
+
for (const [componentName, listeners] of detachedListeners.entries()) {
|
|
317
|
+
const typeCount = new Map<string, number>();
|
|
318
|
+
for (const listener of listeners) {
|
|
319
|
+
const count = typeCount.get(listener.type) ?? 0;
|
|
320
|
+
typeCount.set(listener.type, count + 1);
|
|
321
|
+
}
|
|
322
|
+
for (const [type, count] of typeCount.entries()) {
|
|
323
|
+
eventListenerLeaks.push({
|
|
324
|
+
type,
|
|
325
|
+
componentName,
|
|
326
|
+
count,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return eventListenerLeaks;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
scan(): ScanResult {
|
|
334
|
+
const start = Date.now();
|
|
335
|
+
this.#runGC();
|
|
336
|
+
const weakRefList = this.#elementWeakRefs;
|
|
337
|
+
// TODO: associate elements with URL and other metadata
|
|
338
|
+
const allElements = this.#getTrackedDOMRefs();
|
|
339
|
+
this.#updateElementToComponentInfo(allElements);
|
|
340
|
+
this.recordBoundingRectangles(allElements);
|
|
341
|
+
utils.updateWeakRefList(weakRefList, allElements);
|
|
342
|
+
const scanResult = this.#fiberAnalyzer.scan(
|
|
343
|
+
weakRefList,
|
|
344
|
+
this.#elementToComponentStack,
|
|
345
|
+
);
|
|
346
|
+
const leakedFibers = this.updateFiberNodes(scanResult.fiberNodes);
|
|
347
|
+
scanResult.leakedFibers = leakedFibers;
|
|
348
|
+
|
|
349
|
+
// scan for event listener leaks
|
|
350
|
+
// TODO: show the results in the UI widget
|
|
351
|
+
scanResult.eventListenerLeaks = this.#scanEventListenerLeaks();
|
|
352
|
+
|
|
353
|
+
(window as AnyValue).leakedFibers = this.packLeakedFibers(leakedFibers);
|
|
354
|
+
const end = Date.now();
|
|
355
|
+
this.#log(`scan took ${end - start}ms`);
|
|
356
|
+
return scanResult;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
class LeakedFiber {
|
|
361
|
+
leakedFiber: WeakRef<Fiber>;
|
|
362
|
+
|
|
363
|
+
constructor(fiber: WeakRef<Fiber>) {
|
|
364
|
+
this.leakedFiber = fiber;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
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 {Fiber} from 'react-reconciler';
|
|
11
|
+
import type {BasicExtension} from '../extensions/basic-extension';
|
|
12
|
+
import type ReactMemoryScan from './react-memory-scan';
|
|
13
|
+
|
|
14
|
+
/** @internal */
|
|
15
|
+
export type AnyValue = any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
16
|
+
|
|
17
|
+
/** @internal */
|
|
18
|
+
export type ObjectValue = {[key: string]: AnyValue};
|
|
19
|
+
|
|
20
|
+
/** @internal */
|
|
21
|
+
export type RecordValue =
|
|
22
|
+
| string
|
|
23
|
+
| number
|
|
24
|
+
| boolean
|
|
25
|
+
| null
|
|
26
|
+
| RecordValue[]
|
|
27
|
+
| {[key: string]: RecordValue};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Given any type `T`, return the union type `T` and `null`
|
|
31
|
+
* @typeParam T - The type that will be made nullable.
|
|
32
|
+
*/
|
|
33
|
+
export type Nullable<T> = T | null;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Given any type `T`, return the union type `T`, `null`, and `undefined`.
|
|
37
|
+
* @typeParam T - The type that will be made both nullable and undefinable.
|
|
38
|
+
*/
|
|
39
|
+
export type Optional<T> = T | null | undefined;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Given any type `T`, return the union type `T` and `undefined`.
|
|
43
|
+
* @typeParam T - The type that will be made undefinable.
|
|
44
|
+
*/
|
|
45
|
+
export type Undefinable<T> = T | undefined;
|
|
46
|
+
|
|
47
|
+
/** @internal */
|
|
48
|
+
export type AnyRecord = Record<string, RecordValue>;
|
|
49
|
+
/** @internal */
|
|
50
|
+
export type StringRecord = Record<string, string>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Options for creating a ReactMemoryScan instance
|
|
54
|
+
* @property {boolean} [isDevMode] - When true, enables dev mode features
|
|
55
|
+
*/
|
|
56
|
+
export type CreateOptions = {
|
|
57
|
+
isDevMode?: boolean;
|
|
58
|
+
subscribers?: Array<AnalysisResultCallback>;
|
|
59
|
+
extensions?: Array<BasicExtension>;
|
|
60
|
+
scanIntervalMs?: number;
|
|
61
|
+
trackEventListenerLeaks?: boolean;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Result of scanning react fiber tree and DOM elements
|
|
66
|
+
* @property {Set<string>} components - Set of component names found in the fiber tree
|
|
67
|
+
* @property {Map<string, number>} componentToFiberNodeCount - Map of component names to their instance counts
|
|
68
|
+
* @property {number} totalElements - Total number of DOM elements found
|
|
69
|
+
* @property {number} totalDetachedElements - Number of detached DOM elements found
|
|
70
|
+
* @property {Map<string, number>} detachedComponentToFiberNodeCount - Map of component names to their detached instance counts
|
|
71
|
+
*/
|
|
72
|
+
export type EventListenerLeak = {
|
|
73
|
+
type: string;
|
|
74
|
+
componentName: string;
|
|
75
|
+
count: number;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type ScanResult = {
|
|
79
|
+
components: Set<string>;
|
|
80
|
+
componentToFiberNodeCount: Map<string, number>;
|
|
81
|
+
totalElements: number;
|
|
82
|
+
totalDetachedElements: number;
|
|
83
|
+
detachedComponentToFiberNodeCount: Map<string, number>;
|
|
84
|
+
fiberNodes: Array<WeakRef<Fiber>>;
|
|
85
|
+
leakedFibers: Array<WeakRef<Fiber>>;
|
|
86
|
+
eventListenerLeaks?: EventListenerLeak[];
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Result of analyzing React fiber nodes and DOM elements
|
|
91
|
+
* @property {number} start - Start time of the analysis
|
|
92
|
+
* @property {number} end - End time of the analysis
|
|
93
|
+
*/
|
|
94
|
+
export type AnalysisResult = ScanResult & {
|
|
95
|
+
start: number;
|
|
96
|
+
end: number;
|
|
97
|
+
scanner: ReactMemoryScan;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Callback function type for analysis results
|
|
102
|
+
* @param {AnalysisResult} result - The analysis result
|
|
103
|
+
*/
|
|
104
|
+
export type AnalysisResultCallback = (result: AnalysisResult) => void;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Represents the dimensions and position of an element's bounding box
|
|
108
|
+
* @property {number} x - The x-coordinate of the element's left edge relative to the viewport
|
|
109
|
+
* @property {number} y - The y-coordinate of the element's top edge relative to the viewport
|
|
110
|
+
* @property {number} width - The width of the element's bounding box
|
|
111
|
+
* @property {number} height - The height of the element's bounding box
|
|
112
|
+
* @property {number} top - The distance from the top of the viewport to the element's top edge
|
|
113
|
+
* @property {number} right - The distance from the left of the viewport to the element's right edge
|
|
114
|
+
* @property {number} bottom - The distance from the top of the viewport to the element's bottom edge
|
|
115
|
+
* @property {number} left - The distance from the left of the viewport to the element's left edge
|
|
116
|
+
* @property {number} scrollLeft - The scroll distance from the left of the viewport to the element's left edge
|
|
117
|
+
* @property {number} scrollTop - The scroll distance from the top of the viewport to the element's top edge
|
|
118
|
+
*/
|
|
119
|
+
export interface BoundingRect {
|
|
120
|
+
x: number;
|
|
121
|
+
y: number;
|
|
122
|
+
width: number;
|
|
123
|
+
height: number;
|
|
124
|
+
top: number;
|
|
125
|
+
right: number;
|
|
126
|
+
bottom: number;
|
|
127
|
+
left: number;
|
|
128
|
+
scrollLeft: number;
|
|
129
|
+
scrollTop: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Represents information about a DOM element
|
|
134
|
+
* @property {WeakRef<Element>} element - A weak reference to the DOM element
|
|
135
|
+
* @property {Optional<BoundingRect>} boundingRect - The bounding rectangle of the element
|
|
136
|
+
* @property {Optional<string>} component - The react component name associated with the element
|
|
137
|
+
*/
|
|
138
|
+
export type DOMElementInfo = {
|
|
139
|
+
element: WeakRef<Element>;
|
|
140
|
+
boundingRect: Optional<BoundingRect>;
|
|
141
|
+
componentStack: Optional<string[]>;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** @internal */
|
|
145
|
+
export type DOMElementStats = {
|
|
146
|
+
elements: number;
|
|
147
|
+
detachedElements: number;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export type DOMObserveCallback = (list: Array<WeakRef<Element>>) => void;
|
|
151
|
+
|
|
152
|
+
export type PerformanceConfig = {
|
|
153
|
+
scanIntervalMs: number;
|
|
154
|
+
maxComponentStackDepth: number;
|
|
155
|
+
memoryMeasurementIntervalMs: number;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export type FeatureFlags = {
|
|
159
|
+
enableMutationObserver: boolean;
|
|
160
|
+
enableMemoryTracking: boolean;
|
|
161
|
+
enableComponentStack: boolean;
|
|
162
|
+
enableConsoleLogs: boolean;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export type Config = {
|
|
166
|
+
performance: PerformanceConfig;
|
|
167
|
+
features: FeatureFlags;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export type Entry<K extends object, V> = {
|
|
171
|
+
ref: WeakRef<K>;
|
|
172
|
+
value: V;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export type FallbackMode = 'strong' | 'noop';
|
|
176
|
+
|
|
177
|
+
export type WeakMapPlusOptions = {
|
|
178
|
+
fallback?: FallbackMode;
|
|
179
|
+
cleanupMs?: number;
|
|
180
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
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 {Optional} from './types';
|
|
12
|
+
|
|
13
|
+
const displayNameBlockList = new Set();
|
|
14
|
+
|
|
15
|
+
export function isValidComponentName(name: Optional<string>): boolean {
|
|
16
|
+
return name != null && !displayNameBlockList.has(name);
|
|
17
|
+
}
|