Skip to content

Commit

Permalink
Add fiber summary tooltip to devtools profiling (#18048)
Browse files Browse the repository at this point in the history
* Add tooltip component

* Separate logic of ProfilerWhatChanged to a component

* Add hovered Fiber info tooltip component

* Add flame graph chart tooltip

* Add commit ranked list tooltip

* Fix flow issues

* Minor improvement in filter

* Fix flickering issue

* Resolved issues on useCallbacks and mouse event listeners

* Fix lints

* Remove unnecessary useCallback
  • Loading branch information
M-Izadmehr authored Feb 19, 2020
1 parent 2512c30 commit 44e5f5e
Show file tree
Hide file tree
Showing 13 changed files with 527 additions and 176 deletions.
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,24 @@
.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);
opacity: 1;
/* Make sure this is above the DevTools, which are above the Overlay */
z-index: 10000002;
}

.Tooltip.hidden {
opacity: 0;
}


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

import React, {useRef} from 'react';

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 tooltipRef = useRef(null);

// update the position of the tooltip based on current mouse position
const updateTooltipPosition = (event: SyntheticMouseEvent<*>) => {
const element = tooltipRef.current;
if (element != null) {
// first find the mouse position
const mousePosition = getMousePosition(containerRef.current, event);
// use the mouse position to find the position of tooltip
const {left, top} = getTooltipPosition(element, mousePosition);
// update tooltip position
element.style.left = left;
element.style.top = top;
}
};

const onMouseMove = (event: SyntheticMouseEvent<*>) => {
updateTooltipPosition(event);
};

const tooltipClassName = label === null ? styles.hidden : '';

return (
<div
className={styles.Container}
onMouseMove={onMouseMove}
ref={containerRef}>
<div ref={tooltipRef} className={`${styles.Tooltip} ${tooltipClassName}`}>
{label}
</div>
{children}
</div>
);
}

// Method used to find the position of the tooltip based on current mouse position
function getTooltipPosition(element, mousePosition) {
const {height, mouseX, mouseY, width} = mousePosition;
const TOOLTIP_OFFSET_X = 5;
const TOOLTIP_OFFSET_Y = 15;
let top = 0;
let left = 0;

// Let's check the vertical position.
if (mouseY + TOOLTIP_OFFSET_Y + element.offsetHeight >= height) {
// The tooltip doesn't fit below the mouse cursor (which is our
// default strategy). Therefore we try to position it either above the
// mouse cursor or finally aligned with the window's top edge.
if (mouseY - TOOLTIP_OFFSET_Y - element.offsetHeight > 0) {
// We position the tooltip above the mouse cursor if it fits there.
top = `${mouseY - element.offsetHeight - TOOLTIP_OFFSET_Y}px`;
} else {
// Otherwise we align the tooltip with the window's top edge.
top = '0px';
}
} else {
top = `${mouseY + TOOLTIP_OFFSET_Y}px`;
}

// Now let's check the horizontal position.
if (mouseX + TOOLTIP_OFFSET_X + element.offsetWidth >= width) {
// The tooltip doesn't fit at the right of the mouse cursor (which is
// our default strategy). Therefore we try to position it either at the
// left of the mouse cursor or finally aligned with the window's left
// edge.
if (mouseX - TOOLTIP_OFFSET_X - element.offsetWidth > 0) {
// We position the tooltip at the left of the mouse cursor if it fits
// there.
left = `${mouseX - element.offsetWidth - TOOLTIP_OFFSET_X}px`;
} else {
// Otherwise, align the tooltip with the window's left edge.
left = '0px';
}
} else {
left = `${mouseX + TOOLTIP_OFFSET_X * 2}px`;
}

return {left, top};
}

// method used to find the current mouse position inside the container
function getMousePosition(
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,
onMouseEnter: (event: SyntheticMouseEvent<*>) => mixed,
onMouseLeave: (event: SyntheticMouseEvent<*>) => mixed,
placeLabelAboveNode?: boolean,
textStyle?: Object,
width: number,
Expand All @@ -33,6 +35,8 @@ export default function ChartNode({
isDimmed = false,
label,
onClick,
onMouseEnter,
onMouseLeave,
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}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onDoubleClick={onDoubleClick}
className={styles.Rect}
style={{
Expand Down
Loading

0 comments on commit 44e5f5e

Please sign in to comment.