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