trace.tsx 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197
  1. import type React from 'react';
  2. import {Fragment, useCallback, useMemo, useRef, useState} from 'react';
  3. import {AutoSizer, List} from 'react-virtualized';
  4. import {type Theme, useTheme} from '@emotion/react';
  5. import styled from '@emotion/styled';
  6. import type {Omit} from 'framer-motion/types/types';
  7. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
  10. import PerformanceDuration from 'sentry/components/performanceDuration';
  11. import Placeholder from 'sentry/components/placeholder';
  12. import {IconChevron} from 'sentry/icons';
  13. import {t} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import type {Project} from 'sentry/types';
  16. import useApi from 'sentry/utils/useApi';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import useProjects from 'sentry/utils/useProjects';
  19. import {
  20. isAutogroupedNode,
  21. isMissingInstrumentationNode,
  22. isParentAutogroupedNode,
  23. isSpanNode,
  24. isTraceErrorNode,
  25. isTraceNode,
  26. isTransactionNode,
  27. } from './guards';
  28. import {ParentAutogroupNode, type TraceTree, type TraceTreeNode} from './traceTree';
  29. import {VirtualizedViewManager} from './virtualizedViewManager';
  30. interface TraceProps {
  31. trace: TraceTree;
  32. trace_id: string;
  33. }
  34. function Trace({trace, trace_id}: TraceProps) {
  35. const theme = useTheme();
  36. const api = useApi();
  37. const {projects} = useProjects();
  38. const organization = useOrganization();
  39. const viewManager = useRef<VirtualizedViewManager | null>(null);
  40. const [_rerender, setRender] = useState(0);
  41. if (!viewManager.current) {
  42. viewManager.current = new VirtualizedViewManager({
  43. list: {width: 0.5},
  44. span_list: {width: 0.5},
  45. });
  46. }
  47. if (
  48. trace.root.space &&
  49. (trace.root.space[0] !== viewManager.current.spanSpace[0] ||
  50. trace.root.space[1] !== viewManager.current.spanSpace[1])
  51. ) {
  52. viewManager.current.initializeSpanSpace(trace.root.space);
  53. }
  54. const treeRef = useRef<TraceTree>(trace);
  55. treeRef.current = trace;
  56. const handleFetchChildren = useCallback(
  57. (node: TraceTreeNode<TraceTree.NodeValue>, value: boolean) => {
  58. if (!isTransactionNode(node) && !isSpanNode(node)) {
  59. throw new TypeError('Node must be a transaction or span');
  60. }
  61. treeRef.current
  62. .zoomIn(node, value, {
  63. api,
  64. organization,
  65. })
  66. .then(() => {
  67. setRender(a => (a + 1) % 2);
  68. });
  69. },
  70. [api, organization]
  71. );
  72. const handleExpandNode = useCallback(
  73. (node: TraceTreeNode<TraceTree.NodeValue>, value: boolean) => {
  74. treeRef.current.expand(node, value);
  75. setRender(a => (a + 1) % 2);
  76. },
  77. []
  78. );
  79. const projectLookup = useMemo(() => {
  80. return projects.reduce<Record<Project['slug'], Project>>((acc, project) => {
  81. acc[project.slug] = project;
  82. return acc;
  83. }, {});
  84. }, [projects]);
  85. return (
  86. <Fragment>
  87. <TraceStylingWrapper
  88. ref={r => viewManager.current?.onContainerRef(r)}
  89. className={trace.type === 'loading' ? 'Loading' : ''}
  90. style={{
  91. backgroundColor: '#FFF',
  92. height: '70vh',
  93. width: '100%',
  94. margin: 'auto',
  95. }}
  96. >
  97. <TraceDivider ref={r => viewManager.current?.registerDividerRef(r)} />
  98. <AutoSizer>
  99. {({width, height}) => (
  100. <Fragment>
  101. {trace.indicators.length > 0
  102. ? trace.indicators.map((indicator, i) => {
  103. return (
  104. <div
  105. key={i}
  106. ref={r =>
  107. viewManager.current?.registerIndicatorRef(r, i, indicator)
  108. }
  109. className="TraceIndicator"
  110. >
  111. <div className="TraceIndicatorLine" />
  112. </div>
  113. );
  114. })
  115. : null}
  116. <List
  117. ref={r => viewManager.current?.registerVirtualizedList(r)}
  118. rowHeight={24}
  119. height={height}
  120. width={width}
  121. overscanRowCount={5}
  122. rowCount={treeRef.current.list.length ?? 0}
  123. rowRenderer={p => {
  124. return trace.type === 'loading' ? (
  125. <RenderPlaceholderRow
  126. style={p.style}
  127. node={treeRef.current.list[p.index]}
  128. index={p.index}
  129. theme={theme}
  130. projects={projectLookup}
  131. viewManager={viewManager.current!}
  132. startIndex={
  133. (p.parent as unknown as {_rowStartIndex: number})
  134. ._rowStartIndex ?? 0
  135. }
  136. />
  137. ) : (
  138. <RenderRow
  139. key={p.key}
  140. theme={theme}
  141. startIndex={
  142. (p.parent as unknown as {_rowStartIndex: number})
  143. ._rowStartIndex ?? 0
  144. }
  145. index={p.index}
  146. style={p.style}
  147. trace_id={trace_id}
  148. projects={projectLookup}
  149. node={treeRef.current.list[p.index]}
  150. viewManager={viewManager.current!}
  151. onFetchChildren={handleFetchChildren}
  152. onExpandNode={handleExpandNode}
  153. />
  154. );
  155. }}
  156. />
  157. </Fragment>
  158. )}
  159. </AutoSizer>
  160. </TraceStylingWrapper>
  161. </Fragment>
  162. );
  163. }
  164. export default Trace;
  165. const TraceDivider = styled('div')`
  166. position: absolute;
  167. height: 100%;
  168. background-color: transparent;
  169. top: 0;
  170. z-index: 10;
  171. cursor: col-resize;
  172. &:before {
  173. content: '';
  174. position: absolute;
  175. width: 1px;
  176. height: 100%;
  177. background-color: ${p => p.theme.border};
  178. left: 50%;
  179. }
  180. &:hover&:before {
  181. background-color: ${p => p.theme.purple300};
  182. }
  183. `;
  184. function RenderRow(props: {
  185. index: number;
  186. node: TraceTreeNode<TraceTree.NodeValue>;
  187. onExpandNode: (node: TraceTreeNode<TraceTree.NodeValue>, value: boolean) => void;
  188. onFetchChildren: (node: TraceTreeNode<TraceTree.NodeValue>, value: boolean) => void;
  189. projects: Record<Project['slug'], Project>;
  190. startIndex: number;
  191. style: React.CSSProperties;
  192. theme: Theme;
  193. trace_id: string;
  194. viewManager: VirtualizedViewManager;
  195. }) {
  196. const virtualizedIndex = props.index - props.startIndex;
  197. if (!props.node.value) {
  198. return null;
  199. }
  200. if (isAutogroupedNode(props.node)) {
  201. return (
  202. <div
  203. className="TraceRow Autogrouped"
  204. style={{
  205. top: props.style.top,
  206. height: props.style.height,
  207. }}
  208. >
  209. <div
  210. className="TraceLeftColumn"
  211. ref={r =>
  212. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  213. }
  214. style={{
  215. width: props.viewManager.columns.list.width * 100 + '%',
  216. }}
  217. >
  218. <div
  219. className="TraceLeftColumnInner"
  220. style={{
  221. paddingLeft: props.node.depth * 24,
  222. }}
  223. >
  224. <div className="TraceChildrenCountWrapper">
  225. <Connectors node={props.node} />
  226. <ChildrenCountButton
  227. expanded={!props.node.expanded}
  228. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  229. >
  230. {props.node.groupCount}{' '}
  231. </ChildrenCountButton>
  232. </div>
  233. <span className="TraceOperation">{t('Autogrouped')}</span>
  234. <strong className="TraceEmDash"> — </strong>
  235. <span className="TraceDescription">{props.node.value.autogrouped_by.op}</span>
  236. </div>
  237. </div>
  238. <div
  239. className="TraceRightColumn"
  240. ref={r =>
  241. props.viewManager.registerColumnRef(
  242. 'span_list',
  243. r,
  244. virtualizedIndex,
  245. props.node
  246. )
  247. }
  248. style={{
  249. width: props.viewManager.columns.span_list.width * 100 + '%',
  250. backgroundColor:
  251. props.index % 2 ? undefined : props.theme.backgroundSecondary,
  252. }}
  253. >
  254. {isParentAutogroupedNode(props.node) ? (
  255. <TraceBar
  256. virtualizedIndex={virtualizedIndex}
  257. viewManager={props.viewManager}
  258. color={props.theme.blue300}
  259. node_space={props.node.space}
  260. />
  261. ) : (
  262. <SiblingAutogroupedBar
  263. virtualizedIndex={virtualizedIndex}
  264. viewManager={props.viewManager}
  265. color={props.theme.blue300}
  266. node={props.node}
  267. />
  268. )}
  269. </div>
  270. </div>
  271. );
  272. }
  273. if (isTransactionNode(props.node)) {
  274. return (
  275. <div
  276. className="TraceRow"
  277. style={{
  278. top: props.style.top,
  279. height: props.style.height,
  280. }}
  281. >
  282. <div
  283. className="TraceLeftColumn"
  284. ref={r =>
  285. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  286. }
  287. style={{
  288. width: props.viewManager.columns.list.width * 100 + '%',
  289. }}
  290. >
  291. <div
  292. className="TraceLeftColumnInner"
  293. style={{
  294. paddingLeft: props.node.depth * 24,
  295. }}
  296. >
  297. <div
  298. className={`TraceChildrenCountWrapper ${
  299. props.node.isOrphaned ? 'Orphaned' : ''
  300. }`}
  301. >
  302. <Connectors node={props.node} />
  303. {props.node.children.length > 0 ? (
  304. <ChildrenCountButton
  305. expanded={props.node.expanded || props.node.zoomedIn}
  306. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  307. >
  308. {props.node.children.length}{' '}
  309. </ChildrenCountButton>
  310. ) : null}
  311. </div>
  312. <ProjectBadge project={props.projects[props.node.value.project_slug]} />
  313. <span className="TraceOperation">{props.node.value['transaction.op']}</span>
  314. <strong className="TraceEmDash"> — </strong>
  315. <span>{props.node.value.transaction}</span>
  316. {props.node.canFetchData ? (
  317. <button
  318. onClick={() => props.onFetchChildren(props.node, !props.node.zoomedIn)}
  319. >
  320. {props.node.zoomedIn ? 'Zoom Out' : 'Zoom In'}
  321. </button>
  322. ) : null}
  323. </div>
  324. </div>
  325. <div
  326. ref={r =>
  327. props.viewManager.registerColumnRef(
  328. 'span_list',
  329. r,
  330. virtualizedIndex,
  331. props.node
  332. )
  333. }
  334. className="TraceRightColumn"
  335. style={{
  336. width: props.viewManager.columns.span_list.width * 100 + '%',
  337. backgroundColor:
  338. props.index % 2 ? undefined : props.theme.backgroundSecondary,
  339. }}
  340. >
  341. <TraceBar
  342. virtualizedIndex={virtualizedIndex}
  343. viewManager={props.viewManager}
  344. color={pickBarColor(props.node.value['transaction.op'])}
  345. node_space={props.node.space}
  346. />
  347. </div>
  348. </div>
  349. );
  350. }
  351. if (isSpanNode(props.node)) {
  352. return (
  353. <div
  354. className="TraceRow"
  355. style={{
  356. top: props.style.top,
  357. height: props.style.height,
  358. }}
  359. >
  360. <div
  361. className="TraceLeftColumn"
  362. ref={r =>
  363. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  364. }
  365. style={{
  366. width: props.viewManager.columns.list.width * 100 + '%',
  367. }}
  368. >
  369. <div
  370. className="TraceLeftColumnInner"
  371. style={{
  372. paddingLeft: props.node.depth * 24,
  373. }}
  374. >
  375. <div
  376. className={`TraceChildrenCountWrapper ${
  377. props.node.isOrphaned ? 'Orphaned' : ''
  378. }`}
  379. >
  380. <Connectors node={props.node} />
  381. {props.node.children.length > 0 ? (
  382. <ChildrenCountButton
  383. expanded={props.node.expanded || props.node.zoomedIn}
  384. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  385. >
  386. {props.node.children.length}{' '}
  387. </ChildrenCountButton>
  388. ) : null}
  389. </div>
  390. <span className="TraceOperation">{props.node.value.op ?? '<unknown>'}</span>
  391. <strong className="TraceEmDash"> — </strong>
  392. <span className="TraceDescription" title={props.node.value.description}>
  393. {!props.node.value.description
  394. ? 'unknown'
  395. : props.node.value.description.length > 100
  396. ? props.node.value.description.slice(0, 100).trim() + '\u2026'
  397. : props.node.value.description}
  398. </span>
  399. {props.node.canFetchData ? (
  400. <button
  401. onClick={() => props.onFetchChildren(props.node, !props.node.zoomedIn)}
  402. >
  403. {props.node.zoomedIn ? 'Zoom Out' : 'Zoom In'}
  404. </button>
  405. ) : null}
  406. </div>
  407. </div>
  408. <div
  409. ref={r =>
  410. props.viewManager.registerColumnRef(
  411. 'span_list',
  412. r,
  413. virtualizedIndex,
  414. props.node
  415. )
  416. }
  417. className="TraceRightColumn"
  418. style={{
  419. width: props.viewManager.columns.span_list.width * 100 + '%',
  420. backgroundColor:
  421. props.index % 2 ? undefined : props.theme.backgroundSecondary,
  422. }}
  423. >
  424. <TraceBar
  425. virtualizedIndex={virtualizedIndex}
  426. viewManager={props.viewManager}
  427. color={pickBarColor(props.node.value.op)}
  428. node_space={props.node.space}
  429. />
  430. </div>
  431. </div>
  432. );
  433. }
  434. if (isMissingInstrumentationNode(props.node)) {
  435. return (
  436. <div
  437. className="TraceRow"
  438. style={{
  439. top: props.style.top,
  440. height: props.style.height,
  441. }}
  442. >
  443. <div
  444. className="TraceLeftColumn"
  445. ref={r =>
  446. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  447. }
  448. style={{
  449. width: props.viewManager.columns.list.width * 100 + '%',
  450. }}
  451. >
  452. <div
  453. className="TraceLeftColumnInner"
  454. style={{
  455. paddingLeft: props.node.depth * 24,
  456. }}
  457. >
  458. <div className="TraceChildrenCountWrapper">
  459. <Connectors node={props.node} />
  460. </div>
  461. <span className="TraceOperation">{t('Missing instrumentation')}</span>
  462. </div>
  463. </div>
  464. <div
  465. ref={r =>
  466. props.viewManager.registerColumnRef(
  467. 'span_list',
  468. r,
  469. virtualizedIndex,
  470. props.node
  471. )
  472. }
  473. className="TraceRightColumn"
  474. style={{
  475. width: props.viewManager.columns.span_list.width * 100 + '%',
  476. backgroundColor:
  477. props.index % 2 ? undefined : props.theme.backgroundSecondary,
  478. }}
  479. >
  480. <TraceBar
  481. virtualizedIndex={virtualizedIndex}
  482. viewManager={props.viewManager}
  483. color={pickBarColor('missing-instrumentation')}
  484. node_space={props.node.space}
  485. />
  486. </div>
  487. </div>
  488. );
  489. }
  490. if (isTraceNode(props.node)) {
  491. return (
  492. <div
  493. className="TraceRow"
  494. style={{
  495. top: props.style.top,
  496. height: props.style.height,
  497. }}
  498. >
  499. <div
  500. className="TraceLeftColumn"
  501. ref={r =>
  502. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  503. }
  504. style={{
  505. width: props.viewManager.columns.list.width * 100 + '%',
  506. }}
  507. >
  508. <div
  509. className="TraceLeftColumnInner"
  510. style={{
  511. paddingLeft: props.node.depth * 24,
  512. }}
  513. >
  514. <div className="TraceChildrenCountWrapper Root">
  515. <Connectors node={props.node} />
  516. {props.node.children.length > 0 ? (
  517. <ChildrenCountButton
  518. expanded={props.node.expanded || props.node.zoomedIn}
  519. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  520. >
  521. {props.node.children.length}{' '}
  522. </ChildrenCountButton>
  523. ) : null}
  524. </div>
  525. <span className="TraceOperation">{t('Trace')}</span>
  526. <strong className="TraceEmDash"> — </strong>
  527. <span className="TraceDescription">{props.trace_id}</span>
  528. </div>
  529. </div>
  530. <div
  531. ref={r =>
  532. props.viewManager.registerColumnRef(
  533. 'span_list',
  534. r,
  535. virtualizedIndex,
  536. props.node
  537. )
  538. }
  539. className="TraceRightColumn"
  540. style={{
  541. width: props.viewManager.columns.span_list.width * 100 + '%',
  542. backgroundColor:
  543. props.index % 2 ? undefined : props.theme.backgroundSecondary,
  544. }}
  545. >
  546. <TraceBar
  547. virtualizedIndex={virtualizedIndex}
  548. viewManager={props.viewManager}
  549. color={pickBarColor('missing-instrumentation')}
  550. node_space={props.node.space}
  551. />
  552. </div>
  553. </div>
  554. );
  555. }
  556. if (isTraceErrorNode(props.node)) {
  557. return (
  558. <div
  559. className="TraceRow"
  560. style={{
  561. top: props.style.top,
  562. height: props.style.height,
  563. }}
  564. >
  565. <div
  566. className="TraceLeftColumn"
  567. ref={r =>
  568. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  569. }
  570. style={{
  571. width: props.viewManager.columns.list.width * 100 + '%',
  572. }}
  573. >
  574. <div
  575. className="TraceLeftColumnInner"
  576. style={{
  577. paddingLeft: props.node.depth * 24,
  578. }}
  579. >
  580. <div className="TraceChildrenCountWrapper">
  581. <Connectors node={props.node} />
  582. {props.node.children.length > 0 ? (
  583. <ChildrenCountButton
  584. expanded={props.node.expanded || props.node.zoomedIn}
  585. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  586. >
  587. {props.node.children.length}{' '}
  588. </ChildrenCountButton>
  589. ) : null}
  590. </div>
  591. <span className="TraceOperation">{t('Error')}</span>
  592. <strong className="TraceEmDash"> — </strong>
  593. <span className="TraceDescription">{props.node.value.title}</span>
  594. </div>
  595. </div>
  596. <div
  597. ref={r =>
  598. props.viewManager.registerColumnRef(
  599. 'span_list',
  600. r,
  601. virtualizedIndex,
  602. props.node
  603. )
  604. }
  605. className="TraceRightColumn"
  606. style={{
  607. width: props.viewManager.columns.span_list.width * 100 + '%',
  608. backgroundColor:
  609. props.index % 2 ? undefined : props.theme.backgroundSecondary,
  610. }}
  611. >
  612. {/* @TODO: figure out what to do with trace errors */}
  613. {/* <TraceBar
  614. space={props.space}
  615. start_timestamp={props.node.value.start_timestamp}
  616. timestamp={props.node.value.timestamp}
  617. /> */}
  618. </div>
  619. </div>
  620. );
  621. }
  622. return null;
  623. }
  624. function RenderPlaceholderRow(props: {
  625. index: number;
  626. node: TraceTreeNode<TraceTree.NodeValue>;
  627. projects: Record<Project['slug'], Project>;
  628. startIndex: number;
  629. style: React.CSSProperties;
  630. theme: Theme;
  631. viewManager: VirtualizedViewManager;
  632. }) {
  633. const virtualizedIndex = props.index - props.startIndex;
  634. return (
  635. <div
  636. className="TraceRow"
  637. style={{
  638. top: props.style.top,
  639. height: props.style.height,
  640. pointerEvents: 'none',
  641. color: props.theme.subText,
  642. animationDelay: `${virtualizedIndex * 0.05}s`,
  643. paddingLeft: space(1),
  644. }}
  645. >
  646. <div
  647. className="TraceLeftColumn"
  648. style={{width: props.viewManager.columns.list.width * 100 + '%'}}
  649. >
  650. <div
  651. className="TraceLeftColumnInner"
  652. style={{
  653. paddingLeft: props.node.depth * 24,
  654. }}
  655. >
  656. <div className="TraceChildrenCountWrapper">
  657. <Connectors node={props.node} />
  658. {props.node.children.length > 0 ? (
  659. <ChildrenCountButton
  660. expanded={props.node.expanded || props.node.zoomedIn}
  661. onClick={() => void 0}
  662. >
  663. {props.node.children.length}{' '}
  664. </ChildrenCountButton>
  665. ) : null}
  666. </div>
  667. {isTraceNode(props.node) ? <SmallLoadingIndicator /> : null}
  668. {isTraceNode(props.node) ? (
  669. 'Loading trace...'
  670. ) : (
  671. <Placeholder className="Placeholder" height="10px" width="86%" />
  672. )}
  673. </div>
  674. </div>
  675. <div
  676. className="TraceRightColumn"
  677. style={{
  678. width: props.viewManager.columns.span_list.width * 100 + '%',
  679. }}
  680. >
  681. {isTraceNode(props.node) ? null : (
  682. <Placeholder
  683. className="Placeholder"
  684. height="14px"
  685. width="90%"
  686. style={{margin: 'auto'}}
  687. />
  688. )}
  689. </div>
  690. </div>
  691. );
  692. }
  693. function Connectors(props: {node: TraceTreeNode<TraceTree.NodeValue>}) {
  694. const showVerticalConnector =
  695. ((props.node.expanded || props.node.zoomedIn) && props.node.children.length > 0) ||
  696. (props.node.value && isParentAutogroupedNode(props.node));
  697. // If the tail node of the collapsed node has no children,
  698. // we don't want to render the vertical connector as no children
  699. // are being rendered as the chain is entirely collapsed
  700. const hideVerticalConnector =
  701. showVerticalConnector &&
  702. props.node.value &&
  703. props.node instanceof ParentAutogroupNode &&
  704. !props.node.tail.children.length;
  705. return (
  706. <Fragment>
  707. {/*
  708. @TODO count of rendered connectors could be % 3 as we can
  709. have up to 3 connectors per node, 1 div, 1 before and 1 after
  710. */}
  711. {props.node.connectors.map((c, i) => {
  712. return (
  713. <div
  714. key={i}
  715. style={{left: -(Math.abs(Math.abs(c) - props.node.depth) * 24)}}
  716. className={`TraceVerticalConnector ${c < 0 ? 'Orphaned' : ''}`}
  717. />
  718. );
  719. })}
  720. {showVerticalConnector && !hideVerticalConnector ? (
  721. <div className="TraceExpandedVerticalConnector" />
  722. ) : null}
  723. {props.node.isLastChild ? (
  724. <div className="TraceVerticalLastChildConnector" />
  725. ) : (
  726. <div className="TraceVerticalConnector" />
  727. )}
  728. </Fragment>
  729. );
  730. }
  731. function SmallLoadingIndicator() {
  732. return (
  733. <StyledLoadingIndicator
  734. style={{display: 'inline-block', margin: 0}}
  735. size={8}
  736. hideMessage
  737. relative
  738. />
  739. );
  740. }
  741. const StyledLoadingIndicator = styled(LoadingIndicator)`
  742. transform: translate(-5px, 0);
  743. div:first-child {
  744. border-left: 6px solid ${p => p.theme.gray300};
  745. animation: loading 900ms infinite linear;
  746. }
  747. `;
  748. function ProjectBadge(props: {project: Project}) {
  749. return <ProjectAvatar project={props.project} />;
  750. }
  751. function ChildrenCountButton(props: {
  752. children: React.ReactNode;
  753. expanded: boolean;
  754. onClick: () => void;
  755. }) {
  756. return (
  757. <button className="TraceChildrenCount" onClick={props.onClick}>
  758. {props.children}
  759. <IconChevron
  760. size="xs"
  761. direction={props.expanded ? 'up' : 'down'}
  762. style={{marginLeft: 2}}
  763. />
  764. </button>
  765. );
  766. }
  767. interface TraceBarProps {
  768. color: string;
  769. node_space: [number, number] | null;
  770. viewManager: VirtualizedViewManager;
  771. virtualizedIndex: number;
  772. duration?: number;
  773. }
  774. type SiblingAutogroupedBarProps = Omit<TraceBarProps, 'node_space' | 'duration'> & {
  775. node: TraceTreeNode<TraceTree.NodeValue>;
  776. };
  777. // Render collapsed representation of sibling autogrouping, using multiple bars for when
  778. // there are gaps between siblings.
  779. function SiblingAutogroupedBar(props: SiblingAutogroupedBarProps) {
  780. const bars: React.ReactNode[] = [];
  781. // Start and end represents the earliest start_timestamp and the latest
  782. // end_timestamp for a set of overlapping siblings.
  783. let start = isSpanNode(props.node.children[0])
  784. ? props.node.children[0].value.start_timestamp
  785. : Number.POSITIVE_INFINITY;
  786. let end = isSpanNode(props.node.children[0])
  787. ? props.node.children[0].value.timestamp
  788. : Number.NEGATIVE_INFINITY;
  789. let totalDuration = 0;
  790. for (let i = 0; i < props.node.children.length; i++) {
  791. const node = props.node.children[i];
  792. if (!isSpanNode(node)) {
  793. throw new TypeError('Invalid type of autogrouped child');
  794. }
  795. const hasGap = node.value.start_timestamp > end;
  796. if (!(hasGap || node.isLastChild)) {
  797. start = Math.min(start, node.value.start_timestamp);
  798. end = Math.max(end, node.value.timestamp);
  799. continue;
  800. }
  801. // Render a bar for already collapsed set.
  802. totalDuration += end - start;
  803. bars.push(
  804. <TraceBar
  805. virtualizedIndex={props.virtualizedIndex}
  806. viewManager={props.viewManager}
  807. color={props.color}
  808. node_space={[start, end - start]}
  809. duration={!hasGap ? totalDuration : undefined}
  810. />
  811. );
  812. if (hasGap) {
  813. // Start a new set.
  814. start = node.value.start_timestamp;
  815. end = node.value.timestamp;
  816. // Render a bar if the sibling with a gap is the last sibling.
  817. if (node.isLastChild) {
  818. totalDuration += end - start;
  819. bars.push(
  820. <TraceBar
  821. virtualizedIndex={props.virtualizedIndex}
  822. viewManager={props.viewManager}
  823. color={props.color}
  824. duration={totalDuration}
  825. node_space={[start, end - start]}
  826. />
  827. );
  828. }
  829. }
  830. }
  831. return <Fragment>{bars}</Fragment>;
  832. }
  833. function TraceBar(props: TraceBarProps) {
  834. if (!props.node_space) {
  835. return null;
  836. }
  837. const spanTransform = props.viewManager.computeSpanMatrixTransform(props.node_space);
  838. const inverseTransform = props.viewManager.inverseSpanScaling(props.node_space);
  839. const textPosition = props.viewManager.computeSpanTextPlacement(
  840. spanTransform[4],
  841. props.node_space
  842. );
  843. return (
  844. <div
  845. ref={r =>
  846. props.viewManager.registerSpanBarRef(r, props.node_space!, props.virtualizedIndex)
  847. }
  848. className="TraceBar"
  849. style={{
  850. transform: `matrix(${spanTransform.join(',')})`,
  851. backgroundColor: props.color,
  852. }}
  853. >
  854. <div
  855. className={`TraceBarDuration ${textPosition === 'inside left' ? 'Inside' : ''}`}
  856. style={{
  857. left: textPosition === 'left' || textPosition === 'inside left' ? '0' : '100%',
  858. transform: `matrix(${inverseTransform}, 0,0,1,0,0) translate(${
  859. textPosition === 'left' ? 'calc(-100% - 4px)' : '4px'
  860. }, 0)`,
  861. }}
  862. >
  863. {/* Use node space to calculate duration if the duration prop is not provided. */}
  864. <PerformanceDuration
  865. seconds={props.duration ?? props.node_space[1]}
  866. abbreviation
  867. />
  868. </div>
  869. </div>
  870. );
  871. }
  872. /**
  873. * This is a wrapper around the Trace component to apply styles
  874. * to the trace tree. It exists because we _do not_ want to trigger
  875. * emotion's css parsing logic as it is very slow and will cause
  876. * the scrolling to flicker.
  877. */
  878. const TraceStylingWrapper = styled('div')`
  879. position: relative;
  880. border: 1px solid ${p => p.theme.border};
  881. padding: ${space(0.5)} 0;
  882. border-radius: ${space(0.5)};
  883. @keyframes show {
  884. 0% {
  885. opacity: 0;
  886. transform: translate(0, 2px);
  887. }
  888. 100% {
  889. opacity: 0.7;
  890. transform: translate(0, 0px);
  891. }
  892. }
  893. @keyframes showPlaceholder {
  894. 0% {
  895. opacity: 0;
  896. transform: translate(-8px, 0px);
  897. }
  898. 100% {
  899. opacity: 0.7;
  900. transform: translate(0, 0px);
  901. }
  902. }
  903. .TraceIndicator {
  904. z-index: 1;
  905. width: 3px;
  906. height: 100%;
  907. top: 0;
  908. position: absolute;
  909. .TraceIndicatorLine {
  910. width: 1px;
  911. height: 100%;
  912. position: absolute;
  913. left: 50%;
  914. transform: translateX(-50%);
  915. background: repeating-linear-gradient(
  916. to bottom,
  917. transparent 0 4px,
  918. ${p => p.theme.textColor} 4px 8px
  919. )
  920. 80%/2px 100% no-repeat;
  921. }
  922. }
  923. &.Loading {
  924. .TraceRow {
  925. opacity: 0;
  926. animation: show 0.2s ease-in-out forwards;
  927. }
  928. .Placeholder {
  929. opacity: 0;
  930. transform: translate(-8px, 0px);
  931. animation: showPlaceholder 0.2s ease-in-out forwards;
  932. }
  933. }
  934. .TraceRow {
  935. display: flex;
  936. align-items: center;
  937. position: absolute;
  938. width: 100%;
  939. transition: background-color 0.15s ease-in-out 0s;
  940. font-size: ${p => p.theme.fontSizeSmall};
  941. &:hover {
  942. background-color: ${p => p.theme.backgroundSecondary};
  943. }
  944. &.Autogrouped {
  945. color: ${p => p.theme.blue300};
  946. .TraceDescription {
  947. font-weight: bold;
  948. }
  949. .TraceChildrenCountWrapper {
  950. button {
  951. color: ${p => p.theme.white};
  952. background-color: ${p => p.theme.blue300};
  953. }
  954. }
  955. }
  956. }
  957. .TraceLeftColumn {
  958. height: 100%;
  959. white-space: nowrap;
  960. display: flex;
  961. align-items: center;
  962. overflow: hidden;
  963. will-change: width;
  964. .TraceLeftColumnInner {
  965. height: 100%;
  966. white-space: nowrap;
  967. display: flex;
  968. align-items: center;
  969. will-change: transform;
  970. transform-origin: left center;
  971. transform: translateX(var(--column-translate-x));
  972. }
  973. }
  974. .TraceRightColumn {
  975. height: 100%;
  976. position: relative;
  977. display: flex;
  978. align-items: center;
  979. will-change: width;
  980. z-index: 1;
  981. }
  982. .TraceBar {
  983. position: absolute;
  984. height: 64%;
  985. width: 100%;
  986. background-color: black;
  987. transform-origin: left center;
  988. }
  989. .TraceBarDuration {
  990. display: inline-block;
  991. transform-origin: left center;
  992. font-size: ${p => p.theme.fontSizeExtraSmall};
  993. color: ${p => p.theme.gray300};
  994. white-space: nowrap;
  995. font-variant-numeric: tabular-nums;
  996. position: absolute;
  997. &.Inside {
  998. color: ${p => p.theme.gray100};
  999. }
  1000. }
  1001. .TraceChildrenCount {
  1002. height: 16px;
  1003. white-space: nowrap;
  1004. min-width: 30px;
  1005. display: flex;
  1006. align-items: center;
  1007. justify-content: center;
  1008. border-radius: 99px;
  1009. padding: 0px ${space(0.5)};
  1010. transition: all 0.15s ease-in-out;
  1011. background: ${p => p.theme.background};
  1012. border: 2px solid ${p => p.theme.border};
  1013. line-height: 0;
  1014. z-index: 1;
  1015. font-size: 10px;
  1016. box-shadow: ${p => p.theme.dropShadowLight};
  1017. margin-right: ${space(1)};
  1018. svg {
  1019. width: 7px;
  1020. transition: none;
  1021. }
  1022. }
  1023. .TraceChildrenCountWrapper {
  1024. display: flex;
  1025. justify-content: flex-end;
  1026. align-items: center;
  1027. min-width: 48px;
  1028. height: 100%;
  1029. position: relative;
  1030. button {
  1031. transition: none;
  1032. }
  1033. &.Orphaned {
  1034. .TraceVerticalConnector,
  1035. .TraceVerticalLastChildConnector,
  1036. .TraceExpandedVerticalConnector {
  1037. border-left: 2px dashed ${p => p.theme.border};
  1038. }
  1039. &::before {
  1040. border-bottom: 2px dashed ${p => p.theme.border};
  1041. }
  1042. }
  1043. &.Root {
  1044. &:before,
  1045. .TraceVerticalLastChildConnector {
  1046. visibility: hidden;
  1047. }
  1048. }
  1049. &::before {
  1050. content: '';
  1051. display: block;
  1052. width: 60%;
  1053. height: 2px;
  1054. border-bottom: 2px solid ${p => p.theme.border};
  1055. position: absolute;
  1056. left: 0;
  1057. top: 50%;
  1058. transform: translateY(-50%);
  1059. }
  1060. &::after {
  1061. content: '';
  1062. background-color: rgb(224, 220, 229);
  1063. border-radius: 50%;
  1064. height: 6px;
  1065. width: 6px;
  1066. position: absolute;
  1067. left: 60%;
  1068. top: 50%;
  1069. transform: translateY(-50%);
  1070. }
  1071. }
  1072. .TraceVerticalConnector {
  1073. position: absolute;
  1074. left: 0;
  1075. top: 0;
  1076. bottom: 0;
  1077. height: 100%;
  1078. width: 2px;
  1079. border-left: 2px solid ${p => p.theme.border};
  1080. &.Orphaned {
  1081. border-left: 2px dashed ${p => p.theme.border};
  1082. }
  1083. }
  1084. .TraceVerticalLastChildConnector {
  1085. position: absolute;
  1086. left: 0;
  1087. top: 0;
  1088. bottom: 0;
  1089. height: 50%;
  1090. width: 2px;
  1091. border-left: 2px solid ${p => p.theme.border};
  1092. border-bottom-left-radius: 4px;
  1093. }
  1094. .TraceExpandedVerticalConnector {
  1095. position: absolute;
  1096. bottom: 0;
  1097. height: 50%;
  1098. left: 50%;
  1099. width: 2px;
  1100. border-left: 2px solid ${p => p.theme.border};
  1101. }
  1102. .TraceOperation {
  1103. margin-left: ${space(0.5)};
  1104. text-overflow: ellipsis;
  1105. white-space: nowrap;
  1106. font-weight: bold;
  1107. }
  1108. .TraceEmDash {
  1109. margin-left: ${space(0.5)};
  1110. margin-right: ${space(0.5)};
  1111. }
  1112. .TraceDescription {
  1113. white-space: nowrap;
  1114. }
  1115. `;