1 package gmeasure
2
3 import (
4 "fmt"
5 "math"
6 "sort"
7 "time"
8
9 "github.com/onsi/gomega/gmeasure/table"
10 )
11
12 type MeasurementType uint
13
14 const (
15 MeasurementTypeInvalid MeasurementType = iota
16 MeasurementTypeNote
17 MeasurementTypeDuration
18 MeasurementTypeValue
19 )
20
21 var letEnumSupport = newEnumSupport(map[uint]string{uint(MeasurementTypeInvalid): "INVALID LOG ENTRY TYPE", uint(MeasurementTypeNote): "Note", uint(MeasurementTypeDuration): "Duration", uint(MeasurementTypeValue): "Value"})
22
23 func (s MeasurementType) String() string { return letEnumSupport.String(uint(s)) }
24 func (s *MeasurementType) UnmarshalJSON(b []byte) error {
25 out, err := letEnumSupport.UnmarshJSON(b)
26 *s = MeasurementType(out)
27 return err
28 }
29 func (s MeasurementType) MarshalJSON() ([]byte, error) { return letEnumSupport.MarshJSON(uint(s)) }
30
31
36 type Measurement struct {
37
38 Type MeasurementType
39
40
41 ExperimentName string
42
43
44 Note string
45
46
47 Name string
48
49
50 Style string
51
52
53 Units string
54
55
56
57
58 PrecisionBundle PrecisionBundle
59
60
61 Durations []time.Duration
62
63
64 Values []float64
65
66
67
68 Annotations []string
69 }
70
71 type Measurements []Measurement
72
73 func (m Measurements) IdxWithName(name string) int {
74 for idx, measurement := range m {
75 if measurement.Name == name {
76 return idx
77 }
78 }
79
80 return -1
81 }
82
83 func (m Measurement) report(enableStyling bool) string {
84 out := ""
85 style := m.Style
86 if !enableStyling {
87 style = ""
88 }
89 switch m.Type {
90 case MeasurementTypeNote:
91 out += fmt.Sprintf("%s - Note\n%s\n", m.ExperimentName, m.Note)
92 if style != "" {
93 out = style + out + "{{/}}"
94 }
95 return out
96 case MeasurementTypeValue, MeasurementTypeDuration:
97 out += fmt.Sprintf("%s - %s", m.ExperimentName, m.Name)
98 if m.Units != "" {
99 out += " [" + m.Units + "]"
100 }
101 if style != "" {
102 out = style + out + "{{/}}"
103 }
104 out += "\n"
105 out += m.Stats().String() + "\n"
106 }
107 t := table.NewTable()
108 t.TableStyle.EnableTextStyling = enableStyling
109 switch m.Type {
110 case MeasurementTypeValue:
111 t.AppendRow(table.R(table.C("Value", table.AlignTypeCenter), table.C("Annotation", table.AlignTypeCenter), table.Divider("="), style))
112 for idx := range m.Values {
113 t.AppendRow(table.R(
114 table.C(fmt.Sprintf(m.PrecisionBundle.ValueFormat, m.Values[idx]), table.AlignTypeRight),
115 table.C(m.Annotations[idx], "{{gray}}", table.AlignTypeLeft),
116 ))
117 }
118 case MeasurementTypeDuration:
119 t.AppendRow(table.R(table.C("Duration", table.AlignTypeCenter), table.C("Annotation", table.AlignTypeCenter), table.Divider("="), style))
120 for idx := range m.Durations {
121 t.AppendRow(table.R(
122 table.C(m.Durations[idx].Round(m.PrecisionBundle.Duration).String(), style, table.AlignTypeRight),
123 table.C(m.Annotations[idx], "{{gray}}", table.AlignTypeLeft),
124 ))
125 }
126 }
127 out += t.Render()
128 return out
129 }
130
131
135 func (m Measurement) ColorableString() string {
136 return m.report(true)
137 }
138
139
142 func (m Measurement) String() string {
143 return m.report(false)
144 }
145
146
149 func (m Measurement) Stats() Stats {
150 if m.Type == MeasurementTypeInvalid || m.Type == MeasurementTypeNote {
151 return Stats{}
152 }
153
154 out := Stats{
155 ExperimentName: m.ExperimentName,
156 MeasurementName: m.Name,
157 Style: m.Style,
158 Units: m.Units,
159 PrecisionBundle: m.PrecisionBundle,
160 }
161
162 switch m.Type {
163 case MeasurementTypeValue:
164 out.Type = StatsTypeValue
165 out.N = len(m.Values)
166 if out.N == 0 {
167 return out
168 }
169 indices, sum := make([]int, len(m.Values)), 0.0
170 for idx, v := range m.Values {
171 indices[idx] = idx
172 sum += v
173 }
174 sort.Slice(indices, func(i, j int) bool {
175 return m.Values[indices[i]] < m.Values[indices[j]]
176 })
177 out.ValueBundle = map[Stat]float64{
178 StatMin: m.Values[indices[0]],
179 StatMax: m.Values[indices[out.N-1]],
180 StatMean: sum / float64(out.N),
181 StatStdDev: 0.0,
182 }
183 out.AnnotationBundle = map[Stat]string{
184 StatMin: m.Annotations[indices[0]],
185 StatMax: m.Annotations[indices[out.N-1]],
186 }
187
188 if out.N%2 == 0 {
189 out.ValueBundle[StatMedian] = (m.Values[indices[out.N/2]] + m.Values[indices[out.N/2-1]]) / 2.0
190 } else {
191 out.ValueBundle[StatMedian] = m.Values[indices[(out.N-1)/2]]
192 }
193
194 for _, v := range m.Values {
195 out.ValueBundle[StatStdDev] += (v - out.ValueBundle[StatMean]) * (v - out.ValueBundle[StatMean])
196 }
197 out.ValueBundle[StatStdDev] = math.Sqrt(out.ValueBundle[StatStdDev] / float64(out.N))
198 case MeasurementTypeDuration:
199 out.Type = StatsTypeDuration
200 out.N = len(m.Durations)
201 if out.N == 0 {
202 return out
203 }
204 indices, sum := make([]int, len(m.Durations)), time.Duration(0)
205 for idx, v := range m.Durations {
206 indices[idx] = idx
207 sum += v
208 }
209 sort.Slice(indices, func(i, j int) bool {
210 return m.Durations[indices[i]] < m.Durations[indices[j]]
211 })
212 out.DurationBundle = map[Stat]time.Duration{
213 StatMin: m.Durations[indices[0]],
214 StatMax: m.Durations[indices[out.N-1]],
215 StatMean: sum / time.Duration(out.N),
216 }
217 out.AnnotationBundle = map[Stat]string{
218 StatMin: m.Annotations[indices[0]],
219 StatMax: m.Annotations[indices[out.N-1]],
220 }
221
222 if out.N%2 == 0 {
223 out.DurationBundle[StatMedian] = (m.Durations[indices[out.N/2]] + m.Durations[indices[out.N/2-1]]) / 2
224 } else {
225 out.DurationBundle[StatMedian] = m.Durations[indices[(out.N-1)/2]]
226 }
227 stdDev := 0.0
228 for _, v := range m.Durations {
229 stdDev += float64(v-out.DurationBundle[StatMean]) * float64(v-out.DurationBundle[StatMean])
230 }
231 out.DurationBundle[StatStdDev] = time.Duration(math.Sqrt(stdDev / float64(out.N)))
232 }
233
234 return out
235 }
236
View as plain text