mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Add fiber summary tooltip to devtools profiling (#18048)
* 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
This commit is contained in:
@@ -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;
|
||||
}
|
||||
+138
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -33,6 +35,8 @@ export default function ChartNode({
|
||||
isDimmed = false,
|
||||
label,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onDoubleClick,
|
||||
textStyle,
|
||||
width,
|
||||
@@ -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={{
|
||||
|
||||
+41
-11
@@ -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,
|
||||
@@ -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);
|
||||
|
||||
@@ -118,6 +129,7 @@ function CommitFlamegraph({chartData, commitTree, height, width}: Props) {
|
||||
const itemData = useMemo<ItemData>(
|
||||
() => ({
|
||||
chartData,
|
||||
hoverFiber,
|
||||
scaleX: scale(
|
||||
0,
|
||||
selectedChartNode !== null
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+14
@@ -14,6 +14,7 @@ import {getGradientColor} from './utils';
|
||||
import ChartNode from './ChartNode';
|
||||
import {SettingsContext} from '../Settings/SettingsContext';
|
||||
|
||||
import type {ChartNode as ChartNodeType} from './FlamegraphChartBuilder';
|
||||
import type {ItemData} from './CommitFlamegraph';
|
||||
|
||||
type Props = {
|
||||
@@ -26,6 +27,7 @@ type Props = {
|
||||
function CommitFlamegraphListItem({data, index, style}: Props) {
|
||||
const {
|
||||
chartData,
|
||||
hoverFiber,
|
||||
scaleX,
|
||||
selectedChartNode,
|
||||
selectedChartNodeIndex,
|
||||
@@ -35,6 +37,7 @@ function CommitFlamegraphListItem({data, index, style}: Props) {
|
||||
const {renderPathNodes, maxSelfDuration, rows} = chartData;
|
||||
|
||||
const {lineHeight} = useContext(SettingsContext);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event: SyntheticMouseEvent<*>, id: number, name: string) => {
|
||||
event.stopPropagation();
|
||||
@@ -43,6 +46,15 @@ function CommitFlamegraphListItem({data, index, style}: Props) {
|
||||
[selectFiber],
|
||||
);
|
||||
|
||||
const handleMouseEnter = (nodeData: ChartNodeType) => {
|
||||
const {id, name} = nodeData;
|
||||
hoverFiber({id, name});
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
hoverFiber(null);
|
||||
};
|
||||
|
||||
// List items are absolutely positioned using the CSS "top" attribute.
|
||||
// The "left" value will always be 0.
|
||||
// Since height is fixed, and width is based on the node's duration,
|
||||
@@ -104,6 +116,8 @@ function CommitFlamegraphListItem({data, index, style}: Props) {
|
||||
key={id}
|
||||
label={label}
|
||||
onClick={event => handleClick(event, id, name)}
|
||||
onMouseEnter={() => handleMouseEnter(chartNode)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
textStyle={{color: textColor}}
|
||||
width={nodeWidth}
|
||||
x={nodeOffset - selectedNodeOffset}
|
||||
|
||||
+28
-10
@@ -7,23 +7,27 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React, {useCallback, useContext, useMemo} from 'react';
|
||||
import React, {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 CommitRankedListItem from './CommitRankedListItem';
|
||||
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 './CommitRanked.css';
|
||||
|
||||
import type {TooltipFiberData} from './HoveredFiberInfo';
|
||||
import type {ChartData} from './RankedChartBuilder';
|
||||
import type {CommitTree} from './types';
|
||||
|
||||
export type ItemData = {|
|
||||
chartData: ChartData,
|
||||
hoverFiber: (fiberData: TooltipFiberData | null) => void,
|
||||
scaleX: (value: number, fallbackValue: number) => number,
|
||||
selectedFiberID: number | null,
|
||||
selectedFiberIndex: number,
|
||||
@@ -89,6 +93,7 @@ type Props = {|
|
||||
|};
|
||||
|
||||
function CommitRanked({chartData, commitTree, height, width}: Props) {
|
||||
const [hoveredFiberData, hoverFiber] = useState<number | null>(null);
|
||||
const {lineHeight} = useContext(SettingsContext);
|
||||
const {selectedFiberID, selectFiber} = useContext(ProfilerContext);
|
||||
|
||||
@@ -100,6 +105,7 @@ function CommitRanked({chartData, commitTree, height, width}: Props) {
|
||||
const itemData = useMemo<ItemData>(
|
||||
() => ({
|
||||
chartData,
|
||||
hoverFiber,
|
||||
scaleX: scale(0, chartData.nodes[selectedFiberIndex].value, 0, width),
|
||||
selectedFiberID,
|
||||
selectedFiberIndex,
|
||||
@@ -109,16 +115,28 @@ function CommitRanked({chartData, commitTree, height, width}: Props) {
|
||||
[chartData, selectedFiberID, selectedFiberIndex, 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="svg"
|
||||
itemCount={chartData.nodes.length}
|
||||
itemData={itemData}
|
||||
itemSize={lineHeight}
|
||||
width={width}>
|
||||
{CommitRankedListItem}
|
||||
</FixedSizeList>
|
||||
<Tooltip label={tooltipLabel}>
|
||||
<FixedSizeList
|
||||
height={height}
|
||||
innerElementType="svg"
|
||||
itemCount={chartData.nodes.length}
|
||||
itemData={itemData}
|
||||
itemSize={lineHeight}
|
||||
width={width}>
|
||||
{CommitRankedListItem}
|
||||
</FixedSizeList>
|
||||
>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+21
-2
@@ -24,7 +24,14 @@ type Props = {
|
||||
};
|
||||
|
||||
function CommitRankedListItem({data, index, style}: Props) {
|
||||
const {chartData, scaleX, selectedFiberIndex, selectFiber, width} = data;
|
||||
const {
|
||||
chartData,
|
||||
hoverFiber,
|
||||
scaleX,
|
||||
selectedFiberIndex,
|
||||
selectFiber,
|
||||
width,
|
||||
} = data;
|
||||
|
||||
const node = chartData.nodes[index];
|
||||
|
||||
@@ -33,11 +40,21 @@ function CommitRankedListItem({data, index, style}: Props) {
|
||||
const handleClick = useCallback(
|
||||
event => {
|
||||
event.stopPropagation();
|
||||
selectFiber(node.id, node.name);
|
||||
const {id, name} = node;
|
||||
selectFiber(id, name);
|
||||
},
|
||||
[node, selectFiber],
|
||||
);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
const {id, name} = node;
|
||||
hoverFiber({id, name});
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
hoverFiber(null);
|
||||
};
|
||||
|
||||
// List items are absolutely positioned using the CSS "top" attribute.
|
||||
// The "left" value will always be 0.
|
||||
// Since height is fixed, and width is based on the node's duration,
|
||||
@@ -52,6 +69,8 @@ function CommitRankedListItem({data, index, style}: Props) {
|
||||
key={node.id}
|
||||
label={node.label}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
width={Math.max(minBarWidth, scaleX(node.value, width))}
|
||||
x={0}
|
||||
y={top}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
.Toolbar {
|
||||
height: 2.25rem;
|
||||
padding: 0 0.5rem;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.Content {
|
||||
padding: 0.5rem;
|
||||
user-select: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.Component {
|
||||
flex: 1;
|
||||
font-weight: bold;
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-normal);
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.Label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.CurrentCommit {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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, {Fragment, useContext} from 'react';
|
||||
import {ProfilerContext} from './ProfilerContext';
|
||||
import {formatDuration, formatTime} from './utils';
|
||||
import ProfilerWhatChanged from '../Components/ProfilerWhatChanged';
|
||||
import {StoreContext} from '../context';
|
||||
|
||||
import styles from './HoveredFiberInfo.css';
|
||||
|
||||
import type {ChartNode} from './FlamegraphChartBuilder';
|
||||
|
||||
export type TooltipFiberData = {|
|
||||
id: number,
|
||||
name: string,
|
||||
|};
|
||||
|
||||
export type Props = {
|
||||
fiberData: ChartNode,
|
||||
};
|
||||
|
||||
export default function HoveredFiberInfo({fiberData}: Props) {
|
||||
const {profilerStore} = useContext(StoreContext);
|
||||
const {rootID, selectedCommitIndex} = useContext(ProfilerContext);
|
||||
|
||||
const {id, name} = fiberData;
|
||||
const {profilingCache} = profilerStore;
|
||||
|
||||
const commitIndices = profilingCache.getFiberCommits({
|
||||
fiberID: ((id: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
let renderDurationInfo;
|
||||
let i = 0;
|
||||
for (i = 0; i < commitIndices.length; i++) {
|
||||
const commitIndex = commitIndices[i];
|
||||
if (selectedCommitIndex === commitIndex) {
|
||||
const {duration, timestamp} = profilerStore.getCommitData(
|
||||
((rootID: any): number),
|
||||
commitIndex,
|
||||
);
|
||||
|
||||
renderDurationInfo = (
|
||||
<Fragment>
|
||||
<label className={styles.Label}>Rendered at:</label>
|
||||
<div key={commitIndex} className={styles.CurrentCommit}>
|
||||
{formatTime(timestamp)}s for {formatDuration(duration)}ms
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={styles.Toolbar}>
|
||||
<div className={styles.Component}>{name}</div>
|
||||
</div>
|
||||
<div className={styles.Content}>
|
||||
<ProfilerWhatChanged fiberID={((id: any): number)} />
|
||||
{renderDurationInfo || (
|
||||
<div>Did not render during this profiling session.</div>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
+2
-19
@@ -42,6 +42,7 @@
|
||||
padding: 0.25rem 0.5rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.Commit:focus,
|
||||
.Commit:hover {
|
||||
outline: none;
|
||||
@@ -52,25 +53,7 @@
|
||||
background-color: var(--color-background-selected);
|
||||
color: var(--color-text-selected);
|
||||
}
|
||||
|
||||
.CurrentCommit:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.WhatChangedItem {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.WhatChangedKey {
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-small);
|
||||
line-height: 1;
|
||||
}
|
||||
.WhatChangedKey:first-of-type::before {
|
||||
content: ' (';
|
||||
}
|
||||
.WhatChangedKey::after {
|
||||
content: ', ';
|
||||
}
|
||||
.WhatChangedKey:last-of-type::after {
|
||||
content: ')';
|
||||
}
|
||||
|
||||
+2
-133
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import React, {Fragment, useContext} from 'react';
|
||||
import ProfilerStore from 'react-devtools-shared/src/devtools/ProfilerStore';
|
||||
import ProfilerWhatChanged from '../Components/ProfilerWhatChanged';
|
||||
import {ProfilerContext} from './ProfilerContext';
|
||||
import {formatDuration, formatTime} from './utils';
|
||||
import {StoreContext} from '../context';
|
||||
@@ -75,12 +75,7 @@ export default function SidebarSelectedFiberInfo(_: Props) {
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.Content}>
|
||||
<WhatChanged
|
||||
commitIndex={((selectedCommitIndex: any): number)}
|
||||
fiberID={((selectedFiberID: any): number)}
|
||||
profilerStore={profilerStore}
|
||||
rootID={((rootID: any): number)}
|
||||
/>
|
||||
<ProfilerWhatChanged fiberID={((selectedFiberID: any): number)} />
|
||||
{listItems.length > 0 && (
|
||||
<Fragment>
|
||||
<label className={styles.Label}>Rendered at</label>: {listItems}
|
||||
@@ -93,129 +88,3 @@ export default function SidebarSelectedFiberInfo(_: Props) {
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
type WhatChangedProps = {|
|
||||
commitIndex: number | null,
|
||||
fiberID: number,
|
||||
profilerStore: ProfilerStore,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
function WhatChanged({
|
||||
commitIndex,
|
||||
fiberID,
|
||||
profilerStore,
|
||||
rootID,
|
||||
}: WhatChangedProps) {
|
||||
// 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 (commitIndex === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {changeDescriptions} = profilerStore.getCommitData(
|
||||
((rootID: any): number),
|
||||
commitIndex,
|
||||
);
|
||||
if (changeDescriptions === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const changeDescription = changeDescriptions.get(fiberID);
|
||||
if (changeDescription == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (changeDescription.isFirstMount) {
|
||||
return (
|
||||
<div className={styles.WhatChanged}>
|
||||
<label className={styles.Label}>Why did this render?</label>
|
||||
<div className={styles.WhatChangedItem}>
|
||||
This is the first time the component rendered.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const changes = [];
|
||||
|
||||
if (changeDescription.context === true) {
|
||||
changes.push(
|
||||
<div key="context" className={styles.WhatChangedItem}>
|
||||
• Context changed
|
||||
</div>,
|
||||
);
|
||||
} else if (
|
||||
typeof changeDescription.context === 'object' &&
|
||||
changeDescription.context !== null &&
|
||||
changeDescription.context.length !== 0
|
||||
) {
|
||||
changes.push(
|
||||
<div key="context" className={styles.WhatChangedItem}>
|
||||
• Context changed:
|
||||
{changeDescription.context.map(key => (
|
||||
<span key={key} className={styles.WhatChangedKey}>
|
||||
{key}
|
||||
</span>
|
||||
))}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
if (changeDescription.didHooksChange) {
|
||||
changes.push(
|
||||
<div key="hooks" className={styles.WhatChangedItem}>
|
||||
• Hooks changed
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
changeDescription.props !== null &&
|
||||
changeDescription.props.length !== 0
|
||||
) {
|
||||
changes.push(
|
||||
<div key="props" className={styles.WhatChangedItem}>
|
||||
• Props changed:
|
||||
{changeDescription.props.map(key => (
|
||||
<span key={key} className={styles.WhatChangedKey}>
|
||||
{key}
|
||||
</span>
|
||||
))}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
changeDescription.state !== null &&
|
||||
changeDescription.state.length !== 0
|
||||
) {
|
||||
changes.push(
|
||||
<div key="state" className={styles.WhatChangedItem}>
|
||||
• State changed:
|
||||
{changeDescription.state.map(key => (
|
||||
<span key={key} className={styles.WhatChangedKey}>
|
||||
{key}
|
||||
</span>
|
||||
))}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
if (changes.length === 0) {
|
||||
changes.push(
|
||||
<div key="nothing" className={styles.WhatChangedItem}>
|
||||
The parent component rendered.
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.WhatChanged}>
|
||||
<label className={styles.Label}>Why did this render?</label>
|
||||
{changes}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user