@memlab/heap-analysis 1.0.14 → 1.0.16

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, RunHeapAnalysisOptions } from './PluginUtils';
13
12
  declare abstract class Analysis {
14
13
  process(_options: HeapAnalysisOptions): Promise<void>;
15
14
  /**
@@ -27,33 +26,65 @@ 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
+ * @param options optional configuration for the heap analysis run
30
+ * @returns this API returns {@link AnalyzeSnapshotResult}, which contains
31
+ * the logging file of analysis console output. Alternatively, to get more
32
+ * structured analysis results, check out the documentation of the hosting
33
+ * heap analysis class and call the analysis-specific API to get results
34
+ * after calling this method.
33
35
  * * **Example**:
34
36
  * ```typescript
35
37
  * const analysis = new StringAnalysis();
36
- * await anaysis.analyzeSnapshotFromFile(snapshotFile);
38
+ * // analysis console output is saved in result.analysisOutputFile
39
+ * const result = await analysis.analyzeSnapshotFromFile(snapshotFile);
40
+ * // query analysis-specific and structured results
37
41
  * const stringPatterns = analysis.getTopDuplicatedStringsInCount();
38
42
  * ```
43
+ * Additionally, you can specify a working directory to where
44
+ * the intermediate, logging, and final output files will be dumped:
45
+ * ```typescript
46
+ * const analysis = new StringAnalysis();
47
+ * // analysis console output is saved in result.analysisOutputFile
48
+ * // which is inside the specified working directory
49
+ * const result = await analysis.analyzeSnapshotFromFile(snapshotFile, {
50
+ * // if the specified directory doesn't exist, memlab will create it
51
+ * workDir: '/tmp/your/work/dir',
52
+ * });
53
+ * ```
39
54
  */
40
- analyzeSnapshotFromFile(file: string): Promise<void>;
55
+ analyzeSnapshotFromFile(file: string, options?: RunHeapAnalysisOptions): Promise<AnalyzeSnapshotResult>;
41
56
  /**
42
57
  * Run heap analysis for a series of heap snapshot files
43
58
  * @param directory the absolute path of the directory holding a series of
44
59
  * `.heapsnapshot` files, all snapshot files will be loaded and analyzed
45
60
  * 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.
61
+ * @param options optional configuration for the heap analysis run
62
+ * @returns this API returns {@link AnalyzeSnapshotResult}, which contains
63
+ * the logging file of analysis console output. Alternatively, to get more
64
+ * structured analysis results, check out the documentation of the hosting
65
+ * heap analysis class and call the analysis-specific API to get results
66
+ * after calling this method.
49
67
  * * **Example**:
50
68
  * ```typescript
51
69
  * const analysis = new ShapeUnboundGrowthAnalysis();
52
- * await anaysis.analyzeSnapshotsInDirectory(snapshotDirectory);
70
+ * // analysis console output is saved in result.analysisOutputFile
71
+ * const result = await analysis.analyzeSnapshotsInDirectory(snapshotDirectory);
72
+ * // query analysis-specific and structured results
53
73
  * const shapes = analysis.getShapesWithUnboundGrowth();
54
74
  * ```
75
+ * * Additionally, you can specify a working directory to where
76
+ * the intermediate, logging, and final output files will be dumped:
77
+ * ```typescript
78
+ * const analysis = new ShapeUnboundGrowthAnalysis();
79
+ * // analysis console output is saved in result.analysisOutputFile
80
+ * // which is inside the specified working directory
81
+ * const result = await analysis.analyzeSnapshotsInDirectory(snapshotDirectory, {
82
+ * // if the specified directory doesn't exist, memlab will create it
83
+ * workDir: '/tmp/your/work/dir',
84
+ * });
85
+ * ```
55
86
  */
56
- analyzeSnapshotsInDirectory(directory: string): Promise<void>;
87
+ analyzeSnapshotsInDirectory(directory: string, options?: RunHeapAnalysisOptions): Promise<AnalyzeSnapshotResult>;
57
88
  }
