1/* 2 * 3 * Copyright 2017 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19package stats 20 21import ( 22 "bytes" 23 "fmt" 24 "io" 25 "math" 26 "sort" 27 "strconv" 28 "time" 29) 30 31// Features contains most fields for a benchmark 32type Features struct { 33 NetworkMode string 34 EnableTrace bool 35 Latency time.Duration 36 Kbps int 37 Mtu int 38 MaxConcurrentCalls int 39 ReqSizeBytes int 40 RespSizeBytes int 41 EnableCompressor bool 42 EnableChannelz bool 43} 44 45// String returns the textual output of the Features as string. 46func (f Features) String() string { 47 return fmt.Sprintf("traceMode_%t-latency_%s-kbps_%#v-MTU_%#v-maxConcurrentCalls_"+ 48 "%#v-reqSize_%#vB-respSize_%#vB-Compressor_%t", f.EnableTrace, 49 f.Latency.String(), f.Kbps, f.Mtu, f.MaxConcurrentCalls, f.ReqSizeBytes, f.RespSizeBytes, f.EnableCompressor) 50} 51 52// ConciseString returns the concise textual output of the Features as string, skipping 53// setting with default value. 54func (f Features) ConciseString() string { 55 noneEmptyPos := []bool{f.EnableTrace, f.Latency != 0, f.Kbps != 0, f.Mtu != 0, true, true, true, f.EnableCompressor, f.EnableChannelz} 56 return PartialPrintString(noneEmptyPos, f, false) 57} 58 59// PartialPrintString can print certain features with different format. 60func PartialPrintString(noneEmptyPos []bool, f Features, shared bool) string { 61 s := "" 62 var ( 63 prefix, suffix, linker string 64 isNetwork bool 65 ) 66 if shared { 67 suffix = "\n" 68 linker = ": " 69 } else { 70 prefix = "-" 71 linker = "_" 72 } 73 if noneEmptyPos[0] { 74 s += fmt.Sprintf("%sTrace%s%t%s", prefix, linker, f.EnableTrace, suffix) 75 } 76 if shared && f.NetworkMode != "" { 77 s += fmt.Sprintf("Network: %s \n", f.NetworkMode) 78 isNetwork = true 79 } 80 if !isNetwork { 81 if noneEmptyPos[1] { 82 s += fmt.Sprintf("%slatency%s%s%s", prefix, linker, f.Latency.String(), suffix) 83 } 84 if noneEmptyPos[2] { 85 s += fmt.Sprintf("%skbps%s%#v%s", prefix, linker, f.Kbps, suffix) 86 } 87 if noneEmptyPos[3] { 88 s += fmt.Sprintf("%sMTU%s%#v%s", prefix, linker, f.Mtu, suffix) 89 } 90 } 91 if noneEmptyPos[4] { 92 s += fmt.Sprintf("%sCallers%s%#v%s", prefix, linker, f.MaxConcurrentCalls, suffix) 93 } 94 if noneEmptyPos[5] { 95 s += fmt.Sprintf("%sreqSize%s%#vB%s", prefix, linker, f.ReqSizeBytes, suffix) 96 } 97 if noneEmptyPos[6] { 98 s += fmt.Sprintf("%srespSize%s%#vB%s", prefix, linker, f.RespSizeBytes, suffix) 99 } 100 if noneEmptyPos[7] { 101 s += fmt.Sprintf("%sCompressor%s%t%s", prefix, linker, f.EnableCompressor, suffix) 102 } 103 if noneEmptyPos[8] { 104 s += fmt.Sprintf("%sChannelz%s%t%s", prefix, linker, f.EnableChannelz, suffix) 105 } 106 return s 107} 108 109type percentLatency struct { 110 Percent int 111 Value time.Duration 112} 113 114// BenchResults records features and result of a benchmark. 115type BenchResults struct { 116 RunMode string 117 Features Features 118 Latency []percentLatency 119 Operations int 120 NsPerOp int64 121 AllocedBytesPerOp int64 122 AllocsPerOp int64 123 SharedPosion []bool 124} 125 126// SetBenchmarkResult sets features of benchmark and basic results. 127func (stats *Stats) SetBenchmarkResult(mode string, features Features, o int, allocdBytes, allocs int64, sharedPos []bool) { 128 stats.result.RunMode = mode 129 stats.result.Features = features 130 stats.result.Operations = o 131 stats.result.AllocedBytesPerOp = allocdBytes 132 stats.result.AllocsPerOp = allocs 133 stats.result.SharedPosion = sharedPos 134} 135 136// GetBenchmarkResults returns the result of the benchmark including features and result. 137func (stats *Stats) GetBenchmarkResults() BenchResults { 138 return stats.result 139} 140 141// BenchString output latency stats as the format as time + unit. 142func (stats *Stats) BenchString() string { 143 stats.maybeUpdate() 144 s := stats.result 145 res := s.RunMode + "-" + s.Features.String() + ": \n" 146 if len(s.Latency) != 0 { 147 var statsUnit = s.Latency[0].Value 148 var timeUnit = fmt.Sprintf("%v", statsUnit)[1:] 149 for i := 1; i < len(s.Latency)-1; i++ { 150 res += fmt.Sprintf("%d_Latency: %s %s \t", s.Latency[i].Percent, 151 strconv.FormatFloat(float64(s.Latency[i].Value)/float64(statsUnit), 'f', 4, 64), timeUnit) 152 } 153 res += fmt.Sprintf("Avg latency: %s %s \t", 154 strconv.FormatFloat(float64(s.Latency[len(s.Latency)-1].Value)/float64(statsUnit), 'f', 4, 64), timeUnit) 155 } 156 res += fmt.Sprintf("Count: %v \t", s.Operations) 157 res += fmt.Sprintf("%v Bytes/op\t", s.AllocedBytesPerOp) 158 res += fmt.Sprintf("%v Allocs/op\t", s.AllocsPerOp) 159 160 return res 161} 162 163// Stats is a simple helper for gathering additional statistics like histogram 164// during benchmarks. This is not thread safe. 165type Stats struct { 166 numBuckets int 167 unit time.Duration 168 min, max int64 169 histogram *Histogram 170 171 durations durationSlice 172 dirty bool 173 174 sortLatency bool 175 result BenchResults 176} 177 178type durationSlice []time.Duration 179 180// NewStats creates a new Stats instance. If numBuckets is not positive, 181// the default value (16) will be used. 182func NewStats(numBuckets int) *Stats { 183 if numBuckets <= 0 { 184 numBuckets = 16 185 } 186 return &Stats{ 187 // Use one more bucket for the last unbounded bucket. 188 numBuckets: numBuckets + 1, 189 durations: make(durationSlice, 0, 100000), 190 } 191} 192 193// Add adds an elapsed time per operation to the stats. 194func (stats *Stats) Add(d time.Duration) { 195 stats.durations = append(stats.durations, d) 196 stats.dirty = true 197} 198 199// Clear resets the stats, removing all values. 200func (stats *Stats) Clear() { 201 stats.durations = stats.durations[:0] 202 stats.histogram = nil 203 stats.dirty = false 204 stats.result = BenchResults{} 205} 206 207//Sort method for durations 208func (a durationSlice) Len() int { return len(a) } 209func (a durationSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 210func (a durationSlice) Less(i, j int) bool { return a[i] < a[j] } 211func max(a, b int64) int64 { 212 if a > b { 213 return a 214 } 215 return b 216} 217 218// maybeUpdate updates internal stat data if there was any newly added 219// stats since this was updated. 220func (stats *Stats) maybeUpdate() { 221 if !stats.dirty { 222 return 223 } 224 225 if stats.sortLatency { 226 sort.Sort(stats.durations) 227 stats.min = int64(stats.durations[0]) 228 stats.max = int64(stats.durations[len(stats.durations)-1]) 229 } 230 231 stats.min = math.MaxInt64 232 stats.max = 0 233 for _, d := range stats.durations { 234 if stats.min > int64(d) { 235 stats.min = int64(d) 236 } 237 if stats.max < int64(d) { 238 stats.max = int64(d) 239 } 240 } 241 242 // Use the largest unit that can represent the minimum time duration. 243 stats.unit = time.Nanosecond 244 for _, u := range []time.Duration{time.Microsecond, time.Millisecond, time.Second} { 245 if stats.min <= int64(u) { 246 break 247 } 248 stats.unit = u 249 } 250 251 numBuckets := stats.numBuckets 252 if n := int(stats.max - stats.min + 1); n < numBuckets { 253 numBuckets = n 254 } 255 stats.histogram = NewHistogram(HistogramOptions{ 256 NumBuckets: numBuckets, 257 // max-min(lower bound of last bucket) = (1 + growthFactor)^(numBuckets-2) * baseBucketSize. 258 GrowthFactor: math.Pow(float64(stats.max-stats.min), 1/float64(numBuckets-2)) - 1, 259 BaseBucketSize: 1.0, 260 MinValue: stats.min}) 261 262 for _, d := range stats.durations { 263 stats.histogram.Add(int64(d)) 264 } 265 266 stats.dirty = false 267 268 if stats.durations.Len() != 0 { 269 var percentToObserve = []int{50, 90, 99} 270 // First data record min unit from the latency result. 271 stats.result.Latency = append(stats.result.Latency, percentLatency{Percent: -1, Value: stats.unit}) 272 for _, position := range percentToObserve { 273 stats.result.Latency = append(stats.result.Latency, percentLatency{Percent: position, Value: stats.durations[max(stats.histogram.Count*int64(position)/100-1, 0)]}) 274 } 275 // Last data record the average latency. 276 avg := float64(stats.histogram.Sum) / float64(stats.histogram.Count) 277 stats.result.Latency = append(stats.result.Latency, percentLatency{Percent: -1, Value: time.Duration(avg)}) 278 } 279} 280 281// SortLatency blocks the output 282func (stats *Stats) SortLatency() { 283 stats.sortLatency = true 284} 285 286// Print writes textual output of the Stats. 287func (stats *Stats) Print(w io.Writer) { 288 stats.maybeUpdate() 289 if stats.histogram == nil { 290 fmt.Fprint(w, "Histogram (empty)\n") 291 } else { 292 fmt.Fprintf(w, "Histogram (unit: %s)\n", fmt.Sprintf("%v", stats.unit)[1:]) 293 stats.histogram.PrintWithUnit(w, float64(stats.unit)) 294 } 295} 296 297// String returns the textual output of the Stats as string. 298func (stats *Stats) String() string { 299 var b bytes.Buffer 300 stats.Print(&b) 301 return b.String() 302} 303