index.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. import {useCallback, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Button} from 'sentry/components/button';
  4. import {CompactSelect} from 'sentry/components/compactSelect';
  5. import type {SelectOption} from 'sentry/components/compactSelect/types';
  6. import SearchBar from 'sentry/components/events/searchBar';
  7. import * as Layout from 'sentry/components/layouts/thirds';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  10. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  11. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  12. import Panel from 'sentry/components/panels/panel';
  13. import {AggregateFlamegraph} from 'sentry/components/profiling/flamegraph/aggregateFlamegraph';
  14. import {AggregateFlamegraphTreeTable} from 'sentry/components/profiling/flamegraph/aggregateFlamegraphTreeTable';
  15. import {FlamegraphSearch} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch';
  16. import {ProfileEventsTable} from 'sentry/components/profiling/profileEventsTable';
  17. import {SegmentedControl} from 'sentry/components/segmentedControl';
  18. import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
  19. import {MAX_QUERY_LENGTH} from 'sentry/constants';
  20. import {t} from 'sentry/locale';
  21. import {space} from 'sentry/styles/space';
  22. import type {Organization} from 'sentry/types/organization';
  23. import type {DeepPartial} from 'sentry/types/utils';
  24. import {defined} from 'sentry/utils';
  25. import {browserHistory} from 'sentry/utils/browserHistory';
  26. import EventView from 'sentry/utils/discover/eventView';
  27. import type {CanvasScheduler} from 'sentry/utils/profiling/canvasScheduler';
  28. import {
  29. CanvasPoolManager,
  30. useCanvasScheduler,
  31. } from 'sentry/utils/profiling/canvasScheduler';
  32. import type {FlamegraphState} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext';
  33. import {FlamegraphStateProvider} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContextProvider';
  34. import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider';
  35. import type {Frame} from 'sentry/utils/profiling/frame';
  36. import {useAggregateFlamegraphQuery} from 'sentry/utils/profiling/hooks/useAggregateFlamegraphQuery';
  37. import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents';
  38. import {formatSort} from 'sentry/utils/profiling/hooks/utils';
  39. import {decodeScalar} from 'sentry/utils/queryString';
  40. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  41. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  42. import {useLocation} from 'sentry/utils/useLocation';
  43. import useOrganization from 'sentry/utils/useOrganization';
  44. import usePageFilters from 'sentry/utils/usePageFilters';
  45. import useProjects from 'sentry/utils/useProjects';
  46. import Tab from 'sentry/views/performance/transactionSummary/tabs';
  47. import {
  48. FlamegraphProvider,
  49. useFlamegraph,
  50. } from 'sentry/views/profiling/flamegraphProvider';
  51. import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider';
  52. import type {ProfilingFieldType} from 'sentry/views/profiling/profileSummary/content';
  53. import {getProfilesTableFields} from 'sentry/views/profiling/profileSummary/content';
  54. import PageLayout, {redirectToPerformanceHomepage} from '../pageLayout';
  55. function ProfilesLegacy() {
  56. const location = useLocation();
  57. const organization = useOrganization();
  58. const projects = useProjects();
  59. const profilesCursor = useMemo(
  60. () => decodeScalar(location.query.cursor),
  61. [location.query.cursor]
  62. );
  63. const project = projects.projects.find(p => p.id === location.query.project);
  64. const fields = getProfilesTableFields(project?.platform);
  65. const sortableFields = useMemo(() => new Set(fields), [fields]);
  66. const sort = formatSort<ProfilingFieldType>(decodeScalar(location.query.sort), fields, {
  67. key: 'timestamp',
  68. order: 'desc',
  69. });
  70. const [query, setQuery] = useState(() => {
  71. // The search fields from the URL differ between profiling and
  72. // events dataset. For now, just drop everything except transaction
  73. const search = new MutableSearch('');
  74. const transaction = decodeScalar(location.query.transaction);
  75. if (defined(transaction)) {
  76. search.setFilterValues('transaction', [transaction]);
  77. }
  78. return search;
  79. });
  80. const profiles = useProfileEvents<ProfilingFieldType>({
  81. cursor: profilesCursor,
  82. fields,
  83. query: query.formatString(),
  84. sort,
  85. limit: 30,
  86. referrer: 'api.profiling.transactions-profiles-table',
  87. });
  88. const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
  89. (searchQuery: string) => {
  90. setQuery(new MutableSearch(searchQuery));
  91. browserHistory.push({
  92. ...location,
  93. query: {
  94. ...location.query,
  95. cursor: undefined,
  96. query: searchQuery || undefined,
  97. },
  98. });
  99. },
  100. [location]
  101. );
  102. const transaction = decodeScalar(location.query.transaction);
  103. return (
  104. <PageLayout
  105. location={location}
  106. organization={organization}
  107. projects={projects.projects}
  108. tab={Tab.PROFILING}
  109. generateEventView={() => EventView.fromLocation(location)}
  110. getDocumentTitle={() => t(`Profile: %s`, transaction)}
  111. childComponent={() => {
  112. return (
  113. <Layout.Main fullWidth>
  114. <FilterActions>
  115. <PageFilterBar condensed>
  116. <EnvironmentPageFilter />
  117. <DatePageFilter />
  118. </PageFilterBar>
  119. <SearchBar
  120. searchSource="transaction_profiles"
  121. organization={organization}
  122. projectIds={projects.projects.map(p => parseInt(p.id, 10))}
  123. query={query.formatString()}
  124. onSearch={handleSearch}
  125. maxQueryLength={MAX_QUERY_LENGTH}
  126. />
  127. </FilterActions>
  128. <ProfileEventsTable
  129. columns={fields}
  130. data={profiles.status === 'success' ? profiles.data : null}
  131. error={profiles.status === 'error' ? t('Unable to load profiles') : null}
  132. isLoading={profiles.status === 'loading'}
  133. sort={sort}
  134. sortableColumns={sortableFields}
  135. />
  136. </Layout.Main>
  137. );
  138. }}
  139. />
  140. );
  141. }
  142. function ProfilesWrapper() {
  143. const organization = useOrganization();
  144. const location = useLocation();
  145. const transaction = decodeScalar(location.query.transaction);
  146. if (!transaction) {
  147. redirectToPerformanceHomepage(organization, location);
  148. return null;
  149. }
  150. return <Profiles organization={organization} transaction={transaction} />;
  151. }
  152. const DEFAULT_FLAMEGRAPH_PREFERENCES: DeepPartial<FlamegraphState> = {
  153. preferences: {
  154. sorting: 'alphabetical' satisfies FlamegraphState['preferences']['sorting'],
  155. },
  156. };
  157. const noop = () => void 0;
  158. interface ProfilesProps {
  159. organization: Organization;
  160. transaction: string;
  161. }
  162. function Profiles({organization, transaction}: ProfilesProps) {
  163. const location = useLocation();
  164. const projects = useProjects();
  165. const {selection} = usePageFilters();
  166. // const query = decodeScalar(location.query.query, '');
  167. // const conditions = useMemo(() => {
  168. // const c = new MutableSearch(query);
  169. // c.setFilterValues('event.type', ['transaction']);
  170. // c.setFilterValues('transaction', [transaction]);
  171. // Object.keys(c.filters).forEach(field => {
  172. // if (isAggregateField(field)) {
  173. // c.removeFilter(field);
  174. // }
  175. // });
  176. // }, [transaction, query]);
  177. // const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
  178. // (searchQuery: string) => {
  179. // browserHistory.push({
  180. // ...location,
  181. // query: {
  182. // ...location.query,
  183. // cursor: undefined,
  184. // query: searchQuery || undefined,
  185. // },
  186. // });
  187. // },
  188. // [location]
  189. // );
  190. const [visualization, setVisualization] = useLocalStorageState<
  191. 'flamegraph' | 'call tree'
  192. >('flamegraph-visualization', 'flamegraph');
  193. const onVisualizationChange = useCallback(
  194. (value: 'flamegraph' | 'call tree') => {
  195. setVisualization(value);
  196. },
  197. [setVisualization]
  198. );
  199. const [frameFilter, setFrameFilter] = useLocalStorageState<
  200. 'system' | 'application' | 'all'
  201. >('flamegraph-frame-filter', 'application');
  202. const onFrameFilterChange = useCallback(
  203. (value: 'system' | 'application' | 'all') => {
  204. setFrameFilter(value);
  205. },
  206. [setFrameFilter]
  207. );
  208. const flamegraphFrameFilter: ((frame: Frame) => boolean) | undefined = useMemo(() => {
  209. if (frameFilter === 'all') {
  210. return () => true;
  211. }
  212. if (frameFilter === 'application') {
  213. return frame => frame.is_application;
  214. }
  215. return frame => !frame.is_application;
  216. }, [frameFilter]);
  217. const {data, isLoading, isError} = useAggregateFlamegraphQuery({
  218. transaction,
  219. environments: selection.environments,
  220. projects: selection.projects,
  221. datetime: selection.datetime,
  222. });
  223. const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
  224. const scheduler = useCanvasScheduler(canvasPoolManager);
  225. return (
  226. <PageLayout
  227. location={location}
  228. organization={organization}
  229. projects={projects.projects}
  230. tab={Tab.PROFILING}
  231. generateEventView={() => EventView.fromLocation(location)}
  232. getDocumentTitle={() => t(`Profile: %s`, transaction)}
  233. fillSpace
  234. childComponent={() => {
  235. return (
  236. <StyledMain fullWidth>
  237. <FilterActions>
  238. <PageFilterBar condensed>
  239. <EnvironmentPageFilter />
  240. <DatePageFilter />
  241. </PageFilterBar>
  242. {/* TODO: This doesn't actually search anything
  243. <StyledSearchBar
  244. searchSource="transaction_profiles"
  245. organization={organization}
  246. projectIds={projects.projects.map(p => parseInt(p.id, 10))}
  247. query={query}
  248. onSearch={handleSearch}
  249. maxQueryLength={MAX_QUERY_LENGTH}
  250. />
  251. */}
  252. </FilterActions>
  253. <ProfileVisualization>
  254. <ProfileGroupProvider
  255. traceID=""
  256. type="flamegraph"
  257. input={data ?? null}
  258. frameFilter={flamegraphFrameFilter}
  259. >
  260. <FlamegraphStateProvider initialState={DEFAULT_FLAMEGRAPH_PREFERENCES}>
  261. <FlamegraphThemeProvider>
  262. <FlamegraphProvider>
  263. <AggregateFlamegraphToolbar
  264. scheduler={scheduler}
  265. canvasPoolManager={canvasPoolManager}
  266. visualization={visualization}
  267. onVisualizationChange={onVisualizationChange}
  268. frameFilter={frameFilter}
  269. onFrameFilterChange={onFrameFilterChange}
  270. hideSystemFrames={false}
  271. setHideSystemFrames={noop}
  272. />
  273. <StyledPanel>
  274. {visualization === 'flamegraph' ? (
  275. <AggregateFlamegraph
  276. canvasPoolManager={canvasPoolManager}
  277. scheduler={scheduler}
  278. />
  279. ) : (
  280. <AggregateFlamegraphTreeTable
  281. recursion={null}
  282. expanded={false}
  283. frameFilter={frameFilter}
  284. canvasPoolManager={canvasPoolManager}
  285. withoutBorders
  286. />
  287. )}
  288. </StyledPanel>
  289. {isLoading ? (
  290. <RequestStateMessageContainer>
  291. <LoadingIndicator />
  292. </RequestStateMessageContainer>
  293. ) : isError ? (
  294. <RequestStateMessageContainer>
  295. {t('There was an error loading the flamegraph.')}
  296. </RequestStateMessageContainer>
  297. ) : null}
  298. </FlamegraphProvider>
  299. </FlamegraphThemeProvider>
  300. </FlamegraphStateProvider>
  301. </ProfileGroupProvider>
  302. </ProfileVisualization>
  303. </StyledMain>
  304. );
  305. }}
  306. />
  307. );
  308. }
  309. interface AggregateFlamegraphToolbarProps {
  310. canvasPoolManager: CanvasPoolManager;
  311. frameFilter: 'system' | 'application' | 'all';
  312. hideSystemFrames: boolean;
  313. onFrameFilterChange: (value: 'system' | 'application' | 'all') => void;
  314. onVisualizationChange: (value: 'flamegraph' | 'call tree') => void;
  315. scheduler: CanvasScheduler;
  316. setHideSystemFrames: (value: boolean) => void;
  317. visualization: 'flamegraph' | 'call tree';
  318. }
  319. function AggregateFlamegraphToolbar(props: AggregateFlamegraphToolbarProps) {
  320. const flamegraph = useFlamegraph();
  321. const flamegraphs = useMemo(() => [flamegraph], [flamegraph]);
  322. const spans = useMemo(() => [], []);
  323. const frameSelectOptions: SelectOption<'system' | 'application' | 'all'>[] =
  324. useMemo(() => {
  325. return [
  326. {value: 'system', label: t('System Frames')},
  327. {value: 'application', label: t('Application Frames')},
  328. {value: 'all', label: t('All Frames')},
  329. ];
  330. }, []);
  331. const onResetZoom = useCallback(() => {
  332. props.scheduler.dispatch('reset zoom');
  333. }, [props.scheduler]);
  334. const onFrameFilterChange = useCallback(
  335. (value: {value: 'application' | 'system' | 'all'}) => {
  336. props.onFrameFilterChange(value.value);
  337. },
  338. [props]
  339. );
  340. return (
  341. <AggregateFlamegraphToolbarContainer>
  342. <ViewSelectContainer>
  343. <SegmentedControl
  344. aria-label={t('View')}
  345. size="xs"
  346. value={props.visualization}
  347. onChange={props.onVisualizationChange}
  348. >
  349. <SegmentedControl.Item key="flamegraph">
  350. {t('Flamegraph')}
  351. </SegmentedControl.Item>
  352. <SegmentedControl.Item key="call tree">{t('Call Tree')}</SegmentedControl.Item>
  353. </SegmentedControl>
  354. </ViewSelectContainer>
  355. <AggregateFlamegraphSearch
  356. spans={spans}
  357. canvasPoolManager={props.canvasPoolManager}
  358. flamegraphs={flamegraphs}
  359. />
  360. <Button size="xs" onClick={onResetZoom}>
  361. {t('Reset Zoom')}
  362. </Button>
  363. <CompactSelect
  364. onChange={onFrameFilterChange}
  365. value={props.frameFilter}
  366. size="xs"
  367. options={frameSelectOptions}
  368. />
  369. </AggregateFlamegraphToolbarContainer>
  370. );
  371. }
  372. const FilterActions = styled('div')`
  373. margin-bottom: ${space(2)};
  374. gap: ${space(2)};
  375. display: grid;
  376. grid-template-columns: min-content 1fr;
  377. `;
  378. // const StyledSearchBar = styled(SearchBar)`
  379. // @media (min-width: ${p => p.theme.breakpoints.small}) {
  380. // order: 1;
  381. // grid-column: 1/4;
  382. // }
  383. //
  384. // @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  385. // order: initial;
  386. // grid-column: auto;
  387. // }
  388. // `;
  389. const StyledMain = styled(Layout.Main)`
  390. display: flex;
  391. flex-direction: column;
  392. flex: 1;
  393. `;
  394. const ProfileVisualization = styled('div')`
  395. display: grid;
  396. grid-template-rows: min-content 1fr;
  397. height: 100%;
  398. flex: 1;
  399. `;
  400. const RequestStateMessageContainer = styled('div')`
  401. position: absolute;
  402. left: 0;
  403. right: 0;
  404. top: 0;
  405. bottom: 0;
  406. display: flex;
  407. justify-content: center;
  408. align-items: center;
  409. color: ${p => p.theme.subText};
  410. `;
  411. const AggregateFlamegraphToolbarContainer = styled('div')`
  412. display: flex;
  413. justify-content: space-between;
  414. gap: ${space(1)};
  415. padding-bottom: ${space(1)};
  416. background-color: ${p => p.theme.background};
  417. /*
  418. force height to be the same as profile digest header,
  419. but subtract 1px for the border that doesnt exist on the header
  420. */
  421. height: 41px;
  422. `;
  423. const ViewSelectContainer = styled('div')`
  424. min-width: 160px;
  425. `;
  426. const AggregateFlamegraphSearch = styled(FlamegraphSearch)`
  427. max-width: 300px;
  428. `;
  429. const StyledPanel = styled(Panel)`
  430. overflow: hidden;
  431. display: flex;
  432. `;
  433. function ProfilesIndex() {
  434. const organization = useOrganization();
  435. if (organization.features.includes('continuous-profiling-compat')) {
  436. return <ProfilesWrapper />;
  437. }
  438. return <ProfilesLegacy />;
  439. }
  440. export default ProfilesIndex;