Skip to content

Commit 33ae43f

Browse files
author
Brian Vaughn
committed
DevTools: add post-commit hook
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 d778518 commit 33ae43f

File tree

11 files changed

+273
-43
lines changed

11 files changed

+273
-43
lines changed
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+
let React;
12+
let ReactDOM;
13+
let Scheduler;
14+
let store: Store;
15+
let utils;
16+
let getEffectDurations;
17+
18+
let effectDurations;
19+
let passiveEffectDurations;
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+
const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
40+
hook.onPostCommitFiberRoot = function onPostCommitFiberRoot(
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+
function App() {
52+
React.useEffect(() => {
53+
Scheduler.unstable_advanceTime(10);
54+
});
55+
React.useLayoutEffect(() => {
56+
Scheduler.unstable_advanceTime(100);
57+
});
58+
return null;
59+
}
60+
61+
utils.act(() => store.profilerStore.startProfiling());
62+
utils.act(() => {
63+
const container = document.createElement('div');
64+
ReactDOM.render(<App />, container);
65+
});
66+
utils.act(() => store.profilerStore.stopProfiling());
67+
68+
expect(effectDurations).toHaveLength(1);
69+
const effectDuration = effectDurations[0];
70+
expect(effectDuration === null || effectDuration === 100).toBe(true);
71+
expect(passiveEffectDurations).toHaveLength(1);
72+
const passiveEffectDuration = 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+
function App() {
80+
React.useEffect(() => {
81+
Scheduler.unstable_advanceTime(10);
82+
});
83+
React.useLayoutEffect(() => {
84+
Scheduler.unstable_advanceTime(100);
85+
});
86+
return null;
87+
}
88+
89+
utils.act(() => store.profilerStore.startProfiling());
90+
utils.act(() => {
91+
const container = document.createElement('div');
92+
const root = ReactDOM.unstable_createRoot(container);
93+
root.render(<App />);
94+
});
95+
utils.act(() => store.profilerStore.stopProfiling());
96+
97+
expect(effectDurations).toHaveLength(1);
98+
const effectDuration = effectDurations[0];
99+
expect(effectDuration === null || effectDuration === 100).toBe(true);
100+
expect(passiveEffectDurations).toHaveLength(1);
101+
const passiveEffectDuration = 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+
function App({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+
return null;
122+
}
123+
124+
const container = document.createElement('div');
125+
const root = ReactDOM.unstable_createRoot(container);
126+
127+
utils.act(() => store.profilerStore.startProfiling());
128+
utils.act(() => root.render(<App />));
129+
utils.act(() => root.render(<App shouldCascade={true} />));
130+
utils.act(() => store.profilerStore.stopProfiling());
131+
132+
expect(effectDurations).toHaveLength(3);
133+
expect(passiveEffectDurations).toHaveLength(3);
134+
135+
for (let i = 0; i < effectDurations.length; i++) {
136+
const effectDuration = effectDurations[i];
137+
expect(effectDuration === null || effectDuration === 100).toBe(true);
138+
const passiveEffectDuration = 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
const handleCommitFiberUnmount = () => {
10131013
throw new Error('handleCommitFiberUnmount not supported by this renderer');
10141014
};
1015+
const handlePostCommitFiberRoot = () => {
1016+
throw new Error('handlePostCommitFiberRoot not supported by this renderer');
1017+
};
10151018
const overrideSuspense = () => {
10161019
throw new Error('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__,
@@ -368,6 +369,7 @@ export function getInternalReactConstants(
368369
LegacyHiddenComponent,
369370
MemoComponent,
370371
OffscreenComponent,
372+
Profiler,
371373
ScopeComponent,
372374
SimpleMemoComponent,
373375
SuspenseComponent,
@@ -441,6 +443,8 @@ export function getInternalReactConstants(
441443
return 'Scope';
442444
case SuspenseListComponent:
443445
return 'SuspenseList';
446+
case Profiler:
447+
return 'Profiler';
444448
default:
445449
const typeSymbol = getTypeSymbol(type);
446450

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

@@ -2205,6 +2190,19 @@ export function attach(
22052190
recordUnmount(fiber, false);
22062191
}
22072192

2193+
function handlePostCommitFiberRoot(root) {
2194+
const isProfilingSupported = root.memoizedInteractions != null;
2195+
if (isProfiling && isProfilingSupported) {
2196+
if (currentCommitProfilingMetadata !== null) {
2197+
const {effectDuration, passiveEffectDuration} = getEffectDurations(
2198+
root,
2199+
);
2200+
currentCommitProfilingMetadata.effectDuration = effectDuration;
2201+
currentCommitProfilingMetadata.passiveEffectDuration = passiveEffectDuration;
2202+
}
2203+
}
2204+
}
2205+
22082206
function handleCommitFiberRoot(root, priorityLevel) {
22092207
const current = root.current;
22102208
const alternate = current.alternate;
@@ -2226,23 +2224,6 @@ export function attach(
22262224
const isProfilingSupported = root.memoizedInteractions != null;
22272225

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

@@ -3855,6 +3839,7 @@ export function attach(
38553839
getProfilingData,
38563840
handleCommitFiberRoot,
38573841
handleCommitFiberUnmount,
3842+
handlePostCommitFiberRoot,
38583843
inspectElement,
38593844
logElementToConsole,
38603845
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
@@ -117,6 +117,26 @@ export function copyWithSet(
117117
return updated;
118118
}
119119

120+
export function getEffectDurations(root: Object) {
121+
// Profiling durations are only available for certain builds.
122+
// If available, they'll be stored on the HostRoot.
123+
let effectDuration = null;
124+
let passiveEffectDuration = null;
125+
const hostRoot = root.current;
126+
if (hostRoot != null) {
127+
const stateNode = hostRoot.stateNode;
128+
if (stateNode != null) {
129+
effectDuration =
130+
stateNode.effectDuration != null ? stateNode.effectDuration : null;
131+
passiveEffectDuration =
132+
stateNode.passiveEffectDuration != null
133+
? stateNode.passiveEffectDuration
134+
: null;
135+
}
136+
}
137+
return {effectDuration, passiveEffectDuration};
138+
}
139+
120140
export function serializeToString(data: any): string {
121141
const cache = new Set();
122142
// 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+
function onPostCommitFiberRoot(rendererID, root) {
291+
const rendererInterface = rendererInterfaces.get(rendererID);
292+
if (rendererInterface != null) {
293+
rendererInterface.handlePostCommitFiberRoot(root);
294+
}
295+
}
296+
290297
// TODO: More meaningful names for "rendererInterfaces" and "renderers".
291298
const fiberRoots = {};
292299
const rendererInterfaces = new Map();
@@ -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_wrap as wrap,
2222
} from 'scheduler/tracing';
2323

24+
function sleep(ms) {
25+
const start = performance.now();
26+
let now;
27+
do {
28+
now = performance.now();
29+
} while (now - ms < start);
30+
}
31+
2432
export default function InteractionTracing() {
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)