@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.
Files changed (50) hide show
  1. package/README.md +63 -0
  2. package/dist/index.js +112 -0
  3. package/explainer.md +54 -0
  4. package/package.json +34 -0
  5. package/playwright.config.ts +21 -0
  6. package/src/config/config.ts +32 -0
  7. package/src/core/dom-observer.ts +189 -0
  8. package/src/core/event-listener-tracker.ts +171 -0
  9. package/src/core/react-fiber-analysis.ts +123 -0
  10. package/src/core/react-memory-scan.ts +366 -0
  11. package/src/core/types.ts +180 -0
  12. package/src/core/valid-component-name.ts +17 -0
  13. package/src/extensions/basic-extension.ts +41 -0
  14. package/src/extensions/dom-visualization-extension.ts +42 -0
  15. package/src/index.ts +16 -0
  16. package/src/memlens.lib.js.flow +75 -0
  17. package/src/memlens.lib.ts +22 -0
  18. package/src/memlens.run.ts +21 -0
  19. package/src/tests/bundle/lib.bundle.test.ts +31 -0
  20. package/src/tests/bundle/run.bundle.start.head.test.ts +48 -0
  21. package/src/tests/bundle/run.bundle.start.test.ts +48 -0
  22. package/src/tests/bundle/run.bundle.test.ts +51 -0
  23. package/src/tests/fiber/dev.fiber.complex.dev.test.ts +92 -0
  24. package/src/tests/fiber/dev.fiber.complex.list.dev.test.ts +118 -0
  25. package/src/tests/fiber/dev.fiber.complex.prod.test.ts +92 -0
  26. package/src/tests/fiber/dev.fiber.name.dev.test.ts +77 -0
  27. package/src/tests/fiber/dev.fiber.name.prod.test.ts +79 -0
  28. package/src/tests/lib/babel.prod.js +4 -0
  29. package/src/tests/lib/react-dom-v18.dev.js +29926 -0
  30. package/src/tests/lib/react-dom-v18.prod.js +269 -0
  31. package/src/tests/lib/react-v18.dev.js +3345 -0
  32. package/src/tests/lib/react-v18.prod.js +33 -0
  33. package/src/tests/manual/playwright-open-manual.js +40 -0
  34. package/src/tests/manual/todo-list/todo-with-run.bundle.html +80 -0
  35. package/src/tests/utils/test-utils.ts +28 -0
  36. package/src/utils/intersection-observer.ts +65 -0
  37. package/src/utils/react-fiber-utils.ts +212 -0
  38. package/src/utils/utils.ts +201 -0
  39. package/src/utils/weak-ref-utils.ts +308 -0
  40. package/src/visual/components/component-stack-panel.ts +85 -0
  41. package/src/visual/components/control-widget.ts +96 -0
  42. package/src/visual/components/overlay-rectangle.ts +167 -0
  43. package/src/visual/components/status-text.ts +53 -0
  44. package/src/visual/components/toggle-button.ts +57 -0
  45. package/src/visual/components/visual-overlay.ts +19 -0
  46. package/src/visual/dom-element-visualizer-interactive.ts +358 -0
  47. package/src/visual/dom-element-visualizer.ts +130 -0
  48. package/src/visual/visual-utils.ts +89 -0
  49. package/tsconfig.json +18 -0
  50. 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
+ }