@memlab/core 1.1.22 → 1.1.23

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.
@@ -0,0 +1,13 @@
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 { IHeapSnapshot } from '../../lib/Types';
11
+ /** @internal */
12
+ export declare function getFullHeapFromFile(file: string): Promise<IHeapSnapshot>;
13
+ //# sourceMappingURL=TestUtils.d.ts.map
@@ -0,0 +1,39 @@
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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
12
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
13
+ return new (P || (P = Promise))(function (resolve, reject) {
14
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
15
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
16
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
17
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
18
+ });
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.getFullHeapFromFile = void 0;
22
+ const __1 = require("../..");
23
+ /** @internal */
24
+ function getFullHeapFromFile(file) {
25
+ return __awaiter(this, void 0, void 0, function* () {
26
+ return yield loadProcessedSnapshot({ file });
27
+ });
28
+ }
29
+ exports.getFullHeapFromFile = getFullHeapFromFile;
30
+ function loadProcessedSnapshot(options = {}) {
31
+ return __awaiter(this, void 0, void 0, function* () {
32
+ const opt = { buildNodeIdIndex: true, verbose: true };
33
+ const file = options.file || __1.utils.getSnapshotFilePathWithTabType(/.*/);
34
+ const snapshot = yield __1.utils.getSnapshotFromFile(file, opt);
35
+ __1.analysis.preparePathFinder(snapshot);
36
+ __1.info.flush();
37
+ return snapshot;
38
+ });
39
+ }
@@ -114,7 +114,7 @@ export declare class MemLabConfig {
114
114
  qes: QuickExperiment[];
115
115
  isOndemand: boolean;
116
116
  useExternalSnapshot: boolean;
117
- externalRunMetaFile: string;
117
+ externalRunMetaTemplateFile: string;
118
118
  externalSnapshotVisitOrderFile: string;
119
119
  externalSnapshotDir: Nullable<string>;
120
120
  externalSnapshotFilePaths: string[];
@@ -321,7 +321,10 @@ class MemLabConfig {
321
321
  // node names excluded from the trace finding
322
322
  this.nodeNameBlockList = new Set(['system / PropertyCell']);
323
323
  // edge names excluded from the trace finding
324
- this.edgeNameBlockList = new Set(['feedback_cell']);
324
+ this.edgeNameBlockList = new Set([
325
+ 'feedback_cell',
326
+ 'part of key -> value pair in ephemeron table',
327
+ ]);
325
328
  // node names less preferable in trace finding
326
329
  this.nodeNameGreyList = new Set([
327
330
  'InternalNode',
@@ -427,7 +427,7 @@ class FileManager {
427
427
  config.heapAnalysisLogDir = joinAndProcessDir(options, this.getHeapAnalysisLogDir(options));
428
428
  config.metricsOutDir = joinAndProcessDir(options, loggerOutDir, 'metrics');
429
429
  config.reportScreenshotFile = path_1.default.join(outDir, 'report.png');
430
- config.externalRunMetaFile = this.getRunMetaExternalTemplateFile();
430
+ config.externalRunMetaTemplateFile = this.getRunMetaExternalTemplateFile();
431
431
  config.externalSnapshotVisitOrderFile =
432
432
  this.getSnapshotSequenceExternalTemplateFile();
433
433
  joinAndProcessDir(options, this.getUniqueTraceClusterDir(options));
@@ -49,13 +49,13 @@ class MemoryAnalyst {
49
49
  const controlSnapshotDirs = options.controlWorkDirs.map(controlWorkDir => FileManager_1.default.getCurDataDir({
50
50
  workDir: controlWorkDir,
51
51
  }));
52
- const treatmentSnapshotDir = FileManager_1.default.getCurDataDir({
53
- workDir: options.treatmentWorkDir,
54
- });
52
+ const treatmentSnapshotDirs = options.treatmentWorkDirs.map(treatmentWorkDir => FileManager_1.default.getCurDataDir({
53
+ workDir: treatmentWorkDir,
54
+ }));
55
55
  // check control working dir
56
56
  controlSnapshotDirs.forEach(controlSnapshotDir => Utils_1.default.checkSnapshots({ snapshotDir: controlSnapshotDir }));
57
57
  // check treatment working dir
58
- Utils_1.default.checkSnapshots({ snapshotDir: treatmentSnapshotDir });
58
+ treatmentSnapshotDirs.forEach(treatmentSnapshotDir => Utils_1.default.checkSnapshots({ snapshotDir: treatmentSnapshotDir }));
59
59
  // display control and treatment memory
60
60
  MemoryBarChart_1.default.plotMemoryBarChart(options);
61
61
  return this.diffMemoryLeakTraces(options);
@@ -76,17 +76,26 @@ class MemoryAnalyst {
76
76
  leakPathsFromControlRuns.push(this.filterLeakPaths(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot, { workDir: controlWorkDir }));
77
77
  controlSnapshots.push(snapshotDiff.snapshot);
78
78
  }
79
- // diff snapshots and get treatment raw paths
80
- const snapshotDiff = yield this.diffSnapshots({
81
- loadAllSnapshots: true,
82
- workDir: options.treatmentWorkDir,
83
- });
84
- const treatmentLeakPaths = this.filterLeakPaths(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot, { workDir: options.treatmentWorkDir });
85
- const treatmentSnapshot = snapshotDiff.snapshot;
79
+ // diff snapshots from treatment dirs and get treatment raw paths array
80
+ const treatmentSnapshots = [];
81
+ const leakPathsFromTreatmentRuns = [];
82
+ let firstTreatmentSnapshotDiff = null;
83
+ for (const treatmentWorkDir of options.treatmentWorkDirs) {
84
+ const snapshotDiff = yield this.diffSnapshots({
85
+ loadAllSnapshots: true,
86
+ workDir: treatmentWorkDir,
87
+ });
88
+ if (firstTreatmentSnapshotDiff == null) {
89
+ firstTreatmentSnapshotDiff = snapshotDiff;
90
+ }
91
+ leakPathsFromTreatmentRuns.push(this.filterLeakPaths(snapshotDiff.leakedHeapNodeIdSet, snapshotDiff.snapshot, { workDir: treatmentWorkDir }));
92
+ treatmentSnapshots.push(snapshotDiff.snapshot);
93
+ }
86
94
  const controlPathCounts = JSON.stringify(leakPathsFromControlRuns.map(leakPaths => leakPaths.length));
95
+ const treatmentPathCounts = JSON.stringify(leakPathsFromTreatmentRuns.map(leakPaths => leakPaths.length));
87
96
  Console_1.default.topLevel(`${controlPathCounts} traces from control group`);
88
- Console_1.default.topLevel(`${treatmentLeakPaths.length} traces from treatment group`);
89
- const result = TraceBucket_1.default.clusterControlTreatmentPaths(leakPathsFromControlRuns, controlSnapshots, treatmentLeakPaths, treatmentSnapshot, Utils_1.default.aggregateDominatorMetrics, {
97
+ Console_1.default.topLevel(`${treatmentPathCounts} traces from treatment group`);
98
+ const result = TraceBucket_1.default.clusterControlTreatmentPaths(leakPathsFromControlRuns, controlSnapshots, leakPathsFromTreatmentRuns, treatmentSnapshots, Utils_1.default.aggregateDominatorMetrics, {
90
99
  strategy: Config_1.default.isMLClustering
91
100
  ? new MLTraceSimilarityStrategy_1.default()
92
101
  : undefined,
@@ -95,7 +104,10 @@ class MemoryAnalyst {
95
104
  yield this.serializeClusterUpdate(result.treatmentOnlyClusters);
96
105
  // serialize JSON file with detailed leak trace information
97
106
  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);
107
+ if (firstTreatmentSnapshotDiff == null) {
108
+ throw Utils_1.default.haltOrThrow('treatemnt snapshot diff result not found');
109
+ }
110
+ return LeakTraceDetailsLogger_1.default.logTraces(firstTreatmentSnapshotDiff.leakedHeapNodeIdSet, firstTreatmentSnapshotDiff.snapshot, firstTreatmentSnapshotDiff.listOfLeakedHeapNodeIdSet, treatmentOnlyPaths, Config_1.default.traceJsonOutDir);
99
111
  });
100
112
  }
101
113
  // find all unique pattern of leaks
@@ -11,6 +11,7 @@ import type { Nullable, Optional, RunMetaInfo } from './Types';
11
11
  export declare class RunMetaInfoManager {
12
12
  getRunMetaFilePath(options?: {
13
13
  workDir?: Optional<string>;
14
+ readonly?: Optional<boolean>;
14
15
  }): string;
15
16
  saveRunMetaInfo(runMetaInfo: RunMetaInfo, options?: {
16
17
  workDir?: Optional<string>;
@@ -16,8 +16,10 @@ class RunMetaInfoManager {
16
16
  if ((options === null || options === void 0 ? void 0 : options.workDir) != null) {
17
17
  return FileManager_1.default.getRunMetaFile({ workDir: options.workDir });
18
18
  }
19
- if (Config_1.default.useExternalSnapshot) {
20
- return Config_1.default.externalRunMetaFile;
19
+ if ((options === null || options === void 0 ? void 0 : options.readonly) && Config_1.default.useExternalSnapshot) {
20
+ // only returns the template file if the
21
+ // run meta file is used for readonly purpose
22
+ return Config_1.default.externalRunMetaTemplateFile;
21
23
  }
22
24
  if (Config_1.default.runMetaFile != null) {
23
25
  return Config_1.default.runMetaFile;
@@ -40,7 +42,8 @@ class RunMetaInfoManager {
40
42
  return runMetaInfo;
41
43
  }
42
44
  loadRunMetaInfo(options) {
43
- const file = (options === null || options === void 0 ? void 0 : options.metaFile) || this.getRunMetaFilePath(options);
45
+ const file = (options === null || options === void 0 ? void 0 : options.metaFile) ||
46
+ this.getRunMetaFilePath(Object.assign({ readonly: true }, options));
44
47
  try {
45
48
  return this.loadRunMetaInfoFromFile(file);
46
49
  }
@@ -49,7 +52,8 @@ class RunMetaInfoManager {
49
52
  }
50
53
  }
51
54
  loadRunMetaInfoSilentFail(options) {
52
- const file = (options === null || options === void 0 ? void 0 : options.metaFile) || this.getRunMetaFilePath(options);
55
+ const file = (options === null || options === void 0 ? void 0 : options.metaFile) ||
56
+ this.getRunMetaFilePath(Object.assign({ readonly: true }, options));
53
57
  try {
54
58
  return this.loadRunMetaInfoFromFile(file);
55
59
  }
@@ -0,0 +1,18 @@
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 { IHeapNode, IHeapSnapshot, ISerializationHelper, ISerializedInfo, JSONifyArgs, JSONifyOptions, Nullable } from './Types';
11
+ export declare class SerializationHelper implements ISerializationHelper {
12
+ protected snapshot: Nullable<IHeapSnapshot>;
13
+ setSnapshot(snapshot: IHeapSnapshot): void;
14
+ createOrMergeWrapper(info: ISerializedInfo, _node: IHeapNode, _args: JSONifyArgs, _options: JSONifyOptions): ISerializedInfo;
15
+ }
16
+ declare const _default: typeof SerializationHelper;
17
+ export default _default;
18
+ //# sourceMappingURL=SerializationHelper.d.ts.map
@@ -0,0 +1,36 @@
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.SerializationHelper = void 0;
16
+ const InternalValueSetter_1 = require("./InternalValueSetter");
17
+ const Constant_1 = __importDefault(require("./Constant"));
18
+ class SerializationHelper {
19
+ constructor() {
20
+ this.snapshot = null;
21
+ }
22
+ setSnapshot(snapshot) {
23
+ this.snapshot = snapshot;
24
+ }
25
+ createOrMergeWrapper(info,
26
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
27
+ _node,
28
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
29
+ _args,
30
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
31
+ _options) {
32
+ return info;
33
+ }
34
+ }
35
+ exports.SerializationHelper = SerializationHelper;
36
+ exports.default = (0, InternalValueSetter_1.setInternalValue)(SerializationHelper, __filename, Constant_1.default.internalDir);
@@ -7,12 +7,8 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import { E2EStepInfo, HeapNodeIdSet, IHeapEdge, IHeapNode, IHeapSnapshot, ISerializedInfo, LeakTracePathItem, Nullable } from './Types';
11
- declare type JSONifyArgs = {
12
- leakedIdSet?: Set<number>;
13
- nodeIdsInSnapshots?: Array<Set<number>>;
14
- };
15
- declare function JSONifyPath(path: LeakTracePathItem, _snapshot: IHeapSnapshot, args: JSONifyArgs): Nullable<ISerializedInfo>;
10
+ import { E2EStepInfo, HeapNodeIdSet, IHeapEdge, IHeapNode, IHeapSnapshot, ISerializedInfo, JSONifyArgs, LeakTracePathItem, Nullable } from './Types';
11
+ declare function JSONifyPath(path: LeakTracePathItem, snapshot: IHeapSnapshot, args: JSONifyArgs): Nullable<ISerializedInfo>;
16
12
  declare type SummarizeOptions = {
17
13
  compact?: boolean;
18
14
  color?: boolean;
@@ -18,6 +18,7 @@ const Config_1 = __importDefault(require("./Config"));
18
18
  const Utils_1 = __importDefault(require("./Utils"));
19
19
  const Console_1 = __importDefault(require("./Console"));
20
20
  const TraceFinder_1 = __importDefault(require("../paths/TraceFinder"));
21
+ const SerializationHelper_1 = __importDefault(require("./SerializationHelper"));
21
22
  const REGEXP_NAME_CLEANUP = /[[]\(\)]/g;
22
23
  const EMPTY_JSONIFY_OPTIONS = {
23
24
  fiberNodeReturnTrace: {},
@@ -374,7 +375,12 @@ function JSONifyNode(node, args, options) {
374
375
  if (node.dominatorNode) {
375
376
  info['dominator id (extra)'] = `@${node.dominatorNode.id}`;
376
377
  }
377
- return info;
378
+ // use serialization helper to wrap around
379
+ // the JSON node with additional tagging information
380
+ const { serializationHelper } = options;
381
+ return serializationHelper
382
+ ? serializationHelper.createOrMergeWrapper(info, node, args, options)
383
+ : info;
378
384
  }
379
385
  function JSONifyTabsOrder() {
380
386
  const file = Utils_1.default.getSnapshotSequenceFilePath();
@@ -383,7 +389,7 @@ function JSONifyTabsOrder() {
383
389
  function shouldHighlight(node) {
384
390
  return Utils_1.default.isDetachedDOMNode(node) || Utils_1.default.isDetachedFiberNode(node);
385
391
  }
386
- function JSONifyPath(path, _snapshot, args) {
392
+ function JSONifyPath(path, snapshot, args) {
387
393
  if (!path.node) {
388
394
  return null;
389
395
  }
@@ -393,6 +399,9 @@ function JSONifyPath(path, _snapshot, args) {
393
399
  ret['$tabsOrder:' + JSONifyTabsOrder()] = '';
394
400
  ret[`${idx++}: ${getNodeNameInJSON(path.node, args)}`] = JSONifyNode(path.node, args, Object.assign(Object.assign({}, EMPTY_JSONIFY_OPTIONS), { processedNodeId: new Set() }));
395
401
  let pathItem = path;
402
+ // initialize serialization helper
403
+ const serializationHelper = new SerializationHelper_1.default();
404
+ serializationHelper.setSnapshot(snapshot);
396
405
  while (pathItem === null || pathItem === void 0 ? void 0 : pathItem.edge) {
397
406
  const edge = pathItem.edge;
398
407
  const nextNode = edge.toNode;
@@ -401,7 +410,7 @@ function JSONifyPath(path, _snapshot, args) {
401
410
  nextNode.highlight = true;
402
411
  }
403
412
  const edgeRetainSize = pathItem.edgeRetainSize;
404
- ret[`${idx++}: ${getEdgeNameInJSON(edge, edgeRetainSize)}${getNodeNameInJSON(nextNode, args)}`] = JSONifyNode(nextNode, args, Object.assign(Object.assign({}, EMPTY_JSONIFY_OPTIONS), { processedNodeId: new Set() }));
413
+ ret[`${idx++}: ${getEdgeNameInJSON(edge, edgeRetainSize)}${getNodeNameInJSON(nextNode, args)}`] = JSONifyNode(nextNode, args, Object.assign(Object.assign({}, EMPTY_JSONIFY_OPTIONS), { processedNodeId: new Set(), serializationHelper }));
405
414
  pathItem = pathItem.next;
406
415
  }
407
416
  return ret;
@@ -806,8 +806,9 @@ export declare type TraceClusterMetaInfo = {
806
806
  };
807
807
  /** @internal */
808
808
  export declare type ControlTreatmentClusterResult = {
809
- controlOnlyClusters: TraceCluster[];
809
+ controlLikelyOrOnlyClusters: TraceCluster[];
810
810
  treatmentOnlyClusters: TraceCluster[];
811
+ treatmentLikelyClusters: TraceCluster[];
811
812
  hybridClusters: Array<{
812
813
  control: TraceCluster;
813
814
  treatment: TraceCluster;
@@ -1930,12 +1931,12 @@ export interface IOveralLeakInfo extends Partial<IOveralHeapInfo> {
1930
1931
  /** @internal */
1931
1932
  export declare type DiffLeakOptions = {
1932
1933
  controlWorkDirs: string[];
1933
- treatmentWorkDir: string;
1934
+ treatmentWorkDirs: string[];
1934
1935
  };
1935
1936
  /** @internal */
1936
1937
  export declare type PlotMemoryOptions = {
1937
1938
  controlWorkDirs?: string[];
1938
- treatmentWorkDir?: string;
1939
+ treatmentWorkDirs?: string[];
1939
1940
  workDir?: string;
1940
1941
  } & IMemoryAnalystOptions;
1941
1942
  /** @internal */
@@ -1976,4 +1977,21 @@ export declare type CommandOptionExample = string | {
1976
1977
  description?: string;
1977
1978
  cliOptionExample: string;
1978
1979
  };
1980
+ /** @internal */
1981
+ export declare type JSONifyArgs = {
1982
+ leakedIdSet?: Set<number>;
1983
+ nodeIdsInSnapshots?: Array<Set<number>>;
1984
+ };
1985
+ /** @internal */
1986
+ export interface ISerializationHelper {
1987
+ setSnapshot(snapshot: IHeapSnapshot): void;
1988
+ createOrMergeWrapper(info: ISerializedInfo, node: IHeapNode, args: JSONifyArgs, options: JSONifyOptions): ISerializedInfo;
1989
+ }
1990
+ /** @internal */
1991
+ export declare type JSONifyOptions = {
1992
+ fiberNodeReturnTrace: Record<number, string>;
1993
+ processedNodeId: Set<number>;
1994
+ forceJSONifyDepth?: number;
1995
+ serializationHelper?: ISerializationHelper;
1996
+ };
1979
1997
  //# sourceMappingURL=Types.d.ts.map
@@ -49,6 +49,7 @@ declare function isAlternateNode(node: IHeapNode): boolean;
49
49
  declare function setIsRegularFiberNode(node: IHeapNode): void;
50
50
  declare function isRegularFiberNode(node: IHeapNode): boolean;
51
51
  declare function hasHostRoot(node: IHeapNode): boolean;
52
+ declare function markDetachedFiberNode(node: IHeapNode): boolean;
52
53
  declare type IterateNodeCallback = (node: IHeapNode, snapshot: IHeapSnapshot) => boolean;
53
54
  declare function filterNodesInPlace(idSet: Set<number>, snapshot: IHeapSnapshot, cb: IterateNodeCallback): void;
54
55
  declare function applyToNodes(idSet: Set<number>, snapshot: IHeapSnapshot, cb: (node: IHeapNode, snapshot: IHeapSnapshot) => void, options?: AnyOptions): void;
@@ -229,6 +230,7 @@ declare const _default: {
229
230
  mapToObject: typeof mapToObject;
230
231
  markAllDetachedFiberNode: typeof markAllDetachedFiberNode;
231
232
  markAlternateFiberNode: typeof markAlternateFiberNode;
233
+ markDetachedFiberNode: typeof markDetachedFiberNode;
232
234
  memCache: Record<string, any>;
233
235
  normalizeBaseUrl: typeof normalizeBaseUrl;
234
236
  objectToMap: typeof objectToMap;
package/dist/lib/Utils.js CHANGED
@@ -126,7 +126,7 @@ function isFiberNode(node) {
126
126
  return name === 'FiberNode' || name === 'Detached FiberNode';
127
127
  }
128
128
  // quickly check the detachedness field
129
- // need to call hasHostRoot(node) before this function
129
+ // need to call markDetachedFiberNode(node) before this function
130
130
  // does not traverse and check the existance of HostRoot
131
131
  // NOTE: Doesn't work for FiberNode without detachedness field
132
132
  function isDetachedFiberNode(node) {
@@ -497,6 +497,35 @@ function hasHostRoot(node) {
497
497
  }
498
498
  cur = getReactFiberNode(cur, 'return');
499
499
  }
500
+ return false;
501
+ }
502
+ // The Fiber tree starts with a special type of Fiber node (HostRoot).
503
+ // return true if the node is a mounted Fiber node
504
+ function markDetachedFiberNode(node) {
505
+ if (node && node.is_detached) {
506
+ return false;
507
+ }
508
+ let cur = node;
509
+ const visitedIds = new Set();
510
+ const visitedNodes = new Set();
511
+ while (cur && isFiberNode(cur)) {
512
+ if (cur.id == null || visitedIds.has(cur.id)) {
513
+ break;
514
+ }
515
+ visitedNodes.add(cur);
516
+ // if a Fiber node whose dominator is neither root nor
517
+ // another Fiber node, then consider it as detached Fiber node
518
+ if (cur.dominatorNode && cur.dominatorNode.id !== 1) {
519
+ if (!isFiberNode(cur.dominatorNode)) {
520
+ cur.markAsDetached();
521
+ }
522
+ }
523
+ visitedIds.add(cur.id);
524
+ if (isHostRoot(cur)) {
525
+ return true;
526
+ }
527
+ cur = getReactFiberNode(cur, 'return');
528
+ }
500
529
  for (const visitedNode of visitedNodes) {
501
530
  visitedNode.markAsDetached();
502
531
  }
@@ -1533,8 +1562,9 @@ function dumpSnapshot(file, snapshot) {
1533
1562
  function markAllDetachedFiberNode(snapshot) {
1534
1563
  Console_1.default.overwrite('marking all detached Fiber nodes...');
1535
1564
  snapshot.nodes.forEach(node => {
1536
- // hasHostRoot checks and marks detached Fiber Nodes
1537
- isFiberNode(node) && !hasHostRoot(node);
1565
+ if (isFiberNode(node)) {
1566
+ markDetachedFiberNode(node);
1567
+ }
1538
1568
  });
1539
1569
  }
1540
1570
  function markAlternateFiberNode(snapshot) {
@@ -1891,6 +1921,7 @@ exports.default = {
1891
1921
  mapToObject,
1892
1922
  markAllDetachedFiberNode,
1893
1923
  markAlternateFiberNode,
1924
+ markDetachedFiberNode,
1894
1925
  memCache,
1895
1926
  normalizeBaseUrl,
1896
1927
  objectToMap,
@@ -97,7 +97,7 @@ class MemoryBarChart {
97
97
  }
98
98
  loadPlotData(options = {}) {
99
99
  // plot data for a single run
100
- if (!options.controlWorkDirs && !options.treatmentWorkDir) {
100
+ if (!options.controlWorkDirs && !options.treatmentWorkDirs) {
101
101
  return this.loadPlotDataFromWorkDir(options);
102
102
  }
103
103
  // plot data for control and test run
@@ -105,7 +105,7 @@ class MemoryBarChart {
105
105
  workDir: options.controlWorkDirs && options.controlWorkDirs[0],
106
106
  });
107
107
  const testPlotData = this.loadPlotDataFromWorkDir({
108
- workDir: options.treatmentWorkDir,
108
+ workDir: options.treatmentWorkDirs && options.treatmentWorkDirs[0],
109
109
  });
110
110
  // merge plot data
111
111
  return this.mergePlotData([controlPlotData, testPlotData]);
@@ -21,6 +21,7 @@ const FilterOverSizedNodeAsLeak_rule_1 = require("./rules/FilterOverSizedNodeAsL
21
21
  const FilterStackTraceFrame_rule_1 = require("./rules/FilterStackTraceFrame.rule");
22
22
  const FilterTrivialNode_rule_1 = require("./rules/FilterTrivialNode.rule");
23
23
  const FilterUnmountedFiberNode_rule_1 = require("./rules/FilterUnmountedFiberNode.rule");
24
+ const FilterXMLHTTPRequest_rule_1 = require("./rules/FilterXMLHTTPRequest.rule");
24
25
  const list = [
25
26
  new FilterByExternalFilter_rule_1.FilterByExternalFilterRule(),
26
27
  new FilterTrivialNode_rule_1.FilterTrivialNodeRule(),
@@ -29,5 +30,6 @@ const list = [
29
30
  new FilterUnmountedFiberNode_rule_1.FilterUnmountedFiberNodeRule(),
30
31
  new FilterDetachedDOMElement_rule_1.FilterDetachedDOMElementRule(),
31
32
  new FilterStackTraceFrame_rule_1.FilterStackTraceFrameRule(),
33
+ new FilterXMLHTTPRequest_rule_1.FilterXMLHTTPRequestRule(),
32
34
  ];
33
35
  exports.default = (0, InternalValueSetter_1.setInternalValue)(list, __filename, Constant_1.default.internalDir);
@@ -15,6 +15,5 @@ import { ILeakObjectFilterRule, LeakDecision } from '../BaseLeakFilter.rule';
15
15
  */
16
16
  export declare class FilterDetachedDOMElementRule implements ILeakObjectFilterRule {
17
17
  filter(_config: MemLabConfig, node: IHeapNode): LeakDecision;
18
- protected checkDetachedFiberNode(config: MemLabConfig, node: IHeapNode): boolean;
19
18
  }
20
19
  //# sourceMappingURL=FilterDetachedDOMElement.rule.d.ts.map
@@ -23,18 +23,31 @@ class FilterDetachedDOMElementRule {
23
23
  const isDetached = Utils_1.default.isDetachedDOMNode(node, {
24
24
  ignoreInternalNode: true,
25
25
  });
26
- if (isDetached) {
26
+ if (isDetached &&
27
+ !isDominatedByEdgeName(node, 'stateNode') &&
28
+ !isDetachedDOMNodeDominatedByDehydratedMemoizedState(node)) {
27
29
  return BaseLeakFilter_rule_1.LeakDecision.LEAK;
28
30
  }
29
31
  return BaseLeakFilter_rule_1.LeakDecision.MAYBE_LEAK;
30
32
  }
31
- checkDetachedFiberNode(config, node) {
32
- if (!config.detectFiberNodeLeak ||
33
- !Utils_1.default.isFiberNode(node) ||
34
- Utils_1.default.hasHostRoot(node)) {
35
- return false;
36
- }
37
- return !Utils_1.default.isNodeDominatedByDeletionsArray(node);
38
- }
39
33
  }
40
34
  exports.FilterDetachedDOMElementRule = FilterDetachedDOMElementRule;
35
+ function isDominatedByEdgeName(node, edgeNameOrIndex) {
36
+ var _a;
37
+ const referrerNode = node.getAnyReferrerNode(edgeNameOrIndex);
38
+ if (referrerNode == null) {
39
+ return false;
40
+ }
41
+ return referrerNode.id === ((_a = node.dominatorNode) === null || _a === void 0 ? void 0 : _a.id);
42
+ }
43
+ // check if the input is a detached DOM node dominated by a 'dehydrated'
44
+ // edge from a memoizedState. In this case, the node is not a memory leak
45
+ function isDetachedDOMNodeDominatedByDehydratedMemoizedState(node) {
46
+ var _a;
47
+ const referrerNode = node.getAnyReferrerNode('dehydrated', 'property');
48
+ if (referrerNode == null) {
49
+ return false;
50
+ }
51
+ return (referrerNode.id === ((_a = node.dominatorNode) === null || _a === void 0 ? void 0 : _a.id) &&
52
+ isDominatedByEdgeName(referrerNode, 'memoizedState'));
53
+ }
@@ -26,9 +26,10 @@ class FilterUnmountedFiberNodeRule {
26
26
  return BaseLeakFilter_rule_1.LeakDecision.MAYBE_LEAK;
27
27
  }
28
28
  checkDetachedFiberNode(config, node) {
29
- if (!config.detectFiberNodeLeak ||
30
- !Utils_1.default.isFiberNode(node) ||
31
- Utils_1.default.hasHostRoot(node)) {
29
+ if (!config.detectFiberNodeLeak || !Utils_1.default.isFiberNode(node)) {
30
+ return false;
31
+ }
32
+ if (!Utils_1.default.isDetachedFiberNode(node)) {
32
33
  return false;
33
34
  }
34
35
  return !Utils_1.default.isNodeDominatedByDeletionsArray(node);
@@ -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 { MemLabConfig } from '../../Config';
11
+ import type { IHeapNode } from '../../Types';
12
+ import { ILeakObjectFilterRule, LeakDecision } from '../BaseLeakFilter.rule';
13
+ /**
14
+ * mark XMLHTTPRequest with status ok as memory leaks
15
+ */
16
+ export declare class FilterXMLHTTPRequestRule implements ILeakObjectFilterRule {
17
+ filter(_config: MemLabConfig, node: IHeapNode): LeakDecision;
18
+ protected checkFinishedXMLHTTPRequest(node: IHeapNode): boolean;
19
+ }
20
+ //# sourceMappingURL=FilterXMLHTTPRequest.rule.d.ts.map
@@ -0,0 +1,30 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.FilterXMLHTTPRequestRule = void 0;
13
+ const BaseLeakFilter_rule_1 = require("../BaseLeakFilter.rule");
14
+ /**
15
+ * mark XMLHTTPRequest with status ok as memory leaks
16
+ */
17
+ class FilterXMLHTTPRequestRule {
18
+ filter(_config, node) {
19
+ return this.checkFinishedXMLHTTPRequest(node)
20
+ ? BaseLeakFilter_rule_1.LeakDecision.LEAK
21
+ : BaseLeakFilter_rule_1.LeakDecision.MAYBE_LEAK;
22
+ }
23
+ checkFinishedXMLHTTPRequest(node) {
24
+ if (node.name !== 'XMLHttpRequest' || node.type !== 'native') {
25
+ return false;
26
+ }
27
+ return (node.findAnyReference((edge) => edge.toNode.name === '{"status":"ok"}') != null);
28
+ }
29
+ }
30
+ exports.FilterXMLHTTPRequestRule = FilterXMLHTTPRequestRule;
@@ -37,7 +37,7 @@ export default class NormalizedTrace {
37
37
  private static buildTraceToPathMap;
38
38
  private static pushLeakPathToCluster;
39
39
  private static initEmptyCluster;
40
- static clusterControlTreatmentPaths(leakPathsFromControlRuns: LeakTracePathItem[][], controlSnapshots: IHeapSnapshot[], treatmentPaths: LeakTracePathItem[], treatmentSnapshot: IHeapSnapshot, aggregateDominatorMetrics: AggregateNodeCb, option?: {
40
+ static clusterControlTreatmentPaths(leakPathsFromControlRuns: LeakTracePathItem[][], controlSnapshots: IHeapSnapshot[], leakPathsFromTreatmentRuns: LeakTracePathItem[][], treatmentSnapshots: IHeapSnapshot[], aggregateDominatorMetrics: AggregateNodeCb, option?: {
41
41
  strategy?: IClusterStrategy;
42
42
  }): ControlTreatmentClusterResult;
43
43
  static generateUnClassifiedClusters(paths: LeakTracePathItem[], snapshot: IHeapSnapshot, aggregateDominatorMetrics: AggregateNodeCb): TraceCluster[];
@@ -310,43 +310,64 @@ class NormalizedTrace {
310
310
  leakedNodeIds: new Set(),
311
311
  };
312
312
  }
313
- static clusterControlTreatmentPaths(leakPathsFromControlRuns, controlSnapshots, treatmentPaths, treatmentSnapshot, aggregateDominatorMetrics, option = {}) {
313
+ static clusterControlTreatmentPaths(leakPathsFromControlRuns, controlSnapshots, leakPathsFromTreatmentRuns, treatmentSnapshots, aggregateDominatorMetrics, option = {}) {
314
314
  const result = {
315
- controlOnlyClusters: [],
315
+ controlLikelyOrOnlyClusters: [],
316
316
  treatmentOnlyClusters: [],
317
+ treatmentLikelyClusters: [],
317
318
  hybridClusters: [],
318
319
  };
319
320
  Console_1.default.overwrite('Clustering leak traces');
320
321
  const totalControlPaths = leakPathsFromControlRuns.reduce((count, leakPaths) => count + leakPaths.length, 0);
321
- if (totalControlPaths === 0 && treatmentPaths.length === 0) {
322
+ const totalTreatmentPaths = leakPathsFromTreatmentRuns.reduce((count, leakPaths) => count + leakPaths.length, 0);
323
+ if (totalControlPaths === 0 && totalTreatmentPaths === 0) {
322
324
  Console_1.default.midLevel('No leaks found');
323
325
  return result;
324
326
  }
325
327
  // sample paths if there are too many
326
328
  const flattenedLeakPathsFromControlRuns = leakPathsFromControlRuns.reduce((arr, leakPaths) => [...arr, ...leakPaths], []);
327
329
  const controlPaths = this.samplePaths(flattenedLeakPathsFromControlRuns);
328
- treatmentPaths = this.samplePaths(treatmentPaths);
330
+ const pathsForEachTreatmentGroup = leakPathsFromTreatmentRuns.map((treatmentPaths) => this.samplePaths(treatmentPaths));
329
331
  // build control trace to control path map
330
332
  const controlTraceToPathMap = NormalizedTrace.buildTraceToPathMap(controlPaths);
331
333
  const controlTraces = Array.from(controlTraceToPathMap.keys());
332
- // build treatment trace to treatment path map
333
- const treatmentTraceToPathMap = NormalizedTrace.buildTraceToPathMap(treatmentPaths);
334
- const treatmentTraces = Array.from(treatmentTraceToPathMap.keys());
334
+ // build treatment trace to treatment path maps
335
+ // we need to know the mapping to each treatment group
336
+ // to figure out if a trace cluster contains traces from all treatment groups
337
+ const treatmentTraceToPathMaps = pathsForEachTreatmentGroup.map(treatmentPaths => NormalizedTrace.buildTraceToPathMap(treatmentPaths));
338
+ const treatmentTraceToPathMap = new Map();
339
+ const treatmentTraces = [];
340
+ for (const map of treatmentTraceToPathMaps) {
341
+ for (const [key, value] of map.entries()) {
342
+ treatmentTraceToPathMap.set(key, value);
343
+ treatmentTraces.push(key);
344
+ }
345
+ }
335
346
  // cluster traces from both the control group and the treatment group
336
347
  const { allClusters } = NormalizedTrace.diffTraces([...controlTraces, ...treatmentTraces], [], option);
337
- // pick one of the control heap snapshots
348
+ // pick one of the control and treatment heap snapshots
338
349
  const controlSnapshot = controlSnapshots[0];
350
+ const treatmentSnapshot = treatmentSnapshots[0];
339
351
  // construct TraceCluster from clustering result
340
352
  allClusters.forEach((traces) => {
341
353
  var _a, _b;
342
354
  const controlCluster = NormalizedTrace.initEmptyCluster(controlSnapshot);
343
355
  const treatmentCluster = NormalizedTrace.initEmptyCluster(treatmentSnapshot);
356
+ // a set containing each the treatment group that
357
+ // has at least one trace in this cluster
358
+ const treatmentSetWithClusterTrace = new Set();
344
359
  for (const trace of traces) {
345
360
  const normalizedTrace = trace;
346
361
  if (controlTraceToPathMap.has(normalizedTrace)) {
347
362
  NormalizedTrace.pushLeakPathToCluster(controlTraceToPathMap, normalizedTrace, controlCluster);
348
363
  }
349
364
  else {
365
+ for (let i = 0; i < treatmentTraceToPathMaps.length; ++i) {
366
+ if (treatmentTraceToPathMaps[i].has(normalizedTrace)) {
367
+ treatmentSetWithClusterTrace.add(i);
368
+ break;
369
+ }
370
+ }
350
371
  NormalizedTrace.pushLeakPathToCluster(treatmentTraceToPathMap, normalizedTrace, treatmentCluster);
351
372
  }
352
373
  }
@@ -360,11 +381,19 @@ class NormalizedTrace {
360
381
  if (treatmentClusterSize > 0) {
361
382
  this.calculateClusterRetainedSize(treatmentCluster, treatmentSnapshot, aggregateDominatorMetrics);
362
383
  }
363
- if (controlClusterSize === 0) {
384
+ if (controlClusterSize === 0 &&
385
+ treatmentSetWithClusterTrace.size === leakPathsFromTreatmentRuns.length) {
386
+ // only when the leak cluster consists of traces from all treatment groups
364
387
  result.treatmentOnlyClusters.push(treatmentCluster);
365
388
  }
389
+ else if (controlClusterSize === 0) {
390
+ // when the leak cluster consists of traces from
391
+ // some but not all of treatment groups
392
+ result.treatmentLikelyClusters.push(treatmentCluster);
393
+ }
366
394
  else if (treatmentClusterSize === 0) {
367
- result.controlOnlyClusters.push(controlCluster);
395
+ // when the leak cluster consists of traces from any of the control groups
396
+ result.controlLikelyOrOnlyClusters.push(controlCluster);
368
397
  }
369
398
  else {
370
399
  result.hybridClusters.push({
@@ -374,7 +403,8 @@ class NormalizedTrace {
374
403
  }
375
404
  });
376
405
  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); });
377
- 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); });
406
+ result.treatmentLikelyClusters.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); });
407
+ result.controlLikelyOrOnlyClusters.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); });
378
408
  result.hybridClusters.sort((g1, g2) => {
379
409
  var _a, _b, _c, _d;
380
410
  return ((_a = g2.control.retainedSize) !== null && _a !== void 0 ? _a : 0) +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memlab/core",
3
- "version": "1.1.22",
3
+ "version": "1.1.23",
4
4
  "license": "MIT",
5
5
  "description": "memlab core libraries",
6
6
  "author": "Liang Gong <lgong@fb.com>",