1 package gmeasure_test
2
3 import (
4 "fmt"
5 "strings"
6 "sync"
7 "time"
8
9 . "github.com/onsi/ginkgo/v2"
10 . "github.com/onsi/gomega"
11
12 "github.com/onsi/gomega/gmeasure"
13 )
14
15 var _ = Describe("Experiment", func() {
16 var e *gmeasure.Experiment
17 BeforeEach(func() {
18 e = gmeasure.NewExperiment("Test Experiment")
19 })
20
21 Describe("Recording Notes", func() {
22 It("creates a note Measurement", func() {
23 e.RecordNote("I'm a note", gmeasure.Style("{{blue}}"))
24 measurement := e.Measurements[0]
25 Ω(measurement.Type).Should(Equal(gmeasure.MeasurementTypeNote))
26 Ω(measurement.ExperimentName).Should(Equal("Test Experiment"))
27 Ω(measurement.Note).Should(Equal("I'm a note"))
28 Ω(measurement.Style).Should(Equal("{{blue}}"))
29 })
30 })
31
32 Describe("Recording Durations", func() {
33 commonMeasurementAssertions := func() gmeasure.Measurement {
34 measurement := e.Get("runtime")
35 Ω(measurement.Type).Should(Equal(gmeasure.MeasurementTypeDuration))
36 Ω(measurement.ExperimentName).Should(Equal("Test Experiment"))
37 Ω(measurement.Name).Should(Equal("runtime"))
38 Ω(measurement.Units).Should(Equal("duration"))
39 Ω(measurement.Style).Should(Equal("{{red}}"))
40 Ω(measurement.PrecisionBundle.Duration).Should(Equal(time.Millisecond))
41 return measurement
42 }
43
44 BeforeEach(func() {
45 e.RecordDuration("runtime", time.Second, gmeasure.Annotation("first"), gmeasure.Style("{{red}}"), gmeasure.Precision(time.Millisecond), gmeasure.Units("ignored"))
46 })
47
48 Describe("RecordDuration", func() {
49 It("generates a measurement and records the passed-in duration along with any relevant decorations", func() {
50 e.RecordDuration("runtime", time.Minute, gmeasure.Annotation("second"))
51 measurement := commonMeasurementAssertions()
52 Ω(measurement.Durations).Should(Equal([]time.Duration{time.Second, time.Minute}))
53 Ω(measurement.Annotations).Should(Equal([]string{"first", "second"}))
54 })
55 })
56
57 Describe("MeasureDuration", func() {
58 It("measure the duration of the passed-in function", func() {
59 e.MeasureDuration("runtime", func() {
60 time.Sleep(200 * time.Millisecond)
61 }, gmeasure.Annotation("second"))
62 measurement := commonMeasurementAssertions()
63 Ω(measurement.Durations[0]).Should(Equal(time.Second))
64 Ω(measurement.Durations[1]).Should(BeNumerically("~", 200*time.Millisecond, 20*time.Millisecond))
65 Ω(measurement.Annotations).Should(Equal([]string{"first", "second"}))
66 })
67 })
68
69 Describe("SampleDuration", func() {
70 It("samples the passed-in function according to SampleConfig and records the measured durations", func() {
71 e.SampleDuration("runtime", func(_ int) {
72 time.Sleep(100 * time.Millisecond)
73 }, gmeasure.SamplingConfig{N: 3}, gmeasure.Annotation("sampled"))
74 measurement := commonMeasurementAssertions()
75 Ω(measurement.Durations[0]).Should(Equal(time.Second))
76 Ω(measurement.Durations[1]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
77 Ω(measurement.Durations[2]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
78 Ω(measurement.Durations[3]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
79 Ω(measurement.Annotations).Should(Equal([]string{"first", "sampled", "sampled", "sampled"}))
80 })
81 })
82
83 Describe("SampleAnnotatedDuration", func() {
84 It("samples the passed-in function according to SampleConfig and records the measured durations and returned annotations", func() {
85 e.SampleAnnotatedDuration("runtime", func(idx int) gmeasure.Annotation {
86 time.Sleep(100 * time.Millisecond)
87 return gmeasure.Annotation(fmt.Sprintf("sampled-%d", idx+1))
88 }, gmeasure.SamplingConfig{N: 3}, gmeasure.Annotation("ignored"))
89 measurement := commonMeasurementAssertions()
90 Ω(measurement.Durations[0]).Should(Equal(time.Second))
91 Ω(measurement.Durations[1]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
92 Ω(measurement.Durations[2]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
93 Ω(measurement.Durations[3]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
94 Ω(measurement.Annotations).Should(Equal([]string{"first", "sampled-1", "sampled-2", "sampled-3"}))
95 })
96 })
97 })
98
99 Describe("Stopwatch Support", func() {
100 It("can generate a new stopwatch tied to the experiment", func() {
101 s := e.NewStopwatch()
102 time.Sleep(50 * time.Millisecond)
103 s.Record("runtime", gmeasure.Annotation("first")).Reset()
104 time.Sleep(100 * time.Millisecond)
105 s.Record("runtime", gmeasure.Annotation("second")).Reset()
106 time.Sleep(150 * time.Millisecond)
107 s.Record("runtime", gmeasure.Annotation("third"))
108 measurement := e.Get("runtime")
109 Ω(measurement.Durations[0]).Should(BeNumerically("~", 50*time.Millisecond, 20*time.Millisecond))
110 Ω(measurement.Durations[1]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
111 Ω(measurement.Durations[2]).Should(BeNumerically("~", 150*time.Millisecond, 20*time.Millisecond))
112 Ω(measurement.Annotations).Should(Equal([]string{"first", "second", "third"}))
113 })
114 })
115
116 Describe("Recording Values", func() {
117 commonMeasurementAssertions := func() gmeasure.Measurement {
118 measurement := e.Get("sprockets")
119 Ω(measurement.Type).Should(Equal(gmeasure.MeasurementTypeValue))
120 Ω(measurement.ExperimentName).Should(Equal("Test Experiment"))
121 Ω(measurement.Name).Should(Equal("sprockets"))
122 Ω(measurement.Units).Should(Equal("widgets"))
123 Ω(measurement.Style).Should(Equal("{{yellow}}"))
124 Ω(measurement.PrecisionBundle.ValueFormat).Should(Equal("%.0f"))
125 return measurement
126 }
127
128 BeforeEach(func() {
129 e.RecordValue("sprockets", 3.2, gmeasure.Annotation("first"), gmeasure.Style("{{yellow}}"), gmeasure.Precision(0), gmeasure.Units("widgets"))
130 })
131
132 Describe("RecordValue", func() {
133 It("generates a measurement and records the passed-in value along with any relevant decorations", func() {
134 e.RecordValue("sprockets", 17.4, gmeasure.Annotation("second"))
135 measurement := commonMeasurementAssertions()
136 Ω(measurement.Values).Should(Equal([]float64{3.2, 17.4}))
137 Ω(measurement.Annotations).Should(Equal([]string{"first", "second"}))
138 })
139 })
140
141 Describe("MeasureValue", func() {
142 It("records the value returned by the passed-in function", func() {
143 e.MeasureValue("sprockets", func() float64 {
144 return 17.4
145 }, gmeasure.Annotation("second"))
146 measurement := commonMeasurementAssertions()
147 Ω(measurement.Values).Should(Equal([]float64{3.2, 17.4}))
148 Ω(measurement.Annotations).Should(Equal([]string{"first", "second"}))
149 })
150 })
151
152 Describe("SampleValue", func() {
153 It("samples the passed-in function according to SampleConfig and records the resulting values", func() {
154 e.SampleValue("sprockets", func(idx int) float64 {
155 return 17.4 + float64(idx)
156 }, gmeasure.SamplingConfig{N: 3}, gmeasure.Annotation("sampled"))
157 measurement := commonMeasurementAssertions()
158 Ω(measurement.Values).Should(Equal([]float64{3.2, 17.4, 18.4, 19.4}))
159 Ω(measurement.Annotations).Should(Equal([]string{"first", "sampled", "sampled", "sampled"}))
160 })
161 })
162
163 Describe("SampleAnnotatedValue", func() {
164 It("samples the passed-in function according to SampleConfig and records the returned values and annotations", func() {
165 e.SampleAnnotatedValue("sprockets", func(idx int) (float64, gmeasure.Annotation) {
166 return 17.4 + float64(idx), gmeasure.Annotation(fmt.Sprintf("sampled-%d", idx+1))
167 }, gmeasure.SamplingConfig{N: 3}, gmeasure.Annotation("ignored"))
168 measurement := commonMeasurementAssertions()
169 Ω(measurement.Values).Should(Equal([]float64{3.2, 17.4, 18.4, 19.4}))
170 Ω(measurement.Annotations).Should(Equal([]string{"first", "sampled-1", "sampled-2", "sampled-3"}))
171 })
172 })
173 })
174
175 Describe("Sampling", func() {
176 var indices []int
177 BeforeEach(func() {
178 indices = []int{}
179 })
180
181 ints := func(n int) []int {
182 out := []int{}
183 for i := 0; i < n; i++ {
184 out = append(out, i)
185 }
186 return out
187 }
188
189 It("calls the function repeatedly passing in an index", func() {
190 e.Sample(func(idx int) {
191 indices = append(indices, idx)
192 }, gmeasure.SamplingConfig{N: 3})
193
194 Ω(indices).Should(Equal(ints(3)))
195 })
196
197 It("can cap the maximum number of samples", func() {
198 e.Sample(func(idx int) {
199 indices = append(indices, idx)
200 }, gmeasure.SamplingConfig{N: 10, Duration: time.Minute})
201
202 Ω(indices).Should(Equal(ints(10)))
203 })
204
205 It("can cap the maximum sample time", func() {
206 e.Sample(func(idx int) {
207 indices = append(indices, idx)
208 time.Sleep(10 * time.Millisecond)
209 }, gmeasure.SamplingConfig{N: 100, Duration: 100 * time.Millisecond, MinSamplingInterval: 5 * time.Millisecond})
210
211 Ω(len(indices)).Should(BeNumerically("~", 10, 3))
212 Ω(indices).Should(Equal(ints(len(indices))))
213 })
214
215 It("can ensure a minimum interval between samples", func() {
216 times := map[int]time.Time{}
217 e.Sample(func(idx int) {
218 times[idx] = time.Now()
219 }, gmeasure.SamplingConfig{N: 10, Duration: 200 * time.Millisecond, MinSamplingInterval: 50 * time.Millisecond, NumParallel: 1})
220
221 Ω(len(times)).Should(BeNumerically("~", 4, 2))
222 Ω(times[1]).Should(BeTemporally(">", times[0], 50*time.Millisecond))
223 Ω(times[2]).Should(BeTemporally(">", times[1], 50*time.Millisecond))
224 })
225
226 It("can run samples in parallel", func() {
227 lock := &sync.Mutex{}
228
229 e.Sample(func(idx int) {
230 lock.Lock()
231 indices = append(indices, idx)
232 lock.Unlock()
233 time.Sleep(10 * time.Millisecond)
234 }, gmeasure.SamplingConfig{N: 100, Duration: 100 * time.Millisecond, NumParallel: 3})
235
236 lock.Lock()
237 defer lock.Unlock()
238 Ω(len(indices)).Should(BeNumerically("~", 30, 10))
239 Ω(indices).Should(ConsistOf(ints(len(indices))))
240 })
241
242 It("panics if the SamplingConfig does not specify a ceiling", func() {
243 Expect(func() {
244 e.Sample(func(_ int) {}, gmeasure.SamplingConfig{MinSamplingInterval: time.Second})
245 }).To(PanicWith("you must specify at least one of SamplingConfig.N and SamplingConfig.Duration"))
246 })
247
248 It("panics if the SamplingConfig includes both a minimum interval and a directive to run in parallel", func() {
249 Expect(func() {
250 e.Sample(func(_ int) {}, gmeasure.SamplingConfig{N: 10, MinSamplingInterval: time.Second, NumParallel: 2})
251 }).To(PanicWith("you cannot specify both SamplingConfig.MinSamplingInterval and SamplingConfig.NumParallel"))
252 })
253 })
254
255 Describe("recording multiple entries", func() {
256 It("always appends to the correct measurement (by name)", func() {
257 e.RecordDuration("alpha", time.Second)
258 e.RecordDuration("beta", time.Minute)
259 e.RecordValue("gamma", 1)
260 e.RecordValue("delta", 2.71)
261 e.RecordDuration("alpha", 2*time.Second)
262 e.RecordDuration("beta", 2*time.Minute)
263 e.RecordValue("gamma", 2)
264 e.RecordValue("delta", 3.141)
265
266 Ω(e.Measurements).Should(HaveLen(4))
267 Ω(e.Get("alpha").Durations).Should(Equal([]time.Duration{time.Second, 2 * time.Second}))
268 Ω(e.Get("beta").Durations).Should(Equal([]time.Duration{time.Minute, 2 * time.Minute}))
269 Ω(e.Get("gamma").Values).Should(Equal([]float64{1, 2}))
270 Ω(e.Get("delta").Values).Should(Equal([]float64{2.71, 3.141}))
271 })
272
273 It("panics if you incorrectly mix types", func() {
274 e.RecordDuration("runtime", time.Second)
275 Ω(func() {
276 e.RecordValue("runtime", 3.141)
277 }).Should(PanicWith("attempting to record value with name 'runtime'. That name is already in-use for recording durations."))
278
279 e.RecordValue("sprockets", 2)
280 Ω(func() {
281 e.RecordDuration("sprockets", time.Minute)
282 }).Should(PanicWith("attempting to record duration with name 'sprockets'. That name is already in-use for recording values."))
283 })
284 })
285
286 Describe("Decorators", func() {
287 It("uses the default precisions when none is specified", func() {
288 e.RecordValue("sprockets", 2)
289 e.RecordDuration("runtime", time.Minute)
290
291 Ω(e.Get("sprockets").PrecisionBundle.ValueFormat).Should(Equal("%.3f"))
292 Ω(e.Get("runtime").PrecisionBundle.Duration).Should(Equal(100 * time.Microsecond))
293 })
294
295 It("panics if an unsupported type is passed into Precision", func() {
296 Ω(func() {
297 gmeasure.Precision("aardvark")
298 }).Should(PanicWith("invalid precision type, must be time.Duration or int"))
299 })
300
301 It("panics if an unrecognized argumnet is passed in", func() {
302 Ω(func() {
303 e.RecordValue("sprockets", 2, "boom")
304 }).Should(PanicWith(`unrecognized argument "boom"`))
305 })
306 })
307
308 Describe("Getting Measurements", func() {
309 Context("when the Measurement does not exist", func() {
310 It("returns the zero Measurement", func() {
311 Ω(e.Get("not here")).Should(BeZero())
312 })
313 })
314 })
315
316 Describe("Getting Stats", func() {
317 It("returns the Measurement's Stats", func() {
318 e.RecordValue("alpha", 1)
319 e.RecordValue("alpha", 2)
320 e.RecordValue("alpha", 3)
321 Ω(e.GetStats("alpha")).Should(Equal(e.Get("alpha").Stats()))
322 })
323 })
324
325 Describe("Generating Reports", func() {
326 BeforeEach(func() {
327 e.RecordNote("A note")
328 e.RecordValue("sprockets", 7, gmeasure.Units("widgets"), gmeasure.Precision(0), gmeasure.Style("{{yellow}}"), gmeasure.Annotation("sprockets-1"))
329 e.RecordDuration("runtime", time.Second, gmeasure.Precision(100*time.Millisecond), gmeasure.Style("{{red}}"), gmeasure.Annotation("runtime-1"))
330 e.RecordNote("A blue note", gmeasure.Style("{{blue}}"))
331 e.RecordValue("gear ratio", 10.3, gmeasure.Precision(2), gmeasure.Style("{{green}}"), gmeasure.Annotation("ratio-1"))
332
333 e.RecordValue("sprockets", 8, gmeasure.Annotation("sprockets-2"))
334 e.RecordValue("sprockets", 9, gmeasure.Annotation("sprockets-3"))
335
336 e.RecordDuration("runtime", 2*time.Second, gmeasure.Annotation("runtime-2"))
337 e.RecordValue("gear ratio", 13.758, gmeasure.Precision(2), gmeasure.Annotation("ratio-2"))
338 })
339
340 It("emits a nicely formatted table", func() {
341 expected := strings.Join([]string{
342 "Test Experiment",
343 "Name | N | Min | Median | Mean | StdDev | Max ",
344 "=============================================================================",
345 "A note ",
346 "-----------------------------------------------------------------------------",
347 "sprockets [widgets] | 3 | 7 | 8 | 8 | 1 | 9 ",
348 " | | sprockets-1 | | | | sprockets-3",
349 "-----------------------------------------------------------------------------",
350 "runtime [duration] | 2 | 1s | 1.5s | 1.5s | 500ms | 2s ",
351 " | | runtime-1 | | | | runtime-2 ",
352 "-----------------------------------------------------------------------------",
353 "A blue note ",
354 "-----------------------------------------------------------------------------",
355 "gear ratio | 2 | 10.30 | 12.03 | 12.03 | 1.73 | 13.76 ",
356 " | | ratio-1 | | | | ratio-2 ",
357 "",
358 }, "\n")
359 Ω(e.String()).Should(Equal(expected))
360 })
361
362 It("can also emit a styled table", func() {
363 expected := strings.Join([]string{
364 "{{bold}}Test Experiment",
365 "{{/}}{{bold}}Name {{/}} | {{bold}}N{{/}} | {{bold}}Min {{/}} | {{bold}}Median{{/}} | {{bold}}Mean {{/}} | {{bold}}StdDev{{/}} | {{bold}}Max {{/}}",
366 "=============================================================================",
367 "A note ",
368 "-----------------------------------------------------------------------------",
369 "{{yellow}}sprockets [widgets]{{/}} | {{yellow}}3{{/}} | {{yellow}}7 {{/}} | {{yellow}}8 {{/}} | {{yellow}}8 {{/}} | {{yellow}}1 {{/}} | {{yellow}}9 {{/}}",
370 " | | {{yellow}}sprockets-1{{/}} | | | | {{yellow}}sprockets-3{{/}}",
371 "-----------------------------------------------------------------------------",
372 "{{red}}runtime [duration] {{/}} | {{red}}2{{/}} | {{red}}1s {{/}} | {{red}}1.5s {{/}} | {{red}}1.5s {{/}} | {{red}}500ms {{/}} | {{red}}2s {{/}}",
373 " | | {{red}}runtime-1 {{/}} | | | | {{red}}runtime-2 {{/}}",
374 "-----------------------------------------------------------------------------",
375 "{{blue}}A blue note {{/}}",
376 "-----------------------------------------------------------------------------",
377 "{{green}}gear ratio {{/}} | {{green}}2{{/}} | {{green}}10.30 {{/}} | {{green}}12.03 {{/}} | {{green}}12.03{{/}} | {{green}}1.73 {{/}} | {{green}}13.76 {{/}}",
378 " | | {{green}}ratio-1 {{/}} | | | | {{green}}ratio-2 {{/}}",
379 "",
380 }, "\n")
381 Ω(e.ColorableString()).Should(Equal(expected))
382 })
383 })
384 })
385
View as plain text