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  }