@memlab/heap-analysis 1.0.14 → 1.0.15

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.
@@ -7,9 +7,8 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { AnyValue } from '@memlab/core';
11
- import type { HeapAnalysisOptions } from './PluginUtils';
12
- import { BaseOption } from '@memlab/core';
10
+ import type { AnyValue, BaseOption } from '@memlab/core';
11
+ import type { AnalyzeSnapshotResult, HeapAnalysisOptions } from './PluginUtils';
13
12
  declare abstract class Analysis {
14
13
  process(_options: HeapAnalysisOptions): Promise<void>;
15
14
  /**
@@ -27,33 +26,41 @@ declare abstract class Analysis {
27
26
  /**
28
27
  * Run heap analysis for a single heap snapshot file
29
28
  * @param file the absolute path of a `.heapsnapshot` file.
30
- * @returns this API returns void. To get the analysis results,
31
- * check out the documentation of the hosting heap analysis class and
32
- * call the analysis-specific API to get results after calling this method.
29
+ * @returns this API returns {@link AnalyzeSnapshotResult}, which contains
30
+ * the logging file of analysis console output. Alternatively, to get more
31
+ * structured analysis results, check out the documentation of the hosting
32
+ * heap analysis class and call the analysis-specific API to get results
33
+ * after calling this method.
33
34
  * * **Example**:
34
35
  * ```typescript
35
36
  * const analysis = new StringAnalysis();
36
- * await anaysis.analyzeSnapshotFromFile(snapshotFile);
37
+ * // analysis console output is saved in result.analysisOutputFile
38
+ * const result = await anaysis.analyzeSnapshotFromFile(snapshotFile);
39
+ * // query analysis-specific and structured results
37
40
  * const stringPatterns = analysis.getTopDuplicatedStringsInCount();
38
41
  * ```
39
42
  */
40
- analyzeSnapshotFromFile(file: string): Promise<void>;
43
+ analyzeSnapshotFromFile(file: string): Promise<AnalyzeSnapshotResult>;
41
44
  /**
42
45
  * Run heap analysis for a series of heap snapshot files
43
46
  * @param directory the absolute path of the directory holding a series of
44
47
  * `.heapsnapshot` files, all snapshot files will be loaded and analyzed
45
48
  * in the alphanumerically ascending order of those snapshot file names.
46
- * @returns this API returns void. To get the analysis results,
47
- * check out the documentation of the hosting heap analysis class and
48
- * call the analysis-specific API to get results after calling this method.
49
+ * @returns this API returns {@link AnalyzeSnapshotResult}, which contains
50
+ * the logging file of analysis console output. Alternatively, to get more
51
+ * structured analysis results, check out the documentation of the hosting
52
+ * heap analysis class and call the analysis-specific API to get results
53
+ * after calling this method.
49
54
  * * **Example**:
50
55
  * ```typescript
51
56
  * const analysis = new ShapeUnboundGrowthAnalysis();
52
- * await anaysis.analyzeSnapshotsInDirectory(snapshotDirectory);
57
+ * // analysis console output is saved in result.analysisOutputFile
58
+ * const result = await anaysis.analyzeSnapshotsInDirectory(snapshotDirectory);
59
+ * // query analysis-specific and structured results
53
60
  * const shapes = analysis.getShapesWithUnboundGrowth();
54
61
  * ```
55
62
  */
56
- analyzeSnapshotsInDirectory(directory: string): Promise<void>;
63
+ analyzeSnapshotsInDirectory(directory: string): Promise<AnalyzeSnapshotResult>;
57
64
  }
58
65
  /**
59
66
  *
@@ -68,25 +68,33 @@ class Analysis {
68
68
  /**
69
69
  * Run heap analysis for a single heap snapshot file
70
70
  * @param file the absolute path of a `.heapsnapshot` file.
71
- * @returns this API returns void. To get the analysis results,
72
- * check out the documentation of the hosting heap analysis class and
73
- * call the analysis-specific API to get results after calling this method.
71
+ * @returns this API returns {@link AnalyzeSnapshotResult}, which contains
72
+ * the logging file of analysis console output. Alternatively, to get more
73
+ * structured analysis results, check out the documentation of the hosting
74
+ * heap analysis class and call the analysis-specific API to get results
75
+ * after calling this method.
74
76
  * * **Example**:
75
77
  * ```typescript
76
78
  * const analysis = new StringAnalysis();
77
- * await anaysis.analyzeSnapshotFromFile(snapshotFile);
79
+ * // analysis console output is saved in result.analysisOutputFile
80
+ * const result = await anaysis.analyzeSnapshotFromFile(snapshotFile);
81
+ * // query analysis-specific and structured results
78
82
  * const stringPatterns = analysis.getTopDuplicatedStringsInCount();
79
83
  * ```
80
84
  */
