diff --git a/static/app/components/events/interfaces/spans/profilingMeasurements.tsx b/static/app/components/events/interfaces/spans/profilingMeasurements.tsx index 8b9adaf9e95864..a504fe32f42c4c 100644 --- a/static/app/components/events/interfaces/spans/profilingMeasurements.tsx +++ b/static/app/components/events/interfaces/spans/profilingMeasurements.tsx @@ -211,7 +211,7 @@ function ProfilingMeasurements({ onMouseDown={onStartWindowSelection} > diff --git a/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx b/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx index b52c6d6f27f23b..c87fbf429d9be5 100644 --- a/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx +++ b/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx @@ -46,16 +46,19 @@ import { initializeFlamegraphRenderer, useResizeCanvasObserver, } from 'sentry/utils/profiling/gl/utils'; -import type {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile'; +import type {ContinuousProfileGroup} from 'sentry/utils/profiling/profile/importProfile'; import {FlamegraphRenderer2D} from 'sentry/utils/profiling/renderers/flamegraphRenderer2D'; import {FlamegraphRendererWebGL} from 'sentry/utils/profiling/renderers/flamegraphRendererWebGL'; import {Rect} from 'sentry/utils/profiling/speedscope'; import {UIFrames} from 'sentry/utils/profiling/uiFrames'; -import {fromNanoJoulesToWatts} from 'sentry/utils/profiling/units/units'; +import { + fromNanoJoulesToWatts, + type ProfilingFormatterUnit, +} from 'sentry/utils/profiling/units/units'; import {useDevicePixelRatio} from 'sentry/utils/useDevicePixelRatio'; import {useMemoWithPrevious} from 'sentry/utils/useMemoWithPrevious'; import {useContinuousProfile} from 'sentry/views/profiling/continuousProfileProvider'; -import {useProfileGroup} from 'sentry/views/profiling/profileGroupProvider'; +import {useContinuousProfileGroup} from 'sentry/views/profiling/profileGroupProvider'; import {FlamegraphDrawer} from './flamegraphDrawer/flamegraphDrawer'; import {FlamegraphWarnings} from './flamegraphOverlays/FlamegraphWarnings'; @@ -64,7 +67,7 @@ import {FlamegraphChart} from './flamegraphChart'; import {FlamegraphLayout} from './flamegraphLayout'; import {FlamegraphUIFrames} from './flamegraphUIFrames'; -function getMaxConfigSpace(profileGroup: ProfileGroup): Rect { +function getMaxConfigSpace(profileGroup: ContinuousProfileGroup): Rect { // We have a transaction, so we should do our best to align the profile // with the transaction's timeline. const maxProfileDuration = Math.max(...profileGroup.profiles.map(p => p.duration)); @@ -123,7 +126,7 @@ export function ContinuousFlamegraph(): ReactElement { const dispatch = useDispatchFlamegraphState(); const profiles = useContinuousProfile(); - const profileGroup = useProfileGroup(); + const profileGroup = useContinuousProfileGroup(); const flamegraphTheme = useFlamegraphTheme(); const position = useFlamegraphZoomPosition(); @@ -229,7 +232,11 @@ export function ContinuousFlamegraph(): ReactElement { } return new UIFrames( { + // @TODO + // @ts-expect-error slow: profileGroup.measurements?.slow_frame_renders, + // @TODO + // @ts-expect-error frozen: profileGroup.measurements?.frozen_frame_renders, }, {unit: flamegraph.profile.unit}, @@ -251,25 +258,31 @@ export function ContinuousFlamegraph(): ReactElement { for (const key in profileGroup.measurements) { if (key === 'cpu_energy_usage') { - measures.push({ - ...profileGroup.measurements[key]!, - values: profileGroup.measurements[key]!.values.map(v => { - return { - elapsed_since_start_ns: v.elapsed_since_start_ns, - value: fromNanoJoulesToWatts(v.value, 0.1), - }; - }), - // some versions of cocoa send byte so we need to correct it to watt - unit: 'watt', - name: 'CPU energy usage', - }); + const measurements = profileGroup.measurements[key]!; + const values: ProfileSeriesMeasurement['values'] = []; + + let offset = 0; + for (let i = 0; i < measurements.values.length; i++) { + const value = measurements.values[i]; + const next = measurements.values[i + 1] ?? value; + offset += (next.timestamp - value.timestamp) * 1e3; + + values.push({ + value: fromNanoJoulesToWatts(value.value, 0.1), + elapsed: offset, + }); + } + + // some versions of cocoa send byte so we need to correct it to watt + measures.push({name: 'CPU energy usage', unit: 'watt', values}); } } return new FlamegraphChartModel( Rect.From(flamegraph.configSpace), measures.length > 0 ? measures : [], - flamegraphTheme.COLORS.BATTERY_CHART_COLORS + flamegraphTheme.COLORS.BATTERY_CHART_COLORS, + {timelineUnit: 'milliseconds'} ); }, [profileGroup.measurements, flamegraph.configSpace, flamegraphTheme, hasCPUChart]); @@ -278,7 +291,7 @@ export function ContinuousFlamegraph(): ReactElement { return LOADING_OR_FALLBACK_CPU_CHART; } - const measures: ProfileSeriesMeasurement[] = []; + const cpuMeasurements: ProfileSeriesMeasurement[] = []; for (const key in profileGroup.measurements) { if (key.startsWith('cpu_usage')) { @@ -286,14 +299,31 @@ export function ContinuousFlamegraph(): ReactElement { key === 'cpu_usage' ? 'Average CPU usage' : `CPU Core ${key.replace('cpu_usage_', '')}`; - measures.push({...profileGroup.measurements[key]!, name}); + + const measurements = profileGroup.measurements[key]!; + const values: ProfileSeriesMeasurement['values'] = []; + + let offset = 0; + for (let i = 0; i < measurements.values.length; i++) { + const value = measurements.values[i]; + const next = measurements.values[i + 1] ?? value; + offset += (next.timestamp - value.timestamp) * 1e3; + + values.push({ + value: value.value, + elapsed: offset, + }); + } + + cpuMeasurements.push({name, unit: measurements?.unit, values}); } } return new FlamegraphChartModel( Rect.From(flamegraph.configSpace), - measures.length > 0 ? measures : [], - flamegraphTheme.COLORS.CPU_CHART_COLORS + cpuMeasurements.length > 0 ? cpuMeasurements : [], + flamegraphTheme.COLORS.CPU_CHART_COLORS, + {timelineUnit: 'milliseconds'} ); }, [profileGroup.measurements, flamegraph.configSpace, flamegraphTheme, hasCPUChart]); @@ -306,17 +336,47 @@ export function ContinuousFlamegraph(): ReactElement { const memory_footprint = profileGroup.measurements?.memory_footprint; if (memory_footprint) { + const values: ProfileSeriesMeasurement['values'] = []; + + let offset = 0; + for (let i = 0; i < memory_footprint.values.length; i++) { + const value = memory_footprint.values[i]; + const next = memory_footprint.values[i + 1] ?? value; + offset += (next.timestamp - value.timestamp) * 1e3; + + values.push({ + value: value.value, + elapsed: offset, + }); + } + measures.push({ - ...memory_footprint!, + unit: memory_footprint.unit, name: 'Heap Usage', + values, }); } const native_memory_footprint = profileGroup.measurements?.memory_native_footprint; if (native_memory_footprint) { + const values: ProfileSeriesMeasurement['values'] = []; + + let offset = 0; + for (let i = 0; i < native_memory_footprint.values.length; i++) { + const value = native_memory_footprint.values[i]; + const next = native_memory_footprint.values[i + 1] ?? value; + offset += (next.timestamp - value.timestamp) * 1e3; + + values.push({ + value: value.value, + elapsed: offset, + }); + } + measures.push({ - ...native_memory_footprint!, + unit: native_memory_footprint.unit, name: 'Native Heap Usage', + values, }); } @@ -324,7 +384,7 @@ export function ContinuousFlamegraph(): ReactElement { Rect.From(flamegraph.configSpace), measures.length > 0 ? measures : [], flamegraphTheme.COLORS.MEMORY_CHART_COLORS, - {type: 'area'} + {type: 'area', timelineUnit: 'milliseconds'} ); }, [ profileGroup.measurements, @@ -1080,7 +1140,7 @@ export function ContinuousFlamegraph(): ReactElement { batteryChart={ hasBatteryChart ? ( { return { - elapsed_since_start_ns: v.elapsed_since_start_ns, + elapsed: v.elapsed_since_start_ns, value: fromNanoJoulesToWatts(v.value, 0.1), }; }), @@ -414,7 +414,7 @@ function Flamegraph(): ReactElement { return LOADING_OR_FALLBACK_CPU_CHART; } - const measures: ProfileSeriesMeasurement[] = []; + const cpuMeasurements: ProfileSeriesMeasurement[] = []; for (const key in profileGroup.measurements) { if (key.startsWith('cpu_usage')) { @@ -422,13 +422,24 @@ function Flamegraph(): ReactElement { key === 'cpu_usage' ? 'Average CPU usage' : `CPU Core ${key.replace('cpu_usage_', '')}`; - measures.push({...profileGroup.measurements[key]!, name}); + + const measurements = profileGroup.measurements[key]!; + const values: ProfileSeriesMeasurement['values'] = []; + + for (let i = 0; i < measurements.values.length; i++) { + const value = measurements.values[i]; + values.push({ + value: value.value, + elapsed: value.elapsed_since_start_ns, + }); + } + cpuMeasurements.push({name, unit: measurements?.unit, values}); } } return new FlamegraphChartModel( Rect.From(flamegraph.configSpace), - measures.length > 0 ? measures : [], + cpuMeasurements.length > 0 ? cpuMeasurements : [], flamegraphTheme.COLORS.CPU_CHART_COLORS ); }, [profileGroup.measurements, flamegraph.configSpace, flamegraphTheme, hasCPUChart]); @@ -442,17 +453,39 @@ function Flamegraph(): ReactElement { const memory_footprint = profileGroup.measurements?.memory_footprint; if (memory_footprint) { + const values: ProfileSeriesMeasurement['values'] = []; + + for (let i = 0; i < memory_footprint.values.length; i++) { + const value = memory_footprint.values[i]; + values.push({ + value: value.value, + elapsed: value.elapsed_since_start_ns, + }); + } + measures.push({ - ...memory_footprint!, + unit: memory_footprint.unit, name: 'Heap Usage', + values, }); } const native_memory_footprint = profileGroup.measurements?.memory_native_footprint; if (native_memory_footprint) { + const values: ProfileSeriesMeasurement['values'] = []; + + for (let i = 0; i < native_memory_footprint.values.length; i++) { + const value = native_memory_footprint.values[i]; + values.push({ + value: value.value, + elapsed: value.elapsed_since_start_ns, + }); + } + measures.push({ - ...native_memory_footprint!, + unit: native_memory_footprint.unit, name: 'Native Heap Usage', + values, }); } diff --git a/static/app/components/profiling/flamegraph/flamegraphChart.tsx b/static/app/components/profiling/flamegraph/flamegraphChart.tsx index 14830a1b1bf621..39ff7a42f19182 100644 --- a/static/app/components/profiling/flamegraph/flamegraphChart.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphChart.tsx @@ -16,10 +16,9 @@ import { getPhysicalSpacePositionFromOffset, transformMatrixBetweenRect, } from 'sentry/utils/profiling/gl/utils'; -import type {Profile} from 'sentry/utils/profiling/profile/profile'; import {FlamegraphChartRenderer} from 'sentry/utils/profiling/renderers/chartRenderer'; import type {Rect} from 'sentry/utils/profiling/speedscope'; -import {formatTo} from 'sentry/utils/profiling/units/units'; +import {formatTo, type ProfilingFormatterUnit} from 'sentry/utils/profiling/units/units'; import {useCanvasScroll} from './interactions/useCanvasScroll'; import {useCanvasZoomOrScroll} from './interactions/useCanvasZoomOrScroll'; @@ -38,7 +37,7 @@ interface FlamegraphChartProps { chartCanvas: FlamegraphCanvas | null; chartCanvasRef: HTMLCanvasElement | null; chartView: CanvasView | null; - configViewUnit: Profile['unit']; + configViewUnit: ProfilingFormatterUnit; noMeasurementMessage: string | undefined; setChartCanvasRef: (ref: HTMLCanvasElement | null) => void; status: RequestState['type']; @@ -310,6 +309,7 @@ export function FlamegraphChart({ chartView={chartView} chartRenderer={chartRenderer} canvasBounds={canvasBounds} + configViewUnit={configViewUnit} /> ) : null} {/* transaction loads after profile, so we want to show loading even if it's in initial state */} diff --git a/static/app/components/profiling/flamegraph/flamegraphChartTooltip.tsx b/static/app/components/profiling/flamegraph/flamegraphChartTooltip.tsx index 585b2b9f4f00bd..b7a86cbc2036b1 100644 --- a/static/app/components/profiling/flamegraph/flamegraphChartTooltip.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphChartTooltip.tsx @@ -9,6 +9,7 @@ import type {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas'; import type {FlamegraphChart} from 'sentry/utils/profiling/flamegraphChart'; import type {FlamegraphChartRenderer} from 'sentry/utils/profiling/renderers/chartRenderer'; import type {Rect} from 'sentry/utils/profiling/speedscope'; +import {formatTo, type ProfilingFormatterUnit} from 'sentry/utils/profiling/units/units'; import { FlamegraphTooltipColorIndicator, @@ -23,6 +24,7 @@ export interface FlamegraphChartTooltipProps { chartRenderer: FlamegraphChartRenderer; chartView: CanvasView; configSpaceCursor: vec2; + configViewUnit: ProfilingFormatterUnit; } export function FlamegraphChartTooltip({ @@ -32,11 +34,14 @@ export function FlamegraphChartTooltip({ chart, chartRenderer, chartView, -}: // chartRenderer, -FlamegraphChartTooltipProps) { + configViewUnit, +}: FlamegraphChartTooltipProps) { const series = useMemo(() => { - return chartRenderer.findHoveredSeries(configSpaceCursor, 6e7); - }, [chartRenderer, configSpaceCursor]); + return chartRenderer.findHoveredSeries( + configSpaceCursor, + formatTo(100, 'milliseconds', configViewUnit) + ); + }, [chartRenderer, configSpaceCursor, configViewUnit]); return series.length > 0 ? ( void; onCopyFunctionSource: () => void; onHighlightAllOccurrencesClick: () => void; - profileGroup: ProfileGroup | null; + profileGroup: ProfileGroup | ContinuousProfileGroup | null; disableCallOrderSort?: boolean; disableColorCoding?: boolean; } diff --git a/static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphDrawer.tsx b/static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphDrawer.tsx index 192dc5209e6c91..c98baeb1ba221e 100644 --- a/static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphDrawer.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphDrawer.tsx @@ -18,7 +18,10 @@ import type {FlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/flam import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphPreferences'; import {useDispatchFlamegraphState} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphState'; import type {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame'; -import type {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile'; +import type { + ContinuousProfileGroup, + ProfileGroup, +} from 'sentry/utils/profiling/profile/importProfile'; import {invertCallTree} from 'sentry/utils/profiling/profile/utils'; import {useLocalStorageState} from 'sentry/utils/useLocalStorageState'; import useOrganization from 'sentry/utils/useOrganization'; @@ -34,7 +37,7 @@ interface FlamegraphDrawerProps { flamegraph: Flamegraph; formatDuration: Flamegraph['formatter']; getFrameColor: (frame: FlamegraphFrame) => string; - profileGroup: ProfileGroup; + profileGroup: ProfileGroup | ContinuousProfileGroup; profileTransaction: ReturnType | null; referenceNode: FlamegraphFrame; rootNodes: FlamegraphFrame[]; diff --git a/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx b/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx index aef4a5d633151c..2f21c53c553f78 100644 --- a/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx @@ -11,13 +11,17 @@ import Link from 'sentry/components/links/link'; import Version from 'sentry/components/version'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; -import type {Organization, Project} from 'sentry/types'; import type {EventTransaction} from 'sentry/types/event'; import {DeviceContextKey} from 'sentry/types/event'; +import type {Organization} from 'sentry/types/organization'; +import type {Project} from 'sentry/types/project'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import type {FlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphPreferences'; import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphPreferences'; -import type {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile'; +import type { + ContinuousProfileGroup, + ProfileGroup, +} from 'sentry/utils/profiling/profile/importProfile'; import {makeFormatter} from 'sentry/utils/profiling/units/units'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; @@ -33,7 +37,7 @@ import {ProfilingDetailsFrameTabs, ProfilingDetailsListItem} from './flamegraphD function renderValue( key: string, value: number | string | undefined, - profileGroup?: ProfileGroup + profileGroup?: ProfileGroup | ContinuousProfileGroup ) { if (key === 'threads' && value === undefined) { return profileGroup?.profiles.length; @@ -49,7 +53,7 @@ function renderValue( } interface ProfileDetailsProps { - profileGroup: ProfileGroup; + profileGroup: ProfileGroup | ContinuousProfileGroup; projectId: string; transaction: EventTransaction | null; } @@ -184,7 +188,7 @@ function TransactionDeviceDetails({ profileGroup, transaction, }: { - profileGroup: ProfileGroup; + profileGroup: ProfileGroup | ContinuousProfileGroup; transaction: EventTransaction; }) { const deviceDetails = useMemo(() => { @@ -253,7 +257,7 @@ function TransactionEventDetails({ transaction, }: { organization: Organization; - profileGroup: ProfileGroup; + profileGroup: ProfileGroup | ContinuousProfileGroup; project: Project | undefined; transaction: EventTransaction; }) { @@ -348,7 +352,11 @@ function TransactionEventDetails({ ); } -function ProfileEnvironmentDetails({profileGroup}: {profileGroup: ProfileGroup}) { +function ProfileEnvironmentDetails({ + profileGroup, +}: { + profileGroup: ProfileGroup | ContinuousProfileGroup; +}) { return ( {Object.entries(ENVIRONMENT_DETAILS_KEY).map(([label, key]) => { @@ -371,7 +379,7 @@ function ProfileEventDetails({ transaction, }: { organization: Organization; - profileGroup: ProfileGroup; + profileGroup: ProfileGroup | ContinuousProfileGroup; project: Project | undefined; transaction: EventTransaction | null; }) { diff --git a/static/app/components/profiling/flamegraph/flamegraphToolbar/flamegraphThreadSelector.tsx b/static/app/components/profiling/flamegraph/flamegraphToolbar/flamegraphThreadSelector.tsx index 1761668e0e31e5..d2a616aabb0c88 100644 --- a/static/app/components/profiling/flamegraph/flamegraphToolbar/flamegraphThreadSelector.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphToolbar/flamegraphThreadSelector.tsx @@ -8,13 +8,16 @@ import {t, tn} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {defined} from 'sentry/utils'; import type {FlamegraphState} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext'; -import type {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile'; +import type { + ContinuousProfileGroup, + ProfileGroup, +} from 'sentry/utils/profiling/profile/importProfile'; import type {Profile} from 'sentry/utils/profiling/profile/profile'; import {makeFormatter} from 'sentry/utils/profiling/units/units'; export interface FlamegraphThreadSelectorProps { onThreadIdChange: (threadId: Profile['threadId']) => void; - profileGroup: ProfileGroup; + profileGroup: ProfileGroup | ContinuousProfileGroup; threadId: FlamegraphState['profiles']['threadId']; } diff --git a/static/app/components/profiling/flamegraph/flamegraphZoomView.tsx b/static/app/components/profiling/flamegraph/flamegraphZoomView.tsx index 46ae04d5476aa1..ba9c8faa9ff842 100644 --- a/static/app/components/profiling/flamegraph/flamegraphZoomView.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphZoomView.tsx @@ -28,7 +28,10 @@ import { } from 'sentry/utils/profiling/gl/utils'; import {useContextMenu} from 'sentry/utils/profiling/hooks/useContextMenu'; import {useInternalFlamegraphDebugMode} from 'sentry/utils/profiling/hooks/useInternalFlamegraphDebugMode'; -import type {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile'; +import type { + ContinuousProfileGroup, + ProfileGroup, +} from 'sentry/utils/profiling/profile/importProfile'; import type {FlamegraphRenderer} from 'sentry/utils/profiling/renderers/flamegraphRenderer'; import {FlamegraphTextRenderer} from 'sentry/utils/profiling/renderers/flamegraphTextRenderer'; import {GridRenderer} from 'sentry/utils/profiling/renderers/gridRenderer'; @@ -80,7 +83,7 @@ interface FlamegraphZoomViewProps { flamegraphOverlayCanvasRef: HTMLCanvasElement | null; flamegraphRenderer: FlamegraphRenderer | null; flamegraphView: CanvasView | null; - profileGroup: ProfileGroup; + profileGroup: ProfileGroup | ContinuousProfileGroup; setFlamegraphCanvasRef: React.Dispatch>; setFlamegraphOverlayCanvasRef: React.Dispatch< React.SetStateAction diff --git a/static/app/types/profiling.d.ts b/static/app/types/profiling.d.ts index c5e94c2d84cc8d..dbd174dffa4ee7 100644 --- a/static/app/types/profiling.d.ts +++ b/static/app/types/profiling.d.ts @@ -12,11 +12,21 @@ declare namespace Profiling { value: number; }; + type ContinuousMeasurementValue = { + timestamp: number; + value: number; + }; + type Measurement = { unit: string; values: MeasurementValue[]; }; + type ContinuousMeasurement = { + unit: string; + values: ContinuousMeasurementValue[]; + }; + type Measurements = { cpu_usage?: Measurement; memory_footprint?: Measurement; @@ -26,6 +36,15 @@ declare namespace Profiling { [key: string]: Measurement; }; + type ContinuousMeasurements = { + cpu_usage?: ContinuousMeasurement; + memory_footprint?: ContinuousMeasurement; + frozen_frame_renders?: ContinuousMeasurement; + screen_frame_rates?: ContinuousMeasurement; + slow_frame_renders?: ContinuousMeasurement; + [key: string]: ContinuousMeasurement; + }; + type SentrySampledProfileSample = { stack_id: number; thread_id: string; @@ -117,7 +136,7 @@ declare namespace Profiling { images: Image[]; }; platform: string; - measurements?: Measurements; + measurements?: ContinuousMeasurements; profile: ContinuousProfile; } diff --git a/static/app/utils/profiling/flamegraphChart.tsx b/static/app/utils/profiling/flamegraphChart.tsx index fe818e5ed6f6ea..29531dbc36b759 100644 --- a/static/app/utils/profiling/flamegraphChart.tsx +++ b/static/app/utils/profiling/flamegraphChart.tsx @@ -17,8 +17,10 @@ interface Series { type: 'line' | 'area'; } -export interface ProfileSeriesMeasurement extends Profiling.Measurement { +export interface ProfileSeriesMeasurement { name: string; + unit: string; + values: {elapsed: number; value: number}[]; } function computeLabelPrecision(min: number, max: number): number { @@ -35,6 +37,7 @@ function computeLabelPrecision(min: number, max: number): number { } interface ChartOptions { + timelineUnit?: ProfilingFormatterUnit; type?: 'line' | 'area'; } @@ -64,7 +67,7 @@ export class FlamegraphChart { options: ChartOptions = {} ) { this.series = new Array(); - this.timelineFormatter = makeTimelineFormatter('nanoseconds'); + this.timelineFormatter = makeTimelineFormatter(options.timelineUnit ?? 'nanoseconds'); if (!measurements || !measurements.length) { this.formatter = makeFormatter('percent'); @@ -115,14 +118,14 @@ export class FlamegraphChart { } // Track and update X domain max and min - if (m.elapsed_since_start_ns > this.domains.x[1]) { - this.domains.x[1] = m.elapsed_since_start_ns; + if (m.elapsed > this.domains.x[1]) { + this.domains.x[1] = m.elapsed; } - if (m.elapsed_since_start_ns < this.domains.x[0]) { - this.domains.x[1] = m.elapsed_since_start_ns; + if (m.elapsed < this.domains.x[0]) { + this.domains.x[1] = m.elapsed; } - this.series[j].points[i] = {x: m.elapsed_since_start_ns, y: m.value}; + this.series[j].points[i] = {x: m.elapsed, y: m.value}; } } diff --git a/static/app/utils/profiling/hooks/useDifferentialFlamegraphModel.tsx b/static/app/utils/profiling/hooks/useDifferentialFlamegraphModel.tsx index c7a3d86ce00079..af26d95b1eb2a9 100644 --- a/static/app/utils/profiling/hooks/useDifferentialFlamegraphModel.tsx +++ b/static/app/utils/profiling/hooks/useDifferentialFlamegraphModel.tsx @@ -53,7 +53,12 @@ export function useDifferentialFlamegraphModel( return null; } - return importProfile(props.after.data, '', 'flamegraph', props.frameFilter); + return importProfile( + props.after.data, + '', + 'flamegraph', + props.frameFilter + ) as ProfileGroup; }, [props.after.data, props.frameFilter]); const afterFlamegraph = useMemo(() => { diff --git a/static/app/utils/profiling/profile/eventedProfile.tsx b/static/app/utils/profiling/profile/eventedProfile.tsx index d20f57f8153a38..407cbbbac5e9f0 100644 --- a/static/app/utils/profiling/profile/eventedProfile.tsx +++ b/static/app/utils/profiling/profile/eventedProfile.tsx @@ -1,7 +1,7 @@ import {lastOfArray} from 'sentry/utils/array/lastOfArray'; import {CallTreeNode} from 'sentry/utils/profiling/callTreeNode'; import type {Frame} from 'sentry/utils/profiling/frame'; -import {formatTo} from 'sentry/utils/profiling/units/units'; +import {assertValidProfilingUnit, formatTo} from 'sentry/utils/profiling/units/units'; import {Profile} from './profile'; import type {createFrameIndex} from './utils'; @@ -21,6 +21,7 @@ export class EventedProfile extends Profile { frameFilter?: (frame: Frame) => boolean; } ): EventedProfile { + assertValidProfilingUnit(eventedProfile.unit); const profile = new EventedProfile({ duration: eventedProfile.endValue - eventedProfile.startValue, startedAt: eventedProfile.startValue, diff --git a/static/app/utils/profiling/profile/importProfile.tsx b/static/app/utils/profiling/profile/importProfile.tsx index 239e20c749a820..5bb7639205332d 100644 --- a/static/app/utils/profiling/profile/importProfile.tsx +++ b/static/app/utils/profiling/profile/importProfile.tsx @@ -37,12 +37,25 @@ export interface ImportOptions { export interface ProfileGroup { activeProfileIndex: number; - measurements: Partial; + measurements: Partial; metadata: Partial; name: string; profiles: Profile[]; traceID: string; transactionID: string | null; + type: 'transaction'; + images?: Image[]; +} + +export interface ContinuousProfileGroup { + activeProfileIndex: number; + measurements: Partial; + metadata: Partial; + name: string; + profiles: Profile[]; + traceID: string; + transactionID: string | null; + type: 'continuous'; images?: Image[]; } @@ -51,7 +64,7 @@ export function importProfile( traceID: string, type: 'flamegraph' | 'flamechart', frameFilter?: (frame: Frame) => boolean -): ProfileGroup { +): ProfileGroup | ContinuousProfileGroup { return Sentry.withScope(scope => { const span = Sentry.startInactiveSpan({ op: 'import', @@ -102,6 +115,7 @@ function importJSSelfProfile( const profile = importSingleProfile(input, frameIndex, options); return { + type: 'transaction', traceID, name: traceID, transactionID: null, @@ -175,11 +189,12 @@ function importSentrySampledProfile( } return { + type: 'transaction', transactionID: input.transaction.id, traceID: input.transaction.trace_id, name: input.transaction.name, activeProfileIndex, - measurements: input.measurements, + measurements: input.measurements ?? {}, metadata: { deviceLocale: input.device.locale, deviceManufacturer: input.device.manufacturer, @@ -218,6 +233,7 @@ export function importSchema( ); return { + type: 'transaction', traceID, transactionID: input.metadata.transactionID ?? null, name: input.metadata?.transactionName ?? traceID, @@ -237,7 +253,7 @@ export function importSentryContinuousProfileChunk( input: Readonly, traceID: string, options: ImportOptions -): ProfileGroup { +): ContinuousProfileGroup { const frameIndex = createContinuousProfileFrameIndex( input.profile.frames, input.platform @@ -246,10 +262,11 @@ export function importSentryContinuousProfileChunk( return { traceID, name: '', + type: 'continuous', transactionID: null, activeProfileIndex: 0, profiles: [importSingleProfile(input.profile, frameIndex, options)], - measurements: {}, + measurements: input.measurements ?? {}, metadata: { platform: input.platform, }, @@ -266,9 +283,9 @@ function importSingleProfile( | ReturnType | ReturnType | ReturnType, - {span, type, continuous, frameFilter, profileIds}: ImportOptions + {span, type, frameFilter, profileIds}: ImportOptions ): Profile { - if (continuous && isSentryContinuousProfile(profile)) { + if (isSentryContinuousProfile(profile)) { // In some cases, the SDK may return spans as undefined and we dont want to throw there. if (!span) { return ContinuousProfile.FromProfile(profile, frameIndex); diff --git a/static/app/utils/profiling/profile/profile.spec.tsx b/static/app/utils/profiling/profile/profile.spec.tsx index 658e7c2a8ab9fa..1a92cb0103976d 100644 --- a/static/app/utils/profiling/profile/profile.spec.tsx +++ b/static/app/utils/profiling/profile/profile.spec.tsx @@ -78,7 +78,7 @@ describe('Profile', () => { startedAt: 0, endedAt: 1000, name: 'profile', - unit: 'ms', + unit: 'millisecond', threadId: 0, type: 'flamechart', }); @@ -114,7 +114,7 @@ describe('Profile', () => { startedAt: 0, endedAt: 1000, name: 'profile', - unit: 'ms', + unit: 'millisecond', threadId: 0, type: 'flamechart', }); @@ -156,7 +156,7 @@ describe('Profile', () => { startedAt: 0, endedAt: 1000, name: 'profile', - unit: 'ms', + unit: 'millisecond', threadId: 0, type: 'flamechart', }); @@ -197,7 +197,7 @@ describe('Profile', () => { startedAt: 0, endedAt: 1000, name: 'profile', - unit: 'ms', + unit: 'millisecond', threadId: 0, type: 'flamechart', }); diff --git a/static/app/utils/profiling/profile/profile.tsx b/static/app/utils/profiling/profile/profile.tsx index 75af90ac5930bb..ce85365f92ff0e 100644 --- a/static/app/utils/profiling/profile/profile.tsx +++ b/static/app/utils/profiling/profile/profile.tsx @@ -1,3 +1,5 @@ +import type {ProfilingFormatterUnit} from 'sentry/utils/profiling/units/units'; + import {CallTreeNode} from '../callTreeNode'; import {Frame} from '../frame'; @@ -21,7 +23,7 @@ export class Profile { type: string; // Unit in which the timings are reported in - unit = 'microseconds'; + unit: ProfilingFormatterUnit = 'microseconds'; // Name of the profile name = 'Unknown'; @@ -62,7 +64,7 @@ export class Profile { startedAt: number; threadId: number; type: 'flamechart' | 'flamegraph' | 'empty'; - unit: string; + unit: ProfilingFormatterUnit; timestamp?: number; }) { this.threadId = threadId; diff --git a/static/app/utils/profiling/profile/sampledProfile.tsx b/static/app/utils/profiling/profile/sampledProfile.tsx index 7bbf19b99c23df..cb03cc6aabd8e9 100644 --- a/static/app/utils/profiling/profile/sampledProfile.tsx +++ b/static/app/utils/profiling/profile/sampledProfile.tsx @@ -1,5 +1,7 @@ import {CallTreeNode} from 'sentry/utils/profiling/callTreeNode'; +import {assertValidProfilingUnit} from '../units/units'; + import {Frame} from './../frame'; import {Profile} from './profile'; import type {createFrameIndex} from './utils'; @@ -60,6 +62,7 @@ export class SampledProfile extends Profile { profileIds?: Readonly; } ): Profile { + assertValidProfilingUnit(sampledProfile.unit); const profile = new SampledProfile({ duration: sampledProfile.endValue - sampledProfile.startValue, startedAt: sampledProfile.startValue, @@ -88,7 +91,7 @@ export class SampledProfile extends Profile { ); } - function resolveFrame(index) { + function resolveFrame(index: number) { const resolvedFrame = frameIndex[index]; if (!resolvedFrame) { throw new Error(`Could not resolve frame ${index} in frame index`); diff --git a/static/app/views/profiling/profileGroupProvider.tsx b/static/app/views/profiling/profileGroupProvider.tsx index 93ee12fb388a0c..fdc715af0de26a 100644 --- a/static/app/views/profiling/profileGroupProvider.tsx +++ b/static/app/views/profiling/profileGroupProvider.tsx @@ -2,18 +2,48 @@ import {createContext, useContext, useMemo} from 'react'; import * as Sentry from '@sentry/react'; import type {Frame} from 'sentry/utils/profiling/frame'; -import type {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile'; +import type { + ContinuousProfileGroup, + ProfileGroup, +} from 'sentry/utils/profiling/profile/importProfile'; import {importProfile} from 'sentry/utils/profiling/profile/importProfile'; -type ProfileGroupContextValue = ProfileGroup; - +type ProfileGroupContextValue = ContinuousProfileGroup | ProfileGroup; const ProfileGroupContext = createContext(null); -export function useProfileGroup() { +function assertContinuousProfileGroup( + input: ProfileGroupContextValue | null +): asserts input is ContinuousProfileGroup { + if (input && input.type !== 'continuous') { + throw new Error('ProfileGroup is not of continuous profile type.'); + } +} + +function assertTransactionProfileGroup( + input: ProfileGroupContextValue | null +): asserts input is ProfileGroup { + if (input && input.type !== 'transaction') { + throw new Error('ProfileGroup is not of transaction profile type.'); + } +} + +export function useProfileGroup(): ProfileGroup { const context = useContext(ProfileGroupContext); if (!context) { throw new Error('useProfileGroup was called outside of ProfileGroupProvider'); } + assertTransactionProfileGroup(context); + return context; +} + +export function useContinuousProfileGroup(): ContinuousProfileGroup { + const context = useContext(ProfileGroupContext); + if (!context) { + throw new Error( + 'useContinuousProfileGroup was called outside of ProfileGroupProvider' + ); + } + assertContinuousProfileGroup(context); return context; } @@ -25,6 +55,7 @@ export const LOADING_PROFILE_GROUP: Readonly = { measurements: {}, traceID: '', profiles: [], + type: 'transaction', }; interface ProfileGroupProviderProps {