github.com/cockroachdb/pebble@v0.0.0-20231214172447-ab4952c5f87b/replay/sampled_metric.go (about)

     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  func (m *SampledMetric) record(v int64) {
    28  	if m.first.IsZero() {
    29  		m.first = time.Now()
    30  	}
    31  	m.samples = append(m.samples, sample{
    32  		since: time.Since(m.first),
    33  		value: v,
    34  	})
    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  func (m *SampledMetric) Plot(width, height int, scale float64) string {
    42  	values := m.Values(width)
    43  	for i := range values {
    44  		values[i] *= scale
    45  	}
    46  	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  func (m *SampledMetric) PlotIncreasingPerSec(width, height int, scale float64) string {
    55  	bucketDur, values := m.values(width)
    56  	deltas := make([]float64, width)
    57  	for i := range values {
    58  		if i == 0 {
    59  			deltas[i] = (values[i] * scale) / bucketDur.Seconds()
    60  		} else if values[i] > values[i-1] {
    61  			deltas[i] = (values[i] - values[i-1]) * scale / bucketDur.Seconds()
    62  		}
    63  	}
    64  	return asciigraph.Plot(deltas, asciigraph.Height(height))
    65  }
    66  
    67  // Mean calculates the mean value of the metric.
    68  func (m *SampledMetric) Mean() float64 {
    69  	var sum float64
    70  	if len(m.samples) == 0 {
    71  		return 0.0
    72  	}
    73  	for _, s := range m.samples {
    74  		sum += float64(s.value)
    75  	}
    76  	return sum / float64(len(m.samples))
    77  }
    78  
    79  // Min calculates the mininum value of the metric.
    80  func (m *SampledMetric) Min() int64 {
    81  	min := int64(math.MaxInt64)
    82  	for _, s := range m.samples {
    83  		if min > s.value {
    84  			min = s.value
    85  		}
    86  	}
    87  	return min
    88  }
    89  
    90  // Max calculates the maximum value of the metric.
    91  func (m *SampledMetric) Max() int64 {
    92  	var max int64
    93  	for _, s := range m.samples {
    94  		if max < s.value {
    95  			max = s.value
    96  		}
    97  	}
    98  	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  func (m *SampledMetric) Values(n int) []float64 {
   106  	_, values := m.values(n)
   107  	return values
   108  }
   109  
   110  func (m *SampledMetric) values(buckets int) (bucketDur time.Duration, values []float64) {
   111  	if len(m.samples) == 0 || buckets < 1 {
   112  		return bucketDur, nil
   113  	}
   114  
   115  	values = make([]float64, buckets)
   116  	totalDur := m.samples[len(m.samples)-1].since
   117  	bucketDur = totalDur / time.Duration(buckets)
   118  
   119  	for i, b := 0, 0; i < len(m.samples); i++ {
   120  		// Fill any buckets that precede this value with the previous value.
   121  		bi := int(m.samples[i].since / bucketDur)
   122  		if bi == buckets {
   123  			bi = buckets - 1
   124  		}
   125  		if b < bi {
   126  			b++
   127  			for ; b < bi; b++ {
   128  				values[b] = float64(m.samples[i].value)
   129  			}
   130  		}
   131  		values[bi] = float64(m.samples[i].value)
   132  		b = bi
   133  	}
   134  	return bucketDur, values
   135  }