81
85
  analyzeSnapshotFromFile(file) {
82
86
  return __awaiter(this, void 0, void 0, function* () {
83
- return this.process({
87
+ const analysisOutputFile = core_1.fileManager.initNewHeapAnalysisLogFile();
88
+ core_1.info.registerLogFile(analysisOutputFile);
89
+ yield this.process({
84
90
  args: {
85
91
  _: [],
86
92
  snapshot: file,
87
93
  'snapshot-dir': '<MUST_PROVIDE_SNAPSHOT_DIR>',
88
94
  },
89
95
  });
96
+ core_1.info.unregisterLogFile(analysisOutputFile);
97
+ return { analysisOutputFile };
90
98
  });
91
99
  }
92
100
  /**
@@ -94,25 +102,33 @@ class Analysis {
94
102
  * @param directory the absolute path of the directory holding a series of
95
103
  * `.heapsnapshot` files, all snapshot files will be loaded and analyzed
96
104
  * in the alphanumerically ascending order of those snapshot file names.
97
- * @returns this API returns void. To get the analysis results,
98
- * check out the documentation of the hosting heap analysis class and
99
- * call the analysis-specific API to get results after calling this method.
105
+ * @returns this API returns {@link AnalyzeSnapshotResult}, which contains
106
+ * the logging file of analysis console output. Alternatively, to get more
107
+ * structured analysis results, check out the documentation of the hosting
108
+ * heap analysis class and call the analysis-specific API to get results
109
+ * after calling this method.
100
110
  * * **Example**:
101
111
  * ```typescript
102
112
  * const analysis = new ShapeUnboundGrowthAnalysis();
103
- * await anaysis.analyzeSnapshotsInDirectory(snapshotDirectory);
113
+ * // analysis console output is saved in result.analysisOutputFile
114
+ * const result = await anaysis.analyzeSnapshotsInDirectory(snapshotDirectory);
115
+ * // query analysis-specific and structured results
104
116
  * const shapes = analysis.getShapesWithUnboundGrowth();
105
117
  * ```
106
118
  */
107
119
  analyzeSnapshotsInDirectory(directory) {
108
120
  return __awaiter(this, void 0, void 0, function* () {
109
- return this.process({
121
+ const analysisOutputFile = core_1.fileManager.initNewHeapAnalysisLogFile();
122
+ core_1.info.registerLogFile(analysisOutputFile);
123
+ yield this.process({
110
124
  args: {
111
125
  _: [],
112
126
  snapshot: '<MUST_PROVIDE_SNAPSHOT_FILE>',
113
127
  'snapshot-dir': directory,
114
128
  },
115
129
  });
130
+ core_1.info.unregisterLogFile(analysisOutputFile);
131
+ return { analysisOutputFile };
116
132
  });
117
133
  }
118
134
  }
@@ -25,6 +25,16 @@ export declare type HeapAnalysisOptions = {
25
25
  /** @internal */
26
26
  config?: MemLabConfig;
27
27
  };
28
+ /**
29
+ * This is the return type from calling {@link analyzeSnapshotFromFile}
30
+ * or {@link analyzeSnapshotsInDirectory}.
31
+ */
32
+ export declare type AnalyzeSnapshotResult = {
33
+ /**
34
+ * file path of the console output of the heap analysis call
35
+ */
36
+ analysisOutputFile: string;
37
+ };
28
38
  declare type PrintNodeOption = {
29
39
  indent?: string;
30
40
  printReferences?: boolean;
@@ -7,7 +7,7 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { HeapAnalysisOptions } from '../PluginUtils';
10
+ import type { AnalyzeSnapshotResult, HeapAnalysisOptions } from '../PluginUtils';
11
11
  import type { BaseOption } from '@memlab/core';
12
12
  import BaseAnalysis from '../BaseAnalysis';
13
13
  declare class CollectionUnboundGrowthAnalysis extends BaseAnalysis {
@@ -17,7 +17,7 @@ declare class CollectionUnboundGrowthAnalysis extends BaseAnalysis {
17
17
  /** @internal */
18
18
  getOptions(): BaseOption[];
19
19
  /** @internal */
20
- analyzeSnapshotFromFile(file: string): Promise<void>;
20
+ analyzeSnapshotFromFile(file: string): Promise<AnalyzeSnapshotResult>;
21
21
  /** @internal */
22
22
  process(options: HeapAnalysisOptions): Promise<void>;
23
23
  private checkUnboundCollection;
@@ -7,7 +7,7 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { HeapAnalysisOptions } from '../PluginUtils';
10
+ import type { AnalyzeSnapshotResult, HeapAnalysisOptions } from '../PluginUtils';
11
11
  import { BaseOption } from '@memlab/core';
12
12
  import BaseAnalysis from '../BaseAnalysis';
13
13
  export default class CollectionsHoldingStaleAnalysis extends BaseAnalysis {
@@ -17,7 +17,7 @@ export default class CollectionsHoldingStaleAnalysis extends BaseAnalysis {
17
17
  /** @internal */
18
18
  getOptions(): BaseOption[];
19
19
  /** @internal */
20
- analyzeSnapshotsInDirectory(directory: string): Promise<void>;
20
+ analyzeSnapshotsInDirectory(directory: string): Promise<AnalyzeSnapshotResult>;
21
21
  private staleCollectionMapper;
22
22
  /** @internal */
23
23
  process(options: HeapAnalysisOptions): Promise<void>;
@@ -7,7 +7,7 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { HeapAnalysisOptions } from '../PluginUtils';
10
+ import type { AnalyzeSnapshotResult, HeapAnalysisOptions } from '../PluginUtils';
11
11
  import type { IHeapNode } from '@memlab/core';
12
12
  import { BaseOption } from '@memlab/core';
13
13
  import BaseAnalysis from '../BaseAnalysis';
@@ -18,7 +18,7 @@ export default class DetachedDOMElementAnalysis extends BaseAnalysis {
18
18
  /** @internal */
19
19
  getOptions(): BaseOption[];
20
20
  /** @internal */
21
- analyzeSnapshotsInDirectory(directory: string): Promise<void>;
21
+ analyzeSnapshotsInDirectory(directory: string): Promise<AnalyzeSnapshotResult>;
22
22
  private detachedElements;
23
23
  getDetachedElements(): IHeapNode[];
24
24
  /** @internal */
@@ -7,7 +7,7 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { HeapAnalysisOptions } from '../../PluginUtils';
10
+ import type { AnalyzeSnapshotResult, HeapAnalysisOptions } from '../../PluginUtils';
11
11
  import { BaseOption } from '@memlab/core';
12
12
  import BaseAnalysis from '../../BaseAnalysis';
13
13
  declare class GlobalVariableAnalysis extends BaseAnalysis {
@@ -19,7 +19,7 @@ declare class GlobalVariableAnalysis extends BaseAnalysis {
19
19
  /** @internal */
20
20
  process(options: HeapAnalysisOptions): Promise<void>;
21
21
  /** @internal */
22
- analyzeSnapshotsInDirectory(directory: string): Promise<void>;
22
+ analyzeSnapshotsInDirectory(directory: string): Promise<AnalyzeSnapshotResult>;
23
23
  private shouldFilterOutEdge;
24
24
  /** @internal */
25
25
  private getGlobalVariables;
@@ -7,7 +7,7 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { HeapAnalysisOptions } from '../PluginUtils';
10
+ import type { AnalyzeSnapshotResult, HeapAnalysisOptions } from '../PluginUtils';
11
11
  import { BaseOption } from '@memlab/core';
12
12
  import BaseAnalysis from '../BaseAnalysis';
13
13
  declare class GlobalVariableAnalysis extends BaseAnalysis {
@@ -19,7 +19,7 @@ declare class GlobalVariableAnalysis extends BaseAnalysis {
19
19
  /** @internal */
20
20
  process(options: HeapAnalysisOptions): Promise<void>;
21
21
  /** @internal */
22
- analyzeSnapshotsInDirectory(directory: string): Promise<void>;
22
+ analyzeSnapshotsInDirectory(directory: string): Promise<AnalyzeSnapshotResult>;
23
23
  /** @internal */
24
24
  private getObjectProperties;
25
25
  /** @internal */
@@ -7,7 +7,7 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { HeapAnalysisOptions } from '../PluginUtils';
10
+ import type { AnalyzeSnapshotResult, HeapAnalysisOptions } from '../PluginUtils';
11
11
  import { BaseOption } from '@memlab/core';
12
12
  import BaseAnalysis from '../BaseAnalysis';
13
13
  declare class ObjectFanoutAnalysis extends BaseAnalysis {
@@ -17,7 +17,7 @@ declare class ObjectFanoutAnalysis extends BaseAnalysis {
17
17
  /** @internal */
18
18
  getOptions(): BaseOption[];
19
19
  /** @internal */
20
- analyzeSnapshotsInDirectory(directory: string): Promise<void>;
20
+ analyzeSnapshotsInDirectory(directory: string): Promise<AnalyzeSnapshotResult>;
21
21
  /** @internal */
22
22
  process(options: HeapAnalysisOptions): Promise<void>;
23
23
  private getObjectsWithHighFanout;
@@ -7,7 +7,7 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { HeapAnalysisOptions } from '../PluginUtils';
10
+ import type { AnalyzeSnapshotResult, HeapAnalysisOptions } from '../PluginUtils';
11
11
  import { BaseOption } from '@memlab/core';
12
12
  import BaseAnalysis from '../BaseAnalysis';
13
13
  /**
@@ -44,7 +44,7 @@ declare class ObjectShallowAnalysis extends BaseAnalysis {
44
44
  /** @internal */
45
45
  getOptions(): BaseOption[];
46
46
  /** @internal */
47
- analyzeSnapshotsInDirectory(directory: string): Promise<void>;
47
+ analyzeSnapshotsInDirectory(directory: string): Promise<AnalyzeSnapshotResult>;
48
48
  /** @internal */
49
49
  process(options: HeapAnalysisOptions): Promise<void>;
50
50
  /**
@@ -7,8 +7,8 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { HeapAnalysisOptions } from '../PluginUtils';
11
- import { BaseOption } from '@memlab/core';
10
+ import type { AnalyzeSnapshotResult, HeapAnalysisOptions } from '../PluginUtils';
11
+ import type { BaseOption } from '@memlab/core';
12
12
  import BaseAnalysis from '../BaseAnalysis';
13
13
  declare class ObjectShapeAnalysis extends BaseAnalysis {
14
14
  getCommandName(): string;
@@ -17,9 +17,19 @@ declare class ObjectShapeAnalysis extends BaseAnalysis {
17
17
  /** @internal */
18
18
  getOptions(): BaseOption[];
19
19
  /** @internal */
20
- analyzeSnapshotsInDirectory(directory: string): Promise<void>;
20
+ analyzeSnapshotsInDirectory(directory: string): Promise<AnalyzeSnapshotResult>;
21
21
  /** @internal */
22
22
  process(options: HeapAnalysisOptions): Promise<void>;
23
+ /** @internal */
24
+ breakDownMemoryByShapes(options?: {
25
+ file?: string;
26
+ }): Promise<void>;
27
+ /** @internal */
28
+ private breakDownSnapshotByShapes;
29
+ /** @internal */
30
+ private breakDownByReferrers;
31
+ /** @internal */
32
+ private isTrivialEdgeForBreakDown;
23
33
  }
24
34
  export default ObjectShapeAnalysis;
25
35
  //# sourceMappingURL=ObjectShapeAnalysis.d.ts.map
@@ -21,6 +21,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
21
21
  return (mod && mod.__esModule) ? mod : { "default": mod };
22
22
  };
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
+ const chalk_1 = __importDefault(require("chalk"));
24
25
  const core_1 = require("@memlab/core");
25
26
  const BaseAnalysis_1 = __importDefault(require("../BaseAnalysis"));
26
27
  const PluginUtils_1 = __importDefault(require("../PluginUtils"));
@@ -49,8 +50,132 @@ class ObjectShapeAnalysis extends BaseAnalysis_1.default {
49
50
  process(options) {
50
51
  return __awaiter(this, void 0, void 0, function* () {
51
52
  const snapshotPath = PluginUtils_1.default.getSnapshotFileForAnalysis(options);
52
- yield core_1.analysis.breakDownMemoryByShapes({ file: snapshotPath });
53
+ yield this.breakDownMemoryByShapes({ file: snapshotPath });
53
54
  });
54
55
  }
56
+ /** @internal */
57
+ breakDownMemoryByShapes(options = {}) {
58
+ return __awaiter(this, void 0, void 0, function* () {
59
+ const opt = { buildNodeIdIndex: true, verbose: true };
60
+ const file = options.file ||
61
+ core_1.utils.getSnapshotFilePathWithTabType(/.*/) ||
62
+ '<EMPTY_FILE_PATH>';
63
+ const snapshot = yield core_1.utils.getSnapshotFromFile(file, opt);
64
+ core_1.analysis.preparePathFinder(snapshot);
65
+ const heapInfo = core_1.analysis.getOverallHeapInfo(snapshot, { force: true });
66
+ if (heapInfo) {
67
+ core_1.analysis.printHeapInfo(heapInfo);
68
+ }
69
+ this.breakDownSnapshotByShapes(snapshot);
70
+ });
71
+ }
72
+ /** @internal */
73
+ breakDownSnapshotByShapes(snapshot) {
74
+ core_1.info.overwrite('Breaking down memory by shapes...');
75
+ const breakdown = Object.create(null);
76
+ const population = Object.create(null);
77
+ // group objects based on their shapes
78
+ snapshot.nodes.forEach((node) => {
79
+ if ((node.type !== 'object' && !core_1.utils.isStringNode(node)) ||
80
+ core_1.config.nodeIgnoreSetInShape.has(node.name)) {
81
+ return;
82
+ }
83
+ const key = core_1.serializer.summarizeNodeShape(node);
84
+ breakdown[key] = breakdown[key] || new Set();
85
+ breakdown[key].add(node.id);
86
+ if (population[key] === undefined) {
87
+ population[key] = { examples: [], n: 0 };
88
+ }
89
+ ++population[key].n;
90
+ // retain the top 5 examples
91
+ const examples = population[key].examples;
92
+ examples.push(node);
93
+ examples.sort((n1, n2) => n2.retainedSize - n1.retainedSize);
94
+ if (examples.length > 5) {
95
+ examples.pop();
96
+ }
97
+ });
98
+ // calculate and sort based on retained sizes
99
+ const ret = [];
100
+ for (const key in breakdown) {
101
+ const size = core_1.utils.aggregateDominatorMetrics(breakdown[key], snapshot, () => true, core_1.utils.getRetainedSize);
102
+ ret.push({ key, retainedSize: size });
103
+ }
104
+ ret.sort((o1, o2) => o2.retainedSize - o1.retainedSize);
105
+ core_1.info.topLevel('Object shapes with top retained sizes:');
106
+ core_1.info.lowLevel(' (Use `memlab trace --node-id=@ID` to get trace)\n');
107
+ const topList = ret.slice(0, 40);
108
+ // print settings
109
+ const opt = { color: true, compact: true };
110
+ const dot = chalk_1.default.grey('· ');
111
+ const colon = chalk_1.default.grey(': ');
112
+ // print the shapes with the biggest retained size
113
+ for (const o of topList) {
114
+ const referrerInfo = this.breakDownByReferrers(breakdown[o.key], snapshot);
115
+ const { examples, n } = population[o.key];
116
+ const shapeStr = core_1.serializer.summarizeNodeShape(examples[0], opt);
117
+ const bytes = core_1.utils.getReadableBytes(o.retainedSize);
118
+ const examplesStr = examples
119
+ .map(e => `@${e.id} [${core_1.utils.getReadableBytes(e.retainedSize)}]`)
120
+ .join(' | ');
121
+ const meta = chalk_1.default.grey(` (N: ${n}, Examples: ${examplesStr})`);
122
+ core_1.info.topLevel(`${dot}${shapeStr}${colon}${bytes}${meta}`);
123
+ core_1.info.lowLevel(referrerInfo + '\n');
124
+ }
125
+ }
126
+ /** @internal */
127
+ breakDownByReferrers(ids, snapshot) {
128
+ const edgeNames = Object.create(null);
129
+ for (const id of ids) {
130
+ const node = snapshot.getNodeById(id);
131
+ for (const edge of (node === null || node === void 0 ? void 0 : node.referrers) || []) {
132
+ const source = edge.fromNode;
133
+ if (!core_1.utils.isMeaningfulEdge(edge) ||
134
+ this.isTrivialEdgeForBreakDown(edge)) {
135
+ continue;
136
+ }
137
+ const sourceName = core_1.serializer.summarizeNodeName(source, {
138
+ color: false,
139
+ });
140
+ const edgeName = core_1.serializer.summarizeEdgeName(edge, {
141
+ color: false,
142
+ abstract: true,
143
+ });
144
+ const edgeKey = `[${sourceName}] --${edgeName}--> `;
145
+ edgeNames[edgeKey] = edgeNames[edgeKey] || {
146
+ numberOfEdgesToNode: 0,
147
+ source,
148
+ edge,
149
+ };
150
+ ++edgeNames[edgeKey].numberOfEdgesToNode;
151
+ }
152
+ }
153
+ const referrerInfo = Object.entries(edgeNames)
154
+ .sort((i1, i2) => i2[1].numberOfEdgesToNode - i1[1].numberOfEdgesToNode)
155
+ .slice(0, 4)
156
+ .map(i => {
157
+ const meta = i[1];
158
+ const source = core_1.serializer.summarizeNodeName(meta.source, {
159
+ color: true,
160
+ });
161
+ const edgeName = core_1.serializer.summarizeEdgeName(meta.edge, {
162
+ color: true,
163
+ abstract: true,
164
+ });
165
+ const edgeSummary = `${source} --${edgeName}-->`;
166
+ return ` · ${edgeSummary}: ${meta.numberOfEdgesToNode}`;
167
+ })
168
+ .join('\n');
169
+ return referrerInfo;
170
+ }
171
+ /** @internal */
172
+ isTrivialEdgeForBreakDown(edge) {
173
+ const source = edge.fromNode;
174
+ return (source.type === 'array' ||
175
+ source.name === '(object elements)' ||
176
+ source.name === 'system' ||
177
+ edge.name_or_index === '__proto__' ||
178
+ edge.name_or_index === 'prototype');
179
+ }
55
180
  }
56
181
  exports.default = ObjectShapeAnalysis;
@@ -7,7 +7,7 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { HeapAnalysisOptions } from '../PluginUtils';
10
+ import type { AnalyzeSnapshotResult, HeapAnalysisOptions } from '../PluginUtils';
11
11
  import { BaseOption } from '@memlab/core';
12
12
  import BaseAnalysis from '../BaseAnalysis';
13
13
  declare class ObjectSizeRankAnalysis extends BaseAnalysis {
@@ -17,7 +17,7 @@ declare class ObjectSizeRankAnalysis extends BaseAnalysis {
17
17
  /** @internal */
18
18
  getOptions(): BaseOption[];
19
19
  /** @internal */
20
- analyzeSnapshotsInDirectory(directory: string): Promise<void>;
20
+ analyzeSnapshotsInDirectory(directory: string): Promise<AnalyzeSnapshotResult>;
21
21
  /** @internal */
22
22
  process(options: HeapAnalysisOptions): Promise<void>;
23
23
  }
@@ -7,8 +7,8 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { HeapAnalysisOptions } from '../PluginUtils';
11
- import { BaseOption } from '@memlab/core';
10
+ import type { BaseOption } from '@memlab/core';
11
+ import type { AnalyzeSnapshotResult, HeapAnalysisOptions } from '../PluginUtils';
12
12
  import BaseAnalysis from '../BaseAnalysis';
13
13
  declare class ObjectUnboundGrowthAnalysis extends BaseAnalysis {
14
14
  getCommandName(): string;
@@ -17,9 +17,12 @@ declare class ObjectUnboundGrowthAnalysis extends BaseAnalysis {
17
17
  /** @internal */
18
18
  getOptions(): BaseOption[];
19
19
  /** @internal */
20
- analyzeSnapshotFromFile(file: string): Promise<void>;
20
+ analyzeSnapshotFromFile(file: string): Promise<AnalyzeSnapshotResult>;
21
21
  /** @internal */
22
22
  process(options: HeapAnalysisOptions): Promise<void>;
23
+ private checkUnbound;
24
+ private detectUnboundGrowth;
25
+ private calculateRetainedSizes;
23
26
  }
24
27
  export default ObjectUnboundGrowthAnalysis;
25
28
  //# sourceMappingURL=ObjectUnboundGrowthAnalysis.d.ts.map
@@ -21,6 +21,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
21
21
  return (mod && mod.__esModule) ? mod : { "default": mod };
22
22
  };
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
+ const fs_1 = __importDefault(require("fs"));
24
25
  const core_1 = require("@memlab/core");
25
26
  const BaseAnalysis_1 = __importDefault(require("../BaseAnalysis"));
26
27
  const HeapAnalysisSnapshotDirectoryOption_1 = __importDefault(require("../options/HeapAnalysisSnapshotDirectoryOption"));
@@ -52,8 +53,148 @@ class ObjectUnboundGrowthAnalysis extends BaseAnalysis_1.default {
52
53
  const snapshotDir = PluginUtils_1.default.getSnapshotDirForAnalysis(options);
53
54
  const opt = snapshotDir ? { minSnapshots: 2, snapshotDir } : {};
54
55
  core_1.config.chaseWeakMapEdge = false;
55
- yield core_1.analysis.checkUnbound(opt);
56
+ yield this.checkUnbound(opt);
56
57
  });
57
58
  }
59
+ checkUnbound(options = {}) {
60
+ return __awaiter(this, void 0, void 0, function* () {
61
+ core_1.analysis.visualizeMemoryUsage(options);
62
+ core_1.utils.checkSnapshots(options);
63
+ yield this.detectUnboundGrowth(options);
64
+ });
65
+ }
66
+ // find any objects that keeps growing
67
+ detectUnboundGrowth(options = {}) {
68
+ return __awaiter(this, void 0, void 0, function* () {
69
+ const nodeInfo = Object.create(null);
70
+ let hasCheckedFirstSnapshot = false;
71
+ let snapshot = null;
72
+ const isValidNode = (node) => node.type === 'object' ||
73
+ node.type === 'closure' ||
74
+ node.type === 'regexp';
75
+ const initNodeInfo = (node) => {
76
+ if (!isValidNode(node)) {
77
+ return;
78
+ }
79
+ const n = node.retainedSize;
80
+ nodeInfo[node.id] = {
81
+ type: node.type,
82
+ name: node.name,
83
+ min: n,
84
+ max: n,
85
+ history: [n],
86
+ node,
87
+ };
88
+ };
89
+ const updateNodeInfo = (node) => {
90
+ const item = nodeInfo[node.id];
91
+ if (!item) {
92
+ return;
93
+ }
94
+ if (node.name !== item.name || node.type !== item.type) {
95
+ nodeInfo[node.id] = null;
96
+ return;
97
+ }
98
+ const n = node.retainedSize;
99
+ // only monotonic increase?
100
+ if (core_1.config.monotonicUnboundGrowthOnly && n < item.max) {
101
+ nodeInfo[node.id] = null;
102
+ return;
103
+ }
104
+ item.history.push(n);
105
+ item.max = Math.max(item.max, n);
106
+ item.min = Math.min(item.min, n);
107
+ };
108
+ // summarize the heap objects info in current heap snapshot
109
+ // this is mainly used for better understanding of the % of
110
+ // objects released and allocated over time
111
+ const maybeSummarizeNodeInfo = () => {
112
+ if (!core_1.config.verbose) {
113
+ return;
114
+ }
115
+ let n = 0;
116
+ for (const k in nodeInfo) {
117
+ if (nodeInfo[k]) {
118
+ ++n;
119
+ }
120
+ }
121
+ core_1.info.lowLevel(`Objects tracked: ${n}`);
122
+ };
123
+ core_1.info.overwrite('Checking unbounded objects...');
124
+ const snapshotFiles = options.snapshotDir
125
+ ? // load snapshots from a directory
126
+ core_1.utils.getSnapshotFilesInDir(options.snapshotDir)
127
+ : // load snapshots based on the visit sequence meta data
128
+ core_1.utils.getSnapshotFilesFromTabsOrder();
129
+ for (const file of snapshotFiles) {
130
+ // force GC before loading each snapshot
131
+ if (global.gc) {
132
+ global.gc();
133
+ }
134
+ // load and preprocess heap snapshot
135
+ const opt = { buildNodeIdIndex: true, verbose: true };
136
+ snapshot = yield core_1.utils.getSnapshotFromFile(file, opt);
137
+ this.calculateRetainedSizes(snapshot);
138
+ // keep track of heap objects
139
+ if (!hasCheckedFirstSnapshot) {
140
+ // record Ids in the snapshot
141
+ snapshot.nodes.forEach(initNodeInfo);
142
+ hasCheckedFirstSnapshot = true;
143
+ }
144
+ else {
145
+ snapshot.nodes.forEach(updateNodeInfo);
146
+ maybeSummarizeNodeInfo();
147
+ }
148
+ }
149
+ // exit if no heap snapshot found
150
+ if (!hasCheckedFirstSnapshot) {
151
+ return;
152
+ }
153
+ // post process and print the unbounded objects
154
+ const idsInLastSnapshot = new Set();
155
+ snapshot === null || snapshot === void 0 ? void 0 : snapshot.nodes.forEach(node => {
156
+ idsInLastSnapshot.add(node.id);
157
+ });
158
+ let ids = [];
159
+ for (const key in nodeInfo) {
160
+ const id = parseInt(key, 10);
161
+ const item = nodeInfo[id];
162
+ if (!item) {
163
+ continue;
164
+ }
165
+ if (!idsInLastSnapshot.has(id)) {
166
+ continue;
167
+ }
168
+ if (item.min === item.max) {
169
+ continue;
170
+ }
171
+ // filter out non-significant leaks
172
+ if (item.history[item.history.length - 1] < core_1.config.unboundSizeThreshold) {
173
+ continue;
174
+ }
175
+ ids.push(Object.assign({ id }, item));
176
+ }
177
+ if (ids.length === 0) {
178
+ core_1.info.midLevel('No increasing objects found.');
179
+ return;
180
+ }
181
+ ids = ids
182
+ .sort((o1, o2) => o2.history[o2.history.length - 1] - o1.history[o1.history.length - 1])
183
+ .slice(0, 20);
184
+ // print on terminal
185
+ const str = core_1.serializer.summarizeUnboundedObjects(ids, { color: true });
186
+ core_1.info.topLevel('Top growing objects in sizes:');
187
+ core_1.info.lowLevel(' (Use `memlab trace --node-id=@ID` to get trace)');
188
+ core_1.info.topLevel('\n' + str);
189
+ // save results to file
190
+ const csv = core_1.serializer.summarizeUnboundedObjectsToCSV(ids);
191
+ fs_1.default.writeFileSync(core_1.config.unboundObjectCSV, csv, 'UTF-8');
192
+ });
193
+ }
194
+ calculateRetainedSizes(snapshot) {
195
+ const finder = new core_1.TraceFinder();
196
+ // dominator and retained size
197
+ finder.calculateAllNodesRetainedSizes(snapshot);
198
+ }
58
199
  }
59
200
  exports.default = ObjectUnboundGrowthAnalysis;
@@ -7,7 +7,7 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { HeapAnalysisOptions } from '../PluginUtils';
10
+ import type { AnalyzeSnapshotResult, HeapAnalysisOptions } from '../PluginUtils';
11
11
  import { BaseOption } from '@memlab/core';
12
12
  import BaseAnalysis from '../BaseAnalysis';
13
13
  declare type ShapeSummary = {
@@ -25,7 +25,7 @@ export default class ShapeUnboundGrowthAnalysis extends BaseAnalysis {
25
25
  /** @internal */
26
26
  getOptions(): BaseOption[];
27
27
  /** @internal */
28
- analyzeSnapshotFromFile(file: string): Promise<void>;
28
+ analyzeSnapshotFromFile(file: string): Promise<AnalyzeSnapshotResult>;
29
29
  getShapesWithUnboundGrowth(): ShapeSummary[];
30
30
  /** @internal */
31
31
  process(options: HeapAnalysisOptions): Promise<void>;
@@ -7,7 +7,7 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { HeapAnalysisOptions } from '../PluginUtils';
10
+ import type { AnalyzeSnapshotResult, HeapAnalysisOptions } from '../PluginUtils';
11
11
  import { BaseOption } from '@memlab/core';
12
12
  import BaseAnalysis from '../BaseAnalysis';
13
13
  /**
@@ -58,7 +58,7 @@ export default class StringAnalysis extends BaseAnalysis {
58
58
  /** @internal */
59
59
  getOptions(): BaseOption[];
60
60
  /** @internal */
61
- analyzeSnapshotsInDirectory(directory: string): Promise<void>;
61
+ analyzeSnapshotsInDirectory(directory: string): Promise<AnalyzeSnapshotResult>;
62
62
  private static shouldIgnoreNode;
63
63
  /** @internal */
64
64
  process(options: HeapAnalysisOptions): Promise<void>;
@@ -7,7 +7,7 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { HeapAnalysisOptions } from '../PluginUtils';
10
+ import type { AnalyzeSnapshotResult, HeapAnalysisOptions } from '../PluginUtils';
11
11
  import { BaseOption } from '@memlab/core';
12
12
  import BaseAnalysis from '../BaseAnalysis';
13
13
  export default class UnmountedFiberNodeAnalysis extends BaseAnalysis {
@@ -17,7 +17,7 @@ export default class UnmountedFiberNodeAnalysis extends BaseAnalysis {
17
17
  /** @internal */
18
18
  getOptions(): BaseOption[];
19
19
  /** @internal */
20
- analyzeSnapshotsInDirectory(directory: string): Promise<void>;
20
+ analyzeSnapshotsInDirectory(directory: string): Promise<AnalyzeSnapshotResult>;
21
21
  /** @internal */
22
22
  process(options: HeapAnalysisOptions): Promise<void>;
23
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memlab/heap-analysis",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "license": "MIT",
5
5
  "description": "heap analysis plugins for memlab",
6
6
  "author": "Liang Gong <lgong@fb.com>",
@@ -50,7 +50,7 @@
50
50
  "scripts": {
51
51
  "build-pkg": "tsc",
52
52
  "test-pkg": "jest .",
53
- "publish-patch": "npm version patch --force && npm publish",
53
+ "publish-patch": "npm publish",
54
54
  "clean-pkg": "rm -rf ./dist && rm -rf ./node_modules && rm -f ./tsconfig.tsbuildinfo"
55
55
  },
56
56
  "bugs": {