@memlab/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/__tests__/parser/HeapParser.test.d.ts +11 -0
- package/dist/__tests__/parser/HeapParser.test.d.ts.map +1 -0
- package/dist/__tests__/parser/HeapParser.test.js +54 -0
- package/dist/__tests__/parser/NodeHeap.test.d.ts +11 -0
- package/dist/__tests__/parser/NodeHeap.test.d.ts.map +1 -0
- package/dist/__tests__/parser/NodeHeap.test.js +96 -0
- package/dist/__tests__/parser/StringNode.test.d.ts +11 -0
- package/dist/__tests__/parser/StringNode.test.d.ts.map +1 -0
- package/dist/__tests__/parser/StringNode.test.js +61 -0
- package/dist/__tests__/parser/traverse/HeapNodeTraverse.test.d.ts +16 -0
- package/dist/__tests__/parser/traverse/HeapNodeTraverse.test.d.ts.map +1 -0
- package/dist/__tests__/parser/traverse/HeapNodeTraverse.test.js +140 -0
- package/dist/__tests__/utils/utils.test.d.ts +11 -0
- package/dist/__tests__/utils/utils.test.d.ts.map +1 -0
- package/dist/__tests__/utils/utils.test.js +81 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/lib/BaseOption.d.ts +31 -0
- package/dist/lib/BaseOption.d.ts.map +1 -0
- package/dist/lib/BaseOption.js +109 -0
- package/dist/lib/BrowserInfo.d.ts +33 -0
- package/dist/lib/BrowserInfo.d.ts.map +1 -0
- package/dist/lib/BrowserInfo.js +117 -0
- package/dist/lib/Config.d.ts +203 -0
- package/dist/lib/Config.d.ts.map +1 -0
- package/dist/lib/Config.js +427 -0
- package/dist/lib/Console.d.ts +67 -0
- package/dist/lib/Console.d.ts.map +1 -0
- package/dist/lib/Console.js +344 -0
- package/dist/lib/Constant.d.ts +38 -0
- package/dist/lib/Constant.d.ts.map +1 -0
- package/dist/lib/Constant.js +58 -0
- package/dist/lib/FileManager.d.ts +69 -0
- package/dist/lib/FileManager.d.ts.map +1 -0
- package/dist/lib/FileManager.js +309 -0
- package/dist/lib/HeapAnalyzer.d.ts +51 -0
- package/dist/lib/HeapAnalyzer.d.ts.map +1 -0
- package/dist/lib/HeapAnalyzer.js +719 -0
- package/dist/lib/HeapParser.d.ts +19 -0
- package/dist/lib/HeapParser.d.ts.map +1 -0
- package/dist/lib/HeapParser.js +128 -0
- package/dist/lib/InternalValueSetter.d.ts +14 -0
- package/dist/lib/InternalValueSetter.d.ts.map +1 -0
- package/dist/lib/InternalValueSetter.js +43 -0
- package/dist/lib/NodeHeap.d.ts +16 -0
- package/dist/lib/NodeHeap.d.ts.map +1 -0
- package/dist/lib/NodeHeap.js +62 -0
- package/dist/lib/ProcessManager.d.ts +25 -0
- package/dist/lib/ProcessManager.d.ts.map +1 -0
- package/dist/lib/ProcessManager.js +67 -0
- package/dist/lib/Serializer.d.ts +49 -0
- package/dist/lib/Serializer.d.ts.map +1 -0
- package/dist/lib/Serializer.js +702 -0
- package/dist/lib/StringLoader.d.ts +26 -0
- package/dist/lib/StringLoader.d.ts.map +1 -0
- package/dist/lib/StringLoader.js +290 -0
- package/dist/lib/Types.d.ts +432 -0
- package/dist/lib/Types.d.ts.map +1 -0
- package/dist/lib/Types.js +11 -0
- package/dist/lib/Utils.d.ts +223 -0
- package/dist/lib/Utils.d.ts.map +1 -0
- package/dist/lib/Utils.js +1736 -0
- package/dist/lib/heap-data/HeapEdge.d.ts +27 -0
- package/dist/lib/heap-data/HeapEdge.d.ts.map +1 -0
- package/dist/lib/heap-data/HeapEdge.js +75 -0
- package/dist/lib/heap-data/HeapLocation.d.ts +22 -0
- package/dist/lib/heap-data/HeapLocation.d.ts.map +1 -0
- package/dist/lib/heap-data/HeapLocation.js +40 -0
- package/dist/lib/heap-data/HeapNode.d.ts +55 -0
- package/dist/lib/heap-data/HeapNode.d.ts.map +1 -0
- package/dist/lib/heap-data/HeapNode.js +344 -0
- package/dist/lib/heap-data/HeapSnapshot.d.ts +85 -0
- package/dist/lib/heap-data/HeapSnapshot.d.ts.map +1 -0
- package/dist/lib/heap-data/HeapSnapshot.js +462 -0
- package/dist/lib/heap-data/HeapStringNode.d.ts +18 -0
- package/dist/lib/heap-data/HeapStringNode.d.ts.map +1 -0
- package/dist/lib/heap-data/HeapStringNode.js +43 -0
- package/dist/lib/heap-data/HeapUtils.d.ts +17 -0
- package/dist/lib/heap-data/HeapUtils.d.ts.map +1 -0
- package/dist/lib/heap-data/HeapUtils.js +25 -0
- package/dist/logger/LeakClusterLogger.d.ts +40 -0
- package/dist/logger/LeakClusterLogger.d.ts.map +1 -0
- package/dist/logger/LeakClusterLogger.js +228 -0
- package/dist/logger/LeakTraceDetailsLogger.d.ts +19 -0
- package/dist/logger/LeakTraceDetailsLogger.d.ts.map +1 -0
- package/dist/logger/LeakTraceDetailsLogger.js +50 -0
- package/dist/modes/BaseMode.d.ts +30 -0
- package/dist/modes/BaseMode.d.ts.map +1 -0
- package/dist/modes/BaseMode.js +95 -0
- package/dist/modes/InteractionTestMode.d.ts +23 -0
- package/dist/modes/InteractionTestMode.d.ts.map +1 -0
- package/dist/modes/InteractionTestMode.js +46 -0
- package/dist/modes/MeasureMode.d.ts +23 -0
- package/dist/modes/MeasureMode.d.ts.map +1 -0
- package/dist/modes/MeasureMode.js +58 -0
- package/dist/modes/RunningModes.d.ts +15 -0
- package/dist/modes/RunningModes.d.ts.map +1 -0
- package/dist/modes/RunningModes.js +40 -0
- package/dist/paths/TraceFinder.d.ts +31 -0
- package/dist/paths/TraceFinder.d.ts.map +1 -0
- package/dist/paths/TraceFinder.js +537 -0
- package/dist/trace-cluster/ClusterUtils.d.ts +14 -0
- package/dist/trace-cluster/ClusterUtils.d.ts.map +1 -0
- package/dist/trace-cluster/ClusterUtils.js +17 -0
- package/dist/trace-cluster/ClusterUtilsHelper.d.ts +38 -0
- package/dist/trace-cluster/ClusterUtilsHelper.d.ts.map +1 -0
- package/dist/trace-cluster/ClusterUtilsHelper.js +373 -0
- package/dist/trace-cluster/ClusteringHeuristics.d.ts +22 -0
- package/dist/trace-cluster/ClusteringHeuristics.d.ts.map +1 -0
- package/dist/trace-cluster/ClusteringHeuristics.js +23 -0
- package/dist/trace-cluster/EvalutationMetric.d.ts +22 -0
- package/dist/trace-cluster/EvalutationMetric.d.ts.map +1 -0
- package/dist/trace-cluster/EvalutationMetric.js +158 -0
- package/dist/trace-cluster/TraceBucket.d.ts +36 -0
- package/dist/trace-cluster/TraceBucket.d.ts.map +1 -0
- package/dist/trace-cluster/TraceBucket.js +238 -0
- package/dist/trace-cluster/TraceElement.d.ts +71 -0
- package/dist/trace-cluster/TraceElement.d.ts.map +1 -0
- package/dist/trace-cluster/TraceElement.js +182 -0
- package/dist/trace-cluster/strategies/TraceAsClusterStrategy.d.ts +15 -0
- package/dist/trace-cluster/strategies/TraceAsClusterStrategy.d.ts.map +1 -0
- package/dist/trace-cluster/strategies/TraceAsClusterStrategy.js +37 -0
- package/dist/trace-cluster/strategies/TraceSimilarityStrategy.d.ts +15 -0
- package/dist/trace-cluster/strategies/TraceSimilarityStrategy.d.ts.map +1 -0
- package/dist/trace-cluster/strategies/TraceSimilarityStrategy.js +60 -0
- package/package.json +60 -0
- package/static/run-meta.json +10 -0
- package/static/visit-order.json +27 -0
|
@@ -0,0 +1,719 @@
|
|
|
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
|
+
* @emails oncall+ws_labs
|
|
8
|
+
* @format
|
|
9
|
+
*/
|
|
10
|
+
'use strict';
|
|
11
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
12
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
13
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
14
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
15
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
16
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
17
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
const fs_1 = __importDefault(require("fs"));
|
|
25
|
+
const babar_1 = __importDefault(require("babar"));
|
|
26
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
27
|
+
const LeakClusterLogger_1 = __importDefault(require("../logger/LeakClusterLogger"));
|
|
28
|
+
const LeakTraceDetailsLogger_1 = __importDefault(require("../logger/LeakTraceDetailsLogger"));
|
|
29
|
+
const TraceFinder_1 = __importDefault(require("../paths/TraceFinder"));
|
|
30
|
+
const TraceBucket_1 = __importDefault(require("../trace-cluster/TraceBucket"));
|
|
31
|
+
const Config_1 = __importDefault(require("./Config"));
|
|
32
|
+
const Console_1 = __importDefault(require("./Console"));
|
|
33
|
+
const Serializer_1 = __importDefault(require("./Serializer"));
|
|
34
|
+
const Utils_1 = __importDefault(require("./Utils"));
|
|
35
|
+
class MemoryAnalyst {
|
|
36
|
+
checkLeak() {
|
|
37
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
38
|
+
this.visualizeMemoryUsage();
|
|
39
|
+
Utils_1.default.checkSnapshots();
|
|
40
|
+
yield this.detectMemoryLeaks();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
checkUnbound(options = {}) {
|
|
44
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
45
|
+
this.visualizeMemoryUsage(options);
|
|
46
|
+
Utils_1.default.checkSnapshots(options);
|
|
47
|
+
yield this.detectUnboundGrowth(options);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
breakDownMemoryByShapes(options = {}) {
|
|
51
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
const opt = { buildNodeIdIndex: true, verbose: true };
|
|
53
|
+
const file = options.file ||
|
|
54
|
+
Utils_1.default.getSnapshotFilePathWithTabType(/.*/) ||
|
|
55
|
+
'<EMPTY_FILE_PATH>';
|
|
56
|
+
const snapshot = yield Utils_1.default.getSnapshotFromFile(file, opt);
|
|
57
|
+
this.preparePathFinder(snapshot);
|
|
58
|
+
const heapInfo = this.getOverallHeapInfo(snapshot, { force: true });
|
|
59
|
+
if (heapInfo) {
|
|
60
|
+
this.printHeapInfo(heapInfo);
|
|
61
|
+
}
|
|
62
|
+
this.breakDownSnapshotByShapes(snapshot);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// find any objects that keeps growing
|
|
66
|
+
detectUnboundGrowth(options = {}) {
|
|
67
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
68
|
+
const nodeInfo = Object.create(null);
|
|
69
|
+
let hasCheckedFirstSnapshot = false;
|
|
70
|
+
let snapshot = null;
|
|
71
|
+
const isValidNode = (node) => node.type === 'object' ||
|
|
72
|
+
node.type === 'closure' ||
|
|
73
|
+
node.type === 'regexp';
|
|
74
|
+
const initNodeInfo = (node) => {
|
|
75
|
+
if (!isValidNode(node)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const n = node.retainedSize;
|
|
79
|
+
nodeInfo[node.id] = {
|
|
80
|
+
type: node.type,
|
|
81
|
+
name: node.name,
|
|
82
|
+
min: n,
|
|
83
|
+
max: n,
|
|
84
|
+
history: [n],
|
|
85
|
+
node,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
const updateNodeInfo = (node) => {
|
|
89
|
+
const item = nodeInfo[node.id];
|
|
90
|
+
if (!item) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (node.name !== item.name || node.type !== item.type) {
|
|
94
|
+
nodeInfo[node.id] = null;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const n = node.retainedSize;
|
|
98
|
+
// only monotonic increase?
|
|
99
|
+
if (Config_1.default.monotonicUnboundGrowthOnly && n < item.max) {
|
|
100
|
+
nodeInfo[node.id] = null;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
item.history.push(n);
|
|
104
|
+
item.max = Math.max(item.max, n);
|
|
105
|
+
item.min = Math.min(item.min, n);
|
|
106
|
+
};
|
|
107
|
+
// summarize the heap objects info in current heap snapshot
|
|
108
|
+
// this is mainly used for better understanding of the % of
|
|
109
|
+
// objects released and allocated over time
|
|
110
|
+
const maybeSummarizeNodeInfo = () => {
|
|
111
|
+
if (!Config_1.default.verbose) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
let n = 0;
|
|
115
|
+
for (const k in nodeInfo) {
|
|
116
|
+
if (nodeInfo[k]) {
|
|
117
|
+
++n;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
Console_1.default.lowLevel(`Objects tracked: ${n}`);
|
|
121
|
+
};
|
|
122
|
+
Console_1.default.overwrite('Checking unbounded objects...');
|
|
123
|
+
const snapshotFiles = options.snapshotDir
|
|
124
|
+
? // load snapshots from a directory
|
|
125
|
+
Utils_1.default.getSnapshotFilesInDir(options.snapshotDir)
|
|
126
|
+
: // load snapshots based on the visit sequence meta data
|
|
127
|
+
Utils_1.default.getSnapshotFilesFromTabsOrder();
|
|
128
|
+
for (const file of snapshotFiles) {
|
|
129
|
+
// force GC before loading each snapshot
|
|
130
|
+
if (global.gc) {
|
|
131
|
+
global.gc();
|
|
132
|
+
}
|
|
133
|
+
// load and preprocess heap snapshot
|
|
134
|
+
const opt = { buildNodeIdIndex: true, verbose: true };
|
|
135
|
+
snapshot = yield Utils_1.default.getSnapshotFromFile(file, opt);
|
|
136
|
+
this.calculateRetainedSizes(snapshot);
|
|
137
|
+
// keep track of heap objects
|
|
138
|
+
if (!hasCheckedFirstSnapshot) {
|
|
139
|
+
// record Ids in the snapshot
|
|
140
|
+
snapshot.nodes.forEach(initNodeInfo);
|
|
141
|
+
hasCheckedFirstSnapshot = true;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
snapshot.nodes.forEach(updateNodeInfo);
|
|
145
|
+
maybeSummarizeNodeInfo();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// exit if no heap snapshot found
|
|
149
|
+
if (!hasCheckedFirstSnapshot) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// post process and print the unbounded objects
|
|
153
|
+
const idsInLastSnapshot = new Set();
|
|
154
|
+
snapshot === null || snapshot === void 0 ? void 0 : snapshot.nodes.forEach(node => {
|
|
155
|
+
idsInLastSnapshot.add(node.id);
|
|
156
|
+
});
|
|
157
|
+
let ids = [];
|
|
158
|
+
for (const key in nodeInfo) {
|
|
159
|
+
const id = parseInt(key, 10);
|
|
160
|
+
const item = nodeInfo[id];
|
|
161
|
+
if (!item) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (!idsInLastSnapshot.has(id)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (item.min === item.max) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
// filter out non-significant leaks
|
|
171
|
+
if (item.history[item.history.length - 1] < Config_1.default.unboundSizeThreshold) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
ids.push(Object.assign({ id }, item));
|
|
175
|
+
}
|
|
176
|
+
if (ids.length === 0) {
|
|
177
|
+
Console_1.default.midLevel('No increasing objects found.');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
ids = ids
|
|
181
|
+
.sort((o1, o2) => o2.history[o2.history.length - 1] - o1.history[o1.history.length - 1])
|
|
182
|
+
.slice(0, 20);
|
|
183
|
+
// print on terminal
|
|
184
|
+
const str = Serializer_1.default.summarizeUnboundedObjects(ids, { color: true });
|
|
185
|
+
Console_1.default.topLevel('Top growing objects in sizes:');
|
|
186
|
+
Console_1.default.lowLevel(' (Use `memlab report --nodeId=@ID` to get trace)');
|
|
187
|
+
Console_1.default.topLevel('\n' + str);
|
|
188
|
+
// save results to file
|
|
189
|
+
const csv = Serializer_1.default.summarizeUnboundedObjectsToCSV(ids);
|
|
190
|
+
fs_1.default.writeFileSync(Config_1.default.unboundObjectCSV, csv, 'UTF-8');
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// find all unique pattern of leaks
|
|
194
|
+
detectMemoryLeaks() {
|
|
195
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
196
|
+
const snapshotDiff = yield this.diffSnapshots(true);
|
|
197
|
+
Config_1.default.dumpNodeInfo = false;
|
|
198
|
+
const { paths } = yield this.searchLeakedTraces(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot);
|
|
199
|
+
LeakTraceDetailsLogger_1.default.logTraces(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot, snapshotDiff.listOfLeakedHeapNodeIdSet, paths, Config_1.default.traceJsonOutDir);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
visualizeMemoryUsage(options = {}) {
|
|
203
|
+
if (Config_1.default.useExternalSnapshot || options.snapshotDir) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const tabsOrder = Utils_1.default.loadTabsOrder();
|
|
207
|
+
// if memory usage data is incomplete, skip the visualization
|
|
208
|
+
for (const tab of tabsOrder) {
|
|
209
|
+
if (!(tab.JSHeapUsedSize > 0)) {
|
|
210
|
+
if (Config_1.default.verbose) {
|
|
211
|
+
Console_1.default.error('Memory usage data incomplete');
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const plotData = tabsOrder.map((tab, idx) => [
|
|
217
|
+
idx + 1,
|
|
218
|
+
((tab.JSHeapUsedSize / 100000) | 0) / 10,
|
|
219
|
+
]);
|
|
220
|
+
// the graph component cannot handle an array with a single element
|
|
221
|
+
while (plotData.length < 2) {
|
|
222
|
+
plotData.push([plotData.length + 1, 0]);
|
|
223
|
+
}
|
|
224
|
+
// plot visual settings
|
|
225
|
+
const minY = 1;
|
|
226
|
+
const maxY = plotData.reduce((m, v) => Math.max(m, v[1]), 0) * 1.15;
|
|
227
|
+
const yFractions = 1;
|
|
228
|
+
const yLabelWidth = 1 +
|
|
229
|
+
Math.max(minY.toFixed(yFractions).length, maxY.toFixed(yFractions).length);
|
|
230
|
+
const maxWidth = process.stdout.columns - 10;
|
|
231
|
+
const idealWidth = Math.max(2 * plotData.length + 2 * yLabelWidth, 10);
|
|
232
|
+
const plotWidth = Math.min(idealWidth, maxWidth);
|
|
233
|
+
Console_1.default.topLevel('Memory usage across all steps:');
|
|
234
|
+
Console_1.default.topLevel((0, babar_1.default)(plotData, {
|
|
235
|
+
color: 'green',
|
|
236
|
+
width: plotWidth,
|
|
237
|
+
height: 10,
|
|
238
|
+
xFractions: 0,
|
|
239
|
+
yFractions,
|
|
240
|
+
minY,
|
|
241
|
+
maxY,
|
|
242
|
+
}));
|
|
243
|
+
Console_1.default.topLevel('');
|
|
244
|
+
}
|
|
245
|
+
focus(options = {}) {
|
|
246
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
247
|
+
Console_1.default.overwrite(`Generating report for node @${Config_1.default.focusFiberNodeId}`);
|
|
248
|
+
let snapshotLeakedHeapNodeIdSet = new Set();
|
|
249
|
+
let nodeIdsInSnapshots = [];
|
|
250
|
+
let snapshot;
|
|
251
|
+
if (options.file) {
|
|
252
|
+
const opt = { buildNodeIdIndex: true, verbose: true };
|
|
253
|
+
snapshot = yield Utils_1.default.getSnapshotFromFile(options.file, opt);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
Utils_1.default.checkSnapshots();
|
|
257
|
+
const snapshotDiff = yield this.diffSnapshots(true);
|
|
258
|
+
nodeIdsInSnapshots = snapshotDiff.listOfLeakedHeapNodeIdSet;
|
|
259
|
+
snapshotLeakedHeapNodeIdSet = snapshotDiff.leakedHeapNodeIdSet;
|
|
260
|
+
snapshot = snapshotDiff.snapshot;
|
|
261
|
+
}
|
|
262
|
+
this.dumpPathByNodeId(snapshotLeakedHeapNodeIdSet, snapshot, nodeIdsInSnapshots, Config_1.default.focusFiberNodeId, Config_1.default.viewJsonFile, Config_1.default.singleReportSummary);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
shouldLoadCompleteSnapshot(tabsOrder, tab) {
|
|
266
|
+
for (let i = tabsOrder.length - 1; i >= 0; --i) {
|
|
267
|
+
const curTab = tabsOrder[i];
|
|
268
|
+
if (curTab.type === 'target' || curTab.type === 'final') {
|
|
269
|
+
return curTab === tab;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
diffSnapshots(loadAll = false) {
|
|
275
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
276
|
+
const nodeIdsInSnapshots = [];
|
|
277
|
+
const tabsOrder = Utils_1.default.loadTabsOrder();
|
|
278
|
+
// a set keeping track of node ids generated before the target snapshot
|
|
279
|
+
const baselineIds = new Set();
|
|
280
|
+
let collectBaselineIds = true;
|
|
281
|
+
let targetAllocatedHeapNodeIdSet = null;
|
|
282
|
+
let leakedHeapNodeIdSet = null;
|
|
283
|
+
const options = { verbose: true };
|
|
284
|
+
let snapshot = null;
|
|
285
|
+
for (let i = 0; i < tabsOrder.length; i++) {
|
|
286
|
+
const tab = tabsOrder[i];
|
|
287
|
+
// force GC before loading each snapshot
|
|
288
|
+
if (global.gc) {
|
|
289
|
+
global.gc();
|
|
290
|
+
}
|
|
291
|
+
// when we see the target snapshot, stop collecting node ids allocated so far
|
|
292
|
+
if (tab.type === 'target') {
|
|
293
|
+
collectBaselineIds = false;
|
|
294
|
+
}
|
|
295
|
+
let idsInSnapshot = new Set();
|
|
296
|
+
nodeIdsInSnapshots.push(idsInSnapshot);
|
|
297
|
+
if (!tab.snapshot) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
// in quick mode, there is no need to load all snapshots
|
|
301
|
+
if (!loadAll && !tab.type) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const file = Utils_1.default.getSnapshotFilePath(tab);
|
|
305
|
+
if (this.shouldLoadCompleteSnapshot(tabsOrder, tab)) {
|
|
306
|
+
// final snapshot needs to build node index
|
|
307
|
+
const opt = Object.assign({ buildNodeIdIndex: true }, options);
|
|
308
|
+
snapshot = yield Utils_1.default.getSnapshotFromFile(file, opt);
|
|
309
|
+
// record Ids in the snapshot
|
|
310
|
+
snapshot.nodes.forEach(node => {
|
|
311
|
+
idsInSnapshot.add(node.id);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
idsInSnapshot = yield Utils_1.default.getSnapshotNodeIdsFromFile(file, options);
|
|
316
|
+
nodeIdsInSnapshots.pop();
|
|
317
|
+
nodeIdsInSnapshots.push(idsInSnapshot);
|
|
318
|
+
}
|
|
319
|
+
// collect all node ids allocated before the target snapshot
|
|
320
|
+
if (collectBaselineIds) {
|
|
321
|
+
for (const id of idsInSnapshot) {
|
|
322
|
+
baselineIds.add(id);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (tab.type === 'target') {
|
|
326
|
+
targetAllocatedHeapNodeIdSet = new Set();
|
|
327
|
+
idsInSnapshot.forEach(id => {
|
|
328
|
+
if (!baselineIds.has(id)) {
|
|
329
|
+
targetAllocatedHeapNodeIdSet === null || targetAllocatedHeapNodeIdSet === void 0 ? void 0 : targetAllocatedHeapNodeIdSet.add(id);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
// if final snapshot is not present,
|
|
333
|
+
// search leaks among `Set { target } \ Set { baseline }`
|
|
334
|
+
leakedHeapNodeIdSet = targetAllocatedHeapNodeIdSet;
|
|
335
|
+
}
|
|
336
|
+
if (tab.type === 'final') {
|
|
337
|
+
if (!targetAllocatedHeapNodeIdSet) {
|
|
338
|
+
Utils_1.default.haltOrThrow('no target snapshot before finals snapshot');
|
|
339
|
+
}
|
|
340
|
+
leakedHeapNodeIdSet = new Set();
|
|
341
|
+
snapshot === null || snapshot === void 0 ? void 0 : snapshot.nodes.forEach(node => {
|
|
342
|
+
if (targetAllocatedHeapNodeIdSet === null || targetAllocatedHeapNodeIdSet === void 0 ? void 0 : targetAllocatedHeapNodeIdSet.has(node.id)) {
|
|
343
|
+
leakedHeapNodeIdSet === null || leakedHeapNodeIdSet === void 0 ? void 0 : leakedHeapNodeIdSet.add(node.id);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
targetAllocatedHeapNodeIdSet = null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (!snapshot || !leakedHeapNodeIdSet) {
|
|
350
|
+
throw Utils_1.default.haltOrThrow('Snapshot incomplete', {
|
|
351
|
+
printErrorBeforeHalting: true,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
leakedHeapNodeIdSet: leakedHeapNodeIdSet,
|
|
356
|
+
snapshot,
|
|
357
|
+
listOfLeakedHeapNodeIdSet: nodeIdsInSnapshots,
|
|
358
|
+
};
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
calculateRetainedSizes(snapshot) {
|
|
362
|
+
const finder = new TraceFinder_1.default();
|
|
363
|
+
// dominator and retained size
|
|
364
|
+
finder.calculateAllNodesRetainedSizes(snapshot);
|
|
365
|
+
}
|
|
366
|
+
// initialize the path finder
|
|
367
|
+
preparePathFinder(snapshot) {
|
|
368
|
+
const finder = new TraceFinder_1.default();
|
|
369
|
+
// shortest path for all nodes
|
|
370
|
+
finder.annotateShortestPaths(snapshot);
|
|
371
|
+
// dominator and retained size
|
|
372
|
+
finder.calculateAllNodesRetainedSizes(snapshot);
|
|
373
|
+
// mark detached Fiber nodes
|
|
374
|
+
Utils_1.default.markAllDetachedFiberNode(snapshot);
|
|
375
|
+
// mark alternate Fiber nodes
|
|
376
|
+
Utils_1.default.markAlternateFiberNode(snapshot);
|
|
377
|
+
return finder;
|
|
378
|
+
}
|
|
379
|
+
// summarize the page interaction and dump to the leak text summary file
|
|
380
|
+
dumpPageInteractionSummary() {
|
|
381
|
+
const tabsOrder = Utils_1.default.loadTabsOrder();
|
|
382
|
+
const tabsOrderStr = Serializer_1.default.summarizeTabsOrder(tabsOrder);
|
|
383
|
+
fs_1.default.writeFileSync(Config_1.default.exploreResultFile, tabsOrderStr, 'UTF-8');
|
|
384
|
+
}
|
|
385
|
+
// summarize the leak and print the info in console
|
|
386
|
+
dumpLeakSummaryToConsole(leakedNodeIds, snapshot) {
|
|
387
|
+
if (!Config_1.default.verbose && !Config_1.default.useExternalSnapshot) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
Console_1.default.overwrite('summarizing snapshot diff...');
|
|
391
|
+
const aggregatedLeakSummaryDict = Object.create(null);
|
|
392
|
+
// count the distribution of nodes
|
|
393
|
+
Utils_1.default.applyToNodes(leakedNodeIds, snapshot, node => {
|
|
394
|
+
if (!Utils_1.default.isDebuggableNode(node)) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
const key = `${node.name} (${node.type})`;
|
|
398
|
+
const leakSummary = (aggregatedLeakSummaryDict[key] =
|
|
399
|
+
aggregatedLeakSummaryDict[key] || {
|
|
400
|
+
name: node.name,
|
|
401
|
+
type: node.type,
|
|
402
|
+
count: 0,
|
|
403
|
+
retainedSize: 0,
|
|
404
|
+
});
|
|
405
|
+
leakSummary.count++;
|
|
406
|
+
leakSummary.retainedSize += node.retainedSize | 0;
|
|
407
|
+
});
|
|
408
|
+
const list = Object.entries(aggregatedLeakSummaryDict)
|
|
409
|
+
.sort((e1, e2) => e2[1].retainedSize - e1[1].retainedSize)
|
|
410
|
+
.slice(0, 20)
|
|
411
|
+
.map(entry => {
|
|
412
|
+
const ret = Object.assign(entry[1]);
|
|
413
|
+
ret.retainedSize = Utils_1.default.getReadableBytes(ret.retainedSize);
|
|
414
|
+
return ret;
|
|
415
|
+
});
|
|
416
|
+
Console_1.default.topLevel('Alive objects allocated in target page:');
|
|
417
|
+
Console_1.default.table(list);
|
|
418
|
+
}
|
|
419
|
+
checkDetachedFiberNode(node) {
|
|
420
|
+
if (!Config_1.default.detectFiberNodeLeak ||
|
|
421
|
+
!Utils_1.default.isFiberNode(node) ||
|
|
422
|
+
Utils_1.default.hasHostRoot(node)) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
return !Utils_1.default.isNodeDominatedByDeletionsArray(node);
|
|
426
|
+
}
|
|
427
|
+
isTrivialNode(node) {
|
|
428
|
+
return (node.type === 'number' ||
|
|
429
|
+
Utils_1.default.isStringNode(node) ||
|
|
430
|
+
node.type === 'hidden');
|
|
431
|
+
}
|
|
432
|
+
filterLeakedObjects(leakedNodeIds, snapshot) {
|
|
433
|
+
var _a;
|
|
434
|
+
// call init leak filter hook if exists
|
|
435
|
+
if ((_a = Config_1.default.externalLeakFilter) === null || _a === void 0 ? void 0 : _a.beforeLeakFilter) {
|
|
436
|
+
Config_1.default.externalLeakFilter.beforeLeakFilter(snapshot, leakedNodeIds);
|
|
437
|
+
}
|
|
438
|
+
// start filtering memory leaks
|
|
439
|
+
Utils_1.default.filterNodesInPlace(leakedNodeIds, snapshot, node => {
|
|
440
|
+
// use external leak filter if exists
|
|
441
|
+
if (Config_1.default.externalLeakFilter) {
|
|
442
|
+
return Config_1.default.externalLeakFilter.leakFilter(node, snapshot, leakedNodeIds);
|
|
443
|
+
}
|
|
444
|
+
if (this.isTrivialNode(node)) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
// when analyzing hermes heap snapshots, filter Hermes internal objects
|
|
448
|
+
if (Config_1.default.jsEngine === 'hermes' && Utils_1.default.isHermesInternalObject(node)) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
if (Config_1.default.oversizeObjectAsLeak) {
|
|
452
|
+
return node.retainedSize > Config_1.default.oversizeThreshold;
|
|
453
|
+
}
|
|
454
|
+
// check FiberNodes without a Fiber Root
|
|
455
|
+
if (this.checkDetachedFiberNode(node)) {
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
const isDetached = Utils_1.default.isDetachedDOMNode(node, {
|
|
459
|
+
ignoreInternalNode: true,
|
|
460
|
+
});
|
|
461
|
+
if (isDetached && Config_1.default.targetApp === 'ads-manager') {
|
|
462
|
+
return Utils_1.default.hasReactEdges(node);
|
|
463
|
+
}
|
|
464
|
+
return isDetached || Utils_1.default.isStackTraceFrame(node);
|
|
465
|
+
});
|
|
466
|
+
if (Config_1.default.verbose) {
|
|
467
|
+
Console_1.default.midLevel(`${leakedNodeIds.size} Fiber nodes and Detached elements`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
aggregateDominatorMetrics(ids, snapshot, checkNodeCb, nodeMetricsCb) {
|
|
471
|
+
let ret = 0;
|
|
472
|
+
const dominators = Utils_1.default.getConditionalDominatorIds(ids, snapshot, checkNodeCb);
|
|
473
|
+
Utils_1.default.applyToNodes(dominators, snapshot, node => {
|
|
474
|
+
ret += nodeMetricsCb(node);
|
|
475
|
+
});
|
|
476
|
+
return ret;
|
|
477
|
+
}
|
|
478
|
+
getOverallHeapInfo(snapshot, options = {}) {
|
|
479
|
+
if (!Config_1.default.verbose && !options.force) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
Console_1.default.overwrite('summarizing heap info...');
|
|
483
|
+
const allIds = Utils_1.default.getNodesIdSet(snapshot);
|
|
484
|
+
const heapInfo = {
|
|
485
|
+
fiberNodeSize: this.aggregateDominatorMetrics(allIds, snapshot, Utils_1.default.isFiberNode, this.getRetainedSize),
|
|
486
|
+
regularFiberNodeSize: this.aggregateDominatorMetrics(allIds, snapshot, Utils_1.default.isRegularFiberNode, this.getRetainedSize),
|
|
487
|
+
detachedFiberNodeSize: this.aggregateDominatorMetrics(allIds, snapshot, Utils_1.default.isDetachedFiberNode, this.getRetainedSize),
|
|
488
|
+
alternateFiberNodeSize: this.aggregateDominatorMetrics(allIds, snapshot, Utils_1.default.isAlternateNode, this.getRetainedSize),
|
|
489
|
+
error: this.aggregateDominatorMetrics(allIds, snapshot, node => node.name === 'Error', this.getRetainedSize),
|
|
490
|
+
};
|
|
491
|
+
return heapInfo;
|
|
492
|
+
}
|
|
493
|
+
getRetainedSize(node) {
|
|
494
|
+
return node.retainedSize;
|
|
495
|
+
}
|
|
496
|
+
getOverallLeakInfo(leakedNodeIds, snapshot) {
|
|
497
|
+
if (!Config_1.default.verbose) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const leakInfo = Object.assign(Object.assign({}, this.getOverallHeapInfo(snapshot)), { leakedSize: this.aggregateDominatorMetrics(leakedNodeIds, snapshot, () => true, this.getRetainedSize), leakedFiberNodeSize: this.aggregateDominatorMetrics(leakedNodeIds, snapshot, Utils_1.default.isFiberNode, this.getRetainedSize), leakedAlternateFiberNodeSize: this.aggregateDominatorMetrics(leakedNodeIds, snapshot, Utils_1.default.isAlternateNode, this.getRetainedSize) });
|
|
501
|
+
return leakInfo;
|
|
502
|
+
}
|
|
503
|
+
printHeapInfo(leakInfo) {
|
|
504
|
+
Object.entries(leakInfo)
|
|
505
|
+
.map(([k, v]) => [
|
|
506
|
+
Utils_1.default.camelCaseToReadableString(k),
|
|
507
|
+
Utils_1.default.getReadableBytes(v),
|
|
508
|
+
])
|
|
509
|
+
.forEach(([name, value]) => {
|
|
510
|
+
Console_1.default.topLevel(`· ${name}: ${value}`);
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
breakDownSnapshotByShapes(snapshot) {
|
|
514
|
+
Console_1.default.overwrite('Breaking down memory by shapes...');
|
|
515
|
+
const breakdown = Object.create(null);
|
|
516
|
+
const population = Object.create(null);
|
|
517
|
+
// group objects based on their shapes
|
|
518
|
+
snapshot.nodes.forEach(node => {
|
|
519
|
+
if ((node.type !== 'object' && !Utils_1.default.isStringNode(node)) ||
|
|
520
|
+
Config_1.default.nodeIgnoreSetInShape.has(node.name)) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const key = Serializer_1.default.summarizeNodeShape(node);
|
|
524
|
+
breakdown[key] = breakdown[key] || new Set();
|
|
525
|
+
breakdown[key].add(node.id);
|
|
526
|
+
if (population[key] === undefined) {
|
|
527
|
+
population[key] = { examples: [], n: 0 };
|
|
528
|
+
}
|
|
529
|
+
++population[key].n;
|
|
530
|
+
// retain the top 5 examples
|
|
531
|
+
const examples = population[key].examples;
|
|
532
|
+
examples.push(node);
|
|
533
|
+
examples.sort((n1, n2) => n2.retainedSize - n1.retainedSize);
|
|
534
|
+
if (examples.length > 5) {
|
|
535
|
+
examples.pop();
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
// calculate and sort based on retained sizes
|
|
539
|
+
const ret = [];
|
|
540
|
+
for (const key in breakdown) {
|
|
541
|
+
const size = this.aggregateDominatorMetrics(breakdown[key], snapshot, () => true, this.getRetainedSize);
|
|
542
|
+
ret.push({ key, retainedSize: size });
|
|
543
|
+
}
|
|
544
|
+
ret.sort((o1, o2) => o2.retainedSize - o1.retainedSize);
|
|
545
|
+
Console_1.default.topLevel('Object shapes with top retained sizes:');
|
|
546
|
+
Console_1.default.lowLevel(' (Use `memlab report --nodeId=@ID` to get trace)\n');
|
|
547
|
+
const topList = ret.slice(0, 40);
|
|
548
|
+
// print settings
|
|
549
|
+
const opt = { color: true, compact: true };
|
|
550
|
+
const dot = chalk_1.default.grey('· ');
|
|
551
|
+
const colon = chalk_1.default.grey(': ');
|
|
552
|
+
// print the shapes with the biggest retained size
|
|
553
|
+
for (const o of topList) {
|
|
554
|
+
const referrerInfo = this.breakDownByReferrers(breakdown[o.key], snapshot);
|
|
555
|
+
const { examples, n } = population[o.key];
|
|
556
|
+
const shapeStr = Serializer_1.default.summarizeNodeShape(examples[0], opt);
|
|
557
|
+
const bytes = Utils_1.default.getReadableBytes(o.retainedSize);
|
|
558
|
+
const examplesStr = examples
|
|
559
|
+
.map(e => `@${e.id} [${Utils_1.default.getReadableBytes(e.retainedSize)}]`)
|
|
560
|
+
.join(' | ');
|
|
561
|
+
const meta = chalk_1.default.grey(` (N: ${n}, Examples: ${examplesStr})`);
|
|
562
|
+
Console_1.default.topLevel(`${dot}${shapeStr}${colon}${bytes}${meta}`);
|
|
563
|
+
Console_1.default.lowLevel(referrerInfo + '\n');
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
isTrivialEdgeForBreakDown(edge) {
|
|
567
|
+
const source = edge.fromNode;
|
|
568
|
+
return (source.type === 'array' ||
|
|
569
|
+
source.name === '(object elements)' ||
|
|
570
|
+
source.name === 'system' ||
|
|
571
|
+
edge.name_or_index === '__proto__' ||
|
|
572
|
+
edge.name_or_index === 'prototype');
|
|
573
|
+
}
|
|
574
|
+
breakDownByReferrers(ids, snapshot) {
|
|
575
|
+
const edgeNames = Object.create(null);
|
|
576
|
+
for (const id of ids) {
|
|
577
|
+
const node = snapshot.getNodeById(id);
|
|
578
|
+
for (const edge of (node === null || node === void 0 ? void 0 : node.referrers) || []) {
|
|
579
|
+
const source = edge.fromNode;
|
|
580
|
+
if (!Utils_1.default.isMeaningfulEdge(edge) ||
|
|
581
|
+
this.isTrivialEdgeForBreakDown(edge)) {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const sourceName = Serializer_1.default.summarizeNodeName(source, {
|
|
585
|
+
color: false,
|
|
586
|
+
});
|
|
587
|
+
const edgeName = Serializer_1.default.summarizeEdgeName(edge, {
|
|
588
|
+
color: false,
|
|
589
|
+
abstract: true,
|
|
590
|
+
});
|
|
591
|
+
const edgeKey = `[${sourceName}] --${edgeName}--> `;
|
|
592
|
+
edgeNames[edgeKey] = edgeNames[edgeKey] || {
|
|
593
|
+
numberOfEdgesToNode: 0,
|
|
594
|
+
source,
|
|
595
|
+
edge,
|
|
596
|
+
};
|
|
597
|
+
++edgeNames[edgeKey].numberOfEdgesToNode;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const referrerInfo = Object.entries(edgeNames)
|
|
601
|
+
.sort((i1, i2) => i2[1].numberOfEdgesToNode - i1[1].numberOfEdgesToNode)
|
|
602
|
+
.slice(0, 4)
|
|
603
|
+
.map(i => {
|
|
604
|
+
const meta = i[1];
|
|
605
|
+
const source = Serializer_1.default.summarizeNodeName(meta.source, {
|
|
606
|
+
color: true,
|
|
607
|
+
});
|
|
608
|
+
const edgeName = Serializer_1.default.summarizeEdgeName(meta.edge, {
|
|
609
|
+
color: true,
|
|
610
|
+
abstract: true,
|
|
611
|
+
});
|
|
612
|
+
const edgeSummary = `${source} --${edgeName}-->`;
|
|
613
|
+
return ` · ${edgeSummary}: ${meta.numberOfEdgesToNode}`;
|
|
614
|
+
})
|
|
615
|
+
.join('\n');
|
|
616
|
+
return referrerInfo;
|
|
617
|
+
}
|
|
618
|
+
// find unique paths of leaked nodes
|
|
619
|
+
searchLeakedTraces(leakedNodeIds, snapshot) {
|
|
620
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
621
|
+
const finder = this.preparePathFinder(snapshot);
|
|
622
|
+
// write page interaction summary to the leaks text file
|
|
623
|
+
this.dumpPageInteractionSummary();
|
|
624
|
+
// dump leak summry to console
|
|
625
|
+
this.dumpLeakSummaryToConsole(leakedNodeIds, snapshot);
|
|
626
|
+
// get all leaked objects
|
|
627
|
+
this.filterLeakedObjects(leakedNodeIds, snapshot);
|
|
628
|
+
// get aggregated leak info
|
|
629
|
+
const leakInfo = this.getOverallHeapInfo(snapshot);
|
|
630
|
+
if (leakInfo) {
|
|
631
|
+
this.printHeapInfo(leakInfo);
|
|
632
|
+
}
|
|
633
|
+
if (Config_1.default.verbose) {
|
|
634
|
+
// show a breakdown of different object structures
|
|
635
|
+
this.breakDownSnapshotByShapes(snapshot);
|
|
636
|
+
}
|
|
637
|
+
let numOfLeakedObjects = 0;
|
|
638
|
+
let i = 0;
|
|
639
|
+
const nodeIdInPaths = new Set();
|
|
640
|
+
const paths = [];
|
|
641
|
+
// analysis for each node
|
|
642
|
+
Utils_1.default.applyToNodes(leakedNodeIds, snapshot, node => {
|
|
643
|
+
if (!Config_1.default.isContinuousTest && ++i % 11 === 0) {
|
|
644
|
+
Console_1.default.overwrite(`progress: ${i} / ${leakedNodeIds.size} @${node.id}`);
|
|
645
|
+
}
|
|
646
|
+
// BFS search for path from the leaked node to GC roots
|
|
647
|
+
const p = finder.getPathToGCRoots(snapshot, node);
|
|
648
|
+
if (!p) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (!Utils_1.default.isInterestingPath(p)) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
++numOfLeakedObjects;
|
|
655
|
+
paths.push(p);
|
|
656
|
+
// convert the path to a string
|
|
657
|
+
if (Config_1.default.isFullRun) {
|
|
658
|
+
const pathStr = Serializer_1.default.summarizePath(p, nodeIdInPaths, snapshot);
|
|
659
|
+
fs_1.default.appendFileSync(Config_1.default.exploreResultFile, `\n\n${pathStr}\n\n`, 'UTF-8');
|
|
660
|
+
}
|
|
661
|
+
}, { reverse: true });
|
|
662
|
+
if (Config_1.default.verbose) {
|
|
663
|
+
Console_1.default.midLevel(`${numOfLeakedObjects} leaked objects`);
|
|
664
|
+
}
|
|
665
|
+
// cluster traces from the current run
|
|
666
|
+
const clusters = TraceBucket_1.default.clusterPaths(paths, snapshot, this.aggregateDominatorMetrics);
|
|
667
|
+
yield this.serializeClusterUpdate(clusters);
|
|
668
|
+
if (Config_1.default.logUnclassifiedClusters) {
|
|
669
|
+
// cluster traces from the current run
|
|
670
|
+
const clustersUnclassified = TraceBucket_1.default.generateUnClassifiedClusters(paths, snapshot, this.aggregateDominatorMetrics);
|
|
671
|
+
LeakClusterLogger_1.default.logUnclassifiedClusters(clustersUnclassified);
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
paths: clusters.map(c => c.path),
|
|
675
|
+
};
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
serializeClusterUpdate(clusters, options = {}) {
|
|
679
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
680
|
+
// load existing clusters
|
|
681
|
+
const existingClusters = yield LeakClusterLogger_1.default.loadClusters(Config_1.default.currentUniqueClusterDir);
|
|
682
|
+
// figure out stale and new clusters
|
|
683
|
+
const clusterDiff = TraceBucket_1.default.diffClusters(clusters, existingClusters);
|
|
684
|
+
if (options.reclusterOnly) {
|
|
685
|
+
// only recluster updates
|
|
686
|
+
LeakClusterLogger_1.default.logClusterDiff(clusterDiff);
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
// log clusters traces for the current run
|
|
690
|
+
LeakClusterLogger_1.default.logClusters(clusters, { clusterDiff });
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
dumpPathByNodeId(leakedIdSet, snapshot, nodeIdsInSnapshots, id, pathLoaderFile, summaryFile) {
|
|
695
|
+
Console_1.default.overwrite('start analysis...');
|
|
696
|
+
const finder = this.preparePathFinder(snapshot);
|
|
697
|
+
const nodeIdInPaths = new Set();
|
|
698
|
+
const idSet = new Set([id]);
|
|
699
|
+
LeakTraceDetailsLogger_1.default.setTraceFileEmpty(pathLoaderFile);
|
|
700
|
+
fs_1.default.writeFileSync(summaryFile, 'no path found', 'UTF-8');
|
|
701
|
+
Utils_1.default.applyToNodes(idSet, snapshot, node => {
|
|
702
|
+
const path = finder.getPathToGCRoots(snapshot, node);
|
|
703
|
+
if (!path) {
|
|
704
|
+
Console_1.default.topLevel(`path for node @${id} is not found`);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
LeakTraceDetailsLogger_1.default.logTrace(leakedIdSet, snapshot, nodeIdsInSnapshots, path, pathLoaderFile);
|
|
708
|
+
const tabsOrder = Utils_1.default.loadTabsOrder();
|
|
709
|
+
const interactionSummary = Serializer_1.default.summarizeTabsOrder(tabsOrder);
|
|
710
|
+
let pathSummary = Serializer_1.default.summarizePath(path, nodeIdInPaths, snapshot, { color: true });
|
|
711
|
+
Console_1.default.topLevel(pathSummary);
|
|
712
|
+
pathSummary = Serializer_1.default.summarizePath(path, nodeIdInPaths, snapshot);
|
|
713
|
+
const summary = `Page Interaction: \n${interactionSummary}\n\n` +
|
|
714
|
+
`Path from GC Root to Leaked Object:\n${pathSummary}`;
|
|
715
|
+
fs_1.default.writeFileSync(summaryFile, summary, 'UTF-8');
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
exports.default = new MemoryAnalyst();
|