profileFlamechart.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import {Fragment, useCallback, useEffect, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as qs from 'query-string';
  4. import Alert from 'sentry/components/alert';
  5. import LoadingIndicator from 'sentry/components/loadingIndicator';
  6. import {Flamegraph} from 'sentry/components/profiling/flamegraph/flamegraph';
  7. import {ProfileDragDropImportProps} from 'sentry/components/profiling/flamegraph/flamegraphOverlays/profileDragDropImport';
  8. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  9. import {t} from 'sentry/locale';
  10. import {EntryType} from 'sentry/types';
  11. import {EntrySpans, EventTransaction} from 'sentry/types/event';
  12. import {DeepPartial} from 'sentry/types/utils';
  13. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  14. import {
  15. DEFAULT_FLAMEGRAPH_STATE,
  16. FlamegraphState,
  17. } from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext';
  18. import {FlamegraphStateProvider} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContextProvider';
  19. import {
  20. decodeFlamegraphStateFromQueryParams,
  21. FLAMEGRAPH_LOCALSTORAGE_PREFERENCES_KEY,
  22. FlamegraphStateLocalStorageSync,
  23. FlamegraphStateQueryParamSync,
  24. } from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphQueryParamSync';
  25. import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider';
  26. import {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
  27. import {Profile} from 'sentry/utils/profiling/profile/profile';
  28. import {SpanTree} from 'sentry/utils/profiling/spanTree';
  29. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  30. import useOrganization from 'sentry/utils/useOrganization';
  31. import {
  32. useProfileGroup,
  33. useProfileTransaction,
  34. useSetProfileGroup,
  35. } from './profileGroupProvider';
  36. function collectAllSpanEntriesFromTransaction(
  37. transaction: EventTransaction
  38. ): EntrySpans['data'] {
  39. if (!transaction.entries.length) {
  40. return [];
  41. }
  42. const spans = transaction.entries.filter(
  43. (e): e is EntrySpans => e.type === EntryType.SPANS
  44. );
  45. let allSpans: EntrySpans['data'] = [];
  46. for (const span of spans) {
  47. allSpans = allSpans.concat(span.data);
  48. }
  49. return allSpans;
  50. }
  51. const LoadingGroup: ProfileGroup = {
  52. name: 'Loading',
  53. activeProfileIndex: 0,
  54. transactionID: null,
  55. metadata: {},
  56. traceID: '',
  57. profiles: [Profile.Empty],
  58. };
  59. const LoadingSpanTree = SpanTree.Empty;
  60. function ProfileFlamegraph(): React.ReactElement {
  61. const organization = useOrganization();
  62. const profileGroup = useProfileGroup();
  63. const setProfileGroup = useSetProfileGroup();
  64. const profiledTransaction = useProfileTransaction();
  65. const hasFlameChartSpans = useMemo(() => {
  66. return organization.features.includes('profiling-flamechart-spans');
  67. }, [organization.features]);
  68. const spanTree: SpanTree = useMemo(() => {
  69. if (profiledTransaction.type === 'resolved' && profiledTransaction.data) {
  70. return new SpanTree(
  71. profiledTransaction.data,
  72. collectAllSpanEntriesFromTransaction(profiledTransaction.data)
  73. );
  74. }
  75. return LoadingSpanTree;
  76. }, [profiledTransaction]);
  77. const [storedPreferences] = useLocalStorageState<DeepPartial<FlamegraphState>>(
  78. FLAMEGRAPH_LOCALSTORAGE_PREFERENCES_KEY,
  79. {
  80. preferences: {
  81. layout: DEFAULT_FLAMEGRAPH_STATE.preferences.layout,
  82. view: DEFAULT_FLAMEGRAPH_STATE.preferences.view,
  83. },
  84. }
  85. );
  86. useEffect(() => {
  87. trackAdvancedAnalyticsEvent('profiling_views.profile_flamegraph', {
  88. organization,
  89. });
  90. }, [organization]);
  91. const onImport: ProfileDragDropImportProps['onImport'] = useCallback(
  92. profiles => {
  93. setProfileGroup({type: 'resolved', data: profiles});
  94. },
  95. [setProfileGroup]
  96. );
  97. const initialFlamegraphPreferencesState = useMemo((): DeepPartial<FlamegraphState> => {
  98. const queryStringState = decodeFlamegraphStateFromQueryParams(
  99. qs.parse(window.location.search)
  100. );
  101. return {
  102. ...queryStringState,
  103. preferences: {
  104. ...storedPreferences.preferences,
  105. ...queryStringState.preferences,
  106. timelines: {
  107. ...DEFAULT_FLAMEGRAPH_STATE.preferences.timelines,
  108. ...(storedPreferences?.preferences?.timelines ?? {}),
  109. },
  110. layout:
  111. storedPreferences?.preferences?.layout ??
  112. queryStringState.preferences?.layout ??
  113. DEFAULT_FLAMEGRAPH_STATE.preferences.layout,
  114. },
  115. };
  116. // We only want to decode this when our component mounts
  117. // eslint-disable-next-line react-hooks/exhaustive-deps
  118. }, []);
  119. return (
  120. <SentryDocumentTitle
  121. title={t('Profiling \u2014 Flamechart')}
  122. orgSlug={organization.slug}
  123. >
  124. <FlamegraphStateProvider initialState={initialFlamegraphPreferencesState}>
  125. <FlamegraphThemeProvider>
  126. <FlamegraphStateQueryParamSync />
  127. <FlamegraphStateLocalStorageSync />
  128. <FlamegraphContainer>
  129. {profileGroup.type === 'errored' ? (
  130. <Alert type="error" showIcon>
  131. {profileGroup.error}
  132. </Alert>
  133. ) : profileGroup.type === 'loading' ? (
  134. <Fragment>
  135. <Flamegraph
  136. onImport={onImport}
  137. profiles={LoadingGroup}
  138. spanTree={hasFlameChartSpans ? LoadingSpanTree : null}
  139. />
  140. <LoadingIndicatorContainer>
  141. <LoadingIndicator />
  142. </LoadingIndicatorContainer>
  143. </Fragment>
  144. ) : profileGroup.type === 'resolved' ? (
  145. <Flamegraph
  146. onImport={onImport}
  147. profiles={profileGroup.data}
  148. spanTree={hasFlameChartSpans ? spanTree : null}
  149. />
  150. ) : null}
  151. </FlamegraphContainer>
  152. </FlamegraphThemeProvider>
  153. </FlamegraphStateProvider>
  154. </SentryDocumentTitle>
  155. );
  156. }
  157. const LoadingIndicatorContainer = styled('div')`
  158. position: absolute;
  159. display: flex;
  160. flex-direction: column;
  161. justify-content: center;
  162. width: 100%;
  163. height: 100%;
  164. `;
  165. const FlamegraphContainer = styled('div')`
  166. display: flex;
  167. flex-direction: column;
  168. flex: 1 1 100%;
  169. /*
  170. * The footer component is a sibling of this div.
  171. * Remove it so the flamegraph can take up the
  172. * entire screen.
  173. */
  174. ~ footer {
  175. display: none;
  176. }
  177. `;
  178. export default ProfileFlamegraph;