import { useRef, useState } from 'preact/hooks'; import { getBatchedRectMap } from 'src/new-outlines'; import { getIsProduction } from '~core/index'; import { iife } from '~core/notifications/performance-utils'; import { cn } from '~web/utils/helpers'; import { GroupedFiberRender, NotificationEvent, getTotalTime, isRenderMemoizable, useNotificationsContext, } from './data'; import { HighlightStore, drawHighlights, } from '~core/notifications/outline-overlay'; import { ChevronRight } from './icons'; // todo: cleanup, convoluted ternaries export const fadeOutHighlights = () => { const curr = HighlightStore.value.current ? HighlightStore.value.current : HighlightStore.value.kind === 'transition' ? HighlightStore.value.transitionTo : null; if (!curr) { return; } if (HighlightStore.value.kind === 'transition') { HighlightStore.value = { kind: 'move-out', // because we want to dynamically fade this value current: HighlightStore.value.current?.alpha === 0 ? // we want to only start fading from transition if current is done animating out HighlightStore.value.transitionTo : // if current doesn't exist then transition must exist (HighlightStore.value.current ?? HighlightStore.value.transitionTo), }; return; } HighlightStore.value = { kind: 'move-out', current: { alpha: 0, ...curr, }, }; }; type Bars = Array< | { kind: 'other-frame-drop'; totalTime: number } | { kind: 'other-not-javascript'; totalTime: number } | { kind: 'other-javascript'; totalTime: number } | { kind: 'render'; event: GroupedFiberRender; totalTime: number } >; export const NO_PURGE = ['hover:bg-[#0f0f0f]']; export const RenderBarChart = ({ selectedEvent, }: { selectedEvent: NotificationEvent }) => { const totalInteractionTime = getTotalTime(selectedEvent.timing); const nonRender = totalInteractionTime - selectedEvent.timing.renderTime; const [isProduction] = useState(getIsProduction()); const events = selectedEvent.groupedFiberRenders; const bars: Bars = events.map((event) => ({ event, kind: 'render', totalTime: isProduction ? event.count : event.totalTime, })); const isShowingExtraInfo = iife(() => { switch (selectedEvent.kind) { case 'dropped-frames': { return selectedEvent.timing.renderTime / totalInteractionTime < 0.1; } case 'interaction': { return ( (selectedEvent.timing.otherJSTime + selectedEvent.timing.renderTime) / totalInteractionTime < 0.2 ); } } }); /** * We don't add the extra bars in production because we can't compare them to the renders, so the bar is useless, user can use overview tab to see times */ if (selectedEvent.kind === 'interaction' && !isProduction) { bars.push({ kind: 'other-javascript', totalTime: selectedEvent.timing.otherJSTime, }); } if (isShowingExtraInfo && !isProduction) { if (selectedEvent.kind === 'interaction') { bars.push({ kind: 'other-not-javascript', totalTime: getTotalTime(selectedEvent.timing) - selectedEvent.timing.renderTime - selectedEvent.timing.otherJSTime, }); } else { bars.push({ kind: 'other-frame-drop', totalTime: nonRender, }); } } const debouncedMouseEnter = useRef<{ timer: ReturnType | null; lastCallAt: number | null; }>({ lastCallAt: null, timer: null, }); const totalBarTime = bars.reduce((prev, curr) => prev + curr.totalTime, 0); return (
{iife(() => { if (isProduction && bars.length === 0) { return (

No data available

No data was collected during this period

); } if (bars.length === 0) { return (

No renders collected

There were no renders during this period

); } })} {bars .toSorted((a, b) => b.totalTime - a.totalTime) .map((bar) => ( ))}
); }; const getTransitionState = (state: { current: { alpha: number } | null; transitionTo: { alpha: number }; }) => { if (!state.current) { return 'fading-in'; } if (state.current.alpha > 0) { return 'fading-out' as const; } return 'fading-in' as const; }; const RenderBar = ({ bar, debouncedMouseEnter, totalBarTime, isProduction, bars, depth = 0, }: { depth?: number; bars: Bars; bar: Bars[number]; debouncedMouseEnter: { current: { timer: ReturnType | null; lastCallAt: number | null; }; }; totalBarTime: number; isProduction: boolean | null; }) => { const { setNotificationState, setRoute } = useNotificationsContext(); const [isExpanded, setIsExpanded] = useState(false); const isLeaf = bar.kind === 'render' ? bar.event.parents.size === 0 : true; const parentBars = bars.filter((otherBar) => otherBar.kind === 'render' && bar.kind === 'render' ? bar.event.parents.has(otherBar.event.name) && otherBar.event.name !== bar.event.name : false, ); const missingParentNames = bar.kind === 'render' ? Array.from(bar.event.parents).filter( (parentName) => !bars.some( (b) => b.kind === 'render' && b.event.name === parentName, ), ) : []; const handleBarClick = () => { if (bar.kind === 'render') { setNotificationState((prev) => ({ ...prev, selectedFiber: bar.event, })); setRoute({ route: 'render-explanation', routeMessage: null, }); } else { setRoute({ route: 'other-visualization', routeMessage: { kind: 'auto-open-overview-accordion', name: bar.kind, }, }); } }; return (
{depth === 0 && (
Click to learn more
)}
{isExpanded && (parentBars.length > 0 || missingParentNames.length > 0) && (
{parentBars .toSorted((a, b) => b.totalTime - a.totalTime) .map((parentBar, i) => ( ))} {missingParentNames.map((parentName) => (
{parentName}
))}
)}
); };