123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345 |
- import {useCallback, useMemo, useState} from 'react';
- import styled from '@emotion/styled';
- import {IconArrow} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import space from 'sentry/styles/space';
- import {CanvasPoolManager} from 'sentry/utils/profiling/canvasScheduler';
- import {Flamegraph} from 'sentry/utils/profiling/flamegraph';
- import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
- import {useContextMenu} from 'sentry/utils/profiling/hooks/useContextMenu';
- import {
- UseVirtualizedListProps,
- useVirtualizedTree,
- } from 'sentry/utils/profiling/hooks/useVirtualizedTree/useVirtualizedTree';
- import {VirtualizedTreeNode} from 'sentry/utils/profiling/hooks/useVirtualizedTree/VirtualizedTreeNode';
- import {FrameCallersTableCell} from './frameStack';
- import {FrameStackContextMenu} from './frameStackContextMenu';
- import {FrameStackTableRow} from './frameStackTableRow';
- 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);
- };
- }
- throw new Error(`Unknown sort property ${property}`);
- }
- function skipRecursiveNodes(n: VirtualizedTreeNode<FlamegraphFrame>): boolean {
- return n.node.node.isDirectRecursive();
- }
- interface FrameStackTableProps {
- canvasPoolManager: CanvasPoolManager;
- formatDuration: Flamegraph['formatter'];
- getFrameColor: (frame: FlamegraphFrame) => string;
- recursion: 'collapsed' | null;
- referenceNode: FlamegraphFrame;
- tree: FlamegraphFrame[];
- }
- export function FrameStackTable({
- tree,
- referenceNode,
- canvasPoolManager,
- getFrameColor,
- formatDuration,
- recursion,
- }: FrameStackTableProps) {
- const [scrollContainerRef, setScrollContainerRef] = 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);
- }, [sort, direction]);
- const [clickedContextMenuNode, setClickedContextMenuClose] =
- useState<VirtualizedTreeNode<FlamegraphFrame> | null>(null);
- const contextMenu = useContextMenu({container: scrollContainerRef});
- const handleZoomIntoFrameClick = useCallback(() => {
- if (!clickedContextMenuNode) {
- return;
- }
- canvasPoolManager.dispatch('zoom at frame', [clickedContextMenuNode.node, 'exact']);
- canvasPoolManager.dispatch('highlight frame', [
- clickedContextMenuNode.node,
- 'selected',
- ]);
- }, [canvasPoolManager, clickedContextMenuNode]);
- const renderRow: UseVirtualizedListProps<FlamegraphFrame>['renderRow'] = useCallback(
- (
- r,
- {
- handleRowClick,
- handleRowMouseEnter,
- handleExpandTreeNode,
- handleRowKeyDown,
- tabIndexKey,
- }
- ) => {
- return (
- <FrameStackTableRow
- ref={n => {
- r.ref = n;
- }}
- node={r.item}
- style={r.styles}
- referenceNode={referenceNode}
- frameColor={getFrameColor(r.item.node)}
- formatDuration={formatDuration}
- tabIndex={tabIndexKey === 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 {
- renderedItems,
- scrollContainerStyles,
- containerStyles,
- handleSortingChange,
- clickedGhostRowRef,
- hoveredGhostRowRef,
- } = useVirtualizedTree({
- skipFunction: recursion === 'collapsed' ? skipRecursiveNodes : undefined,
- sortFunction,
- renderRow,
- scrollContainer: scrollContainerRef,
- rowHeight: 24,
- tree,
- });
- const onSortChange = useCallback(
- (newSort: 'total weight' | 'self weight' | 'name') => {
- const newDirection =
- newSort === sort ? (direction === 'asc' ? 'desc' : 'asc') : 'desc';
- setDirection(newDirection);
- setSort(newSort);
- const sortFn = makeSortFunction(newSort, newDirection);
- handleSortingChange(sortFn);
- },
- [sort, direction, handleSortingChange]
- );
- return (
- <FrameBar>
- <FrameCallersTable>
- <FrameCallersTableHeader>
- <FrameWeightCell>
- <TableHeaderButton onClick={() => onSortChange('self weight')}>
- {t('Self Time ')}
- {sort === 'self weight' ? (
- <IconArrow direction={direction === 'desc' ? 'down' : 'up'} />
- ) : null}
- </TableHeaderButton>
- </FrameWeightCell>
- <FrameWeightCell>
- <TableHeaderButton onClick={() => onSortChange('total weight')}>
- {t('Total Time')}{' '}
- {sort === 'total weight' ? (
- <IconArrow direction={direction === 'desc' ? 'down' : 'up'} />
- ) : null}
- </TableHeaderButton>
- </FrameWeightCell>
- <FrameNameCell>
- <TableHeaderButton onClick={() => onSortChange('name')}>
- {t('Frame')}{' '}
- {sort === 'name' ? (
- <IconArrow direction={direction === 'desc' ? 'down' : 'up'} />
- ) : null}
- </TableHeaderButton>
- </FrameNameCell>
- </FrameCallersTableHeader>
- <FrameStackContextMenu
- onZoomIntoFrameClick={handleZoomIntoFrameClick}
- contextMenu={contextMenu}
- />
- <TableItemsContainer>
- {/*
- 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={ref => setScrollContainerRef(ref)}
- 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 />
- <FrameCallersTableCell />
- <FrameCallersTableCell style={{width: '100%'}} />
- </GhostRowContainer>
- </div>
- </div>
- </TableItemsContainer>
- </FrameCallersTable>
- </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.surface100};
- transition: background-color 100ms ease-in-out;
- line-height: 24px;
- &:hover {
- background-color: ${props => props.theme.surface400};
- }
- svg {
- width: 10px;
- height: 10px;
- }
- `;
- const FrameBar = styled('div')`
- overflow: auto;
- width: 100%;
- position: relative;
- background-color: ${p => p.theme.surface100};
- border-top: 1px solid ${p => p.theme.border};
- flex: 1 1 100%;
- 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};
- }
- }
- `;
|