×
Community Blog How is the Flame Graph Created? Exploring Flame Graphs in Pyroscope Source Code (2)

How is the Flame Graph Created? Exploring Flame Graphs in Pyroscope Source Code (2)

This article mainly introduces the source code analysis of the flame graph part and the model definition part in Pyroscope v0.35.1.

By Zongzheng Xi, from Alibaba Cloud Storage Team

>> How is the Flame Graph Created? Exploring Flame Graphs in Pyroscope Source Code (1)

Analysis of Flame Graph in Pyroscope Source Code

The version of the Pyroscope source code described in this article is v0.35.1. Source code analysis mainly focuses on the flame graph part (/packages/pyroscope-flamegraph) and the model definition part (/packages/pyroscope-models), which is not involved on the collection side for the time being.

Code Structure

pyroscope-flamegraph

--- src
 |--- convert                            ->  Some tools, conversion methods, including diff two profiles, flamebearer conversion method to tree, etc.
 |--- fitMode                            ->  The in-row sorting mode of each row of the table with the flame graph. There are two types: Head First and Tail First.
 |--- FlameGraph                         ->  Flame graph main folder
 | |--- FlameGraphComponent              ->  Flame graph component main folder
 | | |--- ...
 | | |--- color.ts                       ->  Flame graph color hashing strategy and Diff linear gradient color matching logic
 | | |--- colorPalette.ts                ->  Flame graph color palette
 | | |--- constants.ts                   ->  Flame graph canvas displays global configurations, such as the width of each bar and the spacing between them.
 | | |--- ContextMenu.tsx                ->  Menu component popped up by right-click flame graph.
 | | |--- ContextMenuHighlight.tsx       ->  Provides highlighting effect for bar in the right-clicked point (called out ContextMenu) in the flame graph.
 | | |--- DiffLegend.tsx                 ->  Color bar component in the middle of the color wheel configuration (default and color-blind mode) of the flame map Diff.
 | | |--- DiffLegendPaletteDropdown.tsx  ->  Drop-down box component of the palette configuration of the flame graph Diff.
 | | |--- Flamegraph_render.ts           ->  Flame graph core drawing rendering code, based on flamebarear, including canvas drawing logic, zoom logic, collapse logic, and highlight linkage logic.
 | | |--- Flamegraph.ts                  ->  Flame graph core class drives Flamegraph_render, includes all component operation logic implementations, adapts to the binary search (xyToBarIndex) of the flameborder data structure in the call stack, and implements a data controllable component that closely cooperates with canvas.
 | | |--- Header.tsx                     ->  Flame graph title component, which mainly changes title according to unit, and displays DiffLegendPaletteDropdown if it is Diff.
 | | |--- Highlight.tsx                  ->  Flame graph highlight polish, set EventListener for canvas to listen for mouse events and add/remove flame graph bar highlight.
 | | |--- index.tsx                      ->  Flame graph entry component, which connects other related components (such as ContextMenu, Tooltip, and Header), encapsulates the xyToData logic for calling FlameGraph, and passes it to other sub-components.
 | | |--- LogoLink.tsx                   ->  svg logo component of Pyroscope
 | | |--- murmur3.ts                     ->  MurmurHash3 hash algorithm, which can map data of any length to a fixed-length hash value for similar color display of similar call stack layers in flame graphs.
 | | |--- testData.ts                    ->  Data format of flame graph examples, including SimpleTree, ComplexTree, and DiffTree
 | | |--- utils.ts                       ->  Auxiliary method for calculating the ratio of the two parts when calculating Diff.
 | | |--- viewTypes.ts                   ->  The display mode of the flame graph, including 'flamegraph' | 'both' | 'table' | 'sandwih'.
 | |--- decode.ts                        ->  Execute the decode method when the flame graph is mounted/changed/covert, and perform secondary operation on the level of the original data structure.
 | |--- FlameGraphRenderer.tsx           ->  Full-function entry of flame graph, including Toolbar and three forms (table, flame graph, sandwich).
 |--- format                             ->  Unit formatting tool folder
 | |--- format.ts                        ->  Formatter of different units is different, roughly divided into Duration, Objects, Bytes, and Nanoseconds. It is used in Tooltips and tables.
 |--- Tooltip                            ->  Box that appears when hovering.
 | |--- ...
 | |--- FlamegraphTooltip.tsx            ->  Tooltip component used for flame graph, obtains bar data through xyToData, and displays it through Formatter.
 | |--- TableTooltip.tsx                 ->  Tooltip widget used for tables, which obtains data by using the data callback method of the table and displays it by using the Formatter.
 | |--- Tooltip.tsx                      ->  Tooltip component is implemented, and the baselineDataExpression is used to determine whether the hover type is a flame graph or a table.
 |--
