Browse Source

feat(profiling) render continuous profile chunk flamechart (#74135)

Renders continuous flamechart for a profile chunk.

Disclaimer:
This is one big copy paste of the flamechart component with removal of
span tree code and any reference to the old txn model. Since the
underlying components are not tied to this model anymore, it means they
can be wired up to work with the continuous model. I opted for this to
have cleaner separation and make it easier to make modifications without
having to think of both models - since the main component mostly just
wires up the different parts of the UI, I think that is a good approach
to take
Jonas 8 months ago
parent
commit
85148ec85a

+ 1199 - 0
static/app/components/profiling/flamegraph/continuousFlamegraph.tsx

@@ -0,0 +1,1199 @@
+import type {ReactElement} from 'react';
+import {
+  Fragment,
+  useCallback,
+  useEffect,
+  useLayoutEffect,
+  useMemo,
+  useState,
+} from 'react';
+import * as Sentry from '@sentry/react';
+import {mat3, vec2} from 'gl-matrix';
+
+import {addErrorMessage} from 'sentry/actionCreators/indicator';
+import {FlamegraphOptionsMenu} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphOptionsMenu';
+import {FlamegraphSearch} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch';
+import type {FlamegraphThreadSelectorProps} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphThreadSelector';
+import {FlamegraphThreadSelector} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphThreadSelector';
+import {FlamegraphToolbar} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphToolbar';
+import type {FlamegraphViewSelectMenuProps} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphViewSelectMenu';
+import {FlamegraphViewSelectMenu} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphViewSelectMenu';
+import {FlamegraphZoomView} from 'sentry/components/profiling/flamegraph/flamegraphZoomView';
+import {FlamegraphZoomViewMinimap} from 'sentry/components/profiling/flamegraph/flamegraphZoomViewMinimap';
+import {t} from 'sentry/locale';
+import {defined} from 'sentry/utils';
+import {
+  CanvasPoolManager,
+  useCanvasScheduler,
+} from 'sentry/utils/profiling/canvasScheduler';
+import {CanvasView} from 'sentry/utils/profiling/canvasView';
+import {Flamegraph as FlamegraphModel} from 'sentry/utils/profiling/flamegraph';
+import type {FlamegraphSearch as FlamegraphSearchType} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphSearch';
+import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphPreferences';
+import {useFlamegraphProfiles} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphProfiles';
+import {useFlamegraphSearch} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphSearch';
+import {useDispatchFlamegraphState} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphState';
+import {useFlamegraphZoomPosition} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphZoomPosition';
+import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
+import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
+import type {ProfileSeriesMeasurement} from 'sentry/utils/profiling/flamegraphChart';
+import {FlamegraphChart as FlamegraphChartModel} from 'sentry/utils/profiling/flamegraphChart';
+import type {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
+import {
+  computeConfigViewWithStrategy,
+  computeMinZoomConfigViewForFrames,
+  formatColorForFrame,
+  initializeFlamegraphRenderer,
+  useResizeCanvasObserver,
+} from 'sentry/utils/profiling/gl/utils';
+import type {ProfileGroup} 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 {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 {FlamegraphDrawer} from './flamegraphDrawer/flamegraphDrawer';
+import {FlamegraphWarnings} from './flamegraphOverlays/FlamegraphWarnings';
+import {useViewKeyboardNavigation} from './interactions/useViewKeyboardNavigation';
+import {FlamegraphChart} from './flamegraphChart';
+import {FlamegraphLayout} from './flamegraphLayout';
+import {FlamegraphUIFrames} from './flamegraphUIFrames';
+
+function getMaxConfigSpace(profileGroup: ProfileGroup): 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));
+  // No transaction was found, so best we can do is align it to the starting
+  // position of the profiles - find the max of profile durations
+  return new Rect(0, 0, maxProfileDuration, 0);
+}
+
+type FlamegraphCandidate = {
+  frame: FlamegraphFrame;
+  threadId: number;
+  isActiveThread?: boolean; // this is the thread referred to by the active profile index
+};
+
+function findLongestMatchingFrame(
+  flamegraph: FlamegraphModel,
+  focusFrame: FlamegraphSearchType['highlightFrames']
+): FlamegraphFrame | null {
+  if (focusFrame === null) {
+    return null;
+  }
+
+  let longestFrame: FlamegraphFrame | null = null;
+
+  const frames: FlamegraphFrame[] = [...flamegraph.root.children];
+  while (frames.length > 0) {
+    const frame = frames.pop()!;
+    if (
+      focusFrame.name === frame.frame.name &&
+      // the image name on a frame is optional treat it the same as the empty string
+      (focusFrame.package === (frame.frame.package || '') ||
+        focusFrame.package === (frame.frame.module || '')) &&
+      (longestFrame?.node?.totalWeight || 0) < frame.node.totalWeight
+    ) {
+      longestFrame = frame;
+    }
+
+    for (let i = 0; i < frame.children.length; i++) {
+      frames.push(frame.children[i]);
+    }
+  }
+
+  return longestFrame;
+}
+
+const LOADING_OR_FALLBACK_FLAMEGRAPH = FlamegraphModel.Empty();
+const LOADING_OR_FALLBACK_UIFRAMES = UIFrames.Empty;
+const LOADING_OR_FALLBACK_BATTERY_CHART = FlamegraphChartModel.Empty;
+const LOADING_OR_FALLBACK_CPU_CHART = FlamegraphChartModel.Empty;
+const LOADING_OR_FALLBACK_MEMORY_CHART = FlamegraphChartModel.Empty;
+
+const noopFormatDuration = () => '';
+
+export function ContinuousFlamegraph(): ReactElement {
+  const devicePixelRatio = useDevicePixelRatio();
+  const dispatch = useDispatchFlamegraphState();
+
+  const profiles = useContinuousProfile();
+  const profileGroup = useProfileGroup();
+
+  const flamegraphTheme = useFlamegraphTheme();
+  const position = useFlamegraphZoomPosition();
+  const {colorCoding, sorting, view} = useFlamegraphPreferences();
+  const {highlightFrames} = useFlamegraphSearch();
+  const flamegraphProfiles = useFlamegraphProfiles();
+
+  const [flamegraphCanvasRef, setFlamegraphCanvasRef] =
+    useState<HTMLCanvasElement | null>(null);
+  const [flamegraphOverlayCanvasRef, setFlamegraphOverlayCanvasRef] =
+    useState<HTMLCanvasElement | null>(null);
+
+  const [flamegraphMiniMapCanvasRef, setFlamegraphMiniMapCanvasRef] =
+    useState<HTMLCanvasElement | null>(null);
+  const [flamegraphMiniMapOverlayCanvasRef, setFlamegraphMiniMapOverlayCanvasRef] =
+    useState<HTMLCanvasElement | null>(null);
+
+  const [uiFramesCanvasRef, setUIFramesCanvasRef] = useState<HTMLCanvasElement | null>(
+    null
+  );
+
+  const [batteryChartCanvasRef, setBatteryChartCanvasRef] =
+    useState<HTMLCanvasElement | null>(null);
+  const [cpuChartCanvasRef, setCpuChartCanvasRef] = useState<HTMLCanvasElement | null>(
+    null
+  );
+  const [memoryChartCanvasRef, setMemoryChartCanvasRef] =
+    useState<HTMLCanvasElement | null>(null);
+
+  const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
+  const scheduler = useCanvasScheduler(canvasPoolManager);
+
+  const hasUIFrames = useMemo(() => {
+    const platform = profileGroup.metadata.platform;
+    return platform === 'cocoa' || platform === 'android';
+  }, [profileGroup.metadata.platform]);
+
+  const hasBatteryChart = useMemo(() => {
+    const platform = profileGroup.metadata.platform;
+    return platform === 'cocoa' || profileGroup.measurements?.cpu_energy_usage;
+  }, [profileGroup.metadata.platform, profileGroup.measurements]);
+
+  const hasCPUChart = useMemo(() => {
+    const platform = profileGroup.metadata.platform;
+    return (
+      platform === 'cocoa' ||
+      platform === 'android' ||
+      platform === 'node' ||
+      profileGroup.measurements?.cpu_usage
+    );
+  }, [profileGroup.metadata.platform, profileGroup.measurements]);
+
+  const hasMemoryChart = useMemo(() => {
+    const platform = profileGroup.metadata.platform;
+    return (
+      platform === 'cocoa' ||
+      platform === 'android' ||
+      platform === 'node' ||
+      profileGroup.measurements?.memory_footprint
+    );
+  }, [profileGroup.metadata.platform, profileGroup.measurements]);
+
+  const profile = useMemo(() => {
+    return profileGroup.profiles.find(p => p.threadId === flamegraphProfiles.threadId);
+  }, [profileGroup, flamegraphProfiles.threadId]);
+
+  const flamegraph = useMemo(() => {
+    if (typeof flamegraphProfiles.threadId !== 'number') {
+      return LOADING_OR_FALLBACK_FLAMEGRAPH;
+    }
+
+    // This could happen if threadId was initialized from query string, but for some
+    // reason the profile was removed from the list of profiles.
+    if (!profile) {
+      return LOADING_OR_FALLBACK_FLAMEGRAPH;
+    }
+
+    const span = Sentry.withScope(scope => {
+      scope.setTag('sorting', sorting.split(' ').join('_'));
+      scope.setTag('view', view.split(' ').join('_'));
+
+      return Sentry.startInactiveSpan({
+        op: 'import',
+        name: 'flamegraph.constructor',
+        forceTransaction: true,
+      });
+    });
+
+    const newFlamegraph = new FlamegraphModel(profile, {
+      inverted: view === 'bottom up',
+      sort: sorting,
+      configSpace: getMaxConfigSpace(profileGroup),
+    });
+
+    span?.end();
+
+    return newFlamegraph;
+  }, [profile, profileGroup, sorting, flamegraphProfiles.threadId, view]);
+
+  const uiFrames = useMemo(() => {
+    if (!hasUIFrames) {
+      return LOADING_OR_FALLBACK_UIFRAMES;
+    }
+    return new UIFrames(
+      {
+        slow: profileGroup.measurements?.slow_frame_renders,
+        frozen: profileGroup.measurements?.frozen_frame_renders,
+      },
+      {unit: flamegraph.profile.unit},
+      flamegraph.configSpace.withHeight(1)
+    );
+  }, [
+    profileGroup.measurements,
+    flamegraph.profile.unit,
+    flamegraph.configSpace,
+    hasUIFrames,
+  ]);
+
+  const batteryChart = useMemo(() => {
+    if (!hasCPUChart) {
+      return LOADING_OR_FALLBACK_BATTERY_CHART;
+    }
+
+    const measures: ProfileSeriesMeasurement[] = [];
+
+    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',
+        });
+      }
+    }
+
+    return new FlamegraphChartModel(
+      Rect.From(flamegraph.configSpace),
+      measures.length > 0 ? measures : [],
+      flamegraphTheme.COLORS.BATTERY_CHART_COLORS
+    );
+  }, [profileGroup.measurements, flamegraph.configSpace, flamegraphTheme, hasCPUChart]);
+
+  const CPUChart = useMemo(() => {
+    if (!hasCPUChart) {
+      return LOADING_OR_FALLBACK_CPU_CHART;
+    }
+
+    const measures: ProfileSeriesMeasurement[] = [];
+
+    for (const key in profileGroup.measurements) {
+      if (key.startsWith('cpu_usage')) {
+        const name =
+          key === 'cpu_usage'
+            ? 'Average CPU usage'
+            : `CPU Core ${key.replace('cpu_usage_', '')}`;
+        measures.push({...profileGroup.measurements[key]!, name});
+      }
+    }
+
+    return new FlamegraphChartModel(
+      Rect.From(flamegraph.configSpace),
+      measures.length > 0 ? measures : [],
+      flamegraphTheme.COLORS.CPU_CHART_COLORS
+    );
+  }, [profileGroup.measurements, flamegraph.configSpace, flamegraphTheme, hasCPUChart]);
+
+  const memoryChart = useMemo(() => {
+    if (!hasMemoryChart) {
+      return LOADING_OR_FALLBACK_MEMORY_CHART;
+    }
+
+    const measures: ProfileSeriesMeasurement[] = [];
+
+    const memory_footprint = profileGroup.measurements?.memory_footprint;
+    if (memory_footprint) {
+      measures.push({
+        ...memory_footprint!,
+        name: 'Heap Usage',
+      });
+    }
+
+    const native_memory_footprint = profileGroup.measurements?.memory_native_footprint;
+    if (native_memory_footprint) {
+      measures.push({
+        ...native_memory_footprint!,
+        name: 'Native Heap Usage',
+      });
+    }
+
+    return new FlamegraphChartModel(
+      Rect.From(flamegraph.configSpace),
+      measures.length > 0 ? measures : [],
+      flamegraphTheme.COLORS.MEMORY_CHART_COLORS,
+      {type: 'area'}
+    );
+  }, [
+    profileGroup.measurements,
+    flamegraph.configSpace,
+    flamegraphTheme,
+    hasMemoryChart,
+  ]);
+
+  const flamegraphCanvas = useMemo(() => {
+    if (!flamegraphCanvasRef) {
+      return null;
+    }
+    return new FlamegraphCanvas(
+      flamegraphCanvasRef,
+      vec2.fromValues(0, flamegraphTheme.SIZES.TIMELINE_HEIGHT * devicePixelRatio)
+    );
+  }, [devicePixelRatio, flamegraphCanvasRef, flamegraphTheme]);
+
+  const flamegraphMiniMapCanvas = useMemo(() => {
+    if (!flamegraphMiniMapCanvasRef) {
+      return null;
+    }
+    return new FlamegraphCanvas(flamegraphMiniMapCanvasRef, vec2.fromValues(0, 0));
+  }, [flamegraphMiniMapCanvasRef]);
+
+  const uiFramesCanvas = useMemo(() => {
+    if (!uiFramesCanvasRef) {
+      return null;
+    }
+    return new FlamegraphCanvas(uiFramesCanvasRef, vec2.fromValues(0, 0));
+  }, [uiFramesCanvasRef]);
+
+  const batteryChartCanvas = useMemo(() => {
+    if (!batteryChartCanvasRef) {
+      return null;
+    }
+    return new FlamegraphCanvas(batteryChartCanvasRef, vec2.fromValues(0, 0));
+  }, [batteryChartCanvasRef]);
+
+  const cpuChartCanvas = useMemo(() => {
+    if (!cpuChartCanvasRef) {
+      return null;
+    }
+    return new FlamegraphCanvas(cpuChartCanvasRef, vec2.fromValues(0, 0));
+  }, [cpuChartCanvasRef]);
+
+  const memoryChartCanvas = useMemo(() => {
+    if (!memoryChartCanvasRef) {
+      return null;
+    }
+    return new FlamegraphCanvas(memoryChartCanvasRef, vec2.fromValues(0, 0));
+  }, [memoryChartCanvasRef]);
+
+  const flamegraphView = useMemoWithPrevious<CanvasView<FlamegraphModel> | null>(
+    previousView => {
+      if (!flamegraphCanvas) {
+        return null;
+      }
+
+      const newView = new CanvasView({
+        canvas: flamegraphCanvas,
+        model: flamegraph,
+        options: {
+          inverted: flamegraph.inverted,
+          minWidth: flamegraph.profile.minFrameDuration,
+          barHeight: flamegraphTheme.SIZES.BAR_HEIGHT,
+          depthOffset: flamegraphTheme.SIZES.FLAMEGRAPH_DEPTH_OFFSET,
+          configSpaceTransform: new Rect(0, 0, 0, 0),
+        },
+      });
+
+      if (
+        // if the profile or the config space of the flamegraph has changed, we do not
+        // want to persist the config view. This is to avoid a case where the new config space
+        // is larger than the previous one, meaning the new view could now be zoomed in even
+        // though the user did not fire any zoom events.
+        previousView?.model.profile === newView.model.profile &&
+        previousView.configSpace.equals(newView.configSpace)
+      ) {
+        if (
+          // if we're still looking at the same profile but only a preference other than
+          // left heavy has changed, we do want to persist the config view
+          previousView.model.sort === 'left heavy' &&
+          newView.model.sort === 'left heavy'
+        ) {
+          newView.setConfigView(
+            previousView.configView.withHeight(newView.configView.height)
+          );
+        }
+      }
+
+      if (defined(highlightFrames)) {
+        let frames = flamegraph.findAllMatchingFrames(
+          highlightFrames.name,
+          highlightFrames.package
+        );
+
+        if (
+          !frames.length &&
+          !highlightFrames.package &&
+          highlightFrames.name &&
+          profileGroup.metadata.platform === 'node'
+        ) {
+          // there is a chance that the reason we did not find any frames is because
+          // for node, we try to infer some package from the frontend code.
+          // If that happens, we'll try and just do a search by name. This logic
+          // is duplicated in flamegraphZoomView.tsx and should be kept in sync
+          frames = flamegraph.findAllMatchingFramesBy(highlightFrames.name, ['name']);
+        }
+
+        if (frames.length > 0) {
+          const rectFrames = frames.map(
+            f => new Rect(f.start, f.depth, f.end - f.start, 1)
+          );
+          const newConfigView = computeMinZoomConfigViewForFrames(
+            newView.configView,
+            rectFrames
+          ).transformRect(newView.configSpaceTransform);
+          newView.setConfigView(newConfigView);
+          return newView;
+        }
+      }
+
+      // Because we render empty flamechart while we fetch the data, we need to make sure
+      // to have some heuristic when the data is fetched to determine if we should
+      // initialize the config view to the full view or a predefined value
+      else if (
+        !defined(highlightFrames) &&
+        position.view &&
+        !position.view.isEmpty() &&
+        previousView?.model === LOADING_OR_FALLBACK_FLAMEGRAPH
+      ) {
+        // We allow min width to be initialize to lower than view.minWidth because
+        // there is a chance that user zoomed into a span duration which may have been updated
+        // after the model was loaded (see L320)
+        newView.setConfigView(position.view, {width: {min: 0}});
+      }
+
+      return newView;
+    },
+
+    // We skip position.view dependency because it will go into an infinite loop
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    [flamegraph, flamegraphCanvas, flamegraphTheme]
+  );
+
+  const uiFramesView = useMemoWithPrevious<CanvasView<UIFrames> | null>(
+    _previousView => {
+      if (!flamegraphView || !flamegraphCanvas || !uiFrames) {
+        return null;
+      }
+
+      const newView = new CanvasView({
+        canvas: flamegraphCanvas,
+        model: uiFrames,
+        mode: 'stretchToFit',
+        options: {
+          inverted: flamegraph.inverted,
+          minWidth: uiFrames.minFrameDuration,
+          barHeight: 10,
+          depthOffset: 0,
+          configSpaceTransform: new Rect(0, 0, 0, 0),
+        },
+      });
+
+      // Initialize configView to whatever the flamegraph configView is
+      newView.setConfigView(
+        flamegraphView.configView.withHeight(newView.configView.height),
+        {width: {min: 0}}
+      );
+
+      return newView;
+    },
+    [flamegraphView, flamegraphCanvas, flamegraph, uiFrames]
+  );
+
+  const batteryChartView = useMemoWithPrevious<CanvasView<FlamegraphChartModel> | null>(
+    _previousView => {
+      if (!flamegraphView || !flamegraphCanvas || !batteryChart || !batteryChartCanvas) {
+        return null;
+      }
+
+      const newView = new CanvasView({
+        canvas: flamegraphCanvas,
+        model: batteryChart,
+        mode: 'anchorBottom',
+        options: {
+          // Invert chart so origin is at bottom left
+          // corner as opposed to top left
+          inverted: true,
+          minWidth: uiFrames.minFrameDuration,
+          barHeight: 0,
+          depthOffset: 0,
+          maxHeight: batteryChart.configSpace.height,
+          minHeight: batteryChart.configSpace.height,
+          configSpaceTransform: new Rect(0, 0, 0, 0),
+        },
+      });
+
+      // Compute the total size of the padding and stretch the view. This ensures that
+      // the total range is rendered and perfectly aligned from top to bottom.
+      newView.setConfigView(
+        flamegraphView.configView.withHeight(newView.configView.height),
+        {
+          width: {min: 1},
+        }
+      );
+
+      return newView;
+    },
+    [
+      flamegraphView,
+      flamegraphCanvas,
+      batteryChart,
+      uiFrames.minFrameDuration,
+      batteryChartCanvas,
+    ]
+  );
+
+  const cpuChartView = useMemoWithPrevious<CanvasView<FlamegraphChartModel> | null>(
+    _previousView => {
+      if (!flamegraphView || !flamegraphCanvas || !CPUChart || !cpuChartCanvas) {
+        return null;
+      }
+
+      const newView = new CanvasView({
+        canvas: flamegraphCanvas,
+        model: CPUChart,
+        mode: 'anchorBottom',
+        options: {
+          // Invert chart so origin is at bottom left
+          // corner as opposed to top left
+          inverted: true,
+          minWidth: uiFrames.minFrameDuration,
+          barHeight: 0,
+          depthOffset: 0,
+          maxHeight: CPUChart.configSpace.height,
+          minHeight: CPUChart.configSpace.height,
+          configSpaceTransform: new Rect(0, 0, 0, 0),
+        },
+      });
+
+      // Compute the total size of the padding and stretch the view. This ensures that
+      // the total range is rendered and perfectly aligned from top to bottom.
+      newView.setConfigView(
+        flamegraphView.configView.withHeight(newView.configView.height),
+        {
+          width: {min: 1},
+        }
+      );
+
+      return newView;
+    },
+    [
+      flamegraphView,
+      flamegraphCanvas,
+      CPUChart,
+      uiFrames.minFrameDuration,
+      cpuChartCanvas,
+    ]
+  );
+
+  const memoryChartView = useMemoWithPrevious<CanvasView<FlamegraphChartModel> | null>(
+    _previousView => {
+      if (!flamegraphView || !flamegraphCanvas || !memoryChart || !memoryChartCanvas) {
+        return null;
+      }
+
+      const newView = new CanvasView({
+        canvas: flamegraphCanvas,
+        model: memoryChart,
+        mode: 'anchorBottom',
+        options: {
+          // Invert chart so origin is at bottom left
+          // corner as opposed to top left
+          inverted: true,
+          minWidth: uiFrames.minFrameDuration,
+          barHeight: 0,
+          depthOffset: 0,
+          maxHeight: memoryChart.configSpace.height,
+          minHeight: memoryChart.configSpace.height,
+          configSpaceTransform: new Rect(0, 0, 0, 0),
+        },
+      });
+
+      // Compute the total size of the padding and stretch the view. This ensures that
+      // the total range is rendered and perfectly aligned from top to bottom.
+      newView.setConfigView(
+        flamegraphView.configView.withHeight(newView.configView.height),
+        {
+          width: {min: 1},
+        }
+      );
+
+      return newView;
+    },
+    [
+      flamegraphView,
+      flamegraphCanvas,
+      memoryChart,
+      uiFrames.minFrameDuration,
+      memoryChartCanvas,
+    ]
+  );
+
+  // We want to make sure that the views have the same min zoom levels so that
+  // if you wheel zoom on one, the other one will also zoom to the same level of detail.
+  // If we dont do this, then at some point during the zoom action the views will
+  // detach and only one will zoom while the other one will stay at the same zoom level.
+  useEffect(() => {
+    const minWidthBetweenViews = Math.min(
+      flamegraphView?.minWidth ?? Number.MAX_SAFE_INTEGER,
+      uiFramesView?.minWidth ?? Number.MAX_SAFE_INTEGER,
+      cpuChartView?.minWidth ?? Number.MAX_SAFE_INTEGER,
+      memoryChartView?.minWidth ?? Number.MAX_SAFE_INTEGER,
+      batteryChartView?.minWidth ?? Number.MAX_SAFE_INTEGER
+    );
+
+    flamegraphView?.setMinWidth?.(minWidthBetweenViews);
+    uiFramesView?.setMinWidth?.(minWidthBetweenViews);
+    cpuChartView?.setMinWidth?.(minWidthBetweenViews);
+    memoryChartView?.setMinWidth?.(minWidthBetweenViews);
+    batteryChartView?.setMinWidth?.(minWidthBetweenViews);
+  }, [flamegraphView, uiFramesView, cpuChartView, memoryChartView, batteryChartView]);
+
+  // Uses a useLayoutEffect to ensure that these top level/global listeners are added before
+  // any of the children components effects actually run. This way we do not lose events
+  // when we register/unregister these top level listeners.
+  useLayoutEffect(() => {
+    if (!flamegraphCanvas || !flamegraphView) {
+      return undefined;
+    }
+
+    // This code below manages the synchronization of the config views between spans and flamegraph
+    // We do so by listening to the config view change event and then updating the other views accordingly which
+    // allows us to keep the X axis in sync between the two views but keep the Y axis independent
+    const onConfigViewChange = (rect: Rect, sourceConfigViewChange: CanvasView<any>) => {
+      if (sourceConfigViewChange === flamegraphView) {
+        flamegraphView.setConfigView(rect.withHeight(flamegraphView.configView.height));
+
+        if (uiFramesView) {
+          uiFramesView.setConfigView(rect);
+        }
+        if (cpuChartView) {
+          cpuChartView.setConfigView(rect);
+        }
+        if (memoryChartView) {
+          memoryChartView.setConfigView(rect);
+        }
+        if (batteryChartView) {
+          batteryChartView.setConfigView(rect);
+        }
+      }
+
+      canvasPoolManager.draw();
+    };
+
+    const onTransformConfigView = (
+      mat: mat3,
+      sourceTransformConfigView: CanvasView<any>
+    ) => {
+      if (sourceTransformConfigView === flamegraphView) {
+        flamegraphView.transformConfigView(mat);
+        if (uiFramesView) {
+          uiFramesView.transformConfigView(mat);
+        }
+        if (batteryChartView) {
+          batteryChartView.transformConfigView(mat);
+        }
+        if (cpuChartView) {
+          cpuChartView.transformConfigView(mat);
+        }
+        if (memoryChartView) {
+          memoryChartView.transformConfigView(mat);
+        }
+      }
+
+      if (
+        sourceTransformConfigView === uiFramesView ||
+        sourceTransformConfigView === cpuChartView ||
+        sourceTransformConfigView === memoryChartView ||
+        sourceTransformConfigView === batteryChartView
+      ) {
+        if (flamegraphView) {
+          const beforeY = flamegraphView.configView.y;
+          flamegraphView.transformConfigView(mat);
+          flamegraphView.setConfigView(flamegraphView.configView.withY(beforeY));
+        }
+        if (uiFramesView) {
+          uiFramesView.transformConfigView(mat);
+        }
+        if (batteryChartView) {
+          batteryChartView.transformConfigView(mat);
+        }
+        if (cpuChartView) {
+          cpuChartView.transformConfigView(mat);
+        }
+        if (memoryChartView) {
+          memoryChartView.transformConfigView(mat);
+        }
+      }
+
+      canvasPoolManager.draw();
+    };
+
+    const onResetZoom = () => {
+      flamegraphView.resetConfigView(flamegraphCanvas);
+      if (uiFramesView && uiFramesCanvas) {
+        uiFramesView.resetConfigView(uiFramesCanvas);
+      }
+      if (batteryChartView && batteryChartCanvas) {
+        batteryChartView.resetConfigView(batteryChartCanvas);
+      }
+      if (cpuChartView && cpuChartCanvas) {
+        cpuChartView.resetConfigView(cpuChartCanvas);
+      }
+      if (memoryChartView && memoryChartCanvas) {
+        memoryChartView.resetConfigView(memoryChartCanvas);
+      }
+      canvasPoolManager.draw();
+    };
+
+    const onZoomIntoFrame = (frame: FlamegraphFrame, strategy: 'min' | 'exact') => {
+      const newConfigView = computeConfigViewWithStrategy(
+        strategy,
+        flamegraphView.configView,
+        new Rect(frame.start, frame.depth, frame.end - frame.start, 1)
+      ).transformRect(flamegraphView.configSpaceTransform);
+
+      flamegraphView.setConfigView(newConfigView);
+
+      if (uiFramesView) {
+        uiFramesView.setConfigView(
+          newConfigView.withHeight(uiFramesView.configView.height)
+        );
+      }
+      if (batteryChartView) {
+        batteryChartView.setConfigView(
+          newConfigView.withHeight(batteryChartView.configView.height)
+        );
+      }
+      if (cpuChartView) {
+        cpuChartView.setConfigView(
+          newConfigView.withHeight(cpuChartView.configView.height)
+        );
+      }
+      if (memoryChartView) {
+        memoryChartView.setConfigView(
+          newConfigView.withHeight(memoryChartView.configView.height)
+        );
+      }
+      canvasPoolManager.draw();
+    };
+
+    scheduler.on('set config view', onConfigViewChange);
+    scheduler.on('transform config view', onTransformConfigView);
+    scheduler.on('reset zoom', onResetZoom);
+    scheduler.on('zoom at frame', onZoomIntoFrame);
+
+    return () => {
+      scheduler.off('set config view', onConfigViewChange);
+      scheduler.off('transform config view', onTransformConfigView);
+      scheduler.off('reset zoom', onResetZoom);
+      scheduler.off('zoom at frame', onZoomIntoFrame);
+    };
+  }, [
+    canvasPoolManager,
+    flamegraphCanvas,
+    flamegraphView,
+    scheduler,
+    uiFramesCanvas,
+    uiFramesView,
+    cpuChartCanvas,
+    cpuChartView,
+    memoryChartCanvas,
+    memoryChartView,
+    batteryChartView,
+    batteryChartCanvas,
+  ]);
+
+  const minimapCanvases = useMemo(() => {
+    return [flamegraphMiniMapCanvasRef, flamegraphMiniMapOverlayCanvasRef];
+  }, [flamegraphMiniMapCanvasRef, flamegraphMiniMapOverlayCanvasRef]);
+
+  useResizeCanvasObserver(
+    minimapCanvases,
+    canvasPoolManager,
+    flamegraphMiniMapCanvas,
+    null
+  );
+
+  const uiFramesCanvases = useMemo(() => {
+    return [uiFramesCanvasRef];
+  }, [uiFramesCanvasRef]);
+
+  const uiFramesCanvasBounds = useResizeCanvasObserver(
+    uiFramesCanvases,
+    canvasPoolManager,
+    uiFramesCanvas,
+    uiFramesView
+  );
+
+  const batteryChartCanvases = useMemo(() => {
+    return [batteryChartCanvasRef];
+  }, [batteryChartCanvasRef]);
+
+  const batteryChartCanvasBounds = useResizeCanvasObserver(
+    batteryChartCanvases,
+    canvasPoolManager,
+    batteryChartCanvas,
+    batteryChartView
+  );
+
+  const cpuChartCanvases = useMemo(() => {
+    return [cpuChartCanvasRef];
+  }, [cpuChartCanvasRef]);
+
+  const cpuChartCanvasBounds = useResizeCanvasObserver(
+    cpuChartCanvases,
+    canvasPoolManager,
+    cpuChartCanvas,
+    cpuChartView
+  );
+
+  const memoryChartCanvases = useMemo(() => {
+    return [memoryChartCanvasRef];
+  }, [memoryChartCanvasRef]);
+  const memoryChartCanvasBounds = useResizeCanvasObserver(
+    memoryChartCanvases,
+    canvasPoolManager,
+    memoryChartCanvas,
+    memoryChartView
+  );
+
+  const flamegraphCanvases = useMemo(() => {
+    return [flamegraphCanvasRef, flamegraphOverlayCanvasRef];
+  }, [flamegraphCanvasRef, flamegraphOverlayCanvasRef]);
+
+  const flamegraphCanvasBounds = useResizeCanvasObserver(
+    flamegraphCanvases,
+    canvasPoolManager,
+    flamegraphCanvas,
+    flamegraphView
+  );
+
+  const flamegraphRenderer = useMemo(() => {
+    if (!flamegraphCanvasRef) {
+      return null;
+    }
+
+    const renderer = initializeFlamegraphRenderer(
+      [FlamegraphRendererWebGL, FlamegraphRenderer2D],
+      [
+        flamegraphCanvasRef,
+        flamegraph,
+        flamegraphTheme,
+        {
+          colorCoding,
+          draw_border: true,
+        },
+      ]
+    );
+
+    if (renderer === null) {
+      Sentry.captureException('Failed to initialize a flamegraph renderer');
+      addErrorMessage('Failed to initialize renderer');
+      return null;
+    }
+
+    return renderer;
+  }, [colorCoding, flamegraph, flamegraphCanvasRef, flamegraphTheme]);
+
+  const getFrameColor = useCallback(
+    (frame: FlamegraphFrame) => {
+      if (!flamegraphRenderer) {
+        return '';
+      }
+      return formatColorForFrame(frame, flamegraphRenderer);
+    },
+    [flamegraphRenderer]
+  );
+
+  const physicalToConfig =
+    flamegraphView && flamegraphCanvas
+      ? mat3.invert(
+          mat3.create(),
+          flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace)
+        )
+      : mat3.create();
+
+  const configSpacePixel = new Rect(0, 0, 1, 1).transformRect(physicalToConfig);
+
+  // Register keyboard navigation
+  useViewKeyboardNavigation(flamegraphView, canvasPoolManager, configSpacePixel.width);
+
+  // referenceNode is passed down to the flamegraphdrawer and is used to determine
+  // the weights of each frame. In other words, in case there is no user selected root, then all
+  // of the frame weights and timing are relative to the entire profile. If there is a user selected
+  // root however, all weights are relative to that sub tree.
+  const referenceNode = useMemo(
+    () =>
+      flamegraphProfiles.selectedRoot ? flamegraphProfiles.selectedRoot : flamegraph.root,
+    [flamegraphProfiles.selectedRoot, flamegraph.root]
+  );
+
+  // In case a user selected root is present, we will display that root + its entire sub tree.
+  // If no root is selected, we will display the entire sub tree down from the root. We start at
+  // root.children because flamegraph.root is a virtual node that we do not want to show in the table.
+  const rootNodes = useMemo(() => {
+    return flamegraphProfiles.selectedRoot
+      ? [flamegraphProfiles.selectedRoot]
+      : flamegraph.root.children;
+  }, [flamegraphProfiles.selectedRoot, flamegraph.root]);
+
+  const onSortingChange: FlamegraphViewSelectMenuProps['onSortingChange'] = useCallback(
+    newSorting => {
+      dispatch({type: 'set sorting', payload: newSorting});
+    },
+    [dispatch]
+  );
+
+  const onViewChange: FlamegraphViewSelectMenuProps['onViewChange'] = useCallback(
+    newView => {
+      dispatch({type: 'set view', payload: newView});
+    },
+    [dispatch]
+  );
+
+  const onThreadIdChange: FlamegraphThreadSelectorProps['onThreadIdChange'] = useCallback(
+    newThreadId => {
+      dispatch({type: 'set thread id', payload: newThreadId});
+    },
+    [dispatch]
+  );
+
+  useEffect(() => {
+    if (defined(flamegraphProfiles.threadId)) {
+      return;
+    }
+    const threadID =
+      typeof profileGroup.activeProfileIndex === 'number'
+        ? profileGroup.profiles[profileGroup.activeProfileIndex]?.threadId
+        : null;
+
+    // if the state has a highlight frame specified, then we want to jump to the
+    // thread containing it, highlight the frames on the thread, and change the
+    // view so it's obvious where it is
+    if (highlightFrames) {
+      const candidate = profileGroup.profiles.reduce<FlamegraphCandidate | null>(
+        (prevCandidate, currentProfile) => {
+          // if the previous candidate is the active thread, it always takes priority
+          if (prevCandidate?.isActiveThread) {
+            return prevCandidate;
+          }
+
+          const graph = new FlamegraphModel(currentProfile, {
+            inverted: false,
+            sort: sorting,
+            configSpace: undefined,
+          });
+
+          const frame = findLongestMatchingFrame(graph, highlightFrames);
+
+          if (!defined(frame)) {
+            return prevCandidate;
+          }
+
+          const newScore = frame.node.totalWeight || 0;
+          const oldScore = prevCandidate?.frame?.node?.totalWeight || 0;
+
+          // if we find the frame on the active thread, it always takes priority
+          if (newScore > 0 && currentProfile.threadId === threadID) {
+            return {
+              frame,
+              threadId: currentProfile.threadId,
+              isActiveThread: true,
+            };
+          }
+
+          return newScore <= oldScore
+            ? prevCandidate
+            : {
+                frame,
+                threadId: currentProfile.threadId,
+              };
+        },
+        null
+      );
+
+      if (defined(candidate)) {
+        dispatch({
+          type: 'set thread id',
+          payload: candidate.threadId,
+        });
+        return;
+      }
+    }
+
+    // fall back case, when we finally load the active profile index from the profile,
+    // make sure we update the thread id so that it is show first
+    if (defined(threadID)) {
+      dispatch({
+        type: 'set thread id',
+        payload: threadID,
+      });
+    }
+  }, [profileGroup, highlightFrames, flamegraphProfiles.threadId, dispatch, sorting]);
+
+  // A bit unfortunate for now, but the search component accepts a list
+  // of model to search through. This will become useful as we  build
+  // differential flamecharts or start comparing different profiles/charts
+  const flamegraphs = useMemo(() => [flamegraph], [flamegraph]);
+  // We currently dont support span search on continuos profiling data
+  const spans = useMemo(() => [], []);
+
+  return (
+    <Fragment>
+      <FlamegraphToolbar>
+        <FlamegraphThreadSelector
+          profileGroup={profileGroup}
+          threadId={flamegraphProfiles.threadId}
+          onThreadIdChange={onThreadIdChange}
+        />
+        <FlamegraphViewSelectMenu
+          view={view}
+          sorting={sorting}
+          onSortingChange={onSortingChange}
+          onViewChange={onViewChange}
+        />
+        <FlamegraphSearch
+          spans={spans}
+          flamegraphs={flamegraphs}
+          canvasPoolManager={canvasPoolManager}
+        />
+        <FlamegraphOptionsMenu canvasPoolManager={canvasPoolManager} />
+      </FlamegraphToolbar>
+
+      <FlamegraphLayout
+        uiFrames={
+          hasUIFrames ? (
+            <FlamegraphUIFrames
+              status={profiles.type}
+              canvasBounds={uiFramesCanvasBounds}
+              canvasPoolManager={canvasPoolManager}
+              setUIFramesCanvasRef={setUIFramesCanvasRef}
+              uiFramesCanvasRef={uiFramesCanvasRef}
+              uiFramesCanvas={uiFramesCanvas}
+              uiFramesView={uiFramesView}
+              uiFrames={uiFrames}
+            />
+          ) : null
+        }
+        batteryChart={
+          hasBatteryChart ? (
+            <FlamegraphChart
+              status={profiles.type}
+              chartCanvasRef={batteryChartCanvasRef}
+              chartCanvas={batteryChartCanvas}
+              setChartCanvasRef={setBatteryChartCanvasRef}
+              canvasBounds={batteryChartCanvasBounds}
+              chartView={batteryChartView}
+              canvasPoolManager={canvasPoolManager}
+              chart={batteryChart}
+              noMeasurementMessage={
+                profileGroup.metadata.platform === 'cocoa'
+                  ? t(
+                      'Upgrade to version 8.9.6 of sentry-cocoa SDK to enable battery usage collection'
+                    )
+                  : ''
+              }
+            />
+          ) : null
+        }
+        memoryChart={
+          hasMemoryChart ? (
+            <FlamegraphChart
+              status={profiles.type}
+              chartCanvasRef={memoryChartCanvasRef}
+              chartCanvas={memoryChartCanvas}
+              setChartCanvasRef={setMemoryChartCanvasRef}
+              canvasBounds={memoryChartCanvasBounds}
+              chartView={memoryChartView}
+              canvasPoolManager={canvasPoolManager}
+              chart={memoryChart}
+              noMeasurementMessage={
+                profileGroup.metadata.platform === 'cocoa'
+                  ? t(
+                      'Upgrade to version 8.9.6 of sentry-cocoa SDK to enable memory usage collection'
+                    )
+                  : profileGroup.metadata.platform === 'node'
+                    ? t(
+                        'Upgrade to version 1.2.0 of @sentry/profiling-node to enable memory usage collection'
+                      )
+                    : ''
+              }
+            />
+          ) : null
+        }
+        cpuChart={
+          hasCPUChart ? (
+            <FlamegraphChart
+              status={profiles.type}
+              chartCanvasRef={cpuChartCanvasRef}
+              chartCanvas={cpuChartCanvas}
+              setChartCanvasRef={setCpuChartCanvasRef}
+              canvasBounds={cpuChartCanvasBounds}
+              chartView={cpuChartView}
+              canvasPoolManager={canvasPoolManager}
+              chart={CPUChart}
+              noMeasurementMessage={
+                profileGroup.metadata.platform === 'cocoa'
+                  ? t(
+                      'Upgrade to version 8.9.6 of sentry-cocoa SDK to enable CPU usage collection'
+                    )
+                  : profileGroup.metadata.platform === 'node'
+                    ? t(
+                        'Upgrade to version 1.2.0 of @sentry/profiling-node to enable CPU usage collection'
+                      )
+                    : ''
+              }
+            />
+          ) : null
+        }
+        spansTreeDepth={null}
+        spans={null}
+        minimap={
+          <FlamegraphZoomViewMinimap
+            canvasPoolManager={canvasPoolManager}
+            flamegraph={flamegraph}
+            flamegraphMiniMapCanvas={flamegraphMiniMapCanvas}
+            flamegraphMiniMapCanvasRef={flamegraphMiniMapCanvasRef}
+            flamegraphMiniMapOverlayCanvasRef={flamegraphMiniMapOverlayCanvasRef}
+            flamegraphMiniMapView={flamegraphView}
+            setFlamegraphMiniMapCanvasRef={setFlamegraphMiniMapCanvasRef}
+            setFlamegraphMiniMapOverlayCanvasRef={setFlamegraphMiniMapOverlayCanvasRef}
+          />
+        }
+        flamegraph={
+          <Fragment>
+            <FlamegraphWarnings flamegraph={flamegraph} requestState={profiles} />
+            <FlamegraphZoomView
+              profileGroup={profileGroup}
+              canvasBounds={flamegraphCanvasBounds}
+              canvasPoolManager={canvasPoolManager}
+              flamegraph={flamegraph}
+              flamegraphRenderer={flamegraphRenderer}
+              flamegraphCanvas={flamegraphCanvas}
+              flamegraphCanvasRef={flamegraphCanvasRef}
+              flamegraphOverlayCanvasRef={flamegraphOverlayCanvasRef}
+              flamegraphView={flamegraphView}
+              setFlamegraphCanvasRef={setFlamegraphCanvasRef}
+              setFlamegraphOverlayCanvasRef={setFlamegraphOverlayCanvasRef}
+            />
+          </Fragment>
+        }
+        flamegraphDrawer={
+          <FlamegraphDrawer
+            profileTransaction={null}
+            profileGroup={profileGroup}
+            getFrameColor={getFrameColor}
+            referenceNode={referenceNode}
+            rootNodes={rootNodes}
+            flamegraph={flamegraph}
+            formatDuration={flamegraph ? flamegraph.formatter : noopFormatDuration}
+            canvasPoolManager={canvasPoolManager}
+            canvasScheduler={scheduler}
+          />
+        }
+      />
+    </Fragment>
+  );
+}

