virtualizedViewManager.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829
  1. import type {List} from 'react-virtualized';
  2. import * as Sentry from '@sentry/react';
  3. import {mat3, vec2} from 'gl-matrix';
  4. import type {Client} from 'sentry/api';
  5. import type {Organization} from 'sentry/types';
  6. import clamp from 'sentry/utils/number/clamp';
  7. import {
  8. isAutogroupedNode,
  9. isParentAutogroupedNode,
  10. isSiblingAutogroupedNode,
  11. isSpanNode,
  12. isTransactionNode,
  13. } from 'sentry/views/performance/newTraceDetails/guards';
  14. import {
  15. type TraceTree,
  16. TraceTreeNode,
  17. } from 'sentry/views/performance/newTraceDetails/traceTree';
  18. const DIVIDER_WIDTH = 6;
  19. function easeOutQuad(x: number): number {
  20. return 1 - (1 - x) * (1 - x);
  21. }
  22. function onPreventBackForwardNavigation(event: WheelEvent) {
  23. if (event.deltaX !== 0) {
  24. event.preventDefault();
  25. }
  26. }
  27. type ViewColumn = {
  28. column_nodes: TraceTreeNode<TraceTree.NodeValue>[];
  29. column_refs: (HTMLElement | undefined)[];
  30. translate: [number, number];
  31. width: number;
  32. };
  33. class View {
  34. public x: number;
  35. public y: number;
  36. public width: number;
  37. public height: number;
  38. constructor(x: number, y: number, width: number, height: number) {
  39. this.x = x;
  40. this.y = y;
  41. this.width = width;
  42. this.height = height;
  43. }
  44. static From(view: View): View {
  45. return new View(view.x, view.y, view.width, view.height);
  46. }
  47. static Empty(): View {
  48. return new View(0, 0, 1000, 1);
  49. }
  50. serialize() {
  51. return [this.x, this.y, this.width, this.height];
  52. }
  53. between(to: View): mat3 {
  54. return mat3.fromValues(
  55. to.width / this.width,
  56. 0,
  57. 0,
  58. to.height / this.height,
  59. 0,
  60. 0,
  61. to.x - this.x * (to.width / this.width),
  62. to.y - this.y * (to.height / this.height),
  63. 1
  64. );
  65. }
  66. transform(mat: mat3): [number, number, number, number] {
  67. const x = this.x * mat[0] + this.y * mat[3] + mat[6];
  68. const y = this.x * mat[1] + this.y * mat[4] + mat[7];
  69. const width = this.width * mat[0] + this.height * mat[3];
  70. const height = this.width * mat[1] + this.height * mat[4];
  71. return [x, y, width, height];
  72. }
  73. get center() {
  74. return this.x + this.width / 2;
  75. }
  76. get left() {
  77. return this.x;
  78. }
  79. get right() {
  80. return this.x + this.width;
  81. }
  82. get top() {
  83. return this.y;
  84. }
  85. get bottom() {
  86. return this.y + this.height;
  87. }
  88. }
  89. /**
  90. * Tracks the state of the virtualized view and manages the resizing of the columns.
  91. * Children components should call the appropriate register*Ref methods to register their
  92. * HTML elements.
  93. */
  94. export class VirtualizedViewManager {
  95. // Represents the space of the entire trace, for example
  96. // a trace starting at 0 and ending at 1000 would have a space of [0, 1000]
  97. to_origin: number = 0;
  98. trace_space: View = View.Empty();
  99. // The view defines what the user is currently looking at, it is a subset
  100. // of the trace space. For example, if the user is currently looking at the
  101. // trace from 500 to 1000, the view would be represented by [x, width] = [500, 500]
  102. trace_view: View = View.Empty();
  103. // Represents the pixel space of the entire trace - this is the container
  104. // that we render to. For example, if the container is 1000px wide, the
  105. // pixel space would be [0, 1000]
  106. trace_physical_space: View = View.Empty();
  107. container_physical_space: View = View.Empty();
  108. measurer: RowMeasurer = new RowMeasurer();
  109. resizeObserver: ResizeObserver | null = null;
  110. virtualizedList: List | null = null;
  111. // HTML refs that we need to keep track of such
  112. // that rendering can be done programmatically
  113. divider: HTMLElement | null = null;
  114. container: HTMLElement | null = null;
  115. indicators: ({indicator: TraceTree['indicators'][0]; ref: HTMLElement} | undefined)[] =
  116. [];
  117. span_bars: ({ref: HTMLElement; space: [number, number]} | undefined)[] = [];
  118. // Column configuration
  119. columns: {
  120. list: ViewColumn;
  121. span_list: ViewColumn;
  122. };
  123. constructor(columns: {
  124. list: Pick<ViewColumn, 'width'>;
  125. span_list: Pick<ViewColumn, 'width'>;
  126. }) {
  127. this.columns = {
  128. list: {...columns.list, column_nodes: [], column_refs: [], translate: [0, 0]},
  129. span_list: {
  130. ...columns.span_list,
  131. column_nodes: [],
  132. column_refs: [],
  133. translate: [0, 0],
  134. },
  135. };
  136. this.onDividerMouseDown = this.onDividerMouseDown.bind(this);
  137. this.onDividerMouseUp = this.onDividerMouseUp.bind(this);
  138. this.onDividerMouseMove = this.onDividerMouseMove.bind(this);
  139. this.onSyncedScrollbarScroll = this.onSyncedScrollbarScroll.bind(this);
  140. this.onWheelZoom = this.onWheelZoom.bind(this);
  141. }
  142. initializeTraceSpace(space: [x: number, y: number, width: number, height: number]) {
  143. this.to_origin = space[0];
  144. this.trace_space = new View(0, 0, space[2], space[3]);
  145. this.trace_view = new View(0, 0, space[2], space[3]);
  146. }
  147. initializePhysicalSpace(width: number, height: number) {
  148. this.container_physical_space = new View(0, 0, width, height);
  149. this.trace_physical_space = new View(
  150. 0,
  151. 0,
  152. width * this.columns.span_list.width,
  153. height
  154. );
  155. }
  156. onContainerRef(container: HTMLElement | null) {
  157. if (container) {
  158. this.initialize(container);
  159. } else {
  160. this.teardown();
  161. }
  162. }
  163. dividerStartVec: [number, number] | null = null;
  164. onDividerMouseDown(event: MouseEvent) {
  165. if (!this.container) {
  166. return;
  167. }
  168. this.dividerStartVec = [event.clientX, event.clientY];
  169. this.container.style.userSelect = 'none';
  170. this.container.addEventListener('mouseup', this.onDividerMouseUp, {passive: true});
  171. this.container.addEventListener('mousemove', this.onDividerMouseMove, {
  172. passive: true,
  173. });
  174. }
  175. onDividerMouseUp(event: MouseEvent) {
  176. if (!this.container || !this.dividerStartVec) {
  177. return;
  178. }
  179. const distance = event.clientX - this.dividerStartVec[0];
  180. const distancePercentage = distance / this.container_physical_space.width;
  181. this.columns.list.width = this.columns.list.width + distancePercentage;
  182. this.columns.span_list.width = this.columns.span_list.width - distancePercentage;
  183. this.container.style.userSelect = 'auto';
  184. this.dividerStartVec = null;
  185. this.container.removeEventListener('mouseup', this.onDividerMouseUp);
  186. this.container.removeEventListener('mousemove', this.onDividerMouseMove);
  187. }
  188. onDividerMouseMove(event: MouseEvent) {
  189. if (!this.dividerStartVec || !this.divider) {
  190. return;
  191. }
  192. const distance = event.clientX - this.dividerStartVec[0];
  193. const distancePercentage = distance / this.container_physical_space.width;
  194. this.trace_physical_space.width =
  195. (this.columns.span_list.width - distancePercentage) *
  196. this.container_physical_space.width;
  197. this.draw({
  198. list: this.columns.list.width + distancePercentage,
  199. span_list: this.columns.span_list.width - distancePercentage,
  200. });
  201. }
  202. registerVirtualizedList(list: List | null) {
  203. this.virtualizedList = list;
  204. }
  205. registerDividerRef(ref: HTMLElement | null) {
  206. if (!ref) {
  207. if (this.divider) {
  208. this.divider.removeEventListener('mousedown', this.onDividerMouseDown);
  209. }
  210. this.divider = null;
  211. return;
  212. }
  213. this.divider = ref;
  214. this.divider.style.width = `${DIVIDER_WIDTH}px`;
  215. ref.addEventListener('mousedown', this.onDividerMouseDown, {passive: true});
  216. }
  217. registerSpanBarRef(ref: HTMLElement | null, space: [number, number], index: number) {
  218. this.span_bars[index] = ref ? {ref, space} : undefined;
  219. }
  220. registerColumnRef(
  221. column: string,
  222. ref: HTMLElement | null,
  223. index: number,
  224. node: TraceTreeNode<any>
  225. ) {
  226. if (!this.columns[column]) {
  227. throw new TypeError('Invalid column');
  228. }
  229. if (typeof index !== 'number' || isNaN(index)) {
  230. throw new TypeError('Invalid index');
  231. }
  232. if (column === 'list') {
  233. const element = this.columns[column].column_refs[index];
  234. if (ref === undefined && element) {
  235. element.removeEventListener('wheel', this.onSyncedScrollbarScroll);
  236. } else if (ref) {
  237. const scrollableElement = ref.children[0];
  238. if (scrollableElement) {
  239. this.measurer.measure(node, scrollableElement as HTMLElement);
  240. ref.addEventListener('wheel', this.onSyncedScrollbarScroll, {passive: true});
  241. }
  242. }
  243. }
  244. if (column === 'span_list') {
  245. const element = this.columns[column].column_refs[index];
  246. if (ref === undefined && element) {
  247. element.removeEventListener('wheel', this.onWheelZoom);
  248. } else if (ref) {
  249. ref.addEventListener('wheel', this.onWheelZoom, {passive: false});
  250. }
  251. }
  252. this.columns[column].column_refs[index] = ref ?? undefined;
  253. this.columns[column].column_nodes[index] = node ?? undefined;
  254. }
  255. registerIndicatorRef(
  256. ref: HTMLElement | null,
  257. index: number,
  258. indicator: TraceTree['indicators'][0]
  259. ) {
  260. if (!ref) {
  261. this.indicators[index] = undefined;
  262. } else {
  263. this.indicators[index] = {ref, indicator};
  264. }
  265. if (ref) {
  266. ref.style.left = this.columns.list.width * 100 + '%';
  267. ref.style.transform = `translateX(${this.computeTransformXFromTimestamp(
  268. indicator.start
  269. )}px)`;
  270. }
  271. }
  272. getConfigSpaceCursor(cursor: {x: number; y: number}): [number, number] {
  273. const left_percentage = cursor.x / this.trace_physical_space.width;
  274. const left_view = left_percentage * this.trace_view.width;
  275. return [this.trace_view.x + left_view, 0];
  276. }
  277. onWheelZoom(event: WheelEvent) {
  278. if (event.metaKey) {
  279. event.preventDefault();
  280. if (!this.onWheelEndRaf) {
  281. this.onWheelStart();
  282. this.enqueueOnWheelEndRaf();
  283. return;
  284. }
  285. const scale = 1 - event.deltaY * 0.01 * -1; // -1 to invert scale
  286. const configSpaceCursor = this.getConfigSpaceCursor({
  287. x: event.offsetX,
  288. y: event.offsetY,
  289. });
  290. const center = vec2.fromValues(configSpaceCursor[0], 0);
  291. const centerScaleMatrix = mat3.create();
  292. mat3.fromTranslation(centerScaleMatrix, center);
  293. mat3.scale(centerScaleMatrix, centerScaleMatrix, vec2.fromValues(scale, 1));
  294. mat3.translate(
  295. centerScaleMatrix,
  296. centerScaleMatrix,
  297. vec2.fromValues(-center[0], 0)
  298. );
  299. const newView = this.trace_view.transform(centerScaleMatrix);
  300. this.setTraceView({
  301. x: newView[0],
  302. width: newView[2],
  303. });
  304. this.draw();
  305. } else {
  306. const physical_delta_pct = event.deltaX / this.trace_physical_space.width;
  307. const view_delta = physical_delta_pct * this.trace_view.width;
  308. this.setTraceView({
  309. x: this.trace_view.x + view_delta,
  310. });
  311. this.draw();
  312. }
  313. }
  314. onWheelEndRaf: number | null = null;
  315. enqueueOnWheelEndRaf() {
  316. if (this.onWheelEndRaf !== null) {
  317. window.cancelAnimationFrame(this.onWheelEndRaf);
  318. }
  319. const start = performance.now();
  320. const rafCallback = (now: number) => {
  321. const elapsed = now - start;
  322. if (elapsed > 100) {
  323. this.onWheelEnd();
  324. } else {
  325. this.onWheelEndRaf = window.requestAnimationFrame(rafCallback);
  326. }
  327. };
  328. this.onWheelEndRaf = window.requestAnimationFrame(rafCallback);
  329. }
  330. onWheelStart() {
  331. for (let i = 0; i < this.columns.span_list.column_refs.length; i++) {
  332. const span_list = this.columns.span_list.column_refs[i];
  333. if (span_list?.children?.[0]) {
  334. (span_list.children[0] as HTMLElement).style.pointerEvents = 'none';
  335. }
  336. }
  337. }
  338. onWheelEnd() {
  339. this.onWheelEndRaf = null;
  340. for (let i = 0; i < this.columns.span_list.column_refs.length; i++) {
  341. const span_list = this.columns.span_list.column_refs[i];
  342. if (span_list?.children?.[0]) {
  343. (span_list.children[0] as HTMLElement).style.pointerEvents = 'auto';
  344. }
  345. }
  346. }
  347. setTraceView(view: {width?: number; x?: number}) {
  348. const x = view.x ?? this.trace_view.x;
  349. const width = view.width ?? this.trace_view.width;
  350. this.trace_view.x = clamp(x, 0, this.trace_space.width - width);
  351. this.trace_view.width = clamp(width, 0, this.trace_space.width);
  352. }
  353. scrollSyncRaf: number | null = null;
  354. onSyncedScrollbarScroll(event: WheelEvent) {
  355. if (this.bringRowIntoViewAnimation !== null) {
  356. window.cancelAnimationFrame(this.bringRowIntoViewAnimation);
  357. this.bringRowIntoViewAnimation = null;
  358. }
  359. this.enqueueOnScrollEndOutOfBoundsCheck();
  360. const columnWidth = this.columns.list.width * this.container_physical_space.width;
  361. this.columns.list.translate[0] = clamp(
  362. this.columns.list.translate[0] - event.deltaX,
  363. -(this.measurer.max - columnWidth + 16), // 16px margin so we dont scroll right to the last px
  364. 0
  365. );
  366. for (let i = 0; i < this.columns.list.column_refs.length; i++) {
  367. const list = this.columns.list.column_refs[i];
  368. if (list?.children?.[0]) {
  369. (list.children[0] as HTMLElement).style.transform =
  370. `translateX(${this.columns.list.translate[0]}px)`;
  371. }
  372. }
  373. // Eventually sync the column translation to the container
  374. if (this.scrollSyncRaf) {
  375. window.cancelAnimationFrame(this.scrollSyncRaf);
  376. }
  377. this.scrollSyncRaf = window.requestAnimationFrame(() => {
  378. // @TODO if user is outside of the container, scroll the container to the left
  379. this.container?.style.setProperty(
  380. '--column-translate-x',
  381. this.columns.list.translate[0] + 'px'
  382. );
  383. });
  384. }
  385. scrollEndSyncRaf: number | null = null;
  386. enqueueOnScrollEndOutOfBoundsCheck() {
  387. if (this.scrollEndSyncRaf !== null) {
  388. window.cancelAnimationFrame(this.scrollEndSyncRaf);
  389. }
  390. const start = performance.now();
  391. const rafCallback = (now: number) => {
  392. const elapsed = now - start;
  393. if (elapsed > 300) {
  394. this.onScrollEndOutOfBoundsCheck();
  395. } else {
  396. this.scrollEndSyncRaf = window.requestAnimationFrame(rafCallback);
  397. }
  398. };
  399. this.scrollEndSyncRaf = window.requestAnimationFrame(rafCallback);
  400. }
  401. onScrollEndOutOfBoundsCheck() {
  402. this.scrollEndSyncRaf = null;
  403. const translation = this.columns.list.translate[0];
  404. let min = Number.POSITIVE_INFINITY;
  405. let max = Number.NEGATIVE_INFINITY;
  406. let innerMostNode: TraceTreeNode<any> | undefined;
  407. const offset = this.virtualizedList?.Grid?.props.overscanRowCount ?? 0;
  408. const renderCount = this.columns.span_list.column_refs.length;
  409. for (let i = offset + 1; i < renderCount - offset; i++) {
  410. const width = this.measurer.cache.get(this.columns.list.column_nodes[i]);
  411. if (width === undefined) {
  412. // this is unlikely to happen, but we should trigger a sync measure event if it does
  413. continue;
  414. }
  415. min = Math.min(min, width);
  416. max = Math.max(max, width);
  417. innerMostNode =
  418. !innerMostNode || this.columns.list.column_nodes[i].depth < innerMostNode.depth
  419. ? this.columns.list.column_nodes[i]
  420. : innerMostNode;
  421. }
  422. if (innerMostNode) {
  423. if (translation + max < 0) {
  424. this.scrollRowIntoViewHorizontally(innerMostNode);
  425. } else if (
  426. translation + innerMostNode.depth * 24 >
  427. this.columns.list.width * this.container_physical_space.width
  428. ) {
  429. this.scrollRowIntoViewHorizontally(innerMostNode);
  430. }
  431. }
  432. }
  433. scrollRowIntoViewHorizontally(node: TraceTreeNode<any>, duration: number = 600) {
  434. const VISUAL_OFFSET = 24 / 2;
  435. const target = Math.min(-node.depth * 24 + VISUAL_OFFSET, 0);
  436. this.animateScrollColumnTo(target, duration);
  437. }
  438. bringRowIntoViewAnimation: number | null = null;
  439. animateScrollColumnTo(x: number, duration: number) {
  440. const start = performance.now();
  441. const startPosition = this.columns.list.translate[0];
  442. const distance = x - startPosition;
  443. const animate = (now: number) => {
  444. const elapsed = now - start;
  445. const progress = duration > 0 ? elapsed / duration : 1;
  446. const eased = easeOutQuad(progress);
  447. const pos = startPosition + distance * eased;
  448. for (let i = 0; i < this.columns.list.column_refs.length; i++) {
  449. const list = this.columns.list.column_refs[i];
  450. if (list?.children?.[0]) {
  451. (list.children[0] as HTMLElement).style.transform = `translateX(${pos}px)`;
  452. }
  453. }
  454. if (progress < 1) {
  455. this.columns.list.translate[0] = pos;
  456. this.bringRowIntoViewAnimation = window.requestAnimationFrame(animate);
  457. } else {
  458. this.columns.list.translate[0] = x;
  459. }
  460. };
  461. this.bringRowIntoViewAnimation = window.requestAnimationFrame(animate);
  462. }
  463. initialize(container: HTMLElement) {
  464. if (this.container !== container && this.resizeObserver !== null) {
  465. this.teardown();
  466. }
  467. this.container = container;
  468. this.container.addEventListener('wheel', onPreventBackForwardNavigation, {
  469. passive: false,
  470. });
  471. this.resizeObserver = new ResizeObserver(entries => {
  472. const entry = entries[0];
  473. if (!entry) {
  474. throw new Error('ResizeObserver entry is undefined');
  475. }
  476. this.initializePhysicalSpace(entry.contentRect.width, entry.contentRect.height);
  477. this.draw();
  478. });
  479. this.resizeObserver.observe(container);
  480. }
  481. computeSpanCSSMatrixTransform(
  482. space: [number, number]
  483. ): [number, number, number, number, number, number] {
  484. const scale = space[1] / this.trace_view.width;
  485. const traceViewToSpace = this.trace_space.between(this.trace_view);
  486. const tracePhysicalToView = this.trace_physical_space.between(this.trace_space);
  487. const to_px = mat3.multiply(mat3.create(), traceViewToSpace, tracePhysicalToView);
  488. return [
  489. Math.max(scale, (1 * to_px[0]) / this.trace_view.width),
  490. 0,
  491. 0,
  492. 1,
  493. (space[0] - this.to_origin) / to_px[0] - this.trace_view.x / to_px[0],
  494. 0,
  495. ];
  496. }
  497. scrollToPath(
  498. tree: TraceTree,
  499. scrollQueue: TraceTree.NodePath[],
  500. rerender: () => void,
  501. {api, organization}: {api: Client; organization: Organization}
  502. ): Promise<TraceTreeNode<TraceTree.NodeValue> | null> {
  503. const segments = [...scrollQueue];
  504. const virtualizedList = this.virtualizedList;
  505. if (!virtualizedList) {
  506. return Promise.resolve(null);
  507. }
  508. // Keep parent reference as we traverse the tree so that we can only
  509. // perform searching in the current level and not the entire tree
  510. let parent: TraceTreeNode<TraceTree.NodeValue> = tree.root;
  511. const scrollToRow = async (): Promise<TraceTreeNode<TraceTree.NodeValue> | null> => {
  512. const path = segments.pop();
  513. const current = findInTreeFromSegment(parent, path!);
  514. if (!current) {
  515. Sentry.captureMessage('Failed to scroll to node in trace tree');
  516. return null;
  517. }
  518. // Reassing the parent to the current node
  519. parent = current;
  520. if (isTransactionNode(current)) {
  521. const nextSegment = segments[segments.length - 1];
  522. if (nextSegment?.startsWith('span:') || nextSegment?.startsWith('ag:')) {
  523. await tree.zoomIn(current, true, {
  524. api,
  525. organization,
  526. });
  527. return scrollToRow();
  528. }
  529. }
  530. if (isAutogroupedNode(current) && segments.length > 0) {
  531. tree.expand(current, true);
  532. return scrollToRow();
  533. }
  534. if (segments.length > 0) {
  535. return scrollToRow();
  536. }
  537. // We are at the last path segment (the node that the user clicked on)
  538. // and we should scroll the view to this node.
  539. const index = tree.list.findIndex(node => node === current);
  540. if (index === -1) {
  541. throw new Error("Couldn't find node in list");
  542. }
  543. rerender();
  544. virtualizedList.scrollToRow(index);
  545. return current;
  546. };
  547. return scrollToRow();
  548. }
  549. computeTransformXFromTimestamp(timestamp: number): number {
  550. const traceViewToSpace = this.trace_space.between(this.trace_view);
  551. const tracePhysicalToView = this.trace_physical_space.between(this.trace_space);
  552. const to_px = mat3.multiply(mat3.create(), traceViewToSpace, tracePhysicalToView);
  553. return (timestamp - this.to_origin) / to_px[0];
  554. }
  555. computeSpanTextPlacement(
  556. translateX: number,
  557. span_space: [number, number]
  558. ): 'left' | 'right' | 'inside left' {
  559. // | <--> | |
  560. // | | <--> |
  561. // | <--------> |
  562. // | | |
  563. // | | |
  564. const traceViewToSpace = this.trace_space.between(this.trace_view);
  565. const tracePhysicalToView = this.trace_physical_space.between(this.trace_space);
  566. const to_px = mat3.multiply(mat3.create(), traceViewToSpace, tracePhysicalToView);
  567. const half = this.trace_physical_space.width / 2;
  568. const spanWidth = span_space[1] / to_px[0];
  569. if (translateX > half) {
  570. return 'left';
  571. }
  572. if (spanWidth > half) {
  573. return 'inside left';
  574. }
  575. return 'right';
  576. }
  577. draw(options: {list?: number; span_list?: number} = {}) {
  578. const list_width = options.list ?? this.columns.list.width;
  579. const span_list_width = options.span_list ?? this.columns.span_list.width;
  580. if (this.divider) {
  581. this.divider.style.transform = `translateX(${
  582. list_width * this.container_physical_space.width - DIVIDER_WIDTH / 2
  583. }px)`;
  584. }
  585. const listWidth = list_width * 100 + '%';
  586. const spanWidth = span_list_width * 100 + '%';
  587. for (let i = 0; i < this.columns.list.column_refs.length; i++) {
  588. const list = this.columns.list.column_refs[i];
  589. if (list) list.style.width = listWidth;
  590. const span = this.columns.span_list.column_refs[i];
  591. if (span) span.style.width = spanWidth;
  592. const span_bar = this.span_bars[i];
  593. if (span_bar) {
  594. const span_transform = this.computeSpanCSSMatrixTransform(span_bar.space);
  595. span_bar.ref.style.transform = `matrix(${span_transform.join(',')}`;
  596. const duration = span_bar.ref.children[0];
  597. if (duration) {
  598. const text_placement = this.computeSpanTextPlacement(
  599. span_transform[4],
  600. span_bar.space
  601. );
  602. (duration as HTMLElement).style.transform = `scaleX(${
  603. 1 / span_transform[0]
  604. }) translate(${text_placement === 'left' ? 'calc(-100% - 4px)' : '4px'}, 0)`;
  605. }
  606. }
  607. }
  608. for (let i = 0; i < this.indicators.length; i++) {
  609. const entry = this.indicators[i];
  610. if (!entry) {
  611. continue;
  612. }
  613. entry.ref.style.left = listWidth;
  614. entry.ref.style.transform = `translateX(${this.computeTransformXFromTimestamp(
  615. entry.indicator.start
  616. )}px)`;
  617. }
  618. }
  619. teardown() {
  620. if (this.container) {
  621. this.container.removeEventListener('wheel', onPreventBackForwardNavigation);
  622. }
  623. if (this.resizeObserver) {
  624. this.resizeObserver.disconnect();
  625. }
  626. }
  627. }
  628. class RowMeasurer {
  629. cache: Map<TraceTreeNode<any>, number> = new Map();
  630. elements: HTMLElement[] = [];
  631. measureQueue: [TraceTreeNode<any>, HTMLElement][] = [];
  632. drainRaf: number | null = null;
  633. max: number = 0;
  634. constructor() {
  635. this.drain = this.drain.bind(this);
  636. }
  637. enqueueMeasure(node: TraceTreeNode<any>, element: HTMLElement) {
  638. if (this.cache.has(node)) {
  639. return;
  640. }
  641. this.measureQueue.push([node, element]);
  642. if (this.drainRaf !== null) {
  643. window.cancelAnimationFrame(this.drainRaf);
  644. }
  645. this.drainRaf = window.requestAnimationFrame(this.drain);
  646. }
  647. drain() {
  648. for (const [node, element] of this.measureQueue) {
  649. this.measure(node, element);
  650. }
  651. }
  652. measure(node: TraceTreeNode<any>, element: HTMLElement): number {
  653. const cache = this.cache.get(node);
  654. if (cache !== undefined) {
  655. return cache;
  656. }
  657. const width = element.getBoundingClientRect().width;
  658. if (width > this.max) {
  659. this.max = width;
  660. }
  661. this.cache.set(node, width);
  662. return width;
  663. }
  664. }
  665. function findInTreeFromSegment(
  666. start: TraceTreeNode<TraceTree.NodeValue>,
  667. segment: TraceTree.NodePath
  668. ): TraceTreeNode<TraceTree.NodeValue> | null {
  669. const [type, id] = segment.split(':');
  670. if (!type || !id) {
  671. throw new TypeError('Node path must be in the format of `type:id`');
  672. }
  673. return TraceTreeNode.Find(start, node => {
  674. if (type === 'txn' && isTransactionNode(node)) {
  675. return node.value.event_id === id;
  676. }
  677. if (type === 'span' && isSpanNode(node)) {
  678. return node.value.span_id === id;
  679. }
  680. if (type === 'ag' && isAutogroupedNode(node)) {
  681. if (isParentAutogroupedNode(node)) {
  682. return node.head.value.span_id === id || node.tail.value.span_id === id;
  683. }
  684. if (isSiblingAutogroupedNode(node)) {
  685. const child = node.children[0];
  686. if (isSpanNode(child)) {
  687. return child.value.span_id === id;
  688. }
  689. }
  690. }
  691. return false;
  692. });
  693. }