trace.tsx 29 KB

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