-
Notifications
You must be signed in to change notification settings - Fork 668
[cpu-profile-summarizer] Add new CLI tool for summarizing many V8 CPU Profiles #5098
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
dmichon-msft
merged 2 commits into
microsoft:main
from
dmichon-msft:cpu-profile-aggregator
Feb 7, 2025
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| // This is a workaround for https://github.com/eslint/eslint/issues/3458 | ||
| require('local-node-rig/profiles/default/includes/eslint/patch/modern-module-resolution'); | ||
| // This is a workaround for https://github.com/microsoft/rushstack/issues/3021 | ||
| require('local-node-rig/profiles/default/includes/eslint/patch/custom-config-package-names'); | ||
|
|
||
| module.exports = { | ||
| extends: [ | ||
| 'local-node-rig/profiles/default/includes/eslint/profile/node-trusted-tool', | ||
| 'local-node-rig/profiles/default/includes/eslint/mixins/friendly-locals' | ||
| ], | ||
| parserOptions: { tsconfigRootDir: __dirname }, | ||
|
|
||
| overrides: [ | ||
| { | ||
| files: ['*.ts', '*.tsx'], | ||
| rules: { | ||
| 'no-console': 'off' | ||
| } | ||
| } | ||
| ] | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # THIS IS A STANDARD TEMPLATE FOR .npmignore FILES IN THIS REPO. | ||
|
|
||
| # Ignore all files by default, to avoid accidentally publishing unintended files. | ||
| * | ||
|
|
||
| # Use negative patterns to bring back the specific things we want to publish. | ||
| !/bin/** | ||
| !/lib/** | ||
| !/lib-*/** | ||
| !/dist/** | ||
|
|
||
| !CHANGELOG.md | ||
| !CHANGELOG.json | ||
| !heft-plugin.json | ||
| !rush-plugin-manifest.json | ||
| !ThirdPartyNotice.txt | ||
|
|
||
| # Ignore certain patterns that should not get published. | ||
| /dist/*.stats.* | ||
| /lib/**/test/ | ||
| /lib-*/**/test/ | ||
| *.test.js | ||
|
|
||
| # NOTE: These don't need to be specified, because NPM includes them automatically. | ||
| # | ||
| # package.json | ||
| # README.md | ||
| # LICENSE | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # DO NOT MODIFY ABOVE THIS LINE! Add any project-specific overrides below. | ||
| # --------------------------------------------------------------------------- |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| @rushstack/cpu-profile-summarizer | ||
|
|
||
| Copyright (c) Microsoft Corporation. All rights reserved. | ||
|
|
||
| MIT License | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining | ||
| a copy of this software and associated documentation files (the | ||
| "Software"), to deal in the Software without restriction, including | ||
| without limitation the rights to use, copy, modify, merge, publish, | ||
| distribute, sublicense, and/or sell copies of the Software, and to | ||
| permit persons to whom the Software is furnished to do so, subject to | ||
| the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be | ||
| included in all copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||
| NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | ||
| LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||
| OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||
| WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # @rushstack/cpu-profile-summarizer | ||
|
|
||
| > 🚨 _EARLY PREVIEW RELEASE_ 🚨 | ||
| > | ||
| > Not all features are implemented yet. To provide suggestions, please | ||
| > [create a GitHub issue](https://github.com/microsoft/rushstack/issues/new/choose). | ||
| > If you have questions, see the [Rush Stack Help page](https://rushstack.io/pages/help/support/) | ||
| > for support resources. | ||
|
|
||
| The `cpu-profile-summarizer` command line tool helps you: | ||
|
|
||
| - Collate self/total CPU usage statistics for an entire monorepo worth of V8 .cpuprofile files | ||
|
|
||
| ## Usage | ||
|
|
||
| It's recommended to install this package globally: | ||
|
|
||
| ``` | ||
| # Install the NPM package | ||
| npm install -g @rushstack/cpu-profile-summarizer | ||
|
|
||
| # Process a folder of cpuprofile files into a summary tsv file | ||
| cpu-profile-summarizer --input FOLDER --output FILE.tsv | ||
| ``` | ||
|
|
||
| The output file is in the tab-separated values (tsv) format. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| #!/usr/bin/env node | ||
| require('../lib/start.js'); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "extends": "local-node-rig/profiles/default/config/jest.config.json" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| // The "rig.json" file directs tools to look for their config files in an external package. | ||
| // Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package | ||
| "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", | ||
|
|
||
| "rigPackageName": "local-node-rig" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| { | ||
| "name": "@rushstack/cpu-profile-summarizer", | ||
| "version": "0.0.0", | ||
| "description": "CLI tool for running analytics on multiple V8 .cpuprofile files", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/microsoft/rushstack.git", | ||
| "directory": "apps/cpu-profile-summarizer" | ||
| }, | ||
| "bin": { | ||
| "cpu-profile-summarizer": "./bin/cpu-profile-summarizer" | ||
| }, | ||
| "license": "MIT", | ||
| "scripts": { | ||
| "start": "node lib/start", | ||
| "build": "heft build --clean", | ||
| "_phase:build": "heft run --only build -- --clean", | ||
| "_phase:test": "heft run --only test -- --clean" | ||
| }, | ||
| "dependencies": { | ||
| "@rushstack/ts-command-line": "workspace:*", | ||
| "@rushstack/worker-pool": "workspace:*" | ||
| }, | ||
| "devDependencies": { | ||
| "@rushstack/heft": "workspace:*", | ||
| "local-node-rig": "workspace:*", | ||
| "typescript": "~5.4.2" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. | ||
| // See LICENSE in the project root for license information. | ||
|
|
||
| import type { IProfileSummary } from './types'; | ||
|
|
||
| /** | ||
| * A message sent to a worker to process a file (or shutdown). | ||
| */ | ||
| export type IMessageToWorker = string | false; | ||
|
|
||
| /** | ||
| * A message sent from a worker to the main thread on success. | ||
| */ | ||
| export interface IWorkerSuccessMessage { | ||
| type: 'success'; | ||
| /** | ||
| * The file requested to be processed. | ||
| */ | ||
| file: string; | ||
| /** | ||
| * The summary of the profile data. | ||
| */ | ||
| data: IProfileSummary; | ||
| } | ||
|
|
||
| /** | ||
| * A message sent from a worker to the main thread on error. | ||
| */ | ||
| export interface IWorkerErrorMessage { | ||
| type: 'error'; | ||
| /** | ||
| * The file requested to be processed. | ||
| */ | ||
| file: string; | ||
| /** | ||
| * The error stack trace or message. | ||
| */ | ||
| data: string; | ||
| } | ||
|
|
||
| /** | ||
| * A message sent from a worker to the main thread. | ||
| */ | ||
| export type IMessageFromWorker = IWorkerSuccessMessage | IWorkerErrorMessage; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. | ||
| // See LICENSE in the project root for license information. | ||
|
|
||
| import { once } from 'node:events'; | ||
| import fs from 'node:fs'; | ||
| import os from 'node:os'; | ||
| import path from 'node:path'; | ||
| import type { Worker } from 'node:worker_threads'; | ||
|
|
||
| import { | ||
| type CommandLineStringListParameter, | ||
| type IRequiredCommandLineStringParameter, | ||
| CommandLineParser | ||
| } from '@rushstack/ts-command-line'; | ||
| import { WorkerPool } from '@rushstack/worker-pool'; | ||
|
|
||
| import type { IMessageFromWorker } from './protocol'; | ||
| import type { INodeSummary, IProfileSummary } from './types'; | ||
|
|
||
| /** | ||
| * Merges summarized information from multiple profiles into a single collection. | ||
| * @param accumulator - The collection to merge the nodes into | ||
| * @param values - The nodes to merge | ||
| */ | ||
| function mergeProfileSummaries( | ||
| accumulator: Map<string, INodeSummary>, | ||
| values: Iterable<[string, INodeSummary]> | ||
| ): void { | ||
| for (const [nodeId, node] of values) { | ||
| const existing: INodeSummary | undefined = accumulator.get(nodeId); | ||
| if (!existing) { | ||
| accumulator.set(nodeId, node); | ||
| } else { | ||
| existing.selfTime += node.selfTime; | ||
| existing.totalTime += node.totalTime; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Scans a directory and its subdirectories for CPU profiles. | ||
| * @param baseDir - The directory to recursively search for CPU profiles | ||
| * @returns All .cpuprofile files found in the directory and its subdirectories | ||
| */ | ||
| function findProfiles(baseDir: string): string[] { | ||
| baseDir = path.resolve(baseDir); | ||
|
|
||
| const files: string[] = []; | ||
| const directories: string[] = [baseDir]; | ||
|
|
||
| for (const dir of directories) { | ||
| const entries: fs.Dirent[] = fs.readdirSync(dir, { withFileTypes: true }); | ||
| for (const entry of entries) { | ||
| if (entry.isFile() && entry.name.endsWith('.cpuprofile')) { | ||
| files.push(`${dir}/${entry.name}`); | ||
| } else if (entry.isDirectory()) { | ||
| directories.push(`${dir}/${entry.name}`); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return files; | ||
| } | ||
|
|
||
| /** | ||
| * Processes a set of CPU profiles and aggregates the results. | ||
| * Uses a worker pool. | ||
| * @param profiles - The set of .cpuprofile files to process | ||
| * @returns A summary of the profiles | ||
| */ | ||
| async function processProfilesAsync(profiles: Set<string>): Promise<IProfileSummary> { | ||
| const maxWorkers: number = Math.min(profiles.size, os.availableParallelism()); | ||
| console.log(`Processing ${profiles.size} profiles using ${maxWorkers} workers...`); | ||
| const workerPool: WorkerPool = new WorkerPool({ | ||
| id: 'cpu-profile-summarizer', | ||
| maxWorkers, | ||
| workerScriptPath: path.resolve(__dirname, 'worker.js') | ||
| }); | ||
|
|
||
| const summary: IProfileSummary = new Map(); | ||
|
|
||
| let processed: number = 0; | ||
| await Promise.all( | ||
| Array.from(profiles, async (profile: string) => { | ||
| const worker: Worker = await workerPool.checkoutWorkerAsync(true); | ||
| const responsePromise: Promise<IMessageFromWorker[]> = once(worker, 'message'); | ||
| worker.postMessage(profile); | ||
| const { 0: messageFromWorker } = await responsePromise; | ||
| if (messageFromWorker.type === 'error') { | ||
| console.error(`Error processing ${profile}: ${messageFromWorker.data}`); | ||
| } else { | ||
| ++processed; | ||
| console.log(`Processed ${profile} (${processed}/${profiles.size})`); | ||
| mergeProfileSummaries(summary, messageFromWorker.data); | ||
| } | ||
| workerPool.checkinWorker(worker); | ||
| }) | ||
| ); | ||
|
|
||
| await workerPool.finishAsync(); | ||
|
|
||
| return summary; | ||
| } | ||
|
|
||
| function writeSummaryToTsv(tsvPath: string, summary: IProfileSummary): void { | ||
| const dir: string = path.dirname(tsvPath); | ||
| fs.mkdirSync(dir, { recursive: true }); | ||
|
|
||
| let tsv: string = `Self Time (seconds)\tTotal Time (seconds)\tFunction Name\tURL\tLine\tColumn`; | ||
| for (const { selfTime, totalTime, functionName, url, lineNumber, columnNumber } of summary.values()) { | ||
| const selfSeconds: string = (selfTime / 1e6).toFixed(3); | ||
| const totalSeconds: string = (totalTime / 1e6).toFixed(3); | ||
|
|
||
| tsv += `\n${selfSeconds}\t${totalSeconds}\t${functionName}\t${url}\t${lineNumber}\t${columnNumber}`; | ||
| } | ||
|
|
||
| fs.writeFileSync(tsvPath, tsv, 'utf8'); | ||
| console.log(`Wrote summary to ${tsvPath}`); | ||
| } | ||
|
|
||
| class CpuProfileSummarizerCommandLineParser extends CommandLineParser { | ||
| private readonly _inputParameter: CommandLineStringListParameter; | ||
| private readonly _outputParameter: IRequiredCommandLineStringParameter; | ||
|
|
||
| public constructor() { | ||
| super({ | ||
| toolFilename: 'cpu-profile-summarizer', | ||
| toolDescription: | ||
| 'This tool summarizes the contents of multiple V8 .cpuprofile reports. ' + | ||
| 'For example, those generated by running `node --cpu-prof`.' | ||
| }); | ||
|
|
||
| this._inputParameter = this.defineStringListParameter({ | ||
| parameterLongName: '--input', | ||
| parameterShortName: '-i', | ||
| description: 'The directory containing .cpuprofile files to summarize', | ||
| argumentName: 'DIR', | ||
| required: true | ||
| }); | ||
|
|
||
| this._outputParameter = this.defineStringParameter({ | ||
| parameterLongName: '--output', | ||
| parameterShortName: '-o', | ||
| description: 'The output file to write the summary to', | ||
| argumentName: 'TSV_FILE', | ||
| required: true | ||
| }); | ||
| } | ||
|
|
||
| protected async onExecute(): Promise<void> { | ||
| const input: readonly string[] = this._inputParameter.values; | ||
| const output: string = this._outputParameter.value; | ||
|
|
||
| if (input.length === 0) { | ||
| throw new Error('No input directories provided'); | ||
| } | ||
|
|
||
| const allProfiles: Set<string> = new Set(); | ||
| for (const dir of input) { | ||
| const resolvedDir: string = path.resolve(dir); | ||
| console.log(`Collating CPU profiles from ${resolvedDir}...`); | ||
| const profiles: string[] = findProfiles(resolvedDir); | ||
| console.log(`Found ${profiles.length} profiles`); | ||
| for (const profile of profiles) { | ||
| allProfiles.add(profile); | ||
| } | ||
| } | ||
|
|
||
| if (allProfiles.size === 0) { | ||
| throw new Error(`No profiles found`); | ||
| } | ||
|
|
||
| const summary: IProfileSummary = await processProfilesAsync(allProfiles); | ||
|
|
||
| writeSummaryToTsv(output, summary); | ||
| } | ||
| } | ||
|
|
||
| process.exitCode = 1; | ||
| const parser: CpuProfileSummarizerCommandLineParser = new CpuProfileSummarizerCommandLineParser(); | ||
|
|
||
| parser | ||
| .executeAsync() | ||
| .then((success: boolean) => { | ||
| if (success) { | ||
| process.exitCode = 0; | ||
| } | ||
| }) | ||
| .catch((error: Error) => { | ||
| console.error(error); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.