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

Add fiber summary tooltip to devtools profiling #18048

Merged
merged 11 commits into from
Feb 19, 2020
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.Component {
margin-bottom: 1rem;
}

.Item {
margin-top: 0.25rem;
}

.Key {
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-small);
line-height: 1;
}

.Key:first-of-type::before {
content: ' (';
}

.Key::after {
content: ', ';
}

.Key:last-of-type::after {
content: ')';
}

.Label {
font-weight: bold;
margin-bottom: 0.5rem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* 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
*/

import React, {useContext} from 'react';
import {ProfilerContext} from '../Profiler/ProfilerContext';
import {StoreContext} from '../context';

import styles from './ProfilerWhatChanged.css';

type ProfilerWhatChangedProps = {|
fiberID: number,
|};

export default function ProfilerWhatChanged({
fiberID,
}: ProfilerWhatChangedProps) {
const {profilerStore} = useContext(StoreContext);
const {rootID, selectedCommitIndex} = useContext(ProfilerContext);

// TRICKY
// Handle edge case where no commit is selected because of a min-duration filter update.
// If the commit index is null, suspending for data below would throw an error.
// TODO (ProfilerContext) This check should not be necessary.
if (selectedCommitIndex === null) {
return null;
}

const {changeDescriptions} = profilerStore.getCommitData(
((rootID: any): number),
selectedCommitIndex,
);

if (changeDescriptions === null) {
return null;
}

const changeDescription = changeDescriptions.get(fiberID);
if (changeDescription == null) {
return null;
}

if (changeDescription.isFirstMount) {
return (
<div className={styles.Component}>
<label className={styles.Label}>Why did this render?</label>
<div className={styles.Item}>
This is the first time the component rendered.
</div>
</div>
);
}

const changes = [];

if (changeDescription.context === true) {
changes.push(
<div key="context" className={styles.Item}>
• Context changed
</div>,
);
} else if (
typeof changeDescription.context === 'object' &&
changeDescription.context !== null &&
changeDescription.context.length !== 0
) {
changes.push(
<div key="context" className={styles.Item}>
• Context changed:
{changeDescription.context.map(key => (
<span key={key} className={styles.Key}>
{key}
</span>
))}
</div>,
);
}

if (changeDescription.didHooksChange) {
changes.push(
<div key="hooks" className={styles.Item}>
• Hooks changed
</div>,
);
}

if (
changeDescription.props !== null &&
changeDescription.props.length !== 0
) {
changes.push(
<div key="props" className={styles.Item}>
• Props changed:
{changeDescription.props.map(key => (
<span key={key} className={styles.Key}>
{key}
</span>
))}
</div>,
);
}

if (
changeDescription.state !== null &&
changeDescription.state.length !== 0
) {
changes.push(
<div key="state" className={styles.Item}>
• State changed:
{changeDescription.state.map(key => (
<span key={key} className={styles.Key}>
{key}
</span>
))}
</div>,
);
}

if (changes.length === 0) {
changes.push(
<div key="nothing" className={styles.Item}>
The parent component rendered.
</div>,
);
}

return (
<div className={styles.Component}>
<label className={styles.Label}>Why did this render?</label>
{changes}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.Tooltip {
position: absolute;
pointer-events: none;
border: none;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
font-family: var(--font-family-sans);
font-size: 12px;
background-color: var(--color-tooltip-background);
color: var(--color-tooltip-text);

/* Make sure this is above the DevTools, which are above the Overlay */
z-index: 10000002;
}

.Container {
width: -moz-max-content;
width: -webkit-max-content;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/** @flow */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny nit, and I'm happy to do this post-merge- BUT! The new Tooltip and WhatChanged components are only used by views in the "Profiler" tab, so they should be in views/Profiler rather than views/Components (which is where views for the "Components" tab live). I would be happy to sort this out with a git mv after the PR merge though!


import React, {useCallback, useRef, useState} from 'react';
import {useSmartTooltip} from '../hooks';

import styles from './Tooltip.css';

const initialTooltipState = {height: 0, mouseX: 0, mouseY: 0, width: 0};

export default function Tooltip({children, label}: any) {
const containerRef = useRef(null);
const [tooltipState, setTooltipState] = useState(initialTooltipState);
const tooltipRef = useSmartTooltip(tooltipState);

const onMouseMove = useCallback(
M-Izadmehr marked this conversation as resolved.
Show resolved Hide resolved
(event: SyntheticMouseEvent<*>) => {
setTooltipState(getTooltipPosition(containerRef.current, event));
},
[setTooltipState],
);

return (
<div
className={styles.Container}
onMouseMove={label !== null ? onMouseMove : undefined}
ref={containerRef}>
{label !== null && (
<div ref={tooltipRef} className={styles.Tooltip}>
{label}
</div>
)}
{children}
</div>
);
}

function getTooltipPosition(
relativeContainer,
mouseEvent: SyntheticMouseEvent<*>,
) {
if (relativeContainer !== null) {
const {height, top, width} = relativeContainer.getBoundingClientRect();

const mouseX = mouseEvent.clientX;
const mouseY = mouseEvent.clientY - top;

return {height, mouseX, mouseY, width};
} else {
return initialTooltipState;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type Props = {|
label: string,
onClick: (event: SyntheticMouseEvent<*>) => mixed,
onDoubleClick?: (event: SyntheticMouseEvent<*>) => mixed,
onMouseOver: (event: SyntheticMouseEvent<*>) => mixed,
onMouseOut: (event: SyntheticMouseEvent<*>) => mixed,
placeLabelAboveNode?: boolean,
textStyle?: Object,
width: number,
Expand All @@ -33,6 +35,8 @@ export default function ChartNode({
isDimmed = false,
label,
onClick,
onMouseOver,
onMouseOut,
onDoubleClick,
textStyle,
width,
Expand All @@ -41,12 +45,13 @@ export default function ChartNode({
}: Props) {
return (
<g className={styles.Group} transform={`translate(${x},${y})`}>
<title>{label}</title>
<rect
width={width}
height={height}
fill={color}
onClick={onClick}
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
M-Izadmehr marked this conversation as resolved.
Show resolved Hide resolved
onDoubleClick={onDoubleClick}
className={styles.Rect}
style={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,33 @@
* @flow
*/

import React, {forwardRef, useCallback, useContext, useMemo} from 'react';
import React, {
forwardRef,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import {FixedSizeList} from 'react-window';
import {ProfilerContext} from './ProfilerContext';
import NoCommitData from './NoCommitData';
import CommitFlamegraphListItem from './CommitFlamegraphListItem';
import HoveredFiberInfo from './HoveredFiberInfo';
import {scale} from './utils';
import {StoreContext} from '../context';
import {SettingsContext} from '../Settings/SettingsContext';
import Tooltip from '../Components/Tooltip';

import styles from './CommitFlamegraph.css';

import type {TooltipFiberData} from './HoveredFiberInfo';
import type {ChartData, ChartNode} from './FlamegraphChartBuilder';
import type {CommitTree} from './types';

export type ItemData = {|
chartData: ChartData,
hoverFiber: (fiberData: TooltipFiberData | null) => void,
scaleX: (value: number, fallbackValue: number) => number,
selectedChartNode: ChartNode | null,
selectedChartNodeIndex: number,
Expand Down Expand Up @@ -91,6 +101,7 @@ type Props = {|
|};

function CommitFlamegraph({chartData, commitTree, height, width}: Props) {
const [hoveredFiberData, hoverFiber] = useState<number | null>(null);
const {lineHeight} = useContext(SettingsContext);
const {selectFiber, selectedFiberID} = useContext(ProfilerContext);

Expand Down Expand Up @@ -118,6 +129,7 @@ function CommitFlamegraph({chartData, commitTree, height, width}: Props) {
const itemData = useMemo<ItemData>(
() => ({
chartData,
hoverFiber,
scaleX: scale(
0,
selectedChartNode !== null
Expand All @@ -131,19 +143,37 @@ function CommitFlamegraph({chartData, commitTree, height, width}: Props) {
selectFiber,
width,
}),
[chartData, selectedChartNode, selectedChartNodeIndex, selectFiber, width],
[
chartData,
hoverFiber,
selectedChartNode,
selectedChartNodeIndex,
selectFiber,
width,
],
);

// Tooltip used to show summary of fiber info on hover
const tooltipLabel = useMemo(
() =>
hoveredFiberData !== null ? (
<HoveredFiberInfo fiberData={hoveredFiberData} />
) : null,
[hoveredFiberData],
);

return (
<FixedSizeList
height={height}
innerElementType={InnerElementType}
itemCount={chartData.depth}
itemData={itemData}
itemSize={lineHeight}
width={width}>
{CommitFlamegraphListItem}
</FixedSizeList>
<Tooltip label={tooltipLabel}>
<FixedSizeList
height={height}
innerElementType={InnerElementType}
itemCount={chartData.depth}
itemData={itemData}
itemSize={lineHeight}
width={width}>
{CommitFlamegraphListItem}
</FixedSizeList>
</Tooltip>
);
}

Expand Down
Loading