@memlab/core 1.1.20 → 1.1.22

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.
Files changed (48) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +4 -1
  3. package/dist/lib/Config.d.ts +11 -0
  4. package/dist/lib/Config.js +26 -1
  5. package/dist/lib/Constant.js +1 -0
  6. package/dist/lib/FileManager.d.ts +3 -0
  7. package/dist/lib/FileManager.js +10 -1
  8. package/dist/lib/HeapAnalyzer.d.ts +5 -1
  9. package/dist/lib/HeapAnalyzer.js +56 -34
  10. package/dist/lib/RunInfoUtils.d.ts +39 -0
  11. package/dist/lib/RunInfoUtils.js +86 -0
  12. package/dist/lib/TraceSampler.d.ts +36 -0
  13. package/dist/lib/TraceSampler.js +78 -0
  14. package/dist/lib/Types.d.ts +12 -2
  15. package/dist/lib/Utils.d.ts +31 -19
  16. package/dist/lib/Utils.js +124 -145
  17. package/dist/lib/charts/MemoryBarChart.d.ts +1 -0
  18. package/dist/lib/charts/MemoryBarChart.js +18 -3
  19. package/dist/lib/heap-data/HeapStringNode.js +25 -16
  20. package/dist/lib/leak-filters/rules/FilterOverSizedNodeAsLeak.rule.js +50 -0
  21. package/dist/lib/trace-filters/BaseTraceFilter.rule.d.ts +29 -0
  22. package/dist/lib/trace-filters/BaseTraceFilter.rule.js +22 -0
  23. package/dist/lib/trace-filters/LeakTraceFilter.d.ts +20 -0
  24. package/dist/lib/trace-filters/LeakTraceFilter.js +37 -0
  25. package/dist/lib/trace-filters/TraceFilterRuleList.d.ts +13 -0
  26. package/dist/lib/trace-filters/TraceFilterRuleList.js +35 -0
  27. package/dist/lib/trace-filters/rules/FilterAttachedDOMToDetachedDOMTrace.rule.d.ts +15 -0
  28. package/dist/lib/trace-filters/rules/FilterAttachedDOMToDetachedDOMTrace.rule.js +55 -0
  29. package/dist/lib/trace-filters/rules/FilterCppRootsToDetachedDOMTrace.rule.d.ts +15 -0
  30. package/dist/lib/trace-filters/rules/FilterCppRootsToDetachedDOMTrace.rule.js +44 -0
  31. package/dist/lib/trace-filters/rules/FilterDOMNodeChainTrace.rule.d.ts +15 -0
  32. package/dist/lib/trace-filters/rules/FilterDOMNodeChainTrace.rule.js +41 -0
  33. package/dist/lib/trace-filters/rules/FilterHermesTrace.rule.d.ts +15 -0
  34. package/dist/lib/trace-filters/rules/FilterHermesTrace.rule.js +29 -0
  35. package/dist/lib/trace-filters/rules/FilterInternalNodeTrace.rule.d.ts +15 -0
  36. package/dist/lib/trace-filters/rules/FilterInternalNodeTrace.rule.js +57 -0
  37. package/dist/lib/trace-filters/rules/FilterPendingActivitiesTrace.rule.d.ts +15 -0
  38. package/dist/lib/trace-filters/rules/FilterPendingActivitiesTrace.rule.js +62 -0
  39. package/dist/lib/trace-filters/rules/FilterShadowRootTrace.rule.d.ts +15 -0
  40. package/dist/lib/trace-filters/rules/FilterShadowRootTrace.rule.js +44 -0
  41. package/dist/lib/trace-filters/rules/FilterStyleEngineTrace.rule.d.ts +15 -0
  42. package/dist/lib/trace-filters/rules/FilterStyleEngineTrace.rule.js +49 -0
  43. package/dist/logger/LeakClusterLogger.js +1 -0
  44. package/dist/paths/TraceFinder.js +17 -3
  45. package/dist/trace-cluster/TraceBucket.d.ts +1 -1
  46. package/dist/trace-cluster/TraceBucket.js +20 -14
  47. package/dist/trace-cluster/strategies/TraceSimilarityStrategy.js +1 -0
  48. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -26,6 +26,8 @@ export { default as utils } from './lib/Utils';
26
26
  /** @internal */
27
27
  export { default as fileManager } from './lib/FileManager';
28
28
  /** @internal */
29
+ export { default as runInfoUtils } from './lib/RunInfoUtils';
30
+ /** @internal */
29
31
  export * from './lib/FileManager';
30
32
  /** @internal */
