utils.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876
  1. import {useLayoutEffect, useState} from 'react';
  2. import Fuse from 'fuse.js';
  3. import {mat3, vec2} from 'gl-matrix';
  4. import {CanvasView} from 'sentry/utils/profiling/canvasView';
  5. import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
  6. import {FlamegraphRenderer} from 'sentry/utils/profiling/renderers/flamegraphRenderer';
  7. import {CanvasPoolManager} from '../canvasScheduler';
  8. import {clamp} from '../colors/utils';
  9. import {FlamegraphCanvas} from '../flamegraphCanvas';
  10. import {SpanChartRenderer2D} from '../renderers/spansRenderer';
  11. import {SpanChartNode} from '../spanChart';
  12. export function createShader(
  13. gl: WebGLRenderingContext,
  14. type: WebGLRenderingContext['VERTEX_SHADER'] | WebGLRenderingContext['FRAGMENT_SHADER'],
  15. source: string
  16. ): WebGLShader {
  17. const shader = gl.createShader(type);
  18. if (!shader) {
  19. throw new Error('Could not create shader');
  20. }
  21. gl.shaderSource(shader, source);
  22. gl.compileShader(shader);
  23. const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  24. if (success) {
  25. return shader;
  26. }
  27. gl.deleteShader(shader);
  28. throw new Error('Failed to compile shader');
  29. }
  30. export function createProgram(
  31. gl: WebGLRenderingContext,
  32. vertexShader: WebGLShader,
  33. fragmentShader: WebGLShader
  34. ): WebGLProgram {
  35. const program = gl.createProgram();
  36. if (!program) {
  37. throw new Error('Could not create program');
  38. }
  39. gl.attachShader(program, vertexShader);
  40. gl.attachShader(program, fragmentShader);
  41. gl.linkProgram(program);
  42. const success = gl.getProgramParameter(program, gl.LINK_STATUS);
  43. if (success) {
  44. return program;
  45. }
  46. gl.deleteProgram(program);
  47. throw new Error('Failed to create program');
  48. }
  49. // Create a projection matrix with origins at 0,0 in top left corner, scaled to width/height
  50. export function makeProjectionMatrix(width: number, height: number): mat3 {
  51. const projectionMatrix = mat3.create();
  52. mat3.translate(projectionMatrix, projectionMatrix, vec2.fromValues(-1, 1));
  53. mat3.scale(
  54. projectionMatrix,
  55. projectionMatrix,
  56. vec2.divide(vec2.create(), vec2.fromValues(2, -2), vec2.fromValues(width, height))
  57. );
  58. return projectionMatrix;
  59. }
  60. const canvasToDisplaySizeMap = new Map<HTMLCanvasElement, [number, number]>();
  61. function onResize(entries: ResizeObserverEntry[]) {
  62. for (const entry of entries) {
  63. let width;
  64. let height;
  65. let dpr = window.devicePixelRatio;
  66. // @ts-ignore use as a progressive enhancement, some browsers don't support this yet
  67. if (entry.devicePixelContentBoxSize) {
  68. // NOTE: Only this path gives the correct answer
  69. // The other paths are imperfect fallbacks
  70. // for browsers that don't provide anyway to do this
  71. // @ts-ignore
  72. width = entry.devicePixelContentBoxSize[0].inlineSize;
  73. // @ts-ignore
  74. height = entry.devicePixelContentBoxSize[0].blockSize;
  75. dpr = 1; // it's already in width and height
  76. } else if (entry.contentBoxSize) {
  77. if (entry.contentBoxSize[0]) {
  78. width = entry.contentBoxSize[0].inlineSize;
  79. height = entry.contentBoxSize[0].blockSize;
  80. } else {
  81. // @ts-ignore
  82. width = entry.contentBoxSize.inlineSize;
  83. // @ts-ignore
  84. height = entry.contentBoxSize.blockSize;
  85. }
  86. } else {
  87. width = entry.contentRect.width;
  88. height = entry.contentRect.height;
  89. }
  90. const displayWidth = Math.round(width * dpr);
  91. const displayHeight = Math.round(height * dpr);
  92. canvasToDisplaySizeMap.set(entry.target as HTMLCanvasElement, [
  93. displayWidth,
  94. displayHeight,
  95. ]);
  96. resizeCanvasToDisplaySize(entry.target as HTMLCanvasElement);
  97. }
  98. }
  99. export const watchForResize = (
  100. canvas: HTMLCanvasElement[],
  101. callback?: (entries: ResizeObserverEntry[], observer: ResizeObserver) => void
  102. ): ResizeObserver => {
  103. const handler: ResizeObserverCallback = (entries, observer) => {
  104. onResize(entries);
  105. callback?.(entries, observer);
  106. };
  107. for (const c of canvas) {
  108. canvasToDisplaySizeMap.set(c, [c.width, c.height]);
  109. }
  110. const resizeObserver = new ResizeObserver(handler);
  111. try {
  112. // only call us of the number of device pixels changed
  113. canvas.forEach(c => {
  114. resizeObserver.observe(c, {box: 'device-pixel-content-box'});
  115. });
  116. } catch (ex) {
  117. // device-pixel-content-box is not supported so fallback to this
  118. canvas.forEach(c => {
  119. resizeObserver.observe(c, {box: 'content-box'});
  120. });
  121. }
  122. return resizeObserver;
  123. };
  124. export function resizeCanvasToDisplaySize(canvas: HTMLCanvasElement): boolean {
  125. // Get the size the browser is displaying the canvas in device pixels.
  126. const size = canvasToDisplaySizeMap.get(canvas);
  127. if (!size) {
  128. const displayWidth = canvas.clientWidth * window.devicePixelRatio;
  129. const displayHeight = canvas.clientHeight * window.devicePixelRatio;
  130. canvas.width = displayWidth;
  131. canvas.height = displayHeight;
  132. return false;
  133. }
  134. const [displayWidth, displayHeight] = size;
  135. // Check if the canvas is not the same size.
  136. const needResize = canvas.width !== displayWidth || canvas.height !== displayHeight;
  137. if (needResize) {
  138. // Make the canvas the same size
  139. canvas.width = displayWidth;
  140. canvas.height = displayHeight;
  141. }
  142. return needResize;
  143. }
  144. export function transformMatrixBetweenRect(from: Rect, to: Rect): mat3 {
  145. return mat3.fromValues(
  146. to.width / from.width,
  147. 0,
  148. 0,
  149. 0,
  150. to.height / from.height,
  151. 0,
  152. to.x - from.x * (to.width / from.width),
  153. to.y - from.y * (to.height / from.height),
  154. 1
  155. );
  156. }
  157. // Utility class to manipulate a virtual rect element. Some of the implementations are based off
  158. // speedscope, however they are not 100% accurate and we've made some changes. It is important to
  159. // note that contructing a lot of these objects at draw time is expensive and should be avoided.
  160. export class Rect {
  161. origin: vec2;
  162. size: vec2;
  163. constructor(x: number, y: number, width: number, height: number) {
  164. this.origin = vec2.fromValues(x, y);
  165. this.size = vec2.fromValues(width, height);
  166. }
  167. clone(): Rect {
  168. return Rect.From(this);
  169. }
  170. isValid(): boolean {
  171. return this.toMatrix().every(n => !isNaN(n));
  172. }
  173. isEmpty(): boolean {
  174. return this.width === 0 && this.height === 0;
  175. }
  176. static Empty(): Rect {
  177. return new Rect(0, 0, 0, 0);
  178. }
  179. static From(rect: Rect): Rect {
  180. return new Rect(rect.x, rect.y, rect.width, rect.height);
  181. }
  182. get x(): number {
  183. return this.origin[0];
  184. }
  185. get y(): number {
  186. return this.origin[1];
  187. }
  188. get width(): number {
  189. return this.size[0];
  190. }
  191. get height(): number {
  192. return this.size[1];
  193. }
  194. get left(): number {
  195. return this.x;
  196. }
  197. get right(): number {
  198. return this.left + this.width;
  199. }
  200. get top(): number {
  201. return this.y;
  202. }
  203. get bottom(): number {
  204. return this.top + this.height;
  205. }
  206. static decode(query: string | ReadonlyArray<string> | null | undefined): Rect | null {
  207. let maybeEncodedRect = query;
  208. if (typeof query === 'string') {
  209. maybeEncodedRect = query.split(',');
  210. }
  211. if (!Array.isArray(maybeEncodedRect)) {
  212. return null;
  213. }
  214. if (maybeEncodedRect.length !== 4) {
  215. return null;
  216. }
  217. const rect = new Rect(
  218. ...(maybeEncodedRect.map(p => parseFloat(p)) as [number, number, number, number])
  219. );
  220. if (rect.isValid()) {
  221. return rect;
  222. }
  223. return null;
  224. }
  225. static encode(rect: Rect): string {
  226. return rect.toString();
  227. }
  228. toString() {
  229. return [this.x, this.y, this.width, this.height].map(n => Math.round(n)).join(',');
  230. }
  231. toMatrix(): mat3 {
  232. const {width: w, height: h, x, y} = this;
  233. // it's easier to display a matrix as a 3x3 array. WebGl matrices are row first and not column first
  234. // https://webglfundamentals.org/webgl/lessons/webgl-matrix-vs-math.html
  235. // prettier-ignore
  236. return mat3.fromValues(
  237. w, 0, 0,
  238. 0, h, 0,
  239. x, y, 1
  240. )
  241. }
  242. hasIntersectionWith(other: Rect): boolean {
  243. const top = Math.max(this.top, other.top);
  244. const bottom = Math.max(top, Math.min(this.bottom, other.bottom));
  245. if (bottom - top === 0) {
  246. return false;
  247. }
  248. const left = Math.max(this.left, other.left);
  249. const right = Math.max(left, Math.min(this.right, other.right));
  250. if (right - left === 0) {
  251. return false;
  252. }
  253. return true;
  254. }
  255. containsX(vec: vec2): boolean {
  256. return vec[0] >= this.left && vec[0] <= this.right;
  257. }
  258. containsY(vec: vec2): boolean {
  259. return vec[1] >= this.top && vec[1] <= this.bottom;
  260. }
  261. contains(vec: vec2): boolean {
  262. return this.containsX(vec) && this.containsY(vec);
  263. }
  264. containsRect(rect: Rect): boolean {
  265. return (
  266. // left bound
  267. this.left <= rect.left &&
  268. // right bound
  269. rect.right <= this.right &&
  270. // top bound
  271. this.top <= rect.top &&
  272. // bottom bound
  273. rect.bottom <= this.bottom
  274. );
  275. }
  276. leftOverlapsWith(rect: Rect): boolean {
  277. return rect.left <= this.left && rect.right >= this.left;
  278. }
  279. rightOverlapsWith(rect: Rect): boolean {
  280. return this.right >= rect.left && this.right <= rect.right;
  281. }
  282. overlapsX(other: Rect): boolean {
  283. return this.left <= other.right && this.right >= other.left;
  284. }
  285. overlapsY(other: Rect): boolean {
  286. return this.top <= other.bottom && this.bottom >= other.top;
  287. }
  288. overlaps(other: Rect): boolean {
  289. return this.overlapsX(other) && this.overlapsY(other);
  290. }
  291. transformRect(transform: mat3 | Readonly<mat3>): Rect {
  292. const x = this.x * transform[0] + this.y * transform[3] + transform[6];
  293. const y = this.x * transform[1] + this.y * transform[4] + transform[7];
  294. const width = this.width * transform[0] + this.height * transform[3];
  295. const height = this.width * transform[1] + this.height * transform[4];
  296. return new Rect(
  297. x + (width < 0 ? width : 0),
  298. y + (height < 0 ? height : 0),
  299. Math.abs(width),
  300. Math.abs(height)
  301. );
  302. }
  303. /**
  304. * Returns a transform that inverts the y axis within the rect.
  305. * This causes the bottom of the rect to be the top of the rect and vice versa.
  306. */
  307. invertYTransform(): mat3 {
  308. return mat3.fromValues(1, 0, 0, 0, -1, 0, 0, this.y * 2 + this.height, 1);
  309. }
  310. withHeight(height: number): Rect {
  311. return new Rect(this.x, this.y, this.width, height);
  312. }
  313. withWidth(width: number): Rect {
  314. return new Rect(this.x, this.y, width, this.height);
  315. }
  316. withX(x: number): Rect {
  317. return new Rect(x, this.y, this.width, this.height);
  318. }
  319. withY(y: number) {
  320. return new Rect(this.x, y, this.width, this.height);
  321. }
  322. toBounds(): [number, number, number, number] {
  323. return [this.x, this.y, this.x + this.width, this.y + this.height];
  324. }
  325. toArray(): [number, number, number, number] {
  326. return [this.x, this.y, this.width, this.height];
  327. }
  328. between(to: Rect): Rect {
  329. return new Rect(to.x, to.y, to.width / this.width, to.height / this.height);
  330. }
  331. translate(x: number, y: number): Rect {
  332. return new Rect(x, y, this.width, this.height);
  333. }
  334. translateX(x: number): Rect {
  335. return new Rect(x, this.y, this.width, this.height);
  336. }
  337. translateY(y: number): Rect {
  338. return new Rect(this.x, y, this.width, this.height);
  339. }
  340. scaleX(x: number): Rect {
  341. return new Rect(this.x, this.y, this.width * x, this.height);
  342. }
  343. scaleY(y: number): Rect {
  344. return new Rect(this.x, this.y, this.width, this.height * y);
  345. }
  346. scale(x: number, y: number): Rect {
  347. return new Rect(this.x * x, this.y * y, this.width * x, this.height * y);
  348. }
  349. scaleOriginBy(x: number, y: number): Rect {
  350. return new Rect(this.x * x, this.y * y, this.width, this.height);
  351. }
  352. scaledBy(x: number, y: number): Rect {
  353. return new Rect(this.x, this.y, this.width * x, this.height * y);
  354. }
  355. equals(rect: Rect): boolean {
  356. if (this.x !== rect.x) {
  357. return false;
  358. }
  359. if (this.y !== rect.y) {
  360. return false;
  361. }
  362. if (this.width !== rect.width) {
  363. return false;
  364. }
  365. if (this.height !== rect.height) {
  366. return false;
  367. }
  368. return true;
  369. }
  370. notEqualTo(rect: Rect): boolean {
  371. return !this.equals(rect);
  372. }
  373. }
  374. function getContext(canvas: HTMLCanvasElement, context: '2d'): CanvasRenderingContext2D;
  375. function getContext(canvas: HTMLCanvasElement, context: 'webgl'): WebGLRenderingContext;
  376. function getContext(canvas: HTMLCanvasElement, context: string): RenderingContext {
  377. const ctx =
  378. context === 'webgl'
  379. ? canvas.getContext(context, {antialias: false})
  380. : canvas.getContext(context);
  381. if (!ctx) {
  382. throw new Error(`Could not get context ${context}`);
  383. }
  384. return ctx;
  385. }
  386. // Exported separately as writing export function for each overload as
  387. // breaks the line width rules and makes it harder to read.
  388. export {getContext};
  389. export function measureText(string: string, ctx?: CanvasRenderingContext2D): Rect {
  390. if (!string) {
  391. return Rect.Empty();
  392. }
  393. const context = ctx || getContext(document.createElement('canvas'), '2d');
  394. const measures = context.measureText(string);
  395. return new Rect(
  396. 0,
  397. 0,
  398. measures.width,
  399. // https://stackoverflow.com/questions/1134586/how-can-you-find-the-height-of-text-on-an-html-canvas
  400. measures.actualBoundingBoxAscent + measures.actualBoundingBoxDescent
  401. );
  402. }
  403. // Taken from speedscope, computes min/max by halving the high/low end
  404. // of the range on each iteration as long as range precision is greater than the given precision.
  405. export function findRangeBinarySearch(
  406. {low, high}: {high: number; low: number},
  407. fn: (val: number) => number,
  408. target: number,
  409. precision = 1
  410. ): [number, number] {
  411. // eslint-disable-next-line
  412. while (true) {
  413. if (high - low <= precision) {
  414. return [low, high];
  415. }
  416. const mid = (high + low) / 2;
  417. if (fn(mid) < target) {
  418. low = mid;
  419. } else {
  420. high = mid;
  421. }
  422. }
  423. }
  424. export function formatColorForSpan(
  425. frame: SpanChartNode,
  426. renderer: SpanChartRenderer2D
  427. ): string {
  428. const color = renderer.getColorForFrame(frame);
  429. if (color.length === 4) {
  430. return `rgba(${color
  431. .slice(0, 3)
  432. .map(n => n * 255)
  433. .join(',')}, ${color[3]})`;
  434. }
  435. return `rgba(${color.map(n => n * 255).join(',')}, 1.0)`;
  436. }
  437. export function formatColorForFrame(
  438. frame: FlamegraphFrame,
  439. renderer: FlamegraphRenderer
  440. ): string {
  441. const color = renderer.getColorForFrame(frame);
  442. if (color.length === 4) {
  443. return `rgba(${color
  444. .slice(0, 3)
  445. .map(n => n * 255)
  446. .join(',')}, ${color[3]})`;
  447. }
  448. return `rgba(${color.map(n => n * 255).join(',')}, 1.0)`;
  449. }
  450. export const ELLIPSIS = '\u2026';
  451. type TrimTextCenter = {
  452. end: number;
  453. length: number;
  454. start: number;
  455. text: string;
  456. };
  457. // Similar to speedscope's implementation, utility fn to trim text in the center with a small bias towards prefixes.
  458. export function trimTextCenter(text: string, low: number): TrimTextCenter {
  459. if (low >= text.length) {
  460. return {
  461. text,
  462. start: 0,
  463. end: 0,
  464. length: 0,
  465. };
  466. }
  467. const prefixLength = Math.floor(low / 2);
  468. // Use 1 character less than the low value to account for ellipsis and favor displaying the prefix
  469. const postfixLength = low - prefixLength - 1;
  470. const start = prefixLength;
  471. const end = Math.floor(text.length - postfixLength + ELLIPSIS.length);
  472. const trimText = `${text.substring(0, start)}${ELLIPSIS}${text.substring(end)}`;
  473. return {
  474. text: trimText,
  475. start,
  476. end,
  477. length: end - start,
  478. };
  479. }
  480. // Utility function to compute a clamped view. This is essentially a bounds check
  481. // to ensure that zoomed viewports stays in the bounds and does not escape the view.
  482. export function computeClampedConfigView(
  483. newConfigView: Rect,
  484. {width, height}: {height: {max: number; min: number}; width: {max: number; min: number}}
  485. ) {
  486. if (!newConfigView.isValid()) {
  487. throw new Error(newConfigView.toString());
  488. }
  489. const clampedWidth = clamp(newConfigView.width, width.min, width.max);
  490. const clampedHeight = clamp(newConfigView.height, height.min, height.max);
  491. const maxX = width.max - clampedWidth;
  492. const maxY = clampedHeight >= height.max ? 0 : height.max - clampedHeight;
  493. const clampedX = clamp(newConfigView.x, 0, maxX);
  494. const clampedY = clamp(newConfigView.y, 0, maxY);
  495. return new Rect(clampedX, clampedY, clampedWidth, clampedHeight);
  496. }
  497. /**
  498. * computeHighlightedBounds determines if a supplied boundary should be reduced in size
  499. * or shifted based on the results of a trim operation
  500. */
  501. export function computeHighlightedBounds(
  502. bounds: Fuse.RangeTuple,
  503. trim: TrimTextCenter
  504. ): Fuse.RangeTuple {
  505. if (!trim.length) {
  506. return bounds;
  507. }
  508. const isStartBetweenTrim = bounds[0] >= trim.start && bounds[0] <= trim.end;
  509. const isEndBetweenTrim = bounds[1] >= trim.start && bounds[1] <= trim.end;
  510. const isFullyTruncated = isStartBetweenTrim && isEndBetweenTrim;
  511. // example:
  512. // -[UIScrollView _smoothScrollDisplayLink:]
  513. // "smooth" in "-[UIScrollView _…ScrollDisplayLink:]"
  514. // ^^
  515. if (isFullyTruncated) {
  516. return [trim.start, trim.start + 1];
  517. }
  518. if (bounds[0] < trim.start) {
  519. // "ScrollView" in '-[UIScrollView _sm…rollDisplayLink:]'
  520. // ^--------^
  521. if (bounds[1] < trim.start) {
  522. return [bounds[0], bounds[1]];
  523. }
  524. // "smoothScroll" in -[UIScrollView _smooth…DisplayLink:]'
  525. // ^-----^
  526. if (isEndBetweenTrim) {
  527. return [bounds[0], trim.start + 1];
  528. }
  529. // "smoothScroll" in -[UIScrollView _sm…llDisplayLink:]'
  530. // ^---^
  531. if (bounds[1] > trim.end) {
  532. return [bounds[0], bounds[1] - trim.length + 1];
  533. }
  534. }
  535. // "smoothScroll" in -[UIScrollView _…scrollDisplayLink:]'
  536. // ^-----^
  537. if (isStartBetweenTrim && bounds[1] > trim.end) {
  538. return [trim.start, bounds[1] - trim.length + 1];
  539. }
  540. // "display" in -[UIScrollView _…scrollDisplayLink:]'
  541. // ^-----^
  542. if (bounds[0] > trim.end) {
  543. return [bounds[0] - trim.length + 1, bounds[1] - trim.length + 1];
  544. }
  545. throw new Error(`Unhandled case: ${JSON.stringify(bounds)} ${trim}`);
  546. }
  547. // Utility function to allow zooming into frames using a specific strategy. Supports
  548. // min zooming and exact strategy. Min zooming means we will zoom into a frame by doing
  549. // the minimal number of moves to get a frame into view - for example, if the view is large
  550. // enough and the frame we are zooming to is just outside of the viewport to the right,
  551. // we will only move the viewport to the right until the frame is in view. Exact strategy
  552. // means we will zoom into the frame by moving the viewport to the exact location of the frame
  553. // and setting the width of the view to that of the frame.
  554. export function computeConfigViewWithStrategy(
  555. strategy: 'min' | 'exact',
  556. view: Rect,
  557. frame: Rect
  558. ): Rect {
  559. if (strategy === 'exact') {
  560. return frame.withHeight(view.height);
  561. }
  562. if (strategy === 'min') {
  563. // If frame is in view, do nothing
  564. if (view.containsRect(frame)) {
  565. return view;
  566. }
  567. // If view width <= frame width, we need to zoom out, so the behavior is the
  568. // same as if we were using 'exact'
  569. if (view.width <= frame.width) {
  570. return frame.withHeight(view.height);
  571. }
  572. // If frame is to the left of the view, translate it left
  573. // to frame.x so that start of the frame is in the view
  574. let offset = view.clone();
  575. if (frame.left < view.left) {
  576. offset = offset.withX(frame.x);
  577. } else if (frame.right > view.right) {
  578. // If the right boundary of a frame is outside of the view, translate the view
  579. // by the difference between the right edge of the frame and the right edge of the view
  580. offset = view.withX(offset.x + frame.right - offset.right);
  581. }
  582. // If frame is above the view, translate view to top of frame
  583. if (frame.bottom < view.top) {
  584. offset = offset.withY(frame.top);
  585. } else if (frame.bottom > view.bottom) {
  586. // If frame is below the view, translate view by the difference
  587. // of the bottom edge of the frame and the view
  588. offset = offset.translateY(offset.y + frame.bottom - offset.bottom);
  589. }
  590. return offset;
  591. }
  592. return frame.withHeight(view.height);
  593. }
  594. // Compute the X and Y position based on offset and canvas resolution
  595. export function getPhysicalSpacePositionFromOffset(offsetX: number, offsetY: number) {
  596. const logicalMousePos = vec2.fromValues(offsetX, offsetY);
  597. return vec2.scale(vec2.create(), logicalMousePos, window.devicePixelRatio);
  598. }
  599. export function getCenterScaleMatrix(
  600. scale: number,
  601. offsetX: number,
  602. offsetY: number,
  603. view: CanvasView<any>,
  604. canvas: FlamegraphCanvas
  605. ): mat3 {
  606. const configSpaceMouse = view.getConfigViewCursor(
  607. vec2.fromValues(offsetX, offsetY),
  608. canvas
  609. );
  610. const configCenter = vec2.fromValues(configSpaceMouse[0], view.configView.y);
  611. const invertedConfigCenter = vec2.multiply(
  612. vec2.create(),
  613. vec2.fromValues(-1, -1),
  614. configCenter
  615. );
  616. const centerScaleMatrix = mat3.create();
  617. mat3.fromTranslation(centerScaleMatrix, configCenter);
  618. mat3.scale(centerScaleMatrix, centerScaleMatrix, vec2.fromValues(scale, 1));
  619. mat3.translate(centerScaleMatrix, centerScaleMatrix, invertedConfigCenter);
  620. return centerScaleMatrix;
  621. }
  622. export function getConfigViewTranslationBetweenVectors(
  623. offsetX: number,
  624. offsetY: number,
  625. start: vec2,
  626. view: CanvasView<any>,
  627. canvas: FlamegraphCanvas,
  628. invert?: boolean
  629. ): mat3 | null {
  630. const physicalMousePos = getPhysicalSpacePositionFromOffset(offsetX, offsetY);
  631. const physicalDelta = invert
  632. ? vec2.subtract(vec2.create(), physicalMousePos, start)
  633. : vec2.subtract(vec2.create(), start, physicalMousePos);
  634. if (physicalDelta[0] === 0 && physicalDelta[1] === 0) {
  635. return null;
  636. }
  637. const physicalToConfig = mat3.invert(
  638. mat3.create(),
  639. view.fromConfigView(canvas.physicalSpace)
  640. );
  641. const [m00, m01, m02, m10, m11, m12] = physicalToConfig;
  642. const configDelta = vec2.transformMat3(vec2.create(), physicalDelta, [
  643. m00,
  644. m01,
  645. m02,
  646. m10,
  647. m11,
  648. m12,
  649. 0,
  650. 0,
  651. 0,
  652. ]);
  653. return mat3.fromTranslation(mat3.create(), configDelta);
  654. }
  655. export function getConfigSpaceTranslationBetweenVectors(
  656. offsetX: number,
  657. offsetY: number,
  658. start: vec2,
  659. view: CanvasView<any>,
  660. canvas: FlamegraphCanvas,
  661. invert?: boolean
  662. ): mat3 | null {
  663. const physicalMousePos = getPhysicalSpacePositionFromOffset(offsetX, offsetY);
  664. const physicalDelta = invert
  665. ? vec2.subtract(vec2.create(), physicalMousePos, start)
  666. : vec2.subtract(vec2.create(), start, physicalMousePos);
  667. if (physicalDelta[0] === 0 && physicalDelta[1] === 0) {
  668. return null;
  669. }
  670. const physicalToConfig = mat3.invert(
  671. mat3.create(),
  672. view.fromConfigSpace(canvas.physicalSpace)
  673. );
  674. const [m00, m01, m02, m10, m11, m12] = physicalToConfig;
  675. const configDelta = vec2.transformMat3(vec2.create(), physicalDelta, [
  676. m00,
  677. m01,
  678. m02,
  679. m10,
  680. m11,
  681. m12,
  682. 0,
  683. 0,
  684. 0,
  685. ]);
  686. return mat3.fromTranslation(mat3.create(), configDelta);
  687. }
  688. export function getMinimapCanvasCursor(
  689. configView: Rect | undefined,
  690. configSpaceCursor: vec2 | null,
  691. borderWidth: number
  692. ) {
  693. if (!configView || !configSpaceCursor) {
  694. return 'col-resize';
  695. }
  696. const nearestEdge = Math.min(
  697. Math.abs(configView.left - configSpaceCursor[0]),
  698. Math.abs(configView.right - configSpaceCursor[0])
  699. );
  700. const isWithinBorderSize = nearestEdge <= borderWidth;
  701. if (isWithinBorderSize) {
  702. return 'ew-resize';
  703. }
  704. if (configView.contains(configSpaceCursor)) {
  705. return 'grab';
  706. }
  707. return 'col-resize';
  708. }
  709. export function useResizeCanvasObserver(
  710. canvases: (HTMLCanvasElement | null)[],
  711. canvasPoolManager: CanvasPoolManager,
  712. canvas: FlamegraphCanvas | null,
  713. view: CanvasView<any> | null
  714. ): Rect {
  715. const [bounds, setCanvasBounds] = useState<Rect>(Rect.Empty());
  716. useLayoutEffect(() => {
  717. if (!canvas || !canvases.length) {
  718. return undefined;
  719. }
  720. if (canvases.some(c => c === null)) {
  721. return undefined;
  722. }
  723. const observer = watchForResize(canvases as HTMLCanvasElement[], entries => {
  724. const contentRect =
  725. entries[0].contentRect ?? entries[0].target.getBoundingClientRect();
  726. setCanvasBounds(
  727. new Rect(contentRect.x, contentRect.y, contentRect.width, contentRect.height)
  728. );
  729. canvas.initPhysicalSpace();
  730. if (view) {
  731. view.resizeConfigSpace(canvas);
  732. }
  733. canvasPoolManager.drawSync();
  734. });
  735. return () => {
  736. observer.disconnect();
  737. };
  738. }, [canvases, canvas, view, canvasPoolManager]);
  739. return bounds;
  740. }