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.
 
 
 

151 lines
3.8 KiB

  1. // Copyright 2016 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package stat
  15. import (
  16. "bytes"
  17. "encoding/csv"
  18. "fmt"
  19. "io"
  20. "math"
  21. "sort"
  22. "strconv"
  23. "text/tabwriter"
  24. "time"
  25. )
  26. type byDuration []time.Duration
  27. func (data byDuration) Len() int { return len(data) }
  28. func (data byDuration) Swap(i, j int) { data[i], data[j] = data[j], data[i] }
  29. func (data byDuration) Less(i, j int) bool { return data[i] < data[j] }
  30. // quantile returns a value representing the kth of q quantiles.
  31. // May alter the order of data.
  32. func quantile(data []time.Duration, k, q int) (quantile time.Duration, ok bool) {
  33. if len(data) < 1 {
  34. return 0, false
  35. }
  36. if k > q {
  37. return 0, false
  38. }
  39. if k < 0 || q < 1 {
  40. return 0, false
  41. }
  42. sort.Sort(byDuration(data))
  43. if k == 0 {
  44. return data[0], true
  45. }
  46. if k == q {
  47. return data[len(data)-1], true
  48. }
  49. bucketSize := float64(len(data)-1) / float64(q)
  50. i := float64(k) * bucketSize
  51. lower := int(math.Trunc(i))
  52. var upper int
  53. if i > float64(lower) && lower+1 < len(data) {
  54. // If the quantile lies between two elements
  55. upper = lower + 1
  56. } else {
  57. upper = lower
  58. }
  59. weightUpper := i - float64(lower)
  60. weightLower := 1 - weightUpper
  61. return time.Duration(weightLower*float64(data[lower]) + weightUpper*float64(data[upper])), true
  62. }
  63. // Aggregate is an aggregate of latencies.
  64. type Aggregate struct {
  65. Name string
  66. Count, Errors int
  67. Min, Median, Max time.Duration
  68. P75, P90, P95, P99 time.Duration // percentiles
  69. }
  70. // NewAggregate constructs an aggregate from latencies. Returns nil if latencies does not contain aggregateable data.
  71. func NewAggregate(name string, latencies []time.Duration, errorCount int) *Aggregate {
  72. agg := Aggregate{Name: name, Count: len(latencies), Errors: errorCount}
  73. if len(latencies) == 0 {
  74. return nil
  75. }
  76. var ok bool
  77. if agg.Min, ok = quantile(latencies, 0, 2); !ok {
  78. return nil
  79. }
  80. if agg.Median, ok = quantile(latencies, 1, 2); !ok {
  81. return nil
  82. }
  83. if agg.Max, ok = quantile(latencies, 2, 2); !ok {
  84. return nil
  85. }
  86. if agg.P75, ok = quantile(latencies, 75, 100); !ok {
  87. return nil
  88. }
  89. if agg.P90, ok = quantile(latencies, 90, 100); !ok {
  90. return nil
  91. }
  92. if agg.P95, ok = quantile(latencies, 95, 100); !ok {
  93. return nil
  94. }
  95. if agg.P99, ok = quantile(latencies, 99, 100); !ok {
  96. return nil
  97. }
  98. return &agg
  99. }
  100. func (agg *Aggregate) String() string {
  101. if agg == nil {
  102. return "no data"
  103. }
  104. var buf bytes.Buffer
  105. tw := tabwriter.NewWriter(&buf, 0, 0, 1, ' ', 0) // one-space padding
  106. fmt.Fprintf(tw, "min:\t%v\nmedian:\t%v\nmax:\t%v\n95th percentile:\t%v\n99th percentile:\t%v\n",
  107. agg.Min, agg.Median, agg.Max, agg.P95, agg.P99)
  108. tw.Flush()
  109. return buf.String()
  110. }
  111. // WriteCSV writes a csv file to the given Writer,
  112. // with a header row and one row per aggregate.
  113. func WriteCSV(aggs []*Aggregate, iow io.Writer) (err error) {
  114. w := csv.NewWriter(iow)
  115. defer func() {
  116. w.Flush()
  117. if err == nil {
  118. err = w.Error()
  119. }
  120. }()
  121. err = w.Write([]string{"name", "count", "errors", "min", "median", "max", "p75", "p90", "p95", "p99"})
  122. if err != nil {
  123. return err
  124. }
  125. for _, agg := range aggs {
  126. err = w.Write([]string{
  127. agg.Name, strconv.Itoa(agg.Count), strconv.Itoa(agg.Errors),
  128. agg.Min.String(), agg.Median.String(), agg.Max.String(),
  129. agg.P75.String(), agg.P90.String(), agg.P95.String(), agg.P99.String(),
  130. })
  131. if err != nil {
  132. return err
  133. }
  134. }
  135. return nil
  136. }