Skip to content

Commit 93a3935

Browse files
authored
[DevTools] Only schedule a single update per Supense when changing timeline (#35927)
1 parent e0cc720 commit 93a3935

6 files changed

Lines changed: 86 additions & 20 deletions

File tree

packages/react-devtools-shared/src/__tests__/store-test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,7 @@ describe('Store', () => {
981981

982982
await actAsync(() => {
983983
agent.overrideSuspenseMilestone({
984+
rendererID: getRendererID(),
984985
suspendedSet: [
985986
store.getElementIDAtIndex(4),
986987
store.getElementIDAtIndex(8),
@@ -1010,6 +1011,7 @@ describe('Store', () => {
10101011

10111012
await actAsync(() => {
10121013
agent.overrideSuspenseMilestone({
1014+
rendererID: getRendererID(),
10131015
suspendedSet: [],
10141016
});
10151017
});

packages/react-devtools-shared/src/backend/agent.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ type OverrideSuspenseParams = {
147147
};
148148

149149
type OverrideSuspenseMilestoneParams = {
150+
rendererID: number,
150151
suspendedSet: Array<number>,
151152
};
152153

@@ -787,15 +788,14 @@ export default class Agent extends EventEmitter<{
787788
};
788789

789790
overrideSuspenseMilestone: OverrideSuspenseMilestoneParams => void = ({
791+
rendererID,
790792
suspendedSet,
791793
}) => {
792-
for (const rendererID in this._rendererInterfaces) {
793-
const renderer = ((this._rendererInterfaces[
794-
(rendererID: any)
795-
]: any): RendererInterface);
796-
if (renderer.supportsTogglingSuspense) {
797-
renderer.overrideSuspenseMilestone(suspendedSet);
798-
}
794+
const renderer = ((this._rendererInterfaces[
795+
(rendererID: any)
796+
]: any): RendererInterface);
797+
if (renderer.supportsTogglingSuspense) {
798+
renderer.overrideSuspenseMilestone(suspendedSet);
799799
}
800800
};
801801

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ type OverrideSuspense = {
145145
};
146146

147147
type OverrideSuspenseMilestone = {
148+
rendererID: number,
148149
suspendedSet: Array<number>,
149150
};
150151

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

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,12 @@ export default class Store extends EventEmitter<{
957957
if (root === null) {
958958
continue;
959959
}
960+
const rendererID = this._rootIDToRendererID.get(rootID);
961+
if (rendererID === undefined) {
962+
throw new Error(
963+
'Failed to find renderer ID for root. This is a bug in React DevTools.',
964+
);
965+
}
960966
// TODO: This includes boundaries that can't be suspended due to no support from the renderer.
961967

962968
const suspense = this.getSuspenseByID(rootID);
@@ -972,6 +978,7 @@ export default class Store extends EventEmitter<{
972978
id: suspense.id,
973979
environment: environmentName,
974980
endTime: suspense.endTime,
981+
rendererID,
975982
};
976983
target.push(rootStep);
977984
} else {
@@ -990,6 +997,7 @@ export default class Store extends EventEmitter<{
990997
uniqueSuspendersOnly,
991998
environments,
992999
0, // Don't pass a minimum end time at the root. The root is always first so doesn't matter.
1000+
rendererID,
9931001
);
9941002
}
9951003
}
@@ -1039,6 +1047,7 @@ export default class Store extends EventEmitter<{
10391047
*/
10401048
getSuspendableDocumentOrderSuspenseTransition(
10411049
uniqueSuspendersOnly: boolean,
1050+
rendererID: number,
10421051
): Array<SuspenseTimelineStep> {
10431052
const target: Array<SuspenseTimelineStep> = [];
10441053
const focusedTransitionID = this._focusedTransition;
@@ -1051,6 +1060,7 @@ export default class Store extends EventEmitter<{
10511060
// TODO: Get environment for Activity
10521061
environment: null,
10531062
endTime: 0,
1063+
rendererID,
10541064
});
10551065

10561066
const transitionChildren = this.getSuspenseChildren(focusedTransitionID);
@@ -1062,6 +1072,7 @@ export default class Store extends EventEmitter<{
10621072
// TODO: Get environment for Activity
10631073
[],
10641074
0, // Don't pass a minimum end time at the root. The root is always first so doesn't matter.
1075+
rendererID,
10651076
);
10661077

10671078
return target;
@@ -1073,6 +1084,7 @@ export default class Store extends EventEmitter<{
10731084
uniqueSuspendersOnly: boolean,
10741085
parentEnvironments: Array<string>,
10751086
parentEndTime: number,
1087+
rendererID: number,
10761088
): void {
10771089
for (let i = 0; i < children.length; i++) {
10781090
const child = this.getSuspenseByID(children[i]);
@@ -1106,6 +1118,7 @@ export default class Store extends EventEmitter<{
11061118
id: child.id,
11071119
environment: environmentName,
11081120
endTime: maxEndTime,
1121+
rendererID,
11091122
});
11101123
}
11111124
this.pushTimelineStepsInDocumentOrder(
@@ -1114,21 +1127,40 @@ export default class Store extends EventEmitter<{
11141127
uniqueSuspendersOnly,
11151128
unionEnvironments,
11161129
maxEndTime,
1130+
rendererID,
11171131
);
11181132
}
11191133
}
11201134

11211135
getEndTimeOrDocumentOrderSuspense(
11221136
uniqueSuspendersOnly: boolean,
11231137
): $ReadOnlyArray<SuspenseTimelineStep> {
1124-
const timeline =
1125-
this._focusedTransition === 0
1126-
? this.getSuspendableDocumentOrderSuspenseInitialPaint(
1127-
uniqueSuspendersOnly,
1128-
)
1129-
: this.getSuspendableDocumentOrderSuspenseTransition(
1130-
uniqueSuspendersOnly,
1131-
);
1138+
let timeline: SuspenseTimelineStep[];
1139+
if (this._focusedTransition === 0) {
1140+
timeline =
1141+
this.getSuspendableDocumentOrderSuspenseInitialPaint(
1142+
uniqueSuspendersOnly,
1143+
);
1144+
} else {
1145+
const focusedTransitionRootID = this.getRootIDForElement(
1146+
this._focusedTransition,
1147+
);
1148+
if (focusedTransitionRootID === null) {
1149+
throw new Error(
1150+
'Failed to find root ID for focused transition. This is a bug in React DevTools.',
1151+
);
1152+
}
1153+
const rendererID = this._rootIDToRendererID.get(focusedTransitionRootID);
1154+
if (rendererID === undefined) {
1155+
throw new Error(
1156+
'Failed to find renderer ID for focused transition root. This is a bug in React DevTools.',
1157+
);
1158+
}
1159+
timeline = this.getSuspendableDocumentOrderSuspenseTransition(
1160+
uniqueSuspendersOnly,
1161+
rendererID,
1162+
);
1163+
}
11321164

11331165
if (timeline.length === 0) {
11341166
return timeline;

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

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import * as React from 'react';
1111
import {useContext, useEffect} from 'react';
12-
import {BridgeContext} from '../context';
12+
import {BridgeContext, StoreContext} from '../context';
1313
import {TreeDispatcherContext} from '../Components/TreeContext';
1414
import {useScrollToHostInstance} from '../hooks';
1515
import {
@@ -20,9 +20,11 @@ import styles from './SuspenseTimeline.css';
2020
import SuspenseScrubber from './SuspenseScrubber';
2121
import Button from '../Button';
2222
import ButtonIcon from '../ButtonIcon';
23+
import type {SuspenseNode} from '../../../frontend/types';
2324

2425
function SuspenseTimelineInput() {
2526
const bridge = useContext(BridgeContext);
27+
const store = useContext(StoreContext);
2628
const treeDispatch = useContext(TreeDispatcherContext);
2729
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
2830
const scrollToHostInstance = useScrollToHostInstance();
@@ -101,15 +103,43 @@ function SuspenseTimelineInput() {
101103
// TODO: useEffectEvent here once it's supported in all versions DevTools supports.
102104
// For now we just exclude it from deps since we don't lint those anyway.
103105
function changeTimelineIndex(newIndex: number) {
106+
const suspendedSetByRendererID = new Map<
107+
number,
108+
Array<SuspenseNode['id']>,
109+
>();
110+
// Unsuspend everything by default.
111+
// We might not encounter every renderer after the milestone e.g.
112+
// if we clicked at the end of the timeline.
113+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
114+
for (const rendererID of store.rootIDToRendererID.values()) {
115+
suspendedSetByRendererID.set(rendererID, []);
116+
}
117+
104118
// Synchronize timeline index with what is resuspended.
105119
// We suspend everything after the current selection. The root isn't showing
106120
// anything suspended in the root. The step after that should have one less
107121
// thing suspended. I.e. the first suspense boundary should be unsuspended
108122
// when it's selected. This also lets you show everything in the last step.
109-
const suspendedSet = timeline.slice(timelineIndex + 1).map(step => step.id);
110-
bridge.send('overrideSuspenseMilestone', {
111-
suspendedSet,
112-
});
123+
for (let i = timelineIndex + 1; i < timeline.length; i++) {
124+
const step = timeline[i];
125+
const {rendererID} = step;
126+
const suspendedSetForRendererID =
127+
suspendedSetByRendererID.get(rendererID);
128+
if (suspendedSetForRendererID === undefined) {
129+
throw new Error(
130+
`Should have initialized suspended set for renderer ID "${rendererID}" earlier. This is a bug in React DevTools.`,
131+
);
132+
}
133+
suspendedSetForRendererID.push(step.id);
134+
}
135+
136+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
137+
for (const [rendererID, suspendedSet] of suspendedSetByRendererID) {
138+
bridge.send('overrideSuspenseMilestone', {
139+
rendererID,
140+
suspendedSet,
141+
});
142+
}
113143
}
114144

115145
useEffect(() => {

packages/react-devtools-shared/src/frontend/types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ export type SuspenseTimelineStep = {
210210
*/
211211
id: SuspenseNode['id'] | Element['id'], // TODO: Will become a group.
212212
environment: null | string,
213+
rendererID: number,
213214
endTime: number,
214215
};
215216

0 commit comments

Comments
 (0)