import type {
ReactComponentInfo,
ReactIOInfo,
ReactAsyncInfo,
} from 'shared/ReactTypes';
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
import {
addValueToProperties,
addObjectToProperties,
} from 'shared/ReactPerformanceTrackProperties';
const supportsUserTiming =
enableProfilerTimer &&
typeof console !== 'undefined' &&
typeof console.timeStamp === 'function' &&
typeof performance !== 'undefined' &&
typeof performance.measure === 'function';
const IO_TRACK = 'Server Requests ⚛';
const COMPONENTS_TRACK = 'Server Components ⚛';
export function markAllTracksInOrder() {
if (supportsUserTiming) {
console.timeStamp(
'Server Requests Track',
0.001,
0.001,
IO_TRACK,
undefined,
'primary-light',
);
console.timeStamp(
'Server Components Track',
0.001,
0.001,
'Primary',
COMPONENTS_TRACK,
'primary-light',
);
}
}
const trackNames = [
'Primary',
'Parallel',
'Parallel\u200b',
'Parallel\u200b\u200b',
'Parallel\u200b\u200b\u200b',
'Parallel\u200b\u200b\u200b\u200b',
'Parallel\u200b\u200b\u200b\u200b\u200b',
'Parallel\u200b\u200b\u200b\u200b\u200b\u200b',
'Parallel\u200b\u200b\u200b\u200b\u200b\u200b\u200b',
'Parallel\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b',
];
export function logComponentRender(
componentInfo: ReactComponentInfo,
trackIdx: number,
startTime: number,
endTime: number,
childrenEndTime: number,
rootEnv: string,
): void {
if (supportsUserTiming && childrenEndTime >= 0 && trackIdx < 10) {
const env = componentInfo.env;
const name = componentInfo.name;
const isPrimaryEnv = env === rootEnv;
const selfTime = endTime - startTime;
const color =
selfTime < 0.5
? isPrimaryEnv
? 'primary-light'
: 'secondary-light'
: selfTime < 50
? isPrimaryEnv
? 'primary'
: 'secondary'
: selfTime < 500
? isPrimaryEnv
? 'primary-dark'
: 'secondary-dark'
: 'error';
const entryName =
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
const debugTask = componentInfo.debugTask;
if (__DEV__ && debugTask) {
const properties: Array<[string, string]> = [];
if (componentInfo.key != null) {
addValueToProperties('key', componentInfo.key, properties, 0, '');
}
if (componentInfo.props != null) {
addObjectToProperties(componentInfo.props, properties, 0, '');
}
debugTask.run(
performance.measure.bind(performance, entryName, {
start: startTime < 0 ? 0 : startTime,
end: childrenEndTime,
detail: {
devtools: {
color: color,
track: trackNames[trackIdx],
trackGroup: COMPONENTS_TRACK,
properties,
},
},
}),
);
} else {
console.timeStamp(
entryName,
startTime < 0 ? 0 : startTime,
childrenEndTime,
trackNames[trackIdx],
COMPONENTS_TRACK,
color,
);
}
}
}
export function logComponentAborted(
componentInfo: ReactComponentInfo,
trackIdx: number,
startTime: number,
endTime: number,
childrenEndTime: number,
rootEnv: string,
): void {
if (supportsUserTiming) {
const env = componentInfo.env;
const name = componentInfo.name;
const isPrimaryEnv = env === rootEnv;
const entryName =
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
if (__DEV__) {
const properties = [
[
'Aborted',
'The stream was aborted before this Component finished rendering.',
],
];
if (componentInfo.key != null) {
addValueToProperties('key', componentInfo.key, properties, 0, '');
}
if (componentInfo.props != null) {
addObjectToProperties(componentInfo.props, properties, 0, '');
}
performance.measure(entryName, {
start: startTime < 0 ? 0 : startTime,
end: childrenEndTime,
detail: {
devtools: {
color: 'warning',
track: trackNames[trackIdx],
trackGroup: COMPONENTS_TRACK,
tooltipText: entryName + ' Aborted',
properties,
},
},
});
} else {
console.timeStamp(
entryName,
startTime < 0 ? 0 : startTime,
childrenEndTime,
trackNames[trackIdx],
COMPONENTS_TRACK,
'warning',
);
}
}
}
export function logComponentErrored(
componentInfo: ReactComponentInfo,
trackIdx: number,
startTime: number,
endTime: number,
childrenEndTime: number,
rootEnv: string,
error: mixed,
): void {
if (supportsUserTiming) {
const env = componentInfo.env;
const name = componentInfo.name;
const isPrimaryEnv = env === rootEnv;
const entryName =
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
if (__DEV__) {
const message =
typeof error === 'object' &&
error !== null &&
typeof error.message === 'string'
?
String(error.message)
:
String(error);
const properties = [['Error', message]];
if (componentInfo.key != null) {
addValueToProperties('key', componentInfo.key, properties, 0, '');
}
if (componentInfo.props != null) {
addObjectToProperties(componentInfo.props, properties, 0, '');
}
performance.measure(entryName, {
start: startTime < 0 ? 0 : startTime,
end: childrenEndTime,
detail: {
devtools: {
color: 'error',
track: trackNames[trackIdx],
trackGroup: COMPONENTS_TRACK,
tooltipText: entryName + ' Errored',
properties,
},
},
});
} else {
console.timeStamp(
entryName,
startTime < 0 ? 0 : startTime,
childrenEndTime,
trackNames[trackIdx],
COMPONENTS_TRACK,
'error',
);
}
}
}
export function logDedupedComponentRender(
componentInfo: ReactComponentInfo,
trackIdx: number,
startTime: number,
endTime: number,
rootEnv: string,
): void {
if (supportsUserTiming && endTime >= 0 && trackIdx < 10) {
const env = componentInfo.env;
const name = componentInfo.name;
const isPrimaryEnv = env === rootEnv;
const color = isPrimaryEnv ? 'primary-light' : 'secondary-light';
const entryName = name + ' [deduped]';
const debugTask = componentInfo.debugTask;
if (__DEV__ && debugTask) {
debugTask.run(
console.timeStamp.bind(
console,
entryName,
startTime < 0 ? 0 : startTime,
endTime,
trackNames[trackIdx],
COMPONENTS_TRACK,
color,
),
);
} else {
console.timeStamp(
entryName,
startTime < 0 ? 0 : startTime,
endTime,
trackNames[trackIdx],
COMPONENTS_TRACK,
color,
);
}
}
}
function getIOColor(
functionName: string,
): 'tertiary-light' | 'tertiary' | 'tertiary-dark' {
switch (functionName.charCodeAt(0) % 3) {
case 0:
return 'tertiary-light';
case 1:
return 'tertiary';
default:
return 'tertiary-dark';
}
}
function getIODescription(value: any): string {
if (!__DEV__) {
return '';
}
try {
switch (typeof value) {
case 'object':
if (value === null) {
return '';
} else if (value instanceof Error) {
return String(value.message);
} else if (typeof value.url === 'string') {
return value.url;
} else if (typeof value.command === 'string') {
return value.command;
} else if (
typeof value.request === 'object' &&
typeof value.request.url === 'string'
) {
return value.request.url;
} else if (
typeof value.response === 'object' &&
typeof value.response.url === 'string'
) {
return value.response.url;
} else if (
typeof value.id === 'string' ||
typeof value.id === 'number' ||
typeof value.id === 'bigint'
) {
return String(value.id);
} else if (typeof value.name === 'string') {
return value.name;
} else {
const str = value.toString();
if (str.startWith('[object ') || str.length < 5 || str.length > 500) {
return '';
}
return str;
}
case 'string':
if (value.length < 5 || value.length > 500) {
return '';
}
return value;
case 'number':
case 'bigint':
return String(value);
default:
return '';
}
} catch (x) {
return '';
}
}
function getIOLongName(
ioInfo: ReactIOInfo,
description: string,
env: void | string,
rootEnv: string,
): string {
const name = ioInfo.name;
const longName = description === '' ? name : name + ' (' + description + ')';
const isPrimaryEnv = env === rootEnv;
return isPrimaryEnv || env === undefined
? longName
: longName + ' [' + env + ']';
}
function getIOShortName(
ioInfo: ReactIOInfo,
description: string,
env: void | string,
rootEnv: string,
): string {
const name = ioInfo.name;
const isPrimaryEnv = env === rootEnv;
const envSuffix = isPrimaryEnv || env === undefined ? '' : ' [' + env + ']';
let desc = '';
const descMaxLength = 30 - name.length - envSuffix.length;
if (descMaxLength > 1) {
const l = description.length;
if (l > 0 && l <= descMaxLength) {
desc = ' (' + description + ')';
} else if (
description.startsWith('http://') ||
description.startsWith('https://') ||
description.startsWith('/')
) {
let queryIdx = description.indexOf('?');
if (queryIdx === -1) {
queryIdx = description.length;
}
if (description.charCodeAt(queryIdx - 1) === 47 ) {
queryIdx--;
}
const slashIdx = description.lastIndexOf('/', queryIdx - 1);
if (queryIdx - slashIdx < descMaxLength) {
desc = ' (' + description.slice(slashIdx + 1, queryIdx) + ')';
}
}
}
return name + desc + envSuffix;
}
export function logComponentAwaitAborted(
asyncInfo: ReactAsyncInfo,
trackIdx: number,
startTime: number,
endTime: number,
rootEnv: string,
): void {
if (supportsUserTiming && endTime > 0) {
const entryName =
'await ' + getIOShortName(asyncInfo.awaited, '', asyncInfo.env, rootEnv);
const debugTask = asyncInfo.debugTask || asyncInfo.awaited.debugTask;
if (__DEV__ && debugTask) {
const properties = [
['Aborted', 'The stream was aborted before this Promise resolved.'],
];
const tooltipText =
getIOLongName(asyncInfo.awaited, '', asyncInfo.env, rootEnv) +
' Aborted';
debugTask.run(
performance.measure.bind(performance, entryName, {
start: startTime < 0 ? 0 : startTime,
end: endTime,
detail: {
devtools: {
color: 'warning',
track: trackNames[trackIdx],
trackGroup: COMPONENTS_TRACK,
properties,
tooltipText,
},
},
}),
);
} else {
console.timeStamp(
entryName,
startTime < 0 ? 0 : startTime,
endTime,
trackNames[trackIdx],
COMPONENTS_TRACK,
'warning',
);
}
}
}
export function logComponentAwaitErrored(
asyncInfo: ReactAsyncInfo,
trackIdx: number,
startTime: number,
endTime: number,
rootEnv: string,
error: mixed,
): void {
if (supportsUserTiming && endTime > 0) {
const description = getIODescription(error);
const entryName =
'await ' +
getIOShortName(asyncInfo.awaited, description, asyncInfo.env, rootEnv);
const debugTask = asyncInfo.debugTask || asyncInfo.awaited.debugTask;
if (__DEV__ && debugTask) {
const message =
typeof error === 'object' &&
error !== null &&
typeof error.message === 'string'
?
String(error.message)
:
String(error);
const properties = [['Rejected', message]];
const tooltipText =
getIOLongName(asyncInfo.awaited, description, asyncInfo.env, rootEnv) +
' Rejected';
debugTask.run(
performance.measure.bind(performance, entryName, {
start: startTime < 0 ? 0 : startTime,
end: endTime,
detail: {
devtools: {
color: 'error',
track: trackNames[trackIdx],
trackGroup: COMPONENTS_TRACK,
properties,
tooltipText,
},
},
}),
);
} else {
console.timeStamp(
entryName,
startTime < 0 ? 0 : startTime,
endTime,
trackNames[trackIdx],
COMPONENTS_TRACK,
'error',
);
}
}
}
export function logComponentAwait(
asyncInfo: ReactAsyncInfo,
trackIdx: number,
startTime: number,
endTime: number,
rootEnv: string,
value: mixed,
): void {
if (supportsUserTiming && endTime > 0) {
const description = getIODescription(value);
const name = getIOShortName(
asyncInfo.awaited,
description,
asyncInfo.env,
rootEnv,
);
const entryName = 'await ' + name;
const color = getIOColor(name);
const debugTask = asyncInfo.debugTask || asyncInfo.awaited.debugTask;
if (__DEV__ && debugTask) {
const properties: Array<[string, string]> = [];
if (typeof value === 'object' && value !== null) {
addObjectToProperties(value, properties, 0, '');
} else if (value !== undefined) {
addValueToProperties('Resolved', value, properties, 0, '');
}
const tooltipText = getIOLongName(
asyncInfo.awaited,
description,
asyncInfo.env,
rootEnv,
);
debugTask.run(
performance.measure.bind(performance, entryName, {
start: startTime < 0 ? 0 : startTime,
end: endTime,
detail: {
devtools: {
color: color,
track: trackNames[trackIdx],
trackGroup: COMPONENTS_TRACK,
properties,
tooltipText,
},
},
}),
);
} else {
console.timeStamp(
entryName,
startTime < 0 ? 0 : startTime,
endTime,
trackNames[trackIdx],
COMPONENTS_TRACK,
color,
);
}
}
}
export function logIOInfoErrored(
ioInfo: ReactIOInfo,
rootEnv: string,
error: mixed,
): void {
const startTime = ioInfo.start;
const endTime = ioInfo.end;
if (supportsUserTiming && endTime >= 0) {
const description = getIODescription(error);
const entryName = getIOShortName(ioInfo, description, ioInfo.env, rootEnv);
const debugTask = ioInfo.debugTask;
if (__DEV__ && debugTask) {
const message =
typeof error === 'object' &&
error !== null &&
typeof error.message === 'string'
?
String(error.message)
:
String(error);
const properties = [['Rejected', message]];
const tooltipText =
getIOLongName(ioInfo, description, ioInfo.env, rootEnv) + ' Rejected';
debugTask.run(
performance.measure.bind(performance, entryName, {
start: startTime < 0 ? 0 : startTime,
end: endTime,
detail: {
devtools: {
color: 'error',
track: IO_TRACK,
properties,
tooltipText,
},
},
}),
);
} else {
console.timeStamp(
entryName,
startTime < 0 ? 0 : startTime,
endTime,
IO_TRACK,
undefined,
'error',
);
}
}
}
export function logIOInfo(
ioInfo: ReactIOInfo,
rootEnv: string,
value: mixed,
): void {
const startTime = ioInfo.start;
const endTime = ioInfo.end;
if (supportsUserTiming && endTime >= 0) {
const description = getIODescription(value);
const entryName = getIOShortName(ioInfo, description, ioInfo.env, rootEnv);
const color = getIOColor(entryName);
const debugTask = ioInfo.debugTask;
if (__DEV__ && debugTask) {
const properties: Array<[string, string]> = [];
if (typeof value === 'object' && value !== null) {
addObjectToProperties(value, properties, 0, '');
} else if (value !== undefined) {
addValueToProperties('Resolved', value, properties, 0, '');
}
const tooltipText = getIOLongName(
ioInfo,
description,
ioInfo.env,
rootEnv,
);
debugTask.run(
performance.measure.bind(performance, entryName, {
start: startTime < 0 ? 0 : startTime,
end: endTime,
detail: {
devtools: {
color: color,
track: IO_TRACK,
properties,
tooltipText,
},
},
}),
);
} else {
console.timeStamp(
entryName,
startTime < 0 ? 0 : startTime,
endTime,
IO_TRACK,
undefined,
color,
);
}
}
}