123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- import {memo, useCallback, useMemo, useState} from 'react';
- import styled from '@emotion/styled';
- import Button from 'sentry/components/button';
- 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 {useFlamegraphProfilesValue} from 'sentry/utils/profiling/flamegraph/useFlamegraphProfiles';
- import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
- import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
- import {useVerticallyResizableDrawer} from 'sentry/utils/profiling/hooks/useResizableDrawer';
- import {invertCallTree} from 'sentry/utils/profiling/profile/utils';
- import {FlamegraphRenderer} from 'sentry/utils/profiling/renderers/flamegraphRenderer';
- import {FrameStackTable} from './frameStackTable';
- interface FrameStackProps {
- canvasPoolManager: CanvasPoolManager;
- flamegraphRenderer: FlamegraphRenderer;
- }
- const FrameStack = memo(function FrameStack(props: FrameStackProps) {
- const theme = useFlamegraphTheme();
- const {selectedNode} = useFlamegraphProfilesValue();
- 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 roots: FlamegraphFrame[] | null = useMemo(() => {
- if (!selectedNode) {
- return null;
- }
- 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([selectedNode], skipFunction)
- : [selectedNode];
- if (tab === 'call order') {
- return maybeFilteredRoots;
- }
- return invertCallTree(maybeFilteredRoots);
- }, [selectedNode, tab, treeType]);
- 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 {height, onMouseDown} = useVerticallyResizableDrawer({
- initialHeight: (theme.SIZES.FLAMEGRAPH_DEPTH_OFFSET + 2) * theme.SIZES.BAR_HEIGHT,
- minHeight: 30,
- });
- return selectedNode ? (
- <FrameDrawer
- style={{
- height,
- }}
- >
- <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: 'ns-resize'}} onMouseDown={onMouseDown} />
- </FrameTabs>
- <FrameStackTable
- {...props}
- recursion={recursion}
- roots={roots ?? []}
- referenceNode={selectedNode}
- canvasPoolManager={props.canvasPoolManager}
- />
- </FrameDrawer>
- ) : null;
- });
- 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;
- }
- `;
- const FrameDrawer = styled('div')`
- display: flex;
- flex-shrink: 0;
- flex-direction: column;
- `;
- 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;
- > 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 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};
|