├── .travis.yml ├── LICENSE ├── README.md ├── buster_test.go └── buster.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Coda Hale 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # buster 2 | 3 | [![GoDoc](http://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/codahale/buster) 4 | [![Build Status](https://img.shields.io/travis/codahale/buster.svg)](https://travis-ci.org/codahale/buster) 5 | [![Apache V2 License](http://img.shields.io/badge/license-Apache%20V2-blue.svg)](https://github.com/codahale/buster/blob/master/LICENSE) 6 | 7 | Buster's a Go library which provides a re-usable framework for 8 | load-testing things. 9 | 10 | Sometimes you just wanna break stuff. 11 | 12 | ![Buster](http://media.giphy.com/media/ibr51CHOw6QZq/giphy.gif) 13 | -------------------------------------------------------------------------------- /buster_test.go: -------------------------------------------------------------------------------- 1 | package buster_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "testing" 10 | "time" 11 | 12 | "github.com/codahale/buster" 13 | ) 14 | 15 | func Example() { 16 | // run a bench for 1 minute, tracking latencies from 1µs to 1 minute 17 | bench := buster.Bench{ 18 | Duration: 1 * time.Minute, 19 | MinLatency: 1 * time.Microsecond, 20 | MaxLatency: 1 * time.Minute, 21 | } 22 | 23 | r := bench.Run( 24 | 10, // concurrent workers 25 | 1000, // 1000 ops/sec total 26 | func(id int, gen *buster.Generator) error { // the job to be run 27 | client := &http.Client{} 28 | 29 | return gen.Do(func() error { 30 | // perform a GET request 31 | resp, err := client.Get("http://www.google.com/") 32 | if err != nil { 33 | return err 34 | } 35 | 36 | // read the body 37 | io.Copy(ioutil.Discard, resp.Body) 38 | return resp.Body.Close() 39 | }) 40 | }, 41 | ) 42 | 43 | fmt.Println(r) 44 | } 45 | 46 | func TestBenchRun(t *testing.T) { 47 | bench := buster.Bench{ 48 | Duration: 1 * time.Second, 49 | MinLatency: 1 * time.Millisecond, 50 | MaxLatency: 1 * time.Second, 51 | } 52 | 53 | r := bench.Run(10, 1000, func(id int, gen *buster.Generator) error { 54 | return gen.Do(func() error { 55 | return nil 56 | }) 57 | }) 58 | 59 | if v, want := r.Concurrency, 10; v != want { 60 | t.Errorf("Concurrency was %d, but expected %d", v, want) 61 | } 62 | } 63 | 64 | func TestBenchRunFailures(t *testing.T) { 65 | bench := buster.Bench{ 66 | Duration: 1 * time.Second, 67 | MinLatency: 1 * time.Millisecond, 68 | MaxLatency: 1 * time.Second, 69 | } 70 | 71 | r := bench.Run(10, 1000, func(id int, gen *buster.Generator) error { 72 | return gen.Do(func() error { 73 | return errors.New("woo hoo") 74 | }) 75 | }) 76 | 77 | if r.Failure == 0 { 78 | t.Errorf("Failure count was 0, but expected %d", r.Failure) 79 | } 80 | } 81 | 82 | func TestBenchRunErrors(t *testing.T) { 83 | bench := buster.Bench{ 84 | Duration: 1 * time.Second, 85 | MinLatency: 1 * time.Millisecond, 86 | MaxLatency: 1 * time.Second, 87 | } 88 | 89 | r := bench.Run(10, 100, func(id int, gen *buster.Generator) error { 90 | return errors.New("woo hoo") 91 | }) 92 | 93 | if v, want := len(r.Errors), 10; v != want { 94 | t.Fatalf("Error count was %d, but expected %d", v, want) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /buster.go: -------------------------------------------------------------------------------- 1 | // Package buster provides a generic framework for load testing. 2 | // 3 | // Specifically, Buster allows you to run a job at a specific concurrency level 4 | // and a fixed rate while monitoring throughput and latency. 5 | // 6 | // The generic nature of Buster makes it suitable for load testing many 7 | // different systems—HTTP servers, databases, RPC services, etc. 8 | package buster 9 | 10 | import ( 11 | "bytes" 12 | "fmt" 13 | "log" 14 | "sync" 15 | "sync/atomic" 16 | "time" 17 | 18 | "github.com/codahale/hdrhistogram" 19 | ) 20 | 21 | // A Generator is a type passed to Job instances to manage load generation. 22 | type Generator struct { 23 | hist *hdrhistogram.Histogram 24 | success, failure *uint64 25 | warmup, duration, period time.Duration 26 | } 27 | 28 | // Do generates load using the given function. 29 | func (gen *Generator) Do(f func() error) error { 30 | ticker := time.NewTicker(gen.period) 31 | defer ticker.Stop() 32 | 33 | timeout := time.After(gen.duration + gen.warmup) 34 | warmed := time.Now().Add(gen.warmup) 35 | 36 | for { 37 | select { 38 | case start := <-ticker.C: 39 | err := f() 40 | if start.After(warmed) { 41 | if err == nil { 42 | // record success 43 | elapsed := us(time.Now().Sub(start)) 44 | if err := gen.hist.RecordCorrectedValue(elapsed, us(gen.period)); err != nil { 45 | log.Println(err) 46 | } 47 | atomic.AddUint64(gen.success, 1) 48 | } else { 49 | // record failure 50 | atomic.AddUint64(gen.failure, 1) 51 | } 52 | } 53 | case <-timeout: 54 | return nil 55 | } 56 | } 57 | } 58 | 59 | // A Result is returned after a number of concurrent jobs are run. 60 | type Result struct { 61 | Concurrency int 62 | Elapsed time.Duration 63 | Success, Failure uint64 64 | Latency *hdrhistogram.Histogram 65 | Errors []error 66 | } 67 | 68 | func (r Result) String() string { 69 | out := bytes.NewBuffer(nil) 70 | 71 | fmt.Fprintf(out, 72 | "%d successes, %d failures, %d errors, %f ops/sec\n", 73 | r.Success, r.Failure, len(r.Errors), 74 | float64(r.Success)/r.Elapsed.Seconds(), 75 | ) 76 | 77 | for _, b := range r.Latency.CumulativeDistribution() { 78 | fmt.Fprintf(out, "p%f = %fms\n", b.Quantile, float64(b.ValueAt)/10000) 79 | } 80 | 81 | return out.String() 82 | } 83 | 84 | // A Job is an arbitrary task. 85 | type Job func(id int, generator *Generator) error 86 | 87 | // A Bench is place where jobs are done. 88 | type Bench struct { 89 | Warmup, Duration, MinLatency, MaxLatency time.Duration 90 | } 91 | 92 | // Run runs the given job at the given concurrency level, at the given rate, 93 | // returning a set of results with aggregated latency and throughput 94 | // measurements. 95 | func (b Bench) Run(concurrency, rate int, job Job) Result { 96 | return b.Runf(concurrency, float64(rate), job) 97 | } 98 | 99 | // Runf runs the given job at the given concurrency level, at the given rate, 100 | // returning a set of results with aggregated latency and throughput 101 | // measurements. 102 | func (b Bench) Runf(concurrency int, rate float64, job Job) Result { 103 | var started, finished sync.WaitGroup 104 | started.Add(1) 105 | finished.Add(concurrency) 106 | 107 | result := Result{ 108 | Concurrency: concurrency, 109 | Latency: hdrhistogram.New(us(b.MinLatency), us(b.MaxLatency), 5), 110 | } 111 | timings := make(chan *hdrhistogram.Histogram, concurrency) 112 | errors := make(chan error, concurrency) 113 | 114 | workerRate := float64(concurrency) / rate 115 | period := time.Duration((workerRate)*1000000) * time.Microsecond 116 | 117 | for i := 0; i < concurrency; i++ { 118 | go func(id int) { 119 | defer finished.Done() 120 | 121 | gen := &Generator{ 122 | hist: hdrhistogram.New(us(b.MinLatency), us(b.MaxLatency), 5), 123 | success: &result.Success, 124 | failure: &result.Failure, 125 | period: period, 126 | duration: b.Duration, 127 | warmup: b.Warmup, 128 | } 129 | 130 | started.Wait() 131 | errors <- job(id, gen) 132 | timings <- gen.hist 133 | }(i) 134 | } 135 | 136 | started.Done() 137 | finished.Wait() 138 | result.Elapsed = b.Duration 139 | 140 | close(timings) 141 | for v := range timings { 142 | result.Latency.Merge(v) 143 | } 144 | 145 | close(errors) 146 | for e := range errors { 147 | if e != nil { 148 | result.Errors = append(result.Errors, e) 149 | } 150 | } 151 | 152 | return result 153 | } 154 | 155 | func us(d time.Duration) int64 { 156 | return d.Nanoseconds() / 1000 157 | } 158 | --------------------------------------------------------------------------------