@memlab/core 1.1.18 → 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 */
@@ -11,6 +11,7 @@ import type { MemLabConfig } from './Config';
11
11
  import type { AnyValue, FileOption } from './Types';
12
12
  /** @internal */
13
13
  export declare class FileManager {
14
+ private memlabConfigCache;
14
15
  getDefaultWorkDir(): string;
15
16
  generateTmpHeapDir(): string;
16
17
  private static transientInstanceIdx;
@@ -63,6 +64,7 @@ export declare class FileManager {
63
64
  initNewHeapAnalysisLogFile(options?: FileOption): string;
64
65
  getAndInitTSCompileIntermediateDir(): string;
65
66
  clearDataDirs(options?: FileOption): void;
67
+ removeSnapshotFiles(options?: FileOption): void;
66
68
  emptyDirIfExists(dir: string): void;
67
69
  emptyTraceLogDataDir(options?: FileOption): void;
68
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"));
@@ -37,6 +36,9 @@ 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
  }
@@ -48,11 +50,12 @@ class FileManager {
48
50
  return dirPath;
49
51
  }
50
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() {
@@ -249,6 +255,23 @@ class FileManager {
249
255
  }
250
256
  }
251
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
+ }
252
275
  emptyDirIfExists(dir) {
253
276
  if (this.isDirectory(dir)) {
254
277
  fs_extra_1.default.emptyDirSync(dir);
@@ -309,10 +332,15 @@ class FileManager {
309
332
  return filePath.includes(`${sep}${internalDir}${sep}`);
310
333
  }
311
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;
312
338
  config.monoRepoDir = Constant_1.default.monoRepoDir;
313
339
  // make sure getWorkDir is called first before
314
340
  // any other get file or get dir calls
315
341
  const workDir = this.getWorkDir(options);
342
+ // remember the current working directory
343
+ // especially if this is a transcient working directory
316
344
  config.workDir = joinAndProcessDir(options, workDir);
317
345
  options = Object.assign(Object.assign({}, options), { workDir });
318
346
  config.dataBaseDir = joinAndProcessDir(options, this.getDataBaseDir(options));
@@ -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);
@@ -801,6 +801,15 @@ export declare type TraceClusterMetaInfo = {
801
801
  meta_data: string;
802
802
  };
803
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 */
804
813
  export interface E2EInteraction {
805
814
  kind: string;
806
815
  timeout?: number;
@@ -1841,6 +1850,17 @@ export interface IOveralLeakInfo extends Partial<IOveralHeapInfo> {
1841
1850
  leakedAlternateFiberNodeSize: number;
1842
1851
  }
1843
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 */
1844
1864
  export interface IMemoryAnalystOptions {
1845
1865
  snapshotDir?: string;
1846
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;
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
  }
@@ -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.18",
3
+ "version": "1.1.19",
4
4
  "license": "MIT",
5
5
  "description": "memlab core libraries",
6
6
  "author": "Liang Gong <lgong@fb.com>",