- FlamegraphRenderer.tsx             ->  Wrapper for FlameGraph/FlameGraphRenderer.tsx.
 |--- index.tsx                          ->  Expose components such as Flamegraph.
 |--- ProfilerTable.tsx                  ->  Implementation of the flame graph table, including singleRow and DoubleRow (Diff view).
 |--- search.ts                          ->  Search tool class, which determines whether the bar name is the same as the search content.
 |--- SharedQueryInput.tsx               ->  Search box function implementation
 |--- Toolbar.tsx                        ->  Flame graph controls bar implementation. You can switch views and sorting, etc.

pyroscope-models

--- src
 |--- decode.ts        ->  TODO: Ideally, this should be moved to the FlamegraphRenderer component. Now it requires too many changes.
 |--- flamebearer.ts   ->  The data structure of the flame graph in the old version.
 |--- groups.ts        ->  Flame graph main folder
 |--- index.ts         ->  Expose index file.
 |--- profile.ts       ->  The new version of the flame graph data structure (the essence is the same and the new version is driven by zod).
 |--- spyName.ts       ->  SPY constants and data structure definitions in different languages.
 |--- trace.ts         ->  Trace-related schema definition
 |--- units.ts         ->  Unit-related constants and data structure definitions

Flame Graph Data Structure

Single Format

const SimpleTree = {
  version: 1,
  flamebearer: {
    names: [
      'total',
      'runtime.mcall',
      'runtime.park_m',
      'runtime.schedule',
      'runtime.resetspinning',
      'runtime.wakep',
      'runtime.startm',
      'runtime.notewakeup',
      'runtime.semawakeup',
      'runtime.pthread_cond_signal',
      'runtime.findrunnable',
      'runtime.netpoll',
      'runtime.kevent',
      'runtime.main',
      'main.main',
      'github.com/pyroscope-io/client/pyroscope.TagWrapper',
      'runtime/pprof.Do',
      'github.com/pyroscope-io/client/pyroscope.TagWrapper.func1',
      'main.main.func1',
      'main.slowFunction',
      'main.slowFunction.func1',
      'main.work',
      'runtime.asyncPreempt',
      'main.fastFunction',
      'main.fastFunction.func1',
    ],
    levels: [
      [0, 609, 0, 0],
      [0, 606, 0, 13, 0, 3, 0, 1],
      [0, 606, 0, 14, 0, 3, 0, 2],
      [0, 606, 0, 15, 0, 3, 0, 3],
      [0, 606, 0, 16, 0, 1, 0, 10, 0, 2, 0, 4],
      [0, 606, 0, 17, 0, 1, 0, 11, 0, 2, 0, 5],
      [0, 606, 0, 18, 0, 1, 1, 12, 0, 2, 0, 6],
      [0, 100, 0, 23, 0, 506, 0, 19, 1, 2, 0, 7],
      [0, 100, 0, 15, 0, 506, 0, 16, 1, 2, 0, 8],
      [0, 100, 0, 16, 0, 506, 0, 20, 1, 2, 2, 9],
      [0, 100, 0, 17, 0, 506, 493, 21],
      [0, 100, 0, 24, 493, 13, 13, 22],
      [0, 100, 97, 21],
      [97, 3, 3, 22],
    ],
    numTicks: 609,
    maxSelf: 493,
  },
  metadata: {
    appName: 'simple.golang.app.cpu',
    name: 'simple.golang.app.cpu 2022-09-06T12:16:31Z',
    startTime: 1662466591,
    endTime: 1662470191,
    query: 'simple.golang.app.cpu{}',
    maxNodes: 1024,
    format: 'single' as const,
    sampleRate: 100,
    spyName: 'gospy' as const,
    units: 'samples' as const,
  }
};

The data structure is the sample data on the pyroscope github, which means the data structure passed into the flame graph component. The example renders the following effect.

