Skip to content

Commit

Permalink
feat(heap-analysis) support loading external analysis plugin
Browse files Browse the repository at this point in the history
Summary:
```
memlab analyze <external-analysis-name> --analysis-plugin <external-analysis-plugin-file> --snapshot <snapshot-file>
```

Differential Revision: D52937130

fbshipit-source-id: 456def2ef5543a523dd72427ca8c5ffb417fad24
  • Loading branch information
JacksonGL authored and facebook-github-bot committed Jan 22, 2024
1 parent 0e5e89b commit 2475ed1
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 35 deletions.
58 changes: 39 additions & 19 deletions packages/cli/src/commands/heap/HeapAnalysisCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -17,14 +17,16 @@ 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 {
return 'analyze';
}

getDescription(): string {
return 'Run heap analysis plugins';
return 'Run heap analysis on heap snapshots.\n';
}

getCategory(): CommandCategory {
Expand All @@ -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) => {
Expand All @@ -49,25 +55,39 @@ export default class RunHeapAnalysisCommand extends BaseCommand {
return ['<PLUGIN_NAME> [PLUGIN_OPTIONS]'];
}

private async errorIfNoSubCommand(
args: ParsedArgs,
analysisMap: Map<string, BaseAnalysis>,
): Promise<void> {
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<void> {
// 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);
}
}
40 changes: 40 additions & 0 deletions packages/cli/src/options/heap/HeapAnalysisPluginOption.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 1 addition & 0 deletions packages/cli/src/options/lib/OptionConstant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
68 changes: 53 additions & 15 deletions packages/heap-analysis/src/HeapAnalysisLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
errorWhenPluginFailed?: boolean;
};

class HeapAnalysisLoader {
private modules: Map<string, BaseAnalysis> = new Map();

public loadAllAnalysis(): Map<string, BaseAnalysis> {
public loadAllAnalysis(
options: HeapAnalysisLoaderOptions = {},
): Map<string, BaseAnalysis> {
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`);
}
Expand Down
3 changes: 2 additions & 1 deletion website/docs/cli/CLI-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_NAME> [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
Expand Down

0 comments on commit 2475ed1

Please sign in to comment.