@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,41 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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 fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
|
|
13
|
+
export function getBundleContent(): string {
|
|
14
|
+
const bundlePath = path.join(__dirname, 'memlens.run.bundle.min.js');
|
|
15
|
+
return fs.readFileSync(bundlePath, 'utf-8');
|
|
16
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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;
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
* @oncall memory_lab
|
|
9
|
+
*/
|
|
10
|
+
import 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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
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('duration:')) {
|
|
25
|
+
bundleLoaded = true;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await page.setContent(`
|
|
30
|
+
<!DOCTYPE html>
|
|
31
|
+
<html>
|
|
32
|
+
<head>
|
|
33
|
+
<script>
|
|
34
|
+
window.TEST_MEMORY_SCAN = true;
|
|
35
|
+
</script>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<div id="test-container"></div>
|
|
39
|
+
<script>
|
|
40
|
+
${bundleCode}
|
|
41
|
+
</script>
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
|
44
|
+
`);
|
|
45
|
+
|
|
46
|
+
await wait(2000);
|
|
47
|
+
|
|
48
|
+
// check if the bundle loaded successfully
|
|
49
|
+
expect(bundleLoaded).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
|
|
12
|
+
import {test, expect} from '@playwright/test';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import {srcPath} from '../utils/test-utils';
|
|
16
|
+
import {libBundleFilePath} from '../utils/test-utils';
|
|
17
|
+
|
|
18
|
+
test('scan should identify react components in a complex fiber tree (React 18 Dev)', async ({
|
|
19
|
+
page,
|
|
20
|
+
}) => {
|
|
21
|
+
const bundleCode = fs.readFileSync(libBundleFilePath, 'utf8');
|
|
22
|
+
|
|
23
|
+
const reactDevPath = path.join(srcPath, 'tests', 'lib', 'react-v18.dev.js');
|
|
24
|
+
const reactDevCode = fs.readFileSync(reactDevPath, 'utf8');
|
|
25
|
+
|
|
26
|
+
const reactDOMDevPath = path.join(
|
|
27
|
+
srcPath,
|
|
28
|
+
'tests',
|
|
29
|
+
'lib',
|
|
30
|
+
'react-dom-v18.dev.js',
|
|
31
|
+
);
|
|
32
|
+
const reactDOMDevCode = fs.readFileSync(reactDOMDevPath, 'utf8');
|
|
33
|
+
|
|
34
|
+
// Create a simple test page with React and a custom component
|
|
35
|
+
await page.setContent(`
|
|
36
|
+
<html>
|
|
37
|
+
<body>
|
|
38
|
+
<div id="root"></div>
|
|
39
|
+
<script>${reactDevCode}</script>
|
|
40
|
+
<script>${reactDOMDevCode}</script>
|
|
41
|
+
<script>${bundleCode}</script>
|
|
42
|
+
<script>
|
|
43
|
+
function TopContainer() {
|
|
44
|
+
return React.createElement(
|
|
45
|
+
'div',
|
|
46
|
+
null,
|
|
47
|
+
React.createElement(TestContainer),
|
|
48
|
+
React.createElement(TestContainer),
|
|
49
|
+
React.createElement(TestContainer),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
function TestContainer() {
|
|
53
|
+
return React.createElement(
|
|
54
|
+
'div',
|
|
55
|
+
null,
|
|
56
|
+
React.createElement(TestComponent),
|
|
57
|
+
React.createElement(TestComponent)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function TestComponent() {
|
|
62
|
+
return React.createElement('div', null, 'Test Component');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ReactDOM.render(
|
|
66
|
+
React.createElement(TopContainer),
|
|
67
|
+
document.getElementById('root')
|
|
68
|
+
);
|
|
69
|
+
</script>
|
|
70
|
+
</body>
|
|
71
|
+
</html>
|
|
72
|
+
`);
|
|
73
|
+
|
|
74
|
+
// Now the global `MemLens` should be available in the page
|
|
75
|
+
const componentIdentified = await page.evaluate(() => {
|
|
76
|
+
const createReactMemoryScan = (window as AnyValue).MemLens
|
|
77
|
+
.createReactMemoryScan;
|
|
78
|
+
const instance = createReactMemoryScan();
|
|
79
|
+
const analysisResult = instance.scan();
|
|
80
|
+
const componentCountCorrect =
|
|
81
|
+
analysisResult.componentToFiberNodeCount.get('TestComponent') === 6;
|
|
82
|
+
const containerCountCorrect =
|
|
83
|
+
analysisResult.componentToFiberNodeCount.get('TestContainer') === 3;
|
|
84
|
+
const topContainerCountCorrect =
|
|
85
|
+
analysisResult.componentToFiberNodeCount.get('TopContainer') === 1;
|
|
86
|
+
return (
|
|
87
|
+
componentCountCorrect && containerCountCorrect && topContainerCountCorrect
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(componentIdentified).toBe(true);
|
|
92
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
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 {AnyValue} from '../../core/types';
|
|
11
|
+
import {test, expect} from '@playwright/test';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import {srcPath} from '../utils/test-utils';
|
|
15
|
+
import {libBundleFilePath} from '../utils/test-utils';
|
|
16
|
+
|
|
17
|
+
test('scan should identify react components in a complex list fiber tree (React 18 Dev)', async ({
|
|
18
|
+
page,
|
|
19
|
+
}) => {
|
|
20
|
+
const bundleCode = fs.readFileSync(libBundleFilePath, 'utf8');
|
|
21
|
+
|
|
22
|
+
const reactDevPath = path.join(srcPath, 'tests', 'lib', 'react-v18.dev.js');
|
|
23
|
+
const reactDevCode = fs.readFileSync(reactDevPath, 'utf8');
|
|
24
|
+
|
|
25
|
+
const reactDOMDevPath = path.join(
|
|
26
|
+
srcPath,
|
|
27
|
+
'tests',
|
|
28
|
+
'lib',
|
|
29
|
+
'react-dom-v18.dev.js',
|
|
30
|
+
);
|
|
31
|
+
const reactDOMDevCode = fs.readFileSync(reactDOMDevPath, 'utf8');
|
|
32
|
+
|
|
33
|
+
// Create a test page with React and some list components
|
|
34
|
+
await page.setContent(`
|
|
35
|
+
<html>
|
|
36
|
+
<body>
|
|
37
|
+
<div id="root"></div>
|
|
38
|
+
<script>${reactDevCode}</script>
|
|
39
|
+
<script>${reactDOMDevCode}</script>
|
|
40
|
+
<script>${bundleCode}</script>
|
|
41
|
+
<script>
|
|
42
|
+
function ListItem({ text, count }) {
|
|
43
|
+
return React.createElement('li', null, \`$\{text} ($\{count})\`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function List({ items }) {
|
|
47
|
+
return React.createElement(
|
|
48
|
+
'ul',
|
|
49
|
+
{ className: 'list-container' },
|
|
50
|
+
items.map((item, index) =>
|
|
51
|
+
React.createElement(ListItem, {
|
|
52
|
+
key: index,
|
|
53
|
+
text: item.text,
|
|
54
|
+
count: item.count
|
|
55
|
+
})
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Counter({ initialCount }) {
|
|
61
|
+
const [count, setCount] = React.useState(initialCount);
|
|
62
|
+
|
|
63
|
+
return React.createElement(
|
|
64
|
+
'div',
|
|
65
|
+
{ className: 'counter' },
|
|
66
|
+
React.createElement('span', null, \`Count: $\{count}\`),
|
|
67
|
+
React.createElement(
|
|
68
|
+
'button',
|
|
69
|
+
{ onClick: () => setCount(count + 1) },
|
|
70
|
+
'Increment'
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function TestContainer() {
|
|
76
|
+
const listSetup = [];
|
|
77
|
+
for (let i = 0; i < 1000; ++i) {
|
|
78
|
+
listSetup.push({ text: 'Item ' + i, count: i });
|
|
79
|
+
}
|
|
80
|
+
const [items] = React.useState(listSetup);
|
|
81
|
+
|
|
82
|
+
return React.createElement(
|
|
83
|
+
'div',
|
|
84
|
+
{ className: 'container' },
|
|
85
|
+
React.createElement('h1', null, 'Test Application'),
|
|
86
|
+
React.createElement(Counter, { initialCount: 0 }),
|
|
87
|
+
React.createElement(Counter, { initialCount: 10 }),
|
|
88
|
+
React.createElement(List, { items: items }),
|
|
89
|
+
React.createElement(List, { items: items.slice(0, 500) })
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ReactDOM.render(
|
|
94
|
+
React.createElement(TestContainer),
|
|
95
|
+
document.getElementById('root')
|
|
96
|
+
);
|
|
97
|
+
</script>
|
|
98
|
+
</body>
|
|
99
|
+
</html>
|
|
100
|
+
`);
|
|
101
|
+
|
|
102
|
+
// Now the global `MemLens` should be available in the page
|
|
103
|
+
const componentIdentified = await page.evaluate(() => {
|
|
104
|
+
const createReactMemoryScan = (window as AnyValue).MemLens
|
|
105
|
+
.createReactMemoryScan;
|
|
106
|
+
const instance = createReactMemoryScan();
|
|
107
|
+
const analysisResult = instance.scan();
|
|
108
|
+
const counterCorrect =
|
|
109
|
+
analysisResult.componentToFiberNodeCount.get('Counter') === 2;
|
|
110
|
+
const listCorrect =
|
|
111
|
+
analysisResult.componentToFiberNodeCount.get('List') === 2;
|
|
112
|
+
const listItemCorrect =
|
|
113
|
+
analysisResult.componentToFiberNodeCount.get('ListItem') === 1500;
|
|
114
|
+
return counterCorrect && listCorrect && listItemCorrect;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(componentIdentified).toBe(true);
|
|
118
|
+
});
|