github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/packages/pyroscope-flamegraph/src/FlameGraph/FlameGraphComponent/color.ts (about) 1 /* eslint-disable camelcase */ 2 import Color from 'color'; 3 import { scaleLinear } from 'd3-scale'; 4 import type { SpyName } from '@pyroscope/models/src'; 5 import murmurhash3_32_gc from './murmur3'; 6 import type { FlamegraphPalette } from './colorPalette'; 7 8 export const defaultColor = Color.rgb(148, 142, 142); 9 export const diffColorRed = Color.rgb(200, 0, 0); 10 export const diffColorGreen = Color.rgb(0, 170, 0); 11 12 export const highlightColor = Color('#48CE73'); 13 14 export function colorBasedOnDiffPercent( 15 palette: FlamegraphPalette, 16 leftPercent: number, 17 rightPercent: number 18 ) { 19 const result = diffPercent(leftPercent, rightPercent); 20 const color = NewDiffColor(palette); 21 return color(result); 22 } 23 24 // TODO move to a different file 25 // difference between 2 percents 26 export function diffPercent(leftPercent: number, rightPercent: number) { 27 if (leftPercent === rightPercent) { 28 return 0; 29 } 30 31 if (leftPercent === 0) { 32 return 100; 33 } 34 35 // https://en.wikipedia.org/wiki/Relative_change_and_difference 36 const result = ((rightPercent - leftPercent) / leftPercent) * 100; 37 38 if (result > 100) { 39 return 100; 40 } 41 if (result < -100) { 42 return -100; 43 } 44 45 return result; 46 } 47 48 export function colorFromPercentage(p: number, alpha: number) { 49 // calculated by drawing a line (https://en.wikipedia.org/wiki/Line_drawing_algorithm) 50 // where p1 = (0, 180) and p2 = (100, 0) 51 // where x is the absolute percentage 52 // and y is the color variation 53 let v = 180 - 1.8 * Math.abs(p); 54 55 if (v > 200) { 56 v = 200; 57 } 58 59 // red 60 if (p > 0) { 61 return Color.rgb(200, v, v).alpha(alpha); 62 } 63 // green 64 if (p < 0) { 65 return Color.rgb(v, 200, v).alpha(alpha); 66 } 67 // grey 68 return Color.rgb(200, 200, 200).alpha(alpha); 69 } 70 71 export function colorGreyscale(v: number, a: number) { 72 return Color.rgb(v, v, v).alpha(a); 73 } 74 75 function spyToRegex(spyName: SpyName): RegExp { 76 // eslint-disable-next-line default-case 77 switch (spyName) { 78 case 'dotnetspy': 79 return /^(?<packageName>.+)\.(.+)\.(.+)\(.*\)$/; 80 // TODO: come up with a clever heuristic 81 case 'ebpfspy': 82 return /^(?<packageName>.+)$/; 83 // tested with pyroscope stacktraces here: https://regex101.com/r/99KReq/1 84 case 'gospy': 85 return /^(?<packageName>.*?\/.*?\.|.*?\.|.+)(?<functionName>.*)$/; 86 // assume scrape is golang, since that's the only language we support right now 87 case 'scrape': 88 return /^(?<packageName>.*?\/.*?\.|.*?\.|.+)(?<functionName>.*)$/; 89 case 'phpspy': 90 return /^(?<packageName>(.*\/)*)(?<filename>.*\.php+)(?<line_info>.*)$/; 91 case 'pyspy': 92 return /^(?<packageName>(.*\/)*)(?<filename>.*\.py+)(?<line_info>.*)$/; 93 case 'rbspy': 94 return /^(?<packageName>(.*\/)*)(?<filename>.*\.rb+)(?<line_info>.*)$/; 95 case 'nodespy': 96 return /^(\.\/node_modules\/)?(?<packageName>[^/]*)(?<filename>.*\.?(jsx?|tsx?)?):(?<functionName>.*):(?<line_info>.*)$/; 97 case 'tracing': 98 return /^(?<packageName>.+?):.*$/; 99 case 'javaspy': 100 // TODO: we might want to add ? after groups 101 return /^(?<packageName>.+\/)(?<filename>.+\.)(?<functionName>.+)$/; 102 case 'pyroscope-rs': 103 return /^(?<packageName>[^::]+)/; 104 case 'unknown': 105 return /^(?<packageName>.+)$/; 106 } 107 108 return /^(?<packageName>.+)$/; 109 } 110 111 // TODO spy names? 112 export function getPackageNameFromStackTrace( 113 spyName: SpyName, 114 stackTrace: string 115 ) { 116 if (stackTrace.length === 0) { 117 return stackTrace; 118 } 119 const regexp = spyToRegex(spyName); 120 const fullStackGroups = stackTrace.match(regexp); 121 if (fullStackGroups && fullStackGroups.groups) { 122 return fullStackGroups.groups['packageName'] || ''; 123 } 124 return stackTrace; 125 } 126 127 export function colorBasedOnPackageName( 128 palette: FlamegraphPalette, 129 name: string 130 ) { 131 const hash = murmurhash3_32_gc(name, 0); 132 const colorIndex = hash % palette.colors.length; 133 const baseClr = palette.colors[colorIndex]; 134 if (!baseClr) { 135 console.warn('Could not calculate color. Defaulting to the first one'); 136 // We assert to Color since the first position is always available 137 return palette.colors[0]; 138 } 139 140 return baseClr; 141 } 142 143 /** 144 * NewDiffColor constructs a function that given a number from -100 to 100 145 * it returns the color for that number in a linear scale 146 * encoded in rgb 147 */ 148 export function NewDiffColor( 149 props: Omit<FlamegraphPalette, 'colors'> 150 ): (n: number) => Color { 151 const { goodColor, neutralColor, badColor } = props; 152 153 const color = scaleLinear() 154 .domain([-100, 0, 100]) 155 // TODO types from DefinitelyTyped seem to mismatch 156 .range([ 157 goodColor.rgb().toString(), 158 neutralColor.rgb().toString(), 159 badColor.rgb().toString(), 160 ] as ShamefulAny); 161 162 return (n: number) => { 163 // convert to our Color object 164 // since that's what users are expecting to use 165 return Color(color(n).toString()); 166 }; 167 }