31
33
  export { default as serializer } from './lib/Serializer';
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ 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.memoryBarChart = 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.runInfoUtils = 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
41
  __exportStar(require("./lib/Types"), exports);
@@ -67,6 +67,9 @@ Object.defineProperty(exports, "utils", { enumerable: true, get: function () { r
67
67
  var FileManager_1 = require("./lib/FileManager");
68
68
  Object.defineProperty(exports, "fileManager", { enumerable: true, get: function () { return __importDefault(FileManager_1).default; } });
69
69
  /** @internal */
70
+ var RunInfoUtils_1 = require("./lib/RunInfoUtils");
71
+ Object.defineProperty(exports, "runInfoUtils", { enumerable: true, get: function () { return __importDefault(RunInfoUtils_1).default; } });
72
+ /** @internal */
70
73
  __exportStar(require("./lib/FileManager"), exports);
71
74
  /** @internal */
72
75
  var Serializer_1 = require("./lib/Serializer");
@@ -38,6 +38,11 @@ declare type ConfigOption = {
38
38
  workDir?: string;
39
39
  };
40
40
  /** @internal */
41
+ export declare enum TraceObjectMode {
42
+ Default = 1,
43
+ SelectedJSObjects = 2
44
+ }
45
+ /** @internal */
41
46
  export declare enum ErrorHandling {
42
47
  Halt = 1,
43
48
  Throw = 2
@@ -99,6 +104,7 @@ export declare class MemLabConfig {
99
104
  dataBuilderDataDir: string;
100
105
  unclassifiedClusterDir: string;
101
106
  externalCookiesFile: Optional<string>;
107
+ extraRunInfoMap: Map<string, string>;
102
108
  heapConfig: Optional<IHeapConfig>;
103
109
  puppeteerConfig: LaunchOptions & BrowserLaunchArgumentOptions & BrowserConnectOptions;
104
110
  openDevtoolsConsole: boolean;
@@ -176,6 +182,7 @@ export declare class MemLabConfig {
176
182
  nodeIgnoreSetInShape: Set<string>;
177
183
  oversizeObjectAsLeak: boolean;
178
184
  oversizeThreshold: number;
185
+ traceAllObjectsMode: TraceObjectMode;
179
186
  clusterRetainedSizeThreshold: number;
180
187
  externalLeakFilter?: Optional<ILeakFilter>;
181
188
  monoRepoDir: string;
@@ -198,6 +205,9 @@ export declare class MemLabConfig {
198
205
  interceptScript: boolean;
199
206
  isAnalyzingMainThread: boolean;
200
207
  targetWorkerTitle: Nullable<string>;
208
+ noReCluster: boolean;
209
+ maxSamplesForClustering: number;
210
+ filterTraceByName: Nullable<string>;
201
211
  constructor(options?: ConfigOption);
202
212
  private initInternalConfigs;
203
213
  private init;
@@ -225,6 +235,7 @@ export declare class MemLabConfig {
225
235
  setDevice(deviceName: string, options?: {
226
236
  manualOverride?: boolean;
227
237
  }): void;
238
+ setRunInfo(key: string, value: string): void;
228
239
  private removeFromSet;
229
240
  private addToSet;
230
241
  enableXvfb(display: string): void;
@@ -35,7 +35,7 @@ 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.MemLabConfig = exports.ErrorHandling = void 0;
38
+ exports.MemLabConfig = exports.ErrorHandling = exports.TraceObjectMode = void 0;
39
39
  const path_1 = __importDefault(require("path"));
40
40
  const RunningModes_1 = __importDefault(require("../modes/RunningModes"));
41
41
  const Console_1 = __importDefault(require("./Console"));
@@ -57,6 +57,12 @@ const defaultViewport = {
57
57
  deviceScaleFactor: 1,
58
58
  };
59
59
  /** @internal */
60
+ var TraceObjectMode;
61
+ (function (TraceObjectMode) {
62
+ TraceObjectMode[TraceObjectMode["Default"] = 1] = "Default";
63
+ TraceObjectMode[TraceObjectMode["SelectedJSObjects"] = 2] = "SelectedJSObjects";
64
+ })(TraceObjectMode = exports.TraceObjectMode || (exports.TraceObjectMode = {}));
65
+ /** @internal */
60
66
  var ErrorHandling;
61
67
  (function (ErrorHandling) {
62
68
  ErrorHandling[ErrorHandling["Halt"] = 1] = "Halt";
@@ -176,6 +182,17 @@ class MemLabConfig {
176
182
  // if true, split dataset into trunks
177
183
  // with random order for sequential clustering
178
184
  this.seqClusteringIsRandomChunks = false;
185
+ // maximum number of samples as input for leak trace clustering
186
+ this.maxSamplesForClustering = 5000;
187
+ // extra E2E run info (other than the fields defined in
188
+ // RunMetaInfo like app, interaction, browserInfo).
189
+ // Information saved in this map will be
190
+ // auto-serialized to run-meta.json when the file is saved
191
+ // and auto-deserialized from run-meta.json when the file is loaded
192
+ this.extraRunInfoMap = new Map();
193
+ // if specified via CLI options, this will filter leak traces by
194
+ // node and edge names in the leak trace
195
+ this.filterTraceByName = null;
179
196
  }
180
197
  // initialize configurable parameters
181
198
  init(options = {}) {
@@ -203,6 +220,8 @@ class MemLabConfig {
203
220
  this.skipGC = false;
204
221
  // true if running in ContinuousTest
205
222
  this.isContinuousTest = false;
223
+ // true if reclustering is turned off
224
+ this.noReCluster = false;
206
225
  // true if running a local test
207
226
  this.isTest = false;
208
227
  // true if running in local puppeteer mode
@@ -335,6 +354,9 @@ class MemLabConfig {
335
354
  this.oversizeObjectAsLeak = false;
336
355
  // if larger than this threshold, consider as memory leak
337
356
  this.oversizeThreshold = 0;
357
+ // when specified default, this mode will trace/diff all objects
358
+ // you can specified other modes (e.g., selected JS objects only)
359
+ this.traceAllObjectsMode = TraceObjectMode.Default;
338
360
  // only report leak clusters with aggregated retained size
339
361
  // bigger than this threshold
340
362
  this.clusterRetainedSizeThreshold = 0;
@@ -507,6 +529,9 @@ class MemLabConfig {
507
529
  this.puppeteerConfig.defaultViewport = null;
508
530
  this.defaultUserAgent = null;
509
531
  }
532
+ setRunInfo(key, value) {
533
+ this.extraRunInfoMap.set(key, value);
534
+ }
510
535
  removeFromSet(set, list) {
511
536
  for (const v of list) {
512
537
  set.delete(v);
@@ -46,6 +46,7 @@ const constants = {
46
46
  '(Write barrier)',
47
47
  '(Retain maps)',
48
48
  '(Unknown)',
49
+ '<Synthetic>',
49
50
  ],
50
51
  namePrefixForScenarioFromFile: '',
51
52
  unset: 'UNSET',
@@ -10,6 +10,8 @@
10
10
  import type { MemLabConfig } from './Config';
11
11
  import type { AnyValue, FileOption } from './Types';
12
12
  /** @internal */
13
+ export declare function joinAndProcessDir(options: FileOption, ...args: AnyValue[]): string;
14
+ /** @internal */
13
15
  export declare class FileManager {
14
16
  private memlabConfigCache;
15
17
  getDefaultWorkDir(): string;
@@ -75,6 +77,7 @@ export declare class FileManager {
75
77
  rmWorkDir(options?: FileOption): void;
76
78
  isWithinInternalDirectory(filePath: string): boolean;
77
79
  createDefaultVisitOrderMetaFile(options?: FileOption): void;
80
+ createDefaultVisitOrderMetaFileWithSingleSnapshot(options: FileOption | undefined, snapshotFile: string): void;
78
81
  initDirs(config: MemLabConfig, options?: FileOption): void;
79
82
  }
80
83
  declare const _default: FileManager;
@@ -12,13 +12,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.FileManager = void 0;
15
+ exports.FileManager = exports.joinAndProcessDir = void 0;
16
16
  const fs_extra_1 = __importDefault(require("fs-extra"));
17
17
  const os_1 = __importDefault(require("os"));
18
18
  const path_1 = __importDefault(require("path"));
19
19
  const Console_1 = __importDefault(require("./Console"));
20
20
  const Constant_1 = __importDefault(require("./Constant"));
21
21
  const Utils_1 = __importDefault(require("./Utils"));
22
+ /** @internal */
22
23
  function joinAndProcessDir(options, ...args) {
23
24
  const filepath = path_1.default.join(...args);
24
25
  if (!fs_extra_1.default.existsSync(filepath)) {
@@ -34,6 +35,7 @@ function joinAndProcessDir(options, ...args) {
34
35
  }
35
36
  return filepath;
36
37
  }
38
+ exports.joinAndProcessDir = joinAndProcessDir;
37
39
  /** @internal */
38
40
  class FileManager {
39
41
  constructor() {
@@ -310,6 +312,9 @@ class FileManager {
310
312
  }
311
313
  }
312
314
  isDirectory(file) {
315
+ if (!fs_extra_1.default.existsSync(file)) {
316
+ return false;
317
+ }
313
318
  const stats = fs_extra_1.default.statSync(file);
314
319
  return stats.isDirectory();
315
320
  }
@@ -356,6 +361,10 @@ class FileManager {
356
361
  }
357
362
  // If there is at least one snapshot, create a snap-seq.json file.
358
363
  // First, get the meta file for leak detection in a single heap snapshot
364
+ this.createDefaultVisitOrderMetaFileWithSingleSnapshot(options, snapshotFile);
365
+ }
366
+ createDefaultVisitOrderMetaFileWithSingleSnapshot(options = FileManager.defaultFileOption, snapshotFile) {
367
+ const snapshotSeqMetaFile = this.getSnapshotSequenceMetaFile(options);
359
368
  const codeDataDir = this.getCodeDataDir();
360
369
  const singleSnapshotMetaFile = path_1.default.join(codeDataDir, 'visit-order-single-snapshot.json');
361
370
  const visitOrder = JSON.parse(fs_extra_1.default.readFileSync(singleSnapshotMetaFile, 'UTF-8'));
@@ -16,6 +16,10 @@ declare type DiffSnapshotsOptions = {
16
16
  declare type WorkDirOptions = {
17
17
  workDir?: string;
18
18
  };
19
+ declare type GetTraceOptions = {
20
+ workDir?: string;
21
+ printConsoleOnly?: boolean;
22
+ };
19
23
  declare class MemoryAnalyst {
20
24
  checkLeak(): Promise<ISerializedInfo[]>;
21
25
  diffLeakByWorkDir(options: DiffLeakOptions): Promise<ISerializedInfo[]>;
@@ -50,7 +54,7 @@ declare class MemoryAnalyst {
50
54
  serializeClusterUpdate(clusters: TraceCluster[], options?: {
51
55
  reclusterOnly?: boolean;
52
56
  }): Promise<void>;
53
- dumpPathByNodeId(leakedIdSet: HeapNodeIdSet, snapshot: IHeapSnapshot, nodeIdsInSnapshots: Array<HeapNodeIdSet>, id: number, pathLoaderFile: string, summaryFile: string, options?: WorkDirOptions): void;
57
+ dumpPathByNodeId(leakedIdSet: HeapNodeIdSet, snapshot: IHeapSnapshot, nodeIdsInSnapshots: Array<HeapNodeIdSet>, id: number, pathLoaderFile: string, summaryFile: string, options?: GetTraceOptions): void;
54
58
  }
55
59
  declare const _default: MemoryAnalyst;
56
60
  export default _default;
@@ -34,6 +34,8 @@ const TraceFinder_1 = __importDefault(require("../paths/TraceFinder"));
34
34
  const TraceBucket_1 = __importDefault(require("../trace-cluster/TraceBucket"));
35
35
  const LeakObjectFilter_1 = require("./leak-filters/LeakObjectFilter");
36
36
  const MLTraceSimilarityStrategy_1 = __importDefault(require("../trace-cluster/strategies/MLTraceSimilarityStrategy"));
37
+ const LeakTraceFilter_1 = require("./trace-filters/LeakTraceFilter");
38
+ const TraceSampler_1 = __importDefault(require("./TraceSampler"));
37
39
  class MemoryAnalyst {
38
40
  checkLeak() {
39
41
  return __awaiter(this, void 0, void 0, function* () {
@@ -44,14 +46,14 @@ class MemoryAnalyst {
44
46
  }
45
47
  diffLeakByWorkDir(options) {
46
48
  return __awaiter(this, void 0, void 0, function* () {
47
- const controlSnapshotDir = FileManager_1.default.getCurDataDir({
48
- workDir: options.controlWorkDir,
49
- });
49
+ const controlSnapshotDirs = options.controlWorkDirs.map(controlWorkDir => FileManager_1.default.getCurDataDir({
50
+ workDir: controlWorkDir,
51
+ }));
50
52
  const treatmentSnapshotDir = FileManager_1.default.getCurDataDir({
51
53
  workDir: options.treatmentWorkDir,
52
54
  });
53
55
  // check control working dir
54
- Utils_1.default.checkSnapshots({ snapshotDir: controlSnapshotDir });
56
+ controlSnapshotDirs.forEach(controlSnapshotDir => Utils_1.default.checkSnapshots({ snapshotDir: controlSnapshotDir }));
55
57
  // check treatment working dir
56
58
  Utils_1.default.checkSnapshots({ snapshotDir: treatmentSnapshotDir });
57
59
  // display control and treatment memory
@@ -63,31 +65,37 @@ class MemoryAnalyst {
63
65
  diffMemoryLeakTraces(options) {
64
66
  return __awaiter(this, void 0, void 0, function* () {
65
67
  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;
68
+ // diff snapshots from control dirs and get control raw paths array
69
+ const controlSnapshots = [];
70
+ const leakPathsFromControlRuns = [];
71
+ for (const controlWorkDir of options.controlWorkDirs) {
72
+ const snapshotDiff = yield this.diffSnapshots({
73
+ loadAllSnapshots: true,
74
+ workDir: controlWorkDir,
75
+ });
76
+ leakPathsFromControlRuns.push(this.filterLeakPaths(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot, { workDir: controlWorkDir }));
77
+ controlSnapshots.push(snapshotDiff.snapshot);
78
+ }
73
79
  // diff snapshots and get treatment raw paths
74
- snapshotDiff = yield this.diffSnapshots({
80
+ const snapshotDiff = yield this.diffSnapshots({
75
81
  loadAllSnapshots: true,
76
82
  workDir: options.treatmentWorkDir,
77
83
  });
78
- const treatmentLeakPaths = this.filterLeakPaths(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot, { workDir: options.controlWorkDir });
84
+ const treatmentLeakPaths = this.filterLeakPaths(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot, { workDir: options.treatmentWorkDir });
79
85
  const treatmentSnapshot = snapshotDiff.snapshot;
80
- Console_1.default.topLevel(`${controlLeakPaths.length} traces from control group`);
86
+ const controlPathCounts = JSON.stringify(leakPathsFromControlRuns.map(leakPaths => leakPaths.length));
87
+ Console_1.default.topLevel(`${controlPathCounts} traces from control group`);
81
88
  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, {
89
+ const result = TraceBucket_1.default.clusterControlTreatmentPaths(leakPathsFromControlRuns, controlSnapshots, treatmentLeakPaths, treatmentSnapshot, Utils_1.default.aggregateDominatorMetrics, {
83
90
  strategy: Config_1.default.isMLClustering
84
91
  ? new MLTraceSimilarityStrategy_1.default()
85
92
  : undefined,
86
93
  });
87
94
  Console_1.default.midLevel(`MemLab found ${result.treatmentOnlyClusters.length} new leak(s) in the treatment group`);
88
95
  yield this.serializeClusterUpdate(result.treatmentOnlyClusters);
89
- // TODO (lgong): log leak traces
90
- return [];
96
+ // serialize JSON file with detailed leak trace information
97
+ const treatmentOnlyPaths = result.treatmentOnlyClusters.map(c => c.path);
98
+ return LeakTraceDetailsLogger_1.default.logTraces(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot, snapshotDiff.listOfLeakedHeapNodeIdSet, treatmentOnlyPaths, Config_1.default.traceJsonOutDir);
91
99
  });
92
100
  }
93
101
  // find all unique pattern of leaks
@@ -124,7 +132,7 @@ class MemoryAnalyst {
124
132
  snapshotLeakedHeapNodeIdSet = snapshotDiff.leakedHeapNodeIdSet;
125
133
  snapshot = snapshotDiff.snapshot;
126
134
  }
127
- this.dumpPathByNodeId(snapshotLeakedHeapNodeIdSet, snapshot, nodeIdsInSnapshots, Config_1.default.focusFiberNodeId, Config_1.default.viewJsonFile, Config_1.default.singleReportSummary);
135
+ this.dumpPathByNodeId(snapshotLeakedHeapNodeIdSet, snapshot, nodeIdsInSnapshots, Config_1.default.focusFiberNodeId, Config_1.default.viewJsonFile, Config_1.default.singleReportSummary, { printConsoleOnly: true });
128
136
  });
129
137
  }
130
138
  shouldLoadCompleteSnapshot(tabsOrder, tab) {
@@ -348,26 +356,37 @@ class MemoryAnalyst {
348
356
  this.printHeapAndLeakInfo(leakedNodeIds, snapshot, options);
349
357
  // get all leaked objects
350
358
  this.filterLeakedObjects(leakedNodeIds, snapshot);
359
+ const leakTraceFilter = new LeakTraceFilter_1.LeakTraceFilter();
351
360
  const nodeIdInPaths = new Set();
352
- const paths = [];
353
- let numOfLeakedObjects = 0;
354
- let i = 0;
361
+ const samplePool = [];
355
362
  // analysis for each node
356
363
  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
364
  // BFS search for path from the leaked node to GC roots
361
365
  const p = finder.getPathToGCRoots(snapshot, node);
362
- if (!p || !Utils_1.default.isInterestingPath(p)) {
366
+ if (p == null ||
367
+ !leakTraceFilter.filter(p, { config: Config_1.default, leakedNodeIds, snapshot })) {
368
+ return;
369
+ }
370
+ // filter leak trace based on CLI-specified node or edge names
371
+ if (!Utils_1.default.pathHasNodeOrEdgeWithName(p, Config_1.default.filterTraceByName)) {
372
+ return;
373
+ }
374
+ // ignore if the leak trace is too long
375
+ if (Utils_1.default.getLeakTracePathLength(p) > 100) {
363
376
  return;
364
377
  }
365
- ++numOfLeakedObjects;
366
- paths.push(p);
367
- this.logLeakTraceSummary(p, nodeIdInPaths, snapshot, options);
378
+ samplePool.push(p);
368
379
  }, { reverse: true });
380
+ const sampler = new TraceSampler_1.default(samplePool.length);
381
+ const paths = samplePool.filter(p => {
382
+ if (sampler.sample()) {
383
+ this.logLeakTraceSummary(p, nodeIdInPaths, snapshot, options);
384
+ return true;
385
+ }
386
+ return false;
387
+ });
369
388
  if (Config_1.default.verbose) {
370
- Console_1.default.midLevel(`${numOfLeakedObjects} leaked objects`);
389
+ Console_1.default.midLevel(`Filter and select ${paths.length} leaked trace`);
371
390
  }
372
391
  return paths;
373
392
  }
@@ -401,11 +420,11 @@ class MemoryAnalyst {
401
420
  clusterHeapObjects(objectIds, snapshot) {
402
421
  const finder = this.preparePathFinder(snapshot);
403
422
  const paths = [];
404
- let i = 0;
423
+ const sampler = new TraceSampler_1.default(objectIds.size);
405
424
  // analysis for each node
406
425
  Utils_1.default.applyToNodes(objectIds, snapshot, node => {
407
- if (++i % 11 === 0) {
408
- Console_1.default.overwrite(`progress: ${i} / ${objectIds.size} @${node.id}`);
426
+ if (!sampler.sample()) {
427
+ return;
409
428
  }
410
429
  // BFS search for path from the leaked node to GC roots
411
430
  const p = finder.getPathToGCRoots(snapshot, node);
@@ -451,10 +470,13 @@ class MemoryAnalyst {
451
470
  return;
452
471
  }
453
472
  LeakTraceDetailsLogger_1.default.logTrace(leakedIdSet, snapshot, nodeIdsInSnapshots, path, pathLoaderFile);
454
- const tabsOrder = Utils_1.default.loadTabsOrder(FileManager_1.default.getSnapshotSequenceMetaFile(options));
455
- const interactionSummary = Serializer_1.default.summarizeTabsOrder(tabsOrder);
456
473
  let pathSummary = Serializer_1.default.summarizePath(path, nodeIdInPaths, snapshot, { color: true });
457
474
  Console_1.default.topLevel(pathSummary);
475
+ if (options.printConsoleOnly) {
476
+ return;
477
+ }
478
+ const tabsOrder = Utils_1.default.loadTabsOrder(FileManager_1.default.getSnapshotSequenceMetaFile(options));
479
+ const interactionSummary = Serializer_1.default.summarizeTabsOrder(tabsOrder);
458
480
  pathSummary = Serializer_1.default.summarizePath(path, nodeIdInPaths, snapshot);
459
481
  const summary = `Page Interaction: \n${interactionSummary}\n\n` +
460
482
  `Path from GC Root to Leaked Object:\n${pathSummary}`;
@@ -0,0 +1,39 @@
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 { Nullable, Optional, RunMetaInfo } from './Types';
11
+ export declare class RunMetaInfoManager {
12
+ getRunMetaFilePath(options?: {
13
+ workDir?: Optional<string>;
14
+ }): string;
15
+ saveRunMetaInfo(runMetaInfo: RunMetaInfo, options?: {
16
+ workDir?: Optional<string>;
17
+ extraRunInfo?: Map<string, string>;
18
+ }): string;
19
+ private loadRunMetaInfoFromFile;
20
+ loadRunMetaInfo(options?: {
21
+ metaFile?: Optional<string>;
22
+ workDir?: Optional<string>;
23
+ }): RunMetaInfo;
24
+ loadRunMetaInfoSilentFail(options?: {
25
+ metaFile?: Optional<string>;
26
+ workDir?: Optional<string>;
27
+ }): Nullable<RunMetaInfo>;
28
+ loadRunMetaExternalTemplate(): RunMetaInfo;
29
+ setConfigFromRunMeta(options?: {
30
+ workDir?: Optional<string>;
31
+ silentFail?: boolean;
32
+ }): void;
33
+ }
34
+ declare const runInfoUtils: {
35
+ runMetaInfoManager: RunMetaInfoManager;
36
+ };
37
+ /** @internal */
38
+ export default runInfoUtils;
39
+ //# sourceMappingURL=RunInfoUtils.d.ts.map
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RunMetaInfoManager = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const BrowserInfo_1 = __importDefault(require("./BrowserInfo"));
9
+ const Config_1 = __importDefault(require("./Config"));
10
+ const Constant_1 = __importDefault(require("./Constant"));
11
+ const FileManager_1 = __importDefault(require("./FileManager"));
12
+ const Utils_1 = __importDefault(require("./Utils"));
13
+ const InternalValueSetter_1 = require("./InternalValueSetter");
14
+ class RunMetaInfoManager {
15
+ getRunMetaFilePath(options) {
16
+ if ((options === null || options === void 0 ? void 0 : options.workDir) != null) {
17
+ return FileManager_1.default.getRunMetaFile({ workDir: options.workDir });
18
+ }
19
+ if (Config_1.default.useExternalSnapshot) {
20
+ return Config_1.default.externalRunMetaFile;
21
+ }
22
+ if (Config_1.default.runMetaFile != null) {
23
+ return Config_1.default.runMetaFile;
24
+ }
25
+ return FileManager_1.default.getRunMetaFile();
26
+ }
27
+ saveRunMetaInfo(runMetaInfo, options) {
28
+ var _a;
29
+ const runMetaFile = this.getRunMetaFilePath(options);
30
+ const serializable = Object.assign(Object.assign({}, runMetaInfo), { extraInfo: Object.assign(Object.assign({}, Utils_1.default.mapToObject(Config_1.default.extraRunInfoMap)), Utils_1.default.mapToObject((_a = options === null || options === void 0 ? void 0 : options.extraRunInfo) !== null && _a !== void 0 ? _a : new Map())) });
31
+ fs_1.default.writeFileSync(runMetaFile, JSON.stringify(serializable, null, 2), 'UTF-8');
32
+ return runMetaFile;
33
+ }
34
+ loadRunMetaInfoFromFile(file) {
35
+ const content = fs_1.default.readFileSync(file, 'UTF-8');
36
+ const runMetaInfo = JSON.parse(content);
37
+ if (runMetaInfo && runMetaInfo.extraInfo) {
38
+ Config_1.default.extraRunInfoMap = Utils_1.default.objectToMap(runMetaInfo.extraInfo);
39
+ }
40
+ return runMetaInfo;
41
+ }
42
+ loadRunMetaInfo(options) {
43
+ const file = (options === null || options === void 0 ? void 0 : options.metaFile) || this.getRunMetaFilePath(options);
44
+ try {
45
+ return this.loadRunMetaInfoFromFile(file);
46
+ }
47
+ catch (_) {
48
+ throw Utils_1.default.haltOrThrow('Run info missing. Please make sure `memlab run` is complete.');
49
+ }
50
+ }
51
+ loadRunMetaInfoSilentFail(options) {
52
+ const file = (options === null || options === void 0 ? void 0 : options.metaFile) || this.getRunMetaFilePath(options);
53
+ try {
54
+ return this.loadRunMetaInfoFromFile(file);
55
+ }
56
+ catch (_) {
57
+ return null;
58
+ }
59
+ }
60
+ loadRunMetaExternalTemplate() {
61
+ const runMetaTemplateFile = FileManager_1.default.getRunMetaExternalTemplateFile();
62
+ return JSON.parse(fs_1.default.readFileSync(runMetaTemplateFile, 'UTF-8'));
63
+ }
64
+ setConfigFromRunMeta(options = {}) {
65
+ const meta = (options === null || options === void 0 ? void 0 : options.silentFail)
66
+ ? this.loadRunMetaInfoSilentFail(options)
67
+ : this.loadRunMetaInfo(options);
68
+ if (meta == null) {
69
+ return;
70
+ }
71
+ if ((meta === null || meta === void 0 ? void 0 : meta.app) == null || (meta === null || meta === void 0 ? void 0 : meta.interaction) == null) {
72
+ if (options === null || options === void 0 ? void 0 : options.silentFail) {
73
+ return;
74
+ }
75
+ throw Utils_1.default.haltOrThrow('No app or interaction infomation');
76
+ }
77
+ Config_1.default.targetApp = meta.app;
78
+ Config_1.default.targetTab = meta.interaction;
79
+ BrowserInfo_1.default.load(meta.browserInfo);
80
+ }
81
+ }
82
+ exports.RunMetaInfoManager = RunMetaInfoManager;
83
+ const runInfoUtils = { runMetaInfoManager: new RunMetaInfoManager() };
84
+ (0, InternalValueSetter_1.setInternalValue)(runInfoUtils, __filename, Constant_1.default.internalDir);
85
+ /** @internal */
86
+ exports.default = runInfoUtils;
@@ -0,0 +1,36 @@
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
+ export declare type TraceSamplerOption = {
11
+ maxSample?: number;
12
+ };
13
+ export default class TraceSampler {
14
+ private maxCount;
15
+ private processed;
16
+ private selected;
17
+ private population;
18
+ constructor(n: number, options?: TraceSamplerOption);
19
+ init(n: number): void;
20
+ private calculateSampleRatio;
21
+ /**
22
+ * The caller decide to give up sampling this time.
23
+ * This `giveup` and the `sample` method in aggregation should be
24
+ * called `this.population` times.
25
+ *
26
+ * For example, if `giveup` is called n1 times,
27
+ * and `sample` is called n2 times, then n1 + n2 === this.population.
28
+ */
29
+ giveup(): void;
30
+ /**
31
+ * This sample method should be called precisely this.population times.
32
+ * @returns true if this sample should be taken
33
+ */
34
+ sample(): boolean;
35
+ }
36
+ //# sourceMappingURL=TraceSampler.d.ts.map
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ *
8
+ * @format
9
+ * @oncall web_perf_infra
10
+ */
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const Config_1 = __importDefault(require("./Config"));
16
+ const Console_1 = __importDefault(require("./Console"));
17
+ const Utils_1 = __importDefault(require("./Utils"));
18
+ class TraceSampler {
19
+ constructor(n, options = {}) {
20
+ var _a;
21
+ // the max number of traces after sampling
22
+ this.maxCount = Config_1.default.maxSamplesForClustering;
23
+ this.processed = 0;
24
+ this.selected = 0;
25
+ this.population = -1;
26
+ this.maxCount = (_a = options.maxSample) !== null && _a !== void 0 ? _a : Config_1.default.maxSamplesForClustering;
27
+ this.init(n);
28
+ }
29
+ init(n) {
30
+ this.processed = 0;
31
+ this.selected = 0;
32
+ this.population = n;
33
+ this.calculateSampleRatio(n);
34
+ }
35
+ calculateSampleRatio(n) {
36
+ const sampleRatio = Math.min(1, this.maxCount / n);
37
+ if (sampleRatio < 1) {
38
+ Console_1.default.warning('Sampling trace due to a large number of traces:');
39
+ Console_1.default.lowLevel(` Number of Traces: ${n}`);
40
+ Console_1.default.lowLevel(` Sampling Ratio: ${Utils_1.default.getReadablePercent(sampleRatio)}`);
41
+ }
42
+ return sampleRatio;
43
+ }
44
+ /**
45
+ * The caller decide to give up sampling this time.
46
+ * This `giveup` and the `sample` method in aggregation should be
47
+ * called `this.population` times.
48
+ *
49
+ * For example, if `giveup` is called n1 times,
50
+ * and `sample` is called n2 times, then n1 + n2 === this.population.
51
+ */
52
+ giveup() {
53
+ ++this.processed;
54
+ }
55
+ /**
56
+ * This sample method should be called precisely this.population times.
57
+ * @returns true if this sample should be taken
58
+ */
59
+ sample() {
60
+ if (this.processed >= this.population) {
61
+ throw Utils_1.default.haltOrThrow(`processing ${this.processed + 1} samples but total population is ${this.population}`);
62
+ }
63
+ // use large number to mod here to avoid too much console I/O
64
+ if (!Config_1.default.isContinuousTest && this.processed % 771 === 0) {
65
+ const percent = Utils_1.default.getReadablePercent(this.processed / this.population);
66
+ Console_1.default.overwrite(`progress: ${this.processed} / ${this.population} (${percent})`);
67
+ }
68
+ const dynamicRatio = (this.maxCount - this.selected) / (this.population - this.processed);
69
+ // increase the counter indicating how many samples has been processed
70
+ ++this.processed;
71
+ if (Math.random() <= dynamicRatio) {
72
+ ++this.selected;
73
+ return true;
74
+ }
75
+ return false;
76
+ }
77
+ }
78
+ exports.default = TraceSampler;