From 2475ed1f47e88269417cf0eb6e14ba7776c47e90 Mon Sep 17 00:00:00 2001 From: Liang Gong Date: Mon, 22 Jan 2024 11:09:06 -0800 Subject: [PATCH] feat(heap-analysis) support loading external analysis plugin Summary: ``` memlab analyze --analysis-plugin --snapshot ``` Differential Revision: D52937130 fbshipit-source-id: 456def2ef5543a523dd72427ca8c5ffb417fad24 --- .../src/commands/heap/HeapAnalysisCommand.ts | 58 ++++++++++------ .../options/heap/HeapAnalysisPluginOption.ts | 40 +++++++++++ .../cli/src/options/lib/OptionConstant.ts | 1 + .../heap-analysis/src/HeapAnalysisLoader.ts | 68 +++++++++++++++---- website/docs/cli/CLI-commands.md | 3 +- 5 files changed, 135 insertions(+), 35 deletions(-) create mode 100644 packages/cli/src/options/heap/HeapAnalysisPluginOption.ts diff --git a/packages/cli/src/commands/heap/HeapAnalysisCommand.ts b/packages/cli/src/commands/heap/HeapAnalysisCommand.ts index 1b19d6e72..d6c12b666 100644 --- a/packages/cli/src/commands/heap/HeapAnalysisCommand.ts +++ b/packages/cli/src/commands/heap/HeapAnalysisCommand.ts @@ -8,7 +8,7 @@ * @oncall web_perf_infra */ -import type {CLIOptions, CommandOptionExample} from '@memlab/core'; +import type {BaseOption, CLIOptions, CommandOptionExample} from '@memlab/core'; import {info, utils} from '@memlab/core'; import BaseCommand, {CommandCategory} from '../../BaseCommand'; @@ -17,6 +17,8 @@ import HeapAnalysisSubCommandWrapper from './HeapAnalysisSubCommandWrapper'; import {BaseAnalysis} from '@memlab/heap-analysis'; import HelperCommand from '../helper/HelperCommand'; import InitDirectoryCommand from '../InitDirectoryCommand'; +import HeapAnalysisPluginOption from '../../options/heap/HeapAnalysisPluginOption'; +import {ParsedArgs} from 'minimist'; export default class RunHeapAnalysisCommand extends BaseCommand { getCommandName(): string { @@ -24,7 +26,7 @@ export default class RunHeapAnalysisCommand extends BaseCommand { } getDescription(): string { - return 'Run heap analysis plugins'; + return 'Run heap analysis on heap snapshots.\n'; } getCategory(): CommandCategory { @@ -35,6 +37,10 @@ export default class RunHeapAnalysisCommand extends BaseCommand { return [new InitDirectoryCommand()]; } + getOptions(): BaseOption[] { + return [new HeapAnalysisPluginOption()]; + } + getSubCommands(): BaseCommand[] { const analyses = [...heapAnalysisLoader.loadAllAnalysis().values()]; return analyses.map((analysis: BaseAnalysis) => { @@ -49,25 +55,39 @@ export default class RunHeapAnalysisCommand extends BaseCommand { return [' [PLUGIN_OPTIONS]']; } + private async errorIfNoSubCommand( + args: ParsedArgs, + analysisMap: Map, + ): Promise { + if (args && args._.length >= 2 && analysisMap.has(args._[1])) { + return; + } + + const helper = new HelperCommand(); + const modules = new Map(); + for (const subCommand of this.getSubCommands()) { + modules.set(subCommand.getCommandName(), subCommand); + } + const errMsg = + args && args._.length < 2 + ? `\n Heap analysis plugin name missing\n` + : `\n Invalid command \`memlab ${this.getCommandName()} ${ + args?._[1] + }\`\n`; + info.error(errMsg); + await helper.run({cliArgs: args, modules, command: this}); + utils.haltOrThrow(errMsg, {printErrorBeforeHalting: false}); + } + async run(options: CLIOptions): Promise { + // process command line arguments and load analysis modules const args = options.cliArgs; - const analysisMap = heapAnalysisLoader.loadAllAnalysis(); - if (!args || args._.length < 2 || !analysisMap.has(args._[1])) { - const helper = new HelperCommand(); + const plugin = options.configFromOptions?.heapAnalysisPlugin; + const analysisMap = heapAnalysisLoader.loadAllAnalysis({ + heapAnalysisPlugin: `${plugin}`, + errorWhenPluginFailed: true, + }); - const modules = new Map(); - for (const subCommand of this.getSubCommands()) { - modules.set(subCommand.getCommandName(), subCommand); - } - const errMsg = - args && args._.length < 2 - ? `\n Heap analysis plugin name missing\n` - : `\n Invalid command \`memlab ${this.getCommandName()} ${ - args?._[1] - }\`\n`; - info.error(errMsg); - await helper.run({cliArgs: args, modules, command: this}); - utils.haltOrThrow(errMsg, {printErrorBeforeHalting: false}); - } + await this.errorIfNoSubCommand(args, analysisMap); } } diff --git a/packages/cli/src/options/heap/HeapAnalysisPluginOption.ts b/packages/cli/src/options/heap/HeapAnalysisPluginOption.ts new file mode 100644 index 000000000..d2e8413fd --- /dev/null +++ b/packages/cli/src/options/heap/HeapAnalysisPluginOption.ts @@ -0,0 +1,40 @@ +/** + * 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 {ParsedArgs} from 'minimist'; +import {AnyRecord, MemLabConfig} from '@memlab/core'; +import {BaseOption} from '@memlab/core'; +import optionConstants from '../lib/OptionConstant'; + +export default class HeapAnalysisPluginOption extends BaseOption { + getOptionName(): string { + return optionConstants.optionNames.HEAP_ANALYSIS_PLUGIN_FILE; + } + + getDescription(): string { + return ( + 'specify the external heap analysis plugin file ' + + '(must be a vanilla JS file ended with `Analysis.js` suffix)' + ); + } + + async parse( + config: MemLabConfig, + args: ParsedArgs, + ): Promise<{workDir?: string}> { + const name = this.getOptionName(); + const ret: AnyRecord = {}; + const heapAnalysisPlugin = args[name]; + if (heapAnalysisPlugin) { + ret.heapAnalysisPlugin = heapAnalysisPlugin; + } + return ret; + } +} diff --git a/packages/cli/src/options/lib/OptionConstant.ts b/packages/cli/src/options/lib/OptionConstant.ts index 776fd3dda..c63ff0603 100644 --- a/packages/cli/src/options/lib/OptionConstant.ts +++ b/packages/cli/src/options/lib/OptionConstant.ts @@ -26,6 +26,7 @@ const optionNames = { FINAL: 'final', FULL: 'full', HEADFUL: 'headful', + HEAP_ANALYSIS_PLUGIN_FILE: 'analysis-plugin', HELP: 'help', IGNORE_LEAK_CLUSTER_SIZE_BELOW: 'ignore-leak-cluster-size-below', INTERACTION: 'interaction', diff --git a/packages/heap-analysis/src/HeapAnalysisLoader.ts b/packages/heap-analysis/src/HeapAnalysisLoader.ts index f0dfb43de..673cf048d 100644 --- a/packages/heap-analysis/src/HeapAnalysisLoader.ts +++ b/packages/heap-analysis/src/HeapAnalysisLoader.ts @@ -8,50 +8,88 @@ * @oncall web_perf_infra */ +import type {Optional} from '@memlab/core'; + import fs from 'fs'; import path from 'path'; -import {utils} from '@memlab/core'; +import {info, utils} from '@memlab/core'; import type BaseAnalysis from './BaseAnalysis'; +export type HeapAnalysisLoaderOptions = { + heapAnalysisPlugin?: Optional; + errorWhenPluginFailed?: boolean; +}; + class HeapAnalysisLoader { private modules: Map = new Map(); - public loadAllAnalysis(): Map { + public loadAllAnalysis( + options: HeapAnalysisLoaderOptions = {}, + ): Map { if (this.modules.size === 0) { // auto load all analysis modules this.modules = new Map(); - this.registerAnalyses(); + this.registerAnalyses(options); } return this.modules; } - private registerAnalyses(): void { + private registerAnalyses(options: HeapAnalysisLoaderOptions = {}): void { const modulesDir = path.resolve(__dirname, 'plugins'); this.registerAnalysesFromDir(modulesDir); + // register external analysis + if (options.heapAnalysisPlugin != null) { + const file = path.resolve(options.heapAnalysisPlugin); + this.registerAnalysisFromFile(file, options); + } } - private registerAnalysesFromDir(modulesDir: string) { + private registerAnalysesFromDir( + modulesDir: string, + options: HeapAnalysisLoaderOptions = {}, + ) { const moduleFiles = fs.readdirSync(modulesDir); for (const moduleFile of moduleFiles) { const modulePath = path.join(modulesDir, moduleFile); + this.registerAnalysisFromFile(modulePath, options); + } + } - // recursively import modules from subdirectories - if (fs.lstatSync(modulePath).isDirectory()) { - this.registerAnalysesFromDir(modulePath); - continue; - } + private registerAnalysisFromFile( + modulePath: string, + options: HeapAnalysisLoaderOptions = {}, + ): void { + // recursively import modules from subdirectories + if (fs.lstatSync(modulePath).isDirectory()) { + this.registerAnalysesFromDir(modulePath, options); + return; + } - // only import modules files ends with with Analysis.js - if (!moduleFile.endsWith('Analysis.js')) { - continue; + // only import modules files ends with with Analysis.js + if (!modulePath.endsWith('Analysis.js')) { + if (options.errorWhenPluginFailed) { + const fileName = path.basename(modulePath); + throw utils.haltOrThrow( + `Analysis plugin file (${fileName}) must end with \`Analysis.js\``, + ); } + return; + } + let commandName = null; + let moduleInstance = null; + try { // eslint-disable-next-line @typescript-eslint/no-var-requires const module = require(modulePath); const moduleConstructor = typeof module.default === 'function' ? module.default : module; - const moduleInstance = new moduleConstructor(); - const commandName = moduleInstance.getCommandName(); + moduleInstance = new moduleConstructor(); + commandName = moduleInstance.getCommandName(); + } catch (err) { + info.error('Failed to load analysis plugin: ' + modulePath); + throw utils.haltOrThrow(utils.getError(err)); + } + if (commandName != null) { if (this.modules.has(commandName)) { utils.haltOrThrow(`heap command ${commandName} is already registered`); } diff --git a/website/docs/cli/CLI-commands.md b/website/docs/cli/CLI-commands.md index fc6a27332..703764fad 100644 --- a/website/docs/cli/CLI-commands.md +++ b/website/docs/cli/CLI-commands.md @@ -170,13 +170,14 @@ memlab trace --node-id=128127 ### memlab analyze -Run heap analysis plugins +Run heap analysis on heap snapshots. ```bash memlab analyze [PLUGIN_OPTIONS] ``` **Options**: + * **`--analysis-plugin`**: specify the external heap analysis plugin file (must be a vanilla JS file ended with `Analysis.js` suffix) * **`--work-dir`**: set the working directory of the current run * **`--help`**, **`-h`**: print helper text * **`--verbose`**, **`-v`**: show more details