58
89
  /**
59
90
  *
@@ -68,25 +68,49 @@ 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
+ * @param options optional configuration for the heap analysis run
72
+ * @returns this API returns {@link AnalyzeSnapshotResult}, which contains
73
+ * the logging file of analysis console output. Alternatively, to get more
74
+ * structured analysis results, check out the documentation of the hosting
75
+ * heap analysis class and call the analysis-specific API to get results
76
+ * after calling this method.
74
77
  * * **Example**:
75
78
  * ```typescript
76
79
  * const analysis = new StringAnalysis();
77
- * await anaysis.analyzeSnapshotFromFile(snapshotFile);
80
+ * // analysis console output is saved in result.analysisOutputFile
81
+ * const result = await analysis.analyzeSnapshotFromFile(snapshotFile);
82
+ * // query analysis-specific and structured results
78
83
  * const stringPatterns = analysis.getTopDuplicatedStringsInCount();
79
84
  * ```
85
+ * Additionally, you can specify a working directory to where
86
+ * the intermediate, logging, and final output files will be dumped:
87
+ * ```typescript
88
+ * const analysis = new StringAnalysis();
89
+ * // analysis console output is saved in result.analysisOutputFile
90
+ * // which is inside the specified working directory
91
+ * const result = await analysis.analyzeSnapshotFromFile(snapshotFile, {
92
+ * // if the specified directory doesn't exist, memlab will create it
93
+ * workDir: '/tmp/your/work/dir',
94
+ * });
95
+ * ```
80
96
  */
