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
19package stats
20
21import (
22	"bytes"
23	"fmt"
24	"io"
25	"math"
26	"sort"
27	"strconv"
28	"time"
29)
30
31// Features contains most fields for a benchmark
32type Features struct {
33	NetworkMode        string
34	EnableTrace        bool
35	Latency            time.Duration
36	Kbps               int
37	Mtu                int
38	MaxConcurrentCalls int
39	ReqSizeBytes       int
40	RespSizeBytes      int
41	EnableCompressor   bool
42	EnableChannelz     bool
43}
44
45// String returns the textual output of the Features as string.
46func (f Features) String() string {
47	return fmt.Sprintf("traceMode_%t-latency_%s-kbps_%#v-MTU_%#v-maxConcurrentCalls_"+
48		"%#v-reqSize_%#vB-respSize_%#vB-Compressor_%t", f.EnableTrace,
49		f.Latency.String(), f.Kbps, f.Mtu, f.MaxConcurrentCalls, f.ReqSizeBytes, f.RespSizeBytes, f.EnableCompressor)
50}
51
52// ConciseString returns the concise textual output of the Features as string, skipping
53// setting with default value.
54func (f Features) ConciseString() string {
55	noneEmptyPos := []bool{f.EnableTrace, f.Latency != 0, f.Kbps != 0, f.Mtu != 0, true, true, true, f.EnableCompressor, f.EnableChannelz}
56	return PartialPrintString(noneEmptyPos, f, false)
57}
58
59// PartialPrintString can print certain features with different format.
60func PartialPrintString(noneEmptyPos []bool, f Features, shared bool) string {
61	s := ""
62	var (
63		prefix, suffix, linker string
64		isNetwork              bool
65	)
66	if shared {
67		suffix = "\n"
68		linker = ": "
69	} else {
70		prefix = "-"
71		linker = "_"
72	}
73	if noneEmptyPos[0] {
74		s += fmt.Sprintf("%sTrace%s%t%s", prefix, linker, f.EnableTrace, suffix)
75	}
76	if shared && f.NetworkMode != "" {
77		s += fmt.Sprintf("Network: %s \n", f.NetworkMode)
78		isNetwork = true
79	}
80	if !isNetwork {
81		if noneEmptyPos[1] {
82			s += fmt.Sprintf("%slatency%s%s%s", prefix, linker, f.Latency.String(), suffix)
83		}
84		if noneEmptyPos[2] {
85			s += fmt.Sprintf("%skbps%s%#v%s", prefix, linker, f.Kbps, suffix)
86		}
87		if noneEmptyPos[3] {
88			s += fmt.Sprintf("%sMTU%s%#v%s", prefix, linker, f.Mtu, suffix)
89		}
90	}
91	if noneEmptyPos[4] {
92		s += fmt.Sprintf("%sCallers%s%#v%s", prefix, linker, f.MaxConcurrentCalls, suffix)
93	}
94	if noneEmptyPos[5] {
95		s += fmt.Sprintf("%sreqSize%s%#vB%s", prefix, linker, f.ReqSizeBytes, suffix)
96	}
97	if noneEmptyPos[6] {
98		s += fmt.Sprintf("%srespSize%s%#vB%s", prefix, linker, f.RespSizeBytes, suffix)
99	}
100	if noneEmptyPos[7] {
101		s += fmt.Sprintf("%sCompressor%s%t%s", prefix, linker, f.EnableCompressor, suffix)
102	}
103	if noneEmptyPos[8] {
104		s += fmt.Sprintf("%sChannelz%s%t%s", prefix, linker, f.EnableChannelz, suffix)
105	}
106	return s
107}
108
109type percentLatency struct {
110	Percent int
111	Value   time.Duration
112}
113
114// BenchResults records features and result of a benchmark.
115type BenchResults struct {
116	RunMode           string
117	Features          Features
118	Latency           []percentLatency
119	Operations        int
120	NsPerOp           int64
121	AllocedBytesPerOp int64
122	AllocsPerOp       int64
123	SharedPosion      []bool
124}
125
126// SetBenchmarkResult sets features of benchmark and basic results.
127func (stats *Stats) SetBenchmarkResult(mode string, features Features, o int, allocdBytes, allocs int64, sharedPos []bool) {
128	stats.result.RunMode = mode
129	stats.result.Features = features
130	stats.result.Operations = o
131	stats.result.AllocedBytesPerOp = allocdBytes
132	stats.result.AllocsPerOp = allocs
133	stats.result.SharedPosion = sharedPos
134}
135
136// GetBenchmarkResults returns the result of the benchmark including features and result.
137func (stats *Stats) GetBenchmarkResults() BenchResults {
138	return stats.result
139}
140
141// BenchString output latency stats as the format as time + unit.
142func (stats *Stats) BenchString() string {
143	stats.maybeUpdate()
144	s := stats.result
145	res := s.RunMode + "-" + s.Features.String() + ": \n"
146	if len(s.Latency) != 0 {
147		var statsUnit = s.Latency[0].Value
148		var timeUnit = fmt.Sprintf("%v", statsUnit)[1:]
149		for i := 1; i < len(s.Latency)-1; i++ {
150			res += fmt.Sprintf("%d_Latency: %s %s \t", s.Latency[i].Percent,
151				strconv.FormatFloat(float64(s.Latency[i].Value)/float64(statsUnit), 'f', 4, 64), timeUnit)
152		}
153		res += fmt.Sprintf("Avg latency: %s %s \t",
154			strconv.FormatFloat(float64(s.Latency[len(s.Latency)-1].Value)/float64(statsUnit), 'f', 4, 64), timeUnit)
155	}
156	res += fmt.Sprintf("Count: %v \t", s.Operations)
157	res += fmt.Sprintf("%v Bytes/op\t", s.AllocedBytesPerOp)
158	res += fmt.Sprintf("%v Allocs/op\t", s.AllocsPerOp)
159
160	return res
161}
162
163// Stats is a simple helper for gathering additional statistics like histogram
164// during benchmarks. This is not thread safe.
165type Stats struct {
166	numBuckets int
167	unit       time.Duration
168	min, max   int64
169	histogram  *Histogram
170
171	durations durationSlice
172	dirty     bool
173
174	sortLatency bool
175	result      BenchResults
176}
177
178type durationSlice []time.Duration
179
180// NewStats creates a new Stats instance. If numBuckets is not positive,
181// the default value (16) will be used.
182func NewStats(numBuckets int) *Stats {
183	if numBuckets <= 0 {
184		numBuckets = 16
185	}
186	return &Stats{
187		// Use one more bucket for the last unbounded bucket.
188		numBuckets: numBuckets + 1,
189		durations:  make(durationSlice, 0, 100000),
190	}
191}
192
193// Add adds an elapsed time per operation to the stats.
194func (stats *Stats) Add(d time.Duration) {
195	stats.durations = append(stats.durations, d)
196	stats.dirty = true
197}
198
199// Clear resets the stats, removing all values.
200func (stats *Stats) Clear() {
201	stats.durations = stats.durations[:0]
202	stats.histogram = nil
203	stats.dirty = false
204	stats.result = BenchResults{}
205}
206
207//Sort method for durations
208func (a durationSlice) Len() int           { return len(a) }
209func (a durationSlice) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
210func (a durationSlice) Less(i, j int) bool { return a[i] < a[j] }
211func max(a, b int64) int64 {
212	if a > b {
213		return a
214	}
215	return b
216}
217
218// maybeUpdate updates internal stat data if there was any newly added
219// stats since this was updated.
220func (stats *Stats) maybeUpdate() {
221	if !stats.dirty {
222		return
223	}
224
225	if stats.sortLatency {
226		sort.Sort(stats.durations)
227		stats.min = int64(stats.durations[0])
228		stats.max = int64(stats.durations[len(stats.durations)-1])
229	}
230
231	stats.min = math.MaxInt64
232	stats.max = 0
233	for _, d := range stats.durations {
234		if stats.min > int64(d) {
235			stats.min = int64(d)
236		}
237		if stats.max < int64(d) {
238			stats.max = int64(d)
239		}
240	}
241
242	// Use the largest unit that can represent the minimum time duration.
243	stats.unit = time.Nanosecond
244	for _, u := range []time.Duration{time.Microsecond, time.Millisecond, time.Second} {
245		if stats.min <= int64(u) {
246			break
247		}
248		stats.unit = u
249	}
250
251	numBuckets := stats.numBuckets
252	if n := int(stats.max - stats.min + 1); n < numBuckets {
253		numBuckets = n
254	}
255	stats.histogram = NewHistogram(HistogramOptions{
256		NumBuckets: numBuckets,
257		// max-min(lower bound of last bucket) = (1 + growthFactor)^(numBuckets-2) * baseBucketSize.
258		GrowthFactor:   math.Pow(float64(stats.max-stats.min), 1/float64(numBuckets-2)) - 1,
259		BaseBucketSize: 1.0,
260		MinValue:       stats.min})
261
262	for _, d := range stats.durations {
263		stats.histogram.Add(int64(d))
264	}
265
266	stats.dirty = false
267
268	if stats.durations.Len() != 0 {
269		var percentToObserve = []int{50, 90, 99}
270		// First data record min unit from the latency result.
271		stats.result.Latency = append(stats.result.Latency, percentLatency{Percent: -1, Value: stats.unit})
272		for _, position := range percentToObserve {
273			stats.result.Latency = append(stats.result.Latency, percentLatency{Percent: position, Value: stats.durations[max(stats.histogram.Count*int64(position)/100-1, 0)]})
274		}
275		// Last data record the average latency.
276		avg := float64(stats.histogram.Sum) / float64(stats.histogram.Count)
277		stats.result.Latency = append(stats.result.Latency, percentLatency{Percent: -1, Value: time.Duration(avg)})
278	}
279}
280
281// SortLatency blocks the output
282func (stats *Stats) SortLatency() {
283	stats.sortLatency = true
284}
285
286// Print writes textual output of the Stats.
287func (stats *Stats) Print(w io.Writer) {
288	stats.maybeUpdate()
289	if stats.histogram == nil {
290		fmt.Fprint(w, "Histogram (empty)\n")
291	} else {
292		fmt.Fprintf(w, "Histogram (unit: %s)\n", fmt.Sprintf("%v", stats.unit)[1:])
293		stats.histogram.PrintWithUnit(w, float64(stats.unit))
294	}
295}
296
297// String returns the textual output of the Stats as string.
298func (stats *Stats) String() string {
299	var b bytes.Buffer
300	stats.Print(&b)
301	return b.String()
302}
303