Skip to content

Commit 3a66917

Browse files
authored
[DevTools] Assign a different color and label based on environment (facebook#34893)
Stacked on facebook#34892. In the timeline scrubber each timeline entry gets a label and color assigned based on the environment computed for that step. In the rects, we find the timeline step that this boundary is part of and use that environment to assign a color. This is slightly different than picking from the boundary itself since it takes into account parent boundaries. In the "suspended by" section we color each entry individually based on the environment that spawned the I/O. <img width="790" height="813" alt="Screenshot 2025-10-17 at 12 18 56 AM" src="https://github.com/user-attachments/assets/c902b1fb-0992-4e24-8e94-a97ca8507551" />
1 parent a083344 commit 3a66917

File tree

7 files changed

+102
-22
lines changed

7 files changed

+102
-22
lines changed

packages/react-devtools-shared/src/devtools/constants.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,8 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
154154
'--color-warning-text-color': '#ffffff',
155155
'--color-warning-text-color-inverted': '#fd4d69',
156156

157-
'--color-suspense': '#0088fa',
158-
'--color-transition': '#6a51b2',
157+
'--color-suspense-default': '#0088fa',
158+
'--color-transition-default': '#6a51b2',
159159
'--color-suspense-server': '#62bc6a',
160160
'--color-transition-server': '#3f7844',
161161
'--color-suspense-other': '#f3ce49',
@@ -315,8 +315,8 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
315315
'--color-warning-text-color': '#ffffff',
316316
'--color-warning-text-color-inverted': '#ee1638',
317317

318-
'--color-suspense': '#61dafb',
319-
'--color-transition': '#6a51b2',
318+
'--color-suspense-default': '#61dafb',
319+
'--color-transition-default': '#6a51b2',
320320
'--color-suspense-server': '#62bc6a',
321321
'--color-transition-server': '#3f7844',
322322
'--color-suspense-other': '#f3ce49',

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import OwnerView from './OwnerView';
2222
import {meta} from '../../../hydration';
2323
import useInferredName from '../useInferredName';
2424

25+
import {getClassNameForEnvironment} from '../SuspenseTab/SuspenseEnvironmentColors.js';
26+
2527
import type {
2628
InspectedElement,
2729
SerializedAsyncInfo,
@@ -181,7 +183,12 @@ function SuspendedByRow({
181183
</>
182184
)}
183185
<div className={styles.CollapsableHeaderFiller} />
184-
<div className={styles.TimeBarContainer}>
186+
<div
187+
className={
188+
styles.TimeBarContainer +
189+
' ' +
190+
getClassNameForEnvironment(ioInfo.env)
191+
}>
185192
<div
186193
className={
187194
!isRejected ? styles.TimeBarSpan : styles.TimeBarSpanErrored
@@ -341,6 +348,7 @@ type GroupProps = {
341348
inspectedElement: InspectedElement,
342349
store: Store,
343350
name: string,
351+
environment: null | string,
344352
suspendedBy: Array<{
345353
index: number,
346354
value: SerializedAsyncInfo,
@@ -355,6 +363,7 @@ function SuspendedByGroup({
355363
inspectedElement,
356364
store,
357365
name,
366+
environment,
358367
suspendedBy,
359368
minTime,
360369
maxTime,
@@ -407,7 +416,12 @@ function SuspendedByGroup({
407416
<span className={styles.CollapsableHeaderTitle}>{pluralizedName}</span>
408417
<div className={styles.CollapsableHeaderFiller} />
409418
{isOpen ? null : (
410-
<div className={styles.TimeBarContainer}>
419+
<div
420+
className={
421+
styles.TimeBarContainer +
422+
' ' +
423+
getClassNameForEnvironment(environment)
424+
}>
411425
<div
412426
className={
413427
!isRejected ? styles.TimeBarSpan : styles.TimeBarSpanErrored
@@ -502,17 +516,21 @@ export default function InspectedElementSuspendedBy({
502516
const groups = [];
503517
let currentGroup = null;
504518
let currentGroupName = null;
519+
let currentGroupEnv = null;
505520
for (let i = 0; i < sortedSuspendedBy.length; i++) {
506521
const entry = sortedSuspendedBy[i];
507522
const name = entry.value.awaited.name;
523+
const env = entry.value.awaited.env;
508524
if (
509525
currentGroupName !== name ||
526+
currentGroupEnv !== env ||
510527
!name ||
511528
name === 'Promise' ||
512529
currentGroup === null
513530
) {
514531
// Create a new group.
515532
currentGroupName = name;
533+
currentGroupEnv = env;
516534
currentGroup = [];
517535
groups.push(currentGroup);
518536
}
@@ -591,6 +609,7 @@ export default function InspectedElementSuspendedBy({
591609
<SuspendedByGroup
592610
key={entries[0].index}
593611
name={entries[0].value.awaited.name}
612+
environment={entries[0].value.awaited.env}
594613
suspendedBy={entries}
595614
bridge={bridge}
596615
element={element}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.SuspenseEnvironmentDefault {
2+
--color-suspense: var(--color-suspense-default);
3+
--color-transition: var(--color-transition-default);
4+
}
5+
6+
.SuspenseEnvironmentServer {
7+
--color-suspense: var(--color-suspense-server);
8+
--color-transition: var(--color-transition-server);
9+
}
10+
11+
.SuspenseEnvironmentOther {
12+
--color-suspense: var(--color-suspense-other);
13+
--color-transition: var(--color-transition-other);
14+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import styles from './SuspenseEnvironmentColors.css';
11+
12+
export function getClassNameForEnvironment(environment: null | string): string {
13+
if (environment === null) {
14+
return styles.SuspenseEnvironmentDefault;
15+
}
16+
if (environment === 'Server') {
17+
return styles.SuspenseEnvironmentServer;
18+
}
19+
return styles.SuspenseEnvironmentOther;
20+
}

packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
SuspenseTreeStateContext,
3131
SuspenseTreeDispatcherContext,
3232
} from './SuspenseTreeContext';
33+
import {getClassNameForEnvironment} from './SuspenseEnvironmentColors.js';
3334

3435
function ScaledRect({
3536
className,
@@ -157,12 +158,25 @@ function SuspenseRects({
157158
hoveredTimelineIndex > -1 &&
158159
timeline[hoveredTimelineIndex].id === suspenseID;
159160

161+
let environment: null | string = null;
162+
for (let i = 0; i < timeline.length; i++) {
163+
const timelineStep = timeline[i];
164+
if (timelineStep.id === suspenseID) {
165+
environment = timelineStep.environment;
166+
break;
167+
}
168+
}
169+
160170
const boundingBox = getBoundingBox(suspense.rects);
161171

162172
return (
163173
<ScaledRect
164174
rect={boundingBox}
165-
className={styles.SuspenseRectsBoundary}
175+
className={
176+
styles.SuspenseRectsBoundary +
177+
' ' +
178+
getClassNameForEnvironment(environment)
179+
}
166180
visible={visible}
167181
selected={selected}
168182
suspended={suspense.isSuspended}
@@ -327,9 +341,8 @@ function SuspenseRectsContainer(): React$Node {
327341
const treeDispatch = useContext(TreeDispatcherContext);
328342
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
329343
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
330-
const {roots, hoveredTimelineIndex, uniqueSuspendersOnly} = useContext(
331-
SuspenseTreeStateContext,
332-
);
344+
const {roots, timeline, hoveredTimelineIndex, uniqueSuspendersOnly} =
345+
useContext(SuspenseTreeStateContext);
333346

334347
// TODO: bbox does not consider uniqueSuspendersOnly filter
335348
const boundingBox = getDocumentBoundingRect(store, roots);
@@ -389,11 +402,16 @@ function SuspenseRectsContainer(): React$Node {
389402
}
390403
}
391404

405+
const rootEnvironment =
406+
timeline.length === 0 ? null : timeline[0].environment;
407+
392408
return (
393409
<div
394410
className={
395411
styles.SuspenseRectsContainer +
396-
(hasRootSuspenders ? ' ' + styles.SuspenseRectsRoot : '')
412+
(hasRootSuspenders ? ' ' + styles.SuspenseRectsRoot : '') +
413+
' ' +
414+
getClassNameForEnvironment(rootEnvironment)
397415
}
398416
onClick={handleClick}
399417
onDoubleClick={handleDoubleClick}

packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.js

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,23 @@
77
* @flow
88
*/
99

10+
import type {SuspenseTimelineStep} from 'react-devtools-shared/src/frontend/types';
11+
1012
import typeof {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
1113

1214
import * as React from 'react';
1315
import {useRef} from 'react';
1416

1517
import styles from './SuspenseScrubber.css';
1618

19+
import {getClassNameForEnvironment} from './SuspenseEnvironmentColors.js';
20+
1721
import Tooltip from '../Components/reach-ui/tooltip';
1822

1923
export default function SuspenseScrubber({
2024
min,
2125
max,
26+
timeline,
2227
value,
2328
highlight,
2429
onBlur,
@@ -29,6 +34,7 @@ export default function SuspenseScrubber({
2934
}: {
3035
min: number,
3136
max: number,
37+
timeline: $ReadOnlyArray<SuspenseTimelineStep>,
3238
value: number,
3339
highlight: number,
3440
onBlur?: () => void,
@@ -54,17 +60,18 @@ export default function SuspenseScrubber({
5460
}
5561
const steps = [];
5662
for (let index = min; index <= max; index++) {
63+
const environment = timeline[index].environment;
64+
const label =
65+
index === min
66+
? // The first step in the timeline is always a Transition (Initial Paint).
67+
'Initial Paint' +
68+
(environment === null ? '' : ' (' + environment + ')')
69+
: // TODO: Consider adding the name of this specific boundary if this step has only one.
70+
environment === null
71+
? 'Suspense'
72+
: environment;
5773
steps.push(
58-
<Tooltip
59-
key={index}
60-
label={
61-
index === min
62-
? // The first step in the timeline is always a Transition (Initial Paint).
63-
// TODO: Support multiple environments.
64-
'Initial Paint'
65-
: // TODO: Consider adding the name of this specific boundary if this step has only one.
66-
'Suspense'
67-
}>
74+
<Tooltip key={index} label={label}>
6875
<div
6976
className={
7077
styles.SuspenseScrubberStep +
@@ -79,9 +86,10 @@ export default function SuspenseScrubber({
7986
styles.SuspenseScrubberBead +
8087
(index === min
8188
? // The first step in the timeline is always a Transition (Initial Paint).
82-
// TODO: Support multiple environments.
8389
' ' + styles.SuspenseScrubberBeadTransition
8490
: '') +
91+
' ' +
92+
getClassNameForEnvironment(environment) +
8593
(index <= value ? ' ' + styles.SuspenseScrubberBeadSelected : '')
8694
}
8795
/>

packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ function SuspenseTimelineInput() {
173173
<SuspenseScrubber
174174
min={min}
175175
max={max}
176+
timeline={timeline}
176177
value={timelineIndex}
177178
highlight={hoveredTimelineIndex}
178179
onChange={handleChange}

0 commit comments

Comments
 (0)