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 }