Skip to content

Commit b93a6e2

Browse files
authored
feat(tools-performance): add subdomains and a Metro perfLoggerFactory implementation (#4090)
1 parent 1670dba commit b93a6e2

File tree

11 files changed

+390
-35
lines changed

11 files changed

+390
-35
lines changed

.changeset/heavy-geese-refuse.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@rnx-kit/tools-performance": patch
3+
---
4+
5+
Add subdomain support and metro logging integration

incubator/tools-performance/README.md

Lines changed: 96 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,23 @@ const trace = getTrace("metro");
108108
const myTrace = someCondition ? customTrace : nullTrace;
109109
```
110110

111+
### Manual event timing
112+
113+
Use `startEvent` on a domain to manually control the start and end of a timed
114+
event. This is useful when start and end points don't wrap a single function
115+
call:
116+
117+
```typescript
118+
import { getDomain } from "@rnx-kit/tools-performance";
119+
120+
const domain = getDomain("metro");
121+
if (domain) {
122+
const endEvent = domain.startEvent("resolve");
123+
// ... do work across multiple steps ...
124+
endEvent(); // records the duration
125+
}
126+
```
127+
111128
### Custom trace functions
112129

113130
Use `createTrace` with a `TraceRecorder` to build trace functions backed by
@@ -172,6 +189,27 @@ trackPerformance({ enable: ["resolve", "transform"] });
172189
// Calls are additive — all three domains above are now enabled
173190
```
174191

192+
### Subdomains
193+
194+
Subdomains create a parent–child relationship between domains. When a parent
195+
domain is enabled, all its registered subdomains are automatically enabled too.
196+
This is useful for organizing related operations under a single toggle.
197+
198+
```typescript
199+
import { registerSubdomain, getDomain } from "@rnx-kit/tools-performance";
200+
201+
// Register "metro:resolver" as a subdomain of "metro"
202+
registerSubdomain("metro", "resolver");
203+
204+
// Enabling "metro" now also enables "metro:resolver"
205+
trackPerformance({ enable: "metro", strategy: "timing" });
206+
207+
const domain = getDomain("metro:resolver"); // enabled via parent
208+
```
209+
210+
Subdomains can be registered before or after the parent is enabled — the
211+
relationship is resolved in either order.
212+
175213
### Checking if tracing is enabled
176214

177215
`isTraceEnabled` checks domain and frequency without creating a domain as a
@@ -245,40 +283,74 @@ const table = formatAsTable(
245283
console.log(table);
246284
```
247285

286+
## Metro Integration
287+
288+
`createPerfLoggerFactory` returns a factory compatible with Metro's
289+
`unstable_perfLoggerFactory` config option. It bridges Metro's performance
290+
logging into the tools-performance domain system under the `"metro"` parent
291+
domain.
292+
293+
```typescript
294+
import {
295+
createPerfLoggerFactory,
296+
trackPerformance,
297+
} from "@rnx-kit/tools-performance";
298+
299+
// Enable the metro domain
300+
trackPerformance({ enable: "metro", strategy: "timing" });
301+
302+
// Pass the factory to Metro config
303+
module.exports = {
304+
unstable_perfLoggerFactory: createPerfLoggerFactory(),
305+
};
306+
```
307+
308+
When Metro calls the factory, it creates subdomains like `metro:start_up`,
309+
`metro:bundling_request`, and `metro:hmr`. Metro's `subSpan` calls create deeper
310+
subdomains (e.g. `metro:start_up:resolver`). Metro's `point` events with
311+
`_start`/`_end` suffixes are mapped to timed events via `startEvent`.
312+
313+
When the `"metro"` domain is not enabled, the factory returns no-op loggers with
314+
zero overhead.
315+
248316
## API Reference
249317

250318
### Module-Level Functions
251319

252-
| Function | Description |
253-
| ------------------------------- | ----------------------------------------------------------------------- |
254-
| `trackPerformance(config?)` | Enable tracking. Config controls domains, strategy, and report options. |
255-
| `getTrace(domain, frequency?)` | Get a trace function for a domain. Returns `nullTrace` if not enabled. |
256-
| `getDomain(name)` | Get the `PerfDomain` for a domain, or `undefined` if not enabled. |
257-
| `isTraceEnabled(domain, freq?)` | Check if tracing is enabled for a domain and optional frequency. |
258-
| `reportPerfData()` | Finish tracking and print the performance report. |
320+
| Function | Description |
321+
| -------------------------------------- | ----------------------------------------------------------------------- |
322+
| `trackPerformance(config?)` | Enable tracking. Config controls domains, strategy, and report options. |
323+
| `getTrace(domain, frequency?)` | Get a trace function for a domain. Returns `nullTrace` if not enabled. |
324+
| `getDomain(name)` | Get the `PerfDomain` for a domain, or `undefined` if not enabled. |
325+
| `isTraceEnabled(domain, freq?)` | Check if tracing is enabled for a domain and optional frequency. |
326+
| `registerSubdomain(domain, subdomain)` | Register a subdomain under a parent. Enabled when the parent is. |
327+
| `reportPerfData()` | Finish tracking and print the performance report. |
328+
| `createPerfLoggerFactory()` | Create a Metro-compatible `unstable_perfLoggerFactory`. |
259329

260330
### PerfTracker
261331

262-
| Member | Description |
263-
| -------------------------- | ---------------------------------------------------------------------- |
264-
| `new PerfTracker(config?)` | Create a new tracker. Auto-registers a process exit handler. |
265-
| `enable(domain)` | Enable tracking for `true` (all), a string, or string array. |
266-
| `isEnabled(domain, freq?)` | Check if a domain is enabled, optionally at a given frequency. |
267-
| `domain(name)` | Get or create a `PerfDomain` for an enabled domain. |
268-
| `finish(processExit?)` | Stop all domains, print the report, and unregister. Only reports once. |
269-
| `updateConfig(config)` | Merge new configuration values. |
332+
| Member | Description |
333+
| -------------------------------- | ---------------------------------------------------------------------- |
334+
| `new PerfTracker(config?)` | Create a new tracker. Auto-registers a process exit handler. |
335+
| `enable(domain)` | Enable tracking for `true` (all), a string, or string array. |
336+
| `isEnabled(domain, freq?)` | Check if a domain is enabled, optionally at a given frequency. |
337+
| `domain(name)` | Get or create a `PerfDomain` for an enabled domain. |
338+
| `registerSubdomain(domain, sub)` | Register a subdomain. Enabling the parent enables the subdomain. |
339+
| `finish(processExit?)` | Stop all domains, print the report, and unregister. Only reports once. |
340+
| `updateConfig(config)` | Merge new configuration values. |
270341

271342
### PerfDomain
272343

273-
| Member | Description |
274-
| ---------------------- | ---------------------------------------------------------------------- |
275-
| `name` | Domain name (readonly). |
276-
| `strategy` | Tracing strategy: `"timing"` or `"node"` (readonly). |
277-
| `frequency` | Current frequency level (mutable). |
278-
| `start()` | Begin domain-level timing (called automatically unless `waitOnStart`). |
279-
| `stop(processExit?)` | End domain-level timing and clean up marks. |
280-
| `enabled(frequency?)` | Check if a frequency level is active for this domain. |
281-
| `getTrace(frequency?)` | Get a trace function, or `nullTrace` if frequency is not active. |
344+
| Member | Description |
345+
| ------------------------ | ---------------------------------------------------------------------- |
346+
| `name` | Domain name (readonly). |
347+
| `strategy` | Tracing strategy: `"timing"` or `"node"` (readonly). |
348+
| `frequency` | Current frequency level (mutable). |
349+
| `start()` | Begin domain-level timing (called automatically unless `waitOnStart`). |
350+
| `stop(processExit?)` | End domain-level timing and clean up marks. |
351+
| `startEvent(tag, freq?)` | Start a timed event. Returns a function to call when the event ends. |
352+
| `enabled(frequency?)` | Check if a frequency level is active for this domain. |
353+
| `getTrace(frequency?)` | Get a trace function, or `nullTrace` if frequency is not active. |
282354

283355
### Trace Primitives
284356

incubator/tools-performance/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"lib/**/*.d.ts",
1818
"lib/**/*.js"
1919
],
20-
"type": "module",
20+
"type": "commonjs",
2121
"sideEffects": false,
2222
"main": "lib/index.js",
2323
"types": "lib/index.d.ts",
@@ -35,7 +35,8 @@
3535
},
3636
"devDependencies": {
3737
"@rnx-kit/scripts": "*",
38-
"@rnx-kit/tsconfig": "*"
38+
"@rnx-kit/tsconfig": "*",
39+
"metro-config": "^0.83.3"
3940
},
4041
"engines": {
4142
"node": ">=22.11"

incubator/tools-performance/src/domain.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createTrace, nullTrace, nullRecordTime } from "./trace.ts";
1+
import { createTrace, nullTrace, nullFunction } from "./trace.ts";
22
import type { TraceRecorder } from "./trace.ts";
33
import type {
44
EventFrequency,
@@ -52,6 +52,19 @@ export class PerfDomain {
5252
return this.enabled(requested) ? this.trace : nullTrace;
5353
}
5454

55+
/**
56+
* Create a wrapper around an event to be able to manually start and stop the timing/marking.
57+
* @param tag The tag for the event
58+
* @returns A function that stops the event when called
59+
*/
60+
startEvent(tag: string, frequency?: EventFrequency): () => void {
61+
if (!this.enabled(frequency)) {
62+
return nullFunction;
63+
}
64+
const startVal = this.record(tag);
65+
return () => this.record(tag, startVal);
66+
}
67+
5568
/**
5669
* Start performance tracking for this namespace. This will be called automatically on creation unless
5770
* the waitOnStart option is set. Trace events will still work without this but you won't get a boundary
@@ -104,7 +117,7 @@ export class PerfDomain {
104117
} = options;
105118
this.frequency = frequency;
106119
this.strategy = recordTime ? "timing" : "node";
107-
this.recordTime = recordTime ?? nullRecordTime;
120+
this.recordTime = recordTime ?? nullFunction;
108121
this.record = recordTime
109122
? this.timingRecorder.bind(this)
110123
: this.markingRecorder.bind(this);

incubator/tools-performance/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export {
44
getDomain,
55
getTrace,
66
isTrackingEnabled,
7+
registerSubdomain,
78
reportPerfData,
89
trackPerformance,
910
} from "./perf.ts";
@@ -24,3 +25,5 @@ export type {
2425
TraceFunction,
2526
TraceStrategy,
2627
} from "./types.ts";
28+
29+
export { createPerfLoggerFactory } from "./metro.ts";
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type {
2+
PerfAnnotations,
3+
PerfLogger,
4+
RootPerfLogger,
5+
PerfLoggerFactoryOptions,
6+
PerfLoggerFactory,
7+
PerfLoggerPointOptions,
8+
} from "metro-config";
9+
import { type PerfDomain } from "./domain.ts";
10+
import { getDomain, registerSubdomain } from "./perf.ts";
11+
import { nullFunction } from "./trace.ts";
12+
13+
function createEmptyLogger(): PerfLogger {
14+
return {
15+
point: nullFunction,
16+
annotate: nullFunction,
17+
subSpan: createEmptyLogger,
18+
};
19+
}
20+
21+
// metro uses point events with _start and _end to indicate the start and end of events
22+
const POINT_START_SUFFIX = "_start";
23+
const POINT_END_SUFFIX = "_end";
24+
25+
function getSubdomainLogger(
26+
base: string,
27+
key?: string
28+
): [PerfLogger, PerfDomain | undefined] {
29+
key = key != null ? `:${key}` : "";
30+
const subdomain = `${base}${key}`;
31+
// register the subdomain to ensure the associations are set up correctly in the tracker
32+
registerSubdomain("metro", subdomain);
33+
// now get the subdomain itself so we can create a logger for it
34+
const domain = getDomain(`metro:${subdomain}`);
35+
const logger = createLogger(subdomain, domain);
36+
return [logger, domain];
37+
}
38+
39+
function createLogger(subdomainName: string, domain?: PerfDomain): PerfLogger {
40+
if (!domain) {
41+
return createEmptyLogger();
42+
}
43+
const openEvents: Record<string, () => void> = {};
44+
return {
45+
point(name: string, _opts?: PerfLoggerPointOptions) {
46+
if (name.endsWith(POINT_START_SUFFIX)) {
47+
const eventKey = name.slice(0, -POINT_START_SUFFIX.length);
48+
// this shouldn't happen but close any open event with the same name just in case
49+
openEvents[eventKey]?.();
50+
// now open the event for this point
51+
openEvents[eventKey] = domain.startEvent(eventKey);
52+
} else if (name.endsWith(POINT_END_SUFFIX)) {
53+
const eventKey = name.slice(0, -POINT_END_SUFFIX.length);
54+
const endEvent = openEvents[eventKey];
55+
if (endEvent) {
56+
endEvent();
57+
delete openEvents[eventKey];
58+
}
59+
}
60+
},
61+
annotate(_annotations: PerfAnnotations) {
62+
// do nothing for annotations
63+
},
64+
subSpan(label: string): PerfLogger {
65+
const [logger] = getSubdomainLogger(subdomainName, label);
66+
return logger;
67+
},
68+
};
69+
}
70+
71+
/**
72+
* Create a PerfLoggerFactory that integrates with tools-performance. This will log events
73+
* based on the "metro" domain being enabled.
74+
*/
75+
export function createPerfLoggerFactory(): PerfLoggerFactory | undefined {
76+
return (
77+
type: "START_UP" | "BUNDLING_REQUEST" | "HMR",
78+
opts?: PerfLoggerFactoryOptions
79+
): RootPerfLogger => {
80+
const keyStr = opts?.key != null ? `#${opts.key}` : undefined;
81+
const [logger, domain] = getSubdomainLogger(type.toLowerCase(), keyStr);
82+
83+
return {
84+
...logger,
85+
start(_startOpts) {
86+
domain?.start();
87+
},
88+
end(_status, _endOpts) {
89+
domain?.stop();
90+
},
91+
};
92+
};
93+
}

incubator/tools-performance/src/perf.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ export function getDomain(name: string) {
5252
return defaultManager?.domain(name);
5353
}
5454

55+
/**
56+
* Register a subdomain under a parent domain. This domain will be enabled whenever the parent domain is enabled but can
57+
* be enabled or tracked separately as well.
58+
*/
59+
export function registerSubdomain(domain: string, subdomain: string) {
60+
defaultManager?.registerSubdomain(domain, subdomain);
61+
}
62+
5563
/**
5664
* Reset the module-level performance tracker, releasing all domains and state.
5765
* Intended for test isolation — not part of the public API.

incubator/tools-performance/src/trace.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ export function nullPassthrough<T>(value: T): T {
3131
return value;
3232
}
3333

34-
/** no-op function matching the recordTime signature */
35-
export function nullRecordTime(_tag: string, _duration?: number): void {
34+
/** no-op function taking any number of parameters and doing nothing */
35+
export function nullFunction(..._args: unknown[]): void {
3636
// intentionally empty
3737
}
3838

0 commit comments

Comments
 (0)