mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[DevTools] Compute environment names for the timeline (#34892)
Stacked on #34885. This refactors the timeline to store not just an id but a complex object for each step. This will later represent a group of boundaries. Each timeline step is assigned an environment name. We pick the last environment name (assumed to have resolved last) from the union of the parent and child environment names. I.e. a child step is considered to be blocked by the parent so if a child isn't blocked on any environment name it still gets marked as the parent's environment name. In a follow up, I'd like to reorder the document order timeline based on environment names to favor loading everything in one environment before the next.
This commit is contained in:
parent
423c44b886
commit
a083344699
107
packages/react-devtools-shared/src/devtools/store.js
vendored
107
packages/react-devtools-shared/src/devtools/store.js
vendored
|
|
@ -34,6 +34,7 @@ import {
|
|||
shallowDiffers,
|
||||
utfDecodeStringWithRanges,
|
||||
parseElementDisplayNameFromBackend,
|
||||
unionOfTwoArrays,
|
||||
} from '../utils';
|
||||
import {localStorageGetItem, localStorageSetItem} from '../storage';
|
||||
import {__DEBUG__} from '../constants';
|
||||
|
|
@ -51,6 +52,7 @@ import type {
|
|||
ComponentFilter,
|
||||
ElementType,
|
||||
SuspenseNode,
|
||||
SuspenseTimelineStep,
|
||||
Rect,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {
|
||||
|
|
@ -895,13 +897,10 @@ export default class Store extends EventEmitter<{
|
|||
*/
|
||||
getSuspendableDocumentOrderSuspense(
|
||||
uniqueSuspendersOnly: boolean,
|
||||
): $ReadOnlyArray<SuspenseNode['id']> {
|
||||
): $ReadOnlyArray<SuspenseTimelineStep> {
|
||||
const target: Array<SuspenseTimelineStep> = [];
|
||||
const roots = this.roots;
|
||||
if (roots.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const list: SuspenseNode['id'][] = [];
|
||||
let rootStep: null | SuspenseTimelineStep = null;
|
||||
for (let i = 0; i < roots.length; i++) {
|
||||
const rootID = roots[i];
|
||||
const root = this.getElementByID(rootID);
|
||||
|
|
@ -912,45 +911,77 @@ export default class Store extends EventEmitter<{
|
|||
|
||||
const suspense = this.getSuspenseByID(rootID);
|
||||
if (suspense !== null) {
|
||||
if (list.length === 0) {
|
||||
// start with an arbitrary root that will allow inspection of the Screen
|
||||
list.push(suspense.id);
|
||||
const environments = suspense.environments;
|
||||
const environmentName =
|
||||
environments.length > 0
|
||||
? environments[environments.length - 1]
|
||||
: null;
|
||||
if (rootStep === null) {
|
||||
// Arbitrarily use the first root as the root step id.
|
||||
rootStep = {
|
||||
id: suspense.id,
|
||||
environment: environmentName,
|
||||
};
|
||||
target.push(rootStep);
|
||||
} else if (rootStep.environment === null) {
|
||||
// If any root has an environment name, then let's use it.
|
||||
rootStep.environment = environmentName;
|
||||
}
|
||||
this.pushTimelineStepsInDocumentOrder(
|
||||
suspense.children,
|
||||
target,
|
||||
uniqueSuspendersOnly,
|
||||
environments,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const stack = [suspense];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
if (current === undefined) {
|
||||
return target;
|
||||
}
|
||||
|
||||
pushTimelineStepsInDocumentOrder(
|
||||
children: Array<SuspenseNode['id']>,
|
||||
target: Array<SuspenseTimelineStep>,
|
||||
uniqueSuspendersOnly: boolean,
|
||||
parentEnvironments: Array<string>,
|
||||
): void {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = this.getSuspenseByID(children[i]);
|
||||
if (child === null) {
|
||||
continue;
|
||||
}
|
||||
// Ignore any suspense boundaries that has no visual representation as this is not
|
||||
// part of the visible loading sequence.
|
||||
// TODO: Consider making visible meta data and other side-effects get virtual rects.
|
||||
const hasRects =
|
||||
current.rects !== null &&
|
||||
current.rects.length > 0 &&
|
||||
current.rects.some(isNonZeroRect);
|
||||
if (
|
||||
hasRects &&
|
||||
(!uniqueSuspendersOnly || current.hasUniqueSuspenders) &&
|
||||
// Roots are already included as part of the Screen
|
||||
current.id !== rootID
|
||||
) {
|
||||
list.push(current.id);
|
||||
child.rects !== null &&
|
||||
child.rects.length > 0 &&
|
||||
child.rects.some(isNonZeroRect);
|
||||
const childEnvironments = child.environments;
|
||||
// Since children are blocked on the parent, they're also blocked by the parent environments.
|
||||
// Only if we discover a novel environment do we add that and it becomes the name we use.
|
||||
const unionEnvironments = unionOfTwoArrays(
|
||||
parentEnvironments,
|
||||
childEnvironments,
|
||||
);
|
||||
const environmentName =
|
||||
unionEnvironments.length > 0
|
||||
? unionEnvironments[unionEnvironments.length - 1]
|
||||
: null;
|
||||
if (hasRects && (!uniqueSuspendersOnly || child.hasUniqueSuspenders)) {
|
||||
target.push({
|
||||
id: child.id,
|
||||
environment: environmentName,
|
||||
});
|
||||
}
|
||||
// Add children in reverse order to maintain document order
|
||||
for (let j = current.children.length - 1; j >= 0; j--) {
|
||||
const childSuspense = this.getSuspenseByID(current.children[j]);
|
||||
if (childSuspense !== null) {
|
||||
stack.push(childSuspense);
|
||||
this.pushTimelineStepsInDocumentOrder(
|
||||
child.children,
|
||||
target,
|
||||
uniqueSuspendersOnly,
|
||||
unionEnvironments,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
getRendererIDForElement(id: number): number | null {
|
||||
let current = this._idToElement.get(id);
|
||||
|
|
@ -1627,6 +1658,7 @@ export default class Store extends EventEmitter<{
|
|||
rects,
|
||||
hasUniqueSuspenders: false,
|
||||
isSuspended: isSuspended,
|
||||
environments: [],
|
||||
});
|
||||
|
||||
hasSuspenseTreeChanged = true;
|
||||
|
|
@ -1812,7 +1844,10 @@ export default class Store extends EventEmitter<{
|
|||
envIndex++
|
||||
) {
|
||||
const environmentNameStringID = operations[i++];
|
||||
environmentNames.push(stringTable[environmentNameStringID]);
|
||||
const environmentName = stringTable[environmentNameStringID];
|
||||
if (environmentName != null) {
|
||||
environmentNames.push(environmentName);
|
||||
}
|
||||
}
|
||||
const suspense = this._idToSuspense.get(id);
|
||||
|
||||
|
|
@ -1836,7 +1871,7 @@ export default class Store extends EventEmitter<{
|
|||
|
||||
suspense.hasUniqueSuspenders = hasUniqueSuspenders;
|
||||
suspense.isSuspended = isSuspended;
|
||||
// TODO: Recompute the environment names.
|
||||
suspense.environments = environmentNames;
|
||||
}
|
||||
|
||||
hasSuspenseTreeChanged = true;
|
||||
|
|
|
|||
|
|
@ -154,7 +154,8 @@ function SuspenseRects({
|
|||
const selected = inspectedElementID === suspenseID;
|
||||
|
||||
const hovered =
|
||||
hoveredTimelineIndex > -1 && timeline[hoveredTimelineIndex] === suspenseID;
|
||||
hoveredTimelineIndex > -1 &&
|
||||
timeline[hoveredTimelineIndex].id === suspenseID;
|
||||
|
||||
const boundingBox = getBoundingBox(suspense.rects);
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ function SuspenseTimelineInput() {
|
|||
const max = timeline.length > 0 ? timeline.length - 1 : 0;
|
||||
|
||||
function switchSuspenseNode(nextTimelineIndex: number) {
|
||||
const nextSelectedSuspenseID = timeline[nextTimelineIndex];
|
||||
const nextSelectedSuspenseID = timeline[nextTimelineIndex].id;
|
||||
treeDispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: nextSelectedSuspenseID,
|
||||
|
|
@ -54,7 +54,7 @@ function SuspenseTimelineInput() {
|
|||
}
|
||||
|
||||
function handleHoverSegment(hoveredIndex: number) {
|
||||
const nextSelectedSuspenseID = timeline[hoveredIndex];
|
||||
const nextSelectedSuspenseID = timeline[hoveredIndex].id;
|
||||
suspenseTreeDispatch({
|
||||
type: 'HOVER_TIMELINE_FOR_ID',
|
||||
payload: nextSelectedSuspenseID,
|
||||
|
|
@ -68,7 +68,7 @@ function SuspenseTimelineInput() {
|
|||
}
|
||||
|
||||
function skipPrevious() {
|
||||
const nextSelectedSuspenseID = timeline[timelineIndex - 1];
|
||||
const nextSelectedSuspenseID = timeline[timelineIndex - 1].id;
|
||||
treeDispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: nextSelectedSuspenseID,
|
||||
|
|
@ -80,7 +80,7 @@ function SuspenseTimelineInput() {
|
|||
}
|
||||
|
||||
function skipForward() {
|
||||
const nextSelectedSuspenseID = timeline[timelineIndex + 1];
|
||||
const nextSelectedSuspenseID = timeline[timelineIndex + 1].id;
|
||||
treeDispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: nextSelectedSuspenseID,
|
||||
|
|
@ -106,7 +106,7 @@ function SuspenseTimelineInput() {
|
|||
// anything suspended in the root. The step after that should have one less
|
||||
// thing suspended. I.e. the first suspense boundary should be unsuspended
|
||||
// when it's selected. This also lets you show everything in the last step.
|
||||
const suspendedSet = timeline.slice(timelineIndex + 1);
|
||||
const suspendedSet = timeline.slice(timelineIndex + 1).map(step => step.id);
|
||||
bridge.send('overrideSuspenseMilestone', {
|
||||
suspendedSet,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@
|
|||
* @flow
|
||||
*/
|
||||
import type {ReactContext} from 'shared/ReactTypes';
|
||||
import type {SuspenseNode} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {
|
||||
SuspenseNode,
|
||||
SuspenseTimelineStep,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
import type Store from '../../store';
|
||||
|
||||
import * as React from 'react';
|
||||
|
|
@ -25,7 +28,7 @@ export type SuspenseTreeState = {
|
|||
lineage: $ReadOnlyArray<SuspenseNode['id']> | null,
|
||||
roots: $ReadOnlyArray<SuspenseNode['id']>,
|
||||
selectedSuspenseID: SuspenseNode['id'] | null,
|
||||
timeline: $ReadOnlyArray<SuspenseNode['id']>,
|
||||
timeline: $ReadOnlyArray<SuspenseTimelineStep>,
|
||||
timelineIndex: number | -1,
|
||||
hoveredTimelineIndex: number | -1,
|
||||
uniqueSuspendersOnly: boolean,
|
||||
|
|
@ -49,7 +52,7 @@ type ACTION_SELECT_SUSPENSE_BY_ID = {
|
|||
type ACTION_SET_SUSPENSE_TIMELINE = {
|
||||
type: 'SET_SUSPENSE_TIMELINE',
|
||||
payload: [
|
||||
$ReadOnlyArray<SuspenseNode['id']>,
|
||||
$ReadOnlyArray<SuspenseTimelineStep>,
|
||||
// The next Suspense ID to select in the timeline
|
||||
SuspenseNode['id'] | null,
|
||||
// Whether this timeline includes only unique suspenders
|
||||
|
|
@ -111,7 +114,7 @@ function getInitialState(store: Store): SuspenseTreeState {
|
|||
store.getSuspendableDocumentOrderSuspense(uniqueSuspendersOnly);
|
||||
const timelineIndex = timeline.length - 1;
|
||||
const selectedSuspenseID =
|
||||
timelineIndex === -1 ? null : timeline[timelineIndex];
|
||||
timelineIndex === -1 ? null : timeline[timelineIndex].id;
|
||||
const lineage =
|
||||
selectedSuspenseID !== null
|
||||
? store.getSuspenseLineage(selectedSuspenseID)
|
||||
|
|
@ -164,37 +167,44 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
|||
selectedSuspenseID = null;
|
||||
}
|
||||
|
||||
let selectedTimelineID =
|
||||
state.timeline === null
|
||||
const selectedTimelineStep =
|
||||
state.timeline === null || state.timelineIndex === -1
|
||||
? null
|
||||
: state.timeline[state.timelineIndex];
|
||||
while (
|
||||
selectedTimelineID !== null &&
|
||||
removedIDs.has(selectedTimelineID)
|
||||
) {
|
||||
// $FlowExpectedError[incompatible-type]
|
||||
let selectedTimelineID: null | number = null;
|
||||
if (selectedTimelineStep !== null) {
|
||||
selectedTimelineID = selectedTimelineStep.id;
|
||||
// $FlowFixMe
|
||||
while (removedIDs.has(selectedTimelineID)) {
|
||||
// $FlowFixMe
|
||||
selectedTimelineID = removedIDs.get(selectedTimelineID);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle different timeline modes (e.g. random order)
|
||||
const nextTimeline = store.getSuspendableDocumentOrderSuspense(
|
||||
state.uniqueSuspendersOnly,
|
||||
);
|
||||
|
||||
let nextTimelineIndex =
|
||||
selectedTimelineID === null || nextTimeline.length === 0
|
||||
? -1
|
||||
: nextTimeline.indexOf(selectedTimelineID);
|
||||
let nextTimelineIndex = -1;
|
||||
if (selectedTimelineID !== null && nextTimeline.length !== 0) {
|
||||
for (let i = 0; i < nextTimeline.length; i++) {
|
||||
if (nextTimeline[i].id === selectedTimelineID) {
|
||||
nextTimelineIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
nextTimeline.length > 0 &&
|
||||
(nextTimelineIndex === -1 || state.autoSelect)
|
||||
) {
|
||||
nextTimelineIndex = nextTimeline.length - 1;
|
||||
selectedSuspenseID = nextTimeline[nextTimelineIndex];
|
||||
selectedSuspenseID = nextTimeline[nextTimelineIndex].id;
|
||||
}
|
||||
|
||||
if (selectedSuspenseID === null && nextTimeline.length > 0) {
|
||||
selectedSuspenseID = nextTimeline[nextTimeline.length - 1];
|
||||
selectedSuspenseID = nextTimeline[nextTimeline.length - 1].id;
|
||||
}
|
||||
|
||||
const nextLineage =
|
||||
|
|
@ -256,12 +266,12 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
|||
nextMilestoneIndex = nextTimeline.indexOf(previousMilestoneID);
|
||||
if (nextMilestoneIndex === -1 && nextTimeline.length > 0) {
|
||||
nextMilestoneIndex = nextTimeline.length - 1;
|
||||
nextSelectedSuspenseID = nextTimeline[nextMilestoneIndex];
|
||||
nextSelectedSuspenseID = nextTimeline[nextMilestoneIndex].id;
|
||||
nextLineage = store.getSuspenseLineage(nextSelectedSuspenseID);
|
||||
}
|
||||
} else if (nextRootID !== null) {
|
||||
nextMilestoneIndex = nextTimeline.length - 1;
|
||||
nextSelectedSuspenseID = nextTimeline[nextMilestoneIndex];
|
||||
nextSelectedSuspenseID = nextTimeline[nextMilestoneIndex].id;
|
||||
nextLineage = store.getSuspenseLineage(nextSelectedSuspenseID);
|
||||
}
|
||||
|
||||
|
|
@ -276,7 +286,7 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
|||
}
|
||||
case 'SUSPENSE_SET_TIMELINE_INDEX': {
|
||||
const nextTimelineIndex = action.payload;
|
||||
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex];
|
||||
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex].id;
|
||||
const nextLineage = store.getSuspenseLineage(
|
||||
nextSelectedSuspenseID,
|
||||
);
|
||||
|
|
@ -301,7 +311,7 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
|||
) {
|
||||
return state;
|
||||
}
|
||||
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex];
|
||||
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex].id;
|
||||
const nextLineage = store.getSuspenseLineage(
|
||||
nextSelectedSuspenseID,
|
||||
);
|
||||
|
|
@ -329,7 +339,7 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
|||
) {
|
||||
// If we're restarting at the end. Then loop around and start again from the beginning.
|
||||
nextTimelineIndex = 0;
|
||||
nextSelectedSuspenseID = state.timeline[nextTimelineIndex];
|
||||
nextSelectedSuspenseID = state.timeline[nextTimelineIndex].id;
|
||||
nextLineage = store.getSuspenseLineage(nextSelectedSuspenseID);
|
||||
}
|
||||
|
||||
|
|
@ -352,7 +362,7 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
|||
if (nextTimelineIndex > state.timeline.length - 1) {
|
||||
return state;
|
||||
}
|
||||
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex];
|
||||
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex].id;
|
||||
const nextLineage = store.getSuspenseLineage(
|
||||
nextSelectedSuspenseID,
|
||||
);
|
||||
|
|
@ -369,8 +379,14 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
|||
}
|
||||
case 'TOGGLE_TIMELINE_FOR_ID': {
|
||||
const suspenseID = action.payload;
|
||||
const timelineIndexForSuspenseID =
|
||||
state.timeline.indexOf(suspenseID);
|
||||
|
||||
let timelineIndexForSuspenseID = -1;
|
||||
for (let i = 0; i < state.timeline.length; i++) {
|
||||
if (state.timeline[i].id === suspenseID) {
|
||||
timelineIndexForSuspenseID = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (timelineIndexForSuspenseID === -1) {
|
||||
// This boundary is no longer in the timeline.
|
||||
return state;
|
||||
|
|
@ -387,7 +403,7 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
|||
timelineIndexForSuspenseID
|
||||
: // Otherwise, if we're currently showing it, jump to right before to hide it.
|
||||
timelineIndexForSuspenseID - 1;
|
||||
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex];
|
||||
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex].id;
|
||||
const nextLineage = store.getSuspenseLineage(
|
||||
nextSelectedSuspenseID,
|
||||
);
|
||||
|
|
@ -403,8 +419,13 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
|||
}
|
||||
case 'HOVER_TIMELINE_FOR_ID': {
|
||||
const suspenseID = action.payload;
|
||||
const timelineIndexForSuspenseID =
|
||||
state.timeline.indexOf(suspenseID);
|
||||
let timelineIndexForSuspenseID = -1;
|
||||
for (let i = 0; i < state.timeline.length; i++) {
|
||||
if (state.timeline[i].id === suspenseID) {
|
||||
timelineIndexForSuspenseID = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
hoveredTimelineIndex: timelineIndexForSuspenseID,
|
||||
|
|
|
|||
|
|
@ -193,6 +193,11 @@ export type Rect = {
|
|||
height: number,
|
||||
};
|
||||
|
||||
export type SuspenseTimelineStep = {
|
||||
id: SuspenseNode['id'], // TODO: Will become a group.
|
||||
environment: null | string,
|
||||
};
|
||||
|
||||
export type SuspenseNode = {
|
||||
id: Element['id'],
|
||||
parentID: SuspenseNode['id'] | 0,
|
||||
|
|
@ -201,6 +206,7 @@ export type SuspenseNode = {
|
|||
rects: null | Array<Rect>,
|
||||
hasUniqueSuspenders: boolean,
|
||||
isSuspended: boolean,
|
||||
environments: Array<string>,
|
||||
};
|
||||
|
||||
// Serialized version of ReactIOInfo
|
||||
|
|
|
|||
15
packages/react-devtools-shared/src/utils.js
vendored
15
packages/react-devtools-shared/src/utils.js
vendored
|
|
@ -1305,3 +1305,18 @@ export function onReloadAndProfileFlagsReset(): void {
|
|||
sessionStorageRemoveItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY);
|
||||
sessionStorageRemoveItem(SESSION_STORAGE_RECORD_TIMELINE_KEY);
|
||||
}
|
||||
|
||||
export function unionOfTwoArrays<T>(a: Array<T>, b: Array<T>): Array<T> {
|
||||
let result = a;
|
||||
for (let i = 0; i < b.length; i++) {
|
||||
const value = b[i];
|
||||
if (a.indexOf(value) === -1) {
|
||||
if (result === a) {
|
||||
// Lazily copy
|
||||
result = a.slice(0);
|
||||
}
|
||||
result.push(value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user