Browse Source

ref(profiling): remove old call tree table (#60052)

Drop the old virtualization for the new and more efficient one used on
the aggregate flamegraph tree table, this fixes some scroll issues as
well as fixes the horizontal scroll overflow bug which caused sticky
columns to disappear outside of the view.
Jonas 1 year ago
parent
commit
ea4a003e7c

+ 1 - 0
static/app/components/profiling/flamegraph/aggregateFlamegraphTreeContextMenu.tsx

@@ -23,6 +23,7 @@ export function AggregateFlamegraphTreeContextMenu(
     () => props.contextMenu.setOpen(false),
     [props.contextMenu]
   );
+
   return props.contextMenu.open ? (
     <Fragment>
       <ProfilingContextMenuLayer onClick={closeContextMenu} />

+ 72 - 468
static/app/components/profiling/flamegraph/aggregateFlamegraphTreeTable.tsx

@@ -1,14 +1,13 @@
-import {forwardRef, Fragment, useCallback, useEffect, useMemo, useState} from 'react';
+import {useCallback, useEffect, useMemo, useState} from 'react';
 import styled from '@emotion/styled';
 
 import InteractionStateLayer from 'sentry/components/interactionStateLayer';
 import PerformanceDuration from 'sentry/components/performanceDuration';
 import QuestionTooltip from 'sentry/components/questionTooltip';
-import {IconArrow, IconSettings, IconUser} from 'sentry/icons';
+import {IconArrow} from 'sentry/icons';
 import {t} from 'sentry/locale';
-import {space} from 'sentry/styles/space';
 import {defined} from 'sentry/utils';
-import {CanvasPoolManager, CanvasScheduler} from 'sentry/utils/profiling/canvasScheduler';
+import {CanvasPoolManager} from 'sentry/utils/profiling/canvasScheduler';
 import {filterFlamegraphTree} from 'sentry/utils/profiling/filterFlamegraphTree';
 import {useFlamegraphProfiles} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphProfiles';
 import {useDispatchFlamegraphState} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphState';
@@ -24,302 +23,26 @@ import {VirtualizedTree} from 'sentry/utils/profiling/hooks/useVirtualizedTree/V
 import {VirtualizedTreeNode} from 'sentry/utils/profiling/hooks/useVirtualizedTree/VirtualizedTreeNode';
 import {VirtualizedTreeRenderedRow} from 'sentry/utils/profiling/hooks/useVirtualizedTree/virtualizedTreeUtils';
 import {invertCallTree} from 'sentry/utils/profiling/profile/utils';
+import {relativeWeight} from 'sentry/utils/profiling/units/units';
 import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
 import {useFlamegraph} from 'sentry/views/profiling/flamegraphProvider';
 import {useProfileGroup} from 'sentry/views/profiling/profileGroupProvider';
 
 import {AggregateFlamegraphTreeContextMenu} from './aggregateFlamegraphTreeContextMenu';
-
-function computeRelativeWeight(base: number, value: number) {
-  // Make sure we dont divide by zero
-  if (!base || !value) {
-    return 0;
-  }
-  return (value / base) * 100;
-}
-
-const enum FastFrameCallersTableClassNames {
-  ROW = 'FrameCallersRow',
-  CELL = 'FrameCallersTableCell',
-  FRAME_CELL = 'FrameCallersTableCellFrame',
-  WEIGHT = 'FrameCallersTableCellWeight',
-  BACKGROUND_WEIGHT = 'FrameCallersTableCellWeightBar',
-  FRAME_TYPE = 'FrameCallersTableCellFrameType',
-  COLOR_INDICATOR = 'FrameCallersTableCellColorIndicator',
-  EXPAND_BUTTON = 'FrameCallersTableCellExpandButton',
-  GHOST_ROW_CELL = 'FrameCallersTableCellGhostRow',
-  GHOST_ROW_CONTAINER = 'FrameCallersTableCellGhostRowContainer',
-}
-
-interface FastFrameCallersRowsProps {
-  formatDuration: (value: number) => string;
-  frameColor: string;
-  node: VirtualizedTreeNode<FlamegraphFrame>;
-  onExpandClick: (
-    node: VirtualizedTreeNode<FlamegraphFrame>,
-    expand: boolean,
-    opts?: {expandChildren: boolean}
-  ) => void;
-  referenceNode: FlamegraphFrame;
-  tabIndex: number;
-}
-
-interface FastFrameCallerRowProps {
-  children: React.ReactNode;
-  onClick: () => void;
-  onContextMenu: (e: React.MouseEvent) => void;
-  onKeyDown: (event: React.KeyboardEvent) => void;
-  onMouseEnter: () => void;
-  tabIndex: number;
-  top: string;
-}
-const FastFrameCallersRow = forwardRef<HTMLDivElement, FastFrameCallerRowProps>(
-  (props, ref) => {
-    return (
-      <div
-        ref={ref}
-        className={FastFrameCallersTableClassNames.ROW}
-        style={{top: props.top}}
-        tabIndex={props.tabIndex}
-        onClick={props.onClick}
-        onKeyDown={props.onKeyDown}
-        onMouseEnter={props.onMouseEnter}
-        onContextMenu={props.onContextMenu}
-      >
-        {props.children}
-      </div>
-    );
-  }
-);
-
-const TEXT_ALIGN_RIGHT: React.CSSProperties = {textAlign: 'right'};
-function FastFrameCallersFixedRows(props: FastFrameCallersRowsProps) {
-  const totalWeight = computeRelativeWeight(
-    props.referenceNode.node.totalWeight,
-    props.node.node.node.totalWeight
-  );
-
-  const totalAggregateDuration = computeRelativeWeight(
-    props.referenceNode.node.aggregate_duration_ns,
-    props.node.node.node.aggregate_duration_ns
-  );
-
-  return (
-    <Fragment>
-      <div className={FastFrameCallersTableClassNames.CELL} style={TEXT_ALIGN_RIGHT}>
-        {props.node.node.node.totalWeight}
-        <div className={FastFrameCallersTableClassNames.WEIGHT}>
-          {totalWeight.toFixed(2)}%
-          <div
-            className={FastFrameCallersTableClassNames.BACKGROUND_WEIGHT}
-            style={{transform: `scaleX(${totalWeight / 100})`}}
-          />
-        </div>
-      </div>
-      <div className={FastFrameCallersTableClassNames.CELL} style={TEXT_ALIGN_RIGHT}>
-        <PerformanceDuration
-          nanoseconds={props.node.node.node.aggregate_duration_ns}
-          abbreviation
-        />
-        <div className={FastFrameCallersTableClassNames.WEIGHT}>
-          {totalAggregateDuration.toFixed(2)}%
-          <div
-            className={FastFrameCallersTableClassNames.BACKGROUND_WEIGHT}
-            style={{transform: `scaleX(${totalAggregateDuration / 100})`}}
-          />
-        </div>
-        <div className={FastFrameCallersTableClassNames.FRAME_TYPE}>
-          {props.node.node.node.frame.is_application ? (
-            <IconUser size="xs" />
-          ) : (
-            <IconSettings size="xs" />
-          )}
-        </div>
-      </div>
-    </Fragment>
-  );
-}
-
-function FastFrameCallersDynamicRows(props: FastFrameCallersRowsProps) {
-  const handleExpanding = (evt: React.MouseEvent) => {
-    evt.stopPropagation();
-    props.onExpandClick(props.node, !props.node.expanded, {
-      expandChildren: evt.metaKey,
-    });
-  };
-
-  return (
-    <div
-      className={FastFrameCallersTableClassNames.FRAME_CELL}
-      style={{paddingLeft: props.node.depth * 14 + 8, width: '100%'}}
-    >
-      <div
-        className={FastFrameCallersTableClassNames.COLOR_INDICATOR}
-        style={{backgroundColor: props.frameColor}}
-      />
-      <button
-        className={FastFrameCallersTableClassNames.EXPAND_BUTTON}
-        style={props.node.expanded ? {transform: 'rotate(90deg)'} : {}}
-        onClick={handleExpanding}
-      >
-        {props.node.node.children.length > 0 ? '\u203A' : null}
-      </button>
-      <div>
-        <div>{props.node.node.frame.name}</div>
-      </div>
-    </div>
-  );
-}
-
-const FrameCallersTable = styled('div')`
-  font-size: ${p => p.theme.fontSizeSmall};
-  margin: 0;
-  overflow: auto;
-  max-height: 100%;
-  height: 100%;
-  width: 100%;
-  display: flex;
-  flex-direction: column;
-  background-color: ${p => p.theme.background};
-
-  .${FastFrameCallersTableClassNames.ROW} {
-    display: flex;
-    line-height: 24px;
-    font-size: 12px;
-    position: absolute;
-    width: 100%;
-
-    &:focus {
-      outline: none;
-    }
-
-    &[tabindex='0'] {
-      background: ${p => p.theme.blue300};
-      color: #fff;
-
-      .${FastFrameCallersTableClassNames.WEIGHT} {
-        color: ${p => p.theme.white};
-        opacity: 0.7;
-      }
-
-      .${FastFrameCallersTableClassNames.BACKGROUND_WEIGHT} {
-        background-color: ${props => props.theme.yellow100};
-        border-bottom: 1px solid ${props => props.theme.yellow200};
-      }
-
-      .${FastFrameCallersTableClassNames.FRAME_TYPE} {
-        color: ${p => p.theme.white};
-        opacity: 0.7;
-      }
-    }
-
-    &[data-hovered='true']:not([tabindex='0']) {
-      background: ${p => p.theme.surface200};
-    }
-  }
-
-  .${FastFrameCallersTableClassNames.CELL} {
-    position: relative;
-    width: 164px;
-    border-right: 1px solid ${p => p.theme.border};
-    display: flex;
-    align-items: center;
-    padding-right: ${space(1)};
-    justify-content: flex-end;
-
-    &:nth-child(2) {
-      padding-right: 0;
-    }
-
-    &:focus {
-      outline: none;
-    }
-  }
-
-  .${FastFrameCallersTableClassNames.FRAME_CELL} {
-    display: flex;
-    align-items: center;
-    padding: 0 ${space(1)};
-
-    &:focus {
-      outline: none;
-    }
-  }
-  .${FastFrameCallersTableClassNames.WEIGHT} {
-    display: inline-block;
-    min-width: 7ch;
-    padding-right: 0px;
-    color: ${p => p.theme.subText};
-    opacity: 1;
-  }
-  .${FastFrameCallersTableClassNames.BACKGROUND_WEIGHT} {
-    pointer-events: none;
-    position: absolute;
-    right: 0;
-    top: 0;
-    background-color: ${props => props.theme.yellow100};
-    border-bottom: 1px solid ${props => props.theme.yellow200};
-    transform-origin: center right;
-    height: 100%;
-    width: 100%;
-  }
-
-  .${FastFrameCallersTableClassNames.FRAME_TYPE} {
-    flex-shrink: 0;
-    width: 26px;
-    height: 12px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    color: ${p => p.theme.subText};
-    opacity: ${_p => 1};
-  }
-
-  .${FastFrameCallersTableClassNames.COLOR_INDICATOR} {
-    width: 12px;
-    height: 12px;
-    border-radius: 2px;
-    display: inline-block;
-    flex-shrink: 0;
-    margin-right: ${space(0.5)};
-  }
-
-  .${FastFrameCallersTableClassNames.EXPAND_BUTTON} {
-    width: 10px;
-    height: 10px;
-    display: flex;
-    flex-shrink: 0;
-    padding: 0;
-    border: none;
-    background-color: transparent;
-    align-items: center;
-    justify-content: center;
-    user-select: none;
-    transform: rotate(0deg);
-    margin-right: ${space(0.25)};
-  }
-
-  .${FastFrameCallersTableClassNames.GHOST_ROW_CELL} {
-    width: 164px;
-    height: 100%;
-    border-right: 1px solid ${p => p.theme.border};
-    position: absolute;
-    left: 0;
-    top: 0;
-
-    &:nth-child(2) {
-      left: 164px;
-    }
-  }
-
-  .${FastFrameCallersTableClassNames.GHOST_ROW_CONTAINER} {
-    display: flex;
-    width: 100%;
-    pointer-events: none;
-    position: absolute;
-    height: 100%;
-  }
-`;
+import {
+  CALL_TREE_FRAME_WEIGHT_CELL_WIDTH_PX,
+  CallTreeDynamicColumnsContainer,
+  CallTreeFixedColumnsContainer,
+  CallTreeTable,
+  CallTreeTableContainer,
+  CallTreeTableDynamicColumns,
+  CallTreeTableFixedColumns,
+  CallTreeTableGhostRow,
+  CallTreeTableHeader,
+  CallTreeTableHeaderButton,
+  CallTreeTableRow,
+  syncCallTreeTableScroll,
+} from './callTreeTable';
 
 function makeSortFunction(
   property: 'sample count' | 'duration' | 'name',
@@ -382,7 +105,6 @@ function skipRecursiveNodes(n: VirtualizedTreeNode<FlamegraphFrame>): boolean {
 
 interface AggregateFlamegraphTreeTableProps {
   canvasPoolManager: CanvasPoolManager;
-  canvasScheduler: CanvasScheduler;
   frameFilter: 'system' | 'application' | 'all';
   recursion: 'collapsed' | null;
   expanded?: boolean;
@@ -390,7 +112,6 @@ interface AggregateFlamegraphTreeTableProps {
 
 export function AggregateFlamegraphTreeTable({
   expanded,
-  canvasScheduler,
   recursion,
   frameFilter,
 }: AggregateFlamegraphTreeTableProps) {
@@ -493,7 +214,7 @@ export function AggregateFlamegraphTreeTable({
         }
       ) => {
         return (
-          <FastFrameCallersRow
+          <CallTreeTableRow
             key={r.key}
             ref={n => {
               r.ref = n;
@@ -505,15 +226,31 @@ export function AggregateFlamegraphTreeTable({
             onMouseEnter={handleRowMouseEnter}
             onContextMenu={contextMenu.handleContextMenu}
           >
-            <FastFrameCallersFixedRows
+            <CallTreeTableFixedColumns
+              type="count"
               node={r.item}
               referenceNode={referenceNode}
               frameColor={getFrameColor(r.item.node)}
               formatDuration={flamegraph.formatter}
               tabIndex={selectedNodeIndex === r.key ? 0 : 1}
+              totalWeight={
+                <PerformanceDuration
+                  nanoseconds={r.item.node.node.aggregate_duration_ns}
+                  abbreviation
+                />
+              }
+              selfWeight={r.item.node.node.totalWeight.toFixed(0)}
+              relativeSelfWeight={relativeWeight(
+                referenceNode.node.totalWeight,
+                r.item.node.node.totalWeight
+              )}
+              relativeTotalWeight={relativeWeight(
+                referenceNode.node.aggregate_duration_ns,
+                r.item.node.node.aggregate_duration_ns
+              )}
               onExpandClick={handleExpandTreeNode}
             />
-          </FastFrameCallersRow>
+          </CallTreeTableRow>
         );
       },
       [referenceNode, flamegraph.formatter, getFrameColor, contextMenu]
@@ -532,7 +269,7 @@ export function AggregateFlamegraphTreeTable({
         }
       ) => {
         return (
-          <FastFrameCallersRow
+          <CallTreeTableRow
             key={r.key}
             ref={n => {
               r.ref = n;
@@ -544,7 +281,8 @@ export function AggregateFlamegraphTreeTable({
             onMouseEnter={handleRowMouseEnter}
             onContextMenu={contextMenu.handleContextMenu}
           >
-            <FastFrameCallersDynamicRows
+            <CallTreeTableDynamicColumns
+              type="count"
               node={r.item}
               referenceNode={referenceNode}
               frameColor={getFrameColor(r.item.node)}
@@ -552,16 +290,12 @@ export function AggregateFlamegraphTreeTable({
               tabIndex={selectedNodeIndex === r.key ? 0 : 1}
               onExpandClick={handleExpandTreeNode}
             />
-          </FastFrameCallersRow>
+          </CallTreeTableRow>
         );
       },
       [referenceNode, flamegraph.formatter, getFrameColor, contextMenu]
     );
 
-  // This is slighlty unfortunate and ugly, but because our two columns are sticky
-  // we need to scroll the container to the left when we scroll to a node. This
-  // should be resolved when we split the virtualization between containers and sync scroll,
-  // but is a larger undertaking and will take a bit longer
   const onScrollToNode: UseVirtualizedTreeProps<FlamegraphFrame>['onScrollToNode'] =
     useCallback(
       (
@@ -569,47 +303,7 @@ export function AggregateFlamegraphTreeTable({
         scrollContainer: HTMLElement | HTMLElement[] | null,
         coordinates?: {depth: number; top: number}
       ) => {
-        if (!scrollContainer) {
-          return;
-        }
-        if (node) {
-          const lastCell = node.ref?.lastChild?.firstChild as
-            | HTMLElement
-            | null
-            | undefined;
-          if (lastCell) {
-            lastCell.scrollIntoView({
-              block: 'nearest',
-            });
-
-            const left = -328 + (node.item.depth * 14 + 8);
-            if (Array.isArray(scrollContainer)) {
-              scrollContainer.forEach(c => {
-                c.scrollBy({
-                  left,
-                });
-              });
-            } else {
-              scrollContainer.scrollBy({
-                left,
-              });
-            }
-          }
-        } else if (coordinates && scrollContainer) {
-          const left = -328 + (coordinates.depth * 14 + 8);
-
-          if (Array.isArray(scrollContainer)) {
-            scrollContainer.forEach(c => {
-              c.scrollBy({
-                left,
-              });
-            });
-          } else {
-            scrollContainer.scrollBy({
-              left,
-            });
-          }
-        }
+        syncCallTreeTableScroll({node, scrollContainer, coordinates});
       },
       []
     );
@@ -629,7 +323,6 @@ export function AggregateFlamegraphTreeTable({
     scrollContainerStyles: scrollContainerStyles,
     containerStyles: fixedContainerStyles,
     handleSortingChange,
-    handleScrollTo,
     handleExpandTreeNode,
     handleRowClick,
     handleRowKeyDown,
@@ -662,19 +355,6 @@ export function AggregateFlamegraphTreeTable({
     [sort, direction, handleSortingChange]
   );
 
-  useEffect(() => {
-    function onShowInTableView(frame: FlamegraphFrame) {
-      handleScrollTo(el => el.node === frame.node);
-    }
-
-    canvasScheduler.on('zoom at frame', onShowInTableView);
-    canvasScheduler.on('show in table view', onShowInTableView);
-    return () => {
-      canvasScheduler.off('show in table view', onShowInTableView);
-      canvasScheduler.off('zoom at frame', onShowInTableView);
-    };
-  }, [canvasScheduler, handleScrollTo]);
-
   const onSortBySampleCount = useCallback(() => {
     onSortChange('sample count');
   }, [onSortChange]);
@@ -687,12 +367,20 @@ export function AggregateFlamegraphTreeTable({
     onSortChange('duration');
   }, [onSortChange]);
 
+  const onBottomUpClick = useCallback(() => {
+    setTreeView('bottom up');
+  }, [setTreeView]);
+
+  const onTopDownClick = useCallback(() => {
+    setTreeView('top down');
+  }, [setTreeView]);
+
   return (
     <FrameBar>
-      <FrameCallersTable>
-        <FrameCallersTableHeader>
+      <CallTreeTable>
+        <CallTreeTableHeader>
           <FrameWeightCell>
-            <TableHeaderButton onClick={onSortBySampleCount}>
+            <CallTreeTableHeaderButton onClick={onSortBySampleCount}>
               <InteractionStateLayer />
               <span>
                 {t('Samples')}{' '}
@@ -705,10 +393,10 @@ export function AggregateFlamegraphTreeTable({
               {sort === 'sample count' ? (
                 <IconArrow direction={direction === 'desc' ? 'down' : 'up'} />
               ) : null}
-            </TableHeaderButton>
+            </CallTreeTableHeaderButton>
           </FrameWeightCell>
           <FrameWeightCell>
-            <TableHeaderButton onClick={onSortByDuration}>
+            <CallTreeTableHeaderButton onClick={onSortByDuration}>
               <InteractionStateLayer />
               <span>
                 {t('Duration')}{' '}
@@ -721,25 +409,25 @@ export function AggregateFlamegraphTreeTable({
               {sort === 'duration' ? (
                 <IconArrow direction={direction === 'desc' ? 'down' : 'up'} />
               ) : null}
-            </TableHeaderButton>
+            </CallTreeTableHeaderButton>
           </FrameWeightCell>
-          <FrameNameCell>
-            <TableHeaderButton onClick={onSortByName}>
+          <div>
+            <CallTreeTableHeaderButton onClick={onSortByName}>
               <InteractionStateLayer />
               {t('Frame')}{' '}
               {sort === 'name' ? (
                 <IconArrow direction={direction === 'desc' ? 'down' : 'up'} />
               ) : null}
-            </TableHeaderButton>
-          </FrameNameCell>
-        </FrameCallersTableHeader>
-        <AggregateFlamegraphTableContainer ref={setTableParentContainer}>
+            </CallTreeTableHeaderButton>
+          </div>
+        </CallTreeTableHeader>
+        <CallTreeTableContainer ref={setTableParentContainer}>
           <AggregateFlamegraphTreeContextMenu
-            onBottomUpClick={() => setTreeView('bottom up')}
-            onTopDownClick={() => setTreeView('top down')}
+            onBottomUpClick={onBottomUpClick}
+            onTopDownClick={onTopDownClick}
             contextMenu={contextMenu}
           />
-          <FixedTableItemsContainer>
+          <CallTreeFixedColumnsContainer>
             {/*
           The order of these two matters because we want clicked state to
           be on top of hover in cases where user is hovering a clicked row.
@@ -755,14 +443,11 @@ export function AggregateFlamegraphTreeTable({
                     selectedNodeIndex,
                   });
                 })}
-                <div className={FastFrameCallersTableClassNames.GHOST_ROW_CONTAINER}>
-                  <div className={FastFrameCallersTableClassNames.GHOST_ROW_CELL} />
-                  <div className={FastFrameCallersTableClassNames.GHOST_ROW_CELL} />
-                </div>
+                <CallTreeTableGhostRow />
               </div>
             </div>
-          </FixedTableItemsContainer>
-          <DynamicTableItemsContainer>
+          </CallTreeFixedColumnsContainer>
+          <CallTreeDynamicColumnsContainer>
             {/*
           The order of these two matters because we want clicked state to
           be on top of hover in cases where user is hovering a clicked row.
@@ -780,70 +465,15 @@ export function AggregateFlamegraphTreeTable({
                 })}
               </div>
             </div>
-          </DynamicTableItemsContainer>
+          </CallTreeDynamicColumnsContainer>
           <div ref={hoveredGhostRowRef} style={{zIndex: 0}} />
           <div ref={clickedGhostRowRef} style={{zIndex: 0}} />
-        </AggregateFlamegraphTableContainer>
-      </FrameCallersTable>
+        </CallTreeTableContainer>
+      </CallTreeTable>
     </FrameBar>
   );
 }
 
-const AggregateFlamegraphTableContainer = styled('div')`
-  position: absolute;
-  left: 0;
-  top: 0;
-  bottom: 0;
-  right: 0;
-`;
-
-const FRAME_WEIGHT_CELL_WIDTH_PX = 164;
-const FixedTableItemsContainer = styled('div')`
-  position: absolute;
-  left: 0;
-  top: 0;
-  height: 100%;
-  width: ${2 * FRAME_WEIGHT_CELL_WIDTH_PX}px;
-  overflow: hidden;
-  z-index: 1;
-
-  /* Hide scrollbar so we dont end up with double scrollbars */
-  > div {
-    -ms-overflow-style: none; /* IE and Edge */
-    scrollbar-width: none; /* Firefox */
-    &::-webkit-scrollbar {
-      display: none;
-    }
-  }
-`;
-
-const DynamicTableItemsContainer = styled('div')`
-  position: absolute;
-  right: 0;
-  top: 0;
-  height: 100%;
-  width: calc(100% - ${2 * FRAME_WEIGHT_CELL_WIDTH_PX}px);
-  overflow: hidden;
-  z-index: 1;
-`;
-
-const TableHeaderButton = styled('button')`
-  display: flex;
-  width: 100%;
-  align-items: center;
-  justify-content: space-between;
-  padding: 0 ${space(1)};
-  border: none;
-  background-color: ${props => props.theme.surface200};
-  transition: background-color 100ms ease-in-out;
-  line-height: 24px;
-
-  svg {
-    width: 10px;
-    height: 10px;
-  }
-`;
-
 const FrameBar = styled('div')`
   overflow: auto;
   width: 100%;
@@ -855,31 +485,5 @@ const FrameBar = styled('div')`
 `;
 
 const FrameWeightCell = styled('div')`
-  width: ${FRAME_WEIGHT_CELL_WIDTH_PX}px;
-`;
-
-const FrameNameCell = styled('div')`
-  flex: 1 1 100%;
-`;
-
-const FrameCallersTableHeader = styled('div')`
-  top: 0;
-  position: sticky;
-  z-index: 2;
-  display: flex;
-
-  > div {
-    position: relative;
-    border-bottom: 1px solid ${p => p.theme.border};
-    background-color: ${p => p.theme.background};
-    white-space: nowrap;
-
-    &:last-child {
-      flex: 1;
-    }
-
-    &:not(:last-child) {
-      border-right: 1px solid ${p => p.theme.border};
-    }
-  }
+  width: ${CALL_TREE_FRAME_WEIGHT_CELL_WIDTH_PX}px;
 `;

+ 505 - 0
static/app/components/profiling/flamegraph/callTreeTable.tsx

@@ -0,0 +1,505 @@
+import {forwardRef, Fragment} from 'react';
+import styled from '@emotion/styled';
+
+import {IconSettings} from 'sentry/icons/iconSettings';
+import {IconUser} from 'sentry/icons/iconUser';
+import {space} from 'sentry/styles/space';
+import type {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
+import type {VirtualizedTreeNode} from 'sentry/utils/profiling/hooks/useVirtualizedTree/VirtualizedTreeNode';
+import {VirtualizedTreeRenderedRow} from 'sentry/utils/profiling/hooks/useVirtualizedTree/virtualizedTreeUtils';
+
+export const enum CallTreeTableClassNames {
+  ROW = 'CallTreeTableRow',
+  CELL = 'CallTreeTableTableCell',
+  FRAME_CELL = 'CallTreeTableTableCellFrame',
+  WEIGHT = 'CallTreeTableTableCellWeight',
+  BACKGROUND_WEIGHT = 'CallTreeTableTableCellWeightBar',
+  FRAME_TYPE = 'CallTreeTableTableCellFrameType',
+  COLOR_INDICATOR = 'CallTreeTableTableCellColorIndicator',
+  EXPAND_BUTTON = 'CallTreeTableTableCellExpandButton',
+  GHOST_ROW_CELL = 'CallTreeTableTableCellGhostRow',
+  GHOST_ROW_CONTAINER = 'CallTreeTableTableCellGhostRowContainer',
+}
+
+export const CallTreeTable = styled('div')`
+  font-size: ${p => p.theme.fontSizeSmall};
+  margin: 0;
+  overflow: auto;
+  max-height: 100%;
+  height: 100%;
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  background-color: ${p => p.theme.background};
+
+  .${CallTreeTableClassNames.ROW} {
+    display: flex;
+    line-height: 24px;
+    font-size: 12px;
+    position: absolute;
+    width: 100%;
+
+    &:focus {
+      outline: none;
+    }
+
+    &[tabindex='0'] {
+      background: ${p => p.theme.blue300};
+      color: #fff;
+
+      .${CallTreeTableClassNames.WEIGHT} {
+        color: ${p => p.theme.white};
+        opacity: 0.7;
+      }
+
+      .${CallTreeTableClassNames.BACKGROUND_WEIGHT} {
+        background-color: ${props => props.theme.yellow100};
+        border-bottom: 1px solid ${props => props.theme.yellow200};
+      }
+
+      .${CallTreeTableClassNames.FRAME_TYPE} {
+        color: ${p => p.theme.white};
+        opacity: 0.7;
+      }
+    }
+
+    &[data-hovered='true']:not([tabindex='0']) {
+      background: ${p => p.theme.surface200};
+    }
+  }
+
+  .${CallTreeTableClassNames.CELL} {
+    position: relative;
+    width: 164px;
+    border-right: 1px solid ${p => p.theme.border};
+    display: flex;
+    align-items: center;
+    padding-right: ${space(1)};
+    justify-content: flex-end;
+
+    &:nth-child(2) {
+      padding-right: 0;
+    }
+
+    &:focus {
+      outline: none;
+    }
+  }
+
+  .${CallTreeTableClassNames.FRAME_CELL} {
+    display: flex;
+    align-items: center;
+    padding: 0 ${space(1)};
+
+    &:focus {
+      outline: none;
+    }
+  }
+  .${CallTreeTableClassNames.WEIGHT} {
+    display: inline-block;
+    min-width: 7ch;
+    padding-right: 0px;
+    color: ${p => p.theme.subText};
+    opacity: 1;
+  }
+  .${CallTreeTableClassNames.BACKGROUND_WEIGHT} {
+    pointer-events: none;
+    position: absolute;
+    right: 0;
+    top: 0;
+    background-color: ${props => props.theme.yellow100};
+    border-bottom: 1px solid ${props => props.theme.yellow200};
+    transform-origin: center right;
+    height: 100%;
+    width: 100%;
+  }
+
+  .${CallTreeTableClassNames.FRAME_TYPE} {
+    flex-shrink: 0;
+    width: 26px;
+    height: 12px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: ${p => p.theme.subText};
+    opacity: ${_p => 1};
+  }
+
+  .${CallTreeTableClassNames.COLOR_INDICATOR} {
+    width: 12px;
+    height: 12px;
+    border-radius: 2px;
+    display: inline-block;
+    flex-shrink: 0;
+    margin-right: ${space(0.5)};
+  }
+
+  .${CallTreeTableClassNames.EXPAND_BUTTON} {
+    width: 10px;
+    height: 10px;
+    display: flex;
+    flex-shrink: 0;
+    padding: 0;
+    border: none;
+    background-color: transparent;
+    align-items: center;
+    justify-content: center;
+    user-select: none;
+    transform: rotate(0deg);
+    margin-right: ${space(0.25)};
+  }
+
+  .${CallTreeTableClassNames.GHOST_ROW_CELL} {
+    width: 164px;
+    height: 100%;
+    border-right: 1px solid ${p => p.theme.border};
+    position: absolute;
+    left: 0;
+    top: 0;
+
+    &:nth-child(2) {
+      left: 164px;
+    }
+  }
+
+  .${CallTreeTableClassNames.GHOST_ROW_CONTAINER} {
+    display: flex;
+    width: 100%;
+    pointer-events: none;
+    position: absolute;
+    height: 100%;
+  }
+`;
+
+export const CALL_TREE_FRAME_WEIGHT_CELL_WIDTH_PX = 164;
+
+export const CallTreeFixedColumnsContainer = styled('div')`
+  position: absolute;
+  left: 0;
+  top: 0;
+  height: 100%;
+  width: ${2 * CALL_TREE_FRAME_WEIGHT_CELL_WIDTH_PX}px;
+  overflow: hidden;
+  z-index: 1;
+
+  /* Hide scrollbar so we dont end up with double scrollbars */
+  > div {
+    -ms-overflow-style: none; /* IE and Edge */
+    scrollbar-width: none; /* Firefox */
+    &::-webkit-scrollbar {
+      display: none;
+    }
+  }
+`;
+
+export const CallTreeDynamicColumnsContainer = styled('div')`
+  position: absolute;
+  right: 0;
+  top: 0;
+  height: 100%;
+  width: calc(100% - ${2 * CALL_TREE_FRAME_WEIGHT_CELL_WIDTH_PX}px);
+  overflow: hidden;
+  z-index: 1;
+`;
+
+export const CallTreeTableHeader = styled('div')`
+  top: 0;
+  z-index: 2;
+  display: flex;
+  flex: 1;
+  flex-grow: 0;
+
+  > div {
+    position: relative;
+    border-bottom: 1px solid ${p => p.theme.border};
+    background-color: ${p => p.theme.background};
+    white-space: nowrap;
+
+    &:last-child {
+      flex: 1;
+    }
+
+    &:not(:last-child) {
+      border-right: 1px solid ${p => p.theme.border};
+    }
+  }
+`;
+
+export const CallTreeTableHeaderButton = styled('button')`
+  display: flex;
+  width: 100%;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 ${space(1)};
+  border: none;
+  background-color: ${props => props.theme.surface200};
+  transition: background-color 100ms ease-in-out;
+  line-height: 24px;
+
+  svg {
+    width: 10px;
+    height: 10px;
+  }
+`;
+
+export const CallTreeTableContainer = styled('div')`
+  position: relative;
+  height: 100%;
+`;
+
+type SyncCallTreeScrollParams = {
+  node: VirtualizedTreeRenderedRow<FlamegraphFrame> | undefined;
+  scrollContainer: HTMLElement | HTMLElement[] | null;
+  coordinates?: {depth: number; top: number};
+};
+
+// This is slighlty unfortunate and ugly, but because our two columns are sticky
+// we need to scroll the container to the left when we scroll to a node. This
+// should be resolved when we split the virtualization between containers and sync scroll,
+// but is a larger undertaking and will take a bit longer
+export function syncCallTreeTableScroll(args: SyncCallTreeScrollParams) {
+  if (!args.scrollContainer) {
+    return;
+  }
+  if (args.node) {
+    const lastCell = args.node.ref?.lastChild?.firstChild as
+      | HTMLElement
+      | null
+      | undefined;
+    if (lastCell) {
+      lastCell.scrollIntoView({
+        block: 'nearest',
+      });
+
+      const left = -328 + (args.node.item.depth * 14 + 8);
+      if (Array.isArray(args.scrollContainer)) {
+        args.scrollContainer.forEach(c => {
+          c.scrollBy({
+            left,
+          });
+        });
+      } else {
+        args.scrollContainer.scrollBy({
+          left,
+        });
+      }
+    }
+  } else if (args.coordinates && args.scrollContainer) {
+    const left = -328 + (args.coordinates.depth * 14 + 8);
+
+    if (Array.isArray(args.scrollContainer)) {
+      args.scrollContainer.forEach(c => {
+        c.scrollBy({
+          left,
+        });
+      });
+    } else {
+      args.scrollContainer.scrollBy({
+        left,
+      });
+    }
+  }
+}
+
+export function makeCallTreeTableSortFunction(
+  property: 'sample count' | 'duration' | 'total weight' | 'self weight' | 'name',
+  direction: 'asc' | 'desc'
+) {
+  if (property === 'duration') {
+    return direction === 'desc'
+      ? (
+          a: VirtualizedTreeNode<FlamegraphFrame>,
+          b: VirtualizedTreeNode<FlamegraphFrame>
+        ) => {
+          return b.node.node.aggregate_duration_ns - a.node.node.aggregate_duration_ns;
+        }
+      : (
+          a: VirtualizedTreeNode<FlamegraphFrame>,
+          b: VirtualizedTreeNode<FlamegraphFrame>
+        ) => {
+          return a.node.node.aggregate_duration_ns - b.node.node.aggregate_duration_ns;
+        };
+  }
+
+  // Sample counts are stored as weights
+  if (property === 'total weight' || property === 'sample count') {
+    return direction === 'desc'
+      ? (
+          a: VirtualizedTreeNode<FlamegraphFrame>,
+          b: VirtualizedTreeNode<FlamegraphFrame>
+        ) => {
+          return b.node.node.totalWeight - a.node.node.totalWeight;
+        }
+      : (
+          a: VirtualizedTreeNode<FlamegraphFrame>,
+          b: VirtualizedTreeNode<FlamegraphFrame>
+        ) => {
+          return a.node.node.totalWeight - b.node.node.totalWeight;
+        };
+  }
+
+  if (property === 'self weight') {
+    return direction === 'desc'
+      ? (
+          a: VirtualizedTreeNode<FlamegraphFrame>,
+          b: VirtualizedTreeNode<FlamegraphFrame>
+        ) => {
+          return b.node.node.selfWeight - a.node.node.selfWeight;
+        }
+      : (
+          a: VirtualizedTreeNode<FlamegraphFrame>,
+          b: VirtualizedTreeNode<FlamegraphFrame>
+        ) => {
+          return a.node.node.selfWeight - b.node.node.selfWeight;
+        };
+  }
+
+  if (property === 'name') {
+    return direction === 'desc'
+      ? (
+          a: VirtualizedTreeNode<FlamegraphFrame>,
+          b: VirtualizedTreeNode<FlamegraphFrame>
+        ) => {
+          return a.node.frame.name.localeCompare(b.node.frame.name);
+        }
+      : (
+          a: VirtualizedTreeNode<FlamegraphFrame>,
+          b: VirtualizedTreeNode<FlamegraphFrame>
+        ) => {
+          return b.node.frame.name.localeCompare(a.node.frame.name);
+        };
+  }
+
+  throw new Error(`Unknown sort property ${property}`);
+}
+
+const TEXT_ALIGN_RIGHT: React.CSSProperties = {textAlign: 'right'};
+
+interface CallTreeTableRowProps {
+  children: React.ReactNode;
+  onClick: () => void;
+  onContextMenu: (e: React.MouseEvent) => void;
+  onKeyDown: (event: React.KeyboardEvent) => void;
+  onMouseEnter: () => void;
+  tabIndex: number;
+  top: string;
+}
+export const CallTreeTableRow = forwardRef<HTMLDivElement, CallTreeTableRowProps>(
+  (props, ref) => {
+    return (
+      <div
+        ref={ref}
+        className={CallTreeTableClassNames.ROW}
+        style={{top: props.top}}
+        tabIndex={props.tabIndex}
+        onClick={props.onClick}
+        onKeyDown={props.onKeyDown}
+        onMouseEnter={props.onMouseEnter}
+        onContextMenu={props.onContextMenu}
+      >
+        {props.children}
+      </div>
+    );
+  }
+);
+
+interface CallTreeTableColumns {
+  formatDuration: (value: number) => string;
+  frameColor: string;
+  node: VirtualizedTreeNode<FlamegraphFrame>;
+  onExpandClick: (
+    node: VirtualizedTreeNode<FlamegraphFrame>,
+    expand: boolean,
+    opts?: {expandChildren: boolean}
+  ) => void;
+  referenceNode: FlamegraphFrame;
+  relativeSelfWeight: number;
+  relativeTotalWeight: number;
+  selfWeight: number | React.ReactNode;
+  tabIndex: number;
+  totalWeight: number | React.ReactNode;
+  type: 'count' | 'time';
+}
+
+export function CallTreeTableFixedColumns(props: CallTreeTableColumns) {
+  return (
+    <Fragment>
+      <div className={CallTreeTableClassNames.CELL} style={TEXT_ALIGN_RIGHT}>
+        {typeof props.selfWeight === 'number'
+          ? props.formatDuration(props.selfWeight)
+          : props.selfWeight}
+        <div className={CallTreeTableClassNames.WEIGHT}>
+          {props.relativeSelfWeight.toFixed(1)}%
+          <div
+            className={CallTreeTableClassNames.BACKGROUND_WEIGHT}
+            style={{transform: `scaleX(${props.relativeSelfWeight / 100})`}}
+          />
+        </div>
+      </div>
+      <div className={CallTreeTableClassNames.CELL} style={TEXT_ALIGN_RIGHT}>
+        {typeof props.totalWeight === 'number'
+          ? props.formatDuration(props.totalWeight)
+          : props.totalWeight}
+        <div className={CallTreeTableClassNames.WEIGHT}>
+          {props.relativeTotalWeight.toFixed(1)}%
+          <div
+            className={CallTreeTableClassNames.BACKGROUND_WEIGHT}
+            style={{transform: `scaleX(${props.relativeTotalWeight / 100})`}}
+          />
+        </div>
+        <div className={CallTreeTableClassNames.FRAME_TYPE}>
+          {props.node.node.node.frame.is_application ? (
+            <IconUser size="xs" />
+          ) : (
+            <IconSettings size="xs" />
+          )}
+        </div>
+      </div>
+    </Fragment>
+  );
+}
+
+export function CallTreeTableDynamicColumns(
+  props: Omit<
+    CallTreeTableColumns,
+    'relativeTotalWeight' | 'relativeSelfWeight' | 'selfWeight' | 'totalWeight'
+  >
+) {
+  const handleExpanding = (evt: React.MouseEvent) => {
+    evt.stopPropagation();
+    props.onExpandClick(props.node, !props.node.expanded, {
+      expandChildren: evt.metaKey,
+    });
+  };
+
+  return (
+    <div
+      className={CallTreeTableClassNames.FRAME_CELL}
+      style={{paddingLeft: props.node.depth * 14 + 8, width: '100%'}}
+    >
+      <div
+        className={CallTreeTableClassNames.COLOR_INDICATOR}
+        style={{backgroundColor: props.frameColor}}
+      />
+      <button
+        className={CallTreeTableClassNames.EXPAND_BUTTON}
+        style={props.node.expanded ? {transform: 'rotate(90deg)'} : {}}
+        onClick={handleExpanding}
+      >
+        {props.node.node.children.length > 0 ? '\u203A' : null}
+      </button>
+      <div>
+        <div>{props.node.node.frame.name}</div>
+      </div>
+    </div>
+  );
+}
+
+export function CallTreeTableGhostRow() {
+  return (
+    <div className={CallTreeTableClassNames.GHOST_ROW_CONTAINER}>
+      <div className={CallTreeTableClassNames.GHOST_ROW_CELL} />
+      <div className={CallTreeTableClassNames.GHOST_ROW_CELL} />
+    </div>
+  );
+}

+ 4 - 35
static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphDrawer.tsx

@@ -82,7 +82,7 @@ const FlamegraphDrawer = memo(function FlamegraphDrawer(props: FlamegraphDrawerP
     setTab('bottom up');
   }, [setTab]);
 
-  const onCallOrderClick = useCallback(() => {
+  const onTopDownClick = useCallback(() => {
     setTab('top down');
   }, [setTab]);
 
@@ -131,7 +131,7 @@ const FlamegraphDrawer = memo(function FlamegraphDrawer(props: FlamegraphDrawerP
             data-title={t('Top Down')}
             priority="link"
             size="zero"
-            onClick={onCallOrderClick}
+            onClick={onTopDownClick}
           >
             {t('Top Down')}
           </Button>
@@ -241,6 +241,8 @@ const FlamegraphDrawer = memo(function FlamegraphDrawer(props: FlamegraphDrawerP
       <FlamegraphTreeTable
         {...props}
         expanded={tab === 'top down'}
+        onTopDownClick={onTopDownClick}
+        onBottomUpClick={onBottomUpClick}
         recursion={recursion}
         flamegraph={props.flamegraph}
         referenceNode={props.referenceNode}
@@ -409,37 +411,4 @@ const LayoutSelectionContainer = styled('div')`
   align-items: center;
 `;
 
-const FRAME_WEIGHT_CELL_WIDTH_PX = 164;
-export const FrameCallersTableCell = styled('div')<{
-  bordered?: boolean;
-  isSelected?: boolean;
-  noPadding?: boolean;
-  textAlign?: React.CSSProperties['textAlign'];
-}>`
-  width: ${FRAME_WEIGHT_CELL_WIDTH_PX}px;
-  position: relative;
-  white-space: nowrap;
-  flex-shrink: 0;
-  padding: 0 ${p => (p.noPadding ? 0 : space(1))} 0 0;
-  text-align: ${p => p.textAlign ?? 'initial'};
-
-  &:first-child,
-  &:nth-child(2) {
-    position: sticky;
-    z-index: 1;
-    background-color: ${p => (p.isSelected ? p.theme.blue300 : p.theme.background)};
-  }
-
-  &:first-child {
-    left: 0;
-  }
-  &:nth-child(2) {
-    left: ${FRAME_WEIGHT_CELL_WIDTH_PX}px;
-  }
-
-  &:not(:last-child) {
-    border-right: 1px solid ${p => p.theme.border};
-  }
-`;
-
 export {FlamegraphDrawer};

+ 17 - 0
static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphTreeContextMenu.tsx

@@ -12,7 +12,9 @@ import {useContextMenu} from 'sentry/utils/profiling/hooks/useContextMenu';
 
 interface FlamegraphTreeContextMenuProps {
   contextMenu: ReturnType<typeof useContextMenu>;
+  onBottomUpClick: (evt: React.MouseEvent<HTMLDivElement>) => void;
   onHighlightAllFramesClick: (evt: React.MouseEvent<HTMLDivElement>) => void;
+  onTopDownClick: (evt: React.MouseEvent<HTMLDivElement>) => void;
   onZoomIntoFrameClick: (evt: React.MouseEvent<HTMLDivElement>) => void;
 }
 
@@ -44,6 +46,21 @@ export function FlamegraphTreeContextMenu(props: FlamegraphTreeContextMenuProps)
             {t('Highlight all occurrences')}
           </ProfilingContextMenuItem>
         </ProfilingContextMenuGroup>
+        <ProfilingContextMenuGroup>
+          <ProfilingContextMenuHeading>{t('View')}</ProfilingContextMenuHeading>
+          <ProfilingContextMenuItem
+            {...props.contextMenu.getMenuItemProps()}
+            onClick={props.onBottomUpClick}
+          >
+            {t('Bottom Up')}
+          </ProfilingContextMenuItem>
+          <ProfilingContextMenuItem
+            {...props.contextMenu.getMenuItemProps()}
+            onClick={props.onTopDownClick}
+          >
+            {t('Top Down')}
+          </ProfilingContextMenuItem>
+        </ProfilingContextMenuGroup>
       </ProfilingContextMenu>
     </Fragment>
   ) : null;

+ 215 - 267
static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphTreeTable.tsx

@@ -1,11 +1,10 @@
-import {useCallback, useEffect, useMemo, useState} from 'react';
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
 import styled from '@emotion/styled';
 
 import InteractionStateLayer from 'sentry/components/interactionStateLayer';
 import QuestionTooltip from 'sentry/components/questionTooltip';
 import {IconArrow} from 'sentry/icons';
 import {t} from 'sentry/locale';
-import {space} from 'sentry/styles/space';
 import {CanvasPoolManager, CanvasScheduler} from 'sentry/utils/profiling/canvasScheduler';
 import {Flamegraph} from 'sentry/utils/profiling/flamegraph';
 import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
@@ -16,65 +15,25 @@ import {
 } from 'sentry/utils/profiling/hooks/useVirtualizedTree/useVirtualizedTree';
 import {VirtualizedTreeNode} from 'sentry/utils/profiling/hooks/useVirtualizedTree/VirtualizedTreeNode';
 import {VirtualizedTreeRenderedRow} from 'sentry/utils/profiling/hooks/useVirtualizedTree/virtualizedTreeUtils';
+import {relativeWeight} from 'sentry/utils/profiling/units/units';
 
-import {FrameCallersTableCell} from './flamegraphDrawer';
-import {FlamegraphTreeContextMenu} from './flamegraphTreeContextMenu';
-import {FlamegraphTreeTableRow} from './flamegraphTreeTableRow';
-
-function makeSortFunction(
-  property: 'total weight' | 'self weight' | 'name',
-  direction: 'asc' | 'desc'
-) {
-  if (property === 'total weight') {
-    return direction === 'desc'
-      ? (
-          a: VirtualizedTreeNode<FlamegraphFrame>,
-          b: VirtualizedTreeNode<FlamegraphFrame>
-        ) => {
-          return b.node.node.totalWeight - a.node.node.totalWeight;
-        }
-      : (
-          a: VirtualizedTreeNode<FlamegraphFrame>,
-          b: VirtualizedTreeNode<FlamegraphFrame>
-        ) => {
-          return a.node.node.totalWeight - b.node.node.totalWeight;
-        };
-  }
-
-  if (property === 'self weight') {
-    return direction === 'desc'
-      ? (
-          a: VirtualizedTreeNode<FlamegraphFrame>,
-          b: VirtualizedTreeNode<FlamegraphFrame>
-        ) => {
-          return b.node.node.selfWeight - a.node.node.selfWeight;
-        }
-      : (
-          a: VirtualizedTreeNode<FlamegraphFrame>,
-          b: VirtualizedTreeNode<FlamegraphFrame>
-        ) => {
-          return a.node.node.selfWeight - b.node.node.selfWeight;
-        };
-  }
-
-  if (property === 'name') {
-    return direction === 'desc'
-      ? (
-          a: VirtualizedTreeNode<FlamegraphFrame>,
-          b: VirtualizedTreeNode<FlamegraphFrame>
-        ) => {
-          return a.node.frame.name.localeCompare(b.node.frame.name);
-        }
-      : (
-          a: VirtualizedTreeNode<FlamegraphFrame>,
-          b: VirtualizedTreeNode<FlamegraphFrame>
-        ) => {
-          return b.node.frame.name.localeCompare(a.node.frame.name);
-        };
-  }
+import {
+  CALL_TREE_FRAME_WEIGHT_CELL_WIDTH_PX,
+  CallTreeDynamicColumnsContainer,
+  CallTreeFixedColumnsContainer,
+  CallTreeTable,
+  CallTreeTableContainer,
+  CallTreeTableDynamicColumns,
+  CallTreeTableFixedColumns,
+  CallTreeTableGhostRow,
+  CallTreeTableHeader,
+  CallTreeTableHeaderButton,
+  CallTreeTableRow,
+  makeCallTreeTableSortFunction,
+  syncCallTreeTableScroll,
+} from '../callTreeTable';
 
-  throw new Error(`Unknown sort property ${property}`);
-}
+import {FlamegraphTreeContextMenu} from './flamegraphTreeContextMenu';
 
 function skipRecursiveNodes(n: VirtualizedTreeNode<FlamegraphFrame>): boolean {
   return n.node.node.isDirectRecursive();
@@ -86,6 +45,8 @@ interface FlamegraphTreeTableProps {
   flamegraph: Flamegraph;
   formatDuration: Flamegraph['formatter'];
   getFrameColor: (frame: FlamegraphFrame) => string;
+  onBottomUpClick: (evt: React.MouseEvent<HTMLDivElement>) => void;
+  onTopDownClick: (evt: React.MouseEvent<HTMLDivElement>) => void;
   recursion: 'collapsed' | null;
   referenceNode: FlamegraphFrame;
   tree: FlamegraphFrame[];
@@ -99,24 +60,41 @@ export function FlamegraphTreeTable({
   canvasPoolManager,
   canvasScheduler,
   getFrameColor,
-  formatDuration,
   recursion,
   flamegraph,
+  onBottomUpClick,
+  onTopDownClick,
 }: FlamegraphTreeTableProps) {
-  const [scrollContainerRef, setScrollContainerRef] = useState<HTMLDivElement | null>(
-    null
-  );
+  const [scrollContainerRef, setFixedScrollContainerRef] =
+    useState<HTMLDivElement | null>(null);
+  const [dynamicScrollContainerRef, setDynamicScrollContainerRef] =
+    useState<HTMLDivElement | null>(null);
+
   const [sort, setSort] = useState<'total weight' | 'self weight' | 'name'>(
     'total weight'
   );
   const [direction, setDirection] = useState<'asc' | 'desc'>('desc');
   const sortFunction = useMemo(() => {
-    return makeSortFunction(sort, direction);
+    return makeCallTreeTableSortFunction(sort, direction);
   }, [sort, direction]);
 
-  const [clickedContextMenuNode, setClickedContextMenuClose] =
+  const [clickedContextMenuNode, setClickedContextMenuNode] =
     useState<VirtualizedTreeNode<FlamegraphFrame> | null>(null);
-  const contextMenu = useContextMenu({container: scrollContainerRef});
+
+  const [tableParentContainer, setTableParentContainer] = useState<HTMLDivElement | null>(
+    null
+  );
+  const contextMenu = useContextMenu({container: tableParentContainer});
+
+  const onRowContextMenu = useCallback(
+    (item: VirtualizedTreeNode<FlamegraphFrame>) => {
+      return (e: React.MouseEvent<Element, MouseEvent>) => {
+        setClickedContextMenuNode(item);
+        contextMenu.handleContextMenu(e);
+      };
+    },
+    [contextMenu]
+  );
 
   const handleZoomIntoFrameClick = useCallback(() => {
     if (!clickedContextMenuNode) {
@@ -146,47 +124,95 @@ export function FlamegraphTreeTable({
     ]);
   }, [canvasPoolManager, clickedContextMenuNode, flamegraph]);
 
-  const renderRow: UseVirtualizedTreeProps<FlamegraphFrame>['renderRow'] = useCallback(
-    (
-      r,
-      {
-        handleRowClick,
-        handleRowMouseEnter,
-        handleExpandTreeNode,
-        handleRowKeyDown,
-        selectedNodeIndex,
-      }
-    ) => {
-      return (
-        <FlamegraphTreeTableRow
-          ref={n => {
-            r.ref = n;
-          }}
-          key={r.key}
-          node={r.item}
-          style={r.styles}
-          referenceNode={referenceNode}
-          frameColor={getFrameColor(r.item.node)}
-          formatDuration={formatDuration}
-          tabIndex={selectedNodeIndex === r.key ? 0 : 1}
-          onClick={handleRowClick}
-          onExpandClick={handleExpandTreeNode}
-          onKeyDown={handleRowKeyDown}
-          onMouseEnter={handleRowMouseEnter}
-          onContextMenu={evt => {
-            setClickedContextMenuClose(r.item);
-            contextMenu.handleContextMenu(evt);
-          }}
-        />
-      );
-    },
-    [contextMenu, formatDuration, referenceNode, getFrameColor]
-  );
+  const fixedRenderRow: UseVirtualizedTreeProps<FlamegraphFrame>['renderRow'] =
+    useCallback(
+      (
+        r,
+        {
+          handleRowClick,
+          handleRowMouseEnter,
+          handleExpandTreeNode,
+          handleRowKeyDown,
+          selectedNodeIndex,
+        }
+      ) => {
+        return (
+          <CallTreeTableRow
+            key={r.key}
+            ref={n => {
+              r.ref = n;
+            }}
+            top={r.styles.top}
+            tabIndex={selectedNodeIndex === r.key ? 0 : 1}
+            onKeyDown={handleRowKeyDown}
+            onClick={handleRowClick}
+            onMouseEnter={handleRowMouseEnter}
+            onContextMenu={onRowContextMenu(r.item)}
+          >
+            <CallTreeTableFixedColumns
+              node={r.item}
+              type="time"
+              referenceNode={referenceNode}
+              totalWeight={r.item.node.node.totalWeight}
+              selfWeight={r.item.node.node.selfWeight}
+              relativeSelfWeight={relativeWeight(
+                referenceNode.node.totalWeight,
+                r.item.node.node.selfWeight
+              )}
+              relativeTotalWeight={relativeWeight(
+                referenceNode.node.totalWeight,
+                r.item.node.node.totalWeight
+              )}
+              frameColor={getFrameColor(r.item.node)}
+              formatDuration={flamegraph.formatter}
+              tabIndex={selectedNodeIndex === r.key ? 0 : 1}
+              onExpandClick={handleExpandTreeNode}
+            />
+          </CallTreeTableRow>
+        );
+      },
+      [referenceNode, flamegraph.formatter, getFrameColor, onRowContextMenu]
+    );
 
-  // This is slighlty unfortunate and ugly, but because our two columns are sticky
-  // we need to scroll the container to the left when we scroll to a node. This
-  // should be resolved when we split the virtualization between containers and sync scroll,
-  // but is a larger undertaking and will take a bit longer
+  const dynamicRenderRow: UseVirtualizedTreeProps<FlamegraphFrame>['renderRow'] =
+    useCallback(
+      (
+        r,
+        {
+          handleRowClick,
+          handleRowMouseEnter,
+          handleExpandTreeNode,
+          handleRowKeyDown,
+          selectedNodeIndex,
+        }
+      ) => {
+        return (
+          <CallTreeTableRow
+            key={r.key}
+            ref={n => {
+              r.ref = n;
+            }}
+            top={r.styles.top}
+            tabIndex={selectedNodeIndex === r.key ? 0 : 1}
+            onKeyDown={handleRowKeyDown}
+            onClick={handleRowClick}
+            onMouseEnter={handleRowMouseEnter}
+            onContextMenu={onRowContextMenu(r.item)}
+          >
+            <CallTreeTableDynamicColumns
+              node={r.item}
+              type="time"
+              referenceNode={referenceNode}
+              frameColor={getFrameColor(r.item.node)}
+              formatDuration={flamegraph.formatter}
+              tabIndex={selectedNodeIndex === r.key ? 0 : 1}
+              onExpandClick={handleExpandTreeNode}
+            />
+          </CallTreeTableRow>
+        );
+      },
+      [referenceNode, flamegraph.formatter, getFrameColor, onRowContextMenu]
+    );
   const onScrollToNode: UseVirtualizedTreeProps<FlamegraphFrame>['onScrollToNode'] =
     useCallback(
       (
@@ -194,63 +220,36 @@ export function FlamegraphTreeTable({
         scrollContainer: HTMLElement | HTMLElement[] | null,
         coordinates?: {depth: number; top: number}
       ) => {
-        if (node) {
-          const lastCell = node.ref?.lastChild?.firstChild as
-            | HTMLElement
-            | null
-            | undefined;
-          if (lastCell) {
-            lastCell.scrollIntoView({
-              block: 'nearest',
-            });
-
-            const left = -328 + (node.item.depth * 14 + 8);
-            if (Array.isArray(scrollContainer)) {
-              scrollContainer.forEach(container => {
-                container.scrollBy({
-                  left,
-                });
-              });
-            } else {
-              scrollContainer?.scrollBy({
-                left,
-              });
-            }
-          }
-        } else if (coordinates) {
-          const left = -328 + (coordinates.depth * 14 + 8);
-
-          if (Array.isArray(scrollContainer)) {
-            scrollContainer.forEach(container => {
-              container.scrollBy({
-                left,
-              });
-            });
-          } else {
-            scrollContainer?.scrollBy({
-              left,
-            });
-          }
-        }
+        syncCallTreeTableScroll({node, scrollContainer, coordinates});
       },
       []
     );
 
+  const scrollContainers = useMemo(() => {
+    return [scrollContainerRef, dynamicScrollContainerRef].filter(
+      c => !!c
+    ) as HTMLElement[];
+  }, [dynamicScrollContainerRef, scrollContainerRef]);
+
   const {
-    renderedItems,
-    scrollContainerStyles,
-    containerStyles,
+    items: renderItems,
+    scrollContainerStyles: scrollContainerStyles,
+    containerStyles: fixedContainerStyles,
     handleSortingChange,
     handleScrollTo,
-    clickedGhostRowRef,
-    hoveredGhostRowRef,
+    handleExpandTreeNode,
+    handleRowClick,
+    handleRowKeyDown,
+    handleRowMouseEnter,
+    selectedNodeIndex,
+    clickedGhostRowRef: clickedGhostRowRef,
+    hoveredGhostRowRef: hoveredGhostRowRef,
   } = useVirtualizedTree({
     expanded,
     skipFunction: recursion === 'collapsed' ? skipRecursiveNodes : undefined,
     sortFunction,
     onScrollToNode,
-    renderRow,
-    scrollContainer: scrollContainerRef,
+    scrollContainer: scrollContainers,
     rowHeight: 24,
     tree,
   });
@@ -263,7 +262,7 @@ export function FlamegraphTreeTable({
       setDirection(newDirection);
       setSort(newSort);
 
-      const sortFn = makeSortFunction(newSort, newDirection);
+      const sortFn = makeCallTreeTableSortFunction(newSort, newDirection);
       handleSortingChange(sortFn);
     },
     [sort, direction, handleSortingChange]
@@ -274,16 +273,20 @@ export function FlamegraphTreeTable({
       handleScrollTo(el => el.node === frame.node);
     }
 
+    canvasScheduler.on('zoom at frame', onShowInTableView);
     canvasScheduler.on('show in table view', onShowInTableView);
-    return () => canvasScheduler.off('show in table view', onShowInTableView);
+    return () => {
+      canvasScheduler.off('show in table view', onShowInTableView);
+      canvasScheduler.off('zoom at frame', onShowInTableView);
+    };
   }, [canvasScheduler, handleScrollTo]);
 
   return (
     <FrameBar>
-      <FrameCallersTable>
-        <FrameCallersTableHeader>
+      <CallTreeTable>
+        <CallTreeTableHeader>
           <FrameWeightCell>
-            <TableHeaderButton onClick={() => onSortChange('self weight')}>
+            <CallTreeTableHeaderButton onClick={() => onSortChange('self weight')}>
               <InteractionStateLayer />
               <span>
                 {t('Self Time')}{' '}
@@ -298,10 +301,10 @@ export function FlamegraphTreeTable({
               {sort === 'self weight' ? (
                 <IconArrow direction={direction === 'desc' ? 'down' : 'up'} />
               ) : null}
-            </TableHeaderButton>
+            </CallTreeTableHeaderButton>
           </FrameWeightCell>
           <FrameWeightCell>
-            <TableHeaderButton onClick={() => onSortChange('total weight')}>
+            <CallTreeTableHeaderButton onClick={() => onSortChange('total weight')}>
               <InteractionStateLayer />
               <span>
                 {t('Total Time')}{' '}
@@ -316,88 +319,73 @@ export function FlamegraphTreeTable({
               {sort === 'total weight' ? (
                 <IconArrow direction={direction === 'desc' ? 'down' : 'up'} />
               ) : null}
-            </TableHeaderButton>
+            </CallTreeTableHeaderButton>
           </FrameWeightCell>
-          <FrameNameCell>
-            <TableHeaderButton onClick={() => onSortChange('name')}>
+          <div>
+            <CallTreeTableHeaderButton onClick={() => onSortChange('name')}>
               <InteractionStateLayer />
               {t('Frame')}{' '}
               {sort === 'name' ? (
                 <IconArrow direction={direction === 'desc' ? 'down' : 'up'} />
               ) : null}
-            </TableHeaderButton>
-          </FrameNameCell>
-        </FrameCallersTableHeader>
-        <FlamegraphTreeContextMenu
-          onZoomIntoFrameClick={handleZoomIntoFrameClick}
-          onHighlightAllFramesClick={onHighlightAllOccurrencesClick}
-          contextMenu={contextMenu}
-        />
-        <TableItemsContainer>
-          {/*
+            </CallTreeTableHeaderButton>
+          </div>
+        </CallTreeTableHeader>
+        <CallTreeTableContainer ref={setTableParentContainer}>
+          <FlamegraphTreeContextMenu
+            onZoomIntoFrameClick={handleZoomIntoFrameClick}
+            onHighlightAllFramesClick={onHighlightAllOccurrencesClick}
+            contextMenu={contextMenu}
+            onBottomUpClick={onBottomUpClick}
+            onTopDownClick={onTopDownClick}
+          />
+          <CallTreeFixedColumnsContainer>
+            {/*
           The order of these two matters because we want clicked state to
           be on top of hover in cases where user is hovering a clicked row.
            */}
-          <div ref={hoveredGhostRowRef} />
-          <div ref={clickedGhostRowRef} />
-          <div
-            ref={setScrollContainerRef}
-            style={scrollContainerStyles}
-            onContextMenu={contextMenu.handleContextMenu}
-          >
-            <div style={containerStyles}>
-              {renderedItems}
-              {/*
-              This is a ghost row, we stretch its width and height to fit the entire table
-              so that borders on columns are shown across the entire table and not just the rows.
-              This is useful when number of rows does not fill up the entire table height.
-             */}
-              <GhostRowContainer>
-                <FrameCallersTableCell bordered />
-                <FrameCallersTableCell bordered />
-                <FrameCallersTableCell style={{width: '100%'}} />
-              </GhostRowContainer>
+            <div ref={setFixedScrollContainerRef} style={scrollContainerStyles}>
+              <div style={fixedContainerStyles}>
+                {renderItems.map(r => {
+                  return fixedRenderRow(r, {
+                    handleRowClick: handleRowClick(r.key),
+                    handleRowMouseEnter: handleRowMouseEnter(r.key),
+                    handleExpandTreeNode,
+                    handleRowKeyDown,
+                    selectedNodeIndex,
+                  });
+                })}
+                <CallTreeTableGhostRow />
+              </div>
             </div>
-          </div>
-        </TableItemsContainer>
-      </FrameCallersTable>
+          </CallTreeFixedColumnsContainer>
+          <CallTreeDynamicColumnsContainer>
+            {/*
+          The order of these two matters because we want clicked state to
+          be on top of hover in cases where user is hovering a clicked row.
+           */}
+            <div ref={setDynamicScrollContainerRef} style={scrollContainerStyles}>
+              <div style={fixedContainerStyles}>
+                {renderItems.map(r => {
+                  return dynamicRenderRow(r, {
+                    handleRowClick: handleRowClick(r.key),
+                    handleRowMouseEnter: handleRowMouseEnter(r.key),
+                    handleExpandTreeNode,
+                    handleRowKeyDown,
+                    selectedNodeIndex,
+                  });
+                })}
+              </div>
+            </div>
+          </CallTreeDynamicColumnsContainer>
+          <div ref={hoveredGhostRowRef} style={{zIndex: 0}} />
+          <div ref={clickedGhostRowRef} style={{zIndex: 0}} />
+        </CallTreeTableContainer>
+      </CallTreeTable>
     </FrameBar>
   );
 }
 
-const TableItemsContainer = styled('div')`
-  position: relative;
-  height: 100%;
-  overflow: hidden;
-  background: ${p => p.theme.background};
-`;
-
-const GhostRowContainer = styled('div')`
-  display: flex;
-  width: 100%;
-  pointer-events: none;
-  position: absolute;
-  height: 100%;
-  z-index: -1;
-`;
-
-const TableHeaderButton = styled('button')`
-  display: flex;
-  width: 100%;
-  align-items: center;
-  justify-content: space-between;
-  padding: 0 ${space(1)};
-  border: none;
-  background-color: ${props => props.theme.surface200};
-  transition: background-color 100ms ease-in-out;
-  line-height: 24px;
-
-  svg {
-    width: 10px;
-    height: 10px;
-  }
-`;
-
 const FrameBar = styled('div')`
   overflow: auto;
   width: 100%;
@@ -408,46 +396,6 @@ const FrameBar = styled('div')`
   grid-area: table;
 `;
 
-const FrameCallersTable = styled('div')`
-  font-size: ${p => p.theme.fontSizeSmall};
-  margin: 0;
-  overflow: auto;
-  max-height: 100%;
-  height: 100%;
-  width: 100%;
-  display: flex;
-  flex-direction: column;
-`;
-
-const FRAME_WEIGHT_CELL_WIDTH_PX = 164;
-
 const FrameWeightCell = styled('div')`
-  width: ${FRAME_WEIGHT_CELL_WIDTH_PX}px;
-`;
-
-const FrameNameCell = styled('div')`
-  flex: 1 1 100%;
-`;
-
-const FrameCallersTableHeader = styled('div')`
-  top: 0;
-  position: sticky;
-  z-index: 1;
-  display: flex;
-  flex: 1;
-
-  > div {
-    position: relative;
-    border-bottom: 1px solid ${p => p.theme.border};
-    background-color: ${p => p.theme.background};
-    white-space: nowrap;
-
-    &:last-child {
-      flex: 1;
-    }
-
-    &:not(:last-child) {
-      border-right: 1px solid ${p => p.theme.border};
-    }
-  }
+  width: ${CALL_TREE_FRAME_WEIGHT_CELL_WIDTH_PX}px;
 `;

+ 0 - 245
static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphTreeTableRow.tsx

@@ -1,245 +0,0 @@
-import {forwardRef, Fragment, useCallback} from 'react';
-import styled from '@emotion/styled';
-
-import {IconSettings, IconUser} from 'sentry/icons';
-import {space} from 'sentry/styles/space';
-import {Flamegraph} from 'sentry/utils/profiling/flamegraph';
-import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
-import {VirtualizedTreeNode} from 'sentry/utils/profiling/hooks/useVirtualizedTree/VirtualizedTreeNode';
-
-import {FrameCallersTableCell} from './flamegraphDrawer';
-
-function computeRelativeWeight(base: number, value: number) {
-  // Make sure we dont divide by zero
-  if (!base || !value) {
-    return 0;
-  }
-  return (value / base) * 100;
-}
-
-interface FlamegraphTreeTableRowProps {
-  formatDuration: Flamegraph['formatter'];
-  frameColor: string;
-  node: VirtualizedTreeNode<FlamegraphFrame>;
-  onClick: React.MouseEventHandler<HTMLDivElement>;
-  onExpandClick: (
-    node: VirtualizedTreeNode<FlamegraphFrame>,
-    expand: boolean,
-    opts?: {expandChildren: boolean}
-  ) => void;
-  onKeyDown: React.KeyboardEventHandler<HTMLDivElement>;
-  onMouseEnter: React.MouseEventHandler<HTMLDivElement>;
-  referenceNode: FlamegraphFrame;
-  style: React.CSSProperties;
-  tabIndex: number;
-  onContextMenu?: React.MouseEventHandler<HTMLDivElement>;
-}
-
-export const FlamegraphTreeTableRow = forwardRef<
-  HTMLDivElement,
-  FlamegraphTreeTableRowProps
->((props, ref) => {
-  return (
-    <FrameCallersRow
-      ref={ref}
-      style={props.style}
-      onContextMenu={props.onContextMenu}
-      tabIndex={props.tabIndex}
-      isSelected={props.tabIndex === 0}
-      onKeyDown={props.onKeyDown}
-      onClick={props.onClick}
-      onMouseEnter={props.onMouseEnter}
-    >
-      <FrameCallersFixedRows {...props} />
-      <FrameCallersFunctionRow {...props} />
-    </FrameCallersRow>
-  );
-});
-
-export function FrameCallersFunctionRow(
-  props: Omit<FlamegraphTreeTableRowProps, 'style'>
-) {
-  const handleExpanding = useCallback(
-    (evt: React.MouseEvent) => {
-      evt.stopPropagation();
-      props.onExpandClick(props.node, !props.node.expanded, {
-        expandChildren: evt.metaKey,
-      });
-    },
-    [props]
-  );
-
-  return (
-    <FrameCallersTableCell
-      // We stretch this table to 100% width.
-      style={{paddingLeft: props.node.depth * 14 + 8, width: '100%'}}
-      className="FrameCallersTableCell"
-    >
-      <FrameNameContainer>
-        {/* @TODO FIX COLOR */}
-        <FrameColorIndicator style={{backgroundColor: props.frameColor}} />
-        <FrameChildrenIndicator
-          tabIndex={-1}
-          onClick={handleExpanding}
-          open={props.node.expanded}
-        >
-          {props.node.node.children.length > 0 ? '\u203A' : null}
-        </FrameChildrenIndicator>
-        <FrameName>{props.node.node.frame.name}</FrameName>
-      </FrameNameContainer>
-    </FrameCallersTableCell>
-  );
-}
-
-export function FrameCallersFixedRows(props: Omit<FlamegraphTreeTableRowProps, 'style'>) {
-  return (
-    <Fragment>
-      <FrameCallersTableCell
-        className="FrameCallersTableCell"
-        isSelected={props.tabIndex === 0}
-        textAlign="right"
-      >
-        {props.formatDuration(props.node.node.node.selfWeight)}
-        <Weight
-          isSelected={props.tabIndex === 0}
-          weight={computeRelativeWeight(
-            props.referenceNode.node.totalWeight,
-            props.node.node.node.selfWeight
-          )}
-        />
-      </FrameCallersTableCell>
-      <FrameCallersTableCell
-        isSelected={props.tabIndex === 0}
-        noPadding
-        textAlign="right"
-        className="FrameCallersTableCell"
-      >
-        <FrameWeightTypeContainer>
-          <FrameWeightContainer>
-            {props.formatDuration(props.node.node.node.totalWeight)}
-            <Weight
-              padded
-              isSelected={props.tabIndex === 0}
-              weight={computeRelativeWeight(
-                props.referenceNode.node.totalWeight,
-                props.node.node.node.totalWeight
-              )}
-            />
-          </FrameWeightContainer>
-          <FrameTypeIndicator isSelected={props.tabIndex === 0}>
-            {props.node.node.node.frame.is_application ? (
-              <IconUser size="xs" />
-            ) : (
-              <IconSettings size="xs" />
-            )}
-          </FrameTypeIndicator>
-        </FrameWeightTypeContainer>
-      </FrameCallersTableCell>
-    </Fragment>
-  );
-}
-const Weight = styled(
-  (props: {isSelected: boolean; weight: number; padded?: boolean}) => {
-    const {weight, padded: __, isSelected: _, ...rest} = props;
-    return (
-      <div {...rest}>
-        {weight.toFixed(1)}%
-        <BackgroundWeightBar style={{transform: `scaleX(${weight / 100})`}} />
-      </div>
-    );
-  }
-)`
-  display: inline-block;
-  min-width: 7ch;
-  padding-right: ${p => (p.padded ? space(0.5) : 0)};
-  color: ${p => (p.isSelected ? p.theme.white : p.theme.subText)};
-  opacity: ${p => (p.isSelected ? 0.8 : 1)};
-`;
-
-const FrameWeightTypeContainer = styled('div')`
-  display: flex;
-  align-items: center;
-  justify-content: flex-end;
-  position: relative;
-`;
-
-const FrameTypeIndicator = styled('div')<{isSelected: boolean}>`
-  flex-shrink: 0;
-  width: 26px;
-  height: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: ${p => (p.isSelected ? p.theme.white : p.theme.subText)};
-  opacity: ${p => (p.isSelected ? 0.8 : 1)};
-`;
-
-const FrameWeightContainer = styled('div')`
-  display: flex;
-  align-items: center;
-  position: relative;
-  justify-content: flex-end;
-  flex: 1 1 100%;
-`;
-
-const BackgroundWeightBar = styled('div')`
-  pointer-events: none;
-  position: absolute;
-  right: 0;
-  top: 0;
-  background-color: ${props => props.theme.yellow100};
-  border-bottom: 1px solid ${props => props.theme.yellow200};
-  transform-origin: center right;
-  height: 100%;
-  width: 100%;
-`;
-
-export const FrameCallersRow = styled('div')<{isSelected: boolean}>`
-  display: flex;
-  width: 100%;
-  color: ${p => (p.isSelected ? p.theme.white : 'inherit')};
-  font-size: ${p => p.theme.fontSizeSmall};
-  line-height: 24px;
-
-  &:focus {
-    outline: none;
-  }
-
-  &[data-hovered='true']:not([tabindex='0']) {
-    > div:first-child,
-    > div:nth-child(2) {
-      background-color: ${p => p.theme.surface200} !important;
-    }
-  }
-`;
-
-const FrameNameContainer = styled('div')`
-  display: flex;
-  align-items: center;
-`;
-
-const FrameChildrenIndicator = styled('button')<{open: boolean}>`
-  width: 10px;
-  height: 10px;
-  display: flex;
-  padding: 0;
-  border: none;
-  background-color: transparent;
-  align-items: center;
-  justify-content: center;
-  user-select: none;
-  transform: ${p => (p.open ? 'rotate(90deg)' : 'rotate(0deg)')};
-`;
-
-const FrameName = styled('span')`
-  margin-left: ${space(0.5)};
-`;
-
-const FrameColorIndicator = styled('div')`
-  width: 12px;
-  height: 12px;
-  border-radius: 2px;
-  display: inline-block;
-  flex-shrink: 0;
-  margin-right: ${space(0.5)};
-`;

+ 8 - 0
static/app/utils/profiling/units/units.ts

@@ -190,3 +190,11 @@ export function makeTimelineFormatter(from: ProfilingFormatterUnit | string) {
     return `${value < 0 ? '-' : ''}${pad(m, 2)}:${pad(s % 60, 2)}.${pad(ms % 1e3, 3)}`;
   };
 }
+
+export function relativeWeight(base: number, value: number) {
+  // Make sure we dont divide by zero
+  if (!base || !value) {
+    return 0;
+  }
+  return (value / base) * 100;
+}

+ 0 - 1
static/app/views/profiling/profileSummary/index.tsx

@@ -504,7 +504,6 @@ function ProfileSummaryPage(props: ProfileSummaryPageProps) {
                               recursion={null}
                               expanded={false}
                               frameFilter={frameFilter}
-                              canvasScheduler={scheduler}
                               canvasPoolManager={canvasPoolManager}
                             />
                           )}