@memlab/core 1.1.17 → 1.1.19

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/index.d.ts CHANGED
@@ -7,11 +7,11 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- /** @internal */
11
- export declare function registerPackage(): Promise<void>;
12
10
  export * from './lib/Types';
13
11
  export * from './lib/NodeHeap';
14
12
  /** @internal */
13
+ export declare function registerPackage(): Promise<void>;
14
+ /** @internal */
15
15
  export { default as config } from './lib/Config';
16
16
  /** @internal */
17
17
  export * from './lib/InternalValueSetter';
@@ -36,6 +36,8 @@ export { default as analysis } from './lib/HeapAnalyzer';
36
36
  /** @internal */
37
37
  export { default as constant } from './lib/Constant';
38
38
  /** @internal */
39
+ export { default as memoryBarChart } from './lib/charts/MemoryBarChart';
40
+ /** @internal */
39
41
  export { default as modes } from './modes/RunningModes';
40
42
  /** @internal */
41
43
  export { default as ProcessManager } from './lib/ProcessManager';
package/dist/index.js CHANGED
@@ -35,9 +35,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
35
35
  return (mod && mod.__esModule) ? mod : { "default": mod };
36
36
  };
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
- exports.TraceFinder = exports.MultiIterationSeqClustering = exports.SequentialClustering = exports.EvaluationMetric = exports.NormalizedTrace = exports.leakClusterLogger = exports.ProcessManager = exports.modes = exports.constant = exports.analysis = exports.browserInfo = exports.serializer = exports.fileManager = exports.utils = exports.BaseOption = exports.info = exports.config = exports.registerPackage = void 0;
38
+ exports.TraceFinder = exports.MultiIterationSeqClustering = exports.SequentialClustering = exports.EvaluationMetric = exports.NormalizedTrace = exports.leakClusterLogger = exports.ProcessManager = exports.modes = exports.memoryBarChart = exports.constant = exports.analysis = exports.browserInfo = exports.serializer = exports.fileManager = exports.utils = exports.BaseOption = exports.info = exports.config = exports.registerPackage = void 0;
39
39
  const path_1 = __importDefault(require("path"));
40
40
  const PackageInfoLoader_1 = require("./lib/PackageInfoLoader");
41
+ __exportStar(require("./lib/Types"), exports);
42
+ __exportStar(require("./lib/NodeHeap"), exports);
41
43
  /** @internal */
