canvasView.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import {mat3, vec2} from 'gl-matrix';
  2. import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
  3. import {
  4. computeClampedConfigView,
  5. transformMatrixBetweenRect,
  6. } from 'sentry/utils/profiling/gl/utils';
  7. import {Rect} from 'sentry/utils/profiling/speedscope';
  8. export class CanvasView<T extends {configSpace: Rect}> {
  9. configView: Rect = Rect.Empty();
  10. configSpace: Readonly<Rect> = Rect.Empty();
  11. configSpaceTransform: mat3 = mat3.create();
  12. inverted: boolean;
  13. minWidth: number;
  14. depthOffset: number;
  15. barHeight: number;
  16. model: T;
  17. canvas: FlamegraphCanvas;
  18. mode: 'anchorTop' | 'anchorBottom' | 'stretchToFit' = 'anchorTop';
  19. constructor({
  20. canvas,
  21. options,
  22. model,
  23. mode,
  24. }: {
  25. canvas: FlamegraphCanvas;
  26. model: T;
  27. options: {
  28. barHeight: number;
  29. configSpaceTransform?: Rect;
  30. depthOffset?: number;
  31. inverted?: boolean;
  32. minWidth?: number;
  33. };
  34. mode?: CanvasView<T>['mode'];
  35. }) {
  36. this.mode = mode || this.mode;
  37. this.inverted = !!options.inverted;
  38. this.minWidth = options.minWidth ?? 0;
  39. this.model = model;
  40. this.canvas = canvas;
  41. this.depthOffset = options.depthOffset ?? 0;
  42. this.barHeight = options.barHeight ? options.barHeight * window.devicePixelRatio : 0;
  43. // This is a transformation matrix that is applied to the configView, it allows us to
  44. // transform an entire view and render it without having to recompute the models.
  45. // This is useful for example when we want to offset a profile by some duration.
  46. this.configSpaceTransform = options.configSpaceTransform
  47. ? mat3.fromValues(
  48. options.configSpaceTransform.width || 1,
  49. 0,
  50. 0,
  51. 0,
  52. options.configSpaceTransform.height || 1,
  53. 0,
  54. options.configSpaceTransform.x || 0,
  55. options.configSpaceTransform.y || 0,
  56. 1
  57. )
  58. : mat3.create();
  59. this.initConfigSpace(canvas);
  60. }
  61. setMinWidth(minWidth: number) {
  62. if (minWidth < 0) {
  63. throw new Error('View min width cannot be negative');
  64. }
  65. this.minWidth = minWidth;
  66. }
  67. isViewAtTopEdgeOf(space: Rect): boolean {
  68. return this.inverted
  69. ? space.bottom === this.configView.bottom
  70. : space.top === this.configView.top;
  71. }
  72. isViewAtBottomEdgeOf(space: Rect): boolean {
  73. return this.inverted
  74. ? space.top === this.configView.top
  75. : space.bottom === this.configView.bottom;
  76. }
  77. private _initConfigSpace(canvas: FlamegraphCanvas): void {
  78. switch (this.mode) {
  79. case 'stretchToFit': {
  80. this.configSpace = new Rect(
  81. 0,
  82. 0,
  83. this.model.configSpace.width,
  84. this.model.configSpace.height + this.depthOffset
  85. );
  86. return;
  87. }
  88. case 'anchorBottom':
  89. case 'anchorTop':
  90. default: {
  91. this.configSpace = new Rect(
  92. 0,
  93. 0,
  94. this.model.configSpace.width,
  95. Math.max(
  96. this.model.configSpace.height + this.depthOffset,
  97. canvas.physicalSpace.height / this.barHeight
  98. )
  99. );
  100. }
  101. }
  102. }
  103. private _initConfigView(canvas: FlamegraphCanvas, space: Rect): void {
  104. switch (this.mode) {
  105. case 'stretchToFit': {
  106. this.configView = Rect.From(space);
  107. return;
  108. }
  109. case 'anchorBottom': {
  110. const newHeight = canvas.physicalSpace.height / this.barHeight;
  111. const newY = Math.max(0, Math.ceil(space.y - (newHeight - space.height)));
  112. this.configView = Rect.From(space).withHeight(newHeight).withY(newY);
  113. return;
  114. }
  115. case 'anchorTop': {
  116. this.configView = Rect.From(space).withHeight(
  117. canvas.physicalSpace.height / this.barHeight
  118. );
  119. return;
  120. }
  121. default:
  122. throw new Error(`Unknown CanvasView mode: ${this.mode}`);
  123. }
  124. }
  125. initConfigSpace(canvas: FlamegraphCanvas): void {
  126. this._initConfigSpace(canvas);
  127. this._initConfigView(canvas, this.configSpace);
  128. }
  129. resizeConfigSpace(canvas: FlamegraphCanvas): void {
  130. this._initConfigSpace(canvas);
  131. this._initConfigView(canvas, this.configView);
  132. }
  133. resetConfigView(canvas: FlamegraphCanvas): void {
  134. this._initConfigView(canvas, this.configSpace);
  135. }
  136. setConfigView(
  137. configView: Rect,
  138. overrides?: {
  139. width: {max?: number; min?: number};
  140. height?: {max?: number; min?: number};
  141. }
  142. ) {
  143. this.configView = computeClampedConfigView(configView, {
  144. width: {
  145. min: this.minWidth,
  146. max: this.configSpace.width,
  147. ...(overrides?.width ?? {}),
  148. },
  149. height: {
  150. min: 0,
  151. max: this.configSpace.height,
  152. ...(overrides?.height ?? {}),
  153. },
  154. });
  155. }
  156. transformConfigView(transformation: mat3) {
  157. this.setConfigView(this.configView.transformRect(transformation));
  158. }
  159. toConfigSpace(space: Rect): mat3 {
  160. const toConfigSpace = transformMatrixBetweenRect(space, this.configSpace);
  161. if (this.inverted) {
  162. mat3.multiply(toConfigSpace, this.configSpace.invertYTransform(), toConfigSpace);
  163. }
  164. return toConfigSpace;
  165. }
  166. toConfigView(space: Rect): mat3 {
  167. const toConfigView = transformMatrixBetweenRect(space, this.configView);
  168. if (this.inverted) {
  169. mat3.multiply(toConfigView, this.configView.invertYTransform(), toConfigView);
  170. }
  171. return toConfigView;
  172. }
  173. fromConfigSpace(space: Rect): mat3 {
  174. const fromConfigSpace = transformMatrixBetweenRect(this.configSpace, space);
  175. if (this.inverted) {
  176. mat3.multiply(fromConfigSpace, space.invertYTransform(), fromConfigSpace);
  177. }
  178. return fromConfigSpace;
  179. }
  180. fromConfigView(space: Rect): mat3 {
  181. const fromConfigView = transformMatrixBetweenRect(this.configView, space);
  182. if (this.inverted) {
  183. mat3.multiply(fromConfigView, space.invertYTransform(), fromConfigView);
  184. }
  185. return fromConfigView;
  186. }
  187. fromTransformedConfigView(space: Rect): mat3 {
  188. const fromConfigView = mat3.multiply(
  189. mat3.create(),
  190. transformMatrixBetweenRect(this.configView, space),
  191. this.configSpaceTransform
  192. );
  193. if (this.inverted) {
  194. mat3.multiply(fromConfigView, space.invertYTransform(), fromConfigView);
  195. }
  196. return fromConfigView;
  197. }
  198. fromTransformedConfigSpace(space: Rect): mat3 {
  199. const fromConfigView = mat3.multiply(
  200. mat3.create(),
  201. transformMatrixBetweenRect(this.configSpace, space),
  202. this.configSpaceTransform
  203. );
  204. if (this.inverted) {
  205. mat3.multiply(fromConfigView, space.invertYTransform(), fromConfigView);
  206. }
  207. return fromConfigView;
  208. }
  209. getConfigSpaceCursor(logicalSpaceCursor: vec2, canvas: FlamegraphCanvas): vec2 {
  210. const physicalSpaceCursor = vec2.transformMat3(
  211. vec2.create(),
  212. logicalSpaceCursor,
  213. canvas.logicalToPhysicalSpace
  214. );
  215. return vec2.transformMat3(
  216. vec2.create(),
  217. physicalSpaceCursor,
  218. this.toConfigSpace(canvas.physicalSpace)
  219. );
  220. }
  221. getTransformedConfigSpaceCursor(
  222. logicalSpaceCursor: vec2,
  223. canvas: FlamegraphCanvas
  224. ): vec2 {
  225. const physicalSpaceCursor = vec2.transformMat3(
  226. vec2.create(),
  227. logicalSpaceCursor,
  228. canvas.logicalToPhysicalSpace
  229. );
  230. const finalMatrix = mat3.multiply(
  231. mat3.create(),
  232. mat3.invert(mat3.create(), this.configSpaceTransform),
  233. this.toConfigSpace(canvas.physicalSpace)
  234. );
  235. const configViewCursor = vec2.transformMat3(
  236. vec2.create(),
  237. physicalSpaceCursor,
  238. finalMatrix
  239. );
  240. return configViewCursor;
  241. }
  242. getConfigViewCursor(logicalSpaceCursor: vec2, canvas: FlamegraphCanvas): vec2 {
  243. const physicalSpaceCursor = vec2.transformMat3(
  244. vec2.create(),
  245. logicalSpaceCursor,
  246. canvas.logicalToPhysicalSpace
  247. );
  248. return vec2.transformMat3(
  249. vec2.create(),
  250. physicalSpaceCursor,
  251. this.toConfigView(canvas.physicalSpace)
  252. );
  253. }
  254. getTransformedConfigViewCursor(
  255. logicalSpaceCursor: vec2,
  256. canvas: FlamegraphCanvas
  257. ): vec2 {
  258. const physicalSpaceCursor = vec2.transformMat3(
  259. vec2.create(),
  260. logicalSpaceCursor,
  261. canvas.logicalToPhysicalSpace
  262. );
  263. const finalMatrix = mat3.multiply(
  264. mat3.create(),
  265. mat3.invert(mat3.create(), this.configSpaceTransform),
  266. this.toConfigView(canvas.physicalSpace)
  267. );
  268. const configViewCursor = vec2.transformMat3(
  269. vec2.create(),
  270. physicalSpaceCursor,
  271. finalMatrix
  272. );
  273. return configViewCursor;
  274. }
  275. /**
  276. * Applies the inverse of the config space transform to the given config space rect
  277. * @returns Rect
  278. */
  279. toOriginConfigView(space: Rect): Rect {
  280. return space.transformRect(mat3.invert(mat3.create(), this.configSpaceTransform));
  281. }
  282. }