Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DevTools: Add post-commit hook #21183

Merged
merged 1 commit into from
Apr 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions packages/react-devtools-shared/src/__tests__/profilingHostRoot-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

describe('profiling HostRoot', () => {
let React;
let ReactDOM;
let Scheduler;
let store: Store;
let utils;
let getEffectDurations;

let effectDurations;
let passiveEffectDurations;

beforeEach(() => {
utils = require('./utils');
utils.beforeEachProfiling();

getEffectDurations = require('../backend/utils').getEffectDurations;

store = global.store;

React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');

effectDurations = [];
passiveEffectDurations = [];

// This is the DevTools hook installed by the env.beforEach()
// The hook is installed as a read-only property on the window,
// so for our test purposes we can just override the commit hook.
const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
hook.onPostCommitFiberRoot = function onPostCommitFiberRoot(
rendererID,
root,
) {
const {effectDuration, passiveEffectDuration} = getEffectDurations(root);
effectDurations.push(effectDuration);
passiveEffectDurations.push(passiveEffectDuration);
};
});

it('should expose passive and layout effect durations for render()', () => {
function App() {
React.useEffect(() => {
Scheduler.unstable_advanceTime(10);
});
React.useLayoutEffect(() => {
Scheduler.unstable_advanceTime(100);
});
return null;
}

utils.act(() => store.profilerStore.startProfiling());
utils.act(() => {
const container = document.createElement('div');
ReactDOM.render(<App />, container);
});
utils.act(() => store.profilerStore.stopProfiling());

expect(effectDurations).toHaveLength(1);
const effectDuration = effectDurations[0];
expect(effectDuration === null || effectDuration === 100).toBe(true);
expect(passiveEffectDurations).toHaveLength(1);
const passiveEffectDuration = passiveEffectDurations[0];
expect(passiveEffectDuration === null || passiveEffectDuration === 10).toBe(
true,
);
});

it('should expose passive and layout effect durations for createRoot()', () => {
function App() {
React.useEffect(() => {
Scheduler.unstable_advanceTime(10);
});
React.useLayoutEffect(() => {
Scheduler.unstable_advanceTime(100);
});
return null;
}

utils.act(() => store.profilerStore.startProfiling());
utils.act(() => {
const container = document.createElement('div');
const root = ReactDOM.unstable_createRoot(container);
root.render(<App />);
});
utils.act(() => store.profilerStore.stopProfiling());

expect(effectDurations).toHaveLength(1);
const effectDuration = effectDurations[0];
expect(effectDuration === null || effectDuration === 100).toBe(true);
expect(passiveEffectDurations).toHaveLength(1);
const passiveEffectDuration = passiveEffectDurations[0];
expect(passiveEffectDuration === null || passiveEffectDuration === 10).toBe(
true,
);
});

it('should properly reset passive and layout effect durations between commits', () => {
function App({shouldCascade}) {
const [, setState] = React.useState(false);
React.useEffect(() => {
Scheduler.unstable_advanceTime(10);
});
React.useLayoutEffect(() => {
Scheduler.unstable_advanceTime(100);
});
React.useLayoutEffect(() => {
if (shouldCascade) {
setState(true);
}
}, [shouldCascade]);
return null;
}

const container = document.createElement('div');
const root = ReactDOM.unstable_createRoot(container);

utils.act(() => store.profilerStore.startProfiling());
utils.act(() => root.render(<App />));
utils.act(() => root.render(<App shouldCascade={true} />));
utils.act(() => store.profilerStore.stopProfiling());

expect(effectDurations).toHaveLength(3);
expect(passiveEffectDurations).toHaveLength(3);

for (let i = 0; i < effectDurations.length; i++) {
const effectDuration = effectDurations[i];
expect(effectDuration === null || effectDuration === 100).toBe(true);
const passiveEffectDuration = passiveEffectDurations[i];
expect(
passiveEffectDuration === null || passiveEffectDuration === 10,
).toBe(true);
}
});
});
4 changes: 4 additions & 0 deletions packages/react-devtools-shared/src/backend/legacy/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,9 @@ export function attach(
const handleCommitFiberUnmount = () => {
throw new Error('handleCommitFiberUnmount not supported by this renderer');
};
const handlePostCommitFiberRoot = () => {
throw new Error('handlePostCommitFiberRoot not supported by this renderer');
};
const overrideSuspense = () => {
throw new Error('overrideSuspense not supported by this renderer');
};
Expand Down Expand Up @@ -1082,6 +1085,7 @@ export function attach(
getProfilingData,
handleCommitFiberRoot,
handleCommitFiberUnmount,
handlePostCommitFiberRoot,
inspectElement,
logElementToConsole,
overrideSuspense,
Expand Down
65 changes: 25 additions & 40 deletions packages/react-devtools-shared/src/backend/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
copyWithDelete,
copyWithRename,
copyWithSet,
getEffectDurations,
} from './utils';
import {
__DEBUG__,
Expand Down Expand Up @@ -369,6 +370,7 @@ export function getInternalReactConstants(
LegacyHiddenComponent,
MemoComponent,
OffscreenComponent,
Profiler,
ScopeComponent,
SimpleMemoComponent,
SuspenseComponent,
Expand Down Expand Up @@ -442,6 +444,8 @@ export function getInternalReactConstants(
return 'Scope';
case SuspenseListComponent:
return 'SuspenseList';
case Profiler:
return 'Profiler';
default:
const typeSymbol = getTypeSymbol(type);

Expand Down Expand Up @@ -2154,25 +2158,6 @@ export function attach(
// Checking root.memoizedInteractions handles multi-renderer edge-case-
// where some v16 renderers support profiling and others don't.
if (isProfiling && root.memoizedInteractions != null) {
// Profiling durations are only available for certain builds.
// If available, they'll be stored on the HostRoot.
let effectDuration = null;
let passiveEffectDuration = null;
const hostRoot = root.current;
if (hostRoot != null) {
const stateNode = hostRoot.stateNode;
if (stateNode != null) {
effectDuration =
stateNode.effectDuration != null
? stateNode.effectDuration
: null;
passiveEffectDuration =
stateNode.passiveEffectDuration != null
? stateNode.passiveEffectDuration
: null;
}
}

// If profiling is active, store commit time and duration, and the current interactions.
// The frontend may request this information after profiling has stopped.
currentCommitProfilingMetadata = {
Expand All @@ -2187,8 +2172,8 @@ export function attach(
),
maxActualDuration: 0,
priorityLevel: null,
effectDuration,
passiveEffectDuration,
effectDuration: null,
passiveEffectDuration: null,
};
}

Expand All @@ -2206,6 +2191,19 @@ export function attach(
recordUnmount(fiber, false);
}

function handlePostCommitFiberRoot(root) {
const isProfilingSupported = root.memoizedInteractions != null;
if (isProfiling && isProfilingSupported) {
if (currentCommitProfilingMetadata !== null) {
const {effectDuration, passiveEffectDuration} = getEffectDurations(
root,
);
currentCommitProfilingMetadata.effectDuration = effectDuration;
currentCommitProfilingMetadata.passiveEffectDuration = passiveEffectDuration;
}
}
}

function handleCommitFiberRoot(root, priorityLevel) {
const current = root.current;
const alternate = current.alternate;
Expand All @@ -2227,23 +2225,6 @@ export function attach(
const isProfilingSupported = root.memoizedInteractions != null;

if (isProfiling && isProfilingSupported) {
// Profiling durations are only available for certain builds.
// If available, they'll be stored on the HostRoot.
let effectDuration = null;
let passiveEffectDuration = null;
const hostRoot = root.current;
if (hostRoot != null) {
const stateNode = hostRoot.stateNode;
if (stateNode != null) {
effectDuration =
stateNode.effectDuration != null ? stateNode.effectDuration : null;
passiveEffectDuration =
stateNode.passiveEffectDuration != null
? stateNode.passiveEffectDuration
: null;
}
}

// If profiling is active, store commit time and duration, and the current interactions.
// The frontend may request this information after profiling has stopped.
currentCommitProfilingMetadata = {
Expand All @@ -2259,8 +2240,11 @@ export function attach(
maxActualDuration: 0,
priorityLevel:
priorityLevel == null ? null : formatPriorityLevel(priorityLevel),
effectDuration,
passiveEffectDuration,

// Initialize to null; if new enough React version is running,
// these values will be read during separate handlePostCommitFiberRoot() call.
effectDuration: null,
passiveEffectDuration: null,
};
}

Expand Down Expand Up @@ -3856,6 +3840,7 @@ export function attach(
getProfilingData,
handleCommitFiberRoot,
handleCommitFiberUnmount,
handlePostCommitFiberRoot,
inspectElement,
logElementToConsole,
prepareViewAttributeSource,
Expand Down
1 change: 1 addition & 0 deletions packages/react-devtools-shared/src/backend/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ export type RendererInterface = {
getPathForElement: (id: number) => Array<PathFrame> | null,
handleCommitFiberRoot: (fiber: Object, commitPriority?: number) => void,
handleCommitFiberUnmount: (fiber: Object) => void,
handlePostCommitFiberRoot: (fiber: Object) => void,
inspectElement: (
requestID: number,
id: number,
Expand Down
20 changes: 20 additions & 0 deletions packages/react-devtools-shared/src/backend/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,26 @@ export function copyWithSet(
return updated;
}

export function getEffectDurations(root: Object) {
// Profiling durations are only available for certain builds.
// If available, they'll be stored on the HostRoot.
let effectDuration = null;
let passiveEffectDuration = null;
const hostRoot = root.current;
if (hostRoot != null) {
const stateNode = hostRoot.stateNode;
if (stateNode != null) {
effectDuration =
stateNode.effectDuration != null ? stateNode.effectDuration : null;
passiveEffectDuration =
stateNode.passiveEffectDuration != null
? stateNode.passiveEffectDuration
: null;
}
}
return {effectDuration, passiveEffectDuration};
}

export function serializeToString(data: any): string {
const cache = new Set();
// Use a custom replacer function to protect against circular references.
Expand Down
8 changes: 8 additions & 0 deletions packages/react-devtools-shared/src/hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,13 @@ export function installHook(target: any): DevToolsHook | null {
}
}

function onPostCommitFiberRoot(rendererID, root) {
const rendererInterface = rendererInterfaces.get(rendererID);
if (rendererInterface != null) {
rendererInterface.handlePostCommitFiberRoot(root);
}
}

// TODO: More meaningful names for "rendererInterfaces" and "renderers".
const fiberRoots = {};
const rendererInterfaces = new Map();
Expand Down Expand Up @@ -315,6 +322,7 @@ export function installHook(target: any): DevToolsHook | null {
checkDCE,
onCommitFiberUnmount,
onCommitFiberRoot,
onPostCommitFiberRoot,
};

Object.defineProperty(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ import {
unstable_wrap as wrap,
} from 'scheduler/tracing';

function sleep(ms) {
const start = performance.now();
let now;
do {
now = performance.now();
} while (now - ms < start);
}

export default function InteractionTracing() {
const [count, setCount] = useState(0);
const [shouldCascade, setShouldCascade] = useState(false);
Expand Down Expand Up @@ -75,7 +83,11 @@ export default function InteractionTracing() {
}, [count, shouldCascade]);

useLayoutEffect(() => {
Math.sqrt(100 * 100 * 100 * 100 * 100);
sleep(150);
});

useEffect(() => {
sleep(300);
});

return (
Expand Down
Loading