profileFlamechart.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import {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 SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  8. import {t} from 'sentry/locale';
  9. import {DeepPartial} from 'sentry/types/utils';
  10. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  11. import {
  12. DEFAULT_FLAMEGRAPH_STATE,
  13. FlamegraphState,
  14. } from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext';
  15. import {FlamegraphStateProvider} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContextProvider';
  16. import {
  17. decodeFlamegraphStateFromQueryParams,
  18. FLAMEGRAPH_LOCALSTORAGE_PREFERENCES_KEY,
  19. FlamegraphStateLocalStorageSync,
  20. FlamegraphStateQueryParamSync,
  21. } from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphQueryParamSync';
  22. import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider';
  23. import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphPreferences';
  24. import {useCurrentProjectFromRouteParam} from 'sentry/utils/profiling/hooks/useCurrentProjectFromRouteParam';
  25. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  26. import useOrganization from 'sentry/utils/useOrganization';
  27. import {useParams} from 'sentry/utils/useParams';
  28. import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider';
  29. import {useProfiles} from './profilesProvider';
  30. function ProfileFlamegraph(): React.ReactElement {
  31. const organization = useOrganization();
  32. const profiles = useProfiles();
  33. const params = useParams();
  34. const [storedPreferences] = useLocalStorageState<DeepPartial<FlamegraphState>>(
  35. FLAMEGRAPH_LOCALSTORAGE_PREFERENCES_KEY,
  36. {
  37. preferences: {
  38. layout: DEFAULT_FLAMEGRAPH_STATE.preferences.layout,
  39. view: DEFAULT_FLAMEGRAPH_STATE.preferences.view,
  40. },
  41. }
  42. );
  43. const currentProject = useCurrentProjectFromRouteParam();
  44. useEffect(() => {
  45. trackAdvancedAnalyticsEvent('profiling_views.profile_flamegraph', {
  46. organization,
  47. project_platform: currentProject?.platform,
  48. project_id: currentProject?.id,
  49. });
  50. // ignore currentProject so we don't block the analytics event
  51. // or fire more than once unnecessarily
  52. // eslint-disable-next-line react-hooks/exhaustive-deps
  53. }, [organization]);
  54. const initialFlamegraphPreferencesState = useMemo((): DeepPartial<FlamegraphState> => {
  55. const queryStringState = decodeFlamegraphStateFromQueryParams(
  56. qs.parse(window.location.search)
  57. );
  58. let type =
  59. queryStringState.preferences?.type ??
  60. storedPreferences.preferences?.type ??
  61. DEFAULT_FLAMEGRAPH_STATE.preferences.type;
  62. let sorting =
  63. queryStringState.preferences?.sorting ??
  64. storedPreferences.preferences?.sorting ??
  65. DEFAULT_FLAMEGRAPH_STATE.preferences.sorting;
  66. // Ensure that the type and sorting is overriden to the default if the feature is not enabled
  67. // or if the view permutation is not compatible with the type
  68. if (
  69. type === 'flamegraph' &&
  70. !organization.features.includes('profiling-flamegraphs')
  71. ) {
  72. type = 'flamechart';
  73. }
  74. // If flamegraph, but user wants call order, switch to alphabetical
  75. if (type === 'flamegraph' && sorting === 'call order') {
  76. sorting = 'alphabetical';
  77. }
  78. // If flamechart, but user wants alphabetical, switch to call order
  79. if (type === 'flamechart' && sorting === 'alphabetical') {
  80. sorting = 'call order';
  81. }
  82. return {
  83. ...queryStringState,
  84. preferences: {
  85. ...storedPreferences.preferences,
  86. ...queryStringState.preferences,
  87. type,
  88. sorting,
  89. timelines: {
  90. ...DEFAULT_FLAMEGRAPH_STATE.preferences.timelines,
  91. ...(storedPreferences?.preferences?.timelines ?? {}),
  92. },
  93. layout:
  94. storedPreferences?.preferences?.layout ??
  95. queryStringState.preferences?.layout ??
  96. DEFAULT_FLAMEGRAPH_STATE.preferences.layout,
  97. },
  98. };
  99. // We only want to decode this when our component mounts
  100. // eslint-disable-next-line react-hooks/exhaustive-deps
  101. }, []);
  102. return (
  103. <SentryDocumentTitle
  104. title={t('Profiling \u2014 Flamechart')}
  105. orgSlug={organization.slug}
  106. >
  107. <FlamegraphStateProvider initialState={initialFlamegraphPreferencesState}>
  108. <ProfileGroupTypeProvider
  109. input={profiles.type === 'resolved' ? profiles.data : null}
  110. traceID={params.eventID}
  111. >
  112. <FlamegraphThemeProvider>
  113. <FlamegraphStateQueryParamSync />
  114. <FlamegraphStateLocalStorageSync />
  115. <FlamegraphContainer>
  116. {profiles.type === 'errored' ? (
  117. <Alert type="error" showIcon>
  118. {profiles.error}
  119. </Alert>
  120. ) : profiles.type === 'loading' ? (
  121. <LoadingIndicatorContainer>
  122. <LoadingIndicator />
  123. </LoadingIndicatorContainer>
  124. ) : null}
  125. <Flamegraph />
  126. </FlamegraphContainer>
  127. </FlamegraphThemeProvider>
  128. </ProfileGroupTypeProvider>
  129. </FlamegraphStateProvider>
  130. </SentryDocumentTitle>
  131. );
  132. }
  133. // This only exists because we need to call useFlamegraphPreferences
  134. // to get the type of visualization that the user is looking at and
  135. // we cannot do it in the component above as it is not a child of the
  136. // FlamegraphStateProvider.
  137. function ProfileGroupTypeProvider({
  138. children,
  139. input,
  140. traceID,
  141. }: {
  142. children: React.ReactNode;
  143. input: Profiling.ProfileInput | null;
  144. traceID: string;
  145. }) {
  146. const preferences = useFlamegraphPreferences();
  147. return (
  148. <ProfileGroupProvider input={input} traceID={traceID} type={preferences.type}>
  149. {children}
  150. </ProfileGroupProvider>
  151. );
  152. }
  153. const LoadingIndicatorContainer = styled('div')`
  154. position: absolute;
  155. display: flex;
  156. flex-direction: column;
  157. justify-content: center;
  158. width: 100%;
  159. height: 100%;
  160. `;
  161. const FlamegraphContainer = styled('div')`
  162. display: flex;
  163. flex-direction: column;
  164. flex: 1 1 100%;
  165. /*
  166. * The footer component is a sibling of this div.
  167. * Remove it so the flamegraph can take up the
  168. * entire screen.
  169. */
  170. ~ footer {
  171. display: none;
  172. }
  173. `;
  174. export default ProfileFlamegraph;