13

Most of the content in the data is easy to understand, as implied by the names. The key is what names and levels mean. This part can be inferred from models/flamebearer.ts in the source code.

14

The levels is a data structure in the shape of a flame graph. It is a two-dimensional array. Each row corresponds to each row in the flame graph. In each row, the four numbers of single-type flame graphs describe a bar. For example, the first row has one bar, and the second row has two bars. Among the four numbers describing the bar, the first column represents offset, which represents the distance from the previous bar to be vacated in the current row. The second column represents the total length of this bar. The third column represents the exclusive (self) length of this bar, which means the sum of the (possibly multi-segment) lengths occupied by itself after all the sub-call stacks of this bar are removed. The fourth column indicates the index of the name array corresponding to the name above the bar.

15

Diff Format

The flame graph in Diff format is similar to that in Single format, but changes from a group of 4 to a group of 7 values. An example set of levels is as follows:

"levels": [
  [0, 20464695, 0, 0, 22639351, 0, 0],
  [
    0, 1573488, 0, 0, 0, 0, 1, 0, 524336, 0, 0, 524336, 0, 2, 0, 1049728, 0,
    0, 524864, 0, 3, 0, 3149185, 0, 0, 3674049, 0, 4, 0, 13643094, 0, 0,
    17391238, 0, 5, 0, 524864, 0, 0, 524864, 0, 6
  ],
  [
    0, 1573488, 0, 0, 0, 0, 7, 0, 524336, 524336, 0, 524336, 524336, 8, 0,
    1049728, 0, 0, 524864, 0, 9, 0, 3149185, 0, 0, 3674049, 0, 10, 0,
    13643094, 0, 0, 17391238, 0, 11, 0, 524864, 0, 0, 524864, 0, 12
  ],
  [
    0, 1573488, 0, 0, 0, 0, 13, 524336, 1049728, 0, 524336, 524864, 0, 14,
    0, 3149185, 0, 0, 3674049, 0, 15, 0, 524361, 524361, 0, 524360, 524360,
    16, 0, 2146687, 0, 0, 5366719, 0, 17, 0, 528394, 528394, 0, 528394,
    528394, 18, 0, 0, 0, 0, 524292, 0, 19, 0, 9387757, 0, 0, 9397105, 0, 20,
    0, 525440, 0, 0, 525440, 0, 21, 0, 0, 0, 0, 524928, 0, 22, 0, 530455, 0,
    0, 0, 0, 23, 0, 524864, 0, 0, 524864, 0, 24
  ],
  [
    0, 1573488, 1573488, 0, 0, 0, 25, 524336, 1049728, 0, 524336, 524864, 0,
    26, 0, 3149185, 0, 0, 3674049, 0, 14, 524361, 2146687, 0, 524360,
    5366719, 0, 27, 528394, 0, 0, 528394, 524292, 0, 28, 0, 9387757, 695248,
    0, 9397105, 695248, 29, 0, 525440, 0, 0, 525440, 0, 30, 0, 0, 0, 0,
    524928, 0, 31, 0, 530455, 0, 0, 0, 0, 32, 0, 524864, 0, 0, 524864, 0, 33
  ],
  [
    2097824, 1049728, 0, 524336, 524864, 0, 34, 0, 3149185, 0, 0, 3674049,
    0, 26, 524361, 2146687, 0, 524360, 5366719, 0, 35, 528394, 0, 0, 528394,
    524292, 0, 36, 695248, 8692509, 789507, 695248, 8701857, 526338, 37, 0,
    525440, 0, 0, 525440, 0, 38, 0, 0, 0, 0, 524928, 0, 39, 0, 530455, 0, 0,
    0, 0, 40, 0, 524864, 0, 0, 524864, 0, 14
  ],
  [
    2097824, 1049728, 0, 524336, 524864, 0, 41, 0, 3149185, 0, 0, 3674049,
    0, 34, 524361, 2146687, 0, 524360, 5366719, 0, 42, 528394, 0, 0, 528394,
    524292, 0, 43, 1484755, 7903001, 7903001, 1221586, 8175519, 8175519, 44,
    0, 525440, 525440, 0, 525440, 525440, 45, 0, 0, 0, 0, 524928, 0, 46, 0,
    530455, 0, 0, 0, 0, 47, 0, 524864, 0, 0, 524864, 0, 48
  ],
  [
    2097824, 1049728, 0, 524336, 524864, 0, 49, 0, 3149185, 0, 0, 3674049,
    0, 41, 524361, 2146687, 0, 524360, 5366719, 0, 50, 528394, 0, 0, 528394,
    524292, 0, 51, 9913197, 0, 0, 9922545, 524928, 0, 52, 0, 530455, 0, 0,
    0, 0, 53, 0, 524864, 0, 0, 524864, 0, 54
  ],
  [
    2097824, 1049728, 1049728, 524336, 524864, 524864, 55, 0, 3149185, 0, 0,
    3674049, 0, 49, 524361, 2146687, 2146687, 524360, 5366719, 5366719, 56,
    528394, 0, 0, 528394, 524292, 524292, 57, 9913197, 0, 0, 9922545,
    524928, 0, 58, 0, 530455, 530455, 0, 0, 0, 59, 0, 524864, 0, 0, 524864,
    0, 41
  ],
  [
    3147552, 3149185, 3149185, 1049200, 3674049, 3674049, 55, 13112639, 0,
    0, 16866310, 524928, 0, 60, 530455, 524864, 0, 0, 524864, 0, 49
  ],
  [
    19409376, 0, 0, 21589559, 524928, 524928, 61, 530455, 524864, 524864, 0,
    524864, 524864, 55
  ]
]

