@memlab/lens 1.0.0 → 1.0.1
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/LICENSE +21 -0
- package/dist/config/config.d.ts +22 -0
- package/dist/core/dom-observer.d.ts +10 -0
- package/dist/core/event-listener-tracker.d.ts +33 -0
- package/dist/core/react-fiber-analysis.d.ts +4 -0
- package/dist/core/react-memory-scan.d.ts +36 -0
- package/{src/core/types.ts → dist/core/types.d.ts} +56 -80
- package/{src/index.ts → dist/core/valid-component-name.d.ts} +2 -7
- package/dist/extensions/basic-extension.d.ts +30 -0
- package/dist/extensions/dom-visualization-extension.d.ts +17 -0
- package/dist/index.d.ts +1 -0
- package/dist/memlens.lib.bundle.js +1695 -0
- package/dist/memlens.lib.bundle.min.js +1 -0
- package/dist/memlens.lib.d.ts +12 -0
- package/dist/memlens.run.bundle.js +2673 -0
- package/dist/memlens.run.bundle.min.js +1 -0
- package/dist/memlens.run.d.ts +1 -0
- package/dist/tests/bundle/lib.bundle.test.d.ts +1 -0
- package/dist/tests/bundle/run.bundle.start.head.test.d.ts +1 -0
- package/dist/tests/bundle/run.bundle.start.test.d.ts +1 -0
- package/dist/tests/bundle/run.bundle.test.d.ts +1 -0
- package/dist/tests/fiber/dev.fiber.complex.dev.test.d.ts +1 -0
- package/dist/tests/fiber/dev.fiber.complex.list.dev.test.d.ts +1 -0
- package/dist/tests/fiber/dev.fiber.complex.prod.test.d.ts +1 -0
- package/dist/tests/fiber/dev.fiber.name.dev.test.d.ts +1 -0
- package/dist/tests/fiber/dev.fiber.name.prod.test.d.ts +1 -0
- package/dist/tests/utils/test-utils.d.ts +11 -0
- package/dist/utils/intersection-observer.d.ts +18 -0
- package/dist/utils/react-fiber-utils.d.ts +56 -0
- package/dist/utils/utils.d.ts +26 -0
- package/dist/utils/weak-ref-utils.d.ts +67 -0
- package/dist/visual/components/component-stack-panel.d.ts +11 -0
- package/dist/visual/components/control-widget.d.ts +13 -0
- package/dist/visual/components/overlay-rectangle.d.ts +11 -0
- package/dist/visual/components/status-text.d.ts +2 -0
- package/dist/visual/components/toggle-button.d.ts +3 -0
- package/dist/visual/components/visual-overlay.d.ts +1 -0
- package/dist/visual/dom-element-visualizer-interactive.d.ts +26 -0
- package/{src/core/valid-component-name.ts → dist/visual/dom-element-visualizer.d.ts} +5 -7
- package/dist/visual/visual-utils.d.ts +16 -0
- package/package.json +5 -1
- package/explainer.md +0 -54
- package/playwright.config.ts +0 -21
- package/src/config/config.ts +0 -32
- package/src/core/dom-observer.ts +0 -189
- package/src/core/event-listener-tracker.ts +0 -171
- package/src/core/react-fiber-analysis.ts +0 -123
- package/src/core/react-memory-scan.ts +0 -366
- package/src/extensions/basic-extension.ts +0 -41
- package/src/extensions/dom-visualization-extension.ts +0 -42
- package/src/memlens.lib.js.flow +0 -75
- package/src/memlens.lib.ts +0 -22
- package/src/memlens.run.ts +0 -21
- package/src/tests/bundle/lib.bundle.test.ts +0 -31
- package/src/tests/bundle/run.bundle.start.head.test.ts +0 -48
- package/src/tests/bundle/run.bundle.start.test.ts +0 -48
- package/src/tests/bundle/run.bundle.test.ts +0 -51
- package/src/tests/fiber/dev.fiber.complex.dev.test.ts +0 -92
- package/src/tests/fiber/dev.fiber.complex.list.dev.test.ts +0 -118
- package/src/tests/fiber/dev.fiber.complex.prod.test.ts +0 -92
- package/src/tests/fiber/dev.fiber.name.dev.test.ts +0 -77
- package/src/tests/fiber/dev.fiber.name.prod.test.ts +0 -79
- package/src/tests/lib/babel.prod.js +0 -4
- package/src/tests/lib/react-dom-v18.dev.js +0 -29926
- package/src/tests/lib/react-dom-v18.prod.js +0 -269
- package/src/tests/lib/react-v18.dev.js +0 -3345
- package/src/tests/lib/react-v18.prod.js +0 -33
- package/src/tests/manual/playwright-open-manual.js +0 -40
- package/src/tests/manual/todo-list/todo-with-run.bundle.html +0 -80
- package/src/tests/utils/test-utils.ts +0 -28
- package/src/utils/intersection-observer.ts +0 -65
- package/src/utils/react-fiber-utils.ts +0 -212
- package/src/utils/utils.ts +0 -201
- package/src/utils/weak-ref-utils.ts +0 -308
- package/src/visual/components/component-stack-panel.ts +0 -85
- package/src/visual/components/control-widget.ts +0 -96
- package/src/visual/components/overlay-rectangle.ts +0 -167
- package/src/visual/components/status-text.ts +0 -53
- package/src/visual/components/toggle-button.ts +0 -57
- package/src/visual/components/visual-overlay.ts +0 -19
- package/src/visual/dom-element-visualizer-interactive.ts +0 -358
- package/src/visual/dom-element-visualizer.ts +0 -130
- package/src/visual/visual-utils.ts +0 -89
- package/tsconfig.json +0 -18
- package/webpack.config.js +0 -105
|
@@ -1,366 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
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 ReactMemoryScan from '../core/react-memory-scan';
|
|
11
|
-
import type {AnalysisResult} from '../core/types';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Base class for React Memory Scanner extensions.
|
|
15
|
-
* Extensions can hook into the scanning process before and after analysis.
|
|
16
|
-
*/
|
|
17
|
-
export abstract class BasicExtension {
|
|
18
|
-
protected readonly scanner: ReactMemoryScan;
|
|
19
|
-
|
|
20
|
-
constructor(scanner: ReactMemoryScan) {
|
|
21
|
-
this.scanner = scanner;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Hook that runs before the memory scan starts.
|
|
26
|
-
* Override this method to perform any setup or pre-scan operations.
|
|
27
|
-
*/
|
|
28
|
-
beforeScan(): void {
|
|
29
|
-
// to be overridden
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Hook that runs after the memory scan completes.
|
|
34
|
-
* Override this method to process or modify the analysis results.
|
|
35
|
-
* @param analysisResult - The results from the memory scan
|
|
36
|
-
*/
|
|
37
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
38
|
-
afterScan(_analysisResult: AnalysisResult): void {
|
|
39
|
-
// to be overridden
|
|
40
|
-
}
|
|
41
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
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 ReactMemoryScan from '../core/react-memory-scan';
|
|
11
|
-
import type {AnalysisResult} from '../core/types';
|
|
12
|
-
|
|
13
|
-
import DOMElementVisualizer from '../visual/dom-element-visualizer';
|
|
14
|
-
import DOMElementVisualizerInteractive from '../visual/dom-element-visualizer-interactive';
|
|
15
|
-
import {BasicExtension} from './basic-extension';
|
|
16
|
-
|
|
17
|
-
const USE_INTERACTIVE_VISUALIZER = true;
|
|
18
|
-
|
|
19
|
-
export class DOMVisualizationExtension extends BasicExtension {
|
|
20
|
-
#domVirtualizer: DOMElementVisualizer;
|
|
21
|
-
|
|
22
|
-
constructor(scanner: ReactMemoryScan) {
|
|
23
|
-
super(scanner);
|
|
24
|
-
if (USE_INTERACTIVE_VISUALIZER) {
|
|
25
|
-
this.#domVirtualizer = new DOMElementVisualizerInteractive();
|
|
26
|
-
} else {
|
|
27
|
-
this.#domVirtualizer = new DOMElementVisualizer();
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
32
|
-
afterScan(_analysisResult: AnalysisResult): void {
|
|
33
|
-
// const start = Date.now();
|
|
34
|
-
const scanner = this.scanner;
|
|
35
|
-
if (scanner.isDevMode()) {
|
|
36
|
-
const detachedDOMInfo = scanner.getDetachedDOMInfo();
|
|
37
|
-
this.#domVirtualizer.repaint(detachedDOMInfo);
|
|
38
|
-
}
|
|
39
|
-
// const end = Date.now();
|
|
40
|
-
// console.log(`repaint took ${end - start}ms`);
|
|
41
|
-
}
|
|
42
|
-
}
|
package/src/memlens.lib.js.flow
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
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
|
-
* @flow strict-local
|
|
8
|
-
* @format
|
|
9
|
-
* @oncall memory_lab
|
|
10
|
-
*/
|
|
11
|
-
// Core types
|
|
12
|
-
export type BoundingRect = {
|
|
13
|
-
bottom: number,
|
|
14
|
-
height: number,
|
|
15
|
-
left: number,
|
|
16
|
-
right: number,
|
|
17
|
-
top: number,
|
|
18
|
-
width: number,
|
|
19
|
-
x: number,
|
|
20
|
-
y: number,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type DOMElementInfo = {
|
|
24
|
-
boundingRect: BoundingRect,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export type Nullable<T> = T | null;
|
|
28
|
-
|
|
29
|
-
// [lib-index.ts]
|
|
30
|
-
export type ReactMemoryScan = {
|
|
31
|
-
constructor: (options: CreateOptions) => void,
|
|
32
|
-
start: () => void,
|
|
33
|
-
stop: () => void,
|
|
34
|
-
scan: () => void,
|
|
35
|
-
visualize: () => void,
|
|
36
|
-
subscribe: (callback: AnalysisResultCallback) => () => void,
|
|
37
|
-
unsubscribe: (callback: AnalysisResultCallback) => void,
|
|
38
|
-
isDevMode: () => boolean,
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
declare export function createReactMemoryScan(
|
|
42
|
-
options: CreateOptions,
|
|
43
|
-
): ReactMemoryScan;
|
|
44
|
-
|
|
45
|
-
export type BasicExtension = {
|
|
46
|
-
beforeScan: () => void,
|
|
47
|
-
afterScan: (result: AnalysisResult) => void,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export type CreateOptions = {
|
|
51
|
-
isDevMode?: boolean,
|
|
52
|
-
subscribers?: Array<AnalysisResultCallback>,
|
|
53
|
-
extensions?: Array<BasicExtension>,
|
|
54
|
-
scanIntervalMs?: number,
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export type ScanResult = {
|
|
58
|
-
components: Set<string>,
|
|
59
|
-
componentToFiberNodeCount: Map<string, number>,
|
|
60
|
-
totalElements: number,
|
|
61
|
-
totalDetachedElements: number,
|
|
62
|
-
detachedComponentToFiberNodeCount: Map<string, number>,
|
|
63
|
-
fiberNodes: Array<WeakRef<Fiber>>,
|
|
64
|
-
leakedFibers: Array<WeakRef<Fiber>>,
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
export type AnalysisResult = {
|
|
68
|
-
...ScanResult,
|
|
69
|
-
start: number,
|
|
70
|
-
end: number,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export type AnalysisResultCallback = (result: AnalysisResult) => void;
|
|
74
|
-
|
|
75
|
-
export type DOMObserveCallback = (list: Array<WeakRef<Element>>) => void;
|
package/src/memlens.lib.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
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 ReactMemoryScan from './core/react-memory-scan';
|
|
11
|
-
import {CreateOptions} from './core/types';
|
|
12
|
-
// import { DOMVisualizationExtension } from './extensions/dom-visualization-extension';
|
|
13
|
-
|
|
14
|
-
export function createReactMemoryScan(
|
|
15
|
-
options: CreateOptions = {},
|
|
16
|
-
): ReactMemoryScan {
|
|
17
|
-
return new ReactMemoryScan(options);
|
|
18
|
-
// const memoryScan = new ReactMemoryScan(options);
|
|
19
|
-
// const domVisualizer = new DOMVisualizationExtension(memoryScan);
|
|
20
|
-
// memoryScan.registerExtension(domVisualizer);
|
|
21
|
-
// return memoryScan;
|
|
22
|
-
}
|
package/src/memlens.run.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
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 ReactMemoryScan from './core/react-memory-scan';
|
|
11
|
-
import {DOMVisualizationExtension} from './extensions/dom-visualization-extension';
|
|
12
|
-
import {hasRunInSession, setRunInSession} from './utils/utils';
|
|
13
|
-
|
|
14
|
-
if (!hasRunInSession()) {
|
|
15
|
-
const memoryScan = new ReactMemoryScan({isDevMode: true});
|
|
16
|
-
const domVisualizer = new DOMVisualizationExtension(memoryScan);
|
|
17
|
-
memoryScan.registerExtension(domVisualizer);
|
|
18
|
-
|
|
19
|
-
memoryScan.start();
|
|
20
|
-
setRunInSession();
|
|
21
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
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 {AnyValue} from '../../core/types';
|
|
11
|
-
import {test, expect} from '@playwright/test';
|
|
12
|
-
import {libBundleFilePath} from '../utils/test-utils';
|
|
13
|
-
|
|
14
|
-
test('test library in browser via addScriptTag', async ({page}) => {
|
|
15
|
-
// Navigate to an empty page (or a test page)
|
|
16
|
-
await page.goto('about:blank');
|
|
17
|
-
|
|
18
|
-
// Inject the lib bundle file (UMD bundle of the library) into the page
|
|
19
|
-
await page.addScriptTag({path: libBundleFilePath});
|
|
20
|
-
|
|
21
|
-
// Now the global `MemLens` should be available in the page
|
|
22
|
-
const libraryLoaded = await page.evaluate(() => {
|
|
23
|
-
const createReactMemoryScan = (window as AnyValue).MemLens
|
|
24
|
-
.createReactMemoryScan;
|
|
25
|
-
const instance = createReactMemoryScan();
|
|
26
|
-
const analysisResult = instance.scan();
|
|
27
|
-
return typeof analysisResult.totalElements === 'number';
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
expect(libraryLoaded).toBe(true);
|
|
31
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
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 {test, expect} from '@playwright/test';
|
|
11
|
-
import fs from 'fs';
|
|
12
|
-
import {wait} from '../utils/test-utils';
|
|
13
|
-
import {runBundleFilePath} from '../utils/test-utils';
|
|
14
|
-
|
|
15
|
-
test.describe('run.bundle.js functionality', () => {
|
|
16
|
-
test('should load and execute correctly', async ({page}) => {
|
|
17
|
-
const bundleCode = fs.readFileSync(runBundleFilePath, 'utf8');
|
|
18
|
-
|
|
19
|
-
let bundleLoaded = false;
|
|
20
|
-
// Listen for console messages
|
|
21
|
-
page.on('console', message => {
|
|
22
|
-
// add console.log(message) if you would like to debug
|
|
23
|
-
const msgText = message.text();
|
|
24
|
-
if (msgText.includes('Tracking React and DOM memory')) {
|
|
25
|
-
bundleLoaded = true;
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
await page.setContent(`
|
|
30
|
-
<!DOCTYPE html>
|
|
31
|
-
<html>
|
|
32
|
-
<head>
|
|
33
|
-
<script>
|
|
34
|
-
${bundleCode}
|
|
35
|
-
</script>
|
|
36
|
-
</head>
|
|
37
|
-
<body>
|
|
38
|
-
<div id="test-container"></div>
|
|
39
|
-
</body>
|
|
40
|
-
</html>
|
|
41
|
-
`);
|
|
42
|
-
|
|
43
|
-
await wait(2000);
|
|
44
|
-
|
|
45
|
-
// check if the bundle loaded successfully
|
|
46
|
-
expect(bundleLoaded).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
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 {test, expect} from '@playwright/test';
|
|
11
|
-
import fs from 'fs';
|
|
12
|
-
import {wait} from '../utils/test-utils';
|
|
13
|
-
import {runBundleFilePath} from '../utils/test-utils';
|
|
14
|
-
|
|
15
|
-
test.describe('run.bundle.js functionality', () => {
|
|
16
|
-
test('should load and execute correctly', async ({page}) => {
|
|
17
|
-
const bundleCode = fs.readFileSync(runBundleFilePath, 'utf8');
|
|
18
|
-
|
|
19
|
-
let bundleLoaded = false;
|
|
20
|
-
// Listen for console messages
|
|
21
|
-
page.on('console', message => {
|
|
22
|
-
// add console.log(message) if you would like to debug
|
|
23
|
-
const msgText = message.text();
|
|
24
|
-
if (msgText.includes('Tracking React and DOM memory')) {
|
|
25
|
-
bundleLoaded = true;
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
await page.setContent(`
|
|
30
|
-
<!DOCTYPE html>
|
|
31
|
-
<html>
|
|
32
|
-
<head>
|
|
33
|
-
</head>
|
|
34
|
-
<body>
|
|
35
|
-
<div id="test-container"></div>
|
|
36
|
-
<script>
|
|
37
|
-
${bundleCode}
|
|
38
|
-
</script>
|
|
39
|
-
</body>
|
|
40
|
-
</html>
|
|
41
|
-
`);
|
|
42
|
-
|
|
43
|
-
await wait(1000);
|
|
44
|
-
|
|
45
|
-
// check if the bundle loaded successfully
|
|
46
|
-
expect(bundleLoaded).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
});
|