Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DevTools] Track Tree Base Duration of Virtual Instances #30817

Merged
merged 3 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 75 additions & 56 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ type FiberInstance = {
componentStack: null | string,
errors: null | Map<string, number>, // error messages and count
warnings: null | Map<string, number>, // warning messages and count
treeBaseDuration: number, // the profiled time of the last render of this subtree
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one unnecessary field when isProfilingSupported isn't true (i.e. in production only builds). We could potentially make that conditionally added based on build but doesn't seem worth it. It's not worse otherwise since we added a map entry for everything otherwise.

data: Fiber, // one of a Fiber pair
};

Expand All @@ -170,6 +171,7 @@ function createFiberInstance(fiber: Fiber): FiberInstance {
componentStack: null,
errors: null,
warnings: null,
treeBaseDuration: 0,
data: fiber,
};
}
Expand All @@ -193,6 +195,7 @@ type VirtualInstance = {
// that old errors/warnings don't disappear when the instance is refreshed.
errors: null | Map<string, number>, // error messages and count
warnings: null | Map<string, number>, // warning messages and count
treeBaseDuration: number, // the profiled time of the last render of this subtree
// The latest info for this instance. This can be updated over time and the
// same info can appear in more than once ServerComponentInstance.
data: ReactComponentInfo,
Expand All @@ -212,6 +215,7 @@ function createVirtualInstance(
componentStack: null,
errors: null,
warnings: null,
treeBaseDuration: 0,
data: debugEntry,
};
}
Expand Down Expand Up @@ -1346,16 +1350,6 @@ export function attach(
}
}

// When profiling is supported, we store the latest tree base durations for each Fiber.
// This is so that we can quickly capture a snapshot of those values if profiling starts.
// If we didn't store these values, we'd have to crawl the tree when profiling started,
// and use a slow path to find each of the current Fibers.
const idToTreeBaseDurationMap: Map<number, number> = new Map();

// When profiling is supported, we store the latest tree base durations for each Fiber.
// This map enables us to filter these times by root when sending them to the frontend.
const idToRootMap: Map<number, number> = new Map();

// When a mount or update is in progress, this value tracks the root that is being operated on.
let currentRootID: number = -1;

Expand Down Expand Up @@ -2192,9 +2186,7 @@ export function attach(
}

