@samitouri / QOS-React-2 / commits / eaa493e532

Profiler should only report stateful hooks that change between renders (#24189)

The Profiler has an advanced feature that shows why a component re-rendered. In the case of props and (class) state, it shows the names of props/state values that changed between renders. For hooks, DevTools tries to detect which ones may been related to the update by comparing prev/next internal hook structures. My initial implementation tried to detect all changed hooks. In hindsight this is confusing, because only stateful hooks (e.g. useState, useReducer, and useSyncExternalStore) can schedule an update. (Other types of hooks can change between renders, but in a reactive way.) This PR changes the behavior to only report hooks that scheduled the update.

Brian Vaughn committed Mar 29, 2022 at 11:11 UTC eaa493e5325cec0d8ba41bc0a4bcb300d1a72b7d
3 files changed +523 -442
packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap
+334 -350
@@ -4543,245 +4543,6 @@ Object {
4543 }
4544 `;
4545
4546 -exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 0 1`] = `
4547 -Object {
4548 - "changeDescriptions": Map {
4549 - 4 => Object {
4550 - "context": null,
4551 - "didHooksChange": false,
4552 - "isFirstMount": true,
4553 - "props": null,
4554 - "state": null,
4555 - },
4556 - },
4557 - "duration": 0,
4558 - "effectDuration": 0,
4559 - "fiberActualDurations": Map {
4560 - 1 => 0,
4561 - 2 => 0,
4562 - 3 => 0,
4563 - 4 => 0,
4564 - },
4565 - "fiberSelfDurations": Map {
4566 - 1 => 0,
4567 - 2 => 0,
4568 - 3 => 0,
4569 - 4 => 0,
4570 - },
4571 - "passiveEffectDuration": 0,
4572 - "priorityLevel": "Immediate",
4573 - "timestamp": 0,
4574 - "updaters": Array [
4575 - Object {
4576 - "displayName": "render()",
4577 - "hocDisplayNames": null,
4578 - "id": 1,
4579 - "key": null,
4580 - "type": 11,
4581 - },
4582 - ],
4583 -}
4584 -`;
4585 -
4586 -exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 1 1`] = `
4587 -Object {
4588 - "changeDescriptions": Map {
4589 - 4 => Object {
4590 - "context": false,
4591 - "didHooksChange": false,
4592 - "hooks": Array [],
4593 - "isFirstMount": false,
4594 - "props": Array [
4595 - "count",
4596 - ],
4597 - "state": null,
4598 - },
4599 - },
4600 - "duration": 0,
4601 - "effectDuration": null,
4602 - "fiberActualDurations": Map {
4603 - 4 => 0,
4604 - 3 => 0,
4605 - 2 => 0,
4606 - 1 => 0,
4607 - },
4608 - "fiberSelfDurations": Map {
4609 - 4 => 0,
4610 - 3 => 0,
4611 - 2 => 0,
4612 - 1 => 0,
4613 - },
4614 - "passiveEffectDuration": null,
4615 - "priorityLevel": "Immediate",
4616 - "timestamp": 0,
4617 - "updaters": Array [
4618 - Object {
4619 - "displayName": "render()",
4620 - "hocDisplayNames": null,
4621 - "id": 1,
4622 - "key": null,
4623 - "type": 11,
4624 - },
4625 - ],
4626 -}
4627 -`;
4628 -
4629 -exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 2 1`] = `
4630 -Object {
4631 - "changeDescriptions": Map {
4632 - 4 => Object {
4633 - "context": false,
4634 - "didHooksChange": true,
4635 - "hooks": Array [
4636 - 1,
4637 - ],
4638 - "isFirstMount": false,
4639 - "props": Array [],
4640 - "state": null,
4641 - },
4642 - },
4643 - "duration": 0,
4644 - "effectDuration": null,
4645 - "fiberActualDurations": Map {
4646 - 4 => 0,
4647 - },
4648 - "fiberSelfDurations": Map {
4649 - 4 => 0,
4650 - },
4651 - "passiveEffectDuration": null,
4652 - "priorityLevel": "Immediate",
4653 - "timestamp": 0,
4654 - "updaters": Array [
4655 - Object {
4656 - "displayName": "Component",
4657 - "hocDisplayNames": null,
4658 - "id": 4,
4659 - "key": null,
4660 - "type": 5,
4661 - },
4662 - ],
4663 -}
4664 -`;
4665 -
4666 -exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 3 1`] = `
4667 -Object {
4668 - "changeDescriptions": Map {
4669 - 4 => Object {
4670 - "context": false,
4671 - "didHooksChange": true,
4672 - "hooks": Array [
4673 - 0,
4674 - ],
4675 - "isFirstMount": false,
4676 - "props": Array [],
4677 - "state": null,
4678 - },
4679 - },
4680 - "duration": 0,
4681 - "effectDuration": null,
4682 - "fiberActualDurations": Map {
4683 - 4 => 0,
4684 - },
4685 - "fiberSelfDurations": Map {
4686 - 4 => 0,
4687 - },
4688 - "passiveEffectDuration": null,
4689 - "priorityLevel": "Immediate",
4690 - "timestamp": 0,
4691 - "updaters": Array [
4692 - Object {
4693 - "displayName": "Component",
4694 - "hocDisplayNames": null,
4695 - "id": 4,
4696 - "key": null,
4697 - "type": 5,
4698 - },
4699 - ],
4700 -}
4701 -`;
4702 -
4703 -exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 4 1`] = `
4704 -Object {
4705 - "changeDescriptions": Map {
4706 - 4 => Object {
4707 - "context": true,
4708 - "didHooksChange": false,
4709 - "hooks": Array [],
4710 - "isFirstMount": false,
4711 - "props": Array [],
4712 - "state": null,
4713 - },
4714 - },
4715 - "duration": 0,
4716 - "effectDuration": null,
4717 - "fiberActualDurations": Map {
4718 - 4 => 0,
4719 - 3 => 0,
4720 - 2 => 0,
4721 - 1 => 0,
4722 - },
4723 - "fiberSelfDurations": Map {
4724 - 4 => 0,
4725 - 3 => 0,
4726 - 2 => 0,
4727 - 1 => 0,
4728 - },
4729 - "passiveEffectDuration": null,
4730 - "priorityLevel": "Immediate",
4731 - "timestamp": 0,
4732 - "updaters": Array [
4733 - Object {
4734 - "displayName": "render()",
4735 - "hocDisplayNames": null,
4736 - "id": 1,
4737 - "key": null,
4738 - "type": 11,
4739 - },
4740 - ],
4741 -}
4742 -`;
4743 -
4744 -exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 5 1`] = `
4745 -Object {
4746 - "changeDescriptions": Map {
4747 - 4 => Object {
4748 - "context": true,
4749 - "didHooksChange": false,
4750 - "hooks": Array [],
4751 - "isFirstMount": false,
4752 - "props": Array [],
4753 - "state": null,
4754 - },
4755 - },
4756 - "duration": 0,
4757 - "effectDuration": null,
4758 - "fiberActualDurations": Map {
4759 - 4 => 0,
4760 - 3 => 0,
4761 - 2 => 0,
4762 - 1 => 0,
4763 - },
4764 - "fiberSelfDurations": Map {
4765 - 4 => 0,
4766 - 3 => 0,
4767 - 2 => 0,
4768 - 1 => 0,
4769 - },
4770 - "passiveEffectDuration": null,
4771 - "priorityLevel": "Immediate",
4772 - "timestamp": 0,
4773 - "updaters": Array [
4774 - Object {
4775 - "displayName": "render()",
4776 - "hocDisplayNames": null,
4777 - "id": 1,
4778 - "key": null,
4779 - "type": 11,
4780 - },
4781 - ],
4782 -}
4783 -`;
4784 -
4546 exports[`ProfilingCache should properly detect changed hooks: imported data 1`] = `
4547 Object {
4548 "dataForRoots": Array [
@@ -4790,7 +4551,7 @@ Object {
4551 Object {
4552 "changeDescriptions": Array [
4553 Array [
4793 - 4,
4554 + 3,
4555 Object {
4556 "context": null,
4557 "didHooksChange": false,
@@ -4815,10 +4576,6 @@ Object {
4576 3,
4577 0,
4578 ],
4818 - Array [
4819 - 4,
4820 - 0,
4821 - ],
4579 ],
4580 "fiberSelfDurations": Array [
4581 Array [
@@ -4833,10 +4590,6 @@ Object {
4590 3,
4591 0,
4592 ],
4836 - Array [
4837 - 4,
4838 - 0,
4839 - ],
4593 ],
4594 "passiveEffectDuration": 0,
4595 "priorityLevel": "Immediate",
@@ -4854,7 +4607,7 @@ Object {
4607 Object {
4608 "changeDescriptions": Array [
4609 Array [
4857 - 4,
4610 + 3,
4611 Object {
4612 "context": false,
4613 "didHooksChange": false,
@@ -4868,12 +4621,8 @@ Object {
4621 ],
4622 ],
4623 "duration": 0,
4871 - "effectDuration": null,
4624 + "effectDuration": 0,
4625 "fiberActualDurations": Array [
4873 - Array [
4874 - 4,
4875 - 0,
4876 - ],
4626 Array [
4627 3,
4628 0,
@@ -4888,10 +4637,6 @@ Object {
4637 ],
4638 ],
4639 "fiberSelfDurations": Array [
4891 - Array [
4892 - 4,
4893 - 0,
4894 - ],
4640 Array [
4641 3,
4642 0,
@@ -4905,7 +4650,7 @@ Object {
4650 0,
4651 ],
4652 ],
4908 - "passiveEffectDuration": null,
4653 + "passiveEffectDuration": 0,
4654 "priorityLevel": "Immediate",
4655 "timestamp": 0,
4656 "updaters": Array [
@@ -4921,7 +4666,7 @@ Object {
4666 Object {
4667 "changeDescriptions": Array [
4668 Array [
4924 - 4,
4669 + 3,
4670 Object {
4671 "context": false,
4672 "didHooksChange": true,
@@ -4935,27 +4680,27 @@ Object {
4680 ],
4681 ],
4682 "duration": 0,
4938 - "effectDuration": null,
4683 + "effectDuration": 0,
4684 "fiberActualDurations": Array [
4685 Array [
4941 - 4,
4686 + 3,
4687 0,
4688 ],
4689 ],
4690 "fiberSelfDurations": Array [
4691 Array [
4947 - 4,
4692 + 3,
4693 0,
4694 ],
4695 ],
4951 - "passiveEffectDuration": null,
4696 + "passiveEffectDuration": 0,
4697 "priorityLevel": "Immediate",
4698 "timestamp": 0,
4699 "updaters": Array [
4700 Object {
4701 "displayName": "Component",
4702 "hocDisplayNames": null,
4958 - "id": 4,
4703 + "id": 3,
4704 "key": null,
4705 "type": 5,
4706 },
@@ -4964,7 +4709,7 @@ Object {
4709 Object {
4710 "changeDescriptions": Array [
4711 Array [
4967 - 4,
4712 + 3,
4713 Object {
4714 "context": false,
4715 "didHooksChange": true,
@@ -4978,27 +4723,27 @@ Object {
4723 ],
4724 ],
4725 "duration": 0,
4981 - "effectDuration": null,
4726 + "effectDuration": 0,
4727 "fiberActualDurations": Array [
4728 Array [
4984 - 4,
4729 + 3,
4730 0,
4731 ],
4732 ],
4733 "fiberSelfDurations": Array [
4734 Array [
4990 - 4,
4735 + 3,
4736 0,
4737 ],
4738 ],
4994 - "passiveEffectDuration": null,
4739 + "passiveEffectDuration": 0,
4740 "priorityLevel": "Immediate",
4741 "timestamp": 0,
4742 "updaters": Array [
4743 Object {
4744 "displayName": "Component",
4745 "hocDisplayNames": null,
5001 - "id": 4,
4746 + "id": 3,
4747 "key": null,
4748 "type": 5,
4749 },
@@ -5007,7 +4752,7 @@ Object {
4752 Object {
4753 "changeDescriptions": Array [
4754 Array [
5010 - 4,
4755 + 3,
4756 Object {
4757 "context": true,
4758 "didHooksChange": false,
@@ -5019,12 +4764,8 @@ Object {
4764 ],
4765 ],
4766 "duration": 0,
5022 - "effectDuration": null,
4767 + "effectDuration": 0,
4768 "fiberActualDurations": Array [
5024 - Array [
5025 - 4,
5026 - 0,
5027 - ],
4769 Array [
4770 3,
4771 0,
@@ -5039,10 +4780,6 @@ Object {
4780 ],
4781 ],
4782 "fiberSelfDurations": Array [
5042 - Array [
5043 - 4,
5044 - 0,
5045 - ],
4783 Array [
4784 3,
4785 0,
@@ -5056,7 +4793,7 @@ Object {
4793 0,
4794 ],
4795 ],
5059 - "passiveEffectDuration": null,
4796 + "passiveEffectDuration": 0,
4797 "priorityLevel": "Immediate",
4798 "timestamp": 0,
4799 "updaters": Array [
@@ -5072,11 +4809,13 @@ Object {
4809 Object {
4810 "changeDescriptions": Array [
4811 Array [
5075 - 4,
4812 + 3,
4813 Object {
5077 - "context": true,
5078 - "didHooksChange": false,
5079 - "hooks": Array [],
4814 + "context": false,
4815 + "didHooksChange": true,
4816 + "hooks": Array [
4817 + 2,
4818 + ],
4819 "isFirstMount": false,
4820 "props": Array [],
4821 "state": null,
@@ -5084,53 +4823,29 @@ Object {
4823 ],
4824 ],
4825 "duration": 0,
5087 - "effectDuration": null,
4826 + "effectDuration": 0,
4827 "fiberActualDurations": Array [
5089 - Array [
5090 - 4,
5091 - 0,
5092 - ],
4828 Array [
4829 3,
4830 0,
4831 ],
5097 - Array [
5098 - 2,
5099 - 0,
5100 - ],
5101 - Array [
5102 - 1,
5103 - 0,
5104 - ],
4832 ],
4833 "fiberSelfDurations": Array [
5107 - Array [
5108 - 4,
5109 - 0,
5110 - ],
4834 Array [
4835 3,
4836 0,
4837 ],
5115 - Array [
5116 - 2,
5117 - 0,
5118 - ],
5119 - Array [
5120 - 1,
5121 - 0,
5122 - ],
4838 ],
5124 - "passiveEffectDuration": null,
4839 + "passiveEffectDuration": 0,
4840 "priorityLevel": "Immediate",
4841 "timestamp": 0,
4842 "updaters": Array [
4843 Object {
5129 - "displayName": "render()",
4844 + "displayName": "Component",
4845 "hocDisplayNames": null,
5131 - "id": 1,
4846 + "id": 3,
4847 "key": null,
5133 - "type": 11,
4848 + "type": 5,
4849 },
4850 ],
4851 },
@@ -5188,23 +4903,13 @@ Object {
4903 0,
4904 1,
4905 3,
5191 - 2,
5192 - 2,
5193 - 0,
5194 - 1,
5195 - 0,
5196 - 4,
5197 - 3,
5198 - 0,
5199 - 1,
5200 - 4,
4906 5,
5202 - 3,
4907 + 2,
4908 0,
4909 2,
4910 0,
4911 4,
5207 - 4,
4912 + 3,
4913 0,
4914 ],
4915 Array [
@@ -5312,6 +5017,22 @@ Object {
5017 "timestamp": 10,
5018 "type": "commit",
5019 },
5020 + Object {
5021 + "batchUID": 2,
5022 + "depth": 1,
5023 + "duration": 0,
5024 + "lanes": "0b0000000000000000000000000000001",
5025 + "timestamp": 10,
5026 + "type": "layout-effects",
5027 + },
5028 + Object {
5029 + "batchUID": 2,
5030 + "depth": 0,
5031 + "duration": 0,
5032 + "lanes": "0b0000000000000000000000000000001",
5033 + "timestamp": 10,
5034 + "type": "passive-effects",
5035 + },
5036 ],
5037 ],
5038 Array [
@@ -5341,6 +5062,22 @@ Object {
5062 "timestamp": 10,
5063 "type": "commit",
5064 },
5065 + Object {
5066 + "batchUID": 3,
5067 + "depth": 1,
5068 + "duration": 0,
5069 + "lanes": "0b0000000000000000000000000000001",
5070 + "timestamp": 10,
5071 + "type": "layout-effects",
5072 + },
5073 + Object {
5074 + "batchUID": 3,
5075 + "depth": 0,
5076 + "duration": 0,
5077 + "lanes": "0b0000000000000000000000000000001",
5078 + "timestamp": 10,
5079 + "type": "passive-effects",
5080 + },
5081 ],
5082 ],
5083 Array [
@@ -5370,6 +5107,22 @@ Object {
5107 "timestamp": 10,
5108 "type": "commit",
5109 },
5110 + Object {
5111 + "batchUID": 4,
5112 + "depth": 1,
5113 + "duration": 0,
5114 + "lanes": "0b0000000000000000000000000000001",
5115 + "timestamp": 10,
5116 + "type": "layout-effects",
5117 + },
5118 + Object {
5119 + "batchUID": 4,
5120 + "depth": 0,
5121 + "duration": 0,
5122 + "lanes": "0b0000000000000000000000000000001",
5123 + "timestamp": 10,
5124 + "type": "passive-effects",
5125 + },
5126 ],
5127 ],
5128 Array [
@@ -5397,7 +5150,23 @@ Object {
5150 "duration": 0,
5151 "lanes": "0b0000000000000000000000000000001",
5152 "timestamp": 10,
5400 - "type": "commit",
5153 + "type": "commit",
5154 + },
5155 + Object {
5156 + "batchUID": 5,
5157 + "depth": 1,
5158 + "duration": 0,
5159 + "lanes": "0b0000000000000000000000000000001",
5160 + "timestamp": 10,
5161 + "type": "layout-effects",
5162 + },
5163 + Object {
5164 + "batchUID": 5,
5165 + "depth": 0,
5166 + "duration": 0,
5167 + "lanes": "0b0000000000000000000000000000001",
5168 + "timestamp": 10,
5169 + "type": "passive-effects",
5170 },
5171 ],
5172 ],
@@ -5428,6 +5197,22 @@ Object {
5197 "timestamp": 10,
5198 "type": "commit",
5199 },
5200 + Object {
5201 + "batchUID": 6,
5202 + "depth": 1,
5203 + "duration": 0,
5204 + "lanes": "0b0000000000000000000000000000001",
5205 + "timestamp": 10,
5206 + "type": "layout-effects",
5207 + },
5208 + Object {
5209 + "batchUID": 6,
5210 + "depth": 0,
5211 + "duration": 0,
5212 + "lanes": "0b0000000000000000000000000000001",
5213 + "timestamp": 10,
5214 + "type": "passive-effects",
5215 + },
5216 ],
5217 ],
5218 ],
@@ -5446,6 +5231,48 @@ Object {
5231 "type": "layout-effect-mount",
5232 "warning": null,
5233 },
5234 + Object {
5235 + "componentName": "Component",
5236 + "duration": 0,
5237 + "timestamp": 10,
5238 + "type": "layout-effect-mount",
5239 + "warning": null,
5240 + },
5241 + Object {
5242 + "componentName": "Component",
5243 + "duration": 0,
5244 + "timestamp": 10,
5245 + "type": "layout-effect-mount",
5246 + "warning": null,
5247 + },
5248 + Object {
5249 + "componentName": "Component",
5250 + "duration": 0,
5251 + "timestamp": 10,
5252 + "type": "passive-effect-mount",
5253 + "warning": null,
5254 + },
5255 + Object {
5256 + "componentName": "Component",
5257 + "duration": 0,
5258 + "timestamp": 10,
5259 + "type": "passive-effect-mount",
5260 + "warning": null,
5261 + },
5262 + Object {
5263 + "componentName": "Component",
5264 + "duration": 0,
5265 + "timestamp": 10,
5266 + "type": "passive-effect-mount",
5267 + "warning": null,
5268 + },
5269 + Object {
5270 + "componentName": "Component",
5271 + "duration": 0,
5272 + "timestamp": 10,
5273 + "type": "passive-effect-mount",
5274 + "warning": null,
5275 + },
5276 Object {
5277 "componentName": "Component",
5278 "duration": 0,
@@ -5460,6 +5287,34 @@ Object {
5287 "type": "render",
5288 "warning": null,
5289 },
5290 + Object {
5291 + "componentName": "Component",
5292 + "duration": 0,
5293 + "timestamp": 10,
5294 + "type": "layout-effect-mount",
5295 + "warning": null,
5296 + },
5297 + Object {
5298 + "componentName": "Component",
5299 + "duration": 0,
5300 + "timestamp": 10,
5301 + "type": "layout-effect-mount",
5302 + "warning": null,
5303 + },
5304 + Object {
5305 + "componentName": "Component",
5306 + "duration": 0,
5307 + "timestamp": 10,
5308 + "type": "passive-effect-mount",
5309 + "warning": null,
5310 + },
5311 + Object {
5312 + "componentName": "Component",
5313 + "duration": 0,
5314 + "timestamp": 10,
5315 + "type": "passive-effect-mount",
5316 + "warning": null,
5317 + },
5318 Object {
5319 "componentName": "Component",
5320 "duration": 0,
@@ -5467,6 +5322,20 @@ Object {
5322 "type": "render",
5323 "warning": null,
5324 },
5325 + Object {
5326 + "componentName": "Component",
5327 + "duration": 0,
5328 + "timestamp": 10,
5329 + "type": "layout-effect-mount",
5330 + "warning": null,
5331 + },
5332 + Object {
5333 + "componentName": "Component",
5334 + "duration": 0,
5335 + "timestamp": 10,
5336 + "type": "passive-effect-mount",
5337 + "warning": null,
5338 + },
5339 Object {
5340 "componentName": "Component",
5341 "duration": 0,
@@ -5474,6 +5343,20 @@ Object {
5343 "type": "render",
5344 "warning": null,
5345 },
5346 + Object {
5347 + "componentName": "Component",
5348 + "duration": 0,
5349 + "timestamp": 10,
5350 + "type": "layout-effect-mount",
5351 + "warning": null,
5352 + },
5353 + Object {
5354 + "componentName": "Component",
5355 + "duration": 0,
5356 + "timestamp": 10,
5357 + "type": "passive-effect-mount",
5358 + "warning": null,
5359 + },
5360 Object {
5361 "componentName": "Component",
5362 "duration": 0,
@@ -5481,6 +5364,20 @@ Object {
5364 "type": "render",
5365 "warning": null,
5366 },
5367 + Object {
5368 + "componentName": "Component",
5369 + "duration": 0,
5370 + "timestamp": 10,
5371 + "type": "layout-effect-mount",
5372 + "warning": null,
5373 + },
5374 + Object {
5375 + "componentName": "Component",
5376 + "duration": 0,
5377 + "timestamp": 10,
5378 + "type": "passive-effect-mount",
5379 + "warning": null,
5380 + },
5381 Object {
5382 "componentName": "Component",
5383 "duration": 0,
@@ -5488,6 +5385,27 @@ Object {
5385 "type": "render",
5386 "warning": null,
5387 },
5388 + Object {
5389 + "componentName": "Component",
5390 + "duration": 0,
5391 + "timestamp": 10,
5392 + "type": "layout-effect-mount",
5393 + "warning": null,
5394 + },
5395 + Object {
5396 + "componentName": "Component",
5397 + "duration": 0,
5398 + "timestamp": 10,
5399 + "type": "passive-effect-mount",
5400 + "warning": null,
5401 + },
5402 + Object {
5403 + "componentName": "Component",
5404 + "duration": 0,
5405 + "timestamp": 10,
5406 + "type": "passive-effect-mount",
5407 + "warning": null,
5408 + },
5409 ],
5410 "duration": 20,
5411 "flamechart": Array [],
@@ -5686,6 +5604,22 @@ Object {
5604 "timestamp": 10,
5605 "type": "commit",
5606 },
5607 + Object {
5608 + "batchUID": 2,
5609 + "depth": 1,
5610 + "duration": 0,
5611 + "lanes": "0b0000000000000000000000000000001",
5612 + "timestamp": 10,
5613 + "type": "layout-effects",
5614 + },
5615 + Object {
5616 + "batchUID": 2,
5617 + "depth": 0,
5618 + "duration": 0,
5619 + "lanes": "0b0000000000000000000000000000001",
5620 + "timestamp": 10,
5621 + "type": "passive-effects",
5622 + },
5623 Object {
5624 "batchUID": 3,
5625 "depth": 0,
@@ -5710,6 +5644,22 @@ Object {
5644 "timestamp": 10,
5645 "type": "commit",
5646 },
5647 + Object {
5648 + "batchUID": 3,
5649 + "depth": 1,
5650 + "duration": 0,
5651 + "lanes": "0b0000000000000000000000000000001",
5652 + "timestamp": 10,
5653 + "type": "layout-effects",
5654 + },
5655 + Object {
5656 + "batchUID": 3,
5657 + "depth": 0,
5658 + "duration": 0,
5659 + "lanes": "0b0000000000000000000000000000001",
5660 + "timestamp": 10,
5661 + "type": "passive-effects",
5662 + },
5663 Object {
5664 "batchUID": 4,
5665 "depth": 0,
@@ -5734,6 +5684,22 @@ Object {
5684 "timestamp": 10,
5685 "type": "commit",
5686 },
5687 + Object {
5688 + "batchUID": 4,
5689 + "depth": 1,
5690 + "duration": 0,
5691 + "lanes": "0b0000000000000000000000000000001",
5692 + "timestamp": 10,
5693 + "type": "layout-effects",
5694 + },
5695 + Object {
5696 + "batchUID": 4,
5697 + "depth": 0,
5698 + "duration": 0,
5699 + "lanes": "0b0000000000000000000000000000001",
5700 + "timestamp": 10,
5701 + "type": "passive-effects",
5702 + },
5703 Object {
5704 "batchUID": 5,
5705 "depth": 0,
@@ -5758,6 +5724,22 @@ Object {
5724 "timestamp": 10,
5725 "type": "commit",
5726 },
5727 + Object {
5728 + "batchUID": 5,
5729 + "depth": 1,
5730 + "duration": 0,
5731 + "lanes": "0b0000000000000000000000000000001",
5732 + "timestamp": 10,
5733 + "type": "layout-effects",
5734 + },
5735 + Object {
5736 + "batchUID": 5,
5737 + "depth": 0,
5738 + "duration": 0,
5739 + "lanes": "0b0000000000000000000000000000001",
5740 + "timestamp": 10,
5741 + "type": "passive-effects",
5742 + },
5743 Object {
5744 "batchUID": 6,
5745 "depth": 0,
@@ -5782,6 +5764,22 @@ Object {
5764 "timestamp": 10,
5765 "type": "commit",
5766 },
5767 + Object {
5768 + "batchUID": 6,
5769 + "depth": 1,
5770 + "duration": 0,
5771 + "lanes": "0b0000000000000000000000000000001",
5772 + "timestamp": 10,
5773 + "type": "layout-effects",
5774 + },
5775 + Object {
5776 + "batchUID": 6,
5777 + "depth": 0,
5778 + "duration": 0,
5779 + "lanes": "0b0000000000000000000000000000001",
5780 + "timestamp": 10,
5781 + "type": "passive-effects",
5782 + },
5783 ],
5784 ],
5785 Array [
@@ -5942,12 +5940,6 @@ Object {
5940 "type": "schedule-render",
5941 "warning": null,
5942 },
5945 - Object {
5946 - "lanes": "0b0000000000000000000000000000001",
5947 - "timestamp": 10,
5948 - "type": "schedule-render",
5949 - "warning": null,
5950 - },
5943 ],
5944 "snapshotHeight": 0,
5945 "snapshots": Array [],
@@ -6039,10 +6031,8 @@ Object {
6031 "changeDescriptions": Map {
6032 5 => Object {
6033 "context": null,
6042 - "didHooksChange": true,
6043 - "hooks": Array [
6044 - 0,
6045 - ],
6034 + "didHooksChange": false,
6035 + "hooks": Array [],
6036 "isFirstMount": false,
6037 "props": Array [
6038 "count",
@@ -6059,10 +6049,8 @@ Object {
6049 },
6050 7 => Object {
6051 "context": null,
6062 - "didHooksChange": true,
6063 - "hooks": Array [
6064 - 0,
6065 - ],
6052 + "didHooksChange": false,
6053 + "hooks": Array [],
6054 "isFirstMount": false,
6055 "props": Array [
6056 "count",
@@ -6506,10 +6494,8 @@ Object {
6494 5,
6495 Object {
6496 "context": null,
6509 - "didHooksChange": true,
6510 - "hooks": Array [
6511 - 0,
6512 - ],
6497 + "didHooksChange": false,
6498 + "hooks": Array [],
6499 "isFirstMount": false,
6500 "props": Array [
6501 "count",
@@ -6532,10 +6518,8 @@ Object {
6518 7,
6519 Object {
6520 "context": null,
6535 - "didHooksChange": true,
6536 - "hooks": Array [
6537 - 0,
6538 - ],
6521 + "didHooksChange": false,
6522 + "hooks": Array [],
6523 "isFirstMount": false,
6524 "props": Array [
6525 "count",
packages/react-devtools-shared/src/__tests__/profilingCache-test.js
+162 -55
@@ -313,7 +313,6 @@ describe('ProfilingCache', () => {
313
314 it('should properly detect changed hooks', () => {
315 const Context = React.createContext(0);
316 - const Context2 = React.createContext(0);
316
317 function reducer(state, action) {
318 switch (action.type) {
@@ -324,6 +323,19 @@ describe('ProfilingCache', () => {
323 }
324 }
325
326 + let snapshot = 0;
327 + function getServerSnapshot() {
328 + return snapshot;
329 + }
330 + function getClientSnapshot() {
331 + return snapshot;
332 + }
333 +
334 + let syncExternalStoreCallback;
335 + function subscribe(callback) {
336 + syncExternalStoreCallback = callback;
337 + }
338 +
339 let dispatch = null;
340 let setState = null;
341
@@ -331,20 +343,31 @@ describe('ProfilingCache', () => {
343 // These hooks may change and initiate re-renders.
344 setState = React.useState('abc')[1];
345 dispatch = React.useReducer(reducer, {value: true})[1];
346 + React.useSyncExternalStore(
347 + subscribe,
348 + getClientSnapshot,
349 + getServerSnapshot,
350 + );
351
352 // This hook's return value may change between renders,
353 // but the hook itself isn't stateful.
354 React.useContext(Context);
338 - React.useContext(Context2);
355
340 - // These hooks and their dependencies may not change between renders.
341 - // We're using them to ensure that they don't trigger false positives.
356 + // These hooks never change in a way that schedules an update.
357 React.useCallback(() => () => {}, [string]);
358 React.useMemo(() => string, [string]);
359 + React.useCallback(() => () => {}, [count]);
360 + React.useMemo(() => count, [count]);
361 + React.useCallback(() => () => {});
362 + React.useMemo(() => string);
363
345 - // These hooks never "change".
364 + // These hooks never change in a way that schedules an update.
365 React.useEffect(() => {}, [string]);
366 React.useLayoutEffect(() => {}, [string]);
367 + React.useEffect(() => {}, [count]);
368 + React.useLayoutEffect(() => {}, [count]);
369 + React.useEffect(() => {});
370 + React.useLayoutEffect(() => {});
371
372 return null;
373 };
@@ -355,9 +378,7 @@ describe('ProfilingCache', () => {
378 utils.act(() =>
379 legacyRender(
380 <Context.Provider value={true}>
358 - <Context2.Provider value={true}>
359 - <Component count={1} />
360 - </Context2.Provider>
381 + <Component count={1} />
382 </Context.Provider>,
383 container,
384 ),
@@ -367,87 +388,173 @@ describe('ProfilingCache', () => {
388 utils.act(() =>
389 legacyRender(
390 <Context.Provider value={true}>
370 - <Context2.Provider value={true}>
371 - <Component count={2} />
372 - </Context2.Provider>
391 + <Component count={2} />
392 </Context.Provider>,
393 container,
394 ),
395 );
396
378 - // Third render has a changed reducer hook
397 + // Third render has a changed reducer hook.
398 utils.act(() => dispatch({type: 'invert'}));
399
381 - // Fourth render has a changed state hook
400 + // Fourth render has a changed state hook.
401 utils.act(() => setState('def'));
402
384 - // Fifth render has a changed context value for context 1, but no changed hook.
403 + // Fifth render has a changed context value, but no changed hook.
404 utils.act(() =>
405 legacyRender(
406 <Context.Provider value={false}>
388 - <Context2.Provider value={true}>
389 - <Component count={2} />
390 - </Context2.Provider>
407 + <Component count={2} />
408 </Context.Provider>,
409 container,
410 ),
411 );
412
396 - // Sixth render has another changed context value for context 2, but no changed hook.
397 - utils.act(() =>
398 - legacyRender(
399 - <Context.Provider value={false}>
400 - <Context2.Provider value={false}>
401 - <Component count={2} />
402 - </Context2.Provider>
403 - </Context.Provider>,
404 - container,
405 - ),
406 - );
413 + // 6th renderer is triggered by a sync external store change.
414 + utils.act(() => {
415 + snapshot++;
416 + syncExternalStoreCallback();
417 + });
418 +
419 utils.act(() => store.profilerStore.stopProfiling());
420
409 - const allCommitData = [];
421 + const rootID = store.roots[0];
422
411 - function Validator({commitIndex, previousCommitDetails, rootID}) {
412 - const commitData = store.profilerStore.getCommitData(rootID, commitIndex);
413 - if (previousCommitDetails != null) {
414 - expect(commitData).toEqual(previousCommitDetails);
415 - } else {
416 - allCommitData.push(commitData);
417 - expect(commitData).toMatchSnapshot(
418 - `CommitDetails commitIndex: ${commitIndex}`,
423 + const allChangeDescriptions = [];
424 +
425 + function getChangeDescriptions(commitIndex, label) {
426 + let changeDescriptions;
427 +
428 + function Validator() {
429 + const commitData = store.profilerStore.getCommitData(
430 + rootID,
431 + commitIndex,
432 );
420 - }
421 - return null;
422 - }
433
424 - const rootID = store.roots[0];
434 + changeDescriptions = commitData.changeDescriptions;
435 +
436 + allChangeDescriptions.push(changeDescriptions);
437 +
438 + return null;
439 + }
440
426 - for (let commitIndex = 0; commitIndex < 6; commitIndex++) {
441 utils.act(() => {
428 - TestRenderer.create(
429 - <Validator
430 - commitIndex={commitIndex}
431 - previousCommitDetails={null}
432 - rootID={rootID}
433 - />,
434 - );
442 + TestRenderer.create(<Validator />);
443 });
444 +
445 + return changeDescriptions;
446 }
447
438 - expect(allCommitData).toHaveLength(6);
448 + // 1st render: No change
449 + expect(getChangeDescriptions(0)).toMatchInlineSnapshot(`
450 + Map {
451 + 3 => Object {
452 + "context": null,
453 + "didHooksChange": false,
454 + "isFirstMount": true,
455 + "props": null,
456 + "state": null,
457 + },
458 + }
459 + `);
460 +
461 + // 2nd render: Changed props
462 + expect(getChangeDescriptions(1)).toMatchInlineSnapshot(`
463 + Map {
464 + 3 => Object {
465 + "context": false,
466 + "didHooksChange": false,
467 + "hooks": Array [],
468 + "isFirstMount": false,
469 + "props": Array [
470 + "count",
471 + ],
472 + "state": null,
473 + },
474 + }
475 + `);
476 +
477 + // 3rd render: Changed useReducer
478 + expect(getChangeDescriptions(2)).toMatchInlineSnapshot(`
479 + Map {
480 + 3 => Object {
481 + "context": false,
482 + "didHooksChange": true,
483 + "hooks": Array [
484 + 1,
485 + ],
486 + "isFirstMount": false,
487 + "props": Array [],
488 + "state": null,
489 + },
490 + }
491 + `);
492 +
493 + // 4th render: Changed useState
494 + expect(getChangeDescriptions(3)).toMatchInlineSnapshot(`
495 + Map {
496 + 3 => Object {
497 + "context": false,
498 + "didHooksChange": true,
499 + "hooks": Array [
500 + 0,
501 + ],
502 + "isFirstMount": false,
503 + "props": Array [],
504 + "state": null,
505 + },
506 + }
507 + `);
508 +
509 + // 5th render: Changed context
510 + expect(getChangeDescriptions(4)).toMatchInlineSnapshot(`
511 + Map {
512 + 3 => Object {
513 + "context": true,
514 + "didHooksChange": false,
515 + "hooks": Array [],
516 + "isFirstMount": false,
517 + "props": Array [],
518 + "state": null,
519 + },
520 + }
521 + `);
522 +
523 + // 6th render: Sync external store
524 + expect(getChangeDescriptions(5)).toMatchInlineSnapshot(`
525 + Map {
526 + 3 => Object {
527 + "context": false,
528 + "didHooksChange": true,
529 + "hooks": Array [
530 + 2,
531 + ],
532 + "isFirstMount": false,
533 + "props": Array [],
534 + "state": null,
535 + },
536 + }
537 + `);
538 +
539 + expect(allChangeDescriptions).toHaveLength(6);
540
541 // Export and re-import profile data and make sure it is retained.
542 utils.exportImportHelper(bridge, store);
543
544 + function ExportImportValidator({commitIndex}) {
545 + const commitData = store.profilerStore.getCommitData(rootID, commitIndex);
546 +
547 + expect(commitData.changeDescriptions).toEqual(
548 + allChangeDescriptions[commitIndex],
549 + );
550 +
551 + return null;
552 + }
553 +
554 for (let commitIndex = 0; commitIndex < 6; commitIndex++) {
555 utils.act(() => {
556 TestRenderer.create(
446 - <Validator
447 - commitIndex={commitIndex}
448 - previousCommitDetails={allCommitData[commitIndex]}
449 - rootID={rootID}
450 - />,
557 + <ExportImportValidator commitIndex={commitIndex} />,
558 );
559 });
560 }
packages/react-devtools-shared/src/backend/renderer.js
+27 -37
@@ -93,7 +93,6 @@ import {
93 enableStyleXFeatures,
94 } from 'react-devtools-feature-flags';
95 import is from 'shared/objectIs';
96 -import isArray from 'shared/isArray';
96 import hasOwnProperty from 'shared/hasOwnProperty';
97 import {getStyleXData} from './StyleX/utils';
98 import {createProfilingHooks} from './profilingHooks';
@@ -1444,50 +1443,41 @@ export function attach(
1443 return null;
1444 }
1445
1447 - function areHookInputsEqual(
1448 - nextDeps: Array<mixed>,
1449 - prevDeps: Array<mixed> | null,
1450 - ) {
1451 - if (prevDeps === null) {
1446 + function isHookThatCanScheduleUpdate(hookObject: any) {
1447 + const queue = hookObject.queue;
1448 + if (!queue) {
1449 return false;
1450 }
1451
1455 - for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
1456 - if (is(nextDeps[i], prevDeps[i])) {
1457 - continue;
1458 - }
1459 - return false;
1460 - }
1461 - return true;
1462 - }
1452 + const boundHasOwnProperty = hasOwnProperty.bind(queue);
1453
1464 - function isEffect(memoizedState) {
1465 - if (memoizedState === null || typeof memoizedState !== 'object') {
1466 - return false;
1467 - }
1468 - const {deps} = memoizedState;
1469 - const boundHasOwnProperty = hasOwnProperty.bind(memoizedState);
1470 - return (
1471 - boundHasOwnProperty('create') &&
1472 - boundHasOwnProperty('destroy') &&
1473 - boundHasOwnProperty('deps') &&
1474 - boundHasOwnProperty('next') &&
1475 - boundHasOwnProperty('tag') &&
1476 - (deps === null || isArray(deps))
1477 - );
1454 + // Detect the shape of useState() or useReducer()
1455 + // using the attributes that are unique to these hooks
1456 + // but also stable (e.g. not tied to current Lanes implementation)
1457 + const isStateOrReducer =
1458 + boundHasOwnProperty('pending') &&
1459 + boundHasOwnProperty('dispatch') &&
1460 + typeof queue.dispatch === 'function';
1461 +
1462 + // Detect useSyncExternalStore()
1463 + const isSyncExternalStore =
1464 + boundHasOwnProperty('value') &&
1465 + boundHasOwnProperty('getSnapshot') &&
1466 + typeof queue.getSnapshot === 'function';
1467 +
1468 + // These are the only types of hooks that can schedule an update.
1469 + return isStateOrReducer || isSyncExternalStore;
1470 }
1471
1480 - function didHookChange(prev: any, next: any): boolean {
1472 + function didStatefulHookChange(prev: any, next: any): boolean {
1473 const prevMemoizedState = prev.memoizedState;
1474 const nextMemoizedState = next.memoizedState;
1475
1484 - if (isEffect(prevMemoizedState) && isEffect(nextMemoizedState)) {
1485 - return (
1486 - prevMemoizedState !== nextMemoizedState &&
1487 - !areHookInputsEqual(nextMemoizedState.deps, prevMemoizedState.deps)
1488 - );
1476 + if (isHookThatCanScheduleUpdate(prev)) {
1477 + return prevMemoizedState !== nextMemoizedState;
1478 }
1490 - return nextMemoizedState !== prevMemoizedState;
1479 +
1480 + return false;
1481 }
1482
1483 function didHooksChange(prev: any, next: any): boolean {
@@ -1503,7 +1493,7 @@ export function attach(
1493 next.hasOwnProperty('queue')
1494 ) {
1495 while (next !== null) {
1506 - if (didHookChange(prev, next)) {
1496 + if (didStatefulHookChange(prev, next)) {
1497 return true;
1498 } else {
1499 next = next.next;
@@ -1530,7 +1520,7 @@ export function attach(
1520 next.hasOwnProperty('queue')
1521 ) {
1522 while (next !== null) {
1533 - if (didHookChange(prev, next)) {
1523 + if (didStatefulHookChange(prev, next)) {
1524 indices.push(index);
1525 }
1526 next = next.next;