@memlab/core 1.1.16 → 1.1.17
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/dist/lib/Config.d.ts +2 -0
- package/dist/lib/Console.d.ts +3 -0
- package/dist/lib/Console.js +81 -28
- package/dist/lib/FileManager.d.ts +4 -0
- package/dist/lib/FileManager.js +27 -0
- package/dist/lib/HeapAnalyzer.d.ts +5 -14
- package/dist/lib/HeapAnalyzer.js +9 -285
- package/dist/lib/Types.d.ts +74 -7
- package/dist/lib/Utils.d.ts +7 -1
- package/dist/lib/Utils.js +49 -1
- package/dist/lib/heap-data/HeapNode.js +3 -4
- package/dist/lib/heap-data/HeapSnapshot.js +2 -2
- package/package.json +2 -2
package/dist/lib/Config.d.ts
CHANGED
|
@@ -71,6 +71,7 @@ export declare class MemLabConfig {
|
|
|
71
71
|
userDataDir: string;
|
|
72
72
|
curDataDir: string;
|
|
73
73
|
webSourceDir: string;
|
|
74
|
+
debugDataDir: string;
|
|
74
75
|
runMetaFile: string;
|
|
75
76
|
snapshotSequenceFile: string;
|
|
76
77
|
exploreResultFile: string;
|
|
@@ -90,6 +91,7 @@ export declare class MemLabConfig {
|
|
|
90
91
|
traceClusterOutDir: string;
|
|
91
92
|
traceJsonOutDir: string;
|
|
92
93
|
metricsOutDir: string;
|
|
94
|
+
heapAnalysisLogDir: string;
|
|
93
95
|
reportScreenshotFile: string;
|
|
94
96
|
newUniqueClusterDir: string;
|
|
95
97
|
staleUniqueClusterDir: string;
|
package/dist/lib/Console.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ declare class MemLabConsole {
|
|
|
23
23
|
private config;
|
|
24
24
|
private sections;
|
|
25
25
|
private log;
|
|
26
|
+
private logFileSet;
|
|
26
27
|
private styles;
|
|
27
28
|
private static singleton;
|
|
28
29
|
protected constructor();
|
|
@@ -40,6 +41,8 @@ declare class MemLabConsole {
|
|
|
40
41
|
private shouldBeConcise;
|
|
41
42
|
private clearPrevOverwriteMsg;
|
|
42
43
|
private printStr;
|
|
44
|
+
registerLogFile(logFile: string): void;
|
|
45
|
+
unregisterLogFile(logFile: string): void;
|
|
43
46
|
beginSection(name: string): void;
|
|
44
47
|
endSection(name: string): void;
|
|
45
48
|
setConfig(config: MemLabConfig): void;
|
package/dist/lib/Console.js
CHANGED
|
@@ -14,10 +14,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
const chalk_1 = __importDefault(require("chalk"));
|
|
16
16
|
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
17
18
|
const readline_1 = __importDefault(require("readline"));
|
|
18
19
|
const string_width_1 = __importDefault(require("string-width"));
|
|
19
20
|
const stdout = process.stdout;
|
|
20
21
|
const TABLE_MAX_WIDTH = 50;
|
|
22
|
+
const LOG_BUFFER_LENGTH = 100;
|
|
21
23
|
const prevLine = '\x1b[F';
|
|
22
24
|
const eraseLine = '\x1b[K';
|
|
23
25
|
const barComplete = chalk_1.default.green('\u2588');
|
|
@@ -42,10 +44,21 @@ function formatTableArg(arg) {
|
|
|
42
44
|
}
|
|
43
45
|
});
|
|
44
46
|
}
|
|
47
|
+
function registerExitCleanup(inst, exitHandler) {
|
|
48
|
+
const p = process;
|
|
49
|
+
// normal exit
|
|
50
|
+
p.on('exit', exitHandler.bind(null, { cleanup: true }));
|
|
51
|
+
// ctrl + c event
|
|
52
|
+
p.on('SIGINT', exitHandler.bind(null, { exit: true }));
|
|
53
|
+
// kill pid
|
|
54
|
+
p.on('SIGUSR1', exitHandler.bind(null, { exit: true }));
|
|
55
|
+
p.on('SIGUSR2', exitHandler.bind(null, { exit: true }));
|
|
56
|
+
}
|
|
45
57
|
class MemLabConsole {
|
|
46
58
|
constructor() {
|
|
47
59
|
this.config = {};
|
|
48
60
|
this.log = [];
|
|
61
|
+
this.logFileSet = new Set();
|
|
49
62
|
this.styles = {
|
|
50
63
|
top: (msg) => msg,
|
|
51
64
|
high: chalk_1.default.dim.bind(chalk_1.default),
|
|
@@ -67,12 +80,15 @@ class MemLabConsole {
|
|
|
67
80
|
}
|
|
68
81
|
const inst = new MemLabConsole();
|
|
69
82
|
MemLabConsole.singleton = inst;
|
|
70
|
-
|
|
83
|
+
const exitHandler = (
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
85
|
+
_options,
|
|
71
86
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
72
|
-
|
|
73
|
-
inst.flushLog();
|
|
87
|
+
_exitCode) => {
|
|
88
|
+
inst.flushLog({ sync: true });
|
|
74
89
|
inst.clearPrevOverwriteMsg();
|
|
75
|
-
}
|
|
90
|
+
};
|
|
91
|
+
registerExitCleanup(inst, exitHandler);
|
|
76
92
|
return inst;
|
|
77
93
|
}
|
|
78
94
|
style(msg, name) {
|
|
@@ -98,28 +114,49 @@ class MemLabConsole {
|
|
|
98
114
|
}
|
|
99
115
|
logMsg(msg) {
|
|
100
116
|
// remove control characters
|
|
101
|
-
const
|
|
117
|
+
const lines = msg.split('\n').map(line => line
|
|
102
118
|
// eslint-disable-next-line no-control-regex
|
|
103
119
|
.replace(/[\u0000-\u001F\u007F-\u009F]/g, '')
|
|
104
|
-
.replace(/\[\d{1,3}m/g, '');
|
|
105
|
-
this.log.push(
|
|
106
|
-
if (this.log.length >
|
|
107
|
-
this.flushLog();
|
|
120
|
+
.replace(/\[\d{1,3}m/g, ''));
|
|
121
|
+
this.log.push(...lines);
|
|
122
|
+
if (this.log.length > LOG_BUFFER_LENGTH) {
|
|
123
|
+
this.flushLog({ sync: true });
|
|
108
124
|
}
|
|
109
125
|
}
|
|
110
|
-
flushLog() {
|
|
126
|
+
flushLog(options = {}) {
|
|
111
127
|
const str = this.log.join('\n');
|
|
112
|
-
if (str.length > 0) {
|
|
113
|
-
const file = this.config.consoleLogFile;
|
|
114
|
-
fs_1.default.appendFile(file, str + '\n', 'UTF-8', () => {
|
|
115
|
-
// NOOP
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
128
|
this.log = [];
|
|
129
|
+
if (str.length === 0) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// synchronous logging
|
|
133
|
+
if (options.sync) {
|
|
134
|
+
for (const logFile of this.logFileSet) {
|
|
135
|
+
try {
|
|
136
|
+
fs_1.default.appendFileSync(logFile, str + '\n', 'UTF-8');
|
|
137
|
+
}
|
|
138
|
+
catch (_a) {
|
|
139
|
+
// fail silently
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
// async logging
|
|
145
|
+
const emptyCallback = () => {
|
|
146
|
+
// no op
|
|
147
|
+
};
|
|
148
|
+
for (const logFile of this.logFileSet) {
|
|
149
|
+
try {
|
|
150
|
+
fs_1.default.appendFile(logFile, str + '\n', 'UTF-8', emptyCallback);
|
|
151
|
+
}
|
|
152
|
+
catch (_b) {
|
|
153
|
+
// fail silently
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
119
157
|
}
|
|
120
158
|
pushMsg(msg, options = {}) {
|
|
121
|
-
|
|
122
|
-
if (this.config.isContinuousTest || len === 0) {
|
|
159
|
+
if (this.sections.arr.length === 0) {
|
|
123
160
|
return;
|
|
124
161
|
}
|
|
125
162
|
// calculate each line's visible width
|
|
@@ -140,7 +177,9 @@ class MemLabConsole {
|
|
|
140
177
|
if (!section || section.msgs.length === 0) {
|
|
141
178
|
return;
|
|
142
179
|
}
|
|
143
|
-
|
|
180
|
+
if (!this.config.muteConsole) {
|
|
181
|
+
stdout.write(eraseLine);
|
|
182
|
+
}
|
|
144
183
|
const msg = section.msgs.pop();
|
|
145
184
|
if (!msg) {
|
|
146
185
|
return;
|
|
@@ -150,8 +189,10 @@ class MemLabConsole {
|
|
|
150
189
|
const line = (_a = lines.pop()) !== null && _a !== void 0 ? _a : 0;
|
|
151
190
|
const width = stdout.columns;
|
|
152
191
|
let n = line === 0 ? 1 : Math.ceil(line / width);
|
|
153
|
-
|
|
154
|
-
|
|
192
|
+
if (!this.config.muteConsole && !this.config.isTest) {
|
|
193
|
+
while (n-- > 0) {
|
|
194
|
+
stdout.write(prevLine + eraseLine);
|
|
195
|
+
}
|
|
155
196
|
}
|
|
156
197
|
}
|
|
157
198
|
}
|
|
@@ -189,11 +230,21 @@ class MemLabConsole {
|
|
|
189
230
|
this.clearPrevMsgInLastSection();
|
|
190
231
|
}
|
|
191
232
|
printStr(msg, options = {}) {
|
|
192
|
-
|
|
233
|
+
this.pushMsg(msg, options);
|
|
234
|
+
if (this.config.isTest) {
|
|
193
235
|
return;
|
|
194
236
|
}
|
|
195
|
-
|
|
196
|
-
|
|
237
|
+
if (this.config.isContinuousTest || !this.config.muteConsole) {
|
|
238
|
+
console.log(msg);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
registerLogFile(logFile) {
|
|
242
|
+
this.flushLog({ sync: true });
|
|
243
|
+
this.logFileSet.add(path_1.default.resolve(logFile));
|
|
244
|
+
}
|
|
245
|
+
unregisterLogFile(logFile) {
|
|
246
|
+
this.flushLog({ sync: true });
|
|
247
|
+
this.logFileSet.delete(path_1.default.resolve(logFile));
|
|
197
248
|
}
|
|
198
249
|
beginSection(name) {
|
|
199
250
|
if (this.config.isContinuousTest) {
|
|
@@ -300,14 +351,16 @@ class MemLabConsole {
|
|
|
300
351
|
this.printStr('');
|
|
301
352
|
}
|
|
302
353
|
overwrite(msg, options = {}) {
|
|
303
|
-
|
|
354
|
+
const str = this.style(msg, options.level || 'low');
|
|
355
|
+
if (this.config.isContinuousTest) {
|
|
356
|
+
this.printStr(msg, { isOverwrite: false });
|
|
304
357
|
return;
|
|
305
358
|
}
|
|
306
|
-
if (this.config.
|
|
307
|
-
|
|
359
|
+
if (this.config.isTest || this.config.muteConsole) {
|
|
360
|
+
this.printStr(str, { isOverwrite: true });
|
|
361
|
+
return;
|
|
308
362
|
}
|
|
309
363
|
this.clearPrevOverwriteMsg();
|
|
310
|
-
const str = this.style(msg, options.level || 'low');
|
|
311
364
|
this.printStr(str, { isOverwrite: true });
|
|
312
365
|
}
|
|
313
366
|
waitForConsole(query) {
|
|
@@ -32,8 +32,11 @@ export declare class FileManager {
|
|
|
32
32
|
getCurDataDir(options: FileOption): string;
|
|
33
33
|
getWebSourceDir(options?: FileOption): string;
|
|
34
34
|
getWebSourceMetaFile(options?: FileOption): string;
|
|
35
|
+
getDebugDataDir(options?: FileOption): string;
|
|
36
|
+
getDebugSourceFile(options?: FileOption): string;
|
|
35
37
|
getPersistDataDir(options: FileOption): string;
|
|
36
38
|
getLoggerOutDir(options?: FileOption): string;
|
|
39
|
+
getHeapAnalysisLogDir(options?: FileOption): string;
|
|
37
40
|
getTraceClustersDir(options?: FileOption): string;
|
|
38
41
|
getTraceJSONDir(options?: FileOption): string;
|
|
39
42
|
getUnclassifiedTraceClusterDir(options?: FileOption): string;
|
|
@@ -61,6 +64,7 @@ export declare class FileManager {
|
|
|
61
64
|
controlWorkDir: string;
|
|
62
65
|
testWorkDir: string;
|
|
63
66
|
};
|
|
67
|
+
initNewHeapAnalysisLogFile(options?: FileOption): string;
|
|
64
68
|
getAndInitTSCompileIntermediateDir(): string;
|
|
65
69
|
clearDataDirs(options?: FileOption): void;
|
|
66
70
|
emptyDirIfExists(dir: string): void;
|
package/dist/lib/FileManager.js
CHANGED
|
@@ -98,12 +98,22 @@ class FileManager {
|
|
|
98
98
|
getWebSourceMetaFile(options = {}) {
|
|
99
99
|
return path_1.default.join(this.getWebSourceDir(options), 'files.json');
|
|
100
100
|
}
|
|
101
|
+
getDebugDataDir(options = {}) {
|
|
102
|
+
return path_1.default.join(this.getDataBaseDir(options), 'debug');
|
|
103
|
+
}
|
|
104
|
+
getDebugSourceFile(options = {}) {
|
|
105
|
+
return path_1.default.join(this.getDebugDataDir(options), 'file.js');
|
|
106
|
+
}
|
|
101
107
|
getPersistDataDir(options) {
|
|
102
108
|
return path_1.default.join(this.getDataBaseDir(options), 'persist');
|
|
103
109
|
}
|
|
104
110
|
getLoggerOutDir(options = {}) {
|
|
105
111
|
return path_1.default.join(this.getDataBaseDir(options), 'logger');
|
|
106
112
|
}
|
|
113
|
+
// all heap analysis results generated
|
|
114
|
+
getHeapAnalysisLogDir(options = {}) {
|
|
115
|
+
return path_1.default.join(this.getLoggerOutDir(options), 'heap-analysis');
|
|
116
|
+
}
|
|
107
117
|
// all trace clusters generated from the current run
|
|
108
118
|
getTraceClustersDir(options = {}) {
|
|
109
119
|
return path_1.default.join(this.getLoggerOutDir(options), 'trace-clusters');
|
|
@@ -200,6 +210,15 @@ class FileManager {
|
|
|
200
210
|
const testWorkDir = joinAndProcessDir({}, expDir, 'test');
|
|
201
211
|
return { controlWorkDir, testWorkDir };
|
|
202
212
|
}
|
|
213
|
+
// create a unique log file created for heap analysis output
|
|
214
|
+
initNewHeapAnalysisLogFile(options = {}) {
|
|
215
|
+
const dir = this.getHeapAnalysisLogDir(options);
|
|
216
|
+
const file = path_1.default.join(dir, `analysis-${Utils_1.default.getUniqueID()}-out.log`);
|
|
217
|
+
if (!fs_extra_1.default.existsSync(file)) {
|
|
218
|
+
fs_extra_1.default.createFileSync(file);
|
|
219
|
+
}
|
|
220
|
+
return file;
|
|
221
|
+
}
|
|
203
222
|
getAndInitTSCompileIntermediateDir() {
|
|
204
223
|
const dir = path_1.default.join(this.getTmpDir(), 'memlab-code');
|
|
205
224
|
this.rmDir(dir);
|
|
@@ -212,6 +231,7 @@ class FileManager {
|
|
|
212
231
|
return;
|
|
213
232
|
}
|
|
214
233
|
this.emptyDirIfExists(this.getWebSourceDir(options));
|
|
234
|
+
this.emptyDirIfExists(this.getDebugDataDir(options));
|
|
215
235
|
const dataSuffix = ['.heapsnapshot', '.json', '.png'];
|
|
216
236
|
const files = fs_extra_1.default.readdirSync(curDataDir);
|
|
217
237
|
for (const file of files) {
|
|
@@ -238,6 +258,8 @@ class FileManager {
|
|
|
238
258
|
this.emptyDirIfExists(this.getUnclassifiedTraceClusterDir(options));
|
|
239
259
|
// all unique cluster info
|
|
240
260
|
this.emptyDirIfExists(this.getUniqueTraceClusterDir(options));
|
|
261
|
+
// all heap analysis results
|
|
262
|
+
this.emptyDirIfExists(this.getHeapAnalysisLogDir(options));
|
|
241
263
|
}
|
|
242
264
|
resetBrowserDir() {
|
|
243
265
|
try {
|
|
@@ -294,9 +316,12 @@ class FileManager {
|
|
|
294
316
|
const outDir = joinAndProcessDir(options, this.getDataOutDir(options));
|
|
295
317
|
config.curDataDir = joinAndProcessDir(options, this.getCurDataDir(options));
|
|
296
318
|
config.webSourceDir = joinAndProcessDir(options, this.getWebSourceDir(options));
|
|
319
|
+
config.debugDataDir = joinAndProcessDir(options, this.getDebugDataDir(options));
|
|
297
320
|
config.dataBuilderDataDir = joinAndProcessDir(options, config.dataBaseDir, 'dataBuilder');
|
|
298
321
|
config.persistentDataDir = joinAndProcessDir(options, this.getPersistDataDir(options));
|
|
322
|
+
// register the default log file
|
|
299
323
|
config.consoleLogFile = path_1.default.join(config.curDataDir, 'console-log.txt');
|
|
324
|
+
Console_1.default.registerLogFile(config.consoleLogFile);
|
|
300
325
|
config.runMetaFile = this.getRunMetaFile(options);
|
|
301
326
|
config.snapshotSequenceFile = this.getSnapshotSequenceMetaFile(options);
|
|
302
327
|
config.browserInfoSummary = path_1.default.join(config.curDataDir, 'browser-info.txt');
|
|
@@ -314,6 +339,8 @@ class FileManager {
|
|
|
314
339
|
config.traceClusterOutDir = joinAndProcessDir(options, this.getTraceClustersDir(options));
|
|
315
340
|
// detailed trace json files for visualization
|
|
316
341
|
config.traceJsonOutDir = joinAndProcessDir(options, this.getTraceJSONDir(options));
|
|
342
|
+
// heap analysis results
|
|
343
|
+
config.heapAnalysisLogDir = joinAndProcessDir(options, this.getHeapAnalysisLogDir(options));
|
|
317
344
|
config.metricsOutDir = joinAndProcessDir(options, loggerOutDir, 'metrics');
|
|
318
345
|
config.reportScreenshotFile = path_1.default.join(outDir, 'report.png');
|
|
319
346
|
const codeDataDir = this.getCodeDataDir();
|
|
@@ -7,15 +7,10 @@
|
|
|
7
7
|
* @format
|
|
8
8
|
* @oncall web_perf_infra
|
|
9
9
|
*/
|
|
10
|
-
import type { E2EStepInfo, HeapNodeIdSet,
|
|
10
|
+
import type { E2EStepInfo, HeapNodeIdSet, IHeapSnapshot, IMemoryAnalystOptions, IMemoryAnalystSnapshotDiff, IOveralHeapInfo, LeakTracePathItem, Optional, IOveralLeakInfo, TraceCluster, ISerializedInfo } from './Types';
|
|
11
11
|
import TraceFinder from '../paths/TraceFinder';
|
|
12
12
|
declare class MemoryAnalyst {
|
|
13
13
|
checkLeak(): Promise<ISerializedInfo[]>;
|
|
14
|
-
checkUnbound(options?: IMemoryAnalystOptions): Promise<void>;
|
|
15
|
-
breakDownMemoryByShapes(options?: {
|
|
16
|
-
file?: string;
|
|
17
|
-
}): Promise<void>;
|
|
18
|
-
detectUnboundGrowth(options?: IMemoryAnalystOptions): Promise<void>;
|
|
19
14
|
detectMemoryLeaks(): Promise<ISerializedInfo[]>;
|
|
20
15
|
visualizeMemoryUsage(options?: IMemoryAnalystOptions): void;
|
|
21
16
|
focus(options?: {
|
|
@@ -23,19 +18,15 @@ declare class MemoryAnalyst {
|
|
|
23
18
|
}): Promise<void>;
|
|
24
19
|
shouldLoadCompleteSnapshot(tabsOrder: E2EStepInfo[], tab: E2EStepInfo): boolean;
|
|
25
20
|
diffSnapshots(loadAll?: boolean): Promise<IMemoryAnalystSnapshotDiff>;
|
|
26
|
-
private calculateRetainedSizes;
|
|
27
21
|
preparePathFinder(snapshot: IHeapSnapshot): TraceFinder;
|
|
28
22
|
private dumpPageInteractionSummary;
|
|
29
23
|
private dumpLeakSummaryToConsole;
|
|
30
24
|
private filterLeakedObjects;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
getOverallHeapInfo(snapshot: IHeapSnapshot, options?: {
|
|
26
|
+
force?: boolean;
|
|
27
|
+
}): Optional<IOveralHeapInfo>;
|
|
34
28
|
getOverallLeakInfo(leakedNodeIds: HeapNodeIdSet, snapshot: IHeapSnapshot): Optional<IOveralLeakInfo>;
|
|
35
|
-
|
|
36
|
-
private breakDownSnapshotByShapes;
|
|
37
|
-
private isTrivialEdgeForBreakDown;
|
|
38
|
-
private breakDownByReferrers;
|
|
29
|
+
printHeapInfo(leakInfo: IOveralHeapInfo): void;
|
|
39
30
|
private printHeapAndLeakInfo;
|
|
40
31
|
private logLeakTraceSummary;
|
|
41
32
|
searchLeakedTraces(leakedNodeIds: HeapNodeIdSet, snapshot: IHeapSnapshot): Promise<{
|
package/dist/lib/HeapAnalyzer.js
CHANGED
|
@@ -23,7 +23,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
24
|
const fs_1 = __importDefault(require("fs"));
|
|
25
25
|
const babar_1 = __importDefault(require("babar"));
|
|
26
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
27
26
|
const LeakClusterLogger_1 = __importDefault(require("../logger/LeakClusterLogger"));
|
|
28
27
|
const LeakTraceDetailsLogger_1 = __importDefault(require("../logger/LeakTraceDetailsLogger"));
|
|
29
28
|
const TraceFinder_1 = __importDefault(require("../paths/TraceFinder"));
|
|
@@ -42,156 +41,6 @@ class MemoryAnalyst {
|
|
|
42
41
|
return yield this.detectMemoryLeaks();
|
|
43
42
|
});
|
|
44
43
|
}
|
|
45
|
-
checkUnbound(options = {}) {
|
|
46
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
47
|
-
this.visualizeMemoryUsage(options);
|
|
48
|
-
Utils_1.default.checkSnapshots(options);
|
|
49
|
-
yield this.detectUnboundGrowth(options);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
breakDownMemoryByShapes(options = {}) {
|
|
53
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
54
|
-
const opt = { buildNodeIdIndex: true, verbose: true };
|
|
55
|
-
const file = options.file ||
|
|
56
|
-
Utils_1.default.getSnapshotFilePathWithTabType(/.*/) ||
|
|
57
|
-
'<EMPTY_FILE_PATH>';
|
|
58
|
-
const snapshot = yield Utils_1.default.getSnapshotFromFile(file, opt);
|
|
59
|
-
this.preparePathFinder(snapshot);
|
|
60
|
-
const heapInfo = this.getOverallHeapInfo(snapshot, { force: true });
|
|
61
|
-
if (heapInfo) {
|
|
62
|
-
this.printHeapInfo(heapInfo);
|
|
63
|
-
}
|
|
64
|
-
this.breakDownSnapshotByShapes(snapshot);
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
// find any objects that keeps growing
|
|
68
|
-
detectUnboundGrowth(options = {}) {
|
|
69
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
70
|
-
const nodeInfo = Object.create(null);
|
|
71
|
-
let hasCheckedFirstSnapshot = false;
|
|
72
|
-
let snapshot = null;
|
|
73
|
-
const isValidNode = (node) => node.type === 'object' ||
|
|
74
|
-
node.type === 'closure' ||
|
|
75
|
-
node.type === 'regexp';
|
|
76
|
-
const initNodeInfo = (node) => {
|
|
77
|
-
if (!isValidNode(node)) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const n = node.retainedSize;
|
|
81
|
-
nodeInfo[node.id] = {
|
|
82
|
-
type: node.type,
|
|
83
|
-
name: node.name,
|
|
84
|
-
min: n,
|
|
85
|
-
max: n,
|
|
86
|
-
history: [n],
|
|
87
|
-
node,
|
|
88
|
-
};
|
|
89
|
-
};
|
|
90
|
-
const updateNodeInfo = (node) => {
|
|
91
|
-
const item = nodeInfo[node.id];
|
|
92
|
-
if (!item) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (node.name !== item.name || node.type !== item.type) {
|
|
96
|
-
nodeInfo[node.id] = null;
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
const n = node.retainedSize;
|
|
100
|
-
// only monotonic increase?
|
|
101
|
-
if (Config_1.default.monotonicUnboundGrowthOnly && n < item.max) {
|
|
102
|
-
nodeInfo[node.id] = null;
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
item.history.push(n);
|
|
106
|
-
item.max = Math.max(item.max, n);
|
|
107
|
-
item.min = Math.min(item.min, n);
|
|
108
|
-
};
|
|
109
|
-
// summarize the heap objects info in current heap snapshot
|
|
110
|
-
// this is mainly used for better understanding of the % of
|
|
111
|
-
// objects released and allocated over time
|
|
112
|
-
const maybeSummarizeNodeInfo = () => {
|
|
113
|
-
if (!Config_1.default.verbose) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
let n = 0;
|
|
117
|
-
for (const k in nodeInfo) {
|
|
118
|
-
if (nodeInfo[k]) {
|
|
119
|
-
++n;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
Console_1.default.lowLevel(`Objects tracked: ${n}`);
|
|
123
|
-
};
|
|
124
|
-
Console_1.default.overwrite('Checking unbounded objects...');
|
|
125
|
-
const snapshotFiles = options.snapshotDir
|
|
126
|
-
? // load snapshots from a directory
|
|
127
|
-
Utils_1.default.getSnapshotFilesInDir(options.snapshotDir)
|
|
128
|
-
: // load snapshots based on the visit sequence meta data
|
|
129
|
-
Utils_1.default.getSnapshotFilesFromTabsOrder();
|
|
130
|
-
for (const file of snapshotFiles) {
|
|
131
|
-
// force GC before loading each snapshot
|
|
132
|
-
if (global.gc) {
|
|
133
|
-
global.gc();
|
|
134
|
-
}
|
|
135
|
-
// load and preprocess heap snapshot
|
|
136
|
-
const opt = { buildNodeIdIndex: true, verbose: true };
|
|
137
|
-
snapshot = yield Utils_1.default.getSnapshotFromFile(file, opt);
|
|
138
|
-
this.calculateRetainedSizes(snapshot);
|
|
139
|
-
// keep track of heap objects
|
|
140
|
-
if (!hasCheckedFirstSnapshot) {
|
|
141
|
-
// record Ids in the snapshot
|
|
142
|
-
snapshot.nodes.forEach(initNodeInfo);
|
|
143
|
-
hasCheckedFirstSnapshot = true;
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
snapshot.nodes.forEach(updateNodeInfo);
|
|
147
|
-
maybeSummarizeNodeInfo();
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
// exit if no heap snapshot found
|
|
151
|
-
if (!hasCheckedFirstSnapshot) {
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
// post process and print the unbounded objects
|
|
155
|
-
const idsInLastSnapshot = new Set();
|
|
156
|
-
snapshot === null || snapshot === void 0 ? void 0 : snapshot.nodes.forEach(node => {
|
|
157
|
-
idsInLastSnapshot.add(node.id);
|
|
158
|
-
});
|
|
159
|
-
let ids = [];
|
|
160
|
-
for (const key in nodeInfo) {
|
|
161
|
-
const id = parseInt(key, 10);
|
|
162
|
-
const item = nodeInfo[id];
|
|
163
|
-
if (!item) {
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
if (!idsInLastSnapshot.has(id)) {
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
if (item.min === item.max) {
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
// filter out non-significant leaks
|
|
173
|
-
if (item.history[item.history.length - 1] < Config_1.default.unboundSizeThreshold) {
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
ids.push(Object.assign({ id }, item));
|
|
177
|
-
}
|
|
178
|
-
if (ids.length === 0) {
|
|
179
|
-
Console_1.default.midLevel('No increasing objects found.');
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
ids = ids
|
|
183
|
-
.sort((o1, o2) => o2.history[o2.history.length - 1] - o1.history[o1.history.length - 1])
|
|
184
|
-
.slice(0, 20);
|
|
185
|
-
// print on terminal
|
|
186
|
-
const str = Serializer_1.default.summarizeUnboundedObjects(ids, { color: true });
|
|
187
|
-
Console_1.default.topLevel('Top growing objects in sizes:');
|
|
188
|
-
Console_1.default.lowLevel(' (Use `memlab trace --node-id=@ID` to get trace)');
|
|
189
|
-
Console_1.default.topLevel('\n' + str);
|
|
190
|
-
// save results to file
|
|
191
|
-
const csv = Serializer_1.default.summarizeUnboundedObjectsToCSV(ids);
|
|
192
|
-
fs_1.default.writeFileSync(Config_1.default.unboundObjectCSV, csv, 'UTF-8');
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
44
|
// find all unique pattern of leaks
|
|
196
45
|
detectMemoryLeaks() {
|
|
197
46
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -368,11 +217,6 @@ class MemoryAnalyst {
|
|
|
368
217
|
};
|
|
369
218
|
});
|
|
370
219
|
}
|
|
371
|
-
calculateRetainedSizes(snapshot) {
|
|
372
|
-
const finder = new TraceFinder_1.default();
|
|
373
|
-
// dominator and retained size
|
|
374
|
-
finder.calculateAllNodesRetainedSizes(snapshot);
|
|
375
|
-
}
|
|
376
220
|
// initialize the path finder
|
|
377
221
|
preparePathFinder(snapshot) {
|
|
378
222
|
const finder = new TraceFinder_1.default();
|
|
@@ -442,14 +286,6 @@ class MemoryAnalyst {
|
|
|
442
286
|
Console_1.default.midLevel(`${leakedNodeIds.size} Fiber nodes and Detached elements`);
|
|
443
287
|
}
|
|
444
288
|
}
|
|
445
|
-
aggregateDominatorMetrics(ids, snapshot, checkNodeCb, nodeMetricsCb) {
|
|
446
|
-
let ret = 0;
|
|
447
|
-
const dominators = Utils_1.default.getConditionalDominatorIds(ids, snapshot, checkNodeCb);
|
|
448
|
-
Utils_1.default.applyToNodes(dominators, snapshot, node => {
|
|
449
|
-
ret += nodeMetricsCb(node);
|
|
450
|
-
});
|
|
451
|
-
return ret;
|
|
452
|
-
}
|
|
453
289
|
getOverallHeapInfo(snapshot, options = {}) {
|
|
454
290
|
if (!Config_1.default.verbose && !options.force) {
|
|
455
291
|
return;
|
|
@@ -457,22 +293,19 @@ class MemoryAnalyst {
|
|
|
457
293
|
Console_1.default.overwrite('summarizing heap info...');
|
|
458
294
|
const allIds = Utils_1.default.getNodesIdSet(snapshot);
|
|
459
295
|
const heapInfo = {
|
|
460
|
-
fiberNodeSize:
|
|
461
|
-
regularFiberNodeSize:
|
|
462
|
-
detachedFiberNodeSize:
|
|
463
|
-
alternateFiberNodeSize:
|
|
464
|
-
error:
|
|
296
|
+
fiberNodeSize: Utils_1.default.aggregateDominatorMetrics(allIds, snapshot, Utils_1.default.isFiberNode, Utils_1.default.getRetainedSize),
|
|
297
|
+
regularFiberNodeSize: Utils_1.default.aggregateDominatorMetrics(allIds, snapshot, Utils_1.default.isRegularFiberNode, Utils_1.default.getRetainedSize),
|
|
298
|
+
detachedFiberNodeSize: Utils_1.default.aggregateDominatorMetrics(allIds, snapshot, Utils_1.default.isDetachedFiberNode, Utils_1.default.getRetainedSize),
|
|
299
|
+
alternateFiberNodeSize: Utils_1.default.aggregateDominatorMetrics(allIds, snapshot, Utils_1.default.isAlternateNode, Utils_1.default.getRetainedSize),
|
|
300
|
+
error: Utils_1.default.aggregateDominatorMetrics(allIds, snapshot, node => node.name === 'Error', Utils_1.default.getRetainedSize),
|
|
465
301
|
};
|
|
466
302
|
return heapInfo;
|
|
467
303
|
}
|
|
468
|
-
getRetainedSize(node) {
|
|
469
|
-
return node.retainedSize;
|
|
470
|
-
}
|
|
471
304
|
getOverallLeakInfo(leakedNodeIds, snapshot) {
|
|
472
305
|
if (!Config_1.default.verbose) {
|
|
473
306
|
return;
|
|
474
307
|
}
|
|
475
|
-
const leakInfo = Object.assign(Object.assign({}, this.getOverallHeapInfo(snapshot)), { leakedSize:
|
|
308
|
+
const leakInfo = Object.assign(Object.assign({}, this.getOverallHeapInfo(snapshot)), { leakedSize: Utils_1.default.aggregateDominatorMetrics(leakedNodeIds, snapshot, () => true, Utils_1.default.getRetainedSize), leakedFiberNodeSize: Utils_1.default.aggregateDominatorMetrics(leakedNodeIds, snapshot, Utils_1.default.isFiberNode, Utils_1.default.getRetainedSize), leakedAlternateFiberNodeSize: Utils_1.default.aggregateDominatorMetrics(leakedNodeIds, snapshot, Utils_1.default.isAlternateNode, Utils_1.default.getRetainedSize) });
|
|
476
309
|
return leakInfo;
|
|
477
310
|
}
|
|
478
311
|
printHeapInfo(leakInfo) {
|
|
@@ -485,111 +318,6 @@ class MemoryAnalyst {
|
|
|
485
318
|
Console_1.default.topLevel(`· ${name}: ${value}`);
|
|
486
319
|
});
|
|
487
320
|
}
|
|
488
|
-
breakDownSnapshotByShapes(snapshot) {
|
|
489
|
-
Console_1.default.overwrite('Breaking down memory by shapes...');
|
|
490
|
-
const breakdown = Object.create(null);
|
|
491
|
-
const population = Object.create(null);
|
|
492
|
-
// group objects based on their shapes
|
|
493
|
-
snapshot.nodes.forEach(node => {
|
|
494
|
-
if ((node.type !== 'object' && !Utils_1.default.isStringNode(node)) ||
|
|
495
|
-
Config_1.default.nodeIgnoreSetInShape.has(node.name)) {
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
const key = Serializer_1.default.summarizeNodeShape(node);
|
|
499
|
-
breakdown[key] = breakdown[key] || new Set();
|
|
500
|
-
breakdown[key].add(node.id);
|
|
501
|
-
if (population[key] === undefined) {
|
|
502
|
-
population[key] = { examples: [], n: 0 };
|
|
503
|
-
}
|
|
504
|
-
++population[key].n;
|
|
505
|
-
// retain the top 5 examples
|
|
506
|
-
const examples = population[key].examples;
|
|
507
|
-
examples.push(node);
|
|
508
|
-
examples.sort((n1, n2) => n2.retainedSize - n1.retainedSize);
|
|
509
|
-
if (examples.length > 5) {
|
|
510
|
-
examples.pop();
|
|
511
|
-
}
|
|
512
|
-
});
|
|
513
|
-
// calculate and sort based on retained sizes
|
|
514
|
-
const ret = [];
|
|
515
|
-
for (const key in breakdown) {
|
|
516
|
-
const size = this.aggregateDominatorMetrics(breakdown[key], snapshot, () => true, this.getRetainedSize);
|
|
517
|
-
ret.push({ key, retainedSize: size });
|
|
518
|
-
}
|
|
519
|
-
ret.sort((o1, o2) => o2.retainedSize - o1.retainedSize);
|
|
520
|
-
Console_1.default.topLevel('Object shapes with top retained sizes:');
|
|
521
|
-
Console_1.default.lowLevel(' (Use `memlab trace --node-id=@ID` to get trace)\n');
|
|
522
|
-
const topList = ret.slice(0, 40);
|
|
523
|
-
// print settings
|
|
524
|
-
const opt = { color: true, compact: true };
|
|
525
|
-
const dot = chalk_1.default.grey('· ');
|
|
526
|
-
const colon = chalk_1.default.grey(': ');
|
|
527
|
-
// print the shapes with the biggest retained size
|
|
528
|
-
for (const o of topList) {
|
|
529
|
-
const referrerInfo = this.breakDownByReferrers(breakdown[o.key], snapshot);
|
|
530
|
-
const { examples, n } = population[o.key];
|
|
531
|
-
const shapeStr = Serializer_1.default.summarizeNodeShape(examples[0], opt);
|
|
532
|
-
const bytes = Utils_1.default.getReadableBytes(o.retainedSize);
|
|
533
|
-
const examplesStr = examples
|
|
534
|
-
.map(e => `@${e.id} [${Utils_1.default.getReadableBytes(e.retainedSize)}]`)
|
|
535
|
-
.join(' | ');
|
|
536
|
-
const meta = chalk_1.default.grey(` (N: ${n}, Examples: ${examplesStr})`);
|
|
537
|
-
Console_1.default.topLevel(`${dot}${shapeStr}${colon}${bytes}${meta}`);
|
|
538
|
-
Console_1.default.lowLevel(referrerInfo + '\n');
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
isTrivialEdgeForBreakDown(edge) {
|
|
542
|
-
const source = edge.fromNode;
|
|
543
|
-
return (source.type === 'array' ||
|
|
544
|
-
source.name === '(object elements)' ||
|
|
545
|
-
source.name === 'system' ||
|
|
546
|
-
edge.name_or_index === '__proto__' ||
|
|
547
|
-
edge.name_or_index === 'prototype');
|
|
548
|
-
}
|
|
549
|
-
breakDownByReferrers(ids, snapshot) {
|
|
550
|
-
const edgeNames = Object.create(null);
|
|
551
|
-
for (const id of ids) {
|
|
552
|
-
const node = snapshot.getNodeById(id);
|
|
553
|
-
for (const edge of (node === null || node === void 0 ? void 0 : node.referrers) || []) {
|
|
554
|
-
const source = edge.fromNode;
|
|
555
|
-
if (!Utils_1.default.isMeaningfulEdge(edge) ||
|
|
556
|
-
this.isTrivialEdgeForBreakDown(edge)) {
|
|
557
|
-
continue;
|
|
558
|
-
}
|
|
559
|
-
const sourceName = Serializer_1.default.summarizeNodeName(source, {
|
|
560
|
-
color: false,
|
|
561
|
-
});
|
|
562
|
-
const edgeName = Serializer_1.default.summarizeEdgeName(edge, {
|
|
563
|
-
color: false,
|
|
564
|
-
abstract: true,
|
|
565
|
-
});
|
|
566
|
-
const edgeKey = `[${sourceName}] --${edgeName}--> `;
|
|
567
|
-
edgeNames[edgeKey] = edgeNames[edgeKey] || {
|
|
568
|
-
numberOfEdgesToNode: 0,
|
|
569
|
-
source,
|
|
570
|
-
edge,
|
|
571
|
-
};
|
|
572
|
-
++edgeNames[edgeKey].numberOfEdgesToNode;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
const referrerInfo = Object.entries(edgeNames)
|
|
576
|
-
.sort((i1, i2) => i2[1].numberOfEdgesToNode - i1[1].numberOfEdgesToNode)
|
|
577
|
-
.slice(0, 4)
|
|
578
|
-
.map(i => {
|
|
579
|
-
const meta = i[1];
|
|
580
|
-
const source = Serializer_1.default.summarizeNodeName(meta.source, {
|
|
581
|
-
color: true,
|
|
582
|
-
});
|
|
583
|
-
const edgeName = Serializer_1.default.summarizeEdgeName(meta.edge, {
|
|
584
|
-
color: true,
|
|
585
|
-
abstract: true,
|
|
586
|
-
});
|
|
587
|
-
const edgeSummary = `${source} --${edgeName}-->`;
|
|
588
|
-
return ` · ${edgeSummary}: ${meta.numberOfEdgesToNode}`;
|
|
589
|
-
})
|
|
590
|
-
.join('\n');
|
|
591
|
-
return referrerInfo;
|
|
592
|
-
}
|
|
593
321
|
printHeapAndLeakInfo(leakedNodeIds, snapshot) {
|
|
594
322
|
// write page interaction summary to the leaks text file
|
|
595
323
|
this.dumpPageInteractionSummary();
|
|
@@ -616,10 +344,6 @@ class MemoryAnalyst {
|
|
|
616
344
|
this.printHeapAndLeakInfo(leakedNodeIds, snapshot);
|
|
617
345
|
// get all leaked objects
|
|
618
346
|
this.filterLeakedObjects(leakedNodeIds, snapshot);
|
|
619
|
-
if (Config_1.default.verbose) {
|
|
620
|
-
// show a breakdown of different object structures
|
|
621
|
-
this.breakDownSnapshotByShapes(snapshot);
|
|
622
|
-
}
|
|
623
347
|
const nodeIdInPaths = new Set();
|
|
624
348
|
const paths = [];
|
|
625
349
|
let numOfLeakedObjects = 0;
|
|
@@ -642,7 +366,7 @@ class MemoryAnalyst {
|
|
|
642
366
|
Console_1.default.midLevel(`${numOfLeakedObjects} leaked objects`);
|
|
643
367
|
}
|
|
644
368
|
// cluster traces from the current run
|
|
645
|
-
const clusters = TraceBucket_1.default.clusterPaths(paths, snapshot,
|
|
369
|
+
const clusters = TraceBucket_1.default.clusterPaths(paths, snapshot, Utils_1.default.aggregateDominatorMetrics, {
|
|
646
370
|
strategy: Config_1.default.isMLClustering
|
|
647
371
|
? new MLTraceSimilarityStrategy_1.default()
|
|
648
372
|
: undefined,
|
|
@@ -651,7 +375,7 @@ class MemoryAnalyst {
|
|
|
651
375
|
yield this.serializeClusterUpdate(clusters);
|
|
652
376
|
if (Config_1.default.logUnclassifiedClusters) {
|
|
653
377
|
// cluster traces from the current run
|
|
654
|
-
const clustersUnclassified = TraceBucket_1.default.generateUnClassifiedClusters(paths, snapshot,
|
|
378
|
+
const clustersUnclassified = TraceBucket_1.default.generateUnClassifiedClusters(paths, snapshot, Utils_1.default.aggregateDominatorMetrics);
|
|
655
379
|
LeakClusterLogger_1.default.logUnclassifiedClusters(clustersUnclassified);
|
|
656
380
|
}
|
|
657
381
|
return {
|
|
@@ -682,7 +406,7 @@ class MemoryAnalyst {
|
|
|
682
406
|
}
|
|
683
407
|
}, { reverse: true });
|
|
684
408
|
// cluster traces from the current run
|
|
685
|
-
const clusters = TraceBucket_1.default.clusterPaths(paths, snapshot,
|
|
409
|
+
const clusters = TraceBucket_1.default.clusterPaths(paths, snapshot, Utils_1.default.aggregateDominatorMetrics, {
|
|
686
410
|
strategy: Config_1.default.isMLClustering
|
|
687
411
|
? new MLTraceSimilarityStrategy_1.default()
|
|
688
412
|
: undefined,
|
package/dist/lib/Types.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @oncall web_perf_infra
|
|
9
9
|
*/
|
|
10
10
|
import { ParsedArgs } from 'minimist';
|
|
11
|
-
import type { LaunchOptions, Page } from 'puppeteer';
|
|
11
|
+
import type { LaunchOptions, Page as PuppeteerPage } from 'puppeteer';
|
|
12
12
|
import type { ErrorHandling, MemLabConfig } from './Config';
|
|
13
13
|
/** @internal */
|
|
14
14
|
export declare type AnyValue = any;
|
|
@@ -56,6 +56,12 @@ export declare type XvfbType = {
|
|
|
56
56
|
display: () => string;
|
|
57
57
|
};
|
|
58
58
|
/** @internal */
|
|
59
|
+
export declare type ShellOptions = {
|
|
60
|
+
dir?: Optional<string>;
|
|
61
|
+
ignoreError?: Optional<boolean>;
|
|
62
|
+
disconnectStdio?: Optional<boolean>;
|
|
63
|
+
};
|
|
64
|
+
/** @internal */
|
|
59
65
|
export declare type CLIArgs = {
|
|
60
66
|
verbose: boolean;
|
|
61
67
|
app: string;
|
|
@@ -85,6 +91,57 @@ export declare type CLIArgs = {
|
|
|
85
91
|
'local-puppeteer': boolean;
|
|
86
92
|
'snapshot-dir': string;
|
|
87
93
|
};
|
|
94
|
+
/**
|
|
95
|
+
* This is the puppeteer [`Page`](https://pptr.dev/api/puppeteer.page)
|
|
96
|
+
* class used by MemLab. The puppeteer `Page` class instance provides
|
|
97
|
+
* APIs to interact with the web browser.
|
|
98
|
+
*
|
|
99
|
+
* The puppeteer `Page` type can be incompatible across different versions.
|
|
100
|
+
* Your local npm-installed puppeteer version may be different from
|
|
101
|
+
* the puppeteer used by MemLab. This may cause some type errors, for example:
|
|
102
|
+
*
|
|
103
|
+
* ```typescript
|
|
104
|
+
* import type {Page} from 'puppeteer';
|
|
105
|
+
* import type {RunOptions} from '@memlab/api';
|
|
106
|
+
*
|
|
107
|
+
* const runOptions: RunOptions = {
|
|
108
|
+
* scenario: {
|
|
109
|
+
* // initial page load url: Google Maps
|
|
110
|
+
* url: () => {
|
|
111
|
+
* return "https://www.google.com/maps/@37.386427,-122.0428214,11z";
|
|
112
|
+
* },
|
|
113
|
+
* // type error here if your local puppeeter version is different
|
|
114
|
+
* // from the puppeteer used by MemLab
|
|
115
|
+
* action: async function (page: Page) {
|
|
116
|
+
* await page.click('button[aria-label="Hotels"]');
|
|
117
|
+
* },
|
|
118
|
+
* },
|
|
119
|
+
* };
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* To avoid the type error in the code example above, MemLab exports the
|
|
123
|
+
* puppeteer `Page` type used by MemLab so that your code can import it
|
|
124
|
+
* when necessary:
|
|
125
|
+
*
|
|
126
|
+
* ```typescript
|
|
127
|
+
* import type {Page} from '@memlab/core' // import Page type from memlab
|
|
128
|
+
* import type {RunOptions} from 'memlab';
|
|
129
|
+
*
|
|
130
|
+
* const runOptions: RunOptions = {
|
|
131
|
+
* scenario: {
|
|
132
|
+
* // initial page load url: Google Maps
|
|
133
|
+
* url: () => {
|
|
134
|
+
* return "https://www.google.com/maps/@37.386427,-122.0428214,11z";
|
|
135
|
+
* },
|
|
136
|
+
* // no type error here
|
|
137
|
+
* action: async function (page: Page) {
|
|
138
|
+
* await page.click('button[aria-label="Hotels"]');
|
|
139
|
+
* },
|
|
140
|
+
* },
|
|
141
|
+
* };
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export declare type Page = PuppeteerPage;
|
|
88
145
|
/**
|
|
89
146
|
* the predicate callback is used to decide if a
|
|
90
147
|
* entity of type `T`.
|
|
@@ -338,7 +395,8 @@ export declare type LeakFilterCallback = (node: IHeapNode, snapshot: IHeapSnapsh
|
|
|
338
395
|
* For concrete examples, check out {@link action} or {@link back}.
|
|
339
396
|
*
|
|
340
397
|
* @param page the puppeteer [`Page`](https://pptr.dev/api/puppeteer.page)
|
|
341
|
-
* object, which provides APIs to interact with the web browser
|
|
398
|
+
* object, which provides APIs to interact with the web browser.
|
|
399
|
+
* To import this type, check out {@link Page}.
|
|
342
400
|
* @returns no return value
|
|
343
401
|
*/
|
|
344
402
|
export declare type InteractionsCallback = (page: Page, args?: OperationArgs) => Promise<void>;
|
|
@@ -420,7 +478,8 @@ export interface IScenario {
|
|
|
420
478
|
*
|
|
421
479
|
* * **Parameters**:
|
|
422
480
|
* * page: `Page` | the puppeteer [`Page`](https://pptr.dev/api/puppeteer.page)
|
|
423
|
-
* object, which provides APIs to interact with the web browser
|
|
481
|
+
* object, which provides APIs to interact with the web browser. To import
|
|
482
|
+
* this type, check out {@link Page}.
|
|
424
483
|
*
|
|
425
484
|
* * **Examples**:
|
|
426
485
|
* ```typescript
|
|
@@ -467,7 +526,8 @@ export interface IScenario {
|
|
|
467
526
|
*
|
|
468
527
|
* * **Parameters**:
|
|
469
528
|
* * page: `Page` | the puppeteer [`Page`](https://pptr.dev/api/puppeteer.page)
|
|
470
|
-
* object, which provides APIs to interact with the web browser
|
|
529
|
+
* object, which provides APIs to interact with the web browser. To import
|
|
530
|
+
* this type, check out {@link Page}.
|
|
471
531
|
*
|
|
472
532
|
* * **Examples**:
|
|
473
533
|
* ```typescript
|
|
@@ -496,7 +556,8 @@ export interface IScenario {
|
|
|
496
556
|
*
|
|
497
557
|
* * **Parameters**:
|
|
498
558
|
* * page: `Page` | the puppeteer [`Page`](https://pptr.dev/api/puppeteer.page)
|
|
499
|
-
* object, which provides APIs to interact with the web browser
|
|
559
|
+
* object, which provides APIs to interact with the web browser. To import
|
|
560
|
+
* this type, check out {@link Page}.
|
|
500
561
|
*
|
|
501
562
|
* * **Examples**:
|
|
502
563
|
* ```typescript
|
|
@@ -539,7 +600,8 @@ export interface IScenario {
|
|
|
539
600
|
*
|
|
540
601
|
* * **Parameters**:
|
|
541
602
|
* * page: `Page` | the puppeteer [`Page`](https://pptr.dev/api/puppeteer.page)
|
|
542
|
-
* object, which provides APIs to interact with the web browser
|
|
603
|
+
* object, which provides APIs to interact with the web browser. To import
|
|
604
|
+
* this type, check out {@link Page}.
|
|
543
605
|
*
|
|
544
606
|
* * **Examples**:
|
|
545
607
|
* ```typescript
|
|
@@ -582,7 +644,8 @@ export interface IScenario {
|
|
|
582
644
|
*
|
|
583
645
|
* * **Parameters**:
|
|
584
646
|
* * page: `Page` | the puppeteer [`Page`](https://pptr.dev/api/puppeteer.page)
|
|
585
|
-
* object, which provides APIs to interact with the web browser
|
|
647
|
+
* object, which provides APIs to interact with the web browser. To import
|
|
648
|
+
* this type, check out {@link Page}.
|
|
586
649
|
* * **Returns**: a boolean value, if it returns `true`, memlab will consider
|
|
587
650
|
* the navigation completes, if it returns `false`, memlab will keep calling
|
|
588
651
|
* this callback until it returns `true`. This is an async callback, you can
|
|
@@ -766,6 +829,7 @@ export interface IDataBuilder {
|
|
|
766
829
|
* Callback function to provide if the page is loaded.
|
|
767
830
|
* For concrete example, check out {@link isPageLoaded}.
|
|
768
831
|
* @param page - puppeteer's [Page](https://pptr.dev/api/puppeteer.page/) object.
|
|
832
|
+
* To import this type, check out {@link Page}.
|
|
769
833
|
* @returns a boolean value, if it returns `true`, memlab will consider
|
|
770
834
|
* the navigation completes, if it returns `false`, memlab will keep calling
|
|
771
835
|
* this callback until it returns `true`. This is an async callback, you can
|
|
@@ -773,6 +837,8 @@ export interface IDataBuilder {
|
|
|
773
837
|
*/
|
|
774
838
|
export declare type CheckPageLoadCallback = (page: Page) => Promise<boolean>;
|
|
775
839
|
/** @internal */
|
|
840
|
+
export declare type PageSetupCallback = (page: Page) => Promise<void>;
|
|
841
|
+
/** @internal */
|
|
776
842
|
export interface IE2EScenarioVisitPlan {
|
|
777
843
|
name: string;
|
|
778
844
|
appName: string;
|
|
@@ -786,6 +852,7 @@ export interface IE2EScenarioVisitPlan {
|
|
|
786
852
|
dataBuilder: Optional<IDataBuilder>;
|
|
787
853
|
isPageLoaded?: CheckPageLoadCallback;
|
|
788
854
|
scenario?: IScenario;
|
|
855
|
+
pageSetup?: PageSetupCallback;
|
|
789
856
|
}
|
|
790
857
|
/** @internal */
|
|
791
858
|
export declare type OperationArgs = {
|
package/dist/lib/Utils.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @format
|
|
8
8
|
* @oncall web_perf_infra
|
|
9
9
|
*/
|
|
10
|
-
import type { HaltOrThrowOptions } from './Types';
|
|
10
|
+
import type { HaltOrThrowOptions, HeapNodeIdSet, ShellOptions } from './Types';
|
|
11
11
|
import type { Browser, Page } from 'puppeteer';
|
|
12
12
|
import type { AnyAyncFunction, AnyOptions, E2EStepInfo, IHeapSnapshot, IHeapNode, IHeapEdge, IScenario, ILeakFilter, LeakTracePathItem, RunMetaInfo, RawHeapSnapshot, Nullable, Optional } from './Types';
|
|
13
13
|
declare function isHermesInternalObject(node: IHeapNode): boolean;
|
|
@@ -122,7 +122,11 @@ declare function getError(maybeError: unknown): Error;
|
|
|
122
122
|
declare function isNodeDominatedByDeletionsArray(node: IHeapNode): boolean;
|
|
123
123
|
declare function getUniqueID(): string;
|
|
124
124
|
declare function getClosureSourceUrl(node: IHeapNode): Nullable<string>;
|
|
125
|
+
export declare function runShell(command: string, options?: ShellOptions): string;
|
|
126
|
+
export declare function getRetainedSize(node: IHeapNode): number;
|
|
127
|
+
export declare function aggregateDominatorMetrics(ids: HeapNodeIdSet, snapshot: IHeapSnapshot, checkNodeCb: (node: IHeapNode) => boolean, nodeMetricsCb: (node: IHeapNode) => number): number;
|
|
125
128
|
declare const _default: {
|
|
129
|
+
aggregateDominatorMetrics: typeof aggregateDominatorMetrics;
|
|
126
130
|
applyToNodes: typeof applyToNodes;
|
|
127
131
|
callAsync: typeof callAsync;
|
|
128
132
|
camelCaseToReadableString: typeof camelCaseToReadableString;
|
|
@@ -148,6 +152,7 @@ declare const _default: {
|
|
|
148
152
|
getReadableBytes: typeof getReadableBytes;
|
|
149
153
|
getReadablePercent: typeof getReadablePercent;
|
|
150
154
|
getReadableTime: typeof getReadableTime;
|
|
155
|
+
getRetainedSize: typeof getRetainedSize;
|
|
151
156
|
getRunMetaFilePath: typeof getRunMetaFilePath;
|
|
152
157
|
getScenarioName: typeof getScenarioName;
|
|
153
158
|
getSingleSnapshotFileForAnalysis: typeof getSingleSnapshotFileForAnalysis;
|
|
@@ -215,6 +220,7 @@ declare const _default: {
|
|
|
215
220
|
repeat: typeof repeat;
|
|
216
221
|
resolveFilePath: typeof resolveFilePath;
|
|
217
222
|
resolveSnapshotFilePath: typeof resolveSnapshotFilePath;
|
|
223
|
+
runShell: typeof runShell;
|
|
218
224
|
setIsAlternateNode: typeof setIsAlternateNode;
|
|
219
225
|
setIsRegularFiberNode: typeof setIsRegularFiberNode;
|
|
220
226
|
shouldShowMoreInfo: typeof shouldShowMoreInfo;
|
package/dist/lib/Utils.js
CHANGED
|
@@ -44,9 +44,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
44
44
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
45
45
|
};
|
|
46
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
-
exports.resolveSnapshotFilePath = void 0;
|
|
47
|
+
exports.aggregateDominatorMetrics = exports.getRetainedSize = exports.runShell = exports.resolveSnapshotFilePath = void 0;
|
|
48
48
|
const fs_1 = __importDefault(require("fs"));
|
|
49
49
|
const path_1 = __importDefault(require("path"));
|
|
50
|
+
const child_process_1 = __importDefault(require("child_process"));
|
|
50
51
|
const process_1 = __importDefault(require("process"));
|
|
51
52
|
const Config_1 = __importStar(require("./Config"));
|
|
52
53
|
const Console_1 = __importDefault(require("./Console"));
|
|
@@ -1751,7 +1752,52 @@ function getClosureSourceUrl(node) {
|
|
|
1751
1752
|
const url = (_b = (_a = urlNode === null || urlNode === void 0 ? void 0 : urlNode.toStringNode()) === null || _a === void 0 ? void 0 : _a.stringValue) !== null && _b !== void 0 ? _b : null;
|
|
1752
1753
|
return url;
|
|
1753
1754
|
}
|
|
1755
|
+
function runShell(command, options = {}) {
|
|
1756
|
+
var _a, _b, _c;
|
|
1757
|
+
const runningDir = (_b = (_a = options.dir) !== null && _a !== void 0 ? _a : Config_1.default.workDir) !== null && _b !== void 0 ? _b : FileManager_1.default.getTmpDir();
|
|
1758
|
+
const execOptions = {
|
|
1759
|
+
cwd: runningDir,
|
|
1760
|
+
stdio: options.disconnectStdio
|
|
1761
|
+
? []
|
|
1762
|
+
: [process_1.default.stdin, process_1.default.stdout, process_1.default.stderr],
|
|
1763
|
+
};
|
|
1764
|
+
if (process_1.default.platform !== 'win32') {
|
|
1765
|
+
execOptions.shell = '/bin/bash';
|
|
1766
|
+
}
|
|
1767
|
+
let ret = '';
|
|
1768
|
+
try {
|
|
1769
|
+
ret = child_process_1.default.execSync(command, execOptions);
|
|
1770
|
+
}
|
|
1771
|
+
catch (ex) {
|
|
1772
|
+
if (Config_1.default.verbose) {
|
|
1773
|
+
if (ex instanceof Error) {
|
|
1774
|
+
Console_1.default.lowLevel(ex.message);
|
|
1775
|
+
Console_1.default.lowLevel((_c = ex.stack) !== null && _c !== void 0 ? _c : '');
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
if (options.ignoreError === true) {
|
|
1779
|
+
return '';
|
|
1780
|
+
}
|
|
1781
|
+
__1.utils.haltOrThrow(`Error when executing command: ${command}`);
|
|
1782
|
+
}
|
|
1783
|
+
return ret && ret.toString('UTF-8');
|
|
1784
|
+
}
|
|
1785
|
+
exports.runShell = runShell;
|
|
1786
|
+
function getRetainedSize(node) {
|
|
1787
|
+
return node.retainedSize;
|
|
1788
|
+
}
|
|
1789
|
+
exports.getRetainedSize = getRetainedSize;
|
|
1790
|
+
function aggregateDominatorMetrics(ids, snapshot, checkNodeCb, nodeMetricsCb) {
|
|
1791
|
+
let ret = 0;
|
|
1792
|
+
const dominators = __1.utils.getConditionalDominatorIds(ids, snapshot, checkNodeCb);
|
|
1793
|
+
__1.utils.applyToNodes(dominators, snapshot, node => {
|
|
1794
|
+
ret += nodeMetricsCb(node);
|
|
1795
|
+
});
|
|
1796
|
+
return ret;
|
|
1797
|
+
}
|
|
1798
|
+
exports.aggregateDominatorMetrics = aggregateDominatorMetrics;
|
|
1754
1799
|
exports.default = {
|
|
1800
|
+
aggregateDominatorMetrics,
|
|
1755
1801
|
applyToNodes,
|
|
1756
1802
|
callAsync,
|
|
1757
1803
|
camelCaseToReadableString,
|
|
@@ -1777,6 +1823,7 @@ exports.default = {
|
|
|
1777
1823
|
getReadableBytes,
|
|
1778
1824
|
getReadablePercent,
|
|
1779
1825
|
getReadableTime,
|
|
1826
|
+
getRetainedSize,
|
|
1780
1827
|
getRunMetaFilePath,
|
|
1781
1828
|
getScenarioName,
|
|
1782
1829
|
getSingleSnapshotFileForAnalysis,
|
|
@@ -1844,6 +1891,7 @@ exports.default = {
|
|
|
1844
1891
|
repeat,
|
|
1845
1892
|
resolveFilePath,
|
|
1846
1893
|
resolveSnapshotFilePath,
|
|
1894
|
+
runShell,
|
|
1847
1895
|
setIsAlternateNode,
|
|
1848
1896
|
setIsRegularFiberNode,
|
|
1849
1897
|
shouldShowMoreInfo,
|
|
@@ -257,10 +257,9 @@ class HeapNode {
|
|
|
257
257
|
get location() {
|
|
258
258
|
const heapSnapshot = this.heapSnapshot;
|
|
259
259
|
const locationIdx = heapSnapshot._nodeIdx2LocationIdx[this.idx];
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return new HeapLocation_1.default(heapSnapshot, locationIdx);
|
|
260
|
+
return locationIdx == null
|
|
261
|
+
? null
|
|
262
|
+
: new HeapLocation_1.default(heapSnapshot, locationIdx);
|
|
264
263
|
}
|
|
265
264
|
// search reference by edge name and edge type
|
|
266
265
|
getReference(edgeName, edgeType) {
|
|
@@ -206,8 +206,8 @@ class HeapSnapshot {
|
|
|
206
206
|
const locationFieldsCount = this._locationFieldsCount;
|
|
207
207
|
let locationIdx = 0;
|
|
208
208
|
while (locationIdx < this._locationCount) {
|
|
209
|
-
const
|
|
210
|
-
this._nodeIdx2LocationIdx[
|
|
209
|
+
const nodeIndex = locations[locationIdx * locationFieldsCount + this._locationObjectIndexOffset];
|
|
210
|
+
this._nodeIdx2LocationIdx[nodeIndex] = locationIdx;
|
|
211
211
|
++locationIdx;
|
|
212
212
|
}
|
|
213
213
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memlab/core",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.17",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "memlab core libraries",
|
|
6
6
|
"author": "Liang Gong <lgong@fb.com>",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"scripts": {
|
|
60
60
|
"build-pkg": "tsc",
|
|
61
61
|
"test-pkg": "jest .",
|
|
62
|
-
"publish-patch": "npm
|
|
62
|
+
"publish-patch": "npm publish",
|
|
63
63
|
"clean-pkg": "rm -rf ./dist && rm -rf ./node_modules && rm -f ./tsconfig.tsbuildinfo"
|
|
64
64
|
},
|
|
65
65
|
"bugs": {
|