You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

304 line
8.7 KiB

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