profileFlamechart.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  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 {DeepPartial} from 'sentry/types/utils';
  11. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  12. import {
  13. DEFAULT_FLAMEGRAPH_STATE,
  14. FlamegraphState,
  15. } from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext';
  16. import {FlamegraphStateProvider} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContextProvider';
  17. import {
  18. decodeFlamegraphStateFromQueryParams,
  19. FLAMEGRAPH_LOCALSTORAGE_PREFERENCES_KEY,
  20. FlamegraphStateLocalStorageSync,
  21. FlamegraphStateQueryParamSync,
  22. } from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphQueryParamSync';
  23. import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider';
  24. import {useCurrentProjectFromRouteParam} from 'sentry/utils/profiling/hooks/useCurrentProjectFromRouteParam';
  25. import {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
  26. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  27. import useOrganization from 'sentry/utils/useOrganization';
  28. import {useProfileGroup, useSetProfileGroup} from './profileGroupProvider';
  29. const LoadingGroup: ProfileGroup = {
  30. name: 'Loading',
  31. activeProfileIndex: 0,
  32. transactionID: null,
  33. metadata: {},
  34. measurements: {},
  35. traceID: '',
  36. profiles: [],
  37. };
  38. function ProfileFlamegraph(): React.ReactElement {
  39. const organization = useOrganization();
  40. const profileGroup = useProfileGroup();
  41. const setProfileGroup = useSetProfileGroup();
  42. const [storedPreferences] = useLocalStorageState<DeepPartial<FlamegraphState>>(
  43. FLAMEGRAPH_LOCALSTORAGE_PREFERENCES_KEY,
  44. {
  45. preferences: {
  46. layout: DEFAULT_FLAMEGRAPH_STATE.preferences.layout,
  47. view: DEFAULT_FLAMEGRAPH_STATE.preferences.view,
  48. },
  49. }
  50. );
  51. const currentProject = useCurrentProjectFromRouteParam();
  52. useEffect(() => {
  53. trackAdvancedAnalyticsEvent('profiling_views.profile_flamegraph', {
  54. organization,
  55. project_platform: currentProject?.platform,
  56. project_id: currentProject?.id,
  57. });
  58. // ignore currentProject so we don't block the analytics event
  59. // or fire more than once unnecessarily
  60. // eslint-disable-next-line react-hooks/exhaustive-deps
  61. }, [organization]);
  62. const onImport: ProfileDragDropImportProps['onImport'] = useCallback(
  63. profiles => {
  64. setProfileGroup({type: 'resolved', data: profiles});
  65. },
  66. [setProfileGroup]
  67. );
  68. const initialFlamegraphPreferencesState = useMemo((): DeepPartial<FlamegraphState> => {
  69. const queryStringState = decodeFlamegraphStateFromQueryParams(
  70. qs.parse(window.location.search)
  71. );
  72. return {
  73. ...queryStringState,
  74. preferences: {
  75. ...storedPreferences.preferences,
  76. ...queryStringState.preferences,
  77. timelines: {
  78. ...DEFAULT_FLAMEGRAPH_STATE.preferences.timelines,
  79. ...(storedPreferences?.preferences?.timelines ?? {}),
  80. },
  81. layout:
  82. storedPreferences?.preferences?.layout ??
  83. queryStringState.preferences?.layout ??
  84. DEFAULT_FLAMEGRAPH_STATE.preferences.layout,
  85. },
  86. };
  87. // We only want to decode this when our component mounts
  88. // eslint-disable-next-line react-hooks/exhaustive-deps
  89. }, []);
  90. return (
  91. <SentryDocumentTitle
  92. title={t('Profiling \u2014 Flamechart')}
  93. orgSlug={organization.slug}
  94. >
  95. <FlamegraphStateProvider initialState={initialFlamegraphPreferencesState}>
  96. <FlamegraphThemeProvider>
  97. <FlamegraphStateQueryParamSync />
  98. <FlamegraphStateLocalStorageSync />
  99. <FlamegraphContainer>
  100. {profileGroup.type === 'errored' ? (
  101. <Alert type="error" showIcon>
  102. {profileGroup.error}
  103. </Alert>
  104. ) : profileGroup.type === 'loading' ? (
  105. <Fragment>
  106. <Flamegraph onImport={onImport} profiles={LoadingGroup} />
  107. <LoadingIndicatorContainer>
  108. <LoadingIndicator />
  109. </LoadingIndicatorContainer>
  110. </Fragment>
  111. ) : profileGroup.type === 'resolved' ? (
  112. <Flamegraph onImport={onImport} profiles={profileGroup.data} />
  113. ) : null}
  114. </FlamegraphContainer>
  115. </FlamegraphThemeProvider>
  116. </FlamegraphStateProvider>
  117. </SentryDocumentTitle>
  118. );
  119. }
  120. const LoadingIndicatorContainer = styled('div')`
  121. position: absolute;
  122. display: flex;
  123. flex-direction: column;
  124. justify-content: center;
  125. width: 100%;
  126. height: 100%;
  127. `;
  128. const FlamegraphContainer = styled('div')`
  129. display: flex;
  130. flex-direction: column;
  131. flex: 1 1 100%;
  132. /*
  133. * The footer component is a sibling of this div.
  134. * Remove it so the flamegraph can take up the
  135. * entire screen.
  136. */
  137. ~ footer {
  138. display: none;
  139. }
  140. `;
  141. export default ProfileFlamegraph;