123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197 |
- import type React from 'react';
- import {Fragment, useCallback, useMemo, useRef, useState} from 'react';
- import {AutoSizer, List} from 'react-virtualized';
- import {type Theme, useTheme} from '@emotion/react';
- import styled from '@emotion/styled';
- import type {Omit} from 'framer-motion/types/types';
- import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
- import LoadingIndicator from 'sentry/components/loadingIndicator';
- import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
- import PerformanceDuration from 'sentry/components/performanceDuration';
- import Placeholder from 'sentry/components/placeholder';
- import {IconChevron} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {Project} from 'sentry/types';
- import useApi from 'sentry/utils/useApi';
- import useOrganization from 'sentry/utils/useOrganization';
- import useProjects from 'sentry/utils/useProjects';
- import {
- isAutogroupedNode,
- isMissingInstrumentationNode,
- isParentAutogroupedNode,
- isSpanNode,
- isTraceErrorNode,
- isTraceNode,
- isTransactionNode,
- } from './guards';
- import {ParentAutogroupNode, type TraceTree, type TraceTreeNode} from './traceTree';
- import {VirtualizedViewManager} from './virtualizedViewManager';
- interface TraceProps {
- trace: TraceTree;
- trace_id: string;
- }
- function Trace({trace, trace_id}: TraceProps) {
- const theme = useTheme();
- const api = useApi();
- const {projects} = useProjects();
- const organization = useOrganization();
- const viewManager = useRef<VirtualizedViewManager | null>(null);
- const [_rerender, setRender] = useState(0);
- if (!viewManager.current) {
- viewManager.current = new VirtualizedViewManager({
- list: {width: 0.5},
- span_list: {width: 0.5},
- });
- }
- if (
- trace.root.space &&
- (trace.root.space[0] !== viewManager.current.spanSpace[0] ||
- trace.root.space[1] !== viewManager.current.spanSpace[1])
- ) {
- viewManager.current.initializeSpanSpace(trace.root.space);
- }
- const treeRef = useRef<TraceTree>(trace);
- treeRef.current = trace;
- const handleFetchChildren = useCallback(
- (node: TraceTreeNode<TraceTree.NodeValue>, value: boolean) => {
- if (!isTransactionNode(node) && !isSpanNode(node)) {
- throw new TypeError('Node must be a transaction or span');
- }
- treeRef.current
- .zoomIn(node, value, {
- api,
- organization,
- })
- .then(() => {
- setRender(a => (a + 1) % 2);
- });
- },
- [api, organization]
- );
- const handleExpandNode = useCallback(
- (node: TraceTreeNode<TraceTree.NodeValue>, value: boolean) => {
- treeRef.current.expand(node, value);
- setRender(a => (a + 1) % 2);
- },
- []
- );
- const projectLookup = useMemo(() => {
- return projects.reduce<Record<Project['slug'], Project>>((acc, project) => {
- acc[project.slug] = project;
- return acc;
- }, {});
- }, [projects]);
- return (
- <Fragment>
- <TraceStylingWrapper
- ref={r => viewManager.current?.onContainerRef(r)}
- className={trace.type === 'loading' ? 'Loading' : ''}
- style={{
- backgroundColor: '#FFF',
- height: '70vh',
- width: '100%',
- margin: 'auto',
- }}
- >
- <TraceDivider ref={r => viewManager.current?.registerDividerRef(r)} />
- <AutoSizer>
- {({width, height}) => (
- <Fragment>
- {trace.indicators.length > 0
- ? trace.indicators.map((indicator, i) => {
- return (
- <div
- key={i}
- ref={r =>
- viewManager.current?.registerIndicatorRef(r, i, indicator)
- }
- className="TraceIndicator"
- >
- <div className="TraceIndicatorLine" />
- </div>
- );
- })
- : null}
- <List
- ref={r => viewManager.current?.registerVirtualizedList(r)}
- rowHeight={24}
- height={height}
- width={width}
- overscanRowCount={5}
- rowCount={treeRef.current.list.length ?? 0}
- rowRenderer={p => {
- return trace.type === 'loading' ? (
- <RenderPlaceholderRow
- style={p.style}
- node={treeRef.current.list[p.index]}
- index={p.index}
- theme={theme}
- projects={projectLookup}
- viewManager={viewManager.current!}
- startIndex={
- (p.parent as unknown as {_rowStartIndex: number})
- ._rowStartIndex ?? 0
- }
- />
- ) : (
- <RenderRow
- key={p.key}
- theme={theme}
- startIndex={
- (p.parent as unknown as {_rowStartIndex: number})
- ._rowStartIndex ?? 0
- }
- index={p.index}
- style={p.style}
- trace_id={trace_id}
- projects={projectLookup}
- node={treeRef.current.list[p.index]}
- viewManager={viewManager.current!}
- onFetchChildren={handleFetchChildren}
- onExpandNode={handleExpandNode}
- />
- );
- }}
- />
- </Fragment>
- )}
- </AutoSizer>
- </TraceStylingWrapper>
- </Fragment>
- );
- }
- export default Trace;
- const TraceDivider = styled('div')`
- position: absolute;
- height: 100%;
- background-color: transparent;
- top: 0;
- z-index: 10;
- cursor: col-resize;
- &:before {
- content: '';
- position: absolute;
- width: 1px;
- height: 100%;
- background-color: ${p => p.theme.border};
- left: 50%;
- }
- &:hover&:before {
- background-color: ${p => p.theme.purple300};
- }
- `;
- function RenderRow(props: {
- index: number;
- node: TraceTreeNode<TraceTree.NodeValue>;
- onExpandNode: (node: TraceTreeNode<TraceTree.NodeValue>, value: boolean) => void;
- onFetchChildren: (node: TraceTreeNode<TraceTree.NodeValue>, value: boolean) => void;
- projects: Record<Project['slug'], Project>;
- startIndex: number;
- style: React.CSSProperties;
- theme: Theme;
- trace_id: string;
- viewManager: VirtualizedViewManager;
- }) {
- const virtualizedIndex = props.index - props.startIndex;
- if (!props.node.value) {
- return null;
- }
- if (isAutogroupedNode(props.node)) {
- return (
- <div
- className="TraceRow Autogrouped"
- style={{
- top: props.style.top,
- height: props.style.height,
- }}
- >
- <div
- className="TraceLeftColumn"
- ref={r =>
- props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
- }
- style={{
- width: props.viewManager.columns.list.width * 100 + '%',
- }}
- >
- <div
- className="TraceLeftColumnInner"
- style={{
- paddingLeft: props.node.depth * 24,
- }}
- >
- <div className="TraceChildrenCountWrapper">
- <Connectors node={props.node} />
- <ChildrenCountButton
- expanded={!props.node.expanded}
- onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
- >
- {props.node.groupCount}{' '}
- </ChildrenCountButton>
- </div>
- <span className="TraceOperation">{t('Autogrouped')}</span>
- <strong className="TraceEmDash"> — </strong>
- <span className="TraceDescription">{props.node.value.autogrouped_by.op}</span>
- </div>
- </div>
- <div
- className="TraceRightColumn"
- ref={r =>
- props.viewManager.registerColumnRef(
- 'span_list',
- r,
- virtualizedIndex,
- props.node
- )
- }
- style={{
- width: props.viewManager.columns.span_list.width * 100 + '%',
- backgroundColor:
- props.index % 2 ? undefined : props.theme.backgroundSecondary,
- }}
- >
- {isParentAutogroupedNode(props.node) ? (
- <TraceBar
- virtualizedIndex={virtualizedIndex}
- viewManager={props.viewManager}
- color={props.theme.blue300}
- node_space={props.node.space}
- />
- ) : (
- <SiblingAutogroupedBar
- virtualizedIndex={virtualizedIndex}
- viewManager={props.viewManager}
- color={props.theme.blue300}
- node={props.node}
- />
- )}
- </div>
- </div>
- );
- }
- if (isTransactionNode(props.node)) {
- return (
- <div
- className="TraceRow"
- style={{
- top: props.style.top,
- height: props.style.height,
- }}
- >
- <div
- className="TraceLeftColumn"
- ref={r =>
- props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
- }
- style={{
- width: props.viewManager.columns.list.width * 100 + '%',
- }}
- >
- <div
- className="TraceLeftColumnInner"
- style={{
- paddingLeft: props.node.depth * 24,
- }}
- >
- <div
- className={`TraceChildrenCountWrapper ${
- props.node.isOrphaned ? 'Orphaned' : ''
- }`}
- >
- <Connectors node={props.node} />
- {props.node.children.length > 0 ? (
- <ChildrenCountButton
- expanded={props.node.expanded || props.node.zoomedIn}
- onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
- >
- {props.node.children.length}{' '}
- </ChildrenCountButton>
- ) : null}
- </div>
- <ProjectBadge project={props.projects[props.node.value.project_slug]} />
- <span className="TraceOperation">{props.node.value['transaction.op']}</span>
- <strong className="TraceEmDash"> — </strong>
- <span>{props.node.value.transaction}</span>
- {props.node.canFetchData ? (
- <button
- onClick={() => props.onFetchChildren(props.node, !props.node.zoomedIn)}
- >
- {props.node.zoomedIn ? 'Zoom Out' : 'Zoom In'}
- </button>
- ) : null}
- </div>
- </div>
- <div
- ref={r =>
- props.viewManager.registerColumnRef(
- 'span_list',
- r,
- virtualizedIndex,
- props.node
- )
- }
- className="TraceRightColumn"
- style={{
- width: props.viewManager.columns.span_list.width * 100 + '%',
- backgroundColor:
- props.index % 2 ? undefined : props.theme.backgroundSecondary,
- }}
- >
- <TraceBar
- virtualizedIndex={virtualizedIndex}
- viewManager={props.viewManager}
- color={pickBarColor(props.node.value['transaction.op'])}
- node_space={props.node.space}
- />
- </div>
- </div>
- );
- }
- if (isSpanNode(props.node)) {
- return (
- <div
- className="TraceRow"
- style={{
- top: props.style.top,
- height: props.style.height,
- }}
- >
- <div
- className="TraceLeftColumn"
- ref={r =>
- props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
- }
- style={{
- width: props.viewManager.columns.list.width * 100 + '%',
- }}
- >
- <div
- className="TraceLeftColumnInner"
- style={{
- paddingLeft: props.node.depth * 24,
- }}
- >
- <div
- className={`TraceChildrenCountWrapper ${
- props.node.isOrphaned ? 'Orphaned' : ''
- }`}
- >
- <Connectors node={props.node} />
- {props.node.children.length > 0 ? (
- <ChildrenCountButton
- expanded={props.node.expanded || props.node.zoomedIn}
- onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
- >
- {props.node.children.length}{' '}
- </ChildrenCountButton>
- ) : null}
- </div>
- <span className="TraceOperation">{props.node.value.op ?? '<unknown>'}</span>
- <strong className="TraceEmDash"> — </strong>
- <span className="TraceDescription" title={props.node.value.description}>
- {!props.node.value.description
- ? 'unknown'
- : props.node.value.description.length > 100
- ? props.node.value.description.slice(0, 100).trim() + '\u2026'
- : props.node.value.description}
- </span>
- {props.node.canFetchData ? (
- <button
- onClick={() => props.onFetchChildren(props.node, !props.node.zoomedIn)}
- >
- {props.node.zoomedIn ? 'Zoom Out' : 'Zoom In'}
- </button>
- ) : null}
- </div>
- </div>
- <div
- ref={r =>
- props.viewManager.registerColumnRef(
- 'span_list',
- r,
- virtualizedIndex,
- props.node
- )
- }
- className="TraceRightColumn"
- style={{
- width: props.viewManager.columns.span_list.width * 100 + '%',
- backgroundColor:
- props.index % 2 ? undefined : props.theme.backgroundSecondary,
- }}
- >
- <TraceBar
- virtualizedIndex={virtualizedIndex}
- viewManager={props.viewManager}
- color={pickBarColor(props.node.value.op)}
- node_space={props.node.space}
- />
- </div>
- </div>
- );
- }
- if (isMissingInstrumentationNode(props.node)) {
- return (
- <div
- className="TraceRow"
- style={{
- top: props.style.top,
- height: props.style.height,
- }}
- >
- <div
- className="TraceLeftColumn"
- ref={r =>
- props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
- }
- style={{
- width: props.viewManager.columns.list.width * 100 + '%',
- }}
- >
- <div
- className="TraceLeftColumnInner"
- style={{
- paddingLeft: props.node.depth * 24,
- }}
- >
- <div className="TraceChildrenCountWrapper">
- <Connectors node={props.node} />
- </div>
- <span className="TraceOperation">{t('Missing instrumentation')}</span>
- </div>
- </div>
- <div
- ref={r =>
- props.viewManager.registerColumnRef(
- 'span_list',
- r,
- virtualizedIndex,
- props.node
- )
- }
- className="TraceRightColumn"
- style={{
- width: props.viewManager.columns.span_list.width * 100 + '%',
- backgroundColor:
- props.index % 2 ? undefined : props.theme.backgroundSecondary,
- }}
- >
- <TraceBar
- virtualizedIndex={virtualizedIndex}
- viewManager={props.viewManager}
- color={pickBarColor('missing-instrumentation')}
- node_space={props.node.space}
- />
- </div>
- </div>
- );
- }
- if (isTraceNode(props.node)) {
- return (
- <div
- className="TraceRow"
- style={{
- top: props.style.top,
- height: props.style.height,
- }}
- >
- <div
- className="TraceLeftColumn"
- ref={r =>
- props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
- }
- style={{
- width: props.viewManager.columns.list.width * 100 + '%',
- }}
- >
- <div
- className="TraceLeftColumnInner"
- style={{
- paddingLeft: props.node.depth * 24,
- }}
- >
- <div className="TraceChildrenCountWrapper Root">
- <Connectors node={props.node} />
- {props.node.children.length > 0 ? (
- <ChildrenCountButton
- expanded={props.node.expanded || props.node.zoomedIn}
- onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
- >
- {props.node.children.length}{' '}
- </ChildrenCountButton>
- ) : null}
- </div>
- <span className="TraceOperation">{t('Trace')}</span>
- <strong className="TraceEmDash"> — </strong>
- <span className="TraceDescription">{props.trace_id}</span>
- </div>
- </div>
- <div
- ref={r =>
- props.viewManager.registerColumnRef(
- 'span_list',
- r,
- virtualizedIndex,
- props.node
- )
- }
- className="TraceRightColumn"
- style={{
- width: props.viewManager.columns.span_list.width * 100 + '%',
- backgroundColor:
- props.index % 2 ? undefined : props.theme.backgroundSecondary,
- }}
- >
- <TraceBar
- virtualizedIndex={virtualizedIndex}
- viewManager={props.viewManager}
- color={pickBarColor('missing-instrumentation')}
- node_space={props.node.space}
- />
- </div>
- </div>
- );
- }
- if (isTraceErrorNode(props.node)) {
- return (
- <div
- className="TraceRow"
- style={{
- top: props.style.top,
- height: props.style.height,
- }}
- >
- <div
- className="TraceLeftColumn"
- ref={r =>
- props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
- }
- style={{
- width: props.viewManager.columns.list.width * 100 + '%',
- }}
- >
- <div
- className="TraceLeftColumnInner"
- style={{
- paddingLeft: props.node.depth * 24,
- }}
- >
- <div className="TraceChildrenCountWrapper">
- <Connectors node={props.node} />
- {props.node.children.length > 0 ? (
- <ChildrenCountButton
- expanded={props.node.expanded || props.node.zoomedIn}
- onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
- >
- {props.node.children.length}{' '}
- </ChildrenCountButton>
- ) : null}
- </div>
- <span className="TraceOperation">{t('Error')}</span>
- <strong className="TraceEmDash"> — </strong>
- <span className="TraceDescription">{props.node.value.title}</span>
- </div>
- </div>
- <div
- ref={r =>
- props.viewManager.registerColumnRef(
- 'span_list',
- r,
- virtualizedIndex,
- props.node
- )
- }
- className="TraceRightColumn"
- style={{
- width: props.viewManager.columns.span_list.width * 100 + '%',
- backgroundColor:
- props.index % 2 ? undefined : props.theme.backgroundSecondary,
- }}
- >
- {/* @TODO: figure out what to do with trace errors */}
- {/* <TraceBar
- space={props.space}
- start_timestamp={props.node.value.start_timestamp}
- timestamp={props.node.value.timestamp}
- /> */}
- </div>
- </div>
- );
- }
- return null;
- }
- function RenderPlaceholderRow(props: {
- index: number;
- node: TraceTreeNode<TraceTree.NodeValue>;
- projects: Record<Project['slug'], Project>;
- startIndex: number;
- style: React.CSSProperties;
- theme: Theme;
- viewManager: VirtualizedViewManager;
- }) {
- const virtualizedIndex = props.index - props.startIndex;
- return (
- <div
- className="TraceRow"
- style={{
- top: props.style.top,
- height: props.style.height,
- pointerEvents: 'none',
- color: props.theme.subText,
- animationDelay: `${virtualizedIndex * 0.05}s`,
- paddingLeft: space(1),
- }}
- >
- <div
- className="TraceLeftColumn"
- style={{width: props.viewManager.columns.list.width * 100 + '%'}}
- >
- <div
- className="TraceLeftColumnInner"
- style={{
- paddingLeft: props.node.depth * 24,
- }}
- >
- <div className="TraceChildrenCountWrapper">
- <Connectors node={props.node} />
- {props.node.children.length > 0 ? (
- <ChildrenCountButton
- expanded={props.node.expanded || props.node.zoomedIn}
- onClick={() => void 0}
- >
- {props.node.children.length}{' '}
- </ChildrenCountButton>
- ) : null}
- </div>
- {isTraceNode(props.node) ? <SmallLoadingIndicator /> : null}
- {isTraceNode(props.node) ? (
- 'Loading trace...'
- ) : (
- <Placeholder className="Placeholder" height="10px" width="86%" />
- )}
- </div>
- </div>
- <div
- className="TraceRightColumn"
- style={{
- width: props.viewManager.columns.span_list.width * 100 + '%',
- }}
- >
- {isTraceNode(props.node) ? null : (
- <Placeholder
- className="Placeholder"
- height="14px"
- width="90%"
- style={{margin: 'auto'}}
- />
- )}
- </div>
- </div>
- );
- }
- function Connectors(props: {node: TraceTreeNode<TraceTree.NodeValue>}) {
- const showVerticalConnector =
- ((props.node.expanded || props.node.zoomedIn) && props.node.children.length > 0) ||
- (props.node.value && isParentAutogroupedNode(props.node));
- // If the tail node of the collapsed node has no children,
- // we don't want to render the vertical connector as no children
- // are being rendered as the chain is entirely collapsed
- const hideVerticalConnector =
- showVerticalConnector &&
- props.node.value &&
- props.node instanceof ParentAutogroupNode &&
- !props.node.tail.children.length;
- return (
- <Fragment>
- {/*
- @TODO count of rendered connectors could be % 3 as we can
- have up to 3 connectors per node, 1 div, 1 before and 1 after
- */}
- {props.node.connectors.map((c, i) => {
- return (
- <div
- key={i}
- style={{left: -(Math.abs(Math.abs(c) - props.node.depth) * 24)}}
- className={`TraceVerticalConnector ${c < 0 ? 'Orphaned' : ''}`}
- />
- );
- })}
- {showVerticalConnector && !hideVerticalConnector ? (
- <div className="TraceExpandedVerticalConnector" />
- ) : null}
- {props.node.isLastChild ? (
- <div className="TraceVerticalLastChildConnector" />
- ) : (
- <div className="TraceVerticalConnector" />
- )}
- </Fragment>
- );
- }
- function SmallLoadingIndicator() {
- return (
- <StyledLoadingIndicator
- style={{display: 'inline-block', margin: 0}}
- size={8}
- hideMessage
- relative
- />
- );
- }
- const StyledLoadingIndicator = styled(LoadingIndicator)`
- transform: translate(-5px, 0);
- div:first-child {
- border-left: 6px solid ${p => p.theme.gray300};
- animation: loading 900ms infinite linear;
- }
- `;
- function ProjectBadge(props: {project: Project}) {
- return <ProjectAvatar project={props.project} />;
- }
- function ChildrenCountButton(props: {
- children: React.ReactNode;
- expanded: boolean;
- onClick: () => void;
- }) {
- return (
- <button className="TraceChildrenCount" onClick={props.onClick}>
- {props.children}
- <IconChevron
- size="xs"
- direction={props.expanded ? 'up' : 'down'}
- style={{marginLeft: 2}}
- />
- </button>
- );
- }
- interface TraceBarProps {
- color: string;
- node_space: [number, number] | null;
- viewManager: VirtualizedViewManager;
- virtualizedIndex: number;
- duration?: number;
- }
- type SiblingAutogroupedBarProps = Omit<TraceBarProps, 'node_space' | 'duration'> & {
- node: TraceTreeNode<TraceTree.NodeValue>;
- };
- // Render collapsed representation of sibling autogrouping, using multiple bars for when
- // there are gaps between siblings.
- function SiblingAutogroupedBar(props: SiblingAutogroupedBarProps) {
- const bars: React.ReactNode[] = [];
- // Start and end represents the earliest start_timestamp and the latest
- // end_timestamp for a set of overlapping siblings.
- let start = isSpanNode(props.node.children[0])
- ? props.node.children[0].value.start_timestamp
- : Number.POSITIVE_INFINITY;
- let end = isSpanNode(props.node.children[0])
- ? props.node.children[0].value.timestamp
- : Number.NEGATIVE_INFINITY;
- let totalDuration = 0;
- for (let i = 0; i < props.node.children.length; i++) {
- const node = props.node.children[i];
- if (!isSpanNode(node)) {
- throw new TypeError('Invalid type of autogrouped child');
- }
- const hasGap = node.value.start_timestamp > end;
- if (!(hasGap || node.isLastChild)) {
- start = Math.min(start, node.value.start_timestamp);
- end = Math.max(end, node.value.timestamp);
- continue;
- }
- // Render a bar for already collapsed set.
- totalDuration += end - start;
- bars.push(
- <TraceBar
- virtualizedIndex={props.virtualizedIndex}
- viewManager={props.viewManager}
- color={props.color}
- node_space={[start, end - start]}
- duration={!hasGap ? totalDuration : undefined}
- />
- );
- if (hasGap) {
- // Start a new set.
- start = node.value.start_timestamp;
- end = node.value.timestamp;
- // Render a bar if the sibling with a gap is the last sibling.
- if (node.isLastChild) {
- totalDuration += end - start;
- bars.push(
- <TraceBar
- virtualizedIndex={props.virtualizedIndex}
- viewManager={props.viewManager}
- color={props.color}
- duration={totalDuration}
- node_space={[start, end - start]}
- />
- );
- }
- }
- }
- return <Fragment>{bars}</Fragment>;
- }
- function TraceBar(props: TraceBarProps) {
- if (!props.node_space) {
- return null;
- }
- const spanTransform = props.viewManager.computeSpanMatrixTransform(props.node_space);
- const inverseTransform = props.viewManager.inverseSpanScaling(props.node_space);
- const textPosition = props.viewManager.computeSpanTextPlacement(
- spanTransform[4],
- props.node_space
- );
- return (
- <div
- ref={r =>
- props.viewManager.registerSpanBarRef(r, props.node_space!, props.virtualizedIndex)
- }
- className="TraceBar"
- style={{
- transform: `matrix(${spanTransform.join(',')})`,
- backgroundColor: props.color,
- }}
- >
- <div
- className={`TraceBarDuration ${textPosition === 'inside left' ? 'Inside' : ''}`}
- style={{
- left: textPosition === 'left' || textPosition === 'inside left' ? '0' : '100%',
- transform: `matrix(${inverseTransform}, 0,0,1,0,0) translate(${
- textPosition === 'left' ? 'calc(-100% - 4px)' : '4px'
- }, 0)`,
- }}
- >
- {/* Use node space to calculate duration if the duration prop is not provided. */}
- <PerformanceDuration
- seconds={props.duration ?? props.node_space[1]}
- abbreviation
- />
- </div>
- </div>
- );
- }
- /**
- * This is a wrapper around the Trace component to apply styles
- * to the trace tree. It exists because we _do not_ want to trigger
- * emotion's css parsing logic as it is very slow and will cause
- * the scrolling to flicker.
- */
- const TraceStylingWrapper = styled('div')`
- position: relative;
- border: 1px solid ${p => p.theme.border};
- padding: ${space(0.5)} 0;
- border-radius: ${space(0.5)};
- @keyframes show {
- 0% {
- opacity: 0;
- transform: translate(0, 2px);
- }
- 100% {
- opacity: 0.7;
- transform: translate(0, 0px);
- }
- }
- @keyframes showPlaceholder {
- 0% {
- opacity: 0;
- transform: translate(-8px, 0px);
- }
- 100% {
- opacity: 0.7;
- transform: translate(0, 0px);
- }
- }
- .TraceIndicator {
- z-index: 1;
- width: 3px;
- height: 100%;
- top: 0;
- position: absolute;
- .TraceIndicatorLine {
- width: 1px;
- height: 100%;
- position: absolute;
- left: 50%;
- transform: translateX(-50%);
- background: repeating-linear-gradient(
- to bottom,
- transparent 0 4px,
- ${p => p.theme.textColor} 4px 8px
- )
- 80%/2px 100% no-repeat;
- }
- }
- &.Loading {
- .TraceRow {
- opacity: 0;
- animation: show 0.2s ease-in-out forwards;
- }
- .Placeholder {
- opacity: 0;
- transform: translate(-8px, 0px);
- animation: showPlaceholder 0.2s ease-in-out forwards;
- }
- }
- .TraceRow {
- display: flex;
- align-items: center;
- position: absolute;
- width: 100%;
- transition: background-color 0.15s ease-in-out 0s;
- font-size: ${p => p.theme.fontSizeSmall};
- &:hover {
- background-color: ${p => p.theme.backgroundSecondary};
- }
- &.Autogrouped {
- color: ${p => p.theme.blue300};
- .TraceDescription {
- font-weight: bold;
- }
- .TraceChildrenCountWrapper {
- button {
- color: ${p => p.theme.white};
- background-color: ${p => p.theme.blue300};
- }
- }
- }
- }
- .TraceLeftColumn {
- height: 100%;
- white-space: nowrap;
- display: flex;
- align-items: center;
- overflow: hidden;
- will-change: width;
- .TraceLeftColumnInner {
- height: 100%;
- white-space: nowrap;
- display: flex;
- align-items: center;
- will-change: transform;
- transform-origin: left center;
- transform: translateX(var(--column-translate-x));
- }
- }
- .TraceRightColumn {
- height: 100%;
- position: relative;
- display: flex;
- align-items: center;
- will-change: width;
- z-index: 1;
- }
- .TraceBar {
- position: absolute;
- height: 64%;
- width: 100%;
- background-color: black;
- transform-origin: left center;
- }
- .TraceBarDuration {
- display: inline-block;
- transform-origin: left center;
- font-size: ${p => p.theme.fontSizeExtraSmall};
- color: ${p => p.theme.gray300};
- white-space: nowrap;
- font-variant-numeric: tabular-nums;
- position: absolute;
- &.Inside {
- color: ${p => p.theme.gray100};
- }
- }
- .TraceChildrenCount {
- height: 16px;
- white-space: nowrap;
- min-width: 30px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 99px;
- padding: 0px ${space(0.5)};
- transition: all 0.15s ease-in-out;
- background: ${p => p.theme.background};
- border: 2px solid ${p => p.theme.border};
- line-height: 0;
- z-index: 1;
- font-size: 10px;
- box-shadow: ${p => p.theme.dropShadowLight};
- margin-right: ${space(1)};
- svg {
- width: 7px;
- transition: none;
- }
- }
- .TraceChildrenCountWrapper {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- min-width: 48px;
- height: 100%;
- position: relative;
- button {
- transition: none;
- }
- &.Orphaned {
- .TraceVerticalConnector,
- .TraceVerticalLastChildConnector,
- .TraceExpandedVerticalConnector {
- border-left: 2px dashed ${p => p.theme.border};
- }
- &::before {
- border-bottom: 2px dashed ${p => p.theme.border};
- }
- }
- &.Root {
- &:before,
- .TraceVerticalLastChildConnector {
- visibility: hidden;
- }
- }
- &::before {
- content: '';
- display: block;
- width: 60%;
- height: 2px;
- border-bottom: 2px solid ${p => p.theme.border};
- position: absolute;
- left: 0;
- top: 50%;
- transform: translateY(-50%);
- }
- &::after {
- content: '';
- background-color: rgb(224, 220, 229);
- border-radius: 50%;
- height: 6px;
- width: 6px;
- position: absolute;
- left: 60%;
- top: 50%;
- transform: translateY(-50%);
- }
- }
- .TraceVerticalConnector {
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- height: 100%;
- width: 2px;
- border-left: 2px solid ${p => p.theme.border};
- &.Orphaned {
- border-left: 2px dashed ${p => p.theme.border};
- }
- }
- .TraceVerticalLastChildConnector {
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- height: 50%;
- width: 2px;
- border-left: 2px solid ${p => p.theme.border};
- border-bottom-left-radius: 4px;
- }
- .TraceExpandedVerticalConnector {
- position: absolute;
- bottom: 0;
- height: 50%;
- left: 50%;
- width: 2px;
- border-left: 2px solid ${p => p.theme.border};
- }
- .TraceOperation {
- margin-left: ${space(0.5)};
- text-overflow: ellipsis;
- white-space: nowrap;
- font-weight: bold;
- }
- .TraceEmDash {
- margin-left: ${space(0.5)};
- margin-right: ${space(0.5)};
- }
- .TraceDescription {
- white-space: nowrap;
- }
- `;
|