Skip to content

Commit b38ac13

Browse files
author
Brian Vaughn
authored
DevTools: Add post-commit hook (#21183)
I recently added UI for the Profiler's commit and post-commit durations to the DevTools, but I made two pretty silly oversights: 1. I used the commit hook (called after mutation+layout effects) to read both the layout and passive effect durations. This is silly because passive effects may not have flushed yet git at this point. 2. I didn't reset the values on the HostRoot node, so they accumulated with each commit. This commitR addresses both issues: 1. First it adds a new DevTools hook, onPostCommitRoot*, to be called after passive effects get flushed. This gives DevTools the opportunity to read passive effect durations (if the build of React being profiled supports it). 2. Second the work loop resets these durations (on the HostRoot) after calling the post-commit hook so address the accumulation problem. I've also added a unit test to guard against this regressing in the future. * Doing this in flushPassiveEffectsImpl seemed simplest, since there are so many places we flush passive effects. Is there any potential problem with this though?
1 parent b943aeb commit b38ac13

11 files changed

Lines changed: 275 additions & 43 deletions

File tree

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
describe('profiling HostRoot',()=>{
11+
letReact;
12+
letReactDOM;
13+
letScheduler;
14+
let store: Store;
15+
letutils;
16+
letgetEffectDurations;
17+
18+
leteffectDurations;
19+
letpassiveEffectDurations;
20+
21+
beforeEach(()=>{
22+
utils=require('./utils');
23+
utils.beforeEachProfiling();
24+
25+
getEffectDurations=require('../backend/utils').getEffectDurations;
26+
27+
store=global.store;
28+
29+
React=require('react');
30+
ReactDOM=require('react-dom');
31+
Scheduler=require('scheduler');
32+
33+
effectDurations=[];
34+
passiveEffectDurations=[];
35+
36+
// This is the DevTools hook installed by the env.beforEach()
37+
// The hook is installed as a read-only property on the window,
38+
// so for our test purposes we can just override the commit hook.
39+
consthook=global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
40+
hook.onPostCommitFiberRoot=functiononPostCommitFiberRoot(
41+
rendererID,
42+
root,
43+
){
44+
const{effectDuration, passiveEffectDuration}=getEffectDurations(root);
45+
effectDurations.push(effectDuration);
46+
passiveEffectDurations.push(passiveEffectDuration);
47+
};
48+
});
49+
50+
it('should expose passive and layout effect durations for render()',()=>{
51+
functionApp(){
52+
React.useEffect(()=>{
53+
Scheduler.unstable_advanceTime(10);
54+
});
55+
React.useLayoutEffect(()=>{
56+
Scheduler.unstable_advanceTime(100);
57+
});
58+
returnnull;
59+
}
60+
61+
utils.act(()=>store.profilerStore.startProfiling());
62+
utils.act(()=>{
63+
constcontainer=document.createElement('div');
64+
ReactDOM.render(<App/>,container);
65+
});
66+
utils.act(()=>store.profilerStore.stopProfiling());
67+
68+
expect(effectDurations).toHaveLength(1);
69+
consteffectDuration=effectDurations[0];
70+
expect(effectDuration===null||effectDuration===100).toBe(true);
71+
expect(passiveEffectDurations).toHaveLength(1);
72+
constpassiveEffectDuration=passiveEffectDurations[0];
73+
expect(passiveEffectDuration===null||passiveEffectDuration===10).toBe(
74+
true,
75+
);
76+
});
77+
78+
it('should expose passive and layout effect durations for createRoot()',()=>{
79+
functionApp(){
80+
React.useEffect(()=>{
81+
Scheduler.unstable_advanceTime(10);
82+
});
83+
React.useLayoutEffect(()=>{
84+
Scheduler.unstable_advanceTime(100);
85+
});
86+
returnnull;
87+
}
88+
89+
utils.act(()=>store.profilerStore.startProfiling());
90+
utils.act(()=>{
91+
constcontainer=document.createElement('div');
92+
constroot=ReactDOM.unstable_createRoot(container);
93+
root.render(<App/>);
94+
});
95+
utils.act(()=>store.profilerStore.stopProfiling());
96+
97+
expect(effectDurations).toHaveLength(1);
98+
consteffectDuration=effectDurations[0];
99+
expect(effectDuration===null||effectDuration===100).toBe(true);
100+
expect(passiveEffectDurations).toHaveLength(1);
101+
constpassiveEffectDuration=passiveEffectDurations[0];
102+
expect(passiveEffectDuration===null||passiveEffectDuration===10).toBe(
103+
true,
104+
);
105+
});
106+
107+
it('should properly reset passive and layout effect durations between commits',()=>{
108+
functionApp({shouldCascade}){
109+
const[,setState]=React.useState(false);
110+
React.useEffect(()=>{
111+
Scheduler.unstable_advanceTime(10);
112+
});
113+
React.useLayoutEffect(()=>{
114+
Scheduler.unstable_advanceTime(100);
115+
});
116+
React.useLayoutEffect(()=>{
117+
if(shouldCascade){
118+
setState(true);
119+
}
120+
},[shouldCascade]);
121+
returnnull;
122+
}
123+
124+
constcontainer=document.createElement('div');
125+
constroot=ReactDOM.unstable_createRoot(container);
126+
127+
utils.act(()=>store.profilerStore.startProfiling());
128+
utils.act(()=>root.render(<App/>));
129+
utils.act(()=>root.render(<AppshouldCascade={true}/>));
130+
utils.act(()=>store.profilerStore.stopProfiling());
131+
132+
expect(effectDurations).toHaveLength(3);
133+
expect(passiveEffectDurations).toHaveLength(3);
134+
135+
for(leti=0;i<effectDurations.length;i++){
136+
consteffectDuration=effectDurations[i];
137+
expect(effectDuration===null||effectDuration===100).toBe(true);
138+
constpassiveEffectDuration=passiveEffectDurations[i];
139+
expect(
140+
passiveEffectDuration===null||passiveEffectDuration===10,
141+
).toBe(true);
142+
}
143+
});
144+
});

‎packages/react-devtools-shared/src/backend/legacy/renderer.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,9 @@ export function attach(
10121012
consthandleCommitFiberUnmount=()=>{
10131013
thrownewError('handleCommitFiberUnmount not supported by this renderer');
10141014
};
1015+
consthandlePostCommitFiberRoot=()=>{
1016+
thrownewError('handlePostCommitFiberRoot not supported by this renderer');
1017+
};
10151018
constoverrideSuspense=()=>{
10161019
thrownewError('overrideSuspense not supported by this renderer');
10171020
};
@@ -1082,6 +1085,7 @@ export function attach(
10821085
getProfilingData,
10831086
handleCommitFiberRoot,
10841087
handleCommitFiberUnmount,
1088+
handlePostCommitFiberRoot,
10851089
inspectElement,
10861090
logElementToConsole,
10871091
overrideSuspense,

‎packages/react-devtools-shared/src/backend/renderer.js‎

Lines changed: 25 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
copyWithDelete,
4343
copyWithRename,
4444
copyWithSet,
45+
getEffectDurations,
4546
}from'./utils';
4647
import{
4748
__DEBUG__,
@@ -369,6 +370,7 @@ export function getInternalReactConstants(
369370
LegacyHiddenComponent,
370371
MemoComponent,
371372
OffscreenComponent,
373+
Profiler,
372374
ScopeComponent,
373375
SimpleMemoComponent,
374376
SuspenseComponent,
@@ -442,6 +444,8 @@ export function getInternalReactConstants(
442444
return'Scope';
443445
caseSuspenseListComponent:
444446
return'SuspenseList';
447+
caseProfiler:
448+
return'Profiler';
445449
default:
446450
consttypeSymbol=getTypeSymbol(type);
447451

@@ -2154,25 +2158,6 @@ export function attach(
21542158
// Checking root.memoizedInteractions handles multi-renderer edge-case-
21552159
// where some v16 renderers support profiling and others don't.
21562160
if(isProfiling&&root.memoizedInteractions!=null){
2157-
// Profiling durations are only available for certain builds.
2158-
// If available, they'll be stored on the HostRoot.
2159-
leteffectDuration=null;
2160-
letpassiveEffectDuration=null;
2161-
consthostRoot=root.current;
2162-
if(hostRoot!=null){
2163-
conststateNode=hostRoot.stateNode;
2164-
if(stateNode!=null){
2165-
effectDuration=
2166-
stateNode.effectDuration!=null
2167-
? stateNode.effectDuration
2168-
: null;
2169-
passiveEffectDuration=
2170-
stateNode.passiveEffectDuration!=null
2171-
? stateNode.passiveEffectDuration
2172-
: null;
2173-
}
2174-
}
2175-
21762161
// If profiling is active, store commit time and duration, and the current interactions.
21772162
// The frontend may request this information after profiling has stopped.
21782163
currentCommitProfilingMetadata={
@@ -2187,8 +2172,8 @@ export function attach(
21872172
),
21882173
maxActualDuration: 0,
21892174
priorityLevel: null,
2190-
effectDuration,
2191-
passiveEffectDuration,
2175+
effectDuration: null,
2176+
passiveEffectDuration: null,
21922177
};
21932178
}
21942179

@@ -2206,6 +2191,19 @@ export function attach(
22062191
recordUnmount(fiber,false);
22072192
}
22082193

2194+
functionhandlePostCommitFiberRoot(root){
2195+
constisProfilingSupported=root.memoizedInteractions!=null;
2196+
if(isProfiling&&isProfilingSupported){
2197+
if(currentCommitProfilingMetadata!==null){
2198+
const{effectDuration,passiveEffectDuration}=getEffectDurations(
2199+
root,
2200+
);
2201+
currentCommitProfilingMetadata.effectDuration=effectDuration;
2202+
currentCommitProfilingMetadata.passiveEffectDuration=passiveEffectDuration;
2203+
}
2204+
}
2205+
}
2206+
22092207
functionhandleCommitFiberRoot(root,priorityLevel){
22102208
constcurrent=root.current;
22112209
constalternate=current.alternate;
@@ -2227,23 +2225,6 @@ export function attach(
22272225
constisProfilingSupported=root.memoizedInteractions!=null;
22282226

22292227
if(isProfiling&&isProfilingSupported){
2230-
// Profiling durations are only available for certain builds.
2231-
// If available, they'll be stored on the HostRoot.
2232-
leteffectDuration=null;
2233-
letpassiveEffectDuration=null;
2234-
consthostRoot=root.current;
2235-
if(hostRoot!=null){
2236-
conststateNode=hostRoot.stateNode;
2237-
if(stateNode!=null){
2238-
effectDuration=
2239-
stateNode.effectDuration!=null ? stateNode.effectDuration : null;
2240-
passiveEffectDuration=
2241-
stateNode.passiveEffectDuration!=null
2242-
? stateNode.passiveEffectDuration
2243-
: null;
2244-
}
2245-
}
2246-
22472228
// If profiling is active, store commit time and duration, and the current interactions.
22482229
// The frontend may request this information after profiling has stopped.
22492230
currentCommitProfilingMetadata={
@@ -2259,8 +2240,11 @@ export function attach(
22592240
maxActualDuration: 0,
22602241
priorityLevel:
22612242
priorityLevel==null ? null : formatPriorityLevel(priorityLevel),
2262-
effectDuration,
2263-
passiveEffectDuration,
2243+
2244+
// Initialize to null; if new enough React version is running,
2245+
// these values will be read during separate handlePostCommitFiberRoot() call.
2246+
effectDuration: null,
2247+
passiveEffectDuration: null,
22642248
};
22652249
}
22662250

@@ -3856,6 +3840,7 @@ export function attach(
38563840
getProfilingData,
38573841
handleCommitFiberRoot,
38583842
handleCommitFiberUnmount,
3843+
handlePostCommitFiberRoot,
38593844
inspectElement,
38603845
logElementToConsole,
38613846
prepareViewAttributeSource,

‎packages/react-devtools-shared/src/backend/types.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ export type RendererInterface = {
326326
getPathForElement: (id: number)=>Array<PathFrame>|null,
327327
handleCommitFiberRoot: (fiber: Object,commitPriority?: number)=>void,
328328
handleCommitFiberUnmount: (fiber: Object)=>void,
329+
handlePostCommitFiberRoot: (fiber: Object)=>void,
329330
inspectElement: (
330331
requestID: number,
331332
id: number,

‎packages/react-devtools-shared/src/backend/utils.js‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,26 @@ export function copyWithSet(
118118
returnupdated;
119119
}
120120

121+
exportfunctiongetEffectDurations(root: Object){
122+
// Profiling durations are only available for certain builds.
123+
// If available, they'll be stored on the HostRoot.
124+
leteffectDuration=null;
125+
letpassiveEffectDuration=null;
126+
consthostRoot=root.current;
127+
if(hostRoot!=null){
128+
conststateNode=hostRoot.stateNode;
129+
if(stateNode!=null){
130+
effectDuration=
131+
stateNode.effectDuration!=null ? stateNode.effectDuration : null;
132+
passiveEffectDuration=
133+
stateNode.passiveEffectDuration!=null
134+
? stateNode.passiveEffectDuration
135+
: null;
136+
}
137+
}
138+
return{effectDuration, passiveEffectDuration};
139+
}
140+
121141
exportfunctionserializeToString(data: any): string{
122142
constcache=newSet();
123143
// Use a custom replacer function to protect against circular references.

‎packages/react-devtools-shared/src/hook.js‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,13 @@ export function installHook(target: any): DevToolsHook | null {
287287
}
288288
}
289289

290+
functiononPostCommitFiberRoot(rendererID,root){
291+
constrendererInterface=rendererInterfaces.get(rendererID);
292+
if(rendererInterface!=null){
293+
rendererInterface.handlePostCommitFiberRoot(root);
294+
}
295+
}
296+
290297
// TODO: More meaningful names for "rendererInterfaces" and "renderers".
291298
constfiberRoots={};
292299
constrendererInterfaces=newMap();
@@ -315,6 +322,7 @@ export function installHook(target: any): DevToolsHook | null {
315322
checkDCE,
316323
onCommitFiberUnmount,
317324
onCommitFiberRoot,
325+
onPostCommitFiberRoot,
318326
};
319327

320328
Object.defineProperty(

‎packages/react-devtools-shell/src/app/InteractionTracing/index.js‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ import {
2121
unstable_wrapaswrap,
2222
}from'scheduler/tracing';
2323

24+
functionsleep(ms){
25+
conststart=performance.now();
26+
letnow;
27+
do{
28+
now=performance.now();
29+
}while(now-ms<start);
30+
}
31+
2432
exportdefaultfunctionInteractionTracing(){
2533
const[count,setCount]=useState(0);
2634
const[shouldCascade,setShouldCascade]=useState(false);
@@ -75,7 +83,11 @@ export default function InteractionTracing() {
7583
},[count,shouldCascade]);
7684

7785
useLayoutEffect(()=>{
78-
Math.sqrt(100*100*100*100*100);
86+
sleep(150);
87+
});
88+
89+
useEffect(()=>{
90+
sleep(300);
7991
});
8092

8193
return(

0 commit comments

Comments
 (0)