@memlab/core 1.1.21 → 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.
@@ -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
@@ -177,6 +182,7 @@ export declare class MemLabConfig {
177
182
  nodeIgnoreSetInShape: Set<string>;
178
183
  oversizeObjectAsLeak: boolean;
179
184
  oversizeThreshold: number;
185
+ traceAllObjectsMode: TraceObjectMode;
180
186
  clusterRetainedSizeThreshold: number;
181
187
  externalLeakFilter?: Optional<ILeakFilter>;
182
188
  monoRepoDir: string;
@@ -199,6 +205,9 @@ export declare class MemLabConfig {
199
205
  interceptScript: boolean;
200
206
  isAnalyzingMainThread: boolean;
201
207
  targetWorkerTitle: Nullable<string>;
208
+ noReCluster: boolean;
209
+ maxSamplesForClustering: number;
210
+ filterTraceByName: Nullable<string>;
202
211
  constructor(options?: ConfigOption);
203
212
  private initInternalConfigs;
204
213
  private init;
@@ -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,12 +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;
179
187
  // extra E2E run info (other than the fields defined in
180
188
  // RunMetaInfo like app, interaction, browserInfo).
181
189
  // Information saved in this map will be
182
190
  // auto-serialized to run-meta.json when the file is saved
183
191
  // and auto-deserialized from run-meta.json when the file is loaded
184
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;
185
196
  }
186
197
  // initialize configurable parameters
187
198
  init(options = {}) {
@@ -209,6 +220,8 @@ class MemLabConfig {
209
220
  this.skipGC = false;
210
221
  // true if running in ContinuousTest
211
222
  this.isContinuousTest = false;
223
+ // true if reclustering is turned off
224
+ this.noReCluster = false;
212
225
  // true if running a local test
213
226
  this.isTest = false;
214
227
  // true if running in local puppeteer mode
@@ -341,6 +354,9 @@ class MemLabConfig {
341
354
  this.oversizeObjectAsLeak = false;
342
355
  // if larger than this threshold, consider as memory leak
343
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;
344
360
  // only report leak clusters with aggregated retained size
345
361
  // bigger than this threshold
346
362
  this.clusterRetainedSizeThreshold = 0;
@@ -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',
@@ -35,6 +35,7 @@ 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
37
  const LeakTraceFilter_1 = require("./trace-filters/LeakTraceFilter");
38
+ const TraceSampler_1 = __importDefault(require("./TraceSampler"));
38
39
  class MemoryAnalyst {
39
40
  checkLeak() {
40
41
  return __awaiter(this, void 0, void 0, function* () {
@@ -357,26 +358,35 @@ class MemoryAnalyst {
357
358
  this.filterLeakedObjects(leakedNodeIds, snapshot);
358
359
  const leakTraceFilter = new LeakTraceFilter_1.LeakTraceFilter();
359
360
  const nodeIdInPaths = new Set();
360
- const paths = [];
361
- let numOfLeakedObjects = 0;
362
- let i = 0;
361
+ const samplePool = [];
363
362
  // analysis for each node
364
363
  Utils_1.default.applyToNodes(leakedNodeIds, snapshot, node => {
365
- if (!Config_1.default.isContinuousTest && ++i % 11 === 0) {
366
- Console_1.default.overwrite(`progress: ${i} / ${leakedNodeIds.size} @${node.id}`);
367
- }
368
364
  // BFS search for path from the leaked node to GC roots
369
365
  const p = finder.getPathToGCRoots(snapshot, node);
370
366
  if (p == null ||
371
367
  !leakTraceFilter.filter(p, { config: Config_1.default, leakedNodeIds, snapshot })) {
372
368
  return;
373
369
  }
374
- ++numOfLeakedObjects;
375
- paths.push(p);
376
- this.logLeakTraceSummary(p, nodeIdInPaths, snapshot, options);
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) {
376
+ return;
377
+ }
378
+ samplePool.push(p);
377
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
+ });
378
388
  if (Config_1.default.verbose) {
379
- Console_1.default.midLevel(`${numOfLeakedObjects} leaked objects`);
389
+ Console_1.default.midLevel(`Filter and select ${paths.length} leaked trace`);
380
390
  }
381
391
  return paths;
382
392
  }
@@ -410,11 +420,11 @@ class MemoryAnalyst {
410
420
  clusterHeapObjects(objectIds, snapshot) {
411
421
  const finder = this.preparePathFinder(snapshot);
412
422
  const paths = [];
413
- let i = 0;
423
+ const sampler = new TraceSampler_1.default(objectIds.size);
414
424
  // analysis for each node
415
425
  Utils_1.default.applyToNodes(objectIds, snapshot, node => {
416
- if (++i % 11 === 0) {
417
- Console_1.default.overwrite(`progress: ${i} / ${objectIds.size} @${node.id}`);
426
+ if (!sampler.sample()) {
427
+ return;
418
428
  }
419
429
  // BFS search for path from the leaked node to GC roots
420
430
  const p = finder.getPathToGCRoots(snapshot, node);
@@ -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;
@@ -1971,4 +1971,9 @@ export interface IHeapConfig {
1971
1971
  }
1972
1972
  /** @internal */
1973
1973
  export declare type ErrorWithMessage = Pick<Error, 'message'>;
1974
+ /** @internal */
1975
+ export declare type CommandOptionExample = string | {
1976
+ description?: string;
1977
+ cliOptionExample: string;
1978
+ };
1974
1979
  //# sourceMappingURL=Types.d.ts.map
@@ -28,6 +28,7 @@ declare function isDOMNodeIncomplete(node: IHeapNode): boolean;
28
28
  declare function isXMLDocumentNode(node: IHeapNode): boolean;
29
29
  declare function isHTMLDocumentNode(node: IHeapNode): boolean;
30
30
  declare function isDOMTextNode(node: IHeapNode): boolean;
31
+ declare function isCppRootsNode(node: IHeapNode): boolean;
31
32
  declare function isRootNode(node: IHeapNode, opt?: AnyOptions): boolean;
32
33
  declare function isDirectPropEdge(edge: IHeapEdge): boolean;
33
34
  declare function isReturnEdge(edge: IHeapEdge): boolean;
@@ -75,6 +76,8 @@ declare function isObjectNode(node: IHeapNode): boolean;
75
76
  declare function isPlainJSObjectNode(node: IHeapNode): boolean;
76
77
  declare function pathHasDetachedHTMLNode(path: LeakTracePathItem): boolean;
77
78
  declare function pathHasEdgeWithIndex(path: LeakTracePathItem, idx: number): boolean;
79
+ declare function pathHasEdgeWithName(path: LeakTracePathItem, edgeName: string): boolean;
80
+ declare function pathHasNodeOrEdgeWithName(path: LeakTracePathItem, name: Optional<string>): boolean;
78
81
  declare function getLastNodeId(path: LeakTracePathItem): number;
79
82
  declare function getReadablePercent(num: number): string;
80
83
  declare function getReadableBytes(bytes: Optional<number>): string;
@@ -183,6 +186,7 @@ declare const _default: {
183
186
  hasReactEdges: typeof hasReactEdges;
184
187
  isAlternateNode: typeof isAlternateNode;
185
188
  isBlinkRootNode: typeof isBlinkRootNode;
189
+ isCppRootsNode: typeof isCppRootsNode;
186
190
  isDOMInternalNode: typeof isDOMInternalNode;
187
191
  isDOMNodeIncomplete: typeof isDOMNodeIncomplete;
188
192
  isDOMTextNode: typeof isDOMTextNode;
@@ -230,6 +234,8 @@ declare const _default: {
230
234
  objectToMap: typeof objectToMap;
231
235
  pathHasDetachedHTMLNode: typeof pathHasDetachedHTMLNode;
232
236
  pathHasEdgeWithIndex: typeof pathHasEdgeWithIndex;
237
+ pathHasEdgeWithName: typeof pathHasEdgeWithName;
238
+ pathHasNodeOrEdgeWithName: typeof pathHasNodeOrEdgeWithName;
233
239
  repeat: typeof repeat;
234
240
  resolveFilePath: typeof resolveFilePath;
235
241
  resolveSnapshotFilePath: typeof resolveSnapshotFilePath;
package/dist/lib/Utils.js CHANGED
@@ -271,6 +271,10 @@ function isHTMLDocumentNode(node) {
271
271
  function isDOMTextNode(node) {
272
272
  return node.type === 'native' && node.name === 'Text';
273
273
  }
274
+ // check if this is a [C++ roots] (synthetic) node
275
+ function isCppRootsNode(node) {
276
+ return node.name === 'C++ roots' && node.type === 'synthetic';
277
+ }
274
278
  function isRootNode(node, opt = {}) {
275
279
  if (!node) {
276
280
  return false;
@@ -499,7 +503,7 @@ function hasHostRoot(node) {
499
503
  return false;
500
504
  }
501
505
  function filterNodesInPlace(idSet, snapshot, cb) {
502
- const ids = Array.from(idSet.keys());
506
+ const ids = Array.from(idSet);
503
507
  for (const id of ids) {
504
508
  const node = snapshot.getNodeById(id);
505
509
  if (node && !cb(node, snapshot)) {
@@ -508,7 +512,7 @@ function filterNodesInPlace(idSet, snapshot, cb) {
508
512
  }
509
513
  }
510
514
  function applyToNodes(idSet, snapshot, cb, options = {}) {
511
- let ids = Array.from(idSet.keys());
515
+ let ids = Array.from(idSet);
512
516
  if (options.shuffle) {
513
517
  ids.sort(() => Math.random() - 0.5);
514
518
  }
@@ -951,6 +955,33 @@ function pathHasEdgeWithIndex(path, idx) {
951
955
  }
952
956
  return false;
953
957
  }
958
+ function pathHasEdgeWithName(path, edgeName) {
959
+ let p = path;
960
+ while (p) {
961
+ if (p.edge && p.edge.name_or_index === edgeName) {
962
+ return true;
963
+ }
964
+ p = p.next;
965
+ }
966
+ return false;
967
+ }
968
+ function pathHasNodeOrEdgeWithName(path, name) {
969
+ if (name == null) {
970
+ return true;
971
+ }
972
+ name = name.toLowerCase();
973
+ let p = path;
974
+ while (p) {
975
+ if (p.edge && `${p.edge.name_or_index}`.toLowerCase().includes(name)) {
976
+ return true;
977
+ }
978
+ if (p.node && `${p.node.name}`.toLowerCase().includes(name)) {
979
+ return true;
980
+ }
981
+ p = p.next;
982
+ }
983
+ return false;
984
+ }
954
985
  function getLastNodeId(path) {
955
986
  if (!path) {
956
987
  return -1;
@@ -1694,11 +1725,14 @@ function runShell(command, options = {}) {
1694
1725
  execOptions.shell = '/bin/bash';
1695
1726
  }
1696
1727
  let ret = null;
1728
+ if (Config_1.default.verbose || Config_1.default.isContinuousTest) {
1729
+ Console_1.default.lowLevel(`running shell command: ${command}`);
1730
+ }
1697
1731
  try {
1698
1732
  ret = child_process_1.default.execSync(command, execOptions);
1699
1733
  }
1700
1734
  catch (ex) {
1701
- if (Config_1.default.verbose) {
1735
+ if (Config_1.default.verbose || Config_1.default.isContinuousTest) {
1702
1736
  if (ex instanceof Error) {
1703
1737
  Console_1.default.lowLevel(ex.message);
1704
1738
  Console_1.default.lowLevel((_c = ex.stack) !== null && _c !== void 0 ? _c : '');
@@ -1814,6 +1848,7 @@ exports.default = {
1814
1848
  hasReactEdges,
1815
1849
  isAlternateNode,
1816
1850
  isBlinkRootNode,
1851
+ isCppRootsNode,
1817
1852
  isDOMInternalNode,
1818
1853
  isDOMNodeIncomplete,
1819
1854
  isDOMTextNode,
@@ -1861,6 +1896,8 @@ exports.default = {
1861
1896
  objectToMap,
1862
1897
  pathHasDetachedHTMLNode,
1863
1898
  pathHasEdgeWithIndex,
1899
+ pathHasEdgeWithName,
1900
+ pathHasNodeOrEdgeWithName,
1864
1901
  repeat,
1865
1902
  resolveFilePath,
1866
1903
  resolveSnapshotFilePath,
@@ -21,25 +21,34 @@ class HeapStringNode extends HeapNode_1.default {
21
21
  }
22
22
  get stringValue() {
23
23
  var _a, _b, _c;
24
- const type = this.type;
25
- if (type === 'concatenated string') {
26
- const firstNode = (_a = this.getReferenceNode('first')) === null || _a === void 0 ? void 0 : _a.toStringNode();
27
- const secondNode = (_b = this.getReferenceNode('second')) === null || _b === void 0 ? void 0 : _b.toStringNode();
28
- if (firstNode == null || secondNode == null) {
29
- throw (0, HeapUtils_1.throwError)(new Error('broken concatenated string'));
24
+ const stack = [this];
25
+ let ret = '';
26
+ while (stack.length > 0) {
27
+ const node = stack.pop();
28
+ const type = node.type;
29
+ if (type === 'concatenated string') {
30
+ const firstNode = (_a = node.getReferenceNode('first')) === null || _a === void 0 ? void 0 : _a.toStringNode();
31
+ const secondNode = (_b = node.getReferenceNode('second')) === null || _b === void 0 ? void 0 : _b.toStringNode();
32
+ if (firstNode == null || secondNode == null) {
33
+ throw (0, HeapUtils_1.throwError)(new Error('broken concatenated string'));
34
+ }
35
+ stack.push(secondNode);
36
+ stack.push(firstNode);
37
+ continue;
30
38
  }
31
- return firstNode.stringValue + secondNode.stringValue;
32
- }
33
- if (type === 'sliced string') {
34
- const parentNode = (_c = this.getReferenceNode('parent')) === null || _c === void 0 ? void 0 : _c.toStringNode();
35
- if (parentNode == null) {
36
- throw (0, HeapUtils_1.throwError)(new Error('broken sliced string'));
39
+ if (type === 'sliced string') {
40
+ const parentNode = (_c = node.getReferenceNode('parent')) === null || _c === void 0 ? void 0 : _c.toStringNode();
41
+ if (parentNode == null) {
42
+ throw (0, HeapUtils_1.throwError)(new Error('broken sliced string'));
43
+ }
44
+ // sliced string in heap snapshot doesn't include
45
+ // the start index and the end index, so this may be inaccurate
46
+ ret += `<sliced string of @${parentNode.id}>`;
47
+ continue;
37
48
  }
38
- // sliced string in heap snapshot doesn't include
39
- // the start index and the end index, so this may be inaccurate
40
- return parentNode.stringValue;
49
+ ret += node.name;
41
50
  }
42
- return this.name;
51
+ return ret;
43
52
  }
44
53
  getJSONifyableObject() {
45
54
  const rep = super.getJSONifyableObject();
@@ -8,8 +8,13 @@
8
8
  * @format
9
9
  * @oncall web_perf_infra
10
10
  */
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
11
14
  Object.defineProperty(exports, "__esModule", { value: true });
12
15
  exports.FilterOverSizedNodeAsLeakRule = void 0;
16
+ const Utils_1 = __importDefault(require("../../Utils"));
17
+ const Config_1 = require("../../Config");
13
18
  const BaseLeakFilter_rule_1 = require("../BaseLeakFilter.rule");
14
19
  /**
15
20
  * trivial nodes are not reported as memory leaks
@@ -17,6 +22,10 @@ const BaseLeakFilter_rule_1 = require("../BaseLeakFilter.rule");
17
22
  class FilterOverSizedNodeAsLeakRule {
18
23
  filter(config, node) {
19
24
  if (config.oversizeObjectAsLeak) {
25
+ // TODO: add support to skip this check
26
+ if (!isHeapNodeUsefulForLeakTraceDiffing(config, node)) {
27
+ return BaseLeakFilter_rule_1.LeakDecision.NOT_LEAK;
28
+ }
20
29
  return node.retainedSize > config.oversizeThreshold
21
30
  ? BaseLeakFilter_rule_1.LeakDecision.LEAK
22
31
  : BaseLeakFilter_rule_1.LeakDecision.NOT_LEAK;
@@ -25,3 +34,44 @@ class FilterOverSizedNodeAsLeakRule {
25
34
  }
26
35
  }
27
36
  exports.FilterOverSizedNodeAsLeakRule = FilterOverSizedNodeAsLeakRule;
37
+ function isHeapNodeUsefulForLeakTraceDiffing(config, node) {
38
+ if (config.traceAllObjectsMode === Config_1.TraceObjectMode.Default) {
39
+ return true;
40
+ }
41
+ const name = node.name;
42
+ if (node.type !== 'object') {
43
+ return false;
44
+ }
45
+ if (name.startsWith('system / ')) {
46
+ return false;
47
+ }
48
+ if (Utils_1.default.isFiberNode(node) && !Utils_1.default.isDetachedFiberNode(node)) {
49
+ return false;
50
+ }
51
+ if (Utils_1.default.isDOMNodeIncomplete(node) && !Utils_1.default.isDetachedDOMNode(node)) {
52
+ return false;
53
+ }
54
+ if (node.getAnyReferrer('__proto__') != null) {
55
+ return false;
56
+ }
57
+ if (node.getAnyReferrer('prototype') != null) {
58
+ return false;
59
+ }
60
+ // react internal objects
61
+ if (node.getAnyReferrer('dependencies') != null) {
62
+ return false;
63
+ }
64
+ if (node.getAnyReferrer('memoizedState') != null) {
65
+ return false;
66
+ }
67
+ if (node.getAnyReferrer('next') != null) {
68
+ return false;
69
+ }
70
+ if (node.getAnyReferrer('deps') != null) {
71
+ return false;
72
+ }
73
+ if (node.getReference('baseQueue') != null) {
74
+ return false;
75
+ }
76
+ return true;
77
+ }
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const Constant_1 = __importDefault(require("../Constant"));
16
16
  const InternalValueSetter_1 = require("../InternalValueSetter");
17
17
  const FilterAttachedDOMToDetachedDOMTrace_rule_1 = require("./rules/FilterAttachedDOMToDetachedDOMTrace.rule");
18
+ const FilterCppRootsToDetachedDOMTrace_rule_1 = require("./rules/FilterCppRootsToDetachedDOMTrace.rule");
18
19
  const FilterDOMNodeChainTrace_rule_1 = require("./rules/FilterDOMNodeChainTrace.rule");
19
20
  const FilterHermesTrace_rule_1 = require("./rules/FilterHermesTrace.rule");
20
21
  const FilterInternalNodeTrace_rule_1 = require("./rules/FilterInternalNodeTrace.rule");
@@ -29,5 +30,6 @@ const list = [
29
30
  new FilterPendingActivitiesTrace_rule_1.FilterPendingActivitiesTraceRule(),
30
31
  new FilterDOMNodeChainTrace_rule_1.FilterDOMNodeChainTraceRule(),
31
32
  new FilterAttachedDOMToDetachedDOMTrace_rule_1.FilterAttachedDOMToDetachedDOMTraceRule(),
33
+ new FilterCppRootsToDetachedDOMTrace_rule_1.FilterCppRootsToDetachedDOMTraceRule(),
32
34
  ];
33
35
  exports.default = (0, InternalValueSetter_1.setInternalValue)(list, __filename, Constant_1.default.internalDir);
@@ -0,0 +1,15 @@
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 { LeakTracePathItem } from '../../Types';
11
+ import { ILeakTraceFilterRule, LeakTraceFilterOptions, TraceDecision } from '../BaseTraceFilter.rule';
12
+ export declare class FilterCppRootsToDetachedDOMTraceRule implements ILeakTraceFilterRule {
13
+ filter(p: LeakTracePathItem, options?: LeakTraceFilterOptions): TraceDecision;
14
+ }
15
+ //# sourceMappingURL=FilterCppRootsToDetachedDOMTrace.rule.d.ts.map
@@ -0,0 +1,44 @@
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
+ exports.FilterCppRootsToDetachedDOMTraceRule = void 0;
16
+ const Config_1 = __importDefault(require("../../Config"));
17
+ const Utils_1 = __importDefault(require("../../Utils"));
18
+ const BaseTraceFilter_rule_1 = require("../BaseTraceFilter.rule");
19
+ class FilterCppRootsToDetachedDOMTraceRule {
20
+ filter(p, options = {}) {
21
+ var _a;
22
+ const curConfig = (_a = options.config) !== null && _a !== void 0 ? _a : Config_1.default;
23
+ // if the path contains edges from [C++ roots] to detached DOM elements
24
+ if (curConfig.hideBrowserLeak && hasCppRootsToDetachedDOMNode(p)) {
25
+ return BaseTraceFilter_rule_1.TraceDecision.NOT_INSIGHTFUL;
26
+ }
27
+ return BaseTraceFilter_rule_1.TraceDecision.MAYBE_INSIGHTFUL;
28
+ }
29
+ }
30
+ exports.FilterCppRootsToDetachedDOMTraceRule = FilterCppRootsToDetachedDOMTraceRule;
31
+ function hasCppRootsToDetachedDOMNode(path) {
32
+ let p = path;
33
+ // all the reference chain consists of DOM elements/nodes
34
+ while (p && p.node) {
35
+ if (Utils_1.default.isCppRootsNode(p.node) &&
36
+ p.next &&
37
+ p.next.node &&
38
+ Utils_1.default.isDOMNodeIncomplete(p.next.node)) {
39
+ return true;
40
+ }
41
+ p = p.next;
42
+ }
43
+ return false;
44
+ }
@@ -450,7 +450,7 @@ class TraceFinder {
450
450
  return Config_1.default.edgeNameGreyList.has(String(edge.name_or_index));
451
451
  }
452
452
  isLessPreferableNode(node) {
453
- return Config_1.default.nodeNameGreyList.has(node.name);
453
+ return Config_1.default.nodeNameGreyList.has(node.name) || Utils_1.default.isCppRootsNode(node);
454
454
  }
455
455
  // each edge is indexed by fromNode's ID, toNode's ID, edge name, and edge type
456
456
  getEdgeKey(edge) {
@@ -22,6 +22,7 @@ const TraceSimilarityStrategy_1 = __importDefault(require("./strategies/TraceSim
22
22
  const TraceAsClusterStrategy_1 = __importDefault(require("./strategies/TraceAsClusterStrategy"));
23
23
  const MLTraceSimilarityStrategy_1 = __importDefault(require("./strategies/MLTraceSimilarityStrategy"));
24
24
  const ClusterUtils_1 = require("./ClusterUtils");
25
+ const TraceSampler_1 = __importDefault(require("../lib/TraceSampler"));
25
26
  // sync up with html/intern/js/webspeed/memlab/lib/LeakCluster.js
26
27
  class NormalizedTrace {
27
28
  constructor(p = null, snapshot = null) {
@@ -127,26 +128,19 @@ class NormalizedTrace {
127
128
  return Math.max(30, Utils_1.default.getNumberAtPercentile(lengthArr, 80));
128
129
  }
129
130
  static samplePaths(paths) {
130
- const maxCount = 5000;
131
+ const maxCount = Config_1.default.maxSamplesForClustering;
131
132
  if (paths.length <= maxCount) {
132
133
  return [...paths];
133
134
  }
134
- const sampleRatio = Math.min(1, maxCount / paths.length);
135
- if (sampleRatio < 1) {
136
- Console_1.default.warning('Sampling trace due to a large number of traces:');
137
- Console_1.default.lowLevel(` Number of Traces: ${paths.length}`);
138
- Console_1.default.lowLevel(` Sampling Ratio: ${Utils_1.default.getReadablePercent(sampleRatio)}`);
139
- }
135
+ const sampler = new TraceSampler_1.default(paths.length);
140
136
  const ret = [];
141
137
  const samplePathMaxLength = NormalizedTrace.getSamplePathMaxLength(paths);
142
138
  if (Config_1.default.verbose) {
143
139
  Console_1.default.lowLevel(` Sample Trace's Max Length: ${samplePathMaxLength}`);
144
140
  }
141
+ paths = paths.filter(p => Utils_1.default.getLeakTracePathLength(p) <= samplePathMaxLength);
145
142
  for (const p of paths) {
146
- if (Utils_1.default.getLeakTracePathLength(p) > samplePathMaxLength) {
147
- continue;
148
- }
149
- if (Math.random() < sampleRatio) {
143
+ if (sampler.sample()) {
150
144
  ret.push(p);
151
145
  }
152
146
  else {
@@ -157,6 +151,9 @@ class NormalizedTrace {
157
151
  }
158
152
  }
159
153
  }
154
+ if (Config_1.default.verbose) {
155
+ Console_1.default.lowLevel(`Number of samples after sampling: ${ret.length}.`);
156
+ }
160
157
  return ret;
161
158
  }
162
159
  static diffTraces(newTraces, existingTraces, // existing representative traces
@@ -194,6 +191,11 @@ class NormalizedTrace {
194
191
  }
195
192
  return traceToClusterMap.get(trace);
196
193
  };
194
+ if (Config_1.default.isContinuousTest) {
195
+ Console_1.default.lowLevel(`${staleClusters.length} stale clusters`);
196
+ Console_1.default.lowLevel(`${clustersToAdd.length} new clusters`);
197
+ Console_1.default.lowLevel(`${allClusters.length} clusters in total`);
198
+ }
197
199
  return {
198
200
  staleClusters: staleClusters.map(traceToCluster),
199
201
  clustersToAdd: clustersToAdd.map(traceToCluster),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memlab/core",
3
- "version": "1.1.21",
3
+ "version": "1.1.22",
4
4
  "license": "MIT",
5
5
  "description": "memlab core libraries",
6
6
  "author": "Liang Gong <lgong@fb.com>",