The specific meanings of the seven values in a group are as follows.

Number of Digits Description Combination Calculation
0 leftOffset barOffset = level[0] + level[3]
barTotal = level[1] + level[4]
barTotalDiff = level[4] - level[1]
barSelf = level[2] + level[5]
barSelfDiff = level[5] - level[2]
1 barLeftTotal
2 leftSelf
3 rightOffset
4 barRightTotal
5 rightSelf
6 name_index

Related Source Code

export type Flamebearer = {
  /**
   * List of names
   */
  names: string[];
  /**
   * List of level
   *
   * This is NOT the same as in the flamebearer
   * that we receive from the server.
   * As in there are some transformations required
   * (see deltaDiffWrapper)
   */
  levels: number[][];
  numTicks: number;
  maxSelf: number;

  /**
   * Sample Rate, used in text information
   */
  sampleRate: number;
  units: Units;

  spyName: SpyName;
  // format: 'double' | 'single';
  //  leftTicks?: number;
  //  rightTicks?: number;
} & addTicks;

export type addTicks =
  | { format: 'double'; leftTicks: number; rightTicks: number }
  | { format: 'single' };

export const singleFF = {
  format: 'single' as const,
  jStep: 4,
  jName: 3,
  getBarOffset: (level: number[], j: number) => level[j],
  getBarTotal: (level: number[], j: number) => level[j + 1],
  getBarTotalDiff: (level: number[], j: number) => 0,
  getBarSelf: (level: number[], j: number) => level[j + 2],
  getBarSelfDiff: (level: number[], j: number) => 0,
  getBarName: (level: number[], j: number) => level[j + 3],
};

export const doubleFF = {
  format: 'double' as const,
  jStep: 7,
  jName: 6,
  getBarOffset: (level: number[], j: number) => level[j] + level[j + 3],
  getBarTotal: (level: number[], j: number) => level[j + 4] + level[j + 1],
  getBarTotalLeft: (level: number[], j: number) => level[j + 1],
  getBarTotalRght: (level: number[], j: number) => level[j + 4],
  getBarTotalDiff: (level: number[], j: number) => {
    return level[j + 4] - level[j + 1];
  },
  getBarSelf: (level: number[], j: number) => level[j + 5] + level[j + 2],
  getBarSelfLeft: (level: number[], j: number) => level[j + 2],
  getBarSelfRght: (level: number[], j: number) => level[j + 5],
  getBarSelfDiff: (level: number[], j: number) => level[j + 5] - level[j + 2],
  getBarName: (level: number[], j: number) => level[j + 6],
};

Algorithm Parsing for Obtaining Data from Flame Graph (xyToData)

Maybe Model

Maybe (from true-myth library) is widely used in pyroscope's data model. The problems solved by the model and the common writing methods are briefly described here. For more usage methods and details, see https://true-myth.js.org/.

