mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
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:
+2295
-1002
File diff suppressed because it is too large
Load Diff
+25
@@ -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,
|
||||
});
|
||||
}
|
||||
+29
@@ -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),
|
||||
});
|
||||
}
|
||||
+3510
-7
File diff suppressed because it is too large
Load Diff
+2283
-1835
File diff suppressed because it is too large
Load Diff
@@ -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', () => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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".
|
||||
|
||||
@@ -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 = {|
|
||||
|
||||
@@ -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
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-13
@@ -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" />
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ function updateTree(
|
||||
|
||||
if (type === ElementTypeRoot) {
|
||||
i++; // isStrictModeCompliant
|
||||
i++; // supportsProfiling flag
|
||||
i++; // Profiling flag
|
||||
i++; // supportsStrictMode flag
|
||||
i++; // hasOwnerMetadata flag
|
||||
|
||||
|
||||
+35
@@ -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);
|
||||
|
||||
+3
-4
@@ -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);
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
+52
-49
@@ -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" />
|
||||
|
||||
+35
@@ -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>
|
||||
);
|
||||
}
|
||||
+24
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|};
|
||||
|
||||
|
||||
+3
-7
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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'
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user