Skip to content

feat(tools-performance): Add @rnx-kit/tools-performance package#4079

Open
JasonVMo wants to merge 3 commits intomainfrom
user/jasonvmo/tools-perf
Open

feat(tools-performance): Add @rnx-kit/tools-performance package#4079
JasonVMo wants to merge 3 commits intomainfrom
user/jasonvmo/tools-perf

Conversation

@JasonVMo
Copy link
Copy Markdown
Collaborator

@JasonVMo JasonVMo commented Apr 9, 2026

Description

This adds a framework for doing lightweight performance tracing, some basic reporting, and enabling tracing at a global level.

Usage

In packages, to add instrumentation:

import { getTrace } from "@rnx-kit/tools-performance";

async function myFunction() {
  // will return a logging tracer if "transform" is enabled, a passthrough if not
  const trace = getTrace("transform");
  // trace a sync closure
  const result1 = trace("func1", () => doSomething(p1, p2));
  // trace a sync function without creating a closure
  const result2 = trace("process result, doSomethingElse, result1, options);
  // trace an async function and return
  const final = await trace("call async", finalCall, result2);
  return final;
}

Right now, to enable this for metro bundling a user would add in their metro.config.js:

import { trackPerformance } from "@rnx-kit/tools-performance";

// these would all work
trackPerformance("transform"); // only turn on this one area
trackPerformance(["transform", "metro"]); // turn on a set of areas
trackPerformance(/* or true */); // turn on all performance tracing

Future things to add (or consider)

  • instrument various parts of our infrastructure
  • add an implementation of unstable_perfLoggerFactory that hooks into this
  • have our metro config automatically add the unstable_perfLoggerFactory if "metro" is enabled
  • consider adding a cli option that will enable perf logging for our tools automatically

Other notes

  • text is styled using the formatting from node:utils
  • simple table formatting in the same format as console.table is added, except with support for formatted text.

* 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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return defaultManager?.isEnabled(category) ?? false;
return Boolean(defaultManager?.isEnabled(category));

Comment on lines +66 to +75
const start = performance.now();
const result = callFunction<TFunc>(fn, args);
if (isPromiseLike(result)) {
return result.then((res: ReturnType<TFunc>) => {
record(tag, performance.now() - start);
return res;
});
}
record(tag, performance.now() - start);
return result;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use performance.mark and performance.measure instead of manually recording time? https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure#examples

Another added benefit is getting tracking for free: https://developer.mozilla.org/en-US/docs/Web/API/Performance/getEntries

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm reworking this a bit to be cleaner and will support both. The perf markers are heavier than timing based ones and have memory bloat issues. Instrumenting babel plugins with them would run out of memory. It only takes a slight adjustment to make either work.

Comment on lines +1 to +2
<!-- We recommend an empty change log entry for a new package: `yarn change --empty` -->

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove

Suggested change
<!-- We recommend an empty change log entry for a new package: `yarn change --empty` -->

}
return result;
});
// use console.table for nice formatting
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you plan to implement this in this PR or later?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old comment. Will clean it up in the next iteration.

Comment on lines +23 to +36
export const exitHandler = (() => {
let callbacks: (() => void)[] | undefined = undefined;

function add(callback: () => void) {
if (!callbacks) {
callbacks = [];
process.on("exit", () => {
for (const cb of callbacks!) {
cb();
}
});
}
callbacks.push(callback);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid force nonnull assertion. Rewrite to help TypeScript infer nullability:

Suggested change
export const exitHandler = (() => {
let callbacks: (() => void)[] | undefined = undefined;
function add(callback: () => void) {
if (!callbacks) {
callbacks = [];
process.on("exit", () => {
for (const cb of callbacks!) {
cb();
}
});
}
callbacks.push(callback);
}
type OnExit = () => void;
export const exitHandler = (() => {
let callbacks: OnExit[] | undefined = undefined;
function add(callback: OnExit) {
if (!callbacks) {
const arr: OnExit[] = [];
process.on("exit", () => {
for (const cb of arr) {
cb();
}
});
callbacks = arr;
}
callbacks.push(callback);
}

Comment on lines +39 to +41
if (callbacks) {
callbacks = callbacks.filter((cb) => cb !== callback);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be using Set<OnExit> instead to ensure uniqueness and gain access to .delete()?

Comment on lines +73 to +77
} else {
for (const cat of category) {
this.enabled.add(cat);
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably also check that category is an actual array:

Suggested change
} else {
for (const cat of category) {
this.enabled.add(cat);
}
}
} else if (Array.isArray(category)) {
for (const cat of category) {
this.enabled.add(cat);
}
} else {
throw new Error(`invalid category: ${category}`);
}

Comment on lines +84 to +90
getTrace(category?: string): TraceFunction {
return this.getCategory(category)?.trace ?? nullTrace;
}

getRecorder(category?: string): TraceRecorder {
return this.getCategory(category)?.record ?? nullRecord;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should return undefined since we're already handling nullable in the module-level getRecorder/getTrace methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants