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;