UNPKG

@memlab/api

Version:
396 lines (395 loc) 16.3 kB
"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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.warmupAndTakeSnapshots = warmupAndTakeSnapshots; exports.run = run; exports.takeSnapshots = takeSnapshots; exports.findLeaks = findLeaks; exports.findLeaksBySnapshotFilePaths = findLeaksBySnapshotFilePaths; exports.analyze = analyze; exports.warmup = warmup; exports.testInBrowser = testInBrowser; const core_1 = require("@memlab/core"); const e2e_1 = require("@memlab/e2e"); const APIUtils_1 = __importDefault(require("./lib/APIUtils")); const BrowserInteractionResultReader_1 = __importDefault(require("./result-reader/BrowserInteractionResultReader")); const APIStateManager_1 = __importDefault(require("./state/APIStateManager")); /** * This API warms up web server, runs E2E interaction, and takes heap snapshots. * This is equivalent to running `memlab warmup-and-snapshot` in CLI. * This is also equivalent to warm up and call {@link takeSnapshots}. * * @param options configure browser interaction run * @returns browser interaction results * * **Examples**: * ```javascript * const {warmupAndTakeSnapshots} = require('@memlab/api'); * * (async function () { * const scenario = { * url: () => 'https://www.facebook.com', * }; * const result = await warmupAndTakeSnapshots({scenario}); * })(); * ``` */ function warmupAndTakeSnapshots() { return __awaiter(this, arguments, void 0, function* (options = {}) { const config = getConfigFromRunOptions(options); const state = APIStateManager_1.default.getAndUpdateState(config, options); const testPlanner = new e2e_1.TestPlanner({ config }); const { evalInBrowserAfterInitLoad } = options; if (!options.skipWarmup) { yield warmup({ testPlanner, config, evalInBrowserAfterInitLoad }); } yield testInBrowser({ testPlanner, config, evalInBrowserAfterInitLoad }); const ret = BrowserInteractionResultReader_1.default.from(config.workDir); APIStateManager_1.default.restoreState(config, state); return ret; }); } /** * This API runs browser interaction and find memory leaks triggered in browser * This is equivalent to running `memlab run` in CLI. * This is also equivalent to warm up, and call {@link takeSnapshots} * and {@link findLeaks}. * * @param runOptions configure browser interaction run * @returns memory leaks detected and a utility reading browser * interaction results from disk * * **Examples**: * ```javascript * const {run} = require('@memlab/api'); * * (async function () { * const scenario = { * url: () => 'https://www.facebook.com', * }; * const {leaks} = await run({scenario}); * })(); * ``` */ function run() { return __awaiter(this, arguments, void 0, function* (options = {}) { const config = getConfigFromRunOptions(options); const state = APIStateManager_1.default.getAndUpdateState(config, options); const testPlanner = new e2e_1.TestPlanner({ config }); const { evalInBrowserAfterInitLoad } = options; if (!options.skipWarmup) { yield warmup({ testPlanner, config, evalInBrowserAfterInitLoad }); } yield testInBrowser({ testPlanner, config, evalInBrowserAfterInitLoad }); const runResult = BrowserInteractionResultReader_1.default.from(config.workDir); const leaks = yield findLeaks(runResult); APIStateManager_1.default.restoreState(config, state); return { leaks, runResult }; }); } /** * This API runs E2E interaction and takes heap snapshots. * This is equivalent to running `memlab snapshot` in CLI. * * @param options configure browser interaction run * @returns a utility reading browser interaction results from disk * * **Examples**: * ```javascript * const {takeSnapshots} = require('@memlab/api'); * * (async function () { * const scenario = { * url: () => 'https://www.facebook.com', * }; * const result = await takeSnapshots({scenario}); * })(); * ``` */ function takeSnapshots() { return __awaiter(this, arguments, void 0, function* (options = {}) { const config = getConfigFromRunOptions(options); const state = APIStateManager_1.default.getAndUpdateState(config, options); const testPlanner = new e2e_1.TestPlanner(); const { evalInBrowserAfterInitLoad } = options; yield testInBrowser({ testPlanner, config, evalInBrowserAfterInitLoad }); const ret = BrowserInteractionResultReader_1.default.from(config.workDir); APIStateManager_1.default.restoreState(config, state); return ret; }); } /** * This API finds memory leaks by analyzing heap snapshot(s). * This is equivalent to `memlab find-leaks` in CLI. * * @param runResult return value of a browser interaction run * @param options configure memory leak detection run * @param options.consoleMode specify the terminal output * mode (see {@link ConsoleMode}) * @returns leak traces detected and clustered from the browser interaction * * **Examples**: * ```javascript * const {findLeaks, takeSnapshots} = require('@memlab/api'); * * (async function () { * const scenario = { * url: () => 'https://www.facebook.com', * }; * const result = await takeSnapshots({scenario, consoleMode: 'SILENT'}); * const leaks = findLeaks(result, {consoleMode: 'CONTINUOUS_TEST'}); * })(); * ``` */ function findLeaks(runResult_1) { return __awaiter(this, arguments, void 0, function* (runResult, options = {}) { const state = APIStateManager_1.default.getAndUpdateState(core_1.config, options); const workDir = runResult.getRootDirectory(); core_1.fileManager.initDirs(core_1.config, { workDir }); core_1.config.chaseWeakMapEdge = false; const ret = yield core_1.analysis.checkLeak(); APIStateManager_1.default.restoreState(core_1.config, state); return ret; }); } /** * This API finds memory leaks by analyzing specified heap snapshots. * This is equivalent to `memlab find-leaks` with * the `--baseline`, `--target`, and `--final` flags in CLI. * * @param baselineSnapshot the file path of the baseline heap snapshot * @param targetSnapshot the file path of the target heap snapshot * @param finalSnapshot the file path of the final heap snapshot * @param options optionally, you can specify a mode for heap analysis * @param options.workDir specify a working directory (other than * the default one) * @param options.consoleMode specify the terminal output * mode (see {@link ConsoleMode}) * @returns leak traces detected and clustered from the browser interaction */ function findLeaksBySnapshotFilePaths(baselineSnapshot_1, targetSnapshot_1, finalSnapshot_1) { return __awaiter(this, arguments, void 0, function* (baselineSnapshot, targetSnapshot, finalSnapshot, options = {}) { const state = APIStateManager_1.default.getAndUpdateState(core_1.config, options); core_1.config.useExternalSnapshot = true; core_1.config.externalSnapshotFilePaths = [ baselineSnapshot, targetSnapshot, finalSnapshot, ]; core_1.fileManager.initDirs(core_1.config, { workDir: options.workDir }); core_1.config.chaseWeakMapEdge = false; const ret = yield core_1.analysis.checkLeak(); APIStateManager_1.default.restoreState(core_1.config, state); return ret; }); } /** * This API analyzes heap snapshot(s) with a specified heap analysis. * This is equivalent to `memlab analyze` in CLI. * * @param runResult return value of a browser interaction run * @param heapAnalyzer instance of a heap analysis * @param args other CLI arguments that needs to be passed to the heap analysis * @returns each analysis may have a different return type, please check out * the type definition or the documentation for the `process` method of the * analysis class you are using for `heapAnalyzer`. * * **Examples**: * ```javascript * const {analyze, takeSnapshots, StringAnalysis} = require('@memlab/api'); * * (async function () { * const scenario = { * url: () => 'https://www.facebook.com', * }; * const result = await takeSnapshots({scenario}); * const analysis = new StringAnalysis(); * await analyze(result, analysis); * })(); * ``` */ function analyze(runResult_1, heapAnalyzer_1) { return __awaiter(this, arguments, void 0, function* (runResult, heapAnalyzer, args = { _: [] }) { const workDir = runResult.getRootDirectory(); core_1.fileManager.initDirs(core_1.config, { workDir }); return yield heapAnalyzer.run({ args }); }); } /** * This warms up web server by sending web requests to the web sever. * This is equivalent to running `memlab warmup` in CLI. * @internal * * @param options configure browser interaction run */ function warmup() { return __awaiter(this, arguments, void 0, function* (options = {}) { var _a, _b; const config = (_a = options.config) !== null && _a !== void 0 ? _a : core_1.config; if (config.verbose) { core_1.info.lowLevel(`Xvfb: ${config.useXVFB}`); } const testPlanner = (_b = options.testPlanner) !== null && _b !== void 0 ? _b : e2e_1.defaultTestPlanner; try { if (config.skipWarmup) { return; } const browser = yield APIUtils_1.default.getBrowser({ warmup: true }); const visitPlan = testPlanner.getVisitPlan(); config.setDevice(visitPlan.device); const numOfWarmup = visitPlan.numOfWarmup || 3; const promises = []; for (let i = 0; i < numOfWarmup; ++i) { promises.push(browser.newPage()); } const pages = yield Promise.all(promises); core_1.info.beginSection('warmup'); yield Promise.all(pages.map((page) => __awaiter(this, void 0, void 0, function* () { yield setupPage(page, { cache: false }); const interactionManager = new e2e_1.E2EInteractionManager(page, browser); yield interactionManager.warmupInPage(); }))).catch(err => { core_1.info.error(err.message); }); core_1.info.endSection('warmup'); yield core_1.utils.closePuppeteer(browser, pages, { warmup: true }); } catch (ex) { const error = core_1.utils.getError(ex); core_1.utils.checkUninstalledLibrary(error); throw ex; } }); } function getConfigFromRunOptions(options) { let config = core_1.MemLabConfig.getInstance(); if (options.workDir) { core_1.fileManager.initDirs(config, { workDir: options.workDir }); } else { config = core_1.MemLabConfig.resetConfigWithTransientDir(); } if ('webWorker' in options) { config.isAnalyzingMainThread = false; const value = options.webWorker; if (typeof value === 'string') { config.targetWorkerTitle = value; } } else { config.isAnalyzingMainThread = true; } config.isFullRun = !!options.snapshotForEachStep; return config; } function setupPage(page_1) { return __awaiter(this, arguments, void 0, function* (page, options = {}) { var _a, _b, _c; const config = (_a = options.config) !== null && _a !== void 0 ? _a : core_1.config; const testPlanner = (_b = options.testPlanner) !== null && _b !== void 0 ? _b : e2e_1.defaultTestPlanner; if (config.emulateDevice) { yield page.emulate(config.emulateDevice); } if (config.defaultUserAgent && config.defaultUserAgent !== 'default') { yield page.setUserAgent(config.defaultUserAgent); } // set login session yield page.setCookie(...testPlanner.getCookies()); const cache = (_c = options.cache) !== null && _c !== void 0 ? _c : true; yield page.setCacheEnabled(cache); // automatically accept dialog page.on('dialog', (dialog) => __awaiter(this, void 0, void 0, function* () { if (config.verbose) { core_1.info.lowLevel(`Browser dialog: ${dialog.message()}`); } yield dialog.accept(); })); }); } function initBrowserInfoInConfig(browser_1) { return __awaiter(this, arguments, void 0, function* (browser, options = {}) { var _a; const config = (_a = options.config) !== null && _a !== void 0 ? _a : core_1.config; core_1.browserInfo.recordPuppeteerConfig(config.puppeteerConfig); const version = yield browser.version(); core_1.browserInfo.recordBrowserVersion(version); if (config.verbose) { core_1.info.lowLevel(JSON.stringify(core_1.browserInfo, null, 2)); } }); } /** * Browser interaction API used by MemLab API and MemLab CLI * @internal */ function testInBrowser() { return __awaiter(this, arguments, void 0, function* (options = {}) { var _a, _b; const config = (_a = options.config) !== null && _a !== void 0 ? _a : core_1.config; if (config.verbose) { core_1.info.lowLevel(`Xvfb: ${config.useXVFB}`); } const testPlanner = (_b = options.testPlanner) !== null && _b !== void 0 ? _b : e2e_1.defaultTestPlanner; let interactionManager = null; let xvfb = null; let browser = null; let page = null; let maybeError = null; try { xvfb = e2e_1.Xvfb.startIfEnabled(); browser = yield APIUtils_1.default.getBrowser(); const pages = yield browser.pages(); page = pages.length > 0 ? pages[0] : yield browser.newPage(); // create and configure web page interaction manager interactionManager = new e2e_1.E2EInteractionManager(page, browser); if (options.evalInBrowserAfterInitLoad) { interactionManager.setEvalFuncAfterInitLoad(options.evalInBrowserAfterInitLoad); } const visitPlan = testPlanner.getVisitPlan(); // setup page configuration config.setDevice(visitPlan.device); yield initBrowserInfoInConfig(browser); core_1.browserInfo.monitorWebConsole(page); yield setupPage(page, options); // interact with the web page and take heap snapshots yield interactionManager.visitAndGetSnapshots(options); } catch (ex) { maybeError = core_1.utils.getError(ex); core_1.utils.checkUninstalledLibrary(maybeError); } finally { if (browser && page) { yield core_1.utils.closePuppeteer(browser, [page]); } if (interactionManager) { interactionManager.clearCDPSession(); } if (xvfb) { xvfb.stop((err) => { if (err) { core_1.utils.haltOrThrow(err); } }); } if (maybeError != null) { core_1.utils.haltOrThrow(maybeError); } } }); }