DevTools: Timeline profiler refactor

Refactor DevTools to record Timeline data (in memory) while profiling. Updated the Profiler UI to import/export Timeline data along with legacy profiler data.

Relates to issue #22529
This commit is contained in:
Brian Vaughn
2022-01-28 13:09:28 -05:00
committed by GitHub
parent 07cba51de9
commit b41b7f2e0b
49 changed files with 9624 additions and 3405 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,25 @@
import hasOwnProperty from 'shared/hasOwnProperty';
const FILTERED_VERSION_STRING = '<filtered-version>';
// test() is part of Jest's serializer API
export function test(maybeProfile) {
if (
maybeProfile != null &&
typeof maybeProfile === 'object' &&
hasOwnProperty.call(maybeProfile, 'reactVersion') &&
maybeProfile.reactVersion !== FILTERED_VERSION_STRING
) {
return true;
}
return false;
}
// print() is part of Jest's serializer API
export function print(profile, serialize, indent) {
return serialize({
...profile,
reactVersion: FILTERED_VERSION_STRING,
});
}
@@ -0,0 +1,29 @@
import hasOwnProperty from 'shared/hasOwnProperty';
import isArray from 'shared/isArray';
function formatLanes(laneArray) {
const lanes = laneArray.reduce((current, reduced) => current + reduced, 0);
return '0b' + lanes.toString(2).padStart(31, '0');
}
// test() is part of Jest's serializer API
export function test(maybeTimelineData) {
if (
maybeTimelineData != null &&
typeof maybeTimelineData === 'object' &&
hasOwnProperty.call(maybeTimelineData, 'lanes') &&
isArray(maybeTimelineData.lanes)
) {
return true;
}
return false;
}
// print() is part of Jest's serializer API
export function print(timelineData, serialize, indent) {
return serialize({
...timelineData,
lanes: formatLanes(timelineData.lanes),
});
}
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -915,17 +915,17 @@ describe('Store', () => {
const containerA = document.createElement('div');
const containerB = document.createElement('div');
expect(store.supportsProfiling).toBe(false);
expect(store.rootSupportsBasicProfiling).toBe(false);
act(() => legacyRender(<Component />, containerA));
expect(store.supportsProfiling).toBe(true);
expect(store.rootSupportsBasicProfiling).toBe(true);
act(() => legacyRender(<Component />, containerB));
act(() => ReactDOM.unmountComponentAtNode(containerA));
expect(store.supportsProfiling).toBe(true);
expect(store.rootSupportsBasicProfiling).toBe(true);
act(() => ReactDOM.unmountComponentAtNode(containerB));
expect(store.supportsProfiling).toBe(false);
expect(store.rootSupportsBasicProfiling).toBe(false);
});
it('should properly serialize non-string key values', () => {
+3
View File
@@ -227,6 +227,9 @@ export function exportImportHelper(bridge: FrontendBridge, store: Store): void {
expect(profilingDataFrontendInitial.dataForRoots).toEqual(
profilingDataFrontend.dataForRoots,
);
expect(profilingDataFrontendInitial.timelineData).toEqual(
profilingDataFrontend.timelineData,
);
// Snapshot the JSON-parsed object, rather than the raw string, because Jest formats the diff nicer.
expect(parsedProfilingDataExport).toMatchSnapshot('imported data');
@@ -387,7 +387,7 @@ export function attach(
pushOperation(id);
pushOperation(ElementTypeRoot);
pushOperation(0); // StrictMode compliant?
pushOperation(0); // Profiling supported?
pushOperation(0); // Profiling flag
pushOperation(0); // StrictMode supported?
pushOperation(hasOwnerMetadata ? 1 : 0);
} else {
+637 -108
View File
@@ -14,9 +14,25 @@ import type {
} from 'react-devtools-shared/src/backend/types';
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import type {Wakeable} from 'shared/ReactTypes';
import type {
BatchUID,
LaneToLabelMap,
ReactComponentMeasure,
ReactMeasure,
ReactMeasureType,
TimelineData,
SuspenseEvent,
} from 'react-devtools-timeline/src/types';
import isArray from 'shared/isArray';
import {SCHEDULING_PROFILER_VERSION} from 'react-devtools-timeline/src/constants';
import {
REACT_TOTAL_NUM_LANES,
SCHEDULING_PROFILER_VERSION,
} from 'react-devtools-timeline/src/constants';
// Add padding to the start/stop time of the profile.
// This makes the UI nicer to use.
const TIME_OFFSET = 10;
let performanceTarget: Performance | null = null;
@@ -53,6 +69,12 @@ if (supportsUserTimingV3) {
performanceTarget = performance;
}
// Some environments (e.g. React Native / Hermes) don't support the performance API yet.
const getCurrentTime =
typeof performance === 'object' && typeof performance.now === 'function'
? () => performance.now()
: () => Date.now();
// Mocking the Performance Object (and User Timing APIs) for testing is fragile.
// This API allows tests to directly override the User Timing APIs.
export function setPerformanceMock_ONLY_FOR_TESTING(
@@ -63,25 +85,48 @@ export function setPerformanceMock_ONLY_FOR_TESTING(
supportsUserTimingV3 = performanceMock !== null;
}
function markAndClear(markName) {
// This method won't be called unless these functions are defined, so we can skip the extra typeof check.
((performanceTarget: any): Performance).mark(markName);
((performanceTarget: any): Performance).clearMarks(markName);
}
export type GetTimelineData = () => TimelineData | null;
export type ToggleProfilingStatus = (value: boolean) => void;
type Response = {|
getTimelineData: GetTimelineData,
profilingHooks: DevToolsProfilingHooks,
toggleProfilingStatus: ToggleProfilingStatus,
|};
export function createProfilingHooks({
getDisplayNameForFiber,
getIsProfiling,
getLaneLabelMap,
reactVersion,
}: {|
getDisplayNameForFiber: (fiber: Fiber) => string | null,
getIsProfiling: () => boolean,
getLaneLabelMap?: () => Map<Lane, string> | null,
reactVersion: string,
|}): DevToolsProfilingHooks {
function markMetadata() {
markAndClear(`--react-version-${reactVersion}`);
markAndClear(`--profiler-version-${SCHEDULING_PROFILER_VERSION}`);
|}): Response {
let currentBatchUID: BatchUID = 0;
let currentReactComponentMeasure: ReactComponentMeasure | null = null;
let currentReactMeasuresStack: Array<ReactMeasure> = [];
let currentTimelineData: TimelineData | null = null;
let isProfiling: boolean = false;
let nextRenderShouldStartNewBatch: boolean = false;
function getRelativeTime() {
const currentTime = getCurrentTime();
if (currentTimelineData) {
if (currentTimelineData.startTime === 0) {
currentTimelineData.startTime = currentTime - TIME_OFFSET;
}
return currentTime - currentTimelineData.startTime;
}
return 0;
}
function getInternalModuleRanges() {
/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
if (
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&
@@ -89,131 +134,405 @@ export function createProfilingHooks({
'function'
) {
// Ask the DevTools hook for module ranges that may have been reported by the current renderer(s).
// Don't do this eagerly like the laneToLabelMap,
// because some modules might not yet have registered their boundaries when the renderer is injected.
const ranges = __REACT_DEVTOOLS_GLOBAL_HOOK__.getInternalModuleRanges();
// This check would not be required,
// except that it's possible for things to override __REACT_DEVTOOLS_GLOBAL_HOOK__.
if (isArray(ranges)) {
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
if (isArray(range) && range.length === 2) {
const [startStackFrame, stopStackFrame] = ranges[i];
return ranges;
}
}
markAndClear(`--react-internal-module-start-${startStackFrame}`);
markAndClear(`--react-internal-module-stop-${stopStackFrame}`);
}
return null;
}
function getTimelineData(): TimelineData | null {
return currentTimelineData;
}
function laneToLanesArray(lanes: Lane) {
const lanesArray = [];
let lane = 1;
for (let index = 0; index < REACT_TOTAL_NUM_LANES; index++) {
if (lane & lanes) {
lanesArray.push(lane);
}
lane *= 2;
}
return lanesArray;
}
const laneToLabelMap: LaneToLabelMap | null =
typeof getLaneLabelMap === 'function' ? getLaneLabelMap() : null;
function markMetadata() {
markAndClear(`--react-version-${reactVersion}`);
markAndClear(`--profiler-version-${SCHEDULING_PROFILER_VERSION}`);
const ranges = getInternalModuleRanges();
if (ranges) {
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
if (isArray(range) && range.length === 2) {
const [startStackFrame, stopStackFrame] = ranges[i];
markAndClear(`--react-internal-module-start-${startStackFrame}`);
markAndClear(`--react-internal-module-stop-${stopStackFrame}`);
}
}
}
if (typeof getLaneLabelMap === 'function') {
const map = getLaneLabelMap();
if (map != null) {
const labels = Array.from(map.values()).join(',');
markAndClear(`--react-lane-labels-${labels}`);
if (laneToLabelMap != null) {
const labels = Array.from(laneToLabelMap.values()).join(',');
markAndClear(`--react-lane-labels-${labels}`);
}
}
function markAndClear(markName) {
// This method won't be called unless these functions are defined, so we can skip the extra typeof check.
((performanceTarget: any): Performance).mark(markName);
((performanceTarget: any): Performance).clearMarks(markName);
}
function recordReactMeasureStarted(
type: ReactMeasureType,
lanes: Lanes,
): void {
// Decide what depth thi work should be rendered at, based on what's on the top of the stack.
// It's okay to render over top of "idle" work but everything else should be on its own row.
let depth = 0;
if (currentReactMeasuresStack.length > 0) {
const top =
currentReactMeasuresStack[currentReactMeasuresStack.length - 1];
depth = top.type === 'render-idle' ? top.depth : top.depth + 1;
}
const lanesArray = laneToLanesArray(lanes);
const reactMeasure: ReactMeasure = {
type,
batchUID: currentBatchUID,
depth,
lanes: lanesArray,
timestamp: getRelativeTime(),
duration: 0,
};
currentReactMeasuresStack.push(reactMeasure);
if (currentTimelineData) {
const {
batchUIDToMeasuresMap,
laneToReactMeasureMap,
} = currentTimelineData;
let reactMeasures = batchUIDToMeasuresMap.get(currentBatchUID);
if (reactMeasures != null) {
reactMeasures.push(reactMeasure);
} else {
batchUIDToMeasuresMap.set(currentBatchUID, [reactMeasure]);
}
lanesArray.forEach(lane => {
reactMeasures = laneToReactMeasureMap.get(lane);
if (reactMeasures) {
reactMeasures.push(reactMeasure);
}
});
}
}
function recordReactMeasureCompleted(type: ReactMeasureType): void {
const currentTime = getRelativeTime();
if (currentReactMeasuresStack.length === 0) {
console.error(
'Unexpected type "%s" completed at %sms while currentReactMeasuresStack is empty.',
type,
currentTime,
);
// Ignore work "completion" user timing mark that doesn't complete anything
return;
}
const top = currentReactMeasuresStack.pop();
if (top.type !== type) {
console.error(
'Unexpected type "%s" completed at %sms before "%s" completed.',
type,
currentTime,
top.type,
);
}
// $FlowFixMe This property should not be writable outside of this function.
top.duration = currentTime - top.timestamp;
if (currentTimelineData) {
currentTimelineData.duration = getRelativeTime() + TIME_OFFSET;
}
}
function markCommitStarted(lanes: Lanes): void {
if (isProfiling) {
recordReactMeasureStarted('commit', lanes);
// TODO (timeline) Re-think this approach to "batching"; I don't think it works for Suspense or pre-rendering.
// This issue applies to the User Timing data also.
nextRenderShouldStartNewBatch = true;
}
if (supportsUserTimingV3) {
markAndClear(`--commit-start-${lanes}`);
// Certain types of metadata should be logged infrequently.
// Normally we would log this during module init,
// but there's no guarantee a user is profiling at that time.
// Commits happen infrequently (less than renders or state updates)
// so we log this extra information along with a commit.
// It will likely be logged more than once but that's okay.
//
// TODO (timeline) Only log this once, when profiling starts.
// For the first phase– refactoring– we'll match the previous behavior.
// Some metadata only needs to be logged once per session,
// but if profiling information is being recorded via the Performance tab,
// DevTools has no way of knowing when the recording starts.
// Because of that, we log thie type of data periodically (once per commit).
markMetadata();
}
}
function markCommitStopped(): void {
if (isProfiling) {
recordReactMeasureCompleted('commit');
recordReactMeasureCompleted('render-idle');
}
if (supportsUserTimingV3) {
markAndClear('--commit-stop');
}
}
function markComponentRenderStarted(fiber: Fiber): void {
if (supportsUserTimingV3) {
if (isProfiling || supportsUserTimingV3) {
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
// TODO (timeline) Record and cache component stack
markAndClear(`--component-render-start-${componentName}`);
if (isProfiling) {
// TODO (timeline) Record and cache component stack
if (isProfiling) {
currentReactComponentMeasure = {
componentName,
duration: 0,
timestamp: getRelativeTime(),
type: 'render',
warning: null,
};
}
}
if (supportsUserTimingV3) {
markAndClear(`--component-render-start-${componentName}`);
}
}
}
function markComponentRenderStopped(): void {
if (isProfiling) {
if (currentReactComponentMeasure) {
if (currentTimelineData) {
currentTimelineData.componentMeasures.push(
currentReactComponentMeasure,
);
}
currentReactComponentMeasure.duration =
getRelativeTime() - currentReactComponentMeasure.timestamp;
currentReactComponentMeasure = null;
}
}
if (supportsUserTimingV3) {
markAndClear('--component-render-stop');
}
}
function markComponentPassiveEffectMountStarted(fiber: Fiber): void {
if (supportsUserTimingV3) {
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
// TODO (timeline) Record and cache component stack
markAndClear(`--component-passive-effect-mount-start-${componentName}`);
}
}
function markComponentPassiveEffectMountStopped(): void {
if (supportsUserTimingV3) {
markAndClear('--component-passive-effect-mount-stop');
}
}
function markComponentPassiveEffectUnmountStarted(fiber: Fiber): void {
if (supportsUserTimingV3) {
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
// TODO (timeline) Record and cache component stack
markAndClear(`--component-passive-effect-unmount-start-${componentName}`);
}
}
function markComponentPassiveEffectUnmountStopped(): void {
if (supportsUserTimingV3) {
markAndClear('--component-passive-effect-unmount-stop');
}
}
function markComponentLayoutEffectMountStarted(fiber: Fiber): void {
if (supportsUserTimingV3) {
if (isProfiling || supportsUserTimingV3) {
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
// TODO (timeline) Record and cache component stack
markAndClear(`--component-layout-effect-mount-start-${componentName}`);
if (isProfiling) {
// TODO (timeline) Record and cache component stack
if (isProfiling) {
currentReactComponentMeasure = {
componentName,
duration: 0,
timestamp: getRelativeTime(),
type: 'layout-effect-mount',
warning: null,
};
}
}
if (supportsUserTimingV3) {
markAndClear(`--component-layout-effect-mount-start-${componentName}`);
}
}
}
function markComponentLayoutEffectMountStopped(): void {
if (isProfiling) {
if (currentReactComponentMeasure) {
if (currentTimelineData) {
currentTimelineData.componentMeasures.push(
currentReactComponentMeasure,
);
}
currentReactComponentMeasure.duration =
getRelativeTime() - currentReactComponentMeasure.timestamp;
currentReactComponentMeasure = null;
}
}
if (supportsUserTimingV3) {
markAndClear('--component-layout-effect-mount-stop');
}
}
function markComponentLayoutEffectUnmountStarted(fiber: Fiber): void {
if (supportsUserTimingV3) {
if (isProfiling || supportsUserTimingV3) {
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
// TODO (timeline) Record and cache component stack
markAndClear(`--component-layout-effect-unmount-start-${componentName}`);
if (isProfiling) {
// TODO (timeline) Record and cache component stack
if (isProfiling) {
currentReactComponentMeasure = {
componentName,
duration: 0,
timestamp: getRelativeTime(),
type: 'layout-effect-unmount',
warning: null,
};
}
}
if (supportsUserTimingV3) {
markAndClear(
`--component-layout-effect-unmount-start-${componentName}`,
);
}
}
}
function markComponentLayoutEffectUnmountStopped(): void {
if (isProfiling) {
if (currentReactComponentMeasure) {
if (currentTimelineData) {
currentTimelineData.componentMeasures.push(
currentReactComponentMeasure,
);
}
currentReactComponentMeasure.duration =
getRelativeTime() - currentReactComponentMeasure.timestamp;
currentReactComponentMeasure = null;
}
}
if (supportsUserTimingV3) {
markAndClear('--component-layout-effect-unmount-stop');
}
}
function markComponentPassiveEffectMountStarted(fiber: Fiber): void {
if (isProfiling || supportsUserTimingV3) {
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
if (isProfiling) {
// TODO (timeline) Record and cache component stack
if (isProfiling) {
currentReactComponentMeasure = {
componentName,
duration: 0,
timestamp: getRelativeTime(),
type: 'passive-effect-mount',
warning: null,
};
}
}
if (supportsUserTimingV3) {
markAndClear(`--component-passive-effect-mount-start-${componentName}`);
}
}
}
function markComponentPassiveEffectMountStopped(): void {
if (isProfiling) {
if (currentReactComponentMeasure) {
if (currentTimelineData) {
currentTimelineData.componentMeasures.push(
currentReactComponentMeasure,
);
}
currentReactComponentMeasure.duration =
getRelativeTime() - currentReactComponentMeasure.timestamp;
currentReactComponentMeasure = null;
}
}
if (supportsUserTimingV3) {
markAndClear('--component-passive-effect-mount-stop');
}
}
function markComponentPassiveEffectUnmountStarted(fiber: Fiber): void {
if (isProfiling || supportsUserTimingV3) {
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
if (isProfiling) {
// TODO (timeline) Record and cache component stack
if (isProfiling) {
currentReactComponentMeasure = {
componentName,
duration: 0,
timestamp: getRelativeTime(),
type: 'passive-effect-unmount',
warning: null,
};
}
}
if (supportsUserTimingV3) {
markAndClear(
`--component-passive-effect-unmount-start-${componentName}`,
);
}
}
}
function markComponentPassiveEffectUnmountStopped(): void {
if (isProfiling) {
if (currentReactComponentMeasure) {
if (currentTimelineData) {
currentTimelineData.componentMeasures.push(
currentReactComponentMeasure,
);
}
currentReactComponentMeasure.duration =
getRelativeTime() - currentReactComponentMeasure.timestamp;
currentReactComponentMeasure = null;
}
}
if (supportsUserTimingV3) {
markAndClear('--component-passive-effect-unmount-stop');
}
}
function markComponentErrored(
fiber: Fiber,
thrownValue: mixed,
lanes: Lanes,
): void {
if (supportsUserTimingV3) {
if (isProfiling || supportsUserTimingV3) {
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
const phase = fiber.alternate === null ? 'mount' : 'update';
@@ -228,8 +547,22 @@ export function createProfilingHooks({
message = thrownValue;
}
// TODO (timeline) Record and cache component stack
markAndClear(`--error-${componentName}-${phase}-${message}`);
if (isProfiling) {
// TODO (timeline) Record and cache component stack
if (currentTimelineData) {
currentTimelineData.thrownErrors.push({
componentName,
message,
phase,
timestamp: getRelativeTime(),
type: 'thrown-error',
});
}
}
if (supportsUserTimingV3) {
markAndClear(`--error-${componentName}-${phase}-${message}`);
}
}
}
@@ -250,7 +583,7 @@ export function createProfilingHooks({
wakeable: Wakeable,
lanes: Lanes,
): void {
if (supportsUserTimingV3) {
if (isProfiling || supportsUserTimingV3) {
const eventType = wakeableIDs.has(wakeable) ? 'resuspend' : 'suspend';
const id = getWakeableID(wakeable);
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
@@ -262,105 +595,301 @@ export function createProfilingHooks({
// When this is available we should pass it along to the Timeline.
const displayName = (wakeable: any).displayName || '';
// TODO (timeline) Record and cache component stack
markAndClear(
`--suspense-${eventType}-${id}-${componentName}-${phase}-${lanes}-${displayName}`,
);
let suspenseEvent: SuspenseEvent | null = null;
if (isProfiling) {
// TODO (timeline) Record and cache component stack
suspenseEvent = {
componentName,
depth: 0,
duration: 0,
id: `${id}`,
phase,
promiseName: displayName,
resolution: 'unresolved',
timestamp: getRelativeTime(),
type: 'suspense',
warning: null,
};
if (currentTimelineData) {
currentTimelineData.suspenseEvents.push(suspenseEvent);
}
}
if (supportsUserTimingV3) {
markAndClear(
`--suspense-${eventType}-${id}-${componentName}-${phase}-${lanes}-${displayName}`,
);
}
wakeable.then(
() => markAndClear(`--suspense-resolved-${id}-${componentName}`),
() => markAndClear(`--suspense-rejected-${id}-${componentName}`),
() => {
if (suspenseEvent) {
suspenseEvent.duration =
getRelativeTime() - suspenseEvent.timestamp;
suspenseEvent.resolution = 'resolved';
}
if (supportsUserTimingV3) {
markAndClear(`--suspense-resolved-${id}-${componentName}`);
}
},
() => {
if (suspenseEvent) {
suspenseEvent.duration =
getRelativeTime() - suspenseEvent.timestamp;
suspenseEvent.resolution = 'rejected';
}
if (supportsUserTimingV3) {
markAndClear(`--suspense-rejected-${id}-${componentName}`);
}
},
);
}
}
function markLayoutEffectsStarted(lanes: Lanes): void {
if (isProfiling) {
recordReactMeasureStarted('layout-effects', lanes);
}
if (supportsUserTimingV3) {
markAndClear(`--layout-effects-start-${lanes}`);
}
}
function markLayoutEffectsStopped(): void {
if (isProfiling) {
recordReactMeasureCompleted('layout-effects');
}
if (supportsUserTimingV3) {
markAndClear('--layout-effects-stop');
}
}
function markPassiveEffectsStarted(lanes: Lanes): void {
if (isProfiling) {
recordReactMeasureStarted('passive-effects', lanes);
}
if (supportsUserTimingV3) {
markAndClear(`--passive-effects-start-${lanes}`);
}
}
function markPassiveEffectsStopped(): void {
if (isProfiling) {
recordReactMeasureCompleted('passive-effects');
}
if (supportsUserTimingV3) {
markAndClear('--passive-effects-stop');
}
}
function markRenderStarted(lanes: Lanes): void {
if (isProfiling) {
if (nextRenderShouldStartNewBatch) {
nextRenderShouldStartNewBatch = false;
currentBatchUID++;
}
// If this is a new batch of work, wrap an "idle" measure around it.
// Log it before the "render" measure to preserve the stack ordering.
if (
currentReactMeasuresStack.length === 0 ||
currentReactMeasuresStack[currentReactMeasuresStack.length - 1].type !==
'render-idle'
) {
recordReactMeasureStarted('render-idle', lanes);
}
recordReactMeasureStarted('render', lanes);
}
if (supportsUserTimingV3) {
markAndClear(`--render-start-${lanes}`);
}
}
function markRenderYielded(): void {
if (isProfiling) {
recordReactMeasureCompleted('render');
}
if (supportsUserTimingV3) {
markAndClear('--render-yield');
}
}
function markRenderStopped(): void {
if (isProfiling) {
recordReactMeasureCompleted('render');
}
if (supportsUserTimingV3) {
markAndClear('--render-stop');
}
}
function markRenderScheduled(lane: Lane): void {
if (isProfiling) {
if (currentTimelineData) {
currentTimelineData.schedulingEvents.push({
lanes: laneToLanesArray(lane),
timestamp: getRelativeTime(),
type: 'schedule-render',
warning: null,
});
}
}
if (supportsUserTimingV3) {
markAndClear(`--schedule-render-${lane}`);
}
}
function markForceUpdateScheduled(fiber: Fiber, lane: Lane): void {
if (supportsUserTimingV3) {
if (isProfiling || supportsUserTimingV3) {
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
// TODO (timeline) Record and cache component stack
markAndClear(`--schedule-forced-update-${lane}-${componentName}`);
if (isProfiling) {
// TODO (timeline) Record and cache component stack
if (currentTimelineData) {
currentTimelineData.schedulingEvents.push({
componentName,
lanes: laneToLanesArray(lane),
timestamp: getRelativeTime(),
type: 'schedule-force-update',
warning: null,
});
}
}
if (supportsUserTimingV3) {
markAndClear(`--schedule-forced-update-${lane}-${componentName}`);
}
}
}
function markStateUpdateScheduled(fiber: Fiber, lane: Lane): void {
if (supportsUserTimingV3) {
if (isProfiling || supportsUserTimingV3) {
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
// TODO (timeline) Record and cache component stack
markAndClear(`--schedule-state-update-${lane}-${componentName}`);
if (isProfiling) {
// TODO (timeline) Record and cache component stack
if (currentTimelineData) {
currentTimelineData.schedulingEvents.push({
componentName,
lanes: laneToLanesArray(lane),
timestamp: getRelativeTime(),
type: 'schedule-state-update',
warning: null,
});
}
}
if (supportsUserTimingV3) {
markAndClear(`--schedule-state-update-${lane}-${componentName}`);
}
}
}
function toggleProfilingStatus(value: boolean) {
if (isProfiling !== value) {
isProfiling = value;
if (isProfiling) {
const internalModuleSourceToRanges = new Map();
if (supportsUserTimingV3) {
const ranges = getInternalModuleRanges();
if (ranges) {
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
if (isArray(range) && range.length === 2) {
const [startStackFrame, stopStackFrame] = ranges[i];
markAndClear(
`--react-internal-module-start-${startStackFrame}`,
);
markAndClear(`--react-internal-module-stop-${stopStackFrame}`);
}
}
}
}
const laneToReactMeasureMap = new Map();
let lane = 1;
for (let index = 0; index < REACT_TOTAL_NUM_LANES; index++) {
laneToReactMeasureMap.set(lane, []);
lane *= 2;
}
currentBatchUID = 0;
currentReactComponentMeasure = null;
currentReactMeasuresStack = [];
currentTimelineData = {
// Session wide metadata; only collected once.
internalModuleSourceToRanges,
laneToLabelMap: laneToLabelMap || new Map(),
reactVersion,
// Data logged by React during profiling session.
componentMeasures: [],
schedulingEvents: [],
suspenseEvents: [],
thrownErrors: [],
// Data inferred based on what React logs.
batchUIDToMeasuresMap: new Map(),
duration: 0,
laneToReactMeasureMap,
startTime: 0,
// Data only available in Chrome profiles.
flamechart: [],
nativeEvents: [],
networkMeasures: [],
otherUserTimingMarks: [],
snapshots: [],
snapshotHeight: 0,
};
nextRenderShouldStartNewBatch = true;
}
}
}
return {
markCommitStarted,
markCommitStopped,
markComponentRenderStarted,
markComponentRenderStopped,
markComponentPassiveEffectMountStarted,
markComponentPassiveEffectMountStopped,
markComponentPassiveEffectUnmountStarted,
markComponentPassiveEffectUnmountStopped,
markComponentLayoutEffectMountStarted,
markComponentLayoutEffectMountStopped,
markComponentLayoutEffectUnmountStarted,
markComponentLayoutEffectUnmountStopped,
markComponentErrored,
markComponentSuspended,
markLayoutEffectsStarted,
markLayoutEffectsStopped,
markPassiveEffectsStarted,
markPassiveEffectsStopped,
markRenderStarted,
markRenderYielded,
markRenderStopped,
markRenderScheduled,
markForceUpdateScheduled,
markStateUpdateScheduled,
getTimelineData,
profilingHooks: {
markCommitStarted,
markCommitStopped,
markComponentRenderStarted,
markComponentRenderStopped,
markComponentPassiveEffectMountStarted,
markComponentPassiveEffectMountStopped,
markComponentPassiveEffectUnmountStarted,
markComponentPassiveEffectUnmountStopped,
markComponentLayoutEffectMountStarted,
markComponentLayoutEffectMountStopped,
markComponentLayoutEffectUnmountStarted,
markComponentLayoutEffectUnmountStopped,
markComponentErrored,
markComponentSuspended,
markLayoutEffectsStarted,
markLayoutEffectsStopped,
markPassiveEffectsStarted,
markPassiveEffectsStopped,
markRenderStarted,
markRenderYielded,
markRenderStopped,
markRenderScheduled,
markForceUpdateScheduled,
markStateUpdateScheduled,
},
toggleProfilingStatus,
};
}
+71 -8
View File
@@ -47,6 +47,8 @@ import {
} from './utils';
import {
__DEBUG__,
PROFILING_FLAG_BASIC_SUPPORT,
PROFILING_FLAG_TIMELINE_SUPPORT,
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
TREE_OPERATION_ADD,
@@ -94,6 +96,7 @@ import hasOwnProperty from 'shared/hasOwnProperty';
import {getStyleXData} from './StyleX/utils';
import {createProfilingHooks} from './profilingHooks';
import type {GetTimelineData, ToggleProfilingStatus} from './profilingHooks';
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import type {
ChangeDescription,
@@ -627,14 +630,22 @@ export function attach(
};
}
let getTimelineData: null | GetTimelineData = null;
let toggleProfilingStatus: null | ToggleProfilingStatus = null;
if (typeof injectProfilingHooks === 'function') {
injectProfilingHooks(
createProfilingHooks({
getDisplayNameForFiber,
getLaneLabelMap,
reactVersion: version,
}),
);
const response = createProfilingHooks({
getDisplayNameForFiber,
getIsProfiling: () => isProfiling,
getLaneLabelMap,
reactVersion: version,
});
// Pass the Profiling hooks to the reconciler for it to call during render.
injectProfilingHooks(response.profilingHooks);
// Hang onto this toggle so we can notify the external methods of profiling status changes.
getTimelineData = response.getTimelineData;
toggleProfilingStatus = response.toggleProfilingStatus;
}
// Tracks Fibers with recently changed number of error/warning messages.
@@ -1910,12 +1921,22 @@ export function attach(
const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner');
const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
// Adding a new field here would require a bridge protocol version bump (a backwads breaking change).
// Instead let's re-purpose a pre-existing field to carry more information.
let profilingFlags = 0;
if (isProfilingSupported) {
profilingFlags = PROFILING_FLAG_BASIC_SUPPORT;
if (typeof injectProfilingHooks === 'function') {
profilingFlags |= PROFILING_FLAG_TIMELINE_SUPPORT;
}
}
if (isRoot) {
pushOperation(TREE_OPERATION_ADD);
pushOperation(id);
pushOperation(ElementTypeRoot);
pushOperation((fiber.mode & StrictModeBits) !== 0 ? 1 : 0);
pushOperation(isProfilingSupported ? 1 : 0);
pushOperation(profilingFlags);
pushOperation(StrictModeBits !== 0 ? 1 : 0);
pushOperation(hasOwnerMetadata ? 1 : 0);
@@ -3959,9 +3980,43 @@ export function attach(
},
);
let timelineData = null;
if (typeof getTimelineData === 'function') {
const currentTimelineData = getTimelineData();
if (currentTimelineData) {
const {
batchUIDToMeasuresMap,
internalModuleSourceToRanges,
laneToLabelMap,
laneToReactMeasureMap,
...rest
} = currentTimelineData;
timelineData = {
...rest,
// Most of the data is safe to parse as-is,
// but we need to convert the nested Arrays back to Maps.
// Most of the data is safe to serialize as-is,
// but we need to convert the Maps to nested Arrays.
batchUIDToMeasuresKeyValueArray: Array.from(
batchUIDToMeasuresMap.entries(),
),
internalModuleSourceToRanges: Array.from(
internalModuleSourceToRanges.entries(),
),
laneToLabelKeyValueArray: Array.from(laneToLabelMap.entries()),
laneToReactMeasureKeyValueArray: Array.from(
laneToReactMeasureMap.entries(),
),
};
}
}
return {
dataForRoots,
rendererID,
timelineData,
};
}
@@ -3999,11 +4054,19 @@ export function attach(
isProfiling = true;
profilingStartTime = getCurrentTime();
rootToCommitProfilingMetadataMap = new Map();
if (toggleProfilingStatus !== null) {
toggleProfilingStatus(true);
}
}
function stopProfiling() {
isProfiling = false;
recordChangeDescriptions = false;
if (toggleProfilingStatus !== null) {
toggleProfilingStatus(false);
}
}
// Automatically start profiling so that we don't miss timing info from initial "mount".
+2
View File
@@ -16,6 +16,7 @@ import type {
Plugins,
} from 'react-devtools-shared/src/types';
import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
import type {TimelineDataExport} from 'react-devtools-timeline/src/types';
type BundleType =
| 0 // PROD
@@ -195,6 +196,7 @@ export type ProfilingDataForRootBackend = {|
export type ProfilingDataBackend = {|
dataForRoots: Array<ProfilingDataForRootBackend>,
rendererID: number,
timelineData: TimelineDataExport | null,
|};
export type PathFrame = {|
+3
View File
@@ -25,6 +25,9 @@ export const TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS = 5;
export const TREE_OPERATION_REMOVE_ROOT = 6;
export const TREE_OPERATION_SET_SUBTREE_MODE = 7;
export const PROFILING_FLAG_BASIC_SUPPORT = 0b01;
export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10;
export const LOCAL_STORAGE_DEFAULT_TAB_KEY = 'React::DevTools::defaultTab';
export const LOCAL_STORAGE_FILTER_PREFERENCES_KEY =
+50 -12
View File
@@ -10,6 +10,8 @@
import EventEmitter from '../events';
import {inspect} from 'util';
import {
PROFILING_FLAG_BASIC_SUPPORT,
PROFILING_FLAG_TIMELINE_SUPPORT,
TREE_OPERATION_ADD,
TREE_OPERATION_REMOVE,
TREE_OPERATION_REMOVE_ROOT,
@@ -72,9 +74,10 @@ type Config = {|
|};
export type Capabilities = {|
supportsBasicProfiling: boolean,
hasOwnerMetadata: boolean,
supportsProfiling: boolean,
supportsStrictMode: boolean,
supportsTimeline: boolean,
|};
/**
@@ -88,8 +91,9 @@ export default class Store extends EventEmitter<{|
mutated: [[Array<number>, Map<number, number>]],
recordChangeDescriptions: [],
roots: [],
rootSupportsBasicProfiling: [],
rootSupportsTimelineProfiling: [],
supportsNativeStyleEditor: [],
supportsProfiling: [],
supportsReloadAndProfile: [],
unsupportedBridgeProtocolDetected: [],
unsupportedRendererVersionDetected: [],
@@ -161,13 +165,16 @@ export default class Store extends EventEmitter<{|
_rootIDToRendererID: Map<number, number> = new Map();
// These options may be initially set by a confiugraiton option when constructing the Store.
// In the case of "supportsProfiling", the option may be updated based on the injected renderers.
_supportsNativeInspection: boolean = true;
_supportsProfiling: boolean = false;
_supportsReloadAndProfile: boolean = false;
_supportsTimeline: boolean = false;
_supportsTraceUpdates: boolean = false;
// These options default to false but may be updated as roots are added and removed.
_rootSupportsBasicProfiling: boolean = false;
_rootSupportsTimelineProfiling: boolean = false;
_unsupportedBridgeProtocol: BridgeProtocol | null = null;
_unsupportedRendererVersionDetected: boolean = false;
@@ -402,6 +409,16 @@ export default class Store extends EventEmitter<{|
return this._roots;
}
// At least one of the currently mounted roots support the Legacy profiler.
get rootSupportsBasicProfiling(): boolean {
return this._rootSupportsBasicProfiling;
}
// At least one of the currently mounted roots support the Timeline profiler.
get rootSupportsTimelineProfiling(): boolean {
return this._rootSupportsTimelineProfiling;
}
get supportsNativeInspection(): boolean {
return this._supportsNativeInspection;
}
@@ -410,6 +427,8 @@ export default class Store extends EventEmitter<{|
return this._isNativeStyleEditorSupported;
}
// This build of DevTools supports the legacy profiler.
// This is a static flag, controled by the Store config.
get supportsProfiling(): boolean {
return this._supportsProfiling;
}
@@ -425,6 +444,8 @@ export default class Store extends EventEmitter<{|
);
}
// This build of DevTools supports the Timeline profiler.
// This is a static flag, controled by the Store config.
get supportsTimeline(): boolean {
return this._supportsTimeline;
}
@@ -903,7 +924,10 @@ export default class Store extends EventEmitter<{|
const isStrictModeCompliant = operations[i] > 0;
i++;
const supportsProfiling = operations[i] > 0;
const supportsBasicProfiling =
(operations[i] & PROFILING_FLAG_BASIC_SUPPORT) !== 0;
const supportsTimeline =
(operations[i] & PROFILING_FLAG_TIMELINE_SUPPORT) !== 0;
i++;
const supportsStrictMode = operations[i] > 0;
@@ -915,9 +939,10 @@ export default class Store extends EventEmitter<{|
this._roots = this._roots.concat(id);
this._rootIDToRendererID.set(id, rendererID);
this._rootIDToCapabilities.set(id, {
supportsBasicProfiling,
hasOwnerMetadata,
supportsProfiling,
supportsStrictMode,
supportsTimeline,
});
// Not all roots support StrictMode;
@@ -1224,25 +1249,38 @@ export default class Store extends EventEmitter<{|
}
if (haveRootsChanged) {
const prevSupportsProfiling = this._supportsProfiling;
const prevRootSupportsProfiling = this._rootSupportsBasicProfiling;
const prevRootSupportsTimelineProfiling = this
._rootSupportsTimelineProfiling;
this._hasOwnerMetadata = false;
this._supportsProfiling = false;
this._rootSupportsBasicProfiling = false;
this._rootSupportsTimelineProfiling = false;
this._rootIDToCapabilities.forEach(
({hasOwnerMetadata, supportsProfiling}) => {
({supportsBasicProfiling, hasOwnerMetadata, supportsTimeline}) => {
if (supportsBasicProfiling) {
this._rootSupportsBasicProfiling = true;
}
if (hasOwnerMetadata) {
this._hasOwnerMetadata = true;
}
if (supportsProfiling) {
this._supportsProfiling = true;
if (supportsTimeline) {
this._rootSupportsTimelineProfiling = true;
}
},
);
this.emit('roots');
if (this._supportsProfiling !== prevSupportsProfiling) {
this.emit('supportsProfiling');
if (this._rootSupportsBasicProfiling !== prevRootSupportsProfiling) {
this.emit('rootSupportsBasicProfiling');
}
if (
this._rootSupportsTimelineProfiling !==
prevRootSupportsTimelineProfiling
) {
this.emit('rootSupportsTimelineProfiling');
}
}
@@ -17,30 +17,27 @@ import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
export default function ClearProfilingDataButton() {
const store = useContext(StoreContext);
const {didRecordCommits, isProfiling, selectedTabID} = useContext(
ProfilerContext,
);
const {didRecordCommits, isProfiling} = useContext(ProfilerContext);
const {file, setFile} = useContext(TimelineContext);
const {profilerStore} = store;
let doesHaveData = false;
if (selectedTabID === 'timeline') {
doesHaveData = file !== null;
} else {
doesHaveData = didRecordCommits;
}
const doesHaveInMemoryData = didRecordCommits;
const doesHaveUserTimingData = file !== null;
const clear = () => {
if (selectedTabID === 'timeline') {
setFile(null);
} else {
if (doesHaveInMemoryData) {
profilerStore.clear();
}
if (doesHaveUserTimingData) {
setFile(null);
}
};
return (
<Button
disabled={isProfiling || !doesHaveData}
disabled={
isProfiling || !(doesHaveInMemoryData || doesHaveUserTimingData)
}
onClick={clear}
title="Clear profiling data">
<ButtonIcon type="clear" />
@@ -194,7 +194,7 @@ function updateTree(
if (type === ElementTypeRoot) {
i++; // isStrictModeCompliant
i++; // supportsProfiling flag
i++; // Profiling flag
i++; // supportsStrictMode flag
i++; // hasOwnerMetadata flag
@@ -0,0 +1,35 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import RecordToggle from './RecordToggle';
import styles from './Profiler.css';
export default function NoProfilingData() {
return (
<div className={styles.Column}>
<div className={styles.Header}>No profiling data has been recorded.</div>
<div className={styles.Row}>
Click the record button <RecordToggle /> to start recording.
</div>
<div className={`${styles.Row} ${styles.LearnMoreRow}`}>
Click{' '}
<a
className={styles.LearnMoreLink}
href="https://fb.me/react-devtools-profiling"
rel="noopener noreferrer"
target="_blank">
here
</a>{' '}
to learn more about profiling.
</div>
</div>
);
}
@@ -0,0 +1,21 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import styles from './Profiler.css';
export default function ProcessingData() {
return (
<div className={styles.Column}>
<div className={styles.Header}>Processing data...</div>
<div className={styles.Row}>This should only take a minute.</div>
</div>
);
}
@@ -61,6 +61,12 @@
justify-content: center;
}
.LearnMoreRow {
margin-top: 1rem;
color: var(--color-dim);
font-size: var(--font-size-sans-small);
}
.Header {
font-size: var(--font-size-sans-large);
margin-bottom: 0.5rem;
@@ -121,3 +127,9 @@
display: flex;
align-items: center;
}
.LearnMoreLink {
color: var(--color-link);
margin-left: 0.25rem;
margin-right: 0.25rem;
}
@@ -22,6 +22,10 @@ import ReloadAndProfileButton from './ReloadAndProfileButton';
import ProfilingImportExportButtons from './ProfilingImportExportButtons';
import SnapshotSelector from './SnapshotSelector';
import SidebarCommitInfo from './SidebarCommitInfo';
import NoProfilingData from './NoProfilingData';
import RecordingInProgress from './RecordingInProgress';
import ProcessingData from './ProcessingData';
import ProfilingNotSupported from './ProfilingNotSupported';
import SidebarSelectedFiberInfo from './SidebarSelectedFiberInfo';
import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
@@ -44,7 +48,9 @@ function Profiler(_: {||}) {
supportsProfiling,
} = useContext(ProfilerContext);
const {searchInputContainerRef} = useContext(TimelineContext);
const {file: timelineTraceEventData, searchInputContainerRef} = useContext(
TimelineContext,
);
const {supportsTimeline} = useContext(StoreContext);
@@ -69,6 +75,8 @@ function Profiler(_: {||}) {
view = <RecordingInProgress />;
} else if (isProcessingData) {
view = <ProcessingData />;
} else if (timelineTraceEventData) {
view = <OnlyTimelineData />;
} else if (supportsProfiling) {
view = <NoProfilingData />;
} else {
@@ -104,9 +112,7 @@ function Profiler(_: {||}) {
<div className={styles.Profiler}>
<div className={styles.LeftColumn}>
<div className={styles.Toolbar}>
<RecordToggle
disabled={!supportsProfiling || selectedTabID === 'timeline'}
/>
<RecordToggle disabled={!supportsProfiling} />
<ReloadAndProfileButton
disabled={selectedTabID === 'timeline' || !supportsProfiling}
/>
@@ -150,6 +156,15 @@ function Profiler(_: {||}) {
);
}
const OnlyTimelineData = () => (
<div className={styles.Column}>
<div className={styles.Header}>Timeline only</div>
<div className={styles.Row}>
The current profile contains only Timeline data.
</div>
</div>
);
const tabs = [
{
id: 'flame-chart',
@@ -176,50 +191,4 @@ const tabsWithTimeline = [
},
];
const NoProfilingData = () => (
<div className={styles.Column}>
<div className={styles.Header}>No profiling data has been recorded.</div>
<div className={styles.Row}>
Click the record button <RecordToggle /> to start recording.
</div>
</div>
);
const ProfilingNotSupported = () => (
<div className={styles.Column}>
<div className={styles.Header}>Profiling not supported.</div>
<p className={styles.Paragraph}>
Profiling support requires either a development or production-profiling
build of React v16.5+.
</p>
<p className={styles.Paragraph}>
Learn more at{' '}
<a
className={styles.Link}
href="https://reactjs.org/link/profiling"
rel="noopener noreferrer"
target="_blank">
reactjs.org/link/profiling
</a>
.
</p>
</div>
);
const ProcessingData = () => (
<div className={styles.Column}>
<div className={styles.Header}>Processing data...</div>
<div className={styles.Row}>This should only take a minute.</div>
</div>
);
const RecordingInProgress = () => (
<div className={styles.Column}>
<div className={styles.Header}>Profiling is in progress...</div>
<div className={styles.Row}>
Click the record button <RecordToggle /> to stop recording.
</div>
</div>
);
export default portaledContent(Profiler);
@@ -19,7 +19,6 @@ import {StoreContext} from '../context';
import type {ProfilingDataFrontend} from './types';
// TODO (timeline) Should this be its own context?
export type TabID = 'flame-chart' | 'ranked-chart' | 'timeline';
export type Context = {|
@@ -96,18 +95,18 @@ function ProfilerContextController({children}: Props) {
isProcessingData: profilerStore.isProcessingData,
isProfiling: profilerStore.isProfiling,
profilingData: profilerStore.profilingData,
supportsProfiling: store.supportsProfiling,
supportsProfiling: store.rootSupportsBasicProfiling,
}),
subscribe: (callback: Function) => {
profilerStore.addListener('profilingData', callback);
profilerStore.addListener('isProcessingData', callback);
profilerStore.addListener('isProfiling', callback);
store.addListener('supportsProfiling', callback);
store.addListener('rootSupportsBasicProfiling', callback);
return () => {
profilerStore.removeListener('profilingData', callback);
profilerStore.removeListener('isProcessingData', callback);
profilerStore.removeListener('isProfiling', callback);
store.removeListener('supportsProfiling', callback);
store.removeListener('rootSupportsBasicProfiling', callback);
};
},
}),
@@ -20,15 +20,15 @@ import {
} from './utils';
import {downloadFile} from '../utils';
import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
import isArray from 'shared/isArray';
import hasOwnProperty from 'shared/hasOwnProperty';
import styles from './ProfilingImportExportButtons.css';
import type {ProfilingDataExport} from './types';
export default function ProfilingImportExportButtons() {
const {isProfiling, profilingData, rootID, selectedTabID} = useContext(
ProfilerContext,
);
const {isProfiling, profilingData, rootID} = useContext(ProfilerContext);
const {setFile} = useContext(TimelineContext);
const store = useContext(StoreContext);
const {profilerStore} = store;
@@ -38,6 +38,8 @@ export default function ProfilingImportExportButtons() {
const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);
const doesHaveInMemoryData = profilerStore.didRecordCommits;
const downloadData = useCallback(() => {
if (rootID === null) {
return;
@@ -74,45 +76,54 @@ export default function ProfilingImportExportButtons() {
}
}, []);
const importProfilerData = useCallback(() => {
const input = inputRef.current;
if (input !== null && input.files.length > 0) {
const fileReader = new FileReader();
fileReader.addEventListener('load', () => {
try {
const raw = ((fileReader.result: any): string);
const profilingDataExport = ((JSON.parse(
raw,
): any): ProfilingDataExport);
profilerStore.profilingData = prepareProfilingDataFrontendFromExport(
profilingDataExport,
);
} catch (error) {
modalDialogDispatch({
id: 'ProfilingImportExportButtons',
type: 'SHOW',
title: 'Import failed',
content: (
<Fragment>
<div>The profiling data you selected cannot be imported.</div>
{error !== null && (
<div className={styles.ErrorMessage}>{error.message}</div>
)}
</Fragment>
),
});
}
});
// TODO (profiling) Handle fileReader errors.
fileReader.readAsText(input.files[0]);
}
}, [modalDialogDispatch, profilerStore]);
const importTimelineDataWrapper = event => {
// TODO (profiling) We should probably use a transition for this and suspend while loading the file.
// Local files load so fast it's probably not very noticeable though.
const handleChange = () => {
const input = inputRef.current;
if (input !== null && input.files.length > 0) {
const file = input.files[0];
setFile(file);
// TODO (profiling) Handle fileReader errors.
const fileReader = new FileReader();
fileReader.addEventListener('load', () => {
const raw = ((fileReader.result: any): string);
const json = JSON.parse(raw);
if (!isArray(json) && hasOwnProperty.call(json, 'version')) {
// This looks like React profiling data.
// But first, clear any User Timing marks; we should only have one type open at a time.
setFile(null);
try {
const profilingDataExport = ((json: any): ProfilingDataExport);
profilerStore.profilingData = prepareProfilingDataFrontendFromExport(
profilingDataExport,
);
} catch (error) {
modalDialogDispatch({
id: 'ProfilingImportExportButtons',
type: 'SHOW',
title: 'Import failed',
content: (
<Fragment>
<div>The profiling data you selected cannot be imported.</div>
{error !== null && (
<div className={styles.ErrorMessage}>{error.message}</div>
)}
</Fragment>
),
});
}
} else {
// Otherwise let's assume this is Trace Event data and pass it to the Timeline preprocessor.
// But first, clear React profiling data; we should only have one type open at a time.
profilerStore.clear();
// TODO (timeline) We shouldn't need to re-open the File but we'll need to refactor to avoid this.
setFile(file);
}
});
fileReader.readAsText(file);
}
};
@@ -124,11 +135,7 @@ export default function ProfilingImportExportButtons() {
className={styles.Input}
type="file"
accept=".json"
onChange={
selectedTabID === 'timeline'
? importTimelineDataWrapper
: importProfilerData
}
onChange={handleChange}
tabIndex={-1}
/>
<a ref={downloadRef} className={styles.Input} />
@@ -139,11 +146,7 @@ export default function ProfilingImportExportButtons() {
<ButtonIcon type="import" />
</Button>
<Button
disabled={
isProfiling ||
!profilerStore.didRecordCommits ||
selectedTabID === 'timeline'
}
disabled={isProfiling || !doesHaveInMemoryData}
onClick={downloadData}
title="Save profile...">
<ButtonIcon type="export" />
@@ -0,0 +1,35 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import styles from './Profiler.css';
export default function ProfilingNotSupported() {
return (
<div className={styles.Column}>
<div className={styles.Header}>Profiling not supported.</div>
<p className={styles.Paragraph}>
Profiling support requires either a development or profiling build of
React v16.5+.
</p>
<p className={styles.Paragraph}>
Learn more at{' '}
<a
className={styles.Link}
href="https://fb.me/react-devtools-profiling"
rel="noopener noreferrer"
target="_blank">
reactjs.org/link/profiling
</a>
.
</p>
</div>
);
}
@@ -0,0 +1,24 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import RecordToggle from './RecordToggle';
import styles from './Profiler.css';
export default function RecordingInProgress() {
return (
<div className={styles.Column}>
<div className={styles.Header}>Profiling is in progress...</div>
<div className={styles.Row}>
Click the record button <RecordToggle /> to stop recording.
</div>
</div>
);
}
@@ -9,6 +9,10 @@
import type {ElementType} from 'react-devtools-shared/src/types';
import type {SerializedElement} from '../Components/types';
import type {
TimelineData,
TimelineDataExport,
} from 'react-devtools-timeline/src/types';
export type CommitTreeNode = {|
id: number,
@@ -103,8 +107,15 @@ export type ProfilingDataForRootFrontend = {|
// Combination of profiling data collected by the renderer interface (backend) and Store (frontend).
export type ProfilingDataFrontend = {|
// Profiling data per root.
// Legacy profiling data is per renderer + root.
dataForRoots: Map<number, ProfilingDataForRootFrontend>,
// Timeline data is per rederer.
timelineData: Array<TimelineData>,
// Some functionality should be disabled for imported data.
// e.g. DevTools should not try to sync selection between Components and Profiler tabs,
// even if there are Fibers with the same IDs.
imported: boolean,
|};
@@ -135,5 +146,11 @@ export type ProfilingDataForRootExport = {|
// Serializable version of ProfilingDataFrontend data.
export type ProfilingDataExport = {|
version: 5,
// Legacy profiling data is per renderer + root.
dataForRoots: Array<ProfilingDataForRootExport>,
// Timeline data is per rederer.
// Note that old exported profiles won't contain this key.
timelineData?: Array<TimelineDataExport>,
|};
@@ -18,6 +18,10 @@ import type {
ProfilingDataFrontend,
SnapshotNode,
} from './types';
import type {
TimelineData,
TimelineDataExport,
} from 'react-devtools-timeline/src/types';
const commitGradient = [
'var(--color-commit-gradient-0)',
@@ -41,7 +45,31 @@ export function prepareProfilingDataFrontendFromBackendAndStore(
): ProfilingDataFrontend {
const dataForRoots: Map<number, ProfilingDataForRootFrontend> = new Map();
const timelineDataArray = [];
dataBackends.forEach(dataBackend => {
const {timelineData} = dataBackend;
if (timelineData != null) {
const {
batchUIDToMeasuresKeyValueArray,
internalModuleSourceToRanges,
laneToLabelKeyValueArray,
laneToReactMeasureKeyValueArray,
...rest
} = timelineData;
timelineDataArray.push({
...rest,
// Most of the data is safe to parse as-is,
// but we need to convert the nested Arrays back to Maps.
batchUIDToMeasuresMap: new Map(batchUIDToMeasuresKeyValueArray),
internalModuleSourceToRanges: new Map(internalModuleSourceToRanges),
laneToLabelMap: new Map(laneToLabelKeyValueArray),
laneToReactMeasureMap: new Map(laneToReactMeasureKeyValueArray),
});
}
dataBackend.dataForRoots.forEach(
({commitData, displayName, initialTreeBaseDurations, rootID}) => {
const operations = operationsByRootID.get(rootID);
@@ -108,7 +136,7 @@ export function prepareProfilingDataFrontendFromBackendAndStore(
);
});
return {dataForRoots, imported: false};
return {dataForRoots, imported: false, timelineData: timelineDataArray};
}
// Converts a Profiling data export into the format required by the Store.
@@ -123,6 +151,50 @@ export function prepareProfilingDataFrontendFromExport(
);
}
const timelineData: Array<TimelineData> = profilingDataExport.timelineData
? profilingDataExport.timelineData.map(
({
batchUIDToMeasuresKeyValueArray,
componentMeasures,
duration,
flamechart,
internalModuleSourceToRanges,
laneToLabelKeyValueArray,
laneToReactMeasureKeyValueArray,
nativeEvents,
networkMeasures,
otherUserTimingMarks,
reactVersion,
schedulingEvents,
snapshots,
snapshotHeight,
startTime,
suspenseEvents,
thrownErrors,
}) => ({
// Most of the data is safe to parse as-is,
// but we need to convert the nested Arrays back to Maps.
batchUIDToMeasuresMap: new Map(batchUIDToMeasuresKeyValueArray),
componentMeasures,
duration,
flamechart,
internalModuleSourceToRanges: new Map(internalModuleSourceToRanges),
laneToLabelMap: new Map(laneToLabelKeyValueArray),
laneToReactMeasureMap: new Map(laneToReactMeasureKeyValueArray),
nativeEvents,
networkMeasures,
otherUserTimingMarks,
reactVersion,
schedulingEvents,
snapshots,
snapshotHeight,
startTime,
suspenseEvents,
thrownErrors,
}),
)
: [];
const dataForRoots: Map<number, ProfilingDataForRootFrontend> = new Map();
profilingDataExport.dataForRoots.forEach(
({
@@ -167,13 +239,65 @@ export function prepareProfilingDataFrontendFromExport(
},
);
return {dataForRoots, imported: true};
return {
dataForRoots,
imported: true,
timelineData,
};
}
// Converts a Store Profiling data into a format that can be safely (JSON) serialized for export.
export function prepareProfilingDataExport(
profilingDataFrontend: ProfilingDataFrontend,
): ProfilingDataExport {
const timelineData: Array<TimelineDataExport> = profilingDataFrontend.timelineData.map(
({
batchUIDToMeasuresMap,
componentMeasures,
duration,
flamechart,
internalModuleSourceToRanges,
laneToLabelMap,
laneToReactMeasureMap,
nativeEvents,
networkMeasures,
otherUserTimingMarks,
reactVersion,
schedulingEvents,
snapshots,
snapshotHeight,
startTime,
suspenseEvents,
thrownErrors,
}) => ({
// Most of the data is safe to serialize as-is,
// but we need to convert the Maps to nested Arrays.
batchUIDToMeasuresKeyValueArray: Array.from(
batchUIDToMeasuresMap.entries(),
),
componentMeasures: componentMeasures,
duration,
flamechart,
internalModuleSourceToRanges: Array.from(
internalModuleSourceToRanges.entries(),
),
laneToLabelKeyValueArray: Array.from(laneToLabelMap.entries()),
laneToReactMeasureKeyValueArray: Array.from(
laneToReactMeasureMap.entries(),
),
nativeEvents,
networkMeasures,
otherUserTimingMarks,
reactVersion,
schedulingEvents,
snapshots,
snapshotHeight,
startTime,
suspenseEvents,
thrownErrors,
}),
);
const dataForRoots: Array<ProfilingDataForRootExport> = [];
profilingDataFrontend.dataForRoots.forEach(
({
@@ -225,6 +349,7 @@ export function prepareProfilingDataExport(
return {
version: PROFILER_EXPORT_VERSION,
dataForRoots,
timelineData,
};
}
+62 -41
View File
@@ -10,7 +10,7 @@
import type {Point} from './view-base';
import type {
ReactHoverContextInfo,
ReactProfilerData,
TimelineData,
ReactMeasure,
ViewState,
} from './types';
@@ -69,7 +69,7 @@ import styles from './CanvasPage.css';
const CONTEXT_MENU_ID = 'canvas';
type Props = {|
profilerData: ReactProfilerData,
profilerData: TimelineData,
viewState: ViewState,
|};
@@ -92,7 +92,7 @@ function CanvasPage({profilerData, viewState}: Props) {
);
}
const copySummary = (data: ReactProfilerData, measure: ReactMeasure) => {
const copySummary = (data: TimelineData, measure: ReactMeasure) => {
const {batchUID, duration, timestamp, type} = measure;
const [startTime, stopTime] = getBatchRange(batchUID, data);
@@ -108,7 +108,7 @@ const copySummary = (data: ReactProfilerData, measure: ReactMeasure) => {
};
const zoomToBatch = (
data: ReactProfilerData,
data: TimelineData,
measure: ReactMeasure,
viewState: ViewState,
width: number,
@@ -145,7 +145,7 @@ const EMPTY_CONTEXT_INFO: ReactHoverContextInfo = {
};
type AutoSizedCanvasProps = {|
data: ReactProfilerData,
data: TimelineData,
height: number,
viewState: ViewState,
width: number,
@@ -295,14 +295,21 @@ function AutoSizedCanvas({
);
}
const nativeEventsView = new NativeEventsView(surface, defaultFrame, data);
nativeEventsViewRef.current = nativeEventsView;
const nativeEventsViewWrapper = createViewHelper(
nativeEventsView,
'events',
true,
true,
);
let nativeEventsViewWrapper = null;
if (data.nativeEvents.length > 0) {
const nativeEventsView = new NativeEventsView(
surface,
defaultFrame,
data,
);
nativeEventsViewRef.current = nativeEventsView;
nativeEventsViewWrapper = createViewHelper(
nativeEventsView,
'events',
true,
true,
);
}
let thrownErrorsViewWrapper = null;
if (data.thrownErrors.length > 0) {
@@ -318,16 +325,19 @@ function AutoSizedCanvas({
);
}
const schedulingEventsView = new SchedulingEventsView(
surface,
defaultFrame,
data,
);
schedulingEventsViewRef.current = schedulingEventsView;
const schedulingEventsViewWrapper = createViewHelper(
schedulingEventsView,
'react updates',
);
let schedulingEventsViewWrapper = null;
if (data.schedulingEvents.length > 0) {
const schedulingEventsView = new SchedulingEventsView(
surface,
defaultFrame,
data,
);
schedulingEventsViewRef.current = schedulingEventsView;
schedulingEventsViewWrapper = createViewHelper(
schedulingEventsView,
'react updates',
);
}
let suspenseEventsViewWrapper = null;
if (data.suspenseEvents.length > 0) {
@@ -401,20 +411,23 @@ function AutoSizedCanvas({
);
}
const flamechartView = new FlamechartView(
surface,
defaultFrame,
data.flamechart,
data.internalModuleSourceToRanges,
data.duration,
);
flamechartViewRef.current = flamechartView;
const flamechartViewWrapper = createViewHelper(
flamechartView,
'flamechart',
true,
true,
);
let flamechartViewWrapper = null;
if (data.flamechart.length > 0) {
const flamechartView = new FlamechartView(
surface,
defaultFrame,
data.flamechart,
data.internalModuleSourceToRanges,
data.duration,
);
flamechartViewRef.current = flamechartView;
flamechartViewWrapper = createViewHelper(
flamechartView,
'flamechart',
true,
true,
);
}
// Root view contains all of the sub views defined above.
// The order we add them below determines their vertical position.
@@ -429,15 +442,21 @@ function AutoSizedCanvas({
if (userTimingMarksViewWrapper !== null) {
rootView.addSubview(userTimingMarksViewWrapper);
}
rootView.addSubview(nativeEventsViewWrapper);
rootView.addSubview(schedulingEventsViewWrapper);
if (nativeEventsViewWrapper !== null) {
rootView.addSubview(nativeEventsViewWrapper);
}
if (schedulingEventsViewWrapper !== null) {
rootView.addSubview(schedulingEventsViewWrapper);
}
if (thrownErrorsViewWrapper !== null) {
rootView.addSubview(thrownErrorsViewWrapper);
}
if (suspenseEventsViewWrapper !== null) {
rootView.addSubview(suspenseEventsViewWrapper);
}
rootView.addSubview(reactMeasuresViewWrapper);
if (reactMeasuresViewWrapper !== null) {
rootView.addSubview(reactMeasuresViewWrapper);
}
if (componentMeasuresViewWrapper !== null) {
rootView.addSubview(componentMeasuresViewWrapper);
}
@@ -447,7 +466,9 @@ function AutoSizedCanvas({
if (networkMeasuresViewWrapper !== null) {
rootView.addSubview(networkMeasuresViewWrapper);
}
rootView.addSubview(flamechartViewWrapper);
if (flamechartViewWrapper !== null) {
rootView.addSubview(flamechartViewWrapper);
}
const verticalScrollOverflowView = new VerticalScrollOverflowView(
surface,
+4 -4
View File
@@ -15,7 +15,7 @@ import type {
ReactComponentMeasure,
ReactHoverContextInfo,
ReactMeasure,
ReactProfilerData,
TimelineData,
SchedulingEvent,
Snapshot,
SuspenseEvent,
@@ -33,7 +33,7 @@ const MAX_TOOLTIP_TEXT_LENGTH = 60;
type Props = {|
canvasRef: {|current: HTMLCanvasElement | null|},
data: ReactProfilerData,
data: TimelineData,
height: number,
hoveredEvent: ReactHoverContextInfo | null,
origin: Point,
@@ -278,7 +278,7 @@ const TooltipSchedulingEvent = ({
data,
schedulingEvent,
}: {|
data: ReactProfilerData,
data: TimelineData,
schedulingEvent: SchedulingEvent,
|}) => {
const label = getSchedulingEventLabel(schedulingEvent);
@@ -433,7 +433,7 @@ const TooltipReactMeasure = ({
data,
measure,
}: {|
data: ReactProfilerData,
data: TimelineData,
measure: ReactMeasure,
|}) => {
const label = getReactMeasureLabel(measure.type);
@@ -1,10 +0,0 @@
/**
* https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications
*/
.Input {
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
}
-59
View File
@@ -1,59 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import {useCallback, useRef} from 'react';
import Button from 'react-devtools-shared/src/devtools/views/Button';
import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
import styles from './ImportButton.css';
type Props = {|
children?: mixed,
onFileSelect: (file: File) => void,
|};
export default function ImportButton({children = null, onFileSelect}: Props) {
const inputRef = useRef<HTMLInputElement | null>(null);
const handleFiles = useCallback(() => {
const input = inputRef.current;
if (input === null) {
return;
}
if (input.files.length > 0) {
onFileSelect(input.files[0]);
}
// Reset input element to allow the same file to be re-imported
input.value = '';
}, [onFileSelect]);
const uploadData = useCallback(() => {
if (inputRef.current !== null) {
inputRef.current.click();
}
}, []);
return (
<>
<input
ref={inputRef}
className={styles.Input}
type="file"
accept=".json"
onChange={handleFiles}
tabIndex={-1}
/>
<Button onClick={uploadData} title="Load profile...">
<ButtonIcon type="import" />
{children}
</Button>
</>
);
}
+56 -60
View File
@@ -9,7 +9,6 @@
import type {ViewState} from './types';
import {isInternalFacebookBuild} from 'react-devtools-feature-flags';
import * as React from 'react';
import {
Suspense,
@@ -20,18 +19,28 @@ import {
useState,
} from 'react';
import {SettingsContext} from 'react-devtools-shared/src/devtools/views/Settings/SettingsContext';
import {ProfilerContext} from 'react-devtools-shared/src/devtools/views/Profiler/ProfilerContext';
import NoProfilingData from 'react-devtools-shared/src/devtools/views/Profiler/NoProfilingData';
import RecordingInProgress from 'react-devtools-shared/src/devtools/views/Profiler/RecordingInProgress';
import {updateColorsToMatchTheme} from './content-views/constants';
import {TimelineContext} from './TimelineContext';
import ImportButton from './ImportButton';
import CanvasPage from './CanvasPage';
import {importFile} from './timelineCache';
import TimelineSearchInput from './TimelineSearchInput';
import TimelineNotSupported from './TimelineNotSupported';
import {TimelineSearchContextController} from './TimelineSearchContext';
import styles from './Timeline.css';
export function Timeline(_: {||}) {
const {file, setFile, viewState} = useContext(TimelineContext);
const {
file,
inMemoryTimelineData,
isTimelineSupported,
setFile,
viewState,
} = useContext(TimelineContext);
const {didRecordCommits, isProfiling} = useContext(ProfilerContext);
const ref = useRef(null);
@@ -60,66 +69,47 @@ export function Timeline(_: {||}) {
};
}, [deferredTheme]);
let content = null;
if (isProfiling) {
content = <RecordingInProgress />;
} else if (inMemoryTimelineData && inMemoryTimelineData.length > 0) {
// TODO (timeline) Support multiple renderers.
const timelineData = inMemoryTimelineData[0];
content = (
<TimelineSearchContextController
profilerData={timelineData}
viewState={viewState}>
<TimelineSearchInput />
<CanvasPage profilerData={timelineData} viewState={viewState} />
</TimelineSearchContextController>
);
} else if (file) {
content = (
<Suspense fallback={<ProcessingData />}>
<FileLoader
file={file}
key={key}
onFileSelect={setFile}
viewState={viewState}
/>
</Suspense>
);
} else if (didRecordCommits) {
content = <NoTimelineData />;
} else if (isTimelineSupported) {
content = <NoProfilingData />;
} else {
content = <TimelineNotSupported />;
}
return (
<div className={styles.Content} ref={ref}>
{file ? (
<Suspense fallback={<ProcessingData />}>
<FileLoader
file={file}
key={key}
onFileSelect={setFile}
viewState={viewState}
/>
</Suspense>
) : (
<Welcome onFileSelect={setFile} />
)}
{content}
</div>
);
}
const Welcome = ({onFileSelect}: {|onFileSelect: (file: File) => void|}) => (
<ol className={styles.WelcomeInstructionsList}>
{isInternalFacebookBuild && (
<li className={styles.WelcomeInstructionsListItem}>
Enable the
<a
className={styles.WelcomeInstructionsListItemLink}
href="https://fburl.com/react-devtools-scheduling-profiler-gk"
rel="noopener noreferrer"
target="_blank">
<code>react_enable_scheduling_profiler</code> GK
</a>
.
</li>
)}
<li className={styles.WelcomeInstructionsListItem}>
Open a website that's built with the
<a
className={styles.WelcomeInstructionsListItemLink}
href="https://reactjs.org/link/profiling"
rel="noopener noreferrer"
target="_blank">
profiling build of ReactDOM
</a>
(version 18 or newer).
</li>
<li className={styles.WelcomeInstructionsListItem}>
Open the "Performance" tab in Chrome and record some performance data.
</li>
<li className={styles.WelcomeInstructionsListItem}>
Click the "Save profile..." button in Chrome to export the data.
</li>
<li className={styles.WelcomeInstructionsListItem}>
Import the data into the profiler:
<br />
<ImportButton onFileSelect={onFileSelect}>
<span className={styles.ImportButtonLabel}>Import</span>
</ImportButton>
</li>
</ol>
);
const ProcessingData = () => (
<div className={styles.EmptyStateContainer}>
<div className={styles.Header}>Processing data...</div>
@@ -136,9 +126,15 @@ const CouldNotLoadProfile = ({error, onFileSelect}) => (
</div>
)}
<div className={styles.Row}>
Try importing
<ImportButton onFileSelect={onFileSelect} />
another Chrome performance profile.
Try importing another Chrome performance profile.
</div>
</div>
);
const NoTimelineData = () => (
<div className={styles.EmptyStateContainer}>
<div className={styles.Row}>
This current profile does not contain timeline data.
</div>
</div>
);
+43 -2
View File
@@ -8,10 +8,19 @@
*/
import * as React from 'react';
import {createContext, useMemo, useRef, useState} from 'react';
import {
createContext,
useContext,
useMemo,
useRef,
useState,
useSyncExternalStore,
} from 'react';
import {StoreContext} from 'react-devtools-shared/src/devtools/views/context';
import type {
HorizontalScrollStateChangeCallback,
TimelineData,
SearchRegExpStateChangeCallback,
ViewState,
} from './types';
@@ -19,6 +28,8 @@ import type {RefObject} from 'shared/ReactTypes';
export type Context = {|
file: File | null,
inMemoryTimelineData: Array<TimelineData> | null,
isTimelineSupported: boolean,
searchInputContainerRef: RefObject,
setFile: (file: File | null) => void,
viewState: ViewState,
@@ -35,6 +46,34 @@ function TimelineContextController({children}: Props) {
const searchInputContainerRef = useRef(null);
const [file, setFile] = useState<string | null>(null);
const store = useContext(StoreContext);
const isTimelineSupported = useSyncExternalStore<boolean>(
function subscribe(callback) {
store.addListener('rootSupportsTimelineProfiling', callback);
return function unsubscribe() {
store.removeListener('rootSupportsTimelineProfiling', callback);
};
},
function getState() {
return store.rootSupportsTimelineProfiling;
},
);
const inMemoryTimelineData = useSyncExternalStore<Array<TimelineData> | null>(
function subscribe(callback) {
store.profilerStore.addListener('isProcessingData', callback);
store.profilerStore.addListener('profilingData', callback);
return function unsubscribe() {
store.profilerStore.removeListener('isProcessingData', callback);
store.profilerStore.removeListener('profilingData', callback);
};
},
function getState() {
return store.profilerStore.profilingData?.timelineData || null;
},
);
// Recreate view state any time new profiling data is imported.
const viewState = useMemo<ViewState>(() => {
const horizontalScrollStateChangeCallbacks: Set<HorizontalScrollStateChangeCallback> = new Set();
@@ -85,11 +124,13 @@ function TimelineContextController({children}: Props) {
const value = useMemo(
() => ({
file,
inMemoryTimelineData,
isTimelineSupported,
searchInputContainerRef,
setFile,
viewState,
}),
[file, setFile, viewState],
[file, inMemoryTimelineData, isTimelineSupported, setFile, viewState],
);
return (
@@ -0,0 +1,38 @@
.Column {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 1rem;
}
.Header {
font-size: var(--font-size-sans-large);
margin-bottom: 0.5rem;
}
.Paragraph {
text-align: center;
margin: 0;
}
.Link {
color: var(--color-link);
}
.LearnMoreRow {
margin-top: 1rem;
color: var(--color-dim);
font-size: var(--font-size-sans-small);
}
.Code {
color: var(--color-bridge-version-number);
}
.MetaGKRow {
background: var(--color-background-hover);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
margin-top: 1rem;
}
@@ -0,0 +1,52 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import {isInternalFacebookBuild} from 'react-devtools-feature-flags';
import styles from './TimelineNotSupported.css';
export default function TimelineNotSupported() {
return (
<div className={styles.Column}>
<div className={styles.Header}>Timeline profiling not supported.</div>
<p className={styles.Paragraph}>
<span>
Timeline profiler requires a development or profiling build of{' '}
<code className={styles.Code}>react-dom@^18</code>.
</span>
</p>
<div className={styles.LearnMoreRow}>
Click{' '}
<a
className={styles.Link}
href="https://fb.me/react-devtools-profiling"
rel="noopener noreferrer"
target="_blank">
here
</a>{' '}
to learn more about profiling.
</div>
{isInternalFacebookBuild && (
<div className={styles.MetaGKRow}>
<strong>Meta only</strong>: Enable the{' '}
<a
className={styles.Link}
href="https://fburl.com/react-devtools-scheduling-profiler-gk"
rel="noopener noreferrer"
target="_blank">
react_enable_scheduling_profiler GK
</a>
.
</div>
)}
</div>
);
}
@@ -10,14 +10,10 @@
import * as React from 'react';
import {createContext, useMemo, useReducer} from 'react';
import type {
ReactComponentMeasure,
ReactProfilerData,
ViewState,
} from './types';
import type {ReactComponentMeasure, TimelineData, ViewState} from './types';
type State = {|
profilerData: ReactProfilerData,
profilerData: TimelineData,
searchIndex: number,
searchRegExp: RegExp | null,
searchResults: Array<ReactComponentMeasure>,
@@ -116,7 +112,7 @@ function reducer(state: State, action: Action): State {
}
export type Context = {|
profilerData: ReactProfilerData,
profilerData: TimelineData,
// Search state
dispatch: Dispatch,
@@ -131,7 +127,7 @@ TimelineSearchContext.displayName = 'TimelineSearchContext';
type Props = {|
children: React$Node,
profilerData: ReactProfilerData,
profilerData: TimelineData,
viewState: ViewState,
|};
@@ -7,11 +7,7 @@
* @flow
*/
import type {
ReactComponentMeasure,
ReactProfilerData,
ViewState,
} from '../types';
import type {ReactComponentMeasure, TimelineData, ViewState} from '../types';
import type {
Interaction,
IntrinsicSize,
@@ -44,7 +40,7 @@ export class ComponentMeasuresView extends View {
_cachedSearchRegExp: RegExp | null = null;
_hoveredComponentMeasure: ReactComponentMeasure | null = null;
_intrinsicSize: IntrinsicSize;
_profilerData: ReactProfilerData;
_profilerData: TimelineData;
_viewState: ViewState;
onHover: ((event: ReactComponentMeasure | null) => void) | null = null;
@@ -52,7 +48,7 @@ export class ComponentMeasuresView extends View {
constructor(
surface: Surface,
frame: Rect,
profilerData: ReactProfilerData,
profilerData: TimelineData,
viewState: ViewState,
) {
super(surface, frame);
@@ -7,7 +7,7 @@
* @flow
*/
import type {NativeEvent, ReactProfilerData} from '../types';
import type {NativeEvent, TimelineData} from '../types';
import type {
Interaction,
IntrinsicSize,
@@ -40,11 +40,11 @@ export class NativeEventsView extends View {
_hoveredEvent: NativeEvent | null = null;
_intrinsicSize: IntrinsicSize;
_maxDepth: number = 0;
_profilerData: ReactProfilerData;
_profilerData: TimelineData;
onHover: ((event: NativeEvent | null) => void) | null = null;
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
constructor(surface: Surface, frame: Rect, profilerData: TimelineData) {
super(surface, frame);
this._profilerData = profilerData;
@@ -7,7 +7,7 @@
* @flow
*/
import type {NetworkMeasure, ReactProfilerData} from '../types';
import type {NetworkMeasure, TimelineData} from '../types';
import type {
Interaction,
IntrinsicSize,
@@ -43,11 +43,11 @@ export class NetworkMeasuresView extends View {
_hoveredNetworkMeasure: NetworkMeasure | null = null;
_intrinsicSize: IntrinsicSize;
_maxDepth: number = 0;
_profilerData: ReactProfilerData;
_profilerData: TimelineData;
onHover: ((event: NetworkMeasure | null) => void) | null = null;
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
constructor(surface: Surface, frame: Rect, profilerData: TimelineData) {
super(surface, frame);
this._profilerData = profilerData;
@@ -7,7 +7,7 @@
* @flow
*/
import type {ReactLane, ReactMeasure, ReactProfilerData} from '../types';
import type {ReactLane, ReactMeasure, TimelineData} from '../types';
import type {
Interaction,
IntrinsicSize,
@@ -33,7 +33,6 @@ import {
} from '../view-base';
import {COLORS, BORDER_SIZE, REACT_MEASURE_HEIGHT} from './constants';
import {REACT_TOTAL_NUM_LANES} from '../constants';
const REACT_LANE_HEIGHT = REACT_MEASURE_HEIGHT + BORDER_SIZE;
const MAX_ROWS_TO_SHOW_INITIALLY = 5;
@@ -41,12 +40,12 @@ const MAX_ROWS_TO_SHOW_INITIALLY = 5;
export class ReactMeasuresView extends View {
_intrinsicSize: IntrinsicSize;
_lanesToRender: ReactLane[];
_profilerData: ReactProfilerData;
_profilerData: TimelineData;
_hoveredMeasure: ReactMeasure | null = null;
onHover: ((measure: ReactMeasure | null) => void) | null = null;
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
constructor(surface: Surface, frame: Rect, profilerData: TimelineData) {
super(surface, frame);
this._profilerData = profilerData;
this._performPreflightComputations();
@@ -55,12 +54,11 @@ export class ReactMeasuresView extends View {
_performPreflightComputations() {
this._lanesToRender = [];
for (let lane: ReactLane = 0; lane < REACT_TOTAL_NUM_LANES; lane++) {
const measuresForLane = this._profilerData.laneToReactMeasureMap.get(
lane,
);
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const [lane, measuresForLane] of this._profilerData
.laneToReactMeasureMap) {
// Only show lanes with measures
if (measuresForLane != null && measuresForLane.length > 0) {
if (measuresForLane.length > 0) {
this._lanesToRender.push(lane);
}
}
@@ -7,7 +7,7 @@
* @flow
*/
import type {SchedulingEvent, ReactProfilerData} from '../types';
import type {SchedulingEvent, TimelineData} from '../types';
import type {
Interaction,
MouseMoveInteraction,
@@ -40,13 +40,13 @@ const EVENT_ROW_HEIGHT_FIXED =
TOP_ROW_PADDING + REACT_EVENT_DIAMETER + TOP_ROW_PADDING;
export class SchedulingEventsView extends View {
_profilerData: ReactProfilerData;
_profilerData: TimelineData;
_intrinsicSize: Size;
_hoveredEvent: SchedulingEvent | null = null;
onHover: ((event: SchedulingEvent | null) => void) | null = null;
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
constructor(surface: Surface, frame: Rect, profilerData: TimelineData) {
super(surface, frame);
this._profilerData = profilerData;
@@ -7,7 +7,7 @@
* @flow
*/
import type {Snapshot, ReactProfilerData} from '../types';
import type {Snapshot, TimelineData} from '../types';
import type {
Interaction,
Point,
@@ -31,11 +31,11 @@ type OnHover = (node: Snapshot | null) => void;
export class SnapshotsView extends View {
_hoverLocation: Point | null = null;
_intrinsicSize: Size;
_profilerData: ReactProfilerData;
_profilerData: TimelineData;
onHover: OnHover | null = null;
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
constructor(surface: Surface, frame: Rect, profilerData: TimelineData) {
super(surface, frame);
this._intrinsicSize = {
@@ -7,7 +7,7 @@
* @flow
*/
import type {SuspenseEvent, ReactProfilerData} from '../types';
import type {SuspenseEvent, TimelineData} from '../types';
import type {
Interaction,
IntrinsicSize,
@@ -47,11 +47,11 @@ export class SuspenseEventsView extends View {
_hoveredEvent: SuspenseEvent | null = null;
_intrinsicSize: IntrinsicSize;
_maxDepth: number = 0;
_profilerData: ReactProfilerData;
_profilerData: TimelineData;
onHover: ((event: SuspenseEvent | null) => void) | null = null;
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
constructor(surface: Surface, frame: Rect, profilerData: TimelineData) {
super(surface, frame);
this._profilerData = profilerData;
@@ -7,7 +7,7 @@
* @flow
*/
import type {ThrownError, ReactProfilerData} from '../types';
import type {ThrownError, TimelineData} from '../types';
import type {
Interaction,
MouseMoveInteraction,
@@ -40,12 +40,12 @@ const EVENT_ROW_HEIGHT_FIXED =
TOP_ROW_PADDING + REACT_EVENT_DIAMETER + TOP_ROW_PADDING;
export class ThrownErrorsView extends View {
_profilerData: ReactProfilerData;
_profilerData: TimelineData;
_intrinsicSize: Size;
_hoveredEvent: ThrownError | null = null;
onHover: ((event: ThrownError | null) => void) | null = null;
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
constructor(surface: Surface, frame: Rect, profilerData: TimelineData) {
super(surface, frame);
this._profilerData = profilerData;
@@ -11,17 +11,17 @@ import {createResource} from 'react-devtools-shared/src/devtools/cache';
import {importFile} from './import-worker';
import type {Resource} from 'react-devtools-shared/src/devtools/cache';
import type {ReactProfilerData} from './types';
import type {TimelineData} from './types';
import type {ImportWorkerOutputData} from './import-worker/index';
export type DataResource = Resource<void, File, ReactProfilerData | Error>;
export type DataResource = Resource<void, File, TimelineData | Error>;
export default function createDataResourceFromImportedFile(
file: File,
): DataResource {
return createResource(
() => {
return new Promise<ReactProfilerData | Error>((resolve, reject) => {
return new Promise<TimelineData | Error>((resolve, reject) => {
const promise = ((importFile(
file,
): any): Promise<ImportWorkerOutputData>);
@@ -13,7 +13,7 @@
import * as importFileModule from './importFile';
import WorkerizedImportFile from './importFile.worker';
import type {ReactProfilerData} from '../types';
import type {TimelineData} from '../types';
type ImportFileModule = typeof importFileModule;
@@ -22,7 +22,7 @@ const workerizedImportFile: ImportFileModule = window.Worker
: importFileModule;
export type ImportWorkerOutputData =
| {|status: 'SUCCESS', processedData: ReactProfilerData|}
| {|status: 'SUCCESS', processedData: TimelineData|}
| {|status: 'INVALID_PROFILE_ERROR', error: Error|}
| {|status: 'UNEXPECTED_ERROR', error: Error|};
@@ -25,7 +25,7 @@ import type {
ReactComponentMeasureType,
ReactMeasure,
ReactMeasureType,
ReactProfilerData,
TimelineData,
SchedulingEvent,
SuspenseEvent,
} from '../types';
@@ -105,7 +105,7 @@ export function getLanesFromTransportDecimalBitmask(
}
function updateLaneToLabelMap(
profilerData: ReactProfilerData,
profilerData: TimelineData,
laneLabelTuplesString: string,
): void {
// These marks appear multiple times in the data;
@@ -143,7 +143,7 @@ function markWorkStarted(
type: ReactMeasureType,
startTime: Milliseconds,
lanes: ReactLane[],
currentProfilerData: ReactProfilerData,
currentProfilerData: TimelineData,
state: ProcessorState,
) {
const {batchUID, measureStack} = state;
@@ -179,7 +179,7 @@ function markWorkStarted(
function markWorkCompleted(
type: ReactMeasureType,
stopTime: Milliseconds,
currentProfilerData: ReactProfilerData,
currentProfilerData: TimelineData,
stack: $PropertyType<ProcessorState, 'measureStack'>,
) {
if (stack.length === 0) {
@@ -229,7 +229,7 @@ function throwIfIncomplete(
function processEventDispatch(
event: TimelineEvent,
timestamp: Milliseconds,
profilerData: ReactProfilerData,
profilerData: TimelineData,
state: ProcessorState,
) {
const data = event.args.data;
@@ -292,7 +292,7 @@ function processEventDispatch(
function processResourceFinish(
event: TimelineEvent,
timestamp: Milliseconds,
profilerData: ReactProfilerData,
profilerData: TimelineData,
state: ProcessorState,
) {
const requestId = event.args.data.requestId;
@@ -314,7 +314,7 @@ function processResourceFinish(
function processResourceReceivedData(
event: TimelineEvent,
timestamp: Milliseconds,
profilerData: ReactProfilerData,
profilerData: TimelineData,
state: ProcessorState,
) {
const requestId = event.args.data.requestId;
@@ -331,7 +331,7 @@ function processResourceReceivedData(
function processResourceReceiveResponse(
event: TimelineEvent,
timestamp: Milliseconds,
profilerData: ReactProfilerData,
profilerData: TimelineData,
state: ProcessorState,
) {
const requestId = event.args.data.requestId;
@@ -344,7 +344,7 @@ function processResourceReceiveResponse(
function processScreenshot(
event: TimelineEvent,
timestamp: Milliseconds,
profilerData: ReactProfilerData,
profilerData: TimelineData,
state: ProcessorState,
) {
const encodedSnapshot = event.args.snapshot; // Base 64 encoded
@@ -385,7 +385,7 @@ function processScreenshot(
function processResourceSendRequest(
event: TimelineEvent,
timestamp: Milliseconds,
profilerData: ReactProfilerData,
profilerData: TimelineData,
state: ProcessorState,
) {
const data = event.args.data;
@@ -428,7 +428,7 @@ function processResourceSendRequest(
function processTimelineEvent(
event: TimelineEvent,
/** Finalized profiler data up to `event`. May be mutated. */
currentProfilerData: ReactProfilerData,
currentProfilerData: TimelineData,
/** Intermediate processor state. May be mutated. */
state: ProcessorState,
) {
@@ -551,10 +551,7 @@ function processTimelineEvent(
timestamp: startTime,
type: 'thrown-error',
});
} // eslint-disable-line brace-style
// React Events - suspense
else if (name.startsWith('--suspense-suspend-')) {
} else if (name.startsWith('--suspense-suspend-')) {
const [
id,
componentName,
@@ -592,7 +589,6 @@ function processTimelineEvent(
phase: ((phase: any): Phase),
promiseName: promiseName || null,
resolution: 'unresolved',
resuspendTimestamps: null,
timestamp: startTime,
type: 'suspense',
warning: null,
@@ -611,16 +607,6 @@ function processTimelineEvent(
currentProfilerData.suspenseEvents.push(suspenseEvent);
state.unresolvedSuspenseEvents.set(id, suspenseEvent);
} else if (name.startsWith('--suspense-resuspend-')) {
const [id] = name.substr(21).split('-');
const suspenseEvent = state.unresolvedSuspenseEvents.get(id);
if (suspenseEvent != null) {
if (suspenseEvent.resuspendTimestamps === null) {
suspenseEvent.resuspendTimestamps = [startTime];
} else {
suspenseEvent.resuspendTimestamps.push(startTime);
}
}
} else if (name.startsWith('--suspense-resolved-')) {
const [id] = name.substr(20).split('-');
const suspenseEvent = state.unresolvedSuspenseEvents.get(id);
@@ -639,10 +625,7 @@ function processTimelineEvent(
suspenseEvent.duration = startTime - suspenseEvent.timestamp;
suspenseEvent.resolution = 'rejected';
}
} // eslint-disable-line brace-style
// React Measures - render
else if (name.startsWith('--render-start-')) {
} else if (name.startsWith('--render-start-')) {
if (state.nextRenderShouldGenerateNewBatchID) {
state.nextRenderShouldGenerateNewBatchID = false;
state.batchUID = ((state.uidCounter++: any): BatchUID);
@@ -701,24 +684,7 @@ function processTimelineEvent(
currentProfilerData,
state.measureStack,
);
} else if (name.startsWith('--render-cancel')) {
state.nextRenderShouldGenerateNewBatchID = true;
markWorkCompleted(
'render',
startTime,
currentProfilerData,
state.measureStack,
);
markWorkCompleted(
'render-idle',
startTime,
currentProfilerData,
state.measureStack,
);
} // eslint-disable-line brace-style
// React Measures - commits
else if (name.startsWith('--commit-start-')) {
} else if (name.startsWith('--commit-start-')) {
state.nextRenderShouldGenerateNewBatchID = true;
const [laneBitmaskString] = name.substr(15).split('-');
@@ -742,10 +708,7 @@ function processTimelineEvent(
currentProfilerData,
state.measureStack,
);
} // eslint-disable-line brace-style
// React Measures - layout effects
else if (name.startsWith('--layout-effects-start-')) {
} else if (name.startsWith('--layout-effects-start-')) {
const [laneBitmaskString] = name.substr(23).split('-');
markWorkStarted(
@@ -762,10 +725,7 @@ function processTimelineEvent(
currentProfilerData,
state.measureStack,
);
} // eslint-disable-line brace-style
// React Measures - passive effects
else if (name.startsWith('--passive-effects-start-')) {
} else if (name.startsWith('--passive-effects-start-')) {
const [laneBitmaskString] = name.substr(24).split('-');
markWorkStarted(
@@ -782,10 +742,7 @@ function processTimelineEvent(
currentProfilerData,
state.measureStack,
);
} // eslint-disable-line brace-style
// Internal module ranges
else if (name.startsWith('--react-internal-module-start-')) {
} else if (name.startsWith('--react-internal-module-start-')) {
const stackFrameStart = name.substr(30);
if (!state.internalModuleStackStringSet.has(stackFrameStart)) {
@@ -796,7 +753,7 @@ function processTimelineEvent(
state.internalModuleCurrentStackFrame = parsedStackFrameStart;
}
} else if (name.startsWith('--react-internal-module-stop-')) {
const stackFrameStop = name.substr(19);
const stackFrameStop = name.substr(29);
if (!state.internalModuleStackStringSet.has(stackFrameStop)) {
state.internalModuleStackStringSet.add(stackFrameStop);
@@ -825,10 +782,7 @@ function processTimelineEvent(
}
}
}
} // eslint-disable-line brace-style
// Other user timing marks/measures
else if (ph === 'R' || ph === 'n') {
} else if (ph === 'R' || ph === 'n') {
// User Timing mark
currentProfilerData.otherUserTimingMarks.push({
name,
@@ -841,10 +795,7 @@ function processTimelineEvent(
} else if (ph === 'i' || ph === 'I') {
// Instant events.
// Note that the capital "I" is a deprecated value that exists in Chrome Canary traces.
} // eslint-disable-line brace-style
// Unrecognized event
else {
} else {
throw new InvalidProfileError(
`Unrecognized event ${JSON.stringify(
event,
@@ -882,7 +833,7 @@ function assertCurrentComponentMeasureType(
function processReactComponentMeasure(
name: string,
startTime: Milliseconds,
currentProfilerData: ReactProfilerData,
currentProfilerData: TimelineData,
state: ProcessorState,
): void {
if (name.startsWith('--component-render-start-')) {
@@ -1048,7 +999,7 @@ function parseStackFrame(stackFrame: string): ErrorStackFrame | null {
export default async function preprocessData(
timeline: TimelineEvent[],
): Promise<ReactProfilerData> {
): Promise<TimelineData> {
const flamechart = preprocessFlamechart(timeline);
const laneToReactMeasureMap = new Map();
@@ -1056,7 +1007,7 @@ export default async function preprocessData(
laneToReactMeasureMap.set(lane, []);
}
const profilerData: ReactProfilerData = {
const profilerData: TimelineData = {
batchUIDToMeasuresMap: new Map(),
componentMeasures: [],
duration: 0,
+5 -8
View File
@@ -8,7 +8,7 @@
*/
import type {Wakeable} from 'shared/ReactTypes';
import type {ReactProfilerData} from './types';
import type {TimelineData} from './types';
import {importFile as importFileWorker} from './import-worker';
@@ -36,10 +36,7 @@ type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
// This is intentionally a module-level Map, rather than a React-managed one.
// Otherwise, refreshing the inspected element cache would also clear this cache.
// Profiler file contents are static anyway.
const fileNameToProfilerDataMap: Map<
string,
Record<ReactProfilerData>,
> = new Map();
const fileNameToProfilerDataMap: Map<string, Record<TimelineData>> = new Map();
function readRecord<T>(record: Record<T>): ResolvedRecord<T> | RejectedRecord {
if (record.status === Resolved) {
@@ -53,7 +50,7 @@ function readRecord<T>(record: Record<T>): ResolvedRecord<T> | RejectedRecord {
}
}
export function importFile(file: File): ReactProfilerData | Error {
export function importFile(file: File): TimelineData | Error {
const fileName = file.name;
let record = fileNameToProfilerDataMap.get(fileName);
@@ -74,7 +71,7 @@ export function importFile(file: File): ReactProfilerData | Error {
callbacks.clear();
};
const newRecord: Record<ReactProfilerData> = (record = {
const newRecord: Record<TimelineData> = (record = {
status: Pending,
value: wakeable,
});
@@ -82,7 +79,7 @@ export function importFile(file: File): ReactProfilerData | Error {
importFileWorker(file).then(data => {
switch (data.status) {
case 'SUCCESS':
const resolvedRecord = ((newRecord: any): ResolvedRecord<ReactProfilerData>);
const resolvedRecord = ((newRecord: any): ResolvedRecord<TimelineData>);
resolvedRecord.status = Resolved;
resolvedRecord.value = data.processedData;
break;
+26 -3
View File
@@ -68,7 +68,6 @@ export type SuspenseEvent = {|
+phase: Phase | null,
promiseName: string | null,
resolution: 'rejected' | 'resolved' | 'unresolved',
resuspendTimestamps: Array<number> | null,
+type: 'suspense',
|};
@@ -196,13 +195,15 @@ export type InternalModuleSourceToRanges = Map<
Array<[ErrorStackFrame, ErrorStackFrame]>,
>;
export type ReactProfilerData = {|
export type LaneToLabelMap = Map<ReactLane, string>;
export type TimelineData = {|
batchUIDToMeasuresMap: Map<BatchUID, ReactMeasure[]>,
componentMeasures: ReactComponentMeasure[],
duration: number,
flamechart: Flamechart,
internalModuleSourceToRanges: InternalModuleSourceToRanges,
laneToLabelMap: Map<ReactLane, string>,
laneToLabelMap: LaneToLabelMap,
laneToReactMeasureMap: Map<ReactLane, ReactMeasure[]>,
nativeEvents: NativeEvent[],
networkMeasures: NetworkMeasure[],
@@ -216,6 +217,28 @@ export type ReactProfilerData = {|
thrownErrors: ThrownError[],
|};
export type TimelineDataExport = {|
batchUIDToMeasuresKeyValueArray: Array<[BatchUID, ReactMeasure[]]>,
componentMeasures: ReactComponentMeasure[],
duration: number,
flamechart: Flamechart,
internalModuleSourceToRanges: Array<
[string, Array<[ErrorStackFrame, ErrorStackFrame]>],
>,
laneToLabelKeyValueArray: Array<[ReactLane, string]>,
laneToReactMeasureKeyValueArray: Array<[ReactLane, ReactMeasure[]]>,
nativeEvents: NativeEvent[],
networkMeasures: NetworkMeasure[],
otherUserTimingMarks: UserTimingMark[],
reactVersion: string | null,
schedulingEvents: SchedulingEvent[],
snapshots: Snapshot[],
snapshotHeight: number,
startTime: number,
suspenseEvents: SuspenseEvent[],
thrownErrors: ThrownError[],
|};
export type ReactHoverContextInfo = {|
componentMeasure: ReactComponentMeasure | null,
flamechartStackFrame: FlamechartStackFrame | null,
@@ -13,12 +13,12 @@ import type {
BatchUID,
Milliseconds,
ReactMeasure,
ReactProfilerData,
TimelineData,
} from '../types';
function unmemoizedGetBatchRange(
batchUID: BatchUID,
data: ReactProfilerData,
data: TimelineData,
minStartTime?: number = 0,
): [Milliseconds, Milliseconds] {
const measures = data.batchUIDToMeasuresMap.get(batchUID);
+6
View File
@@ -70,9 +70,15 @@ module.exports = Object.assign({}, baseConfig, {
require.resolve(
'../../packages/react-devtools-shared/src/__tests__/__serializers__/inspectedElementSerializer.js'
),
require.resolve(
'../../packages/react-devtools-shared/src/__tests__/__serializers__/profilingSerializer.js'
),
require.resolve(
'../../packages/react-devtools-shared/src/__tests__/__serializers__/storeSerializer.js'
),
require.resolve(
'../../packages/react-devtools-shared/src/__tests__/__serializers__/timelineDataSerializer.js'
),
require.resolve(
'../../packages/react-devtools-shared/src/__tests__/__serializers__/treeContextStateSerializer.js'
),