github.com/grafana/pyroscope@v1.18.0/pkg/util/time.go (about)

     1  // SPDX-License-Identifier: AGPL-3.0-only
     2  // Provenance-includes-location: https://github.com/cortexproject/cortex/blob/master/pkg/util/time.go
     3  // Provenance-includes-license: Apache-2.0
     4  // Provenance-includes-copyright: The Cortex Authors.
     5  
     6  package util
     7  
     8  import (
     9  	"math"
    10  	"math/rand"
    11  	"net/http"
    12  	"sort"
    13  	"strconv"
    14  	"time"
    15  
    16  	"github.com/grafana/dskit/httpgrpc"
    17  	"github.com/prometheus/common/model"
    18  )
    19  
    20  func TimeToMillis(t time.Time) int64 {
    21  	return t.Unix()*1000 + int64(t.Nanosecond())/int64(time.Millisecond)
    22  }
    23  
    24  // TimeFromMillis is a helper to turn milliseconds -> time.Time
    25  func TimeFromMillis(ms int64) time.Time {
    26  	return time.Unix(ms/1000, (ms%1000)*int64(time.Millisecond)).UTC()
    27  }
    28  
    29  // FormatTimeMillis returns a human readable version of the input time (in milliseconds).
    30  func FormatTimeMillis(ms int64) string {
    31  	return TimeFromMillis(ms).String()
    32  }
    33  
    34  // FormatTimeModel returns a human readable version of the input time.
    35  func FormatTimeModel(t model.Time) string {
    36  	return TimeFromMillis(int64(t)).String()
    37  }
    38  
    39  // ParseTime parses the string into an int64, milliseconds since epoch.
    40  func ParseTime(s string) (int64, error) {
    41  	if t, err := strconv.ParseFloat(s, 64); err == nil {
    42  		s, ns := math.Modf(t)
    43  		ns = math.Round(ns*1000) / 1000
    44  		tm := time.Unix(int64(s), int64(ns*float64(time.Second)))
    45  		return TimeToMillis(tm), nil
    46  	}
    47  	if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
    48  		return TimeToMillis(t), nil
    49  	}
    50  	return 0, httpgrpc.Errorf(http.StatusBadRequest, "cannot parse %q to a valid timestamp", s)
    51  }
    52  
    53  // DurationWithJitter returns random duration from "input - input*variance" to "input + input*variance" interval.
    54  func DurationWithJitter(input time.Duration, variancePerc float64) time.Duration {
    55  	// No duration? No jitter.
    56  	if input == 0 {
    57  		return 0
    58  	}
    59  
    60  	variance := int64(float64(input) * variancePerc)
    61  	jitter := rand.Int63n(variance*2) - variance
    62  
    63  	return input + time.Duration(jitter)
    64  }
    65  
    66  // DurationWithPositiveJitter returns random duration from "input" to "input + input*variance" interval.
    67  func DurationWithPositiveJitter(input time.Duration, variancePerc float64) time.Duration {
    68  	// No duration? No jitter.
    69  	if input == 0 {
    70  		return 0
    71  	}
    72  
    73  	variance := int64(float64(input) * variancePerc)
    74  	jitter := rand.Int63n(variance)
    75  
    76  	return input + time.Duration(jitter)
    77  }
    78  
    79  // DurationWithNegativeJitter returns random duration from "input - input*variance" to "input" interval.
    80  func DurationWithNegativeJitter(input time.Duration, variancePerc float64) time.Duration {
    81  	// No duration? No jitter.
    82  	if input == 0 {
    83  		return 0
    84  	}
    85  
    86  	variance := int64(float64(input) * variancePerc)
    87  	jitter := rand.Int63n(variance)
    88  
    89  	return input - time.Duration(jitter)
    90  }
    91  
    92  // NewDisableableTicker essentially wraps NewTicker but allows the ticker to be disabled by passing
    93  // zero duration as the interval. Returns a function for stopping the ticker, and the ticker channel.
    94  func NewDisableableTicker(interval time.Duration) (func(), <-chan time.Time) {
    95  	if interval == 0 {
    96  		return func() {}, nil
    97  	}
    98  
    99  	tick := time.NewTicker(interval)
   100  	return func() { tick.Stop() }, tick.C
   101  }
   102  
   103  type TimeRange struct {
   104  	Start      time.Time
   105  	End        time.Time
   106  	Resolution time.Duration
   107  }
   108  
   109  // SplitTimeRangeByResolution splits the given time range into the
   110  // minimal number of non-overlapping sub-ranges aligned with resolutions.
   111  // All ranges have inclusive start and end; one millisecond step.
   112  func SplitTimeRangeByResolution(start, end time.Time, resolutions []time.Duration, fn func(TimeRange)) {
   113  	if len(resolutions) == 0 {
   114  		fn(TimeRange{Start: start, End: end})
   115  		return
   116  	}
   117  
   118  	// Sorting resolutions in ascending order.
   119  	sort.Slice(resolutions, func(i, j int) bool {
   120  		return resolutions[i] > resolutions[j]
   121  	})
   122  
   123  	// Time ranges are inclusive on both ends. In order to simplify calculation
   124  	// of resolution alignment, we add a millisecond to the end time.
   125  	// Added millisecond is subtracted from the final ranges.
   126  	end = end.Add(time.Millisecond)
   127  	var (
   128  		c = start       // Current range start position.
   129  		r time.Duration // Current resolution.
   130  	)
   131  
   132  	for c.Before(end) {
   133  		var d time.Duration = -1
   134  		// Find the lowest resolution aligned with the current position.
   135  		for _, res := range resolutions {
   136  			if c.UnixNano()%res.Nanoseconds() == 0 && !c.Add(res).After(end) {
   137  				d = res
   138  				break
   139  			}
   140  		}
   141  		res := d
   142  		if d < 0 {
   143  			// No suitable resolution found: add distance
   144  			// to the next closest aligned boundary.
   145  			l := resolutions[len(resolutions)-1]
   146  			d = l - time.Duration(c.UnixNano()%l.Nanoseconds())
   147  		}
   148  		if end.Before(c.Add(d)) {
   149  			d = end.Sub(c)
   150  		}
   151  		// If the resolution has changed, emit a new range.
   152  		if r != res && c.After(start) {
   153  			fn(TimeRange{Start: start, End: c.Add(-time.Millisecond), Resolution: r})
   154  			start = c
   155  		}
   156  		c = c.Add(d)
   157  		r = res
   158  	}
   159  
   160  	if start != c {
   161  		fn(TimeRange{Start: start, End: c.Add(-time.Millisecond), Resolution: r})
   162  	}
   163  }