if (isProfilingSupported) {
idToRootMap.set(id, currentRootID);

recordProfilingDurations(fiber);
recordProfilingDurations(fiberInstance);
}
return fiberInstance;
}
Expand All @@ -2207,8 +2199,6 @@ export function attach(

idToDevToolsInstanceMap.set(id, instance);

const isProfilingSupported = false; // TODO: Support Tree Base Duration Based on Children.

const componentInfo = instance.data;

const key =
Expand Down Expand Up @@ -2245,12 +2235,6 @@ export function attach(
pushOperation(ownerID);
pushOperation(displayNameStringID);
pushOperation(keyStringID);

if (isProfilingSupported) {
idToRootMap.set(id, currentRootID);
// TODO: Include tree base duration of children somehow.
// recordProfilingDurations(...);
}
}

function recordUnmount(fiberInstance: FiberInstance): void {
Expand Down Expand Up @@ -2285,12 +2269,6 @@ export function attach(
}

untrackFiber(fiberInstance);

const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
if (isProfilingSupported) {
idToRootMap.delete(id);
idToTreeBaseDurationMap.delete(id);
}
}

// Running state of the remaining children from the previous version of this parent that
Expand Down Expand Up @@ -2401,6 +2379,8 @@ export function attach(
traceNearestHostComponentUpdate,
virtualLevel + 1,
);
// Must be called after all children have been appended.
recordVirtualProfilingDurations(virtualInstance);
} finally {
reconcilingParent = stashedParent;
previouslyReconciledSibling = stashedPrevious;
Expand All @@ -2416,12 +2396,6 @@ export function attach(

const id = instance.id;
pendingRealUnmountedIDs.push(id);

const isProfilingSupported = false; // TODO: Profiling support.
if (isProfilingSupported) {
idToRootMap.delete(id);
idToTreeBaseDurationMap.delete(id);
}
}

function mountVirtualChildrenRecursively(
Expand Down Expand Up @@ -2655,11 +2629,12 @@ export function attach(
removeChild(instance);
}

function recordProfilingDurations(fiber: Fiber) {
const id = getFiberIDThrows(fiber);
function recordProfilingDurations(fiberInstance: FiberInstance) {
const id = fiberInstance.id;
const fiber = fiberInstance.data;
const {actualDuration, treeBaseDuration} = fiber;

idToTreeBaseDurationMap.set(id, treeBaseDuration || 0);
fiberInstance.treeBaseDuration = treeBaseDuration || 0;

if (isProfiling) {
const {alternate} = fiber;
Expand Down Expand Up @@ -2722,6 +2697,38 @@ export function attach(
}
}

function recordVirtualProfilingDurations(virtualInstance: VirtualInstance) {
const id = virtualInstance.id;

let treeBaseDuration = 0;
// Add up the base duration of the child instances. The virtual base duration
// will be the same as children's duration since we don't take up any render
// time in the virtual instance.
for (
let child = virtualInstance.firstChild;
child !== null;
child = child.nextSibling
) {
treeBaseDuration += child.treeBaseDuration;
}

if (isProfiling) {
const previousTreeBaseDuration = virtualInstance.treeBaseDuration;
if (treeBaseDuration !== previousTreeBaseDuration) {
// Tree base duration updates are included in the operations typed array.
// So we have to convert them from milliseconds to microseconds so we can send them as ints.
const convertedTreeBaseDuration = Math.floor(
(treeBaseDuration || 0) * 1000,
);
pushOperation(TREE_OPERATION_UPDATE_TREE_BASE_DURATION);
pushOperation(id);
pushOperation(convertedTreeBaseDuration);
}
}

virtualInstance.treeBaseDuration = treeBaseDuration;
}

function recordResetChildren(parentInstance: DevToolsInstance) {
if (__DEBUG__) {
if (
Expand Down Expand Up @@ -2789,6 +2796,8 @@ export function attach(
) {
recordResetChildren(virtualInstance);
}
// Must be called after all children have been appended.
recordVirtualProfilingDurations(virtualInstance);
} finally {
unmountRemainingChildren();
reconcilingParent = stashedParent;
Expand Down Expand Up @@ -3236,11 +3245,11 @@ export function attach(
}
}

if (shouldIncludeInTree) {
if (fiberInstance !== null) {
const isProfilingSupported =
nextFiber.hasOwnProperty('treeBaseDuration');
if (isProfilingSupported) {
recordProfilingDurations(nextFiber);
recordProfilingDurations(fiberInstance);
}
}
if (shouldResetChildren) {
Expand Down Expand Up @@ -5094,8 +5103,8 @@ export function attach(
let currentCommitProfilingMetadata: CommitProfilingData | null = null;
let displayNamesByRootID: DisplayNamesByRootID | null = null;
let idToContextsMap: Map<number, any> | null = null;
let initialTreeBaseDurationsMap: Map<number, number> | null = null;
let initialIDToRootMap: Map<number, number> | null = null;
let initialTreeBaseDurationsMap: Map<number, Array<[number, number]>> | null =
null;
let isProfiling: boolean = false;
let profilingStartTime: number = 0;
let recordChangeDescriptions: boolean = false;
Expand All @@ -5114,24 +5123,15 @@ export function attach(
rootToCommitProfilingMetadataMap.forEach(
(commitProfilingMetadata, rootID) => {
const commitData: Array<CommitDataBackend> = [];
const initialTreeBaseDurations: Array<[number, number]> = [];

const displayName =
(displayNamesByRootID !== null && displayNamesByRootID.get(rootID)) ||
'Unknown';

if (initialTreeBaseDurationsMap != null) {
initialTreeBaseDurationsMap.forEach((treeBaseDuration, id) => {
if (
initialIDToRootMap != null &&
initialIDToRootMap.get(id) === rootID
) {
// We don't need to convert milliseconds to microseconds in this case,
// because the profiling summary is JSON serialized.
initialTreeBaseDurations.push([id, treeBaseDuration]);
}
});
}
const initialTreeBaseDurations: Array<[number, number]> =
(initialTreeBaseDurationsMap !== null &&
initialTreeBaseDurationsMap.get(rootID)) ||
[];

commitProfilingMetadata.forEach((commitProfilingData, commitIndex) => {
const {
Expand Down Expand Up @@ -5218,6 +5218,22 @@ export function attach(
};
}

function snapshotTreeBaseDurations(
instance: DevToolsInstance,
target: Array<[number, number]>,
) {
// We don't need to convert milliseconds to microseconds in this case,
// because the profiling summary is JSON serialized.
target.push([instance.id, instance.treeBaseDuration]);
for (
let child = instance.firstChild;
child !== null;
child = child.nextSibling
) {
snapshotTreeBaseDurations(child, target);
}
}

function startProfiling(shouldRecordChangeDescriptions: boolean) {
if (isProfiling) {
return;
Expand All @@ -5230,16 +5246,19 @@ export function attach(
// since either of these may change during the profiling session
// (e.g. when a fiber is re-rendered or when a fiber gets removed).
displayNamesByRootID = new Map();
initialTreeBaseDurationsMap = new Map(idToTreeBaseDurationMap);
initialIDToRootMap = new Map(idToRootMap);
initialTreeBaseDurationsMap = new Map();
idToContextsMap = new Map();

hook.getFiberRoots(rendererID).forEach(root => {
const rootID = getFiberIDThrows(root.current);
const rootInstance = getFiberInstanceThrows(root.current);
const rootID = rootInstance.id;
((displayNamesByRootID: any): DisplayNamesByRootID).set(
rootID,
getDisplayNameForRoot(root.current),
);
const initialTreeBaseDurations: Array<[number, number]> = [];
snapshotTreeBaseDurations(rootInstance, initialTreeBaseDurations);
(initialTreeBaseDurationsMap: any).set(rootID, initialTreeBaseDurations);

if (shouldRecordChangeDescriptions) {
// Record all contexts at the time profiling is started.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export default function HoveredFiberInfo({fiberData}: Props): React.Node {
)}

<div className={styles.Content}>
{renderDurationInfo || <div>Did not render.</div>}
{renderDurationInfo || <div>Did not client render.</div>}

<WhatChanged fiberID={id} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export default function SidebarSelectedFiberInfo(): React.Node {
</div>
)}
{listItems.length === 0 && (
<div>Did not render during this profiling session.</div>
<div>Did not render on the client during this profiling session.</div>
)}
</div>
</Fragment>
Expand Down
Loading