Skip to content

Commit 0dc1e05

Browse files
JacksonGLfacebook-github-bot
authored andcommitted
feat(api): SnapshotResultReader API for detecting memory leaks soly based on heap snapshots
Summary: This diff adds a new `SnapshotResultReader` extending the `BaseResultReader` base class so that we can use the `findLeaks(reader)` API to detect memory leaks when we only have three snapshots taken manually (without the other meta data generated by memlab run) Here is an example code snippet on how it could be used: ``` const {SnapshotResultReader, findLeaks} = require('memlab/api'); // baseline, target, and final are file paths of heap snapshot files const reader = SnapshotResultReader.fromSnapshots(baseline, target, final); const leaks = await findLeaks(reader); ``` Related #35 Differential Revision: D48510720 fbshipit-source-id: 2bada7192ad7f6d99356666b62878c89d7021ffb
1 parent c843368 commit 0dc1e05

11 files changed

+411
-16
lines changed

packages/api/src/API.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
import {BaseAnalysis} from '@memlab/heap-analysis';
3838
import APIUtils from './lib/APIUtils';
3939
import BrowserInteractionResultReader from './result-reader/BrowserInteractionResultReader';
40+
import BaseResultReader from './result-reader/BaseResultReader';
4041

4142
/**
4243
* Options for configuring browser interaction run, all fields are optional
@@ -230,7 +231,7 @@ export async function takeSnapshots(
230231
* ```
231232
*/
232233
export async function findLeaks(
233-
runResult: BrowserInteractionResultReader,
234+
runResult: BaseResultReader,
234235
): Promise<ISerializedInfo[]> {
235236
const workDir = runResult.getRootDirectory();
236237
fileManager.initDirs(defaultConfig, {workDir});
@@ -292,7 +293,7 @@ export async function findLeaksBySnapshotFilePaths(
292293
* ```
293294
*/
294295
export async function analyze(
295-
runResult: BrowserInteractionResultReader,
296+
runResult: BaseResultReader,
296297
heapAnalyzer: BaseAnalysis,
297298
args: ParsedArgs = {_: []},
298299
): Promise<void> {

packages/api/src/__tests__/API/E2EResultReader.test.ts

+50-1
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212

1313
import fs from 'fs';
1414
import BrowserInteractionResultReader from '../../result-reader/BrowserInteractionResultReader';
15-
import {warmupAndTakeSnapshots} from '../../index';
15+
import {findLeaks, warmupAndTakeSnapshots} from '../../index';
1616
import {scenario, testSetup, testTimeout} from './lib/E2ETestSettings';
17+
import SnapshotResultReader from '../../result-reader/SnapshotResultReader';
1718

1819
beforeEach(testSetup);
1920

@@ -78,3 +79,51 @@ test(
7879
},
7980
testTimeout,
8081
);
82+
83+
function injectDetachedDOMElements() {
84+
// @ts-ignore
85+
window.injectHookForLink4 = () => {
86+
class TestObject {
87+
key: 'value';
88+
}
89+
const arr = [];
90+
for (let i = 0; i < 23; ++i) {
91+
arr.push(document.createElement('div'));
92+
}
93+
// @ts-ignore
94+
window.__injectedValue = arr;
95+
// @ts-ignore
96+
window._path_1 = {x: {y: document.createElement('div')}};
97+
// @ts-ignore
98+
window._path_2 = new Set([document.createElement('div')]);
99+
// @ts-ignore
100+
window._randomObject = [new TestObject()];
101+
};
102+
}
103+
104+
test(
105+
'SnapshotResultReader is working as expected',
106+
async () => {
107+
const result = await warmupAndTakeSnapshots({
108+
scenario,
109+
evalInBrowserAfterInitLoad: injectDetachedDOMElements,
110+
});
111+
const snapshotFiles = result.getSnapshotFiles();
112+
expect(snapshotFiles.length).toBe(3);
113+
const reader = SnapshotResultReader.fromSnapshots(
114+
snapshotFiles[0],
115+
snapshotFiles[1],
116+
snapshotFiles[2],
117+
);
118+
const leaks = await findLeaks(reader);
119+
// detected all different leak trace cluster
120+
expect(leaks.length >= 1).toBe(true);
121+
// expect all traces are found
122+
expect(
123+
leaks.some(leak => JSON.stringify(leak).includes('__injectedValue')),
124+
);
125+
expect(leaks.some(leak => JSON.stringify(leak).includes('_path_1')));
126+
expect(leaks.some(leak => JSON.stringify(leak).includes('_path_2')));
127+
},
128+
testTimeout,
129+
);

packages/api/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export async function registerPackage(): Promise<void> {
1818
export * from './API';
1919
export * from '@memlab/heap-analysis';
2020
export {default as BrowserInteractionResultReader} from './result-reader/BrowserInteractionResultReader';
21+
export {default as SnapshotResultReader} from './result-reader/SnapshotResultReader';
2122
export {
2223
dumpNodeHeapSnapshot,
2324
getNodeInnocentHeap,

packages/api/src/result-reader/BrowserInteractionResultReader.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import BaseResultReader from './BaseResultReader';
1818
/**
1919
* A utility entity to read all generated files from
2020
* the directory holding the data and results from the
21-
* last browser interaction run
21+
* last MemLab browser interaction run
2222
*/
2323
export default class BrowserInteractionResultReader extends BaseResultReader {
2424
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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 web_perf_infra
9+
*/
10+
11+
import {config, E2EStepInfo, RunMetaInfo} from '@memlab/core';
12+
13+
import fs from 'fs-extra';
14+
import {FileManager, RunMetaInfoManager, utils} from '@memlab/core';
15+
import BaseResultReader from './BaseResultReader';
16+
17+
/**
18+
* A utility entity to read all MemLab files generated from
19+
* baseline, target and final heap snapshots.
20+
*
21+
* The most useful feature of this class is when you have
22+
* three separate snapshots (baseline, target, and final)
23+
* that are not taken from MemLab, but you still would
24+
* like to use the `findLeaks` to detect memory leaks:
25+
*
26+
* ```javascript
27+
* const {SnapshotResultReader, findLeaks} = require('@memlab/api');
28+
*
29+
* // baseline, target, and final are file paths of heap snapshot files
30+
* const reader = SnapshotResultReader.fromSnapshots(baseline, target, final);
31+
* const leaks = await findLeaks(reader);
32+
* ```
33+
*/
34+
export default class SnapshotResultReader extends BaseResultReader {
35+
private baselineSnapshot: string;
36+
private targetSnapshot: string;
37+
private finalSnapshot: string;
38+
39+
/**
40+
* build a result reader
41+
* @param workDir absolute path of the directory where the data
42+
* and generated files of the memlab run were stored
43+
*/
44+
protected constructor(
45+
baselineSnapshot: string,
46+
targetSnapshot: string,
47+
finalSnapshot: string,
48+
) {
49+
const fileManager = new FileManager();
50+
const workDir = fileManager.generateTmpHeapDir();
51+
fs.ensureDirSync(workDir);
52+
super(workDir);
53+
this.baselineSnapshot = baselineSnapshot;
54+
this.targetSnapshot = targetSnapshot;
55+
this.finalSnapshot = finalSnapshot;
56+
this.checkSnapshotFiles();
57+
this.createMetaFilesOnDisk(fileManager, workDir);
58+
}
59+
60+
private createMetaFilesOnDisk(
61+
fileManager: FileManager,
62+
workDir: string,
63+
): void {
64+
fileManager.initDirs(config, {workDir});
65+
const visitOrder = this.getInteractionSteps();
66+
const snapSeqFile = fileManager.getSnapshotSequenceMetaFile({workDir});
67+
fs.writeFileSync(snapSeqFile, JSON.stringify(visitOrder, null, 2), 'UTF-8');
68+
}
69+
70+
private checkSnapshotFiles(): void {
71+
if (
72+
!fs.existsSync(this.baselineSnapshot) ||
73+
!fs.existsSync(this.targetSnapshot) ||
74+
!fs.existsSync(this.finalSnapshot)
75+
) {
76+
throw utils.haltOrThrow(
77+
'invalid file path of baseline, target, or final heap snapshots',
78+
);
79+
}
80+
}
81+
82+
/**
83+
* Build a result reader from baseline, target, and final heap snapshot files.
84+
* The three snapshot files do not have to be in the same directory.
85+
* @param baselineSnapshot file path of the baseline heap snapshot
86+
* @param targetSnapshot file path of the target heap snapshot
87+
* @param finalSnapshot file path of the final heap snapshot
88+
* @returns the ResultReader instance
89+
*
90+
* * **Examples**:
91+
* ```javascript
92+
* const {SnapshotResultReader, findLeaks} = require('@memlab/api');
93+
*
94+
* // baseline, target, and final are file paths of heap snapshot files
95+
* const reader = SnapshotResultReader.fromSnapshots(baseline, target, final);
96+
* const leaks = await findLeaks(reader);
97+
* ```
98+
*/
99+
static fromSnapshots(
100+
baselineSnapshot: string,
101+
targetSnapshot: string,
102+
finalSnapshot: string,
103+
): SnapshotResultReader {
104+
return new SnapshotResultReader(
105+
baselineSnapshot,
106+
targetSnapshot,
107+
finalSnapshot,
108+
);
109+
}
110+
111+
/**
112+
* @internal
113+
*/
114+
public static from(workDir = ''): BaseResultReader {
115+
throw utils.haltOrThrow('SnapshotResultReader.from is not supported');
116+
return new BaseResultReader(workDir);
117+
}
118+
119+
/**
120+
* get all snapshot files related to this SnapshotResultReader
121+
* @returns an array of snapshot file's absolute path
122+
*
123+
* * **Examples**:
124+
* ```javascript
125+
* const {SnapshotResultReader} = require('@memlab/api');
126+
*
127+
* // baseline, target, and final are file paths of heap snapshot files
128+
* const reader = SnapshotResultReader.fromSnapshots(baseline, target, final);
129+
* const paths = reader.getSnapshotFiles();
130+
* ```
131+
*/
132+
public getSnapshotFiles(): string[] {
133+
return [this.baselineSnapshot, this.targetSnapshot, this.finalSnapshot];
134+
}
135+
136+
/**
137+
* @internal
138+
*/
139+
public getSnapshotFileDir(): string {
140+
throw utils.haltOrThrow(
141+
'SnapshotResultReader getSnapshotFileDir() method is not supported',
142+
);
143+
return '';
144+
}
145+
146+
/**
147+
* browser interaction step sequence
148+
* @returns an array of browser interaction step information
149+
*
150+
* * **Examples**:
151+
* ```javascript
152+
* const {SnapshotResultReader} = require('@memlab/api');
153+
*
154+
* // baseline, target, and final are file paths of heap snapshot files
155+
* const reader = SnapshotResultReader.fromSnapshots(baseline, target, final);
156+
* const paths = reader.getInteractionSteps();
157+
* ```
158+
*/
159+
public getInteractionSteps(): E2EStepInfo[] {
160+
return this.fileManager.createVisitOrderWithSnapshots(
161+
this.baselineSnapshot,
162+
this.targetSnapshot,
163+
this.finalSnapshot,
164+
);
165+
}
166+
167+
/**
168+
* @internal
169+
* general meta data of the browser interaction run
170+
* @returns meta data about the entire browser interaction
171+
*
172+
* * **Examples**:
173+
* ```javascript
174+
* const {SnapshotResultReader} = require('@memlab/api');
175+
*
176+
* // baseline, target, and final are file paths of heap snapshot files
177+
* const reader = SnapshotResultReader.fromSnapshots(baseline, target, final);
178+
* const metaInfo = reader.getRunMetaInfo();
179+
* ```
180+
*/
181+
public getRunMetaInfo(): RunMetaInfo {
182+
return new RunMetaInfoManager().loadRunMetaExternalTemplate();
183+
}
184+
185+
/**
186+
* @internal
187+
*/
188+
public cleanup(): void {
189+
// do nothing
190+
}
191+
}

packages/core/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export {default as NormalizedTrace} from './trace-cluster/TraceBucket';
5757
/** @internal */
5858
export {default as EvaluationMetric} from './trace-cluster/EvalutationMetric';
5959
/** @internal */
60+
export {RunMetaInfoManager} from './lib/RunInfoUtils';
61+
/** @internal */
6062
export * from './lib/PackageInfoLoader';
6163
/** @internal */
6264
export {default as SequentialClustering} from './trace-cluster/SequentialClustering';

packages/core/src/lib/FileManager.ts

+30
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,36 @@ export class FileManager {
528528
);
529529
}
530530

531+
/**
532+
* create visit order data structure based on specified
533+
* baseline, target, and final heap snapshots
534+
*/
535+
public createVisitOrderWithSnapshots(
536+
baselineSnapshot: string,
537+
targetSnapshot: string,
538+
finalSnapshot: string,
539+
): E2EStepInfo[] {
540+
const snapshotTemplateFile = this.getSnapshotSequenceExternalTemplateFile();
541+
const visitOrder = JSON.parse(
542+
fs.readFileSync(snapshotTemplateFile, 'UTF-8'),
543+
) as E2EStepInfo[];
544+
// fill in snapshot file name for each entry with snapshot: true
545+
visitOrder.forEach(step => {
546+
switch (step.name) {
547+
case 'baseline':
548+
step.snapshotFile = baselineSnapshot;
549+
break;
550+
case 'target':
551+
step.snapshotFile = targetSnapshot;
552+
break;
553+
case 'final':
554+
step.snapshotFile = finalSnapshot;
555+
break;
556+
}
557+
});
558+
return visitOrder;
559+
}
560+
531561
public initDirs(
532562
config: MemLabConfig,
533563
options: FileOption = FileManager.defaultFileOption,

packages/core/src/lib/Utils.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1520,7 +1520,9 @@ function getSnapshotFilePath(
15201520
options: {workDir?: string} = {},
15211521
): string {
15221522
if (tab.snapshotFile) {
1523-
return path.join(fileManager.getCurDataDir(options), tab.snapshotFile);
1523+
return path.isAbsolute(tab.snapshotFile)
1524+
? tab.snapshotFile
1525+
: path.join(fileManager.getCurDataDir(options), tab.snapshotFile);
15241526
}
15251527
const fileName = `s${tab.idx}.heapsnapshot`;
15261528
if (options.workDir) {

website/docs/api/classes/api_src.BrowserInteractionResultReader.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ custom_edit_url: null
77

88
A utility entity to read all generated files from
99
the directory holding the data and results from the
10-
last browser interaction run
10+
last MemLab browser interaction run
1111

1212
## Hierarchy
1313

0 commit comments

Comments
 (0)