Skip to content

Commit

Permalink
[DevTools][State Tooltip] Added a stack of of component owners
Browse files Browse the repository at this point in the history
You now get some idea of the context of a component that causes a state change,
with the tooltip showing the stack of containing components. This is limited to
at most 5 for now from the originating component.

resolves #24170
  • Loading branch information
blakef committed May 20, 2022
1 parent 6e2f38f commit f88e9f0
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 2 deletions.
19 changes: 19 additions & 0 deletions packages/react-devtools-shared/src/backend/profilingHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ import {
// This makes the UI nicer to use.
const TIME_OFFSET = 10;

// In timeline when a component causes a state changes we a capture a sample of it's
// parent component's names. This helps developers locate the component. Too many and
// the popover becomes crowded and too few it uses its utility. I like 5, maybe you do
// too.
const MAX_PARENT_COMPONENTS_NAMES = 5;

let performanceTarget: Performance | null = null;

// If performance exists and supports the subset of the User Timing API that we require.
Expand Down Expand Up @@ -774,6 +780,18 @@ export function createProfilingHooks({
}
}

function getParentsNames(
fiber: Fiber,
maxDepth = MAX_PARENT_COMPONENTS_NAMES,
): string[] {
const out: string[] = [];
let parent: Fiber | null = fiber;
while (maxDepth-- > 0 && parent !== null && (parent = parent.return)) {
out.unshift(getDisplayNameForFiber(parent) || 'Unknown');
}
return out;
}

function markStateUpdateScheduled(fiber: Fiber, lane: Lane): void {
if (isProfiling || supportsUserTimingV3) {
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
Expand All @@ -783,6 +801,7 @@ export function createProfilingHooks({
if (currentTimelineData) {
currentTimelineData.schedulingEvents.push({
componentName,
parents: getParentsNames(fiber),
lanes: laneToLanesArray(lane),
timestamp: getRelativeTime(),
type: 'schedule-state-update',
Expand Down
35 changes: 34 additions & 1 deletion packages/react-devtools-timeline/src/EventTooltip.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,37 @@

.DimText {
color: var(--color-dim);
}
}

.Tree,
.Tree ul {
list-style-type: none;
}
.Tree {
padding-inline-start: 0;
}
.Tree ul {
padding-inline-start: 2ch;
position: relative;
}

.Tree ul li:before {
position: absolute;
content: '';
left: 0.5ch;
top: 0;
width: 1ch;
height: 1ch;
border-left: 1px solid var(--color-tooltip-text);
border-bottom: 1px solid var(--color-tooltip-text);
}

.Tree ul ul {
margin-inline-start: -1ch;
}

.Tree li {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
25 changes: 25 additions & 0 deletions packages/react-devtools-timeline/src/EventTooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import * as React from 'react';
import {formatDuration, formatTimestamp, trimString} from './utils/formatting';
import {getBatchRange} from './utils/getBatchRange';
import useSmartTooltip from './utils/useSmartTooltip';
import {isStateUpdateEvent} from './utils/flow';
import styles from './EventTooltip.css';

const MAX_TOOLTIP_TEXT_LENGTH = 60;
Expand Down Expand Up @@ -274,6 +275,19 @@ const TooltipNetworkMeasure = ({
);
};

const Tree = ({components}: {|components: string[]|}) =>
Array.from(components)
.reverse()
.reduce(
(children, component, i) => (
<ul className={i === components.length - 1 ? styles.Tree : null}>
<li>{component}</li>
{children}
</ul>
),
null,
);

const TooltipSchedulingEvent = ({
data,
schedulingEvent,
Expand Down Expand Up @@ -307,6 +321,16 @@ const TooltipSchedulingEvent = ({

const {componentName, timestamp, warning} = schedulingEvent;

let fiberTrace;
if (isStateUpdateEvent(schedulingEvent) && schedulingEvent.parents !== null) {
fiberTrace = (
<>
<div className={styles.Divider} />
<Tree components={schedulingEvent.parents} />
</>
);
}

return (
<>
<div className={styles.TooltipSection}>
Expand All @@ -316,6 +340,7 @@ const TooltipSchedulingEvent = ({
</span>
)}
{label}
{fiberTrace}
<div className={styles.Divider} />
<div className={styles.DetailsGrid}>
{laneLabels !== null && lanes !== null && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,9 +526,10 @@ function processTimelineEvent(
} else if (name.startsWith('--schedule-state-update-')) {
const [laneBitmaskString, componentName] = name.substr(24).split('-');

const stateUpdateEvent = {
const stateUpdateEvent: SchedulingEvent = {
type: 'schedule-state-update',
lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
parents: null,
componentName,
timestamp: startTime,
warning: null,
Expand Down
1 change: 1 addition & 0 deletions packages/react-devtools-timeline/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type ReactScheduleRenderEvent = {|
|};
export type ReactScheduleStateUpdateEvent = {|
...BaseReactScheduleEvent,
+parents: string[] | null,
+type: 'schedule-state-update',
|};
export type ReactScheduleForceUpdateEvent = {|
Expand Down
13 changes: 13 additions & 0 deletions packages/react-devtools-timeline/src/utils/flow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* 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 type {SchedulingEvent} from '../types';

export function isStateUpdateEvent(event: SchedulingEvent): boolean %checks {
return event.type === 'schedule-state-update';
}

0 comments on commit f88e9f0

Please sign in to comment.