Skip to content

Commit b1c519f

Browse files
authored
[DevTools] Only show boundaries with unique suspenders by default in the timeline (facebook#34397)
1 parent 8c15014 commit b1c519f

File tree

10 files changed

+187
-22
lines changed

10 files changed

+187
-22
lines changed

packages/react-devtools-shared/src/backend/fiber/renderer.js

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ import {
8888
SUSPENSE_TREE_OPERATION_REMOVE,
8989
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
9090
SUSPENSE_TREE_OPERATION_RESIZE,
91+
SUSPENSE_TREE_OPERATION_SUSPENDERS,
9192
UNKNOWN_SUSPENDERS_NONE,
9293
UNKNOWN_SUSPENDERS_REASON_PRODUCTION,
9394
UNKNOWN_SUSPENDERS_REASON_OLD_VERSION,
@@ -2016,6 +2017,7 @@ export function attach(
20162017
const pendingOperations: OperationsArray = [];
20172018
const pendingRealUnmountedIDs: Array<FiberInstance['id']> = [];
20182019
const pendingRealUnmountedSuspenseIDs: Array<FiberInstance['id']> = [];
2020+
const pendingSuspenderChanges: Set<FiberInstance['id']> = new Set();
20192021
let pendingOperationsQueue: Array<OperationsArray> | null = [];
20202022
const pendingStringTable: Map<string, StringTableEntry> = new Map();
20212023
let pendingStringTableLength: number = 0;
@@ -2047,6 +2049,7 @@ export function attach(
20472049
pendingOperations.length === 0 &&
20482050
pendingRealUnmountedIDs.length === 0 &&
20492051
pendingRealUnmountedSuspenseIDs.length === 0 &&
2052+
pendingSuspenderChanges.size === 0 &&
20502053
pendingUnmountedRootID === null
20512054
);
20522055
}
@@ -2113,6 +2116,7 @@ export function attach(
21132116
pendingRealUnmountedIDs.length +
21142117
(pendingUnmountedRootID === null ? 0 : 1);
21152118
const numUnmountSuspenseIDs = pendingRealUnmountedSuspenseIDs.length;
2119+
const numSuspenderChanges = pendingSuspenderChanges.size;
21162120
21172121
const operations = new Array<number>(
21182122
// Identify which renderer this update is coming from.
@@ -2128,7 +2132,10 @@ export function attach(
21282132
// [TREE_OPERATION_REMOVE, removedIDLength, ...ids]
21292133
(numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) +
21302134
// Regular operations
2131-
pendingOperations.length,
2135+
pendingOperations.length +
2136+
// All suspender changes are batched in a single message.
2137+
// [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders]]
2138+
(numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 2 : 0),
21322139
);
21332140
21342141
// Identify which renderer this update is coming from.
@@ -2191,19 +2198,39 @@ export function attach(
21912198
i++;
21922199
}
21932200
}
2194-
// Fill in the rest of the operations.
2201+
2202+
// Fill in pending operations.
21952203
for (let j = 0; j < pendingOperations.length; j++) {
21962204
operations[i + j] = pendingOperations[j];
21972205
}
21982206
i += pendingOperations.length;
21992207
2208+
// Suspender changes might affect newly mounted nodes that we already recorded
2209+
// in pending operations.
2210+
if (numSuspenderChanges > 0) {
2211+
operations[i++] = SUSPENSE_TREE_OPERATION_SUSPENDERS;
2212+
operations[i++] = numSuspenderChanges;
2213+
pendingSuspenderChanges.forEach(fiberIdWithChanges => {
2214+
const suspense = idToSuspenseNodeMap.get(fiberIdWithChanges);
2215+
if (suspense === undefined) {
2216+
// Probably forgot to cleanup pendingSuspenderChanges when this node was removed.
2217+
throw new Error(
2218+
`Could not send suspender changes for "${fiberIdWithChanges}" since the Fiber no longer exists.`,
2219+
);
2220+
}
2221+
operations[i++] = fiberIdWithChanges;
2222+
operations[i++] = suspense.hasUniqueSuspenders ? 1 : 0;
2223+
});
2224+
}
2225+
22002226
// Let the frontend know about tree operations.
22012227
flushOrQueueOperations(operations);
22022228
22032229
// Reset all of the pending state now that we've told the frontend about it.
22042230
pendingOperations.length = 0;
22052231
pendingRealUnmountedIDs.length = 0;
22062232
pendingRealUnmountedSuspenseIDs.length = 0;
2233+
pendingSuspenderChanges.clear();
22072234
pendingUnmountedRootID = null;
22082235
pendingStringTable.clear();
22092236
pendingStringTableLength = 0;
@@ -2688,6 +2715,19 @@ export function attach(
26882715
}
26892716
}
26902717
2718+
function recordSuspenseSuspenders(suspenseNode: SuspenseNode): void {
2719+
if (__DEBUG__) {
2720+
console.log('recordSuspenseSuspenders()', suspenseNode);
2721+
}
2722+
const fiberInstance = suspenseNode.instance;
2723+
if (fiberInstance.kind !== FIBER_INSTANCE) {
2724+
// TODO: Suspender updates of filtered Suspense nodes are currently dropped.
2725+
return;
2726+
}
2727+
2728+
pendingSuspenderChanges.add(fiberInstance.id);
2729+
}
2730+
26912731
function recordSuspenseUnmount(suspenseInstance: SuspenseNode): void {
26922732
if (__DEBUG__) {
26932733
console.log(
@@ -2709,6 +2749,7 @@ export function attach(
27092749
// and later arrange them in the correct order.
27102750
pendingRealUnmountedSuspenseIDs.push(id);
27112751
2752+
pendingSuspenderChanges.delete(id);
27122753
idToSuspenseNodeMap.delete(id);
27132754
}
27142755
@@ -2779,6 +2820,7 @@ export function attach(
27792820
) {
27802821
// This didn't exist in the parent before, so let's mark this boundary as having a unique suspender.
27812822
parentSuspenseNode.hasUniqueSuspenders = true;
2823+
recordSuspenseSuspenders(parentSuspenseNode);
27822824
}
27832825
}
27842826
// We have observed at least one known reason this might have been suspended.
@@ -2820,6 +2862,9 @@ export function attach(
28202862
// We have found a child boundary that depended on the unblocked I/O.
28212863
// It can now be marked as having unique suspenders. We can skip its children
28222864
// since they'll still be blocked by this one.
2865+
if (!node.hasUniqueSuspenders) {
2866+
recordSuspenseSuspenders(node);
2867+
}
28232868
node.hasUniqueSuspenders = true;
28242869
node.hasUnknownSuspenders = false;
28252870
} else if (node.firstChild !== null) {
@@ -3522,6 +3567,9 @@ export function attach(
35223567
// Unfortunately if we don't have any DEV time debug info or debug thenables then
35233568
// we have no meta data to show. However, we still mark this Suspense boundary as
35243569
// participating in the loading sequence since apparently it can suspend.
3570+
if (!suspenseNode.hasUniqueSuspenders) {
3571+
recordSuspenseSuspenders(suspenseNode);
3572+
}
35253573
suspenseNode.hasUniqueSuspenders = true;
35263574
// We have not seen any reason yet for why this suspense node might have been
35273575
// suspended but it clearly has been at some point. If we later discover a reason

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const SUSPENSE_TREE_OPERATION_ADD = 8;
2828
export const SUSPENSE_TREE_OPERATION_REMOVE = 9;
2929
export const SUSPENSE_TREE_OPERATION_REORDER_CHILDREN = 10;
3030
export const SUSPENSE_TREE_OPERATION_RESIZE = 11;
31+
export const SUSPENSE_TREE_OPERATION_SUSPENDERS = 12;
3132

3233
export const PROFILING_FLAG_BASIC_SUPPORT = 0b01;
3334
export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10;

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

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
SUSPENSE_TREE_OPERATION_REMOVE,
2525
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
2626
SUSPENSE_TREE_OPERATION_RESIZE,
27+
SUSPENSE_TREE_OPERATION_SUSPENDERS,
2728
} from '../constants';
2829
import {ElementTypeRoot} from '../frontend/types';
2930
import {
@@ -879,8 +880,13 @@ export default class Store extends EventEmitter<{
879880
return null;
880881
}
881882

883+
/**
884+
* @param rootID
885+
* @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
886+
*/
882887
getSuspendableDocumentOrderSuspense(
883888
rootID: Element['id'] | void,
889+
uniqueSuspendersOnly: boolean,
884890
): $ReadOnlyArray<SuspenseNode['id']> {
885891
if (rootID === undefined) {
886892
return [];
@@ -892,7 +898,7 @@ export default class Store extends EventEmitter<{
892898
if (!this.supportsTogglingSuspense(root.id)) {
893899
return [];
894900
}
895-
const suspenseTreeList: SuspenseNode['id'][] = [];
901+
const list: SuspenseNode['id'][] = [];
896902
const suspense = this.getSuspenseByID(root.id);
897903
if (suspense !== null) {
898904
const stack = [suspense];
@@ -901,9 +907,11 @@ export default class Store extends EventEmitter<{
901907
if (current === undefined) {
902908
continue;
903909
}
904-
// Include the root even if we won't suspend it.
910+
// Include the root even if we won't show it suspended (because that's just blank).
905911
// You should be able to see what suspended the shell.
906-
suspenseTreeList.push(current.id);
912+
if (!uniqueSuspendersOnly || current.hasUniqueSuspenders) {
913+
list.push(current.id);
914+
}
907915
// Add children in reverse order to maintain document order
908916
for (let j = current.children.length - 1; j >= 0; j--) {
909917
const childSuspense = this.getSuspenseByID(current.children[j]);
@@ -914,7 +922,7 @@ export default class Store extends EventEmitter<{
914922
}
915923
}
916924

917-
return suspenseTreeList;
925+
return list;
918926
}
919927

920928
getRendererIDForElement(id: number): number | null {
@@ -1580,6 +1588,7 @@ export default class Store extends EventEmitter<{
15801588
children: [],
15811589
name,
15821590
rects,
1591+
hasUniqueSuspenders: false,
15831592
});
15841593

15851594
hasSuspenseTreeChanged = true;
@@ -1749,6 +1758,42 @@ export default class Store extends EventEmitter<{
17491758

17501759
break;
17511760
}
1761+
case SUSPENSE_TREE_OPERATION_SUSPENDERS: {
1762+
const changeLength = operations[i + 1];
1763+
i += 2;
1764+
1765+
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
1766+
const id = operations[i];
1767+
const hasUniqueSuspenders = operations[i + 1] === 1;
1768+
const suspense = this._idToSuspense.get(id);
1769+
1770+
if (suspense === undefined) {
1771+
this._throwAndEmitError(
1772+
Error(
1773+
`Cannot update suspenders of suspense node "${id}" because no matching node was found in the Store.`,
1774+
),
1775+
);
1776+
1777+
break;
1778+
}
1779+
1780+
i += 2;
1781+
1782+
if (__DEBUG__) {
1783+
const previousHasUniqueSuspenders = suspense.hasUniqueSuspenders;
1784+
debug(
1785+
'Suspender changes',
1786+
`Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} (was ${String(previousHasUniqueSuspenders)})`,
1787+
);
1788+
}
1789+
1790+
suspense.hasUniqueSuspenders = hasUniqueSuspenders;
1791+
}
1792+
1793+
hasSuspenseTreeChanged = true;
1794+
1795+
break;
1796+
}
17521797
default:
17531798
this._throwAndEmitError(
17541799
new UnsupportedBridgeOperationError(

packages/react-devtools-shared/src/devtools/views/ButtonIcon.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ type Props = {
5252
type: IconType,
5353
};
5454

55-
const materialIconsViewBox = '0 -960 960 960';
55+
const panelIcons = '0 -960 960 820';
5656
export default function ButtonIcon({className = '', type}: Props): React.Node {
5757
let pathData = null;
5858
let viewBox = '0 0 24 24';
@@ -131,27 +131,27 @@ export default function ButtonIcon({className = '', type}: Props): React.Node {
131131
break;
132132
case 'panel-left-close':
133133
pathData = PATH_MATERIAL_PANEL_LEFT_CLOSE;
134-
viewBox = materialIconsViewBox;
134+
viewBox = panelIcons;
135135
break;
136136
case 'panel-left-open':
137137
pathData = PATH_MATERIAL_PANEL_LEFT_OPEN;
138-
viewBox = materialIconsViewBox;
138+
viewBox = panelIcons;
139139
break;
140140
case 'panel-right-close':
141141
pathData = PATH_MATERIAL_PANEL_RIGHT_CLOSE;
142-
viewBox = materialIconsViewBox;
142+
viewBox = panelIcons;
143143
break;
144144
case 'panel-right-open':
145145
pathData = PATH_MATERIAL_PANEL_RIGHT_OPEN;
146-
viewBox = materialIconsViewBox;
146+
viewBox = panelIcons;
147147
break;
148148
case 'panel-bottom-open':
149149
pathData = PATH_MATERIAL_PANEL_BOTTOM_OPEN;
150-
viewBox = materialIconsViewBox;
150+
viewBox = panelIcons;
151151
break;
152152
case 'panel-bottom-close':
153153
pathData = PATH_MATERIAL_PANEL_BOTTOM_CLOSE;
154-
viewBox = materialIconsViewBox;
154+
viewBox = panelIcons;
155155
break;
156156
case 'suspend':
157157
pathData = PATH_SUSPEND;

packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
SUSPENSE_TREE_OPERATION_REMOVE,
2121
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
2222
SUSPENSE_TREE_OPERATION_RESIZE,
23+
SUSPENSE_TREE_OPERATION_SUSPENDERS,
2324
} from 'react-devtools-shared/src/constants';
2425
import {
2526
parseElementDisplayNameFromBackend,
@@ -452,6 +453,18 @@ function updateTree(
452453
break;
453454
}
454455

456+
case SUSPENSE_TREE_OPERATION_SUSPENDERS: {
457+
const changesLength = ((operations[i + 1]: any): number);
458+
459+
if (__DEBUG__) {
460+
const changes = operations.slice(i + 2, i + 2 + changesLength * 2);
461+
debug('Suspender changes', `[${changes.join(',')}]`);
462+
}
463+
464+
i += 2 + changesLength * 2;
465+
break;
466+
}
467+
455468
default:
456469
throw Error(`Unsupported Bridge operation "${operation}"`);
457470
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
display: flex;
44
flex-direction: row;
55
padding: 0.25rem;
6+
align-items: center;
67
}
78

89
.SuspenseTimelineInput {

0 commit comments

Comments
 (0)