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.
 
 
 

150 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. type Aggregate struct {
  64. Name string
  65. Count, Errors int
  66. Min, Median, Max time.Duration
  67. P75, P90, P95, P99 time.Duration // percentiles
  68. }
  69. // NewAggregate constructs an aggregate from latencies. Returns nil if latencies does not contain aggregateable data.
  70. func NewAggregate(name string, latencies []time.Duration, errorCount int) *Aggregate {
  71. agg := Aggregate{Name: name, Count: len(latencies), Errors: errorCount}
  72. if len(latencies) == 0 {
  73. return nil
  74. }
  75. var ok bool
  76. if agg.Min, ok = quantile(latencies, 0, 2); !ok {
  77. return nil
  78. }
  79. if agg.Median, ok = quantile(latencies, 1, 2); !ok {
  80. return nil
  81. }
  82. if agg.Max, ok = quantile(latencies, 2, 2); !ok {
  83. return nil
  84. }
  85. if agg.P75, ok = quantile(latencies, 75, 100); !ok {
  86. return nil
  87. }
  88. if agg.P90, ok = quantile(latencies, 90, 100); !ok {
  89. return nil
  90. }
  91. if agg.P95, ok = quantile(latencies, 95, 100); !ok {
  92. return nil
  93. }
  94. if agg.P99, ok = quantile(latencies, 99, 100); !ok {
  95. return nil
  96. }
  97. return &agg
  98. }
  99. func (agg *Aggregate) String() string {
  100. if agg == nil {
  101. return "no data"
  102. }
  103. var buf bytes.Buffer
  104. tw := tabwriter.NewWriter(&buf, 0, 0, 1, ' ', 0) // one-space padding
  105. fmt.Fprintf(tw, "min:\t%v\nmedian:\t%v\nmax:\t%v\n95th percentile:\t%v\n99th percentile:\t%v\n",
  106. agg.Min, agg.Median, agg.Max, agg.P95, agg.P99)
  107. tw.Flush()
  108. return buf.String()
  109. }
  110. // WriteCSV writes a csv file to the given Writer,
  111. // with a header row and one row per aggregate.
  112. func WriteCSV(aggs []*Aggregate, iow io.Writer) (err error) {
  113. w := csv.NewWriter(iow)
  114. defer func() {
  115. w.Flush()
  116. if err == nil {
  117. err = w.Error()
  118. }
  119. }()
  120. err = w.Write([]string{"name", "count", "errors", "min", "median", "max", "p75", "p90", "p95", "p99"})
  121. if err != nil {
  122. return err
  123. }
  124. for _, agg := range aggs {
  125. err = w.Write([]string{
  126. agg.Name, strconv.Itoa(agg.Count), strconv.Itoa(agg.Errors),
  127. agg.Min.String(), agg.Median.String(), agg.Max.String(),
  128. agg.P75.String(), agg.P90.String(), agg.P95.String(), agg.P99.String(),
  129. })
  130. if err != nil {
  131. return err
  132. }
  133. }
  134. return nil
  135. }