github.com/grafana/pyroscope@v1.18.0/pkg/model/flamegraph.go (about)

     1  package model
     2  
     3  import (
     4  	"sort"
     5  	"sync"
     6  
     7  	"github.com/samber/lo"
     8  
     9  	"github.com/grafana/pyroscope/pkg/og/storage/metadata"
    10  	"github.com/grafana/pyroscope/pkg/og/structs/flamebearer"
    11  
    12  	querierv1 "github.com/grafana/pyroscope/api/gen/proto/go/querier/v1"
    13  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
    14  )
    15  
    16  func NewFlameGraph(t *Tree, maxNodes int64) *querierv1.FlameGraph {
    17  	var total, max int64
    18  	for _, node := range t.root {
    19  		total += node.total
    20  	}
    21  	names := []string{}
    22  	nameLocationCache := map[string]int{}
    23  	res := []*Stack[int64]{}
    24  	defer func() {
    25  		for _, stack := range res {
    26  			stackIntPool.Put(stack)
    27  		}
    28  	}()
    29  
    30  	minVal := t.minValue(maxNodes)
    31  
    32  	stack := stackNodePool.Get().(*Stack[stackNode])
    33  	defer stackNodePool.Put(stack)
    34  	stack.Reset()
    35  	stack.Push(stackNode{xOffset: 0, level: 0, node: &node{children: t.root, total: total}})
    36  
    37  	for {
    38  		current, hasMoreNodes := stack.Pop()
    39  		if !hasMoreNodes {
    40  			break
    41  		}
    42  		if current.node.self > max {
    43  			max = current.node.self
    44  		}
    45  		var i int
    46  		var ok bool
    47  		name := current.node.name
    48  		if i, ok = nameLocationCache[name]; !ok {
    49  			i = len(names)
    50  			if i == 0 {
    51  				name = "total"
    52  			}
    53  			nameLocationCache[name] = i
    54  			names = append(names, name)
    55  		}
    56  
    57  		if current.level == len(res) {
    58  			s := stackIntPool.Get().(*Stack[int64])
    59  			s.Reset()
    60  			res = append(res, s)
    61  		}
    62  
    63  		// i+0 = x offset
    64  		// i+1 = total
    65  		// i+2 = self
    66  		// i+3 = index in names array
    67  		level := res[current.level]
    68  		level.Push(int64(i))
    69  		level.Push((current.node.self))
    70  		level.Push((current.node.total))
    71  		level.Push(int64(current.xOffset))
    72  		current.xOffset += int(current.node.self)
    73  
    74  		otherTotal := int64(0)
    75  		for _, child := range current.node.children {
    76  			if child.total >= minVal && child.name != "other" {
    77  				stack.Push(stackNode{xOffset: current.xOffset, level: current.level + 1, node: child})
    78  				current.xOffset += int(child.total)
    79  			} else {
    80  				otherTotal += child.total
    81  			}
    82  		}
    83  		if otherTotal != 0 {
    84  			child := &node{
    85  				name:   "other",
    86  				parent: current.node,
    87  				self:   otherTotal,
    88  				total:  otherTotal,
    89  			}
    90  			stack.Push(stackNode{xOffset: current.xOffset, level: current.level + 1, node: child})
    91  			current.xOffset += int(child.total)
    92  		}
    93  	}
    94  
    95  	result := make([][]int64, len(res))
    96  	for i := range result {
    97  		result[i] = res[i].Slice()
    98  	}
    99  	// delta encode xoffsets
   100  	for _, l := range result {
   101  		prev := int64(0)
   102  		for i := 0; i < len(l); i += 4 {
   103  			l[i] -= prev
   104  			prev += l[i] + l[i+1]
   105  		}
   106  	}
   107  	levels := make([]*querierv1.Level, len(result))
   108  	for i := range levels {
   109  		levels[i] = &querierv1.Level{
   110  			Values: result[i],
   111  		}
   112  	}
   113  
   114  	return &querierv1.FlameGraph{
   115  		Names:   names,
   116  		Levels:  levels,
   117  		Total:   total,
   118  		MaxSelf: max,
   119  	}
   120  }
   121  
   122  // ExportToFlamebearer exports the flamegraph to a Flamebearer struct.
   123  func ExportToFlamebearer(fg *querierv1.FlameGraph, profileType *typesv1.ProfileType) *flamebearer.FlamebearerProfile {
   124  	if fg == nil {
   125  		fg = &querierv1.FlameGraph{}
   126  	}
   127  	unit := metadata.Units(profileType.SampleUnit)
   128  	sampleRate := uint32(100)
   129  
   130  	switch profileType.SampleType {
   131  	case "inuse_objects", "alloc_objects", "goroutine", "samples":
   132  		unit = metadata.ObjectsUnits
   133  	case "cpu":
   134  		unit = metadata.SamplesUnits
   135  		sampleRate = uint32(1_000_000_000)
   136  
   137  	}
   138  	levels := make([][]int, len(fg.Levels))
   139  	for i := range levels {
   140  		levels[i] = lo.Map(fg.Levels[i].Values, func(v int64, i int) int { return int(v) })
   141  	}
   142  	return &flamebearer.FlamebearerProfile{
   143  		Version: 1,
   144  		FlamebearerProfileV1: flamebearer.FlamebearerProfileV1{
   145  			Flamebearer: flamebearer.FlamebearerV1{
   146  				Names:    fg.Names,
   147  				NumTicks: int(fg.Total),
   148  				MaxSelf:  int(fg.MaxSelf),
   149  				Levels:   levels,
   150  			},
   151  			Metadata: flamebearer.FlamebearerMetadataV1{
   152  				Format:     "single",
   153  				Units:      unit,
   154  				Name:       profileType.SampleType,
   155  				SampleRate: sampleRate,
   156  			},
   157  		},
   158  	}
   159  }
   160  
   161  func ExportDiffToFlamebearer(fg *querierv1.FlameGraphDiff, profileType *typesv1.ProfileType) *flamebearer.FlamebearerProfile {
   162  	// Since a normal flamegraph and a diff are so similar, convert it to reuse the export function
   163  	singleFlamegraph := &querierv1.FlameGraph{
   164  		Names:   fg.Names,
   165  		Levels:  fg.Levels,
   166  		Total:   fg.Total,
   167  		MaxSelf: fg.MaxSelf,
   168  	}
   169  
   170  	fb := ExportToFlamebearer(singleFlamegraph, profileType)
   171  	fb.LeftTicks = uint64(fg.LeftTicks)
   172  	fb.RightTicks = uint64(fg.RightTicks)
   173  	fb.Metadata.Format = "double"
   174  
   175  	return fb
   176  }
   177  
   178  type FlameGraphMerger struct {
   179  	mu sync.Mutex
   180  	t  *Tree
   181  }
   182  
   183  func NewFlameGraphMerger() *FlameGraphMerger {
   184  	return &FlameGraphMerger{t: new(Tree)}
   185  }
   186  
   187  // MergeFlameGraph adds the flame graph stack traces to the resulting
   188  // flame graph. The call is thread-safe, but the resulting flame graph
   189  // or tree should be only accessed after all the samples are merged.
   190  func (m *FlameGraphMerger) MergeFlameGraph(src *querierv1.FlameGraph) {
   191  	m.mu.Lock()
   192  	defer m.mu.Unlock()
   193  	deltaDecoding(src.Levels, 0, 4)
   194  	dst := make([]string, 0, len(src.Levels))
   195  	for i, l := range src.Levels {
   196  		if i == 0 {
   197  			// Skip the root node ("total").
   198  			continue
   199  		}
   200  		for j := 0; j < len(l.Values); j += 4 {
   201  			self := l.Values[j+2]
   202  			if self > 0 {
   203  				dst = buildStack(dst, src, i, j)
   204  				m.t.InsertStack(self, dst...)
   205  			}
   206  		}
   207  	}
   208  }
   209  
   210  func (m *FlameGraphMerger) MergeTreeBytes(src []byte) error {
   211  	t, err := UnmarshalTree(src)
   212  	if err != nil {
   213  		return err
   214  	}
   215  	m.mu.Lock()
   216  	defer m.mu.Unlock()
   217  	m.t.Merge(t)
   218  	return nil
   219  }
   220  
   221  func (m *FlameGraphMerger) Tree() *Tree { return m.t }
   222  
   223  func (m *FlameGraphMerger) FlameGraph(maxNodes int64) *querierv1.FlameGraph {
   224  	t := m.t
   225  	if t == nil {
   226  		t = new(Tree)
   227  	}
   228  	return NewFlameGraph(t, maxNodes)
   229  }
   230  
   231  func deltaDecoding(levels []*querierv1.Level, start, step int) {
   232  	for _, l := range levels {
   233  		prev := int64(0)
   234  		for i := start; i < len(l.Values); i += step {
   235  			delta := l.Values[i] + l.Values[i+1]
   236  			l.Values[i] += prev
   237  			prev += delta
   238  		}
   239  	}
   240  }
   241  
   242  func buildStack(dst []string, f *querierv1.FlameGraph, level, idx int) []string {
   243  	if cap(dst) < level {
   244  		// Actually, it should never be the case, because
   245  		// we know the depth in advance and can allocate
   246  		// dst with the right capacity.
   247  		dst = make([]string, level, level*2)
   248  	} else {
   249  		dst = dst[:level]
   250  	}
   251  	dst[level-1] = f.Names[f.Levels[level].Values[idx+3]]
   252  	x := f.Levels[level].Values[idx]
   253  	for i := level - 1; i > 0; i-- {
   254  		j := sort.Search(len(f.Levels[i].Values)/4, func(j int) bool { return f.Levels[i].Values[j*4] > x }) - 1
   255  		dst[i-1] = f.Names[f.Levels[i].Values[j*4+3]]
   256  		x = f.Levels[i].Values[j*4]
   257  	}
   258  	return dst
   259  }