mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Scheduling profiler UX changes (#21990)
This commit is contained in:
+187
-94
@@ -31,8 +31,9 @@ import {copy} from 'clipboard-js';
|
||||
import prettyMilliseconds from 'pretty-ms';
|
||||
|
||||
import {
|
||||
BackgroundColorView,
|
||||
HorizontalPanAndZoomView,
|
||||
ResizableSplitView,
|
||||
ResizableView,
|
||||
Surface,
|
||||
VerticalScrollView,
|
||||
View,
|
||||
@@ -45,8 +46,9 @@ import {
|
||||
import {
|
||||
FlamechartView,
|
||||
NativeEventsView,
|
||||
ReactEventsView,
|
||||
ReactMeasuresView,
|
||||
SchedulingEventsView,
|
||||
SuspenseEventsView,
|
||||
TimeAxisMarkersView,
|
||||
UserTimingMarksView,
|
||||
} from './content-views';
|
||||
@@ -128,7 +130,8 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
const surfaceRef = useRef(new Surface());
|
||||
const userTimingMarksViewRef = useRef(null);
|
||||
const nativeEventsViewRef = useRef(null);
|
||||
const reactEventsViewRef = useRef(null);
|
||||
const schedulingEventsViewRef = useRef(null);
|
||||
const suspenseEventsViewRef = useRef(null);
|
||||
const reactMeasuresViewRef = useRef(null);
|
||||
const flamechartViewRef = useRef(null);
|
||||
const syncedHorizontalPanAndZoomViewsRef = useRef<HorizontalPanAndZoomView[]>(
|
||||
@@ -152,21 +155,53 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
);
|
||||
};
|
||||
|
||||
// Top content
|
||||
function createViewHelper(
|
||||
view: View,
|
||||
resizeLabel: string = '',
|
||||
shouldScrollVertically: boolean = false,
|
||||
shouldResizeVertically: boolean = false,
|
||||
): View {
|
||||
let verticalScrollView = null;
|
||||
if (shouldScrollVertically) {
|
||||
verticalScrollView = new VerticalScrollView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
view,
|
||||
);
|
||||
}
|
||||
|
||||
const topContentStack = new View(
|
||||
surface,
|
||||
defaultFrame,
|
||||
verticallyStackedLayout,
|
||||
);
|
||||
const horizontalPanAndZoomView = new HorizontalPanAndZoomView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
verticalScrollView !== null ? verticalScrollView : view,
|
||||
data.duration,
|
||||
syncAllHorizontalPanAndZoomViewStates,
|
||||
);
|
||||
|
||||
syncedHorizontalPanAndZoomViewsRef.current.push(horizontalPanAndZoomView);
|
||||
|
||||
let viewToReturn = horizontalPanAndZoomView;
|
||||
if (shouldResizeVertically) {
|
||||
viewToReturn = new ResizableView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
horizontalPanAndZoomView,
|
||||
canvasRef,
|
||||
resizeLabel,
|
||||
);
|
||||
}
|
||||
|
||||
return viewToReturn;
|
||||
}
|
||||
|
||||
const axisMarkersView = new TimeAxisMarkersView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
data.duration,
|
||||
);
|
||||
topContentStack.addSubview(axisMarkersView);
|
||||
const axisMarkersViewWrapper = createViewHelper(axisMarkersView);
|
||||
|
||||
let userTimingMarksViewWrapper = null;
|
||||
if (data.otherUserTimingMarks.length > 0) {
|
||||
const userTimingMarksView = new UserTimingMarksView(
|
||||
surface,
|
||||
@@ -175,29 +210,38 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
data.duration,
|
||||
);
|
||||
userTimingMarksViewRef.current = userTimingMarksView;
|
||||
topContentStack.addSubview(userTimingMarksView);
|
||||
userTimingMarksViewWrapper = createViewHelper(userTimingMarksView);
|
||||
}
|
||||
|
||||
const nativeEventsView = new NativeEventsView(surface, defaultFrame, data);
|
||||
nativeEventsViewRef.current = nativeEventsView;
|
||||
topContentStack.addSubview(nativeEventsView);
|
||||
const nativeEventsViewWrapper = createViewHelper(
|
||||
nativeEventsView,
|
||||
'events',
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
const reactEventsView = new ReactEventsView(surface, defaultFrame, data);
|
||||
reactEventsViewRef.current = reactEventsView;
|
||||
topContentStack.addSubview(reactEventsView);
|
||||
|
||||
const topContentHorizontalPanAndZoomView = new HorizontalPanAndZoomView(
|
||||
const schedulingEventsView = new SchedulingEventsView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
topContentStack,
|
||||
data.duration,
|
||||
syncAllHorizontalPanAndZoomViewStates,
|
||||
);
|
||||
syncedHorizontalPanAndZoomViewsRef.current.push(
|
||||
topContentHorizontalPanAndZoomView,
|
||||
data,
|
||||
);
|
||||
schedulingEventsViewRef.current = schedulingEventsView;
|
||||
const schedulingEventsViewWrapper = createViewHelper(schedulingEventsView);
|
||||
|
||||
// Resizable content
|
||||
const suspenseEventsView = new SuspenseEventsView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
data,
|
||||
);
|
||||
suspenseEventsViewRef.current = suspenseEventsView;
|
||||
const suspenseEventsViewWrapper = createViewHelper(
|
||||
suspenseEventsView,
|
||||
'suspense',
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
const reactMeasuresView = new ReactMeasuresView(
|
||||
surface,
|
||||
@@ -205,20 +249,11 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
data,
|
||||
);
|
||||
reactMeasuresViewRef.current = reactMeasuresView;
|
||||
const reactMeasuresVerticalScrollView = new VerticalScrollView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
const reactMeasuresViewWrapper = createViewHelper(
|
||||
reactMeasuresView,
|
||||
);
|
||||
const reactMeasuresHorizontalPanAndZoomView = new HorizontalPanAndZoomView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
reactMeasuresVerticalScrollView,
|
||||
data.duration,
|
||||
syncAllHorizontalPanAndZoomViewStates,
|
||||
);
|
||||
syncedHorizontalPanAndZoomViewsRef.current.push(
|
||||
reactMeasuresHorizontalPanAndZoomView,
|
||||
'react',
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
const flamechartView = new FlamechartView(
|
||||
@@ -228,30 +263,15 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
data.duration,
|
||||
);
|
||||
flamechartViewRef.current = flamechartView;
|
||||
const flamechartVerticalScrollView = new VerticalScrollView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
const flamechartViewWrapper = createViewHelper(
|
||||
flamechartView,
|
||||
);
|
||||
const flamechartHorizontalPanAndZoomView = new HorizontalPanAndZoomView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
flamechartVerticalScrollView,
|
||||
data.duration,
|
||||
syncAllHorizontalPanAndZoomViewStates,
|
||||
);
|
||||
syncedHorizontalPanAndZoomViewsRef.current.push(
|
||||
flamechartHorizontalPanAndZoomView,
|
||||
);
|
||||
|
||||
const resizableContentStack = new ResizableSplitView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
reactMeasuresHorizontalPanAndZoomView,
|
||||
flamechartHorizontalPanAndZoomView,
|
||||
canvasRef,
|
||||
'flamechart',
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
// Root view contains all of the sub views defined above.
|
||||
// The order we add them below determines their vertical position.
|
||||
const rootView = new View(
|
||||
surface,
|
||||
defaultFrame,
|
||||
@@ -260,8 +280,18 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
lastViewTakesUpRemainingSpaceLayout,
|
||||
),
|
||||
);
|
||||
rootView.addSubview(topContentHorizontalPanAndZoomView);
|
||||
rootView.addSubview(resizableContentStack);
|
||||
rootView.addSubview(axisMarkersViewWrapper);
|
||||
if (userTimingMarksViewWrapper !== null) {
|
||||
rootView.addSubview(userTimingMarksViewWrapper);
|
||||
}
|
||||
rootView.addSubview(nativeEventsViewWrapper);
|
||||
rootView.addSubview(schedulingEventsViewWrapper);
|
||||
rootView.addSubview(suspenseEventsViewWrapper);
|
||||
rootView.addSubview(reactMeasuresViewWrapper);
|
||||
rootView.addSubview(flamechartViewWrapper);
|
||||
|
||||
// If subviews are less than the available height, fill remaining height with a solid color.
|
||||
rootView.addSubview(new BackgroundColorView(surface, defaultFrame));
|
||||
|
||||
surfaceRef.current.rootView = rootView;
|
||||
}, [data]);
|
||||
@@ -278,6 +308,39 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wheel events should always hide the current toolltip.
|
||||
switch (interaction.type) {
|
||||
case 'wheel-control':
|
||||
case 'wheel-meta':
|
||||
case 'wheel-plain':
|
||||
case 'wheel-shift':
|
||||
setHoveredEvent(prevHoverEvent => {
|
||||
if (prevHoverEvent === null) {
|
||||
return prevHoverEvent;
|
||||
} else if (
|
||||
prevHoverEvent.flamechartStackFrame !== null ||
|
||||
prevHoverEvent.measure !== null ||
|
||||
prevHoverEvent.nativeEvent !== null ||
|
||||
prevHoverEvent.schedulingEvent !== null ||
|
||||
prevHoverEvent.suspenseEvent !== null ||
|
||||
prevHoverEvent.userTimingMark !== null
|
||||
) {
|
||||
return {
|
||||
data: prevHoverEvent.data,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
schedulingEvent: null,
|
||||
suspenseEvent: null,
|
||||
userTimingMark: null,
|
||||
};
|
||||
} else {
|
||||
return prevHoverEvent;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
const surface = surfaceRef.current;
|
||||
surface.handleInteraction(interaction);
|
||||
|
||||
@@ -310,12 +373,13 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
userTimingMarksView.onHover = userTimingMark => {
|
||||
if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) {
|
||||
setHoveredEvent({
|
||||
userTimingMark,
|
||||
nativeEvent: null,
|
||||
reactEvent: null,
|
||||
data,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
data,
|
||||
nativeEvent: null,
|
||||
schedulingEvent: null,
|
||||
suspenseEvent: null,
|
||||
userTimingMark,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -326,28 +390,47 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
nativeEventsView.onHover = nativeEvent => {
|
||||
if (!hoveredEvent || hoveredEvent.nativeEvent !== nativeEvent) {
|
||||
setHoveredEvent({
|
||||
userTimingMark: null,
|
||||
nativeEvent,
|
||||
reactEvent: null,
|
||||
data,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
data,
|
||||
nativeEvent,
|
||||
schedulingEvent: null,
|
||||
suspenseEvent: null,
|
||||
userTimingMark: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const {current: reactEventsView} = reactEventsViewRef;
|
||||
if (reactEventsView) {
|
||||
reactEventsView.onHover = reactEvent => {
|
||||
if (!hoveredEvent || hoveredEvent.reactEvent !== reactEvent) {
|
||||
const {current: schedulingEventsView} = schedulingEventsViewRef;
|
||||
if (schedulingEventsView) {
|
||||
schedulingEventsView.onHover = schedulingEvent => {
|
||||
if (!hoveredEvent || hoveredEvent.schedulingEvent !== schedulingEvent) {
|
||||
setHoveredEvent({
|
||||
userTimingMark: null,
|
||||
nativeEvent: null,
|
||||
reactEvent,
|
||||
data,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
schedulingEvent,
|
||||
suspenseEvent: null,
|
||||
userTimingMark: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const {current: suspenseEventsView} = suspenseEventsViewRef;
|
||||
if (suspenseEventsView) {
|
||||
suspenseEventsView.onHover = suspenseEvent => {
|
||||
if (!hoveredEvent || hoveredEvent.suspenseEvent !== suspenseEvent) {
|
||||
setHoveredEvent({
|
||||
data,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
schedulingEvent: null,
|
||||
suspenseEvent,
|
||||
userTimingMark: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -358,12 +441,13 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
reactMeasuresView.onHover = measure => {
|
||||
if (!hoveredEvent || hoveredEvent.measure !== measure) {
|
||||
setHoveredEvent({
|
||||
userTimingMark: null,
|
||||
nativeEvent: null,
|
||||
reactEvent: null,
|
||||
data,
|
||||
flamechartStackFrame: null,
|
||||
measure,
|
||||
data,
|
||||
nativeEvent: null,
|
||||
schedulingEvent: null,
|
||||
suspenseEvent: null,
|
||||
userTimingMark: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -377,12 +461,13 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
hoveredEvent.flamechartStackFrame !== flamechartStackFrame
|
||||
) {
|
||||
setHoveredEvent({
|
||||
userTimingMark: null,
|
||||
nativeEvent: null,
|
||||
reactEvent: null,
|
||||
data,
|
||||
flamechartStackFrame,
|
||||
measure: null,
|
||||
data,
|
||||
nativeEvent: null,
|
||||
schedulingEvent: null,
|
||||
suspenseEvent: null,
|
||||
userTimingMark: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -407,10 +492,17 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const {current: reactEventsView} = reactEventsViewRef;
|
||||
if (reactEventsView) {
|
||||
reactEventsView.setHoveredEvent(
|
||||
hoveredEvent ? hoveredEvent.reactEvent : null,
|
||||
const {current: schedulingEventsView} = schedulingEventsViewRef;
|
||||
if (schedulingEventsView) {
|
||||
schedulingEventsView.setHoveredEvent(
|
||||
hoveredEvent ? hoveredEvent.schedulingEvent : null,
|
||||
);
|
||||
}
|
||||
|
||||
const {current: suspenseEventsView} = suspenseEventsViewRef;
|
||||
if (suspenseEventsView) {
|
||||
suspenseEventsView.setHoveredEvent(
|
||||
hoveredEvent ? hoveredEvent.suspenseEvent : null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -443,24 +535,25 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
reactEvent,
|
||||
flamechartStackFrame,
|
||||
measure,
|
||||
schedulingEvent,
|
||||
suspenseEvent,
|
||||
} = contextData.hoveredEvent;
|
||||
return (
|
||||
<Fragment>
|
||||
{reactEvent !== null && (
|
||||
{schedulingEvent !== null && (
|
||||
<ContextMenuItem
|
||||
onClick={() => copy(reactEvent.componentName)}
|
||||
onClick={() => copy(schedulingEvent.componentName)}
|
||||
title="Copy component name">
|
||||
Copy component name
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{reactEvent !== null && reactEvent.componentStack && (
|
||||
{suspenseEvent !== null && (
|
||||
<ContextMenuItem
|
||||
onClick={() => copy(reactEvent.componentStack)}
|
||||
title="Copy component stack">
|
||||
Copy component stack
|
||||
onClick={() => copy(suspenseEvent.componentName)}
|
||||
title="Copy component name">
|
||||
Copy component name
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{measure !== null && (
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
.Tooltip {
|
||||
position: fixed;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.TooltipSection,
|
||||
.TooltipWarningSection {
|
||||
display: block;
|
||||
border-radius: 0.125rem;
|
||||
max-width: 300px;
|
||||
padding: 0.25rem;
|
||||
@@ -12,11 +16,15 @@
|
||||
color: var(--color-tooltip-text);
|
||||
font-size: 11px;
|
||||
}
|
||||
.TooltipWarningSection {
|
||||
margin-top: 0.25rem;
|
||||
background-color: var(--color-warning-background);
|
||||
}
|
||||
|
||||
.Divider {
|
||||
height: 1px;
|
||||
background-color: #aaa;
|
||||
margin: 0.5rem 0;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.DetailsGrid {
|
||||
@@ -40,7 +48,6 @@
|
||||
|
||||
.FlamechartStackFrameName {
|
||||
word-break: break-word;
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
|
||||
.ComponentName {
|
||||
@@ -49,32 +56,20 @@
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.ComponentStack {
|
||||
overflow: hidden;
|
||||
max-width: 35em;
|
||||
max-height: 10em;
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.5;
|
||||
-webkit-mask-image: linear-gradient(
|
||||
180deg,
|
||||
var(--color-tooltip-background),
|
||||
var(--color-tooltip-background) 5em,
|
||||
transparent
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
180deg,
|
||||
var(--color-tooltip-background),
|
||||
var(--color-tooltip-background) 5em,
|
||||
transparent
|
||||
);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.ReactMeasureLabel {
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
|
||||
.UserTimingLabel {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.NativeEventName {
|
||||
font-weight: bold;
|
||||
word-break: break-word;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.InfoText,
|
||||
.WarningText {
|
||||
color: var(--color-warning-text-color);
|
||||
}
|
||||
+186
-158
@@ -11,18 +11,18 @@ import type {Point} from './view-base';
|
||||
import type {
|
||||
FlamechartStackFrame,
|
||||
NativeEvent,
|
||||
ReactEvent,
|
||||
ReactHoverContextInfo,
|
||||
ReactMeasure,
|
||||
ReactProfilerData,
|
||||
Return,
|
||||
SchedulingEvent,
|
||||
SuspenseEvent,
|
||||
UserTimingMark,
|
||||
} from './types';
|
||||
|
||||
import * as React from 'react';
|
||||
import {Fragment, useRef} from 'react';
|
||||
import prettyMilliseconds from 'pretty-ms';
|
||||
import {COLORS} from './content-views/constants';
|
||||
import {useRef} from 'react';
|
||||
import {formatDuration, formatTimestamp, trimString} from './utils/formatting';
|
||||
import {getBatchRange} from './utils/getBatchRange';
|
||||
import useSmartTooltip from './utils/useSmartTooltip';
|
||||
import styles from './EventTooltip.css';
|
||||
@@ -34,22 +34,7 @@ type Props = {|
|
||||
origin: Point,
|
||||
|};
|
||||
|
||||
function formatTimestamp(ms) {
|
||||
return ms.toLocaleString(undefined, {minimumFractionDigits: 2}) + 'ms';
|
||||
}
|
||||
|
||||
function formatDuration(ms) {
|
||||
return prettyMilliseconds(ms, {millisecondsDecimalDigits: 3});
|
||||
}
|
||||
|
||||
function trimmedString(string: string, length: number): string {
|
||||
if (string.length > length) {
|
||||
return `${string.substr(0, length - 1)}…`;
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
function getReactEventLabel(event: ReactEvent): string | null {
|
||||
function getSchedulingEventLabel(event: SchedulingEvent): string | null {
|
||||
switch (event.type) {
|
||||
case 'schedule-render':
|
||||
return 'render scheduled';
|
||||
@@ -57,30 +42,6 @@ function getReactEventLabel(event: ReactEvent): string | null {
|
||||
return 'state update scheduled';
|
||||
case 'schedule-force-update':
|
||||
return 'force update scheduled';
|
||||
case 'suspense-suspend':
|
||||
return 'suspended';
|
||||
case 'suspense-resolved':
|
||||
return 'suspense resolved';
|
||||
case 'suspense-rejected':
|
||||
return 'suspense rejected';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getReactEventColor(event: ReactEvent): string | null {
|
||||
switch (event.type) {
|
||||
case 'schedule-render':
|
||||
return COLORS.REACT_SCHEDULE_HOVER;
|
||||
case 'schedule-state-update':
|
||||
case 'schedule-force-update':
|
||||
return event.isCascading
|
||||
? COLORS.REACT_SCHEDULE_CASCADING_HOVER
|
||||
: COLORS.REACT_SCHEDULE_HOVER;
|
||||
case 'suspense-suspend':
|
||||
case 'suspense-resolved':
|
||||
case 'suspense-rejected':
|
||||
return COLORS.REACT_SUSPEND_HOVER;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -89,15 +50,15 @@ function getReactEventColor(event: ReactEvent): string | null {
|
||||
function getReactMeasureLabel(type): string | null {
|
||||
switch (type) {
|
||||
case 'commit':
|
||||
return 'commit';
|
||||
return 'react commit';
|
||||
case 'render-idle':
|
||||
return 'idle';
|
||||
return 'react idle';
|
||||
case 'render':
|
||||
return 'render';
|
||||
return 'react render';
|
||||
case 'layout-effects':
|
||||
return 'layout effects';
|
||||
return 'react layout effects';
|
||||
case 'passive-effects':
|
||||
return 'passive effects';
|
||||
return 'react passive effects';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -120,10 +81,11 @@ export default function EventTooltip({
|
||||
}
|
||||
|
||||
const {
|
||||
nativeEvent,
|
||||
reactEvent,
|
||||
measure,
|
||||
flamechartStackFrame,
|
||||
measure,
|
||||
nativeEvent,
|
||||
schedulingEvent,
|
||||
suspenseEvent,
|
||||
userTimingMark,
|
||||
} = hoveredEvent;
|
||||
|
||||
@@ -131,9 +93,19 @@ export default function EventTooltip({
|
||||
return (
|
||||
<TooltipNativeEvent nativeEvent={nativeEvent} tooltipRef={tooltipRef} />
|
||||
);
|
||||
} else if (reactEvent !== null) {
|
||||
} else if (schedulingEvent !== null) {
|
||||
return (
|
||||
<TooltipReactEvent reactEvent={reactEvent} tooltipRef={tooltipRef} />
|
||||
<TooltipSchedulingEvent
|
||||
schedulingEvent={schedulingEvent}
|
||||
tooltipRef={tooltipRef}
|
||||
/>
|
||||
);
|
||||
} else if (suspenseEvent !== null) {
|
||||
return (
|
||||
<TooltipSuspenseEvent
|
||||
suspenseEvent={suspenseEvent}
|
||||
tooltipRef={tooltipRef}
|
||||
/>
|
||||
);
|
||||
} else if (measure !== null) {
|
||||
return (
|
||||
@@ -158,16 +130,6 @@ export default function EventTooltip({
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatComponentStack(componentStack: string): string {
|
||||
const lines = componentStack.split('\n').map(line => line.trim());
|
||||
lines.shift();
|
||||
|
||||
if (lines.length > 5) {
|
||||
return lines.slice(0, 5).join('\n') + '\n...';
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
const TooltipFlamechartNode = ({
|
||||
stackFrame,
|
||||
tooltipRef,
|
||||
@@ -175,35 +137,25 @@ const TooltipFlamechartNode = ({
|
||||
stackFrame: FlamechartStackFrame,
|
||||
tooltipRef: Return<typeof useRef>,
|
||||
}) => {
|
||||
const {
|
||||
name,
|
||||
timestamp,
|
||||
duration,
|
||||
scriptUrl,
|
||||
locationLine,
|
||||
locationColumn,
|
||||
} = stackFrame;
|
||||
const {name, timestamp, duration, locationLine, locationColumn} = stackFrame;
|
||||
return (
|
||||
<div className={styles.Tooltip} ref={tooltipRef}>
|
||||
{formatDuration(duration)}
|
||||
<span className={styles.FlamechartStackFrameName}>{name}</span>
|
||||
<div className={styles.DetailsGrid}>
|
||||
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
||||
<div>{formatTimestamp(timestamp)}</div>
|
||||
{scriptUrl && (
|
||||
<>
|
||||
<div className={styles.DetailsGridLabel}>Script URL:</div>
|
||||
<div className={styles.DetailsGridURL}>{scriptUrl}</div>
|
||||
</>
|
||||
)}
|
||||
{(locationLine !== undefined || locationColumn !== undefined) && (
|
||||
<>
|
||||
<div className={styles.DetailsGridLabel}>Location:</div>
|
||||
<div>
|
||||
line {locationLine}, column {locationColumn}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className={styles.TooltipSection}>
|
||||
<span className={styles.FlamechartStackFrameName}>{name}</span>
|
||||
<div className={styles.DetailsGrid}>
|
||||
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
||||
<div>{formatTimestamp(timestamp)}</div>
|
||||
<div className={styles.DetailsGridLabel}>Duration:</div>
|
||||
<div>{formatDuration(duration)}</div>
|
||||
{(locationLine !== undefined || locationColumn !== undefined) && (
|
||||
<>
|
||||
<div className={styles.DetailsGridLabel}>Location:</div>
|
||||
<div>
|
||||
line {locationLine}, column {locationColumn}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -216,75 +168,142 @@ const TooltipNativeEvent = ({
|
||||
nativeEvent: NativeEvent,
|
||||
tooltipRef: Return<typeof useRef>,
|
||||
}) => {
|
||||
const {duration, timestamp, type, warnings} = nativeEvent;
|
||||
|
||||
const warningElements = [];
|
||||
if (warnings !== null) {
|
||||
warnings.forEach((warning, index) => {
|
||||
warningElements.push(
|
||||
<Fragment key={index}>
|
||||
<div className={styles.DetailsGridLabel}>Warning:</div>
|
||||
<div>{warning}</div>
|
||||
</Fragment>,
|
||||
);
|
||||
});
|
||||
}
|
||||
const {duration, timestamp, type, warning} = nativeEvent;
|
||||
|
||||
return (
|
||||
<div className={styles.Tooltip} ref={tooltipRef}>
|
||||
<span className={styles.ComponentName}>{trimmedString(type, 768)}</span>
|
||||
event
|
||||
<div className={styles.Divider} />
|
||||
<div className={styles.DetailsGrid}>
|
||||
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
||||
<div>{formatTimestamp(timestamp)}</div>
|
||||
<div className={styles.DetailsGridLabel}>Duration:</div>
|
||||
<div>{formatDuration(duration)}</div>
|
||||
{warningElements}
|
||||
<div className={styles.TooltipSection}>
|
||||
<span className={styles.NativeEventName}>{trimString(type, 768)}</span>
|
||||
event
|
||||
<div className={styles.Divider} />
|
||||
<div className={styles.DetailsGrid}>
|
||||
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
||||
<div>{formatTimestamp(timestamp)}</div>
|
||||
<div className={styles.DetailsGridLabel}>Duration:</div>
|
||||
<div>{formatDuration(duration)}</div>
|
||||
</div>
|
||||
</div>
|
||||
{warning !== null && (
|
||||
<div className={styles.TooltipWarningSection}>
|
||||
<div className={styles.WarningText}>{warning}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TooltipReactEvent = ({
|
||||
reactEvent,
|
||||
const TooltipSchedulingEvent = ({
|
||||
schedulingEvent,
|
||||
tooltipRef,
|
||||
}: {
|
||||
reactEvent: ReactEvent,
|
||||
schedulingEvent: SchedulingEvent,
|
||||
tooltipRef: Return<typeof useRef>,
|
||||
}) => {
|
||||
const label = getReactEventLabel(reactEvent);
|
||||
const color = getReactEventColor(reactEvent);
|
||||
if (!label || !color) {
|
||||
const label = getSchedulingEventLabel(schedulingEvent);
|
||||
if (!label) {
|
||||
if (__DEV__) {
|
||||
console.warn('Unexpected reactEvent type "%s"', reactEvent.type);
|
||||
console.warn(
|
||||
'Unexpected schedulingEvent type "%s"',
|
||||
schedulingEvent.type,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const {componentName, componentStack, timestamp} = reactEvent;
|
||||
let laneLabels = null;
|
||||
let lanes = null;
|
||||
switch (schedulingEvent.type) {
|
||||
case 'schedule-render':
|
||||
case 'schedule-state-update':
|
||||
case 'schedule-force-update':
|
||||
laneLabels = schedulingEvent.laneLabels;
|
||||
lanes = schedulingEvent.lanes;
|
||||
break;
|
||||
}
|
||||
|
||||
const {componentName, timestamp, warning} = schedulingEvent;
|
||||
|
||||
return (
|
||||
<div className={styles.Tooltip} ref={tooltipRef}>
|
||||
{componentName && (
|
||||
<span className={styles.ComponentName} style={{color}}>
|
||||
{trimmedString(componentName, 768)}
|
||||
</span>
|
||||
)}
|
||||
{label}
|
||||
<div className={styles.Divider} />
|
||||
<div className={styles.DetailsGrid}>
|
||||
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
||||
<div>{formatTimestamp(timestamp)}</div>
|
||||
{componentStack && (
|
||||
<Fragment>
|
||||
<div className={styles.DetailsGridLabel}>Component stack:</div>
|
||||
<pre className={styles.ComponentStack}>
|
||||
{formatComponentStack(componentStack)}
|
||||
</pre>
|
||||
</Fragment>
|
||||
<div className={styles.TooltipSection}>
|
||||
{componentName && (
|
||||
<span className={styles.ComponentName}>
|
||||
{trimString(componentName, 100)}
|
||||
</span>
|
||||
)}
|
||||
{label}
|
||||
<div className={styles.Divider} />
|
||||
<div className={styles.DetailsGrid}>
|
||||
{laneLabels !== null && lanes !== null && (
|
||||
<>
|
||||
<div className={styles.DetailsGridLabel}>Lanes:</div>
|
||||
<div>
|
||||
{laneLabels.join(', ')} ({lanes.join(', ')})
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
||||
<div>{formatTimestamp(timestamp)}</div>
|
||||
</div>
|
||||
</div>
|
||||
{warning !== null && (
|
||||
<div className={styles.TooltipWarningSection}>
|
||||
<div className={styles.WarningText}>{warning}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TooltipSuspenseEvent = ({
|
||||
suspenseEvent,
|
||||
tooltipRef,
|
||||
}: {
|
||||
suspenseEvent: SuspenseEvent,
|
||||
tooltipRef: Return<typeof useRef>,
|
||||
}) => {
|
||||
const {
|
||||
componentName,
|
||||
duration,
|
||||
phase,
|
||||
resolution,
|
||||
timestamp,
|
||||
warning,
|
||||
} = suspenseEvent;
|
||||
|
||||
let label = 'suspended';
|
||||
if (phase !== null) {
|
||||
label += ` during ${phase}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.Tooltip} ref={tooltipRef}>
|
||||
<div className={styles.TooltipSection}>
|
||||
{componentName && (
|
||||
<span className={styles.ComponentName}>
|
||||
{trimString(componentName, 100)}
|
||||
</span>
|
||||
)}
|
||||
{label}
|
||||
<div className={styles.Divider} />
|
||||
<div className={styles.DetailsGrid}>
|
||||
<div className={styles.DetailsGridLabel}>Status:</div>
|
||||
<div>{resolution}</div>
|
||||
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
||||
<div>{formatTimestamp(timestamp)}</div>
|
||||
{duration !== null && (
|
||||
<>
|
||||
<div className={styles.DetailsGridLabel}>Duration:</div>
|
||||
<div>{formatDuration(duration)}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{warning !== null && (
|
||||
<div className={styles.TooltipWarningSection}>
|
||||
<div className={styles.WarningText}>{warning}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -311,21 +330,28 @@ const TooltipReactMeasure = ({
|
||||
|
||||
return (
|
||||
<div className={styles.Tooltip} ref={tooltipRef}>
|
||||
{formatDuration(duration)}
|
||||
<span className={styles.ReactMeasureLabel}>{label}</span>
|
||||
<div className={styles.Divider} />
|
||||
<div className={styles.DetailsGrid}>
|
||||
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
||||
<div>{formatTimestamp(timestamp)}</div>
|
||||
<div className={styles.DetailsGridLabel}>Batch duration:</div>
|
||||
<div>{formatDuration(stopTime - startTime)}</div>
|
||||
<div className={styles.DetailsGridLabel}>
|
||||
Lane{lanes.length === 1 ? '' : 's'}:
|
||||
</div>
|
||||
<div>
|
||||
{laneLabels.length > 0
|
||||
? `${laneLabels.join(', ')} (${lanes.join(', ')})`
|
||||
: lanes.join(', ')}
|
||||
<div className={styles.TooltipSection}>
|
||||
<span className={styles.ReactMeasureLabel}>{label}</span>
|
||||
<div className={styles.Divider} />
|
||||
<div className={styles.DetailsGrid}>
|
||||
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
||||
<div>{formatTimestamp(timestamp)}</div>
|
||||
{measure.type !== 'render-idle' && (
|
||||
<>
|
||||
<div className={styles.DetailsGridLabel}>Duration:</div>
|
||||
<div>{formatDuration(duration)}</div>
|
||||
</>
|
||||
)}
|
||||
<div className={styles.DetailsGridLabel}>Batch duration:</div>
|
||||
<div>{formatDuration(stopTime - startTime)}</div>
|
||||
<div className={styles.DetailsGridLabel}>
|
||||
Lane{lanes.length === 1 ? '' : 's'}:
|
||||
</div>
|
||||
<div>
|
||||
{laneLabels.length > 0
|
||||
? `${laneLabels.join(', ')} (${lanes.join(', ')})`
|
||||
: lanes.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -342,11 +368,13 @@ const TooltipUserTimingMark = ({
|
||||
const {name, timestamp} = mark;
|
||||
return (
|
||||
<div className={styles.Tooltip} ref={tooltipRef}>
|
||||
<span className={styles.UserTimingLabel}>{name}</span>
|
||||
<div className={styles.Divider} />
|
||||
<div className={styles.DetailsGrid}>
|
||||
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
||||
<div>{formatTimestamp(timestamp)}</div>
|
||||
<div className={styles.TooltipSection}>
|
||||
<span className={styles.UserTimingLabel}>{name}</span>
|
||||
<div className={styles.Divider} />
|
||||
<div className={styles.DetailsGrid}>
|
||||
<div className={styles.DetailsGridLabel}>Timestamp:</div>
|
||||
<div>{formatTimestamp(timestamp)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -41,8 +41,18 @@ export function SchedulingProfiler(_: {||}) {
|
||||
// The easiest way to guarangee this happens is to recreate the inner Canvas component.
|
||||
const [key, setKey] = useState<string>(theme);
|
||||
useLayoutEffect(() => {
|
||||
updateColorsToMatchTheme();
|
||||
setKey(deferredTheme);
|
||||
const pollForTheme = () => {
|
||||
if (updateColorsToMatchTheme()) {
|
||||
clearInterval(intervalID);
|
||||
setKey(deferredTheme);
|
||||
}
|
||||
};
|
||||
|
||||
const intervalID = setInterval(pollForTheme, 50);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalID);
|
||||
};
|
||||
}, [deferredTheme]);
|
||||
|
||||
return (
|
||||
|
||||
+21
-81
@@ -21,12 +21,11 @@ import type {
|
||||
} from '../view-base';
|
||||
|
||||
import {
|
||||
ColorView,
|
||||
BackgroundColorView,
|
||||
Surface,
|
||||
View,
|
||||
layeredLayout,
|
||||
rectContainsPoint,
|
||||
rectEqualToRect,
|
||||
intersectionOfRects,
|
||||
rectIntersectsRect,
|
||||
verticallyStackedLayout,
|
||||
@@ -36,11 +35,10 @@ import {
|
||||
positioningScaleFactor,
|
||||
timestampToPosition,
|
||||
} from './utils/positioning';
|
||||
import {drawText} from './utils/text';
|
||||
import {
|
||||
COLORS,
|
||||
FONT_SIZE,
|
||||
FLAMECHART_FRAME_HEIGHT,
|
||||
TEXT_PADDING,
|
||||
COLOR_HOVER_DIM_DELTA,
|
||||
BORDER_SIZE,
|
||||
} from './constants';
|
||||
@@ -71,29 +69,6 @@ function hoverColorForStackFrame(stackFrame: FlamechartStackFrame): string {
|
||||
return hslaColorToString(color);
|
||||
}
|
||||
|
||||
const cachedFlamechartTextWidths = new Map();
|
||||
const trimFlamechartText = (
|
||||
context: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
width: number,
|
||||
) => {
|
||||
for (let i = text.length - 1; i >= 0; i--) {
|
||||
const trimmedText = i === text.length - 1 ? text : text.substr(0, i) + '…';
|
||||
|
||||
let measuredWidth = cachedFlamechartTextWidths.get(trimmedText);
|
||||
if (measuredWidth == null) {
|
||||
measuredWidth = context.measureText(trimmedText).width;
|
||||
cachedFlamechartTextWidths.set(trimmedText, measuredWidth);
|
||||
}
|
||||
|
||||
if (measuredWidth <= width) {
|
||||
return trimmedText;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
class FlamechartStackLayerView extends View {
|
||||
/** Layer to display */
|
||||
_stackLayer: FlamechartStackLayer;
|
||||
@@ -161,10 +136,6 @@ class FlamechartStackLayerView extends View {
|
||||
visibleArea.size.height,
|
||||
);
|
||||
|
||||
context.textAlign = 'left';
|
||||
context.textBaseline = 'middle';
|
||||
context.font = `${FONT_SIZE}px sans-serif`;
|
||||
|
||||
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
|
||||
|
||||
for (let i = 0; i < _stackLayer.length; i++) {
|
||||
@@ -201,45 +172,7 @@ class FlamechartStackLayerView extends View {
|
||||
drawableRect.size.height,
|
||||
);
|
||||
|
||||
if (width > TEXT_PADDING * 2) {
|
||||
const trimmedName = trimFlamechartText(
|
||||
context,
|
||||
name,
|
||||
width - TEXT_PADDING * 2 + (x < 0 ? x : 0),
|
||||
);
|
||||
|
||||
if (trimmedName !== null) {
|
||||
context.fillStyle = COLORS.TEXT_COLOR;
|
||||
|
||||
// Prevent text from being drawn outside `viewableArea`
|
||||
const textOverflowsViewableArea = !rectEqualToRect(
|
||||
drawableRect,
|
||||
nodeRect,
|
||||
);
|
||||
if (textOverflowsViewableArea) {
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.rect(
|
||||
drawableRect.origin.x,
|
||||
drawableRect.origin.y,
|
||||
drawableRect.size.width,
|
||||
drawableRect.size.height,
|
||||
);
|
||||
context.closePath();
|
||||
context.clip();
|
||||
}
|
||||
|
||||
context.fillText(
|
||||
trimmedName,
|
||||
nodeRect.origin.x + TEXT_PADDING - (x < 0 ? x : 0),
|
||||
nodeRect.origin.y + FLAMECHART_FRAME_HEIGHT / 2,
|
||||
);
|
||||
|
||||
if (textOverflowsViewableArea) {
|
||||
context.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
drawText(name, context, nodeRect, drawableRect, width);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,13 +195,17 @@ class FlamechartStackLayerView extends View {
|
||||
const flamechartStackFrame = _stackLayer[currentIndex];
|
||||
const {timestamp, duration} = flamechartStackFrame;
|
||||
|
||||
const width = durationToWidth(duration, scaleFactor);
|
||||
const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame));
|
||||
if (x <= location.x && x + width >= location.x) {
|
||||
this.currentCursor = 'pointer';
|
||||
viewRefs.hoveredView = this;
|
||||
_onHover(flamechartStackFrame);
|
||||
return;
|
||||
const width = durationToWidth(duration, scaleFactor);
|
||||
|
||||
// Don't show tooltips for nodes that are too small to render at this zoom level.
|
||||
if (Math.floor(width - BORDER_SIZE) >= 1) {
|
||||
if (x <= location.x && x + width >= location.x) {
|
||||
this.currentCursor = 'context-menu';
|
||||
viewRefs.hoveredView = this;
|
||||
_onHover(flamechartStackFrame);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (x > location.x) {
|
||||
@@ -336,10 +273,8 @@ export class FlamechartView extends View {
|
||||
return rowView;
|
||||
});
|
||||
|
||||
// Add a plain background view to prevent gaps from appearing between
|
||||
// flamechartRowViews.
|
||||
const colorView = new ColorView(surface, frame, COLORS.BACKGROUND);
|
||||
this.addSubview(colorView);
|
||||
// Add a plain background view to prevent gaps from appearing between flamechartRowViews.
|
||||
this.addSubview(new BackgroundColorView(surface, frame));
|
||||
this.addSubview(this._verticalStackView);
|
||||
}
|
||||
|
||||
@@ -359,7 +294,12 @@ export class FlamechartView extends View {
|
||||
|
||||
desiredSize() {
|
||||
// Ignore the wishes of the background color view
|
||||
return this._verticalStackView.desiredSize();
|
||||
const intrinsicSize = this._verticalStackView.desiredSize();
|
||||
return {
|
||||
...intrinsicSize,
|
||||
// Collapsed by default
|
||||
maxInitialHeight: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+18
-67
@@ -22,6 +22,8 @@ import {
|
||||
positionToTimestamp,
|
||||
timestampToPosition,
|
||||
} from './utils/positioning';
|
||||
import {drawText} from './utils/text';
|
||||
import {formatDuration} from '../utils/formatting';
|
||||
import {
|
||||
View,
|
||||
Surface,
|
||||
@@ -29,40 +31,10 @@ import {
|
||||
rectIntersectsRect,
|
||||
intersectionOfRects,
|
||||
} from '../view-base';
|
||||
import {
|
||||
COLORS,
|
||||
TEXT_PADDING,
|
||||
NATIVE_EVENT_HEIGHT,
|
||||
FONT_SIZE,
|
||||
BORDER_SIZE,
|
||||
} from './constants';
|
||||
import {COLORS, NATIVE_EVENT_HEIGHT, BORDER_SIZE} from './constants';
|
||||
|
||||
const ROW_WITH_BORDER_HEIGHT = NATIVE_EVENT_HEIGHT + BORDER_SIZE;
|
||||
|
||||
// TODO (scheduling profiler) Make this a reusable util
|
||||
const cachedFlamechartTextWidths = new Map();
|
||||
const trimFlamechartText = (
|
||||
context: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
width: number,
|
||||
) => {
|
||||
for (let i = text.length - 1; i >= 0; i--) {
|
||||
const trimmedText = i === text.length - 1 ? text : text.substr(0, i) + '…';
|
||||
|
||||
let measuredWidth = cachedFlamechartTextWidths.get(trimmedText);
|
||||
if (measuredWidth == null) {
|
||||
measuredWidth = context.measureText(trimmedText).width;
|
||||
cachedFlamechartTextWidths.set(trimmedText, measuredWidth);
|
||||
}
|
||||
|
||||
if (measuredWidth <= width) {
|
||||
return trimmedText;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export class NativeEventsView extends View {
|
||||
_depthToNativeEvent: Map<number, NativeEvent[]>;
|
||||
_hoveredEvent: NativeEvent | null = null;
|
||||
@@ -117,7 +89,7 @@ export class NativeEventsView extends View {
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a single `NativeEvent` as a circle in the canvas.
|
||||
* Draw a single `NativeEvent` as a box/span with text inside of it.
|
||||
*/
|
||||
_drawSingleNativeEvent(
|
||||
context: CanvasRenderingContext2D,
|
||||
@@ -128,7 +100,7 @@ export class NativeEventsView extends View {
|
||||
showHoverHighlight: boolean,
|
||||
) {
|
||||
const {frame} = this;
|
||||
const {depth, duration, timestamp, type, warnings} = event;
|
||||
const {depth, duration, timestamp, type, warning} = event;
|
||||
|
||||
baseY += depth * ROW_WITH_BORDER_HEIGHT;
|
||||
|
||||
@@ -152,10 +124,10 @@ export class NativeEventsView extends View {
|
||||
|
||||
const drawableRect = intersectionOfRects(eventRect, rect);
|
||||
context.beginPath();
|
||||
if (warnings !== null) {
|
||||
if (warning !== null) {
|
||||
context.fillStyle = showHoverHighlight
|
||||
? COLORS.NATIVE_EVENT_WARNING_HOVER
|
||||
: COLORS.NATIVE_EVENT_WARNING;
|
||||
? COLORS.WARNING_BACKGROUND_HOVER
|
||||
: COLORS.WARNING_BACKGROUND;
|
||||
} else {
|
||||
context.fillStyle = showHoverHighlight
|
||||
? COLORS.NATIVE_EVENT_HOVER
|
||||
@@ -168,32 +140,9 @@ export class NativeEventsView extends View {
|
||||
drawableRect.size.height,
|
||||
);
|
||||
|
||||
// Render event type label
|
||||
context.textAlign = 'left';
|
||||
context.textBaseline = 'middle';
|
||||
context.font = `${FONT_SIZE}px sans-serif`;
|
||||
const label = `${type} - ${formatDuration(duration)}`;
|
||||
|
||||
if (width > TEXT_PADDING * 2) {
|
||||
const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame));
|
||||
const trimmedName = trimFlamechartText(
|
||||
context,
|
||||
type,
|
||||
width - TEXT_PADDING * 2 + (x < 0 ? x : 0),
|
||||
);
|
||||
|
||||
if (trimmedName !== null) {
|
||||
context.fillStyle =
|
||||
warnings !== null
|
||||
? COLORS.NATIVE_EVENT_WARNING_TEXT
|
||||
: COLORS.TEXT_COLOR;
|
||||
|
||||
context.fillText(
|
||||
trimmedName,
|
||||
eventRect.origin.x + TEXT_PADDING - (x < 0 ? x : 0),
|
||||
eventRect.origin.y + NATIVE_EVENT_HEIGHT / 2,
|
||||
);
|
||||
}
|
||||
}
|
||||
drawText(label, context, eventRect, drawableRect, width);
|
||||
}
|
||||
|
||||
draw(context: CanvasRenderingContext2D) {
|
||||
@@ -242,12 +191,16 @@ export class NativeEventsView extends View {
|
||||
},
|
||||
};
|
||||
if (rectIntersectsRect(borderFrame, visibleArea)) {
|
||||
const borderDrawableRect = intersectionOfRects(
|
||||
borderFrame,
|
||||
visibleArea,
|
||||
);
|
||||
context.fillStyle = COLORS.PRIORITY_BORDER;
|
||||
context.fillRect(
|
||||
visibleArea.origin.x,
|
||||
frame.origin.y + (i + 1) * ROW_WITH_BORDER_HEIGHT - BORDER_SIZE,
|
||||
visibleArea.size.width,
|
||||
BORDER_SIZE,
|
||||
borderDrawableRect.origin.x,
|
||||
borderDrawableRect.origin.y,
|
||||
borderDrawableRect.size.width,
|
||||
borderDrawableRect.size.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -285,8 +238,6 @@ export class NativeEventsView extends View {
|
||||
hoverTimestamp >= timestamp &&
|
||||
hoverTimestamp <= timestamp + duration
|
||||
) {
|
||||
this.currentCursor = 'pointer';
|
||||
|
||||
viewRefs.hoveredView = this;
|
||||
|
||||
onHover(nativeEvent);
|
||||
|
||||
+10
-8
@@ -12,7 +12,7 @@ import type {
|
||||
Interaction,
|
||||
MouseMoveInteraction,
|
||||
Rect,
|
||||
Size,
|
||||
SizeWithMaxHeight,
|
||||
ViewRefs,
|
||||
} from '../view-base';
|
||||
|
||||
@@ -34,6 +34,7 @@ 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;
|
||||
|
||||
function getMeasuresForLane(
|
||||
allMeasures: ReactMeasure[],
|
||||
@@ -44,7 +45,7 @@ function getMeasuresForLane(
|
||||
|
||||
export class ReactMeasuresView extends View {
|
||||
_profilerData: ReactProfilerData;
|
||||
_intrinsicSize: Size;
|
||||
_intrinsicSize: SizeWithMaxHeight;
|
||||
|
||||
_lanesToRender: ReactLane[];
|
||||
_laneToMeasures: Map<ReactLane, ReactMeasure[]>;
|
||||
@@ -77,6 +78,7 @@ export class ReactMeasuresView extends View {
|
||||
this._intrinsicSize = {
|
||||
width: this._profilerData.duration,
|
||||
height: this._lanesToRender.length * REACT_LANE_HEIGHT,
|
||||
maxInitialHeight: MAX_ROWS_TO_SHOW_INITIALLY * REACT_LANE_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -132,7 +134,7 @@ export class ReactMeasuresView extends View {
|
||||
case 'commit':
|
||||
fillStyle = COLORS.REACT_COMMIT;
|
||||
hoveredFillStyle = COLORS.REACT_COMMIT_HOVER;
|
||||
groupSelectedFillStyle = COLORS.REACT_COMMIT_SELECTED;
|
||||
groupSelectedFillStyle = COLORS.REACT_COMMIT_HOVER;
|
||||
break;
|
||||
case 'render-idle':
|
||||
// We could render idle time as diagonal hashes.
|
||||
@@ -140,22 +142,22 @@ export class ReactMeasuresView extends View {
|
||||
// color = context.createPattern(getIdlePattern(), 'repeat');
|
||||
fillStyle = COLORS.REACT_IDLE;
|
||||
hoveredFillStyle = COLORS.REACT_IDLE_HOVER;
|
||||
groupSelectedFillStyle = COLORS.REACT_IDLE_SELECTED;
|
||||
groupSelectedFillStyle = COLORS.REACT_IDLE_HOVER;
|
||||
break;
|
||||
case 'render':
|
||||
fillStyle = COLORS.REACT_RENDER;
|
||||
hoveredFillStyle = COLORS.REACT_RENDER_HOVER;
|
||||
groupSelectedFillStyle = COLORS.REACT_RENDER_SELECTED;
|
||||
groupSelectedFillStyle = COLORS.REACT_RENDER_HOVER;
|
||||
break;
|
||||
case 'layout-effects':
|
||||
fillStyle = COLORS.REACT_LAYOUT_EFFECTS;
|
||||
hoveredFillStyle = COLORS.REACT_LAYOUT_EFFECTS_HOVER;
|
||||
groupSelectedFillStyle = COLORS.REACT_LAYOUT_EFFECTS_SELECTED;
|
||||
groupSelectedFillStyle = COLORS.REACT_LAYOUT_EFFECTS_HOVER;
|
||||
break;
|
||||
case 'passive-effects':
|
||||
fillStyle = COLORS.REACT_PASSIVE_EFFECTS;
|
||||
hoveredFillStyle = COLORS.REACT_PASSIVE_EFFECTS_HOVER;
|
||||
groupSelectedFillStyle = COLORS.REACT_PASSIVE_EFFECTS_SELECTED;
|
||||
groupSelectedFillStyle = COLORS.REACT_PASSIVE_EFFECTS_HOVER;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unexpected measure type "${type}"`);
|
||||
@@ -306,7 +308,7 @@ export class ReactMeasuresView extends View {
|
||||
hoverTimestamp >= timestamp &&
|
||||
hoverTimestamp <= timestamp + duration
|
||||
) {
|
||||
this.currentCursor = 'pointer';
|
||||
this.currentCursor = 'context-menu';
|
||||
viewRefs.hoveredView = this;
|
||||
onHover(measure);
|
||||
return;
|
||||
|
||||
+35
-58
@@ -7,7 +7,7 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactEvent, ReactProfilerData} from '../types';
|
||||
import type {SchedulingEvent, ReactProfilerData} from '../types';
|
||||
import type {
|
||||
Interaction,
|
||||
MouseMoveInteraction,
|
||||
@@ -39,20 +39,12 @@ import {
|
||||
const EVENT_ROW_HEIGHT_FIXED =
|
||||
TOP_ROW_PADDING + REACT_EVENT_DIAMETER + TOP_ROW_PADDING;
|
||||
|
||||
function isSuspenseEvent(event: ReactEvent): boolean %checks {
|
||||
return (
|
||||
event.type === 'suspense-suspend' ||
|
||||
event.type === 'suspense-resolved' ||
|
||||
event.type === 'suspense-rejected'
|
||||
);
|
||||
}
|
||||
|
||||
export class ReactEventsView extends View {
|
||||
export class SchedulingEventsView extends View {
|
||||
_profilerData: ReactProfilerData;
|
||||
_intrinsicSize: Size;
|
||||
|
||||
_hoveredEvent: ReactEvent | null = null;
|
||||
onHover: ((event: ReactEvent | null) => void) | null = null;
|
||||
_hoveredEvent: SchedulingEvent | null = null;
|
||||
onHover: ((event: SchedulingEvent | null) => void) | null = null;
|
||||
|
||||
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
|
||||
super(surface, frame);
|
||||
@@ -68,7 +60,7 @@ export class ReactEventsView extends View {
|
||||
return this._intrinsicSize;
|
||||
}
|
||||
|
||||
setHoveredEvent(hoveredEvent: ReactEvent | null) {
|
||||
setHoveredEvent(hoveredEvent: SchedulingEvent | null) {
|
||||
if (this._hoveredEvent === hoveredEvent) {
|
||||
return;
|
||||
}
|
||||
@@ -77,18 +69,18 @@ export class ReactEventsView extends View {
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a single `ReactEvent` as a circle in the canvas.
|
||||
* Draw a single `SchedulingEvent` as a circle in the canvas.
|
||||
*/
|
||||
_drawSingleReactEvent(
|
||||
_drawSingleSchedulingEvent(
|
||||
context: CanvasRenderingContext2D,
|
||||
rect: Rect,
|
||||
event: ReactEvent,
|
||||
event: SchedulingEvent,
|
||||
baseY: number,
|
||||
scaleFactor: number,
|
||||
showHoverHighlight: boolean,
|
||||
) {
|
||||
const {frame} = this;
|
||||
const {timestamp, type} = event;
|
||||
const {timestamp, type, warning} = event;
|
||||
|
||||
const x = timestampToPosition(timestamp, scaleFactor, frame);
|
||||
const radius = REACT_EVENT_DIAMETER / 2;
|
||||
@@ -105,34 +97,25 @@ export class ReactEventsView extends View {
|
||||
|
||||
let fillStyle = null;
|
||||
|
||||
switch (type) {
|
||||
case 'native-event':
|
||||
return;
|
||||
case 'schedule-render':
|
||||
case 'schedule-state-update':
|
||||
case 'schedule-force-update':
|
||||
if (event.isCascading) {
|
||||
fillStyle = showHoverHighlight
|
||||
? COLORS.REACT_SCHEDULE_CASCADING_HOVER
|
||||
: COLORS.REACT_SCHEDULE_CASCADING;
|
||||
} else {
|
||||
if (warning !== null) {
|
||||
fillStyle = showHoverHighlight
|
||||
? COLORS.WARNING_BACKGROUND_HOVER
|
||||
: COLORS.WARNING_BACKGROUND;
|
||||
} else {
|
||||
switch (type) {
|
||||
case 'schedule-render':
|
||||
case 'schedule-state-update':
|
||||
case 'schedule-force-update':
|
||||
fillStyle = showHoverHighlight
|
||||
? COLORS.REACT_SCHEDULE_HOVER
|
||||
: COLORS.REACT_SCHEDULE;
|
||||
}
|
||||
break;
|
||||
case 'suspense-suspend':
|
||||
case 'suspense-resolved':
|
||||
case 'suspense-rejected':
|
||||
fillStyle = showHoverHighlight
|
||||
? COLORS.REACT_SUSPEND_HOVER
|
||||
: COLORS.REACT_SUSPEND;
|
||||
break;
|
||||
default:
|
||||
if (__DEV__) {
|
||||
console.warn('Unexpected event type "%s"', type);
|
||||
}
|
||||
break;
|
||||
break;
|
||||
default:
|
||||
if (__DEV__) {
|
||||
console.warn('Unexpected event type "%s"', type);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fillStyle !== null) {
|
||||
@@ -148,7 +131,7 @@ export class ReactEventsView extends View {
|
||||
draw(context: CanvasRenderingContext2D) {
|
||||
const {
|
||||
frame,
|
||||
_profilerData: {reactEvents},
|
||||
_profilerData: {schedulingEvents},
|
||||
_hoveredEvent,
|
||||
visibleArea,
|
||||
} = this;
|
||||
@@ -168,20 +151,14 @@ export class ReactEventsView extends View {
|
||||
frame,
|
||||
);
|
||||
|
||||
const highlightedEvents: ReactEvent[] = [];
|
||||
const highlightedEvents: SchedulingEvent[] = [];
|
||||
|
||||
reactEvents.forEach(event => {
|
||||
if (
|
||||
event === _hoveredEvent ||
|
||||
(_hoveredEvent &&
|
||||
isSuspenseEvent(event) &&
|
||||
isSuspenseEvent(_hoveredEvent) &&
|
||||
event.id === _hoveredEvent.id)
|
||||
) {
|
||||
schedulingEvents.forEach(event => {
|
||||
if (event === _hoveredEvent) {
|
||||
highlightedEvents.push(event);
|
||||
return;
|
||||
}
|
||||
this._drawSingleReactEvent(
|
||||
this._drawSingleSchedulingEvent(
|
||||
context,
|
||||
visibleArea,
|
||||
event,
|
||||
@@ -194,7 +171,7 @@ export class ReactEventsView extends View {
|
||||
// Draw the highlighted items on top so they stand out.
|
||||
// This is helpful if there are multiple (overlapping) items close to each other.
|
||||
highlightedEvents.forEach(event => {
|
||||
this._drawSingleReactEvent(
|
||||
this._drawSingleSchedulingEvent(
|
||||
context,
|
||||
visibleArea,
|
||||
event,
|
||||
@@ -244,7 +221,7 @@ export class ReactEventsView extends View {
|
||||
}
|
||||
|
||||
const {
|
||||
_profilerData: {reactEvents},
|
||||
_profilerData: {schedulingEvents},
|
||||
} = this;
|
||||
const scaleFactor = positioningScaleFactor(
|
||||
this._intrinsicSize.width,
|
||||
@@ -258,15 +235,15 @@ export class ReactEventsView extends View {
|
||||
|
||||
// Because data ranges may overlap, we want to find the last intersecting item.
|
||||
// This will always be the one on "top" (the one the user is hovering over).
|
||||
for (let index = reactEvents.length - 1; index >= 0; index--) {
|
||||
const event = reactEvents[index];
|
||||
for (let index = schedulingEvents.length - 1; index >= 0; index--) {
|
||||
const event = schedulingEvents[index];
|
||||
const {timestamp} = event;
|
||||
|
||||
if (
|
||||
timestamp - eventTimestampAllowance <= hoverTimestamp &&
|
||||
hoverTimestamp <= timestamp + eventTimestampAllowance
|
||||
) {
|
||||
this.currentCursor = 'pointer';
|
||||
this.currentCursor = 'context-menu';
|
||||
viewRefs.hoveredView = this;
|
||||
onHover(event);
|
||||
return;
|
||||
+368
@@ -0,0 +1,368 @@
|
||||
/**
|
||||
* 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 {SuspenseEvent, ReactProfilerData} from '../types';
|
||||
import type {
|
||||
Interaction,
|
||||
MouseMoveInteraction,
|
||||
Rect,
|
||||
SizeWithMaxHeight,
|
||||
ViewRefs,
|
||||
} from '../view-base';
|
||||
|
||||
import {
|
||||
durationToWidth,
|
||||
positioningScaleFactor,
|
||||
positionToTimestamp,
|
||||
timestampToPosition,
|
||||
widthToDuration,
|
||||
} from './utils/positioning';
|
||||
import {drawText} from './utils/text';
|
||||
import {formatDuration} from '../utils/formatting';
|
||||
import {
|
||||
View,
|
||||
Surface,
|
||||
rectContainsPoint,
|
||||
rectIntersectsRect,
|
||||
intersectionOfRects,
|
||||
} from '../view-base';
|
||||
import {
|
||||
BORDER_SIZE,
|
||||
COLORS,
|
||||
PENDING_SUSPENSE_EVENT_SIZE,
|
||||
SUSPENSE_EVENT_HEIGHT,
|
||||
} from './constants';
|
||||
|
||||
const ROW_WITH_BORDER_HEIGHT = SUSPENSE_EVENT_HEIGHT + BORDER_SIZE;
|
||||
const MAX_ROWS_TO_SHOW_INITIALLY = 3;
|
||||
|
||||
export class SuspenseEventsView extends View {
|
||||
_depthToSuspenseEvent: Map<number, SuspenseEvent[]>;
|
||||
_hoveredEvent: SuspenseEvent | null = null;
|
||||
_intrinsicSize: SizeWithMaxHeight;
|
||||
_maxDepth: number = 0;
|
||||
_profilerData: ReactProfilerData;
|
||||
|
||||
onHover: ((event: SuspenseEvent | null) => void) | null = null;
|
||||
|
||||
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
|
||||
super(surface, frame);
|
||||
|
||||
this._profilerData = profilerData;
|
||||
|
||||
this._performPreflightComputations();
|
||||
}
|
||||
|
||||
_performPreflightComputations() {
|
||||
this._depthToSuspenseEvent = new Map();
|
||||
|
||||
const {duration, suspenseEvents} = this._profilerData;
|
||||
|
||||
suspenseEvents.forEach(event => {
|
||||
const depth = event.depth;
|
||||
|
||||
this._maxDepth = Math.max(this._maxDepth, depth);
|
||||
|
||||
if (!this._depthToSuspenseEvent.has(depth)) {
|
||||
this._depthToSuspenseEvent.set(depth, [event]);
|
||||
} else {
|
||||
// $FlowFixMe This is unnecessary.
|
||||
this._depthToSuspenseEvent.get(depth).push(event);
|
||||
}
|
||||
});
|
||||
|
||||
this._intrinsicSize = {
|
||||
width: duration,
|
||||
height: (this._maxDepth + 1) * ROW_WITH_BORDER_HEIGHT,
|
||||
maxInitialHeight: ROW_WITH_BORDER_HEIGHT * MAX_ROWS_TO_SHOW_INITIALLY,
|
||||
};
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
return this._intrinsicSize;
|
||||
}
|
||||
|
||||
setHoveredEvent(hoveredEvent: SuspenseEvent | null) {
|
||||
if (this._hoveredEvent === hoveredEvent) {
|
||||
return;
|
||||
}
|
||||
this._hoveredEvent = hoveredEvent;
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a single `SuspenseEvent` as a box/span with text inside of it.
|
||||
*/
|
||||
_drawSingleSuspenseEvent(
|
||||
context: CanvasRenderingContext2D,
|
||||
rect: Rect,
|
||||
event: SuspenseEvent,
|
||||
baseY: number,
|
||||
scaleFactor: number,
|
||||
showHoverHighlight: boolean,
|
||||
) {
|
||||
const {frame} = this;
|
||||
const {
|
||||
componentName,
|
||||
depth,
|
||||
duration,
|
||||
phase,
|
||||
resolution,
|
||||
timestamp,
|
||||
warning,
|
||||
} = event;
|
||||
|
||||
baseY += depth * ROW_WITH_BORDER_HEIGHT;
|
||||
|
||||
let fillStyle = ((null: any): string);
|
||||
if (warning !== null) {
|
||||
fillStyle = showHoverHighlight
|
||||
? COLORS.WARNING_BACKGROUND_HOVER
|
||||
: COLORS.WARNING_BACKGROUND;
|
||||
} else {
|
||||
switch (resolution) {
|
||||
case 'rejected':
|
||||
fillStyle = showHoverHighlight
|
||||
? COLORS.REACT_SUSPENSE_REJECTED_EVENT_HOVER
|
||||
: COLORS.REACT_SUSPENSE_REJECTED_EVENT;
|
||||
break;
|
||||
case 'resolved':
|
||||
fillStyle = showHoverHighlight
|
||||
? COLORS.REACT_SUSPENSE_RESOLVED_EVENT_HOVER
|
||||
: COLORS.REACT_SUSPENSE_RESOLVED_EVENT;
|
||||
break;
|
||||
case 'unresolved':
|
||||
fillStyle = showHoverHighlight
|
||||
? COLORS.REACT_SUSPENSE_UNRESOLVED_EVENT_HOVER
|
||||
: COLORS.REACT_SUSPENSE_UNRESOLVED_EVENT;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const xStart = timestampToPosition(timestamp, scaleFactor, frame);
|
||||
|
||||
// Pending suspense events (ones that never resolved) won't have durations.
|
||||
// So instead we draw them as diamonds.
|
||||
if (duration === null) {
|
||||
const size = PENDING_SUSPENSE_EVENT_SIZE;
|
||||
const halfSize = size / 2;
|
||||
|
||||
baseY += (SUSPENSE_EVENT_HEIGHT - PENDING_SUSPENSE_EVENT_SIZE) / 2;
|
||||
|
||||
const y = baseY + halfSize;
|
||||
|
||||
const suspenseRect: Rect = {
|
||||
origin: {
|
||||
x: xStart - halfSize,
|
||||
y: baseY,
|
||||
},
|
||||
size: {width: size, height: size},
|
||||
};
|
||||
if (!rectIntersectsRect(suspenseRect, rect)) {
|
||||
return; // Not in view
|
||||
}
|
||||
|
||||
const drawableRect = intersectionOfRects(suspenseRect, rect);
|
||||
|
||||
// Clip diamonds so they don't overflow if the view has been resized (smaller).
|
||||
const region = new Path2D();
|
||||
region.rect(
|
||||
drawableRect.origin.x,
|
||||
drawableRect.origin.y,
|
||||
drawableRect.size.width,
|
||||
drawableRect.size.height,
|
||||
);
|
||||
context.save();
|
||||
context.clip(region);
|
||||
context.beginPath();
|
||||
context.fillStyle = fillStyle;
|
||||
context.moveTo(xStart, y - halfSize);
|
||||
context.lineTo(xStart + halfSize, y);
|
||||
context.lineTo(xStart, y + halfSize);
|
||||
context.lineTo(xStart - halfSize, y);
|
||||
context.fill();
|
||||
context.restore();
|
||||
} else {
|
||||
const xStop = timestampToPosition(
|
||||
timestamp + duration,
|
||||
scaleFactor,
|
||||
frame,
|
||||
);
|
||||
const eventRect: Rect = {
|
||||
origin: {
|
||||
x: xStart,
|
||||
y: baseY,
|
||||
},
|
||||
size: {width: xStop - xStart, height: SUSPENSE_EVENT_HEIGHT},
|
||||
};
|
||||
if (!rectIntersectsRect(eventRect, rect)) {
|
||||
return; // Not in view
|
||||
}
|
||||
|
||||
const width = durationToWidth(duration, scaleFactor);
|
||||
if (width < 1) {
|
||||
return; // Too small to render at this zoom level
|
||||
}
|
||||
|
||||
const drawableRect = intersectionOfRects(eventRect, rect);
|
||||
context.beginPath();
|
||||
context.fillStyle = fillStyle;
|
||||
context.fillRect(
|
||||
drawableRect.origin.x,
|
||||
drawableRect.origin.y,
|
||||
drawableRect.size.width,
|
||||
drawableRect.size.height,
|
||||
);
|
||||
|
||||
let label = 'suspended';
|
||||
if (componentName != null) {
|
||||
label = `${componentName} ${label}`;
|
||||
}
|
||||
if (phase !== null) {
|
||||
label += ` during ${phase}`;
|
||||
}
|
||||
if (resolution !== 'unresolved') {
|
||||
label += ` - ${formatDuration(duration)}`;
|
||||
}
|
||||
|
||||
drawText(label, context, eventRect, drawableRect, width);
|
||||
}
|
||||
}
|
||||
|
||||
draw(context: CanvasRenderingContext2D) {
|
||||
const {
|
||||
frame,
|
||||
_profilerData: {suspenseEvents},
|
||||
_hoveredEvent,
|
||||
visibleArea,
|
||||
} = this;
|
||||
|
||||
context.fillStyle = COLORS.PRIORITY_BACKGROUND;
|
||||
context.fillRect(
|
||||
visibleArea.origin.x,
|
||||
visibleArea.origin.y,
|
||||
visibleArea.size.width,
|
||||
visibleArea.size.height,
|
||||
);
|
||||
|
||||
// Draw events
|
||||
const scaleFactor = positioningScaleFactor(
|
||||
this._intrinsicSize.width,
|
||||
frame,
|
||||
);
|
||||
|
||||
suspenseEvents.forEach(event => {
|
||||
this._drawSingleSuspenseEvent(
|
||||
context,
|
||||
visibleArea,
|
||||
event,
|
||||
frame.origin.y,
|
||||
scaleFactor,
|
||||
event === _hoveredEvent,
|
||||
);
|
||||
});
|
||||
|
||||
// Render bottom borders.
|
||||
for (let i = 0; i <= this._maxDepth; i++) {
|
||||
const borderFrame: Rect = {
|
||||
origin: {
|
||||
x: frame.origin.x,
|
||||
y: frame.origin.y + (i + 1) * ROW_WITH_BORDER_HEIGHT - BORDER_SIZE,
|
||||
},
|
||||
size: {
|
||||
width: frame.size.width,
|
||||
height: BORDER_SIZE,
|
||||
},
|
||||
};
|
||||
if (rectIntersectsRect(borderFrame, visibleArea)) {
|
||||
const borderDrawableRect = intersectionOfRects(
|
||||
borderFrame,
|
||||
visibleArea,
|
||||
);
|
||||
context.fillStyle = COLORS.PRIORITY_BORDER;
|
||||
context.fillRect(
|
||||
borderDrawableRect.origin.x,
|
||||
borderDrawableRect.origin.y,
|
||||
borderDrawableRect.size.width,
|
||||
borderDrawableRect.size.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
|
||||
const {frame, _intrinsicSize, onHover, visibleArea} = this;
|
||||
if (!onHover) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {location} = interaction.payload;
|
||||
if (!rectContainsPoint(location, visibleArea)) {
|
||||
onHover(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
|
||||
const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
|
||||
|
||||
const adjustedCanvasMouseY = location.y - frame.origin.y;
|
||||
const depth = Math.floor(adjustedCanvasMouseY / ROW_WITH_BORDER_HEIGHT);
|
||||
const suspenseEventsAtDepth = this._depthToSuspenseEvent.get(depth);
|
||||
|
||||
if (suspenseEventsAtDepth) {
|
||||
// Find the event being hovered over.
|
||||
for (let index = suspenseEventsAtDepth.length - 1; index >= 0; index--) {
|
||||
const suspenseEvent = suspenseEventsAtDepth[index];
|
||||
const {duration, timestamp} = suspenseEvent;
|
||||
|
||||
if (duration === null) {
|
||||
const timestampAllowance = widthToDuration(
|
||||
PENDING_SUSPENSE_EVENT_SIZE / 2,
|
||||
scaleFactor,
|
||||
);
|
||||
|
||||
if (
|
||||
timestamp - timestampAllowance <= hoverTimestamp &&
|
||||
hoverTimestamp <= timestamp + timestampAllowance
|
||||
) {
|
||||
this.currentCursor = 'context-menu';
|
||||
|
||||
viewRefs.hoveredView = this;
|
||||
|
||||
onHover(suspenseEvent);
|
||||
return;
|
||||
}
|
||||
} else if (
|
||||
hoverTimestamp >= timestamp &&
|
||||
hoverTimestamp <= timestamp + duration
|
||||
) {
|
||||
this.currentCursor = 'context-menu';
|
||||
|
||||
viewRefs.hoveredView = this;
|
||||
|
||||
onHover(suspenseEvent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onHover(null);
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
|
||||
switch (interaction.type) {
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction, viewRefs);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -209,7 +209,7 @@ export class UserTimingMarksView extends View {
|
||||
frame,
|
||||
);
|
||||
const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
|
||||
const markTimestampAllowance = widthToDuration(
|
||||
const timestampAllowance = widthToDuration(
|
||||
USER_TIMING_MARK_SIZE / 2,
|
||||
scaleFactor,
|
||||
);
|
||||
@@ -221,10 +221,10 @@ export class UserTimingMarksView extends View {
|
||||
const {timestamp} = mark;
|
||||
|
||||
if (
|
||||
timestamp - markTimestampAllowance <= hoverTimestamp &&
|
||||
hoverTimestamp <= timestamp + markTimestampAllowance
|
||||
timestamp - timestampAllowance <= hoverTimestamp &&
|
||||
hoverTimestamp <= timestamp + timestampAllowance
|
||||
) {
|
||||
this.currentCursor = 'pointer';
|
||||
this.currentCursor = 'context-menu';
|
||||
viewRefs.hoveredView = this;
|
||||
onHover(mark);
|
||||
return;
|
||||
|
||||
+58
-45
@@ -15,6 +15,8 @@ export const MARKER_TEXT_PADDING = 8;
|
||||
export const COLOR_HOVER_DIM_DELTA = 5;
|
||||
export const TOP_ROW_PADDING = 4;
|
||||
export const NATIVE_EVENT_HEIGHT = 14;
|
||||
export const SUSPENSE_EVENT_HEIGHT = 14;
|
||||
export const PENDING_SUSPENSE_EVENT_SIZE = 8;
|
||||
export const REACT_EVENT_DIAMETER = 6;
|
||||
export const USER_TIMING_MARK_SIZE = 8;
|
||||
export const REACT_MEASURE_HEIGHT = 9;
|
||||
@@ -43,44 +45,51 @@ export let COLORS = {
|
||||
BACKGROUND: '',
|
||||
NATIVE_EVENT: '',
|
||||
NATIVE_EVENT_HOVER: '',
|
||||
NATIVE_EVENT_WARNING: '',
|
||||
NATIVE_EVENT_WARNING_HOVER: '',
|
||||
NATIVE_EVENT_WARNING_TEXT: '',
|
||||
PRIORITY_BACKGROUND: '',
|
||||
PRIORITY_BORDER: '',
|
||||
PRIORITY_LABEL: '',
|
||||
USER_TIMING: '',
|
||||
USER_TIMING_HOVER: '',
|
||||
REACT_IDLE: '',
|
||||
REACT_IDLE_SELECTED: '',
|
||||
REACT_IDLE_HOVER: '',
|
||||
REACT_RENDER: '',
|
||||
REACT_RENDER_SELECTED: '',
|
||||
REACT_RENDER_HOVER: '',
|
||||
REACT_COMMIT: '',
|
||||
REACT_COMMIT_SELECTED: '',
|
||||
REACT_COMMIT_HOVER: '',
|
||||
REACT_LAYOUT_EFFECTS: '',
|
||||
REACT_LAYOUT_EFFECTS_SELECTED: '',
|
||||
REACT_LAYOUT_EFFECTS_HOVER: '',
|
||||
REACT_PASSIVE_EFFECTS: '',
|
||||
REACT_PASSIVE_EFFECTS_SELECTED: '',
|
||||
REACT_PASSIVE_EFFECTS_HOVER: '',
|
||||
REACT_RESIZE_BAR: '',
|
||||
REACT_RESIZE_BAR_ACTIVE: '',
|
||||
REACT_RESIZE_BAR_BORDER: '',
|
||||
REACT_RESIZE_BAR_DOT: '',
|
||||
REACT_SCHEDULE: '',
|
||||
REACT_SCHEDULE_HOVER: '',
|
||||
REACT_SCHEDULE_CASCADING: '',
|
||||
REACT_SCHEDULE_CASCADING_HOVER: '',
|
||||
REACT_SUSPEND: '',
|
||||
REACT_SUSPEND_HOVER: '',
|
||||
REACT_SUSPENSE_REJECTED_EVENT: '',
|
||||
REACT_SUSPENSE_REJECTED_EVENT_HOVER: '',
|
||||
REACT_SUSPENSE_RESOLVED_EVENT: '',
|
||||
REACT_SUSPENSE_RESOLVED_EVENT_HOVER: '',
|
||||
REACT_SUSPENSE_UNRESOLVED_EVENT: '',
|
||||
REACT_SUSPENSE_UNRESOLVED_EVENT_HOVER: '',
|
||||
REACT_WORK_BORDER: '',
|
||||
SCROLL_CARET: '',
|
||||
TEXT_COLOR: '',
|
||||
TIME_MARKER_LABEL: '',
|
||||
WARNING_BACKGROUND: '',
|
||||
WARNING_BACKGROUND_HOVER: '',
|
||||
WARNING_TEXT: '',
|
||||
WARNING_TEXT_INVERED: '',
|
||||
};
|
||||
|
||||
export function updateColorsToMatchTheme(): void {
|
||||
export function updateColorsToMatchTheme(): boolean {
|
||||
const computedStyle = getComputedStyle((document.body: any));
|
||||
|
||||
// Check to see if styles have been initialized...
|
||||
if (computedStyle.getPropertyValue('--color-background') == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
COLORS = {
|
||||
BACKGROUND: computedStyle.getPropertyValue('--color-background'),
|
||||
NATIVE_EVENT: computedStyle.getPropertyValue(
|
||||
@@ -89,15 +98,6 @@ export function updateColorsToMatchTheme(): void {
|
||||
NATIVE_EVENT_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-native-event-hover',
|
||||
),
|
||||
NATIVE_EVENT_WARNING: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-native-event-warning',
|
||||
),
|
||||
NATIVE_EVENT_WARNING_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-native-event-warning-hover',
|
||||
),
|
||||
NATIVE_EVENT_WARNING_TEXT: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-native-event-warning-text',
|
||||
),
|
||||
PRIORITY_BACKGROUND: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-priority-background',
|
||||
),
|
||||
@@ -114,73 +114,86 @@ export function updateColorsToMatchTheme(): void {
|
||||
REACT_IDLE: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-idle',
|
||||
),
|
||||
REACT_IDLE_SELECTED: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-idle-selected',
|
||||
),
|
||||
REACT_IDLE_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-idle-hover',
|
||||
),
|
||||
REACT_RENDER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-render',
|
||||
),
|
||||
REACT_RENDER_SELECTED: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-render-selected',
|
||||
),
|
||||
REACT_RENDER_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-render-hover',
|
||||
),
|
||||
REACT_COMMIT: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-commit',
|
||||
),
|
||||
REACT_COMMIT_SELECTED: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-commit-selected',
|
||||
),
|
||||
REACT_COMMIT_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-commit-hover',
|
||||
),
|
||||
REACT_LAYOUT_EFFECTS: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-layout-effects',
|
||||
),
|
||||
REACT_LAYOUT_EFFECTS_SELECTED: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-layout-effects-selected',
|
||||
),
|
||||
REACT_LAYOUT_EFFECTS_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-layout-effects-hover',
|
||||
),
|
||||
REACT_PASSIVE_EFFECTS: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-passive-effects',
|
||||
),
|
||||
REACT_PASSIVE_EFFECTS_SELECTED: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-passive-effects-selected',
|
||||
),
|
||||
REACT_PASSIVE_EFFECTS_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-passive-effects-hover',
|
||||
),
|
||||
REACT_RESIZE_BAR: computedStyle.getPropertyValue('--color-resize-bar'),
|
||||
REACT_RESIZE_BAR_ACTIVE: computedStyle.getPropertyValue(
|
||||
'--color-resize-bar-active',
|
||||
),
|
||||
REACT_RESIZE_BAR_BORDER: computedStyle.getPropertyValue(
|
||||
'--color-resize-bar-border',
|
||||
),
|
||||
REACT_RESIZE_BAR_DOT: computedStyle.getPropertyValue(
|
||||
'--color-resize-bar-dot',
|
||||
),
|
||||
REACT_SCHEDULE: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-schedule',
|
||||
),
|
||||
REACT_SCHEDULE_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-schedule-hover',
|
||||
),
|
||||
REACT_SCHEDULE_CASCADING: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-schedule-cascading',
|
||||
REACT_SUSPENSE_REJECTED_EVENT: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-suspense-rejected',
|
||||
),
|
||||
REACT_SCHEDULE_CASCADING_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-schedule-cascading-hover',
|
||||
REACT_SUSPENSE_REJECTED_EVENT_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-suspense-rejected-hover',
|
||||
),
|
||||
REACT_SUSPEND: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-suspend',
|
||||
REACT_SUSPENSE_RESOLVED_EVENT: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-suspense-resolved',
|
||||
),
|
||||
REACT_SUSPEND_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-suspend-hover',
|
||||
REACT_SUSPENSE_RESOLVED_EVENT_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-suspense-resolved-hover',
|
||||
),
|
||||
REACT_SUSPENSE_UNRESOLVED_EVENT: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-suspense-unresolved',
|
||||
),
|
||||
REACT_SUSPENSE_UNRESOLVED_EVENT_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-suspense-unresolved-hover',
|
||||
),
|
||||
REACT_WORK_BORDER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-react-work-border',
|
||||
),
|
||||
SCROLL_CARET: computedStyle.getPropertyValue('--color-scroll-caret'),
|
||||
TEXT_COLOR: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-text-color',
|
||||
),
|
||||
TIME_MARKER_LABEL: computedStyle.getPropertyValue('--color-text'),
|
||||
WARNING_BACKGROUND: computedStyle.getPropertyValue(
|
||||
'--color-warning-background',
|
||||
),
|
||||
WARNING_BACKGROUND_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-warning-background-hover',
|
||||
),
|
||||
WARNING_TEXT: computedStyle.getPropertyValue('--color-warning-text-color'),
|
||||
WARNING_TEXT_INVERED: computedStyle.getPropertyValue(
|
||||
'--color-warning-text-color-inverted',
|
||||
),
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
|
||||
export * from './FlamechartView';
|
||||
export * from './NativeEventsView';
|
||||
export * from './ReactEventsView';
|
||||
export * from './ReactMeasuresView';
|
||||
export * from './SchedulingEventsView';
|
||||
export * from './SuspenseEventsView';
|
||||
export * from './TimeAxisMarkersView';
|
||||
export * from './UserTimingMarksView';
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 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 {Rect} from '../../view-base';
|
||||
|
||||
import {rectEqualToRect} from '../../view-base';
|
||||
import {COLORS, FONT_SIZE, TEXT_PADDING} from '../constants';
|
||||
|
||||
const cachedTextWidths = new Map();
|
||||
|
||||
export function trimText(
|
||||
context: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
width: number,
|
||||
): string | null {
|
||||
for (let i = text.length - 1; i >= 0; i--) {
|
||||
const trimmedText = i === text.length - 1 ? text : text.substr(0, i) + '…';
|
||||
|
||||
let measuredWidth = cachedTextWidths.get(trimmedText);
|
||||
if (measuredWidth == null) {
|
||||
measuredWidth = context.measureText(trimmedText).width;
|
||||
cachedTextWidths.set(trimmedText, measuredWidth);
|
||||
}
|
||||
|
||||
if (measuredWidth <= width) {
|
||||
return trimmedText;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function drawText(
|
||||
text: string,
|
||||
context: CanvasRenderingContext2D,
|
||||
fullRect: Rect,
|
||||
drawableRect: Rect,
|
||||
availableWidth: number,
|
||||
textAlign: 'left' | 'center' = 'left',
|
||||
fillStyle: string = COLORS.TEXT_COLOR,
|
||||
): void {
|
||||
if (availableWidth > TEXT_PADDING * 2) {
|
||||
context.textAlign = textAlign;
|
||||
context.textBaseline = 'middle';
|
||||
context.font = `${FONT_SIZE}px sans-serif`;
|
||||
|
||||
const {x, y} = fullRect.origin;
|
||||
|
||||
const trimmedName = trimText(
|
||||
context,
|
||||
text,
|
||||
availableWidth - TEXT_PADDING * 2 + (x < 0 ? x : 0),
|
||||
);
|
||||
|
||||
if (trimmedName !== null) {
|
||||
context.fillStyle = fillStyle;
|
||||
|
||||
// Prevent text from visibly overflowing its container when clipped.
|
||||
const textOverflowsViewableArea = !rectEqualToRect(
|
||||
drawableRect,
|
||||
fullRect,
|
||||
);
|
||||
if (textOverflowsViewableArea) {
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.rect(
|
||||
drawableRect.origin.x,
|
||||
drawableRect.origin.y,
|
||||
drawableRect.size.width,
|
||||
drawableRect.size.height,
|
||||
);
|
||||
context.closePath();
|
||||
context.clip();
|
||||
}
|
||||
|
||||
let textX;
|
||||
if (textAlign === 'center') {
|
||||
textX = x + availableWidth / 2 + TEXT_PADDING - (x < 0 ? x : 0);
|
||||
} else {
|
||||
textX = x + TEXT_PADDING - (x < 0 ? x : 0);
|
||||
}
|
||||
|
||||
const textY = y + fullRect.size.height / 2;
|
||||
|
||||
context.fillText(trimmedName, textX, textY);
|
||||
|
||||
if (textOverflowsViewableArea) {
|
||||
context.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-9
@@ -208,8 +208,9 @@ describe(preprocessData, () => {
|
||||
measures: [],
|
||||
nativeEvents: [],
|
||||
otherUserTimingMarks: [],
|
||||
reactEvents: [],
|
||||
schedulingEvents: [],
|
||||
startTime: 1,
|
||||
suspenseEvents: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -300,16 +301,17 @@ describe(preprocessData, () => {
|
||||
],
|
||||
nativeEvents: [],
|
||||
otherUserTimingMarks: [],
|
||||
reactEvents: [
|
||||
schedulingEvents: [
|
||||
{
|
||||
componentStack: '',
|
||||
laneLabels: [],
|
||||
lanes: [9],
|
||||
timestamp: 0.002,
|
||||
type: 'schedule-render',
|
||||
warning: null,
|
||||
},
|
||||
],
|
||||
startTime: 1,
|
||||
suspenseEvents: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -372,16 +374,17 @@ describe(preprocessData, () => {
|
||||
timestamp: 0.004,
|
||||
},
|
||||
],
|
||||
reactEvents: [
|
||||
schedulingEvents: [
|
||||
{
|
||||
componentStack: '',
|
||||
laneLabels: ['Sync'],
|
||||
lanes: [0],
|
||||
timestamp: 0.005,
|
||||
type: 'schedule-render',
|
||||
warning: null,
|
||||
},
|
||||
],
|
||||
startTime: 1,
|
||||
suspenseEvents: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -507,25 +510,25 @@ describe(preprocessData, () => {
|
||||
timestamp: 0.004,
|
||||
},
|
||||
],
|
||||
reactEvents: [
|
||||
schedulingEvents: [
|
||||
{
|
||||
componentStack: '',
|
||||
laneLabels: ['Default'],
|
||||
lanes: [4],
|
||||
timestamp: 0.005,
|
||||
type: 'schedule-render',
|
||||
warning: null,
|
||||
},
|
||||
{
|
||||
componentName: 'App',
|
||||
componentStack: '',
|
||||
isCascading: false,
|
||||
laneLabels: ['Default'],
|
||||
lanes: [4],
|
||||
timestamp: 0.013,
|
||||
type: 'schedule-state-update',
|
||||
warning: null,
|
||||
},
|
||||
],
|
||||
startTime: 1,
|
||||
suspenseEvents: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+178
-108
@@ -20,6 +20,7 @@ import type {
|
||||
ReactLane,
|
||||
ReactMeasureType,
|
||||
ReactProfilerData,
|
||||
SuspenseEvent,
|
||||
} from '../types';
|
||||
|
||||
import {REACT_TOTAL_NUM_LANES} from '../constants';
|
||||
@@ -34,13 +35,25 @@ type MeasureStackElement = {|
|
||||
|};
|
||||
|
||||
type ProcessorState = {|
|
||||
nextRenderShouldGenerateNewBatchID: boolean,
|
||||
batchUID: BatchUID,
|
||||
uidCounter: BatchUID,
|
||||
measureStack: MeasureStackElement[],
|
||||
nativeEventStack: NativeEvent[],
|
||||
nextRenderShouldGenerateNewBatchID: boolean,
|
||||
uidCounter: BatchUID,
|
||||
unresolvedSuspenseEvents: Map<string, SuspenseEvent>,
|
||||
|};
|
||||
|
||||
let nativeEventStack: Array<NativeEvent> = [];
|
||||
const NATIVE_EVENT_DURATION_THRESHOLD = 20;
|
||||
|
||||
const WARNING_STRINGS = {
|
||||
LONG_EVENT_HANDLER:
|
||||
'An event handler scheduled a big update with React. Consider using the Transition API to defer some of this work.',
|
||||
NESTED_UPDATE:
|
||||
'A nested update was scheduled during layout. These updates require React to re-render synchronously before the browser can paint.',
|
||||
SUSPENDD_DURING_UPATE:
|
||||
'A component suspended during an update which caused a fallback to be shown. ' +
|
||||
"Consider using the Transition API to avoid hiding components after they've been mounted.",
|
||||
};
|
||||
|
||||
// Exported for tests
|
||||
export function getLanesFromTransportDecimalBitmask(
|
||||
@@ -174,44 +187,54 @@ function processTimelineEvent(
|
||||
const stackTrace = args.data.stackTrace;
|
||||
if (stackTrace) {
|
||||
const topFrame = stackTrace[stackTrace.length - 1];
|
||||
if (topFrame.url.includes('node_modules/react-dom')) {
|
||||
if (topFrame.url.includes('/react-dom.')) {
|
||||
// Filter out fake React events dispatched by invokeGuardedCallbackDev.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const timestamp = (ts - currentProfilerData.startTime) / 1000;
|
||||
const duration = event.dur / 1000;
|
||||
// Reduce noise from events like DOMActivate, load/unload, etc. which are usually not relevant
|
||||
if (
|
||||
type.startsWith('blur') ||
|
||||
type.startsWith('click') ||
|
||||
type.startsWith('focus') ||
|
||||
type.startsWith('mouse') ||
|
||||
type.startsWith('pointer')
|
||||
) {
|
||||
const timestamp = (ts - currentProfilerData.startTime) / 1000;
|
||||
const duration = event.dur / 1000;
|
||||
|
||||
let depth = 0;
|
||||
let depth = 0;
|
||||
|
||||
while (nativeEventStack.length > 0) {
|
||||
const prevNativeEvent = nativeEventStack[nativeEventStack.length - 1];
|
||||
const prevStopTime =
|
||||
prevNativeEvent.timestamp + prevNativeEvent.duration;
|
||||
while (state.nativeEventStack.length > 0) {
|
||||
const prevNativeEvent =
|
||||
state.nativeEventStack[state.nativeEventStack.length - 1];
|
||||
const prevStopTime =
|
||||
prevNativeEvent.timestamp + prevNativeEvent.duration;
|
||||
|
||||
if (timestamp < prevStopTime) {
|
||||
depth = prevNativeEvent.depth + 1;
|
||||
break;
|
||||
} else {
|
||||
nativeEventStack.pop();
|
||||
if (timestamp < prevStopTime) {
|
||||
depth = prevNativeEvent.depth + 1;
|
||||
break;
|
||||
} else {
|
||||
state.nativeEventStack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
const nativeEvent = {
|
||||
depth,
|
||||
duration,
|
||||
timestamp,
|
||||
type,
|
||||
warning: null,
|
||||
};
|
||||
|
||||
currentProfilerData.nativeEvents.push(nativeEvent);
|
||||
|
||||
// Keep track of curent event in case future ones overlap.
|
||||
// We separate them into different vertical lanes in this case.
|
||||
state.nativeEventStack.push(nativeEvent);
|
||||
}
|
||||
|
||||
const nativeEvent = {
|
||||
depth,
|
||||
duration,
|
||||
timestamp,
|
||||
type,
|
||||
warnings: null,
|
||||
};
|
||||
|
||||
currentProfilerData.nativeEvents.push(nativeEvent);
|
||||
|
||||
// Keep track of curent event in case future ones overlap.
|
||||
// We separate them into different vertical lanes in this case.
|
||||
nativeEventStack.push(nativeEvent);
|
||||
}
|
||||
break;
|
||||
case 'blink.user_timing':
|
||||
@@ -219,92 +242,141 @@ function processTimelineEvent(
|
||||
|
||||
// React Events - schedule
|
||||
if (name.startsWith('--schedule-render-')) {
|
||||
const [
|
||||
laneBitmaskString,
|
||||
laneLabels,
|
||||
...splitComponentStack
|
||||
] = name.substr(18).split('-');
|
||||
currentProfilerData.reactEvents.push({
|
||||
const [laneBitmaskString, laneLabels] = name.substr(18).split('-');
|
||||
currentProfilerData.schedulingEvents.push({
|
||||
type: 'schedule-render',
|
||||
lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
|
||||
laneLabels: laneLabels ? laneLabels.split(',') : [],
|
||||
componentStack: splitComponentStack.join('-'),
|
||||
timestamp: startTime,
|
||||
warning: null,
|
||||
});
|
||||
} else if (name.startsWith('--schedule-forced-update-')) {
|
||||
const [
|
||||
laneBitmaskString,
|
||||
laneLabels,
|
||||
componentName,
|
||||
...splitComponentStack
|
||||
] = name.substr(25).split('-');
|
||||
const isCascading = !!state.measureStack.find(
|
||||
({type}) => type === 'commit',
|
||||
);
|
||||
currentProfilerData.reactEvents.push({
|
||||
const [laneBitmaskString, laneLabels, componentName] = name
|
||||
.substr(25)
|
||||
.split('-');
|
||||
|
||||
let warning = null;
|
||||
if (state.measureStack.find(({type}) => type === 'commit')) {
|
||||
// TODO (scheduling profiler) Only warn if the subsequent update is longer than some threshold.
|
||||
warning = WARNING_STRINGS.NESTED_UPDATE;
|
||||
}
|
||||
|
||||
currentProfilerData.schedulingEvents.push({
|
||||
type: 'schedule-force-update',
|
||||
lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
|
||||
laneLabels: laneLabels ? laneLabels.split(',') : [],
|
||||
componentName,
|
||||
componentStack: splitComponentStack.join('-'),
|
||||
timestamp: startTime,
|
||||
isCascading,
|
||||
warning,
|
||||
});
|
||||
} else if (name.startsWith('--schedule-state-update-')) {
|
||||
const [
|
||||
laneBitmaskString,
|
||||
laneLabels,
|
||||
componentName,
|
||||
...splitComponentStack
|
||||
] = name.substr(24).split('-');
|
||||
const isCascading = !!state.measureStack.find(
|
||||
({type}) => type === 'commit',
|
||||
);
|
||||
currentProfilerData.reactEvents.push({
|
||||
const [laneBitmaskString, laneLabels, componentName] = name
|
||||
.substr(24)
|
||||
.split('-');
|
||||
|
||||
let warning = null;
|
||||
if (state.measureStack.find(({type}) => type === 'commit')) {
|
||||
// TODO (scheduling profiler) Only warn if the subsequent update is longer than some threshold.
|
||||
warning = WARNING_STRINGS.NESTED_UPDATE;
|
||||
}
|
||||
|
||||
currentProfilerData.schedulingEvents.push({
|
||||
type: 'schedule-state-update',
|
||||
lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
|
||||
laneLabels: laneLabels ? laneLabels.split(',') : [],
|
||||
componentName,
|
||||
componentStack: splitComponentStack.join('-'),
|
||||
timestamp: startTime,
|
||||
isCascading,
|
||||
warning,
|
||||
});
|
||||
} // eslint-disable-line brace-style
|
||||
|
||||
// React Events - suspense
|
||||
else if (name.startsWith('--suspense-suspend-')) {
|
||||
const [id, componentName, ...splitComponentStack] = name
|
||||
.substr(19)
|
||||
.split('-');
|
||||
currentProfilerData.reactEvents.push({
|
||||
type: 'suspense-suspend',
|
||||
id,
|
||||
componentName,
|
||||
componentStack: splitComponentStack.join('-'),
|
||||
timestamp: startTime,
|
||||
const [id, componentName, ...rest] = name.substr(19).split('-');
|
||||
|
||||
// Older versions of the scheduling profiler data didn't contain phase or lane values.
|
||||
let phase = null;
|
||||
let warning = null;
|
||||
if (rest.length === 3) {
|
||||
switch (rest[0]) {
|
||||
case 'mount':
|
||||
case 'update':
|
||||
phase = rest[0];
|
||||
break;
|
||||
}
|
||||
|
||||
if (phase === 'update') {
|
||||
const laneLabels = rest[2];
|
||||
// HACK This is a bit gross but the numeric lane value might change between render versions.
|
||||
if (!laneLabels.includes('Transition')) {
|
||||
warning = WARNING_STRINGS.SUSPENDD_DURING_UPATE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const availableDepths = new Array(
|
||||
state.unresolvedSuspenseEvents.size + 1,
|
||||
).fill(true);
|
||||
state.unresolvedSuspenseEvents.forEach(({depth}) => {
|
||||
availableDepths[depth] = false;
|
||||
});
|
||||
|
||||
let depth = 0;
|
||||
for (let i = 0; i < availableDepths.length; i++) {
|
||||
if (availableDepths[i]) {
|
||||
depth = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO (scheduling profiler) Maybe we should calculate depth in post,
|
||||
// so unresolved Suspense requests don't take up space.
|
||||
// We can't know if they'll be resolved or not at this point.
|
||||
// We'll just give them a default (fake) duration width.
|
||||
|
||||
const suspenseEvent = {
|
||||
componentName,
|
||||
depth,
|
||||
duration: null,
|
||||
id,
|
||||
phase,
|
||||
resolution: 'unresolved',
|
||||
resuspendTimestamps: null,
|
||||
timestamp: startTime,
|
||||
type: 'suspense',
|
||||
warning,
|
||||
};
|
||||
|
||||
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, componentName, ...splitComponentStack] = name
|
||||
.substr(20)
|
||||
.split('-');
|
||||
currentProfilerData.reactEvents.push({
|
||||
type: 'suspense-resolved',
|
||||
id,
|
||||
componentName,
|
||||
componentStack: splitComponentStack.join('-'),
|
||||
timestamp: startTime,
|
||||
});
|
||||
const [id] = name.substr(20).split('-');
|
||||
const suspenseEvent = state.unresolvedSuspenseEvents.get(id);
|
||||
if (suspenseEvent != null) {
|
||||
state.unresolvedSuspenseEvents.delete(id);
|
||||
|
||||
suspenseEvent.duration = startTime - suspenseEvent.timestamp;
|
||||
suspenseEvent.resolution = 'resolved';
|
||||
}
|
||||
} else if (name.startsWith('--suspense-rejected-')) {
|
||||
const [id, componentName, ...splitComponentStack] = name
|
||||
.substr(20)
|
||||
.split('-');
|
||||
currentProfilerData.reactEvents.push({
|
||||
type: 'suspense-rejected',
|
||||
id,
|
||||
componentName,
|
||||
componentStack: splitComponentStack.join('-'),
|
||||
timestamp: startTime,
|
||||
});
|
||||
const [id] = name.substr(20).split('-');
|
||||
const suspenseEvent = state.unresolvedSuspenseEvents.get(id);
|
||||
if (suspenseEvent != null) {
|
||||
state.unresolvedSuspenseEvents.delete(id);
|
||||
|
||||
suspenseEvent.duration = startTime - suspenseEvent.timestamp;
|
||||
suspenseEvent.resolution = 'rejected';
|
||||
}
|
||||
} // eslint-disable-line brace-style
|
||||
|
||||
// React Measures - render
|
||||
@@ -335,17 +407,14 @@ function processTimelineEvent(
|
||||
state,
|
||||
);
|
||||
|
||||
for (let i = 0; i < nativeEventStack.length; i++) {
|
||||
const nativeEvent = nativeEventStack[i];
|
||||
for (let i = 0; i < state.nativeEventStack.length; i++) {
|
||||
const nativeEvent = state.nativeEventStack[i];
|
||||
const stopTime = nativeEvent.timestamp + nativeEvent.duration;
|
||||
if (stopTime > startTime) {
|
||||
const warning =
|
||||
'An event handler scheduled a synchronous update with React.';
|
||||
if (nativeEvent.warnings === null) {
|
||||
nativeEvent.warnings = new Set([warning]);
|
||||
} else {
|
||||
nativeEvent.warnings.add(warning);
|
||||
}
|
||||
if (
|
||||
stopTime > startTime &&
|
||||
nativeEvent.duration > NATIVE_EVENT_DURATION_THRESHOLD
|
||||
) {
|
||||
nativeEvent.warning = WARNING_STRINGS.LONG_EVENT_HANDLER;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
@@ -509,18 +578,17 @@ function preprocessFlamechart(rawData: TimelineEvent[]): Flamechart {
|
||||
export default function preprocessData(
|
||||
timeline: TimelineEvent[],
|
||||
): ReactProfilerData {
|
||||
nativeEventStack = [];
|
||||
|
||||
const flamechart = preprocessFlamechart(timeline);
|
||||
|
||||
const profilerData: ReactProfilerData = {
|
||||
startTime: 0,
|
||||
duration: 0,
|
||||
nativeEvents: [],
|
||||
reactEvents: [],
|
||||
measures: [],
|
||||
flamechart,
|
||||
measures: [],
|
||||
nativeEvents: [],
|
||||
otherUserTimingMarks: [],
|
||||
schedulingEvents: [],
|
||||
startTime: 0,
|
||||
suspenseEvents: [],
|
||||
};
|
||||
|
||||
// Sort `timeline`. JSON Array Format trace events need not be ordered. See:
|
||||
@@ -551,9 +619,11 @@ export default function preprocessData(
|
||||
|
||||
const state: ProcessorState = {
|
||||
batchUID: 0,
|
||||
uidCounter: 0,
|
||||
nextRenderShouldGenerateNewBatchID: true,
|
||||
measureStack: [],
|
||||
nativeEventStack: [],
|
||||
nextRenderShouldGenerateNewBatchID: true,
|
||||
uidCounter: 0,
|
||||
unresolvedSuspenseEvents: new Map(),
|
||||
};
|
||||
|
||||
timeline.forEach(event => processTimelineEvent(event, profilerData, state));
|
||||
|
||||
+25
-34
@@ -26,13 +26,13 @@ export type NativeEvent = {|
|
||||
+duration: Milliseconds,
|
||||
+timestamp: Milliseconds,
|
||||
+type: string,
|
||||
warnings: Set<string> | null,
|
||||
warning: string | null,
|
||||
|};
|
||||
|
||||
type BaseReactEvent = {|
|
||||
+componentName?: string,
|
||||
+componentStack?: string,
|
||||
+timestamp: Milliseconds,
|
||||
warning: string | null,
|
||||
|};
|
||||
|
||||
type BaseReactScheduleEvent = {|
|
||||
@@ -42,44 +42,33 @@ type BaseReactScheduleEvent = {|
|
||||
|};
|
||||
export type ReactScheduleRenderEvent = {|
|
||||
...BaseReactScheduleEvent,
|
||||
type: 'schedule-render',
|
||||
+type: 'schedule-render',
|
||||
|};
|
||||
export type ReactScheduleStateUpdateEvent = {|
|
||||
...BaseReactScheduleEvent,
|
||||
type: 'schedule-state-update',
|
||||
isCascading: boolean,
|
||||
+type: 'schedule-state-update',
|
||||
|};
|
||||
export type ReactScheduleForceUpdateEvent = {|
|
||||
...BaseReactScheduleEvent,
|
||||
type: 'schedule-force-update',
|
||||
isCascading: boolean,
|
||||
+type: 'schedule-force-update',
|
||||
|};
|
||||
|
||||
type BaseReactSuspenseEvent = {|
|
||||
export type SuspenseEvent = {|
|
||||
...BaseReactEvent,
|
||||
id: string,
|
||||
|};
|
||||
export type ReactSuspenseSuspendEvent = {|
|
||||
...BaseReactSuspenseEvent,
|
||||
type: 'suspense-suspend',
|
||||
|};
|
||||
export type ReactSuspenseResolvedEvent = {|
|
||||
...BaseReactSuspenseEvent,
|
||||
type: 'suspense-resolved',
|
||||
|};
|
||||
export type ReactSuspenseRejectedEvent = {|
|
||||
...BaseReactSuspenseEvent,
|
||||
type: 'suspense-rejected',
|
||||
depth: number,
|
||||
duration: number | null,
|
||||
+id: string,
|
||||
+phase: 'mount' | 'update' | null,
|
||||
resolution: 'rejected' | 'resolved' | 'unresolved',
|
||||
resuspendTimestamps: Array<number> | null,
|
||||
+type: 'suspense',
|
||||
|};
|
||||
|
||||
export type ReactEvent =
|
||||
export type SchedulingEvent =
|
||||
| ReactScheduleRenderEvent
|
||||
| ReactScheduleStateUpdateEvent
|
||||
| ReactScheduleForceUpdateEvent
|
||||
| ReactSuspenseSuspendEvent
|
||||
| ReactSuspenseResolvedEvent
|
||||
| ReactSuspenseRejectedEvent;
|
||||
export type ReactEventType = $PropertyType<ReactEvent, 'type'>;
|
||||
| ReactScheduleForceUpdateEvent;
|
||||
export type SchedulingEventType = $PropertyType<SchedulingEvent, 'type'>;
|
||||
|
||||
export type ReactMeasureType =
|
||||
| 'commit'
|
||||
@@ -128,20 +117,22 @@ export type FlamechartStackLayer = FlamechartStackFrame[];
|
||||
export type Flamechart = FlamechartStackLayer[];
|
||||
|
||||
export type ReactProfilerData = {|
|
||||
startTime: number,
|
||||
duration: number,
|
||||
nativeEvents: NativeEvent[],
|
||||
reactEvents: ReactEvent[],
|
||||
measures: ReactMeasure[],
|
||||
flamechart: Flamechart,
|
||||
measures: ReactMeasure[],
|
||||
nativeEvents: NativeEvent[],
|
||||
otherUserTimingMarks: UserTimingMark[],
|
||||
schedulingEvents: SchedulingEvent[],
|
||||
startTime: number,
|
||||
suspenseEvents: SuspenseEvent[],
|
||||
|};
|
||||
|
||||
export type ReactHoverContextInfo = {|
|
||||
nativeEvent: NativeEvent | null,
|
||||
reactEvent: ReactEvent | null,
|
||||
measure: ReactMeasure | null,
|
||||
data: $ReadOnly<ReactProfilerData> | null,
|
||||
flamechartStackFrame: FlamechartStackFrame | null,
|
||||
measure: ReactMeasure | null,
|
||||
nativeEvent: NativeEvent | null,
|
||||
schedulingEvent: SchedulingEvent | null,
|
||||
suspenseEvent: SuspenseEvent | null,
|
||||
userTimingMark: UserTimingMark | null,
|
||||
|};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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 prettyMilliseconds from 'pretty-ms';
|
||||
|
||||
export function formatTimestamp(ms: number) {
|
||||
return (
|
||||
ms.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1,
|
||||
}) + 'ms'
|
||||
);
|
||||
}
|
||||
|
||||
export function formatDuration(ms: number) {
|
||||
return prettyMilliseconds(ms, {millisecondsDecimalDigits: 1});
|
||||
}
|
||||
|
||||
export function trimString(string: string, length: number): string {
|
||||
if (string.length > length) {
|
||||
return `${string.substr(0, length - 1)}…`;
|
||||
}
|
||||
return string;
|
||||
}
|
||||
@@ -9,7 +9,8 @@
|
||||
|
||||
import {useLayoutEffect, useRef} from 'react';
|
||||
|
||||
const TOOLTIP_OFFSET = 4;
|
||||
const TOOLTIP_OFFSET_BOTTOM = 10;
|
||||
const TOOLTIP_OFFSET_TOP = 5;
|
||||
|
||||
export default function useSmartTooltip({
|
||||
canvasRef,
|
||||
@@ -37,41 +38,41 @@ export default function useSmartTooltip({
|
||||
const element = ref.current;
|
||||
if (element !== null) {
|
||||
// Let's check the vertical position.
|
||||
if (mouseY + TOOLTIP_OFFSET + element.offsetHeight >= height) {
|
||||
if (mouseY + TOOLTIP_OFFSET_BOTTOM + element.offsetHeight >= height) {
|
||||
// The tooltip doesn't fit below the mouse cursor (which is our
|
||||
// default strategy). Therefore we try to position it either above the
|
||||
// mouse cursor or finally aligned with the window's top edge.
|
||||
if (mouseY - TOOLTIP_OFFSET - element.offsetHeight > 0) {
|
||||
if (mouseY - TOOLTIP_OFFSET_TOP - element.offsetHeight > 0) {
|
||||
// We position the tooltip above the mouse cursor if it fits there.
|
||||
element.style.top = `${mouseY -
|
||||
element.offsetHeight -
|
||||
TOOLTIP_OFFSET}px`;
|
||||
TOOLTIP_OFFSET_TOP}px`;
|
||||
} else {
|
||||
// Otherwise we align the tooltip with the window's top edge.
|
||||
element.style.top = '0px';
|
||||
}
|
||||
} else {
|
||||
element.style.top = `${mouseY + TOOLTIP_OFFSET}px`;
|
||||
element.style.top = `${mouseY + TOOLTIP_OFFSET_BOTTOM}px`;
|
||||
}
|
||||
|
||||
// Now let's check the horizontal position.
|
||||
if (mouseX + TOOLTIP_OFFSET + element.offsetWidth >= width) {
|
||||
if (mouseX + TOOLTIP_OFFSET_BOTTOM + element.offsetWidth >= width) {
|
||||
// The tooltip doesn't fit at the right of the mouse cursor (which is
|
||||
// our default strategy). Therefore we try to position it either at the
|
||||
// left of the mouse cursor or finally aligned with the window's left
|
||||
// edge.
|
||||
if (mouseX - TOOLTIP_OFFSET - element.offsetWidth > 0) {
|
||||
if (mouseX - TOOLTIP_OFFSET_TOP - element.offsetWidth > 0) {
|
||||
// We position the tooltip at the left of the mouse cursor if it fits
|
||||
// there.
|
||||
element.style.left = `${mouseX -
|
||||
element.offsetWidth -
|
||||
TOOLTIP_OFFSET}px`;
|
||||
TOOLTIP_OFFSET_TOP}px`;
|
||||
} else {
|
||||
// Otherwise, align the tooltip with the window's left edge.
|
||||
element.style.left = '0px';
|
||||
}
|
||||
} else {
|
||||
element.style.left = `${mouseX + TOOLTIP_OFFSET}px`;
|
||||
element.style.left = `${mouseX + TOOLTIP_OFFSET_BOTTOM}px`;
|
||||
}
|
||||
}
|
||||
}, [mouseX, mouseY, ref]);
|
||||
|
||||
+5
-21
@@ -7,33 +7,17 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {Rect} from './geometry';
|
||||
|
||||
import {Surface} from './Surface';
|
||||
import {View} from './View';
|
||||
import {COLORS} from '../content-views/constants';
|
||||
|
||||
/**
|
||||
* View that fills its visible area with a CSS color.
|
||||
*/
|
||||
export class ColorView extends View {
|
||||
_color: string;
|
||||
|
||||
constructor(surface: Surface, frame: Rect, color: string) {
|
||||
super(surface, frame);
|
||||
this._color = color;
|
||||
}
|
||||
|
||||
setColor(color: string) {
|
||||
if (this._color === color) {
|
||||
return;
|
||||
}
|
||||
this._color = color;
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
|
||||
export class BackgroundColorView extends View {
|
||||
draw(context: CanvasRenderingContext2D) {
|
||||
const {_color, visibleArea} = this;
|
||||
context.fillStyle = _color;
|
||||
const {visibleArea} = this;
|
||||
|
||||
context.fillStyle = COLORS.BACKGROUND;
|
||||
context.fillRect(
|
||||
visibleArea.origin.x,
|
||||
visibleArea.origin.y,
|
||||
+35
-51
@@ -14,8 +14,6 @@ import type {
|
||||
MouseUpInteraction,
|
||||
WheelPlainInteraction,
|
||||
WheelWithShiftInteraction,
|
||||
WheelWithControlInteraction,
|
||||
WheelWithMetaInteraction,
|
||||
} from './useCanvasInteraction';
|
||||
import type {Rect} from './geometry';
|
||||
import type {ScrollState} from './utils/scrollState';
|
||||
@@ -202,7 +200,7 @@ export class HorizontalPanAndZoomView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
_handleWheelPlain(interaction: WheelPlainInteraction) {
|
||||
_handleWheel(interaction: WheelPlainInteraction | WheelWithShiftInteraction) {
|
||||
const {
|
||||
location,
|
||||
delta: {deltaX, deltaY},
|
||||
@@ -214,51 +212,41 @@ export class HorizontalPanAndZoomView extends View {
|
||||
|
||||
const absDeltaX = Math.abs(deltaX);
|
||||
const absDeltaY = Math.abs(deltaY);
|
||||
|
||||
// Vertical scrolling zooms in and out (unless the SHIFT modifier is used).
|
||||
// Horizontal scrolling pans.
|
||||
if (absDeltaY > absDeltaX) {
|
||||
return; // Scrolling vertically
|
||||
if (absDeltaY < MOVE_WHEEL_DELTA_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (interaction.type === 'wheel-shift') {
|
||||
// Shift modifier is for scrolling, not zooming.
|
||||
return;
|
||||
}
|
||||
|
||||
const newState = zoomState({
|
||||
state: this._scrollState,
|
||||
multiplier: 1 + 0.005 * -deltaY,
|
||||
fixedPoint: location.x - this._scrollState.offset,
|
||||
|
||||
minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
|
||||
maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
|
||||
containerLength: this.frame.size.width,
|
||||
});
|
||||
this._setStateAndInformCallbacksIfChanged(newState);
|
||||
} else {
|
||||
if (absDeltaX < MOVE_WHEEL_DELTA_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newState = translateState({
|
||||
state: this._scrollState,
|
||||
delta: -deltaX,
|
||||
containerLength: this.frame.size.width,
|
||||
});
|
||||
this._setStateAndInformCallbacksIfChanged(newState);
|
||||
}
|
||||
if (absDeltaX < MOVE_WHEEL_DELTA_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newState = translateState({
|
||||
state: this._scrollState,
|
||||
delta: -deltaX,
|
||||
containerLength: this.frame.size.width,
|
||||
});
|
||||
this._setStateAndInformCallbacksIfChanged(newState);
|
||||
}
|
||||
|
||||
_handleWheelZoom(
|
||||
interaction:
|
||||
| WheelWithShiftInteraction
|
||||
| WheelWithControlInteraction
|
||||
| WheelWithMetaInteraction,
|
||||
) {
|
||||
const {
|
||||
location,
|
||||
delta: {deltaY},
|
||||
} = interaction.payload;
|
||||
|
||||
if (!rectContainsPoint(location, this.frame)) {
|
||||
return; // Not scrolling on view
|
||||
}
|
||||
|
||||
const absDeltaY = Math.abs(deltaY);
|
||||
if (absDeltaY < MOVE_WHEEL_DELTA_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newState = zoomState({
|
||||
state: this._scrollState,
|
||||
multiplier: 1 + 0.005 * -deltaY,
|
||||
fixedPoint: location.x - this._scrollState.offset,
|
||||
|
||||
minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
|
||||
maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
|
||||
containerLength: this.frame.size.width,
|
||||
});
|
||||
this._setStateAndInformCallbacksIfChanged(newState);
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
|
||||
@@ -273,12 +261,8 @@ export class HorizontalPanAndZoomView extends View {
|
||||
this._handleMouseUp(interaction, viewRefs);
|
||||
break;
|
||||
case 'wheel-plain':
|
||||
this._handleWheelPlain(interaction);
|
||||
break;
|
||||
case 'wheel-shift':
|
||||
case 'wheel-control':
|
||||
case 'wheel-meta':
|
||||
this._handleWheelZoom(interaction);
|
||||
this._handleWheel(interaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
-339
@@ -1,339 +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 type {
|
||||
Interaction,
|
||||
MouseDownInteraction,
|
||||
MouseMoveInteraction,
|
||||
MouseUpInteraction,
|
||||
} from './useCanvasInteraction';
|
||||
import type {Rect, Size} from './geometry';
|
||||
import type {ViewRefs} from './Surface';
|
||||
|
||||
import {COLORS} from '../content-views/constants';
|
||||
import nullthrows from 'nullthrows';
|
||||
import {Surface} from './Surface';
|
||||
import {View} from './View';
|
||||
import {rectContainsPoint} from './geometry';
|
||||
import {layeredLayout, noopLayout} from './layouter';
|
||||
import {ColorView} from './ColorView';
|
||||
import {clamp} from './utils/clamp';
|
||||
|
||||
type ResizeBarState = 'normal' | 'hovered' | 'dragging';
|
||||
|
||||
type ResizingState = $ReadOnly<{|
|
||||
/** Distance between top of resize bar and mouseY */
|
||||
cursorOffsetInBarFrame: number,
|
||||
/** Mouse's vertical coordinates relative to canvas */
|
||||
mouseY: number,
|
||||
|}>;
|
||||
|
||||
type LayoutState = $ReadOnly<{|
|
||||
/** Resize bar's vertical position relative to resize view's frame.origin.y */
|
||||
barOffsetY: number,
|
||||
|}>;
|
||||
|
||||
function getColorForBarState(state: ResizeBarState): string {
|
||||
switch (state) {
|
||||
case 'normal':
|
||||
case 'hovered':
|
||||
case 'dragging':
|
||||
return COLORS.REACT_RESIZE_BAR;
|
||||
}
|
||||
throw new Error(`Unknown resize bar state ${state}`);
|
||||
}
|
||||
|
||||
class ResizeBar extends View {
|
||||
_intrinsicContentSize: Size = {
|
||||
width: 0,
|
||||
height: 5,
|
||||
};
|
||||
|
||||
_interactionState: ResizeBarState = 'normal';
|
||||
|
||||
constructor(surface: Surface, frame: Rect) {
|
||||
super(surface, frame, layeredLayout);
|
||||
this.addSubview(new ColorView(surface, frame, ''));
|
||||
this._updateColor();
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
return this._intrinsicContentSize;
|
||||
}
|
||||
|
||||
_getColorView(): ColorView {
|
||||
return (this.subviews[0]: any);
|
||||
}
|
||||
|
||||
_updateColor() {
|
||||
this._getColorView().setColor(getColorForBarState(this._interactionState));
|
||||
}
|
||||
|
||||
_setInteractionState(state: ResizeBarState) {
|
||||
if (this._interactionState === state) {
|
||||
return;
|
||||
}
|
||||
this._interactionState = state;
|
||||
this._updateColor();
|
||||
}
|
||||
|
||||
_handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) {
|
||||
const cursorInView = rectContainsPoint(
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
);
|
||||
if (cursorInView) {
|
||||
this._setInteractionState('dragging');
|
||||
viewRefs.activeView = this;
|
||||
}
|
||||
}
|
||||
|
||||
_handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
|
||||
const cursorInView = rectContainsPoint(
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
);
|
||||
|
||||
if (cursorInView || viewRefs.activeView === this) {
|
||||
this.currentCursor = 'ns-resize';
|
||||
}
|
||||
if (cursorInView) {
|
||||
viewRefs.hoveredView = this;
|
||||
}
|
||||
|
||||
if (this._interactionState === 'dragging') {
|
||||
return;
|
||||
}
|
||||
this._setInteractionState(cursorInView ? 'hovered' : 'normal');
|
||||
}
|
||||
|
||||
_handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) {
|
||||
const cursorInView = rectContainsPoint(
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
);
|
||||
if (this._interactionState === 'dragging') {
|
||||
this._setInteractionState(cursorInView ? 'hovered' : 'normal');
|
||||
}
|
||||
|
||||
if (viewRefs.activeView === this) {
|
||||
viewRefs.activeView = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
|
||||
switch (interaction.type) {
|
||||
case 'mousedown':
|
||||
this._handleMouseDown(interaction, viewRefs);
|
||||
return;
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction, viewRefs);
|
||||
return;
|
||||
case 'mouseup':
|
||||
this._handleMouseUp(interaction, viewRefs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ResizableSplitView extends View {
|
||||
_canvasRef: {current: HTMLCanvasElement | null};
|
||||
_resizingState: ResizingState | null = null;
|
||||
_layoutState: LayoutState;
|
||||
|
||||
constructor(
|
||||
surface: Surface,
|
||||
frame: Rect,
|
||||
topSubview: View,
|
||||
bottomSubview: View,
|
||||
canvasRef: {current: HTMLCanvasElement | null},
|
||||
) {
|
||||
super(surface, frame, noopLayout);
|
||||
|
||||
this._canvasRef = canvasRef;
|
||||
|
||||
this.addSubview(topSubview);
|
||||
this.addSubview(new ResizeBar(surface, frame));
|
||||
this.addSubview(bottomSubview);
|
||||
|
||||
const topSubviewDesiredSize = topSubview.desiredSize();
|
||||
this._layoutState = {
|
||||
barOffsetY: topSubviewDesiredSize ? topSubviewDesiredSize.height : 0,
|
||||
};
|
||||
}
|
||||
|
||||
_getTopSubview(): View {
|
||||
return this.subviews[0];
|
||||
}
|
||||
|
||||
_getResizeBar(): View {
|
||||
return this.subviews[1];
|
||||
}
|
||||
|
||||
_getBottomSubview(): View {
|
||||
return this.subviews[2];
|
||||
}
|
||||
|
||||
_getResizeBarDesiredSize(): Size {
|
||||
return nullthrows(
|
||||
this._getResizeBar().desiredSize(),
|
||||
'Resize bar must have desired size',
|
||||
);
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
const topSubviewDesiredSize = this._getTopSubview().desiredSize();
|
||||
const resizeBarDesiredSize = this._getResizeBarDesiredSize();
|
||||
const bottomSubviewDesiredSize = this._getBottomSubview().desiredSize();
|
||||
|
||||
const topSubviewDesiredWidth = topSubviewDesiredSize
|
||||
? topSubviewDesiredSize.width
|
||||
: 0;
|
||||
const bottomSubviewDesiredWidth = bottomSubviewDesiredSize
|
||||
? bottomSubviewDesiredSize.width
|
||||
: 0;
|
||||
|
||||
const topSubviewDesiredHeight = topSubviewDesiredSize
|
||||
? topSubviewDesiredSize.height
|
||||
: 0;
|
||||
const bottomSubviewDesiredHeight = bottomSubviewDesiredSize
|
||||
? bottomSubviewDesiredSize.height
|
||||
: 0;
|
||||
|
||||
return {
|
||||
width: Math.max(
|
||||
topSubviewDesiredWidth,
|
||||
resizeBarDesiredSize.width,
|
||||
bottomSubviewDesiredWidth,
|
||||
),
|
||||
height:
|
||||
topSubviewDesiredHeight +
|
||||
resizeBarDesiredSize.height +
|
||||
bottomSubviewDesiredHeight,
|
||||
};
|
||||
}
|
||||
|
||||
layoutSubviews() {
|
||||
this._updateLayoutState();
|
||||
this._updateSubviewFrames();
|
||||
super.layoutSubviews();
|
||||
}
|
||||
|
||||
_updateLayoutState() {
|
||||
const {frame, visibleArea, _resizingState} = this;
|
||||
|
||||
const resizeBarDesiredSize = this._getResizeBarDesiredSize();
|
||||
// Allow bar to travel to bottom of the visible area of this view but no further
|
||||
const maxPossibleBarOffset =
|
||||
visibleArea.size.height - resizeBarDesiredSize.height;
|
||||
const topSubviewDesiredSize = this._getTopSubview().desiredSize();
|
||||
const maxBarOffset = topSubviewDesiredSize
|
||||
? Math.min(maxPossibleBarOffset, topSubviewDesiredSize.height)
|
||||
: maxPossibleBarOffset;
|
||||
|
||||
let proposedBarOffsetY = this._layoutState.barOffsetY;
|
||||
// Update bar offset if dragging bar
|
||||
if (_resizingState) {
|
||||
const {mouseY, cursorOffsetInBarFrame} = _resizingState;
|
||||
proposedBarOffsetY = mouseY - frame.origin.y - cursorOffsetInBarFrame;
|
||||
}
|
||||
|
||||
this._layoutState = {
|
||||
...this._layoutState,
|
||||
barOffsetY: clamp(0, maxBarOffset, proposedBarOffsetY),
|
||||
};
|
||||
}
|
||||
|
||||
_updateSubviewFrames() {
|
||||
const {
|
||||
frame: {
|
||||
origin: {x, y},
|
||||
size: {width, height},
|
||||
},
|
||||
_layoutState: {barOffsetY},
|
||||
} = this;
|
||||
|
||||
const resizeBarDesiredSize = this._getResizeBarDesiredSize();
|
||||
|
||||
let currentY = y;
|
||||
|
||||
this._getTopSubview().setFrame({
|
||||
origin: {x, y: currentY},
|
||||
size: {width, height: barOffsetY},
|
||||
});
|
||||
currentY += this._getTopSubview().frame.size.height;
|
||||
|
||||
this._getResizeBar().setFrame({
|
||||
origin: {x, y: currentY},
|
||||
size: {width, height: resizeBarDesiredSize.height},
|
||||
});
|
||||
currentY += this._getResizeBar().frame.size.height;
|
||||
|
||||
this._getBottomSubview().setFrame({
|
||||
origin: {x, y: currentY},
|
||||
// Fill remaining height
|
||||
size: {width, height: height + y - currentY},
|
||||
});
|
||||
}
|
||||
|
||||
_handleMouseDown(interaction: MouseDownInteraction) {
|
||||
const cursorLocation = interaction.payload.location;
|
||||
const resizeBarFrame = this._getResizeBar().frame;
|
||||
if (rectContainsPoint(cursorLocation, resizeBarFrame)) {
|
||||
const mouseY = cursorLocation.y;
|
||||
this._resizingState = {
|
||||
cursorOffsetInBarFrame: mouseY - resizeBarFrame.origin.y,
|
||||
mouseY,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
_handleMouseMove(interaction: MouseMoveInteraction) {
|
||||
const {_resizingState} = this;
|
||||
if (_resizingState) {
|
||||
this._resizingState = {
|
||||
..._resizingState,
|
||||
mouseY: interaction.payload.location.y,
|
||||
};
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
_handleMouseUp(interaction: MouseUpInteraction) {
|
||||
if (this._resizingState) {
|
||||
this._resizingState = null;
|
||||
}
|
||||
}
|
||||
|
||||
_didGrab: boolean = false;
|
||||
|
||||
getCursorActiveSubView(interaction: Interaction): View | null {
|
||||
const cursorLocation = interaction.payload.location;
|
||||
const resizeBarFrame = this._getResizeBar().frame;
|
||||
if (rectContainsPoint(cursorLocation, resizeBarFrame)) {
|
||||
return this;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
|
||||
switch (interaction.type) {
|
||||
case 'mousedown':
|
||||
this._handleMouseDown(interaction);
|
||||
return;
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction);
|
||||
return;
|
||||
case 'mouseup':
|
||||
this._handleMouseUp(interaction);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
/**
|
||||
* 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 {
|
||||
ClickInteraction,
|
||||
DoubleClickInteraction,
|
||||
Interaction,
|
||||
MouseDownInteraction,
|
||||
MouseMoveInteraction,
|
||||
MouseUpInteraction,
|
||||
} from './useCanvasInteraction';
|
||||
import type {Rect} from './geometry';
|
||||
import type {ViewRefs} from './Surface';
|
||||
|
||||
import {BORDER_SIZE, COLORS} from '../content-views/constants';
|
||||
import {drawText} from '../content-views/utils/text';
|
||||
import {Surface} from './Surface';
|
||||
import {View} from './View';
|
||||
import {intersectionOfRects, rectContainsPoint} from './geometry';
|
||||
import {noopLayout} from './layouter';
|
||||
import {clamp} from './utils/clamp';
|
||||
|
||||
type ResizeBarState = 'normal' | 'hovered' | 'dragging';
|
||||
|
||||
type ResizingState = $ReadOnly<{|
|
||||
/** Distance between top of resize bar and mouseY */
|
||||
cursorOffsetInBarFrame: number,
|
||||
/** Mouse's vertical coordinates relative to canvas */
|
||||
mouseY: number,
|
||||
|}>;
|
||||
|
||||
type LayoutState = $ReadOnly<{|
|
||||
/** Resize bar's vertical position relative to resize view's frame.origin.y */
|
||||
barOffsetY: number,
|
||||
|}>;
|
||||
|
||||
const RESIZE_BAR_DOT_RADIUS = 1;
|
||||
const RESIZE_BAR_DOT_SPACING = 4;
|
||||
const RESIZE_BAR_HEIGHT = 8;
|
||||
const RESIZE_BAR_WITH_LABEL_HEIGHT = 16;
|
||||
|
||||
const HIDDEN_RECT = {
|
||||
origin: {x: 0, y: 0},
|
||||
size: {width: 0, height: 0},
|
||||
};
|
||||
|
||||
class ResizeBar extends View {
|
||||
_interactionState: ResizeBarState = 'normal';
|
||||
_label: string;
|
||||
|
||||
showLabel: boolean = false;
|
||||
|
||||
constructor(surface: Surface, frame: Rect, label: string) {
|
||||
super(surface, frame, noopLayout);
|
||||
|
||||
this._label = label;
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
return this.showLabel
|
||||
? {height: RESIZE_BAR_WITH_LABEL_HEIGHT, width: 0}
|
||||
: {height: RESIZE_BAR_HEIGHT, width: 0};
|
||||
}
|
||||
|
||||
draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
|
||||
const {visibleArea} = this;
|
||||
const {x, y} = visibleArea.origin;
|
||||
const {width, height} = visibleArea.size;
|
||||
|
||||
const isActive =
|
||||
this._interactionState === 'dragging' ||
|
||||
(this._interactionState === 'hovered' && viewRefs.activeView === null);
|
||||
|
||||
context.fillStyle = isActive
|
||||
? COLORS.REACT_RESIZE_BAR_ACTIVE
|
||||
: COLORS.REACT_RESIZE_BAR;
|
||||
context.fillRect(x, y, width, height);
|
||||
|
||||
context.fillStyle = COLORS.REACT_RESIZE_BAR_BORDER;
|
||||
context.fillRect(x, y, width, BORDER_SIZE);
|
||||
context.fillRect(x, y + height - BORDER_SIZE, width, BORDER_SIZE);
|
||||
|
||||
const horizontalCenter = x + width / 2;
|
||||
const verticalCenter = y + height / 2;
|
||||
|
||||
if (this.showLabel) {
|
||||
// When the resize view is collapsed entirely,
|
||||
// rather than showing a resize bar– this view displays a label.
|
||||
const labelRect: Rect = {
|
||||
origin: {
|
||||
x: 0,
|
||||
y: y + height - RESIZE_BAR_WITH_LABEL_HEIGHT,
|
||||
},
|
||||
size: {
|
||||
width: visibleArea.size.width,
|
||||
height: visibleArea.size.height,
|
||||
},
|
||||
};
|
||||
|
||||
const drawableRect = intersectionOfRects(labelRect, this.visibleArea);
|
||||
|
||||
drawText(
|
||||
this._label,
|
||||
context,
|
||||
labelRect,
|
||||
drawableRect,
|
||||
visibleArea.size.width,
|
||||
'center',
|
||||
COLORS.REACT_RESIZE_BAR_DOT,
|
||||
);
|
||||
} else {
|
||||
// Otherwise draw horizontally centered resize bar dots
|
||||
context.beginPath();
|
||||
context.fillStyle = COLORS.REACT_RESIZE_BAR_DOT;
|
||||
context.arc(
|
||||
horizontalCenter,
|
||||
verticalCenter,
|
||||
RESIZE_BAR_DOT_RADIUS,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
);
|
||||
context.arc(
|
||||
horizontalCenter + RESIZE_BAR_DOT_SPACING,
|
||||
verticalCenter,
|
||||
RESIZE_BAR_DOT_RADIUS,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
);
|
||||
context.arc(
|
||||
horizontalCenter - RESIZE_BAR_DOT_SPACING,
|
||||
verticalCenter,
|
||||
RESIZE_BAR_DOT_RADIUS,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
);
|
||||
context.fill();
|
||||
}
|
||||
}
|
||||
|
||||
_setInteractionState(state: ResizeBarState) {
|
||||
if (this._interactionState === state) {
|
||||
return;
|
||||
}
|
||||
this._interactionState = state;
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
|
||||
_handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) {
|
||||
const cursorInView = rectContainsPoint(
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
);
|
||||
if (cursorInView) {
|
||||
this._setInteractionState('dragging');
|
||||
viewRefs.activeView = this;
|
||||
}
|
||||
}
|
||||
|
||||
_handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
|
||||
const cursorInView = rectContainsPoint(
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
);
|
||||
|
||||
if (viewRefs.activeView === this) {
|
||||
// If we're actively dragging this resize bar,
|
||||
// show the cursor even if the pointer isn't hovering over this view.
|
||||
this.currentCursor = 'ns-resize';
|
||||
} else if (cursorInView) {
|
||||
if (this.showLabel) {
|
||||
this.currentCursor = 'pointer';
|
||||
} else {
|
||||
this.currentCursor = 'ns-resize';
|
||||
}
|
||||
}
|
||||
|
||||
if (cursorInView) {
|
||||
viewRefs.hoveredView = this;
|
||||
}
|
||||
|
||||
if (this._interactionState === 'dragging') {
|
||||
return;
|
||||
}
|
||||
this._setInteractionState(cursorInView ? 'hovered' : 'normal');
|
||||
}
|
||||
|
||||
_handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) {
|
||||
const cursorInView = rectContainsPoint(
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
);
|
||||
if (this._interactionState === 'dragging') {
|
||||
this._setInteractionState(cursorInView ? 'hovered' : 'normal');
|
||||
}
|
||||
|
||||
if (viewRefs.activeView === this) {
|
||||
viewRefs.activeView = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
|
||||
switch (interaction.type) {
|
||||
case 'mousedown':
|
||||
this._handleMouseDown(interaction, viewRefs);
|
||||
return;
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction, viewRefs);
|
||||
return;
|
||||
case 'mouseup':
|
||||
this._handleMouseUp(interaction, viewRefs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ResizableView extends View {
|
||||
_canvasRef: {current: HTMLCanvasElement | null};
|
||||
_layoutState: LayoutState;
|
||||
_resizeBar: ResizeBar;
|
||||
_resizingState: ResizingState | null = null;
|
||||
_subview: View;
|
||||
|
||||
constructor(
|
||||
surface: Surface,
|
||||
frame: Rect,
|
||||
subview: View,
|
||||
canvasRef: {current: HTMLCanvasElement | null},
|
||||
label: string,
|
||||
) {
|
||||
super(surface, frame, noopLayout);
|
||||
|
||||
this._canvasRef = canvasRef;
|
||||
|
||||
this._subview = subview;
|
||||
this._resizeBar = new ResizeBar(surface, frame, label);
|
||||
|
||||
this.addSubview(this._subview);
|
||||
this.addSubview(this._resizeBar);
|
||||
|
||||
const subviewDesiredSize = subview.desiredSize();
|
||||
this._updateLayoutStateAndResizeBar(
|
||||
subviewDesiredSize.maxInitialHeight != null
|
||||
? Math.min(
|
||||
subviewDesiredSize.maxInitialHeight,
|
||||
subviewDesiredSize.height,
|
||||
)
|
||||
: subviewDesiredSize.height,
|
||||
);
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
const resizeBarDesiredSize = this._resizeBar.desiredSize();
|
||||
|
||||
return {
|
||||
width: this.frame.size.width,
|
||||
height: this._layoutState.barOffsetY + resizeBarDesiredSize.height,
|
||||
};
|
||||
}
|
||||
|
||||
layoutSubviews() {
|
||||
this._updateLayoutState();
|
||||
this._updateSubviewFrames();
|
||||
|
||||
super.layoutSubviews();
|
||||
}
|
||||
|
||||
_updateLayoutStateAndResizeBar(barOffsetY: number) {
|
||||
if (barOffsetY <= RESIZE_BAR_WITH_LABEL_HEIGHT - RESIZE_BAR_HEIGHT) {
|
||||
barOffsetY = 0;
|
||||
}
|
||||
|
||||
this._layoutState = {
|
||||
...this._layoutState,
|
||||
barOffsetY,
|
||||
};
|
||||
|
||||
this._resizeBar.showLabel = barOffsetY === 0;
|
||||
}
|
||||
|
||||
_updateLayoutState() {
|
||||
const {frame, _resizingState} = this;
|
||||
|
||||
// Allow bar to travel to bottom of the visible area of this view but no further
|
||||
const subviewDesiredSize = this._subview.desiredSize();
|
||||
const maxBarOffset = subviewDesiredSize.height;
|
||||
|
||||
let proposedBarOffsetY = this._layoutState.barOffsetY;
|
||||
// Update bar offset if dragging bar
|
||||
if (_resizingState) {
|
||||
const {mouseY, cursorOffsetInBarFrame} = _resizingState;
|
||||
proposedBarOffsetY = mouseY - frame.origin.y - cursorOffsetInBarFrame;
|
||||
}
|
||||
|
||||
this._updateLayoutStateAndResizeBar(
|
||||
clamp(0, maxBarOffset, proposedBarOffsetY),
|
||||
);
|
||||
}
|
||||
|
||||
_updateSubviewFrames() {
|
||||
const {
|
||||
frame: {
|
||||
origin: {x, y},
|
||||
size: {width},
|
||||
},
|
||||
_layoutState: {barOffsetY},
|
||||
} = this;
|
||||
|
||||
const resizeBarDesiredSize = this._resizeBar.desiredSize();
|
||||
|
||||
if (barOffsetY === 0) {
|
||||
this._subview.setFrame(HIDDEN_RECT);
|
||||
} else {
|
||||
this._subview.setFrame({
|
||||
origin: {x, y},
|
||||
size: {width, height: barOffsetY},
|
||||
});
|
||||
}
|
||||
|
||||
this._resizeBar.setFrame({
|
||||
origin: {x, y: y + barOffsetY},
|
||||
size: {width, height: resizeBarDesiredSize.height},
|
||||
});
|
||||
}
|
||||
|
||||
_handleClick(interaction: ClickInteraction) {
|
||||
const cursorInView = rectContainsPoint(
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
);
|
||||
if (cursorInView) {
|
||||
if (this._layoutState.barOffsetY === 0) {
|
||||
// Clicking on the collapsed label should expand.
|
||||
const subviewDesiredSize = this._subview.desiredSize();
|
||||
this._updateLayoutStateAndResizeBar(subviewDesiredSize.height);
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleDoubleClick(interaction: DoubleClickInteraction) {
|
||||
const cursorInView = rectContainsPoint(
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
);
|
||||
if (cursorInView) {
|
||||
if (this._layoutState.barOffsetY > 0) {
|
||||
// Double clicking on the expanded view should collapse.
|
||||
this._updateLayoutStateAndResizeBar(0);
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleMouseDown(interaction: MouseDownInteraction) {
|
||||
const cursorLocation = interaction.payload.location;
|
||||
const resizeBarFrame = this._resizeBar.frame;
|
||||
if (rectContainsPoint(cursorLocation, resizeBarFrame)) {
|
||||
const mouseY = cursorLocation.y;
|
||||
this._resizingState = {
|
||||
cursorOffsetInBarFrame: mouseY - resizeBarFrame.origin.y,
|
||||
mouseY,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
_handleMouseMove(interaction: MouseMoveInteraction) {
|
||||
const {_resizingState} = this;
|
||||
if (_resizingState) {
|
||||
this._resizingState = {
|
||||
..._resizingState,
|
||||
mouseY: interaction.payload.location.y,
|
||||
};
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
_handleMouseUp(interaction: MouseUpInteraction) {
|
||||
if (this._resizingState) {
|
||||
this._resizingState = null;
|
||||
}
|
||||
}
|
||||
|
||||
getCursorActiveSubView(interaction: Interaction): View | null {
|
||||
const cursorLocation = interaction.payload.location;
|
||||
const resizeBarFrame = this._resizeBar.frame;
|
||||
if (rectContainsPoint(cursorLocation, resizeBarFrame)) {
|
||||
return this;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
|
||||
switch (interaction.type) {
|
||||
case 'click':
|
||||
this._handleClick(interaction);
|
||||
return;
|
||||
case 'double-click':
|
||||
this._handleDoubleClick(interaction);
|
||||
return;
|
||||
case 'mousedown':
|
||||
this._handleMouseDown(interaction);
|
||||
return;
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction);
|
||||
return;
|
||||
case 'mouseup':
|
||||
this._handleMouseUp(interaction);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ export class Surface {
|
||||
origin: zeroPoint,
|
||||
size: _canvasSize,
|
||||
});
|
||||
rootView.displayIfNeeded(_context);
|
||||
rootView.displayIfNeeded(_context, this._viewRefs);
|
||||
}
|
||||
|
||||
getCurrentCursor(): string | null {
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// TODO Vertically stack views (via verticallyStackedLayout).
|
||||
// If stacked views are taller than the available height, a vertical scrollbar will be shown on the side,
|
||||
// and width will be adjusted to subtract the width of the scrollbar.
|
||||
+58
-4
@@ -12,10 +12,11 @@ import type {
|
||||
MouseDownInteraction,
|
||||
MouseMoveInteraction,
|
||||
MouseUpInteraction,
|
||||
WheelPlainInteraction,
|
||||
WheelWithShiftInteraction,
|
||||
} from './useCanvasInteraction';
|
||||
import type {Rect} from './geometry';
|
||||
import type {ScrollState} from './utils/scrollState';
|
||||
import type {ViewRefs} from './Surface';
|
||||
|
||||
import {Surface} from './Surface';
|
||||
import {View} from './View';
|
||||
@@ -26,6 +27,11 @@ import {
|
||||
translateState,
|
||||
} from './utils/scrollState';
|
||||
import {MOVE_WHEEL_DELTA_THRESHOLD} from './constants';
|
||||
import {COLORS} from '../content-views/constants';
|
||||
|
||||
const CARET_MARGIN = 3;
|
||||
const CARET_WIDTH = 5;
|
||||
const CARET_HEIGHT = 3;
|
||||
|
||||
export class VerticalScrollView extends View {
|
||||
_scrollState: ScrollState = {offset: 0, length: 0};
|
||||
@@ -48,6 +54,54 @@ export class VerticalScrollView extends View {
|
||||
return this._contentView.desiredSize();
|
||||
}
|
||||
|
||||
draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
|
||||
super.draw(context, viewRefs);
|
||||
|
||||
// Show carets if there's scroll overflow above or below the viewable area.
|
||||
if (this.frame.size.height > CARET_HEIGHT * 2 + CARET_MARGIN * 3) {
|
||||
const offset = this._scrollState.offset;
|
||||
const desiredSize = this._contentView.desiredSize();
|
||||
|
||||
const above = offset;
|
||||
const below = this.frame.size.height - desiredSize.height - offset;
|
||||
|
||||
if (above < 0 || below < 0) {
|
||||
const {visibleArea} = this;
|
||||
const {x, y} = visibleArea.origin;
|
||||
const {width, height} = visibleArea.size;
|
||||
const horizontalCenter = x + width / 2;
|
||||
|
||||
const halfWidth = CARET_WIDTH;
|
||||
const left = horizontalCenter + halfWidth;
|
||||
const right = horizontalCenter - halfWidth;
|
||||
|
||||
if (above < 0) {
|
||||
const topY = y + CARET_MARGIN;
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(horizontalCenter, topY);
|
||||
context.lineTo(left, topY + CARET_HEIGHT);
|
||||
context.lineTo(right, topY + CARET_HEIGHT);
|
||||
context.closePath();
|
||||
context.fillStyle = COLORS.SCROLL_CARET;
|
||||
context.fill();
|
||||
}
|
||||
|
||||
if (below < 0) {
|
||||
const bottomY = y + height - CARET_MARGIN;
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(horizontalCenter, bottomY);
|
||||
context.lineTo(left, bottomY - CARET_HEIGHT);
|
||||
context.lineTo(right, bottomY - CARET_HEIGHT);
|
||||
context.closePath();
|
||||
context.fillStyle = COLORS.SCROLL_CARET;
|
||||
context.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference to the content view. This view is also the only view in
|
||||
* `this.subviews`.
|
||||
@@ -103,7 +157,7 @@ export class VerticalScrollView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
_handleWheelPlain(interaction: WheelPlainInteraction) {
|
||||
_handleWheelShift(interaction: WheelWithShiftInteraction) {
|
||||
const {
|
||||
location,
|
||||
delta: {deltaX, deltaY},
|
||||
@@ -141,8 +195,8 @@ export class VerticalScrollView extends View {
|
||||
case 'mouseup':
|
||||
this._handleMouseUp(interaction);
|
||||
break;
|
||||
case 'wheel-plain':
|
||||
this._handleWheelPlain(interaction);
|
||||
case 'wheel-shift':
|
||||
this._handleWheelShift(interaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import type {Interaction} from './useCanvasInteraction';
|
||||
import type {Rect, Size} from './geometry';
|
||||
import type {Rect, Size, SizeWithMaxHeight} from './geometry';
|
||||
import type {Layouter} from './layouter';
|
||||
import type {ViewRefs} from './Surface';
|
||||
|
||||
@@ -140,7 +140,7 @@ export class View {
|
||||
*
|
||||
* Can be overridden by subclasses.
|
||||
*/
|
||||
desiredSize(): ?Size {
|
||||
desiredSize(): Size | SizeWithMaxHeight {
|
||||
if (this._needsDisplay) {
|
||||
this.layoutSubviews();
|
||||
}
|
||||
@@ -186,7 +186,7 @@ export class View {
|
||||
* 1. Lays out subviews with `layoutSubviews`.
|
||||
* 2. Draws content with `draw`.
|
||||
*/
|
||||
displayIfNeeded(context: CanvasRenderingContext2D) {
|
||||
displayIfNeeded(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
|
||||
if (
|
||||
(this._needsDisplay || this._subviewsNeedDisplay) &&
|
||||
rectIntersectsRect(this.frame, this.visibleArea) &&
|
||||
@@ -195,7 +195,7 @@ export class View {
|
||||
this.layoutSubviews();
|
||||
if (this._needsDisplay) this._needsDisplay = false;
|
||||
if (this._subviewsNeedDisplay) this._subviewsNeedDisplay = false;
|
||||
this.draw(context);
|
||||
this.draw(context, viewRefs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,11 +239,11 @@ export class View {
|
||||
*
|
||||
* @see displayIfNeeded
|
||||
*/
|
||||
draw(context: CanvasRenderingContext2D) {
|
||||
draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
|
||||
const {subviews, visibleArea} = this;
|
||||
subviews.forEach(subview => {
|
||||
if (rectIntersectsRect(visibleArea, subview.visibleArea)) {
|
||||
subview.displayIfNeeded(context);
|
||||
subview.displayIfNeeded(context, viewRefs);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -252,10 +252,9 @@ export class View {
|
||||
* Handle an `interaction`.
|
||||
*
|
||||
* To be overwritten by subclasses that wish to handle interactions.
|
||||
*
|
||||
* NOTE: Do not call directly! Use `handleInteractionAndPropagateToSubviews`
|
||||
*/
|
||||
// Internal note: Do not call directly! Use
|
||||
// `handleInteractionAndPropagateToSubviews` so that interactions are
|
||||
// propagated to subviews.
|
||||
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {}
|
||||
|
||||
/**
|
||||
@@ -272,9 +271,18 @@ export class View {
|
||||
interaction: Interaction,
|
||||
viewRefs: ViewRefs,
|
||||
) {
|
||||
const {subviews, visibleArea} = this;
|
||||
|
||||
if (visibleArea.size.height === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleInteraction(interaction, viewRefs);
|
||||
this.subviews.forEach(subview =>
|
||||
subview.handleInteractionAndPropagateToSubviews(interaction, viewRefs),
|
||||
);
|
||||
|
||||
subviews.forEach(subview => {
|
||||
if (rectIntersectsRect(visibleArea, subview.visibleArea)) {
|
||||
subview.handleInteractionAndPropagateToSubviews(interaction, viewRefs);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
export type Point = $ReadOnly<{|x: number, y: number|}>;
|
||||
export type Size = $ReadOnly<{|width: number, height: number|}>;
|
||||
export type SizeWithMaxHeight = {|
|
||||
...Size,
|
||||
maxInitialHeight?: number,
|
||||
|};
|
||||
export type Rect = $ReadOnly<{|origin: Point, size: Size|}>;
|
||||
|
||||
/**
|
||||
@@ -70,6 +74,15 @@ function boxToRect(box: Box): Rect {
|
||||
}
|
||||
|
||||
export function rectIntersectsRect(rect1: Rect, rect2: Rect): boolean {
|
||||
if (
|
||||
rect1.size.width === 0 ||
|
||||
rect1.size.height === 0 ||
|
||||
rect2.size.width === 0 ||
|
||||
rect2.size.height === 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [top1, right1, bottom1, left1] = rectToBox(rect1);
|
||||
const [top2, right2, bottom2, left2] = rectToBox(rect2);
|
||||
return !(
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export * from './ColorView';
|
||||
export * from './BackgroundColorView';
|
||||
export * from './HorizontalPanAndZoomView';
|
||||
export * from './ResizableSplitView';
|
||||
export * from './ResizableView';
|
||||
export * from './Surface';
|
||||
export * from './VerticalScrollView';
|
||||
export * from './View';
|
||||
|
||||
@@ -43,8 +43,7 @@ export function collapseLayoutIntoViews(layout: Layout) {
|
||||
export const noopLayout: Layouter = layout => layout;
|
||||
|
||||
/**
|
||||
* Layer views on top of each other. All views' frames will be set to
|
||||
* `containerFrame`.
|
||||
* Layer views on top of each other. All views' frames will be set to `containerFrame`.
|
||||
*
|
||||
* Equivalent to composing:
|
||||
* - `alignToContainerXLayout`,
|
||||
@@ -52,12 +51,13 @@ export const noopLayout: Layouter = layout => layout;
|
||||
* - `containerWidthLayout`, and
|
||||
* - `containerHeightLayout`.
|
||||
*/
|
||||
export const layeredLayout: Layouter = (layout, containerFrame) =>
|
||||
layout.map(layoutInfo => ({...layoutInfo, frame: containerFrame}));
|
||||
export const layeredLayout: Layouter = (layout, containerFrame) => {
|
||||
return layout.map(layoutInfo => ({...layoutInfo, frame: containerFrame}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Stacks `views` vertically in `frame`. All views in `views` will have their
|
||||
* widths set to the frame's width.
|
||||
* Stacks `views` vertically in `frame`.
|
||||
* All views in `views` will have their widths set to the frame's width.
|
||||
*/
|
||||
export const verticallyStackedLayout: Layouter = (layout, containerFrame) => {
|
||||
let currentY = containerFrame.origin.y;
|
||||
|
||||
+61
-1
@@ -10,9 +10,23 @@
|
||||
import type {NormalizedWheelDelta} from './utils/normalizeWheel';
|
||||
import type {Point} from './geometry';
|
||||
|
||||
import {useEffect} from 'react';
|
||||
import {useEffect, useRef} from 'react';
|
||||
import {normalizeWheel} from './utils/normalizeWheel';
|
||||
|
||||
export type ClickInteraction = {|
|
||||
type: 'click',
|
||||
payload: {|
|
||||
event: MouseEvent,
|
||||
location: Point,
|
||||
|},
|
||||
|};
|
||||
export type DoubleClickInteraction = {|
|
||||
type: 'double-click',
|
||||
payload: {|
|
||||
event: MouseEvent,
|
||||
location: Point,
|
||||
|},
|
||||
|};
|
||||
export type MouseDownInteraction = {|
|
||||
type: 'mousedown',
|
||||
payload: {|
|
||||
@@ -68,6 +82,8 @@ export type WheelWithMetaInteraction = {|
|
||||
|};
|
||||
|
||||
export type Interaction =
|
||||
| ClickInteraction
|
||||
| DoubleClickInteraction
|
||||
| MouseDownInteraction
|
||||
| MouseMoveInteraction
|
||||
| MouseUpInteraction
|
||||
@@ -99,6 +115,9 @@ export function useCanvasInteraction(
|
||||
canvasRef: {|current: HTMLCanvasElement | null|},
|
||||
interactor: (interaction: Interaction) => void,
|
||||
) {
|
||||
const isMouseDownRef = useRef<boolean>(false);
|
||||
const didMouseMoveWhileDownRef = useRef<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) {
|
||||
@@ -113,7 +132,38 @@ export function useCanvasInteraction(
|
||||
};
|
||||
}
|
||||
|
||||
const onCanvasClick: MouseEventHandler = event => {
|
||||
if (didMouseMoveWhileDownRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
interactor({
|
||||
type: 'click',
|
||||
payload: {
|
||||
event,
|
||||
location: localToCanvasCoordinates({x: event.x, y: event.y}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onCanvasDoubleClick: MouseEventHandler = event => {
|
||||
if (didMouseMoveWhileDownRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
interactor({
|
||||
type: 'double-click',
|
||||
payload: {
|
||||
event,
|
||||
location: localToCanvasCoordinates({x: event.x, y: event.y}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onCanvasMouseDown: MouseEventHandler = event => {
|
||||
didMouseMoveWhileDownRef.current = false;
|
||||
isMouseDownRef.current = true;
|
||||
|
||||
interactor({
|
||||
type: 'mousedown',
|
||||
payload: {
|
||||
@@ -124,6 +174,10 @@ export function useCanvasInteraction(
|
||||
};
|
||||
|
||||
const onDocumentMouseMove: MouseEventHandler = event => {
|
||||
if (isMouseDownRef.current) {
|
||||
didMouseMoveWhileDownRef.current = true;
|
||||
}
|
||||
|
||||
interactor({
|
||||
type: 'mousemove',
|
||||
payload: {
|
||||
@@ -134,6 +188,8 @@ export function useCanvasInteraction(
|
||||
};
|
||||
|
||||
const onDocumentMouseUp: MouseEventHandler = event => {
|
||||
isMouseDownRef.current = false;
|
||||
|
||||
interactor({
|
||||
type: 'mouseup',
|
||||
payload: {
|
||||
@@ -179,6 +235,8 @@ export function useCanvasInteraction(
|
||||
ownerDocument.addEventListener('mousemove', onDocumentMouseMove);
|
||||
ownerDocument.addEventListener('mouseup', onDocumentMouseUp);
|
||||
|
||||
canvas.addEventListener('click', onCanvasClick);
|
||||
canvas.addEventListener('dblclick', onCanvasDoubleClick);
|
||||
canvas.addEventListener('mousedown', onCanvasMouseDown);
|
||||
canvas.addEventListener('wheel', onCanvasWheel);
|
||||
|
||||
@@ -186,6 +244,8 @@ export function useCanvasInteraction(
|
||||
ownerDocument.removeEventListener('mousemove', onDocumentMouseMove);
|
||||
ownerDocument.removeEventListener('mouseup', onDocumentMouseUp);
|
||||
|
||||
canvas.removeEventListener('click', onCanvasClick);
|
||||
canvas.removeEventListener('dblclick', onCanvasDoubleClick);
|
||||
canvas.removeEventListener('mousedown', onCanvasMouseDown);
|
||||
canvas.removeEventListener('wheel', onCanvasWheel);
|
||||
};
|
||||
|
||||
@@ -45,20 +45,21 @@ function Profiler(_: {||}) {
|
||||
|
||||
const {supportsSchedulingProfiler} = useContext(StoreContext);
|
||||
|
||||
let showRightColumn = true;
|
||||
let isLegacyProfilerSelected = false;
|
||||
|
||||
let view = null;
|
||||
if (didRecordCommits || selectedTabID === 'scheduling-profiler') {
|
||||
switch (selectedTabID) {
|
||||
case 'flame-chart':
|
||||
isLegacyProfilerSelected = true;
|
||||
view = <CommitFlamegraph />;
|
||||
break;
|
||||
case 'ranked-chart':
|
||||
isLegacyProfilerSelected = true;
|
||||
view = <CommitRanked />;
|
||||
break;
|
||||
case 'scheduling-profiler':
|
||||
view = <SchedulingProfiler />;
|
||||
showRightColumn = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -119,7 +120,7 @@ function Profiler(_: {||}) {
|
||||
<RootSelector />
|
||||
<div className={styles.Spacer} />
|
||||
<SettingsModalContextToggle />
|
||||
{didRecordCommits && (
|
||||
{isLegacyProfilerSelected && didRecordCommits && (
|
||||
<Fragment>
|
||||
<div className={styles.VRule} />
|
||||
<SnapshotSelector />
|
||||
@@ -131,7 +132,9 @@ function Profiler(_: {||}) {
|
||||
<ModalDialog />
|
||||
</div>
|
||||
</div>
|
||||
{showRightColumn && <div className={styles.RightColumn}>{sidebar}</div>}
|
||||
{isLegacyProfilerSelected && (
|
||||
<div className={styles.RightColumn}>{sidebar}</div>
|
||||
)}
|
||||
<SettingsModal />
|
||||
</div>
|
||||
</SettingsModalContextController>
|
||||
|
||||
+4
-1
@@ -212,7 +212,10 @@ function ProfilerContextController({children}: Props) {
|
||||
const [selectedCommitIndex, selectCommitIndex] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedTabID, selectTab] = useState<TabID>('flame-chart');
|
||||
const [selectedTabID, selectTab] = useLocalStorage<TabID>(
|
||||
'React::DevTools::Profiler::defaultTab',
|
||||
'flame-chart',
|
||||
);
|
||||
|
||||
if (isProfiling) {
|
||||
batchedUpdates(() => {
|
||||
|
||||
+26
-44
@@ -412,6 +412,9 @@ export function updateThemeVariables(
|
||||
updateStyleHelper(theme, 'color-record-hover', documentElements);
|
||||
updateStyleHelper(theme, 'color-record-inactive', documentElements);
|
||||
updateStyleHelper(theme, 'color-resize-bar', documentElements);
|
||||
updateStyleHelper(theme, 'color-resize-bar-active', documentElements);
|
||||
updateStyleHelper(theme, 'color-resize-bar-border', documentElements);
|
||||
updateStyleHelper(theme, 'color-resize-bar-dot', documentElements);
|
||||
updateStyleHelper(theme, 'color-color-scroll-thumb', documentElements);
|
||||
updateStyleHelper(theme, 'color-color-scroll-track', documentElements);
|
||||
updateStyleHelper(theme, 'color-search-match', documentElements);
|
||||
@@ -431,21 +434,6 @@ export function updateThemeVariables(
|
||||
'color-scheduling-profiler-native-event-hover',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-native-event-warning',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-native-event-warning-hover',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-native-event-warning-text',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-selected-tree-highlight-active',
|
||||
@@ -481,11 +469,6 @@ export function updateThemeVariables(
|
||||
'color-scheduling-profiler-react-idle',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-react-idle-selected',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-react-idle-hover',
|
||||
@@ -496,11 +479,6 @@ export function updateThemeVariables(
|
||||
'color-scheduling-profiler-react-render',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-react-render-selected',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-react-render-hover',
|
||||
@@ -511,11 +489,6 @@ export function updateThemeVariables(
|
||||
'color-scheduling-profiler-react-commit',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-react-commit-selected',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-react-commit-hover',
|
||||
@@ -526,11 +499,6 @@ export function updateThemeVariables(
|
||||
'color-scheduling-profiler-react-layout-effects',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-react-layout-effects-selected',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-react-layout-effects-hover',
|
||||
@@ -541,11 +509,6 @@ export function updateThemeVariables(
|
||||
'color-scheduling-profiler-react-passive-effects',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-react-passive-effects-selected',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-react-passive-effects-hover',
|
||||
@@ -563,22 +526,32 @@ export function updateThemeVariables(
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-react-schedule-cascading',
|
||||
'color-scheduling-profiler-react-suspense-rejected-event',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-react-schedule-cascading-hover',
|
||||
'color-scheduling-profiler-react-suspense-rejected-hover',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-react-suspend',
|
||||
'color-scheduling-profiler-react-suspense-resolved',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-react-suspend-hover',
|
||||
'color-scheduling-profiler-react-suspense-resolved-hover',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-react-suspense-unresolved',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-scheduling-profiler-react-suspense-unresolved-hover',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
@@ -586,6 +559,7 @@ export function updateThemeVariables(
|
||||
'color-scheduling-profiler-react-work-border',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(theme, 'color-scroll-caret', documentElements);
|
||||
updateStyleHelper(theme, 'color-shadow', documentElements);
|
||||
updateStyleHelper(theme, 'color-tab-selected-border', documentElements);
|
||||
updateStyleHelper(theme, 'color-text', documentElements);
|
||||
@@ -597,6 +571,14 @@ export function updateThemeVariables(
|
||||
updateStyleHelper(theme, 'color-toggle-text', documentElements);
|
||||
updateStyleHelper(theme, 'color-tooltip-background', documentElements);
|
||||
updateStyleHelper(theme, 'color-tooltip-text', documentElements);
|
||||
updateStyleHelper(theme, 'color-warning-background', documentElements);
|
||||
updateStyleHelper(theme, 'color-warning-background-hover', documentElements);
|
||||
updateStyleHelper(theme, 'color-warning-text-color', documentElements);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-warning-text-color-inverted',
|
||||
documentElements,
|
||||
);
|
||||
|
||||
// Font smoothing varies based on the theme.
|
||||
updateStyleHelper(theme, 'font-smoothing', documentElements);
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function TabBar({
|
||||
<Fragment>
|
||||
{tabs.map(tab => {
|
||||
if (tab === null) {
|
||||
return <div className={styles.VRule} />;
|
||||
return <div key="VRule" className={styles.VRule} />;
|
||||
}
|
||||
|
||||
const {icon, id, label, title} = tab;
|
||||
|
||||
@@ -77,46 +77,43 @@
|
||||
--light-color-record-active: #fc3a4b;
|
||||
--light-color-record-hover: #3578e5;
|
||||
--light-color-record-inactive: #0088fa;
|
||||
--light-color-resize-bar: #cccccc;
|
||||
--light-color-resize-bar: #eeeeee;
|
||||
--light-color-resize-bar-active: #dcdcdc;
|
||||
--light-color-resize-bar-border: #d1d1d1;
|
||||
--light-color-resize-bar-dot: #333333;
|
||||
--light-color-scheduling-profiler-native-event: #ccc;
|
||||
--light-color-scheduling-profiler-native-event-hover: #aaa;
|
||||
--light-color-scheduling-profiler-native-event-warning: #ee1638;
|
||||
--light-color-scheduling-profiler-native-event-warning-hover: #da1030;
|
||||
--light-color-scheduling-profiler-native-event-warning-text: #fff;
|
||||
--light-color-scheduling-profiler-priority-background: #f6f6f6;
|
||||
--light-color-scheduling-profiler-priority-border: #eeeeee;
|
||||
--light-color-scheduling-profiler-user-timing: #c9cacd;
|
||||
--light-color-scheduling-profiler-user-timing-hover:#93959a;
|
||||
--light-color-scheduling-profiler-react-idle: #edf6ff;
|
||||
--light-color-scheduling-profiler-react-idle-selected:#EDF6FF;
|
||||
--light-color-scheduling-profiler-react-idle-hover:#EDF6FF;
|
||||
--light-color-scheduling-profiler-user-timing-hover: #93959a;
|
||||
--light-color-scheduling-profiler-react-idle: #d3e5f6;
|
||||
--light-color-scheduling-profiler-react-idle-hover: #c3d9ef;
|
||||
--light-color-scheduling-profiler-react-render: #9fc3f3;
|
||||
--light-color-scheduling-profiler-react-render-selected:#64A9F5;
|
||||
--light-color-scheduling-profiler-react-render-hover:#2683E2;
|
||||
--light-color-scheduling-profiler-react-commit: #ff718e;
|
||||
--light-color-scheduling-profiler-react-commit-selected:#FF5277;
|
||||
--light-color-scheduling-profiler-react-commit-hover:#ed0030;
|
||||
--light-color-scheduling-profiler-react-layout-effects:#c88ff0;
|
||||
--light-color-scheduling-profiler-react-layout-effects-selected:#934FC1;
|
||||
--light-color-scheduling-profiler-react-layout-effects-hover:#601593;
|
||||
--light-color-scheduling-profiler-react-passive-effects:#c88ff0;
|
||||
--light-color-scheduling-profiler-react-passive-effects-selected:#934FC1;
|
||||
--light-color-scheduling-profiler-react-passive-effects-hover:#601593;
|
||||
--light-color-scheduling-profiler-react-render-hover: #83afe9;
|
||||
--light-color-scheduling-profiler-react-commit: #c88ff0;
|
||||
--light-color-scheduling-profiler-react-commit-hover: #b281d6;
|
||||
--light-color-scheduling-profiler-react-layout-effects: #b281d6;
|
||||
--light-color-scheduling-profiler-react-layout-effects-hover: #9d71bd;
|
||||
--light-color-scheduling-profiler-react-passive-effects: #b281d6;
|
||||
--light-color-scheduling-profiler-react-passive-effects-hover: #9d71bd;
|
||||
--light-color-scheduling-profiler-react-schedule: #9fc3f3;
|
||||
--light-color-scheduling-profiler-react-schedule-hover:#2683E2;
|
||||
--light-color-scheduling-profiler-react-schedule-cascading:#ff718e;
|
||||
--light-color-scheduling-profiler-react-schedule-cascading-hover:#ed0030;
|
||||
--light-color-scheduling-profiler-react-suspend: #a6e59f;
|
||||
--light-color-scheduling-profiler-react-suspend-hover:#13bc00;
|
||||
--light-color-scheduling-profiler-react-schedule-hover: #2683E2;
|
||||
--light-color-scheduling-profiler-react-suspense-rejected: #f1cc14;
|
||||
--light-color-scheduling-profiler-react-suspense-rejected-hover: #ffdf37;
|
||||
--light-color-scheduling-profiler-react-suspense-resolved: #a6e59f;
|
||||
--light-color-scheduling-profiler-react-suspense-resolved-hover: #89d281;
|
||||
--light-color-scheduling-profiler-react-suspense-unresolved: #c9cacd;
|
||||
--light-color-scheduling-profiler-react-suspense-unresolved-hover: #93959a;
|
||||
--light-color-scheduling-profiler-text-color: #000000;
|
||||
--light-color-scheduling-profiler-react-work-border:#ffffff;
|
||||
--light-color-scheduling-profiler-react-work-border: #ffffff;
|
||||
--light-color-scroll-thumb: #c2c2c2;
|
||||
--light-color-scroll-track: #fafafa;
|
||||
--light-color-search-match: yellow;
|
||||
--light-color-search-match-current: #f7923b;
|
||||
--light-color-selected-tree-highlight-active: rgba(0, 136, 250, 0.1);
|
||||
--light-color-selected-tree-highlight-inactive: rgba(0, 0, 0, 0.05);
|
||||
--light-color-shadow: rgba(0, 0, 0, 0.25);
|
||||
--light-color-scroll-caret: rgba(150, 150, 150, 0.5);
|
||||
--light-color-tab-selected-border: #0088fa;
|
||||
--light-color-text: #000000;
|
||||
--light-color-text-invalid: #ff0000;
|
||||
@@ -127,6 +124,10 @@
|
||||
--light-color-toggle-text: #ffffff;
|
||||
--light-color-tooltip-background: rgba(0, 0, 0, 0.9);
|
||||
--light-color-tooltip-text: #ffffff;
|
||||
--light-color-warning-background: #fb3655;
|
||||
--light-color-warning-background-hover: #f82042;
|
||||
--light-color-warning-text-color: #ffffff;
|
||||
--light-color-warning-text-color-inverted: #fd4d69;
|
||||
|
||||
/* Dark theme */
|
||||
--dark-color-attribute-name: #9d87d2;
|
||||
@@ -202,45 +203,43 @@
|
||||
--dark-color-record-active: #fc3a4b;
|
||||
--dark-color-record-hover: #a2e9fc;
|
||||
--dark-color-record-inactive: #61dafb;
|
||||
--dark-color-resize-bar: #3d424a;
|
||||
--dark-color-resize-bar: #282c34;
|
||||
--dark-color-resize-bar-active: #31363f;
|
||||
--dark-color-resize-bar-border: #3d424a;
|
||||
--dark-color-resize-bar-dot: #cfd1d5;
|
||||
--dark-color-scheduling-profiler-native-event: #b2b2b2;
|
||||
--dark-color-scheduling-profiler-native-event-hover: #949494;
|
||||
--dark-color-scheduling-profiler-native-event-warning: #ee1638;
|
||||
--dark-color-scheduling-profiler-native-event-warning-hover: #da1030;
|
||||
--dark-color-scheduling-profiler-native-event-warning-text: #fff;
|
||||
--dark-color-scheduling-profiler-priority-background: #1d2129;
|
||||
--dark-color-scheduling-profiler-priority-border: #282c34;
|
||||
--dark-color-scheduling-profiler-user-timing: #c9cacd;
|
||||
--dark-color-scheduling-profiler-user-timing-hover:#93959a;
|
||||
--dark-color-scheduling-profiler-user-timing-hover: #93959a;
|
||||
--dark-color-scheduling-profiler-react-idle: #3d485b;
|
||||
--dark-color-scheduling-profiler-react-idle-selected:#465269;
|
||||
--dark-color-scheduling-profiler-react-idle-hover:#465269;
|
||||
--dark-color-scheduling-profiler-react-render: #9fc3f3;
|
||||
--dark-color-scheduling-profiler-react-render-selected:#64A9F5;
|
||||
--dark-color-scheduling-profiler-react-render-hover:#2683E2;
|
||||
--dark-color-scheduling-profiler-react-commit: #ff718e;
|
||||
--dark-color-scheduling-profiler-react-commit-selected:#FF5277;
|
||||
--dark-color-scheduling-profiler-react-commit-hover:#ed0030;
|
||||
--dark-color-scheduling-profiler-react-layout-effects:#c88ff0;
|
||||
--dark-color-scheduling-profiler-react-layout-effects-selected:#934FC1;
|
||||
--dark-color-scheduling-profiler-react-layout-effects-hover:#601593;
|
||||
--dark-color-scheduling-profiler-react-passive-effects:#c88ff0;
|
||||
--dark-color-scheduling-profiler-react-passive-effects-selected:#934FC1;
|
||||
--dark-color-scheduling-profiler-react-passive-effects-hover:#601593;
|
||||
--dark-color-scheduling-profiler-react-schedule: #9fc3f3;
|
||||
--dark-color-scheduling-profiler-react-schedule-hover:#2683E2;
|
||||
--dark-color-scheduling-profiler-react-schedule-cascading:#ff718e;
|
||||
--dark-color-scheduling-profiler-react-schedule-cascading-hover:#ed0030;
|
||||
--dark-color-scheduling-profiler-react-suspend: #a6e59f;
|
||||
--dark-color-scheduling-profiler-react-suspend-hover:#13bc00;
|
||||
--dark-color-scheduling-profiler-react-idle-hover: #465269;
|
||||
--dark-color-scheduling-profiler-react-render: #2683E2;
|
||||
--dark-color-scheduling-profiler-react-render-hover: #1a76d4;
|
||||
--dark-color-scheduling-profiler-react-commit: #731fad;
|
||||
--dark-color-scheduling-profiler-react-commit-hover: #611b94;
|
||||
--dark-color-scheduling-profiler-react-layout-effects: #611b94;
|
||||
--dark-color-scheduling-profiler-react-layout-effects-hover: #51167a;
|
||||
--dark-color-scheduling-profiler-react-passive-effects: #611b94;
|
||||
--dark-color-scheduling-profiler-react-passive-effects-hover: #51167a;
|
||||
--dark-color-scheduling-profiler-react-schedule: #2683E2;
|
||||
--dark-color-scheduling-profiler-react-schedule-hover: #1a76d4;
|
||||
--dark-color-scheduling-profiler-react-suspense-rejected: #f1cc14;
|
||||
--dark-color-scheduling-profiler-react-suspense-rejected-hover: #e4c00f;
|
||||
--dark-color-scheduling-profiler-react-suspense-resolved: #a6e59f;
|
||||
--dark-color-scheduling-profiler-react-suspense-resolved-hover: #89d281;
|
||||
--dark-color-scheduling-profiler-react-suspense-unresolved: #c9cacd;
|
||||
--dark-color-scheduling-profiler-react-suspense-unresolved-hover: #93959a;
|
||||
--dark-color-scheduling-profiler-text-color: #000000;
|
||||
--dark-color-scheduling-profiler-react-work-border:#ffffff;
|
||||
--dark-color-scheduling-profiler-react-work-border: #ffffff;
|
||||
--dark-color-scroll-thumb: #afb3b9;
|
||||
--dark-color-scroll-track: #313640;
|
||||
--dark-color-search-match: yellow;
|
||||
--dark-color-search-match-current: #f7923b;
|
||||
--dark-color-selected-tree-highlight-active: rgba(23, 143, 185, 0.15);
|
||||
--dark-color-selected-tree-highlight-inactive: rgba(255, 255, 255, 0.05);
|
||||
--dark-color-scroll-caret: #4f5766;
|
||||
--dark-color-shadow: rgba(0, 0, 0, 0.5);
|
||||
--dark-color-tab-selected-border: #178fb9;
|
||||
--dark-color-text: #ffffff;
|
||||
@@ -252,6 +251,10 @@
|
||||
--dark-color-toggle-text: #ffffff;
|
||||
--dark-color-tooltip-background: rgba(255, 255, 255, 0.95);
|
||||
--dark-color-tooltip-text: #000000;
|
||||
--dark-color-warning-background: #ee1638;
|
||||
--dark-color-warning-background-hover: #da1030;
|
||||
--dark-color-warning-text-color: #ffffff;
|
||||
--dark-color-warning-text-color-inverted: #ee1638;
|
||||
|
||||
/* Font smoothing */
|
||||
--light-font-smoothing: auto;
|
||||
|
||||
+1
-8
@@ -103,14 +103,7 @@ export function loadHookNames(
|
||||
|
||||
let didTimeout = false;
|
||||
|
||||
const response = loadHookNamesFunction(hooksTree);
|
||||
console.log(
|
||||
'loadHookNamesFunction:',
|
||||
loadHookNamesFunction,
|
||||
'->',
|
||||
response,
|
||||
);
|
||||
response.then(
|
||||
loadHookNamesFunction(hooksTree).then(
|
||||
function onSuccess(hookNames) {
|
||||
if (didTimeout) {
|
||||
return;
|
||||
|
||||
@@ -244,7 +244,7 @@ function throwException(
|
||||
}
|
||||
|
||||
if (enableSchedulingProfiler) {
|
||||
markComponentSuspended(sourceFiber, wakeable);
|
||||
markComponentSuspended(sourceFiber, wakeable, rootRenderLanes);
|
||||
}
|
||||
|
||||
// Reset the memoizedState to what it was before we attempted to render it.
|
||||
|
||||
@@ -244,7 +244,7 @@ function throwException(
|
||||
}
|
||||
|
||||
if (enableSchedulingProfiler) {
|
||||
markComponentSuspended(sourceFiber, wakeable);
|
||||
markComponentSuspended(sourceFiber, wakeable, rootRenderLanes);
|
||||
}
|
||||
|
||||
// Reset the memoizedState to what it was before we attempted to render it.
|
||||
|
||||
+13
-3
@@ -109,13 +109,23 @@ function getWakeableID(wakeable: Wakeable): number {
|
||||
return ((wakeableIDs.get(wakeable): any): number);
|
||||
}
|
||||
|
||||
export function markComponentSuspended(fiber: Fiber, wakeable: Wakeable): void {
|
||||
export function markComponentSuspended(
|
||||
fiber: Fiber,
|
||||
wakeable: Wakeable,
|
||||
lanes: Lanes,
|
||||
): void {
|
||||
if (enableSchedulingProfiler) {
|
||||
if (supportsUserTimingV3) {
|
||||
const eventType = wakeableIDs.has(wakeable) ? 'resuspend' : 'suspend';
|
||||
const id = getWakeableID(wakeable);
|
||||
const componentName = getComponentNameFromFiber(fiber) || 'Unknown';
|
||||
// TODO Add component stack id
|
||||
markAndClear(`--suspense-suspend-${id}-${componentName}`);
|
||||
const phase = fiber.alternate === null ? 'mount' : 'update';
|
||||
// TODO (scheduling profiler) Add component stack id if we re-add component stack info.
|
||||
markAndClear(
|
||||
`--suspense-${eventType}-${id}-${componentName}-${phase}-${formatLanes(
|
||||
lanes,
|
||||
)}`,
|
||||
);
|
||||
wakeable.then(
|
||||
() => markAndClear(`--suspense-resolved-${id}-${componentName}`),
|
||||
() => markAndClear(`--suspense-rejected-${id}-${componentName}`),
|
||||
|
||||
@@ -208,7 +208,7 @@ describe('SchedulingProfiler', () => {
|
||||
`--react-init-${ReactVersion}`,
|
||||
`--schedule-render-${formatLanes(ReactFiberLane.SyncLane)}`,
|
||||
`--render-start-${formatLanes(ReactFiberLane.SyncLane)}`,
|
||||
'--suspense-suspend-0-Example',
|
||||
'--suspense-suspend-0-Example-mount-1-Sync',
|
||||
'--render-stop',
|
||||
`--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`,
|
||||
`--layout-effects-start-${formatLanes(ReactFiberLane.SyncLane)}`,
|
||||
@@ -239,7 +239,7 @@ describe('SchedulingProfiler', () => {
|
||||
`--react-init-${ReactVersion}`,
|
||||
`--schedule-render-${formatLanes(ReactFiberLane.SyncLane)}`,
|
||||
`--render-start-${formatLanes(ReactFiberLane.SyncLane)}`,
|
||||
'--suspense-suspend-0-Example',
|
||||
'--suspense-suspend-0-Example-mount-1-Sync',
|
||||
'--render-stop',
|
||||
`--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`,
|
||||
`--layout-effects-start-${formatLanes(ReactFiberLane.SyncLane)}`,
|
||||
@@ -278,7 +278,7 @@ describe('SchedulingProfiler', () => {
|
||||
|
||||
expectMarksToEqual([
|
||||
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
|
||||
'--suspense-suspend-0-Example',
|
||||
'--suspense-suspend-0-Example-mount-16-Default',
|
||||
'--render-stop',
|
||||
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
|
||||
`--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
|
||||
@@ -317,7 +317,7 @@ describe('SchedulingProfiler', () => {
|
||||
|
||||
expectMarksToEqual([
|
||||
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
|
||||
'--suspense-suspend-0-Example',
|
||||
'--suspense-suspend-0-Example-mount-16-Default',
|
||||
'--render-stop',
|
||||
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
|
||||
`--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
|
||||
|
||||
Reference in New Issue
Block a user