@memlab/heap-analysis 1.0.8 → 1.0.10
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/LICENSE +21 -0
- package/dist/PluginUtils.d.ts +6 -0
- package/dist/PluginUtils.js +35 -0
- package/dist/plugins/CollectionUnboundGrowthAnalysis.d.ts +26 -0
- package/dist/plugins/CollectionUnboundGrowthAnalysis.js +169 -0
- package/dist/plugins/ObjectContentAnalysis.js +1 -0
- package/dist/plugins/ObjectUnboundGrowthAnalysis.js +2 -1
- package/dist/plugins/ShapeUnboundGrowthAnalysis.js +2 -1
- package/package.json +5 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/PluginUtils.d.ts
CHANGED
|
@@ -314,18 +314,24 @@ declare function aggregateDominatorMetrics(ids: Set<number>, snapshot: IHeapSnap
|
|
|
314
314
|
*/
|
|
315
315
|
declare function getDominatorNodes(ids: Set<number>, snapshot: IHeapSnapshot): Set<number>;
|
|
316
316
|
declare function filterOutLargestObjects(snapshot: IHeapSnapshot, objectFilter: (node: IHeapNode) => boolean, listSize?: number): IHeapNode[];
|
|
317
|
+
declare function calculateRetainedSizes(snapshot: IHeapSnapshot): void;
|
|
318
|
+
declare function isCollectObject(node: IHeapNode): boolean;
|
|
319
|
+
declare function getCollectionFanout(node: IHeapNode): number;
|
|
317
320
|
declare const _default: {
|
|
318
321
|
aggregateDominatorMetrics: typeof aggregateDominatorMetrics;
|
|
322
|
+
calculateRetainedSizes: typeof calculateRetainedSizes;
|
|
319
323
|
defaultAnalysisArgs: {
|
|
320
324
|
args: {
|
|
321
325
|
_: never[];
|
|
322
326
|
};
|
|
323
327
|
};
|
|
324
328
|
filterOutLargestObjects: typeof filterOutLargestObjects;
|
|
329
|
+
getCollectionFanout: typeof getCollectionFanout;
|
|
325
330
|
getDominatorNodes: typeof getDominatorNodes;
|
|
326
331
|
getObjectOutgoingEdgeCount: typeof getObjectOutgoingEdgeCount;
|
|
327
332
|
getSnapshotDirForAnalysis: typeof getSnapshotDirForAnalysis;
|
|
328
333
|
getSnapshotFileForAnalysis: typeof getSnapshotFileForAnalysis;
|
|
334
|
+
isCollectObject: typeof isCollectObject;
|
|
329
335
|
isNodeWorthInspecting: typeof isNodeWorthInspecting;
|
|
330
336
|
loadHeapSnapshot: typeof loadHeapSnapshot;
|
|
331
337
|
getHeapFromFile: typeof getHeapFromFile;
|
package/dist/PluginUtils.js
CHANGED
|
@@ -604,14 +604,49 @@ function filterOutLargestObjects(snapshot, objectFilter, listSize = 50) {
|
|
|
604
604
|
});
|
|
605
605
|
return largeObjects;
|
|
606
606
|
}
|
|
607
|
+
function calculateRetainedSizes(snapshot) {
|
|
608
|
+
const finder = new core_2.TraceFinder();
|
|
609
|
+
// dominator and retained size
|
|
610
|
+
finder.calculateAllNodesRetainedSizes(snapshot);
|
|
611
|
+
}
|
|
612
|
+
function isCollectObject(node) {
|
|
613
|
+
if (node.type !== 'object') {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
return (node.name === 'Map' ||
|
|
617
|
+
node.name === 'Set' ||
|
|
618
|
+
node.name === 'WeakMap' ||
|
|
619
|
+
node.name === 'WeakSet');
|
|
620
|
+
}
|
|
621
|
+
function getCollectionFanout(node) {
|
|
622
|
+
if (node.type !== 'object') {
|
|
623
|
+
return 0;
|
|
624
|
+
}
|
|
625
|
+
if (node.name === 'Array') {
|
|
626
|
+
return node.edge_count;
|
|
627
|
+
}
|
|
628
|
+
else if (node.name === 'Map' ||
|
|
629
|
+
node.name === 'Set' ||
|
|
630
|
+
node.name === 'WeakMap' ||
|
|
631
|
+
node.name === 'WeakSet') {
|
|
632
|
+
const table = node.getReferenceNode('table');
|
|
633
|
+
if (table) {
|
|
634
|
+
return table.edge_count;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return 0;
|
|
638
|
+
}
|
|
607
639
|
exports.default = {
|
|
608
640
|
aggregateDominatorMetrics,
|
|
641
|
+
calculateRetainedSizes,
|
|
609
642
|
defaultAnalysisArgs,
|
|
610
643
|
filterOutLargestObjects,
|
|
644
|
+
getCollectionFanout,
|
|
611
645
|
getDominatorNodes,
|
|
612
646
|
getObjectOutgoingEdgeCount,
|
|
613
647
|
getSnapshotDirForAnalysis,
|
|
614
648
|
getSnapshotFileForAnalysis,
|
|
649
|
+
isCollectObject,
|
|
615
650
|
isNodeWorthInspecting,
|
|
616
651
|
loadHeapSnapshot,
|
|
617
652
|
getHeapFromFile,
|
|
@@ -0,0 +1,26 @@
|
|
|
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 ws_labs
|
|
9
|
+
*/
|
|
10
|
+
import type { HeapAnalysisOptions } from '../PluginUtils';
|
|
11
|
+
import type { BaseOption } from '@memlab/core';
|
|
12
|
+
import BaseAnalysis from '../BaseAnalysis';
|
|
13
|
+
declare class CollectionUnboundGrowthAnalysis extends BaseAnalysis {
|
|
14
|
+
getCommandName(): string;
|
|
15
|
+
/** @internal */
|
|
16
|
+
getDescription(): string;
|
|
17
|
+
/** @internal */
|
|
18
|
+
getOptions(): BaseOption[];
|
|
19
|
+
/** @internal */
|
|
20
|
+
analyzeSnapshotFromFile(file: string): Promise<void>;
|
|
21
|
+
/** @internal */
|
|
22
|
+
process(options: HeapAnalysisOptions): Promise<void>;
|
|
23
|
+
private checkUnboundCollection;
|
|
24
|
+
}
|
|
25
|
+
export default CollectionUnboundGrowthAnalysis;
|
|
26
|
+
//# sourceMappingURL=CollectionUnboundGrowthAnalysis.d.ts.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*
|
|
8
|
+
* @format
|
|
9
|
+
* @oncall ws_labs
|
|
10
|
+
*/
|
|
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 core_1 = require("@memlab/core");
|
|
26
|
+
const BaseAnalysis_1 = __importDefault(require("../BaseAnalysis"));
|
|
27
|
+
const HeapAnalysisSnapshotDirectoryOption_1 = __importDefault(require("../options/HeapAnalysisSnapshotDirectoryOption"));
|
|
28
|
+
const PluginUtils_1 = __importDefault(require("../PluginUtils"));
|
|
29
|
+
const __1 = require("..");
|
|
30
|
+
class CollectionUnboundGrowthAnalysis extends BaseAnalysis_1.default {
|
|
31
|
+
getCommandName() {
|
|
32
|
+
return 'unbound-collection';
|
|
33
|
+
}
|
|
34
|
+
/** @internal */
|
|
35
|
+
getDescription() {
|
|
36
|
+
return ('Check unbound collection growth ' +
|
|
37
|
+
'(e.g., Map with growing number of entries)');
|
|
38
|
+
}
|
|
39
|
+
/** @internal */
|
|
40
|
+
getOptions() {
|
|
41
|
+
return [new HeapAnalysisSnapshotDirectoryOption_1.default()];
|
|
42
|
+
}
|
|
43
|
+
/** @internal */
|
|
44
|
+
analyzeSnapshotFromFile(file) {
|
|
45
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
47
|
+
const f = file;
|
|
48
|
+
throw core_1.utils.haltOrThrow(`${this.constructor.name} does not support analyzeSnapshotFromFile`);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** @internal */
|
|
52
|
+
process(options) {
|
|
53
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
54
|
+
const snapshotDir = PluginUtils_1.default.getSnapshotDirForAnalysis(options);
|
|
55
|
+
const opt = snapshotDir ? { minSnapshots: 2, snapshotDir } : {};
|
|
56
|
+
core_1.config.chaseWeakMapEdge = false;
|
|
57
|
+
core_1.analysis.visualizeMemoryUsage(opt);
|
|
58
|
+
core_1.utils.checkSnapshots(opt);
|
|
59
|
+
yield this.checkUnboundCollection(opt);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
checkUnboundCollection(options) {
|
|
63
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
64
|
+
const nodeInfo = Object.create(null);
|
|
65
|
+
let hasCheckedFirstSnapshot = false;
|
|
66
|
+
let snapshot = null;
|
|
67
|
+
const initNodeInfo = (node) => {
|
|
68
|
+
if (!__1.PluginUtils.isCollectObject(node)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const n = __1.PluginUtils.getCollectionFanout(node);
|
|
72
|
+
nodeInfo[node.id] = {
|
|
73
|
+
type: node.type,
|
|
74
|
+
name: node.name,
|
|
75
|
+
min: n,
|
|
76
|
+
max: n,
|
|
77
|
+
history: [n],
|
|
78
|
+
node,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
const updateNodeInfo = (node) => {
|
|
82
|
+
const item = nodeInfo[node.id];
|
|
83
|
+
if (!item) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (node.name !== item.name || node.type !== item.type) {
|
|
87
|
+
nodeInfo[node.id] = null;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const n = __1.PluginUtils.getCollectionFanout(node);
|
|
91
|
+
// only monotonic increase?
|
|
92
|
+
if (core_1.config.monotonicUnboundGrowthOnly && n < item.max) {
|
|
93
|
+
nodeInfo[node.id] = null;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
item.history.push(n);
|
|
97
|
+
item.max = Math.max(item.max, n);
|
|
98
|
+
item.min = Math.min(item.min, n);
|
|
99
|
+
};
|
|
100
|
+
core_1.info.overwrite('Checking unbounded collections...');
|
|
101
|
+
const snapshotFiles = options.snapshotDir
|
|
102
|
+
? // load snapshots from a directory
|
|
103
|
+
core_1.utils.getSnapshotFilesInDir(options.snapshotDir)
|
|
104
|
+
: // load snapshots based on the visit sequence meta data
|
|
105
|
+
core_1.utils.getSnapshotFilesFromTabsOrder();
|
|
106
|
+
for (const file of snapshotFiles) {
|
|
107
|
+
// force GC before loading each snapshot
|
|
108
|
+
if (global.gc) {
|
|
109
|
+
global.gc();
|
|
110
|
+
}
|
|
111
|
+
// load and preprocess heap snapshot
|
|
112
|
+
const opt = { buildNodeIdIndex: true, verbose: true };
|
|
113
|
+
snapshot = yield core_1.utils.getSnapshotFromFile(file, opt);
|
|
114
|
+
__1.PluginUtils.calculateRetainedSizes(snapshot);
|
|
115
|
+
// keep track of heap objects
|
|
116
|
+
if (!hasCheckedFirstSnapshot) {
|
|
117
|
+
// record Ids in the snapshot
|
|
118
|
+
snapshot.nodes.forEach(initNodeInfo);
|
|
119
|
+
hasCheckedFirstSnapshot = true;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
snapshot.nodes.forEach(updateNodeInfo);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// exit if no heap snapshot found
|
|
126
|
+
if (!hasCheckedFirstSnapshot) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// post process and print the unbounded objects
|
|
130
|
+
const idsInLastSnapshot = new Set();
|
|
131
|
+
snapshot === null || snapshot === void 0 ? void 0 : snapshot.nodes.forEach(node => {
|
|
132
|
+
idsInLastSnapshot.add(node.id);
|
|
133
|
+
});
|
|
134
|
+
let ids = [];
|
|
135
|
+
for (const key in nodeInfo) {
|
|
136
|
+
const id = parseInt(key, 10);
|
|
137
|
+
const item = nodeInfo[id];
|
|
138
|
+
if (!item) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (!idsInLastSnapshot.has(id)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (item.min === item.max) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
ids.push(Object.assign({ id }, item));
|
|
148
|
+
}
|
|
149
|
+
if (ids.length === 0) {
|
|
150
|
+
core_1.info.midLevel('No increasing collections found.');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
ids = ids
|
|
154
|
+
.sort((o1, o2) => o2.history[o2.history.length - 1] - o1.history[o1.history.length - 1])
|
|
155
|
+
.slice(0, 20);
|
|
156
|
+
const formatter = (n) => `${n}`;
|
|
157
|
+
ids.forEach(item => (item.historyNumberFormatter = formatter));
|
|
158
|
+
// print on terminal
|
|
159
|
+
const str = core_1.serializer.summarizeUnboundedObjects(ids, { color: true });
|
|
160
|
+
core_1.info.topLevel('Top growing objects in sizes:');
|
|
161
|
+
core_1.info.lowLevel(' (Use `memlab trace --node-id=@ID` to get trace)');
|
|
162
|
+
core_1.info.topLevel('\n' + str);
|
|
163
|
+
// save results to file
|
|
164
|
+
const csv = core_1.serializer.summarizeUnboundedObjectsToCSV(ids);
|
|
165
|
+
fs_1.default.writeFileSync(core_1.config.unboundObjectCSV, csv, 'UTF-8');
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
exports.default = CollectionUnboundGrowthAnalysis;
|
|
@@ -47,6 +47,7 @@ class GlobalVariableAnalysis extends BaseAnalysis_1.default {
|
|
|
47
47
|
const node = snapshot.getNodeById(nodeId);
|
|
48
48
|
if (!node) {
|
|
49
49
|
core_1.info.error(`Object @${nodeId} is not found.`);
|
|
50
|
+
core_1.info.lowLevel(`Specify an object by --node-id`);
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
53
|
// print object info
|
|
@@ -31,7 +31,8 @@ class ObjectUnboundGrowthAnalysis extends BaseAnalysis_1.default {
|
|
|
31
31
|
}
|
|
32
32
|
/** @internal */
|
|
33
33
|
getDescription() {
|
|
34
|
-
return 'Check unbound object growth'
|
|
34
|
+
return ('Check unbound object growth ' +
|
|
35
|
+
'(a single object with growing retained size)');
|
|
35
36
|
}
|
|
36
37
|
/** @internal */
|
|
37
38
|
getOptions() {
|
|
@@ -37,7 +37,8 @@ class ShapeUnboundGrowthAnalysis extends BaseAnalysis_1.default {
|
|
|
37
37
|
}
|
|
38
38
|
/** @internal */
|
|
39
39
|
getDescription() {
|
|
40
|
-
return 'Get shapes with unbound growth'
|
|
40
|
+
return ('Get shapes with unbound growth ' +
|
|
41
|
+
'(a class of objects with growing aggregated retained size)');
|
|
41
42
|
}
|
|
42
43
|
/** @internal */
|
|
43
44
|
getOptions() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memlab/heap-analysis",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "heap analysis plugins for memlab",
|
|
6
6
|
"author": "Liang Gong <lgong@fb.com>",
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
"main": "dist/index.js",
|
|
17
17
|
"types": "dist/index.d.ts",
|
|
18
18
|
"files": [
|
|
19
|
-
"dist"
|
|
19
|
+
"dist",
|
|
20
|
+
"LICENSE"
|
|
20
21
|
],
|
|
21
22
|
"dependencies": {
|
|
22
|
-
"@memlab/core": "^1.1.
|
|
23
|
-
"@memlab/e2e": "^1.0.
|
|
23
|
+
"@memlab/core": "^1.1.10",
|
|
24
|
+
"@memlab/e2e": "^1.0.11",
|
|
24
25
|
"ansi": "^0.3.1",
|
|
25
26
|
"babar": "^0.2.0",
|
|
26
27
|
"chalk": "^4.0.0",
|