samplePanel.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import {Fragment} from 'react';
  2. import {Link} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import keyBy from 'lodash/keyBy';
  5. import * as qs from 'query-string';
  6. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  7. import {Button} from 'sentry/components/button';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {DurationUnit, RateUnit} from 'sentry/utils/discover/fields';
  11. import {PageAlertProvider} from 'sentry/utils/performance/contexts/pageAlert';
  12. import {decodeScalar} from 'sentry/utils/queryString';
  13. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  14. import useLocationQuery from 'sentry/utils/url/useLocationQuery';
  15. import useOrganization from 'sentry/utils/useOrganization';
  16. import useProjects from 'sentry/utils/useProjects';
  17. import useRouter from 'sentry/utils/useRouter';
  18. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  19. import {Referrer} from 'sentry/views/performance/cache/referrers';
  20. import {TransactionDurationChart} from 'sentry/views/performance/cache/samplePanel/charts/transactionDurationChart';
  21. import {BASE_FILTERS} from 'sentry/views/performance/cache/settings';
  22. import {SpanSamplesTable} from 'sentry/views/performance/cache/tables/spanSamplesTable';
  23. import {MetricReadout} from 'sentry/views/performance/metricReadout';
  24. import * as ModuleLayout from 'sentry/views/performance/moduleLayout';
  25. import DetailPanel from 'sentry/views/starfish/components/detailPanel';
  26. import {getTimeSpentExplanation} from 'sentry/views/starfish/components/tableCells/timeSpentCell';
  27. import {useIndexedSpans} from 'sentry/views/starfish/queries/useIndexedSpans';
  28. import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics';
  29. import {useTransactions} from 'sentry/views/starfish/queries/useTransactions';
  30. import {
  31. SpanFunction,
  32. SpanIndexedField,
  33. type SpanIndexedQueryFilters,
  34. SpanMetricsField,
  35. type SpanMetricsQueryFilters,
  36. } from 'sentry/views/starfish/types';
  37. import {DataTitles, getThroughputTitle} from 'sentry/views/starfish/views/spans/types';
  38. // This is similar to http sample table, its difficult to use the generic span samples sidebar as we require a bunch of custom things.
  39. export function CacheSamplePanel() {
  40. const router = useRouter();
  41. const organization = useOrganization();
  42. const query = useLocationQuery({
  43. fields: {
  44. project: decodeScalar,
  45. transaction: decodeScalar,
  46. },
  47. });
  48. // `detailKey` controls whether the panel is open. If all required properties are ailable, concat them to make a key, otherwise set to `undefined` and hide the panel
  49. const detailKey = query.transaction
  50. ? [query.transaction].filter(Boolean).join(':')
  51. : undefined;
  52. const isPanelOpen = Boolean(detailKey);
  53. const filters: SpanMetricsQueryFilters = {
  54. ...BASE_FILTERS,
  55. transaction: query.transaction,
  56. 'project.id': query.project,
  57. };
  58. const {data: cacheTransactionMetrics, isFetching: areCacheTransactionMetricsFetching} =
  59. useSpanMetrics({
  60. search: MutableSearch.fromQueryObject(filters),
  61. fields: [
  62. `${SpanFunction.SPM}()`,
  63. `${SpanFunction.CACHE_MISS_RATE}()`,
  64. `${SpanFunction.TIME_SPENT_PERCENTAGE}()`,
  65. `sum(${SpanMetricsField.SPAN_SELF_TIME})`,
  66. ],
  67. enabled: isPanelOpen,
  68. referrer: Referrer.SAMPLES_CACHE_METRICS_RIBBON,
  69. });
  70. const sampleFilters: SpanIndexedQueryFilters = {
  71. ...BASE_FILTERS,
  72. transaction: query.transaction,
  73. project_id: query.project,
  74. };
  75. const {
  76. data: cacheSpanSamplesData,
  77. isFetching: isCacheSpanSamplesFetching,
  78. refetch: refetchSpanSamples,
  79. } = useIndexedSpans({
  80. search: MutableSearch.fromQueryObject(sampleFilters).addFreeText('has:cache.hit'),
  81. fields: [
  82. SpanIndexedField.PROJECT,
  83. SpanIndexedField.TRACE,
  84. SpanIndexedField.TRANSACTION_ID,
  85. SpanIndexedField.ID,
  86. SpanIndexedField.TIMESTAMP,
  87. SpanIndexedField.SPAN_DESCRIPTION,
  88. SpanIndexedField.CACHE_HIT,
  89. SpanIndexedField.SPAN_OP,
  90. SpanIndexedField.CACHE_ITEM_SIZE,
  91. ],
  92. sorts: [SPAN_SAMPLES_SORT],
  93. limit: SPAN_SAMPLE_LIMIT,
  94. enabled: isPanelOpen,
  95. referrer: Referrer.SAMPLES_CACHE_SPAN_SAMPLES,
  96. });
  97. const {
  98. data: transactionData,
  99. error: transactionError,
  100. isFetching: isFetchingTransactions,
  101. } = useTransactions(
  102. cacheSpanSamplesData?.map(span => span['transaction.id']) || [],
  103. Referrer.SAMPLES_CACHE_SPAN_SAMPLES
  104. );
  105. const transactionDurationsMap = keyBy(transactionData, 'id');
  106. const spansWithDuration =
  107. cacheSpanSamplesData?.map(span => ({
  108. ...span,
  109. 'transaction.duration':
  110. transactionDurationsMap[span['transaction.id']]?.['transaction.duration'],
  111. })) || [];
  112. const {projects} = useProjects();
  113. const project = projects.find(p => query.project === p.id);
  114. const handleClose = () => {
  115. router.replace({
  116. pathname: router.location.pathname,
  117. query: {
  118. ...router.location.query,
  119. transaction: undefined,
  120. transactionMethod: undefined,
  121. },
  122. });
  123. };
  124. return (
  125. <PageAlertProvider>
  126. <DetailPanel detailKey={detailKey} onClose={handleClose}>
  127. <ModuleLayout.Layout>
  128. <ModuleLayout.Full>
  129. <HeaderContainer>
  130. {project && (
  131. <SpanSummaryProjectAvatar
  132. project={project}
  133. direction="left"
  134. size={40}
  135. hasTooltip
  136. tooltip={project.slug}
  137. />
  138. )}
  139. <TitleContainer>
  140. <Title>
  141. <Link
  142. to={normalizeUrl(
  143. `/organizations/${organization.slug}/performance/summary?${qs.stringify(
  144. {
  145. project: query.project,
  146. transaction: query.transaction,
  147. }
  148. )}`
  149. )}
  150. >
  151. {query.transaction}
  152. </Link>
  153. </Title>
  154. </TitleContainer>
  155. </HeaderContainer>
  156. </ModuleLayout.Full>
  157. <ModuleLayout.Full>
  158. <MetricsRibbon>
  159. <MetricReadout
  160. align="left"
  161. title={getThroughputTitle('cache')}
  162. value={cacheTransactionMetrics?.[0]?.[`${SpanFunction.SPM}()`]}
  163. unit={RateUnit.PER_MINUTE}
  164. isLoading={areCacheTransactionMetricsFetching}
  165. />
  166. <MetricReadout
  167. align="left"
  168. title={DataTitles.cacheMissRate}
  169. value={
  170. cacheTransactionMetrics?.[0]?.[`${SpanFunction.CACHE_MISS_RATE}()`]
  171. }
  172. unit="percentage"
  173. isLoading={areCacheTransactionMetricsFetching}
  174. />
  175. <MetricReadout
  176. align="left"
  177. title={DataTitles.timeSpent}
  178. value={cacheTransactionMetrics?.[0]?.['sum(span.self_time)']}
  179. unit={DurationUnit.MILLISECOND}
  180. tooltip={getTimeSpentExplanation(
  181. cacheTransactionMetrics?.[0]?.['time_spent_percentage()']
  182. )}
  183. isLoading={areCacheTransactionMetricsFetching}
  184. />
  185. </MetricsRibbon>
  186. </ModuleLayout.Full>
  187. <Fragment>
  188. <ModuleLayout.Full>
  189. <TransactionDurationChart />
  190. </ModuleLayout.Full>
  191. </Fragment>
  192. <Fragment>
  193. <ModuleLayout.Full>
  194. <SpanSamplesTable
  195. data={spansWithDuration ?? []}
  196. meta={{
  197. fields: {
  198. 'transaction.duration': 'duration',
  199. [SpanIndexedField.CACHE_ITEM_SIZE]: 'size',
  200. },
  201. units: {[SpanIndexedField.CACHE_ITEM_SIZE]: 'byte'},
  202. }}
  203. isLoading={isCacheSpanSamplesFetching || isFetchingTransactions}
  204. error={transactionError}
  205. />
  206. </ModuleLayout.Full>
  207. </Fragment>
  208. <Fragment>
  209. <ModuleLayout.Full>
  210. <Button onClick={() => refetchSpanSamples()}>
  211. {t('Try Different Samples')}
  212. </Button>
  213. </ModuleLayout.Full>
  214. </Fragment>
  215. </ModuleLayout.Layout>
  216. </DetailPanel>
  217. </PageAlertProvider>
  218. );
  219. }
  220. const SPAN_SAMPLE_LIMIT = 10;
  221. const SPAN_SAMPLES_SORT = {
  222. field: 'span_id',
  223. kind: 'desc' as const,
  224. };
  225. const SpanSummaryProjectAvatar = styled(ProjectAvatar)`
  226. padding-right: ${space(1)};
  227. `;
  228. const HeaderContainer = styled('div')`
  229. display: grid;
  230. grid-template-rows: auto auto auto;
  231. @media (min-width: ${p => p.theme.breakpoints.small}) {
  232. grid-template-rows: auto;
  233. grid-template-columns: auto 1fr auto;
  234. }
  235. `;
  236. const TitleContainer = styled('div')`
  237. width: 100%;
  238. position: relative;
  239. height: 40px;
  240. `;
  241. const Title = styled('h4')`
  242. position: absolute;
  243. bottom: 0;
  244. margin-bottom: 0;
  245. `;
  246. const MetricsRibbon = styled('div')`
  247. display: flex;
  248. flex-wrap: wrap;
  249. gap: ${space(4)};
  250. `;