package main import ( "bytes" "fmt" "math" "sort" "text/tabwriter" "time" ) type byDuration []time.Duration func (data byDuration) Len() int { return len(data) } func (data byDuration) Swap(i, j int) { data[i], data[j] = data[j], data[i] } func (data byDuration) Less(i, j int) bool { return data[i] < data[j] } // quantile returns a value representing the kth of q quantiles. // May alter the order of data. func quantile(data []time.Duration, k, q int) (quantile time.Duration, ok bool) { if len(data) < 1 { return 0, false } if k > q { return 0, false } if k < 0 || q < 1 { return 0, false } sort.Sort(byDuration(data)) if k == 0 { return data[0], true } if k == q { return data[len(data)-1], true } bucketSize := float64(len(data)-1) / float64(q) i := float64(k) * bucketSize lower := int(math.Trunc(i)) var upper int if i > float64(lower) && lower+1 < len(data) { // If the quantile lies between two elements upper = lower + 1 } else { upper = lower } weightUpper := i - float64(lower) weightLower := 1 - weightUpper return time.Duration(weightLower*float64(data[lower]) + weightUpper*float64(data[upper])), true } type aggregate struct { min, median, max time.Duration p95, p99 time.Duration // percentiles } // newAggregate constructs an aggregate from latencies. Returns nil if latencies does not contain aggregateable data. func newAggregate(latencies []time.Duration) *aggregate { var agg aggregate if len(latencies) == 0 { return nil } var ok bool if agg.min, ok = quantile(latencies, 0, 2); !ok { return nil } if agg.median, ok = quantile(latencies, 1, 2); !ok { return nil } if agg.max, ok = quantile(latencies, 2, 2); !ok { return nil } if agg.p95, ok = quantile(latencies, 95, 100); !ok { return nil } if agg.p99, ok = quantile(latencies, 99, 100); !ok { return nil } return &agg } func (agg *aggregate) String() string { if agg == nil { return "no data" } var buf bytes.Buffer tw := tabwriter.NewWriter(&buf, 0, 0, 1, ' ', 0) // one-space padding fmt.Fprintf(tw, "min:\t%v\nmedian:\t%v\nmax:\t%v\n95th percentile:\t%v\n99th percentile:\t%v\n", agg.min, agg.median, agg.max, agg.p95, agg.p99) tw.Flush() return buf.String() }