speedscope.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. // Port of some helper classes from speedscope, a lot of these have been changed to fit our usage and the port
  2. // is not probably no longer very accurate so see speedscope source for better references.
  3. // MIT License
  4. // Copyright (c) 2018 Jamie Wong
  5. // Permission is hereby granted, free of charge, to any person obtaining a copy
  6. // of this software and associated documentation files (the "Software"), to deal
  7. // in the Software without restriction, including without limitation the rights
  8. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the Software is
  10. // furnished to do so, subject to the following conditions:
  11. // The above copyright notice and this permission notice shall be included in all
  12. // copies or substantial portions of the Software.
  13. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  19. // SOFTWARE.
  20. import {mat3, vec2} from 'gl-matrix';
  21. import {clamp} from 'sentry/utils/profiling/colors/utils';
  22. import {ColorChannels, LCH} from './flamegraph/flamegraphTheme';
  23. import {ELLIPSIS, TrimTextCenter} from './gl/utils';
  24. export class Rect {
  25. origin: vec2;
  26. size: vec2;
  27. constructor(x: number, y: number, width: number, height: number) {
  28. this.origin = vec2.fromValues(x, y);
  29. this.size = vec2.fromValues(width, height);
  30. }
  31. clone(): Rect {
  32. return Rect.From(this);
  33. }
  34. isValid(): boolean {
  35. return this.toMatrix().every(n => !isNaN(n));
  36. }
  37. isEmpty(): boolean {
  38. return this.width === 0 && this.height === 0;
  39. }
  40. static Empty(): Rect {
  41. return new Rect(0, 0, 0, 0);
  42. }
  43. static From(rect: Rect): Rect {
  44. return new Rect(rect.x, rect.y, rect.width, rect.height);
  45. }
  46. get x(): number {
  47. return this.origin[0];
  48. }
  49. get y(): number {
  50. return this.origin[1];
  51. }
  52. get width(): number {
  53. return this.size[0];
  54. }
  55. get height(): number {
  56. return this.size[1];
  57. }
  58. get left(): number {
  59. return this.x;
  60. }
  61. get right(): number {
  62. return this.left + this.width;
  63. }
  64. get top(): number {
  65. return this.y;
  66. }
  67. get bottom(): number {
  68. return this.top + this.height;
  69. }
  70. get centerX(): number {
  71. return this.x + this.width / 2;
  72. }
  73. get centerY(): number {
  74. return this.y + this.height / 2;
  75. }
  76. get center(): vec2 {
  77. return [this.centerX, this.centerY];
  78. }
  79. static decode(query: string | ReadonlyArray<string> | null | undefined): Rect | null {
  80. let maybeEncodedRect = query;
  81. if (typeof query === 'string') {
  82. maybeEncodedRect = query.split(',');
  83. }
  84. if (!Array.isArray(maybeEncodedRect)) {
  85. return null;
  86. }
  87. if (maybeEncodedRect.length !== 4) {
  88. return null;
  89. }
  90. const rect = new Rect(
  91. ...(maybeEncodedRect.map(p => parseFloat(p)) as [number, number, number, number])
  92. );
  93. if (rect.isValid()) {
  94. return rect;
  95. }
  96. return null;
  97. }
  98. static encode(rect: Rect): string {
  99. return rect.toString();
  100. }
  101. toString() {
  102. return [this.x, this.y, this.width, this.height].map(n => Math.round(n)).join(',');
  103. }
  104. toMatrix(): mat3 {
  105. const {width: w, height: h, x, y} = this;
  106. // it's easier to display a matrix as a 3x3 array. WebGl matrices are row first and not column first
  107. // https://webglfundamentals.org/webgl/lessons/webgl-matrix-vs-math.html
  108. // prettier-ignore
  109. return mat3.fromValues(
  110. w, 0, 0,
  111. 0, h, 0,
  112. x, y, 1
  113. );
  114. }
  115. hasIntersectionWith(other: Rect): boolean {
  116. const top = Math.max(this.top, other.top);
  117. const bottom = Math.max(top, Math.min(this.bottom, other.bottom));
  118. if (bottom - top === 0) {
  119. return false;
  120. }
  121. const left = Math.max(this.left, other.left);
  122. const right = Math.max(left, Math.min(this.right, other.right));
  123. if (right - left === 0) {
  124. return false;
  125. }
  126. return true;
  127. }
  128. containsX(vec: vec2): boolean {
  129. return vec[0] >= this.left && vec[0] <= this.right;
  130. }
  131. containsY(vec: vec2): boolean {
  132. return vec[1] >= this.top && vec[1] <= this.bottom;
  133. }
  134. contains(vec: vec2): boolean {
  135. return this.containsX(vec) && this.containsY(vec);
  136. }
  137. containsRect(rect: Rect): boolean {
  138. return (
  139. this.left <= rect.left &&
  140. rect.right <= this.right &&
  141. this.top <= rect.top &&
  142. rect.bottom <= this.bottom
  143. );
  144. }
  145. leftOverlapsWith(rect: Rect): boolean {
  146. return rect.left <= this.left && rect.right >= this.left;
  147. }
  148. rightOverlapsWith(rect: Rect): boolean {
  149. return this.right >= rect.left && this.right <= rect.right;
  150. }
  151. overlapsX(other: Rect): boolean {
  152. return this.left <= other.right && this.right >= other.left;
  153. }
  154. overlapsY(other: Rect): boolean {
  155. return this.top <= other.bottom && this.bottom >= other.top;
  156. }
  157. overlaps(other: Rect): boolean {
  158. return this.overlapsX(other) && this.overlapsY(other);
  159. }
  160. transformRect(transform: mat3 | Readonly<mat3> | null): Rect {
  161. if (!transform) {
  162. return this.clone();
  163. }
  164. const x = this.x * transform[0] + this.y * transform[3] + transform[6];
  165. const y = this.x * transform[1] + this.y * transform[4] + transform[7];
  166. const width = this.width * transform[0] + this.height * transform[3];
  167. const height = this.width * transform[1] + this.height * transform[4];
  168. return new Rect(
  169. x + (width < 0 ? width : 0),
  170. y + (height < 0 ? height : 0),
  171. Math.abs(width),
  172. Math.abs(height)
  173. );
  174. }
  175. /**
  176. * Returns a transform that inverts the y axis within the rect.
  177. * This causes the bottom of the rect to be the top of the rect and vice versa.
  178. */
  179. invertYTransform(): mat3 {
  180. return mat3.fromValues(1, 0, 0, 0, -1, 0, 0, this.y * 2 + this.height, 1);
  181. }
  182. withHeight(height: number): Rect {
  183. return new Rect(this.x, this.y, this.width, height);
  184. }
  185. withWidth(width: number): Rect {
  186. return new Rect(this.x, this.y, width, this.height);
  187. }
  188. withX(x: number): Rect {
  189. return new Rect(x, this.y, this.width, this.height);
  190. }
  191. withY(y: number) {
  192. return new Rect(this.x, y, this.width, this.height);
  193. }
  194. toBounds(): [number, number, number, number] {
  195. return [this.x, this.y, this.x + this.width, this.y + this.height];
  196. }
  197. toArray(): [number, number, number, number] {
  198. return [this.x, this.y, this.width, this.height];
  199. }
  200. between(to: Rect): Rect {
  201. return new Rect(to.x, to.y, to.width / this.width, to.height / this.height);
  202. }
  203. translate(x: number, y: number): Rect {
  204. return new Rect(x, y, this.width, this.height);
  205. }
  206. translateX(x: number): Rect {
  207. return new Rect(x, this.y, this.width, this.height);
  208. }
  209. translateY(y: number): Rect {
  210. return new Rect(this.x, y, this.width, this.height);
  211. }
  212. scaleX(x: number): Rect {
  213. return new Rect(this.x, this.y, this.width * x, this.height);
  214. }
  215. scaleY(y: number): Rect {
  216. return new Rect(this.x, this.y, this.width, this.height * y);
  217. }
  218. scale(x: number, y: number): Rect {
  219. return new Rect(this.x * x, this.y * y, this.width * x, this.height * y);
  220. }
  221. scaleOriginBy(x: number, y: number): Rect {
  222. return new Rect(this.x * x, this.y * y, this.width, this.height);
  223. }
  224. scaledBy(x: number, y: number): Rect {
  225. return new Rect(this.x, this.y, this.width * x, this.height * y);
  226. }
  227. equals(rect: Rect): boolean {
  228. if (this.x !== rect.x) {
  229. return false;
  230. }
  231. if (this.y !== rect.y) {
  232. return false;
  233. }
  234. if (this.width !== rect.width) {
  235. return false;
  236. }
  237. if (this.height !== rect.height) {
  238. return false;
  239. }
  240. return true;
  241. }
  242. notEqualTo(rect: Rect): boolean {
  243. return !this.equals(rect);
  244. }
  245. }
  246. export function findRangeBinarySearch(
  247. {low, high}: {high: number; low: number},
  248. fn: (val: number) => number,
  249. target: number,
  250. precision = 1
  251. ): [number, number] {
  252. // eslint-disable-next-line
  253. while (true) {
  254. if (high - low <= precision) {
  255. return [low, high];
  256. }
  257. const mid = (high + low) / 2;
  258. if (fn(mid) < target) {
  259. low = mid;
  260. } else {
  261. high = mid;
  262. }
  263. }
  264. }
  265. export const fract = (x: number): number => x - Math.floor(x);
  266. export const triangle = (x: number): number => 2.0 * Math.abs(fract(x) - 0.5) - 1.0;
  267. export function fromLumaChromaHue(L: number, C: number, H: number): ColorChannels {
  268. const hPrime = H / 60;
  269. const X = C * (1 - Math.abs((hPrime % 2) - 1));
  270. const [R1, G1, B1] =
  271. hPrime < 1
  272. ? [C, X, 0]
  273. : hPrime < 2
  274. ? [X, C, 0]
  275. : hPrime < 3
  276. ? [0, C, X]
  277. : hPrime < 4
  278. ? [0, X, C]
  279. : hPrime < 5
  280. ? [X, 0, C]
  281. : [C, 0, X];
  282. const m = L - (0.35 * R1 + 0.35 * G1 + 0.35 * B1);
  283. return [clamp(R1 + m, 0, 1), clamp(G1 + m, 0, 1), clamp(B1 + m, 0, 1.0)];
  284. }
  285. // Modified to allow only a part of the spectrum
  286. export function makeColorBucketTheme(
  287. lch: LCH,
  288. spectrum = 360,
  289. offset = 0
  290. ): (t: number) => ColorChannels {
  291. return t => {
  292. const x = triangle(30.0 * t);
  293. const tx = 0.9 * t;
  294. const H = spectrum < 360 ? offset + spectrum * tx : spectrum * tx;
  295. const C = lch.C_0 + lch.C_d * x;
  296. const L = lch.L_0 - lch.L_d * x;
  297. return fromLumaChromaHue(L, C, H);
  298. };
  299. }
  300. export function trimTextCenter(text: string, low: number): TrimTextCenter {
  301. if (low >= text.length) {
  302. return {
  303. text,
  304. start: 0,
  305. end: 0,
  306. length: 0,
  307. };
  308. }
  309. const prefixLength = Math.floor(low / 2);
  310. // Use 1 character less than the low value to account for ellipsis and favor displaying the prefix
  311. const postfixLength = low - prefixLength - 1;
  312. const start = prefixLength;
  313. const end = Math.floor(text.length - postfixLength + ELLIPSIS.length);
  314. const trimText = `${text.substring(0, start)}${ELLIPSIS}${text.substring(end)}`;
  315. return {
  316. text: trimText,
  317. start,
  318. end,
  319. length: end - start,
  320. };
  321. }
  322. // @TODO drop when we move to sampled profile
  323. export interface SpeedscopeSchema {
  324. profiles: ReadonlyArray<
  325. Readonly<Profiling.EventedProfile | Profiling.SampledProfile | JSSelfProfiling.Trace>
  326. >;
  327. shared: {
  328. frames: ReadonlyArray<Omit<Profiling.FrameInfo, 'key'>>;
  329. profile_ids?: ReadonlyArray<string>;
  330. };
  331. activeProfileIndex?: number;
  332. }
  333. // This differs, but the underlying logic is similar, we just support a mat3 as the transform
  334. // because our charts can be offset on the x axis.
  335. export function computeInterval(
  336. configView: Rect,
  337. logicalSpaceToConfigView: mat3,
  338. getInterval: (mat: mat3, x: number) => number
  339. ): number[] {
  340. const target = 200;
  341. const targetInterval = getInterval(logicalSpaceToConfigView, target) - configView.left;
  342. const minInterval = Math.pow(10, Math.floor(Math.log10(targetInterval)));
  343. let interval = minInterval;
  344. if (targetInterval / interval > 5) {
  345. interval *= 5;
  346. } else if (targetInterval / interval > 2) {
  347. interval *= 2;
  348. }
  349. let x = Math.ceil(configView.left / interval) * interval;
  350. const intervals: number[] = [];
  351. while (x <= configView.right) {
  352. intervals.push(x);
  353. x += interval;
  354. }
  355. return intervals;
  356. }