mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 00:20:04 +01:00
[DevTools] Sort suspense timeline by end time instead of just document order (#35011)
Right now it's possible for things like server environments to appear before other content in the timeline just because it's in a different document order. Ofc the order in production is not guaranteed but we can at least use the timing information we have as a hint towards the actual order. Unfortunately since the end time of the RSC stream itself is always after the content that resolved to produce it, it becomes kind of determined by the chunking. Similarly since for a clean refresh, the scripts and styles will typically load after the server content they appear later. Similarly SSR typically finishes after the RSC parts. Therefore a hack here is that I artificially delay everything with a non-null environment (RSC) so that RSC always comes after client-side (Suspense). This is also consistent with how we color things that have an environment even if children are just Suspense. To ensure that we never show a child before a parent, in the timeline, each child has a minimum time of its parent.
This commit is contained in:
parent
4f93170066
commit
0a5fb67ddf
|
|
@ -301,6 +301,7 @@ type SuspenseNode = {
|
||||||
rects: null | Array<Rect>, // The bounding rects of content children.
|
rects: null | Array<Rect>, // The bounding rects of content children.
|
||||||
suspendedBy: Map<ReactIOInfo, Set<DevToolsInstance>>, // Tracks which data we're suspended by and the children that suspend it.
|
suspendedBy: Map<ReactIOInfo, Set<DevToolsInstance>>, // Tracks which data we're suspended by and the children that suspend it.
|
||||||
environments: Map<string, number>, // Tracks the Flight environment names that suspended this. I.e. if the server blocked this.
|
environments: Map<string, number>, // Tracks the Flight environment names that suspended this. I.e. if the server blocked this.
|
||||||
|
endTime: number, // Track a short cut to the maximum end time value within the suspendedBy set.
|
||||||
// Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all
|
// Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all
|
||||||
// also in the parent sets. This determine whether this could contribute in the loading sequence.
|
// also in the parent sets. This determine whether this could contribute in the loading sequence.
|
||||||
hasUniqueSuspenders: boolean,
|
hasUniqueSuspenders: boolean,
|
||||||
|
|
@ -330,6 +331,7 @@ function createSuspenseNode(
|
||||||
rects: null,
|
rects: null,
|
||||||
suspendedBy: new Map(),
|
suspendedBy: new Map(),
|
||||||
environments: new Map(),
|
environments: new Map(),
|
||||||
|
endTime: 0,
|
||||||
hasUniqueSuspenders: false,
|
hasUniqueSuspenders: false,
|
||||||
hasUnknownSuspenders: false,
|
hasUnknownSuspenders: false,
|
||||||
});
|
});
|
||||||
|
|
@ -2156,8 +2158,8 @@ export function attach(
|
||||||
// Regular operations
|
// Regular operations
|
||||||
pendingOperations.length +
|
pendingOperations.length +
|
||||||
// All suspender changes are batched in a single message.
|
// All suspender changes are batched in a single message.
|
||||||
// [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders, isSuspended]]
|
// [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders, endTime, isSuspended]]
|
||||||
(numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 3 : 0),
|
(numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 4 : 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Identify which renderer this update is coming from.
|
// Identify which renderer this update is coming from.
|
||||||
|
|
@ -2242,6 +2244,7 @@ export function attach(
|
||||||
}
|
}
|
||||||
operations[i++] = fiberIdWithChanges;
|
operations[i++] = fiberIdWithChanges;
|
||||||
operations[i++] = suspense.hasUniqueSuspenders ? 1 : 0;
|
operations[i++] = suspense.hasUniqueSuspenders ? 1 : 0;
|
||||||
|
operations[i++] = Math.round(suspense.endTime * 1000);
|
||||||
const instance = suspense.instance;
|
const instance = suspense.instance;
|
||||||
const isSuspended =
|
const isSuspended =
|
||||||
// TODO: Track if other SuspenseNode like SuspenseList rows are suspended.
|
// TODO: Track if other SuspenseNode like SuspenseList rows are suspended.
|
||||||
|
|
@ -2912,12 +2915,19 @@ export function attach(
|
||||||
// like owner instances to link down into the tree.
|
// like owner instances to link down into the tree.
|
||||||
if (!suspendedBySet.has(parentInstance)) {
|
if (!suspendedBySet.has(parentInstance)) {
|
||||||
suspendedBySet.add(parentInstance);
|
suspendedBySet.add(parentInstance);
|
||||||
|
const virtualEndTime = getVirtualEndTime(ioInfo);
|
||||||
if (
|
if (
|
||||||
!parentSuspenseNode.hasUniqueSuspenders &&
|
!parentSuspenseNode.hasUniqueSuspenders &&
|
||||||
!ioExistsInSuspenseAncestor(parentSuspenseNode, ioInfo)
|
!ioExistsInSuspenseAncestor(parentSuspenseNode, ioInfo)
|
||||||
) {
|
) {
|
||||||
// This didn't exist in the parent before, so let's mark this boundary as having a unique suspender.
|
// This didn't exist in the parent before, so let's mark this boundary as having a unique suspender.
|
||||||
parentSuspenseNode.hasUniqueSuspenders = true;
|
parentSuspenseNode.hasUniqueSuspenders = true;
|
||||||
|
if (parentSuspenseNode.endTime < virtualEndTime) {
|
||||||
|
parentSuspenseNode.endTime = virtualEndTime;
|
||||||
|
}
|
||||||
|
recordSuspenseSuspenders(parentSuspenseNode);
|
||||||
|
} else if (parentSuspenseNode.endTime < virtualEndTime) {
|
||||||
|
parentSuspenseNode.endTime = virtualEndTime;
|
||||||
recordSuspenseSuspenders(parentSuspenseNode);
|
recordSuspenseSuspenders(parentSuspenseNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2979,6 +2989,26 @@ export function attach(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getVirtualEndTime(ioInfo: ReactIOInfo): number {
|
||||||
|
if (ioInfo.env != null) {
|
||||||
|
// Sort client side content first so that scripts and streams don't
|
||||||
|
// cover up the effect of server time.
|
||||||
|
return ioInfo.end + 1000000;
|
||||||
|
}
|
||||||
|
return ioInfo.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeEndTime(suspenseNode: SuspenseNode) {
|
||||||
|
let maxEndTime = 0;
|
||||||
|
suspenseNode.suspendedBy.forEach((set, ioInfo) => {
|
||||||
|
const virtualEndTime = getVirtualEndTime(ioInfo);
|
||||||
|
if (virtualEndTime > maxEndTime) {
|
||||||
|
maxEndTime = virtualEndTime;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return maxEndTime;
|
||||||
|
}
|
||||||
|
|
||||||
function removePreviousSuspendedBy(
|
function removePreviousSuspendedBy(
|
||||||
instance: DevToolsInstance,
|
instance: DevToolsInstance,
|
||||||
previousSuspendedBy: null | Array<ReactAsyncInfo>,
|
previousSuspendedBy: null | Array<ReactAsyncInfo>,
|
||||||
|
|
@ -2996,6 +3026,7 @@ export function attach(
|
||||||
if (previousSuspendedBy !== null && suspenseNode !== null) {
|
if (previousSuspendedBy !== null && suspenseNode !== null) {
|
||||||
const nextSuspendedBy = instance.suspendedBy;
|
const nextSuspendedBy = instance.suspendedBy;
|
||||||
let changedEnvironment = false;
|
let changedEnvironment = false;
|
||||||
|
let mayHaveChangedEndTime = false;
|
||||||
for (let i = 0; i < previousSuspendedBy.length; i++) {
|
for (let i = 0; i < previousSuspendedBy.length; i++) {
|
||||||
const asyncInfo = previousSuspendedBy[i];
|
const asyncInfo = previousSuspendedBy[i];
|
||||||
if (
|
if (
|
||||||
|
|
@ -3009,6 +3040,11 @@ export function attach(
|
||||||
const ioInfo = asyncInfo.awaited;
|
const ioInfo = asyncInfo.awaited;
|
||||||
const suspendedBySet = suspenseNode.suspendedBy.get(ioInfo);
|
const suspendedBySet = suspenseNode.suspendedBy.get(ioInfo);
|
||||||
|
|
||||||
|
if (suspenseNode.endTime === getVirtualEndTime(ioInfo)) {
|
||||||
|
// This may be the only remaining entry at this end time. Recompute the end time.
|
||||||
|
mayHaveChangedEndTime = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
suspendedBySet === undefined ||
|
suspendedBySet === undefined ||
|
||||||
!suspendedBySet.delete(instance)
|
!suspendedBySet.delete(instance)
|
||||||
|
|
@ -3066,7 +3102,11 @@ export function attach(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changedEnvironment) {
|
const newEndTime = mayHaveChangedEndTime
|
||||||
|
? computeEndTime(suspenseNode)
|
||||||
|
: suspenseNode.endTime;
|
||||||
|
if (changedEnvironment || newEndTime !== suspenseNode.endTime) {
|
||||||
|
suspenseNode.endTime = newEndTime;
|
||||||
recordSuspenseSuspenders(suspenseNode);
|
recordSuspenseSuspenders(suspenseNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -925,7 +925,7 @@ export default class Store extends EventEmitter<{
|
||||||
*/
|
*/
|
||||||
getSuspendableDocumentOrderSuspense(
|
getSuspendableDocumentOrderSuspense(
|
||||||
uniqueSuspendersOnly: boolean,
|
uniqueSuspendersOnly: boolean,
|
||||||
): $ReadOnlyArray<SuspenseTimelineStep> {
|
): Array<SuspenseTimelineStep> {
|
||||||
const target: Array<SuspenseTimelineStep> = [];
|
const target: Array<SuspenseTimelineStep> = [];
|
||||||
const roots = this.roots;
|
const roots = this.roots;
|
||||||
let rootStep: null | SuspenseTimelineStep = null;
|
let rootStep: null | SuspenseTimelineStep = null;
|
||||||
|
|
@ -949,17 +949,25 @@ export default class Store extends EventEmitter<{
|
||||||
rootStep = {
|
rootStep = {
|
||||||
id: suspense.id,
|
id: suspense.id,
|
||||||
environment: environmentName,
|
environment: environmentName,
|
||||||
|
endTime: suspense.endTime,
|
||||||
};
|
};
|
||||||
target.push(rootStep);
|
target.push(rootStep);
|
||||||
} else if (rootStep.environment === null) {
|
} else {
|
||||||
// If any root has an environment name, then let's use it.
|
if (rootStep.environment === null) {
|
||||||
rootStep.environment = environmentName;
|
// If any root has an environment name, then let's use it.
|
||||||
|
rootStep.environment = environmentName;
|
||||||
|
}
|
||||||
|
if (suspense.endTime > rootStep.endTime) {
|
||||||
|
// If any root has a higher end time, let's use that.
|
||||||
|
rootStep.endTime = suspense.endTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.pushTimelineStepsInDocumentOrder(
|
this.pushTimelineStepsInDocumentOrder(
|
||||||
suspense.children,
|
suspense.children,
|
||||||
target,
|
target,
|
||||||
uniqueSuspendersOnly,
|
uniqueSuspendersOnly,
|
||||||
environments,
|
environments,
|
||||||
|
0, // Don't pass a minimum end time at the root. The root is always first so doesn't matter.
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -972,6 +980,7 @@ export default class Store extends EventEmitter<{
|
||||||
target: Array<SuspenseTimelineStep>,
|
target: Array<SuspenseTimelineStep>,
|
||||||
uniqueSuspendersOnly: boolean,
|
uniqueSuspendersOnly: boolean,
|
||||||
parentEnvironments: Array<string>,
|
parentEnvironments: Array<string>,
|
||||||
|
parentEndTime: number,
|
||||||
): void {
|
): void {
|
||||||
for (let i = 0; i < children.length; i++) {
|
for (let i = 0; i < children.length; i++) {
|
||||||
const child = this.getSuspenseByID(children[i]);
|
const child = this.getSuspenseByID(children[i]);
|
||||||
|
|
@ -996,10 +1005,15 @@ export default class Store extends EventEmitter<{
|
||||||
unionEnvironments.length > 0
|
unionEnvironments.length > 0
|
||||||
? unionEnvironments[unionEnvironments.length - 1]
|
? unionEnvironments[unionEnvironments.length - 1]
|
||||||
: null;
|
: null;
|
||||||
|
// The end time of a child boundary can in effect never be earlier than its parent even if
|
||||||
|
// everything unsuspended before that.
|
||||||
|
const maxEndTime =
|
||||||
|
parentEndTime > child.endTime ? parentEndTime : child.endTime;
|
||||||
if (hasRects && (!uniqueSuspendersOnly || child.hasUniqueSuspenders)) {
|
if (hasRects && (!uniqueSuspendersOnly || child.hasUniqueSuspenders)) {
|
||||||
target.push({
|
target.push({
|
||||||
id: child.id,
|
id: child.id,
|
||||||
environment: environmentName,
|
environment: environmentName,
|
||||||
|
endTime: maxEndTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.pushTimelineStepsInDocumentOrder(
|
this.pushTimelineStepsInDocumentOrder(
|
||||||
|
|
@ -1007,10 +1021,28 @@ export default class Store extends EventEmitter<{
|
||||||
target,
|
target,
|
||||||
uniqueSuspendersOnly,
|
uniqueSuspendersOnly,
|
||||||
unionEnvironments,
|
unionEnvironments,
|
||||||
|
maxEndTime,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEndTimeOrDocumentOrderSuspense(
|
||||||
|
uniqueSuspendersOnly: boolean,
|
||||||
|
): $ReadOnlyArray<SuspenseTimelineStep> {
|
||||||
|
const timeline =
|
||||||
|
this.getSuspendableDocumentOrderSuspense(uniqueSuspendersOnly);
|
||||||
|
if (timeline.length === 0) {
|
||||||
|
return timeline;
|
||||||
|
}
|
||||||
|
const root = timeline[0];
|
||||||
|
// We mutate in place since we assume we've got a fresh array.
|
||||||
|
timeline.sort((a, b) => {
|
||||||
|
// Root is always first
|
||||||
|
return a === root ? -1 : b === root ? 1 : a.endTime - b.endTime;
|
||||||
|
});
|
||||||
|
return timeline;
|
||||||
|
}
|
||||||
|
|
||||||
getRendererIDForElement(id: number): number | null {
|
getRendererIDForElement(id: number): number | null {
|
||||||
let current = this._idToElement.get(id);
|
let current = this._idToElement.get(id);
|
||||||
while (current !== undefined) {
|
while (current !== undefined) {
|
||||||
|
|
@ -1688,6 +1720,7 @@ export default class Store extends EventEmitter<{
|
||||||
hasUniqueSuspenders: false,
|
hasUniqueSuspenders: false,
|
||||||
isSuspended: isSuspended,
|
isSuspended: isSuspended,
|
||||||
environments: [],
|
environments: [],
|
||||||
|
endTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
hasSuspenseTreeChanged = true;
|
hasSuspenseTreeChanged = true;
|
||||||
|
|
@ -1884,6 +1917,7 @@ export default class Store extends EventEmitter<{
|
||||||
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
|
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
|
||||||
const id = operations[i++];
|
const id = operations[i++];
|
||||||
const hasUniqueSuspenders = operations[i++] === 1;
|
const hasUniqueSuspenders = operations[i++] === 1;
|
||||||
|
const endTime = operations[i++] / 1000;
|
||||||
const isSuspended = operations[i++] === 1;
|
const isSuspended = operations[i++] === 1;
|
||||||
const environmentNamesLength = operations[i++];
|
const environmentNamesLength = operations[i++];
|
||||||
const environmentNames = [];
|
const environmentNames = [];
|
||||||
|
|
@ -1919,6 +1953,7 @@ export default class Store extends EventEmitter<{
|
||||||
}
|
}
|
||||||
|
|
||||||
suspense.hasUniqueSuspenders = hasUniqueSuspenders;
|
suspense.hasUniqueSuspenders = hasUniqueSuspenders;
|
||||||
|
suspense.endTime = endTime;
|
||||||
suspense.isSuspended = isSuspended;
|
suspense.isSuspended = isSuspended;
|
||||||
suspense.environments = environmentNames;
|
suspense.environments = environmentNames;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -460,13 +460,14 @@ function updateTree(
|
||||||
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
|
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
|
||||||
const suspenseNodeId = operations[i++];
|
const suspenseNodeId = operations[i++];
|
||||||
const hasUniqueSuspenders = operations[i++] === 1;
|
const hasUniqueSuspenders = operations[i++] === 1;
|
||||||
|
const endTime = operations[i++] / 1000;
|
||||||
const isSuspended = operations[i++] === 1;
|
const isSuspended = operations[i++] === 1;
|
||||||
const environmentNamesLength = operations[i++];
|
const environmentNamesLength = operations[i++];
|
||||||
i += environmentNamesLength;
|
i += environmentNamesLength;
|
||||||
if (__DEBUG__) {
|
if (__DEBUG__) {
|
||||||
debug(
|
debug(
|
||||||
'Suspender changes',
|
'Suspender changes',
|
||||||
`Suspense node ${suspenseNodeId} unique suspenders set to ${String(hasUniqueSuspenders)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`,
|
`Suspense node ${suspenseNodeId} unique suspenders set to ${String(hasUniqueSuspenders)} ending at ${String(endTime)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ function ToggleUniqueSuspenders() {
|
||||||
function handleToggleUniqueSuspenders() {
|
function handleToggleUniqueSuspenders() {
|
||||||
const nextUniqueSuspendersOnly = !uniqueSuspendersOnly;
|
const nextUniqueSuspendersOnly = !uniqueSuspendersOnly;
|
||||||
// TODO: Handle different timeline modes (e.g. random order)
|
// TODO: Handle different timeline modes (e.g. random order)
|
||||||
const nextTimeline = store.getSuspendableDocumentOrderSuspense(
|
const nextTimeline = store.getEndTimeOrDocumentOrderSuspense(
|
||||||
nextUniqueSuspendersOnly,
|
nextUniqueSuspendersOnly,
|
||||||
);
|
);
|
||||||
suspenseTreeDispatch({
|
suspenseTreeDispatch({
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ type Props = {
|
||||||
function getInitialState(store: Store): SuspenseTreeState {
|
function getInitialState(store: Store): SuspenseTreeState {
|
||||||
const uniqueSuspendersOnly = true;
|
const uniqueSuspendersOnly = true;
|
||||||
const timeline =
|
const timeline =
|
||||||
store.getSuspendableDocumentOrderSuspense(uniqueSuspendersOnly);
|
store.getEndTimeOrDocumentOrderSuspense(uniqueSuspendersOnly);
|
||||||
const timelineIndex = timeline.length - 1;
|
const timelineIndex = timeline.length - 1;
|
||||||
const selectedSuspenseID =
|
const selectedSuspenseID =
|
||||||
timelineIndex === -1 ? null : timeline[timelineIndex].id;
|
timelineIndex === -1 ? null : timeline[timelineIndex].id;
|
||||||
|
|
@ -182,7 +182,7 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Handle different timeline modes (e.g. random order)
|
// TODO: Handle different timeline modes (e.g. random order)
|
||||||
const nextTimeline = store.getSuspendableDocumentOrderSuspense(
|
const nextTimeline = store.getEndTimeOrDocumentOrderSuspense(
|
||||||
state.uniqueSuspendersOnly,
|
state.uniqueSuspendersOnly,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,7 @@ export type Rect = {
|
||||||
export type SuspenseTimelineStep = {
|
export type SuspenseTimelineStep = {
|
||||||
id: SuspenseNode['id'], // TODO: Will become a group.
|
id: SuspenseNode['id'], // TODO: Will become a group.
|
||||||
environment: null | string,
|
environment: null | string,
|
||||||
|
endTime: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SuspenseNode = {
|
export type SuspenseNode = {
|
||||||
|
|
@ -207,6 +208,7 @@ export type SuspenseNode = {
|
||||||
hasUniqueSuspenders: boolean,
|
hasUniqueSuspenders: boolean,
|
||||||
isSuspended: boolean,
|
isSuspended: boolean,
|
||||||
environments: Array<string>,
|
environments: Array<string>,
|
||||||
|
endTime: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Serialized version of ReactIOInfo
|
// Serialized version of ReactIOInfo
|
||||||
|
|
|
||||||
3
packages/react-devtools-shared/src/utils.js
vendored
3
packages/react-devtools-shared/src/utils.js
vendored
|
|
@ -432,11 +432,12 @@ export function printOperationsArray(operations: Array<number>) {
|
||||||
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
|
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
|
||||||
const id = operations[i++];
|
const id = operations[i++];
|
||||||
const hasUniqueSuspenders = operations[i++] === 1;
|
const hasUniqueSuspenders = operations[i++] === 1;
|
||||||
|
const endTime = operations[i++] / 1000;
|
||||||
const isSuspended = operations[i++] === 1;
|
const isSuspended = operations[i++] === 1;
|
||||||
const environmentNamesLength = operations[i++];
|
const environmentNamesLength = operations[i++];
|
||||||
i += environmentNamesLength;
|
i += environmentNamesLength;
|
||||||
logs.push(
|
logs.push(
|
||||||
`Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`,
|
`Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} ending at ${String(endTime)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user