@memlab/core 1.1.9 → 1.1.11
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 +1 -1
- package/dist/lib/Config.js +5 -8
- package/dist/lib/HeapAnalyzer.d.ts +8 -0
- package/dist/lib/HeapAnalyzer.js +31 -0
- package/dist/lib/NodeHeap.js +1 -1
- package/dist/lib/Types.d.ts +33 -2
- package/dist/lib/Utils.d.ts +2 -0
- package/dist/lib/Utils.js +16 -2
- package/dist/lib/heap-data/MemLabTagStore.d.ts +1 -1
- package/dist/lib/heap-data/MemLabTagStore.js +1 -1
- package/dist/paths/TraceFinder.js +23 -4
- package/dist/trace-cluster/TraceBucket.js +7 -2
- package/package.json +1 -1
package/dist/lib/Config.d.ts
CHANGED
|
@@ -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(
|
|
209
|
+
set reportLeaksInTimers(shouldReport: boolean);
|
|
210
210
|
get reportLeaksInTimers(): boolean;
|
|
211
211
|
setDevice(deviceName: string, options?: {
|
|
212
212
|
manualOverride?: boolean;
|
package/dist/lib/Config.js
CHANGED
|
@@ -50,7 +50,7 @@ class MemLabConfig {
|
|
|
50
50
|
initInternalConfigs() {
|
|
51
51
|
// DO NOT SET PARAMETER HERE
|
|
52
52
|
this._isFullRun = false;
|
|
53
|
-
this._reportLeaksInTimers =
|
|
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
|
-
//
|
|
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(
|
|
400
|
-
if (
|
|
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 =
|
|
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>;
|
package/dist/lib/HeapAnalyzer.js
CHANGED
|
@@ -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
|
package/dist/lib/NodeHeap.js
CHANGED
|
@@ -93,7 +93,7 @@ exports.tagObject = tagObject;
|
|
|
93
93
|
* ```
|
|
94
94
|
*/
|
|
95
95
|
function dumpNodeHeapSnapshot() {
|
|
96
|
-
const randomID =
|
|
96
|
+
const randomID = `${Math.random()}`.replace('0.', '');
|
|
97
97
|
const file = path_1.default.join(FileManager_1.default.generateTmpHeapDir(), `nodejs-${randomID}.heapsnapshot`);
|
|
98
98
|
if (fs_extra_1.default.existsSync(file)) {
|
|
99
99
|
fs_extra_1.default.removeSync(file);
|
package/dist/lib/Types.d.ts
CHANGED
|
@@ -431,6 +431,35 @@ export interface IScenario {
|
|
|
431
431
|
* for memory leak filtering.
|
|
432
432
|
*/
|
|
433
433
|
url: () => string;
|
|
434
|
+
/**
|
|
435
|
+
* `setup` is the callback function that will be called only once
|
|
436
|
+
* after the initial page load. This callback can be used to log in
|
|
437
|
+
* if you have to (we recommend using {@link cookies})
|
|
438
|
+
* or to prepare data before the {@link action} call.
|
|
439
|
+
*
|
|
440
|
+
* * **Parameters**:
|
|
441
|
+
* * page: `Page` | the puppeteer [`Page`](https://pptr.dev/api/puppeteer.page)
|
|
442
|
+
* object, which provides APIs to interact with the web browser
|
|
443
|
+
*
|
|
444
|
+
* * **Examples**:
|
|
445
|
+
* ```typescript
|
|
446
|
+
* const scenario = {
|
|
447
|
+
* url: () => 'https://www.npmjs.com/',
|
|
448
|
+
* setup: async (page) => {
|
|
449
|
+
* // log in or prepare data for the interaction
|
|
450
|
+
* },
|
|
451
|
+
* action: async (page) => {
|
|
452
|
+
* await page.click('a[href="/link"]');
|
|
453
|
+
* },
|
|
454
|
+
* back: async (page) => {
|
|
455
|
+
* await page.click('a[href="/back"]');
|
|
456
|
+
* },
|
|
457
|
+
* }
|
|
458
|
+
*
|
|
459
|
+
* module.exports = scenario;
|
|
460
|
+
* ```
|
|
461
|
+
*/
|
|
462
|
+
setup?: InteractionsCallback;
|
|
434
463
|
/**
|
|
435
464
|
* `action` is the callback function that defines the interaction
|
|
436
465
|
* where you want to trigger memory leaks after the initial page load.
|
|
@@ -473,7 +502,7 @@ export interface IScenario {
|
|
|
473
502
|
* }
|
|
474
503
|
*
|
|
475
504
|
* module.exports = scenario;
|
|
476
|
-
```
|
|
505
|
+
* ```
|
|
477
506
|
*/
|
|
478
507
|
action?: InteractionsCallback;
|
|
479
508
|
/**
|
|
@@ -728,10 +757,12 @@ export interface IE2EScenarioVisitPlan {
|
|
|
728
757
|
numOfWarmup: number;
|
|
729
758
|
dataBuilder: Optional<IDataBuilder>;
|
|
730
759
|
isPageLoaded?: CheckPageLoadCallback;
|
|
760
|
+
scenario?: IScenario;
|
|
731
761
|
}
|
|
732
762
|
/** @internal */
|
|
733
763
|
export declare type OperationArgs = {
|
|
734
764
|
isPageLoaded?: CheckPageLoadCallback;
|
|
765
|
+
scenario?: Optional<IScenario>;
|
|
735
766
|
showProgress?: boolean;
|
|
736
767
|
failedURLs?: AnyRecord;
|
|
737
768
|
pageHistoryLength?: number[];
|
|
@@ -1228,7 +1259,7 @@ export interface IHeapNodeBasic {
|
|
|
1228
1259
|
*/
|
|
1229
1260
|
export declare type EdgeIterationCallback = (edge: IHeapEdge) => Optional<{
|
|
1230
1261
|
stop: boolean;
|
|
1231
|
-
}
|
|
1262
|
+
}> | void;
|
|
1232
1263
|
/**
|
|
1233
1264
|
* An `IHeapNode` instance represents a JS heap object in a heap snapshot.
|
|
1234
1265
|
* A heap snapshot is generally a graph where graph nodes are JS heap objects
|
package/dist/lib/Utils.d.ts
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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(
|
|
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) {
|