github.com/grafana/pyroscope@v1.18.0/pkg/og/convert/pprof/strprofile/profile.go (about)

     1  package strprofile
     2  
     3  import (
     4  	"encoding/binary"
     5  	"encoding/json"
     6  	"fmt"
     7  	"sort"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/cespare/xxhash/v2"
    12  
    13  	profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
    14  )
    15  
    16  type Options struct {
    17  	NoPrettyPrint bool
    18  	NoDuration    bool
    19  	NoTime        bool
    20  	NoCompact     bool
    21  	IncludeIDs    bool
    22  }
    23  
    24  type Location struct {
    25  	ID      uint64   `json:"id,omitempty"`
    26  	Address string   `json:"address,omitempty"`
    27  	Lines   []Line   `json:"lines,omitempty"`
    28  	Mapping *Mapping `json:"mapping,omitempty"`
    29  }
    30  
    31  type Line struct {
    32  	Function *Function `json:"function"`
    33  	Line     int64     `json:"line,omitempty"`
    34  }
    35  
    36  type Function struct {
    37  	ID         uint64 `json:"id,omitempty"`
    38  	Name       string `json:"name"`
    39  	SystemName string `json:"system_name,omitempty"`
    40  	Filename   string `json:"filename,omitempty"`
    41  	StartLine  int64  `json:"start_line,omitempty"`
    42  }
    43  
    44  type Mapping struct {
    45  	ID       uint64 `json:"id,omitempty"`
    46  	Start    string `json:"start"`
    47  	Limit    string `json:"limit"`
    48  	Offset   string `json:"offset,omitempty"`
    49  	Filename string `json:"filename,omitempty"`
    50  	BuildID  string `json:"build_id,omitempty"`
    51  }
    52  
    53  type SampleType struct {
    54  	Type string `json:"type"`
    55  	Unit string `json:"unit"`
    56  }
    57  
    58  type Label struct {
    59  	Key   string `json:"key"`
    60  	Value string `json:"value"`
    61  }
    62  
    63  type Sample struct {
    64  	Locations []Location `json:"locations,omitempty"`
    65  	Values    []int64    `json:"values"`
    66  	Labels    []Label    `json:"labels,omitempty"`
    67  }
    68  
    69  type Profile struct {
    70  	SampleTypes   []SampleType `json:"sample_types"`
    71  	Samples       []Sample     `json:"samples"`
    72  	TimeNanos     string       `json:"time_nanos,omitempty"`
    73  	DurationNanos string       `json:"duration_nanos,omitempty"`
    74  	Period        string       `json:"period,omitempty"`
    75  }
    76  
    77  type CompactLocation struct {
    78  	ID      uint64   `json:"id,omitempty"`
    79  	Address string   `json:"address,omitempty"`
    80  	Lines   []string `json:"lines,omitempty"`
    81  	Mapping string   `json:"mapping,omitempty"`
    82  }
    83  
    84  type CompactSample struct {
    85  	Locations []CompactLocation `json:"locations,omitempty"`
    86  	Values    string            `json:"values"`
    87  	Labels    string            `json:"labels,omitempty"`
    88  }
    89  
    90  type CompactProfile struct {
    91  	SampleTypes   []SampleType    `json:"sample_types"`
    92  	Samples       []CompactSample `json:"samples"`
    93  	TimeNanos     string          `json:"time_nanos,omitempty"`
    94  	DurationNanos string          `json:"duration_nanos,omitempty"`
    95  	Period        string          `json:"period,omitempty"`
    96  }
    97  
    98  func Stringify(p *profilev1.Profile, opts Options) (string, error) {
    99  	var err error
   100  	var sp interface{}
   101  
   102  	if !opts.NoCompact {
   103  		sp = ToCompactProfile(p, opts)
   104  	} else {
   105  		sp = ToDetailedProfile(p, opts)
   106  	}
   107  
   108  	var jsonData []byte
   109  	if !opts.NoPrettyPrint {
   110  		jsonData, err = json.MarshalIndent(sp, "", "  ")
   111  	} else {
   112  		jsonData, err = json.Marshal(sp)
   113  	}
   114  	if err != nil {
   115  		return "", err
   116  	}
   117  
   118  	return string(jsonData), nil
   119  }
   120  
   121  func ToDetailedProfile(p *profilev1.Profile, opts Options) Profile {
   122  	sp := Profile{
   123  		Period: fmt.Sprintf("%d", p.Period),
   124  	}
   125  	if !opts.NoTime {
   126  		sp.TimeNanos = fmt.Sprintf("%d", p.TimeNanos)
   127  	}
   128  	if !opts.NoDuration {
   129  		sp.DurationNanos = fmt.Sprintf("%d", p.DurationNanos)
   130  	}
   131  
   132  	for _, st := range p.SampleType {
   133  		sp.SampleTypes = append(sp.SampleTypes, SampleType{
   134  			Type: p.StringTable[st.Type],
   135  			Unit: p.StringTable[st.Unit],
   136  		})
   137  	}
   138  
   139  	// Create maps for quick lookups
   140  	functionMap := make(map[uint64]*profilev1.Function)
   141  	for _, f := range p.Function {
   142  		functionMap[f.Id] = f
   143  	}
   144  
   145  	mappingMap := make(map[uint64]*profilev1.Mapping)
   146  	for _, m := range p.Mapping {
   147  		mappingMap[m.Id] = m
   148  	}
   149  
   150  	for _, sample := range p.Sample {
   151  		ss := Sample{
   152  			Values: sample.Value,
   153  		}
   154  
   155  		for _, locID := range sample.LocationId {
   156  			loc := findLocation(p.Location, locID)
   157  			if loc == nil {
   158  				continue
   159  			}
   160  
   161  			sLoc := Location{
   162  				Address: fmt.Sprintf("0x%x", loc.Address),
   163  			}
   164  			if opts.IncludeIDs {
   165  				sLoc.ID = loc.Id
   166  			}
   167  
   168  			if loc.MappingId != 0 {
   169  				if mapping := mappingMap[loc.MappingId]; mapping != nil {
   170  					m := &Mapping{
   171  						Start:    fmt.Sprintf("0x%x", mapping.MemoryStart),
   172  						Limit:    fmt.Sprintf("0x%x", mapping.MemoryLimit),
   173  						Offset:   fmt.Sprintf("0x%x", mapping.FileOffset),
   174  						Filename: p.StringTable[mapping.Filename],
   175  						BuildID:  p.StringTable[mapping.BuildId],
   176  					}
   177  					if opts.IncludeIDs {
   178  						m.ID = mapping.Id
   179  					}
   180  					sLoc.Mapping = m
   181  				}
   182  			}
   183  
   184  			for _, line := range loc.Line {
   185  				if fn := functionMap[line.FunctionId]; fn != nil {
   186  					f := &Function{
   187  						Name:       p.StringTable[fn.Name],
   188  						SystemName: p.StringTable[fn.SystemName],
   189  						Filename:   p.StringTable[fn.Filename],
   190  						StartLine:  fn.StartLine,
   191  					}
   192  					if opts.IncludeIDs {
   193  						f.ID = fn.Id
   194  					}
   195  					sLine := Line{
   196  						Function: f,
   197  						Line:     line.Line,
   198  					}
   199  					sLoc.Lines = append(sLoc.Lines, sLine)
   200  				}
   201  			}
   202  
   203  			ss.Locations = append(ss.Locations, sLoc)
   204  		}
   205  
   206  		if len(sample.Label) > 0 {
   207  			ss.Labels = make([]Label, 0, len(sample.Label))
   208  			for _, label := range sample.Label {
   209  				key := p.StringTable[label.Key]
   210  				var value string
   211  				if label.Str != 0 {
   212  					value = p.StringTable[label.Str]
   213  				} else {
   214  					value = strconv.FormatInt(label.Num, 10)
   215  				}
   216  				ss.Labels = append(ss.Labels, Label{
   217  					Key:   key,
   218  					Value: value,
   219  				})
   220  			}
   221  		}
   222  
   223  		sp.Samples = append(sp.Samples, ss)
   224  	}
   225  	return sp
   226  }
   227  
   228  func ToCompactProfile(p *profilev1.Profile, opts Options) CompactProfile {
   229  	sp := CompactProfile{
   230  		Period: fmt.Sprintf("%d", p.Period),
   231  	}
   232  	if !opts.NoTime {
   233  		sp.TimeNanos = fmt.Sprintf("%d", p.TimeNanos)
   234  	}
   235  	if !opts.NoDuration {
   236  		sp.DurationNanos = fmt.Sprintf("%d", p.DurationNanos)
   237  	}
   238  
   239  	for _, st := range p.SampleType {
   240  		sp.SampleTypes = append(sp.SampleTypes, SampleType{
   241  			Type: p.StringTable[st.Type],
   242  			Unit: p.StringTable[st.Unit],
   243  		})
   244  	}
   245  
   246  	functionMap := make(map[uint64]*profilev1.Function)
   247  	for _, f := range p.Function {
   248  		functionMap[f.Id] = f
   249  	}
   250  
   251  	mappingMap := make(map[uint64]*profilev1.Mapping)
   252  	for _, m := range p.Mapping {
   253  		mappingMap[m.Id] = m
   254  	}
   255  
   256  	for _, sample := range p.Sample {
   257  		values := make([]string, len(sample.Value))
   258  		for i, v := range sample.Value {
   259  			values[i] = strconv.FormatInt(v, 10)
   260  		}
   261  
   262  		ss := CompactSample{
   263  			Values: strings.Join(values, ","),
   264  		}
   265  
   266  		for _, locID := range sample.LocationId {
   267  			loc := findLocation(p.Location, locID)
   268  			if loc == nil {
   269  				continue
   270  			}
   271  
   272  			sLoc := CompactLocation{
   273  				Address: fmt.Sprintf("0x%x", loc.Address),
   274  			}
   275  			if opts.IncludeIDs {
   276  				sLoc.ID = loc.Id
   277  			}
   278  
   279  			if loc.MappingId != 0 {
   280  				if mapping := mappingMap[loc.MappingId]; mapping != nil {
   281  					idStr := ""
   282  					if opts.IncludeIDs {
   283  						idStr = fmt.Sprintf("[id=%d]", mapping.Id)
   284  					}
   285  					sLoc.Mapping = fmt.Sprintf("0x%x-0x%x@0x%x %s(%s)%s",
   286  						mapping.MemoryStart,
   287  						mapping.MemoryLimit,
   288  						mapping.FileOffset,
   289  						p.StringTable[mapping.Filename],
   290  						p.StringTable[mapping.BuildId],
   291  						idStr)
   292  				}
   293  			}
   294  
   295  			for _, line := range loc.Line {
   296  				if fn := functionMap[line.FunctionId]; fn != nil {
   297  					idStr := ""
   298  					if opts.IncludeIDs {
   299  						idStr = fmt.Sprintf("[id=%d]", fn.Id)
   300  					}
   301  					lineStr := fmt.Sprintf("%s[%s]@%s:%d%s",
   302  						p.StringTable[fn.Name],
   303  						p.StringTable[fn.SystemName],
   304  						p.StringTable[fn.Filename],
   305  						line.Line,
   306  						idStr)
   307  					sLoc.Lines = append(sLoc.Lines, lineStr)
   308  				}
   309  			}
   310  
   311  			ss.Locations = append(ss.Locations, sLoc)
   312  		}
   313  
   314  		if len(sample.Label) > 0 {
   315  			labels := make([]string, 0, len(sample.Label))
   316  			for _, label := range sample.Label {
   317  				key := p.StringTable[label.Key]
   318  				var value string
   319  				if label.Str != 0 {
   320  					value = p.StringTable[label.Str]
   321  				} else {
   322  					value = strconv.FormatInt(label.Num, 10)
   323  				}
   324  				labels = append(labels, fmt.Sprintf("%s=%s", key, value))
   325  			}
   326  			sort.Strings(labels)
   327  			ss.Labels = strings.Join(labels, ",")
   328  		}
   329  
   330  		sp.Samples = append(sp.Samples, ss)
   331  	}
   332  	return sp
   333  }
   334  
   335  func findLocation(locations []*profilev1.Location, id uint64) *profilev1.Location {
   336  	for _, loc := range locations {
   337  		if loc.Id == id {
   338  			return loc
   339  		}
   340  	}
   341  	return nil
   342  }
   343  
   344  func SortProfileSamples(p CompactProfile) {
   345  	h := xxhash.New()
   346  	s := &sortedSample{
   347  		samples: p.Samples,
   348  		hashes:  make([]uint64, len(p.Samples)),
   349  	}
   350  	for i := range s.samples {
   351  		s.hashes[i] = sampleHash(h, s.samples[i])
   352  	}
   353  	sort.Sort(s)
   354  }
   355  
   356  type sortedSample struct {
   357  	samples []CompactSample
   358  	hashes  []uint64
   359  }
   360  
   361  func (s *sortedSample) Len() int { return len(s.samples) }
   362  
   363  func (s *sortedSample) Less(i, j int) bool { return s.hashes[i] < s.hashes[j] }
   364  
   365  func (s *sortedSample) Swap(i, j int) {
   366  	s.samples[i], s.samples[j] = s.samples[j], s.samples[i]
   367  	s.hashes[i], s.hashes[j] = s.hashes[j], s.hashes[i]
   368  }
   369  
   370  func sampleHash(d *xxhash.Digest, s CompactSample) uint64 {
   371  	d.Reset()
   372  	tmp := make([]byte, 8)
   373  	_, _ = d.WriteString(s.Labels)
   374  	_, _ = d.WriteString(s.Values)
   375  	for i := range s.Locations {
   376  		binary.LittleEndian.PutUint64(tmp, s.Locations[i].ID)
   377  		_, _ = d.Write(tmp)
   378  		_, _ = d.WriteString(s.Locations[i].Address)
   379  		_, _ = d.WriteString(s.Locations[i].Mapping)
   380  		for j := range s.Locations[i].Lines {
   381  			_, _ = d.WriteString(s.Locations[i].Lines[j])
   382  		}
   383  	}
   384  	return d.Sum64()
   385  }