/* * * 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 registers stats used for creating benchmarks 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() }