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.
 
 
 

209 lines
5.5 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
  19. import (
  20. "bufio"
  21. "bytes"
  22. "fmt"
  23. "os"
  24. "runtime"
  25. "sort"
  26. "strings"
  27. "sync"
  28. "testing"
  29. )
  30. var (
  31. curB *testing.B
  32. curBenchName string
  33. curStats map[string]*Stats
  34. orgStdout *os.File
  35. nextOutPos int
  36. injectCond *sync.Cond
  37. injectDone chan struct{}
  38. )
  39. // AddStats adds a new unnamed Stats instance to the current benchmark. You need
  40. // to run benchmarks by calling RunTestMain() to inject the stats to the
  41. // benchmark results. If numBuckets is not positive, the default value (16) will
  42. // be used. Please note that this calls b.ResetTimer() since it may be blocked
  43. // until the previous benchmark stats is printed out. So AddStats() should
  44. // typically be called at the very beginning of each benchmark function.
  45. func AddStats(b *testing.B, numBuckets int) *Stats {
  46. return AddStatsWithName(b, "", numBuckets)
  47. }
  48. // AddStatsWithName adds a new named Stats instance to the current benchmark.
  49. // With this, you can add multiple stats in a single benchmark. You need
  50. // to run benchmarks by calling RunTestMain() to inject the stats to the
  51. // benchmark results. If numBuckets is not positive, the default value (16) will
  52. // be used. Please note that this calls b.ResetTimer() since it may be blocked
  53. // until the previous benchmark stats is printed out. So AddStatsWithName()
  54. // should typically be called at the very beginning of each benchmark function.
  55. func AddStatsWithName(b *testing.B, name string, numBuckets int) *Stats {
  56. var benchName string
  57. for i := 1; ; i++ {
  58. pc, _, _, ok := runtime.Caller(i)
  59. if !ok {
  60. panic("benchmark function not found")
  61. }
  62. p := strings.Split(runtime.FuncForPC(pc).Name(), ".")
  63. benchName = p[len(p)-1]
  64. if strings.HasPrefix(benchName, "run") {
  65. break
  66. }
  67. }
  68. procs := runtime.GOMAXPROCS(-1)
  69. if procs != 1 {
  70. benchName = fmt.Sprintf("%s-%d", benchName, procs)
  71. }
  72. stats := NewStats(numBuckets)
  73. if injectCond != nil {
  74. // We need to wait until the previous benchmark stats is printed out.
  75. injectCond.L.Lock()
  76. for curB != nil && curBenchName != benchName {
  77. injectCond.Wait()
  78. }
  79. curB = b
  80. curBenchName = benchName
  81. curStats[name] = stats
  82. injectCond.L.Unlock()
  83. }
  84. b.ResetTimer()
  85. return stats
  86. }
  87. // RunTestMain runs the tests with enabling injection of benchmark stats. It
  88. // returns an exit code to pass to os.Exit.
  89. func RunTestMain(m *testing.M) int {
  90. startStatsInjector()
  91. defer stopStatsInjector()
  92. return m.Run()
  93. }
  94. // startStatsInjector starts stats injection to benchmark results.
  95. func startStatsInjector() {
  96. orgStdout = os.Stdout
  97. r, w, _ := os.Pipe()
  98. os.Stdout = w
  99. nextOutPos = 0
  100. resetCurBenchStats()
  101. injectCond = sync.NewCond(&sync.Mutex{})
  102. injectDone = make(chan struct{})
  103. go func() {
  104. defer close(injectDone)
  105. scanner := bufio.NewScanner(r)
  106. scanner.Split(splitLines)
  107. for scanner.Scan() {
  108. injectStatsIfFinished(scanner.Text())
  109. }
  110. if err := scanner.Err(); err != nil {
  111. panic(err)
  112. }
  113. }()
  114. }
  115. // stopStatsInjector stops stats injection and restores os.Stdout.
  116. func stopStatsInjector() {
  117. os.Stdout.Close()
  118. <-injectDone
  119. injectCond = nil
  120. os.Stdout = orgStdout
  121. }
  122. // splitLines is a split function for a bufio.Scanner that returns each line
  123. // of text, teeing texts to the original stdout even before each line ends.
  124. func splitLines(data []byte, eof bool) (advance int, token []byte, err error) {
  125. if eof && len(data) == 0 {
  126. return 0, nil, nil
  127. }
  128. if i := bytes.IndexByte(data, '\n'); i >= 0 {
  129. orgStdout.Write(data[nextOutPos : i+1])
  130. nextOutPos = 0
  131. return i + 1, data[0:i], nil
  132. }
  133. orgStdout.Write(data[nextOutPos:])
  134. nextOutPos = len(data)
  135. if eof {
  136. // This is a final, non-terminated line. Return it.
  137. return len(data), data, nil
  138. }
  139. return 0, nil, nil
  140. }
  141. // injectStatsIfFinished prints out the stats if the current benchmark finishes.
  142. func injectStatsIfFinished(line string) {
  143. injectCond.L.Lock()
  144. defer injectCond.L.Unlock()
  145. // We assume that the benchmark results start with "Benchmark".
  146. if curB == nil || !strings.HasPrefix(line, "Benchmark") {
  147. return
  148. }
  149. if !curB.Failed() {
  150. // Output all stats in alphabetical order.
  151. names := make([]string, 0, len(curStats))
  152. for name := range curStats {
  153. names = append(names, name)
  154. }
  155. sort.Strings(names)
  156. for _, name := range names {
  157. stats := curStats[name]
  158. // The output of stats starts with a header like "Histogram (unit: ms)"
  159. // followed by statistical properties and the buckets. Add the stats name
  160. // if it is a named stats and indent them as Go testing outputs.
  161. lines := strings.Split(stats.String(), "\n")
  162. if n := len(lines); n > 0 {
  163. if name != "" {
  164. name = ": " + name
  165. }
  166. fmt.Fprintf(orgStdout, "--- %s%s\n", lines[0], name)
  167. for _, line := range lines[1 : n-1] {
  168. fmt.Fprintf(orgStdout, "\t%s\n", line)
  169. }
  170. }
  171. }
  172. }
  173. resetCurBenchStats()
  174. injectCond.Signal()
  175. }
  176. // resetCurBenchStats resets the current benchmark stats.
  177. func resetCurBenchStats() {
  178. curB = nil
  179. curBenchName = ""
  180. curStats = make(map[string]*Stats)
  181. }