diff --git a/packages/react-devtools-extensions/webpack.backend.js b/packages/react-devtools-extensions/webpack.backend.js index 80198868f1876..542a1a95205a2 100644 --- a/packages/react-devtools-extensions/webpack.backend.js +++ b/packages/react-devtools-extensions/webpack.backend.js @@ -69,6 +69,7 @@ module.exports = { new DefinePlugin({ __DEV__: true, __PROFILE__: false, + __EXPERIMENTAL__: true, __DEV____DEV__: true, 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, diff --git a/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js b/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js index 107192a4770a9..66dfaaeea2804 100644 --- a/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js +++ b/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js @@ -1257,6 +1257,10 @@ describe('Timeline profiler', () => { Object { "componentName": "Example", "lanes": "0b0000000000000000000000000000100", + "parents": Array [ + 2, + 1, + ], "timestamp": 10, "type": "schedule-state-update", "warning": null, @@ -1264,6 +1268,10 @@ describe('Timeline profiler', () => { Object { "componentName": "Example", "lanes": "0b0000000000000000000000001000000", + "parents": Array [ + 2, + 1, + ], "timestamp": 10, "type": "schedule-state-update", "warning": null, @@ -1271,6 +1279,10 @@ describe('Timeline profiler', () => { Object { "componentName": "Example", "lanes": "0b0000000000000000000000001000000", + "parents": Array [ + 2, + 1, + ], "timestamp": 10, "type": "schedule-state-update", "warning": null, @@ -1278,6 +1290,10 @@ describe('Timeline profiler', () => { Object { "componentName": "Example", "lanes": "0b0000000000000000000000000010000", + "parents": Array [ + 2, + 1, + ], "timestamp": 10, "type": "schedule-state-update", "warning": null, @@ -1615,6 +1631,10 @@ describe('Timeline profiler', () => { Object { "componentName": "Example", "lanes": "0b0000000000000000000000000000001", + "parents": Array [ + 1, + 2, + ], "timestamp": 20, "type": "schedule-state-update", "warning": null, @@ -1742,6 +1762,10 @@ describe('Timeline profiler', () => { Object { "componentName": "Example", "lanes": "0b0000000000000000000000000010000", + "parents": Array [ + 1, + 2, + ], "timestamp": 10, "type": "schedule-state-update", "warning": null, @@ -1873,6 +1897,10 @@ describe('Timeline profiler', () => { Object { "componentName": "Example", "lanes": "0b0000000000000000000000000000001", + "parents": Array [ + 1, + 2, + ], "timestamp": 21, "type": "schedule-state-update", "warning": null, @@ -1935,6 +1963,10 @@ describe('Timeline profiler', () => { Object { "componentName": "Example", "lanes": "0b0000000000000000000000000010000", + "parents": Array [ + 2, + 1, + ], "timestamp": 21, "type": "schedule-state-update", "warning": null, @@ -1983,6 +2015,10 @@ describe('Timeline profiler', () => { Object { "componentName": "Example", "lanes": "0b0000000000000000000000000010000", + "parents": Array [ + 1, + 2, + ], "timestamp": 20, "type": "schedule-state-update", "warning": null, @@ -2066,6 +2102,10 @@ describe('Timeline profiler', () => { Object { "componentName": "ErrorBoundary", "lanes": "0b0000000000000000000000000000001", + "parents": Array [ + 1, + 2, + ], "timestamp": 20, "type": "schedule-state-update", "warning": null, @@ -2178,6 +2218,10 @@ describe('Timeline profiler', () => { Object { "componentName": "ErrorBoundary", "lanes": "0b0000000000000000000000000000001", + "parents": Array [ + 1, + 2, + ], "timestamp": 30, "type": "schedule-state-update", "warning": null, diff --git a/packages/react-devtools-shared/src/__tests__/preprocessData-test.js b/packages/react-devtools-shared/src/__tests__/preprocessData-test.js index 4257c925ef33d..6a5315c96bd8e 100644 --- a/packages/react-devtools-shared/src/__tests__/preprocessData-test.js +++ b/packages/react-devtools-shared/src/__tests__/preprocessData-test.js @@ -325,90 +325,91 @@ describe('Timeline profiler', () => { randomSample, ]); expect(data).toMatchInlineSnapshot(` - Object { - "batchUIDToMeasuresMap": Map {}, - "componentMeasures": Array [], - "duration": 0.005, - "flamechart": Array [], - "internalModuleSourceToRanges": Map {}, - "laneToLabelMap": Map { - 0 => "Sync", - 1 => "InputContinuousHydration", - 2 => "InputContinuous", - 3 => "DefaultHydration", - 4 => "Default", - 5 => "TransitionHydration", - 6 => "Transition", - 7 => "Transition", - 8 => "Transition", - 9 => "Transition", - 10 => "Transition", - 11 => "Transition", - 12 => "Transition", - 13 => "Transition", - 14 => "Transition", - 15 => "Transition", - 16 => "Transition", - 17 => "Transition", - 18 => "Transition", - 19 => "Transition", - 20 => "Transition", - 21 => "Transition", - 22 => "Retry", - 23 => "Retry", - 24 => "Retry", - 25 => "Retry", - 26 => "Retry", - 27 => "SelectiveHydration", - 28 => "IdleHydration", - 29 => "Idle", - 30 => "Offscreen", - }, - "laneToReactMeasureMap": Map { - 0 => Array [], - 1 => Array [], - 2 => Array [], - 3 => Array [], - 4 => Array [], - 5 => Array [], - 6 => Array [], - 7 => Array [], - 8 => Array [], - 9 => Array [], - 10 => Array [], - 11 => Array [], - 12 => Array [], - 13 => Array [], - 14 => Array [], - 15 => Array [], - 16 => Array [], - 17 => Array [], - 18 => Array [], - 19 => Array [], - 20 => Array [], - 21 => Array [], - 22 => Array [], - 23 => Array [], - 24 => Array [], - 25 => Array [], - 26 => Array [], - 27 => Array [], - 28 => Array [], - 29 => Array [], - 30 => Array [], - }, - "nativeEvents": Array [], - "networkMeasures": Array [], - "otherUserTimingMarks": Array [], - "reactVersion": "", - "schedulingEvents": Array [], - "snapshotHeight": 0, - "snapshots": Array [], - "startTime": 1, - "suspenseEvents": Array [], - "thrownErrors": Array [], - } - `); + Object { + "batchUIDToMeasuresMap": Map {}, + "componentDisplayNames": Map {}, + "componentMeasures": Array [], + "duration": 0.005, + "flamechart": Array [], + "internalModuleSourceToRanges": Map {}, + "laneToLabelMap": Map { + 0 => "Sync", + 1 => "InputContinuousHydration", + 2 => "InputContinuous", + 3 => "DefaultHydration", + 4 => "Default", + 5 => "TransitionHydration", + 6 => "Transition", + 7 => "Transition", + 8 => "Transition", + 9 => "Transition", + 10 => "Transition", + 11 => "Transition", + 12 => "Transition", + 13 => "Transition", + 14 => "Transition", + 15 => "Transition", + 16 => "Transition", + 17 => "Transition", + 18 => "Transition", + 19 => "Transition", + 20 => "Transition", + 21 => "Transition", + 22 => "Retry", + 23 => "Retry", + 24 => "Retry", + 25 => "Retry", + 26 => "Retry", + 27 => "SelectiveHydration", + 28 => "IdleHydration", + 29 => "Idle", + 30 => "Offscreen", + }, + "laneToReactMeasureMap": Map { + 0 => Array [], + 1 => Array [], + 2 => Array [], + 3 => Array [], + 4 => Array [], + 5 => Array [], + 6 => Array [], + 7 => Array [], + 8 => Array [], + 9 => Array [], + 10 => Array [], + 11 => Array [], + 12 => Array [], + 13 => Array [], + 14 => Array [], + 15 => Array [], + 16 => Array [], + 17 => Array [], + 18 => Array [], + 19 => Array [], + 20 => Array [], + 21 => Array [], + 22 => Array [], + 23 => Array [], + 24 => Array [], + 25 => Array [], + 26 => Array [], + 27 => Array [], + 28 => Array [], + 29 => Array [], + 30 => Array [], + }, + "nativeEvents": Array [], + "networkMeasures": Array [], + "otherUserTimingMarks": Array [], + "reactVersion": "", + "schedulingEvents": Array [], + "snapshotHeight": 0, + "snapshots": Array [], + "startTime": 1, + "suspenseEvents": Array [], + "thrownErrors": Array [], + } + `); }); // @reactVersion >= 18.0 @@ -487,6 +488,7 @@ describe('Timeline profiler', () => { }, ], }, + "componentDisplayNames": Map {}, "componentMeasures": Array [], "duration": 0.011, "flamechart": Array [], @@ -657,6 +659,7 @@ describe('Timeline profiler', () => { }, ], }, + "componentDisplayNames": Map {}, "componentMeasures": Array [], "duration": 0.016, "flamechart": Array [], @@ -905,6 +908,7 @@ describe('Timeline profiler', () => { }, ], }, + "componentDisplayNames": Map {}, "componentMeasures": Array [ Object { "componentName": "App", @@ -1116,6 +1120,7 @@ describe('Timeline profiler', () => { Object { "componentName": "App", "lanes": "0b0000000000000000000000000000100", + "parents": null, "timestamp": 0.021, "type": "schedule-state-update", "warning": null, @@ -1991,6 +1996,7 @@ describe('Timeline profiler', () => { }, ], }, + "componentDisplayNames": Map {}, "componentMeasures": Array [], "duration": 20, "flamechart": Array [], @@ -2222,6 +2228,10 @@ describe('Timeline profiler', () => { }, ], }, + "componentDisplayNames": Map { + 2 => "App", + 1 => "createRoot()", + }, "componentMeasures": Array [ Object { "componentName": "App", @@ -2416,6 +2426,10 @@ describe('Timeline profiler', () => { Object { "componentName": "App", "lanes": "0b0000000000000000000000000010000", + "parents": Array [ + 2, + 1, + ], "timestamp": 10, "type": "schedule-state-update", "warning": null, diff --git a/packages/react-devtools-shared/src/backend/profilingHooks.js b/packages/react-devtools-shared/src/backend/profilingHooks.js index c02d08ca92320..b98597f133429 100644 --- a/packages/react-devtools-shared/src/backend/profilingHooks.js +++ b/packages/react-devtools-shared/src/backend/profilingHooks.js @@ -20,8 +20,8 @@ import type { ReactComponentMeasure, ReactMeasure, ReactMeasureType, - TimelineData, SuspenseEvent, + TimelineData, } from 'react-devtools-timeline/src/types'; import isArray from 'shared/isArray'; @@ -96,11 +96,15 @@ type Response = {| export function createProfilingHooks({ getDisplayNameForFiber, + getDisplayNameForFiberID, + getFiberParentIDs, getIsProfiling, getLaneLabelMap, reactVersion, }: {| getDisplayNameForFiber: (fiber: Fiber) => string | null, + getDisplayNameForFiberID: (id: number) => string | null, + getFiberParentIDs: (fiber: Fiber) => number[], getIsProfiling: () => boolean, getLaneLabelMap?: () => Map | null, reactVersion: string, @@ -108,6 +112,7 @@ export function createProfilingHooks({ let currentBatchUID: BatchUID = 0; let currentReactComponentMeasure: ReactComponentMeasure | null = null; let currentReactMeasuresStack: Array = []; + let componentIDs: Set | null = null; let currentTimelineData: TimelineData | null = null; let isProfiling: boolean = false; let nextRenderShouldStartNewBatch: boolean = false; @@ -781,8 +786,13 @@ export function createProfilingHooks({ if (isProfiling) { // TODO (timeline) Record and cache component stack if (currentTimelineData) { + const parents = getFiberParentIDs(fiber); + if (componentIDs != null) { + parents.forEach(id => componentIDs && componentIDs.add(id)); + } currentTimelineData.schedulingEvents.push({ componentName, + parents, lanes: laneToLanesArray(lane), timestamp: getRelativeTime(), type: 'schedule-state-update', @@ -828,6 +838,10 @@ export function createProfilingHooks({ lane *= 2; } + // Components we need to track to display names + link to source in the + // UI + componentIDs = new Set(); + currentBatchUID = 0; currentReactComponentMeasure = null; currentReactMeasuresStack = []; @@ -835,6 +849,7 @@ export function createProfilingHooks({ // Session wide metadata; only collected once. internalModuleSourceToRanges, laneToLabelMap: laneToLabelMap || new Map(), + componentDisplayNames: new Map(), reactVersion, // Data logged by React during profiling session. @@ -858,6 +873,13 @@ export function createProfilingHooks({ snapshotHeight: 0, }; nextRenderShouldStartNewBatch = true; + } else if (componentIDs != null && currentTimelineData != null) { + currentTimelineData.componentDisplayNames = new Map( + Array.from(componentIDs).map(id => [ + id, + getDisplayNameForFiberID(id) || 'Unknown', + ]), + ); } } } diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 5f9033af9903c..54f2a7ca587d3 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -653,11 +653,21 @@ export function attach( }; } + function getFiberParentIDs(fiber: Fiber): number[] { + const ids = []; + for (let ptr = fiber; ptr; ptr = ptr.return) { + ids.push(getOrGenerateFiberID(ptr)); + } + return ids; + } + let getTimelineData: null | GetTimelineData = null; let toggleProfilingStatus: null | ToggleProfilingStatus = null; if (typeof injectProfilingHooks === 'function') { const response = createProfilingHooks({ getDisplayNameForFiber, + getDisplayNameForFiberID, + getFiberParentIDs, getIsProfiling: () => isProfiling, getLaneLabelMap, reactVersion: version, @@ -4050,6 +4060,7 @@ export function attach( if (currentTimelineData) { const { batchUIDToMeasuresMap, + componentDisplayNames, internalModuleSourceToRanges, laneToLabelMap, laneToReactMeasureMap, @@ -4066,6 +4077,9 @@ export function attach( batchUIDToMeasuresKeyValueArray: Array.from( batchUIDToMeasuresMap.entries(), ), + componentDisplayNamesValueArray: Array.from( + componentDisplayNames.entries(), + ), internalModuleSourceToRanges: Array.from( internalModuleSourceToRanges.entries(), ), diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 81ed15b04ae78..3f44f4528b897 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -490,3 +490,13 @@ export type DevToolsHook = { ... }; + +export type ReactFiberMetadata = { + displayName: string, + source?: DebugSource, +}; + +export type DebugSource = {| + ...Source, + columnNumber: number, +|}; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js index f9f98fae656f3..00a58a0ca1e6d 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js @@ -17,6 +17,7 @@ import CommitFlamegraph from './CommitFlamegraph'; import CommitRanked from './CommitRanked'; import RootSelector from './RootSelector'; import {Timeline} from 'react-devtools-timeline/src/Timeline'; +import SidebarEventInfo from './SidebarEventInfo'; import RecordToggle from './RecordToggle'; import ReloadAndProfileButton from './ReloadAndProfileButton'; import ProfilingImportExportButtons from './ProfilingImportExportButtons'; @@ -102,6 +103,9 @@ function Profiler(_: {||}) { } } break; + case 'timeline': + sidebar = ; + break; default: break; } @@ -145,9 +149,7 @@ function Profiler(_: {||}) { - {isLegacyProfilerSelected && ( -
{sidebar}
- )} +
{sidebar}
diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.css b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.css new file mode 100644 index 0000000000000..6ec289d12549a --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.css @@ -0,0 +1,67 @@ +.Toolbar { + height: 2.25rem; + padding: 0 0.5rem; + flex: 0 0 auto; + display: flex; + align-items: center; + border-bottom: 1px solid var(--color-border); +} + +.Content { + padding: 0.5rem; + user-select: none; + overflow: auto; +} + +.List { + list-style: none; + margin: 0; + padding: 0; +} + +.ListItem { + margin: 0; +} + +.Label { + display: flex; + justify-content: space-between; + + font-weight: bold; +} + +[data-source="true"]:hover .Label > .Button { + background-color: var(--color-background-hover); +} + +.Value { + font-family: var(--font-family-monospace); + font-size: var(--font-size-monospace-normal); +} + +.NothingSelected { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--color-dim); +} + +.Button { + display: flex; + flex: 1; + + max-width: 95%; + overflow: hidden; + text-overflow: ellipsis; + + cursor: pointer; +} + +.Button > span { + display: block; + text-align: left; +} + +.Source { +} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.js b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.js new file mode 100644 index 0000000000000..90762e8a6e9f6 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.js @@ -0,0 +1,102 @@ +/** + * 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 {isStateUpdateEvent} from 'react-devtools-timeline/src/utils/flow'; +import Button from '../Button'; +import ButtonIcon from '../ButtonIcon'; +import ViewElementSourceContext from '../Components/ViewElementSourceContext'; +import {StoreContext} from '../context'; +import {useContext, useMemo} from 'react'; +import {ProfilerContext} from './ProfilerContext'; + +import styles from './SidebarEventInfo.css'; + +export type Props = {||}; + +export default function SidebarEventInfo(_: Props) { + const {profilingData, selectedCommitIndex} = useContext(ProfilerContext); + const {viewElementSourceFunction} = useContext(ViewElementSourceContext); + const store = useContext(StoreContext); + + const {parents, componentDisplayNames} = useMemo(() => { + if ( + selectedCommitIndex == null || + profilingData == null || + profilingData.timelineData.length === 0 + ) { + return {}; + } + const { + schedulingEvents, + // eslint-disable-next-line no-shadow + componentDisplayNames, + } = profilingData.timelineData[0]; + + const event = schedulingEvents[selectedCommitIndex]; + if (!isStateUpdateEvent(event)) { + return {}; + } + + // eslint-disable-next-line no-shadow + let parents = null; + if (event.parents) { + parents = event.parents.filter(id => componentDisplayNames.has(id)); + } + + return { + parents, + componentDisplayNames, + }; + }, [profilingData, selectedCommitIndex]); + + const canInspect = (id: number) => + !!(viewElementSourceFunction && store.getElementByID(id)); + + let components; + if (parents) { + components = parents.map((id, index) => { + let displayName; + if (componentDisplayNames) { + displayName = componentDisplayNames.get(id); + } + + const hasSource = canInspect(id); + + const onClick = () => { + const element = store.getElementByID(id); + if (hasSource && viewElementSourceFunction && element) { + viewElementSourceFunction(id, (element: any)); + } + }; + + return ( +
  • + +
  • + ); + }); + } + + return ( + <> +
    Event Component Tree
    +
    +
      {components}
    +
    + + ); +} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/utils.js b/packages/react-devtools-shared/src/devtools/views/Profiler/utils.js index 69134bf956d0d..579a8f852089c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/utils.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/utils.js @@ -52,9 +52,11 @@ export function prepareProfilingDataFrontendFromBackendAndStore( if (timelineData != null) { const { batchUIDToMeasuresKeyValueArray, + componentDisplayNamesValueArray, internalModuleSourceToRanges, laneToLabelKeyValueArray, laneToReactMeasureKeyValueArray, + schedulingEvents, ...rest } = timelineData; @@ -64,9 +66,11 @@ export function prepareProfilingDataFrontendFromBackendAndStore( // 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), + componentDisplayNames: new Map(componentDisplayNamesValueArray), internalModuleSourceToRanges: new Map(internalModuleSourceToRanges), laneToLabelMap: new Map(laneToLabelKeyValueArray), laneToReactMeasureMap: new Map(laneToReactMeasureKeyValueArray), + schedulingEvents, }); } @@ -155,6 +159,7 @@ export function prepareProfilingDataFrontendFromExport( ? profilingDataExport.timelineData.map( ({ batchUIDToMeasuresKeyValueArray, + componentDisplayNamesValueArray, componentMeasures, duration, flamechart, @@ -175,6 +180,7 @@ export function prepareProfilingDataFrontendFromExport( // 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), + componentDisplayNames: new Map(componentDisplayNamesValueArray), componentMeasures, duration, flamechart, @@ -253,6 +259,7 @@ export function prepareProfilingDataExport( const timelineData: Array = profilingDataFrontend.timelineData.map( ({ batchUIDToMeasuresMap, + componentDisplayNames, componentMeasures, duration, flamechart, @@ -275,6 +282,9 @@ export function prepareProfilingDataExport( batchUIDToMeasuresKeyValueArray: Array.from( batchUIDToMeasuresMap.entries(), ), + componentDisplayNamesValueArray: Array.from( + componentDisplayNames.entries(), + ), componentMeasures: componentMeasures, duration, flamechart, diff --git a/packages/react-devtools-timeline/src/CanvasPage.js b/packages/react-devtools-timeline/src/CanvasPage.js index 8ef42b1dd37d9..116d7b28567df 100644 --- a/packages/react-devtools-timeline/src/CanvasPage.js +++ b/packages/react-devtools-timeline/src/CanvasPage.js @@ -63,6 +63,7 @@ import useContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/useCo import {getBatchRange} from './utils/getBatchRange'; import {MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL} from './view-base/constants'; import {TimelineSearchContext} from './TimelineSearchContext'; +import {ProfilerContext} from 'react-devtools-shared/src/devtools/views/Profiler/ProfilerContext'; import styles from './CanvasPage.css'; @@ -528,6 +529,8 @@ function AutoSizedCanvas({ ref: canvasRef, }); + const {selectCommitIndex} = useContext(ProfilerContext); + useEffect(() => { const {current: userTimingMarksView} = userTimingMarksViewRef; if (userTimingMarksView) { @@ -563,6 +566,9 @@ function AutoSizedCanvas({ }); } }; + schedulingEventsView.onClick = (schedulingEvent, eventIndex) => { + selectCommitIndex(eventIndex); + }; } const {current: suspenseEventsView} = suspenseEventsViewRef; diff --git a/packages/react-devtools-timeline/src/EventTooltip.css b/packages/react-devtools-timeline/src/EventTooltip.css index bf3862892bd7d..99626d6c9ade6 100644 --- a/packages/react-devtools-timeline/src/EventTooltip.css +++ b/packages/react-devtools-timeline/src/EventTooltip.css @@ -86,4 +86,4 @@ .DimText { color: var(--color-dim); -} \ No newline at end of file +} diff --git a/packages/react-devtools-timeline/src/content-views/SchedulingEventsView.js b/packages/react-devtools-timeline/src/content-views/SchedulingEventsView.js index 3798d261e0b1b..4c35ee6828943 100644 --- a/packages/react-devtools-timeline/src/content-views/SchedulingEventsView.js +++ b/packages/react-devtools-timeline/src/content-views/SchedulingEventsView.js @@ -9,6 +9,7 @@ import type {SchedulingEvent, TimelineData} from '../types'; import type { + ClickInteraction, Interaction, MouseMoveInteraction, Rect, @@ -45,6 +46,9 @@ export class SchedulingEventsView extends View { _hoveredEvent: SchedulingEvent | null = null; onHover: ((event: SchedulingEvent | null) => void) | null = null; + onClick: + | ((event: SchedulingEvent | null, eventIndex: number | null) => void) + | null = null; constructor(surface: Surface, frame: Rect, profilerData: TimelineData) { super(surface, frame); @@ -243,7 +247,7 @@ export class SchedulingEventsView extends View { timestamp - eventTimestampAllowance <= hoverTimestamp && hoverTimestamp <= timestamp + eventTimestampAllowance ) { - this.currentCursor = 'context-menu'; + this.currentCursor = 'pointer'; viewRefs.hoveredView = this; onHover(event); return; @@ -253,11 +257,32 @@ export class SchedulingEventsView extends View { onHover(null); } + /** + * @private + */ + _handleClick(interaction: ClickInteraction) { + const {onClick} = this; + if (onClick) { + const { + _profilerData: {schedulingEvents}, + } = this; + const eventIndex = schedulingEvents.findIndex( + event => event === this._hoveredEvent, + ); + // onHover is going to take care of all the difficult logic here of + // figuring out which event when they're proximity is close. + onClick(this._hoveredEvent, eventIndex >= 0 ? eventIndex : null); + } + } + handleInteraction(interaction: Interaction, viewRefs: ViewRefs) { switch (interaction.type) { case 'mousemove': this._handleMouseMove(interaction, viewRefs); break; + case 'click': + this._handleClick(interaction); + break; } } } diff --git a/packages/react-devtools-timeline/src/import-worker/preprocessData.js b/packages/react-devtools-timeline/src/import-worker/preprocessData.js index 110b4e8923fb2..9b477b5754bd9 100644 --- a/packages/react-devtools-timeline/src/import-worker/preprocessData.js +++ b/packages/react-devtools-timeline/src/import-worker/preprocessData.js @@ -526,9 +526,10 @@ function processTimelineEvent( } else if (name.startsWith('--schedule-state-update-')) { const [laneBitmaskString, componentName] = name.substr(24).split('-'); - const stateUpdateEvent = { + const stateUpdateEvent: SchedulingEvent = { type: 'schedule-state-update', lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), + parents: null, componentName, timestamp: startTime, warning: null, @@ -1009,6 +1010,7 @@ export default async function preprocessData( const profilerData: TimelineData = { batchUIDToMeasuresMap: new Map(), + componentDisplayNames: new Map(), componentMeasures: [], duration: 0, flamechart, diff --git a/packages/react-devtools-timeline/src/types.js b/packages/react-devtools-timeline/src/types.js index 9469523a1e2c2..8d3f8e9fa20d0 100644 --- a/packages/react-devtools-timeline/src/types.js +++ b/packages/react-devtools-timeline/src/types.js @@ -51,6 +51,7 @@ export type ReactScheduleRenderEvent = {| |}; export type ReactScheduleStateUpdateEvent = {| ...BaseReactScheduleEvent, + +parents: number[] | null, +type: 'schedule-state-update', |}; export type ReactScheduleForceUpdateEvent = {| @@ -199,6 +200,7 @@ export type LaneToLabelMap = Map; export type TimelineData = {| batchUIDToMeasuresMap: Map, + componentDisplayNames: Map, componentMeasures: ReactComponentMeasure[], duration: number, flamechart: Flamechart, @@ -219,6 +221,7 @@ export type TimelineData = {| export type TimelineDataExport = {| batchUIDToMeasuresKeyValueArray: Array<[BatchUID, ReactMeasure[]]>, + componentDisplayNamesValueArray: Array<[number, string]>, componentMeasures: ReactComponentMeasure[], duration: number, flamechart: Flamechart, diff --git a/packages/react-devtools-timeline/src/utils/flow.js b/packages/react-devtools-timeline/src/utils/flow.js new file mode 100644 index 0000000000000..0754e23a001f9 --- /dev/null +++ b/packages/react-devtools-timeline/src/utils/flow.js @@ -0,0 +1,13 @@ +/** + * 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 type {SchedulingEvent} from '../types'; + +export function isStateUpdateEvent(event: SchedulingEvent): boolean %checks { + return event.type === 'schedule-state-update'; +}