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 }