@@ -18,6 +18,7 @@ import {
1818 type ListCommandResult ,
1919 loadCoverageProvider ,
2020 logger ,
21+ PhaseTracker ,
2122 type ProjectContext ,
2223 type Reporter ,
2324 type Rstest ,
@@ -95,6 +96,18 @@ type RsbuildInstance = rsbuild.RsbuildInstance;
9596const __dirname = dirname ( fileURLToPath ( import . meta. url ) ) ;
9697const OPTIONS_PLACEHOLDER = '__RSTEST_OPTIONS_PLACEHOLDER__' ;
9798
99+ /**
100+ * Monotonic counter for synthetic per-file Perfetto `pid` values in `--trace`
101+ * mode. Browser host runs every test file inside the same Node process, so
102+ * without an override every file would emit events under the same `pid` and
103+ * share a single track labelled `worker <hostPid>`. Giving each file its own
104+ * synthetic `pid` makes the track title surface the file path instead,
105+ * matching node mode's default `isolate: true` behavior. The 1_000_000_000
106+ * base keeps each synthetic `pid` well clear of real OS `pid` values in
107+ * mixed-mode traces.
108+ */
109+ let nextBrowserFilePid = 1_000_000_000 ;
110+
98111/**
99112 * Serialize JSON for inline <script> injection.
100113 * Escapes '<' to prevent accidental </script> break-out.
@@ -1693,10 +1706,22 @@ export const runBrowserController = async (
16931706 context : Rstest ,
16941707 options ?: BrowserTestRunOptions ,
16951708) : Promise < BrowserTestRunResult | void > => {
1696- const { skipOnTestRunEnd = false , allowEmptyWatchRun = false } =
1697- options ?? { } ;
1709+ const {
1710+ skipOnTestRunEnd = false ,
1711+ allowEmptyWatchRun = false ,
1712+ onTraceEvents,
1713+ } = options ?? { } ;
16981714 const buildStart = Date . now ( ) ;
16991715 const isWatchMode = context . command === 'watch' ;
1716+
1717+ // Per-file PhaseTrackers, populated only when `--trace` is on (caller
1718+ // passes `onTraceEvents`). The browser host shares one Node process across
1719+ // every test file, so each tracker is assigned a synthetic per-file pid
1720+ // (`nextBrowserFilePid`) that lets Perfetto render each file as its own
1721+ // process track with the file path as the title.
1722+ const phaseTrackers = onTraceEvents
1723+ ? new Map < string , PhaseTracker > ( )
1724+ : undefined ;
17001725 const browserProjects = getBrowserProjects ( context ) ;
17011726 const useHeadlessDirect = browserProjects . every (
17021727 ( project ) => project . normalizedConfig . browser . headless ,
@@ -2180,6 +2205,17 @@ export const runBrowserController = async (
21802205 const handleTestFileStart = async (
21812206 payload : TestFileStartPayload ,
21822207 ) : Promise < void > => {
2208+ if ( phaseTrackers ) {
2209+ const tracker = new PhaseTracker ( {
2210+ trace : {
2211+ testPath : payload . testPath ,
2212+ project : payload . projectName ,
2213+ } ,
2214+ pid : nextBrowserFilePid ++ ,
2215+ } ) ;
2216+ tracker . transition ( 'prepare' ) ;
2217+ phaseTrackers . set ( payload . testPath , tracker ) ;
2218+ }
21832219 await Promise . all (
21842220 context . reporters . map ( ( reporter ) =>
21852221 ( reporter as Reporter ) . onTestFileStart ?.( {
@@ -2194,6 +2230,7 @@ export const runBrowserController = async (
21942230 const handleTestFileReady = async (
21952231 payload : TestFileReadyPayload ,
21962232 ) : Promise < void > => {
2233+ phaseTrackers ?. get ( payload . testPath ) ?. transition ( 'tests' ) ;
21972234 await Promise . all (
21982235 context . reporters . map ( ( reporter ) =>
21992236 ( reporter as Reporter ) . onTestFileReady ?.( payload ) ,
@@ -2204,6 +2241,7 @@ export const runBrowserController = async (
22042241 const handleTestSuiteStart = async (
22052242 payload : TestSuiteStartPayload ,
22062243 ) : Promise < void > => {
2244+ phaseTrackers ?. get ( payload . testPath ) ?. recordSuiteStart ( payload ) ;
22072245 await Promise . all (
22082246 context . reporters . map ( ( reporter ) =>
22092247 ( reporter as Reporter ) . onTestSuiteStart ?.( payload ) ,
@@ -2214,6 +2252,7 @@ export const runBrowserController = async (
22142252 const handleTestSuiteResult = async (
22152253 payload : TestSuiteResultPayload ,
22162254 ) : Promise < void > => {
2255+ phaseTrackers ?. get ( payload . testPath ) ?. recordSuiteResult ( payload ) ;
22172256 await Promise . all (
22182257 context . reporters . map ( ( reporter ) =>
22192258 ( reporter as Reporter ) . onTestSuiteResult ?.( payload ) ,
@@ -2234,6 +2273,7 @@ export const runBrowserController = async (
22342273 const handleTestCaseStart = async (
22352274 payload : TestCaseStartPayload ,
22362275 ) : Promise < void > => {
2276+ phaseTrackers ?. get ( payload . testPath ) ?. recordCaseStart ( payload ) ;
22372277 await Promise . all (
22382278 context . reporters . map ( ( reporter ) =>
22392279 ( reporter as Reporter ) . onTestCaseStart ?.( payload ) ,
@@ -2243,6 +2283,7 @@ export const runBrowserController = async (
22432283
22442284 const handleTestCaseResult = async ( payload : TestResult ) : Promise < void > => {
22452285 caseResults . push ( payload ) ;
2286+ phaseTrackers ?. get ( payload . testPath ) ?. recordCaseResult ( payload ) ;
22462287 await Promise . all (
22472288 context . reporters . map ( ( reporter ) =>
22482289 ( reporter as Reporter ) . onTestCaseResult ?.( payload ) ,
@@ -2269,6 +2310,16 @@ export const runBrowserController = async (
22692310 context . snapshotManager . add ( payload . snapshotResult ) ;
22702311 }
22712312
2313+ if ( phaseTrackers ) {
2314+ const tracker = phaseTrackers . get ( payload . testPath ) ;
2315+ if ( tracker ) {
2316+ tracker . end ( ) ;
2317+ const events = tracker . getTraceEvents ( ) ;
2318+ if ( events ) onTraceEvents ?.( events ) ;
2319+ phaseTrackers . delete ( payload . testPath ) ;
2320+ }
2321+ }
2322+
22722323 if ( context . normalizedConfig . silent === 'passed-only' ) {
22732324 await flushBufferedLogsForTask ( {
22742325 taskId : payload . testId ,
0 commit comments