github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/workload/histogram/histogram.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package histogram
    12  
    13  import (
    14  	"encoding/json"
    15  	"fmt"
    16  	"io"
    17  	"os"
    18  	"sort"
    19  	"sync"
    20  	"time"
    21  
    22  	"github.com/cockroachdb/cockroach/pkg/util/syncutil"
    23  	"github.com/cockroachdb/cockroach/pkg/util/timeutil"
    24  	"github.com/codahale/hdrhistogram"
    25  )
    26  
    27  const (
    28  	sigFigs    = 1
    29  	minLatency = 100 * time.Microsecond
    30  )
    31  
    32  // NamedHistogram is a named histogram for use in Operations. It is threadsafe
    33  // but intended to be thread-local.
    34  type NamedHistogram struct {
    35  	name string
    36  	mu   struct {
    37  		syncutil.Mutex
    38  		current *hdrhistogram.Histogram
    39  	}
    40  }
    41  
    42  func newNamedHistogram(reg *Registry, name string) *NamedHistogram {
    43  	w := &NamedHistogram{name: name}
    44  	w.mu.current = reg.newHistogram()
    45  	return w
    46  }
    47  
    48  // Record saves a new datapoint and should be called once per logical operation.
    49  func (w *NamedHistogram) Record(elapsed time.Duration) {
    50  	maxLatency := time.Duration(w.mu.current.HighestTrackableValue())
    51  	if elapsed < minLatency {
    52  		elapsed = minLatency
    53  	} else if elapsed > maxLatency {
    54  		elapsed = maxLatency
    55  	}
    56  
    57  	w.mu.Lock()
    58  	err := w.mu.current.RecordValue(elapsed.Nanoseconds())
    59  	w.mu.Unlock()
    60  
    61  	if err != nil {
    62  		// Note that a histogram only drops recorded values that are out of range,
    63  		// but we clamp the latency value to the configured range to prevent such
    64  		// drops. This code path should never happen.
    65  		panic(fmt.Sprintf(`%s: recording value: %s`, w.name, err))
    66  	}
    67  }
    68  
    69  // tick resets the current histogram to a new "period". The old one's data
    70  // should be saved via the closure argument.
    71  func (w *NamedHistogram) tick(
    72  	newHistogram *hdrhistogram.Histogram, fn func(h *hdrhistogram.Histogram),
    73  ) {
    74  	w.mu.Lock()
    75  	h := w.mu.current
    76  	w.mu.current = newHistogram
    77  	w.mu.Unlock()
    78  	fn(h)
    79  }
    80  
    81  // Registry is a thread-safe enclosure for a (possibly large) number of
    82  // named histograms. It allows for "tick"ing them periodically to reset the
    83  // counts and also supports aggregations.
    84  type Registry struct {
    85  	mu struct {
    86  		syncutil.Mutex
    87  		registered map[string][]*NamedHistogram
    88  	}
    89  
    90  	start         time.Time
    91  	cumulative    map[string]*hdrhistogram.Histogram
    92  	prevTick      map[string]time.Time
    93  	histogramPool *sync.Pool
    94  }
    95  
    96  // NewRegistry returns an initialized Registry. maxLat is the maximum time that
    97  // queries are expected to take to execute which is needed to initialize the
    98  // pool of histograms.
    99  func NewRegistry(maxLat time.Duration) *Registry {
   100  	r := &Registry{
   101  		start:      timeutil.Now(),
   102  		cumulative: make(map[string]*hdrhistogram.Histogram),
   103  		prevTick:   make(map[string]time.Time),
   104  		histogramPool: &sync.Pool{
   105  			New: func() interface{} {
   106  				return hdrhistogram.New(minLatency.Nanoseconds(), maxLat.Nanoseconds(), sigFigs)
   107  			},
   108  		},
   109  	}
   110  	r.mu.registered = make(map[string][]*NamedHistogram)
   111  	return r
   112  }
   113  
   114  func (w *Registry) newHistogram() *hdrhistogram.Histogram {
   115  	h := w.histogramPool.Get().(*hdrhistogram.Histogram)
   116  	return h
   117  }
   118  
   119  // GetHandle returns a thread-local handle for creating and registering
   120  // NamedHistograms.
   121  func (w *Registry) GetHandle() *Histograms {
   122  	hists := &Histograms{reg: w}
   123  	hists.mu.hists = make(map[string]*NamedHistogram)
   124  	return hists
   125  }
   126  
   127  // Tick aggregates all registered histograms, grouped by name. It is expected to
   128  // be called periodically from one goroutine.
   129  func (w *Registry) Tick(fn func(Tick)) {
   130  	merged := make(map[string]*hdrhistogram.Histogram)
   131  	var names []string
   132  	var wg sync.WaitGroup
   133  
   134  	w.mu.Lock()
   135  	for name, nameRegistered := range w.mu.registered {
   136  		wg.Add(1)
   137  		registered := append([]*NamedHistogram(nil), nameRegistered...)
   138  		merged[name] = w.newHistogram()
   139  		names = append(names, name)
   140  		go func(registered []*NamedHistogram, merged *hdrhistogram.Histogram) {
   141  			for _, hist := range registered {
   142  				hist.tick(w.newHistogram(), func(h *hdrhistogram.Histogram) {
   143  					merged.Merge(h)
   144  					h.Reset()
   145  					w.histogramPool.Put(h)
   146  				})
   147  			}
   148  			wg.Done()
   149  		}(registered, merged[name])
   150  	}
   151  	w.mu.Unlock()
   152  
   153  	wg.Wait()
   154  
   155  	now := timeutil.Now()
   156  	sort.Strings(names)
   157  	for _, name := range names {
   158  		mergedHist := merged[name]
   159  		if _, ok := w.cumulative[name]; !ok {
   160  			w.cumulative[name] = w.newHistogram()
   161  		}
   162  		w.cumulative[name].Merge(mergedHist)
   163  
   164  		prevTick, ok := w.prevTick[name]
   165  		if !ok {
   166  			prevTick = w.start
   167  		}
   168  		w.prevTick[name] = now
   169  		fn(Tick{
   170  			Name:       name,
   171  			Hist:       mergedHist,
   172  			Cumulative: w.cumulative[name],
   173  			Elapsed:    now.Sub(prevTick),
   174  			Now:        now,
   175  		})
   176  		mergedHist.Reset()
   177  		w.histogramPool.Put(mergedHist)
   178  	}
   179  }
   180  
   181  // Histograms is a thread-local handle for creating and registering
   182  // NamedHistograms.
   183  type Histograms struct {
   184  	reg *Registry
   185  	mu  struct {
   186  		syncutil.RWMutex
   187  		hists map[string]*NamedHistogram
   188  	}
   189  }
   190  
   191  // Get returns a NamedHistogram with the given name, creating and registering it
   192  // if necessary. The result is cached, so no need to cache it in the workload.
   193  func (w *Histograms) Get(name string) *NamedHistogram {
   194  	// Fast path for existing histograms, which is the common case by far.
   195  	w.mu.RLock()
   196  	hist, ok := w.mu.hists[name]
   197  	if ok {
   198  		w.mu.RUnlock()
   199  		return hist
   200  	}
   201  	w.mu.RUnlock()
   202  
   203  	w.mu.Lock()
   204  	hist, ok = w.mu.hists[name]
   205  	if !ok {
   206  		hist = newNamedHistogram(w.reg, name)
   207  		w.mu.hists[name] = hist
   208  	}
   209  	w.mu.Unlock()
   210  
   211  	if !ok {
   212  		w.reg.mu.Lock()
   213  		w.reg.mu.registered[name] = append(w.reg.mu.registered[name], hist)
   214  		w.reg.mu.Unlock()
   215  	}
   216  
   217  	return hist
   218  }
   219  
   220  // Copy makes a new histogram which is a copy of h.
   221  func Copy(h *hdrhistogram.Histogram) *hdrhistogram.Histogram {
   222  	dup := hdrhistogram.New(h.LowestTrackableValue(), h.HighestTrackableValue(),
   223  		int(h.SignificantFigures()))
   224  	dup.Merge(h)
   225  	return dup
   226  }
   227  
   228  // Tick is an aggregation of ticking all histograms in a
   229  // Registry with a given name.
   230  type Tick struct {
   231  	// Name is the name given to the histograms represented by this tick.
   232  	Name string
   233  	// Hist is the merged result of the represented histograms for this tick.
   234  	// Hist.TotalCount() is the number of operations that occurred for this tick.
   235  	Hist *hdrhistogram.Histogram
   236  	// Cumulative is the merged result of the represented histograms for all
   237  	// time. Cumulative.TotalCount() is the total number of operations that have
   238  	// occurred over all time.
   239  	Cumulative *hdrhistogram.Histogram
   240  	// Elapsed is the amount of time since the last tick.
   241  	Elapsed time.Duration
   242  	// Now is the time at which the tick was gathered. It covers the period
   243  	// [Now-Elapsed,Now).
   244  	Now time.Time
   245  }
   246  
   247  // Snapshot creates a SnapshotTick from the receiver.
   248  func (t Tick) Snapshot() SnapshotTick {
   249  	return SnapshotTick{
   250  		Name:    t.Name,
   251  		Elapsed: t.Elapsed,
   252  		Now:     t.Now,
   253  		Hist:    t.Hist.Export(),
   254  	}
   255  }
   256  
   257  // SnapshotTick parallels Tick but replace the histogram with a
   258  // snapshot that is suitable for serialization. Additionally, it only contains
   259  // the per-tick histogram, not the cumulative histogram. (The cumulative
   260  // histogram can be computed by aggregating all of the per-tick histograms).
   261  type SnapshotTick struct {
   262  	Name    string
   263  	Hist    *hdrhistogram.Snapshot
   264  	Elapsed time.Duration
   265  	Now     time.Time
   266  }
   267  
   268  // DecodeSnapshots decodes a file with SnapshotTicks into a series.
   269  func DecodeSnapshots(path string) (map[string][]SnapshotTick, error) {
   270  	f, err := os.Open(path)
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  	defer func() { _ = f.Close() }()
   275  	dec := json.NewDecoder(f)
   276  	ret := make(map[string][]SnapshotTick)
   277  	for {
   278  		var tick SnapshotTick
   279  		if err := dec.Decode(&tick); err == io.EOF {
   280  			break
   281  		} else if err != nil {
   282  			return nil, err
   283  		}
   284  		ret[tick.Name] = append(ret[tick.Name], tick)
   285  	}
   286  	return ret, nil
   287  }