pax_global_header00006660000000000000000000000064135320356600014515gustar00rootroot0000000000000052 comment=7d18152acf44268c09cbaace9cc06a453b187e88 tachymeter-2.0.0/000077500000000000000000000000001353203566000136615ustar00rootroot00000000000000tachymeter-2.0.0/LICENSE000066400000000000000000000020701353203566000146650ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 Jamie Alquiza Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tachymeter-2.0.0/README.md000066400000000000000000000173271353203566000151520ustar00rootroot00000000000000[![GoDoc](https://godoc.org/github.com/jamiealquiza/tachymeter?status.svg)](https://godoc.org/github.com/jamiealquiza/tachymeter) # tachymeter Tachymeter captures event timings and returns latency and rate statistics: _"In a loop with 1,000 database calls, what was the 95%ile and lowest observed latency? What was the per-second rate?"_ Tachymeter stores data in a lossless sliding window. This means it's accurate but take o(n) space in relation to the desired sample size. # Examples Code [examples](https://github.com/jamiealquiza/tachymeter/tree/master/example). Tachymeter is also suitable for general purpose use, such as [load testing tools](https://github.com/jamiealquiza/sangrenel). # Usage After initializing a `tachymeter`, event durations in the form of [`time.Duration`](https://golang.org/pkg/time/#Duration) are added using the `AddTime(t time.Duration)` call. Once all desired timing have been collected, the data is summarized by calling the `Calc()`, returning a [`*Metrics`](https://godoc.org/github.com/jamiealquiza/tachymeter#Metrics)). `*Metrics` fields can be accessed directly or via other [output methods](https://github.com/jamiealquiza/tachymeter#output-methods). ```golang import "github.com/jamiealquiza/tachymeter" func main() { // Initialize a tachymeter with a sample window // size of 50 events. t := tachymeter.New(&tachymeter.Config{Size: 50}) for i := 0; i < 100; i++ { start := time.Now() doSomeWork() // We add the time that // each doSomeWork() call took. t.AddTime(time.Since(start)) } // The timing summaries are calculated // and printed to console. fmt.Println(t.Calc()) } ``` ``` 50 samples of 100 events Cumulative: 671.871ms HMean: 125.38µs Avg.: 13.43742ms p50: 13.165ms p75: 20.058ms p95: 27.536ms p99: 30.043ms p999: 30.043ms Long 5%: 29.749ms Short 5%: 399.666µs Max: 30.043ms Min: 4µs Range: 30.039ms StdDev: 8.385117ms Rate/sec.: 74.42 ``` ### Output Descriptions - `Cumulative`: Aggregate of all sample durations. - `HMean`: Event duration harmonic mean. - `Avg.`: Average event duration per sample. - `p`: Nth %ile. - `Long 5%`: Average event duration of the longest 5%. - `Short 5%`: Average event duration of the shortest 5%. - `Max`: Max observed event duration. - `Min`: Min observed event duration. - `Range`: The delta between the max and min sample time - `StdDev`: The population standard deviation - `Rate/sec.`: Per-second rate based on cumulative time and sample count. # Output Methods Tachymeter output is stored in two primary forms: - A [`*Metrics`](https://godoc.org/github.com/jamiealquiza/tachymeter#Metrics), which holds the calculated percentiles, rates and other information detailed in the [Output Descriptions](https://github.com/jamiealquiza/tachymeter#output-descriptions) section - A [`*Histogram`](https://godoc.org/github.com/jamiealquiza/tachymeter#Histogram) of all measured event durations, nested in the `Metrics.Histogram` field `t` represents a tachymeter instance. Calling `t.Calc()` returns a `*Metrics`. `Metrics` and the nested `Histogram` types can be access in several ways: ### `Metrics`: raw struct ```golang metrics := t.Calc() fmt.Printf("Median latency: %s\n", metrics.Time.P50) ``` Output: ``` Median latency: 13.165ms ``` ### `Metrics`: JSON string ```golang fmt.Printf("%s\n\", metrics.JSON()) ``` Output: ``` {"Time":{"Cumulative":"671.871ms","HMean":"125.38µs","Avg":"13.43742ms","P50":"13.165ms","P75":"20.058ms","P95":"27.536ms","P99":"30.043ms","P999":"30.043ms","Long5p":"29.749ms","Short5p":"399.666µs","Max":"30.043ms","Min":"4µs","Range":"30.039ms","StdDev":"8.385117ms"},"Rate":{"Second":74.41904770409796},"Samples":50,"Count":100,"Histogram":[{"4µs - 3.007ms":5},{"3.007ms - 6.011ms":4},{"6.011ms - 9.015ms":10},{"9.015ms - 12.019ms":6},{"12.019ms - 15.023ms":7},{"15.023ms - 18.027ms":3},{"18.027ms - 21.031ms":4},{"21.031ms - 24.035ms":3},{"24.035ms - 27.039ms":3},{"27.039ms - 30.043ms":5}]} ``` ### `Metrics`: pre-formatted, multi-line string ```golang fmt.Println(metrics) ``` Output: ``` 50 samples of 100 events Cumulative: 671.871ms HMean: 125.38µs Avg.: 13.43742ms p50: 13.165ms p75: 20.058ms p95: 27.536ms p99: 30.043ms p999: 30.043ms Long 5%: 29.749ms Short 5%: 399.666µs Max: 30.043ms Min: 4µs Range: 30.039ms StdDev: 8.385117ms Rate/sec.: 74.42 ``` ### `Histogram`: text The `Histogram.String(int)` method generates a text version of the histogram. Histogram bar scaling is specified with width `int`. ```golang fmt.Println(metrics.Histogram.String(25)) ``` Output: ``` 4µs - 3.007ms ----- 3.007ms - 6.011ms --- 6.011ms - 9.015ms --------------- 9.015ms - 12.019ms ------- 12.019ms - 15.023ms --------- 15.023ms - 18.027ms - 18.027ms - 21.031ms --- 21.031ms - 24.035ms - 24.035ms - 27.039ms - 27.039ms - 30.043ms ----- ``` ### `Histogram`: HTML A `Histogram` can be written as HTML histograms. The `Metrics.WriteHTML(p string)` method is called where `p` is an output path where the HTML file should be written. ```golang err := metrics.WriteHTML(".") ``` Output: ![ss](https://user-images.githubusercontent.com/4108044/37558972-a40374f2-29e2-11e8-9df2-60b2927a8fa4.png) Tachymeter also provides a `Timeline` type that's used to gather a series of `*Metrics` (each `*Metrics` object holding data summarizing a series of measured events). `*Metrics` are added to a `*Timeline` using the `AddEvent(m *Metrics)` method. Once the desired number of `*Metrics` has been collected, `WriteHTML` can be called on the `*Timeline`, resulting in an single HTML page with a histogram for each captured `*Metrics`. An example use case may be a benchmark where tachymeter is used to summarize the timing results of a loop, but several iterations of the loop are to be called in series. See the [tachymeter-graphing example](https://github.com/jamiealquiza/tachymeter/tree/master/example/tachymeter-graphing) for further details. ### Configuration Tachymeter is initialized with a `Size` parameter that specifies the maximum sample count that can be held. This is done to set bounds on tachymeter memory usage (since it's a lossless storage structure). The `AddTime` method is o(1) and typically sub-microsecond modern hardware. If the actual event count is smaller than or equal to the configured tachymeter size, all of the measured events will be included in the calculated results. If the event count exceeds the tachymeter size, the oldest data will be overwritten. In this scenario, the last window of `Size` events will be used for output calculations. # Accurate Rates With Parallelism By default, tachymeter calculates rate based on the number of events possible per-second according to average event duration. This model doesn't work in asynchronous or parallelized scenarios since events may be overlapping in time. For example, with many Goroutines writing durations to a shared tachymeter in parallel, the global rate must be determined by using the total event count over the total wall time elapsed. Tachymeter exposes a `SetWallTime` method for these scenarios. Example: ```golang <...> func main() { // Initialize tachymeter. c := tachymeter.New(&tachymeter.Config{Size: 50}) // Start wall time for all Goroutines. wallTimeStart := time.Now() var wg sync.WaitGroup // Run tasks. for i := 0; i < 5; i++ { wg.Add(1) go someTask(t, wg) } wg.Wait() // When finished, set elapsed wall time. t.SetWallTime(time.Since(wallTimeStart)) // Rate outputs will be accurate. fmt.Println(t.Calc().String()) } func someTask(t *tachymeter.Tachymeter, wg *sync.WaitGroup) { defer wg.Done() start := time.Now() // doSomeSlowDbCall() // Task we're timing added here. t.AddTime(time.Since(start)) } <...> ``` tachymeter-2.0.0/calcs.go000066400000000000000000000077161353203566000153100ustar00rootroot00000000000000package tachymeter import ( "fmt" "math" "sort" "sync/atomic" "time" ) // Calc summarizes Tachymeter sample data // and returns it in the form of a *Metrics. func (m *Tachymeter) Calc() *Metrics { metrics := &Metrics{} if atomic.LoadUint64(&m.Count) == 0 { return metrics } m.Lock() metrics.Samples = int(math.Min(float64(atomic.LoadUint64(&m.Count)), float64(m.Size))) metrics.Count = int(atomic.LoadUint64(&m.Count)) times := make(timeSlice, metrics.Samples) copy(times, m.Times[:metrics.Samples]) sort.Sort(times) metrics.Time.Cumulative = times.cumulative() var rateTime float64 if m.WallTime != 0 { rateTime = float64(metrics.Count) / float64(m.WallTime) } else { rateTime = float64(metrics.Samples) / float64(metrics.Time.Cumulative) } metrics.Rate.Second = rateTime * 1e9 m.Unlock() metrics.Time.Avg = times.avg() metrics.Time.HMean = times.hMean() metrics.Time.P50 = times[times.Len()/2] metrics.Time.P75 = times.p(0.75) metrics.Time.P95 = times.p(0.95) metrics.Time.P99 = times.p(0.99) metrics.Time.P999 = times.p(0.999) metrics.Time.Long5p = times.long5p() metrics.Time.Short5p = times.short5p() metrics.Time.Min = times.min() metrics.Time.Max = times.max() metrics.Time.Range = times.srange() metrics.Time.StdDev = times.stdDev() metrics.Histogram, metrics.HistogramBinSize = times.hgram(m.HBins) return metrics } // hgram returns a histogram of event durations in // b bins, along with the bin size. func (ts timeSlice) hgram(b int) (*Histogram, time.Duration) { res := time.Duration(1000) // Interval is the time range / n bins. interval := time.Duration(int64(ts.srange()) / int64(b)) high := ts.min() + interval low := ts.min() max := ts.max() hgram := &Histogram{} pos := 1 // Bin position. bstring := fmt.Sprintf("%s - %s", low/res*res, high/res*res) bin := map[string]uint64{} for _, v := range ts { // If v fits in the current bin, // increment the bin count. if v <= high { bin[bstring]++ } else { // If not, prepare the next bin. *hgram = append(*hgram, bin) bin = map[string]uint64{} // Update the high/low range values. low = high + time.Nanosecond high += interval // if we're going into the // last bin, set high to max. if pos == b-1 { high = max } bstring = fmt.Sprintf("%s - %s", low/res*res, high/res*res) // The value didn't fit in the previous // bin, so the new bin count should // be incremented. bin[bstring]++ pos++ } } *hgram = append(*hgram, bin) return hgram, interval } // These should be self-explanatory: func (ts timeSlice) cumulative() time.Duration { var total time.Duration for _, t := range ts { total += t } return total } func (ts timeSlice) hMean() time.Duration { var total float64 for _, t := range ts { total += (1 / float64(t)) } return time.Duration(float64(ts.Len()) / total) } func (ts timeSlice) avg() time.Duration { var total time.Duration for _, t := range ts { total += t } return time.Duration(int(total) / ts.Len()) } func (ts timeSlice) p(p float64) time.Duration { return ts[int(float64(ts.Len())*p+0.5)-1] } func (ts timeSlice) stdDev() time.Duration { m := ts.avg() s := 0.00 for _, t := range ts { s += math.Pow(float64(m-t), 2) } msq := s / float64(ts.Len()) return time.Duration(math.Sqrt(msq)) } func (ts timeSlice) long5p() time.Duration { set := ts[int(float64(ts.Len())*0.95+0.5):] if len(set) <= 1 { return ts[ts.Len()-1] } var t time.Duration var i int for _, n := range set { t += n i++ } return time.Duration(int(t) / i) } func (ts timeSlice) short5p() time.Duration { set := ts[:int(float64(ts.Len())*0.05+0.5)] if len(set) <= 1 { return ts[0] } var t time.Duration var i int for _, n := range set { t += n i++ } return time.Duration(int(t) / i) } func (ts timeSlice) min() time.Duration { return ts[0] } func (ts timeSlice) max() time.Duration { return ts[ts.Len()-1] } func (ts timeSlice) srange() time.Duration { return ts.max() - ts.min() } tachymeter-2.0.0/calcs_test.go000066400000000000000000000067401353203566000163430ustar00rootroot00000000000000package tachymeter_test import ( //"fmt" "sort" "testing" "time" "github.com/jamiealquiza/tachymeter" ) func TestCalc(t *testing.T) { ta := tachymeter.New(&tachymeter.Config{Size: 30}) // These end up overwritten; we're // putting 32 events in a size 30 Tachymeter. ta.AddTime(12 * time.Millisecond) ta.AddTime(96 * time.Millisecond) ta.AddTime(9 * time.Millisecond) ta.AddTime(4 * time.Millisecond) ta.AddTime(88 * time.Millisecond) ta.AddTime(37 * time.Millisecond) ta.AddTime(42 * time.Millisecond) ta.AddTime(77 * time.Millisecond) ta.AddTime(93 * time.Millisecond) ta.AddTime(89 * time.Millisecond) ta.AddTime(12 * time.Millisecond) ta.AddTime(36 * time.Millisecond) ta.AddTime(54 * time.Millisecond) ta.AddTime(21 * time.Millisecond) ta.AddTime(17 * time.Millisecond) ta.AddTime(14 * time.Millisecond) ta.AddTime(67 * time.Millisecond) ta.AddTime(9 * time.Millisecond) ta.AddTime(4 * time.Millisecond) ta.AddTime(88 * time.Millisecond) ta.AddTime(37 * time.Millisecond) ta.AddTime(42 * time.Millisecond) ta.AddTime(77 * time.Millisecond) ta.AddTime(93 * time.Millisecond) ta.AddTime(89 * time.Millisecond) ta.AddTime(12 * time.Millisecond) ta.AddTime(36 * time.Millisecond) ta.AddTime(54 * time.Millisecond) ta.AddTime(21 * time.Millisecond) ta.AddTime(17 * time.Millisecond) ta.AddTime(14 * time.Millisecond) ta.AddTime(67 * time.Millisecond) metrics := ta.Calc() if metrics.Samples != 30 { t.Error("Expected 30, got ", metrics.Samples) } if metrics.Count != 32 { t.Error("Expected 32, got ", metrics.Count) } expectedDurs := []time.Duration{ 4000000, 4000000, 9000000, 9000000, 12000000, 12000000, 14000000, 14000000, 17000000, 17000000, 21000000, 21000000, 36000000, 36000000, 37000000, 37000000, 42000000, 42000000, 54000000, 54000000, 67000000, 67000000, 77000000, 77000000, 88000000, 88000000, 89000000, 89000000, 93000000, 93000000, } sort.Sort(ta.Times) for n, d := range ta.Times { if d != expectedDurs[n] { t.Errorf("Expected %d, got %d\n", expectedDurs[n], d) } } if metrics.Time.Cumulative != 1320000000 { t.Errorf("Expected 1320000000, got %d\n", metrics.Time.Cumulative) } if metrics.Time.Avg != 44000000 { t.Errorf("Expected 44000000, got %d\n", metrics.Time.Avg) } if metrics.Time.P50 != 37000000 { t.Errorf("Expected 37000000, got %d\n", metrics.Time.P50) } if metrics.Time.P75 != 77000000 { t.Errorf("Expected 77000000, got %d\n", metrics.Time.P75) } if metrics.Time.P95 != 93000000 { t.Errorf("Expected 93000000, got %d\n", metrics.Time.P95) } if metrics.Time.P99 != 93000000 { t.Errorf("Expected 93000000, got %d\n", metrics.Time.P99) } if metrics.Time.P999 != 93000000 { t.Errorf("Expected 93000000, got %d\n", metrics.Time.P999) } if metrics.Time.Long5p != 93000000 { t.Errorf("Expected 93000000, got %d\n", metrics.Time.Long5p) } if metrics.Time.Short5p != 4000000 { t.Errorf("Expected 4000000, got %d\n", metrics.Time.Short5p) } if metrics.Time.Max != 93000000 { t.Errorf("Expected 93000000, got %d\n", metrics.Time.Max) } if metrics.Time.Min != 4000000 { t.Errorf("Expected 4000000, got %d\n", metrics.Time.Min) } if metrics.Time.Range != 89000000 { t.Errorf("Expected 89000000, got %d\n", metrics.Time.Range) } if metrics.Time.StdDev != 30772281 { t.Errorf("Expected abc, got %d\n", metrics.Time.StdDev) } if metrics.Rate.Second != 22.72727272727273 { t.Errorf("Expected 22.73, got %0.2f\n", metrics.Rate.Second) } } tachymeter-2.0.0/example/000077500000000000000000000000001353203566000153145ustar00rootroot00000000000000tachymeter-2.0.0/example/tachymeter-graphing/000077500000000000000000000000001353203566000212565ustar00rootroot00000000000000tachymeter-2.0.0/example/tachymeter-graphing/README.md000066400000000000000000000026531353203566000225430ustar00rootroot00000000000000This example uses the tachymeter [Timeline](https://godoc.org/github.com/jamiealquiza/tachymeter#Timeline) type for gathering summary metrics from several iterations of a measured loop, outputting a single HTML page with histograms and summaries per iteration. ### Install - `$ go get github.com/jamiealquiza/tachymeter` - `$ go install github.com/jamiealquiza/tachymeter/example/tachymeter-graphing` ### Example code ```go package main import ( "fmt" "math/rand" "time" "github.com/jamiealquiza/tachymeter" ) func main() { c := tachymeter.New(&tachymeter.Config{Size: 50}) tl := tachymeter.Timeline{} // Run 3 iterations of a loop that we're // interesting in summarizing with tachymeter. for iter := 0; iter <3; iter++ { fmt.Printf("Running iteration %d\n", iter) // Capture timing data from the loop. for i := 0; i < 100; i++ { start := time.Now() time.Sleep(time.Duration(rand.Intn(30)) * time.Millisecond) c.AddTime(time.Since(start)) } // Add each loop tachymeter // to the event timeline. tl.AddEvent(c.Calc()) c.Reset() } // Write out an HTML page with the // histogram for all iterations. err := tl.WriteHTML(".") if err != nil { fmt.Println(err) } fmt.Println("Results written") } ``` ### Output ``` $ $GOPATH/bin/tachymeter-graphing Running iteration 0 Running iteration 1 Running iteration 2 Results written ``` ## [HTML Example Output](https://jamiealquiza.github.io/tachymeter/) tachymeter-2.0.0/example/tachymeter-graphing/main.go000066400000000000000000000014621353203566000225340ustar00rootroot00000000000000package main import ( "fmt" "math/rand" "time" "github.com/jamiealquiza/tachymeter" ) func main() { c := tachymeter.New(&tachymeter.Config{Size: 50}) tl := tachymeter.Timeline{} // Run 3 iterations of a loop that we're // interesting in summarizing with tachymeter. for iter := 0; iter < 3; iter++ { fmt.Printf("Running iteration %d\n", iter) // Capture timing data from the loop. for i := 0; i < 100; i++ { start := time.Now() time.Sleep(time.Duration(rand.Intn(30)) * time.Millisecond) c.AddTime(time.Since(start)) } // Add each loop tachymeter // to the event timeline. tl.AddEvent(c.Calc()) c.Reset() } // Write out an HTML page with the // histogram for all iterations. err := tl.WriteHTML(".") if err != nil { fmt.Println(err) } fmt.Println("Results written") } tachymeter-2.0.0/example/tachymeter-simple/000077500000000000000000000000001353203566000207505ustar00rootroot00000000000000tachymeter-2.0.0/example/tachymeter-simple/README.md000066400000000000000000000040151353203566000222270ustar00rootroot00000000000000This example measures the durations of each iteration of a single loop and prints the summarized output. ### Install - `$ go get github.com/jamiealquiza/tachymeter` - `$ go install github.com/jamiealquiza/tachymeter/example/tachymeter-simple` ### Example code ```go package main import ( "fmt" "math/rand" "time" "github.com/jamiealquiza/tachymeter" ) func main() { c := tachymeter.New(&tachymeter.Config{Size: 50}) // Measure events. for i := 0; i < 100; i++ { start := time.Now() time.Sleep(time.Duration(rand.Intn(30)) * time.Millisecond) c.AddTime(time.Since(start)) } // Calc output. results := c.Calc() // Print JSON format to console. fmt.Printf("%s\n\n", results.JSON()) // Print pre-formatted console output. fmt.Printf("%s\n\n", results) // Print text histogram. fmt.Println(results.Histogram.String(15)) } ``` ### Output ``` $ $GOPATH/bin/tachymeter-simple {"Time":{"Cumulative":"671.871ms","HMean":"125.38µs","Avg":"13.43742ms","P50":"13.165ms","P75":"20.058ms","P95":"27.536ms","P99":"30.043ms","P999":"30.043ms","Long5p":"29.749ms","Short5p":"399.666µs","Max":"30.043ms","Min":"4µs","Range":"30.039ms","StdDev":"8.385117ms"},"Rate":{"Second":74.41904770409796},"Samples":50,"Count":100,"Histogram":[{"4µs - 3.007ms":5},{"3.007ms - 6.011ms":4},{"6.011ms - 9.015ms":10},{"9.015ms - 12.019ms":6},{"12.019ms - 15.023ms":7},{"15.023ms - 18.027ms":3},{"18.027ms - 21.031ms":4},{"21.031ms - 24.035ms":3},{"24.035ms - 27.039ms":3},{"27.039ms - 30.043ms":5}]} 50 samples of 100 events Cumulative: 671.871ms HMean: 125.38µs Avg.: 13.43742ms p50: 13.165ms p75: 20.058ms p95: 27.536ms p99: 30.043ms p999: 30.043ms Long 5%: 29.749ms Short 5%: 399.666µs Max: 30.043ms Min: 4µs Range: 30.039ms StdDev: 8.385117ms Rate/sec.: 74.42 4µs - 3.007ms ----- 3.007ms - 6.011ms --- 6.011ms - 9.015ms --------------- 9.015ms - 12.019ms ------- 12.019ms - 15.023ms --------- 15.023ms - 18.027ms - 18.027ms - 21.031ms --- 21.031ms - 24.035ms - 24.035ms - 27.039ms - 27.039ms - 30.043ms ----- ``` tachymeter-2.0.0/example/tachymeter-simple/main.go000066400000000000000000000011051353203566000222200ustar00rootroot00000000000000package main import ( "fmt" "math/rand" "time" "github.com/jamiealquiza/tachymeter" ) func main() { c := tachymeter.New(&tachymeter.Config{Size: 50}) // Measure events. for i := 0; i < 100; i++ { start := time.Now() time.Sleep(time.Duration(rand.Intn(30)) * time.Millisecond) c.AddTime(time.Since(start)) } // Calc output. results := c.Calc() // Print JSON format to console. fmt.Printf("%s\n\n", results.JSON()) // Print pre-formatted console output. fmt.Printf("%s\n\n", results) // Print text histogram. fmt.Println(results.Histogram.String(25)) } tachymeter-2.0.0/htmltemplate.go000066400000000000000000004545151353203566000167260ustar00rootroot00000000000000package tachymeter const ( tab string = ` ` nl string = "\n" ) var ( head = ` tachymeter ` container = `
` graph = ` ` tail = "\n" template = head + container + graph + tail ) tachymeter-2.0.0/tachymeter.go000066400000000000000000000156761353203566000163740ustar00rootroot00000000000000// Package tachymeter yields summarized data // describing a series of timed events. package tachymeter import ( "bytes" "encoding/json" "fmt" "math" "strings" "sync" "sync/atomic" "time" ) // Config holds tachymeter initialization // parameters. Size defines the sample capacity. // Tachymeter is thread safe. type Config struct { Size int Safe bool // Deprecated. Flag held on to as to not break existing users. HBins int // Histogram bins. } // Tachymeter holds event durations // and counts. type Tachymeter struct { sync.Mutex Size uint64 Times timeSlice Count uint64 WallTime time.Duration HBins int } // timeslice holds time.Duration values. type timeSlice []time.Duration // Satisfy sort for timeSlice. func (p timeSlice) Len() int { return len(p) } func (p timeSlice) Less(i, j int) bool { return int64(p[i]) < int64(p[j]) } func (p timeSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } // Histogram is a map["low-high duration"]count of events that // fall within the low-high time duration range. type Histogram []map[string]uint64 // Metrics holds the calculated outputs // produced from a Tachymeter sample set. type Metrics struct { Time struct { // All values under Time are selected entirely from events within the sample window. Cumulative time.Duration // Cumulative time of all sampled events. HMean time.Duration // Event duration harmonic mean. Avg time.Duration // Event duration average. P50 time.Duration // Event duration nth percentiles .. P75 time.Duration P95 time.Duration P99 time.Duration P999 time.Duration Long5p time.Duration // Average of the longest 5% event durations. Short5p time.Duration // Average of the shortest 5% event durations. Max time.Duration // Highest event duration. Min time.Duration // Lowest event duration. StdDev time.Duration // Standard deviation. Range time.Duration // Event duration range (Max-Min). } Rate struct { // Per-second rate based on event duration avg. via Metrics.Cumulative / Metrics.Samples. // If SetWallTime was called, event duration avg = wall time / Metrics.Count Second float64 } Histogram *Histogram // Frequency distribution of event durations in len(Histogram) bins of HistogramBinSize. HistogramBinSize time.Duration // The width of a histogram bin in time. Samples int // Number of events included in the sample set. Count int // Total number of events observed. } // New initializes a new Tachymeter. func New(c *Config) *Tachymeter { var hSize int if c.HBins != 0 { hSize = c.HBins } else { hSize = 10 } return &Tachymeter{ Size: uint64(c.Size), Times: make([]time.Duration, c.Size), HBins: hSize, } } // Reset resets a Tachymeter // instance for reuse. func (m *Tachymeter) Reset() { // This lock is obviously not needed for // the m.Count update, rather to prevent a // Tachymeter reset while Calc is being called. m.Lock() atomic.StoreUint64(&m.Count, 0) m.Unlock() } // AddTime adds a time.Duration to Tachymeter. func (m *Tachymeter) AddTime(t time.Duration) { m.Times[(atomic.AddUint64(&m.Count, 1)-1)%m.Size] = t } // SetWallTime optionally sets an elapsed wall time duration. // This affects rate output by using total events counted over time. // This is useful for concurrent/parallelized events that overlap // in wall time and are writing to a shared Tachymeter instance. func (m *Tachymeter) SetWallTime(t time.Duration) { m.WallTime = t } // WriteHTML writes a histograph // html file to the cwd. func (m *Metrics) WriteHTML(p string) error { w := Timeline{} w.AddEvent(m) return w.WriteHTML(p) } // String satisfies the String interface. func (m *Metrics) String() string { return fmt.Sprintf(`%d samples of %d events Cumulative: %s HMean: %s Avg.: %s p50: %s p75: %s p95: %s p99: %s p999: %s Long 5%%: %s Short 5%%: %s Max: %s Min: %s Range: %s StdDev: %s Rate/sec.: %.2f`, m.Samples, m.Count, m.Time.Cumulative, m.Time.HMean, m.Time.Avg, m.Time.P50, m.Time.P75, m.Time.P95, m.Time.P99, m.Time.P999, m.Time.Long5p, m.Time.Short5p, m.Time.Max, m.Time.Min, m.Time.Range, m.Time.StdDev, m.Rate.Second) } // JSON returns a *Metrics as // a JSON string. func (m *Metrics) JSON() string { j, _ := json.Marshal(m) return string(j) } // MarshalJSON defines the output formatting // for the JSON() method. This is exported as a // requirement but not intended for end users. func (m *Metrics) MarshalJSON() ([]byte, error) { return json.Marshal(&struct { Time struct { Cumulative string HMean string Avg string P50 string P75 string P95 string P99 string P999 string Long5p string Short5p string Max string Min string Range string StdDev string } Rate struct { Second float64 } Samples int Count int Histogram *Histogram }{ Time: struct { Cumulative string HMean string Avg string P50 string P75 string P95 string P99 string P999 string Long5p string Short5p string Max string Min string Range string StdDev string }{ Cumulative: m.Time.Cumulative.String(), HMean: m.Time.HMean.String(), Avg: m.Time.Avg.String(), P50: m.Time.P50.String(), P75: m.Time.P75.String(), P95: m.Time.P95.String(), P99: m.Time.P99.String(), P999: m.Time.P999.String(), Long5p: m.Time.Long5p.String(), Short5p: m.Time.Short5p.String(), Max: m.Time.Max.String(), Min: m.Time.Min.String(), Range: m.Time.Range.String(), StdDev: m.Time.StdDev.String(), }, Rate: struct{ Second float64 }{ Second: m.Rate.Second, }, Histogram: m.Histogram, Samples: m.Samples, Count: m.Count, }) } // String returns a formatted Metrics string scaled // to a width of s. func (h *Histogram) String(s int) string { if h == nil { return "" } var min, max uint64 = math.MaxUint64, 0 // Get the histogram min/max counts. for _, bin := range *h { for _, v := range bin { if v > max { max = v } if v < min { min = v } } } // Handle cases of no or // a single bin. switch len(*h) { case 0: return "" case 1: min = 0 } var b bytes.Buffer // Build histogram string. for _, bin := range *h { for k, v := range bin { // Get the bar length. blen := scale(float64(v), float64(min), float64(max), 1, float64(s)) line := fmt.Sprintf("%22s %s\n", k, strings.Repeat("-", int(blen))) b.WriteString(line) } } return b.String() } // Scale scales the input x with the input-min a0, // input-max a1, output-min b0, and output-max b1. func scale(x, a0, a1, b0, b1 float64) float64 { a, b := x-a0, a1-a0 var c float64 if a == 0 { c = 0 } else { c = a / b } return c*(b1-b0) + b0 } tachymeter-2.0.0/tachymeter_test.go000066400000000000000000000020361353203566000174150ustar00rootroot00000000000000package tachymeter_test import ( "testing" "time" "github.com/jamiealquiza/tachymeter" ) func BenchmarkAddTime(b *testing.B) { b.StopTimer() ta := tachymeter.New(&tachymeter.Config{Size: b.N}) d := time.Millisecond b.StartTimer() for i := 0; i < b.N; i++ { ta.AddTime(d) } } func BenchmarkAddTimeSampling(b *testing.B) { b.StopTimer() ta := tachymeter.New(&tachymeter.Config{Size: 100}) d := time.Millisecond b.StartTimer() for i := 0; i < b.N; i++ { ta.AddTime(d) } } func TestReset(t *testing.T) { ta := tachymeter.New(&tachymeter.Config{Size: 3}) ta.AddTime(time.Second) ta.AddTime(time.Second) ta.AddTime(time.Second) ta.Reset() if ta.Count != 0 { t.Fail() } } func TestAddTime(t *testing.T) { ta := tachymeter.New(&tachymeter.Config{Size: 3}) ta.AddTime(time.Millisecond) if ta.Times[0] != time.Millisecond { t.Fail() } } func TestSetWallTime(t *testing.T) { ta := tachymeter.New(&tachymeter.Config{Size: 3}) ta.SetWallTime(time.Millisecond) if ta.WallTime != time.Millisecond { t.Fail() } } tachymeter-2.0.0/timeline.go000066400000000000000000000046251353203566000160250ustar00rootroot00000000000000package tachymeter import ( "bytes" "encoding/json" "fmt" "io/ioutil" "path/filepath" "strconv" "strings" "time" ) // Timeline holds a []*timelineEvents, // which nest *Metrics for analyzing // multiple collections of measured events. type Timeline struct { timeline []*timelineEvent } // timelineEvent holds a *Metrics and // time that it was added to the Timeline. type timelineEvent struct { Metrics *Metrics Created time.Time } // AddEvent adds a *Metrics to the *Timeline. func (t *Timeline) AddEvent(m *Metrics) { t.timeline = append(t.timeline, &timelineEvent{ Metrics: m, Created: time.Now(), }) } // WriteHTML takes an absolute path p and writes an // html file to 'p/tachymeter-.html' of all // histograms held by the *Timeline, in series. func (t *Timeline) WriteHTML(p string) error { path, err := filepath.Abs(p) if err != nil { return err } var b bytes.Buffer b.WriteString(head) // Append graph + info entry for each timeline // event. for n := range t.timeline { // Graph div. b.WriteString(fmt.Sprintf(`%s
%s`, tab, nl)) b.WriteString(fmt.Sprintf(`%s%s%s`, tab, tab, n, nl)) b.WriteString(fmt.Sprintf(`%s
%s`, tab, nl)) // Info div. b.WriteString(fmt.Sprintf(`%s
%s`, tab, nl)) b.WriteString(fmt.Sprintf(`%s

Iteration %d

%s`, tab, n+1, nl)) b.WriteString(t.timeline[n].Metrics.String()) b.WriteString(fmt.Sprintf("%s%s

%s", nl, tab, nl)) } // Write graphs. for id, m := range t.timeline { s := genGraphHTML(m, id) b.WriteString(s) } b.WriteString(tail) // Write file. d := []byte(b.String()) fname := fmt.Sprintf("%s/tachymeter-%d.html", path, time.Now().Unix()) err = ioutil.WriteFile(fname, d, 0644) if err != nil { return err } return nil } // genGraphHTML takes a *timelineEvent and id (used for each graph // html element ID) and creates a chart.js graph output. func genGraphHTML(te *timelineEvent, id int) string { keys := []string{} values := []uint64{} for _, b := range *te.Metrics.Histogram { for k, v := range b { keys = append(keys, k) values = append(values, v) } } keysj, _ := json.Marshal(keys) valuesj, _ := json.Marshal(values) out := strings.Replace(graph, "XCANVASID", strconv.Itoa(id), 1) out = strings.Replace(out, "XKEYS", string(keysj), 1) out = strings.Replace(out, "XVALUES", string(valuesj), 1) return out }