123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 |
- /*
- *
- * Copyright 2017 gRPC authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
- package stats
- import (
- "bytes"
- "fmt"
- "io"
- "math"
- "sort"
- "strconv"
- "time"
- )
- // Features contains most fields for a benchmark
- type Features struct {
- NetworkMode string
- EnableTrace bool
- Latency time.Duration
- Kbps int
- Mtu int
- MaxConcurrentCalls int
- ReqSizeBytes int
- RespSizeBytes int
- EnableCompressor bool
- EnableChannelz bool
- }
- // String returns the textual output of the Features as string.
- func (f Features) String() string {
- return fmt.Sprintf("traceMode_%t-latency_%s-kbps_%#v-MTU_%#v-maxConcurrentCalls_"+
- "%#v-reqSize_%#vB-respSize_%#vB-Compressor_%t", f.EnableTrace,
- f.Latency.String(), f.Kbps, f.Mtu, f.MaxConcurrentCalls, f.ReqSizeBytes, f.RespSizeBytes, f.EnableCompressor)
- }
- // ConciseString returns the concise textual output of the Features as string, skipping
- // setting with default value.
- func (f Features) ConciseString() string {
- noneEmptyPos := []bool{f.EnableTrace, f.Latency != 0, f.Kbps != 0, f.Mtu != 0, true, true, true, f.EnableCompressor, f.EnableChannelz}
- return PartialPrintString(noneEmptyPos, f, false)
- }
- // PartialPrintString can print certain features with different format.
- func PartialPrintString(noneEmptyPos []bool, f Features, shared bool) string {
- s := ""
- var (
- prefix, suffix, linker string
- isNetwork bool
- )
- if shared {
- suffix = "\n"
- linker = ": "
- } else {
- prefix = "-"
- linker = "_"
- }
- if noneEmptyPos[0] {
- s += fmt.Sprintf("%sTrace%s%t%s", prefix, linker, f.EnableTrace, suffix)
- }
- if shared && f.NetworkMode != "" {
- s += fmt.Sprintf("Network: %s \n", f.NetworkMode)
- isNetwork = true
- }
- if !isNetwork {
- if noneEmptyPos[1] {
- s += fmt.Sprintf("%slatency%s%s%s", prefix, linker, f.Latency.String(), suffix)
- }
- if noneEmptyPos[2] {
- s += fmt.Sprintf("%skbps%s%#v%s", prefix, linker, f.Kbps, suffix)
- }
- if noneEmptyPos[3] {
- s += fmt.Sprintf("%sMTU%s%#v%s", prefix, linker, f.Mtu, suffix)
- }
- }
- if noneEmptyPos[4] {
- s += fmt.Sprintf("%sCallers%s%#v%s", prefix, linker, f.MaxConcurrentCalls, suffix)
- }
- if noneEmptyPos[5] {
- s += fmt.Sprintf("%sreqSize%s%#vB%s", prefix, linker, f.ReqSizeBytes, suffix)
- }
- if noneEmptyPos[6] {
- s += fmt.Sprintf("%srespSize%s%#vB%s", prefix, linker, f.RespSizeBytes, suffix)
- }
- if noneEmptyPos[7] {
- s += fmt.Sprintf("%sCompressor%s%t%s", prefix, linker, f.EnableCompressor, suffix)
- }
- if noneEmptyPos[8] {
- s += fmt.Sprintf("%sChannelz%s%t%s", prefix, linker, f.EnableChannelz, suffix)
- }
- return s
- }
- type percentLatency struct {
- Percent int
- Value time.Duration
- }
- // BenchResults records features and result of a benchmark.
- type BenchResults struct {
- RunMode string
- Features Features
- Latency []percentLatency
- Operations int
- NsPerOp int64
- AllocedBytesPerOp int64
- AllocsPerOp int64
- SharedPosion []bool
- }
- // SetBenchmarkResult sets features of benchmark and basic results.
- func (stats *Stats) SetBenchmarkResult(mode string, features Features, o int, allocdBytes, allocs int64, sharedPos []bool) {
- stats.result.RunMode = mode
- stats.result.Features = features
- stats.result.Operations = o
- stats.result.AllocedBytesPerOp = allocdBytes
- stats.result.AllocsPerOp = allocs
- stats.result.SharedPosion = sharedPos
- }
- // GetBenchmarkResults returns the result of the benchmark including features and result.
- func (stats *Stats) GetBenchmarkResults() BenchResults {
- return stats.result
- }
- // BenchString output latency stats as the format as time + unit.
- func (stats *Stats) BenchString() string {
- stats.maybeUpdate()
- s := stats.result
- res := s.RunMode + "-" + s.Features.String() + ": \n"
- if len(s.Latency) != 0 {
- var statsUnit = s.Latency[0].Value
- var timeUnit = fmt.Sprintf("%v", statsUnit)[1:]
- for i := 1; i < len(s.Latency)-1; i++ {
- res += fmt.Sprintf("%d_Latency: %s %s \t", s.Latency[i].Percent,
- strconv.FormatFloat(float64(s.Latency[i].Value)/float64(statsUnit), 'f', 4, 64), timeUnit)
- }
- res += fmt.Sprintf("Avg latency: %s %s \t",
- strconv.FormatFloat(float64(s.Latency[len(s.Latency)-1].Value)/float64(statsUnit), 'f', 4, 64), timeUnit)
- }
- res += fmt.Sprintf("Count: %v \t", s.Operations)
- res += fmt.Sprintf("%v Bytes/op\t", s.AllocedBytesPerOp)
- res += fmt.Sprintf("%v Allocs/op\t", s.AllocsPerOp)
- return res
- }
- // Stats is a simple helper for gathering additional statistics like histogram
- // during benchmarks. This is not thread safe.
- type Stats struct {
- numBuckets int
- unit time.Duration
- min, max int64
- histogram *Histogram
- durations durationSlice
- dirty bool
- sortLatency bool
- result BenchResults
- }
- type durationSlice []time.Duration
- // NewStats creates a new Stats instance. If numBuckets is not positive,
- // the default value (16) will be used.
- func NewStats(numBuckets int) *Stats {
- if numBuckets <= 0 {
- numBuckets = 16
- }
- return &Stats{
- // Use one more bucket for the last unbounded bucket.
- numBuckets: numBuckets + 1,
- durations: make(durationSlice, 0, 100000),
- }
- }
- // Add adds an elapsed time per operation to the stats.
- func (stats *Stats) Add(d time.Duration) {
- stats.durations = append(stats.durations, d)
- stats.dirty = true
- }
- // Clear resets the stats, removing all values.
- func (stats *Stats) Clear() {
- stats.durations = stats.durations[:0]
- stats.histogram = nil
- stats.dirty = false
- stats.result = BenchResults{}
- }
- //Sort method for durations
- func (a durationSlice) Len() int { return len(a) }
- func (a durationSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
- func (a durationSlice) Less(i, j int) bool { return a[i] < a[j] }
- func max(a, b int64) int64 {
- if a > b {
- return a
- }
- return b
- }
- // maybeUpdate updates internal stat data if there was any newly added
- // stats since this was updated.
- func (stats *Stats) maybeUpdate() {
- if !stats.dirty {
- return
- }
- if stats.sortLatency {
- sort.Sort(stats.durations)
- stats.min = int64(stats.durations[0])
- stats.max = int64(stats.durations[len(stats.durations)-1])
- }
- stats.min = math.MaxInt64
- stats.max = 0
- for _, d := range stats.durations {
- if stats.min > int64(d) {
- stats.min = int64(d)
- }
- if stats.max < int64(d) {
- stats.max = int64(d)
- }
- }
- // Use the largest unit that can represent the minimum time duration.
- stats.unit = time.Nanosecond
- for _, u := range []time.Duration{time.Microsecond, time.Millisecond, time.Second} {
- if stats.min <= int64(u) {
- break
- }
- stats.unit = u
- }
- numBuckets := stats.numBuckets
- if n := int(stats.max - stats.min + 1); n < numBuckets {
- numBuckets = n
- }
- stats.histogram = NewHistogram(HistogramOptions{
- NumBuckets: numBuckets,
- // max-min(lower bound of last bucket) = (1 + growthFactor)^(numBuckets-2) * baseBucketSize.
- GrowthFactor: math.Pow(float64(stats.max-stats.min), 1/float64(numBuckets-2)) - 1,
- BaseBucketSize: 1.0,
- MinValue: stats.min})
- for _, d := range stats.durations {
- stats.histogram.Add(int64(d))
- }
- stats.dirty = false
- if stats.durations.Len() != 0 {
- var percentToObserve = []int{50, 90, 99}
- // First data record min unit from the latency result.
- stats.result.Latency = append(stats.result.Latency, percentLatency{Percent: -1, Value: stats.unit})
- for _, position := range percentToObserve {
- stats.result.Latency = append(stats.result.Latency, percentLatency{Percent: position, Value: stats.durations[max(stats.histogram.Count*int64(position)/100-1, 0)]})
- }
- // Last data record the average latency.
- avg := float64(stats.histogram.Sum) / float64(stats.histogram.Count)
- stats.result.Latency = append(stats.result.Latency, percentLatency{Percent: -1, Value: time.Duration(avg)})
- }
- }
- // SortLatency blocks the output
- func (stats *Stats) SortLatency() {
- stats.sortLatency = true
- }
- // Print writes textual output of the Stats.
- func (stats *Stats) Print(w io.Writer) {
- stats.maybeUpdate()
- if stats.histogram == nil {
- fmt.Fprint(w, "Histogram (empty)\n")
- } else {
- fmt.Fprintf(w, "Histogram (unit: %s)\n", fmt.Sprintf("%v", stats.unit)[1:])
- stats.histogram.PrintWithUnit(w, float64(stats.unit))
- }
- }
- // String returns the textual output of the Stats as string.
- func (stats *Stats) String() string {
- var b bytes.Buffer
- stats.Print(&b)
- return b.String()
- }
|