+ 3 - 2
static/app/components/profiling/flamegraph/flamegraph.tsx

@@ -1453,7 +1453,7 @@ function Flamegraph(): ReactElement {
             />
           ) : null
         }
-        spansTreeDepth={spanChart?.depth}
+        spansTreeDepth={spanChart?.depth ?? null}
         spans={
           spanChart ? (
             <FlamegraphSpans
@@ -1481,7 +1481,7 @@ function Flamegraph(): ReactElement {
         }
         flamegraph={
           <ProfileDragDropImport onImport={onImport}>
-            <FlamegraphWarnings flamegraph={flamegraph} />
+            <FlamegraphWarnings flamegraph={flamegraph} requestState={profiles} />
             <FlamegraphZoomView
               profileGroup={profileGroup}
               canvasBounds={flamegraphCanvasBounds}
@@ -1499,6 +1499,7 @@ function Flamegraph(): ReactElement {
         }
         flamegraphDrawer={
           <FlamegraphDrawer
+            profileTransaction={profiledTransaction}
             profileGroup={profileGroup}
             getFrameColor={getFrameColor}
             referenceNode={referenceNode}

+ 5 - 3
static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphDrawer.tsx

@@ -23,7 +23,7 @@ import {invertCallTree} from 'sentry/utils/profiling/profile/utils';
 import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
 import useOrganization from 'sentry/utils/useOrganization';
 import {useParams} from 'sentry/utils/useParams';
-import {useProfileTransaction} from 'sentry/views/profiling/profilesProvider';
+import type {useProfileTransaction} from 'sentry/views/profiling/profilesProvider';
 
 import {FlamegraphTreeTable} from './flamegraphTreeTable';
 import {ProfileDetails} from './profileDetails';
@@ -35,6 +35,7 @@ interface FlamegraphDrawerProps {
   formatDuration: Flamegraph['formatter'];
   getFrameColor: (frame: FlamegraphFrame) => string;
   profileGroup: ProfileGroup;
+  profileTransaction: ReturnType<typeof useProfileTransaction> | null;
   referenceNode: FlamegraphFrame;
   rootNodes: FlamegraphFrame[];
   onResize?: MouseEventHandler<HTMLElement>;
@@ -46,7 +47,6 @@ const FlamegraphDrawer = memo(function FlamegraphDrawer(props: FlamegraphDrawerP
   const orgSlug = useOrganization().slug;
   const flamegraphPreferences = useFlamegraphPreferences();
   const dispatch = useDispatchFlamegraphState();
-  const profileTransaction = useProfileTransaction();
 
   const [tab, setTab] = useLocalStorageState<'bottom up' | 'top down'>(
     'profiling-drawer-view',
@@ -257,7 +257,9 @@ const FlamegraphDrawer = memo(function FlamegraphDrawer(props: FlamegraphDrawerP
 
       <ProfileDetails
         transaction={
-          profileTransaction.type === 'resolved' ? profileTransaction.data : null
+          props.profileTransaction && props.profileTransaction.type === 'resolved'
+            ? props.profileTransaction.data
+            : null
         }
         projectId={params.projectId}
         profileGroup={props.profileGroup}

+ 1 - 1
static/app/components/profiling/flamegraph/flamegraphLayout.tsx

@@ -28,8 +28,8 @@ interface FlamegraphLayoutProps {
   memoryChart: React.ReactElement | null;
   minimap: React.ReactElement;
   spans: React.ReactElement | null;
+  spansTreeDepth: number | null;
   uiFrames: React.ReactElement | null;
-  spansTreeDepth?: number;
 }
 
 export function FlamegraphLayout(props: FlamegraphLayoutProps) {

+ 5 - 5
static/app/components/profiling/flamegraph/flamegraphOverlays/FlamegraphWarnings.tsx

@@ -2,28 +2,28 @@ import styled from '@emotion/styled';
 
 import {ExportProfileButton} from 'sentry/components/profiling/exportProfileButton';
 import {t} from 'sentry/locale';
+import type {RequestState} from 'sentry/types/core';
 import type {Flamegraph} from 'sentry/utils/profiling/flamegraph';
 import useOrganization from 'sentry/utils/useOrganization';
 import {useParams} from 'sentry/utils/useParams';
-import {useProfiles} from 'sentry/views/profiling/profilesProvider';
 
 interface FlamegraphWarningProps {
   flamegraph: Flamegraph;
+  requestState: RequestState<any>;
 }
 
 export function FlamegraphWarnings(props: FlamegraphWarningProps) {
   const orgSlug = useOrganization().slug;
   const params = useParams();
-  const profiles = useProfiles();
 
-  if (profiles.type === 'loading') {
+  if (props.requestState.type === 'loading') {
     return null;
   }
 
-  if (profiles.type === 'errored') {
+  if (props.requestState.type === 'errored') {
     return (
       <Overlay>
-        <p>{profiles.error || t('Failed to load profile')}</p>
+        <p>{props.requestState.error || t('Failed to load profile')}</p>
       </Overlay>
     );
   }

+ 2 - 2
static/app/utils/profiling/profile/continuousProfile.spec.tsx

@@ -99,10 +99,10 @@ describe('ContinuousProfile', () => {
     profile.forEach(open, close);
 
     expect(timings).toEqual([
-      ['foo', 'open'],
       ['bar', 'open'],
-      ['bar', 'close'],
+      ['foo', 'open'],
       ['foo', 'close'],
+      ['bar', 'close'],
     ]);
   });
 });

+ 5 - 7
static/app/utils/profiling/profile/continuousProfile.tsx

@@ -48,7 +48,7 @@ export class ContinuousProfile extends Profile {
       const stack = chunk.stacks[sample.stack_id];
       let size = 0;
 
-      for (let j = 0; j < stack.length; j++) {
+      for (let j = stack.length - 1; j >= 0; j--) {
         frame = resolveFrame(stack[j]);
         if (frame) resolvedStack[size++] = frame;
       }
@@ -164,11 +164,9 @@ function getThreadData(profile: Profiling.ContinuousProfile): {
   const {samples, thread_metadata = {}} = profile;
   const sample = samples[0];
   const threadId = parseInt(sample.thread_id, 10);
-  const threadName = thread_metadata?.[threadId]?.name;
 
-  if (threadName) {
-    return {threadId, threadName};
-  }
-
-  return {threadId, threadName: ''};
+  return {
+    threadId,
+    threadName: thread_metadata?.[threadId]?.name ?? `Thread ${threadId}`,
+  };
 }

+ 2 - 0
static/app/views/profiling/continuousProfileFlamegraph.tsx

@@ -3,6 +3,7 @@ import styled from '@emotion/styled';
 import * as qs from 'query-string';
 
 import LoadingIndicator from 'sentry/components/loadingIndicator';
+import {ContinuousFlamegraph} from 'sentry/components/profiling/flamegraph/continuousFlamegraph';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import {t} from 'sentry/locale';
 import type {DeepPartial} from 'sentry/types/utils';
@@ -99,6 +100,7 @@ function ContinuousProfileFlamegraph(): React.ReactElement {
                   <LoadingIndicator />
                 </LoadingIndicatorContainer>
               ) : null}
+              <ContinuousFlamegraph />
             </FlamegraphContainer>
           </FlamegraphThemeProvider>
         </ProfileGroupTypeProvider>