-
Notifications
You must be signed in to change notification settings - Fork 117
feat(tools-performance): Add @rnx-kit/tools-performance package #4079
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
Merged
Changes from 3 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
d8893dd
add the tools-performance package for perf tracing
JasonVMo 264842d
tools-performance implementation
JasonVMo 2b8d771
docs(changeset): Initial implementation of tools-performance package
JasonVMo 99e19cc
Merge branch 'main' of https://github.com/microsoft/rnx-kit into user…
JasonVMo bf91eee
rework organization, table output, and tests
JasonVMo e503841
address a few more PR comments
JasonVMo 42e7fef
Merge branch 'main' of https://github.com/microsoft/rnx-kit into user…
JasonVMo 22b0d20
export some missing types
JasonVMo 215c602
quick bug fixes for tools-performance
JasonVMo 3cd3fca
remove readme comment
JasonVMo 8efc836
Apply suggestions from code review
JasonVMo f5f4df3
fix build break
JasonVMo 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@rnx-kit/tools-performance": minor | ||
| --- | ||
|
|
||
| Initial implementation of tools-performance package |
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,251 @@ | ||
| <!-- We recommend an empty change log entry for a new package: `yarn change --empty` --> | ||
|
|
||
| # @rnx-kit/tools-performance | ||
|
|
||
| [](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml) | ||
| [](https://www.npmjs.com/package/@rnx-kit/tools-performance) | ||
|
|
||
| 🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧 | ||
|
|
||
| ### THIS TOOL IS EXPERIMENTAL — USE WITH CAUTION | ||
|
|
||
| 🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧 | ||
|
|
||
| Lightweight performance tracing and reporting for Node.js tooling. Provides a | ||
| simple API for measuring the duration of synchronous and asynchronous operations, | ||
| categorizing them by area, and printing a summary table on process exit. | ||
|
|
||
| ## Motivation | ||
|
|
||
| Build tools like Metro bundlers, dependency resolvers, and transformers benefit | ||
| from visibility into where time is spent. This package provides a low-overhead | ||
| way to instrument code, collect timing data across categories, and produce a | ||
| human-readable report — without adding heavy dependencies. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```sh | ||
| yarn add @rnx-kit/tools-performance --dev | ||
| ``` | ||
|
|
||
| or if you're using npm | ||
|
|
||
| ```sh | ||
| npm add --save-dev @rnx-kit/tools-performance | ||
| ``` | ||
|
|
||
| ## Quick Start | ||
|
|
||
| The simplest way to get started is with the module-level API: | ||
|
|
||
| ```typescript | ||
| import { | ||
| trackPerformance, | ||
| getTrace, | ||
| reportPerfData, | ||
| } from "@rnx-kit/tools-performance"; | ||
|
|
||
| // Enable tracking (optionally scoped to a category) | ||
| trackPerformance(true); | ||
|
|
||
| const trace = getTrace(); | ||
|
|
||
| // Trace a sync function | ||
| const result = trace("parse", () => parseConfig(configPath)); | ||
|
|
||
| // Trace an async function | ||
| const bundle = await trace("bundle", async () => { | ||
| return await buildBundle(entryPoint); | ||
| }); | ||
|
|
||
| // Print the report (also prints automatically on process exit) | ||
| reportPerfData(); | ||
| ``` | ||
|
|
||
| Output: | ||
|
|
||
| ``` | ||
| Performance results: | ||
| ┌────────┬───────┬───────┬─────┐ | ||
| │ name │ calls │ total │ avg │ | ||
| ├────────┼───────┼───────┼─────┤ | ||
| │ parse │ 1 │ 12 │ 12 │ | ||
| │ bundle │ 1 │ 450 │ 450 │ | ||
| └────────┴───────┴───────┴─────┘ | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| ### Module-Level API | ||
|
|
||
| For typical usage, the module-level functions manage a shared `PerfManager` | ||
| instance automatically: | ||
|
|
||
| ```typescript | ||
| import { | ||
| trackPerformance, | ||
| getTrace, | ||
| getRecorder, | ||
| isTrackingEnabled, | ||
| reportPerfData, | ||
| } from "@rnx-kit/tools-performance"; | ||
|
|
||
| // Enable all tracking | ||
| trackPerformance(true); | ||
|
|
||
| // Or enable specific categories | ||
| trackPerformance("metro"); | ||
| trackPerformance(["resolve", "transform"]); | ||
|
|
||
| // Get a trace function (returns nullTrace passthrough if not enabled) | ||
| const trace = getTrace("metro"); | ||
| const result = trace("operation", myFunction, arg1, arg2); | ||
|
|
||
| // Get a low-level recorder for manual timing | ||
| const record = getRecorder("metro"); | ||
| record("custom-op"); // mark start | ||
| // ... do work ... | ||
| record("custom-op", elapsed); // mark completion with duration in ms | ||
|
|
||
| // Check if tracking is enabled | ||
| if (isTrackingEnabled("metro")) { | ||
| // ... | ||
| } | ||
|
|
||
| // Print report early (also auto-reports on process exit) | ||
| reportPerfData(); | ||
|
|
||
| // Peek at results without finalizing (continues tracking, still reports at exit) | ||
| reportPerfData(true); | ||
| ``` | ||
|
|
||
| ### PerfManager Class | ||
|
|
||
| For more control, use `PerfManager` directly: | ||
|
|
||
| ```typescript | ||
| import { PerfManager } from "@rnx-kit/tools-performance"; | ||
|
|
||
| const mgr = new PerfManager({ | ||
| sort: "total", | ||
| cols: ["name", "calls", "total", "avg"], | ||
| maxOperationWidth: 40, | ||
| }); | ||
|
|
||
| mgr.enable("metro"); | ||
|
|
||
| const trace = mgr.getTrace("metro"); | ||
| await trace("bundle", buildBundle, entryPoint); | ||
|
|
||
| // Get raw results (no ANSI styling) for programmatic use | ||
| const results = mgr.getResults(); | ||
|
|
||
| // Print formatted report | ||
| mgr.report(); | ||
| ``` | ||
|
|
||
| ### Trace Function | ||
|
|
||
| The `trace` function wraps any function and measures its execution time. It | ||
| handles both sync and async functions transparently: | ||
|
|
||
| ```typescript | ||
| // With a closure (most common) | ||
| const result = trace("myOp", () => doWork(a, b)); | ||
|
|
||
| // Without a closure — args are type-checked against the function signature | ||
| const result = trace("myOp", doWork, a, b); | ||
|
|
||
| // Async functions are detected automatically | ||
| const result = await trace("fetch", () => fetch(url)); | ||
| ``` | ||
|
|
||
| If the traced function throws (sync) or rejects (async), the error propagates | ||
| normally. The start is recorded but no completion is logged, which shows up as an | ||
| error count in the report. | ||
|
|
||
| ### Creating Custom Trace Functions | ||
|
|
||
| Use `createTrace` to build a trace function backed by a custom recorder: | ||
|
|
||
| ```typescript | ||
| import { createTrace } from "@rnx-kit/tools-performance"; | ||
| import type { TraceRecorder } from "@rnx-kit/tools-performance"; | ||
|
|
||
| const myRecorder: TraceRecorder = (tag, durationMs) => { | ||
| if (durationMs !== undefined) { | ||
| console.log(`${tag} took ${durationMs}ms`); | ||
| } | ||
| }; | ||
|
|
||
| const trace = createTrace(myRecorder); | ||
| trace("work", () => doExpensiveWork()); | ||
| ``` | ||
|
|
||
| ### Null Implementations | ||
|
|
||
| `nullTrace` and `nullRecord` are no-op implementations useful as defaults when | ||
| tracking is disabled: | ||
|
|
||
| ```typescript | ||
| import { nullTrace, nullRecord } from "@rnx-kit/tools-performance"; | ||
|
|
||
| // nullTrace passes through to the function with no overhead beyond arg forwarding | ||
| const result = nullTrace("tag", myFunction, arg1, arg2); | ||
|
|
||
| // nullRecord does nothing | ||
| nullRecord("tag", 42); | ||
| ``` | ||
|
|
||
| ## API Reference | ||
|
|
||
| ### Module-Level Functions | ||
|
|
||
| | Function | Description | | ||
| | ---------------------------------- | ---------------------------------------------------------------------------- | | ||
| | `trackPerformance(mode?, config?)` | Enable tracking. `mode`: `true`, a category string, or array of categories. | | ||
| | `getTrace(category?)` | Get a trace function for a category. Returns `nullTrace` if not enabled. | | ||
| | `getRecorder(category?)` | Get a recorder function for a category. Returns `nullRecord` if not enabled. | | ||
| | `isTrackingEnabled(category?)` | Check if tracking is enabled for a category. | | ||
| | `reportPerfData(peekOnly?)` | Print the performance report. Pass `true` to peek without finalizing. | | ||
|
|
||
| ### PerfManager | ||
|
|
||
| | Member | Description | | ||
| | -------------------------- | --------------------------------------------------------------- | | ||
| | `new PerfManager(config?)` | Create a new manager. Auto-registers a process exit handler. | | ||
| | `enable(category)` | Enable tracking for `true` (all), a string, or string array. | | ||
| | `isEnabled(category?)` | Check if a category is enabled. | | ||
| | `getTrace(category?)` | Get a trace function for the category. | | ||
| | `getRecorder(category?)` | Get a recorder function for the category. | | ||
| | `getResults()` | Get raw `PerfDataEntry[]` (no ANSI codes) for programmatic use. | | ||
| | `report(peekOnly?)` | Print the formatted report to the console. | | ||
| | `updateConfig(config)` | Merge new configuration values. | | ||
|
|
||
| ### Trace Primitives | ||
|
|
||
| | Function | Description | | ||
| | ------------------------------ | ----------------------------------------------------------- | | ||
| | `createTrace(recorder)` | Create a trace function backed by a custom `TraceRecorder`. | | ||
| | `nullTrace(tag, fn, ...args)` | No-op trace — calls `fn(...args)` directly. | | ||
| | `nullRecord(tag, durationMs?)` | No-op recorder. | | ||
|
|
||
| ### PerformanceConfiguration | ||
|
|
||
| | Field | Type | Default | Description | | ||
| | ------------------- | ------------------------------------ | ----------------------------------------- | ------------------------------------------------------------- | | ||
| | `sort` | `PerfDataColumn \| PerfDataColumn[]` | insertion order | Columns to sort by, in precedence order. | | ||
| | `cols` | `PerfDataColumn[]` | `["name","calls","total","avg","errors"]` | Columns to display. Errors auto-hidden when all are 0. | | ||
| | `maxOperationWidth` | `number` | `50` | Max visible width for operation names (truncated with `...`). | | ||
|
|
||
| ### PerfDataEntry | ||
|
|
||
| | Field | Type | Description | | ||
| | ----------- | -------- | ------------------------------------------------------ | | ||
| | `name` | `string` | Composed label: `"area: operation"` or `"operation"`. | | ||
| | `area` | `string` | Category name (empty string if unscoped). | | ||
| | `operation` | `string` | Operation name. | | ||
| | `calls` | `number` | Number of times the operation was started. | | ||
| | `total` | `number` | Total duration in milliseconds (completed calls only). | | ||
| | `avg` | `number` | Average duration per completed call. | | ||
| | `errors` | `number` | Calls started but never completed (threw or rejected). | | ||
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 @@ | ||
| { | ||
| "name": "@rnx-kit/tools-performance", | ||
| "version": "0.0.1", | ||
| "description": "EXPERIMENTAL - USE WITH CAUTION - tools-performance", | ||
| "homepage": "https://github.com/microsoft/rnx-kit/tree/main/incubator/tools-performance#readme", | ||
| "license": "MIT", | ||
| "author": { | ||
| "name": "Microsoft Open Source", | ||
| "email": "microsoftopensource@users.noreply.github.com" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/microsoft/rnx-kit", | ||
| "directory": "incubator/tools-performance" | ||
| }, | ||
| "files": [ | ||
| "lib/**/*.d.ts", | ||
| "lib/**/*.js" | ||
| ], | ||
| "type": "module", | ||
| "sideEffects": false, | ||
| "main": "lib/index.js", | ||
| "types": "lib/index.d.ts", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./lib/index.d.ts", | ||
| "default": "./lib/index.js" | ||
| } | ||
| }, | ||
| "scripts": { | ||
| "build": "rnx-kit-scripts build", | ||
| "format": "rnx-kit-scripts format", | ||
| "lint": "rnx-kit-scripts lint", | ||
| "test": "rnx-kit-scripts test" | ||
| }, | ||
| "devDependencies": { | ||
| "@rnx-kit/scripts": "*", | ||
| "@rnx-kit/tsconfig": "*" | ||
| }, | ||
| "engines": { | ||
| "node": ">=22.11" | ||
| }, | ||
| "experimental": true | ||
| } |
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,18 @@ | ||
| export { createTrace, nullRecord, nullTrace } from "./trace.ts"; | ||
| export { | ||
| getRecorder, | ||
| getTrace, | ||
| isTrackingEnabled, | ||
| reportPerfData, | ||
| trackPerformance, | ||
| } from "./perf.ts"; | ||
| export { reportResults, formatTable } from "./report.ts"; | ||
| export { PerfManager } from "./tracker.ts"; | ||
| export type { | ||
| PerfArea, | ||
| PerfDataColumn, | ||
| PerfDataEntry, | ||
| PerformanceConfiguration, | ||
| TraceFunction, | ||
| TraceRecorder, | ||
| } from "./types.ts"; |
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,59 @@ | ||
| import { nullRecord, nullTrace } from "./trace.ts"; | ||
| import { PerfManager } from "./tracker.ts"; | ||
| import type { PerfArea, PerformanceConfiguration } from "./types.ts"; | ||
|
|
||
| let defaultManager: PerfManager | undefined = undefined; | ||
|
|
||
| /** | ||
| * Start tracking performance for the specified mode and configuration. Mode can be: | ||
| * - true to enable all tracking | ||
| * - a specific area to track (e.g. "metro", "resolve", "transform", "serialize") | ||
| * - a list of areas to track | ||
| * - undefined which will default to true and enable all tracking | ||
| * Calling multiple times will be additive in terms of areas tracked and will overwrite the configuration | ||
| * | ||
| * @param mode tracking modes to enable | ||
| * @param config performance configuration | ||
| */ | ||
| export function trackPerformance( | ||
| mode: true | PerfArea | PerfArea[] = true, | ||
| config?: PerformanceConfiguration | ||
| ) { | ||
| if (!defaultManager) { | ||
| defaultManager = new PerfManager(config); | ||
| } else if (config) { | ||
| defaultManager.updateConfig(config); | ||
| } | ||
| defaultManager.enable(mode); | ||
| } | ||
|
|
||
| /** | ||
| * Finish tracking (rather than waiting for process exit) and print the report to the console. | ||
| * @param peekOnly if true, will print the report but continue tracking and still report out at process exit. | ||
| */ | ||
| export function reportPerfData(peekOnly = false) { | ||
| defaultManager?.report(peekOnly); | ||
| } | ||
|
|
||
| /** | ||
| * Check if tracking is enabled for a specific category. If category is undefined, checks if all tracking is enabled. | ||
| */ | ||
| export function isTrackingEnabled(category?: string) { | ||
| return defaultManager?.isEnabled(category) ?? false; | ||
JasonVMo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /** | ||
| * Get a trace function for the specified category. If not enabled it will be a non-tracking passthrough | ||
| * @param category category or undefined for unscoped | ||
| */ | ||
| export function getTrace(category?: string) { | ||
| return defaultManager?.getTrace(category) ?? nullTrace; | ||
| } | ||
|
|
||
| /** | ||
| * Get a recorder function for the specified category. If not enabled it will be a non-tracking no-op | ||
| * @param category category or undefined for unscoped | ||
| */ | ||
| export function getRecorder(category?: string) { | ||
| return defaultManager?.getRecorder(category) ?? nullRecord; | ||
| } | ||
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.