@samitouri / QOS-React-2 / commits / 5660c52b89

Scheduling Profiler: Improve warnings and add unit tests (#22038)

* Scheduling Profiler: Updated instructions to mentioned v18+ requirement * Moved long-event warning to post processing This lets us rule out non-React work or React work that started before the event and finished quickly during the event. Also added unit tests for this warning and the various cases. * Moved long-event warning to post processing This lets us rule out non-React work or React work that started before the event and finished quickly during the event. Also added unit tests for this warning and the various cases. * Updated nested update warning text * Udpate warning about suspending outside of a transition Handle edge case where component suspends before the first commit (and label metadata) has been logged. Add unit tests. * Fixed logic error in getBatchRange() with minStartTime * PR feedback: Combined a conditional statement

Brian Vaughn committed Aug 6, 2021 at 14:57 UTC 5660c52b89ec19eec36a974e4ff16908b1dbba3c
5 files changed +977 -443
packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.css
+1
@@ -54,6 +54,7 @@
54 .WelcomeInstructionsListItemLink {
55 color: var(--color-link);
56 margin-left: 0.25rem;
57 + margin-right: 0.25rem;
58 }
59
60 .ImportButtonLabel {
packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js
+1 -1
@@ -86,7 +86,7 @@ const Welcome = ({onFileSelect}: {|onFileSelect: (file: File) => void|}) => (
86 target="_blank">
87 profiling build of ReactDOM
88 </a>
89 - .
89 + (version 18 or newer).
90 </li>
91 <li className={styles.WelcomeInstructionsListItem}>
92 Open the "Performance" tab in Chrome and record some performance data.
packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
+869 -402
@@ -60,6 +60,7 @@ describe(getLanesFromTransportDecimalBitmask, () => {
60 describe(preprocessData, () => {
61 let React;
62 let ReactDOM;
63 + let Scheduler;
64
65 let act;
66 let clearedMarks;
@@ -135,6 +136,26 @@ describe(preprocessData, () => {
136 });
137 }
138
139 + function createNativeEventEntry(type, duration) {
140 + return createUserTimingEntry({
141 + cat: 'devtools.timeline',
142 + name: 'EventDispatch',
143 + args: {data: {type}},
144 + dur: duration,
145 + tdur: duration,
146 + });
147 + }
148 +
149 + function creactCpuProfilerSample() {
150 + return createUserTimingEntry({
151 + args: {data: {startTime: ++startTime}},
152 + cat: 'disabled-by-default-v8.cpu_profiler',
153 + id: '0x1',
154 + name: 'Profile',
155 + ph: 'P',
156 + });
157 + }
158 +
159 function createBoilerplateEntries() {
160 return [
161 createProfilerVersionEntry(),
@@ -144,13 +165,7 @@ describe(preprocessData, () => {
165 }
166
167 function createUserTimingData(sampleMarks) {
147 - const cpuProfilerSample = createUserTimingEntry({
148 - args: {data: {startTime: ++startTime}},
149 - cat: 'disabled-by-default-v8.cpu_profiler',
150 - id: '0x1',
151 - name: 'Profile',
152 - ph: 'P',
153 - });
168 + const cpuProfilerSample = creactCpuProfilerSample();
169
170 const randomSample = createUserTimingEntry({
171 dur: 100,
@@ -168,7 +183,7 @@ describe(preprocessData, () => {
183 pid: ++pid,
184 tid: ++tid,
185 ts: ++startTime,
171 - args: {data: {navigationId: 'fake-navigation-id'}},
186 + args: {data: {}},
187 cat: 'blink.user_timing',
188 name: markName,
189 ph: 'R',
@@ -185,6 +200,7 @@ describe(preprocessData, () => {
200
201 React = require('react');
202 ReactDOM = require('react-dom');
203 + Scheduler = require('scheduler');
204
205 const TestUtils = require('react-dom/test-utils');
206 act = TestUtils.act;
@@ -219,13 +235,7 @@ describe(preprocessData, () => {
235 });
236
237 it('should return empty data given a timeline with no React scheduling profiling marks', () => {
222 - const cpuProfilerSample = createUserTimingEntry({
223 - args: {data: {startTime: ++startTime}},
224 - cat: 'disabled-by-default-v8.cpu_profiler',
225 - id: '0x1',
226 - name: 'Profile',
227 - ph: 'P',
228 - });
238 + const cpuProfilerSample = creactCpuProfilerSample();
239 const randomSample = createUserTimingEntry({
240 dur: 100,
241 tdur: 200,
@@ -292,13 +302,7 @@ describe(preprocessData, () => {
302 });
303
304 it('should process legacy data format (before lane labels were added)', () => {
295 - const cpuProfilerSample = createUserTimingEntry({
296 - args: {data: {startTime: ++startTime}},
297 - cat: 'disabled-by-default-v8.cpu_profiler',
298 - id: '0x1',
299 - name: 'Profile',
300 - ph: 'P',
301 - });
305 + const cpuProfilerSample = creactCpuProfilerSample();
306
307 if (gate(flags => flags.enableSchedulingProfiler)) {
308 // Data below is hard-coded based on an older profile sample.
@@ -336,102 +340,102 @@ describe(preprocessData, () => {
340 }),
341 ]);
342 expect(data).toMatchInlineSnapshot(`
339 - Object {
340 - "componentMeasures": Array [],
341 - "duration": 0.011,
342 - "flamechart": Array [],
343 - "laneToLabelMap": Map {
344 - 0 => "Sync",
345 - 1 => "InputContinuousHydration",
346 - 2 => "InputContinuous",
347 - 3 => "DefaultHydration",
348 - 4 => "Default",
349 - 5 => "TransitionHydration",
350 - 6 => "Transition",
351 - 7 => "Transition",
352 - 8 => "Transition",
353 - 9 => "Transition",
354 - 10 => "Transition",
355 - 11 => "Transition",
356 - 12 => "Transition",
357 - 13 => "Transition",
358 - 14 => "Transition",
359 - 15 => "Transition",
360 - 16 => "Transition",
361 - 17 => "Transition",
362 - 18 => "Transition",
363 - 19 => "Transition",
364 - 20 => "Transition",
365 - 21 => "Transition",
366 - 22 => "Retry",
367 - 23 => "Retry",
368 - 24 => "Retry",
369 - 25 => "Retry",
370 - 26 => "Retry",
371 - 27 => "SelectiveHydration",
372 - 28 => "IdleHydration",
373 - 29 => "Idle",
374 - 30 => "Offscreen",
375 - },
376 - "measures": Array [
377 - Object {
378 - "batchUID": 0,
379 - "depth": 0,
380 - "duration": 0.004999999999999999,
381 - "lanes": Array [
382 - 9,
383 - ],
384 - "timestamp": 0.006,
385 - "type": "render-idle",
386 - },
387 - Object {
388 - "batchUID": 0,
389 - "depth": 0,
390 - "duration": 0.001,
391 - "lanes": Array [
392 - 9,
393 - ],
394 - "timestamp": 0.006,
395 - "type": "render",
396 - },
397 - Object {
398 - "batchUID": 0,
399 - "depth": 0,
400 - "duration": 0.002999999999999999,
401 - "lanes": Array [
402 - 9,
403 - ],
404 - "timestamp": 0.008,
405 - "type": "commit",
406 - },
407 - Object {
408 - "batchUID": 0,
409 - "depth": 1,
410 - "duration": 0.0010000000000000009,
411 - "lanes": Array [
412 - 9,
413 - ],
414 - "timestamp": 0.009,
415 - "type": "layout-effects",
416 - },
417 - ],
418 - "nativeEvents": Array [],
419 - "otherUserTimingMarks": Array [],
420 - "reactVersion": "17.0.3",
421 - "schedulingEvents": Array [
422 - Object {
423 - "lanes": Array [
424 - 9,
425 - ],
426 - "timestamp": 0.005,
427 - "type": "schedule-render",
428 - "warning": null,
429 - },
430 - ],
431 - "startTime": 1,
432 - "suspenseEvents": Array [],
433 - }
434 - `);
343 + Object {
344 + "componentMeasures": Array [],
345 + "duration": 0.011,
346 + "flamechart": Array [],
347 + "laneToLabelMap": Map {
348 + 0 => "Sync",
349 + 1 => "InputContinuousHydration",
350 + 2 => "InputContinuous",
351 + 3 => "DefaultHydration",
352 + 4 => "Default",
353 + 5 => "TransitionHydration",
354 + 6 => "Transition",
355 + 7 => "Transition",
356 + 8 => "Transition",
357 + 9 => "Transition",
358 + 10 => "Transition",
359 + 11 => "Transition",
360 + 12 => "Transition",
361 + 13 => "Transition",
362 + 14 => "Transition",
363 + 15 => "Transition",
364 + 16 => "Transition",
365 + 17 => "Transition",
366 + 18 => "Transition",
367 + 19 => "Transition",
368 + 20 => "Transition",
369 + 21 => "Transition",
370 + 22 => "Retry",
371 + 23 => "Retry",
372 + 24 => "Retry",
373 + 25 => "Retry",
374 + 26 => "Retry",
375 + 27 => "SelectiveHydration",
376 + 28 => "IdleHydration",
377 + 29 => "Idle",
378 + 30 => "Offscreen",
379 + },
380 + "measures": Array [
381 + Object {
382 + "batchUID": 0,
383 + "depth": 0,
384 + "duration": 0.004999999999999999,
385 + "lanes": Array [
386 + 9,
387 + ],
388 + "timestamp": 0.006,
389 + "type": "render-idle",
390 + },
391 + Object {
392 + "batchUID": 0,
393 + "depth": 0,
394 + "duration": 0.001,
395 + "lanes": Array [
396 + 9,
397 + ],
398 + "timestamp": 0.006,
399 + "type": "render",
400 + },
401 + Object {
402 + "batchUID": 0,
403 + "depth": 0,
404 + "duration": 0.002999999999999999,
405 + "lanes": Array [
406 + 9,
407 + ],
408 + "timestamp": 0.008,
409 + "type": "commit",
410 + },
411 + Object {
412 + "batchUID": 0,
413 + "depth": 1,
414 + "duration": 0.0010000000000000009,
415 + "lanes": Array [
416 + 9,
417 + ],
418 + "timestamp": 0.009,
419 + "type": "layout-effects",
420 + },
421 + ],
422 + "nativeEvents": Array [],
423 + "otherUserTimingMarks": Array [],
424 + "reactVersion": "17.0.3",
425 + "schedulingEvents": Array [
426 + Object {
427 + "lanes": Array [
428 + 9,
429 + ],
430 + "timestamp": 0.005,
431 + "type": "schedule-render",
432 + "warning": null,
433 + },
434 + ],
435 + "startTime": 1,
436 + "suspenseEvents": Array [],
437 + }
438 + `);
439 }
440 });
441
@@ -444,107 +448,107 @@ describe(preprocessData, () => {
448 ...createUserTimingData(clearedMarks),
449 ]);
450 expect(data).toMatchInlineSnapshot(`
447 - Object {
448 - "componentMeasures": Array [],
449 - "duration": 0.013,
450 - "flamechart": Array [],
451 - "laneToLabelMap": Map {
452 - 0 => "Sync",
453 - 1 => "InputContinuousHydration",
454 - 2 => "InputContinuous",
455 - 3 => "DefaultHydration",
456 - 4 => "Default",
457 - 5 => "TransitionHydration",
458 - 6 => "Transition",
459 - 7 => "Transition",
460 - 8 => "Transition",
461 - 9 => "Transition",
462 - 10 => "Transition",
463 - 11 => "Transition",
464 - 12 => "Transition",
465 - 13 => "Transition",
466 - 14 => "Transition",
467 - 15 => "Transition",
468 - 16 => "Transition",
469 - 17 => "Transition",
470 - 18 => "Transition",
471 - 19 => "Transition",
472 - 20 => "Transition",
473 - 21 => "Transition",
474 - 22 => "Retry",
475 - 23 => "Retry",
476 - 24 => "Retry",
477 - 25 => "Retry",
478 - 26 => "Retry",
479 - 27 => "SelectiveHydration",
480 - 28 => "IdleHydration",
481 - 29 => "Idle",
482 - 30 => "Offscreen",
483 - },
484 - "measures": Array [
485 - Object {
486 - "batchUID": 0,
487 - "depth": 0,
488 - "duration": 0.008,
489 - "lanes": Array [
490 - 0,
491 - ],
492 - "timestamp": 0.005,
493 - "type": "render-idle",
494 - },
495 - Object {
496 - "batchUID": 0,
497 - "depth": 0,
498 - "duration": 0.001,
499 - "lanes": Array [
500 - 0,
501 - ],
502 - "timestamp": 0.005,
503 - "type": "render",
504 - },
505 - Object {
506 - "batchUID": 0,
507 - "depth": 0,
508 - "duration": 0.005999999999999999,
509 - "lanes": Array [
510 - 0,
511 - ],
512 - "timestamp": 0.007,
513 - "type": "commit",
514 - },
515 - Object {
516 - "batchUID": 0,
517 - "depth": 1,
518 - "duration": 0.0010000000000000009,
519 - "lanes": Array [
520 - 0,
521 - ],
522 - "timestamp": 0.011,
523 - "type": "layout-effects",
524 - },
525 - ],
526 - "nativeEvents": Array [],
527 - "otherUserTimingMarks": Array [
528 - Object {
529 - "name": "__v3",
530 - "timestamp": 0.003,
531 - },
532 - ],
533 - "reactVersion": "17.0.3",
534 - "schedulingEvents": Array [
535 - Object {
536 - "lanes": Array [
537 - 0,
538 - ],
539 - "timestamp": 0.004,
540 - "type": "schedule-render",
541 - "warning": null,
542 - },
543 - ],
544 - "startTime": 4,
545 - "suspenseEvents": Array [],
546 - }
547 - `);
451 + Object {
452 + "componentMeasures": Array [],
453 + "duration": 0.013,
454 + "flamechart": Array [],
455 + "laneToLabelMap": Map {
456 + 0 => "Sync",
457 + 1 => "InputContinuousHydration",
458 + 2 => "InputContinuous",
459 + 3 => "DefaultHydration",
460 + 4 => "Default",
461 + 5 => "TransitionHydration",
462 + 6 => "Transition",
463 + 7 => "Transition",
464 + 8 => "Transition",
465 + 9 => "Transition",
466 + 10 => "Transition",
467 + 11 => "Transition",
468 + 12 => "Transition",
469 + 13 => "Transition",
470 + 14 => "Transition",
471 + 15 => "Transition",
472 + 16 => "Transition",
473 + 17 => "Transition",
474 + 18 => "Transition",
475 + 19 => "Transition",
476 + 20 => "Transition",
477 + 21 => "Transition",
478 + 22 => "Retry",
479 + 23 => "Retry",
480 + 24 => "Retry",
481 + 25 => "Retry",
482 + 26 => "Retry",
483 + 27 => "SelectiveHydration",
484 + 28 => "IdleHydration",
485 + 29 => "Idle",
486 + 30 => "Offscreen",
487 + },
488 + "measures": Array [
489 + Object {
490 + "batchUID": 0,
491 + "depth": 0,
492 + "duration": 0.008,
493 + "lanes": Array [
494 + 0,
495 + ],
496 + "timestamp": 0.005,
497 + "type": "render-idle",
498 + },
499 + Object {
500 + "batchUID": 0,
501 + "depth": 0,
502 + "duration": 0.001,
503 + "lanes": Array [
504 + 0,
505 + ],
506 + "timestamp": 0.005,
507 + "type": "render",
508 + },
509 + Object {
510 + "batchUID": 0,
511 + "depth": 0,
512 + "duration": 0.005999999999999999,
513 + "lanes": Array [
514 + 0,
515 + ],
516 + "timestamp": 0.007,
517 + "type": "commit",
518 + },
519 + Object {
520 + "batchUID": 0,
521 + "depth": 1,
522 + "duration": 0.0010000000000000009,
523 + "lanes": Array [
524 + 0,
525 + ],
526 + "timestamp": 0.011,
527 + "type": "layout-effects",
528 + },
529 + ],
530 + "nativeEvents": Array [],
531 + "otherUserTimingMarks": Array [
532 + Object {
533 + "name": "__v3",
534 + "timestamp": 0.003,
535 + },
536 + ],
537 + "reactVersion": "17.0.3",
538 + "schedulingEvents": Array [
539 + Object {
540 + "lanes": Array [
541 + 0,
542 + ],
543 + "timestamp": 0.004,
544 + "type": "schedule-render",
545 + "warning": null,
546 + },
547 + ],
548 + "startTime": 4,
549 + "suspenseEvents": Array [],
550 + }
551 + `);
552 }
553 });
554
@@ -568,189 +572,189 @@ describe(preprocessData, () => {
572 ...createUserTimingData(clearedMarks),
573 ]);
574 expect(data).toMatchInlineSnapshot(`
571 - Object {
572 - "componentMeasures": Array [
573 - Object {
574 - "componentName": "App",
575 - "duration": 0.001,
576 - "timestamp": 0.006,
577 - "warning": null,
578 - },
579 - Object {
580 - "componentName": "App",
581 - "duration": 0.0010000000000000009,
582 - "timestamp": 0.02,
583 - "warning": null,
584 - },
585 - ],
586 - "duration": 0.031,
587 - "flamechart": Array [],
588 - "laneToLabelMap": Map {
589 - 0 => "Sync",
590 - 1 => "InputContinuousHydration",
591 - 2 => "InputContinuous",
592 - 3 => "DefaultHydration",
593 - 4 => "Default",
594 - 5 => "TransitionHydration",
595 - 6 => "Transition",
596 - 7 => "Transition",
597 - 8 => "Transition",
598 - 9 => "Transition",
599 - 10 => "Transition",
600 - 11 => "Transition",
601 - 12 => "Transition",
602 - 13 => "Transition",
603 - 14 => "Transition",
604 - 15 => "Transition",
605 - 16 => "Transition",
606 - 17 => "Transition",
607 - 18 => "Transition",
608 - 19 => "Transition",
609 - 20 => "Transition",
610 - 21 => "Transition",
611 - 22 => "Retry",
612 - 23 => "Retry",
613 - 24 => "Retry",
614 - 25 => "Retry",
615 - 26 => "Retry",
616 - 27 => "SelectiveHydration",
617 - 28 => "IdleHydration",
618 - 29 => "Idle",
619 - 30 => "Offscreen",
620 - },
621 - "measures": Array [
622 - Object {
623 - "batchUID": 0,
624 - "depth": 0,
625 - "duration": 0.009999999999999998,
626 - "lanes": Array [
627 - 4,
628 - ],
629 - "timestamp": 0.005,
630 - "type": "render-idle",
631 - },
632 - Object {
633 - "batchUID": 0,
634 - "depth": 0,
635 - "duration": 0.003,
636 - "lanes": Array [
637 - 4,
638 - ],
639 - "timestamp": 0.005,
640 - "type": "render",
641 - },
642 - Object {
643 - "batchUID": 0,
644 - "depth": 0,
645 - "duration": 0.006,
646 - "lanes": Array [
647 - 4,
648 - ],
649 - "timestamp": 0.009,
650 - "type": "commit",
651 - },
652 - Object {
653 - "batchUID": 0,
654 - "depth": 1,
655 - "duration": 0.0010000000000000009,
656 - "lanes": Array [
657 - 4,
658 - ],
659 - "timestamp": 0.013,
660 - "type": "layout-effects",
661 - },
662 - Object {
663 - "batchUID": 0,
664 - "depth": 0,
665 - "duration": 0.0019999999999999983,
666 - "lanes": Array [
667 - 4,
668 - ],
669 - "timestamp": 0.016,
670 - "type": "passive-effects",
671 - },
672 - Object {
673 - "batchUID": 1,
674 - "depth": 0,
675 - "duration": 0.010000000000000002,
676 - "lanes": Array [
677 - 4,
678 - ],
679 - "timestamp": 0.019,
680 - "type": "render-idle",
681 - },
682 - Object {
683 - "batchUID": 1,
684 - "depth": 0,
685 - "duration": 0.002999999999999999,
686 - "lanes": Array [
687 - 4,
688 - ],
689 - "timestamp": 0.019,
690 - "type": "render",
691 - },
692 - Object {
693 - "batchUID": 1,
694 - "depth": 0,
695 - "duration": 0.006000000000000002,
696 - "lanes": Array [
697 - 4,
698 - ],
699 - "timestamp": 0.023,
700 - "type": "commit",
701 - },
702 - Object {
703 - "batchUID": 1,
704 - "depth": 1,
705 - "duration": 0.0010000000000000009,
706 - "lanes": Array [
707 - 4,
708 - ],
709 - "timestamp": 0.027,
710 - "type": "layout-effects",
711 - },
712 - Object {
713 - "batchUID": 1,
714 - "depth": 0,
715 - "duration": 0.0010000000000000009,
716 - "lanes": Array [
717 - 4,
718 - ],
719 - "timestamp": 0.03,
720 - "type": "passive-effects",
721 - },
722 - ],
723 - "nativeEvents": Array [],
724 - "otherUserTimingMarks": Array [
725 - Object {
726 - "name": "__v3",
727 - "timestamp": 0.003,
728 - },
729 - ],
730 - "reactVersion": "17.0.3",
731 - "schedulingEvents": Array [
732 - Object {
733 - "lanes": Array [
734 - 4,
735 - ],
736 - "timestamp": 0.004,
737 - "type": "schedule-render",
738 - "warning": null,
739 - },
740 - Object {
741 - "componentName": "App",
742 - "lanes": Array [
743 - 4,
744 - ],
745 - "timestamp": 0.017,
746 - "type": "schedule-state-update",
747 - "warning": null,
748 - },
749 - ],
750 - "startTime": 4,
751 - "suspenseEvents": Array [],
752 - }
753 - `);
575 + Object {
576 + "componentMeasures": Array [
577 + Object {
578 + "componentName": "App",
579 + "duration": 0.001,
580 + "timestamp": 0.006,
581 + "warning": null,
582 + },
583 + Object {
584 + "componentName": "App",
585 + "duration": 0.0010000000000000009,
586 + "timestamp": 0.02,
587 + "warning": null,
588 + },
589 + ],
590 + "duration": 0.031,
591 + "flamechart": Array [],
592 + "laneToLabelMap": Map {
593 + 0 => "Sync",
594 + 1 => "InputContinuousHydration",
595 + 2 => "InputContinuous",
596 + 3 => "DefaultHydration",
597 + 4 => "Default",
598 + 5 => "TransitionHydration",
599 + 6 => "Transition",
600 + 7 => "Transition",
601 + 8 => "Transition",
602 + 9 => "Transition",
603 + 10 => "Transition",
604 + 11 => "Transition",
605 + 12 => "Transition",
606 + 13 => "Transition",
607 + 14 => "Transition",
608 + 15 => "Transition",
609 + 16 => "Transition",
610 + 17 => "Transition",
611 + 18 => "Transition",
612 + 19 => "Transition",
613 + 20 => "Transition",
614 + 21 => "Transition",
615 + 22 => "Retry",
616 + 23 => "Retry",
617 + 24 => "Retry",
618 + 25 => "Retry",
619 + 26 => "Retry",
620 + 27 => "SelectiveHydration",
621 + 28 => "IdleHydration",
622 + 29 => "Idle",
623 + 30 => "Offscreen",
624 + },
625 + "measures": Array [
626 + Object {
627 + "batchUID": 0,
628 + "depth": 0,
629 + "duration": 0.009999999999999998,
630 + "lanes": Array [
631 + 4,
632 + ],
633 + "timestamp": 0.005,
634 + "type": "render-idle",
635 + },
636 + Object {
637 + "batchUID": 0,
638 + "depth": 0,
639 + "duration": 0.003,
640 + "lanes": Array [
641 + 4,
642 + ],
643 + "timestamp": 0.005,
644 + "type": "render",
645 + },
646 + Object {
647 + "batchUID": 0,
648 + "depth": 0,
649 + "duration": 0.006,
650 + "lanes": Array [
651 + 4,
652 + ],
653 + "timestamp": 0.009,
654 + "type": "commit",
655 + },
656 + Object {
657 + "batchUID": 0,
658 + "depth": 1,
659 + "duration": 0.0010000000000000009,
660 + "lanes": Array [
661 + 4,
662 + ],
663 + "timestamp": 0.013,
664 + "type": "layout-effects",
665 + },
666 + Object {
667 + "batchUID": 0,
668 + "depth": 0,
669 + "duration": 0.0019999999999999983,
670 + "lanes": Array [
671 + 4,
672 + ],
673 + "timestamp": 0.016,
674 + "type": "passive-effects",
675 + },
676 + Object {
677 + "batchUID": 1,
678 + "depth": 0,
679 + "duration": 0.010000000000000002,
680 + "lanes": Array [
681 + 4,
682 + ],
683 + "timestamp": 0.019,
684 + "type": "render-idle",
685 + },
686 + Object {
687 + "batchUID": 1,
688 + "depth": 0,
689 + "duration": 0.002999999999999999,
690 + "lanes": Array [
691 + 4,
692 + ],
693 + "timestamp": 0.019,
694 + "type": "render",
695 + },
696 + Object {
697 + "batchUID": 1,
698 + "depth": 0,
699 + "duration": 0.006000000000000002,
700 + "lanes": Array [
701 + 4,
702 + ],
703 + "timestamp": 0.023,
704 + "type": "commit",
705 + },
706 + Object {
707 + "batchUID": 1,
708 + "depth": 1,
709 + "duration": 0.0010000000000000009,
710 + "lanes": Array [
711 + 4,
712 + ],
713 + "timestamp": 0.027,
714 + "type": "layout-effects",
715 + },
716 + Object {
717 + "batchUID": 1,
718 + "depth": 0,
719 + "duration": 0.0010000000000000009,
720 + "lanes": Array [
721 + 4,
722 + ],
723 + "timestamp": 0.03,
724 + "type": "passive-effects",
725 + },
726 + ],
727 + "nativeEvents": Array [],
728 + "otherUserTimingMarks": Array [
729 + Object {
730 + "name": "__v3",
731 + "timestamp": 0.003,
732 + },
733 + ],
734 + "reactVersion": "17.0.3",
735 + "schedulingEvents": Array [
736 + Object {
737 + "lanes": Array [
738 + 4,
739 + ],
740 + "timestamp": 0.004,
741 + "type": "schedule-render",
742 + "warning": null,
743 + },
744 + Object {
745 + "componentName": "App",
746 + "lanes": Array [
747 + 4,
748 + ],
749 + "timestamp": 0.017,
750 + "type": "schedule-state-update",
751 + "warning": null,
752 + },
753 + ],
754 + "startTime": 4,
755 + "suspenseEvents": Array [],
756 + }
757 + `);
758 }
759 });
760
@@ -833,5 +837,468 @@ describe(preprocessData, () => {
837 `);
838 });
839
840 + describe('warnings', () => {
841 + describe('long event handlers', () => {
842 + it('should not warn when React scedules a (sync) update inside of a short event handler', () => {
843 + function App() {
844 + return null;
845 + }
846 +
847 + if (gate(flags => flags.enableSchedulingProfiler)) {
848 + const testMarks = [
849 + creactCpuProfilerSample(),
850 + ...createBoilerplateEntries(),
851 + createNativeEventEntry('click', 5),
852 + ];
853 +
854 + clearedMarks.splice(0);
855 +
856 + ReactDOM.render(<App />, document.createElement('div'));
857 +
858 + testMarks.push(...createUserTimingData(clearedMarks));
859 +
860 + const data = preprocessData(testMarks);
861 + const event = data.nativeEvents.find(({type}) => type === 'click');
862 + expect(event.warning).toBe(null);
863 + }
864 + });
865 +
866 + it('should not warn about long events if the cause was non-React JavaScript', () => {
867 + function App() {
868 + return null;
869 + }
870 +
871 + if (gate(flags => flags.enableSchedulingProfiler)) {
872 + const testMarks = [
873 + creactCpuProfilerSample(),
874 + ...createBoilerplateEntries(),
875 + createNativeEventEntry('click', 25000),
876 + ];
877 +
878 + startTime += 2000;
879 +
880 + clearedMarks.splice(0);
881 +
882 + ReactDOM.render(<App />, document.createElement('div'));
883 +
884 + testMarks.push(...createUserTimingData(clearedMarks));
885 +
886 + const data = preprocessData(testMarks);
887 + const event = data.nativeEvents.find(({type}) => type === 'click');
888 + expect(event.warning).toBe(null);
889 + }
890 + });
891 +
892 + it('should warn when React scedules a long (sync) update inside of an event', () => {
893 + function App() {
894 + return null;
895 + }
896 +
897 + if (gate(flags => flags.enableSchedulingProfiler)) {
898 + const testMarks = [
899 + creactCpuProfilerSample(),
900 + ...createBoilerplateEntries(),
901 + createNativeEventEntry('click', 25000),
902 + ];
903 +
904 + clearedMarks.splice(0);
905 +
906 + ReactDOM.render(<App />, document.createElement('div'));
907 +
908 + clearedMarks.forEach(markName => {
909 + if (markName === '--render-stop') {
910 + // Fake a long running render
911 + startTime += 20000;
912 + }
913 +
914 + testMarks.push({
915 + pid: ++pid,
916 + tid: ++tid,
917 + ts: ++startTime,
918 + args: {data: {}},
919 + cat: 'blink.user_timing',
920 + name: markName,
921 + ph: 'R',
922 + });
923 + });
924 +
925 + const data = preprocessData(testMarks);
926 + const event = data.nativeEvents.find(({type}) => type === 'click');
927 + expect(event.warning).toMatchInlineSnapshot(
928 + `"An event handler scheduled a big update with React. Consider using the Transition API to defer some of this work."`,
929 + );
930 + }
931 + });
932 +
933 + it('should not warn when React finishes a previously long (async) update with a short (sync) update inside of an event', () => {
934 + function Yield({id, value}) {
935 + Scheduler.unstable_yieldValue(`${id}:${value}`);
936 + return null;
937 + }
938 +
939 + if (gate(flags => flags.enableSchedulingProfiler)) {
940 + const testMarks = [
941 + creactCpuProfilerSample(),
942 + ...createBoilerplateEntries(),
943 + ];
944 +
945 + // Advance the clock by some arbitrary amount.
946 + startTime += 50000;
947 +
948 + const root = ReactDOM.createRoot(document.createElement('div'));
949 +
950 + React.startTransition(() => {
951 + // Start rendering an async update (but don't finish).
952 + root.render(
953 + <>
954 + <Yield id="A" value={1} />
955 + <Yield id="B" value={1} />
956 + </>,
957 + );
958 + expect(Scheduler).toFlushAndYieldThrough(['A:1']);
959 +
960 + testMarks.push(...createUserTimingData(clearedMarks));
961 + clearedMarks.splice(0);
962 +
963 + // Advance the clock some more to make the pending React update seem long.
964 + startTime += 20000;
965 +
966 + // Fake a long "click" event in the middle
967 + // and schedule a sync update that will also flush the previous work.
968 + testMarks.push(createNativeEventEntry('click', 25000));
969 + ReactDOM.flushSync(() => {
970 + root.render(
971 + <>
972 + <Yield id="A" value={2} />
973 + <Yield id="B" value={2} />
974 + </>,
975 + );
976 + });
977 + });
978 +
979 + expect(Scheduler).toHaveYielded(['A:2', 'B:2']);
980 +
981 + testMarks.push(...createUserTimingData(clearedMarks));
982 +
983 + const data = preprocessData(testMarks);
984 + const event = data.nativeEvents.find(({type}) => type === 'click');
985 + expect(event.warning).toBe(null);
986 + }
987 + });
988 + });
989 +
990 + describe('nested updates', () => {
991 + it('should not warn about short nested (state) updates during layout effects', () => {
992 + function Component() {
993 + const [didMount, setDidMount] = React.useState(false);
994 + Scheduler.unstable_yieldValue(
995 + `Component ${didMount ? 'update' : 'mount'}`,
996 + );
997 + React.useLayoutEffect(() => {
998 + setDidMount(true);
999 + }, []);
1000 + return didMount;
1001 + }
1002 +
1003 + if (gate(flags => flags.enableSchedulingProfiler)) {
1004 + const root = ReactDOM.createRoot(document.createElement('div'));
1005 + act(() => {
1006 + root.render(<Component />);
1007 + });
1008 +
1009 + expect(Scheduler).toHaveYielded([
1010 + 'Component mount',
1011 + 'Component update',
1012 + ]);
1013 +
1014 + const data = preprocessData([
1015 + ...createBoilerplateEntries(),
1016 + ...createUserTimingData(clearedMarks),
1017 + ]);
1018 +
1019 + const event = data.schedulingEvents.find(
1020 + ({type}) => type === 'schedule-state-update',
1021 + );
1022 + expect(event.warning).toBe(null);
1023 + }
1024 + });
1025 +
1026 + it('should not warn about short (forced) updates during layout effects', () => {
1027 + class Component extends React.Component {
1028 + _didMount: boolean = false;
1029 + componentDidMount() {
1030 + this._didMount = true;
1031 + this.forceUpdate();
1032 + }
1033 + render() {
1034 + Scheduler.unstable_yieldValue(
1035 + `Component ${this._didMount ? 'update' : 'mount'}`,
1036 + );
1037 + return null;
1038 + }
1039 + }
1040 +
1041 + if (gate(flags => flags.enableSchedulingProfiler)) {
1042 + const root = ReactDOM.createRoot(document.createElement('div'));
1043 + act(() => {
1044 + root.render(<Component />);
1045 + });
1046 +
1047 + expect(Scheduler).toHaveYielded([
1048 + 'Component mount',
1049 + 'Component update',
1050 + ]);
1051 +
1052 + const data = preprocessData([
1053 + ...createBoilerplateEntries(),
1054 + ...createUserTimingData(clearedMarks),
1055 + ]);
1056 +
1057 + const event = data.schedulingEvents.find(
1058 + ({type}) => type === 'schedule-force-update',
1059 + );
1060 + expect(event.warning).toBe(null);
1061 + }
1062 + });
1063 +
1064 + it('should warn about long nested (state) updates during layout effects', () => {
1065 + function Component() {
1066 + const [didMount, setDidMount] = React.useState(false);
1067 + Scheduler.unstable_yieldValue(
1068 + `Component ${didMount ? 'update' : 'mount'}`,
1069 + );
1070 + // Fake a long render
1071 + startTime += 20000;
1072 + React.useLayoutEffect(() => {
1073 + setDidMount(true);
1074 + }, []);
1075 + return didMount;
1076 + }
1077 +
1078 + if (gate(flags => flags.enableSchedulingProfiler)) {
1079 + const cpuProfilerSample = creactCpuProfilerSample();
1080 +
1081 + const root = ReactDOM.createRoot(document.createElement('div'));
1082 + act(() => {
1083 + root.render(<Component />);
1084 + });
1085 +
1086 + expect(Scheduler).toHaveYielded([
1087 + 'Component mount',
1088 + 'Component update',
1089 + ]);
1090 +
1091 + const testMarks = [];
1092 + clearedMarks.forEach(markName => {
1093 + if (markName === '--component-render-start-Component') {
1094 + // Fake a long running render
1095 + startTime += 20000;
1096 + }
1097 +
1098 + testMarks.push({
1099 + pid: ++pid,
1100 + tid: ++tid,
1101 + ts: ++startTime,
1102 + args: {data: {}},
1103 + cat: 'blink.user_timing',
1104 + name: markName,
1105 + ph: 'R',
1106 + });
1107 + });
1108 +
1109 + const data = preprocessData([
1110 + cpuProfilerSample,
1111 + ...createBoilerplateEntries(),
1112 + ...testMarks,
1113 + ]);
1114 +
1115 + const event = data.schedulingEvents.find(
1116 + ({type}) => type === 'schedule-state-update',
1117 + );
1118 + expect(event.warning).toMatchInlineSnapshot(
1119 + `"A big nested update was scheduled during layout. Nested updates require React to re-render synchronously before the browser can paint. Consider delaying this update by moving it to a passive effect (useEffect)."`,
1120 + );
1121 + }
1122 + });
1123 +
1124 + it('should warn about long nested (forced) updates during layout effects', () => {
1125 + class Component extends React.Component {
1126 + _didMount: boolean = false;
1127 + componentDidMount() {
1128 + this._didMount = true;
1129 + this.forceUpdate();
1130 + }
1131 + render() {
1132 + Scheduler.unstable_yieldValue(
1133 + `Component ${this._didMount ? 'update' : 'mount'}`,
1134 + );
1135 + return null;
1136 + }
1137 + }
1138 +
1139 + if (gate(flags => flags.enableSchedulingProfiler)) {
1140 + const cpuProfilerSample = creactCpuProfilerSample();
1141 +
1142 + const root = ReactDOM.createRoot(document.createElement('div'));
1143 + act(() => {
1144 + root.render(<Component />);
1145 + });
1146 +
1147 + expect(Scheduler).toHaveYielded([
1148 + 'Component mount',
1149 + 'Component update',
1150 + ]);
1151 +
1152 + const testMarks = [];
1153 + clearedMarks.forEach(markName => {
1154 + if (markName === '--component-render-start-Component') {
1155 + // Fake a long running render
1156 + startTime += 20000;
1157 + }
1158 +
1159 + testMarks.push({
1160 + pid: ++pid,
1161 + tid: ++tid,
1162 + ts: ++startTime,
1163 + args: {data: {}},
1164 + cat: 'blink.user_timing',
1165 + name: markName,
1166 + ph: 'R',
1167 + });
1168 + });
1169 +
1170 + const data = preprocessData([
1171 + cpuProfilerSample,
1172 + ...createBoilerplateEntries(),
1173 + ...testMarks,
1174 + ]);
1175 +
1176 + const event = data.schedulingEvents.find(
1177 + ({type}) => type === 'schedule-force-update',
1178 + );
1179 + expect(event.warning).toMatchInlineSnapshot(
1180 + `"A big nested update was scheduled during layout. Nested updates require React to re-render synchronously before the browser can paint. Consider delaying this update by moving it to a passive effect (useEffect)."`,
1181 + );
1182 + }
1183 + });
1184 + });
1185 +
1186 + describe('suspend during an update', () => {
1187 + // This also tests an edge case where the a component suspends while profiling
1188 + // before the first commit is logged (so the lane-to-labels map will not yet exist).
1189 + it('should warn about suspending during an udpate', () => {
1190 + let promise = null;
1191 + let resolvedValue = null;
1192 + function readValue(value) {
1193 + if (resolvedValue !== null) {
1194 + return resolvedValue;
1195 + } else if (promise === null) {
1196 + promise = Promise.resolve(true).then(() => {
1197 + resolvedValue = value;
1198 + });
1199 + }
1200 + throw promise;
1201 + }
1202 +
1203 + function Component({shouldSuspend}) {
1204 + Scheduler.unstable_yieldValue(`Component ${shouldSuspend}`);
1205 + if (shouldSuspend) {
1206 + readValue(123);
1207 + }
1208 + return null;
1209 + }
1210 +
1211 + if (gate(flags => flags.enableSchedulingProfiler)) {
1212 + // Mount and commit the app
1213 + const root = ReactDOM.createRoot(document.createElement('div'));
1214 + act(() =>
1215 + root.render(
1216 + <React.Suspense fallback="Loading...">
1217 + <Component shouldSuspend={false} />
1218 + </React.Suspense>,
1219 + ),
1220 + );
1221 +
1222 + clearedMarks.splice(0);
1223 +
1224 + const testMarks = [creactCpuProfilerSample()];
1225 +
1226 + // Start profiling and suspend during a render.
1227 + act(() =>
1228 + root.render(
1229 + <React.Suspense fallback="Loading...">
1230 + <Component shouldSuspend={true} />
1231 + </React.Suspense>,
1232 + ),
1233 + );
1234 +
1235 + testMarks.push(...createUserTimingData(clearedMarks));
1236 +
1237 + const data = preprocessData(testMarks);
1238 + expect(data.suspenseEvents).toHaveLength(1);
1239 + expect(data.suspenseEvents[0].warning).toMatchInlineSnapshot(
1240 + `"A component suspended during an update which caused a fallback to be shown. Consider using the Transition API to avoid hiding components after they've been mounted."`,
1241 + );
1242 + }
1243 + });
1244 +
1245 + it('should not warn about suspending during an transition', async () => {
1246 + let promise = null;
1247 + let resolvedValue = null;
1248 + function readValue(value) {
1249 + if (resolvedValue !== null) {
1250 + return resolvedValue;
1251 + } else if (promise === null) {
1252 + promise = Promise.resolve(true).then(() => {
1253 + resolvedValue = value;
1254 + });
1255 + }
1256 + throw promise;
1257 + }
1258 +
1259 + function Component({shouldSuspend}) {
1260 + Scheduler.unstable_yieldValue(`Component ${shouldSuspend}`);
1261 + if (shouldSuspend) {
1262 + readValue(123);
1263 + }
1264 + return null;
1265 + }
1266 +
1267 + if (gate(flags => flags.enableSchedulingProfiler)) {
1268 + // Mount and commit the app
1269 + const root = ReactDOM.createRoot(document.createElement('div'));
1270 + act(() =>
1271 + root.render(
1272 + <React.Suspense fallback="Loading...">
1273 + <Component shouldSuspend={false} />
1274 + </React.Suspense>,
1275 + ),
1276 + );
1277 +
1278 + clearedMarks.splice(0);
1279 +
1280 + const testMarks = [creactCpuProfilerSample()];
1281 +
1282 + // Start profiling and suspend during a render.
1283 + await act(async () =>
1284 + React.startTransition(() =>
1285 + root.render(
1286 + <React.Suspense fallback="Loading...">
1287 + <Component shouldSuspend={true} />
1288 + </React.Suspense>,
1289 + ),
1290 + ),
1291 + );
1292 +
1293 + testMarks.push(...createUserTimingData(clearedMarks));
1294 +
1295 + const data = preprocessData(testMarks);
1296 + expect(data.suspenseEvents).toHaveLength(1);
1297 + expect(data.suspenseEvents[0].warning).toBe(null);
1298 + }
1299 + });
1300 + });
1301 + });
1302 +
1303 // TODO: Add test for flamechart parsing
1304 });
packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
+96 -36
@@ -22,11 +22,13 @@ import type {
22 ReactComponentMeasure,
23 ReactMeasureType,
24 ReactProfilerData,
25 + SchedulingEvent,
26 SuspenseEvent,
27 } from '../types';
28
29 import {REACT_TOTAL_NUM_LANES, SCHEDULING_PROFILER_VERSION} from '../constants';
30 import InvalidProfileError from './InvalidProfileError';
31 +import {getBatchRange} from '../utils/getBatchRange';
32
33 type MeasureStackElement = {|
34 type: ReactMeasureType,
@@ -42,6 +44,12 @@ type ProcessorState = {|
44 measureStack: MeasureStackElement[],
45 nativeEventStack: NativeEvent[],
46 nextRenderShouldGenerateNewBatchID: boolean,
47 + potentialLongEvents: Array<[NativeEvent, BatchUID]>,
48 + potentialLongNestedUpdate: SchedulingEvent | null,
49 + potentialLongNestedUpdates: Array<[SchedulingEvent, BatchUID]>,
50 + potentialSuspenseEventsOutsideOfTransition: Array<
51 + [SuspenseEvent, ReactLane[]],
52 + >,
53 uidCounter: BatchUID,
54 unresolvedSuspenseEvents: Map<string, SuspenseEvent>,
55 |};
@@ -52,7 +60,9 @@ const WARNING_STRINGS = {
60 LONG_EVENT_HANDLER:
61 'An event handler scheduled a big update with React. Consider using the Transition API to defer some of this work.',
62 NESTED_UPDATE:
55 - 'A nested update was scheduled during layout. These updates require React to re-render synchronously before the browser can paint.',
63 + 'A big nested update was scheduled during layout. ' +
64 + 'Nested updates require React to re-render synchronously before the browser can paint. ' +
65 + 'Consider delaying this update by moving it to a passive effect (useEffect).',
66 SUSPEND_DURING_UPATE:
67 'A component suspended during an update which caused a fallback to be shown. ' +
68 "Consider using the Transition API to avoid hiding components after they've been mounted.",
@@ -309,37 +319,39 @@ function processTimelineEvent(
319 } else if (name.startsWith('--schedule-forced-update-')) {
320 const [laneBitmaskString, componentName] = name.substr(25).split('-');
321
312 - let warning = null;
313 - if (state.measureStack.find(({type}) => type === 'commit')) {
314 - // TODO (scheduling profiler) Only warn if the subsequent update is longer than some threshold.
315 - // This might be easier to do if we separated warnings into a second pass.
316 - warning = WARNING_STRINGS.NESTED_UPDATE;
317 - }
318 -
319 - currentProfilerData.schedulingEvents.push({
322 + const forceUpdateEvent = {
323 type: 'schedule-force-update',
324 lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
325 componentName,
326 timestamp: startTime,
324 - warning,
325 - });
326 - } else if (name.startsWith('--schedule-state-update-')) {
327 - const [laneBitmaskString, componentName] = name.substr(24).split('-');
327 + warning: null,
328 + };
329
329 - let warning = null;
330 + // If this is a nested update, make a note of it.
331 + // Once we're done processing events, we'll check to see if it was a long update and warn about it.
332 if (state.measureStack.find(({type}) => type === 'commit')) {
331 - // TODO (scheduling profiler) Only warn if the subsequent update is longer than some threshold.
332 - // This might be easier to do if we separated warnings into a second pass.
333 - warning = WARNING_STRINGS.NESTED_UPDATE;
333 + state.potentialLongNestedUpdate = forceUpdateEvent;
334 }
335
336 - currentProfilerData.schedulingEvents.push({
336 + currentProfilerData.schedulingEvents.push(forceUpdateEvent);
337 + } else if (name.startsWith('--schedule-state-update-')) {
338 + const [laneBitmaskString, componentName] = name.substr(24).split('-');
339 +
340 + const stateUpdateEvent = {
341 type: 'schedule-state-update',
342 lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
343 componentName,
344 timestamp: startTime,
341 - warning,
342 - });
345 + warning: null,
346 + };
347 +
348 + // If this is a nested update, make a note of it.
349 + // Once we're done processing events, we'll check to see if it was a long update and warn about it.
350 + if (state.measureStack.find(({type}) => type === 'commit')) {
351 + state.potentialLongNestedUpdate = stateUpdateEvent;
352 + }
353 +
354 + currentProfilerData.schedulingEvents.push(stateUpdateEvent);
355 } // eslint-disable-line brace-style
356
357 // React Events - suspense
@@ -349,16 +361,6 @@ function processTimelineEvent(
361 .split('-');
362 const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
363
352 - // TODO It's possible we don't have lane-to-label mapping yet (since it's logged during commit phase)
353 - // We may need to do this sort of error checking in a separate pass.
354 - let warning = null;
355 - if (phase === 'update') {
356 - // HACK This is a bit gross but the numeric lane value might change between render versions.
357 - if (lanes.some(lane => laneToLabelMap.get(lane) === 'Transition')) {
358 - warning = WARNING_STRINGS.SUSPEND_DURING_UPATE;
359 - }
360 - }
361 -
364 const availableDepths = new Array(
365 state.unresolvedSuspenseEvents.size + 1,
366 ).fill(true);
@@ -389,9 +391,20 @@ function processTimelineEvent(
391 resuspendTimestamps: null,
392 timestamp: startTime,
393 type: 'suspense',
392 - warning,
394 + warning: null,
395 };
396
397 + if (phase === 'update') {
398 + // If a component suspended during an update, we should verify that it was during a transition.
399 + // We need the lane metadata to verify this though.
400 + // Since that data is only logged during commit, we may not have it yet.
401 + // Store these events for post-processing then.
402 + state.potentialSuspenseEventsOutsideOfTransition.push([
403 + suspenseEvent,
404 + lanes,
405 + ]);
406 + }
407 +
408 currentProfilerData.suspenseEvents.push(suspenseEvent);
409 state.unresolvedSuspenseEvents.set(id, suspenseEvent);
410 } else if (name.startsWith('--suspense-resuspend-')) {
@@ -430,6 +443,17 @@ function processTimelineEvent(
443 state.nextRenderShouldGenerateNewBatchID = false;
444 state.batchUID = ((state.uidCounter++: any): BatchUID);
445 }
446 +
447 + // If this render is the result of a nested update, make a note of it.
448 + // Once we're done processing events, we'll check to see if it was a long update and warn about it.
449 + if (state.potentialLongNestedUpdate !== null) {
450 + state.potentialLongNestedUpdates.push([
451 + state.potentialLongNestedUpdate,
452 + state.batchUID,
453 + ]);
454 + state.potentialLongNestedUpdate = null;
455 + }
456 +
457 const [laneBitmaskString] = name.substr(15).split('-');
458
459 throwIfIncomplete('render', state.measureStack);
@@ -453,11 +477,14 @@ function processTimelineEvent(
477 for (let i = 0; i < state.nativeEventStack.length; i++) {
478 const nativeEvent = state.nativeEventStack[i];
479 const stopTime = nativeEvent.timestamp + nativeEvent.duration;
456 - if (
457 - stopTime > startTime &&
458 - nativeEvent.duration > NATIVE_EVENT_DURATION_THRESHOLD
459 - ) {
460 - nativeEvent.warning = WARNING_STRINGS.LONG_EVENT_HANDLER;
480 +
481 + // If React work was scheduled during an event handler, and the event had a long duration,
482 + // it might be because the React render was long and stretched the event.
483 + // It might also be that the React work was short and that something else stretched the event.
484 + // Make a note of this event for now and we'll examine the batch of React render work later.
485 + // (We can't know until we're done processing the React update anyway.)
486 + if (stopTime > startTime) {
487 + state.potentialLongEvents.push([nativeEvent, state.batchUID]);
488 }
489 }
490 } else if (
@@ -666,6 +693,10 @@ export default function preprocessData(
693 measureStack: [],
694 nativeEventStack: [],
695 nextRenderShouldGenerateNewBatchID: true,
696 + potentialLongEvents: [],
697 + potentialLongNestedUpdate: null,
698 + potentialLongNestedUpdates: [],
699 + potentialSuspenseEventsOutsideOfTransition: [],
700 uidCounter: 0,
701 unresolvedSuspenseEvents: new Map(),
702 };
@@ -684,5 +715,34 @@ export default function preprocessData(
715 console.error('Incomplete events or measures', measureStack);
716 }
717
718 + // Check for warnings.
719 + state.potentialLongEvents.forEach(([nativeEvent, batchUID]) => {
720 + // See how long the subsequent batch of React work was.
721 + // Ignore any work that was already started.
722 + const [startTime, stopTime] = getBatchRange(
723 + batchUID,
724 + profilerData,
725 + nativeEvent.timestamp,
726 + );
727 + if (stopTime - startTime > NATIVE_EVENT_DURATION_THRESHOLD) {
728 + nativeEvent.warning = WARNING_STRINGS.LONG_EVENT_HANDLER;
729 + }
730 + });
731 + state.potentialLongNestedUpdates.forEach(([schedulingEvent, batchUID]) => {
732 + // See how long the subsequent batch of React work was.
733 + const [startTime, stopTime] = getBatchRange(batchUID, profilerData);
734 + if (stopTime - startTime > NATIVE_EVENT_DURATION_THRESHOLD) {
735 + schedulingEvent.warning = WARNING_STRINGS.NESTED_UPDATE;
736 + }
737 + });
738 + state.potentialSuspenseEventsOutsideOfTransition.forEach(
739 + ([suspenseEvent, lanes]) => {
740 + // HACK This is a bit gross but the numeric lane value might change between render versions.
741 + if (!lanes.some(lane => laneToLabelMap.get(lane) === 'Transition')) {
742 + suspenseEvent.warning = WARNING_STRINGS.SUSPEND_DURING_UPATE;
743 + }
744 + },
745 + );
746 +
747 return profilerData;
748 }
packages/react-devtools-scheduling-profiler/src/utils/getBatchRange.js
+10 -4
@@ -14,6 +14,7 @@ import type {BatchUID, Milliseconds, ReactProfilerData} from '../types';
14 function unmemoizedGetBatchRange(
15 batchUID: BatchUID,
16 data: ReactProfilerData,
17 + minStartTime?: ?number,
18 ): [Milliseconds, Milliseconds] {
19 const {measures} = data;
20
@@ -22,18 +23,23 @@ function unmemoizedGetBatchRange(
23
24 let i = 0;
25
26 + // Find the first measure in the current batch.
27 for (i; i < measures.length; i++) {
28 const measure = measures[i];
29 if (measure.batchUID === batchUID) {
28 - startTime = measure.timestamp;
29 - break;
30 + if (minStartTime == null || measure.timestamp >= minStartTime) {
31 + startTime = measure.timestamp;
32 + break;
33 + }
34 }
35 }
36
37 + // Find the last measure in the current batch.
38 for (i; i < measures.length; i++) {
39 const measure = measures[i];
35 - stopTime = measure.timestamp;
36 - if (measure.batchUID !== batchUID) {
40 + if (measure.batchUID === batchUID) {
41 + stopTime = measure.timestamp;
42 + } else {
43 break;
44 }
45 }