import {didFiberRender} from 'react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection';
import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
import type {RendererInternals, ProfilingState} from './DevToolsFacade';
import type {ToolError} from './DevToolsFacadeTreeTools';
import {getTypeTag} from './DevToolsFacadeTreeTools';
export type CommitComponent = {
uid: string,
name: string,
type: string,
actualDuration: number | null,
selfDuration: number | null,
};
export type TraceOverviewRow = {
commit: number,
committedAt: number,
renderDuration: number | null,
layoutDuration: number | null,
passiveDuration: number | null,
componentsChanged: number,
};
export type CommitReport = {
committedAt: number,
priority: string,
renderDuration: number | null,
layoutDuration: number | null,
passiveDuration: number | null,
components: Array<CommitComponent>,
};
export type StartProfilingResult = {status: 'started', traceName: string};
export type StopProfilingResult = {
status: 'stopped',
traceName: string,
commits: number,
};
export type ProfilerTools = {
startProfiling: (traceName?: string) => StartProfilingResult | ToolError,
stopProfiling: () => StopProfilingResult | ToolError,
getTraceOverview: (traceName: string) => Array<TraceOverviewRow> | ToolError,
getCommitReport: (
traceName: string,
commitIndex: number,
) => CommitReport | ToolError,
};
type CommitRecord = {
timestamp: number,
priority: string,
renderDuration: number | null,
layoutDuration: number | null,
passiveDuration: number | null,
durations: Array<CommitComponent>,
};
type TraceData = {
startTime: number,
commits: Array<CommitRecord>,
};
function priorityToString(
internals: RendererInternals,
schedulerPriority: number | void,
): string {
const {
ImmediatePriority,
UserBlockingPriority,
NormalPriority,
IdlePriority,
} = internals.ReactPriorityLevels;
switch (schedulerPriority) {
case ImmediatePriority:
return 'Sync';
case UserBlockingPriority:
return 'UserBlocking';
case NormalPriority:
return 'Normal';
case IdlePriority:
return 'Idle';
default:
return 'Normal';
}
}
export function createProfilerTools(
rendererInternals: Map<number, RendererInternals>,
profilingState: ProfilingState,
getUid: (fiber: Fiber) => string,
): ProfilerTools {
function collectDurations(
internals: RendererInternals,
fiber: Fiber,
durations: Array<CommitComponent>,
): void {
const {ReactTypeOfWork, getDisplayNameForFiber} = internals;
const displayName = getDisplayNameForFiber(fiber);
if (displayName != null) {
const prevFiber = fiber.alternate;
if (
prevFiber == null ||
didFiberRender(ReactTypeOfWork, prevFiber, fiber)
) {
const actual =
fiber.actualDuration != null ? fiber.actualDuration : null;
let self: number | null = actual;
if (actual != null) {
let selfDuration: number = actual;
let child = fiber.child;
while (child !== null) {
selfDuration -= child.actualDuration || 0;
child = child.sibling;
}
self = selfDuration;
}
durations.push({
uid: getUid(fiber),
name: displayName,
type: getTypeTag(ReactTypeOfWork, fiber.tag),
actualDuration: actual,
selfDuration: self,
});
}
}
let child = fiber.child;
while (child !== null) {
collectDurations(internals, child, durations);
child = child.sibling;
}
}
const pendingPassive: Map<FiberRoot, CommitRecord> = new Map();
function startProfiling(
traceName?: string,
): StartProfilingResult | ToolError {
if (profilingState.isActive) {
return {
error:
'Already profiling trace "' +
(profilingState.currentTraceName || '') +
'"',
};
}
const resolvedTraceName = traceName || 'trace-' + Date.now();
const trace: TraceData = {startTime: Date.now(), commits: []};
profilingState.traces.set(resolvedTraceName, trace);
profilingState.isActive = true;
profilingState.currentTraceName = resolvedTraceName;
profilingState.onCommit = function onCommit(
rendererID: number,
root: FiberRoot,
schedulerPriority: number | void,
) {
const internals = rendererInternals.get(rendererID);
if (internals == null) {
console.error(
'react-devtools-facade: Missing internals for renderer %s, commit not recorded.',
rendererID,
);
return;
}
const durations: Array<CommitComponent> = [];
collectDurations(internals, root.current, durations);
const rootFiber = root.current;
const record: CommitRecord = {
timestamp: Date.now(),
priority: priorityToString(internals, schedulerPriority),
renderDuration:
rootFiber.actualDuration != null ? rootFiber.actualDuration : null,
layoutDuration:
root.effectDuration != null ? root.effectDuration : null,
passiveDuration: null,
durations,
};
trace.commits.push(record);
pendingPassive.set(root, record);
};
profilingState.onPostCommit = function onPostCommit(root: FiberRoot) {
const record = pendingPassive.get(root);
if (record != null) {
record.passiveDuration =
root.passiveEffectDuration != null
? root.passiveEffectDuration
: null;
pendingPassive.delete(root);
}
};
return {status: 'started', traceName: resolvedTraceName};
}
function stopProfiling(): StopProfilingResult | ToolError {
if (!profilingState.isActive) {
return {error: 'Not currently profiling'};
}
const traceName = profilingState.currentTraceName;
if (traceName == null) {
return {error: 'No active trace'};
}
const trace = profilingState.traces.get(traceName);
const commitCount = trace ? trace.commits.length : 0;
profilingState.isActive = false;
profilingState.currentTraceName = null;
profilingState.onCommit = null;
profilingState.onPostCommit = null;
pendingPassive.clear();
return {status: 'stopped', traceName, commits: commitCount};
}
function getTrace(traceName: string): TraceData | null {
return profilingState.traces.get(traceName) || null;
}
function getTraceOverview(
traceName: string,
): Array<TraceOverviewRow> | ToolError {
const trace = getTrace(traceName);
if (trace == null) {
return {error: 'Unknown trace "' + traceName + '"'};
}
const rows: Array<TraceOverviewRow> = [];
for (let i = 0; i < trace.commits.length; i++) {
const commit = trace.commits[i];
rows.push({
commit: i,
committedAt: commit.timestamp - trace.startTime,
renderDuration: commit.renderDuration,
layoutDuration: commit.layoutDuration,
passiveDuration: commit.passiveDuration,
componentsChanged: commit.durations.length,
});
}
return rows;
}
function getCommitReport(
traceName: string,
commitIndex: number,
): CommitReport | ToolError {
const trace = getTrace(traceName);
if (trace == null) {
return {error: 'Unknown trace "' + traceName + '"'};
}
if (commitIndex < 0 || commitIndex >= trace.commits.length) {
return {error: 'Commit index out of range'};
}
const commit = trace.commits[commitIndex];
const components = commit.durations
.slice()
.sort((a, b) => (b.actualDuration || 0) - (a.actualDuration || 0));
return {
committedAt: commit.timestamp - trace.startTime,
priority: commit.priority,
renderDuration: commit.renderDuration,
layoutDuration: commit.layoutDuration,
passiveDuration: commit.passiveDuration,
components,
};
}
return {
startProfiling,
stopProfiling,
getTraceOverview,
getCommitReport,
};
}