@samitouri / QOS-React-2 / commits / 79ddf5b574

[Flight] Track Timing Information (#31716)

Stacked on #31715. This adds profiling data for Server Components to the RSC stream (but doesn't yet use it for anything). This is on behind `enableProfilerTimer` which is on for Dev and Profiling builds. However, for now there's no Profiling build of Flight so in practice only in DEV. It's gated on `enableComponentPerformanceTrack` which is experimental only for now. We first emit a timeOrigin in the beginning of the stream. This provides us a relative time to emit timestamps against for cross environment transfer so that we can log it in terms of absolute times. Using this as a separate field allows the actual relative timestamps to be a bit more compact representation and preserves floating point precision. We emit a timestamp before emitting a Server Component which represents the start time of the Server Component. The end time is either when the next Server Component starts or when we finish the task. We omit the end time for simple tasks that are outlined without Server Components. By encoding this as part of the debugInfo stream, this information can be forwarded between Server to Server RSC.

Sebastian Markbåge committed Dec 10, 2024 at 20:46 UTC 79ddf5b574db085104d917c24a964cbd5b824e09
4 files changed +209 -38
packages/react-client/src/ReactFlightClient.js
+29
@@ -48,6 +48,8 @@ import {
48 enableFlightReadableStream,
49 enableOwnerStacks,
50 enableServerComponentLogs,
51 + enableProfilerTimer,
52 + enableComponentPerformanceTrack,
53 } from 'shared/ReactFeatureFlags';
54
55 import {
@@ -286,6 +288,7 @@ export type Response = {
288 _rowLength: number, // remaining bytes in the row. 0 indicates that we're looking for a newline.
289 _buffer: Array<Uint8Array>, // chunks received so far as part of this row
290 _tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from
291 + _timeOrigin: number, // Profiling-only
292 _debugRootOwner?: null | ReactComponentInfo, // DEV-only
293 _debugRootStack?: null | Error, // DEV-only
294 _debugRootTask?: null | ConsoleTask, // DEV-only
@@ -1585,6 +1588,9 @@ function ResponseInstance(
1588 this._rowLength = 0;
1589 this._buffer = [];
1590 this._tempRefs = temporaryReferences;
1591 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
1592 + this._timeOrigin = 0;
1593 + }
1594 if (__DEV__) {
1595 // TODO: The Flight Client can be used in a Client Environment too and we should really support
1596 // getting the owner there as well, but currently the owner of ReactComponentInfo is typed as only
@@ -2512,6 +2518,16 @@ function resolveDebugInfo(
2518 debugInfo;
2519 initializeFakeStack(response, componentInfoOrAsyncInfo);
2520 }
2521 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
2522 + if (typeof debugInfo.time === 'number') {
2523 + // Adjust the time to the current environment's time space.
2524 + // Since this might be a deduped object, we clone it to avoid
2525 + // applying the adjustment twice.
2526 + debugInfo = {
2527 + time: debugInfo.time + response._timeOrigin,
2528 + };
2529 + }
2530 + }
2531
2532 const chunk = getChunk(response, id);
2533 const chunkDebugInfo: ReactDebugInfo =
@@ -2792,6 +2808,19 @@ function processFullStringRow(
2808 resolveText(response, id, row);
2809 return;
2810 }
2811 + case 78 /* "N" */: {
2812 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
2813 + // Track the time origin for future debug info. We track it relative
2814 + // to the current environment's time space.
2815 + const timeOrigin: number = +row;
2816 + response._timeOrigin =
2817 + timeOrigin -
2818 + // $FlowFixMe[prop-missing]
2819 + performance.timeOrigin;
2820 + return;
2821 + }
2822 + // Fallthrough to share the error with Debug and Console entries.
2823 + }
2824 case 68 /* "D" */: {
2825 if (__DEV__) {
2826 const chunk: ResolvedModelChunk<
packages/react-client/src/__tests__/ReactFlight-test.js
+50 -6
@@ -125,6 +125,20 @@ let assertConsoleErrorDev;
125
126 describe('ReactFlight', () => {
127 beforeEach(() => {
128 + // Mock performance.now for timing tests
129 + let time = 10;
130 + const now = jest.fn().mockImplementation(() => {
131 + return time++;
132 + });
133 + Object.defineProperty(performance, 'timeOrigin', {
134 + value: time,
135 + configurable: true,
136 + });
137 + Object.defineProperty(performance, 'now', {
138 + value: now,
139 + configurable: true,
140 + });
141 +
142 jest.resetModules();
143 jest.mock('react', () => require('react/react.react-server'));
144 ReactServer = require('react');
@@ -274,6 +288,7 @@ describe('ReactFlight', () => {
288 });
289 });
290
291 + // @gate !__DEV__ || enableComponentPerformanceTrack
292 it('can render a Client Component using a module reference and render there', async () => {
293 function UserClient(props) {
294 return (
@@ -300,6 +315,7 @@ describe('ReactFlight', () => {
315 expect(getDebugInfo(greeting)).toEqual(
316 __DEV__
317 ? [
318 + {time: 11},
319 {
320 name: 'Greeting',
321 env: 'Server',
@@ -313,6 +329,7 @@ describe('ReactFlight', () => {
329 lastName: 'Smith',
330 },
331 },
332 + {time: 12},
333 ]
334 : undefined,
335 );
@@ -322,6 +339,7 @@ describe('ReactFlight', () => {
339 expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>);
340 });
341
342 + // @gate !__DEV__ || enableComponentPerformanceTrack
343 it('can render a shared forwardRef Component', async () => {
344 const Greeting = React.forwardRef(function Greeting(
345 {firstName, lastName},
@@ -343,6 +361,7 @@ describe('ReactFlight', () => {
361 expect(getDebugInfo(promise)).toEqual(
362 __DEV__
363 ? [
364 + {time: 11},
365 {
366 name: 'Greeting',
367 env: 'Server',
@@ -356,6 +375,7 @@ describe('ReactFlight', () => {
375 lastName: 'Smith',
376 },
377 },
378 + {time: 12},
379 ]
380 : undefined,
381 );
@@ -2659,6 +2679,7 @@ describe('ReactFlight', () => {
2679 );
2680 });
2681
2682 + // @gate !__DEV__ || enableComponentPerformanceTrack
2683 it('preserves debug info for server-to-server pass through', async () => {
2684 function ThirdPartyLazyComponent() {
2685 return <span>!</span>;
@@ -2705,6 +2726,7 @@ describe('ReactFlight', () => {
2726 expect(getDebugInfo(promise)).toEqual(
2727 __DEV__
2728 ? [
2729 + {time: 18},
2730 {
2731 name: 'ServerComponent',
2732 env: 'Server',
@@ -2717,15 +2739,18 @@ describe('ReactFlight', () => {
2739 transport: expect.arrayContaining([]),
2740 },
2741 },
2742 + {time: 19},
2743 ]
2744 : undefined,
2745 );
2746 const result = await promise;
2747 +
2748 const thirdPartyChildren = await result.props.children[1];
2749 // We expect the debug info to be transferred from the inner stream to the outer.
2750 expect(getDebugInfo(thirdPartyChildren[0])).toEqual(
2751 __DEV__
2752 ? [
2753 + {time: 13},
2754 {
2755 name: 'ThirdPartyComponent',
2756 env: 'third-party',
@@ -2736,12 +2761,15 @@ describe('ReactFlight', () => {
2761 : undefined,
2762 props: {},
2763 },
2764 + {time: 14},
2765 + {time: 21}, // This last one is when the promise resolved into the first party.
2766 ]
2767 : undefined,
2768 );
2769 expect(getDebugInfo(thirdPartyChildren[1])).toEqual(
2770 __DEV__
2771 ? [
2772 + {time: 15},
2773 {
2774 name: 'ThirdPartyLazyComponent',
2775 env: 'third-party',
@@ -2752,12 +2780,14 @@ describe('ReactFlight', () => {
2780 : undefined,
2781 props: {},
2782 },
2783 + {time: 16},
2784 ]
2785 : undefined,
2786 );
2787 expect(getDebugInfo(thirdPartyChildren[2])).toEqual(
2788 __DEV__
2789 ? [
2790 + {time: 11},
2791 {
2792 name: 'ThirdPartyFragmentComponent',
2793 env: 'third-party',
@@ -2768,6 +2798,7 @@ describe('ReactFlight', () => {
2798 : undefined,
2799 props: {},
2800 },
2801 + {time: 12},
2802 ]
2803 : undefined,
2804 );
@@ -2833,6 +2864,7 @@ describe('ReactFlight', () => {
2864 expect(getDebugInfo(promise)).toEqual(
2865 __DEV__
2866 ? [
2867 + {time: 14},
2868 {
2869 name: 'ServerComponent',
2870 env: 'Server',
@@ -2845,6 +2877,7 @@ describe('ReactFlight', () => {
2877 transport: expect.arrayContaining([]),
2878 },
2879 },
2880 + {time: 15},
2881 ]
2882 : undefined,
2883 );
@@ -2853,6 +2886,7 @@ describe('ReactFlight', () => {
2886 expect(getDebugInfo(thirdPartyFragment)).toEqual(
2887 __DEV__
2888 ? [
2889 + {time: 16},
2890 {
2891 name: 'Keyed',
2892 env: 'Server',
@@ -2865,6 +2899,7 @@ describe('ReactFlight', () => {
2899 children: {},
2900 },
2901 },
2902 + {time: 17},
2903 ]
2904 : undefined,
2905 );
@@ -2872,6 +2907,7 @@ describe('ReactFlight', () => {
2907 expect(getDebugInfo(thirdPartyFragment.props.children)).toEqual(
2908 __DEV__
2909 ? [
2910 + {time: 11},
2911 {
2912 name: 'ThirdPartyAsyncIterableComponent',
2913 env: 'third-party',
@@ -2882,6 +2918,7 @@ describe('ReactFlight', () => {
2918 : undefined,
2919 props: {},
2920 },
2921 + {time: 12},
2922 ]
2923 : undefined,
2924 );
@@ -3017,6 +3054,7 @@ describe('ReactFlight', () => {
3054 }
3055 });
3056
3057 + // @gate !__DEV__ || enableComponentPerformanceTrack
3058 it('can change the environment name inside a component', async () => {
3059 let env = 'A';
3060 function Component(props) {
@@ -3041,6 +3079,7 @@ describe('ReactFlight', () => {
3079 expect(getDebugInfo(greeting)).toEqual(
3080 __DEV__
3081 ? [
3082 + {time: 11},
3083 {
3084 name: 'Component',
3085 env: 'A',
@@ -3054,6 +3093,7 @@ describe('ReactFlight', () => {
3093 {
3094 env: 'B',
3095 },
3096 + {time: 12},
3097 ]
3098 : undefined,
3099 );
@@ -3205,6 +3245,7 @@ describe('ReactFlight', () => {
3245 );
3246 });
3247
3248 + // @gate !__DEV__ || enableComponentPerformanceTrack
3249 it('uses the server component debug info as the element owner in DEV', async () => {
3250 function Container({children}) {
3251 return children;
@@ -3244,7 +3285,9 @@ describe('ReactFlight', () => {
3285 },
3286 };
3287 expect(getDebugInfo(greeting)).toEqual([
3288 + {time: 11},
3289 greetInfo,
3290 + {time: 12},
3291 {
3292 name: 'Container',
3293 env: 'Server',
@@ -3262,10 +3305,11 @@ describe('ReactFlight', () => {
3305 }),
3306 },
3307 },
3308 + {time: 13},
3309 ]);
3310 // The owner that created the span was the outer server component.
3311 // We expect the debug info to be referentially equal to the owner.
3268 - expect(greeting._owner).toBe(greeting._debugInfo[0]);
3312 + expect(greeting._owner).toBe(greeting._debugInfo[1]);
3313 } else {
3314 expect(greeting._debugInfo).toBe(undefined);
3315 expect(greeting._owner).toBe(undefined);
@@ -3531,7 +3575,7 @@ describe('ReactFlight', () => {
3575 expect(caughtError.digest).toBe('digest("my-error")');
3576 });
3577
3534 - // @gate __DEV__
3578 + // @gate __DEV__ && enableComponentPerformanceTrack
3579 it('can render deep but cut off JSX in debug info', async () => {
3580 function createDeepJSX(n) {
3581 if (n <= 0) {
@@ -3555,7 +3599,7 @@ describe('ReactFlight', () => {
3599 await act(async () => {
3600 const rootModel = await ReactNoopFlightClient.read(transport);
3601 const root = rootModel.root;
3558 - const children = root._debugInfo[0].props.children;
3602 + const children = root._debugInfo[1].props.children;
3603 expect(children.type).toBe('div');
3604 expect(children.props.children.type).toBe('div');
3605 ReactNoop.render(root);
@@ -3564,7 +3608,7 @@ describe('ReactFlight', () => {
3608 expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
3609 });
3610
3567 - // @gate __DEV__
3611 + // @gate __DEV__ && enableComponentPerformanceTrack
3612 it('can render deep but cut off Map/Set in debug info', async () => {
3613 function createDeepMap(n) {
3614 if (n <= 0) {
@@ -3603,8 +3647,8 @@ describe('ReactFlight', () => {
3647
3648 await act(async () => {
3649 const rootModel = await ReactNoopFlightClient.read(transport);
3606 - const set = rootModel.set._debugInfo[0].props.set;
3607 - const map = rootModel.map._debugInfo[0].props.map;
3650 + const set = rootModel.set._debugInfo[1].props.set;
3651 + const map = rootModel.map._debugInfo[1].props.map;
3652 expect(set instanceof Set).toBe(true);
3653 expect(set.size).toBe(1);
3654 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+22 -4
@@ -49,6 +49,20 @@ function normalizeCodeLocInfo(str) {
49
50 describe('ReactFlightDOMEdge', () => {
51 beforeEach(() => {
52 + // Mock performance.now for timing tests
53 + let time = 10;
54 + const now = jest.fn().mockImplementation(() => {
55 + return time++;
56 + });
57 + Object.defineProperty(performance, 'timeOrigin', {
58 + value: time,
59 + configurable: true,
60 + });
61 + Object.defineProperty(performance, 'now', {
62 + value: now,
63 + configurable: true,
64 + });
65 +
66 jest.resetModules();
67
68 reactServerAct = require('internal-test-utils').serverAct;
@@ -401,7 +415,7 @@ describe('ReactFlightDOMEdge', () => {
415
416 const serializedContent = await readResult(stream1);
417
404 - expect(serializedContent.length).toBeLessThan(425);
418 + expect(serializedContent.length).toBeLessThan(490);
419 expect(timesRendered).toBeLessThan(5);
420
421 const model = await ReactServerDOMClient.createFromReadableStream(stream2, {
@@ -472,7 +486,7 @@ describe('ReactFlightDOMEdge', () => {
486 const [stream1, stream2] = passThrough(stream).tee();
487
488 const serializedContent = await readResult(stream1);
475 - expect(serializedContent.length).toBeLessThan(__DEV__ ? 605 : 400);
489 + expect(serializedContent.length).toBeLessThan(__DEV__ ? 680 : 400);
490 expect(timesRendered).toBeLessThan(5);
491
492 const model = await serverAct(() =>
@@ -506,7 +520,7 @@ describe('ReactFlightDOMEdge', () => {
520 ),
521 );
522 const serializedContent = await readResult(stream);
509 - const expectedDebugInfoSize = __DEV__ ? 300 * 20 : 0;
523 + const expectedDebugInfoSize = __DEV__ ? 320 * 20 : 0;
524 expect(serializedContent.length).toBeLessThan(150 + expectedDebugInfoSize);
525 });
526
@@ -934,6 +948,7 @@ describe('ReactFlightDOMEdge', () => {
948 );
949 });
950
951 + // @gate !__DEV__ || enableComponentPerformanceTrack
952 it('supports async server component debug info as the element owner in DEV', async () => {
953 function Container({children}) {
954 return children;
@@ -989,16 +1004,19 @@ describe('ReactFlightDOMEdge', () => {
1004 owner: null,
1005 });
1006 expect(lazyWrapper._debugInfo).toEqual([
1007 + {time: 11},
1008 greetInfo,
1009 + {time: 12},
1010 expect.objectContaining({
1011 name: 'Container',
1012 env: 'Server',
1013 owner: greetInfo,
1014 }),
1015 + {time: 13},
1016 ]);
1017 // The owner that created the span was the outer server component.
1018 // We expect the debug info to be referentially equal to the owner.
1001 - expect(greeting._owner).toBe(lazyWrapper._debugInfo[0]);
1019 + expect(greeting._owner).toBe(lazyWrapper._debugInfo[1]);
1020 } else {
1021 expect(lazyWrapper._debugInfo).toBe(undefined);
1022 expect(greeting._owner).toBe(undefined);
packages/react-server/src/ReactFlightServer.js
+108 -28
@@ -20,6 +20,8 @@ import {
20 enableTaint,
21 enableServerComponentLogs,
22 enableOwnerStacks,
23 + enableProfilerTimer,
24 + enableComponentPerformanceTrack,
25 } from 'shared/ReactFeatureFlags';
26
27 import {enableFlightReadableStream} from 'shared/ReactFeatureFlags';
@@ -345,6 +347,7 @@ type Task = {
347 keyPath: null | string, // parent server component keys
348 implicitSlot: boolean, // true if the root server component of this sequence had a null key
349 thenableState: ThenableState | null,
350 + timed: boolean, // Profiling-only. Whether we need to track the completion time of this task.
351 environmentName: string, // DEV-only. Used to track if the environment for this task changed.
352 debugOwner: null | ReactComponentInfo, // DEV-only
353 debugStack: null | Error, // DEV-only
@@ -392,6 +395,8 @@ export type Request = {
395 onPostpone: (reason: string) => void,
396 onAllReady: () => void,
397 onFatalError: mixed => void,
398 + // Profiling-only
399 + timeOrigin: number,
400 // DEV-only
401 environmentName: () => string,
402 filterStackFrame: (url: string, functionName: string) => boolean,
@@ -517,6 +522,23 @@ function RequestInstance(
522 : filterStackFrame;
523 this.didWarnForKey = null;
524 }
525 +
526 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
527 + // We start by serializing the time origin. Any future timestamps will be
528 + // emitted relatively to this origin. Instead of using performance.timeOrigin
529 + // as this origin, we use the timestamp at the start of the request.
530 + // This avoids leaking unnecessary information like how long the server has
531 + // been running and allows for more compact representation of each timestamp.
532 + // The time origin is stored as an offset in the time space of this environment.
533 + const timeOrigin = (this.timeOrigin = performance.now());
534 + emitTimeOriginChunk(
535 + this,
536 + timeOrigin +
537 + // $FlowFixMe[prop-missing]
538 + performance.timeOrigin,
539 + );
540 + }
541 +
542 const rootTask = createTask(
543 this,
544 model,
@@ -690,6 +712,11 @@ function serializeThenable(
712 },
713 );
714
715 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
716 + // If this is async we need to time when this task finishes.
717 + newTask.timed = true;
718 + }
719 +
720 return newTask.id;
721 }
722
@@ -1240,6 +1267,13 @@ function renderFunctionComponent<Props>(
1267 // being no references to this as an owner.
1268
1269 outlineComponentInfo(request, componentDebugInfo);
1270 +
1271 + // Track when we started rendering this component.
1272 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
1273 + task.timed = true;
1274 + emitTimingChunk(request, componentDebugID, performance.now());
1275 + }
1276 +
1277 emitDebugChunk(request, componentDebugID, componentDebugInfo);
1278
1279 // We've emitted the latest environment for this task so we track that.
@@ -1769,6 +1803,10 @@ function renderElement(
1803 }
1804
1805 function pingTask(request: Request, task: Task): void {
1806 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
1807 + // If this was async we need to emit the time when it completes.
1808 + task.timed = true;
1809 + }
1810 const pingedTasks = request.pingedTasks;
1811 pingedTasks.push(task);
1812 if (pingedTasks.length === 1) {
@@ -1862,8 +1900,11 @@ function createTask(
1900 thenableState: null,
1901 }: Omit<
1902 Task,
1865 - 'environmentName' | 'debugOwner' | 'debugStack' | 'debugTask',
1903 + 'timed' | 'environmentName' | 'debugOwner' | 'debugStack' | 'debugTask',
1904 >): any);
1905 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
1906 + task.timed = false;
1907 + }
1908 if (__DEV__) {
1909 task.environmentName = request.environmentName();
1910 task.debugOwner = debugOwner;
@@ -3769,6 +3810,17 @@ function emitConsoleChunk(
3810 request.completedRegularChunks.push(processedChunk);
3811 }
3812
3813 +function emitTimeOriginChunk(request: Request, timeOrigin: number): void {
3814 + // We emit the time origin once. All ReactTimeInfo timestamps later in the stream
3815 + // are relative to this time origin. This allows for more compact number encoding
3816 + // and lower precision loss.
3817 + request.pendingChunks++;
3818 + const row = ':N' + timeOrigin + '\n';
3819 + const processedChunk = stringToChunk(row);
3820 + // TODO: Move to its own priority queue.
3821 + request.completedRegularChunks.push(processedChunk);
3822 +}
3823 +
3824 function forwardDebugInfo(
3825 request: Request,
3826 id: number,
@@ -3776,16 +3828,38 @@ function forwardDebugInfo(
3828 ) {
3829 for (let i = 0; i < debugInfo.length; i++) {
3830 request.pendingChunks++;
3779 - if (typeof debugInfo[i].name === 'string') {
3780 - // We outline this model eagerly so that we can refer to by reference as an owner.
3781 - // If we had a smarter way to dedupe we might not have to do this if there ends up
3782 - // being no references to this as an owner.
3783 - outlineComponentInfo(request, (debugInfo[i]: any));
3831 + if (typeof debugInfo[i].time === 'number') {
3832 + // When forwarding time we need to ensure to convert it to the time space of the payload.
3833 + emitTimingChunk(request, id, debugInfo[i].time);
3834 + } else {
3835 + if (typeof debugInfo[i].name === 'string') {
3836 + // We outline this model eagerly so that we can refer to by reference as an owner.
3837 + // If we had a smarter way to dedupe we might not have to do this if there ends up
3838 + // being no references to this as an owner.
3839 + outlineComponentInfo(request, (debugInfo[i]: any));
3840 + }
3841 + emitDebugChunk(request, id, debugInfo[i]);
3842 }
3785 - emitDebugChunk(request, id, debugInfo[i]);
3843 }
3844 }
3845
3846 +function emitTimingChunk(
3847 + request: Request,
3848 + id: number,
3849 + timestamp: number,
3850 +): void {
3851 + if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
3852 + return;
3853 + }
3854 + request.pendingChunks++;
3855 + const relativeTimestamp = timestamp - request.timeOrigin;
3856 + const row =
3857 + serializeRowHeader('D', id) + '{"time":' + relativeTimestamp + '}\n';
3858 + const processedChunk = stringToChunk(row);
3859 + // TODO: Move to its own priority queue.
3860 + request.completedRegularChunks.push(processedChunk);
3861 +}
3862 +
3863 function emitChunk(
3864 request: Request,
3865 task: Task,
@@ -3877,6 +3951,11 @@ function emitChunk(
3951 }
3952
3953 function erroredTask(request: Request, task: Task, error: mixed): void {
3954 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
3955 + if (task.timed) {
3956 + emitTimingChunk(request, task.id, performance.now());
3957 + }
3958 + }
3959 request.abortableTasks.delete(task);
3960 task.status = ERRORED;
3961 if (
@@ -3939,21 +4018,27 @@ function retryTask(request: Request, task: Task): void {
4018 task.keyPath = null;
4019 task.implicitSlot = false;
4020
4021 + if (__DEV__) {
4022 + const currentEnv = (0, request.environmentName)();
4023 + if (currentEnv !== task.environmentName) {
4024 + request.pendingChunks++;
4025 + // The environment changed since we last emitted any debug information for this
4026 + // task. We emit an entry that just includes the environment name change.
4027 + emitDebugChunk(request, task.id, {env: currentEnv});
4028 + }
4029 + }
4030 + // We've finished rendering. Log the end time.
4031 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
4032 + if (task.timed) {
4033 + emitTimingChunk(request, task.id, performance.now());
4034 + }
4035 + }
4036 +
4037 if (typeof resolvedModel === 'object' && resolvedModel !== null) {
4038 // We're not in a contextual place here so we can refer to this object by this ID for
4039 // any future references.
4040 request.writtenObjects.set(resolvedModel, serializeByValueID(task.id));
4041
3947 - if (__DEV__) {
3948 - const currentEnv = (0, request.environmentName)();
3949 - if (currentEnv !== task.environmentName) {
3950 - request.pendingChunks++;
3951 - // The environment changed since we last emitted any debug information for this
3952 - // task. We emit an entry that just includes the environment name change.
3953 - emitDebugChunk(request, task.id, {env: currentEnv});
3954 - }
3955 - }
3956 -
4042 // Object might contain unresolved values like additional elements.
4043 // This is simulating what the JSON loop would do if this was part of it.
4044 emitChunk(request, task, resolvedModel);
@@ -3962,17 +4047,6 @@ function retryTask(request: Request, task: Task): void {
4047 // We don't need to escape it again so it's not passed the toJSON replacer.
4048 // $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
4049 const json: string = stringify(resolvedModel);
3965 -
3966 - if (__DEV__) {
3967 - const currentEnv = (0, request.environmentName)();
3968 - if (currentEnv !== task.environmentName) {
3969 - request.pendingChunks++;
3970 - // The environment changed since we last emitted any debug information for this
3971 - // task. We emit an entry that just includes the environment name change.
3972 - emitDebugChunk(request, task.id, {env: currentEnv});
3973 - }
3974 - }
3975 -
4050 emitModelChunk(request, task.id, json);
4051 }
4052
@@ -4082,6 +4156,12 @@ function abortTask(task: Task, request: Request, errorId: number): void {
4156 return;
4157 }
4158 task.status = ABORTED;
4159 + // Track when we aborted this task as its end time.
4160 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
4161 + if (task.timed) {
4162 + emitTimingChunk(request, task.id, performance.now());
4163 + }
4164 + }
4165 // Instead of emitting an error per task.id, we emit a model that only
4166 // has a single value referencing the error.
4167 const ref = serializeByValueID(errorId);