github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/storage/storage_exemplars_merge.go (about)

     1  package storage
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math"
     7  	"math/big"
     8  	"time"
     9  
    10  	"github.com/pyroscope-io/pyroscope/pkg/storage/heatmap"
    11  	"github.com/pyroscope-io/pyroscope/pkg/storage/metadata"
    12  	"github.com/pyroscope-io/pyroscope/pkg/storage/segment"
    13  	"github.com/pyroscope-io/pyroscope/pkg/storage/tree"
    14  )
    15  
    16  type MergeExemplarsInput struct {
    17  	AppName    string
    18  	ProfileIDs []string
    19  	StartTime  time.Time
    20  	EndTime    time.Time
    21  
    22  	// FIXME: Not implemented: parameters are ignored.
    23  	ExemplarsSelection ExemplarsSelection
    24  	HeatmapParams      heatmap.HeatmapParams
    25  }
    26  
    27  type MergeExemplarsOutput struct {
    28  	Tree          *tree.Tree
    29  	Count         uint64
    30  	Metadata      metadata.Metadata
    31  	HeatmapSketch heatmap.HeatmapSketch // FIXME: Not implemented: the field is never populated.
    32  	Telemetry     map[string]interface{}
    33  }
    34  
    35  func (s *Storage) MergeExemplars(ctx context.Context, mi MergeExemplarsInput) (out MergeExemplarsOutput, err error) {
    36  	m, err := s.mergeExemplars(ctx, mi)
    37  	if err != nil {
    38  		return out, err
    39  	}
    40  
    41  	out.Tree = m.tree
    42  	out.Count = m.count
    43  	if m.segment != nil {
    44  		out.Metadata = m.segment.GetMetadata()
    45  	}
    46  
    47  	if out.Count > 1 && out.Metadata.AggregationType == metadata.AverageAggregationType {
    48  		out.Tree = out.Tree.Clone(big.NewRat(1, int64(out.Count)))
    49  	}
    50  
    51  	return out, nil
    52  }
    53  
    54  type exemplarsMerge struct {
    55  	tree      *tree.Tree
    56  	count     uint64
    57  	segment   *segment.Segment
    58  	lastEntry *exemplarEntry
    59  }
    60  
    61  func (s *Storage) mergeExemplars(ctx context.Context, mi MergeExemplarsInput) (out exemplarsMerge, err error) {
    62  	out.tree = tree.New()
    63  	startTime := unixNano(mi.StartTime)
    64  	endTime := unixNano(mi.EndTime)
    65  	err = s.exemplars.fetch(ctx, mi.AppName, mi.ProfileIDs, func(e exemplarEntry) error {
    66  		if exemplarMatchesTimeRange(e, startTime, endTime) {
    67  			out.tree.Merge(e.Tree)
    68  			out.count++
    69  			out.lastEntry = &e
    70  		}
    71  		return nil
    72  	})
    73  	if err != nil || out.lastEntry == nil {
    74  		return out, err
    75  	}
    76  	// Note that exemplar entry labels don't contain the app name and profile ID.
    77  	if out.lastEntry.Labels == nil {
    78  		out.lastEntry.Labels = make(map[string]string)
    79  	}
    80  	r, ok := s.segments.Lookup(segment.AppSegmentKey(mi.AppName))
    81  	if !ok {
    82  		return out, fmt.Errorf("no metadata found for app %q", mi.AppName)
    83  	}
    84  	out.segment = r.(*segment.Segment)
    85  	return out, nil
    86  }
    87  
    88  func unixNano(t time.Time) int64 {
    89  	if t.IsZero() {
    90  		return 0
    91  	}
    92  	return t.UnixNano()
    93  }
    94  
    95  // exemplarMatchesTimeRange reports whether the exemplar is eligible for the
    96  // given time range. Potentially, we could take exact fraction and scale the
    97  // exemplar proportionally, in the way we do it in aggregate queries. However,
    98  // with exemplars down-sampling does not seem to be a good idea as it may be
    99  // confusing.
   100  //
   101  // For backward compatibility, an exemplar is considered eligible if the time
   102  // range is not specified, or if the exemplar does not have timestamps.
   103  func exemplarMatchesTimeRange(e exemplarEntry, startTime, endTime int64) bool {
   104  	if startTime == 0 || endTime == 0 || e.StartTime == 0 || e.EndTime == 0 {
   105  		return true
   106  	}
   107  	return !math.IsNaN(overlap(startTime, endTime, e.StartTime, e.EndTime))
   108  }
   109  
   110  // overlap returns the overlap of the ranges
   111  // indicating the exemplar time range fraction.
   112  //
   113  //   query:    from  – until
   114  //   exemplar: start – end
   115  //
   116  // Special cases:
   117  //   +Inf - query matches or includes exemplar
   118  //    NaN - ranges don't overlap
   119  //
   120  func overlap(from, until, start, end int64) float64 {
   121  	span := end - start
   122  	o := min(until, end) - max(from, start)
   123  	switch {
   124  	case o <= 0:
   125  		return math.NaN()
   126  	case o == span:
   127  		return math.Inf(0)
   128  	default:
   129  		return float64(o) / float64(span)
   130  	}
   131  }
   132  
   133  func min(a, b int64) int64 {
   134  	if b < a {
   135  		return b
   136  	}
   137  	return a
   138  }
   139  
   140  func max(a, b int64) int64 {
   141  	if b > a {
   142  		return b
   143  	}
   144  	return a
   145  }