github.com/grafana/pyroscope@v1.18.0/pkg/og/structs/flamebearer/flamebearer.go (about)

     1  package flamebearer
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  
     7  	"github.com/grafana/pyroscope/pkg/og/storage/heatmap"
     8  	"github.com/grafana/pyroscope/pkg/og/storage/metadata"
     9  	"github.com/grafana/pyroscope/pkg/og/storage/segment"
    10  	"github.com/grafana/pyroscope/pkg/og/storage/tree"
    11  )
    12  
    13  //revive:disable:max-public-structs Config structs
    14  
    15  // swagger:model
    16  // FlamebearerProfile is a versioned flambearer based profile.
    17  // It's the native format both for rendering and file saving (in adhoc mode).
    18  type FlamebearerProfile struct {
    19  	// Version of the data format. No version / version zero is an unformalized format.
    20  	Version uint `json:"version"`
    21  	FlamebearerProfileV1
    22  	Telemetry map[string]interface{} `json:"telemetry,omitempty"`
    23  }
    24  
    25  // swagger:model
    26  // FlamebearerProfileV1 defines the v1 of the profile format
    27  type FlamebearerProfileV1 struct {
    28  	// Flamebearer data.
    29  	// required: true
    30  	Flamebearer FlamebearerV1 `json:"flamebearer"`
    31  	// Metadata associated to the profile.
    32  	// required: true
    33  	Metadata FlamebearerMetadataV1 `json:"metadata"`
    34  	// Timeline associated to the profile, used for continuous profiling only.
    35  	Timeline *FlamebearerTimelineV1            `json:"timeline"`
    36  	Groups   map[string]*FlamebearerTimelineV1 `json:"groups"`
    37  	Heatmap  *Heatmap                          `json:"heatmap"`
    38  	// Number of samples in the left / base profile. Only used in "double" format.
    39  	LeftTicks uint64 `json:"leftTicks,omitempty"`
    40  	// Number of samples in the right / diff profile. Only used in "double" format.
    41  	RightTicks uint64 `json:"rightTicks,omitempty"`
    42  }
    43  
    44  // swagger:model
    45  // FlamebearerV1 defines the actual profiling data.
    46  type FlamebearerV1 struct {
    47  	// Names is the sequence of symbol names.
    48  	// required: true
    49  	Names []string `json:"names"`
    50  	// Levels contains the flamebearer nodes. Each level represents a row in the flamegraph.
    51  	// For each row / level, there's a sequence of values. These values are grouped in chunks
    52  	// which size depend on the flamebearer format: 4 for "single", 7 for "double".
    53  	// For "single" format, each chunk has the following data:
    54  	//     i+0 = x offset (prefix sum of the level total values), delta encoded.
    55  	//     i+1 = total samples (including the samples in its children nodes).
    56  	//     i+2 = self samples (excluding the samples in its children nodes).
    57  	//     i+3 = index in names array
    58  	//
    59  	// For "double" format, each chunk has the following data:
    60  	//     i+0 = x offset (prefix sum of the level total values), delta encoded, base / left tree.
    61  	//     i+1 = total samples (including the samples in its children nodes)   , base / left tree.
    62  	//     i+2 = self samples (excluding the samples in its children nodes)    , base / left tree.
    63  	//     i+3 = x offset (prefix sum of the level total values), delta encoded, diff / right tree.
    64  	//     i+4 = total samples (including the samples in its children nodes)   , diff / right tree.
    65  	//     i+5 = self samples (excluding the samples in its children nodes)    , diff / right tree.
    66  	//     i+6 = index in the names array
    67  	//
    68  	// required: true
    69  	Levels [][]int `json:"levels"`
    70  	// Total number of samples.
    71  	// required: true
    72  	NumTicks int `json:"numTicks"`
    73  	// Maximum self value in any node.
    74  	// required: true
    75  	MaxSelf int `json:"maxSelf"`
    76  }
    77  
    78  type FlamebearerMetadataV1 struct {
    79  	// Data format. Supported values are "single" and "double" (diff).
    80  	// required: true
    81  	Format string `json:"format"`
    82  	// Name of the spy / profiler used to generate the profile, if any.
    83  	SpyName string `json:"spyName"`
    84  	// Sample rate at which the profiler was operating.
    85  	SampleRate uint32 `json:"sampleRate"`
    86  	// The unit of measurement for the profiled data.
    87  	Units metadata.Units `json:"units"`
    88  	// A name that identifies the profile.
    89  	Name string `json:"name"`
    90  }
    91  
    92  type FlamebearerTimelineV1 struct {
    93  	// Time at which the timeline starts, as a Unix timestamp.
    94  	// required: true
    95  	StartTime int64 `json:"startTime"`
    96  	// A sequence of samples starting at startTime, spaced by durationDelta seconds
    97  	// required: true
    98  	Samples []uint64 `json:"samples"`
    99  	// Time delta between samples, in seconds.
   100  	// required: true
   101  	DurationDelta int64         `json:"durationDelta"`
   102  	Watermarks    map[int]int64 `json:"watermarks"`
   103  }
   104  
   105  type Heatmap struct {
   106  	// Values matrix contain values that indicate count of value occurrences,
   107  	// satisfying boundaries of X and Y bins: [StartTime:EndTime) and (MinValue:MaxValue].
   108  	// A value can be accessed via Values[x][y], where:
   109  	//   0 <= x < TimeBuckets, and
   110  	//   0 <= y < ValueBuckets.
   111  	Values [][]uint64 `json:"values"`
   112  	// TimeBuckets denote number of bins on X axis.
   113  	// Length of Values array.
   114  	TimeBuckets int64 `json:"timeBuckets"`
   115  	// ValueBuckets denote number of bins on Y axis.
   116  	// Length of any item in the Values array.
   117  	ValueBuckets int64 `json:"valueBuckets"`
   118  	// StartTime and EndTime indicate boundaries of X axis: [StartTime:EndTime).
   119  	StartTime int64 `json:"startTime"`
   120  	EndTime   int64 `json:"endTime"`
   121  	// MinValue and MaxValue indicate boundaries of Y axis: (MinValue:MaxValue].
   122  	MinValue uint64 `json:"minValue"`
   123  	MaxValue uint64 `json:"maxValue"`
   124  	// MinDepth and MaxDepth indicate boundaries of Z axis: [MinDepth:MaxDepth].
   125  	// MinDepth is the minimal non-zero value that can be found in Values.
   126  	MinDepth uint64 `json:"minDepth"`
   127  	MaxDepth uint64 `json:"maxDepth"`
   128  }
   129  
   130  type ProfileConfig struct {
   131  	Name      string
   132  	MaxNodes  int
   133  	Metadata  metadata.Metadata
   134  	Tree      *tree.Tree
   135  	Timeline  *segment.Timeline
   136  	Heatmap   *heatmap.Heatmap
   137  	Groups    map[string]*segment.Timeline
   138  	Telemetry map[string]interface{}
   139  }
   140  
   141  func NewProfile(in ProfileConfig) FlamebearerProfile {
   142  	fb := in.Tree.FlamebearerStruct(in.MaxNodes)
   143  	return FlamebearerProfile{
   144  		Version:   1,
   145  		Telemetry: in.Telemetry,
   146  		FlamebearerProfileV1: FlamebearerProfileV1{
   147  			Flamebearer: newFlambearer(fb),
   148  			Metadata:    newMetadata(in.Name, fb.Format, in.Metadata),
   149  			Timeline:    newTimeline(in.Timeline),
   150  			Heatmap:     newHeatmap(in.Heatmap),
   151  			Groups:      convertGroups(in.Groups),
   152  		},
   153  	}
   154  }
   155  
   156  func convertGroups(v map[string]*segment.Timeline) map[string]*FlamebearerTimelineV1 {
   157  	res := make(map[string]*FlamebearerTimelineV1)
   158  	for k, v := range v {
   159  		res[k] = newTimeline(v)
   160  	}
   161  	return res
   162  }
   163  
   164  func newFlambearer(fb *tree.Flamebearer) FlamebearerV1 {
   165  	return FlamebearerV1{
   166  		Names:    fb.Names,
   167  		Levels:   fb.Levels,
   168  		NumTicks: fb.NumTicks,
   169  		MaxSelf:  fb.MaxSelf,
   170  	}
   171  }
   172  
   173  func newMetadata(name string, format tree.Format, md metadata.Metadata) FlamebearerMetadataV1 {
   174  	return FlamebearerMetadataV1{
   175  		Name:       name,
   176  		Format:     string(format),
   177  		SpyName:    md.SpyName,
   178  		SampleRate: md.SampleRate,
   179  		Units:      md.Units,
   180  	}
   181  }
   182  
   183  func newTimeline(timeline *segment.Timeline) *FlamebearerTimelineV1 {
   184  	if timeline == nil {
   185  		return nil
   186  	}
   187  	return &FlamebearerTimelineV1{
   188  		StartTime:     timeline.StartTime,
   189  		Samples:       timeline.Samples,
   190  		DurationDelta: timeline.DurationDeltaNormalized,
   191  		Watermarks:    timeline.Watermarks,
   192  	}
   193  }
   194  
   195  func newHeatmap(h *heatmap.Heatmap) *Heatmap {
   196  	if h == nil {
   197  		return nil
   198  	}
   199  	return &Heatmap{
   200  		Values:       h.Values,
   201  		TimeBuckets:  h.TimeBuckets,
   202  		ValueBuckets: h.ValueBuckets,
   203  		StartTime:    h.StartTime.UnixNano(),
   204  		EndTime:      h.EndTime.UnixNano(),
   205  		MinValue:     h.MinValue,
   206  		MaxValue:     h.MaxValue,
   207  		MinDepth:     h.MinDepth,
   208  		MaxDepth:     h.MaxDepth,
   209  	}
   210  }
   211  
   212  func (fb FlamebearerProfile) Validate() error {
   213  	if fb.Version > 1 {
   214  		return fmt.Errorf("unsupported version %d", fb.Version)
   215  	}
   216  	return fb.FlamebearerProfileV1.Validate()
   217  }
   218  
   219  // Validate the V1 profile.
   220  // A custom validation is used as the constraints are hard to define in a generic way
   221  // (e.g. using https://github.com/go-playground/validator)
   222  func (fb FlamebearerProfileV1) Validate() error {
   223  	format := tree.Format(fb.Metadata.Format)
   224  	if format != tree.FormatSingle && format != tree.FormatDouble {
   225  		return fmt.Errorf("unsupported format %s", format)
   226  	}
   227  	if len(fb.Flamebearer.Names) == 0 {
   228  		return fmt.Errorf("a profile must have at least one symbol name")
   229  	}
   230  	if len(fb.Flamebearer.Levels) == 0 {
   231  		return fmt.Errorf("a profile must have at least one profiling level")
   232  	}
   233  	var mod int
   234  	switch format {
   235  	case tree.FormatSingle:
   236  		mod = 4
   237  	case tree.FormatDouble:
   238  		mod = 7
   239  	default: // This shouldn't happen at this point.
   240  		return fmt.Errorf("unsupported format %s", format)
   241  	}
   242  	for _, l := range fb.Flamebearer.Levels {
   243  		if len(l)%mod != 0 {
   244  			return fmt.Errorf("a profile level should have a multiple of %d values, but there's a level with %d values", mod, len(l))
   245  		}
   246  		for i := mod - 1; i < len(l); i += mod {
   247  			if l[i] >= len(fb.Flamebearer.Names) {
   248  				return fmt.Errorf("invalid name index %d, it should be smaller than %d", l[i], len(fb.Flamebearer.Levels))
   249  			}
   250  			if l[i] < 0 {
   251  				return fmt.Errorf("invalid name index %d, it should be a non-negative value", l[i])
   252  			}
   253  		}
   254  	}
   255  	return nil
   256  }
   257  
   258  // ProfileToTree converts a FlamebearerProfile into a Tree
   259  // It currently only supports Single profiles
   260  func ProfileToTree(fb FlamebearerProfile) (*tree.Tree, error) {
   261  	if fb.Metadata.Format != string(tree.FormatSingle) {
   262  		return nil, fmt.Errorf("unsupported flamebearer format %s", fb.Metadata.Format)
   263  	}
   264  	if fb.Version != 1 {
   265  		return nil, fmt.Errorf("unsupported flamebearer version %d", fb.Version)
   266  	}
   267  
   268  	return flamebearerV1ToTree(fb.Flamebearer)
   269  }
   270  
   271  func flamebearerV1ToTree(fb FlamebearerV1) (*tree.Tree, error) {
   272  	t := tree.New()
   273  	deltaDecoding(fb.Levels, 0, 4)
   274  	for i, l := range fb.Levels {
   275  		if i == 0 {
   276  			// Skip the first level: it'll contain the root ("total") node..
   277  			continue
   278  		}
   279  		for j := 0; j < len(l); j += 4 {
   280  			self := l[j+2]
   281  			if self > 0 {
   282  				t.InsertStackString(buildStack(fb, i, j), uint64(self))
   283  			}
   284  		}
   285  	}
   286  	return t, nil
   287  }
   288  
   289  func deltaDecoding(levels [][]int, start, step int) {
   290  	for _, l := range levels {
   291  		prev := 0
   292  		for i := start; i < len(l); i += step {
   293  			delta := l[i] + l[i+1]
   294  			l[i] += prev
   295  			prev += delta
   296  		}
   297  	}
   298  }
   299  
   300  func buildStack(fb FlamebearerV1, level, idx int) []string {
   301  	// The stack will contain names in the range [1, level].
   302  	// Level 0 is not included as its the root ("total") node.
   303  	stack := make([]string, level)
   304  	stack[level-1] = fb.Names[fb.Levels[level][idx+3]]
   305  	x := fb.Levels[level][idx]
   306  	for i := level - 1; i > 0; i-- {
   307  		j := sort.Search(len(fb.Levels[i])/4, func(j int) bool { return fb.Levels[i][j*4] > x }) - 1
   308  		stack[i-1] = fb.Names[fb.Levels[i][j*4+3]]
   309  		x = fb.Levels[i][j*4]
   310  	}
   311  	return stack
   312  }