@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.
Files changed (130) hide show
  1. package/README.md +11 -0
  2. package/dist/__tests__/parser/HeapParser.test.d.ts +11 -0
  3. package/dist/__tests__/parser/HeapParser.test.d.ts.map +1 -0
  4. package/dist/__tests__/parser/HeapParser.test.js +54 -0
  5. package/dist/__tests__/parser/NodeHeap.test.d.ts +11 -0
  6. package/dist/__tests__/parser/NodeHeap.test.d.ts.map +1 -0
  7. package/dist/__tests__/parser/NodeHeap.test.js +96 -0
  8. package/dist/__tests__/parser/StringNode.test.d.ts +11 -0
  9. package/dist/__tests__/parser/StringNode.test.d.ts.map +1 -0
  10. package/dist/__tests__/parser/StringNode.test.js +61 -0
  11. package/dist/__tests__/parser/traverse/HeapNodeTraverse.test.d.ts +16 -0
  12. package/dist/__tests__/parser/traverse/HeapNodeTraverse.test.d.ts.map +1 -0
  13. package/dist/__tests__/parser/traverse/HeapNodeTraverse.test.js +140 -0
  14. package/dist/__tests__/utils/utils.test.d.ts +11 -0
  15. package/dist/__tests__/utils/utils.test.d.ts.map +1 -0
  16. package/dist/__tests__/utils/utils.test.js +81 -0
  17. package/dist/index.d.ts +29 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +62 -0
  20. package/dist/lib/BaseOption.d.ts +31 -0
  21. package/dist/lib/BaseOption.d.ts.map +1 -0
  22. package/dist/lib/BaseOption.js +109 -0
  23. package/dist/lib/BrowserInfo.d.ts +33 -0
  24. package/dist/lib/BrowserInfo.d.ts.map +1 -0
  25. package/dist/lib/BrowserInfo.js +117 -0
  26. package/dist/lib/Config.d.ts +203 -0
  27. package/dist/lib/Config.d.ts.map +1 -0
  28. package/dist/lib/Config.js +427 -0
  29. package/dist/lib/Console.d.ts +67 -0
  30. package/dist/lib/Console.d.ts.map +1 -0
  31. package/dist/lib/Console.js +344 -0
  32. package/dist/lib/Constant.d.ts +38 -0
  33. package/dist/lib/Constant.d.ts.map +1 -0
  34. package/dist/lib/Constant.js +58 -0
  35. package/dist/lib/FileManager.d.ts +69 -0
  36. package/dist/lib/FileManager.d.ts.map +1 -0
  37. package/dist/lib/FileManager.js +309 -0
  38. package/dist/lib/HeapAnalyzer.d.ts +51 -0
  39. package/dist/lib/HeapAnalyzer.d.ts.map +1 -0
  40. package/dist/lib/HeapAnalyzer.js +719 -0
  41. package/dist/lib/HeapParser.d.ts +19 -0
  42. package/dist/lib/HeapParser.d.ts.map +1 -0
  43. package/dist/lib/HeapParser.js +128 -0
  44. package/dist/lib/InternalValueSetter.d.ts +14 -0
  45. package/dist/lib/InternalValueSetter.d.ts.map +1 -0
  46. package/dist/lib/InternalValueSetter.js +43 -0
  47. package/dist/lib/NodeHeap.d.ts +16 -0
  48. package/dist/lib/NodeHeap.d.ts.map +1 -0
  49. package/dist/lib/NodeHeap.js +62 -0
  50. package/dist/lib/ProcessManager.d.ts +25 -0
  51. package/dist/lib/ProcessManager.d.ts.map +1 -0
  52. package/dist/lib/ProcessManager.js +67 -0
  53. package/dist/lib/Serializer.d.ts +49 -0
  54. package/dist/lib/Serializer.d.ts.map +1 -0
  55. package/dist/lib/Serializer.js +702 -0
  56. package/dist/lib/StringLoader.d.ts +26 -0
  57. package/dist/lib/StringLoader.d.ts.map +1 -0
  58. package/dist/lib/StringLoader.js +290 -0
  59. package/dist/lib/Types.d.ts +432 -0
  60. package/dist/lib/Types.d.ts.map +1 -0
  61. package/dist/lib/Types.js +11 -0
  62. package/dist/lib/Utils.d.ts +223 -0
  63. package/dist/lib/Utils.d.ts.map +1 -0
  64. package/dist/lib/Utils.js +1736 -0
  65. package/dist/lib/heap-data/HeapEdge.d.ts +27 -0
  66. package/dist/lib/heap-data/HeapEdge.d.ts.map +1 -0
  67. package/dist/lib/heap-data/HeapEdge.js +75 -0
  68. package/dist/lib/heap-data/HeapLocation.d.ts +22 -0
  69. package/dist/lib/heap-data/HeapLocation.d.ts.map +1 -0
  70. package/dist/lib/heap-data/HeapLocation.js +40 -0
  71. package/dist/lib/heap-data/HeapNode.d.ts +55 -0
  72. package/dist/lib/heap-data/HeapNode.d.ts.map +1 -0
  73. package/dist/lib/heap-data/HeapNode.js +344 -0
  74. package/dist/lib/heap-data/HeapSnapshot.d.ts +85 -0
  75. package/dist/lib/heap-data/HeapSnapshot.d.ts.map +1 -0
  76. package/dist/lib/heap-data/HeapSnapshot.js +462 -0
  77. package/dist/lib/heap-data/HeapStringNode.d.ts +18 -0
  78. package/dist/lib/heap-data/HeapStringNode.d.ts.map +1 -0
  79. package/dist/lib/heap-data/HeapStringNode.js +43 -0
  80. package/dist/lib/heap-data/HeapUtils.d.ts +17 -0
  81. package/dist/lib/heap-data/HeapUtils.d.ts.map +1 -0
  82. package/dist/lib/heap-data/HeapUtils.js +25 -0
  83. package/dist/logger/LeakClusterLogger.d.ts +40 -0
  84. package/dist/logger/LeakClusterLogger.d.ts.map +1 -0
  85. package/dist/logger/LeakClusterLogger.js +228 -0
  86. package/dist/logger/LeakTraceDetailsLogger.d.ts +19 -0
  87. package/dist/logger/LeakTraceDetailsLogger.d.ts.map +1 -0
  88. package/dist/logger/LeakTraceDetailsLogger.js +50 -0
  89. package/dist/modes/BaseMode.d.ts +30 -0
  90. package/dist/modes/BaseMode.d.ts.map +1 -0
  91. package/dist/modes/BaseMode.js +95 -0
  92. package/dist/modes/InteractionTestMode.d.ts +23 -0
  93. package/dist/modes/InteractionTestMode.d.ts.map +1 -0
  94. package/dist/modes/InteractionTestMode.js +46 -0
  95. package/dist/modes/MeasureMode.d.ts +23 -0
  96. package/dist/modes/MeasureMode.d.ts.map +1 -0
  97. package/dist/modes/MeasureMode.js +58 -0
  98. package/dist/modes/RunningModes.d.ts +15 -0
  99. package/dist/modes/RunningModes.d.ts.map +1 -0
  100. package/dist/modes/RunningModes.js +40 -0
  101. package/dist/paths/TraceFinder.d.ts +31 -0
  102. package/dist/paths/TraceFinder.d.ts.map +1 -0
  103. package/dist/paths/TraceFinder.js +537 -0
  104. package/dist/trace-cluster/ClusterUtils.d.ts +14 -0
  105. package/dist/trace-cluster/ClusterUtils.d.ts.map +1 -0
  106. package/dist/trace-cluster/ClusterUtils.js +17 -0
  107. package/dist/trace-cluster/ClusterUtilsHelper.d.ts +38 -0
  108. package/dist/trace-cluster/ClusterUtilsHelper.d.ts.map +1 -0
  109. package/dist/trace-cluster/ClusterUtilsHelper.js +373 -0
  110. package/dist/trace-cluster/ClusteringHeuristics.d.ts +22 -0
  111. package/dist/trace-cluster/ClusteringHeuristics.d.ts.map +1 -0
  112. package/dist/trace-cluster/ClusteringHeuristics.js +23 -0
  113. package/dist/trace-cluster/EvalutationMetric.d.ts +22 -0
  114. package/dist/trace-cluster/EvalutationMetric.d.ts.map +1 -0
  115. package/dist/trace-cluster/EvalutationMetric.js +158 -0
  116. package/dist/trace-cluster/TraceBucket.d.ts +36 -0
  117. package/dist/trace-cluster/TraceBucket.d.ts.map +1 -0
  118. package/dist/trace-cluster/TraceBucket.js +238 -0
  119. package/dist/trace-cluster/TraceElement.d.ts +71 -0
  120. package/dist/trace-cluster/TraceElement.d.ts.map +1 -0
  121. package/dist/trace-cluster/TraceElement.js +182 -0
  122. package/dist/trace-cluster/strategies/TraceAsClusterStrategy.d.ts +15 -0
  123. package/dist/trace-cluster/strategies/TraceAsClusterStrategy.d.ts.map +1 -0
  124. package/dist/trace-cluster/strategies/TraceAsClusterStrategy.js +37 -0
  125. package/dist/trace-cluster/strategies/TraceSimilarityStrategy.d.ts +15 -0
  126. package/dist/trace-cluster/strategies/TraceSimilarityStrategy.d.ts.map +1 -0
  127. package/dist/trace-cluster/strategies/TraceSimilarityStrategy.js +60 -0
  128. package/package.json +60 -0
  129. package/static/run-meta.json +10 -0
  130. 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();