What pain points did Maybe solve?

Maybe mainly addresses the null/undefined problem. It provides a consistent and defined approach to handle null/undefined throughout the codebase. By encapsulating values in containers, regardless of whether they contain something, operations can be safely executed. These containers allow us to make reliable assumptions about parameter values when writing functions, as we extract the question "does this variable contain a valid value?" to the API boundary, eliminating the need to handle this concern in each function.

I believe that Pyroscope's use of methods like Maybe to drive xyToData demonstrates a consideration for code cleanliness and maintainability.

How to use Maybe?

Let's denote A as the Maybe<T> type value that may or may not exist. If the value exists, it is represented as Just(value). If it doesn't exist, it is Nothing, offering a type-safe container to handle the possibility of null values. Using Maybe allows you to avoid null/undefined checks in your codebase and use it like a worry-free array. TypeScript checks the behavior of this type at compile time, with minimal runtime overhead, mainly due to the small cost of container objects and lightweight wrapping/unwrapping functionality. In terms of usage, Maybe follows a method call rule.

import Maybe from 'true-myth/maybe';

// Construct a `Just` where you have a value to use, and the function accepts
// a `Maybe`.
const aKnownNumber = Maybe.just(12);

// Construct a `Nothing` where you don't have a value to use, but the
// function requires a value (and accepts a `Maybe`).
const aKnownNothing = Maybe.nothing<string>();

// Construct a `Maybe` where you don't know whether the value will exist or
// not, using `of`.
type WhoKnows = { mightBeAThing?: boolean[] };

const whoKnows: WhoKnows = {};
const wrappedWhoKnows = Maybe.of(whoKnows.mightBeAThing);
console.log(toString(wrappedWhoKnows)); // Nothing

const whoElseKnows: WhoKnows = { mightBeAThing: [true, false] };
const wrappedWhoElseKnows = Maybe.of(whoElseKnows.mightBeAThing);
console.log(toString(wrappedWhoElseKnows)); // "Just(true,false)"
import { isVoid } from 'true-myth/utils';
import Maybe, { Just, Nothing } from 'true-myth/maybe';

// Construct a `Just` where you have a value to use, and the function accepts
// a `Maybe`.
const aKnownNumber = new Just(12);

// Once the item is constructed, you can apply methods directly on it.
const fromMappedJust = aKnownNumber.map((x) => x * 2).unwrapOr(0);
console.log(fromMappedJust); // 24

// Construct a `Nothing` where you don't have a value to use, but the
// function requires a value (and accepts a `Maybe<string>`).
const aKnownNothing = new Nothing();

// The same operations will behave safely on a `Nothing` as on a `Just`:
const fromMappedNothing = aKnownNothing.map((x) => x * 2).unwrapOr(0);
console.log(fromMappedNothing); // 0

// Construct a `Maybe` where you don't know whether the value will exist or
// not, using `isVoid` to decide which to construct.
type WhoKnows = { mightBeAThing?: boolean[] };

const whoKnows: WhoKnows = {};
const wrappedWhoKnows = !isVoid(whoKnows.mightBeAThing)
  ? new Just(whoKnows.mightBeAThing)
  : new Nothing();

console.log(wrappedWhoKnows.toString()); // Nothing

const whoElseKnows: WhoKnows = { mightBeAThing: [true, false] };
const wrappedWhoElseKnows = !isVoid(whoElseKnows.mightBeAThing)
  ? new Just(whoElseKnows.mightBeAThing)
  : new Nothing();

console.log(wrappedWhoElseKnows.toString()); // "Just(true,false)"

Component Internal Data Structure and Description

1.  bar: Describes one "bar" in the flame graph. The same line may contain multiple "bars".

2.  Node: Refers to a reference to the bar in the flame graph. The data structure is {i, j}, which is the index of the flame graph.

3.  XYWithinBounds: Indicates the XY coordinates within the canvas range. The data structure is {x, y}, which is the XY of MouseEvent.

4.  this.zoom: The node zoomed in in the current state. Zooming in means the effect of the operation of clicking the flame map with the left button.

16

5.  this.focusedNode: The node that is focused on in the current state. Right-click Collapsed Nodes Above.

17

The Whole Process of Clicking on the Flame Graph

Start by clicking

The OnClick event bound to the canvas is as follows.

