Line data Source code
1 : // Copyright 2023 The LevelDB-Go and Pebble Authors. All rights reserved. Use 2 : // of this source code is governed by a BSD-style license that can be found in 3 : // the LICENSE file. 4 : 5 : package replay 6 : 7 : import ( 8 : "math" 9 : "time" 10 : 11 : "github.com/guptarohit/asciigraph" 12 : ) 13 : 14 : // SampledMetric holds a metric that is sampled at various points of workload 15 : // replay. Samples are collected when a new step in the workload is applied to 16 : // the database, and whenever a compaction completes. 17 : type SampledMetric struct { 18 : samples []sample 19 : first time.Time 20 : } 21 : 22 : type sample struct { 23 : since time.Duration 24 : value int64 25 : } 26 : 27 1 : func (m *SampledMetric) record(v int64) { 28 1 : if m.first.IsZero() { 29 1 : m.first = time.Now() 30 1 : } 31 1 : m.samples = append(m.samples, sample{ 32 1 : since: time.Since(m.first), 33 1 : value: v, 34 1 : }) 35 : } 36 : 37 : // Plot returns an ASCII graph plot of the metric over time, with the provided 38 : // width and height determining the size of the graph and the number of representable discrete x and y 39 : // points. All values are first 40 : // multiplied by the provided scale parameter before graphing. 41 1 : func (m *SampledMetric) Plot(width, height int, scale float64) string { 42 1 : values := m.Values(width) 43 1 : for i := range values { 44 1 : values[i] *= scale 45 1 : } 46 1 : return asciigraph.Plot(values, asciigraph.Height(height)) 47 : } 48 : 49 : // PlotIncreasingPerSec returns an ASCII graph plot of the increasing delta of a 50 : // metric over time, per-second. The provided width and height determine the 51 : // size of the graph and the number of representable discrete x and y points. 52 : // All deltas are multiplied by the provided scale parameter and scaled to 53 : // per-second before graphing. 54 1 : func (m *SampledMetric) PlotIncreasingPerSec(width, height int, scale float64) string { 55 1 : bucketDur, values := m.values(width) 56 1 : deltas := make([]float64, width) 57 1 : for i := range values { 58 1 : if i == 0 { 59 1 : deltas[i] = (values[i] * scale) / bucketDur.Seconds() 60 1 : } else if values[i] > values[i-1] { 61 1 : deltas[i] = (values[i] - values[i-1]) * scale / bucketDur.Seconds() 62 1 : } 63 : } 64 1 : return asciigraph.Plot(deltas, asciigraph.Height(height)) 65 : } 66 : 67 : // Mean calculates the mean value of the metric. 68 1 : func (m *SampledMetric) Mean() float64 { 69 1 : var sum float64 70 1 : if len(m.samples) == 0 { 71 0 : return 0.0 72 0 : } 73 1 : for _, s := range m.samples { 74 1 : sum += float64(s.value) 75 1 : } 76 1 : return sum / float64(len(m.samples)) 77 : } 78 : 79 : // Min calculates the mininum value of the metric. 80 0 : func (m *SampledMetric) Min() int64 { 81 0 : min := int64(math.MaxInt64) 82 0 : for _, s := range m.samples { 83 0 : if min > s.value { 84 0 : min = s.value 85 0 : } 86 : } 87 0 : return min 88 : } 89 : 90 : // Max calculates the maximum value of the metric. 91 1 : func (m *SampledMetric) Max() int64 { 92 1 : var max int64 93 1 : for _, s := range m.samples { 94 1 : if max < s.value { 95 1 : max = s.value 96 1 : } 97 : } 98 1 : return max 99 : } 100 : 101 : // Values returns the values of the metric, distributed across n discrete 102 : // buckets that are equally spaced over time. If multiple values fall within a 103 : // bucket, the latest recorded value is used. If no values fall within a bucket, 104 : // the next recorded value is used. 105 1 : func (m *SampledMetric) Values(n int) []float64 { 106 1 : _, values := m.values(n) 107 1 : return values 108 1 : } 109 : 110 1 : func (m *SampledMetric) values(buckets int) (bucketDur time.Duration, values []float64) { 111 1 : if len(m.samples) == 0 || buckets < 1 { 112 0 : return bucketDur, nil 113 0 : } 114 : 115 1 : values = make([]float64, buckets) 116 1 : totalDur := m.samples[len(m.samples)-1].since 117 1 : bucketDur = totalDur / time.Duration(buckets) 118 1 : 119 1 : for i, b := 0, 0; i < len(m.samples); i++ { 120 1 : // Fill any buckets that precede this value with the previous value. 121 1 : bi := int(m.samples[i].since / bucketDur) 122 1 : if bi == buckets { 123 1 : bi = buckets - 1 124 1 : } 125 1 : if b < bi { 126 1 : b++ 127 1 : for ; b < bi; b++ { 128 1 : values[b] = float64(m.samples[i].value) 129 1 : } 130 : } 131 1 : values[bi] = float64(m.samples[i].value) 132 1 : b = bi 133 : } 134 1 : return bucketDur, values 135 : }