github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/packages/pyroscope-flamegraph/src/format/format.ts (about)

     1  /* eslint-disable max-classes-per-file */
     2  import { Units } from '@pyroscope/models/src/units';
     3  
     4  export function numberWithCommas(x: number): string {
     5    return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
     6  }
     7  
     8  export function formatPercent(ratio: number) {
     9    const percent = ratioToPercent(ratio);
    10    return `${percent}%`;
    11  }
    12  
    13  export function ratioToPercent(ratio: number) {
    14    return Math.round(10000 * ratio) / 100;
    15  }
    16  
    17  export function diffPercent(leftPercent: number, rightPercent: number): number {
    18    // difference between 2 percents
    19    // https://en.wikipedia.org/wiki/Relative_change_and_difference
    20    return ((rightPercent - leftPercent) / leftPercent) * 100;
    21  }
    22  
    23  export function getFormatter(max: number, sampleRate: number, unit: Units) {
    24    switch (unit) {
    25      case 'samples':
    26        return new DurationFormatter(max / sampleRate);
    27      case 'objects':
    28        return new ObjectsFormatter(max);
    29      case 'goroutines':
    30        return new ObjectsFormatter(max);
    31      case 'bytes':
    32        return new BytesFormatter(max);
    33      case 'lock_nanoseconds':
    34        return new NanosecondsFormatter(max);
    35      case 'lock_samples':
    36        return new ObjectsFormatter(max);
    37      case 'trace_samples':
    38        return new DurationFormatter(max / sampleRate, '', true);
    39      case 'exceptions':
    40        return new ObjectsFormatter(max);
    41      default:
    42        console.warn(`Unsupported unit: '${unit}'. Defaulting to ''`);
    43        return new DurationFormatter(max / sampleRate, ' ');
    44    }
    45  }
    46  
    47  // this is a class and not a function because we can save some time by
    48  //   precalculating divider and suffix and not doing it on each iteration
    49  export class DurationFormatter {
    50    divider = 1;
    51  
    52    enableSubsecondPrecision = false;
    53  
    54    suffix = 'second';
    55  
    56    durations: [number, string][] = [
    57      [60, 'minute'],
    58      [60, 'hour'],
    59      [24, 'day'],
    60      [30, 'month'],
    61      [12, 'year'],
    62    ];
    63  
    64    units = '';
    65  
    66    constructor(
    67      maxDur: number,
    68      units?: string,
    69      enableSubsecondPrecision?: boolean
    70    ) {
    71      if (enableSubsecondPrecision) {
    72        this.enableSubsecondPrecision = enableSubsecondPrecision;
    73        this.durations = [[1000, 'ms'], [1000, 'second'], ...this.durations];
    74        this.suffix = `μs`;
    75        maxDur *= 1e6; // Converting seconds to μs
    76      }
    77      this.units = units || '';
    78      // eslint-disable-next-line no-plusplus
    79      for (let i = 0; i < this.durations.length; i++) {
    80        const level = this.durations[i];
    81        if (!level) {
    82          console.warn('Could not calculate level');
    83          break;
    84        }
    85  
    86        if (maxDur >= level[0]) {
    87          this.divider *= level[0];
    88          maxDur /= level[0];
    89          // eslint-disable-next-line prefer-destructuring
    90          this.suffix = level[1];
    91        } else {
    92          break;
    93        }
    94      }
    95    }
    96  
    97    format(samples: number, sampleRate: number, withUnits = true): string {
    98      if (this.enableSubsecondPrecision) {
    99        sampleRate /= 1e6;
   100      }
   101      const n = samples / sampleRate / this.divider;
   102      let nStr = n.toFixed(2);
   103  
   104      if (n === 0) {
   105        nStr = '0.00';
   106      } else if (n >= 0 && n < 0.01) {
   107        nStr = '< 0.01';
   108      } else if (n <= 0 && n > -0.01) {
   109        nStr = '< 0.01';
   110      }
   111  
   112      return withUnits
   113        ? `${nStr} ${
   114            this.units ||
   115            `${this.suffix}${n === 1 || this.suffix.length === 2 ? '' : 's'}`
   116          }`
   117        : nStr;
   118    }
   119  
   120    formatPrecise(samples: number, sampleRate: number) {
   121      if (this.enableSubsecondPrecision) {
   122        sampleRate /= 1e6;
   123      }
   124      const n = samples / sampleRate / this.divider;
   125  
   126      return `${parseFloat(n.toFixed(5))} ${
   127        this.units ||
   128        `${this.suffix}${n === 1 || this.suffix.length === 2 ? '' : 's'}`
   129      }`;
   130    }
   131  }
   132  
   133  // this is a class and not a function because we can save some time by
   134  //   precalculating divider and suffix and not doing it on each iteration
   135  export class NanosecondsFormatter {
   136    divider = 1;
   137  
   138    multiplier = 1;
   139  
   140    suffix = 'second';
   141  
   142    durations: [number, string][] = [
   143      [60, 'minute'],
   144      [60, 'hour'],
   145      [24, 'day'],
   146      [30, 'month'],
   147      [12, 'year'],
   148    ];
   149  
   150    constructor(maxDur: number) {
   151      maxDur /= 1000000000;
   152      // eslint-disable-next-line no-plusplus
   153      for (let i = 0; i < this.durations.length; i++) {
   154        const level = this.durations[i];
   155        if (!level) {
   156          console.warn('Could not calculate level');
   157          break;
   158        }
   159  
   160        if (maxDur >= level[0]) {
   161          this.divider *= level[0];
   162          maxDur /= level[0];
   163          // eslint-disable-next-line prefer-destructuring
   164          this.suffix = level[1];
   165        } else {
   166          break;
   167        }
   168      }
   169    }
   170  
   171    format(samples: number) {
   172      const n = samples / 1000000000 / this.divider;
   173      let nStr = n.toFixed(2);
   174  
   175      if (n >= 0 && n < 0.01) {
   176        nStr = '< 0.01';
   177      } else if (n <= 0 && n > -0.01) {
   178        nStr = '< 0.01';
   179      }
   180  
   181      return `${nStr} ${this.suffix}${n === 1 ? '' : 's'}`;
   182    }
   183  
   184    formatPrecise(samples: number) {
   185      const n = samples / 1000000000 / this.divider;
   186  
   187      return `${parseFloat(n.toFixed(5))} ${this.suffix}${n === 1 ? '' : 's'}`;
   188    }
   189  }
   190  
   191  export class ObjectsFormatter {
   192    divider = 1;
   193  
   194    suffix = '';
   195  
   196    objects: [number, string][] = [
   197      [1000, 'K'],
   198      [1000, 'M'],
   199      [1000, 'G'],
   200      [1000, 'T'],
   201      [1000, 'P'],
   202    ];
   203  
   204    constructor(maxObjects: number) {
   205      // eslint-disable-next-line no-plusplus
   206      for (let i = 0; i < this.objects.length; i++) {
   207        const level = this.objects[i];
   208        if (!level) {
   209          console.warn('Could not calculate level');
   210          break;
   211        }
   212  
   213        if (maxObjects >= level[0]) {
   214          this.divider *= level[0];
   215          maxObjects /= level[0];
   216          // eslint-disable-next-line prefer-destructuring
   217          this.suffix = level[1];
   218        } else {
   219          break;
   220        }
   221      }
   222    }
   223  
   224    // TODO:
   225    // how to indicate that sampleRate doesn't matter?
   226    format(samples: number) {
   227      const n = samples / this.divider;
   228      let nStr = n.toFixed(2);
   229  
   230      if (n >= 0 && n < 0.01) {
   231        nStr = '< 0.01';
   232      } else if (n <= 0 && n > -0.01) {
   233        nStr = '< 0.01';
   234      }
   235      return `${nStr} ${this.suffix}`;
   236    }
   237  
   238    formatPrecise(samples: number) {
   239      const n = samples / this.divider;
   240  
   241      return `${parseFloat(n.toFixed(5))} ${this.suffix}`;
   242    }
   243  }
   244  
   245  export class BytesFormatter {
   246    divider = 1;
   247  
   248    suffix = 'bytes';
   249  
   250    bytes: [number, string][] = [
   251      [1024, 'KB'],
   252      [1024, 'MB'],
   253      [1024, 'GB'],
   254      [1024, 'TB'],
   255      [1024, 'PB'],
   256    ];
   257  
   258    constructor(maxBytes: number) {
   259      // eslint-disable-next-line no-plusplus
   260      for (let i = 0; i < this.bytes.length; i++) {
   261        const level = this.bytes[i];
   262        if (!level) {
   263          console.warn('Could not calculate level');
   264          break;
   265        }
   266  
   267        if (maxBytes >= level[0]) {
   268          this.divider *= level[0];
   269          maxBytes /= level[0];
   270  
   271          // eslint-disable-next-line prefer-destructuring
   272          const suffix = level[1];
   273          if (!suffix) {
   274            console.warn('Could not calculate suffix');
   275            this.suffix = '';
   276          } else {
   277            this.suffix = suffix;
   278          }
   279        } else {
   280          break;
   281        }
   282      }
   283    }
   284  
   285    format(samples: number) {
   286      const n = samples / this.divider;
   287      let nStr = n.toFixed(2);
   288  
   289      if (n >= 0 && n < 0.01) {
   290        nStr = '< 0.01';
   291      } else if (n <= 0 && n > -0.01) {
   292        nStr = '< 0.01';
   293      }
   294  
   295      return `${nStr} ${this.suffix}`;
   296    }
   297  
   298    formatPrecise(samples: number) {
   299      const n = samples / this.divider;
   300  
   301      return `${parseFloat(n.toFixed(5))} ${this.suffix}`;
   302    }
   303  }