const onClick = (e: React.MouseEvent<HTMLCanvasElement>) => {
  const opt = getFlamegraph().xyToBar(
    e.nativeEvent.offsetX,
    e.nativeEvent.offsetY
  );
// opt is the Maybe<XYWithinBounds> object that is retrieved based on the xy position. Then, a bar that contains index, position, and data is constructed based on a series of xyToData methods (described in detail later).
  opt.match({
// Click on an unreasonable position in canvas and do nothing.
    Nothing: () => {},
// If the position is reasonable, retrieve the bar data (including index, position, and data).
    Just: (bar) => {
// Zoom is the currently enlarged Maybe<Node> object (not necessarily opt). Zoom means the effect of the operation of clicking the flame graph with the left button.
      zoom.match({
// If no zoom exists, zoom is executed at the current position (opt).
        Nothing: () => {
          onZoom(opt);
        },
// If there is currently a node z that has been zoomed in, take out the bar data.
        Just: (z) => {
// Determine whether opt and zoom are the same index.
          if (bar.i === z.i && bar.j === z.j) {
// If yes, the cancellation is amplified and restored.
            onZoom(Maybe.nothing());
          } else {
// If no, perform the zoom operation on the currently clicked opt.
            onZoom(opt);
          }
        },
      });
    },
  });
};

xyToIndex

The xyToIndex method is the core method of i, j of the mouse screen x, y into the data structure.

private xyToBarIndex(x: number, y: number) {
  if (x < 0 || y < 0) {
    throw new Error(`x and y must be bigger than 0. x = ${x}, y = ${y}`);
  }

// It means that if the bar on the top in focused mode is clicked, or if the Total in non-focused mode is clicked, { i: 0, j: 0 } is returned.
  if (this.isFocused() && y <= BAR_HEIGHT) {
    return { i: 0, j: 0 };
  }

// When performing collapse (focusing operation), there will be a virtual collapsed node at the top. We need to reduce it here.
  const computedY = this.isFocused() ? y - BAR_HEIGHT : y;

  const compensatedFocusedY = this.focusedNode.mapOrElse(
    () => 0,
    (node) => {
      return node.i <= 0 ? 0 : node.i;
    }
  );

// Think of it as a set of if-else.
  const compensation = this.zoom.match({
    Just: () => {
      return this.focusedNode.match({
        Just: () => {
// There are focus and zoom, mainly focus.
          return compensatedFocusedY;
        },
        Nothing: () => {
// Only zoom
          return 0;
        },
      });
    },

    Nothing: () => {
      return this.focusedNode.match({
        Just: () => {
// Only focus
          return compensatedFocusedY;
        },
        Nothing: () => {
// Neither focus nor zoom
          return 0;
        },
      });
    },
  });

// You can locate the position of i based on the preceding information.
  const i = Math.floor(computedY / PX_PER_LEVEL) + compensation;
  if (i >= 0 && i < this.flamebearer.levels.length) {
    const level = this.flamebearer.levels[i];
    if (!level) {
      throw new Error(`Could not find level: '${i}'`);
    }
// The location of j is found by using a binary search algorithm.
    const j = this.binarySearchLevel(x, level);
    return { i, j };
  }
  return { i: 0, j: 0 };
}

In the xyToIndex method, the position of the flame is calculated by discussing the state classification of the i graph, and then the binary search is carried out in the level where i is located to find j.

// binary search of a block in a stack level
private binarySearchLevel(x: number, level: number[]) {
  const { ff } = this;
  let i = 0;
  let j = level.length - ff.jStep;

  while (i <= j) {
    /* eslint-disable-next-line no-bitwise */
    const m = ff.jStep * ((i / ff.jStep + j / ff.jStep) >> 1);
    const x0 = this.tickToX(ff.getBarOffset(level, m));
    const x1 = this.tickToX(
      ff.getBarOffset(level, m) + ff.getBarTotal(level, m)
    );

    if (x0 <= x && x1 >= x) {
      return x1 - x0 > COLLAPSE_THRESHOLD ? m : -1;
    }
    if (x0 > x) {
      j = m - ff.jStep;
    } else {
      i = m + ff.jStep;
    }
  }
  return -1;
}

