main
js 649 lines 18.3 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 /* eslint-disable react-internal/no-production-logging */
11
12 import type {
13 ReactComponentInfo,
14 ReactIOInfo,
15 ReactAsyncInfo,
16 } from 'shared/ReactTypes';
17
18 import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
19
20 import {
21 addValueToProperties,
22 addObjectToProperties,
23 } from 'shared/ReactPerformanceTrackProperties';
24
25 import {getIODescription} from 'shared/ReactIODescription';
26
27 const supportsUserTiming =
28 enableProfilerTimer &&
29 typeof console !== 'undefined' &&
30 typeof console.timeStamp === 'function' &&
31 typeof performance !== 'undefined' &&
32 // $FlowFixMe[method-unbinding]
33 typeof performance.measure === 'function';
34
35 const IO_TRACK = 'Server Requests ⚛';
36 const COMPONENTS_TRACK = 'Server Components ⚛';
37
38 export function markAllTracksInOrder() {
39 if (supportsUserTiming) {
40 // Ensure we create the Server Component track groups earlier than the Client Scheduler
41 // and Client Components. We can always add the 0 time slot even if it's in the past.
42 // That's still considered for ordering.
43 console.timeStamp(
44 'Server Requests Track',
45 0.001,
46 0.001,
47 IO_TRACK,
48 undefined,
49 'primary-light',
50 );
51 console.timeStamp(
52 'Server Components Track',
53 0.001,
54 0.001,
55 'Primary',
56 COMPONENTS_TRACK,
57 'primary-light',
58 );
59 }
60 }
61
62 const trackNames = [
63 'Primary',
64 'Parallel',
65 'Parallel\u200b', // Padded with zero-width space to give each track a unique name.
66 'Parallel\u200b\u200b',
67 'Parallel\u200b\u200b\u200b',
68 'Parallel\u200b\u200b\u200b\u200b',
69 'Parallel\u200b\u200b\u200b\u200b\u200b',
70 'Parallel\u200b\u200b\u200b\u200b\u200b\u200b',
71 'Parallel\u200b\u200b\u200b\u200b\u200b\u200b\u200b',
72 'Parallel\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b',
73 ];
74
75 export function logComponentRender(
76 componentInfo: ReactComponentInfo,
77 trackIdx: number,
78 startTime: number,
79 endTime: number,
80 childrenEndTime: number,
81 rootEnv: string,
82 ): void {
83 if (supportsUserTiming && childrenEndTime >= 0 && trackIdx < 10) {
84 const env = componentInfo.env;
85 const name = componentInfo.name;
86 const isPrimaryEnv = env === rootEnv;
87 const selfTime = endTime - startTime;
88 const color =
89 selfTime < 0.5
90 ? isPrimaryEnv
91 ? 'primary-light'
92 : 'secondary-light'
93 : selfTime < 50
94 ? isPrimaryEnv
95 ? 'primary'
96 : 'secondary'
97 : selfTime < 500
98 ? isPrimaryEnv
99 ? 'primary-dark'
100 : 'secondary-dark'
101 : 'error';
102 const entryName =
103 isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
104 const debugTask = componentInfo.debugTask;
105 const measureName = '\u200b' + entryName;
106 if (__DEV__ && debugTask) {
107 const properties: Array<[string, string]> = [];
108 if (componentInfo.key != null) {
109 addValueToProperties('key', componentInfo.key, properties, 0, '');
110 }
111 if (componentInfo.props != null) {
112 addObjectToProperties(componentInfo.props, properties, 0, '');
113 }
114
115 debugTask.run(
116 // $FlowFixMe[method-unbinding]
117 performance.measure.bind(performance, measureName, {
118 start: startTime < 0 ? 0 : startTime,
119 end: childrenEndTime,
120 detail: {
121 devtools: {
122 color: color,
123 track: trackNames[trackIdx],
124 trackGroup: COMPONENTS_TRACK,
125 properties,
126 },
127 },
128 }),
129 );
130 performance.clearMeasures(measureName);
131 } else {
132 console.timeStamp(
133 measureName,
134 startTime < 0 ? 0 : startTime,
135 childrenEndTime,
136 trackNames[trackIdx],
137 COMPONENTS_TRACK,
138 color,
139 );
140 }
141 }
142 }
143
144 export function logComponentAborted(
145 componentInfo: ReactComponentInfo,
146 trackIdx: number,
147 startTime: number,
148 endTime: number,
149 childrenEndTime: number,
150 rootEnv: string,
151 ): void {
152 if (supportsUserTiming) {
153 const env = componentInfo.env;
154 const name = componentInfo.name;
155 const isPrimaryEnv = env === rootEnv;
156 const entryName =
157 isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
158 const measureName = '\u200b' + entryName;
159 if (__DEV__) {
160 const properties: Array<[string, string]> = [
161 [
162 'Aborted',
163 'The stream was aborted before this Component finished rendering.',
164 ],
165 ];
166 if (componentInfo.key != null) {
167 addValueToProperties('key', componentInfo.key, properties, 0, '');
168 }
169 if (componentInfo.props != null) {
170 addObjectToProperties(componentInfo.props, properties, 0, '');
171 }
172
173 performance.measure(measureName, {
174 start: startTime < 0 ? 0 : startTime,
175 end: childrenEndTime,
176 detail: {
177 devtools: {
178 color: 'warning',
179 track: trackNames[trackIdx],
180 trackGroup: COMPONENTS_TRACK,
181 tooltipText: entryName + ' Aborted',
182 properties,
183 },
184 },
185 });
186 performance.clearMeasures(measureName);
187 } else {
188 console.timeStamp(
189 measureName,
190 startTime < 0 ? 0 : startTime,
191 childrenEndTime,
192 trackNames[trackIdx],
193 COMPONENTS_TRACK,
194 'warning',
195 );
196 }
197 }
198 }
199
200 export function logComponentErrored(
201 componentInfo: ReactComponentInfo,
202 trackIdx: number,
203 startTime: number,
204 endTime: number,
205 childrenEndTime: number,
206 rootEnv: string,
207 error: mixed,
208 ): void {
209 if (supportsUserTiming) {
210 const env = componentInfo.env;
211 const name = componentInfo.name;
212 const isPrimaryEnv = env === rootEnv;
213 const entryName =
214 isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
215 const measureName = '\u200b' + entryName;
216 if (__DEV__) {
217 const message =
218 typeof error === 'object' &&
219 error !== null &&
220 typeof error.message === 'string'
221 ? // eslint-disable-next-line react-internal/safe-string-coercion
222 String(error.message)
223 : // eslint-disable-next-line react-internal/safe-string-coercion
224 String(error);
225 const properties: Array<[string, string]> = [['Error', message]];
226 if (componentInfo.key != null) {
227 addValueToProperties('key', componentInfo.key, properties, 0, '');
228 }
229 if (componentInfo.props != null) {
230 addObjectToProperties(componentInfo.props, properties, 0, '');
231 }
232
233 performance.measure(measureName, {
234 start: startTime < 0 ? 0 : startTime,
235 end: childrenEndTime,
236 detail: {
237 devtools: {
238 color: 'error',
239 track: trackNames[trackIdx],
240 trackGroup: COMPONENTS_TRACK,
241 tooltipText: entryName + ' Errored',
242 properties,
243 },
244 },
245 });
246 performance.clearMeasures(measureName);
247 } else {
248 console.timeStamp(
249 measureName,
250 startTime < 0 ? 0 : startTime,
251 childrenEndTime,
252 trackNames[trackIdx],
253 COMPONENTS_TRACK,
254 'error',
255 );
256 }
257 }
258 }
259
260 export function logDedupedComponentRender(
261 componentInfo: ReactComponentInfo,
262 trackIdx: number,
263 startTime: number,
264 endTime: number,
265 rootEnv: string,
266 ): void {
267 if (supportsUserTiming && endTime >= 0 && trackIdx < 10) {
268 const env = componentInfo.env;
269 const name = componentInfo.name;
270 const isPrimaryEnv = env === rootEnv;
271 const color = isPrimaryEnv ? 'primary-light' : 'secondary-light';
272 const entryName = name + ' [deduped]';
273 const debugTask = componentInfo.debugTask;
274 if (__DEV__ && debugTask) {
275 debugTask.run(
276 // $FlowFixMe[method-unbinding]
277 console.timeStamp.bind(
278 console,
279 entryName,
280 startTime < 0 ? 0 : startTime,
281 endTime,
282 trackNames[trackIdx],
283 COMPONENTS_TRACK,
284 color,
285 ),
286 );
287 } else {
288 console.timeStamp(
289 entryName,
290 startTime < 0 ? 0 : startTime,
291 endTime,
292 trackNames[trackIdx],
293 COMPONENTS_TRACK,
294 color,
295 );
296 }
297 }
298 }
299
300 function getIOColor(
301 functionName: string,
302 ): 'tertiary-light' | 'tertiary' | 'tertiary-dark' {
303 // Add some color variation to be able to distinguish various sources.
304 switch (functionName.charCodeAt(0) % 3) {
305 case 0:
306 return 'tertiary-light';
307 case 1:
308 return 'tertiary';
309 default:
310 return 'tertiary-dark';
311 }
312 }
313
314 function getIOLongName(
315 ioInfo: ReactIOInfo,
316 description: string,
317 env: void | string,
318 rootEnv: string,
319 ): string {
320 const name = ioInfo.name;
321 const longName = description === '' ? name : name + ' (' + description + ')';
322 const isPrimaryEnv = env === rootEnv;
323 return isPrimaryEnv || env === undefined
324 ? longName
325 : longName + ' [' + env + ']';
326 }
327
328 function getIOShortName(
329 ioInfo: ReactIOInfo,
330 description: string,
331 env: void | string,
332 rootEnv: string,
333 ): string {
334 const name = ioInfo.name;
335 const isPrimaryEnv = env === rootEnv;
336 const envSuffix = isPrimaryEnv || env === undefined ? '' : ' [' + env + ']';
337 let desc = '';
338 const descMaxLength = 30 - name.length - envSuffix.length;
339 if (descMaxLength > 1) {
340 const l = description.length;
341 if (l > 0 && l <= descMaxLength) {
342 // We can fit the full description
343 desc = ' (' + description + ')';
344 } else if (
345 description.startsWith('http://') ||
346 description.startsWith('https://') ||
347 description.startsWith('/')
348 ) {
349 // Looks like a URL. Let's see if we can extract something shorter.
350 // We don't have to do a full parse so let's try something cheaper.
351 let queryIdx = description.indexOf('?');
352 if (queryIdx === -1) {
353 queryIdx = description.length;
354 }
355 if (description.charCodeAt(queryIdx - 1) === 47 /* "/" */) {
356 // Ends with slash. Look before that.
357 queryIdx--;
358 }
359 const slashIdx = description.lastIndexOf('/', queryIdx - 1);
360 if (queryIdx - slashIdx < descMaxLength) {
361 // This may now be either the file name or the host.
362 // Include the slash to make it more obvious what we trimmed.
363 desc = ' (…' + description.slice(slashIdx, queryIdx) + ')';
364 } else {
365 // cut out the middle to not exceed the max length
366 const start = description.slice(slashIdx, slashIdx + descMaxLength / 2);
367 const end = description.slice(queryIdx - descMaxLength / 2, queryIdx);
368 desc = ' (' + (slashIdx > 0 ? '' : '') + start + '' + end + ')';
369 }
370 }
371 }
372 return name + desc + envSuffix;
373 }
374
375 export function logComponentAwaitAborted(
376 asyncInfo: ReactAsyncInfo,
377 trackIdx: number,
378 startTime: number,
379 endTime: number,
380 rootEnv: string,
381 ): void {
382 if (supportsUserTiming && endTime > 0) {
383 const entryName =
384 'await ' + getIOShortName(asyncInfo.awaited, '', asyncInfo.env, rootEnv);
385 const debugTask = asyncInfo.debugTask || asyncInfo.awaited.debugTask;
386 if (__DEV__ && debugTask) {
387 const properties = [
388 ['Aborted', 'The stream was aborted before this Promise resolved.'],
389 ];
390 const tooltipText =
391 getIOLongName(asyncInfo.awaited, '', asyncInfo.env, rootEnv) +
392 ' Aborted';
393 debugTask.run(
394 // $FlowFixMe[method-unbinding]
395 performance.measure.bind(performance, entryName, {
396 start: startTime < 0 ? 0 : startTime,
397 end: endTime,
398 detail: {
399 devtools: {
400 color: 'warning',
401 track: trackNames[trackIdx],
402 trackGroup: COMPONENTS_TRACK,
403 properties,
404 tooltipText,
405 },
406 },
407 }),
408 );
409 performance.clearMeasures(entryName);
410 } else {
411 console.timeStamp(
412 entryName,
413 startTime < 0 ? 0 : startTime,
414 endTime,
415 trackNames[trackIdx],
416 COMPONENTS_TRACK,
417 'warning',
418 );
419 }
420 }
421 }
422
423 export function logComponentAwaitErrored(
424 asyncInfo: ReactAsyncInfo,
425 trackIdx: number,
426 startTime: number,
427 endTime: number,
428 rootEnv: string,
429 error: mixed,
430 ): void {
431 if (supportsUserTiming && endTime > 0) {
432 const description = getIODescription(error);
433 const entryName =
434 'await ' +
435 getIOShortName(asyncInfo.awaited, description, asyncInfo.env, rootEnv);
436 const debugTask = asyncInfo.debugTask || asyncInfo.awaited.debugTask;
437 if (__DEV__ && debugTask) {
438 const message =
439 typeof error === 'object' &&
440 error !== null &&
441 typeof error.message === 'string'
442 ? // eslint-disable-next-line react-internal/safe-string-coercion
443 String(error.message)
444 : // eslint-disable-next-line react-internal/safe-string-coercion
445 String(error);
446 const properties = [['Rejected', message]];
447 const tooltipText =
448 getIOLongName(asyncInfo.awaited, description, asyncInfo.env, rootEnv) +
449 ' Rejected';
450 debugTask.run(
451 // $FlowFixMe[method-unbinding]
452 performance.measure.bind(performance, entryName, {
453 start: startTime < 0 ? 0 : startTime,
454 end: endTime,
455 detail: {
456 devtools: {
457 color: 'error',
458 track: trackNames[trackIdx],
459 trackGroup: COMPONENTS_TRACK,
460 properties,
461 tooltipText,
462 },
463 },
464 }),
465 );
466 performance.clearMeasures(entryName);
467 } else {
468 console.timeStamp(
469 entryName,
470 startTime < 0 ? 0 : startTime,
471 endTime,
472 trackNames[trackIdx],
473 COMPONENTS_TRACK,
474 'error',
475 );
476 }
477 }
478 }
479
480 export function logComponentAwait(
481 asyncInfo: ReactAsyncInfo,
482 trackIdx: number,
483 startTime: number,
484 endTime: number,
485 rootEnv: string,
486 value: mixed,
487 ): void {
488 if (supportsUserTiming && endTime > 0) {
489 const description = getIODescription(value);
490 const name = getIOShortName(
491 asyncInfo.awaited,
492 description,
493 asyncInfo.env,
494 rootEnv,
495 );
496 const entryName = 'await ' + name;
497 const color = getIOColor(name);
498 const debugTask = asyncInfo.debugTask || asyncInfo.awaited.debugTask;
499 if (__DEV__ && debugTask) {
500 const properties: Array<[string, string]> = [];
501 if (typeof value === 'object' && value !== null) {
502 addObjectToProperties(value, properties, 0, '');
503 } else if (value !== undefined) {
504 addValueToProperties('awaited value', value, properties, 0, '');
505 }
506 const tooltipText = getIOLongName(
507 asyncInfo.awaited,
508 description,
509 asyncInfo.env,
510 rootEnv,
511 );
512 debugTask.run(
513 // $FlowFixMe[method-unbinding]
514 performance.measure.bind(performance, entryName, {
515 start: startTime < 0 ? 0 : startTime,
516 end: endTime,
517 detail: {
518 devtools: {
519 color: color,
520 track: trackNames[trackIdx],
521 trackGroup: COMPONENTS_TRACK,
522 properties,
523 tooltipText,
524 },
525 },
526 }),
527 );
528 performance.clearMeasures(entryName);
529 } else {
530 console.timeStamp(
531 entryName,
532 startTime < 0 ? 0 : startTime,
533 endTime,
534 trackNames[trackIdx],
535 COMPONENTS_TRACK,
536 color,
537 );
538 }
539 }
540 }
541
542 export function logIOInfoErrored(
543 ioInfo: ReactIOInfo,
544 rootEnv: string,
545 error: mixed,
546 ): void {
547 const startTime = ioInfo.start;
548 const endTime = ioInfo.end;
549 if (supportsUserTiming && endTime >= 0) {
550 const description = getIODescription(error);
551 const entryName = getIOShortName(ioInfo, description, ioInfo.env, rootEnv);
552 const debugTask = ioInfo.debugTask;
553 const measureName = '\u200b' + entryName;
554 if (__DEV__ && debugTask) {
555 const message =
556 typeof error === 'object' &&
557 error !== null &&
558 typeof error.message === 'string'
559 ? // eslint-disable-next-line react-internal/safe-string-coercion
560 String(error.message)
561 : // eslint-disable-next-line react-internal/safe-string-coercion
562 String(error);
563 const properties = [['rejected with', message]];
564 const tooltipText =
565 getIOLongName(ioInfo, description, ioInfo.env, rootEnv) + ' Rejected';
566
567 debugTask.run(
568 // $FlowFixMe[method-unbinding]
569 performance.measure.bind(performance, measureName, {
570 start: startTime < 0 ? 0 : startTime,
571 end: endTime,
572 detail: {
573 devtools: {
574 color: 'error',
575 track: IO_TRACK,
576 properties,
577 tooltipText,
578 },
579 },
580 }),
581 );
582 performance.clearMeasures(measureName);
583 } else {
584 console.timeStamp(
585 measureName,
586 startTime < 0 ? 0 : startTime,
587 endTime,
588 IO_TRACK,
589 undefined,
590 'error',
591 );
592 }
593 }
594 }
595
596 export function logIOInfo(
597 ioInfo: ReactIOInfo,
598 rootEnv: string,
599 value: mixed,
600 ): void {
601 const startTime = ioInfo.start;
602 const endTime = ioInfo.end;
603 if (supportsUserTiming && endTime >= 0) {
604 const description = getIODescription(value);
605 const entryName = getIOShortName(ioInfo, description, ioInfo.env, rootEnv);
606 const color = getIOColor(entryName);
607 const debugTask = ioInfo.debugTask;
608 const measureName = '\u200b' + entryName;
609 if (__DEV__ && debugTask) {
610 const properties: Array<[string, string]> = [];
611 if (typeof value === 'object' && value !== null) {
612 addObjectToProperties(value, properties, 0, '');
613 } else if (value !== undefined) {
614 addValueToProperties('Resolved', value, properties, 0, '');
615 }
616 const tooltipText = getIOLongName(
617 ioInfo,
618 description,
619 ioInfo.env,
620 rootEnv,
621 );
622 debugTask.run(
623 // $FlowFixMe[method-unbinding]
624 performance.measure.bind(performance, measureName, {
625 start: startTime < 0 ? 0 : startTime,
626 end: endTime,
627 detail: {
628 devtools: {
629 color: color,
630 track: IO_TRACK,
631 properties,
632 tooltipText,
633 },
634 },
635 }),
636 );
637 performance.clearMeasures(measureName);
638 } else {
639 console.timeStamp(
640 measureName,
641 startTime < 0 ? 0 : startTime,
642 endTime,
643 IO_TRACK,
644 undefined,
645 color,
646 );
647 }
648 }
649 }