@samitouri / QOS-React / commits / cd4e4d7599

Use console.timeStamp instead of performance.measure in Component Performance Track (#32736)

This is a new extension that Chrome added to the existing `console.timeStamp` similar to the extensions added to `performance.measure`. This one should be significantly faster because it doesn't have the extra object indirection, it doesn't return a `PerformanceMeasure` entry and doesn't register itself with the global system of entries. I also use `performance.measure` in DEV for errors since we can attach the error to the `properties` extension which doesn't exist for `console.timeStamp`. A downside of using this API is that there's no programmatic API for the site itself to collect its own logs from React. Which the previous allowed us to use the standard `performance.getEntries()` for. The recommendation instead will be for the site to patch `console.timeStamp` if it wants to collect measurements from React just like you're recommended to patch `console.error` or `fetch` or whatever to collect other instrumentation metrics. This extension works in Chrome canary but it doesn't yet work fully in Chrome stable. We might want to wait until it has propagated to Chrome to stable. It should be in Chrome 136.

Sebastian Markbåge committed Apr 29, 2025 at 21:40 UTC cd4e4d759975916a503d14abe46052f9f5826048
3 files changed +438 -296
packages/react-client/src/ReactFlightPerformanceTrack.js
+63 -60
@@ -7,52 +7,35 @@
7 * @flow
8 */
9
10 +/* eslint-disable react-internal/no-production-logging */
11 +
12 import type {ReactComponentInfo} from 'shared/ReactTypes';
13
14 import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
15
16 const supportsUserTiming =
17 enableProfilerTimer &&
16 - typeof performance !== 'undefined' &&
17 - // $FlowFixMe[method-unbinding]
18 - typeof performance.measure === 'function';
18 + typeof console !== 'undefined' &&
19 + typeof console.timeStamp === 'function';
20
21 const COMPONENTS_TRACK = 'Server Components ⚛';
22
22 -const componentsTrackMarker = {
23 - startTime: 0.001,
24 - detail: {
25 - devtools: {
26 - color: 'primary-light',
27 - track: 'Primary',
28 - trackGroup: COMPONENTS_TRACK,
29 - },
30 - },
31 -};
32 -
23 export function markAllTracksInOrder() {
24 if (supportsUserTiming) {
25 // Ensure we create the Server Component track groups earlier than the Client Scheduler
26 // and Client Components. We can always add the 0 time slot even if it's in the past.
27 // That's still considered for ordering.
38 - performance.mark('Server Components Track', componentsTrackMarker);
28 + console.timeStamp(
29 + 'Server Components Track',
30 + 0.001,
31 + 0.001,
32 + 'Primary',
33 + COMPONENTS_TRACK,
34 + 'primary-light',
35 + );
36 }
37 }
38
42 -// Reused to avoid thrashing the GC.
43 -const reusableComponentDevToolDetails = {
44 - color: 'primary',
45 - track: '',
46 - trackGroup: COMPONENTS_TRACK,
47 -};
48 -const reusableComponentOptions = {
49 - start: -0,
50 - end: -0,
51 - detail: {
52 - devtools: reusableComponentDevToolDetails,
53 - },
54 -};
55 -
39 const trackNames = [
40 'Primary',
41 'Parallel',
@@ -79,7 +62,7 @@ export function logComponentRender(
62 const name = componentInfo.name;
63 const isPrimaryEnv = env === rootEnv;
64 const selfTime = endTime - startTime;
82 - reusableComponentDevToolDetails.color =
65 + const color =
66 selfTime < 0.5
67 ? isPrimaryEnv
68 ? 'primary-light'
@@ -93,12 +76,16 @@ export function logComponentRender(
76 ? 'primary-dark'
77 : 'secondary-dark'
78 : 'error';
96 - reusableComponentDevToolDetails.track = trackNames[trackIdx];
97 - reusableComponentOptions.start = startTime < 0 ? 0 : startTime;
98 - reusableComponentOptions.end = childrenEndTime;
79 const entryName =
80 isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
101 - performance.measure(entryName, reusableComponentOptions);
81 + console.timeStamp(
82 + entryName,
83 + startTime < 0 ? 0 : startTime,
84 + childrenEndTime,
85 + trackNames[trackIdx],
86 + COMPONENTS_TRACK,
87 + color,
88 + );
89 }
90 }
91
@@ -112,8 +99,17 @@ export function logComponentErrored(
99 error: mixed,
100 ): void {
101 if (supportsUserTiming) {
115 - const properties = [];
116 - if (__DEV__) {
102 + const env = componentInfo.env;
103 + const name = componentInfo.name;
104 + const isPrimaryEnv = env === rootEnv;
105 + const entryName =
106 + isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
107 + if (
108 + __DEV__ &&
109 + typeof performance !== 'undefined' &&
110 + // $FlowFixMe[method-unbinding]
111 + typeof performance.measure === 'function'
112 + ) {
113 const message =
114 typeof error === 'object' &&
115 error !== null &&
@@ -122,26 +118,30 @@ export function logComponentErrored(
118 String(error.message)
119 : // eslint-disable-next-line react-internal/safe-string-coercion
120 String(error);
125 - properties.push(['Error', message]);
126 - }
127 - const env = componentInfo.env;
128 - const name = componentInfo.name;
129 - const isPrimaryEnv = env === rootEnv;
130 - const entryName =
131 - isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
132 - performance.measure(entryName, {
133 - start: startTime < 0 ? 0 : startTime,
134 - end: childrenEndTime,
135 - detail: {
136 - devtools: {
137 - color: 'error',
138 - track: trackNames[trackIdx],
139 - trackGroup: COMPONENTS_TRACK,
140 - tooltipText: entryName + ' Errored',
141 - properties,
121 + const properties = [['Error', message]];
122 + performance.measure(entryName, {
123 + start: startTime < 0 ? 0 : startTime,
124 + end: childrenEndTime,
125 + detail: {
126 + devtools: {
127 + color: 'error',
128 + track: trackNames[trackIdx],
129 + trackGroup: COMPONENTS_TRACK,
130 + tooltipText: entryName + ' Errored',
131 + properties,
132 + },
133 },
143 - },
144 - });
134 + });
135 + } else {
136 + console.timeStamp(
137 + entryName,
138 + startTime < 0 ? 0 : startTime,
139 + childrenEndTime,
140 + trackNames[trackIdx],
141 + COMPONENTS_TRACK,
142 + 'error',
143 + );
144 + }
145 }
146 }
147
@@ -153,11 +153,14 @@ export function logDedupedComponentRender(
153 ): void {
154 if (supportsUserTiming && endTime >= 0 && trackIdx < 10) {
155 const name = componentInfo.name;
156 - reusableComponentDevToolDetails.color = 'tertiary-light';
157 - reusableComponentDevToolDetails.track = trackNames[trackIdx];
158 - reusableComponentOptions.start = startTime < 0 ? 0 : startTime;
159 - reusableComponentOptions.end = endTime;
156 const entryName = name + ' [deduped]';
161 - performance.measure(entryName, reusableComponentOptions);
157 + console.timeStamp(
158 + entryName,
159 + startTime < 0 ? 0 : startTime,
160 + endTime,
161 + trackNames[trackIdx],
162 + COMPONENTS_TRACK,
163 + 'tertiary-light',
164 + );
165 }
166 }
packages/react-reconciler/src/ReactFiberPerformanceTrack.js
+335 -236
@@ -7,6 +7,8 @@
7 * @flow
8 */
9
10 +/* eslint-disable react-internal/no-production-logging */
11 +
12 import type {Fiber} from './ReactInternalTypes';
13
14 import type {Lanes} from './ReactFiberLane';
@@ -28,27 +30,14 @@ import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
30
31 const supportsUserTiming =
32 enableProfilerTimer &&
31 - typeof performance !== 'undefined' &&
32 - // $FlowFixMe[method-unbinding]
33 - typeof performance.measure === 'function';
33 + typeof console !== 'undefined' &&
34 + typeof console.timeStamp === 'function';
35
36 const COMPONENTS_TRACK = 'Components ⚛';
36 -
37 -// Reused to avoid thrashing the GC.
38 -const reusableComponentDevToolDetails = {
39 - color: 'primary',
40 - track: COMPONENTS_TRACK,
41 -};
42 -const reusableComponentOptions = {
43 - start: -0,
44 - end: -0,
45 - detail: {
46 - devtools: reusableComponentDevToolDetails,
47 - },
48 -};
49 -
37 const LANES_TRACK_GROUP = 'Scheduler ⚛';
38
39 +let currentTrack: string = 'Blocking'; // Lane
40 +
41 const reusableLaneDevToolDetails = {
42 color: 'primary',
43 track: 'Blocking', // Lane
@@ -63,63 +52,47 @@ const reusableLaneOptions = {
52 };
53
54 export function setCurrentTrackFromLanes(lanes: Lanes): void {
66 - reusableLaneDevToolDetails.track = getGroupNameOfHighestPriorityLane(lanes);
55 + currentTrack = getGroupNameOfHighestPriorityLane(lanes);
56 }
57
69 -const blockingLaneMarker = {
70 - startTime: 0.003,
71 - detail: {
72 - devtools: {
73 - color: 'primary-light',
74 - track: 'Blocking',
75 - trackGroup: LANES_TRACK_GROUP,
76 - },
77 - },
78 -};
79 -
80 -const transitionLaneMarker = {
81 - startTime: 0.003,
82 - detail: {
83 - devtools: {
84 - color: 'primary-light',
85 - track: 'Transition',
86 - trackGroup: LANES_TRACK_GROUP,
87 - },
88 - },
89 -};
90 -
91 -const suspenseLaneMarker = {
92 - startTime: 0.003,
93 - detail: {
94 - devtools: {
95 - color: 'primary-light',
96 - track: 'Suspense',
97 - trackGroup: LANES_TRACK_GROUP,
98 - },
99 - },
100 -};
101 -
102 -const idleLaneMarker = {
103 - startTime: 0.003,
104 - detail: {
105 - devtools: {
106 - color: 'primary-light',
107 - track: 'Idle',
108 - trackGroup: LANES_TRACK_GROUP,
109 - },
110 - },
111 -};
112 -
58 export function markAllLanesInOrder() {
59 if (supportsUserTiming) {
60 // Ensure we create all tracks in priority order. Currently performance.mark() are in
61 // first insertion order but performance.measure() are in the reverse order. We can
62 // always add the 0 time slot even if it's in the past. That's still considered for
63 // ordering.
119 - performance.mark('Blocking Track', blockingLaneMarker);
120 - performance.mark('Transition Track', transitionLaneMarker);
121 - performance.mark('Suspense Track', suspenseLaneMarker);
122 - performance.mark('Idle Track', idleLaneMarker);
64 + console.timeStamp(
65 + 'Blocking Track',
66 + 0.003,
67 + 0.003,
68 + 'Blocking',
69 + LANES_TRACK_GROUP,
70 + 'primary-light',
71 + );
72 + console.timeStamp(
73 + 'Transition Track',
74 + 0.003,
75 + 0.003,
76 + 'Transition',
77 + LANES_TRACK_GROUP,
78 + 'primary-light',
79 + );
80 + console.timeStamp(
81 + 'Suspense Track',
82 + 0.003,
83 + 0.003,
84 + 'Suspense',
85 + LANES_TRACK_GROUP,
86 + 'primary-light',
87 + );
88 + console.timeStamp(
89 + 'Idle Track',
90 + 0.003,
91 + 0.003,
92 + 'Idle',
93 + LANES_TRACK_GROUP,
94 + 'primary-light',
95 + );
96 }
97 }
98
@@ -130,10 +103,14 @@ function logComponentTrigger(
103 trigger: string,
104 ) {
105 if (supportsUserTiming) {
133 - reusableComponentDevToolDetails.color = 'warning';
134 - reusableComponentOptions.start = startTime;
135 - reusableComponentOptions.end = endTime;
136 - performance.measure(trigger, reusableComponentOptions);
106 + console.timeStamp(
107 + trigger,
108 + startTime,
109 + endTime,
110 + COMPONENTS_TRACK,
111 + undefined,
112 + 'warning',
113 + );
114 }
115 }
116
@@ -187,7 +164,7 @@ export function logComponentRender(
164 selfTime -= (child.actualDuration: any);
165 }
166 }
190 - reusableComponentDevToolDetails.color =
167 + const color =
168 selfTime < 0.5
169 ? wasHydrated
170 ? 'tertiary-light'
@@ -201,9 +178,14 @@ export function logComponentRender(
178 ? 'tertiary-dark'
179 : 'primary-dark'
180 : 'error';
204 - reusableComponentOptions.start = startTime;
205 - reusableComponentOptions.end = endTime;
206 - performance.measure(name, reusableComponentOptions);
181 + console.timeStamp(
182 + name,
183 + startTime,
184 + endTime,
185 + COMPONENTS_TRACK,
186 + undefined,
187 + color,
188 + );
189 }
190 }
191
@@ -219,8 +201,13 @@ export function logComponentErrored(
201 // Skip
202 return;
203 }
222 - const properties = [];
223 - if (__DEV__) {
204 + if (
205 + __DEV__ &&
206 + typeof performance !== 'undefined' &&
207 + // $FlowFixMe[method-unbinding]
208 + typeof performance.measure === 'function'
209 + ) {
210 + const properties = [];
211 for (let i = 0; i < errors.length; i++) {
212 const capturedValue = errors[i];
213 const error = capturedValue.value;
@@ -234,22 +221,31 @@ export function logComponentErrored(
221 String(error);
222 properties.push(['Error', message]);
223 }
237 - }
238 - performance.measure(name, {
239 - start: startTime,
240 - end: endTime,
241 - detail: {
242 - devtools: {
243 - color: 'error',
244 - track: COMPONENTS_TRACK,
245 - tooltipText:
246 - fiber.tag === SuspenseComponent
247 - ? 'Hydration failed'
248 - : 'Error boundary caught an error',
249 - properties,
224 + performance.measure(name, {
225 + start: startTime,
226 + end: endTime,
227 + detail: {
228 + devtools: {
229 + color: 'error',
230 + track: COMPONENTS_TRACK,
231 + tooltipText:
232 + fiber.tag === SuspenseComponent
233 + ? 'Hydration failed'
234 + : 'Error boundary caught an error',
235 + properties,
236 + },
237 },
251 - },
252 - });
238 + });
239 + } else {
240 + console.timeStamp(
241 + name,
242 + startTime,
243 + endTime,
244 + COMPONENTS_TRACK,
245 + undefined,
246 + 'error',
247 + );
248 + }
249 }
250 }
251
@@ -265,8 +261,13 @@ function logComponentEffectErrored(
261 // Skip
262 return;
263 }
268 - const properties = [];
269 - if (__DEV__) {
264 + if (
265 + __DEV__ &&
266 + typeof performance !== 'undefined' &&
267 + // $FlowFixMe[method-unbinding]
268 + typeof performance.measure === 'function'
269 + ) {
270 + const properties = [];
271 for (let i = 0; i < errors.length; i++) {
272 const capturedValue = errors[i];
273 const error = capturedValue.value;
@@ -280,19 +281,28 @@ function logComponentEffectErrored(
281 String(error);
282 properties.push(['Error', message]);
283 }
283 - }
284 - performance.measure(name, {
285 - start: startTime,
286 - end: endTime,
287 - detail: {
288 - devtools: {
289 - color: 'error',
290 - track: COMPONENTS_TRACK,
291 - tooltipText: 'A lifecycle or effect errored',
292 - properties,
284 + performance.measure(name, {
285 + start: startTime,
286 + end: endTime,
287 + detail: {
288 + devtools: {
289 + color: 'error',
290 + track: COMPONENTS_TRACK,
291 + tooltipText: 'A lifecycle or effect errored',
292 + properties,
293 + },
294 },
294 - },
295 - });
295 + });
296 + } else {
297 + console.timeStamp(
298 + name,
299 + startTime,
300 + endTime,
301 + COMPONENTS_TRACK,
302 + undefined,
303 + 'error',
304 + );
305 + }
306 }
307 }
308
@@ -313,7 +323,7 @@ export function logComponentEffect(
323 return;
324 }
325 if (supportsUserTiming) {
316 - reusableComponentDevToolDetails.color =
326 + const color =
327 selfTime < 1
328 ? 'secondary-light'
329 : selfTime < 100
@@ -321,9 +331,14 @@ export function logComponentEffect(
331 : selfTime < 500
332 ? 'secondary-dark'
333 : 'error';
324 - reusableComponentOptions.start = startTime;
325 - reusableComponentOptions.end = endTime;
326 - performance.measure(name, reusableComponentOptions);
334 + console.timeStamp(
335 + name,
336 + startTime,
337 + endTime,
338 + COMPONENTS_TRACK,
339 + undefined,
340 + color,
341 + );
342 }
343 }
344
@@ -335,7 +350,7 @@ export function logYieldTime(startTime: number, endTime: number): void {
350 return;
351 }
352 // Being blocked on CPU is potentially bad so we color it by how long it took.
338 - reusableComponentDevToolDetails.color =
353 + const color =
354 yieldDuration < 5
355 ? 'primary-light'
356 : yieldDuration < 10
@@ -343,13 +358,18 @@ export function logYieldTime(startTime: number, endTime: number): void {
358 : yieldDuration < 100
359 ? 'primary-dark'
360 : 'error';
346 - reusableComponentOptions.start = startTime;
347 - reusableComponentOptions.end = endTime;
361 // This get logged in the components track if we don't commit which leaves them
362 // hanging by themselves without context. It's a useful indicator for why something
363 // might be starving this render though.
364 // TODO: Considering adding these to a queue and only logging them if we commit.
352 - performance.measure('Blocked', reusableComponentOptions);
365 + console.timeStamp(
366 + 'Blocked',
367 + startTime,
368 + endTime,
369 + COMPONENTS_TRACK,
370 + undefined,
371 + color,
372 + );
373 }
374 }
375
@@ -359,10 +379,14 @@ export function logSuspendedYieldTime(
379 suspendedFiber: Fiber,
380 ): void {
381 if (supportsUserTiming) {
362 - reusableComponentDevToolDetails.color = 'primary-light';
363 - reusableComponentOptions.start = startTime;
364 - reusableComponentOptions.end = endTime;
365 - performance.measure('Suspended', reusableComponentOptions);
382 + console.timeStamp(
383 + 'Suspended',
384 + startTime,
385 + endTime,
386 + COMPONENTS_TRACK,
387 + undefined,
388 + 'primary-light',
389 + );
390 }
391 }
392
@@ -372,10 +396,14 @@ export function logActionYieldTime(
396 suspendedFiber: Fiber,
397 ): void {
398 if (supportsUserTiming) {
375 - reusableComponentDevToolDetails.color = 'primary-light';
376 - reusableComponentOptions.start = startTime;
377 - reusableComponentOptions.end = endTime;
378 - performance.measure('Action', reusableComponentOptions);
399 + console.timeStamp(
400 + 'Action',
401 + startTime,
402 + endTime,
403 + COMPONENTS_TRACK,
404 + undefined,
405 + 'primary-light',
406 + );
407 }
408 }
409
@@ -389,38 +417,40 @@ export function logBlockingStart(
417 lanes: Lanes,
418 ): void {
419 if (supportsUserTiming) {
392 - reusableLaneDevToolDetails.track = 'Blocking';
420 + currentTrack = 'Blocking';
421 // If a blocking update was spawned within render or an effect, that's considered a cascading render.
422 // If you have a second blocking update within the same event, that suggests multiple flushSync or
423 // setState in a microtask which is also considered a cascade.
424 if (eventTime > 0 && eventType !== null) {
425 // Log the time from the event timeStamp until we called setState.
398 - reusableLaneDevToolDetails.color = eventIsRepeat
399 - ? 'secondary-light'
400 - : 'warning';
401 - reusableLaneOptions.start = eventTime;
402 - reusableLaneOptions.end = updateTime > 0 ? updateTime : renderStartTime;
403 - performance.measure(
426 + const color = eventIsRepeat ? 'secondary-light' : 'warning';
427 + console.timeStamp(
428 eventIsRepeat ? '' : 'Event: ' + eventType,
405 - reusableLaneOptions,
429 + eventTime,
430 + updateTime > 0 ? updateTime : renderStartTime,
431 + currentTrack,
432 + LANES_TRACK_GROUP,
433 + color,
434 );
435 }
436 if (updateTime > 0) {
437 // Log the time from when we called setState until we started rendering.
410 - reusableLaneDevToolDetails.color = isSpawnedUpdate
438 + const color = isSpawnedUpdate
439 ? 'error'
440 : includesOnlyHydrationOrOffscreenLanes(lanes)
441 ? 'tertiary-light'
442 : 'primary-light';
415 - reusableLaneOptions.start = updateTime;
416 - reusableLaneOptions.end = renderStartTime;
417 - performance.measure(
443 + console.timeStamp(
444 isSpawnedUpdate
445 ? 'Cascading Update'
446 : renderStartTime - updateTime > 5
447 ? 'Update Blocked'
448 : 'Update',
423 - reusableLaneOptions,
449 + updateTime,
450 + renderStartTime,
451 + currentTrack,
452 + LANES_TRACK_GROUP,
453 + color,
454 );
455 }
456 }
@@ -435,39 +465,45 @@ export function logTransitionStart(
465 renderStartTime: number,
466 ): void {
467 if (supportsUserTiming) {
438 - reusableLaneDevToolDetails.track = 'Transition';
468 + currentTrack = 'Transition';
469 if (eventTime > 0 && eventType !== null) {
470 // Log the time from the event timeStamp until we started a transition.
441 - reusableLaneDevToolDetails.color = eventIsRepeat
442 - ? 'secondary-light'
443 - : 'warning';
444 - reusableLaneOptions.start = eventTime;
445 - reusableLaneOptions.end =
471 + const color = eventIsRepeat ? 'secondary-light' : 'warning';
472 + const endTime =
473 startTime > 0
474 ? startTime
475 : updateTime > 0
476 ? updateTime
477 : renderStartTime;
451 - performance.measure(
478 + console.timeStamp(
479 eventIsRepeat ? '' : 'Event: ' + eventType,
453 - reusableLaneOptions,
480 + eventTime,
481 + endTime,
482 + currentTrack,
483 + LANES_TRACK_GROUP,
484 + color,
485 );
486 }
487 if (startTime > 0) {
488 // Log the time from when we started an async transition until we called setState or started rendering.
458 - reusableLaneDevToolDetails.color = 'primary-dark';
459 - reusableLaneOptions.start = startTime;
460 - reusableLaneOptions.end = updateTime > 0 ? updateTime : renderStartTime;
461 - performance.measure('Action', reusableLaneOptions);
489 + console.timeStamp(
490 + 'Action',
491 + startTime,
492 + updateTime > 0 ? updateTime : renderStartTime,
493 + currentTrack,
494 + LANES_TRACK_GROUP,
495 + 'primary-dark',
496 + );
497 }
498 if (updateTime > 0) {
499 // Log the time from when we called setState until we started rendering.
465 - reusableLaneDevToolDetails.color = 'primary-light';
466 - reusableLaneOptions.start = updateTime;
467 - reusableLaneOptions.end = renderStartTime;
468 - performance.measure(
500 + console.timeStamp(
501 renderStartTime - updateTime > 5 ? 'Update Blocked' : 'Update',
470 - reusableLaneOptions,
502 + updateTime,
503 + renderStartTime,
504 + currentTrack,
505 + LANES_TRACK_GROUP,
506 + 'primary-light',
507 );
508 }
509 }
@@ -479,20 +515,20 @@ export function logRenderPhase(
515 lanes: Lanes,
516 ): void {
517 if (supportsUserTiming) {
482 - reusableLaneDevToolDetails.color = includesOnlyHydrationOrOffscreenLanes(
483 - lanes,
484 - )
518 + const color = includesOnlyHydrationOrOffscreenLanes(lanes)
519 ? 'tertiary-dark'
520 : 'primary-dark';
487 - reusableLaneOptions.start = startTime;
488 - reusableLaneOptions.end = endTime;
489 - performance.measure(
521 + console.timeStamp(
522 includesOnlyOffscreenLanes(lanes)
523 ? 'Prepared'
524 : includesOnlyHydrationLanes(lanes)
525 ? 'Hydrated'
526 : 'Render',
495 - reusableLaneOptions,
527 + startTime,
528 + endTime,
529 + currentTrack,
530 + LANES_TRACK_GROUP,
531 + color,
532 );
533 }
534 }
@@ -503,20 +539,20 @@ export function logInterruptedRenderPhase(
539 lanes: Lanes,
540 ): void {
541 if (supportsUserTiming) {
506 - reusableLaneDevToolDetails.color = includesOnlyHydrationOrOffscreenLanes(
507 - lanes,
508 - )
542 + const color = includesOnlyHydrationOrOffscreenLanes(lanes)
543 ? 'tertiary-dark'
544 : 'primary-dark';
511 - reusableLaneOptions.start = startTime;
512 - reusableLaneOptions.end = endTime;
513 - performance.measure(
545 + console.timeStamp(
546 includesOnlyOffscreenLanes(lanes)
547 ? 'Prewarm'
548 : includesOnlyHydrationLanes(lanes)
549 ? 'Interrupted Hydration'
550 : 'Interrupted Render',
519 - reusableLaneOptions,
551 + startTime,
552 + endTime,
553 + currentTrack,
554 + LANES_TRACK_GROUP,
555 + color,
556 );
557 }
558 }
@@ -527,14 +563,17 @@ export function logSuspendedRenderPhase(
563 lanes: Lanes,
564 ): void {
565 if (supportsUserTiming) {
530 - reusableLaneDevToolDetails.color = includesOnlyHydrationOrOffscreenLanes(
531 - lanes,
532 - )
566 + const color = includesOnlyHydrationOrOffscreenLanes(lanes)
567 ? 'tertiary-dark'
568 : 'primary-dark';
535 - reusableLaneOptions.start = startTime;
536 - reusableLaneOptions.end = endTime;
537 - performance.measure('Prewarm', reusableLaneOptions);
569 + console.timeStamp(
570 + 'Prewarm',
571 + startTime,
572 + endTime,
573 + currentTrack,
574 + LANES_TRACK_GROUP,
575 + color,
576 + );
577 }
578 }
579
@@ -545,14 +584,17 @@ export function logSuspendedWithDelayPhase(
584 ): void {
585 // This means the render was suspended and cannot commit until it gets unblocked.
586 if (supportsUserTiming) {
548 - reusableLaneDevToolDetails.color = includesOnlyHydrationOrOffscreenLanes(
549 - lanes,
550 - )
587 + const color = includesOnlyHydrationOrOffscreenLanes(lanes)
588 ? 'tertiary-dark'
589 : 'primary-dark';
553 - reusableLaneOptions.start = startTime;
554 - reusableLaneOptions.end = endTime;
555 - performance.measure('Suspended', reusableLaneOptions);
590 + console.timeStamp(
591 + 'Suspended',
592 + startTime,
593 + endTime,
594 + currentTrack,
595 + LANES_TRACK_GROUP,
596 + color,
597 + );
598 }
599 }
600
@@ -564,8 +606,13 @@ export function logRecoveredRenderPhase(
606 hydrationFailed: boolean,
607 ): void {
608 if (supportsUserTiming) {
567 - const properties = [];
568 - if (__DEV__) {
609 + if (
610 + __DEV__ &&
611 + typeof performance !== 'undefined' &&
612 + // $FlowFixMe[method-unbinding]
613 + typeof performance.measure === 'function'
614 + ) {
615 + const properties = [];
616 for (let i = 0; i < recoverableErrors.length; i++) {
617 const capturedValue = recoverableErrors[i];
618 const error = capturedValue.value;
@@ -579,22 +626,31 @@ export function logRecoveredRenderPhase(
626 String(error);
627 properties.push(['Recoverable Error', message]);
628 }
582 - }
583 - performance.measure('Recovered', {
584 - start: startTime,
585 - end: endTime,
586 - detail: {
587 - devtools: {
588 - color: 'primary-dark',
589 - track: reusableLaneDevToolDetails.track,
590 - trackGroup: LANES_TRACK_GROUP,
591 - tooltipText: hydrationFailed
592 - ? 'Hydration Failed'
593 - : 'Recovered after Error',
594 - properties,
629 + performance.measure('Recovered', {
630 + start: startTime,
631 + end: endTime,
632 + detail: {
633 + devtools: {
634 + color: 'primary-dark',
635 + track: currentTrack,
636 + trackGroup: LANES_TRACK_GROUP,
637 + tooltipText: hydrationFailed
638 + ? 'Hydration Failed'
639 + : 'Recovered after Error',
640 + properties,
641 + },
642 },
596 - },
597 - });
643 + });
644 + } else {
645 + console.timeStamp(
646 + 'Recovered',
647 + startTime,
648 + endTime,
649 + currentTrack,
650 + LANES_TRACK_GROUP,
651 + 'error',
652 + );
653 + }
654 }
655 }
656
@@ -604,10 +660,14 @@ export function logErroredRenderPhase(
660 lanes: Lanes,
661 ): void {
662 if (supportsUserTiming) {
607 - reusableLaneDevToolDetails.color = 'error';
608 - reusableLaneOptions.start = startTime;
609 - reusableLaneOptions.end = endTime;
610 - performance.measure('Errored', reusableLaneOptions);
663 + console.timeStamp(
664 + 'Errored',
665 + startTime,
666 + endTime,
667 + currentTrack,
668 + LANES_TRACK_GROUP,
669 + 'error',
670 + );
671 }
672 }
673
@@ -616,10 +676,14 @@ export function logInconsistentRender(
676 endTime: number,
677 ): void {
678 if (supportsUserTiming) {
619 - reusableLaneDevToolDetails.color = 'error';
620 - reusableLaneOptions.start = startTime;
621 - reusableLaneOptions.end = endTime;
622 - performance.measure('Teared Render', reusableLaneOptions);
679 + console.timeStamp(
680 + 'Teared Render',
681 + startTime,
682 + endTime,
683 + currentTrack,
684 + LANES_TRACK_GROUP,
685 + 'error',
686 + );
687 }
688 }
689
@@ -629,10 +693,14 @@ export function logSuspenseThrottlePhase(
693 ): void {
694 // This was inside a throttled Suspense boundary commit.
695 if (supportsUserTiming) {
632 - reusableLaneDevToolDetails.color = 'secondary-light';
633 - reusableLaneOptions.start = startTime;
634 - reusableLaneOptions.end = endTime;
635 - performance.measure('Throttled', reusableLaneOptions);
696 + console.timeStamp(
697 + 'Throttled',
698 + startTime,
699 + endTime,
700 + currentTrack,
701 + LANES_TRACK_GROUP,
702 + 'secondary-light',
703 + );
704 }
705 }
706
@@ -642,12 +710,16 @@ export function logSuspendedCommitPhase(
710 ): void {
711 // This means the commit was suspended on CSS or images.
712 if (supportsUserTiming) {
645 - reusableLaneDevToolDetails.color = 'secondary-light';
646 - reusableLaneOptions.start = startTime;
647 - reusableLaneOptions.end = endTime;
713 // TODO: Include the exact reason and URLs of what resources suspended.
714 // TODO: This might also be Suspended while waiting on a View Transition.
650 - performance.measure('Suspended on CSS or Images', reusableLaneOptions);
715 + console.timeStamp(
716 + 'Suspended on CSS or Images',
717 + startTime,
718 + endTime,
719 + currentTrack,
720 + LANES_TRACK_GROUP,
721 + 'secondary-light',
722 + );
723 }
724 }
725
@@ -658,8 +730,13 @@ export function logCommitErrored(
730 passive: boolean,
731 ): void {
732 if (supportsUserTiming) {
661 - const properties = [];
662 - if (__DEV__) {
733 + if (
734 + __DEV__ &&
735 + typeof performance !== 'undefined' &&
736 + // $FlowFixMe[method-unbinding]
737 + typeof performance.measure === 'function'
738 + ) {
739 + const properties = [];
740 for (let i = 0; i < errors.length; i++) {
741 const capturedValue = errors[i];
742 const error = capturedValue.value;
@@ -673,20 +750,31 @@ export function logCommitErrored(
750 String(error);
751 properties.push(['Error', message]);
752 }
676 - }
677 - performance.measure('Errored', {
678 - start: startTime,
679 - end: endTime,
680 - detail: {
681 - devtools: {
682 - color: 'error',
683 - track: reusableLaneDevToolDetails.track,
684 - trackGroup: LANES_TRACK_GROUP,
685 - tooltipText: passive ? 'Remaining Effects Errored' : 'Commit Errored',
686 - properties,
753 + performance.measure('Errored', {
754 + start: startTime,
755 + end: endTime,
756 + detail: {
757 + devtools: {
758 + color: 'error',
759 + track: currentTrack,
760 + trackGroup: LANES_TRACK_GROUP,
761 + tooltipText: passive
762 + ? 'Remaining Effects Errored'
763 + : 'Commit Errored',
764 + properties,
765 + },
766 },
688 - },
689 - });
767 + });
768 + } else {
769 + console.timeStamp(
770 + 'Errored',
771 + startTime,
772 + endTime,
773 + currentTrack,
774 + LANES_TRACK_GROUP,
775 + 'error',
776 + );
777 + }
778 }
779 }
780
@@ -700,10 +788,16 @@ export function logCommitPhase(
788 return;
789 }
790 if (supportsUserTiming) {
703 - reusableLaneDevToolDetails.color = 'secondary-dark';
791 reusableLaneOptions.start = startTime;
792 reusableLaneOptions.end = endTime;
706 - performance.measure('Commit', reusableLaneOptions);
793 + console.timeStamp(
794 + 'Commit',
795 + startTime,
796 + endTime,
797 + currentTrack,
798 + LANES_TRACK_GROUP,
799 + 'secondary-dark',
800 + );
801 }
802 }
803
@@ -713,12 +807,13 @@ export function logPaintYieldPhase(
807 delayedUntilPaint: boolean,
808 ): void {
809 if (supportsUserTiming) {
716 - reusableLaneDevToolDetails.color = 'secondary-light';
717 - reusableLaneOptions.start = startTime;
718 - reusableLaneOptions.end = endTime;
719 - performance.measure(
810 + console.timeStamp(
811 delayedUntilPaint ? 'Waiting for Paint' : '',
721 - reusableLaneOptions,
812 + startTime,
813 + endTime,
814 + currentTrack,
815 + LANES_TRACK_GROUP,
816 + 'secondary-light',
817 );
818 }
819 }
@@ -733,9 +828,13 @@ export function logPassiveCommitPhase(
828 return;
829 }
830 if (supportsUserTiming) {
736 - reusableLaneDevToolDetails.color = 'secondary-dark';
737 - reusableLaneOptions.start = startTime;
738 - reusableLaneOptions.end = endTime;
739 - performance.measure('Remaining Effects', reusableLaneOptions);
831 + console.timeStamp(
832 + 'Remaining Effects',
833 + startTime,
834 + endTime,
835 + currentTrack,
836 + LANES_TRACK_GROUP,
837 + 'secondary-dark',
838 + );
839 }
840 }
scripts/flow/environment.js
+40
@@ -33,6 +33,46 @@ declare interface ConsoleTask {
33 run<T>(f: () => T): T;
34 }
35
36 +declare var console: {
37 + assert(condition: mixed, ...data: Array<any>): void,
38 + clear(): void,
39 + count(label?: string): void,
40 + countReset(label?: string): void,
41 + debug(...data: Array<any>): void,
42 + dir(...data: Array<any>): void,
43 + dirxml(...data: Array<any>): void,
44 + error(...data: Array<any>): void,
45 + _exception(...data: Array<any>): void,
46 + group(...data: Array<any>): void,
47 + groupCollapsed(...data: Array<any>): void,
48 + groupEnd(): void,
49 + info(...data: Array<any>): void,
50 + log(...data: Array<any>): void,
51 + profile(name?: string): void,
52 + profileEnd(name?: string): void,
53 + table(
54 + tabularData:
55 + | {[key: string]: any, ...}
56 + | Array<{[key: string]: any, ...}>
57 + | Array<Array<any>>,
58 + ): void,
59 + time(label?: string): void,
60 + timeEnd(label: string): void,
61 + timeStamp(
62 + label?: string,
63 + start?: string | number,
64 + end?: string | number,
65 + trackName?: string,
66 + trackGroup?: string,
67 + color?: string,
68 + ): void,
69 + timeLog(label?: string, ...data?: Array<any>): void,
70 + trace(...data: Array<any>): void,
71 + warn(...data: Array<any>): void,
72 + createTask(label: string): ConsoleTask,
73 + ...
74 +};
75 +
76 type ScrollTimelineOptions = {
77 source: Element,
78 axis?: 'block' | 'inline' | 'x' | 'y',