traceTextMeasurer.tsx 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. import theme from 'sentry/utils/theme';
  2. export class TraceTextMeasurer {
  3. queue: string[] = [];
  4. drainRaf: number | null = null;
  5. cache: Map<string, number> = new Map();
  6. number: number = 0;
  7. dot: number = 0;
  8. duration: Record<string, number> = {};
  9. constructor() {
  10. this.drain = this.drain.bind(this);
  11. const canvas = document.createElement('canvas');
  12. const ctx = canvas.getContext('2d');
  13. if (!ctx) {
  14. for (const duration of ['ns', 'ms', 's', 'm', 'min', 'h', 'd']) {
  15. // If for some reason we fail to create a canvas context, we can
  16. // use a fallback value for the durations. It shouldnt happen,
  17. // but it's better to have a fallback than to crash the entire app.
  18. // I've made a couple manual measurements to determine a good fallback
  19. // and 6.5px per letter seems like a reasonable approximation.
  20. const PX_PER_LETTER = 6.5;
  21. this.duration[duration] = duration.length * PX_PER_LETTER;
  22. }
  23. return;
  24. }
  25. canvas.width = 50 * window.devicePixelRatio ?? 1;
  26. canvas.height = 50 * window.devicePixelRatio ?? 1;
  27. ctx.font = '11px' + theme.text.family;
  28. this.dot = ctx.measureText('.').width;
  29. for (let i = 0; i < 10; i++) {
  30. const measurement = ctx.measureText(i.toString());
  31. this.number = Math.max(this.number, measurement.width);
  32. }
  33. for (const duration of ['ns', 'ms', 's', 'm', 'min', 'h', 'd']) {
  34. this.duration[duration] = ctx.measureText(duration).width;
  35. }
  36. }
  37. drain() {
  38. for (const string of this.queue) {
  39. this.measure(string);
  40. }
  41. }
  42. computeStringLength(string: string): number {
  43. let width = 0;
  44. for (let i = 0; i < string.length; i++) {
  45. switch (string[i]) {
  46. case '.':
  47. width += this.dot;
  48. break;
  49. case '0':
  50. case '1':
  51. case '2':
  52. case '3':
  53. case '4':
  54. case '5':
  55. case '6':
  56. case '7':
  57. case '8':
  58. case '9':
  59. width += this.number;
  60. break;
  61. default:
  62. const remaining = string.slice(i);
  63. if (this.duration[remaining]) {
  64. width += this.duration[remaining];
  65. return width;
  66. }
  67. }
  68. }
  69. return width;
  70. }
  71. measure(string: string): number {
  72. const cached_width = this.cache.get(string);
  73. if (cached_width !== undefined) {
  74. return cached_width;
  75. }
  76. const width = this.computeStringLength(string);
  77. this.cache.set(string, width);
  78. return width;
  79. }
  80. }