stats.go 8.7 KB

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