import {
importFromChromeTimeline,
Flamechart as SpeedscopeFlamechart,
} from '@elg/speedscope';
import type {TimelineEvent} from '@elg/speedscope';
import type {
ErrorStackFrame,
BatchUID,
Flamechart,
Milliseconds,
NativeEvent,
NetworkMeasure,
Phase,
ReactLane,
ReactComponentMeasure,
ReactComponentMeasureType,
ReactMeasure,
ReactMeasureType,
TimelineData,
SchedulingEvent,
SuspenseEvent,
} from '../types';
import {
REACT_TOTAL_NUM_LANES,
SCHEDULING_PROFILER_VERSION,
SNAPSHOT_MAX_HEIGHT,
} from '../constants';
import InvalidProfileError from './InvalidProfileError';
import {getBatchRange} from '../utils/getBatchRange';
import ErrorStackParser from 'error-stack-parser';
type MeasureStackElement = {
type: ReactMeasureType,
depth: number,
measure: ReactMeasure,
startTime: Milliseconds,
stopTime?: Milliseconds,
};
type ProcessorState = {
asyncProcessingPromises: Promise<any>[],
batchUID: BatchUID,
currentReactComponentMeasure: ReactComponentMeasure | null,
internalModuleCurrentStackFrame: ErrorStackFrame | null,
internalModuleStackStringSet: Set<string>,
measureStack: MeasureStackElement[],
nativeEventStack: NativeEvent[],
nextRenderShouldGenerateNewBatchID: boolean,
potentialLongEvents: Array<[NativeEvent, BatchUID]>,
potentialLongNestedUpdate: SchedulingEvent | null,
potentialLongNestedUpdates: Array<[SchedulingEvent, BatchUID]>,
potentialSuspenseEventsOutsideOfTransition: Array<
[SuspenseEvent, ReactLane[]],
>,
requestIdToNetworkMeasureMap: Map<string, NetworkMeasure>,
uidCounter: BatchUID,
unresolvedSuspenseEvents: Map<string, SuspenseEvent>,
};
const NATIVE_EVENT_DURATION_THRESHOLD = 20;
const NESTED_UPDATE_DURATION_THRESHOLD = 20;
const WARNING_STRINGS = {
LONG_EVENT_HANDLER:
'An event handler scheduled a big update with React. Consider using the Transition API to defer some of this work.',
NESTED_UPDATE:
'A big nested update was scheduled during layout. ' +
'Nested updates require React to re-render synchronously before the browser can paint. ' +
'Consider delaying this update by moving it to a passive effect (useEffect).',
SUSPEND_DURING_UPDATE:
'A component suspended during an update which caused a fallback to be shown. ' +
"Consider using the Transition API to avoid hiding components after they've been mounted.",
};
export function getLanesFromTransportDecimalBitmask(
laneBitmaskString: string,
): ReactLane[] {
const laneBitmask = parseInt(laneBitmaskString, 10);
if (laneBitmask < 0) {
return [];
}
const lanes = [];
let powersOfTwo = 0;
while (powersOfTwo <= REACT_TOTAL_NUM_LANES) {
if ((1 << powersOfTwo) & laneBitmask) {
lanes.push(powersOfTwo);
}
powersOfTwo++;
}
return lanes;
}
function updateLaneToLabelMap(
profilerData: TimelineData,
laneLabelTuplesString: string,
): void {
if (profilerData.laneToLabelMap.size === 0) {
const laneLabelTuples = laneLabelTuplesString.split(',');
for (let laneIndex = 0; laneIndex < laneLabelTuples.length; laneIndex++) {
profilerData.laneToLabelMap.set(laneIndex, laneLabelTuples[laneIndex]);
}
}
}
let profilerVersion = null;
function getLastType(stack: $PropertyType<ProcessorState, 'measureStack'>) {
if (stack.length > 0) {
const {type} = stack[stack.length - 1];
return type;
}
return null;
}
function getDepth(stack: $PropertyType<ProcessorState, 'measureStack'>) {
if (stack.length > 0) {
const {depth, type} = stack[stack.length - 1];
return type === 'render-idle' ? depth : depth + 1;
}
return 0;
}
function markWorkStarted(
type: ReactMeasureType,
startTime: Milliseconds,
lanes: ReactLane[],
currentProfilerData: TimelineData,
state: ProcessorState,
) {
const {batchUID, measureStack} = state;
const depth = getDepth(measureStack);
const measure: ReactMeasure = {
type,
batchUID,
depth,
lanes,
timestamp: startTime,
duration: 0,
};
state.measureStack.push({depth, measure, startTime, type});
const measures = currentProfilerData.batchUIDToMeasuresMap.get(batchUID);
if (measures != null) {
measures.push(measure);
} else {
currentProfilerData.batchUIDToMeasuresMap.set(state.batchUID, [measure]);
}
lanes.forEach(lane => {
((currentProfilerData.laneToReactMeasureMap.get(
lane,
): any): ReactMeasure[]).push(measure);
});
}
function markWorkCompleted(
type: ReactMeasureType,
stopTime: Milliseconds,
currentProfilerData: TimelineData,
stack: $PropertyType<ProcessorState, 'measureStack'>,
) {
if (stack.length === 0) {
console.error(
'Unexpected type "%s" completed at %sms while stack is empty.',
type,
stopTime,
);
return;
}
const last = stack[stack.length - 1];
if (last.type !== type) {
console.error(
'Unexpected type "%s" completed at %sms before "%s" completed.',
type,
stopTime,
last.type,
);
}
const {measure, startTime} = stack.pop();
if (!measure) {
console.error('Could not find matching measure for type "%s".', type);
}
measure.duration = stopTime - startTime;
}
function throwIfIncomplete(
type: ReactMeasureType,
stack: $PropertyType<ProcessorState, 'measureStack'>,
) {
const lastIndex = stack.length - 1;
if (lastIndex >= 0) {
const last = stack[lastIndex];
if (last.stopTime === undefined && last.type === type) {
throw new InvalidProfileError(
`Unexpected type "${type}" started before "${last.type}" completed.`,
);
}
}
}
function processEventDispatch(
event: TimelineEvent,
timestamp: Milliseconds,
profilerData: TimelineData,
state: ProcessorState,
) {
const data = event.args.data;
const type = data.type;
if (type.startsWith('react-')) {
const stackTrace = data.stackTrace;
if (stackTrace) {
const topFrame = stackTrace[stackTrace.length - 1];
if (topFrame.url.includes('/react-dom.')) {
return;
}
}
}
if (
type === 'blur' ||
type === 'click' ||
type === 'input' ||
type.startsWith('focus') ||
type.startsWith('key') ||
type.startsWith('mouse') ||
type.startsWith('pointer')
) {
const duration = event.dur / 1000;
let depth = 0;
while (state.nativeEventStack.length > 0) {
const prevNativeEvent =
state.nativeEventStack[state.nativeEventStack.length - 1];
const prevStopTime = prevNativeEvent.timestamp + prevNativeEvent.duration;
if (timestamp < prevStopTime) {
depth = prevNativeEvent.depth + 1;
break;
} else {
state.nativeEventStack.pop();
}
}
const nativeEvent = {
depth,
duration,
timestamp,
type,
warning: null,
};
profilerData.nativeEvents.push(nativeEvent);
state.nativeEventStack.push(nativeEvent);
}
}
function processResourceFinish(
event: TimelineEvent,
timestamp: Milliseconds,
profilerData: TimelineData,
state: ProcessorState,
) {
const requestId = event.args.data.requestId;
const networkMeasure = state.requestIdToNetworkMeasureMap.get(requestId);
if (networkMeasure != null) {
networkMeasure.finishTimestamp = timestamp;
if (networkMeasure.firstReceivedDataTimestamp === 0) {
networkMeasure.firstReceivedDataTimestamp = timestamp;
}
if (networkMeasure.lastReceivedDataTimestamp === 0) {
networkMeasure.lastReceivedDataTimestamp = timestamp;
}
state.requestIdToNetworkMeasureMap.delete(event.args.data.requestId);
}
}
function processResourceReceivedData(
event: TimelineEvent,
timestamp: Milliseconds,
profilerData: TimelineData,
state: ProcessorState,
) {
const requestId = event.args.data.requestId;
const networkMeasure = state.requestIdToNetworkMeasureMap.get(requestId);
if (networkMeasure != null) {
if (networkMeasure.firstReceivedDataTimestamp === 0) {
networkMeasure.firstReceivedDataTimestamp = timestamp;
}
networkMeasure.lastReceivedDataTimestamp = timestamp;
networkMeasure.finishTimestamp = timestamp;
}
}
function processResourceReceiveResponse(
event: TimelineEvent,
timestamp: Milliseconds,
profilerData: TimelineData,
state: ProcessorState,
) {
const requestId = event.args.data.requestId;
const networkMeasure = state.requestIdToNetworkMeasureMap.get(requestId);
if (networkMeasure != null) {
networkMeasure.receiveResponseTimestamp = timestamp;
}
}
function processScreenshot(
event: TimelineEvent,
timestamp: Milliseconds,
profilerData: TimelineData,
state: ProcessorState,
) {
const encodedSnapshot = event.args.snapshot;
const snapshot = {
height: 0,
image: null,
imageSource: `data:image/png;base64,${encodedSnapshot}`,
timestamp,
width: 0,
};
let resolveFn = ((null: any): Function);
state.asyncProcessingPromises.push(
new Promise(resolve => {
resolveFn = resolve;
}),
);
fetch(snapshot.imageSource)
.then(response => response.blob())
.then(blob => {
createImageBitmap(blob).then(bitmap => {
snapshot.height = bitmap.height;
snapshot.width = bitmap.width;
resolveFn();
});
});
profilerData.snapshots.push(snapshot);
}
function processResourceSendRequest(
event: TimelineEvent,
timestamp: Milliseconds,
profilerData: TimelineData,
state: ProcessorState,
) {
const data = event.args.data;
const requestId = data.requestId;
const availableDepths = new Array<boolean>(
state.requestIdToNetworkMeasureMap.size + 1,
).fill(true);
state.requestIdToNetworkMeasureMap.forEach(({depth}) => {
availableDepths[depth] = false;
});
let depth = 0;
for (let i = 0; i < availableDepths.length; i++) {
if (availableDepths[i]) {
depth = i;
break;
}
}
const networkMeasure: NetworkMeasure = {
depth,
finishTimestamp: 0,
firstReceivedDataTimestamp: 0,
lastReceivedDataTimestamp: 0,
requestId,
requestMethod: data.requestMethod,
priority: data.priority,
sendRequestTimestamp: timestamp,
receiveResponseTimestamp: 0,
url: data.url,
};
state.requestIdToNetworkMeasureMap.set(requestId, networkMeasure);
profilerData.networkMeasures.push(networkMeasure);
networkMeasure.sendRequestTimestamp = timestamp;
}
function processTimelineEvent(
event: TimelineEvent,
currentProfilerData: TimelineData,
state: ProcessorState,
) {
const {cat, name, ts, ph} = event;
const startTime = (ts - currentProfilerData.startTime) / 1000;
switch (cat) {
case 'disabled-by-default-devtools.screenshot':
processScreenshot(event, startTime, currentProfilerData, state);
break;
case 'devtools.timeline':
switch (name) {
case 'EventDispatch':
processEventDispatch(event, startTime, currentProfilerData, state);
break;
case 'ResourceFinish':
processResourceFinish(event, startTime, currentProfilerData, state);
break;
case 'ResourceReceivedData':
processResourceReceivedData(
event,
startTime,
currentProfilerData,
state,
);
break;
case 'ResourceReceiveResponse':
processResourceReceiveResponse(
event,
startTime,
currentProfilerData,
state,
);
break;
case 'ResourceSendRequest':
processResourceSendRequest(
event,
startTime,
currentProfilerData,
state,
);
break;
}
break;
case 'blink.user_timing':
if (name.startsWith('--react-version-')) {
const [reactVersion] = name.slice(16).split('-');
currentProfilerData.reactVersion = reactVersion;
} else if (name.startsWith('--profiler-version-')) {
const [versionString] = name.slice(19).split('-');
profilerVersion = parseInt(versionString, 10);
if (profilerVersion !== SCHEDULING_PROFILER_VERSION) {
throw new InvalidProfileError(
`This version of profiling data (${versionString}) is not supported by the current profiler.`,
);
}
} else if (name.startsWith('--react-lane-labels-')) {
const [laneLabelTuplesString] = name.slice(20).split('-');
updateLaneToLabelMap(currentProfilerData, laneLabelTuplesString);
} else if (name.startsWith('--component-')) {
processReactComponentMeasure(
name,
startTime,
currentProfilerData,
state,
);
} else if (name.startsWith('--schedule-render-')) {
const [laneBitmaskString] = name.slice(18).split('-');
currentProfilerData.schedulingEvents.push({
type: 'schedule-render',
lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
timestamp: startTime,
warning: null,
});
} else if (name.startsWith('--schedule-forced-update-')) {
const [laneBitmaskString, componentName] = name.slice(25).split('-');
const forceUpdateEvent = {
type: 'schedule-force-update',
lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
componentName,
timestamp: startTime,
warning: null,
};
if (state.measureStack.find(({type}) => type === 'commit')) {
state.potentialLongNestedUpdate = forceUpdateEvent;
}
currentProfilerData.schedulingEvents.push(forceUpdateEvent);
} else if (name.startsWith('--schedule-state-update-')) {
const [laneBitmaskString, componentName] = name.slice(24).split('-');
const stateUpdateEvent = {
type: 'schedule-state-update',
lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
componentName,
timestamp: startTime,
warning: null,
};
if (state.measureStack.find(({type}) => type === 'commit')) {
state.potentialLongNestedUpdate = stateUpdateEvent;
}
currentProfilerData.schedulingEvents.push(stateUpdateEvent);
} else if (name.startsWith('--error-')) {
const [componentName, phase, message] = name.slice(8).split('-');
currentProfilerData.thrownErrors.push({
componentName,
message,
phase: ((phase: any): Phase),
timestamp: startTime,
type: 'thrown-error',
});
} else if (name.startsWith('--suspense-suspend-')) {
const [id, componentName, phase, laneBitmaskString, promiseName] = name
.slice(19)
.split('-');
const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
const availableDepths = new Array<boolean>(
state.unresolvedSuspenseEvents.size + 1,
).fill(true);
state.unresolvedSuspenseEvents.forEach(({depth}) => {
availableDepths[depth] = false;
});
let depth = 0;
for (let i = 0; i < availableDepths.length; i++) {
if (availableDepths[i]) {
depth = i;
break;
}
}
const suspenseEvent = {
componentName,
depth,
duration: null,
id,
phase: ((phase: any): Phase),
promiseName: promiseName || null,
resolution: 'unresolved',
timestamp: startTime,
type: 'suspense',
warning: null,
};
if (phase === 'update') {
state.potentialSuspenseEventsOutsideOfTransition.push([
suspenseEvent,
lanes,
]);
}
currentProfilerData.suspenseEvents.push(suspenseEvent);
state.unresolvedSuspenseEvents.set(id, suspenseEvent);
} else if (name.startsWith('--suspense-resolved-')) {
const [id] = name.slice(20).split('-');
const suspenseEvent = state.unresolvedSuspenseEvents.get(id);
if (suspenseEvent != null) {
state.unresolvedSuspenseEvents.delete(id);
suspenseEvent.duration = startTime - suspenseEvent.timestamp;
suspenseEvent.resolution = 'resolved';
}
} else if (name.startsWith('--suspense-rejected-')) {
const [id] = name.slice(20).split('-');
const suspenseEvent = state.unresolvedSuspenseEvents.get(id);
if (suspenseEvent != null) {
state.unresolvedSuspenseEvents.delete(id);
suspenseEvent.duration = startTime - suspenseEvent.timestamp;
suspenseEvent.resolution = 'rejected';
}
} else if (name.startsWith('--render-start-')) {
if (state.nextRenderShouldGenerateNewBatchID) {
state.nextRenderShouldGenerateNewBatchID = false;
state.batchUID = ((state.uidCounter++: any): BatchUID);
}
if (state.potentialLongNestedUpdate !== null) {
state.potentialLongNestedUpdates.push([
state.potentialLongNestedUpdate,
state.batchUID,
]);
state.potentialLongNestedUpdate = null;
}
const [laneBitmaskString] = name.slice(15).split('-');
throwIfIncomplete('render', state.measureStack);
if (getLastType(state.measureStack) !== 'render-idle') {
markWorkStarted(
'render-idle',
startTime,
getLanesFromTransportDecimalBitmask(laneBitmaskString),
currentProfilerData,
state,
);
}
markWorkStarted(
'render',
startTime,
getLanesFromTransportDecimalBitmask(laneBitmaskString),
currentProfilerData,
state,
);
for (let i = 0; i < state.nativeEventStack.length; i++) {
const nativeEvent = state.nativeEventStack[i];
const stopTime = nativeEvent.timestamp + nativeEvent.duration;
if (stopTime > startTime) {
state.potentialLongEvents.push([nativeEvent, state.batchUID]);
}
}
} else if (
name.startsWith('--render-stop') ||
name.startsWith('--render-yield')
) {
markWorkCompleted(
'render',
startTime,
currentProfilerData,
state.measureStack,
);
} else if (name.startsWith('--commit-start-')) {
state.nextRenderShouldGenerateNewBatchID = true;
const [laneBitmaskString] = name.slice(15).split('-');
markWorkStarted(
'commit',
startTime,
getLanesFromTransportDecimalBitmask(laneBitmaskString),
currentProfilerData,
state,
);
} else if (name.startsWith('--commit-stop')) {
markWorkCompleted(
'commit',
startTime,
currentProfilerData,
state.measureStack,
);
markWorkCompleted(
'render-idle',
startTime,
currentProfilerData,
state.measureStack,
);
} else if (name.startsWith('--layout-effects-start-')) {
const [laneBitmaskString] = name.slice(23).split('-');
markWorkStarted(
'layout-effects',
startTime,
getLanesFromTransportDecimalBitmask(laneBitmaskString),
currentProfilerData,
state,
);
} else if (name.startsWith('--layout-effects-stop')) {
markWorkCompleted(
'layout-effects',
startTime,
currentProfilerData,
state.measureStack,
);
} else if (name.startsWith('--passive-effects-start-')) {
const [laneBitmaskString] = name.slice(24).split('-');
markWorkStarted(
'passive-effects',
startTime,
getLanesFromTransportDecimalBitmask(laneBitmaskString),
currentProfilerData,
state,
);
} else if (name.startsWith('--passive-effects-stop')) {
markWorkCompleted(
'passive-effects',
startTime,
currentProfilerData,
state.measureStack,
);
} else if (name.startsWith('--react-internal-module-start-')) {
const stackFrameStart = name.slice(30);
if (!state.internalModuleStackStringSet.has(stackFrameStart)) {
state.internalModuleStackStringSet.add(stackFrameStart);
const parsedStackFrameStart = parseStackFrame(stackFrameStart);
state.internalModuleCurrentStackFrame = parsedStackFrameStart;
}
} else if (name.startsWith('--react-internal-module-stop-')) {
const stackFrameStop = name.slice(29);
if (!state.internalModuleStackStringSet.has(stackFrameStop)) {
state.internalModuleStackStringSet.add(stackFrameStop);
const parsedStackFrameStop = parseStackFrame(stackFrameStop);
if (
parsedStackFrameStop !== null &&
state.internalModuleCurrentStackFrame !== null
) {
const parsedStackFrameStart = state.internalModuleCurrentStackFrame;
state.internalModuleCurrentStackFrame = null;
const range = [parsedStackFrameStart, parsedStackFrameStop];
const ranges = currentProfilerData.internalModuleSourceToRanges.get(
parsedStackFrameStart.fileName,
);
if (ranges == null) {
currentProfilerData.internalModuleSourceToRanges.set(
parsedStackFrameStart.fileName,
[range],
);
} else {
ranges.push(range);
}
}
}
} else if (ph === 'R' || ph === 'n') {
currentProfilerData.otherUserTimingMarks.push({
name,
timestamp: startTime,
});
} else if (ph === 'b') {
} else if (ph === 'e') {
} else if (ph === 'i' || ph === 'I') {
} else {
throw new InvalidProfileError(
`Unrecognized event ${JSON.stringify(
event,
)}! This is likely a bug in this profiler tool.`,
);
}
break;
}
}
function assertNoOverlappingComponentMeasure(state: ProcessorState) {
if (state.currentReactComponentMeasure !== null) {
console.error(
'Component measure started while another measure in progress:',
state.currentReactComponentMeasure,
);
}
}
function assertCurrentComponentMeasureType(
state: ProcessorState,
type: ReactComponentMeasureType,
): void {
if (state.currentReactComponentMeasure === null) {
console.error(
`Component measure type "${type}" stopped while no measure was in progress`,
);
} else if (state.currentReactComponentMeasure.type !== type) {
console.error(
`Component measure type "${type}" stopped while type ${state.currentReactComponentMeasure.type} in progress`,
);
}
}
function processReactComponentMeasure(
name: string,
startTime: Milliseconds,
currentProfilerData: TimelineData,
state: ProcessorState,
): void {
if (name.startsWith('--component-render-start-')) {
const [componentName] = name.slice(25).split('-');
assertNoOverlappingComponentMeasure(state);
state.currentReactComponentMeasure = {
componentName,
timestamp: startTime,
duration: 0,
type: 'render',
warning: null,
};
} else if (name === '--component-render-stop') {
assertCurrentComponentMeasureType(state, 'render');
if (state.currentReactComponentMeasure !== null) {
const componentMeasure = state.currentReactComponentMeasure;
componentMeasure.duration = startTime - componentMeasure.timestamp;
state.currentReactComponentMeasure = null;
currentProfilerData.componentMeasures.push(componentMeasure);
}
} else if (name.startsWith('--component-layout-effect-mount-start-')) {
const [componentName] = name.slice(38).split('-');
assertNoOverlappingComponentMeasure(state);
state.currentReactComponentMeasure = {
componentName,
timestamp: startTime,
duration: 0,
type: 'layout-effect-mount',
warning: null,
};
} else if (name === '--component-layout-effect-mount-stop') {
assertCurrentComponentMeasureType(state, 'layout-effect-mount');
if (state.currentReactComponentMeasure !== null) {
const componentMeasure = state.currentReactComponentMeasure;
componentMeasure.duration = startTime - componentMeasure.timestamp;
state.currentReactComponentMeasure = null;
currentProfilerData.componentMeasures.push(componentMeasure);
}
} else if (name.startsWith('--component-layout-effect-unmount-start-')) {
const [componentName] = name.slice(40).split('-');
assertNoOverlappingComponentMeasure(state);
state.currentReactComponentMeasure = {
componentName,
timestamp: startTime,
duration: 0,
type: 'layout-effect-unmount',
warning: null,
};
} else if (name === '--component-layout-effect-unmount-stop') {
assertCurrentComponentMeasureType(state, 'layout-effect-unmount');
if (state.currentReactComponentMeasure !== null) {
const componentMeasure = state.currentReactComponentMeasure;
componentMeasure.duration = startTime - componentMeasure.timestamp;
state.currentReactComponentMeasure = null;
currentProfilerData.componentMeasures.push(componentMeasure);
}
} else if (name.startsWith('--component-passive-effect-mount-start-')) {
const [componentName] = name.slice(39).split('-');
assertNoOverlappingComponentMeasure(state);
state.currentReactComponentMeasure = {
componentName,
timestamp: startTime,
duration: 0,
type: 'passive-effect-mount',
warning: null,
};
} else if (name === '--component-passive-effect-mount-stop') {
assertCurrentComponentMeasureType(state, 'passive-effect-mount');
if (state.currentReactComponentMeasure !== null) {
const componentMeasure = state.currentReactComponentMeasure;
componentMeasure.duration = startTime - componentMeasure.timestamp;
state.currentReactComponentMeasure = null;
currentProfilerData.componentMeasures.push(componentMeasure);
}
} else if (name.startsWith('--component-passive-effect-unmount-start-')) {
const [componentName] = name.slice(41).split('-');
assertNoOverlappingComponentMeasure(state);
state.currentReactComponentMeasure = {
componentName,
timestamp: startTime,
duration: 0,
type: 'passive-effect-unmount',
warning: null,
};
} else if (name === '--component-passive-effect-unmount-stop') {
assertCurrentComponentMeasureType(state, 'passive-effect-unmount');
if (state.currentReactComponentMeasure !== null) {
const componentMeasure = state.currentReactComponentMeasure;
componentMeasure.duration = startTime - componentMeasure.timestamp;
state.currentReactComponentMeasure = null;
currentProfilerData.componentMeasures.push(componentMeasure);
}
}
}
function preprocessFlamechart(rawData: TimelineEvent[]): Flamechart {
let parsedData;
try {
parsedData = importFromChromeTimeline(rawData, 'react-devtools');
} catch (error) {
const errorToRethrow = new InvalidProfileError(error.message);
errorToRethrow.stack = error.stack;
throw errorToRethrow;
}
const profile = parsedData.profiles[0];
const speedscopeFlamechart = new SpeedscopeFlamechart({
getTotalWeight: profile.getTotalWeight.bind(profile),
forEachCall: profile.forEachCall.bind(profile),
formatValue: profile.formatValue.bind(profile),
getColorBucketForFrame: () => 0,
});
const flamechart: Flamechart = speedscopeFlamechart.getLayers().map(layer =>
layer.map(
({
start,
end,
node: {
frame: {name, file, line, col},
},
}) => ({
name,
timestamp: start / 1000,
duration: (end - start) / 1000,
scriptUrl: file,
locationLine: line,
locationColumn: col,
}),
),
);
return flamechart;
}
function parseStackFrame(stackFrame: string): ErrorStackFrame | null {
const error = new Error();
error.stack = stackFrame;
const frames = ErrorStackParser.parse(error);
return frames.length === 1 ? frames[0] : null;
}
export default async function preprocessData(
timeline: TimelineEvent[],
): Promise<TimelineData> {
const flamechart = preprocessFlamechart(timeline);
const laneToReactMeasureMap: Map<ReactLane, Array<ReactMeasure>> = new Map();
for (let lane: ReactLane = 0; lane < REACT_TOTAL_NUM_LANES; lane++) {
laneToReactMeasureMap.set(lane, []);
}
const profilerData: TimelineData = {
batchUIDToMeasuresMap: new Map(),
componentMeasures: [],
duration: 0,
flamechart,
internalModuleSourceToRanges: new Map(),
laneToLabelMap: new Map(),
laneToReactMeasureMap,
nativeEvents: [],
networkMeasures: [],
otherUserTimingMarks: [],
reactVersion: null,
schedulingEvents: [],
snapshots: [],
snapshotHeight: 0,
startTime: 0,
suspenseEvents: [],
thrownErrors: [],
};
timeline = timeline.filter(Boolean).sort((a, b) => (a.ts > b.ts ? 1 : -1));
const indexOfProfileEvent = timeline.findIndex(
event => event.name === 'Profile',
);
if (indexOfProfileEvent === -1) {
return profilerData;
}
profilerData.startTime = timeline[indexOfProfileEvent].args.data.startTime;
profilerData.duration =
(timeline[timeline.length - 1].ts - profilerData.startTime) / 1000;
const state: ProcessorState = {
asyncProcessingPromises: [],
batchUID: 0,
currentReactComponentMeasure: null,
internalModuleCurrentStackFrame: null,
internalModuleStackStringSet: new Set(),
measureStack: [],
nativeEventStack: [],
nextRenderShouldGenerateNewBatchID: true,
potentialLongEvents: [],
potentialLongNestedUpdate: null,
potentialLongNestedUpdates: [],
potentialSuspenseEventsOutsideOfTransition: [],
requestIdToNetworkMeasureMap: new Map(),
uidCounter: 0,
unresolvedSuspenseEvents: new Map(),
};
timeline.forEach(event => processTimelineEvent(event, profilerData, state));
if (profilerVersion === null) {
if (
profilerData.schedulingEvents.length === 0 &&
profilerData.batchUIDToMeasuresMap.size === 0
) {
throw new InvalidProfileError(
'No React marks were found in the provided profile.' +
' Please provide profiling data from an React application running in development or profiling mode.',
);
}
throw new InvalidProfileError(
`This version of profiling data is not supported by the current profiler.`,
);
}
const {measureStack} = state;
if (measureStack.length > 0) {
console.error('Incomplete events or measures', measureStack);
}
state.potentialLongEvents.forEach(([nativeEvent, batchUID]) => {
const [startTime, stopTime] = getBatchRange(
batchUID,
profilerData,
nativeEvent.timestamp,
);
if (stopTime - startTime > NATIVE_EVENT_DURATION_THRESHOLD) {
nativeEvent.warning = WARNING_STRINGS.LONG_EVENT_HANDLER;
}
});
state.potentialLongNestedUpdates.forEach(([schedulingEvent, batchUID]) => {
const [startTime, stopTime] = getBatchRange(batchUID, profilerData);
if (stopTime - startTime > NESTED_UPDATE_DURATION_THRESHOLD) {
if (
!schedulingEvent.lanes.some(
lane => profilerData.laneToLabelMap.get(lane) === 'Transition',
)
) {
}
}
});
state.potentialSuspenseEventsOutsideOfTransition.forEach(
([suspenseEvent, lanes]) => {
if (
!lanes.some(
lane => profilerData.laneToLabelMap.get(lane) === 'Transition',
)
) {
suspenseEvent.warning = WARNING_STRINGS.SUSPEND_DURING_UPDATE;
}
},
);
await Promise.all(state.asyncProcessingPromises);
if (profilerData.snapshots.length > 0) {
const snapshot = profilerData.snapshots[0];
profilerData.snapshotHeight = Math.min(
snapshot.height,
SNAPSHOT_MAX_HEIGHT,
);
}
return profilerData;
}