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,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 */
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, {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