Skip to content

Commit

Permalink
feat(api): support configuring console mode in APIs (#110)
Browse files Browse the repository at this point in the history
Summary:
Support setting the console output mode in MemLab programming API:

```
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'});
})();
```

Differential Revision: D53361876

fbshipit-source-id: 10f8b501a1253173571da49f106c0819ca66e898
  • Loading branch information
JacksonGL authored and facebook-github-bot committed Feb 3, 2024
1 parent d8b8311 commit f5ab171
Showing 21 changed files with 454 additions and 178 deletions.
60 changes: 41 additions & 19 deletions packages/api/src/API.ts
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ import type {
Nullable,
Optional,
} from '@memlab/core';
import {ConsoleMode} from './state/ConsoleModeManager';

import {
analysis,
@@ -38,6 +39,7 @@ import {BaseAnalysis} from '@memlab/heap-analysis';
import APIUtils from './lib/APIUtils';
import BrowserInteractionResultReader from './result-reader/BrowserInteractionResultReader';
import BaseResultReader from './result-reader/BaseResultReader';
import stateManager from './state/APIStateManager';

/**
* Options for configuring browser interaction run, all fields are optional
@@ -82,6 +84,11 @@ export type RunOptions = {
* skip warmup page load for the target web app
*/
skipWarmup?: boolean;
/**
* specifying the terminal output mode, default is `default`.
* For more details. please check out {@link ConsoleMode}
*/
consoleMode?: ConsoleMode;
};

/**
@@ -134,15 +141,16 @@ export async function warmupAndTakeSnapshots(
options: RunOptions = {},
): Promise<BrowserInteractionResultReader> {
const config = getConfigFromRunOptions(options);
config.externalCookiesFile = options.cookiesFile;
config.scenario = options.scenario;
const state = stateManager.getAndUpdateState(config, options);
const testPlanner = new TestPlanner({config});
const {evalInBrowserAfterInitLoad} = options;
if (!options.skipWarmup) {
await warmup({testPlanner, config, evalInBrowserAfterInitLoad});
}
await testInBrowser({testPlanner, config, evalInBrowserAfterInitLoad});
return BrowserInteractionResultReader.from(config.workDir);
const ret = BrowserInteractionResultReader.from(config.workDir);
stateManager.restoreState(config, state);
return ret;
}

/**
@@ -166,18 +174,18 @@ export async function warmupAndTakeSnapshots(
* })();
* ```
*/
export async function run(runOptions: RunOptions = {}): Promise<RunResult> {
const config = getConfigFromRunOptions(runOptions);
config.externalCookiesFile = runOptions.cookiesFile;
config.scenario = runOptions.scenario;
export async function run(options: RunOptions = {}): Promise<RunResult> {
const config = getConfigFromRunOptions(options);
const state = stateManager.getAndUpdateState(config, options);
const testPlanner = new TestPlanner({config});
const {evalInBrowserAfterInitLoad} = runOptions;
if (!runOptions.skipWarmup) {
const {evalInBrowserAfterInitLoad} = options;
if (!options.skipWarmup) {
await warmup({testPlanner, config, evalInBrowserAfterInitLoad});
}
await testInBrowser({testPlanner, config, evalInBrowserAfterInitLoad});
const runResult = BrowserInteractionResultReader.from(config.workDir);
const leaks = await findLeaks(runResult);
stateManager.restoreState(config, state);
return {leaks, runResult};
}

@@ -203,19 +211,23 @@ export async function takeSnapshots(
options: RunOptions = {},
): Promise<BrowserInteractionResultReader> {
const config = getConfigFromRunOptions(options);
config.externalCookiesFile = options.cookiesFile;
config.scenario = options.scenario;
const state = stateManager.getAndUpdateState(config, options);
const testPlanner = new TestPlanner();
const {evalInBrowserAfterInitLoad} = options;
await testInBrowser({testPlanner, config, evalInBrowserAfterInitLoad});
return BrowserInteractionResultReader.from(config.workDir);
const ret = BrowserInteractionResultReader.from(config.workDir);
stateManager.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
@@ -225,18 +237,22 @@ export async function takeSnapshots(
* const scenario = {
* url: () => 'https://www.facebook.com',
* };
* const result = await takeSnapshots({scenario});
* const leaks = findLeaks(result);
* const result = await takeSnapshots({scenario, consoleMode: 'SILENT'});
* const leaks = findLeaks(result, {consoleMode: 'CONTINUOUS_TEST'});
* })();
* ```
*/
export async function findLeaks(
runResult: BaseResultReader,
options: {consoleMode?: ConsoleMode} = {},
): Promise<ISerializedInfo[]> {
const state = stateManager.getAndUpdateState(defaultConfig, options);
const workDir = runResult.getRootDirectory();
fileManager.initDirs(defaultConfig, {workDir});
defaultConfig.chaseWeakMapEdge = false;
return await analysis.checkLeak();
const ret = await analysis.checkLeak();
stateManager.restoreState(defaultConfig, state);
return ret;
}

/**
@@ -247,16 +263,20 @@ export async function findLeaks(
* @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 working
* directory (other than the default one) for heap analysis
* @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
*/
export async function findLeaksBySnapshotFilePaths(
baselineSnapshot: string,
targetSnapshot: string,
finalSnapshot: string,
options: {workDir?: string} = {},
options: {workDir?: string; consoleMode?: ConsoleMode} = {},
): Promise<ISerializedInfo[]> {
const state = stateManager.getAndUpdateState(defaultConfig, options);
defaultConfig.useExternalSnapshot = true;
defaultConfig.externalSnapshotFilePaths = [
baselineSnapshot,
@@ -265,7 +285,9 @@ export async function findLeaksBySnapshotFilePaths(
];
fileManager.initDirs(defaultConfig, {workDir: options.workDir});
defaultConfig.chaseWeakMapEdge = false;
return await analysis.checkLeak();
const ret = await analysis.checkLeak();
stateManager.restoreState(defaultConfig, state);
return ret;
}

/**
1 change: 1 addition & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ export async function registerPackage(): Promise<void> {

export * from './API';
export * from '@memlab/heap-analysis';
export * from './state/ConsoleModeManager';
export {default as BrowserInteractionResultReader} from './result-reader/BrowserInteractionResultReader';
export {default as SnapshotResultReader} from './result-reader/SnapshotResultReader';
export {
47 changes: 47 additions & 0 deletions packages/api/src/state/APIStateManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* 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 web_perf_infra
*/

import type {MemLabConfig, Optional, PuppeteerConfig} from '@memlab/core';
import type {ConsoleMode} from './ConsoleModeManager';
import type {RunOptions} from '../API';

import consoleModeManager from './ConsoleModeManager';
import puppeteerConfigManager from './PuppeteerConfigManager';

export class APIState {
modes: Optional<Set<ConsoleMode>>;
puppeteerConfig: Optional<PuppeteerConfig>;
}

/**
* Manage, save, and restore the current state of the API.
*/
class APIStateManager {
getAndUpdateState(config: MemLabConfig, options: RunOptions = {}) {
const state = new APIState();
state.modes = consoleModeManager.getAndUpdateState(config, options);
state.puppeteerConfig = puppeteerConfigManager.getAndUpdateState(
config,
options,
);
return state;
}

restoreState(config: MemLabConfig, state: APIState) {
if (state.modes) {
consoleModeManager.restoreState(config, state.modes);
}
if (state.puppeteerConfig) {
puppeteerConfigManager.restoreState(config, state.puppeteerConfig);
}
}
}

export default new APIStateManager();
115 changes: 115 additions & 0 deletions packages/api/src/state/ConsoleModeManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* 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 web_perf_infra
*/

import {MemLabConfig, Nullable, Optional, utils} from '@memlab/core';
import type {RunOptions} from '../API';

/**
* enum of all console mode options
*/
export enum ConsoleMode {
/**
* mute all terminal output, equivalent to using `--silent`
*/
SILENT = 'SILENT',
/**
* continuous test mode, no terminal output overwrite or animation,
* equivalent to using `--sc`
*/
CONTINUOUS_TEST = 'CONTINUOUS_TEST',
/**
* the default mode, there could be terminal output overwrite and animation,
*/
DEFAULT = 'DEFAULT',
/**
* verbose mode, there could be terminal output overwrite and animation
*/
VERBOSE = 'VERBOSE',
}

/**
* Manage, save, and restore the current state of the Console modes.
* @internal
*/
class ConsoleModeManager {
getAndUpdateState(config: MemLabConfig, options: RunOptions = {}) {
return this.setConsoleMode(config, options.consoleMode, true);
}

restoreState(config: MemLabConfig, modes: Nullable<Set<ConsoleMode>>) {
if (modes == null) {
return;
}
this.resetConsoleMode(config);
for (const mode of modes) {
this.setConsoleMode(config, mode, false);
}
}

private resetConsoleMode(config: MemLabConfig): void {
config.muteConsole = false;
config.isContinuousTest = false;
config.verbose = false;
}

private setConsoleMode(
config: MemLabConfig,
mode: Optional<ConsoleMode>,
reset: boolean,
): Nullable<Set<ConsoleMode>> {
let existingModes: Nullable<Set<ConsoleMode>> =
this.getExistingConsoleModes(config);
switch (mode) {
case ConsoleMode.SILENT:
reset && this.resetConsoleMode(config);
config.muteConsole = true;
break;
case ConsoleMode.CONTINUOUS_TEST:
reset && this.resetConsoleMode(config);
config.isContinuousTest = true;
break;
case ConsoleMode.DEFAULT:
reset && this.resetConsoleMode(config);
config.muteConsole = false;
config.isContinuousTest = false;
break;
case ConsoleMode.VERBOSE:
reset && this.resetConsoleMode(config);
config.verbose = true;
break;
default:
if (mode == null) {
existingModes = null;
} else {
throw utils.haltOrThrow(`Unknown console mode: ${mode}`);
}
}
return existingModes;
}

private getExistingConsoleModes(config: MemLabConfig): Set<ConsoleMode> {
const modes = new Set<ConsoleMode>([ConsoleMode.DEFAULT]);
if (config.muteConsole) {
modes.add(ConsoleMode.SILENT);
modes.delete(ConsoleMode.DEFAULT);
}
if (config.isContinuousTest) {
modes.add(ConsoleMode.CONTINUOUS_TEST);
modes.delete(ConsoleMode.DEFAULT);
}
if (config.verbose) {
modes.add(ConsoleMode.VERBOSE);
}
return modes;
}
}

/** @internal */
export default new ConsoleModeManager();
31 changes: 31 additions & 0 deletions packages/api/src/state/PuppeteerConfigManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* 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 web_perf_infra
*/

import {MemLabConfig, PuppeteerConfig} from '@memlab/core';
import {RunOptions} from '../API';

/**
* Manage, save, and restore the current state of the PuppeteerConfig.
*/
class PuppeteerStateManager {
getAndUpdateState(config: MemLabConfig, options: RunOptions = {}) {
const existing = config.puppeteerConfig;
config.puppeteerConfig = {...config.puppeteerConfig};
config.externalCookiesFile = options.cookiesFile;
config.scenario = options.scenario;
return existing;
}

restoreState(config: MemLabConfig, puppeteerConfig: PuppeteerConfig) {
config.puppeteerConfig = puppeteerConfig;
}
}

export default new PuppeteerStateManager();
Loading

0 comments on commit f5ab171

Please sign in to comment.