81
- analyzeSnapshotFromFile(file) {
97
+ analyzeSnapshotFromFile(file, options = {}) {
82
98
  return __awaiter(this, void 0, void 0, function* () {
83
- return this.process({
99
+ if (options.workDir) {
100
+ // set and init the new work dir
101
+ core_1.config.defaultFileManagerOption = options;
102
+ }
103
+ const analysisOutputFile = core_1.fileManager.initNewHeapAnalysisLogFile();
104
+ core_1.info.registerLogFile(analysisOutputFile);
105
+ yield this.process({
84
106
  args: {
85
107
  _: [],
86
108
  snapshot: file,
87
109
  'snapshot-dir': '<MUST_PROVIDE_SNAPSHOT_DIR>',
88
110
  },
89
111
  });
112
+ core_1.info.unregisterLogFile(analysisOutputFile);
113
+ return { analysisOutputFile };
90
114
  });
91
115
  }
92
116
  /**
@@ -94,25 +118,49 @@ class Analysis {
94
118
  * @param directory the absolute path of the directory holding a series of
95
119
  * `.heapsnapshot` files, all snapshot files will be loaded and analyzed
96
120
  * 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.
121
+ * @param options optional configuration for the heap analysis run
122
+ * @returns this API returns {@link AnalyzeSnapshotResult}, which contains
123
+ * the logging file of analysis console output. Alternatively, to get more
124
+ * structured analysis results, check out the documentation of the hosting
125
+ * heap analysis class and call the analysis-specific API to get results
126
+ * after calling this method.
100
127
  * * **Example**:
101
128
  * ```typescript
102
129
  * const analysis = new ShapeUnboundGrowthAnalysis();
103
- * await anaysis.analyzeSnapshotsInDirectory(snapshotDirectory);
130
+ * // analysis console output is saved in result.analysisOutputFile
131
+ * const result = await analysis.analyzeSnapshotsInDirectory(snapshotDirectory);
132
+ * // query analysis-specific and structured results
104
133
  * const shapes = analysis.getShapesWithUnboundGrowth();
105
134
  * ```
135
+ * * Additionally, you can specify a working directory to where
136
+ * the intermediate, logging, and final output files will be dumped:
137
+ * ```typescript
138
+ * const analysis = new ShapeUnboundGrowthAnalysis();
139
+ * // analysis console output is saved in result.analysisOutputFile
140
+ * // which is inside the specified working directory
141
+ * const result = await analysis.analyzeSnapshotsInDirectory(snapshotDirectory, {
142
+ * // if the specified directory doesn't exist, memlab will create it
143
+ * workDir: '/tmp/your/work/dir',
144
+ * });
145
+ * ```
106
146
  */
107
- analyzeSnapshotsInDirectory(directory) {
147
+ analyzeSnapshotsInDirectory(directory, options = {}) {
108
148
  return __awaiter(this, void 0, void 0, function* () {
109
- return this.process({
149
+ if (options.workDir) {
150
+ // set and init the new work dir
151
+ core_1.config.defaultFileManagerOption = options;
152
+ }
153
+ const analysisOutputFile = core_1.fileManager.initNewHeapAnalysisLogFile();
154
+ core_1.info.registerLogFile(analysisOutputFile);
155
+ yield this.process({
110
156
  args: {
111
157
  _: [],
112
158
  snapshot: '<MUST_PROVIDE_SNAPSHOT_FILE>',
113
159
  'snapshot-dir': directory,
114
160
  },
115
161
  });
162
+ core_1.info.unregisterLogFile(analysisOutputFile);
163
+ return { analysisOutputFile };
116
164
  });
117
165
  }
118
166
  }
@@ -25,6 +25,27 @@ export declare type HeapAnalysisOptions = {
25
25
  /** @internal */
26
26
  config?: MemLabConfig;
27
27
  };
28
+ /**
29
+ * This is the input option for {@link analyzeSnapshotFromFile}
30
+ * and {@link analyzeSnapshotsInDirectory}.
31
+ */
32
+ export declare type RunHeapAnalysisOptions = {
33
+ /**
34
+ * specify the working directory to where the intermediate, logging,
35
+ * and output files should be saved
36
+ */
37
+ workDir?: string;
38
+ };
39
+ /**
40
+ * This is the return type from calling {@link analyzeSnapshotFromFile}
41
+ * or {@link analyzeSnapshotsInDirectory}.
42
+ */
43
+ export declare type AnalyzeSnapshotResult = {
44
+ /**
45
+ * file path of the console output of the heap analysis call
46
+ */
47
+ analysisOutputFile: string;
48
+ };
28
49
  declare type PrintNodeOption = {
29
50
  indent?: string;
30
51
  printReferences?: boolean;
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export declare function registerPackage(): Promise<void>;
12
12
  export declare const getDominatorNodes: (ids: Set<number>, snapshot: import("@memlab/core").IHeapSnapshot) => Set<number>,
13
13
  /** @deprecated */
14
14
  getHeapFromFile: (file: string) => Promise<import("@memlab/core").IHeapSnapshot>, getFullHeapFromFile: (file: string) => Promise<import("@memlab/core").IHeapSnapshot>, getSnapshotDirForAnalysis: (options: import("./PluginUtils").HeapAnalysisOptions) => import("@memlab/core").Nullable<string>, getSnapshotFileForAnalysis: (options: import("./PluginUtils").HeapAnalysisOptions) => string, loadHeapSnapshot: (options: import("./PluginUtils").HeapAnalysisOptions) => Promise<import("@memlab/core").IHeapSnapshot>, snapshotMapReduce: <T1, T2>(mapCallback: (snapshot: import("@memlab/core").IHeapSnapshot, i: number, file: string) => T1, reduceCallback: (results: T1[]) => T2, options: import("./PluginUtils").HeapAnalysisOptions) => Promise<T2>, takeNodeFullHeap: () => Promise<import("@memlab/core").IHeapSnapshot>;
15
- export type { HeapAnalysisOptions } from './PluginUtils';
15
+ export type { AnalyzeSnapshotResult, HeapAnalysisOptions, RunHeapAnalysisOptions, } from './PluginUtils';
16
16
  export { default as BaseAnalysis } from './BaseAnalysis';
17
17
  export { default as DetachedDOMElementAnalysis } from './plugins/DetachedDOMElementAnalysis';
18
18
  export { default as GlobalVariableAnalysis } from './plugins/GlobalVariableAnalysis/GlobalVariableAnalysis';
@@ -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.16",
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": {