@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.
- package/dist/lib/Config.d.ts +9 -0
- package/dist/lib/Config.js +17 -1
- package/dist/lib/Constant.js +1 -0
- package/dist/lib/HeapAnalyzer.js +23 -13
- package/dist/lib/TraceSampler.d.ts +36 -0
- package/dist/lib/TraceSampler.js +78 -0
- package/dist/lib/Types.d.ts +5 -0
- package/dist/lib/Utils.d.ts +6 -0
- package/dist/lib/Utils.js +40 -3
- package/dist/lib/heap-data/HeapStringNode.js +25 -16
- package/dist/lib/leak-filters/rules/FilterOverSizedNodeAsLeak.rule.js +50 -0
- package/dist/lib/trace-filters/TraceFilterRuleList.js +2 -0
- package/dist/lib/trace-filters/rules/FilterCppRootsToDetachedDOMTrace.rule.d.ts +15 -0
- package/dist/lib/trace-filters/rules/FilterCppRootsToDetachedDOMTrace.rule.js +44 -0
- package/dist/paths/TraceFinder.js +1 -1
- package/dist/trace-cluster/TraceBucket.js +13 -11
- package/package.json +1 -1
package/dist/lib/Config.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/Config.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.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;
|
package/dist/lib/Constant.js
CHANGED
package/dist/lib/HeapAnalyzer.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
417
|
-
|
|
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;
|
package/dist/lib/Types.d.ts
CHANGED
|
@@ -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
|
package/dist/lib/Utils.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
131
|
+
const maxCount = Config_1.default.maxSamplesForClustering;
|
|
131
132
|
if (paths.length <= maxCount) {
|
|
132
133
|
return [...paths];
|
|
133
134
|
}
|
|
134
|
-
const
|
|
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 (
|
|
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),
|