123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- import {memo, MouseEventHandler, useCallback, useMemo, useState} from 'react';
- import styled from '@emotion/styled';
- import Button from 'sentry/components/button';
- import {IconPanel} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import space from 'sentry/styles/space';
- import {CanvasPoolManager} from 'sentry/utils/profiling/canvasScheduler';
- import {filterFlamegraphTree} from 'sentry/utils/profiling/filterFlamegraphTree';
- import {Flamegraph} from 'sentry/utils/profiling/flamegraph';
- import {FlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphPreferences';
- import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/useFlamegraphPreferences';
- import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
- import {invertCallTree} from 'sentry/utils/profiling/profile/utils';
- import {FrameStackTable} from './frameStackTable';
- interface FrameStackProps {
- canvasPoolManager: CanvasPoolManager;
- formatDuration: Flamegraph['formatter'];
- getFrameColor: (frame: FlamegraphFrame) => string;
- referenceNode: FlamegraphFrame;
- rootNodes: FlamegraphFrame[];
- onResize?: MouseEventHandler<HTMLElement>;
- }
- const FrameStack = memo(function FrameStack(props: FrameStackProps) {
- const [flamegraphPreferences, dispatchFlamegraphPreferences] =
- useFlamegraphPreferences();
- const [tab, setTab] = useState<'bottom up' | 'call order'>('call order');
- const [treeType, setTreeType] = useState<'all' | 'application' | 'system'>('all');
- const [recursion, setRecursion] = useState<'collapsed' | null>(null);
- const maybeFilteredOrInvertedTree: FlamegraphFrame[] | null = useMemo(() => {
- const skipFunction: (f: FlamegraphFrame) => boolean =
- treeType === 'application'
- ? f => !f.frame.is_application
- : treeType === 'system'
- ? f => f.frame.is_application
- : () => false;
- const maybeFilteredRoots =
- treeType !== 'all'
- ? filterFlamegraphTree(props.rootNodes, skipFunction)
- : props.rootNodes;
- if (tab === 'call order') {
- return maybeFilteredRoots;
- }
- return invertCallTree(maybeFilteredRoots);
- }, [tab, treeType, props.rootNodes]);
- const handleRecursionChange = useCallback(
- (evt: React.ChangeEvent<HTMLInputElement>) => {
- setRecursion(evt.currentTarget.checked ? 'collapsed' : null);
- },
- []
- );
- const onBottomUpClick = useCallback(() => {
- setTab('bottom up');
- }, []);
- const onCallOrderClick = useCallback(() => {
- setTab('call order');
- }, []);
- const onAllApplicationsClick = useCallback(() => {
- setTreeType('all');
- }, []);
- const onApplicationsClick = useCallback(() => {
- setTreeType('application');
- }, []);
- const onSystemsClick = useCallback(() => {
- setTreeType('system');
- }, []);
- const onTableLeftClick = useCallback(() => {
- dispatchFlamegraphPreferences({type: 'set layout', payload: 'table_left'});
- }, [dispatchFlamegraphPreferences]);
- const onTableBottomClick = useCallback(() => {
- dispatchFlamegraphPreferences({type: 'set layout', payload: 'table_bottom'});
- }, [dispatchFlamegraphPreferences]);
- const onTableRightClick = useCallback(() => {
- dispatchFlamegraphPreferences({type: 'set layout', payload: 'table_right'});
- }, [dispatchFlamegraphPreferences]);
- return (
- <FrameDrawer layout={flamegraphPreferences.layout}>
- <FrameTabs>
- <li className={tab === 'bottom up' ? 'active' : undefined}>
- <Button
- data-title={t('Bottom Up')}
- priority="link"
- size="zero"
- onClick={onBottomUpClick}
- >
- {t('Bottom Up')}
- </Button>
- </li>
- <li className={tab === 'call order' ? 'active' : undefined}>
- <Button
- data-title={t('Call Order')}
- priority="link"
- size="zero"
- onClick={onCallOrderClick}
- >
- {t('Call Order')}
- </Button>
- </li>
- <Separator />
- <li className={treeType === 'all' ? 'active' : undefined}>
- <Button
- data-title={t('All Frames')}
- priority="link"
- size="zero"
- onClick={onAllApplicationsClick}
- >
- {t('All Frames')}
- </Button>
- </li>
- <li className={treeType === 'application' ? 'active' : undefined}>
- <Button
- data-title={t('Application Frames')}
- priority="link"
- size="zero"
- onClick={onApplicationsClick}
- >
- {t('Application Frames')}
- </Button>
- </li>
- <li className={treeType === 'system' ? 'active' : undefined}>
- <Button
- data-title={t('System Frames')}
- priority="link"
- size="zero"
- onClick={onSystemsClick}
- >
- {t('System Frames')}
- </Button>
- </li>
- <Separator />
- <li>
- <FrameDrawerLabel>
- <input
- type="checkbox"
- checked={recursion === 'collapsed'}
- onChange={handleRecursionChange}
- />
- {t('Collapse recursion')}
- </FrameDrawerLabel>
- </li>
- <li
- style={{
- flex: '1 1 100%',
- cursor:
- flamegraphPreferences.layout === 'table_bottom' ? 'ns-resize' : undefined,
- }}
- onMouseDown={
- flamegraphPreferences.layout === 'table_bottom' ? props.onResize : undefined
- }
- />
- <li>
- <LayoutSelectionContainer>
- <StyledButton
- active={flamegraphPreferences.layout === 'table_left'}
- onClick={onTableLeftClick}
- size="xs"
- title={t('Table left')}
- >
- <IconPanel size="xs" direction="right" />
- </StyledButton>
- <StyledButton
- active={flamegraphPreferences.layout === 'table_bottom'}
- onClick={onTableBottomClick}
- size="xs"
- title={t('Table bottom')}
- >
- <IconPanel size="xs" direction="down" />
- </StyledButton>
- <StyledButton
- active={flamegraphPreferences.layout === 'table_right'}
- onClick={onTableRightClick}
- size="xs"
- title={t('Table right')}
- >
- <IconPanel size="xs" direction="left" />
- </StyledButton>
- </LayoutSelectionContainer>
- </li>
- </FrameTabs>
- <FrameStackTable
- {...props}
- recursion={recursion}
- referenceNode={props.referenceNode}
- tree={maybeFilteredOrInvertedTree ?? []}
- canvasPoolManager={props.canvasPoolManager}
- />
- {flamegraphPreferences.layout === 'table_left' ||
- flamegraphPreferences.layout === 'table_right' ? (
- <ResizableVerticalDrawer>
- {/* The border should be 1px, but we want the actual handler to be wider
- to improve the user experience and not have users have to click on the exact pixel */}
- <InvisibleHandler onMouseDown={props.onResize} />
- </ResizableVerticalDrawer>
- ) : null}
- </FrameDrawer>
- );
- });
- const ResizableVerticalDrawer = styled('div')`
- width: 1px;
- grid-area: drawer;
- background-color: ${p => p.theme.border};
- position: relative;
- `;
- const InvisibleHandler = styled('div')`
- opacity: 0;
- width: ${space(1)};
- position: absolute;
- inset: 0;
- cursor: ew-resize;
- transform: translateX(-50%);
- background-color: transparent;
- `;
- const FrameDrawerLabel = styled('label')`
- display: flex;
- align-items: center;
- white-space: nowrap;
- margin-bottom: 0;
- height: 100%;
- font-weight: normal;
- > input {
- margin: 0 ${space(0.5)} 0 0;
- }
- `;
- // Linter produces a false positive for the grid layout. I did not manage to find out
- // how to "fix it" or why it is not working, I imagine it could be due to the ternary?
- const FrameDrawer = styled('div')<{layout: FlamegraphPreferences['layout']}>`
- display: grid;
- grid-template-rows: auto 1fr;
- grid-template-columns: ${({layout}) =>
- layout === 'table_left' ? '1fr auto' : layout === 'table_right' ? 'auto 1fr' : '1fr'};
- /* false positive for grid layout */
- /* stylelint-disable */
- grid-template-areas: ${({layout}) =>
- layout === 'table_bottom'
- ? `
- 'tabs'
- 'table'
- 'drawer'
- `
- : layout === 'table_left'
- ? `
- 'tabs drawer'
- 'table drawer'
- `
- : `
- 'drawer tabs'
- 'drawer table'
- `};
- `;
- const Separator = styled('li')`
- width: 1px;
- height: 66%;
- margin: 0 ${space(0.5)};
- background: ${p => p.theme.border};
- transform: translateY(29%);
- `;
- const FrameTabs = styled('ul')`
- display: flex;
- list-style-type: none;
- padding: 0 ${space(1)};
- margin: 0;
- border-top: 1px solid ${prop => prop.theme.border};
- background-color: ${props => props.theme.surface400};
- user-select: none;
- grid-area: tabs;
- > li {
- font-size: ${p => p.theme.fontSizeSmall};
- margin-right: ${space(1)};
- button {
- border: none;
- border-top: 2px solid transparent;
- border-bottom: 2px solid transparent;
- border-radius: 0;
- margin: 0;
- padding: ${space(0.5)} 0;
- color: ${p => p.theme.textColor};
- &::after {
- display: block;
- content: attr(data-title);
- font-weight: bold;
- height: 1px;
- color: transparent;
- overflow: hidden;
- visibility: hidden;
- white-space: nowrap;
- }
- &:hover {
- color: ${p => p.theme.textColor};
- }
- }
- &.active button {
- font-weight: bold;
- border-bottom: 2px solid ${prop => prop.theme.active};
- }
- }
- `;
- const StyledButton = styled(Button)<{active: boolean}>`
- border: none;
- background-color: transparent;
- box-shadow: none;
- transition: none !important;
- opacity: ${p => (p.active ? 0.7 : 0.5)};
- &:not(:last-child) {
- margin-right: ${space(1)};
- }
- &:hover {
- border: none;
- background-color: transparent;
- box-shadow: none;
- opacity: ${p => (p.active ? 0.6 : 0.5)};
- }
- `;
- const LayoutSelectionContainer = styled('div')`
- display: flex;
- align-items: center;
- `;
- const FRAME_WEIGHT_CELL_WIDTH_PX = 164;
- export const FrameCallersTableCell = styled('div')<{
- 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 {FrameStack};
|