The algorithm cleverly combines the characteristics of binary search and flame graph. Please note that the concept of i and j of binary search is distinguished from the concept of i and j of the flame graph. Here, the i and j of the binary search only represent the index of Array represented by a row in flame graph. The binary search is carried out on the Array, but the bar dimension is jumped through jStep (Single jStep = 4; Diff jStep = 7), so that m's landing point must be the start time of bar in the middle of i and j. After M is determined, the relevant bar information can be obtained through getBarTotal and getBarOffset described in the data structure, and then passed into tickToX. The final result is the real x range of the intermediate bar, which is compared with the incoming x range. If it falls in the range, the j-index of bar is determined. Otherwise, the binary search method is continued.

18

tickToX method will not be developed too much here. The judgment logic is complicated, but the judgment principle is similar to that of xyToIndex. It is to classify and discuss zoom and focusedNode, determine the current Range (which may be changed by zoom and focus operations), and then determine the Px occupied by each Tick, which can be calculated.

xyToAnything

With xyToIndex capabilities, along with Maybe and data structures, you can easily expose the ability to get data.

private xyToBarPosition = (xy: XYWithinBounds) => {
    const { ff } = this;
    const { i, j } = this.xyToBarIndex(xy.x, xy.y);

    const topLevel = this.focusedNode.mapOrElse(
      () => 0,
      (node) => (node.i < 0 ? 0 : node.i - 1)
    );

    const level = this.flamebearer.levels[i];
    if (!level) {
      throw new Error(`Could not find level: '${i}'`);
    }
    const posX = Math.max(this.tickToX(ff.getBarOffset(level, j)), 0);

    // lower bound is 0
    const posY = Math.max((i - topLevel) * PX_PER_LEVEL, 0);

    const sw = Math.min(
      this.tickToX(ff.getBarOffset(level, j) + ff.getBarTotal(level, j)) - posX,
      this.getCanvasWidth()
    );

    return {
      x: posX,
      y: posY,
      width: sw,
    };
  };

  private xyToBarData = (xy: XYWithinBounds) => {
    const { i, j } = this.xyToBarIndex(xy.x, xy.y);
    const level = this.flamebearer.levels[i];
    if (!level) {
      throw new Error(`Could not find level: '${i}'`);
    }

    switch (this.flamebearer.format) {
      case 'single': {
        const ff = singleFF;

        return {
          format: 'single' as const,
          name: this.flamebearer.names[ff.getBarName(level, j)],
          self: ff.getBarSelf(level, j),
          offset: ff.getBarOffset(level, j),
          total: ff.getBarTotal(level, j),
        };
      }
      case 'double': {
        const ff = doubleFF;

        return {
          format: 'double' as const,
          barTotal: ff.getBarTotal(level, j),
          totalLeft: ff.getBarTotalLeft(level, j),
          totalRight: ff.getBarTotalRght(level, j),
          totalDiff: ff.getBarTotalDiff(level, j),
          name: this.flamebearer.names[ff.getBarName(level, j)],
        };
      }

      default: {
        throw new Error(`Unsupported type`);
      }
    }
};

The logic is simple and will not be mentioned here.

Log Service-Performance Monitoring for Flame Graph Optimization

The performance monitoring feature of Log Service is developed based on Apache 2.0, which is an open-source protocol of Pyroscope v0.35. It optimizes the ability of integrating log service feature.

Comparison Overview

Pyroscope v0.35.1 SLS
❌ProfileTable A large number of reRender Issues Performance optimization: The rendering performance of the flame graph is about 50% higher than that of the open-source version.
❌You cannot select multiple tags in the same tag. The UI supports fewer tags. Logic optimization: Log service provides more flexible tag selection logic and supports not only the aggregation logic of SUM but also the profile aggregation logic of AVG.
☑️ When the call stack is deep, the table display is lengthy, and the flame graph interaction capability is monotonous. ✅Interaction optimization: Deep stack optimization, search integration, one-click diff, and flame graph interaction menus.
❌No unified integration of related resources. ✅Experience optimization: Integrates the highly interactive and open SLS dashboard ecosystem to provide more imagination.

References

Related Links

Disclaimer: The views expressed herein are for reference only and don't necessarily represent the official views of Alibaba Cloud.

0 1 0
Share on

Alibaba Cloud Community

1,048 posts | 257 followers

You may also like

Comments