42
44
  function registerPackage() {
43
45
  return __awaiter(this, void 0, void 0, function* () {
@@ -45,8 +47,6 @@ function registerPackage() {
45
47
  });
46
48
  }
47
49
  exports.registerPackage = registerPackage;
48
- __exportStar(require("./lib/Types"), exports);
49
- __exportStar(require("./lib/NodeHeap"), exports);
50
50
  /** @internal */
51
51
  var Config_1 = require("./lib/Config");
52
52
  Object.defineProperty(exports, "config", { enumerable: true, get: function () { return __importDefault(Config_1).default; } });
@@ -81,6 +81,9 @@ Object.defineProperty(exports, "analysis", { enumerable: true, get: function ()
81
81
  var Constant_1 = require("./lib/Constant");
82
82
  Object.defineProperty(exports, "constant", { enumerable: true, get: function () { return __importDefault(Constant_1).default; } });
83
83
  /** @internal */
84
+ var MemoryBarChart_1 = require("./lib/charts/MemoryBarChart");
85
+ Object.defineProperty(exports, "memoryBarChart", { enumerable: true, get: function () { return __importDefault(MemoryBarChart_1).default; } });
86
+ /** @internal */
84
87
  var RunningModes_1 = require("./modes/RunningModes");
85
88
  Object.defineProperty(exports, "modes", { enumerable: true, get: function () { return __importDefault(RunningModes_1).default; } });
86
89
  /** @internal */
@@ -8,8 +8,7 @@
8
8
  * @oncall web_perf_infra
9
9
  */
10
10
  import type { LaunchOptions, Permission } from 'puppeteer';
11
- import type { AnyFunction, AnyValue, IClusterStrategy, IRunningMode, IScenario, Nullable, Optional, QuickExperiment, ILeakFilter, IPackageInfo } from './Types';
12
- import { IHeapConfig } from '..';
11
+ import type { AnyFunction, AnyValue, FileOption, IClusterStrategy, IRunningMode, IScenario, IHeapConfig, Nullable, Optional, QuickExperiment, ILeakFilter, IPackageInfo } from './Types';
13
12
  interface BrowserLaunchArgumentOptions {
14
13
  headless?: boolean;
15
14
  userDataDir?: string;
@@ -217,6 +216,8 @@ export declare class MemLabConfig {
217
216
  set disableWebSecurity(disable: boolean);
218
217
  get disableWebSecurity(): boolean;
219
218
  get browserBinaryPath(): string;
219
+ set defaultFileManagerOption(fileOption: FileOption);
220
+ get defaultFileManagerOption(): FileOption;
220
221
  set reportLeaksInTimers(shouldReport: boolean);
221
222
  get reportLeaksInTimers(): boolean;
222
223
  setDevice(deviceName: string, options?: {
@@ -8,6 +8,29 @@
8
8
  * @format
9
9
  * @oncall web_perf_infra
10
10
  */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
11
34
  var __importDefault = (this && this.__importDefault) || function (mod) {
12
35
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
36
  };
@@ -17,7 +40,7 @@ const path_1 = __importDefault(require("path"));
17
40
  const RunningModes_1 = __importDefault(require("../modes/RunningModes"));
18
41
  const Console_1 = __importDefault(require("./Console"));
19
42
  const Constant_1 = __importDefault(require("./Constant"));
20
- const FileManager_1 = __importDefault(require("./FileManager"));
43
+ const FileManager_1 = __importStar(require("./FileManager"));
21
44
  const InternalValueSetter_1 = require("./InternalValueSetter");
22
45
  const devices = Constant_1.default.isFRL
23
46
  ? {}
@@ -429,6 +452,17 @@ class MemLabConfig {
429
452
  get browserBinaryPath() {
430
453
  return path_1.default.join(this.browserDir, this.browser);
431
454
  }
455
+ // Default input option for file manager.
456
+ // If no other input option is provided, the file manager
457
+ // will generate directories and files based on this default option.
458
+ set defaultFileManagerOption(fileOption) {
459
+ FileManager_1.FileManager.defaultFileOption = fileOption;
460
+ // initialize file and directory paths
461
+ FileManager_1.default.initDirs(this, fileOption);
462
+ }
463
+ get defaultFileManagerOption() {
464
+ return FileManager_1.FileManager.defaultFileOption;
465
+ }
432
466
  set reportLeaksInTimers(shouldReport) {
433
467
  if (shouldReport) {
434
468
  this.removeFromSet(this.nodeNameBlockList, this._timerNodes);
@@ -8,35 +8,32 @@
8
8
  * @oncall web_perf_infra
9
9
  */
10
10
  import type { MemLabConfig } from './Config';
11
- import type { AnyValue, Optional } from './Types';
12
- /** @internal */
13
- export declare type FileOption = {
14
- workDir?: Optional<string>;
15
- clear?: boolean;
16
- transient?: boolean;
17
- };
11
+ import type { AnyValue, FileOption } from './Types';
18
12
  /** @internal */
19
13
  export declare class FileManager {
14
+ private memlabConfigCache;
20
15
  getDefaultWorkDir(): string;
21
16
  generateTmpHeapDir(): string;
22
17
  private static transientInstanceIdx;
18
+ static defaultFileOption: FileOption;
23
19
  getWorkDir(options?: FileOption): string;
24
20
  getChromeBinaryZipFile(): string;
25
21
  getChromeBinaryTimeStampFile(): string;
26
22
  getChromeBinaryDir(): string;
27
- clearUserDataDir(options: FileOption): void;
28
- getDataBaseDir(options: FileOption): string;
23
+ getDataBaseDir(options?: FileOption): string;
29
24
  getCodeDataDir(): string;
30
25
  getClusterSampleDataDir(): string;
31
- getUserDataDir(options: FileOption): string;
32
- getCurDataDir(options: FileOption): string;
26
+ getUserDataDir(options?: FileOption): string;
27
+ clearUserDataDir(options?: FileOption): void;
28
+ getCurDataDir(options?: FileOption): string;
33
29
  getWebSourceDir(options?: FileOption): string;
34
30
  getWebSourceMetaFile(options?: FileOption): string;
35
31
  getDebugDataDir(options?: FileOption): string;
36
32
  getDebugSourceFile(options?: FileOption): string;
37
- getPersistDataDir(options: FileOption): string;
33
+ getPersistDataDir(options?: FileOption): string;
38
34
  getLoggerOutDir(options?: FileOption): string;
39
35
  getHeapAnalysisLogDir(options?: FileOption): string;
36
+ getHeapSaveLogJSONFile(options?: FileOption): string;
40
37
  getTraceClustersDir(options?: FileOption): string;
41
38
  getTraceJSONDir(options?: FileOption): string;
42
39
  getUnclassifiedTraceClusterDir(options?: FileOption): string;
@@ -67,6 +64,7 @@ export declare class FileManager {
67
64
  initNewHeapAnalysisLogFile(options?: FileOption): string;
68
65
  getAndInitTSCompileIntermediateDir(): string;
69
66
  clearDataDirs(options?: FileOption): void;
67
+ removeSnapshotFiles(options?: FileOption): void;
70
68
  emptyDirIfExists(dir: string): void;
71
69
  emptyTraceLogDataDir(options?: FileOption): void;
72
70
  resetBrowserDir(): void;
@@ -13,7 +13,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.FileManager = void 0;
16
- const minimist_1 = __importDefault(require("minimist"));
17
16
  const fs_extra_1 = __importDefault(require("fs-extra"));
18
17
  const os_1 = __importDefault(require("os"));
19
18
  const path_1 = __importDefault(require("path"));
@@ -24,7 +23,7 @@ function joinAndProcessDir(options, ...args) {
24
23
  const filepath = path_1.default.join(...args);
25
24
  if (!fs_extra_1.default.existsSync(filepath)) {
26
25
  try {
27
- fs_extra_1.default.mkdirSync(filepath);
26
+ fs_extra_1.default.mkdirpSync(filepath);
28
27
  }
29
28
  catch (ex) {
30
29
  const err = Utils_1.default.getError(ex);
@@ -37,22 +36,26 @@ function joinAndProcessDir(options, ...args) {
37
36
  }
38
37
  /** @internal */
39
38
  class FileManager {
39
+ constructor() {
40
+ this.memlabConfigCache = null;
41
+ }
40
42
  getDefaultWorkDir() {
41
43
  return path_1.default.join(this.getTmpDir(), 'memlab');
42
44
  }
43
45
  generateTmpHeapDir() {
44
46
  const dirPath = path_1.default.join(this.getTmpDir(), 'memlab-' + Utils_1.default.getUniqueID());
45
47
  if (!fs_extra_1.default.existsSync(dirPath)) {
46
- fs_extra_1.default.mkdirSync(dirPath);
48
+ fs_extra_1.default.mkdirpSync(dirPath);
47
49
  }
48
50
  return dirPath;
49
51
  }
50
- getWorkDir(options = {}) {
52
+ getWorkDir(options = FileManager.defaultFileOption) {
53
+ var _a;
51
54
  // workDir options supercedes all the other options
52
55
  if (options.workDir) {
53
56
  return path_1.default.resolve(options.workDir);
54
57
  }
55
- // transient options supercedes other the CLI options
58
+ // transient options supercedes the other CLI options
56
59
  if (options.transient) {
57
60
  const idx = ++FileManager.transientInstanceIdx;
58
61
  const instanceId = `${process.pid}-${Date.now()}-${idx}`;
@@ -60,8 +63,11 @@ class FileManager {
60
63
  return path_1.default.resolve(workDir);
61
64
  }
62
65
  // workDir from the CLI options
63
- const argv = (0, minimist_1.default)(process.argv.slice(2));
64
- const workDir = argv['work-dir'] || this.getDefaultWorkDir();
66
+ const workDir = FileManager.defaultFileOption.workDir ||
67
+ (
68
+ // in case there is a transcient working directory generated
69
+ (_a = this.memlabConfigCache) === null || _a === void 0 ? void 0 : _a.workDir) ||
70
+ this.getDefaultWorkDir();
65
71
  return path_1.default.resolve(workDir);
66
72
  }
67
73
  getChromeBinaryZipFile() {
@@ -73,11 +79,7 @@ class FileManager {
73
79
  getChromeBinaryDir() {
74
80
  return path_1.default.join(this.getDefaultWorkDir(), 'chrome');
75
81
  }
76
- clearUserDataDir(options) {
77
- const userDataDir = this.getUserDataDir(options);
78
- this.rmDir(userDataDir);
79
- }
80
- getDataBaseDir(options) {
82
+ getDataBaseDir(options = FileManager.defaultFileOption) {
81
83
  return path_1.default.join(this.getWorkDir(options), 'data');
82
84
  }
83
85
  getCodeDataDir() {
@@ -86,66 +88,74 @@ class FileManager {
86
88
  getClusterSampleDataDir() {
87
89
  return path_1.default.join(this.getCodeDataDir(), 'cluster');
88
90
  }
89
- getUserDataDir(options) {
91
+ getUserDataDir(options = FileManager.defaultFileOption) {
90
92
  return path_1.default.join(this.getDataBaseDir(options), 'profile');
91
93
  }
92
- getCurDataDir(options) {
94
+ clearUserDataDir(options = FileManager.defaultFileOption) {
95
+ const userDataDir = this.getUserDataDir(options);
96
+ this.rmDir(userDataDir);
97
+ }
98
+ getCurDataDir(options = FileManager.defaultFileOption) {
93
99
  return path_1.default.join(this.getDataBaseDir(options), 'cur');
94
100
  }
95
- getWebSourceDir(options = {}) {
101
+ getWebSourceDir(options = FileManager.defaultFileOption) {
96
102
  return path_1.default.join(this.getCurDataDir(options), 'code');
97
103
  }
98
- getWebSourceMetaFile(options = {}) {
104
+ getWebSourceMetaFile(options = FileManager.defaultFileOption) {
99
105
  return path_1.default.join(this.getWebSourceDir(options), 'files.json');
100
106
  }
101
- getDebugDataDir(options = {}) {
107
+ getDebugDataDir(options = FileManager.defaultFileOption) {
102
108
  return path_1.default.join(this.getDataBaseDir(options), 'debug');
103
109
  }
104
- getDebugSourceFile(options = {}) {
110
+ getDebugSourceFile(options = FileManager.defaultFileOption) {
105
111
  return path_1.default.join(this.getDebugDataDir(options), 'file.js');
106
112
  }
107
- getPersistDataDir(options) {
113
+ getPersistDataDir(options = FileManager.defaultFileOption) {
108
114
  return path_1.default.join(this.getDataBaseDir(options), 'persist');
109
115
  }
110
- getLoggerOutDir(options = {}) {
116
+ getLoggerOutDir(options = FileManager.defaultFileOption) {
111
117
  return path_1.default.join(this.getDataBaseDir(options), 'logger');
112
118
  }
113
119
  // all heap analysis results generated
114
- getHeapAnalysisLogDir(options = {}) {
120
+ getHeapAnalysisLogDir(options = FileManager.defaultFileOption) {
115
121
  return path_1.default.join(this.getLoggerOutDir(options), 'heap-analysis');
116
122
  }
123
+ // memlab save-heap result log file
124
+ getHeapSaveLogJSONFile(options = FileManager.defaultFileOption) {
125
+ return path_1.default.join(this.getHeapAnalysisLogDir(options), 'save-heap.json');
126
+ }
117
127
  // all trace clusters generated from the current run
118
- getTraceClustersDir(options = {}) {
128
+ getTraceClustersDir(options = FileManager.defaultFileOption) {
119
129
  return path_1.default.join(this.getLoggerOutDir(options), 'trace-clusters');
120
130
  }
121
131
  // stores JSON file (with heap object and reference details for visualization)
122
132
  // of all trace clusters (each cluster has a representative trace)
123
- getTraceJSONDir(options = {}) {
133
+ getTraceJSONDir(options = FileManager.defaultFileOption) {
124
134
  return path_1.default.join(this.getLoggerOutDir(options), 'trace-jsons');
125
135
  }
126
- getUnclassifiedTraceClusterDir(options = {}) {
136
+ getUnclassifiedTraceClusterDir(options = FileManager.defaultFileOption) {
127
137
  return path_1.default.join(this.getLoggerOutDir(options), 'unclassified-clusters');
128
138
  }
129
- getUniqueTraceClusterDir(options = {}) {
139
+ getUniqueTraceClusterDir(options = FileManager.defaultFileOption) {
130
140
  return path_1.default.join(this.getLoggerOutDir(options), 'unique-trace-clusters');
131
141
  }
132
- getNewUniqueTraceClusterDir(options = {}) {
142
+ getNewUniqueTraceClusterDir(options = FileManager.defaultFileOption) {
133
143
  return path_1.default.join(this.getUniqueTraceClusterDir(options), 'add');
134
144
  }
135
- getStaleUniqueTraceClusterDir(options = {}) {
145
+ getStaleUniqueTraceClusterDir(options = FileManager.defaultFileOption) {
136
146
  return path_1.default.join(this.getUniqueTraceClusterDir(options), 'delete');
137
147
  }
138
- getAllClusterSummaryFile(options = {}) {
148
+ getAllClusterSummaryFile(options = FileManager.defaultFileOption) {
139
149
  return path_1.default.join(this.getUniqueTraceClusterDir(options), 'unique-clusters-summary.txt');
140
150
  }
141
- getExistingUniqueTraceClusterDir(options = {}) {
151
+ getExistingUniqueTraceClusterDir(options = FileManager.defaultFileOption) {
142
152
  return path_1.default.join(this.getUniqueTraceClusterDir(options), 'existing');
143
153
  }
144
154
  getAllFilesInDir(dir) {
145
155
  const files = fs_extra_1.default.readdirSync(dir);
146
156
  return files.map((file) => path_1.default.join(dir, file));
147
157
  }
148
- getDataOutDir(options = {}) {
158
+ getDataOutDir(options = FileManager.defaultFileOption) {
149
159
  return path_1.default.join(this.getDataBaseDir(options), 'out');
150
160
  }
151
161
  getCoreProjectBaseDir() {
@@ -157,19 +167,19 @@ class FileManager {
157
167
  getDocDir() {
158
168
  return path_1.default.join(this.getMonoRepoDir(), 'website', 'docs');
159
169
  }
160
- getReportOutDir(options = {}) {
170
+ getReportOutDir(options = FileManager.defaultFileOption) {
161
171
  return path_1.default.join(this.getPersistDataDir(options), 'reports');
162
172
  }
163
- getPreviewReportDir(options = {}) {
173
+ getPreviewReportDir(options = FileManager.defaultFileOption) {
164
174
  return path_1.default.join(this.getPersistDataDir(options), 'report-preview');
165
175
  }
166
- getLeakSummaryFile(options = {}) {
176
+ getLeakSummaryFile(options = FileManager.defaultFileOption) {
167
177
  return path_1.default.join(this.getDataOutDir(options), 'leaks.txt');
168
178
  }
169
- getRunMetaFile(options = {}) {
179
+ getRunMetaFile(options = FileManager.defaultFileOption) {
170
180
  return path_1.default.join(this.getCurDataDir(options), 'run-meta.json');
171
181
  }
172
- getSnapshotSequenceMetaFile(options = {}) {
182
+ getSnapshotSequenceMetaFile(options = FileManager.defaultFileOption) {
173
183
  return path_1.default.join(this.getCurDataDir(options), 'snap-seq.json');
174
184
  }
175
185
  getInputDataDir() {
@@ -192,6 +202,7 @@ class FileManager {
192
202
  }
193
203
  return ret;
194
204
  }
205
+ // system default tmp dir
195
206
  getTmpDir() {
196
207
  return os_1.default.tmpdir();
197
208
  }
@@ -211,7 +222,7 @@ class FileManager {
211
222
  return { controlWorkDir, testWorkDir };
212
223
  }
213
224
  // create a unique log file created for heap analysis output
214
- initNewHeapAnalysisLogFile(options = {}) {
225
+ initNewHeapAnalysisLogFile(options = FileManager.defaultFileOption) {
215
226
  const dir = this.getHeapAnalysisLogDir(options);
216
227
  const file = path_1.default.join(dir, `analysis-${Utils_1.default.getUniqueID()}-out.log`);
217
228
  if (!fs_extra_1.default.existsSync(file)) {
@@ -222,10 +233,10 @@ class FileManager {
222
233
  getAndInitTSCompileIntermediateDir() {
223
234
  const dir = path_1.default.join(this.getTmpDir(), 'memlab-code');
224
235
  this.rmDir(dir);
225
- fs_extra_1.default.mkdirSync(dir);
236
+ fs_extra_1.default.mkdirpSync(dir);
226
237
  return dir;
227
238
  }
228
- clearDataDirs(options = {}) {
239
+ clearDataDirs(options = FileManager.defaultFileOption) {
229
240
  const curDataDir = this.getCurDataDir(options);
230
241
  if (!fs_extra_1.default.existsSync(curDataDir)) {
231
242
  return;
@@ -244,12 +255,29 @@ class FileManager {
244
255
  }
245
256
  }
246
257
  }
258
+ removeSnapshotFiles(options = FileManager.defaultFileOption) {
259
+ const curDataDir = this.getCurDataDir(options);
260
+ if (!fs_extra_1.default.existsSync(curDataDir)) {
261
+ return;
262
+ }
263
+ const dataSuffix = ['.heapsnapshot'];
264
+ const files = fs_extra_1.default.readdirSync(curDataDir);
265
+ for (const file of files) {
266
+ inner: for (const suffix of dataSuffix) {
267
+ if (file.endsWith(suffix)) {
268
+ const filepath = path_1.default.join(curDataDir, file);
269
+ fs_extra_1.default.unlinkSync(filepath);
270
+ break inner;
271
+ }
272
+ }
273
+ }
274
+ }
247
275
  emptyDirIfExists(dir) {
248
276
  if (this.isDirectory(dir)) {
249
277
  fs_extra_1.default.emptyDirSync(dir);
250
278
  }
251
279
  }
252
- emptyTraceLogDataDir(options = {}) {
280
+ emptyTraceLogDataDir(options = FileManager.defaultFileOption) {
253
281
  // all leak trace clusters
254
282
  this.emptyDirIfExists(this.getTraceClustersDir(options));
255
283
  // all JSON files for trace visualization
@@ -290,7 +318,7 @@ class FileManager {
290
318
  this.iterateAllFiles(filepath, callback);
291
319
  });
292
320
  }
293
- rmWorkDir(options = {}) {
321
+ rmWorkDir(options = FileManager.defaultFileOption) {
294
322
  try {
295
323
  this.rmDir(this.getWorkDir(options));
296
324
  }
@@ -303,11 +331,16 @@ class FileManager {
303
331
  const internalDir = Constant_1.default.internalDir;
304
332
  return filePath.includes(`${sep}${internalDir}${sep}`);
305
333
  }
306
- initDirs(config, options = {}) {
334
+ initDirs(config, options = FileManager.defaultFileOption) {
335
+ // cache the last processed memlab config instance
336
+ // the instance should be a singleton
337
+ this.memlabConfigCache = config;
307
338
  config.monoRepoDir = Constant_1.default.monoRepoDir;
308
339
  // make sure getWorkDir is called first before
309
340
  // any other get file or get dir calls
310
341
  const workDir = this.getWorkDir(options);
342
+ // remember the current working directory
343
+ // especially if this is a transcient working directory
311
344
  config.workDir = joinAndProcessDir(options, workDir);
312
345
  options = Object.assign(Object.assign({}, options), { workDir });
313
346
  config.dataBaseDir = joinAndProcessDir(options, this.getDataBaseDir(options));
@@ -356,4 +389,5 @@ class FileManager {
356
389
  }
357
390
  exports.FileManager = FileManager;
358
391
  FileManager.transientInstanceIdx = 0;
392
+ FileManager.defaultFileOption = {};
359
393
  exports.default = new FileManager();
@@ -7,17 +7,25 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { E2EStepInfo, HeapNodeIdSet, IHeapSnapshot, IMemoryAnalystOptions, IMemoryAnalystSnapshotDiff, IOveralHeapInfo, LeakTracePathItem, Optional, IOveralLeakInfo, TraceCluster, ISerializedInfo } from './Types';
10
+ import type { E2EStepInfo, HeapNodeIdSet, IHeapSnapshot, IMemoryAnalystSnapshotDiff, IOveralHeapInfo, LeakTracePathItem, Optional, IOveralLeakInfo, TraceCluster, ISerializedInfo, DiffLeakOptions } from './Types';
11
11
  import TraceFinder from '../paths/TraceFinder';
12
+ declare type DiffSnapshotsOptions = {
13
+ loadAllSnapshots?: boolean;
14
+ workDir?: string;
15
+ };
16
+ declare type WorkDirOptions = {
17
+ workDir?: string;
18
+ };
12
19
  declare class MemoryAnalyst {
13
20
  checkLeak(): Promise<ISerializedInfo[]>;
21
+ diffLeakByWorkDir(options: DiffLeakOptions): Promise<ISerializedInfo[]>;
22
+ diffMemoryLeakTraces(options: DiffLeakOptions): Promise<ISerializedInfo[]>;
14
23
  detectMemoryLeaks(): Promise<ISerializedInfo[]>;
15
- visualizeMemoryUsage(options?: IMemoryAnalystOptions): void;
16
24
  focus(options?: {
17
25
  file?: string;
18
26
  }): Promise<void>;
19
27
  shouldLoadCompleteSnapshot(tabsOrder: E2EStepInfo[], tab: E2EStepInfo): boolean;
20
- diffSnapshots(loadAll?: boolean): Promise<IMemoryAnalystSnapshotDiff>;
28
+ diffSnapshots(options?: DiffSnapshotsOptions): Promise<IMemoryAnalystSnapshotDiff>;
21
29
  preparePathFinder(snapshot: IHeapSnapshot): TraceFinder;
22
30
  private dumpPageInteractionSummary;
23
31
  private dumpLeakSummaryToConsole;
@@ -29,12 +37,11 @@ declare class MemoryAnalyst {
29
37
  printHeapInfo(leakInfo: IOveralHeapInfo): void;
30
38
  private printHeapAndLeakInfo;
31
39
  private logLeakTraceSummary;
32
- searchLeakedTraces(leakedNodeIds: HeapNodeIdSet, snapshot: IHeapSnapshot): Promise<{
33
- paths: LeakTracePathItem[];
34
- }>;
40
+ filterLeakPaths(leakedNodeIds: HeapNodeIdSet, snapshot: IHeapSnapshot, options?: WorkDirOptions): LeakTracePathItem[];
41
+ findLeakTraces(leakedNodeIds: HeapNodeIdSet, snapshot: IHeapSnapshot, options?: WorkDirOptions): Promise<LeakTracePathItem[]>;
35
42
  /**
36
43
  * Given a set of heap object ids, cluster them based on the similarity
37
- * of their retainer traces and return a
44
+ * of their retainer traces
38
45
  * @param leakedNodeIds
39
46
  * @param snapshot
40
47
  * @returns
@@ -43,7 +50,7 @@ declare class MemoryAnalyst {
43
50
  serializeClusterUpdate(clusters: TraceCluster[], options?: {
44
51
  reclusterOnly?: boolean;
45
52
  }): Promise<void>;
46
- dumpPathByNodeId(leakedIdSet: HeapNodeIdSet, snapshot: IHeapSnapshot, nodeIdsInSnapshots: Array<HeapNodeIdSet>, id: number, pathLoaderFile: string, summaryFile: string): void;
53
+ dumpPathByNodeId(leakedIdSet: HeapNodeIdSet, snapshot: IHeapSnapshot, nodeIdsInSnapshots: Array<HeapNodeIdSet>, id: number, pathLoaderFile: string, summaryFile: string, options?: WorkDirOptions): void;
47
54
  }
48
55
  declare const _default: MemoryAnalyst;
49
56
  export default _default;
@@ -22,77 +22,83 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
22
22
  };
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
24
  const fs_1 = __importDefault(require("fs"));
25
- const babar_1 = __importDefault(require("babar"));
26
- const LeakClusterLogger_1 = __importDefault(require("../logger/LeakClusterLogger"));
27
- const LeakTraceDetailsLogger_1 = __importDefault(require("../logger/LeakTraceDetailsLogger"));
28
- const TraceFinder_1 = __importDefault(require("../paths/TraceFinder"));
29
- const TraceBucket_1 = __importDefault(require("../trace-cluster/TraceBucket"));
30
25
  const Config_1 = __importDefault(require("./Config"));
31
26
  const Console_1 = __importDefault(require("./Console"));
32
27
  const Serializer_1 = __importDefault(require("./Serializer"));
33
28
  const Utils_1 = __importDefault(require("./Utils"));
29
+ const FileManager_1 = __importDefault(require("./FileManager"));
30
+ const MemoryBarChart_1 = __importDefault(require("./charts/MemoryBarChart"));
31
+ const LeakClusterLogger_1 = __importDefault(require("../logger/LeakClusterLogger"));
32
+ const LeakTraceDetailsLogger_1 = __importDefault(require("../logger/LeakTraceDetailsLogger"));
33
+ const TraceFinder_1 = __importDefault(require("../paths/TraceFinder"));
34
+ const TraceBucket_1 = __importDefault(require("../trace-cluster/TraceBucket"));
34
35
  const LeakObjectFilter_1 = require("./leak-filters/LeakObjectFilter");
35
36
  const MLTraceSimilarityStrategy_1 = __importDefault(require("../trace-cluster/strategies/MLTraceSimilarityStrategy"));
36
37
  class MemoryAnalyst {
37
38
  checkLeak() {
38
39
  return __awaiter(this, void 0, void 0, function* () {
39
- this.visualizeMemoryUsage();
40
+ MemoryBarChart_1.default.plotMemoryBarChart();
40
41
  Utils_1.default.checkSnapshots();
41
42
  return yield this.detectMemoryLeaks();
42
43
  });
43
44
  }
45
+ diffLeakByWorkDir(options) {
46
+ return __awaiter(this, void 0, void 0, function* () {
47
+ const controlSnapshotDir = FileManager_1.default.getCurDataDir({
48
+ workDir: options.controlWorkDir,
49
+ });
50
+ const treatmentSnapshotDir = FileManager_1.default.getCurDataDir({
51
+ workDir: options.treatmentWorkDir,
52
+ });
53
+ // check control working dir
54
+ Utils_1.default.checkSnapshots({ snapshotDir: controlSnapshotDir });
55
+ // check treatment working dir
56
+ Utils_1.default.checkSnapshots({ snapshotDir: treatmentSnapshotDir });
57
+ // display control and treatment memory
58
+ MemoryBarChart_1.default.plotMemoryBarChart(options);
59
+ return this.diffMemoryLeakTraces(options);
60
+ });
61
+ }
62
+ // find all unique pattern of leaks
63
+ diffMemoryLeakTraces(options) {
64
+ return __awaiter(this, void 0, void 0, function* () {
65
+ Config_1.default.dumpNodeInfo = false;
66
+ // diff snapshots and get control raw paths
67
+ let snapshotDiff = yield this.diffSnapshots({
68
+ loadAllSnapshots: true,
69
+ workDir: options.controlWorkDir,
70
+ });
71
+ const controlLeakPaths = this.filterLeakPaths(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot, { workDir: options.controlWorkDir });
72
+ const controlSnapshot = snapshotDiff.snapshot;
73
+ // diff snapshots and get treatment raw paths
74
+ snapshotDiff = yield this.diffSnapshots({
75
+ loadAllSnapshots: true,
76
+ workDir: options.treatmentWorkDir,
77
+ });
78
+ const treatmentLeakPaths = this.filterLeakPaths(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot, { workDir: options.controlWorkDir });
79
+ const treatmentSnapshot = snapshotDiff.snapshot;
80
+ Console_1.default.topLevel(`${controlLeakPaths.length} traces from control group`);
81
+ Console_1.default.topLevel(`${treatmentLeakPaths.length} traces from treatment group`);
82
+ const result = TraceBucket_1.default.clusterControlTreatmentPaths(controlLeakPaths, controlSnapshot, treatmentLeakPaths, treatmentSnapshot, Utils_1.default.aggregateDominatorMetrics, {
83
+ strategy: Config_1.default.isMLClustering
84
+ ? new MLTraceSimilarityStrategy_1.default()
85
+ : undefined,
86
+ });
87
+ Console_1.default.midLevel(`MemLab found ${result.treatmentOnlyClusters.length} new leak(s) in the treatment group`);
88
+ yield this.serializeClusterUpdate(result.treatmentOnlyClusters);
89
+ // TODO (lgong): log leak traces
90
+ return [];
91
+ });
92
+ }
44
93
  // find all unique pattern of leaks
45
94
  detectMemoryLeaks() {
46
95
  return __awaiter(this, void 0, void 0, function* () {
47
- const snapshotDiff = yield this.diffSnapshots(true);
96
+ const snapshotDiff = yield this.diffSnapshots({ loadAllSnapshots: true });
48
97
  Config_1.default.dumpNodeInfo = false;
49
- const { paths } = yield this.searchLeakedTraces(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot);
98
+ const paths = yield this.findLeakTraces(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot);
50
99
  return LeakTraceDetailsLogger_1.default.logTraces(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot, snapshotDiff.listOfLeakedHeapNodeIdSet, paths, Config_1.default.traceJsonOutDir);
51
100
  });
52
101
  }
53
- visualizeMemoryUsage(options = {}) {
54
- if (Config_1.default.useExternalSnapshot || options.snapshotDir) {
55
- return;
56
- }
57
- const tabsOrder = Utils_1.default.loadTabsOrder();
58
- // if memory usage data is incomplete, skip the visualization
59
- for (const tab of tabsOrder) {
60
- if (!(tab.JSHeapUsedSize > 0)) {
61
- if (Config_1.default.verbose) {
62
- Console_1.default.error('Memory usage data incomplete');
63
- }
64
- return;
65
- }
66
- }
67
- const plotData = tabsOrder.map((tab, idx) => [
68
- idx + 1,
69
- ((tab.JSHeapUsedSize / 100000) | 0) / 10,
70
- ]);
71
- // the graph component cannot handle an array with a single element
72
- while (plotData.length < 2) {
73
- plotData.push([plotData.length + 1, 0]);
74
- }
75
- // plot visual settings
76
- const minY = 1;
77
- const maxY = plotData.reduce((m, v) => Math.max(m, v[1]), 0) * 1.15;
78
- const yFractions = 1;
79
- const yLabelWidth = 1 +
80
- Math.max(minY.toFixed(yFractions).length, maxY.toFixed(yFractions).length);
81
- const maxWidth = process.stdout.columns - 10;
82
- const idealWidth = Math.max(2 * plotData.length + 2 * yLabelWidth, 10);
83
- const plotWidth = Math.min(idealWidth, maxWidth);
84
- Console_1.default.topLevel('Memory usage across all steps:');
85
- Console_1.default.topLevel((0, babar_1.default)(plotData, {
86
- color: 'green',
87
- width: plotWidth,
88
- height: 10,
89
- xFractions: 0,
90
- yFractions,
91
- minY,
92
- maxY,
93
- }));
94
- Console_1.default.topLevel('');
95
- }
96
102
  focus(options = {}) {
97
103
  return __awaiter(this, void 0, void 0, function* () {
98
104
  Console_1.default.overwrite(`Generating report for node @${Config_1.default.focusFiberNodeId}`);
@@ -113,7 +119,7 @@ class MemoryAnalyst {
113
119
  }
114
120
  else {
115
121
  Utils_1.default.checkSnapshots();
116
- const snapshotDiff = yield this.diffSnapshots(true);
122
+ const snapshotDiff = yield this.diffSnapshots({ loadAllSnapshots: true });
117
123
  nodeIdsInSnapshots = snapshotDiff.listOfLeakedHeapNodeIdSet;
118
124
  snapshotLeakedHeapNodeIdSet = snapshotDiff.leakedHeapNodeIdSet;
119
125
  snapshot = snapshotDiff.snapshot;
@@ -130,16 +136,16 @@ class MemoryAnalyst {
130
136
  }
131
137
  return false;
132
138
  }
133
- diffSnapshots(loadAll = false) {
139
+ diffSnapshots(options = {}) {
134
140
  return __awaiter(this, void 0, void 0, function* () {
135
141
  const nodeIdsInSnapshots = [];
136
- const tabsOrder = Utils_1.default.loadTabsOrder();
142
+ const tabsOrder = Utils_1.default.loadTabsOrder(FileManager_1.default.getSnapshotSequenceMetaFile(options));
137
143
  // a set keeping track of node ids generated before the target snapshot
138
144
  const baselineIds = new Set();
139
145
  let collectBaselineIds = true;
140
146
  let targetAllocatedHeapNodeIdSet = null;
141
147
  let leakedHeapNodeIdSet = null;
142
- const options = { verbose: true };
148
+ const parseSnapshotOptions = { verbose: true, workDir: options.workDir };
143
149
  let snapshot = null;
144
150
  for (let i = 0; i < tabsOrder.length; i++) {
145
151
  const tab = tabsOrder[i];
@@ -157,13 +163,13 @@ class MemoryAnalyst {
157
163
  continue;
158
164
  }
159
165
  // in quick mode, there is no need to load all snapshots
160
- if (!loadAll && !tab.type) {
166
+ if (!options.loadAllSnapshots && !tab.type) {
161
167
  continue;
162
168
  }
163
- const file = Utils_1.default.getSnapshotFilePath(tab);
169
+ const file = Utils_1.default.getSnapshotFilePath(tab, options);
164
170
  if (this.shouldLoadCompleteSnapshot(tabsOrder, tab)) {
165
171
  // final snapshot needs to build node index
166
- const opt = Object.assign({ buildNodeIdIndex: true }, options);
172
+ const opt = Object.assign(Object.assign({ buildNodeIdIndex: true }, parseSnapshotOptions), { workDir: options.workDir });
167
173
  snapshot = yield Utils_1.default.getSnapshotFromFile(file, opt);
168
174
  // record Ids in the snapshot
169
175
  snapshot.nodes.forEach(node => {
@@ -171,7 +177,7 @@ class MemoryAnalyst {
171
177
  });
172
178
  }
173
179
  else {
174
- idsInSnapshot = yield Utils_1.default.getSnapshotNodeIdsFromFile(file, options);
180
+ idsInSnapshot = yield Utils_1.default.getSnapshotNodeIdsFromFile(file, parseSnapshotOptions);
175
181
  nodeIdsInSnapshots.pop();
176
182
  nodeIdsInSnapshots.push(idsInSnapshot);
177
183
  }
@@ -234,10 +240,10 @@ class MemoryAnalyst {
234
240
  return finder;
235
241
  }
236
242
  // summarize the page interaction and dump to the leak text summary file
237
- dumpPageInteractionSummary() {
238
- const tabsOrder = Utils_1.default.loadTabsOrder();
243
+ dumpPageInteractionSummary(options = {}) {
244
+ const tabsOrder = Utils_1.default.loadTabsOrder(FileManager_1.default.getSnapshotSequenceMetaFile(options));
239
245
  const tabsOrderStr = Serializer_1.default.summarizeTabsOrder(tabsOrder);
240
- fs_1.default.writeFileSync(Config_1.default.exploreResultFile, tabsOrderStr, 'UTF-8');
246
+ fs_1.default.writeFileSync(FileManager_1.default.getLeakSummaryFile(options), tabsOrderStr, 'UTF-8');
241
247
  }
242
248
  // summarize the leak and print the info in console
243
249
  dumpLeakSummaryToConsole(leakedNodeIds, snapshot) {
@@ -318,9 +324,9 @@ class MemoryAnalyst {
318
324
  Console_1.default.topLevel(`· ${name}: ${value}`);
319
325
  });
320
326
  }
321
- printHeapAndLeakInfo(leakedNodeIds, snapshot) {
327
+ printHeapAndLeakInfo(leakedNodeIds, snapshot, options = {}) {
322
328
  // write page interaction summary to the leaks text file
323
- this.dumpPageInteractionSummary();
329
+ this.dumpPageInteractionSummary(options);
324
330
  // dump leak summry to console
325
331
  this.dumpLeakSummaryToConsole(leakedNodeIds, snapshot);
326
332
  // get aggregated leak info
@@ -329,42 +335,46 @@ class MemoryAnalyst {
329
335
  this.printHeapInfo(heapInfo);
330
336
  }
331
337
  }
332
- logLeakTraceSummary(trace, nodeIdInPaths, snapshot) {
338
+ logLeakTraceSummary(trace, nodeIdInPaths, snapshot, options = {}) {
333
339
  if (!Config_1.default.isFullRun) {
334
340
  return;
335
341
  }
336
342
  // convert the path to a string
337
343
  const pathStr = Serializer_1.default.summarizePath(trace, nodeIdInPaths, snapshot);
338
- fs_1.default.appendFileSync(Config_1.default.exploreResultFile, `\n\n${pathStr}\n\n`, 'UTF-8');
344
+ fs_1.default.appendFileSync(FileManager_1.default.getLeakSummaryFile(options), `\n\n${pathStr}\n\n`, 'UTF-8');
345
+ }
346
+ filterLeakPaths(leakedNodeIds, snapshot, options = {}) {
347
+ const finder = this.preparePathFinder(snapshot);
348
+ this.printHeapAndLeakInfo(leakedNodeIds, snapshot, options);
349
+ // get all leaked objects
350
+ this.filterLeakedObjects(leakedNodeIds, snapshot);
351
+ const nodeIdInPaths = new Set();
352
+ const paths = [];
353
+ let numOfLeakedObjects = 0;
354
+ let i = 0;
355
+ // analysis for each node
356
+ Utils_1.default.applyToNodes(leakedNodeIds, snapshot, node => {
357
+ if (!Config_1.default.isContinuousTest && ++i % 11 === 0) {
358
+ Console_1.default.overwrite(`progress: ${i} / ${leakedNodeIds.size} @${node.id}`);
359
+ }
360
+ // BFS search for path from the leaked node to GC roots
361
+ const p = finder.getPathToGCRoots(snapshot, node);
362
+ if (!p || !Utils_1.default.isInterestingPath(p)) {
363
+ return;
364
+ }
365
+ ++numOfLeakedObjects;
366
+ paths.push(p);
367
+ this.logLeakTraceSummary(p, nodeIdInPaths, snapshot, options);
368
+ }, { reverse: true });
369
+ if (Config_1.default.verbose) {
370
+ Console_1.default.midLevel(`${numOfLeakedObjects} leaked objects`);
371
+ }
372
+ return paths;
339
373
  }
340
374
  // find unique paths of leaked nodes
341
- searchLeakedTraces(leakedNodeIds, snapshot) {
375
+ findLeakTraces(leakedNodeIds, snapshot, options = {}) {
342
376
  return __awaiter(this, void 0, void 0, function* () {
343
- const finder = this.preparePathFinder(snapshot);
344
- this.printHeapAndLeakInfo(leakedNodeIds, snapshot);
345
- // get all leaked objects
346
- this.filterLeakedObjects(leakedNodeIds, snapshot);
347
- const nodeIdInPaths = new Set();
348
- const paths = [];
349
- let numOfLeakedObjects = 0;
350
- let i = 0;
351
- // analysis for each node
352
- Utils_1.default.applyToNodes(leakedNodeIds, snapshot, node => {
353
- if (!Config_1.default.isContinuousTest && ++i % 11 === 0) {
354
- Console_1.default.overwrite(`progress: ${i} / ${leakedNodeIds.size} @${node.id}`);
355
- }
356
- // BFS search for path from the leaked node to GC roots
357
- const p = finder.getPathToGCRoots(snapshot, node);
358
- if (!p || !Utils_1.default.isInterestingPath(p)) {
359
- return;
360
- }
361
- ++numOfLeakedObjects;
362
- paths.push(p);
363
- this.logLeakTraceSummary(p, nodeIdInPaths, snapshot);
364
- }, { reverse: true });
365
- if (Config_1.default.verbose) {
366
- Console_1.default.midLevel(`${numOfLeakedObjects} leaked objects`);
367
- }
377
+ const paths = this.filterLeakPaths(leakedNodeIds, snapshot, options);
368
378
  // cluster traces from the current run
369
379
  const clusters = TraceBucket_1.default.clusterPaths(paths, snapshot, Utils_1.default.aggregateDominatorMetrics, {
370
380
  strategy: Config_1.default.isMLClustering
@@ -378,14 +388,12 @@ class MemoryAnalyst {
378
388
  const clustersUnclassified = TraceBucket_1.default.generateUnClassifiedClusters(paths, snapshot, Utils_1.default.aggregateDominatorMetrics);
379
389
  LeakClusterLogger_1.default.logUnclassifiedClusters(clustersUnclassified);
380
390
  }
381
- return {
382
- paths: clusters.map(c => c.path),
383
- };
391
+ return clusters.map(c => c.path);
384
392
  });
385
393
  }
386
394
  /**
387
395
  * Given a set of heap object ids, cluster them based on the similarity
388
- * of their retainer traces and return a
396
+ * of their retainer traces
389
397
  * @param leakedNodeIds
390
398
  * @param snapshot
391
399
  * @returns
@@ -429,7 +437,7 @@ class MemoryAnalyst {
429
437
  }
430
438
  });
431
439
  }
432
- dumpPathByNodeId(leakedIdSet, snapshot, nodeIdsInSnapshots, id, pathLoaderFile, summaryFile) {
440
+ dumpPathByNodeId(leakedIdSet, snapshot, nodeIdsInSnapshots, id, pathLoaderFile, summaryFile, options = {}) {
433
441
  Console_1.default.overwrite('start analysis...');
434
442
  const finder = this.preparePathFinder(snapshot);
435
443
  const nodeIdInPaths = new Set();
@@ -443,7 +451,7 @@ class MemoryAnalyst {
443
451
  return;
444
452
  }
445
453
  LeakTraceDetailsLogger_1.default.logTrace(leakedIdSet, snapshot, nodeIdsInSnapshots, path, pathLoaderFile);
446
- const tabsOrder = Utils_1.default.loadTabsOrder();
454
+ const tabsOrder = Utils_1.default.loadTabsOrder(FileManager_1.default.getSnapshotSequenceMetaFile(options));
447
455
  const interactionSummary = Serializer_1.default.summarizeTabsOrder(tabsOrder);
448
456
  let pathSummary = Serializer_1.default.summarizePath(path, nodeIdInPaths, snapshot, { color: true });
449
457
  Console_1.default.topLevel(pathSummary);
@@ -43,6 +43,12 @@ export declare type HaltOrThrowOptions = {
43
43
  printCallback?: () => void;
44
44
  };
45
45
  /** @internal */
46
+ export declare type FileOption = {
47
+ workDir?: Optional<string>;
48
+ clear?: boolean;
49
+ transient?: boolean;
50
+ };
51
+ /** @internal */
46
52
  export declare type CLIOptions = {
47
53
  cliArgs: ParsedArgs;
48
54
  configFromOptions?: AnyRecord;
@@ -795,6 +801,15 @@ export declare type TraceClusterMetaInfo = {
795
801
  meta_data: string;
796
802
  };
797
803
  /** @internal */
804
+ export declare type ControlTreatmentClusterResult = {
805
+ controlOnlyClusters: TraceCluster[];
806
+ treatmentOnlyClusters: TraceCluster[];
807
+ hybridClusters: Array<{
808
+ control: TraceCluster;
809
+ treatment: TraceCluster;
810
+ }>;
811
+ };
812
+ /** @internal */
798
813
  export interface E2EInteraction {
799
814
  kind: string;
800
815
  timeout?: number;
@@ -1835,6 +1850,17 @@ export interface IOveralLeakInfo extends Partial<IOveralHeapInfo> {
1835
1850
  leakedAlternateFiberNodeSize: number;
1836
1851
  }
1837
1852
  /** @internal */
1853
+ export declare type DiffLeakOptions = {
1854
+ controlWorkDir: string;
1855
+ treatmentWorkDir: string;
1856
+ };
1857
+ /** @internal */
1858
+ export declare type PlotMemoryOptions = {
1859
+ controlWorkDir?: string;
1860
+ treatmentWorkDir?: string;
1861
+ workDir?: string;
1862
+ } & IMemoryAnalystOptions;
1863
+ /** @internal */
1838
1864
  export interface IMemoryAnalystOptions {
1839
1865
  snapshotDir?: string;
1840
1866
  minSnapshots?: number;
@@ -95,7 +95,9 @@ declare function checkSnapshots(options?: {
95
95
  export declare function resolveSnapshotFilePath(snapshotFile: Nullable<string>): string;
96
96
  declare function getSnapshotDirForAnalysis(): string;
97
97
  declare function getSingleSnapshotFileForAnalysis(): string;
98
- declare function getSnapshotFilePath(tab: E2EStepInfo): string;
98
+ declare function getSnapshotFilePath(tab: E2EStepInfo, options?: {
99
+ workDir?: string;
100
+ }): string;
99
101
  declare function equalOrMatch(v1: any, v2: any): boolean;
100
102
  declare function getSnapshotFilePathWithTabType(type: string | RegExp): Nullable<string>;
101
103
  declare function isMeaningfulNode(node: IHeapNode): boolean;
@@ -122,7 +124,7 @@ declare function getError(maybeError: unknown): Error;
122
124
  declare function isNodeDominatedByDeletionsArray(node: IHeapNode): boolean;
123
125
  declare function getUniqueID(): string;
124
126
  declare function getClosureSourceUrl(node: IHeapNode): Nullable<string>;
125
- export declare function runShell(command: string, options?: ShellOptions): string;
127
+ export declare function runShell(command: string, options?: ShellOptions): Nullable<string>;
126
128
  export declare function getRetainedSize(node: IHeapNode): number;
127
129
  export declare function aggregateDominatorMetrics(ids: HeapNodeIdSet, snapshot: IHeapSnapshot, checkNodeCb: (node: IHeapNode) => boolean, nodeMetricsCb: (node: IHeapNode) => number): number;
128
130
  declare const _default: {
package/dist/lib/Utils.js CHANGED
@@ -1346,13 +1346,17 @@ function getSingleSnapshotFileForAnalysis() {
1346
1346
  }
1347
1347
  return resolveSnapshotFilePath(path);
1348
1348
  }
1349
- function getSnapshotFilePath(tab) {
1349
+ function getSnapshotFilePath(tab, options = {}) {
1350
+ const fileName = `s${tab.idx}.heapsnapshot`;
1351
+ if (options.workDir) {
1352
+ return path_1.default.join(FileManager_1.default.getCurDataDir(options), fileName);
1353
+ }
1350
1354
  if (!Config_1.default.useExternalSnapshot) {
1351
- return path_1.default.join(Config_1.default.curDataDir, `s${tab.idx}.heapsnapshot`);
1355
+ return path_1.default.join(Config_1.default.curDataDir, fileName);
1352
1356
  }
1353
1357
  // if we are loading snapshot from external snapshot dir
1354
1358
  if (Config_1.default.externalSnapshotDir) {
1355
- return path_1.default.join(Config_1.default.externalSnapshotDir, `s${tab.idx}.heapsnapshot`);
1359
+ return path_1.default.join(Config_1.default.externalSnapshotDir, fileName);
1356
1360
  }
1357
1361
  return Config_1.default.externalSnapshotFilePaths[tab.idx - 1];
1358
1362
  }
@@ -1764,7 +1768,7 @@ function runShell(command, options = {}) {
1764
1768
  if (process_1.default.platform !== 'win32') {
1765
1769
  execOptions.shell = '/bin/bash';
1766
1770
  }
1767
- let ret = '';
1771
+ let ret = null;
1768
1772
  try {
1769
1773
  ret = child_process_1.default.execSync(command, execOptions);
1770
1774
  }
@@ -0,0 +1,20 @@
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
+ import type { PlotMemoryOptions } from '../Types';
11
+ declare class MemoryBarChart {
12
+ plotMemoryBarChart(options?: PlotMemoryOptions): void;
13
+ private loadPlotDataFromTabsOrder;
14
+ private loadPlotDataFromWorkDir;
15
+ private loadPlotData;
16
+ private mergePlotData;
17
+ }
18
+ declare const _default: MemoryBarChart;
19
+ export default _default;
20
+ //# sourceMappingURL=MemoryBarChart.d.ts.map
@@ -0,0 +1,110 @@
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
+ 'use strict';
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const babar_1 = __importDefault(require("babar"));
16
+ const Config_1 = __importDefault(require("../Config"));
17
+ const Console_1 = __importDefault(require("../Console"));
18
+ const Utils_1 = __importDefault(require("../Utils"));
19
+ const FileManager_1 = __importDefault(require("../FileManager"));
20
+ class MemoryBarChart {
21
+ plotMemoryBarChart(options = {}) {
22
+ if (Config_1.default.useExternalSnapshot || options.snapshotDir) {
23
+ return;
24
+ }
25
+ let plotData;
26
+ try {
27
+ plotData = this.loadPlotData(options);
28
+ }
29
+ catch (ex) {
30
+ Console_1.default.warning(`plot data not load correctly: ${Utils_1.default.getError(ex).message}`);
31
+ return;
32
+ }
33
+ // normalize plot data
34
+ const minY = 1;
35
+ const maxY = plotData.reduce((m, v) => Math.max(m, v[1]), 0) * 1.15;
36
+ const yFractions = 1;
37
+ const yLabelWidth = 1 +
38
+ Math.max(minY.toFixed(yFractions).length, maxY.toFixed(yFractions).length);
39
+ const maxWidth = process.stdout.columns - 10;
40
+ const idealWidth = Math.max(2 * plotData.length + 2 * yLabelWidth, 10);
41
+ const plotWidth = Math.min(idealWidth, maxWidth);
42
+ Console_1.default.topLevel('Memory usage across all steps:');
43
+ Console_1.default.topLevel((0, babar_1.default)(plotData, {
44
+ color: 'green',
45
+ width: plotWidth,
46
+ height: 10,
47
+ xFractions: 0,
48
+ yFractions,
49
+ minY,
50
+ maxY,
51
+ }));
52
+ Console_1.default.topLevel('');
53
+ }
54
+ loadPlotDataFromTabsOrder(tabsOrder) {
55
+ for (const tab of tabsOrder) {
56
+ if (!(tab.JSHeapUsedSize > 0)) {
57
+ if (Config_1.default.verbose) {
58
+ Console_1.default.error('Memory usage data incomplete');
59
+ }
60
+ return [];
61
+ }
62
+ }
63
+ const plotData = tabsOrder.map((tab, idx) => [
64
+ idx + 1,
65
+ ((tab.JSHeapUsedSize / 100000) | 0) / 10,
66
+ ]);
67
+ // the graph component cannot handle an array with a single element
68
+ while (plotData.length < 2) {
69
+ plotData.push([plotData.length + 1, 0]);
70
+ }
71
+ return plotData;
72
+ }
73
+ loadPlotDataFromWorkDir(options = {}) {
74
+ const tabsOrder = Utils_1.default.loadTabsOrder(FileManager_1.default.getSnapshotSequenceMetaFile(options));
75
+ return this.loadPlotDataFromTabsOrder(tabsOrder);
76
+ }
77
+ loadPlotData(options = {}) {
78
+ // plot data for a single run
79
+ if (!options.controlWorkDir && !options.treatmentWorkDir) {
80
+ return this.loadPlotDataFromWorkDir(options);
81
+ }
82
+ // plot data for control and test run
83
+ const controlPlotData = this.loadPlotDataFromWorkDir({
84
+ workDir: options.controlWorkDir,
85
+ });
86
+ const testPlotData = this.loadPlotDataFromWorkDir({
87
+ workDir: options.treatmentWorkDir,
88
+ });
89
+ // merge plot data
90
+ return this.mergePlotData([controlPlotData, testPlotData]);
91
+ }
92
+ mergePlotData(plotDataArray) {
93
+ const plotData = [];
94
+ let xIndex = 1; // starts from 1
95
+ for (let i = 0; i < plotDataArray.length; ++i) {
96
+ const data = plotDataArray[i];
97
+ for (const [, yValue] of data) {
98
+ plotData.push([xIndex++, yValue]);
99
+ }
100
+ // push blank separators
101
+ if (i < plotDataArray.length - 1) {
102
+ for (let k = 0; k < 3; ++k) {
103
+ plotData.push([xIndex++, 0]);
104
+ }
105
+ }
106
+ }
107
+ return plotData;
108
+ }
109
+ }
110
+ exports.default = new MemoryBarChart();
@@ -7,13 +7,16 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { IHeapNode, IHeapSnapshot, LeakTrace, LeakTracePathItem, Optional, TraceCluster, TraceClusterDiff, IClusterStrategy } from '../lib/Types';
10
+ import type { IHeapNode, IHeapSnapshot, LeakTrace, LeakTracePathItem, Optional, TraceCluster, TraceClusterDiff, IClusterStrategy, ControlTreatmentClusterResult } from '../lib/Types';
11
11
  import type { NormalizedTraceElement } from './TraceElement';
12
12
  declare type AggregateNodeCb = (ids: Set<number>, snapshot: IHeapSnapshot, checkCb: (node: IHeapNode) => boolean, calculateCb: (node: IHeapNode) => number) => number;
13
13
  export default class NormalizedTrace {
14
14
  private trace;
15
15
  private traceSummary;
16
16
  constructor(p?: LeakTracePathItem | null, snapshot?: IHeapSnapshot | null);
17
+ static getPathLastNode(p: LeakTracePathItem, options?: {
18
+ untilFirstDetachedDOMElem?: boolean;
19
+ }): Optional<IHeapNode>;
17
20
  static pathToTrace(p: LeakTracePathItem, options?: {
18
21
  untilFirstDetachedDOMElem?: boolean;
19
22
  }): NormalizedTraceElement[];
@@ -30,6 +33,12 @@ export default class NormalizedTrace {
30
33
  static clusterPaths(paths: LeakTracePathItem[], snapshot: IHeapSnapshot, aggregateDominatorMetrics: AggregateNodeCb, option?: {
31
34
  strategy?: IClusterStrategy;
32
35
  }): TraceCluster[];
36
+ private static buildTraceToPathMap;
37
+ private static pushLeakPathToCluster;
38
+ private static initEmptyCluster;
39
+ static clusterControlTreatmentPaths(controlPaths: LeakTracePathItem[], controlSnapshot: IHeapSnapshot, treatmentPaths: LeakTracePathItem[], treatmentSnapshot: IHeapSnapshot, aggregateDominatorMetrics: AggregateNodeCb, option?: {
40
+ strategy?: IClusterStrategy;
41
+ }): ControlTreatmentClusterResult;
33
42
  static generateUnClassifiedClusters(paths: LeakTracePathItem[], snapshot: IHeapSnapshot, aggregateDominatorMetrics: AggregateNodeCb): TraceCluster[];
34
43
  static loadCluster(): NormalizedTrace[];
35
44
  static saveCluster(clusters: NormalizedTrace[]): void;
@@ -38,6 +38,27 @@ class NormalizedTrace {
38
38
  : '';
39
39
  }
40
40
  }
41
+ static getPathLastNode(p, options = {}) {
42
+ const skipRest = !!options.untilFirstDetachedDOMElem;
43
+ const shouldSkip = (node) => {
44
+ // only consider the trace from GC root to the first detached element
45
+ // NOTE: do not use utils.isDetachedDOMNode, which relies on
46
+ // the fact that p.node is a HeapNode
47
+ return (skipRest &&
48
+ node.name.startsWith('Detached ') &&
49
+ node.name !== 'Detached InternalNode');
50
+ };
51
+ let curItem = p;
52
+ while (curItem.next) {
53
+ if (curItem.node) {
54
+ if (shouldSkip(curItem.node)) {
55
+ break;
56
+ }
57
+ }
58
+ curItem = curItem.next;
59
+ }
60
+ return curItem === null || curItem === void 0 ? void 0 : curItem.node;
61
+ }
41
62
  // convert path to leak trace
42
63
  static pathToTrace(p, options = {}) {
43
64
  const skipRest = !!options.untilFirstDetachedDOMElem;
@@ -114,6 +135,13 @@ class NormalizedTrace {
114
135
  if (Math.random() < sampleRatio) {
115
136
  ret.push(p);
116
137
  }
138
+ else {
139
+ // force sample objects with non-trvial self size
140
+ const lastNode = NormalizedTrace.getPathLastNode(p);
141
+ if (lastNode && lastNode.self_size >= 100000) {
142
+ ret.push(p);
143
+ }
144
+ }
117
145
  }
118
146
  return ret;
119
147
  }
@@ -233,6 +261,109 @@ class NormalizedTrace {
233
261
  clusters.sort((c1, c2) => { var _a, _b; return ((_a = c2.retainedSize) !== null && _a !== void 0 ? _a : 0) - ((_b = c1.retainedSize) !== null && _b !== void 0 ? _b : 0); });
234
262
  return clusters;
235
263
  }
264
+ static buildTraceToPathMap(paths) {
265
+ const traceToPathMap = new Map();
266
+ for (const p of paths) {
267
+ const trace = NormalizedTrace.pathToTrace(p, {
268
+ untilFirstDetachedDOMElem: true,
269
+ });
270
+ traceToPathMap.set(trace, p);
271
+ }
272
+ return traceToPathMap;
273
+ }
274
+ static pushLeakPathToCluster(traceToPathMap, trace, cluster) {
275
+ // if this is a control path, update control cluster
276
+ const curPath = traceToPathMap.get(trace);
277
+ if (cluster.count === 0) {
278
+ cluster.path = curPath;
279
+ // add representative object id if there is one
280
+ const lastNode = trace[trace.length - 1];
281
+ if ('id' in lastNode) {
282
+ cluster.id = lastNode.id;
283
+ }
284
+ }
285
+ cluster.count = cluster.count + 1;
286
+ NormalizedTrace.addLeakedNodeToCluster(cluster, curPath);
287
+ }
288
+ static initEmptyCluster(snapshot) {
289
+ return {
290
+ path: {},
291
+ count: 0,
292
+ snapshot,
293
+ retainedSize: 0,
294
+ leakedNodeIds: new Set(),
295
+ };
296
+ }
297
+ static clusterControlTreatmentPaths(controlPaths, controlSnapshot, treatmentPaths, treatmentSnapshot, aggregateDominatorMetrics, option = {}) {
298
+ const result = {
299
+ controlOnlyClusters: [],
300
+ treatmentOnlyClusters: [],
301
+ hybridClusters: [],
302
+ };
303
+ Console_1.default.overwrite('Clustering leak traces');
304
+ if (controlPaths.length === 0 && treatmentPaths.length === 0) {
305
+ Console_1.default.midLevel('No leaks found');
306
+ return result;
307
+ }
308
+ // sample paths if there are too many
309
+ controlPaths = this.samplePaths(controlPaths);
310
+ treatmentPaths = this.samplePaths(treatmentPaths);
311
+ // build control trace to control path map
312
+ const controlTraceToPathMap = NormalizedTrace.buildTraceToPathMap(controlPaths);
313
+ const controlTraces = Array.from(controlTraceToPathMap.keys());
314
+ // build treatment trace to treatment path map
315
+ const treatmentTraceToPathMap = NormalizedTrace.buildTraceToPathMap(treatmentPaths);
316
+ const treatmentTraces = Array.from(treatmentTraceToPathMap.keys());
317
+ // cluster traces from both the control group and the treatment group
318
+ const { allClusters } = NormalizedTrace.diffTraces([...controlTraces, ...treatmentTraces], [], option);
319
+ // construct TraceCluster from clustering result
320
+ allClusters.forEach((traces) => {
321
+ var _a, _b;
322
+ const controlCluster = NormalizedTrace.initEmptyCluster(controlSnapshot);
323
+ const treatmentCluster = NormalizedTrace.initEmptyCluster(treatmentSnapshot);
324
+ for (const trace of traces) {
325
+ const normalizedTrace = trace;
326
+ if (controlTraceToPathMap.has(normalizedTrace)) {
327
+ NormalizedTrace.pushLeakPathToCluster(controlTraceToPathMap, normalizedTrace, controlCluster);
328
+ }
329
+ else {
330
+ NormalizedTrace.pushLeakPathToCluster(treatmentTraceToPathMap, normalizedTrace, treatmentCluster);
331
+ }
332
+ }
333
+ const controlClusterSize = (_a = controlCluster.count) !== null && _a !== void 0 ? _a : 0;
334
+ const treatmentClusterSize = (_b = treatmentCluster.count) !== null && _b !== void 0 ? _b : 0;
335
+ // calculate aggregated cluster size for control cluster
336
+ if (controlClusterSize > 0) {
337
+ this.calculateClusterRetainedSize(controlCluster, controlSnapshot, aggregateDominatorMetrics);
338
+ }
339
+ // calculate aggregated cluster size for treatment cluster
340
+ if (treatmentClusterSize > 0) {
341
+ this.calculateClusterRetainedSize(treatmentCluster, treatmentSnapshot, aggregateDominatorMetrics);
342
+ }
343
+ if (controlClusterSize === 0) {
344
+ result.treatmentOnlyClusters.push(treatmentCluster);
345
+ }
346
+ else if (treatmentClusterSize === 0) {
347
+ result.controlOnlyClusters.push(controlCluster);
348
+ }
349
+ else {
350
+ result.hybridClusters.push({
351
+ control: controlCluster,
352
+ treatment: treatmentCluster,
353
+ });
354
+ }
355
+ });
356
+ result.treatmentOnlyClusters.sort((c1, c2) => { var _a, _b; return ((_a = c2.retainedSize) !== null && _a !== void 0 ? _a : 0) - ((_b = c1.retainedSize) !== null && _b !== void 0 ? _b : 0); });
357
+ result.controlOnlyClusters.sort((c1, c2) => { var _a, _b; return ((_a = c2.retainedSize) !== null && _a !== void 0 ? _a : 0) - ((_b = c1.retainedSize) !== null && _b !== void 0 ? _b : 0); });
358
+ result.hybridClusters.sort((g1, g2) => {
359
+ var _a, _b, _c, _d;
360
+ return ((_a = g2.control.retainedSize) !== null && _a !== void 0 ? _a : 0) +
361
+ ((_b = g2.treatment.retainedSize) !== null && _b !== void 0 ? _b : 0) -
362
+ ((_c = g1.control.retainedSize) !== null && _c !== void 0 ? _c : 0) -
363
+ ((_d = g1.treatment.retainedSize) !== null && _d !== void 0 ? _d : 0);
364
+ });
365
+ return result;
366
+ }
236
367
  static generateUnClassifiedClusters(paths, snapshot, aggregateDominatorMetrics) {
237
368
  return this.clusterPaths(paths, snapshot, aggregateDominatorMetrics, {
238
369
  strategy: new TraceAsClusterStrategy_1.default(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memlab/core",
3
- "version": "1.1.17",
3
+ "version": "1.1.19",
4
4
  "license": "MIT",
5
5
  "description": "memlab core libraries",
6
6
  "author": "Liang Gong <lgong@fb.com>",