@memlab/core 1.1.9 → 1.1.10

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.
@@ -206,7 +206,7 @@ export declare class MemLabConfig {
206
206
  set isHeadfulBrowser(isHeadful: boolean);
207
207
  get isHeadfulBrowser(): boolean;
208
208
  get browserBinaryPath(): string;
209
- set reportLeaksInTimers(flag: boolean);
209
+ set reportLeaksInTimers(shouldReport: boolean);
210
210
  get reportLeaksInTimers(): boolean;
211
211
  setDevice(deviceName: string, options?: {
212
212
  manualOverride?: boolean;
@@ -50,7 +50,7 @@ class MemLabConfig {
50
50
  initInternalConfigs() {
51
51
  // DO NOT SET PARAMETER HERE
52
52
  this._isFullRun = false;
53
- this._reportLeaksInTimers = false;
53
+ this._reportLeaksInTimers = true;
54
54
  this._deviceManualOverridden = false;
55
55
  this._timerNodes = ['Pending activities'];
56
56
  this._timerEdges = [];
@@ -313,7 +313,7 @@ class MemLabConfig {
313
313
  static getInstance() {
314
314
  if (!MemLabConfig.instance) {
315
315
  const config = new MemLabConfig();
316
- // do not consider objects kept alive by timers as leaks
316
+ // consider objects kept alive by timers as leaks
317
317
  config.reportLeaksInTimers = true;
318
318
  // assign configuration to console manager
319
319
  Console_1.default.setConfig(config);
@@ -396,11 +396,8 @@ class MemLabConfig {
396
396
  get browserBinaryPath() {
397
397
  return path_1.default.join(this.browserDir, this.browser);
398
398
  }
399
- set reportLeaksInTimers(flag) {
400
- if (typeof flag !== 'boolean') {
401
- return;
402
- }
403
- if (flag) {
399
+ set reportLeaksInTimers(shouldReport) {
400
+ if (shouldReport) {
404
401
  this.removeFromSet(this.nodeNameBlockList, this._timerNodes);
405
402
  this.removeFromSet(this.edgeNameBlockList, this._timerEdges);
406
403
  }
@@ -408,7 +405,7 @@ class MemLabConfig {
408
405
  this.addToSet(this.nodeNameBlockList, this._timerNodes);
409
406
  this.addToSet(this.edgeNameBlockList, this._timerEdges);
410
407
  }
411
- this._reportLeaksInTimers = flag;
408
+ this._reportLeaksInTimers = shouldReport;
412
409
  }
413
410
  get reportLeaksInTimers() {
414
411
  return this._reportLeaksInTimers;
@@ -41,6 +41,14 @@ declare class MemoryAnalyst {
41
41
  searchLeakedTraces(leakedNodeIds: HeapNodeIdSet, snapshot: IHeapSnapshot): Promise<{
42
42
  paths: LeakTracePathItem[];
43
43
  }>;
44
+ /**
45
+ * Given a set of heap object ids, cluster them based on the similarity
46
+ * of their retainer traces and return a
47
+ * @param leakedNodeIds
48
+ * @param snapshot
49
+ * @returns
50
+ */
51
+ clusterHeapObjects(objectIds: HeapNodeIdSet, snapshot: IHeapSnapshot): TraceCluster[];
44
52
  serializeClusterUpdate(clusters: TraceCluster[], options?: {
45
53
  reclusterOnly?: boolean;
46
54
  }): Promise<void>;
@@ -647,6 +647,7 @@ class MemoryAnalyst {
647
647
  ? new MLTraceSimilarityStrategy_1.default()
648
648
  : undefined,
649
649
  });
650
+ Console_1.default.midLevel(`MemLab found ${clusters.length} leak(s)`);
650
651
  yield this.serializeClusterUpdate(clusters);
651
652
  if (Config_1.default.logUnclassifiedClusters) {
652
653
  // cluster traces from the current run
@@ -658,6 +659,36 @@ class MemoryAnalyst {
658
659
  };
659
660
  });
660
661
  }
662
+ /**
663
+ * Given a set of heap object ids, cluster them based on the similarity
664
+ * of their retainer traces and return a
665
+ * @param leakedNodeIds
666
+ * @param snapshot
667
+ * @returns
668
+ */
669
+ clusterHeapObjects(objectIds, snapshot) {
670
+ const finder = this.preparePathFinder(snapshot);
671
+ const paths = [];
672
+ let i = 0;
673
+ // analysis for each node
674
+ Utils_1.default.applyToNodes(objectIds, snapshot, node => {
675
+ if (++i % 11 === 0) {
676
+ Console_1.default.overwrite(`progress: ${i} / ${objectIds.size} @${node.id}`);
677
+ }
678
+ // BFS search for path from the leaked node to GC roots
679
+ const p = finder.getPathToGCRoots(snapshot, node);
680
+ if (p) {
681
+ paths.push(p);
682
+ }
683
+ }, { reverse: true });
684
+ // cluster traces from the current run
685
+ const clusters = TraceBucket_1.default.clusterPaths(paths, snapshot, this.aggregateDominatorMetrics, {
686
+ strategy: Config_1.default.isMLClustering
687
+ ? new MLTraceSimilarityStrategy_1.default()
688
+ : undefined,
689
+ });
690
+ return clusters;
691
+ }
661
692
  serializeClusterUpdate(clusters, options = {}) {
662
693
  return __awaiter(this, void 0, void 0, function* () {
663
694
  // load existing clusters
@@ -1228,7 +1228,7 @@ export interface IHeapNodeBasic {
1228
1228
  */
1229
1229
  export declare type EdgeIterationCallback = (edge: IHeapEdge) => Optional<{
1230
1230
  stop: boolean;
1231
- }>;
1231
+ }> | void;
1232
1232
  /**
1233
1233
  * An `IHeapNode` instance represents a JS heap object in a heap snapshot.
1234
1234
  * A heap snapshot is generally a graph where graph nodes are JS heap objects
@@ -22,6 +22,7 @@ declare function isEssentialEdge(nodeIndex: number, edgeType: string, rootNodeIn
22
22
  declare function isFiberNodeDeletionsEdge(edge: IHeapEdge): boolean;
23
23
  declare function isBlinkRootNode(node: IHeapNode): boolean;
24
24
  declare function isPendingActivityNode(node: IHeapNode): boolean;
25
+ declare function isDOMNodeIncomplete(node: IHeapNode): boolean;
25
26
  declare function isRootNode(node: IHeapNode, opt?: AnyOptions): boolean;
26
27
  declare function isDirectPropEdge(edge: IHeapEdge): boolean;
27
28
  declare function isReturnEdge(edge: IHeapEdge): boolean;
@@ -171,6 +172,7 @@ declare const _default: {
171
172
  isDetachedDOMNode: typeof isDetachedDOMNode;
172
173
  isDirectPropEdge: typeof isDirectPropEdge;
173
174
  isDocumentDOMTreesRoot: typeof isDocumentDOMTreesRoot;
175
+ isDOMNodeIncomplete: typeof isDOMNodeIncomplete;
174
176
  isEssentialEdge: typeof isEssentialEdge;
175
177
  isFiberNode: typeof isFiberNode;
176
178
  isFiberNodeDeletionsEdge: typeof isFiberNodeDeletionsEdge;
package/dist/lib/Utils.js CHANGED
@@ -212,6 +212,17 @@ function isPendingActivityNode(node) {
212
212
  }
213
213
  return node.type === 'synthetic' && node.name === 'Pending activities';
214
214
  }
215
+ // check the node against a curated list of known HTML Elements
216
+ // the list may be incomplete
217
+ function isDOMNodeIncomplete(node) {
218
+ let name = node.name;
219
+ const pattern = /^HTML.*Element$/;
220
+ const detachedPrefix = 'Detached ';
221
+ if (name.startsWith(detachedPrefix)) {
222
+ name = name.substring(detachedPrefix.length);
223
+ }
224
+ return pattern.test(name);
225
+ }
215
226
  function isRootNode(node, opt = {}) {
216
227
  if (!node) {
217
228
  return false;
@@ -358,14 +369,16 @@ function getNodesIdSet(snapshot) {
358
369
  });
359
370
  return set;
360
371
  }
361
- // given a set of nodes S, return a subset S' where
372
+ // given a set of nodes S, return a minimal subset S' where
362
373
  // no nodes are dominated by nodes in S
363
374
  function getConditionalDominatorIds(ids, snapshot, condCb) {
364
375
  const dominatorIds = new Set();
376
+ const fullDominatorIds = new Set();
365
377
  // set all node ids
366
378
  applyToNodes(ids, snapshot, node => {
367
379
  if (condCb(node)) {
368
380
  dominatorIds.add(node.id);
381
+ fullDominatorIds.add(node.id);
369
382
  }
370
383
  });
371
384
  // traverse the dominators and remove the node
@@ -377,7 +390,7 @@ function getConditionalDominatorIds(ids, snapshot, condCb) {
377
390
  if (visited.has(cur.id)) {
378
391
  break;
379
392
  }
380
- if (dominatorIds.has(cur.id)) {
393
+ if (fullDominatorIds.has(cur.id)) {
381
394
  dominatorIds.delete(node.id);
382
395
  break;
383
396
  }
@@ -1733,6 +1746,7 @@ exports.default = {
1733
1746
  isDetachedDOMNode,
1734
1747
  isDirectPropEdge,
1735
1748
  isDocumentDOMTreesRoot,
1749
+ isDOMNodeIncomplete,
1736
1750
  isEssentialEdge,
1737
1751
  isFiberNode,
1738
1752
  isFiberNodeDeletionsEdge,
@@ -4,8 +4,8 @@
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  *
7
- * @emails oncall+ws_labs
8
7
  * @format
8
+ * @oncall ws_labs
9
9
  */
10
10
  import type { AnyValue, IHeapSnapshot } from '../Types';
11
11
  declare type AnyObject = Record<AnyValue, AnyValue>;
@@ -5,8 +5,8 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  *
8
- * @emails oncall+ws_labs
9
8
  * @format
9
+ * @oncall ws_labs
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const __1 = require("../..");
@@ -388,10 +388,18 @@ class TraceFinder {
388
388
  shouldIgnoreEdgeInTraceFinding(edge) {
389
389
  const fromNode = edge.fromNode;
390
390
  const toNode = edge.toNode;
391
- return (Config_1.default.hideBrowserLeak &&
392
- (Utils_1.default.isBlinkRootNode(fromNode) ||
393
- Utils_1.default.isPendingActivityNode(fromNode)) &&
394
- Utils_1.default.isDetachedDOMNode(toNode));
391
+ const isDetachedNode = Utils_1.default.isDetachedDOMNode(toNode);
392
+ if (Config_1.default.hideBrowserLeak &&
393
+ Utils_1.default.isBlinkRootNode(fromNode) &&
394
+ isDetachedNode) {
395
+ return true;
396
+ }
397
+ if (!Config_1.default.reportLeaksInTimers &&
398
+ Utils_1.default.isPendingActivityNode(fromNode) &&
399
+ isDetachedNode) {
400
+ return true;
401
+ }
402
+ return false;
395
403
  }
396
404
  shouldTraverseEdge(edge, options = {}) {
397
405
  if (this.isBlockListedEdge(edge)) {
@@ -411,9 +419,20 @@ class TraceFinder {
411
419
  if (Config_1.default.edgeNameBlockList.has(String(nameOrIndex))) {
412
420
  return true;
413
421
  }
422
+ if (Config_1.default.nodeNameBlockList.has(edge.toNode.name)) {
423
+ return true;
424
+ }
425
+ if (Config_1.default.nodeNameBlockList.has(edge.fromNode.name)) {
426
+ return true;
427
+ }
414
428
  return false;
415
429
  }
416
430
  isLessPreferableEdge(edge) {
431
+ // pending activities -> DOM element is less preferrable
432
+ if (Utils_1.default.isPendingActivityNode(edge.fromNode) &&
433
+ Utils_1.default.isDOMNodeIncomplete(edge.toNode)) {
434
+ return true;
435
+ }
417
436
  return Config_1.default.edgeNameGreyList.has(String(edge.name_or_index));
418
437
  }
419
438
  isLessPreferableNode(node) {
@@ -211,12 +211,18 @@ class NormalizedTrace {
211
211
  const { allClusters } = NormalizedTrace.diffTraces(traces, [], option);
212
212
  // construct TraceCluster from clustering result
213
213
  let clusters = allClusters.map((traces) => {
214
+ const representativeTrace = traces[0];
214
215
  const cluster = {
215
- path: traceToPathMap.get(traces[0]),
216
+ path: traceToPathMap.get(representativeTrace),
216
217
  count: traces.length,
217
218
  snapshot,
218
219
  retainedSize: 0,
219
220
  };
221
+ // add representative object id if there is one
222
+ const lastNode = representativeTrace[representativeTrace.length - 1];
223
+ if ('id' in lastNode) {
224
+ cluster.id = lastNode.id;
225
+ }
220
226
  traces.forEach((trace) => {
221
227
  NormalizedTrace.addLeakedNodeToCluster(cluster, traceToPathMap.get(trace));
222
228
  });
@@ -225,7 +231,6 @@ class NormalizedTrace {
225
231
  });
226
232
  clusters = NormalizedTrace.filterClusters(clusters);
227
233
  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); });
228
- Console_1.default.midLevel(`MemLab found ${clusters.length} leak(s)`);
229
234
  return clusters;
230
235
  }
231
236
  static generateUnClassifiedClusters(paths, snapshot, aggregateDominatorMetrics) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memlab/core",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "license": "MIT",
5
5
  "description": "memlab core libraries",
6
6
  "author": "Liang Gong <lgong@fb.com>",