github.com/hxx258456/ccgo@v0.0.5-0.20230213014102-48b35f46f66f/grpc/benchmark/stats/stats.go (about) 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 19 // Package stats tracks the statistics associated with benchmark runs. 20 package stats 21 22 import ( 23 "bytes" 24 "fmt" 25 "log" 26 "math" 27 "runtime" 28 "sort" 29 "strconv" 30 "sync" 31 "time" 32 33 grpc "github.com/hxx258456/ccgo/grpc" 34 ) 35 36 // FeatureIndex is an enum for features that usually differ across individual 37 // benchmark runs in a single execution. These are usually configured by the 38 // user through command line flags. 39 type FeatureIndex int 40 41 // FeatureIndex enum values corresponding to individually settable features. 42 const ( 43 EnableTraceIndex FeatureIndex = iota 44 ReadLatenciesIndex 45 ReadKbpsIndex 46 ReadMTUIndex 47 MaxConcurrentCallsIndex 48 ReqSizeBytesIndex 49 RespSizeBytesIndex 50 ReqPayloadCurveIndex 51 RespPayloadCurveIndex 52 CompModesIndex 53 EnableChannelzIndex 54 EnablePreloaderIndex 55 56 // MaxFeatureIndex is a place holder to indicate the total number of feature 57 // indices we have. Any new feature indices should be added above this. 58 MaxFeatureIndex 59 ) 60 61 // Features represent configured options for a specific benchmark run. This is 62 // usually constructed from command line arguments passed by the caller. See 63 // benchmark/benchmain/main.go for defined command line flags. This is also 64 // part of the BenchResults struct which is serialized and written to a file. 65 type Features struct { 66 // Network mode used for this benchmark run. Could be one of Local, LAN, WAN 67 // or Longhaul. 68 NetworkMode string 69 // UseBufCon indicates whether an in-memory connection was used for this 70 // benchmark run instead of system network I/O. 71 UseBufConn bool 72 // EnableKeepalive indicates if keepalives were enabled on the connections 73 // used in this benchmark run. 74 EnableKeepalive bool 75 // BenchTime indicates the duration of the benchmark run. 76 BenchTime time.Duration 77 78 // Features defined above are usually the same for all benchmark runs in a 79 // particular invocation, while the features defined below could vary from 80 // run to run based on the configured command line. These features have a 81 // corresponding featureIndex value which is used for a variety of reasons. 82 83 // EnableTrace indicates if tracing was enabled. 84 EnableTrace bool 85 // Latency is the simulated one-way network latency used. 86 Latency time.Duration 87 // Kbps is the simulated network throughput used. 88 Kbps int 89 // MTU is the simulated network MTU used. 90 MTU int 91 // MaxConcurrentCalls is the number of concurrent RPCs made during this 92 // benchmark run. 93 MaxConcurrentCalls int 94 // ReqSizeBytes is the request size in bytes used in this benchmark run. 95 // Unused if ReqPayloadCurve is non-nil. 96 ReqSizeBytes int 97 // RespSizeBytes is the response size in bytes used in this benchmark run. 98 // Unused if RespPayloadCurve is non-nil. 99 RespSizeBytes int 100 // ReqPayloadCurve is a histogram representing the shape a random 101 // distribution request payloads should take. 102 ReqPayloadCurve *PayloadCurve 103 // RespPayloadCurve is a histogram representing the shape a random 104 // distribution request payloads should take. 105 RespPayloadCurve *PayloadCurve 106 // ModeCompressor represents the compressor mode used. 107 ModeCompressor string 108 // EnableChannelz indicates if channelz was turned on. 109 EnableChannelz bool 110 // EnablePreloader indicates if preloading was turned on. 111 EnablePreloader bool 112 } 113 114 // String returns all the feature values as a string. 115 func (f Features) String() string { 116 var reqPayloadString, respPayloadString string 117 if f.ReqPayloadCurve != nil { 118 reqPayloadString = fmt.Sprintf("reqPayloadCurve_%s", f.ReqPayloadCurve.ShortHash()) 119 } else { 120 reqPayloadString = fmt.Sprintf("reqSize_%vB", f.ReqSizeBytes) 121 } 122 if f.RespPayloadCurve != nil { 123 respPayloadString = fmt.Sprintf("respPayloadCurve_%s", f.RespPayloadCurve.ShortHash()) 124 } else { 125 respPayloadString = fmt.Sprintf("respSize_%vB", f.RespSizeBytes) 126 } 127 return fmt.Sprintf("networkMode_%v-bufConn_%v-keepalive_%v-benchTime_%v-"+ 128 "trace_%v-latency_%v-kbps_%v-MTU_%v-maxConcurrentCalls_%v-%s-%s-"+ 129 "compressor_%v-channelz_%v-preloader_%v", 130 f.NetworkMode, f.UseBufConn, f.EnableKeepalive, f.BenchTime, f.EnableTrace, 131 f.Latency, f.Kbps, f.MTU, f.MaxConcurrentCalls, reqPayloadString, 132 respPayloadString, f.ModeCompressor, f.EnableChannelz, f.EnablePreloader) 133 } 134 135 // SharedFeatures returns the shared features as a pretty printable string. 136 // 'wantFeatures' is a bitmask of wanted features, indexed by FeaturesIndex. 137 func (f Features) SharedFeatures(wantFeatures []bool) string { 138 var b bytes.Buffer 139 if f.NetworkMode != "" { 140 b.WriteString(fmt.Sprintf("Network: %v\n", f.NetworkMode)) 141 } 142 if f.UseBufConn { 143 b.WriteString(fmt.Sprintf("UseBufConn: %v\n", f.UseBufConn)) 144 } 145 if f.EnableKeepalive { 146 b.WriteString(fmt.Sprintf("EnableKeepalive: %v\n", f.EnableKeepalive)) 147 } 148 b.WriteString(fmt.Sprintf("BenchTime: %v\n", f.BenchTime)) 149 f.partialString(&b, wantFeatures, ": ", "\n") 150 return b.String() 151 } 152 153 // PrintableName returns a one line name which includes the features specified 154 // by 'wantFeatures' which is a bitmask of wanted features, indexed by 155 // FeaturesIndex. 156 func (f Features) PrintableName(wantFeatures []bool) string { 157 var b bytes.Buffer 158 f.partialString(&b, wantFeatures, "_", "-") 159 return b.String() 160 } 161 162 // partialString writes features specified by 'wantFeatures' to the provided 163 // bytes.Buffer. 164 func (f Features) partialString(b *bytes.Buffer, wantFeatures []bool, sep, delim string) { 165 for i, sf := range wantFeatures { 166 if sf { 167 switch FeatureIndex(i) { 168 case EnableTraceIndex: 169 b.WriteString(fmt.Sprintf("Trace%v%v%v", sep, f.EnableTrace, delim)) 170 case ReadLatenciesIndex: 171 b.WriteString(fmt.Sprintf("Latency%v%v%v", sep, f.Latency, delim)) 172 case ReadKbpsIndex: 173 b.WriteString(fmt.Sprintf("Kbps%v%v%v", sep, f.Kbps, delim)) 174 case ReadMTUIndex: 175 b.WriteString(fmt.Sprintf("MTU%v%v%v", sep, f.MTU, delim)) 176 case MaxConcurrentCallsIndex: 177 b.WriteString(fmt.Sprintf("Callers%v%v%v", sep, f.MaxConcurrentCalls, delim)) 178 case ReqSizeBytesIndex: 179 b.WriteString(fmt.Sprintf("ReqSize%v%vB%v", sep, f.ReqSizeBytes, delim)) 180 case RespSizeBytesIndex: 181 b.WriteString(fmt.Sprintf("RespSize%v%vB%v", sep, f.RespSizeBytes, delim)) 182 case ReqPayloadCurveIndex: 183 if f.ReqPayloadCurve != nil { 184 b.WriteString(fmt.Sprintf("ReqPayloadCurve%vSHA-256:%v%v", sep, f.ReqPayloadCurve.Hash(), delim)) 185 } 186 case RespPayloadCurveIndex: 187 if f.RespPayloadCurve != nil { 188 b.WriteString(fmt.Sprintf("RespPayloadCurve%vSHA-256:%v%v", sep, f.RespPayloadCurve.Hash(), delim)) 189 } 190 case CompModesIndex: 191 b.WriteString(fmt.Sprintf("Compressor%v%v%v", sep, f.ModeCompressor, delim)) 192 case EnableChannelzIndex: 193 b.WriteString(fmt.Sprintf("Channelz%v%v%v", sep, f.EnableChannelz, delim)) 194 case EnablePreloaderIndex: 195 b.WriteString(fmt.Sprintf("Preloader%v%v%v", sep, f.EnablePreloader, delim)) 196 default: 197 log.Fatalf("Unknown feature index %v. maxFeatureIndex is %v", i, MaxFeatureIndex) 198 } 199 } 200 } 201 } 202 203 // BenchResults records features and results of a benchmark run. A collection 204 // of these structs is usually serialized and written to a file after a 205 // benchmark execution, and could later be read for pretty-printing or 206 // comparison with other benchmark results. 207 type BenchResults struct { 208 // GoVersion is the version of the compiler the benchmark was compiled with. 209 GoVersion string 210 // GrpcVersion is the gRPC version being benchmarked. 211 GrpcVersion string 212 // RunMode is the workload mode for this benchmark run. This could be unary, 213 // stream or unconstrained. 214 RunMode string 215 // Features represents the configured feature options for this run. 216 Features Features 217 // SharedFeatures represents the features which were shared across all 218 // benchmark runs during one execution. It is a slice indexed by 219 // 'FeaturesIndex' and a value of true indicates that the associated 220 // feature is shared across all runs. 221 SharedFeatures []bool 222 // Data contains the statistical data of interest from the benchmark run. 223 Data RunData 224 } 225 226 // RunData contains statistical data of interest from a benchmark run. 227 type RunData struct { 228 // TotalOps is the number of operations executed during this benchmark run. 229 // Only makes sense for unary and streaming workloads. 230 TotalOps uint64 231 // SendOps is the number of send operations executed during this benchmark 232 // run. Only makes sense for unconstrained workloads. 233 SendOps uint64 234 // RecvOps is the number of receive operations executed during this benchmark 235 // run. Only makes sense for unconstrained workloads. 236 RecvOps uint64 237 // AllocedBytes is the average memory allocation in bytes per operation. 238 AllocedBytes float64 239 // Allocs is the average number of memory allocations per operation. 240 Allocs float64 241 // ReqT is the average request throughput associated with this run. 242 ReqT float64 243 // RespT is the average response throughput associated with this run. 244 RespT float64 245 246 // We store different latencies associated with each run. These latencies are 247 // only computed for unary and stream workloads as they are not very useful 248 // for unconstrained workloads. 249 250 // Fiftieth is the 50th percentile latency. 251 Fiftieth time.Duration 252 // Ninetieth is the 90th percentile latency. 253 Ninetieth time.Duration 254 // Ninetyninth is the 99th percentile latency. 255 NinetyNinth time.Duration 256 // Average is the average latency. 257 Average time.Duration 258 } 259 260 type durationSlice []time.Duration 261 262 func (a durationSlice) Len() int { return len(a) } 263 func (a durationSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 264 func (a durationSlice) Less(i, j int) bool { return a[i] < a[j] } 265 266 // Stats is a helper for gathering statistics about individual benchmark runs. 267 type Stats struct { 268 mu sync.Mutex 269 numBuckets int 270 hw *histWrapper 271 results []BenchResults 272 startMS runtime.MemStats 273 stopMS runtime.MemStats 274 } 275 276 type histWrapper struct { 277 unit time.Duration 278 histogram *Histogram 279 durations durationSlice 280 } 281 282 // NewStats creates a new Stats instance. If numBuckets is not positive, the 283 // default value (16) will be used. 284 func NewStats(numBuckets int) *Stats { 285 if numBuckets <= 0 { 286 numBuckets = 16 287 } 288 // Use one more bucket for the last unbounded bucket. 289 s := &Stats{numBuckets: numBuckets + 1} 290 s.hw = &histWrapper{} 291 return s 292 } 293 294 // StartRun is to be invoked to indicate the start of a new benchmark run. 295 func (s *Stats) StartRun(mode string, f Features, sf []bool) { 296 s.mu.Lock() 297 defer s.mu.Unlock() 298 299 runtime.ReadMemStats(&s.startMS) 300 s.results = append(s.results, BenchResults{ 301 GoVersion: runtime.Version(), 302 GrpcVersion: grpc.Version, 303 RunMode: mode, 304 Features: f, 305 SharedFeatures: sf, 306 }) 307 } 308 309 // EndRun is to be invoked to indicate the end of the ongoing benchmark run. It 310 // computes a bunch of stats and dumps them to stdout. 311 func (s *Stats) EndRun(count uint64) { 312 s.mu.Lock() 313 defer s.mu.Unlock() 314 315 runtime.ReadMemStats(&s.stopMS) 316 r := &s.results[len(s.results)-1] 317 r.Data = RunData{ 318 TotalOps: count, 319 AllocedBytes: float64(s.stopMS.TotalAlloc-s.startMS.TotalAlloc) / float64(count), 320 Allocs: float64(s.stopMS.Mallocs-s.startMS.Mallocs) / float64(count), 321 ReqT: float64(count) * float64(r.Features.ReqSizeBytes) * 8 / r.Features.BenchTime.Seconds(), 322 RespT: float64(count) * float64(r.Features.RespSizeBytes) * 8 / r.Features.BenchTime.Seconds(), 323 } 324 s.computeLatencies(r) 325 s.dump(r) 326 s.hw = &histWrapper{} 327 } 328 329 // EndUnconstrainedRun is similar to EndRun, but is to be used for 330 // unconstrained workloads. 331 func (s *Stats) EndUnconstrainedRun(req uint64, resp uint64) { 332 s.mu.Lock() 333 defer s.mu.Unlock() 334 335 runtime.ReadMemStats(&s.stopMS) 336 r := &s.results[len(s.results)-1] 337 r.Data = RunData{ 338 SendOps: req, 339 RecvOps: resp, 340 AllocedBytes: float64(s.stopMS.TotalAlloc-s.startMS.TotalAlloc) / float64((req+resp)/2), 341 Allocs: float64(s.stopMS.Mallocs-s.startMS.Mallocs) / float64((req+resp)/2), 342 ReqT: float64(req) * float64(r.Features.ReqSizeBytes) * 8 / r.Features.BenchTime.Seconds(), 343 RespT: float64(resp) * float64(r.Features.RespSizeBytes) * 8 / r.Features.BenchTime.Seconds(), 344 } 345 s.computeLatencies(r) 346 s.dump(r) 347 s.hw = &histWrapper{} 348 } 349 350 // AddDuration adds an elapsed duration per operation to the stats. This is 351 // used by unary and stream modes where request and response stats are equal. 352 func (s *Stats) AddDuration(d time.Duration) { 353 s.mu.Lock() 354 defer s.mu.Unlock() 355 356 s.hw.durations = append(s.hw.durations, d) 357 } 358 359 // GetResults returns the results from all benchmark runs. 360 func (s *Stats) GetResults() []BenchResults { 361 s.mu.Lock() 362 defer s.mu.Unlock() 363 364 return s.results 365 } 366 367 // computeLatencies computes percentile latencies based on durations stored in 368 // the stats object and updates the corresponding fields in the result object. 369 func (s *Stats) computeLatencies(result *BenchResults) { 370 if len(s.hw.durations) == 0 { 371 return 372 } 373 sort.Sort(s.hw.durations) 374 minDuration := int64(s.hw.durations[0]) 375 maxDuration := int64(s.hw.durations[len(s.hw.durations)-1]) 376 377 // Use the largest unit that can represent the minimum time duration. 378 s.hw.unit = time.Nanosecond 379 for _, u := range []time.Duration{time.Microsecond, time.Millisecond, time.Second} { 380 if minDuration <= int64(u) { 381 break 382 } 383 s.hw.unit = u 384 } 385 386 numBuckets := s.numBuckets 387 if n := int(maxDuration - minDuration + 1); n < numBuckets { 388 numBuckets = n 389 } 390 s.hw.histogram = NewHistogram(HistogramOptions{ 391 NumBuckets: numBuckets, 392 // max-min(lower bound of last bucket) = (1 + growthFactor)^(numBuckets-2) * baseBucketSize. 393 GrowthFactor: math.Pow(float64(maxDuration-minDuration), 1/float64(numBuckets-2)) - 1, 394 BaseBucketSize: 1.0, 395 MinValue: minDuration, 396 }) 397 for _, d := range s.hw.durations { 398 s.hw.histogram.Add(int64(d)) 399 } 400 result.Data.Fiftieth = s.hw.durations[max(s.hw.histogram.Count*int64(50)/100-1, 0)] 401 result.Data.Ninetieth = s.hw.durations[max(s.hw.histogram.Count*int64(90)/100-1, 0)] 402 result.Data.NinetyNinth = s.hw.durations[max(s.hw.histogram.Count*int64(99)/100-1, 0)] 403 result.Data.Average = time.Duration(float64(s.hw.histogram.Sum) / float64(s.hw.histogram.Count)) 404 } 405 406 // dump returns a printable version. 407 func (s *Stats) dump(result *BenchResults) { 408 var b bytes.Buffer 409 410 // Go and gRPC version information. 411 b.WriteString(fmt.Sprintf("%s/grpc%s\n", result.GoVersion, result.GrpcVersion)) 412 413 // This prints the run mode and all features of the bench on a line. 414 b.WriteString(fmt.Sprintf("%s-%s:\n", result.RunMode, result.Features.String())) 415 416 unit := s.hw.unit 417 tUnit := fmt.Sprintf("%v", unit)[1:] // stores one of s, ms, μs, ns 418 419 if l := result.Data.Fiftieth; l != 0 { 420 b.WriteString(fmt.Sprintf("50_Latency: %s%s\t", strconv.FormatFloat(float64(l)/float64(unit), 'f', 4, 64), tUnit)) 421 } 422 if l := result.Data.Ninetieth; l != 0 { 423 b.WriteString(fmt.Sprintf("90_Latency: %s%s\t", strconv.FormatFloat(float64(l)/float64(unit), 'f', 4, 64), tUnit)) 424 } 425 if l := result.Data.NinetyNinth; l != 0 { 426 b.WriteString(fmt.Sprintf("99_Latency: %s%s\t", strconv.FormatFloat(float64(l)/float64(unit), 'f', 4, 64), tUnit)) 427 } 428 if l := result.Data.Average; l != 0 { 429 b.WriteString(fmt.Sprintf("Avg_Latency: %s%s\t", strconv.FormatFloat(float64(l)/float64(unit), 'f', 4, 64), tUnit)) 430 } 431 b.WriteString(fmt.Sprintf("Bytes/op: %v\t", result.Data.AllocedBytes)) 432 b.WriteString(fmt.Sprintf("Allocs/op: %v\t\n", result.Data.Allocs)) 433 434 // This prints the histogram stats for the latency. 435 if s.hw.histogram == nil { 436 b.WriteString("Histogram (empty)\n") 437 } else { 438 b.WriteString(fmt.Sprintf("Histogram (unit: %s)\n", tUnit)) 439 s.hw.histogram.PrintWithUnit(&b, float64(unit)) 440 } 441 442 // Print throughput data. 443 req := result.Data.SendOps 444 if req == 0 { 445 req = result.Data.TotalOps 446 } 447 resp := result.Data.RecvOps 448 if resp == 0 { 449 resp = result.Data.TotalOps 450 } 451 b.WriteString(fmt.Sprintf("Number of requests: %v\tRequest throughput: %v bit/s\n", req, result.Data.ReqT)) 452 b.WriteString(fmt.Sprintf("Number of responses: %v\tResponse throughput: %v bit/s\n", resp, result.Data.RespT)) 453 fmt.Println(b.String()) 454 } 455 456 func max(a, b int64) int64 { 457 if a > b { 458 return a 459 } 460 return b 461 }