@memlab/core
Version:
memlab core libraries
645 lines (644 loc) • 27 kB
JavaScript
"use strict";
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @oncall memory_lab
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MemLabConfig = exports.ErrorHandling = exports.OutputFormat = exports.TraceObjectMode = void 0;
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const RunningModes_1 = __importDefault(require("../modes/RunningModes"));
const Console_1 = __importDefault(require("./Console"));
const Constant_1 = __importDefault(require("./Constant"));
const FileManager_1 = __importStar(require("./FileManager"));
const InternalValueSetter_1 = require("./InternalValueSetter");
const devices = Constant_1.default.isFRL
? {}
: Constant_1.default.isFB
? // eslint-disable-next-line @typescript-eslint/no-var-requires
require('puppeteer-core').KnownDevices
: // eslint-disable-next-line @typescript-eslint/no-var-requires
require('puppeteer').KnownDevices;
// default viewport config for desktop
const windowWidth = 1680;
const windowHeight = 1080;
const defaultViewport = {
width: windowWidth,
height: windowHeight,
deviceScaleFactor: 1,
};
/** @internal */
var TraceObjectMode;
(function (TraceObjectMode) {
TraceObjectMode[TraceObjectMode["Default"] = 1] = "Default";
TraceObjectMode[TraceObjectMode["SelectedJSObjects"] = 2] = "SelectedJSObjects";
})(TraceObjectMode || (exports.TraceObjectMode = TraceObjectMode = {}));
/** @internal */
var OutputFormat;
(function (OutputFormat) {
OutputFormat[OutputFormat["Text"] = 1] = "Text";
OutputFormat[OutputFormat["Json"] = 2] = "Json";
})(OutputFormat || (exports.OutputFormat = OutputFormat = {}));
/** @internal */
var ErrorHandling;
(function (ErrorHandling) {
ErrorHandling[ErrorHandling["Halt"] = 1] = "Halt";
ErrorHandling[ErrorHandling["Throw"] = 2] = "Throw";
})(ErrorHandling || (exports.ErrorHandling = ErrorHandling = {}));
/** @internal */
class MemLabConfig {
constructor(options = {}) {
// init properties, they can be configured manually
this.init(options);
// init built-in properties, they should not be configured manually
this.initInternalConfigs();
}
initInternalConfigs() {
// DO NOT SET PARAMETER HERE
this._isFullRun = false;
this._reportLeaksInTimers = true;
this._deviceManualOverridden = false;
this._timerNodes = ['Pending activities'];
this._timerEdges = [];
this._isHeadfulBrowser = false;
this._disableWebSecurity = false;
this.targetApp = Constant_1.default.unset;
this.targetTab = Constant_1.default.unset;
this.analysisMode = Constant_1.default.unset;
this.focusFiberNodeId = -1;
this.isFB = Constant_1.default.isFB;
this._chromeUserDataDir = os_1.default.tmpdir();
// assuming the Evn doesn't support Xvfb before checking
this.machineSupportsXVFB = false;
// by default we want to use Xvfb if the Env supports it
this.useXVFB = true;
this.specifiedEngine = false;
// set by the heap-analysis package if HeapConfig is used
this.heapConfig = null;
// set puppeteer configuration
this.puppeteerConfig = {
headless: !this._isHeadfulBrowser,
devtools: this.openDevtoolsConsole,
protocolTimeout: Constant_1.default.defaultProtocolTimeout,
// IMPORTANT: test ContinuousTest before change this config
ignoreHTTPSErrors: true,
// Support running on Windows
ignoreDefaultArgs: ['--disable-extensions'],
userDataDir: this._chromeUserDataDir,
// Chromium in ContinuousTest needs this args
args: [
'--no-sandbox',
// Disable popup notification
'--disable-notifications',
// Automatically allows media stream requests
'--use-fake-ui-for-media-stream',
// Feeds fake video stream
'--use-fake-device-for-media-stream',
// V8 GC move fewer objects around
'--js-flags="--no-move-object-start"',
// Alreay on by default, just want to make sure
'--enable-precise-memory-info',
// sites using WebGL (three.js) will fail
// in headless mode without WebGL
'--enable-webgl',
'browser-test',
],
defaultViewport,
};
// by default don't emulate any device, use the desktop config
this.emulateDevice = null;
// the default JS engine of snapshot
this.jsEngine = Constant_1.default.defaultEngine;
// the default browser (Chromium)
this._browser = 'chrome';
// a list of package information
this.packageInfo = [];
// a set of additional GKs to be enabled
this.addEnableGK = new Set();
// a set of additional GKs to be disabled
this.addDisableGK = new Set();
// a list of additional QEs to be enabled
this.qes = [];
// check if running in ondemand devvm
this.isOndemand = false;
// configuration for analyzing external snapshots
this.useExternalSnapshot = false;
// new version of heap snapshot has a detachedness field
this.snapshotHasDetachedness = false;
// by default running in regular mode
this.runningMode = RunningModes_1.default.get('regular', this);
// intercept and log JavaScript Code in browser
this.interceptScript = false;
// rewrite JavaScript Code in browser
this.instrumentJS = false;
// external heap snapshot paths, if enabled
this.externalSnapshotFilePaths = [];
// mute the console output, if enabled
this.muteConsole = false;
// log all leak traces, each as an unclassified cluster
this.logUnclassifiedClusters = false;
// If true, the detailed JSON file of each representative
// trace (for visualization) will include detailed object
// info for each Fiber node on the return chain.
// This may bloat the trace size from 100KB to 50MB.
this.includeObjectInfoInTraceReturnChain = false;
// by default halt the program when utils.haltOrThrow is calleds
this.errorHandling = ErrorHandling.Halt;
// by default use clustering based on heurastics
this.isMLClustering = false;
// linkage max distance for caclulating distances among clusters
this.mlClusteringLinkageMaxDistance = 0.7;
// TF/IDF maximum document frequency
this.mlMaxDF = 1;
// if true, evaluating results with sequential clustering
this.isSequentialClustering = false;
// if true, evaluating results with sequential
// clustering with multiple iterations
this.isMultiIterationSeqClustering = false;
// split the sample leak traces into 4 smaller ones by default.
this.seqClusteringSplitCount = 4;
// the number of iterations for multi-iteration sequential clustering
this.multiIterSeqClusteringIteration = 1;
// the number of trace samples to retain from each cluster
this.multiIterSeqClusteringSampleSize = Infinity;
// if true, split dataset into trunks
// with random order for sequential clustering
this.seqClusteringIsRandomChunks = false;
// maximum number of samples as input for leak trace clustering
this.maxSamplesForClustering = 5000;
// extra E2E run info (other than the fields defined in
// RunMetaInfo like app, interaction, browserInfo).
// Information saved in this map will be
// auto-serialized to run-meta.json when the file is saved
// and auto-deserialized from run-meta.json when the file is loaded
this.extraRunInfoMap = new Map();
// if specified via CLI options, this will filter leak traces by
// node and edge names in the leak trace
this.filterTraceByName = null;
// if true, memlab will not wait for the browser to close successfully
this.skipBrowserCloseWait = false;
// if true, serialized leak trace will have a simplified representation
// for code object, which could be large and cause OOM during serualization
this.simplifyCodeSerialization = !Constant_1.default.isFB;
// when JSONify the leak trace for each node (object), we serialize at most
// the specified number of edges (references)
this.maxNumOfEdgesToJSONifyPerNode = 80;
// when JSONifying the leak trace, we serialize by invoking JSONifyNode up
// to the specified depth within the objects in the trace
this.maxLevelsOfTraceToJSONify = 4;
// HTML tags prioritized for display in MemLab's leak trace when showing
// DOM and detached DOM elements. If the representation is too long,
// lower-priority tags may be omitted.
this.defaultPrioritizedHTMLTagAttributes = new Set([
'id',
'role',
'type',
'data-testid',
'data-testids',
'name',
'class',
]);
// set the default running timeout if used
this.defaultRunTimeoutInSec = 30 * 60;
}
// initialize configurable parameters
init(options = {}) {
// if true dump extra debug info to terminal
this.verbose = false;
// dump the web console's output to terminal
this.dumpWebConsole = false;
// open dev-tools console when interacting with the page
this.openDevtoolsConsole = false;
// clear console, this may eliminate leaks retained by console
this.clearConsole = false;
// consider dev-tools console when search for leak trace
this.traverseDevToolsConsole = true;
// if true skip all warmup
this.skipWarmup = false;
// if true skip all snapshots
this.skipSnapshot = false;
// if true skip all screenshots
this.skipScreenshot = false;
// if true skip all scrolling
this.skipScroll = false;
// if true skip all extra operations on target and final
this.skipExtraOps = false;
// if true skip all GC
this.skipGC = false;
// true if running in ContinuousTest
this.isContinuousTest = false;
// true if reclustering is turned off
this.noReCluster = false;
// true if running a local test
this.isTest = false;
// true if running in local puppeteer mode
this.isLocalPuppeteer = false;
// true if pausing on every step
this.isManualDebug = false;
// number of warmup repeat in each browser tab instance
this.warmupRepeat = 2;
// by default, analyzing heap in main thread
this.isAnalyzingMainThread = true;
// target worker's title
this.targetWorkerTitle = null;
// if true, display leaked component outlines in headful browser
this.displayLeakOutlines = false;
// default waiting time when there is no page load checker callback
this.delayWhenNoPageLoadCheck = 2000;
// repeat the intermediate sequence
this.repeatIntermediateTabs = 1;
// the # of retries when interaction fails
this.interactionFailRetry = 1;
// the # of retries when initial load fails
this.initialLoadFailRetry = 2;
// timeout for checking the presence of non-optional click target
this.presenceCheckTimeout = 2 * 60 * 1000;
// default waiting time before exit when exception occurs during page interaction
this.delayBeforeExitUponException = 2 * 60 * 1000;
// Chrome window width
this.windowWidth = windowWidth;
// Chrome window height
this.windowHeight = windowHeight;
// disable all scroll
this.disableScroll = false;
// default number of scrolls on target page
this.scrollRepeat = 5;
// extra waiting time for the target page
this.extraWaitingForTarget = 0;
// extra waiting time for the final page
this.extraWaitingForFinal = 0;
// if a click operation does not setup a delay, use this delay after click
this.defaultAfterClickDelay = 2000;
// delay after garbage collection
this.waitAfterGC = 1400;
// delay after checking visual completion
this.waitAfterPageLoad = 500;
// delay after browser interaction
this.waitAfterOperation = 200;
// timeout for warmup page load (30 seconds)
this.warmupPageLoadTimeout = 30 * 1000;
// timeout for initial page load (10 minute)
this.initialPageLoadTimeout = 10 * 60 * 1000;
// extra delay after scrolling
this.waitAfterScrolling = 5000;
// extra delay after typing
this.waitAfterTyping = 1000;
// page load checker: default wait for network idle in scenario test
this.waitForNetworkInDefaultScenario = 10000;
// default repeat for stress testing
this.stressTestRepeat = 5;
// only show leaks with detached HTML elements
this.avoidLeakWithoutDetachedElements = false;
// if true, hide potential browser memory leak
this.hideBrowserLeak = true;
// recursively dump the key object of the WeakMap
this.chaseWeakMapEdge = true;
// consider outstanding Fiber nodes from target page as leaks
this.detectFiberNodeLeak = true;
// granted permissions to browser, so it won't ask by popping up dialog
this.grantedPermissions = [
'geolocation',
'camera',
'microphone',
'accelerometer',
'gyroscope',
'magnetometer',
'payment-handler',
'background-sync',
];
// unbounded growth check only reports objects that are monotonic increasing
this.monotonicUnboundGrowthOnly = false;
// object size below the threshold won't be reported
this.unboundSizeThreshold = 10 * 1024;
// threshold for switching between fast store and slower store
// of NumericDictionary used by heap snapshot parser
this.heapParserDictFastStoreSize = 5000000;
// if true reset the GK list in visit synthesizer
this.resetGK = false;
// default userAgent, if undefined use puppeteer's default value
this.defaultUserAgent = Constant_1.default.defaultUserAgent;
// dump the heap node information in path summary (focuse mode)
this.dumpNodeInfo = false;
// the max steps when doing BFS search for shortest path
this.maxSearchSteps = Infinity;
// the max number of outgoing edges to be considered for a single node,
// when doing BFS search for shortest path
this.maxSearchReferences = Infinity;
// the set of nodes to expand one more level in the HTML report
this.nodeToShowMoreInfo = new Set();
// if true ignore DevTools console leak
this.ignoreDevToolsConsoleLeak = false;
// if true ignore InternalNode when searching for leak trace
this.ignoreInternalNode = false;
// node names excluded from the trace finding
this.nodeNameBlockList = new Set(['system / PropertyCell']);
// edge names excluded from the trace finding
this.edgeNameBlockList = new Set([
'feedback_cell',
'part of key -> value pair in ephemeron table',
]);
// node names less preferable in trace finding
this.nodeNameGreyList = new Set([
'InternalNode',
'Pending activities',
'StyleEngine',
...Constant_1.default.V8SyntheticRoots,
]);
// edge names less preferable in trace finding
this.edgeNameGreyList = new Set(['alternate', 'firstEffect', 'lastEffect']);
// browser port for debugging
this.localBrowserPort = 57305;
// skip running the following test types in ContinuousTest
this.ignoreTypesInContinuousTest = new Set();
// skip running the following apps in ContinuousTest
this.ignoreAppsInContinuousTest = new Set();
// a threshold hold to prevent exceeding refer header size limit
this.URLParamLengthLimit = 400;
// a set of ignored edges when clustering object shapes
this.edgeIgnoreSetInShape = new Set(['__proto__', 'map']);
// a set of ignored nodes when clustering object shapes
this.nodeIgnoreSetInShape = new Set([
'(closure)',
'(array)',
'(number)',
'(system)',
'system / Context',
'system / Oddball',
]);
// if true consider over sized objects as memory leak
this.oversizeObjectAsLeak = false;
// if larger than this threshold, consider as memory leak
this.oversizeThreshold = 0;
// when specified default, this mode will trace/diff all objects
// you can specified other modes (e.g., selected JS objects only)
this.traceAllObjectsMode = TraceObjectMode.Default;
// only report leak clusters with aggregated retained size
// bigger than this threshold
this.clusterRetainedSizeThreshold = 0;
// initialize file and directory paths
FileManager_1.default.initDirs(this, options);
}
getAdditionalConfigInContinuousTest(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_app,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_interaction) {
return [];
}
// default config instance created from CLI
static getInstance() {
if (!MemLabConfig.instance) {
const config = new MemLabConfig();
// consider objects kept alive by timers as leaks
config.reportLeaksInTimers = true;
// assign configuration to console manager
Console_1.default.setConfig(config);
// set the singleton
MemLabConfig.instance = config;
}
return MemLabConfig.instance;
}
static resetConfigWithTransientDir() {
const config = MemLabConfig.getInstance();
FileManager_1.default.initDirs(config, { transient: true });
return config;
}
haltOrThrow(msg,
// eslint-disable-next-line @typescript-eslint/no-empty-function
secondaryPrintCallback = () => { }) {
if (this.errorHandling === ErrorHandling.Halt) {
Console_1.default.error(msg);
secondaryPrintCallback();
process.exit(1);
}
else {
throw new Error(msg);
}
}
setTarget(app, tab) {
this.targetApp = app;
this.targetTab = tab;
}
set userDataDir(chromeUserDataDir) {
this._chromeUserDataDir = chromeUserDataDir;
if (this.puppeteerConfig != null) {
this.puppeteerConfig.userDataDir = chromeUserDataDir;
}
}
get userDataDir() {
return this._chromeUserDataDir;
}
set scenario(scenario) {
this._scenario = scenario;
if (scenario == null) {
return;
}
let hasCallback = false;
const externalFilter = {};
// set leak filter
const { leakFilter, beforeLeakFilter, retainerReferenceFilter } = scenario;
if (typeof leakFilter === 'function') {
hasCallback = true;
externalFilter.leakFilter = leakFilter;
}
// set leak filter init callback
if (typeof beforeLeakFilter === 'function') {
hasCallback = true;
externalFilter.beforeLeakFilter = beforeLeakFilter;
}
// set retainer reference filter callback
if (typeof retainerReferenceFilter === 'function') {
hasCallback = true;
externalFilter.retainerReferenceFilter = retainerReferenceFilter;
}
if (hasCallback) {
this.externalLeakFilter = externalFilter;
}
}
get scenario() {
return this._scenario;
}
set isFullRun(isFull) {
this._isFullRun = isFull;
}
get isFullRun() {
return this._isFullRun;
}
set browser(v) {
if (!Constant_1.default.supportedBrowsers[v]) {
this.haltOrThrow(`Invalid browser: ${v} ` +
`(supported browsers: ${Object.keys(Constant_1.default.supportedBrowsers).join(', ')})`);
}
this._browser = Constant_1.default.supportedBrowsers[v];
this.puppeteerConfig.executablePath = this.browserBinaryPath;
if (this.verbose) {
Console_1.default.lowLevel(`set browser: ${this.puppeteerConfig.executablePath}`);
}
}
get browser() {
return this._browser || 'chrome';
}
set isHeadfulBrowser(isHeadful) {
this._isHeadfulBrowser = isHeadful;
this.puppeteerConfig.headless = !isHeadful;
if (isHeadful) {
// if running in headful mode
this.disableXvfb();
}
}
get isHeadfulBrowser() {
return this._isHeadfulBrowser;
}
set disableWebSecurity(disable) {
this._disableWebSecurity = disable;
const args = this.puppeteerConfig.args;
const flag = '--disable-web-security';
const index = args.indexOf(flag);
if (disable) {
// add the flag
if (index < 0) {
args.push(flag);
}
}
else {
// remove the flag
if (index >= 0) {
args.splice(index, 1);
}
}
}
get disableWebSecurity() {
return this._disableWebSecurity;
}
get browserBinaryPath() {
return path_1.default.join(this.browserDir, this.browser);
}
// Default input option for file manager.
// If no other input option is provided, the file manager
// will generate directories and files based on this default option.
set defaultFileManagerOption(fileOption) {
FileManager_1.FileManager.defaultFileOption = fileOption;
// initialize file and directory paths
FileManager_1.default.initDirs(this, fileOption);
}
get defaultFileManagerOption() {
return FileManager_1.FileManager.defaultFileOption;
}
set reportLeaksInTimers(shouldReport) {
if (shouldReport) {
this.removeFromSet(this.nodeNameBlockList, this._timerNodes);
this.removeFromSet(this.edgeNameBlockList, this._timerEdges);
}
else {
this.addToSet(this.nodeNameBlockList, this._timerNodes);
this.addToSet(this.edgeNameBlockList, this._timerEdges);
}
this._reportLeaksInTimers = shouldReport;
}
get reportLeaksInTimers() {
return this._reportLeaksInTimers;
}
setDevice(deviceName, options = {}) {
if (!options.manualOverride && this._deviceManualOverridden) {
return;
}
this._deviceManualOverridden = !!options.manualOverride;
// set pc device
if (deviceName == null || deviceName === 'pc') {
this.emulateDevice = null;
this.defaultUserAgent = Constant_1.default.defaultUserAgent;
this.puppeteerConfig.defaultViewport = defaultViewport;
return;
}
// set mobile device
const name = deviceName.split('-').join(' ');
if (!devices[name]) {
const supportedDevices = Array.from(Object.keys(devices))
.filter(name => isNaN(parseInt(name, 10)))
.map(name => name.split(' ').join('-'));
this.haltOrThrow(`Invalid device: ${name}`, () => {
Console_1.default.lowLevel(`(supported devies: ${supportedDevices.join(', ')})`);
});
}
this.emulateDevice = devices[name];
this.puppeteerConfig.defaultViewport = null;
this.defaultUserAgent = null;
}
setRunInfo(key, value) {
this.extraRunInfoMap.set(key, value);
}
removeFromSet(set, list) {
for (const v of list) {
set.delete(v);
}
}
addToSet(set, list) {
for (const v of list) {
set.add(v);
}
}
enableXvfb(display) {
var _a;
this.useXVFB = true;
this.puppeteerConfig.devtools = true;
(_a = this.puppeteerConfig.args) === null || _a === void 0 ? void 0 : _a.push(`--display=${display}`);
}
disableXvfb() {
const args = [];
for (const arg of this.puppeteerConfig.args || []) {
if (!arg.startsWith('--display=')) {
args.push(arg);
}
}
this.puppeteerConfig.args = args;
this.puppeteerConfig.devtools = false;
this.useXVFB = false;
}
}
exports.MemLabConfig = MemLabConfig;
/** @internal */
const config = MemLabConfig.getInstance();
(0, InternalValueSetter_1.setInternalValue)(config, __filename, Constant_1.default.internalDir);
/** @internal */
exports.default = config;