@memlab/core 1.1.18 → 1.1.19
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/index.d.ts +4 -2
- package/dist/index.js +6 -3
- package/dist/lib/FileManager.d.ts +2 -0
- package/dist/lib/FileManager.js +32 -4
- package/dist/lib/HeapAnalyzer.d.ts +15 -8
- package/dist/lib/HeapAnalyzer.js +106 -98
- package/dist/lib/Types.d.ts +20 -0
- package/dist/lib/Utils.d.ts +3 -1
- package/dist/lib/Utils.js +7 -3
- package/dist/lib/charts/MemoryBarChart.d.ts +20 -0
- package/dist/lib/charts/MemoryBarChart.js +110 -0
- package/dist/trace-cluster/TraceBucket.d.ts +10 -1
- package/dist/trace-cluster/TraceBucket.js +131 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* @format
|
|
8
8
|
* @oncall web_perf_infra
|
|
9
9
|
*/
|
|
10
|
-
/** @internal */
|
|
11
|
-
export declare function registerPackage(): Promise<void>;
|
|
12
10
|
export * from './lib/Types';
|
|
13
11
|
export * from './lib/NodeHeap';
|
|
14
12
|
/** @internal */
|
|
13
|
+
export declare function registerPackage(): Promise<void>;
|
|
14
|
+
/** @internal */
|
|
15
15
|
export { default as config } from './lib/Config';
|
|
16
16
|
/** @internal */
|
|
17
17
|
export * from './lib/InternalValueSetter';
|
|
@@ -36,6 +36,8 @@ export { default as analysis } from './lib/HeapAnalyzer';
|
|
|
36
36
|
/** @internal */
|
|
37
37
|
export { default as constant } from './lib/Constant';
|
|
38
38
|
/** @internal */
|
|
39
|
+
export { default as memoryBarChart } from './lib/charts/MemoryBarChart';
|
|
40
|
+
/** @internal */
|
|
39
41
|
export { default as modes } from './modes/RunningModes';
|
|
40
42
|
/** @internal */
|
|
41
43
|
export { default as ProcessManager } from './lib/ProcessManager';
|
package/dist/index.js
CHANGED
|
@@ -35,9 +35,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
35
35
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
36
36
|
};
|
|
37
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
-
exports.TraceFinder = exports.MultiIterationSeqClustering = exports.SequentialClustering = exports.EvaluationMetric = exports.NormalizedTrace = exports.leakClusterLogger = exports.ProcessManager = exports.modes = exports.constant = exports.analysis = exports.browserInfo = exports.serializer = exports.fileManager = exports.utils = exports.BaseOption = exports.info = exports.config = exports.registerPackage = void 0;
|
|
38
|
+
exports.TraceFinder = exports.MultiIterationSeqClustering = exports.SequentialClustering = exports.EvaluationMetric = exports.NormalizedTrace = exports.leakClusterLogger = exports.ProcessManager = exports.modes = exports.memoryBarChart = exports.constant = exports.analysis = exports.browserInfo = exports.serializer = exports.fileManager = exports.utils = exports.BaseOption = exports.info = exports.config = exports.registerPackage = void 0;
|
|
39
39
|
const path_1 = __importDefault(require("path"));
|
|
40
40
|
const PackageInfoLoader_1 = require("./lib/PackageInfoLoader");
|
|
41
|
+
__exportStar(require("./lib/Types"), exports);
|
|
42
|
+
__exportStar(require("./lib/NodeHeap"), exports);
|
|
41
43
|
/** @internal */
|
|
42
44
|
function registerPackage() {
|
|
43
45
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -45,8 +47,6 @@ function registerPackage() {
|
|
|
45
47
|
});
|
|
46
48
|
}
|
|
47
49
|
exports.registerPackage = registerPackage;
|
|
48
|
-
__exportStar(require("./lib/Types"), exports);
|
|
49
|
-
__exportStar(require("./lib/NodeHeap"), exports);
|
|
50
50
|
/** @internal */
|
|
51
51
|
var Config_1 = require("./lib/Config");
|
|
52
52
|
Object.defineProperty(exports, "config", { enumerable: true, get: function () { return __importDefault(Config_1).default; } });
|
|
@@ -81,6 +81,9 @@ Object.defineProperty(exports, "analysis", { enumerable: true, get: function ()
|
|
|
81
81
|
var Constant_1 = require("./lib/Constant");
|
|
82
82
|
Object.defineProperty(exports, "constant", { enumerable: true, get: function () { return __importDefault(Constant_1).default; } });
|
|
83
83
|
/** @internal */
|
|
84
|
+
var MemoryBarChart_1 = require("./lib/charts/MemoryBarChart");
|
|
85
|
+
Object.defineProperty(exports, "memoryBarChart", { enumerable: true, get: function () { return __importDefault(MemoryBarChart_1).default; } });
|
|
86
|
+
/** @internal */
|
|
84
87
|
var RunningModes_1 = require("./modes/RunningModes");
|
|
85
88
|
Object.defineProperty(exports, "modes", { enumerable: true, get: function () { return __importDefault(RunningModes_1).default; } });
|
|
86
89
|
/** @internal */
|
|
@@ -11,6 +11,7 @@ import type { MemLabConfig } from './Config';
|
|
|
11
11
|
import type { AnyValue, FileOption } from './Types';
|
|
12
12
|
/** @internal */
|
|
13
13
|
export declare class FileManager {
|
|
14
|
+
private memlabConfigCache;
|
|
14
15
|
getDefaultWorkDir(): string;
|
|
15
16
|
generateTmpHeapDir(): string;
|
|
16
17
|
private static transientInstanceIdx;
|
|
@@ -63,6 +64,7 @@ export declare class FileManager {
|
|
|
63
64
|
initNewHeapAnalysisLogFile(options?: FileOption): string;
|
|
64
65
|
getAndInitTSCompileIntermediateDir(): string;
|
|
65
66
|
clearDataDirs(options?: FileOption): void;
|
|
67
|
+
removeSnapshotFiles(options?: FileOption): void;
|
|
66
68
|
emptyDirIfExists(dir: string): void;
|
|
67
69
|
emptyTraceLogDataDir(options?: FileOption): void;
|
|
68
70
|
resetBrowserDir(): void;
|
package/dist/lib/FileManager.js
CHANGED
|
@@ -13,7 +13,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.FileManager = void 0;
|
|
16
|
-
const minimist_1 = __importDefault(require("minimist"));
|
|
17
16
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
18
17
|
const os_1 = __importDefault(require("os"));
|
|
19
18
|
const path_1 = __importDefault(require("path"));
|
|
@@ -37,6 +36,9 @@ function joinAndProcessDir(options, ...args) {
|
|
|
37
36
|
}
|
|
38
37
|
/** @internal */
|
|
39
38
|
class FileManager {
|
|
39
|
+
constructor() {
|
|
40
|
+
this.memlabConfigCache = null;
|
|
41
|
+
}
|
|
40
42
|
getDefaultWorkDir() {
|
|
41
43
|
return path_1.default.join(this.getTmpDir(), 'memlab');
|
|
42
44
|
}
|
|
@@ -48,11 +50,12 @@ class FileManager {
|
|
|
48
50
|
return dirPath;
|
|
49
51
|
}
|
|
50
52
|
getWorkDir(options = FileManager.defaultFileOption) {
|
|
53
|
+
var _a;
|
|
51
54
|
// workDir options supercedes all the other options
|
|
52
55
|
if (options.workDir) {
|
|
53
56
|
return path_1.default.resolve(options.workDir);
|
|
54
57
|
}
|
|
55
|
-
// transient options supercedes other
|
|
58
|
+
// transient options supercedes the other CLI options
|
|
56
59
|
if (options.transient) {
|
|
57
60
|
const idx = ++FileManager.transientInstanceIdx;
|
|
58
61
|
const instanceId = `${process.pid}-${Date.now()}-${idx}`;
|
|
@@ -60,8 +63,11 @@ class FileManager {
|
|
|
60
63
|
return path_1.default.resolve(workDir);
|
|
61
64
|
}
|
|
62
65
|
// workDir from the CLI options
|
|
63
|
-
const
|
|
64
|
-
|
|
66
|
+
const workDir = FileManager.defaultFileOption.workDir ||
|
|
67
|
+
(
|
|
68
|
+
// in case there is a transcient working directory generated
|
|
69
|
+
(_a = this.memlabConfigCache) === null || _a === void 0 ? void 0 : _a.workDir) ||
|
|
70
|
+
this.getDefaultWorkDir();
|
|
65
71
|
return path_1.default.resolve(workDir);
|
|
66
72
|
}
|
|
67
73
|
getChromeBinaryZipFile() {
|
|
@@ -249,6 +255,23 @@ class FileManager {
|
|
|
249
255
|
}
|
|
250
256
|
}
|
|
251
257
|
}
|
|
258
|
+
removeSnapshotFiles(options = FileManager.defaultFileOption) {
|
|
259
|
+
const curDataDir = this.getCurDataDir(options);
|
|
260
|
+
if (!fs_extra_1.default.existsSync(curDataDir)) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const dataSuffix = ['.heapsnapshot'];
|
|
264
|
+
const files = fs_extra_1.default.readdirSync(curDataDir);
|
|
265
|
+
for (const file of files) {
|
|
266
|
+
inner: for (const suffix of dataSuffix) {
|
|
267
|
+
if (file.endsWith(suffix)) {
|
|
268
|
+
const filepath = path_1.default.join(curDataDir, file);
|
|
269
|
+
fs_extra_1.default.unlinkSync(filepath);
|
|
270
|
+
break inner;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
252
275
|
emptyDirIfExists(dir) {
|
|
253
276
|
if (this.isDirectory(dir)) {
|
|
254
277
|
fs_extra_1.default.emptyDirSync(dir);
|
|
@@ -309,10 +332,15 @@ class FileManager {
|
|
|
309
332
|
return filePath.includes(`${sep}${internalDir}${sep}`);
|
|
310
333
|
}
|
|
311
334
|
initDirs(config, options = FileManager.defaultFileOption) {
|
|
335
|
+
// cache the last processed memlab config instance
|
|
336
|
+
// the instance should be a singleton
|
|
337
|
+
this.memlabConfigCache = config;
|
|
312
338
|
config.monoRepoDir = Constant_1.default.monoRepoDir;
|
|
313
339
|
// make sure getWorkDir is called first before
|
|
314
340
|
// any other get file or get dir calls
|
|
315
341
|
const workDir = this.getWorkDir(options);
|
|
342
|
+
// remember the current working directory
|
|
343
|
+
// especially if this is a transcient working directory
|
|
316
344
|
config.workDir = joinAndProcessDir(options, workDir);
|
|
317
345
|
options = Object.assign(Object.assign({}, options), { workDir });
|
|
318
346
|
config.dataBaseDir = joinAndProcessDir(options, this.getDataBaseDir(options));
|
|
@@ -7,17 +7,25 @@
|
|
|
7
7
|
* @format
|
|
8
8
|
* @oncall web_perf_infra
|
|
9
9
|
*/
|
|
10
|
-
import type { E2EStepInfo, HeapNodeIdSet, IHeapSnapshot,
|
|
10
|
+
import type { E2EStepInfo, HeapNodeIdSet, IHeapSnapshot, IMemoryAnalystSnapshotDiff, IOveralHeapInfo, LeakTracePathItem, Optional, IOveralLeakInfo, TraceCluster, ISerializedInfo, DiffLeakOptions } from './Types';
|
|
11
11
|
import TraceFinder from '../paths/TraceFinder';
|
|
12
|
+
declare type DiffSnapshotsOptions = {
|
|
13
|
+
loadAllSnapshots?: boolean;
|
|
14
|
+
workDir?: string;
|
|
15
|
+
};
|
|
16
|
+
declare type WorkDirOptions = {
|
|
17
|
+
workDir?: string;
|
|
18
|
+
};
|
|
12
19
|
declare class MemoryAnalyst {
|
|
13
20
|
checkLeak(): Promise<ISerializedInfo[]>;
|
|
21
|
+
diffLeakByWorkDir(options: DiffLeakOptions): Promise<ISerializedInfo[]>;
|
|
22
|
+
diffMemoryLeakTraces(options: DiffLeakOptions): Promise<ISerializedInfo[]>;
|
|
14
23
|
detectMemoryLeaks(): Promise<ISerializedInfo[]>;
|
|
15
|
-
visualizeMemoryUsage(options?: IMemoryAnalystOptions): void;
|
|
16
24
|
focus(options?: {
|
|
17
25
|
file?: string;
|
|
18
26
|
}): Promise<void>;
|
|
19
27
|
shouldLoadCompleteSnapshot(tabsOrder: E2EStepInfo[], tab: E2EStepInfo): boolean;
|
|
20
|
-
diffSnapshots(
|
|
28
|
+
diffSnapshots(options?: DiffSnapshotsOptions): Promise<IMemoryAnalystSnapshotDiff>;
|
|
21
29
|
preparePathFinder(snapshot: IHeapSnapshot): TraceFinder;
|
|
22
30
|
private dumpPageInteractionSummary;
|
|
23
31
|
private dumpLeakSummaryToConsole;
|
|
@@ -29,12 +37,11 @@ declare class MemoryAnalyst {
|
|
|
29
37
|
printHeapInfo(leakInfo: IOveralHeapInfo): void;
|
|
30
38
|
private printHeapAndLeakInfo;
|
|
31
39
|
private logLeakTraceSummary;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}>;
|
|
40
|
+
filterLeakPaths(leakedNodeIds: HeapNodeIdSet, snapshot: IHeapSnapshot, options?: WorkDirOptions): LeakTracePathItem[];
|
|
41
|
+
findLeakTraces(leakedNodeIds: HeapNodeIdSet, snapshot: IHeapSnapshot, options?: WorkDirOptions): Promise<LeakTracePathItem[]>;
|
|
35
42
|
/**
|
|
36
43
|
* Given a set of heap object ids, cluster them based on the similarity
|
|
37
|
-
* of their retainer traces
|
|
44
|
+
* of their retainer traces
|
|
38
45
|
* @param leakedNodeIds
|
|
39
46
|
* @param snapshot
|
|
40
47
|
* @returns
|
|
@@ -43,7 +50,7 @@ declare class MemoryAnalyst {
|
|
|
43
50
|
serializeClusterUpdate(clusters: TraceCluster[], options?: {
|
|
44
51
|
reclusterOnly?: boolean;
|
|
45
52
|
}): Promise<void>;
|
|
46
|
-
dumpPathByNodeId(leakedIdSet: HeapNodeIdSet, snapshot: IHeapSnapshot, nodeIdsInSnapshots: Array<HeapNodeIdSet>, id: number, pathLoaderFile: string, summaryFile: string): void;
|
|
53
|
+
dumpPathByNodeId(leakedIdSet: HeapNodeIdSet, snapshot: IHeapSnapshot, nodeIdsInSnapshots: Array<HeapNodeIdSet>, id: number, pathLoaderFile: string, summaryFile: string, options?: WorkDirOptions): void;
|
|
47
54
|
}
|
|
48
55
|
declare const _default: MemoryAnalyst;
|
|
49
56
|
export default _default;
|
package/dist/lib/HeapAnalyzer.js
CHANGED
|
@@ -22,77 +22,83 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
22
22
|
};
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
24
|
const fs_1 = __importDefault(require("fs"));
|
|
25
|
-
const babar_1 = __importDefault(require("babar"));
|
|
26
|
-
const LeakClusterLogger_1 = __importDefault(require("../logger/LeakClusterLogger"));
|
|
27
|
-
const LeakTraceDetailsLogger_1 = __importDefault(require("../logger/LeakTraceDetailsLogger"));
|
|
28
|
-
const TraceFinder_1 = __importDefault(require("../paths/TraceFinder"));
|
|
29
|
-
const TraceBucket_1 = __importDefault(require("../trace-cluster/TraceBucket"));
|
|
30
25
|
const Config_1 = __importDefault(require("./Config"));
|
|
31
26
|
const Console_1 = __importDefault(require("./Console"));
|
|
32
27
|
const Serializer_1 = __importDefault(require("./Serializer"));
|
|
33
28
|
const Utils_1 = __importDefault(require("./Utils"));
|
|
29
|
+
const FileManager_1 = __importDefault(require("./FileManager"));
|
|
30
|
+
const MemoryBarChart_1 = __importDefault(require("./charts/MemoryBarChart"));
|
|
31
|
+
const LeakClusterLogger_1 = __importDefault(require("../logger/LeakClusterLogger"));
|
|
32
|
+
const LeakTraceDetailsLogger_1 = __importDefault(require("../logger/LeakTraceDetailsLogger"));
|
|
33
|
+
const TraceFinder_1 = __importDefault(require("../paths/TraceFinder"));
|
|
34
|
+
const TraceBucket_1 = __importDefault(require("../trace-cluster/TraceBucket"));
|
|
34
35
|
const LeakObjectFilter_1 = require("./leak-filters/LeakObjectFilter");
|
|
35
36
|
const MLTraceSimilarityStrategy_1 = __importDefault(require("../trace-cluster/strategies/MLTraceSimilarityStrategy"));
|
|
36
37
|
class MemoryAnalyst {
|
|
37
38
|
checkLeak() {
|
|
38
39
|
return __awaiter(this, void 0, void 0, function* () {
|
|
39
|
-
|
|
40
|
+
MemoryBarChart_1.default.plotMemoryBarChart();
|
|
40
41
|
Utils_1.default.checkSnapshots();
|
|
41
42
|
return yield this.detectMemoryLeaks();
|
|
42
43
|
});
|
|
43
44
|
}
|
|
45
|
+
diffLeakByWorkDir(options) {
|
|
46
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
47
|
+
const controlSnapshotDir = FileManager_1.default.getCurDataDir({
|
|
48
|
+
workDir: options.controlWorkDir,
|
|
49
|
+
});
|
|
50
|
+
const treatmentSnapshotDir = FileManager_1.default.getCurDataDir({
|
|
51
|
+
workDir: options.treatmentWorkDir,
|
|
52
|
+
});
|
|
53
|
+
// check control working dir
|
|
54
|
+
Utils_1.default.checkSnapshots({ snapshotDir: controlSnapshotDir });
|
|
55
|
+
// check treatment working dir
|
|
56
|
+
Utils_1.default.checkSnapshots({ snapshotDir: treatmentSnapshotDir });
|
|
57
|
+
// display control and treatment memory
|
|
58
|
+
MemoryBarChart_1.default.plotMemoryBarChart(options);
|
|
59
|
+
return this.diffMemoryLeakTraces(options);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// find all unique pattern of leaks
|
|
63
|
+
diffMemoryLeakTraces(options) {
|
|
64
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
65
|
+
Config_1.default.dumpNodeInfo = false;
|
|
66
|
+
// diff snapshots and get control raw paths
|
|
67
|
+
let snapshotDiff = yield this.diffSnapshots({
|
|
68
|
+
loadAllSnapshots: true,
|
|
69
|
+
workDir: options.controlWorkDir,
|
|
70
|
+
});
|
|
71
|
+
const controlLeakPaths = this.filterLeakPaths(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot, { workDir: options.controlWorkDir });
|
|
72
|
+
const controlSnapshot = snapshotDiff.snapshot;
|
|
73
|
+
// diff snapshots and get treatment raw paths
|
|
74
|
+
snapshotDiff = yield this.diffSnapshots({
|
|
75
|
+
loadAllSnapshots: true,
|
|
76
|
+
workDir: options.treatmentWorkDir,
|
|
77
|
+
});
|
|
78
|
+
const treatmentLeakPaths = this.filterLeakPaths(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot, { workDir: options.controlWorkDir });
|
|
79
|
+
const treatmentSnapshot = snapshotDiff.snapshot;
|
|
80
|
+
Console_1.default.topLevel(`${controlLeakPaths.length} traces from control group`);
|
|
81
|
+
Console_1.default.topLevel(`${treatmentLeakPaths.length} traces from treatment group`);
|
|
82
|
+
const result = TraceBucket_1.default.clusterControlTreatmentPaths(controlLeakPaths, controlSnapshot, treatmentLeakPaths, treatmentSnapshot, Utils_1.default.aggregateDominatorMetrics, {
|
|
83
|
+
strategy: Config_1.default.isMLClustering
|
|
84
|
+
? new MLTraceSimilarityStrategy_1.default()
|
|
85
|
+
: undefined,
|
|
86
|
+
});
|
|
87
|
+
Console_1.default.midLevel(`MemLab found ${result.treatmentOnlyClusters.length} new leak(s) in the treatment group`);
|
|
88
|
+
yield this.serializeClusterUpdate(result.treatmentOnlyClusters);
|
|
89
|
+
// TODO (lgong): log leak traces
|
|
90
|
+
return [];
|
|
91
|
+
});
|
|
92
|
+
}
|
|
44
93
|
// find all unique pattern of leaks
|
|
45
94
|
detectMemoryLeaks() {
|
|
46
95
|
return __awaiter(this, void 0, void 0, function* () {
|
|
47
|
-
const snapshotDiff = yield this.diffSnapshots(true);
|
|
96
|
+
const snapshotDiff = yield this.diffSnapshots({ loadAllSnapshots: true });
|
|
48
97
|
Config_1.default.dumpNodeInfo = false;
|
|
49
|
-
const
|
|
98
|
+
const paths = yield this.findLeakTraces(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot);
|
|
50
99
|
return LeakTraceDetailsLogger_1.default.logTraces(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot, snapshotDiff.listOfLeakedHeapNodeIdSet, paths, Config_1.default.traceJsonOutDir);
|
|
51
100
|
});
|
|
52
101
|
}
|
|
53
|
-
visualizeMemoryUsage(options = {}) {
|
|
54
|
-
if (Config_1.default.useExternalSnapshot || options.snapshotDir) {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
const tabsOrder = Utils_1.default.loadTabsOrder();
|
|
58
|
-
// if memory usage data is incomplete, skip the visualization
|
|
59
|
-
for (const tab of tabsOrder) {
|
|
60
|
-
if (!(tab.JSHeapUsedSize > 0)) {
|
|
61
|
-
if (Config_1.default.verbose) {
|
|
62
|
-
Console_1.default.error('Memory usage data incomplete');
|
|
63
|
-
}
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
const plotData = tabsOrder.map((tab, idx) => [
|
|
68
|
-
idx + 1,
|
|
69
|
-
((tab.JSHeapUsedSize / 100000) | 0) / 10,
|
|
70
|
-
]);
|
|
71
|
-
// the graph component cannot handle an array with a single element
|
|
72
|
-
while (plotData.length < 2) {
|
|
73
|
-
plotData.push([plotData.length + 1, 0]);
|
|
74
|
-
}
|
|
75
|
-
// plot visual settings
|
|
76
|
-
const minY = 1;
|
|
77
|
-
const maxY = plotData.reduce((m, v) => Math.max(m, v[1]), 0) * 1.15;
|
|
78
|
-
const yFractions = 1;
|
|
79
|
-
const yLabelWidth = 1 +
|
|
80
|
-
Math.max(minY.toFixed(yFractions).length, maxY.toFixed(yFractions).length);
|
|
81
|
-
const maxWidth = process.stdout.columns - 10;
|
|
82
|
-
const idealWidth = Math.max(2 * plotData.length + 2 * yLabelWidth, 10);
|
|
83
|
-
const plotWidth = Math.min(idealWidth, maxWidth);
|
|
84
|
-
Console_1.default.topLevel('Memory usage across all steps:');
|
|
85
|
-
Console_1.default.topLevel((0, babar_1.default)(plotData, {
|
|
86
|
-
color: 'green',
|
|
87
|
-
width: plotWidth,
|
|
88
|
-
height: 10,
|
|
89
|
-
xFractions: 0,
|
|
90
|
-
yFractions,
|
|
91
|
-
minY,
|
|
92
|
-
maxY,
|
|
93
|
-
}));
|
|
94
|
-
Console_1.default.topLevel('');
|
|
95
|
-
}
|
|
96
102
|
focus(options = {}) {
|
|
97
103
|
return __awaiter(this, void 0, void 0, function* () {
|
|
98
104
|
Console_1.default.overwrite(`Generating report for node @${Config_1.default.focusFiberNodeId}`);
|
|
@@ -113,7 +119,7 @@ class MemoryAnalyst {
|
|
|
113
119
|
}
|
|
114
120
|
else {
|
|
115
121
|
Utils_1.default.checkSnapshots();
|
|
116
|
-
const snapshotDiff = yield this.diffSnapshots(true);
|
|
122
|
+
const snapshotDiff = yield this.diffSnapshots({ loadAllSnapshots: true });
|
|
117
123
|
nodeIdsInSnapshots = snapshotDiff.listOfLeakedHeapNodeIdSet;
|
|
118
124
|
snapshotLeakedHeapNodeIdSet = snapshotDiff.leakedHeapNodeIdSet;
|
|
119
125
|
snapshot = snapshotDiff.snapshot;
|
|
@@ -130,16 +136,16 @@ class MemoryAnalyst {
|
|
|
130
136
|
}
|
|
131
137
|
return false;
|
|
132
138
|
}
|
|
133
|
-
diffSnapshots(
|
|
139
|
+
diffSnapshots(options = {}) {
|
|
134
140
|
return __awaiter(this, void 0, void 0, function* () {
|
|
135
141
|
const nodeIdsInSnapshots = [];
|
|
136
|
-
const tabsOrder = Utils_1.default.loadTabsOrder();
|
|
142
|
+
const tabsOrder = Utils_1.default.loadTabsOrder(FileManager_1.default.getSnapshotSequenceMetaFile(options));
|
|
137
143
|
// a set keeping track of node ids generated before the target snapshot
|
|
138
144
|
const baselineIds = new Set();
|
|
139
145
|
let collectBaselineIds = true;
|
|
140
146
|
let targetAllocatedHeapNodeIdSet = null;
|
|
141
147
|
let leakedHeapNodeIdSet = null;
|
|
142
|
-
const
|
|
148
|
+
const parseSnapshotOptions = { verbose: true, workDir: options.workDir };
|
|
143
149
|
let snapshot = null;
|
|
144
150
|
for (let i = 0; i < tabsOrder.length; i++) {
|
|
145
151
|
const tab = tabsOrder[i];
|
|
@@ -157,13 +163,13 @@ class MemoryAnalyst {
|
|
|
157
163
|
continue;
|
|
158
164
|
}
|
|
159
165
|
// in quick mode, there is no need to load all snapshots
|
|
160
|
-
if (!
|
|
166
|
+
if (!options.loadAllSnapshots && !tab.type) {
|
|
161
167
|
continue;
|
|
162
168
|
}
|
|
163
|
-
const file = Utils_1.default.getSnapshotFilePath(tab);
|
|
169
|
+
const file = Utils_1.default.getSnapshotFilePath(tab, options);
|
|
164
170
|
if (this.shouldLoadCompleteSnapshot(tabsOrder, tab)) {
|
|
165
171
|
// final snapshot needs to build node index
|
|
166
|
-
const opt = Object.assign({ buildNodeIdIndex: true }, options);
|
|
172
|
+
const opt = Object.assign(Object.assign({ buildNodeIdIndex: true }, parseSnapshotOptions), { workDir: options.workDir });
|
|
167
173
|
snapshot = yield Utils_1.default.getSnapshotFromFile(file, opt);
|
|
168
174
|
// record Ids in the snapshot
|
|
169
175
|
snapshot.nodes.forEach(node => {
|
|
@@ -171,7 +177,7 @@ class MemoryAnalyst {
|
|
|
171
177
|
});
|
|
172
178
|
}
|
|
173
179
|
else {
|
|
174
|
-
idsInSnapshot = yield Utils_1.default.getSnapshotNodeIdsFromFile(file,
|
|
180
|
+
idsInSnapshot = yield Utils_1.default.getSnapshotNodeIdsFromFile(file, parseSnapshotOptions);
|
|
175
181
|
nodeIdsInSnapshots.pop();
|
|
176
182
|
nodeIdsInSnapshots.push(idsInSnapshot);
|
|
177
183
|
}
|
|
@@ -234,10 +240,10 @@ class MemoryAnalyst {
|
|
|
234
240
|
return finder;
|
|
235
241
|
}
|
|
236
242
|
// summarize the page interaction and dump to the leak text summary file
|
|
237
|
-
dumpPageInteractionSummary() {
|
|
238
|
-
const tabsOrder = Utils_1.default.loadTabsOrder();
|
|
243
|
+
dumpPageInteractionSummary(options = {}) {
|
|
244
|
+
const tabsOrder = Utils_1.default.loadTabsOrder(FileManager_1.default.getSnapshotSequenceMetaFile(options));
|
|
239
245
|
const tabsOrderStr = Serializer_1.default.summarizeTabsOrder(tabsOrder);
|
|
240
|
-
fs_1.default.writeFileSync(
|
|
246
|
+
fs_1.default.writeFileSync(FileManager_1.default.getLeakSummaryFile(options), tabsOrderStr, 'UTF-8');
|
|
241
247
|
}
|
|
242
248
|
// summarize the leak and print the info in console
|
|
243
249
|
dumpLeakSummaryToConsole(leakedNodeIds, snapshot) {
|
|
@@ -318,9 +324,9 @@ class MemoryAnalyst {
|
|
|
318
324
|
Console_1.default.topLevel(`· ${name}: ${value}`);
|
|
319
325
|
});
|
|
320
326
|
}
|
|
321
|
-
printHeapAndLeakInfo(leakedNodeIds, snapshot) {
|
|
327
|
+
printHeapAndLeakInfo(leakedNodeIds, snapshot, options = {}) {
|
|
322
328
|
// write page interaction summary to the leaks text file
|
|
323
|
-
this.dumpPageInteractionSummary();
|
|
329
|
+
this.dumpPageInteractionSummary(options);
|
|
324
330
|
// dump leak summry to console
|
|
325
331
|
this.dumpLeakSummaryToConsole(leakedNodeIds, snapshot);
|
|
326
332
|
// get aggregated leak info
|
|
@@ -329,42 +335,46 @@ class MemoryAnalyst {
|
|
|
329
335
|
this.printHeapInfo(heapInfo);
|
|
330
336
|
}
|
|
331
337
|
}
|
|
332
|
-
logLeakTraceSummary(trace, nodeIdInPaths, snapshot) {
|
|
338
|
+
logLeakTraceSummary(trace, nodeIdInPaths, snapshot, options = {}) {
|
|
333
339
|
if (!Config_1.default.isFullRun) {
|
|
334
340
|
return;
|
|
335
341
|
}
|
|
336
342
|
// convert the path to a string
|
|
337
343
|
const pathStr = Serializer_1.default.summarizePath(trace, nodeIdInPaths, snapshot);
|
|
338
|
-
fs_1.default.appendFileSync(
|
|
344
|
+
fs_1.default.appendFileSync(FileManager_1.default.getLeakSummaryFile(options), `\n\n${pathStr}\n\n`, 'UTF-8');
|
|
345
|
+
}
|
|
346
|
+
filterLeakPaths(leakedNodeIds, snapshot, options = {}) {
|
|
347
|
+
const finder = this.preparePathFinder(snapshot);
|
|
348
|
+
this.printHeapAndLeakInfo(leakedNodeIds, snapshot, options);
|
|
349
|
+
// get all leaked objects
|
|
350
|
+
this.filterLeakedObjects(leakedNodeIds, snapshot);
|
|
351
|
+
const nodeIdInPaths = new Set();
|
|
352
|
+
const paths = [];
|
|
353
|
+
let numOfLeakedObjects = 0;
|
|
354
|
+
let i = 0;
|
|
355
|
+
// analysis for each node
|
|
356
|
+
Utils_1.default.applyToNodes(leakedNodeIds, snapshot, node => {
|
|
357
|
+
if (!Config_1.default.isContinuousTest && ++i % 11 === 0) {
|
|
358
|
+
Console_1.default.overwrite(`progress: ${i} / ${leakedNodeIds.size} @${node.id}`);
|
|
359
|
+
}
|
|
360
|
+
// BFS search for path from the leaked node to GC roots
|
|
361
|
+
const p = finder.getPathToGCRoots(snapshot, node);
|
|
362
|
+
if (!p || !Utils_1.default.isInterestingPath(p)) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
++numOfLeakedObjects;
|
|
366
|
+
paths.push(p);
|
|
367
|
+
this.logLeakTraceSummary(p, nodeIdInPaths, snapshot, options);
|
|
368
|
+
}, { reverse: true });
|
|
369
|
+
if (Config_1.default.verbose) {
|
|
370
|
+
Console_1.default.midLevel(`${numOfLeakedObjects} leaked objects`);
|
|
371
|
+
}
|
|
372
|
+
return paths;
|
|
339
373
|
}
|
|
340
374
|
// find unique paths of leaked nodes
|
|
341
|
-
|
|
375
|
+
findLeakTraces(leakedNodeIds, snapshot, options = {}) {
|
|
342
376
|
return __awaiter(this, void 0, void 0, function* () {
|
|
343
|
-
const
|
|
344
|
-
this.printHeapAndLeakInfo(leakedNodeIds, snapshot);
|
|
345
|
-
// get all leaked objects
|
|
346
|
-
this.filterLeakedObjects(leakedNodeIds, snapshot);
|
|
347
|
-
const nodeIdInPaths = new Set();
|
|
348
|
-
const paths = [];
|
|
349
|
-
let numOfLeakedObjects = 0;
|
|
350
|
-
let i = 0;
|
|
351
|
-
// analysis for each node
|
|
352
|
-
Utils_1.default.applyToNodes(leakedNodeIds, snapshot, node => {
|
|
353
|
-
if (!Config_1.default.isContinuousTest && ++i % 11 === 0) {
|
|
354
|
-
Console_1.default.overwrite(`progress: ${i} / ${leakedNodeIds.size} @${node.id}`);
|
|
355
|
-
}
|
|
356
|
-
// BFS search for path from the leaked node to GC roots
|
|
357
|
-
const p = finder.getPathToGCRoots(snapshot, node);
|
|
358
|
-
if (!p || !Utils_1.default.isInterestingPath(p)) {
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
++numOfLeakedObjects;
|
|
362
|
-
paths.push(p);
|
|
363
|
-
this.logLeakTraceSummary(p, nodeIdInPaths, snapshot);
|
|
364
|
-
}, { reverse: true });
|
|
365
|
-
if (Config_1.default.verbose) {
|
|
366
|
-
Console_1.default.midLevel(`${numOfLeakedObjects} leaked objects`);
|
|
367
|
-
}
|
|
377
|
+
const paths = this.filterLeakPaths(leakedNodeIds, snapshot, options);
|
|
368
378
|
// cluster traces from the current run
|
|
369
379
|
const clusters = TraceBucket_1.default.clusterPaths(paths, snapshot, Utils_1.default.aggregateDominatorMetrics, {
|
|
370
380
|
strategy: Config_1.default.isMLClustering
|
|
@@ -378,14 +388,12 @@ class MemoryAnalyst {
|
|
|
378
388
|
const clustersUnclassified = TraceBucket_1.default.generateUnClassifiedClusters(paths, snapshot, Utils_1.default.aggregateDominatorMetrics);
|
|
379
389
|
LeakClusterLogger_1.default.logUnclassifiedClusters(clustersUnclassified);
|
|
380
390
|
}
|
|
381
|
-
return
|
|
382
|
-
paths: clusters.map(c => c.path),
|
|
383
|
-
};
|
|
391
|
+
return clusters.map(c => c.path);
|
|
384
392
|
});
|
|
385
393
|
}
|
|
386
394
|
/**
|
|
387
395
|
* Given a set of heap object ids, cluster them based on the similarity
|
|
388
|
-
* of their retainer traces
|
|
396
|
+
* of their retainer traces
|
|
389
397
|
* @param leakedNodeIds
|
|
390
398
|
* @param snapshot
|
|
391
399
|
* @returns
|
|
@@ -429,7 +437,7 @@ class MemoryAnalyst {
|
|
|
429
437
|
}
|
|
430
438
|
});
|
|
431
439
|
}
|
|
432
|
-
dumpPathByNodeId(leakedIdSet, snapshot, nodeIdsInSnapshots, id, pathLoaderFile, summaryFile) {
|
|
440
|
+
dumpPathByNodeId(leakedIdSet, snapshot, nodeIdsInSnapshots, id, pathLoaderFile, summaryFile, options = {}) {
|
|
433
441
|
Console_1.default.overwrite('start analysis...');
|
|
434
442
|
const finder = this.preparePathFinder(snapshot);
|
|
435
443
|
const nodeIdInPaths = new Set();
|
|
@@ -443,7 +451,7 @@ class MemoryAnalyst {
|
|
|
443
451
|
return;
|
|
444
452
|
}
|
|
445
453
|
LeakTraceDetailsLogger_1.default.logTrace(leakedIdSet, snapshot, nodeIdsInSnapshots, path, pathLoaderFile);
|
|
446
|
-
const tabsOrder = Utils_1.default.loadTabsOrder();
|
|
454
|
+
const tabsOrder = Utils_1.default.loadTabsOrder(FileManager_1.default.getSnapshotSequenceMetaFile(options));
|
|
447
455
|
const interactionSummary = Serializer_1.default.summarizeTabsOrder(tabsOrder);
|
|
448
456
|
let pathSummary = Serializer_1.default.summarizePath(path, nodeIdInPaths, snapshot, { color: true });
|
|
449
457
|
Console_1.default.topLevel(pathSummary);
|
package/dist/lib/Types.d.ts
CHANGED
|
@@ -801,6 +801,15 @@ export declare type TraceClusterMetaInfo = {
|
|
|
801
801
|
meta_data: string;
|
|
802
802
|
};
|
|
803
803
|
/** @internal */
|
|
804
|
+
export declare type ControlTreatmentClusterResult = {
|
|
805
|
+
controlOnlyClusters: TraceCluster[];
|
|
806
|
+
treatmentOnlyClusters: TraceCluster[];
|
|
807
|
+
hybridClusters: Array<{
|
|
808
|
+
control: TraceCluster;
|
|
809
|
+
treatment: TraceCluster;
|
|
810
|
+
}>;
|
|
811
|
+
};
|
|
812
|
+
/** @internal */
|
|
804
813
|
export interface E2EInteraction {
|
|
805
814
|
kind: string;
|
|
806
815
|
timeout?: number;
|
|
@@ -1841,6 +1850,17 @@ export interface IOveralLeakInfo extends Partial<IOveralHeapInfo> {
|
|
|
1841
1850
|
leakedAlternateFiberNodeSize: number;
|
|
1842
1851
|
}
|
|
1843
1852
|
/** @internal */
|
|
1853
|
+
export declare type DiffLeakOptions = {
|
|
1854
|
+
controlWorkDir: string;
|
|
1855
|
+
treatmentWorkDir: string;
|
|
1856
|
+
};
|
|
1857
|
+
/** @internal */
|
|
1858
|
+
export declare type PlotMemoryOptions = {
|
|
1859
|
+
controlWorkDir?: string;
|
|
1860
|
+
treatmentWorkDir?: string;
|
|
1861
|
+
workDir?: string;
|
|
1862
|
+
} & IMemoryAnalystOptions;
|
|
1863
|
+
/** @internal */
|
|
1844
1864
|
export interface IMemoryAnalystOptions {
|
|
1845
1865
|
snapshotDir?: string;
|
|
1846
1866
|
minSnapshots?: number;
|
package/dist/lib/Utils.d.ts
CHANGED
|
@@ -95,7 +95,9 @@ declare function checkSnapshots(options?: {
|
|
|
95
95
|
export declare function resolveSnapshotFilePath(snapshotFile: Nullable<string>): string;
|
|
96
96
|
declare function getSnapshotDirForAnalysis(): string;
|
|
97
97
|
declare function getSingleSnapshotFileForAnalysis(): string;
|
|
98
|
-
declare function getSnapshotFilePath(tab: E2EStepInfo
|
|
98
|
+
declare function getSnapshotFilePath(tab: E2EStepInfo, options?: {
|
|
99
|
+
workDir?: string;
|
|
100
|
+
}): string;
|
|
99
101
|
declare function equalOrMatch(v1: any, v2: any): boolean;
|
|
100
102
|
declare function getSnapshotFilePathWithTabType(type: string | RegExp): Nullable<string>;
|
|
101
103
|
declare function isMeaningfulNode(node: IHeapNode): boolean;
|
package/dist/lib/Utils.js
CHANGED
|
@@ -1346,13 +1346,17 @@ function getSingleSnapshotFileForAnalysis() {
|
|
|
1346
1346
|
}
|
|
1347
1347
|
return resolveSnapshotFilePath(path);
|
|
1348
1348
|
}
|
|
1349
|
-
function getSnapshotFilePath(tab) {
|
|
1349
|
+
function getSnapshotFilePath(tab, options = {}) {
|
|
1350
|
+
const fileName = `s${tab.idx}.heapsnapshot`;
|
|
1351
|
+
if (options.workDir) {
|
|
1352
|
+
return path_1.default.join(FileManager_1.default.getCurDataDir(options), fileName);
|
|
1353
|
+
}
|
|
1350
1354
|
if (!Config_1.default.useExternalSnapshot) {
|
|
1351
|
-
return path_1.default.join(Config_1.default.curDataDir,
|
|
1355
|
+
return path_1.default.join(Config_1.default.curDataDir, fileName);
|
|
1352
1356
|
}
|
|
1353
1357
|
// if we are loading snapshot from external snapshot dir
|
|
1354
1358
|
if (Config_1.default.externalSnapshotDir) {
|
|
1355
|
-
return path_1.default.join(Config_1.default.externalSnapshotDir,
|
|
1359
|
+
return path_1.default.join(Config_1.default.externalSnapshotDir, fileName);
|
|
1356
1360
|
}
|
|
1357
1361
|
return Config_1.default.externalSnapshotFilePaths[tab.idx - 1];
|
|
1358
1362
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
import type { PlotMemoryOptions } from '../Types';
|
|
11
|
+
declare class MemoryBarChart {
|
|
12
|
+
plotMemoryBarChart(options?: PlotMemoryOptions): void;
|
|
13
|
+
private loadPlotDataFromTabsOrder;
|
|
14
|
+
private loadPlotDataFromWorkDir;
|
|
15
|
+
private loadPlotData;
|
|
16
|
+
private mergePlotData;
|
|
17
|
+
}
|
|
18
|
+
declare const _default: MemoryBarChart;
|
|
19
|
+
export default _default;
|
|
20
|
+
//# sourceMappingURL=MemoryBarChart.d.ts.map
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
'use strict';
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const babar_1 = __importDefault(require("babar"));
|
|
16
|
+
const Config_1 = __importDefault(require("../Config"));
|
|
17
|
+
const Console_1 = __importDefault(require("../Console"));
|
|
18
|
+
const Utils_1 = __importDefault(require("../Utils"));
|
|
19
|
+
const FileManager_1 = __importDefault(require("../FileManager"));
|
|
20
|
+
class MemoryBarChart {
|
|
21
|
+
plotMemoryBarChart(options = {}) {
|
|
22
|
+
if (Config_1.default.useExternalSnapshot || options.snapshotDir) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
let plotData;
|
|
26
|
+
try {
|
|
27
|
+
plotData = this.loadPlotData(options);
|
|
28
|
+
}
|
|
29
|
+
catch (ex) {
|
|
30
|
+
Console_1.default.warning(`plot data not load correctly: ${Utils_1.default.getError(ex).message}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// normalize plot data
|
|
34
|
+
const minY = 1;
|
|
35
|
+
const maxY = plotData.reduce((m, v) => Math.max(m, v[1]), 0) * 1.15;
|
|
36
|
+
const yFractions = 1;
|
|
37
|
+
const yLabelWidth = 1 +
|
|
38
|
+
Math.max(minY.toFixed(yFractions).length, maxY.toFixed(yFractions).length);
|
|
39
|
+
const maxWidth = process.stdout.columns - 10;
|
|
40
|
+
const idealWidth = Math.max(2 * plotData.length + 2 * yLabelWidth, 10);
|
|
41
|
+
const plotWidth = Math.min(idealWidth, maxWidth);
|
|
42
|
+
Console_1.default.topLevel('Memory usage across all steps:');
|
|
43
|
+
Console_1.default.topLevel((0, babar_1.default)(plotData, {
|
|
44
|
+
color: 'green',
|
|
45
|
+
width: plotWidth,
|
|
46
|
+
height: 10,
|
|
47
|
+
xFractions: 0,
|
|
48
|
+
yFractions,
|
|
49
|
+
minY,
|
|
50
|
+
maxY,
|
|
51
|
+
}));
|
|
52
|
+
Console_1.default.topLevel('');
|
|
53
|
+
}
|
|
54
|
+
loadPlotDataFromTabsOrder(tabsOrder) {
|
|
55
|
+
for (const tab of tabsOrder) {
|
|
56
|
+
if (!(tab.JSHeapUsedSize > 0)) {
|
|
57
|
+
if (Config_1.default.verbose) {
|
|
58
|
+
Console_1.default.error('Memory usage data incomplete');
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const plotData = tabsOrder.map((tab, idx) => [
|
|
64
|
+
idx + 1,
|
|
65
|
+
((tab.JSHeapUsedSize / 100000) | 0) / 10,
|
|
66
|
+
]);
|
|
67
|
+
// the graph component cannot handle an array with a single element
|
|
68
|
+
while (plotData.length < 2) {
|
|
69
|
+
plotData.push([plotData.length + 1, 0]);
|
|
70
|
+
}
|
|
71
|
+
return plotData;
|
|
72
|
+
}
|
|
73
|
+
loadPlotDataFromWorkDir(options = {}) {
|
|
74
|
+
const tabsOrder = Utils_1.default.loadTabsOrder(FileManager_1.default.getSnapshotSequenceMetaFile(options));
|
|
75
|
+
return this.loadPlotDataFromTabsOrder(tabsOrder);
|
|
76
|
+
}
|
|
77
|
+
loadPlotData(options = {}) {
|
|
78
|
+
// plot data for a single run
|
|
79
|
+
if (!options.controlWorkDir && !options.treatmentWorkDir) {
|
|
80
|
+
return this.loadPlotDataFromWorkDir(options);
|
|
81
|
+
}
|
|
82
|
+
// plot data for control and test run
|
|
83
|
+
const controlPlotData = this.loadPlotDataFromWorkDir({
|
|
84
|
+
workDir: options.controlWorkDir,
|
|
85
|
+
});
|
|
86
|
+
const testPlotData = this.loadPlotDataFromWorkDir({
|
|
87
|
+
workDir: options.treatmentWorkDir,
|
|
88
|
+
});
|
|
89
|
+
// merge plot data
|
|
90
|
+
return this.mergePlotData([controlPlotData, testPlotData]);
|
|
91
|
+
}
|
|
92
|
+
mergePlotData(plotDataArray) {
|
|
93
|
+
const plotData = [];
|
|
94
|
+
let xIndex = 1; // starts from 1
|
|
95
|
+
for (let i = 0; i < plotDataArray.length; ++i) {
|
|
96
|
+
const data = plotDataArray[i];
|
|
97
|
+
for (const [, yValue] of data) {
|
|
98
|
+
plotData.push([xIndex++, yValue]);
|
|
99
|
+
}
|
|
100
|
+
// push blank separators
|
|
101
|
+
if (i < plotDataArray.length - 1) {
|
|
102
|
+
for (let k = 0; k < 3; ++k) {
|
|
103
|
+
plotData.push([xIndex++, 0]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return plotData;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
exports.default = new MemoryBarChart();
|
|
@@ -7,13 +7,16 @@
|
|
|
7
7
|
* @format
|
|
8
8
|
* @oncall web_perf_infra
|
|
9
9
|
*/
|
|
10
|
-
import type { IHeapNode, IHeapSnapshot, LeakTrace, LeakTracePathItem, Optional, TraceCluster, TraceClusterDiff, IClusterStrategy } from '../lib/Types';
|
|
10
|
+
import type { IHeapNode, IHeapSnapshot, LeakTrace, LeakTracePathItem, Optional, TraceCluster, TraceClusterDiff, IClusterStrategy, ControlTreatmentClusterResult } from '../lib/Types';
|
|
11
11
|
import type { NormalizedTraceElement } from './TraceElement';
|
|
12
12
|
declare type AggregateNodeCb = (ids: Set<number>, snapshot: IHeapSnapshot, checkCb: (node: IHeapNode) => boolean, calculateCb: (node: IHeapNode) => number) => number;
|
|
13
13
|
export default class NormalizedTrace {
|
|
14
14
|
private trace;
|
|
15
15
|
private traceSummary;
|
|
16
16
|
constructor(p?: LeakTracePathItem | null, snapshot?: IHeapSnapshot | null);
|
|
17
|
+
static getPathLastNode(p: LeakTracePathItem, options?: {
|
|
18
|
+
untilFirstDetachedDOMElem?: boolean;
|
|
19
|
+
}): Optional<IHeapNode>;
|
|
17
20
|
static pathToTrace(p: LeakTracePathItem, options?: {
|
|
18
21
|
untilFirstDetachedDOMElem?: boolean;
|
|
19
22
|
}): NormalizedTraceElement[];
|
|
@@ -30,6 +33,12 @@ export default class NormalizedTrace {
|
|
|
30
33
|
static clusterPaths(paths: LeakTracePathItem[], snapshot: IHeapSnapshot, aggregateDominatorMetrics: AggregateNodeCb, option?: {
|
|
31
34
|
strategy?: IClusterStrategy;
|
|
32
35
|
}): TraceCluster[];
|
|
36
|
+
private static buildTraceToPathMap;
|
|
37
|
+
private static pushLeakPathToCluster;
|
|
38
|
+
private static initEmptyCluster;
|
|
39
|
+
static clusterControlTreatmentPaths(controlPaths: LeakTracePathItem[], controlSnapshot: IHeapSnapshot, treatmentPaths: LeakTracePathItem[], treatmentSnapshot: IHeapSnapshot, aggregateDominatorMetrics: AggregateNodeCb, option?: {
|
|
40
|
+
strategy?: IClusterStrategy;
|
|
41
|
+
}): ControlTreatmentClusterResult;
|
|
33
42
|
static generateUnClassifiedClusters(paths: LeakTracePathItem[], snapshot: IHeapSnapshot, aggregateDominatorMetrics: AggregateNodeCb): TraceCluster[];
|
|
34
43
|
static loadCluster(): NormalizedTrace[];
|
|
35
44
|
static saveCluster(clusters: NormalizedTrace[]): void;
|
|
@@ -38,6 +38,27 @@ class NormalizedTrace {
|
|
|
38
38
|
: '';
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
static getPathLastNode(p, options = {}) {
|
|
42
|
+
const skipRest = !!options.untilFirstDetachedDOMElem;
|
|
43
|
+
const shouldSkip = (node) => {
|
|
44
|
+
// only consider the trace from GC root to the first detached element
|
|
45
|
+
// NOTE: do not use utils.isDetachedDOMNode, which relies on
|
|
46
|
+
// the fact that p.node is a HeapNode
|
|
47
|
+
return (skipRest &&
|
|
48
|
+
node.name.startsWith('Detached ') &&
|
|
49
|
+
node.name !== 'Detached InternalNode');
|
|
50
|
+
};
|
|
51
|
+
let curItem = p;
|
|
52
|
+
while (curItem.next) {
|
|
53
|
+
if (curItem.node) {
|
|
54
|
+
if (shouldSkip(curItem.node)) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
curItem = curItem.next;
|
|
59
|
+
}
|
|
60
|
+
return curItem === null || curItem === void 0 ? void 0 : curItem.node;
|
|
61
|
+
}
|
|
41
62
|
// convert path to leak trace
|
|
42
63
|
static pathToTrace(p, options = {}) {
|
|
43
64
|
const skipRest = !!options.untilFirstDetachedDOMElem;
|
|
@@ -114,6 +135,13 @@ class NormalizedTrace {
|
|
|
114
135
|
if (Math.random() < sampleRatio) {
|
|
115
136
|
ret.push(p);
|
|
116
137
|
}
|
|
138
|
+
else {
|
|
139
|
+
// force sample objects with non-trvial self size
|
|
140
|
+
const lastNode = NormalizedTrace.getPathLastNode(p);
|
|
141
|
+
if (lastNode && lastNode.self_size >= 100000) {
|
|
142
|
+
ret.push(p);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
117
145
|
}
|
|
118
146
|
return ret;
|
|
119
147
|
}
|
|
@@ -233,6 +261,109 @@ class NormalizedTrace {
|
|
|
233
261
|
clusters.sort((c1, c2) => { var _a, _b; return ((_a = c2.retainedSize) !== null && _a !== void 0 ? _a : 0) - ((_b = c1.retainedSize) !== null && _b !== void 0 ? _b : 0); });
|
|
234
262
|
return clusters;
|
|
235
263
|
}
|
|
264
|
+
static buildTraceToPathMap(paths) {
|
|
265
|
+
const traceToPathMap = new Map();
|
|
266
|
+
for (const p of paths) {
|
|
267
|
+
const trace = NormalizedTrace.pathToTrace(p, {
|
|
268
|
+
untilFirstDetachedDOMElem: true,
|
|
269
|
+
});
|
|
270
|
+
traceToPathMap.set(trace, p);
|
|
271
|
+
}
|
|
272
|
+
return traceToPathMap;
|
|
273
|
+
}
|
|
274
|
+
static pushLeakPathToCluster(traceToPathMap, trace, cluster) {
|
|
275
|
+
// if this is a control path, update control cluster
|
|
276
|
+
const curPath = traceToPathMap.get(trace);
|
|
277
|
+
if (cluster.count === 0) {
|
|
278
|
+
cluster.path = curPath;
|
|
279
|
+
// add representative object id if there is one
|
|
280
|
+
const lastNode = trace[trace.length - 1];
|
|
281
|
+
if ('id' in lastNode) {
|
|
282
|
+
cluster.id = lastNode.id;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
cluster.count = cluster.count + 1;
|
|
286
|
+
NormalizedTrace.addLeakedNodeToCluster(cluster, curPath);
|
|
287
|
+
}
|
|
288
|
+
static initEmptyCluster(snapshot) {
|
|
289
|
+
return {
|
|
290
|
+
path: {},
|
|
291
|
+
count: 0,
|
|
292
|
+
snapshot,
|
|
293
|
+
retainedSize: 0,
|
|
294
|
+
leakedNodeIds: new Set(),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
static clusterControlTreatmentPaths(controlPaths, controlSnapshot, treatmentPaths, treatmentSnapshot, aggregateDominatorMetrics, option = {}) {
|
|
298
|
+
const result = {
|
|
299
|
+
controlOnlyClusters: [],
|
|
300
|
+
treatmentOnlyClusters: [],
|
|
301
|
+
hybridClusters: [],
|
|
302
|
+
};
|
|
303
|
+
Console_1.default.overwrite('Clustering leak traces');
|
|
304
|
+
if (controlPaths.length === 0 && treatmentPaths.length === 0) {
|
|
305
|
+
Console_1.default.midLevel('No leaks found');
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
// sample paths if there are too many
|
|
309
|
+
controlPaths = this.samplePaths(controlPaths);
|
|
310
|
+
treatmentPaths = this.samplePaths(treatmentPaths);
|
|
311
|
+
// build control trace to control path map
|
|
312
|
+
const controlTraceToPathMap = NormalizedTrace.buildTraceToPathMap(controlPaths);
|
|
313
|
+
const controlTraces = Array.from(controlTraceToPathMap.keys());
|
|
314
|
+
// build treatment trace to treatment path map
|
|
315
|
+
const treatmentTraceToPathMap = NormalizedTrace.buildTraceToPathMap(treatmentPaths);
|
|
316
|
+
const treatmentTraces = Array.from(treatmentTraceToPathMap.keys());
|
|
317
|
+
// cluster traces from both the control group and the treatment group
|
|
318
|
+
const { allClusters } = NormalizedTrace.diffTraces([...controlTraces, ...treatmentTraces], [], option);
|
|
319
|
+
// construct TraceCluster from clustering result
|
|
320
|
+
allClusters.forEach((traces) => {
|
|
321
|
+
var _a, _b;
|
|
322
|
+
const controlCluster = NormalizedTrace.initEmptyCluster(controlSnapshot);
|
|
323
|
+
const treatmentCluster = NormalizedTrace.initEmptyCluster(treatmentSnapshot);
|
|
324
|
+
for (const trace of traces) {
|
|
325
|
+
const normalizedTrace = trace;
|
|
326
|
+
if (controlTraceToPathMap.has(normalizedTrace)) {
|
|
327
|
+
NormalizedTrace.pushLeakPathToCluster(controlTraceToPathMap, normalizedTrace, controlCluster);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
NormalizedTrace.pushLeakPathToCluster(treatmentTraceToPathMap, normalizedTrace, treatmentCluster);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const controlClusterSize = (_a = controlCluster.count) !== null && _a !== void 0 ? _a : 0;
|
|
334
|
+
const treatmentClusterSize = (_b = treatmentCluster.count) !== null && _b !== void 0 ? _b : 0;
|
|
335
|
+
// calculate aggregated cluster size for control cluster
|
|
336
|
+
if (controlClusterSize > 0) {
|
|
337
|
+
this.calculateClusterRetainedSize(controlCluster, controlSnapshot, aggregateDominatorMetrics);
|
|
338
|
+
}
|
|
339
|
+
// calculate aggregated cluster size for treatment cluster
|
|
340
|
+
if (treatmentClusterSize > 0) {
|
|
341
|
+
this.calculateClusterRetainedSize(treatmentCluster, treatmentSnapshot, aggregateDominatorMetrics);
|
|
342
|
+
}
|
|
343
|
+
if (controlClusterSize === 0) {
|
|
344
|
+
result.treatmentOnlyClusters.push(treatmentCluster);
|
|
345
|
+
}
|
|
346
|
+
else if (treatmentClusterSize === 0) {
|
|
347
|
+
result.controlOnlyClusters.push(controlCluster);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
result.hybridClusters.push({
|
|
351
|
+
control: controlCluster,
|
|
352
|
+
treatment: treatmentCluster,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
result.treatmentOnlyClusters.sort((c1, c2) => { var _a, _b; return ((_a = c2.retainedSize) !== null && _a !== void 0 ? _a : 0) - ((_b = c1.retainedSize) !== null && _b !== void 0 ? _b : 0); });
|
|
357
|
+
result.controlOnlyClusters.sort((c1, c2) => { var _a, _b; return ((_a = c2.retainedSize) !== null && _a !== void 0 ? _a : 0) - ((_b = c1.retainedSize) !== null && _b !== void 0 ? _b : 0); });
|
|
358
|
+
result.hybridClusters.sort((g1, g2) => {
|
|
359
|
+
var _a, _b, _c, _d;
|
|
360
|
+
return ((_a = g2.control.retainedSize) !== null && _a !== void 0 ? _a : 0) +
|
|
361
|
+
((_b = g2.treatment.retainedSize) !== null && _b !== void 0 ? _b : 0) -
|
|
362
|
+
((_c = g1.control.retainedSize) !== null && _c !== void 0 ? _c : 0) -
|
|
363
|
+
((_d = g1.treatment.retainedSize) !== null && _d !== void 0 ? _d : 0);
|
|
364
|
+
});
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
236
367
|
static generateUnClassifiedClusters(paths, snapshot, aggregateDominatorMetrics) {
|
|
237
368
|
return this.clusterPaths(paths, snapshot, aggregateDominatorMetrics, {
|
|
238
369
|
strategy: new TraceAsClusterStrategy_1.default(),
|