@memlab/heap-analysis 1.0.24 → 1.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/HeapAnalysisLoader.d.ts +7 -1
- package/dist/HeapAnalysisLoader.d.ts.map +1 -1
- package/dist/HeapAnalysisLoader.js +36 -14
- package/dist/plugins/ReactComponentHookAnalysis.d.ts +65 -0
- package/dist/plugins/ReactComponentHookAnalysis.d.ts.map +1 -0
- package/dist/plugins/ReactComponentHookAnalysis.js +310 -0
- package/package.json +3 -3
|
@@ -7,12 +7,18 @@
|
|
|
7
7
|
* @format
|
|
8
8
|
* @oncall web_perf_infra
|
|
9
9
|
*/
|
|
10
|
+
import type { Optional } from '@memlab/core';
|
|
10
11
|
import type BaseAnalysis from './BaseAnalysis';
|
|
12
|
+
export type HeapAnalysisLoaderOptions = {
|
|
13
|
+
heapAnalysisPlugin?: Optional<string>;
|
|
14
|
+
errorWhenPluginFailed?: boolean;
|
|
15
|
+
};
|
|
11
16
|
declare class HeapAnalysisLoader {
|
|
12
17
|
private modules;
|
|
13
|
-
loadAllAnalysis(): Map<string, BaseAnalysis>;
|
|
18
|
+
loadAllAnalysis(options?: HeapAnalysisLoaderOptions): Map<string, BaseAnalysis>;
|
|
14
19
|
private registerAnalyses;
|
|
15
20
|
private registerAnalysesFromDir;
|
|
21
|
+
private registerAnalysisFromFile;
|
|
16
22
|
}
|
|
17
23
|
declare const _default: HeapAnalysisLoader;
|
|
18
24
|
export default _default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HeapAnalysisLoader.d.ts","sourceRoot":"","sources":["../src/HeapAnalysisLoader.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"HeapAnalysisLoader.d.ts","sourceRoot":"","sources":["../src/HeapAnalysisLoader.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,cAAc,CAAC;AAK3C,OAAO,KAAK,YAAY,MAAM,gBAAgB,CAAC;AAE/C,MAAM,MAAM,yBAAyB,GAAG;IACtC,kBAAkB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACtC,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,cAAM,kBAAkB;IACtB,OAAO,CAAC,OAAO,CAAwC;IAEhD,eAAe,CACpB,OAAO,GAAE,yBAA8B,GACtC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC;IAS5B,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,uBAAuB;IAW/B,OAAO,CAAC,wBAAwB;CAyCjC;;AAED,wBAAwC"}
|
|
@@ -19,36 +19,58 @@ class HeapAnalysisLoader {
|
|
|
19
19
|
constructor() {
|
|
20
20
|
this.modules = new Map();
|
|
21
21
|
}
|
|
22
|
-
loadAllAnalysis() {
|
|
22
|
+
loadAllAnalysis(options = {}) {
|
|
23
23
|
if (this.modules.size === 0) {
|
|
24
24
|
// auto load all analysis modules
|
|
25
25
|
this.modules = new Map();
|
|
26
|
-
this.registerAnalyses();
|
|
26
|
+
this.registerAnalyses(options);
|
|
27
27
|
}
|
|
28
28
|
return this.modules;
|
|
29
29
|
}
|
|
30
|
-
registerAnalyses() {
|
|
30
|
+
registerAnalyses(options = {}) {
|
|
31
31
|
const modulesDir = path_1.default.resolve(__dirname, 'plugins');
|
|
32
32
|
this.registerAnalysesFromDir(modulesDir);
|
|
33
|
+
// register external analysis
|
|
34
|
+
if (options.heapAnalysisPlugin != null) {
|
|
35
|
+
const file = path_1.default.resolve(options.heapAnalysisPlugin);
|
|
36
|
+
this.registerAnalysisFromFile(file, options);
|
|
37
|
+
}
|
|
33
38
|
}
|
|
34
|
-
registerAnalysesFromDir(modulesDir) {
|
|
39
|
+
registerAnalysesFromDir(modulesDir, options = {}) {
|
|
35
40
|
const moduleFiles = fs_1.default.readdirSync(modulesDir);
|
|
36
41
|
for (const moduleFile of moduleFiles) {
|
|
37
42
|
const modulePath = path_1.default.join(modulesDir, moduleFile);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
this.registerAnalysisFromFile(modulePath, options);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
registerAnalysisFromFile(modulePath, options = {}) {
|
|
47
|
+
// recursively import modules from subdirectories
|
|
48
|
+
if (fs_1.default.lstatSync(modulePath).isDirectory()) {
|
|
49
|
+
this.registerAnalysesFromDir(modulePath, options);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// only import modules files ends with with Analysis.js
|
|
53
|
+
if (!modulePath.endsWith('Analysis.js')) {
|
|
54
|
+
if (options.errorWhenPluginFailed) {
|
|
55
|
+
const fileName = path_1.default.basename(modulePath);
|
|
56
|
+
throw core_1.utils.haltOrThrow(`Analysis plugin file (${fileName}) must end with \`Analysis.js\``);
|
|
46
57
|
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
let commandName = null;
|
|
61
|
+
let moduleInstance = null;
|
|
62
|
+
try {
|
|
47
63
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
48
64
|
const module = require(modulePath);
|
|
49
65
|
const moduleConstructor = typeof module.default === 'function' ? module.default : module;
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
moduleInstance = new moduleConstructor();
|
|
67
|
+
commandName = moduleInstance.getCommandName();
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
core_1.info.error('Failed to load analysis plugin: ' + modulePath);
|
|
71
|
+
throw core_1.utils.haltOrThrow(core_1.utils.getError(err));
|
|
72
|
+
}
|
|
73
|
+
if (commandName != null) {
|
|
52
74
|
if (this.modules.has(commandName)) {
|
|
53
75
|
core_1.utils.haltOrThrow(`heap command ${commandName} is already registered`);
|
|
54
76
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
* @oncall web_perf_infra
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* A heap analysis calculating the memory breakdown of React
|
|
12
|
+
* components and their React hooks.
|
|
13
|
+
*
|
|
14
|
+
* The idea of this heap analysis comes from the tech talk by Giulio Zausa in
|
|
15
|
+
* React Berlin Day 2023. For more context and overview about how the analysis
|
|
16
|
+
* works, please check out the talk here:
|
|
17
|
+
* https://portal.gitnation.org/contents/how-much-ram-is-your-usememo-using-lets-profile-it
|
|
18
|
+
*/
|
|
19
|
+
import type { AnalyzeSnapshotResult, HeapAnalysisOptions } from '../PluginUtils';
|
|
20
|
+
import type { BaseOption } from '@memlab/core';
|
|
21
|
+
import BaseAnalysis from '../BaseAnalysis';
|
|
22
|
+
declare class ReactComponentHookAnalysis extends BaseAnalysis {
|
|
23
|
+
private isHeapSnapshotMinified;
|
|
24
|
+
private fiberNodeName;
|
|
25
|
+
getCommandName(): string;
|
|
26
|
+
/** @internal */
|
|
27
|
+
getDescription(): string;
|
|
28
|
+
/** @internal */
|
|
29
|
+
getOptions(): BaseOption[];
|
|
30
|
+
/** @internal */
|
|
31
|
+
analyzeSnapshotsInDirectory(directory: string): Promise<AnalyzeSnapshotResult>;
|
|
32
|
+
/** @internal */
|
|
33
|
+
process(options: HeapAnalysisOptions): Promise<void>;
|
|
34
|
+
/** @internal */
|
|
35
|
+
breakDownMemoryByReactComponents(options?: {
|
|
36
|
+
file?: string;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
/** @internal */
|
|
39
|
+
private walkHookChain;
|
|
40
|
+
/**
|
|
41
|
+
* This methods get readable React component name corresponds to
|
|
42
|
+
* a specific FiberNode object.
|
|
43
|
+
* @internal
|
|
44
|
+
**/
|
|
45
|
+
private getComponentNameFromFiberNode;
|
|
46
|
+
/**
|
|
47
|
+
* Detects Fiber nodes in the heap snaphot and returns the string name
|
|
48
|
+
* representation for the FiberNode objects.
|
|
49
|
+
* For unminified heap snapshot, this method returns 'FiberNode'.
|
|
50
|
+
* For minified heap snapshot, this method returns the FiberNode object's
|
|
51
|
+
* minified name.
|
|
52
|
+
* @internal
|
|
53
|
+
**/
|
|
54
|
+
private probeHeapAndFiberInfo;
|
|
55
|
+
/** @internal */
|
|
56
|
+
private hasFiberNodeAttributes;
|
|
57
|
+
/** @internal */
|
|
58
|
+
private breakDownSnapshotByReactComponents;
|
|
59
|
+
/** @internal */
|
|
60
|
+
private printHeapInfo;
|
|
61
|
+
/** @internal */
|
|
62
|
+
private printReactComponentStats;
|
|
63
|
+
}
|
|
64
|
+
export default ReactComponentHookAnalysis;
|
|
65
|
+
//# sourceMappingURL=ReactComponentHookAnalysis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReactComponentHookAnalysis.d.ts","sourceRoot":"","sources":["../../src/plugins/ReactComponentHookAnalysis.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,qBAAqB,EAAE,mBAAmB,EAAC,MAAM,gBAAgB,CAAC;AAC/E,OAAO,KAAK,EACV,UAAU,EAMX,MAAM,cAAc,CAAC;AAItB,OAAO,YAAY,MAAM,iBAAiB,CAAC;AAyC3C,cAAM,0BAA2B,SAAQ,YAAY;IACnD,OAAO,CAAC,sBAAsB,CAAS;IACvC,OAAO,CAAC,aAAa,CAA0B;IAE/C,cAAc,IAAI,MAAM;IAIxB,gBAAgB;IAChB,cAAc,IAAI,MAAM;IASxB,gBAAgB;IAChB,UAAU,IAAI,UAAU,EAAE;IAI1B,gBAAgB;IACH,2BAA2B,CACtC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,qBAAqB,CAAC;IAQjC,gBAAgB;IACV,OAAO,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAK1D,gBAAgB;IACV,gCAAgC,CAAC,OAAO,GAAE;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAM;IAkBpE,gBAAgB;IAChB,OAAO,CAAC,aAAa;IAmBrB;;;;QAII;IACJ,OAAO,CAAC,6BAA6B;IAgDrC;;;;;;;QAOI;IACJ,OAAO,CAAC,qBAAqB;IA4B7B,gBAAgB;IAChB,OAAO,CAAC,sBAAsB;IAS9B,gBAAgB;IAChB,OAAO,CAAC,kCAAkC;IAmG1C,gBAAgB;IAChB,OAAO,CAAC,aAAa;IA4BrB,gBAAgB;IAChB,OAAO,CAAC,wBAAwB;CAkFjC;AAED,eAAe,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,310 @@
|
|
|
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 web_perf_infra
|
|
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 chalk_1 = __importDefault(require("chalk"));
|
|
25
|
+
const core_1 = require("@memlab/core");
|
|
26
|
+
const BaseAnalysis_1 = __importDefault(require("../BaseAnalysis"));
|
|
27
|
+
const PluginUtils_1 = __importDefault(require("../PluginUtils"));
|
|
28
|
+
const HeapAnalysisSnapshotFileOption_1 = __importDefault(require("../options/HeapAnalysisSnapshotFileOption"));
|
|
29
|
+
const SIZE_TO_PRINT = 10;
|
|
30
|
+
const FIBER_NODE_PROPERTIES = new Set([
|
|
31
|
+
'alternate',
|
|
32
|
+
'child',
|
|
33
|
+
'memoizedProps',
|
|
34
|
+
'memoizedState',
|
|
35
|
+
'return',
|
|
36
|
+
'sibling',
|
|
37
|
+
'type',
|
|
38
|
+
]);
|
|
39
|
+
function getProperty(node, prop) {
|
|
40
|
+
var _a;
|
|
41
|
+
return (_a = node.references.find(ref => ref.name_or_index === prop)) === null || _a === void 0 ? void 0 : _a.toNode;
|
|
42
|
+
}
|
|
43
|
+
class ReactComponentHookAnalysis extends BaseAnalysis_1.default {
|
|
44
|
+
constructor() {
|
|
45
|
+
super(...arguments);
|
|
46
|
+
this.isHeapSnapshotMinified = false;
|
|
47
|
+
this.fiberNodeName = null;
|
|
48
|
+
}
|
|
49
|
+
getCommandName() {
|
|
50
|
+
return 'react-hooks';
|
|
51
|
+
}
|
|
52
|
+
/** @internal */
|
|
53
|
+
getDescription() {
|
|
54
|
+
return ('Show a memory breakdown of the most memory-consuming React components ' +
|
|
55
|
+
'and their React hooks. This works best with unminified heap snapshots ' +
|
|
56
|
+
'taken from React apps running in Dev mode. But also supports minified ' +
|
|
57
|
+
'heap snapshots taken from React apps in production mode.');
|
|
58
|
+
}
|
|
59
|
+
/** @internal */
|
|
60
|
+
getOptions() {
|
|
61
|
+
return [new HeapAnalysisSnapshotFileOption_1.default()];
|
|
62
|
+
}
|
|
63
|
+
/** @internal */
|
|
64
|
+
analyzeSnapshotsInDirectory(directory) {
|
|
65
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
67
|
+
const d = directory;
|
|
68
|
+
throw core_1.utils.haltOrThrow(`${this.constructor.name} does not support analyzeSnapshotsInDirectory`);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** @internal */
|
|
72
|
+
process(options) {
|
|
73
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
74
|
+
const snapshotPath = PluginUtils_1.default.getSnapshotFileForAnalysis(options);
|
|
75
|
+
yield this.breakDownMemoryByReactComponents({ file: snapshotPath });
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** @internal */
|
|
79
|
+
breakDownMemoryByReactComponents(options = {}) {
|
|
80
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
81
|
+
const opt = { buildNodeIdIndex: true, verbose: true };
|
|
82
|
+
const file = options.file ||
|
|
83
|
+
core_1.utils.getSnapshotFilePathWithTabType(/.*/) ||
|
|
84
|
+
'<EMPTY_FILE_PATH>';
|
|
85
|
+
const snapshot = yield core_1.utils.getSnapshotFromFile(file, opt);
|
|
86
|
+
core_1.analysis.preparePathFinder(snapshot);
|
|
87
|
+
this.probeHeapAndFiberInfo(snapshot);
|
|
88
|
+
const heapInfo = core_1.analysis.getOverallHeapInfo(snapshot, { force: true });
|
|
89
|
+
if (heapInfo && !this.isHeapSnapshotMinified) {
|
|
90
|
+
this.printHeapInfo(heapInfo);
|
|
91
|
+
core_1.info.topLevel('\n');
|
|
92
|
+
}
|
|
93
|
+
const componentStatsMap = this.breakDownSnapshotByReactComponents(snapshot);
|
|
94
|
+
this.printReactComponentStats(componentStatsMap);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** @internal */
|
|
98
|
+
walkHookChain(memoizedStateNode, types, i) {
|
|
99
|
+
var _a, _b;
|
|
100
|
+
if (memoizedStateNode == null) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
const nextNode = getProperty(memoizedStateNode, 'next');
|
|
104
|
+
return [
|
|
105
|
+
{
|
|
106
|
+
type: (_a = types[i]) !== null && _a !== void 0 ? _a : 'unknown hook - React Dev mode only',
|
|
107
|
+
size: memoizedStateNode.retainedSize - ((_b = nextNode === null || nextNode === void 0 ? void 0 : nextNode.retainedSize) !== null && _b !== void 0 ? _b : 0),
|
|
108
|
+
},
|
|
109
|
+
...this.walkHookChain(nextNode, types, i + 1),
|
|
110
|
+
];
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* This methods get readable React component name corresponds to
|
|
115
|
+
* a specific FiberNode object.
|
|
116
|
+
* @internal
|
|
117
|
+
**/
|
|
118
|
+
getComponentNameFromFiberNode(node, fiberNodeObjectName) {
|
|
119
|
+
var _a, _b, _c, _d;
|
|
120
|
+
if (node.name !== fiberNodeObjectName) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const componentName = null;
|
|
124
|
+
// get fiberNode.type
|
|
125
|
+
const typeNode = getProperty(node, 'type');
|
|
126
|
+
if (typeNode == null) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
// if fiberNode.type itself is a string
|
|
130
|
+
if (typeNode.isString) {
|
|
131
|
+
return (_a = typeNode.toStringNode()) === null || _a === void 0 ? void 0 : _a.stringValue;
|
|
132
|
+
}
|
|
133
|
+
// try to get component name from fiberNode.type.__debugModuleSource
|
|
134
|
+
const debugModuleName = getProperty(typeNode, '__debugModuleSource');
|
|
135
|
+
if (debugModuleName === null || debugModuleName === void 0 ? void 0 : debugModuleName.isString) {
|
|
136
|
+
return (_b = debugModuleName.toStringNode()) === null || _b === void 0 ? void 0 : _b.stringValue;
|
|
137
|
+
}
|
|
138
|
+
// try to get component name from fiberNode.type.displayName
|
|
139
|
+
const displayNameNode = getProperty(typeNode, 'displayName');
|
|
140
|
+
if (displayNameNode != null) {
|
|
141
|
+
let componentName = (_c = displayNameNode.toStringNode()) === null || _c === void 0 ? void 0 : _c.stringValue;
|
|
142
|
+
// if the heap snapshot is minified replace
|
|
143
|
+
// "a [from parentComponent.react]" with
|
|
144
|
+
// "<minified component> from [from parentComponent.react]"
|
|
145
|
+
if (this.isHeapSnapshotMinified && (componentName === null || componentName === void 0 ? void 0 : componentName.includes('['))) {
|
|
146
|
+
componentName = componentName === null || componentName === void 0 ? void 0 : componentName.replace(/^[^[]*/, '<minified component> ');
|
|
147
|
+
}
|
|
148
|
+
return componentName;
|
|
149
|
+
}
|
|
150
|
+
else if (componentName === 'Object') {
|
|
151
|
+
const typeofNodeId = (_d = getProperty(typeNode, '$$typeof')) === null || _d === void 0 ? void 0 : _d.id;
|
|
152
|
+
return `Component (@${typeofNodeId})`;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Detects Fiber nodes in the heap snaphot and returns the string name
|
|
158
|
+
* representation for the FiberNode objects.
|
|
159
|
+
* For unminified heap snapshot, this method returns 'FiberNode'.
|
|
160
|
+
* For minified heap snapshot, this method returns the FiberNode object's
|
|
161
|
+
* minified name.
|
|
162
|
+
* @internal
|
|
163
|
+
**/
|
|
164
|
+
probeHeapAndFiberInfo(snapshot) {
|
|
165
|
+
let foundFiberNodeWithUnminifiedName = false;
|
|
166
|
+
const likelyFiberNodes = new Map();
|
|
167
|
+
snapshot.nodes.forEach((node) => {
|
|
168
|
+
var _a;
|
|
169
|
+
if (node.name === 'FiberNode' && node.isString === false) {
|
|
170
|
+
foundFiberNodeWithUnminifiedName = true;
|
|
171
|
+
}
|
|
172
|
+
else if (this.hasFiberNodeAttributes(node)) {
|
|
173
|
+
likelyFiberNodes.set(node.name, ((_a = likelyFiberNodes.get(node.name)) !== null && _a !== void 0 ? _a : 0) + 1);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
if (foundFiberNodeWithUnminifiedName) {
|
|
177
|
+
this.fiberNodeName = 'FiberNode';
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const entries = Array.from(likelyFiberNodes.entries()).sort((e1, e2) => e2[1] - e1[1]);
|
|
181
|
+
if (entries.length === 0) {
|
|
182
|
+
this.fiberNodeName = null;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this.isHeapSnapshotMinified = true;
|
|
186
|
+
this.fiberNodeName = entries[0][0];
|
|
187
|
+
}
|
|
188
|
+
/** @internal */
|
|
189
|
+
hasFiberNodeAttributes(node) {
|
|
190
|
+
for (const prop of FIBER_NODE_PROPERTIES) {
|
|
191
|
+
if (!node.findAnyReference(ref => ref.name_or_index === prop)) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
/** @internal */
|
|
198
|
+
breakDownSnapshotByReactComponents(snapshot) {
|
|
199
|
+
core_1.info.overwrite('Breaking down memory by React components...');
|
|
200
|
+
const componentMemMap = new Map();
|
|
201
|
+
const fiberNodeName = this.fiberNodeName;
|
|
202
|
+
if (fiberNodeName == null) {
|
|
203
|
+
throw core_1.utils.haltOrThrow('No FiberNode detected in the heap snapshot.');
|
|
204
|
+
}
|
|
205
|
+
snapshot.nodes.forEach((node) => {
|
|
206
|
+
var _a;
|
|
207
|
+
const componentName = this.getComponentNameFromFiberNode(node, fiberNodeName);
|
|
208
|
+
if (componentName == null) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const record = (_a = componentMemMap.get(componentName)) !== null && _a !== void 0 ? _a : {
|
|
212
|
+
fiberNodeIds: [],
|
|
213
|
+
totalRetainedSize: 0,
|
|
214
|
+
totalShallowSize: 0,
|
|
215
|
+
memoizedStateIds: [],
|
|
216
|
+
totalMemoizedStateRetainedSize: 0,
|
|
217
|
+
hooks: [],
|
|
218
|
+
memoizedPropsIds: [],
|
|
219
|
+
totalMemoizedPropsRetainedSize: 0,
|
|
220
|
+
children: 0,
|
|
221
|
+
sibling: 0,
|
|
222
|
+
};
|
|
223
|
+
componentMemMap.set(componentName, record);
|
|
224
|
+
record.fiberNodeIds.push(node.id);
|
|
225
|
+
record.totalShallowSize += node.self_size;
|
|
226
|
+
const debugHookTypesNode = getProperty(node, '_debugHookTypes');
|
|
227
|
+
const types = [];
|
|
228
|
+
if (debugHookTypesNode) {
|
|
229
|
+
for (let index = 0; index < 1000; ++index) {
|
|
230
|
+
const element = getProperty(debugHookTypesNode, index);
|
|
231
|
+
if (element == null) {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
types.push(element.name);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const memoizedStateNode = getProperty(node, 'memoizedState');
|
|
238
|
+
if (memoizedStateNode != null) {
|
|
239
|
+
record.memoizedStateIds.push(memoizedStateNode.id);
|
|
240
|
+
record.hooks = this.walkHookChain(memoizedStateNode, types, 0);
|
|
241
|
+
}
|
|
242
|
+
const memoizedPropsNode = getProperty(node, 'memoizedProps');
|
|
243
|
+
if (memoizedPropsNode != null) {
|
|
244
|
+
record.memoizedPropsIds.push(memoizedPropsNode.id);
|
|
245
|
+
}
|
|
246
|
+
const childrenNode = getProperty(node, 'child');
|
|
247
|
+
if (childrenNode != null) {
|
|
248
|
+
record.children += childrenNode.retainedSize;
|
|
249
|
+
}
|
|
250
|
+
const siblingNode = getProperty(node, 'sibling');
|
|
251
|
+
if (siblingNode != null) {
|
|
252
|
+
record.sibling += siblingNode.retainedSize;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
// aggregate and calculate the retained sizes
|
|
256
|
+
for (const [, record] of componentMemMap) {
|
|
257
|
+
record.totalRetainedSize = core_1.utils.aggregateDominatorMetrics(new Set(record.fiberNodeIds), snapshot, () => true, core_1.utils.getRetainedSize);
|
|
258
|
+
record.totalMemoizedStateRetainedSize = core_1.utils.aggregateDominatorMetrics(new Set(record.memoizedStateIds), snapshot, () => true, core_1.utils.getRetainedSize);
|
|
259
|
+
record.totalMemoizedPropsRetainedSize = core_1.utils.aggregateDominatorMetrics(new Set(record.memoizedPropsIds), snapshot, () => true, core_1.utils.getRetainedSize);
|
|
260
|
+
}
|
|
261
|
+
return componentMemMap;
|
|
262
|
+
}
|
|
263
|
+
/** @internal */
|
|
264
|
+
printHeapInfo(heapInfo) {
|
|
265
|
+
const key = chalk_1.default.white.bind(chalk_1.default);
|
|
266
|
+
const sep = chalk_1.default.grey.bind(chalk_1.default);
|
|
267
|
+
const size = (n) => chalk_1.default.yellow(core_1.utils.getReadableBytes(n));
|
|
268
|
+
core_1.info.topLevel('\nHeap Overall Statistics:');
|
|
269
|
+
core_1.info.topLevel(` ${key('Fiber node total retained size')}${sep(':')} ${size(heapInfo.fiberNodeSize)}`);
|
|
270
|
+
core_1.info.topLevel(` ${key('Rendered Fiber node retained size')}${sep(':')} ${size(heapInfo.regularFiberNodeSize)}`);
|
|
271
|
+
core_1.info.topLevel(` ${key('Alternate Fiber node retained size')}${sep(':')} ${size(heapInfo.alternateFiberNodeSize)}`);
|
|
272
|
+
core_1.info.topLevel(` ${key('Detached Fiber node retained size')}${sep(':')} ${size(heapInfo.detachedFiberNodeSize)}`);
|
|
273
|
+
}
|
|
274
|
+
/** @internal */
|
|
275
|
+
printReactComponentStats(componentStatsMap) {
|
|
276
|
+
const key = chalk_1.default.white.bind(chalk_1.default);
|
|
277
|
+
const sep = chalk_1.default.grey.bind(chalk_1.default);
|
|
278
|
+
const num = chalk_1.default.blue.bind(chalk_1.default);
|
|
279
|
+
const size = (n) => chalk_1.default.yellow(core_1.utils.getReadableBytes(n));
|
|
280
|
+
const entries = Array.from(componentStatsMap.entries()).sort((entry1, entry2) => {
|
|
281
|
+
return entry2[1].totalRetainedSize - entry1[1].totalRetainedSize;
|
|
282
|
+
});
|
|
283
|
+
core_1.info.topLevel(`Found ${entries.length} React component types. Top ${SIZE_TO_PRINT} results:\n`);
|
|
284
|
+
let numPrinted = 0;
|
|
285
|
+
for (const [name, stat] of entries) {
|
|
286
|
+
if (numPrinted++ >= SIZE_TO_PRINT) {
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
let indent = ' ';
|
|
290
|
+
core_1.info.topLevel(`${indent}${name}${sep(':')}`);
|
|
291
|
+
indent += ' ';
|
|
292
|
+
core_1.info.topLevel(`${indent}${key('Instances')}${sep(':')} ${num(stat.fiberNodeIds.length)}`);
|
|
293
|
+
core_1.info.topLevel(`${indent}${key('Total retained size')}${sep(':')} ${size(stat.totalRetainedSize)}`);
|
|
294
|
+
core_1.info.topLevel(`${indent}${key('Total shallow size')}${sep(':')} ${size(stat.totalShallowSize)}`);
|
|
295
|
+
core_1.info.topLevel(`${indent}${key('Children retained size')}${sep(':')} ${size(stat.children)}`);
|
|
296
|
+
core_1.info.topLevel(`${indent}${key('Sibling retained size')}${sep(':')} ${size(stat.sibling)}`);
|
|
297
|
+
core_1.info.topLevel(`${indent}${key('Total memoizedProps retained size')}${sep(':')} ${size(stat.totalMemoizedPropsRetainedSize)}`);
|
|
298
|
+
core_1.info.topLevel(`${indent}${key('Total memoizedState retained size')}${sep(':')} ${size(stat.totalMemoizedStateRetainedSize)}`);
|
|
299
|
+
const totalHookSize = stat.hooks.reduce((acc, cur) => acc + cur.size, 0);
|
|
300
|
+
core_1.info.topLevel(`${indent}${key('React hooks')}${sep(':')} (total size per component: ${size(totalHookSize)})`);
|
|
301
|
+
const hookStats = stat.hooks;
|
|
302
|
+
for (let i = 0; i < hookStats.length; ++i) {
|
|
303
|
+
const hookStat = hookStats[i];
|
|
304
|
+
core_1.info.topLevel(`${indent} [${num(i)}]${sep(':')} ${size(hookStat.size)} (${sep(hookStat.type)})`);
|
|
305
|
+
}
|
|
306
|
+
core_1.info.topLevel('');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
exports.default = ReactComponentHookAnalysis;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memlab/heap-analysis",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.25",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "heap analysis plugins for memlab",
|
|
6
6
|
"author": "Liang Gong <lgong@fb.com>",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"LICENSE"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@memlab/core": "^1.1.
|
|
24
|
-
"@memlab/e2e": "^1.0.
|
|
23
|
+
"@memlab/core": "^1.1.28",
|
|
24
|
+
"@memlab/e2e": "^1.0.28",
|
|
25
25
|
"ansi": "^0.3.1",
|
|
26
26
|
"babar": "^0.2.0",
|
|
27
27
|
"chalk": "^4.0.0",
|