diff --git a/cmd/tile38-benchmark/main.go b/cmd/tile38-benchmark/main.go index 97533edb..4ec53e85 100644 --- a/cmd/tile38-benchmark/main.go +++ b/cmd/tile38-benchmark/main.go @@ -1,12 +1,7 @@ package main import ( - "bufio" - "errors" "fmt" - "io" - "io/ioutil" - "math" "math/rand" "net" "os" @@ -15,6 +10,7 @@ import ( "sync/atomic" "time" + "github.com/tidwall/redbench" "github.com/tidwall/tile38/core" ) @@ -134,19 +130,22 @@ func parseArgs() bool { return true } -func fillOpts() *Options { - return &Options{ - JSON: json, - CSV: csv, - Clients: clients, - Pipeline: pipeline, - Quiet: quiet, - Requests: requests, - } +func fillOpts() *redbench.Options { + opts := *redbench.DefaultOptions + opts.CSV = csv + opts.Clients = clients + opts.Pipeline = pipeline + opts.Quiet = quiet + opts.Requests = requests + opts.Stderr = os.Stderr + opts.Stdout = os.Stdout + return &opts } + func randPoint() (lat, lon float64) { return rand.Float64()*180 - 90, rand.Float64()*360 - 180 } + func randRect() (minlat, minlon, maxlat, maxlon float64) { for { minlat, minlon = randPoint() @@ -156,6 +155,14 @@ func randRect() (minlat, minlon, maxlat, maxlon float64) { } } } +func prepFn(conn net.Conn) bool { + if json { + conn.Write([]byte("output json\r\n")) + resp := make([]byte, 100) + conn.Read(resp) + } + return true +} func main() { rand.Seed(time.Now().UnixNano()) if !parseArgs() { @@ -165,35 +172,35 @@ func main() { for _, test := range strings.Split(tests, ",") { switch strings.ToUpper(strings.TrimSpace(test)) { case "PING": - RedisBench("PING", addr, fillOpts(), - func(buf []byte, _ ServerType) []byte { - return AppendCommand(buf, "PING") + redbench.Bench("PING", addr, fillOpts(), prepFn, + func(buf []byte) []byte { + return redbench.AppendCommand(buf, "PING") }, ) case "SET": if redis { - RedisBench("SET", addr, fillOpts(), - func(buf []byte, _ ServerType) []byte { - return AppendCommand(buf, "SET", "key:__rand_int__", "xxx") + redbench.Bench("SET", addr, fillOpts(), prepFn, + func(buf []byte) []byte { + return redbench.AppendCommand(buf, "SET", "key:__rand_int__", "xxx") }, ) } else { var i int64 - RedisBench("SET (point)", addr, fillOpts(), - func(buf []byte, _ ServerType) []byte { + redbench.Bench("SET (point)", addr, fillOpts(), prepFn, + func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) lat, lon := randPoint() - return AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "POINT", + return redbench.AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "POINT", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), ) }, ) - RedisBench("SET (rect)", addr, fillOpts(), - func(buf []byte, _ ServerType) []byte { + redbench.Bench("SET (rect)", addr, fillOpts(), prepFn, + func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) minlat, minlon, maxlat, maxlon := randRect() - return AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "BOUNDS", + return redbench.AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "BOUNDS", strconv.FormatFloat(minlat, 'f', 5, 64), strconv.FormatFloat(minlon, 'f', 5, 64), strconv.FormatFloat(maxlat, 'f', 5, 64), @@ -201,65 +208,65 @@ func main() { ) }, ) - RedisBench("SET (string)", addr, fillOpts(), - func(buf []byte, _ ServerType) []byte { + redbench.Bench("SET (string)", addr, fillOpts(), prepFn, + func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) - return AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "STRING", "xxx") + return redbench.AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "STRING", "xxx") }, ) } case "GET": if redis { - RedisBench("GET", addr, fillOpts(), - func(buf []byte, _ ServerType) []byte { - return AppendCommand(buf, "GET", "key:__rand_int__") + redbench.Bench("GET", addr, fillOpts(), prepFn, + func(buf []byte) []byte { + return redbench.AppendCommand(buf, "GET", "key:__rand_int__") }, ) } else { var i int64 - RedisBench("GET (point)", addr, fillOpts(), - func(buf []byte, _ ServerType) []byte { + redbench.Bench("GET (point)", addr, fillOpts(), prepFn, + func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) - return AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "POINT") + return redbench.AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "POINT") }, ) - RedisBench("GET (rect)", addr, fillOpts(), - func(buf []byte, _ ServerType) []byte { + redbench.Bench("GET (rect)", addr, fillOpts(), prepFn, + func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) - return AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "BOUNDS") + return redbench.AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "BOUNDS") }, ) - RedisBench("GET (string)", addr, fillOpts(), - func(buf []byte, _ ServerType) []byte { + redbench.Bench("GET (string)", addr, fillOpts(), prepFn, + func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) - return AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "OBJECT") + return redbench.AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "OBJECT") }, ) } case "SEARCH": if !redis { - RedisBench("SEARCH (nearby 1km)", addr, fillOpts(), - func(buf []byte, _ ServerType) []byte { + redbench.Bench("SEARCH (nearby 1km)", addr, fillOpts(), prepFn, + func(buf []byte) []byte { lat, lon := randPoint() - return AppendCommand(buf, "NEARBY", "key:bench", "COUNT", "POINT", + return redbench.AppendCommand(buf, "NEARBY", "key:bench", "COUNT", "POINT", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), "1000") }, ) - RedisBench("SEARCH (nearby 10km)", addr, fillOpts(), - func(buf []byte, _ ServerType) []byte { + redbench.Bench("SEARCH (nearby 10km)", addr, fillOpts(), prepFn, + func(buf []byte) []byte { lat, lon := randPoint() - return AppendCommand(buf, "NEARBY", "key:bench", "COUNT", "POINT", + return redbench.AppendCommand(buf, "NEARBY", "key:bench", "COUNT", "POINT", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), "10000") }, ) - RedisBench("SEARCH (nearby 100km)", addr, fillOpts(), - func(buf []byte, _ ServerType) []byte { + redbench.Bench("SEARCH (nearby 100km)", addr, fillOpts(), prepFn, + func(buf []byte) []byte { lat, lon := randPoint() - return AppendCommand(buf, "NEARBY", "key:bench", "COUNT", "POINT", + return redbench.AppendCommand(buf, "NEARBY", "key:bench", "COUNT", "POINT", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), "100000") @@ -269,253 +276,3 @@ func main() { } } } -func readResp(rd *bufio.Reader, n int) error { - for i := 0; i < n; i++ { - line, err := rd.ReadBytes('\n') - if err != nil { - return err - } - switch line[0] { - default: - return errors.New("invalid server response") - case '+', ':': - case '-': - //panic(string(line)) - case '$': - n, err := strconv.ParseInt(string(line[1:len(line)-2]), 10, 64) - if err != nil { - return err - } - if _, err = io.CopyN(ioutil.Discard, rd, n+2); err != nil { - return err - } - case '*': - n, err := strconv.ParseInt(string(line[1:len(line)-2]), 10, 64) - if err != nil { - return err - } - readResp(rd, int(n)) - } - } - return nil -} - -type ServerType int - -const ( - Redis = 1 - Tile38 = 2 -) - -type Options struct { - Requests int - Clients int - Pipeline int - Quiet bool - CSV bool - JSON bool -} - -func RedisBench( - name string, - addr string, - opts *Options, - fill func(buf []byte, server ServerType) []byte, -) { - var server ServerType - var totalPayload uint64 - var count uint64 - var duration int64 - rpc := opts.Requests / opts.Clients - rpcex := opts.Requests % opts.Clients - var tstop int64 - remaining := int64(opts.Clients) - errs := make([]error, opts.Clients) - durs := make([][]time.Duration, opts.Clients) - conns := make([]net.Conn, opts.Clients) - - // create all clients - for i := 0; i < opts.Clients; i++ { - crequests := rpc - if i == opts.Clients-1 { - crequests += rpcex - } - durs[i] = make([]time.Duration, crequests) - for j := 0; j < len(durs[i]); j++ { - durs[i][j] = -1 - } - conn, err := net.Dial("tcp", addr) - if err != nil { - if i == 0 { - fmt.Fprintf(os.Stderr, "%s\n", err.Error()) - return - } - errs[i] = err - } - conns[i] = conn - if conn != nil { - if i == 0 { - conn.Write([]byte("info server\r\n")) - resp := make([]byte, 500) - conn.Read(resp) - if strings.Contains(string(resp), "redis_version") { - if strings.Contains(string(resp), "tile38_version") { - server = Tile38 - } else { - server = Redis - } - } - } - if opts.JSON { - conn.Write([]byte("output json\r\n")) - resp := make([]byte, 100) - conn.Read(resp) - } - } - } - - tstart := time.Now() - for i := 0; i < opts.Clients; i++ { - crequests := rpc - if i == opts.Clients-1 { - crequests += rpcex - } - - go func(conn net.Conn, client, crequests int) { - defer func() { - atomic.AddInt64(&remaining, -1) - }() - if conn == nil { - return - } - err := func() error { - var buf []byte - rd := bufio.NewReader(conn) - for i := 0; i < crequests; i += opts.Pipeline { - n := opts.Pipeline - if i+n > crequests { - n = crequests - i - } - buf = buf[:0] - for i := 0; i < n; i++ { - buf = fill(buf, server) - } - atomic.AddUint64(&totalPayload, uint64(len(buf))) - start := time.Now() - _, err := conn.Write(buf) - if err != nil { - return err - } - if err := readResp(rd, n); err != nil { - return err - } - stop := time.Since(start) - for j := 0; j < n; j++ { - durs[client][i+j] = stop / time.Duration(n) - } - atomic.AddInt64(&duration, int64(stop)) - atomic.AddUint64(&count, uint64(n)) - atomic.StoreInt64(&tstop, int64(time.Since(tstart))) - } - return nil - }() - if err != nil { - errs[client] = err - } - }(conns[i], i, crequests) - } - var die bool - for { - remaining := int(atomic.LoadInt64(&remaining)) // active clients - count := int(atomic.LoadUint64(&count)) // completed requests - real := time.Duration(atomic.LoadInt64(&tstop)) // real duration - totalPayload := int(atomic.LoadUint64(&totalPayload)) // size of all bytes sent - more := remaining > 0 - var realrps float64 - if real > 0 { - realrps = float64(count) / (float64(real) / float64(time.Second)) - } - if !opts.CSV { - fmt.Printf("\r%s: %.2f", name, realrps) - if more { - fmt.Printf("\r") - } else if opts.Quiet { - fmt.Printf(" requests per second\n") - } else { - fmt.Printf("\r====== %s ======\n", name) - fmt.Printf(" %d requests completed in %.2f seconds\n", opts.Requests, float64(real)/float64(time.Second)) - fmt.Printf(" %d parallel clients\n", opts.Clients) - fmt.Printf(" %d bytes payload\n", totalPayload/opts.Requests) - fmt.Printf(" keep alive: 1\n") - fmt.Printf("\n") - var limit time.Duration - var lastper float64 - for { - limit += time.Millisecond - var hits, count int - for i := 0; i < len(durs); i++ { - for j := 0; j < len(durs[i]); j++ { - dur := durs[i][j] - if dur == -1 { - continue - } - if dur < limit { - hits++ - } - count++ - } - } - per := float64(hits) / float64(count) - if math.Floor(per*10000) == math.Floor(lastper*10000) { - continue - } - lastper = per - fmt.Printf("%.2f%% <= %d milliseconds\n", per*100, (limit-time.Millisecond)/time.Millisecond) - if per == 1.0 { - break - } - } - fmt.Printf("%.2f requests per second\n\n", realrps) - } - } - if !more { - if opts.CSV { - fmt.Printf("\"%s\",\"%.2f\"\n", name, realrps) - } - for _, err := range errs { - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - die = true - if count == 0 { - break - } - } - } - break - } - time.Sleep(time.Second / 5) - } - - // close clients - for i := 0; i < len(conns); i++ { - if conns[i] != nil { - conns[i].Close() - } - } - if die { - os.Exit(1) - } -} -func AppendCommand(buf []byte, args ...string) []byte { - buf = append(buf, '*') - buf = strconv.AppendInt(buf, int64(len(args)), 10) - buf = append(buf, '\r', '\n') - for _, arg := range args { - buf = append(buf, '$') - buf = strconv.AppendInt(buf, int64(len(arg)), 10) - buf = append(buf, '\r', '\n') - buf = append(buf, arg...) - buf = append(buf, '\r', '\n') - } - return buf -} diff --git a/vendor/github.com/tidwall/redbench/LICENSE b/vendor/github.com/tidwall/redbench/LICENSE new file mode 100644 index 00000000..090ad05f --- /dev/null +++ b/vendor/github.com/tidwall/redbench/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2017 Josh Baker + +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. diff --git a/vendor/github.com/tidwall/redbench/README.md b/vendor/github.com/tidwall/redbench/README.md new file mode 100644 index 00000000..28d3e6a7 --- /dev/null +++ b/vendor/github.com/tidwall/redbench/README.md @@ -0,0 +1,93 @@ +# Redbench +[![GoDoc](https://img.shields.io/badge/api-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/tidwall/redbench) + +Redbench is a Go package that allows for bootstrapping benchmarks +for servers using a custom implementation of the Redis protocol. It provides +the same inputs and outputs as the +[redis-benchmark](https://redis.io/topics/benchmarks) tool. The purpose of +this library is to provide benchmarking for +[Redcon](https://github.com/tidwall/redcon) compatible servers such as +[Tile38](https://github.com/tidwall/tile38), but also works well for Redis +operations that are not covered by the `redis-benchmark` tool such as the +`GEO*` commands, custom lua scripts, or [Redis Modules](http://antirez.com/news/106). + +## Getting Started + +### Installing + +To start using Redbench, install Go and run `go get`: + +```sh +$ go get -u github.com/tidwall/redbench +``` + +This will retrieve the library. + +### Example + +The following example will run a benchmark for the `PING,SET,GET,GEOADD,GEORADIUS` +commands on a server at 127.0.0.1:6379. + +```go +package main + +import ( + "math/rand" + "strconv" + "time" + + "github.com/tidwall/redbench" +) + +func main() { + redbench.Bench("PING", "127.0.0.1:6379", nil, nil, func(buf []byte) []byte { + return redbench.AppendCommand(buf, "PING") + }) + redbench.Bench("SET", "127.0.0.1:6379", nil, nil, func(buf []byte) []byte { + return redbench.AppendCommand(buf, "SET", "key:string", "val") + }) + redbench.Bench("GET", "127.0.0.1:6379", nil, nil, func(buf []byte) []byte { + return redbench.AppendCommand(buf, "GET", "key:string") + }) + rand.Seed(time.Now().UnixNano()) + redbench.Bench("GEOADD", "127.0.0.1:6379", nil, nil, func(buf []byte) []byte { + return redbench.AppendCommand(buf, "GEOADD", "key:geo", + strconv.FormatFloat(rand.Float64()*360-180, 'f', 7, 64), + strconv.FormatFloat(rand.Float64()*170-85, 'f', 7, 64), + strconv.Itoa(rand.Int())) + }) + redbench.Bench("GEORADIUS", "127.0.0.1:6379", nil, nil, func(buf []byte) []byte { + return redbench.AppendCommand(buf, "GEORADIUS", "key:geo", + strconv.FormatFloat(rand.Float64()*360-180, 'f', 7, 64), + strconv.FormatFloat(rand.Float64()*170-85, 'f', 7, 64), + "10", "km") + }) +} +``` + +The operations is like as executing, but without the `GEO*` commands. + +``` +$ redis-benchmark -t PING,SET,GET +``` + +### Custom Options + +```go +type Options struct { + Requests int + Clients int + Pipeline int + Quiet bool + CSV bool + Stdout io.Writer + Stderr io.Writer +} +``` + +## Contact +Josh Baker [@tidwall](http://twitter.com/tidwall) + +## License +Redbench source code is available under the MIT [License](/LICENSE). + diff --git a/vendor/github.com/tidwall/redbench/bench.go b/vendor/github.com/tidwall/redbench/bench.go new file mode 100644 index 00000000..8403497c --- /dev/null +++ b/vendor/github.com/tidwall/redbench/bench.go @@ -0,0 +1,271 @@ +package redbench + +import ( + "bufio" + "errors" + "fmt" + "io" + "io/ioutil" + "math" + "net" + "os" + "strconv" + "sync/atomic" + "time" +) + +func readResp(rd *bufio.Reader, n int) error { + for i := 0; i < n; i++ { + line, err := rd.ReadBytes('\n') + if err != nil { + return err + } + switch line[0] { + default: + return errors.New("invalid server response") + case '+', ':': + case '-': + case '$': + n, err := strconv.ParseInt(string(line[1:len(line)-2]), 10, 64) + if err != nil { + return err + } + if _, err = io.CopyN(ioutil.Discard, rd, n+2); err != nil { + return err + } + case '*': + n, err := strconv.ParseInt(string(line[1:len(line)-2]), 10, 64) + if err != nil { + return err + } + readResp(rd, int(n)) + } + } + return nil +} + +// Options represents various options used by the Bench() function. +type Options struct { + Requests int + Clients int + Pipeline int + Quiet bool + CSV bool + Stdout io.Writer + Stderr io.Writer +} + +// DefaultsOptions are the default options used by the Bench() function. +var DefaultOptions = &Options{ + Requests: 100000, + Clients: 50, + Pipeline: 1, + Quiet: false, + CSV: false, + Stdout: os.Stdout, + Stderr: os.Stderr, +} + +// Bench performs a benchmark on the server at the specified address. +func Bench( + name string, + addr string, + opts *Options, + prep func(conn net.Conn) bool, + fill func(buf []byte) []byte, +) { + if opts == nil { + opts = DefaultOptions + } + if opts.Stderr == nil { + opts.Stderr = ioutil.Discard + } + if opts.Stdout == nil { + opts.Stdout = ioutil.Discard + } + var totalPayload uint64 + var count uint64 + var duration int64 + rpc := opts.Requests / opts.Clients + rpcex := opts.Requests % opts.Clients + var tstop int64 + remaining := int64(opts.Clients) + errs := make([]error, opts.Clients) + durs := make([][]time.Duration, opts.Clients) + conns := make([]net.Conn, opts.Clients) + + // create all clients + for i := 0; i < opts.Clients; i++ { + crequests := rpc + if i == opts.Clients-1 { + crequests += rpcex + } + durs[i] = make([]time.Duration, crequests) + for j := 0; j < len(durs[i]); j++ { + durs[i][j] = -1 + } + conn, err := net.Dial("tcp", addr) + if err != nil { + if i == 0 { + fmt.Fprintf(opts.Stderr, "%s\n", err.Error()) + return + } + errs[i] = err + } + if conn != nil && prep != nil { + if !prep(conn) { + conn.Close() + conn = nil + } + } + conns[i] = conn + } + + tstart := time.Now() + for i := 0; i < opts.Clients; i++ { + crequests := rpc + if i == opts.Clients-1 { + crequests += rpcex + } + + go func(conn net.Conn, client, crequests int) { + defer func() { + atomic.AddInt64(&remaining, -1) + }() + if conn == nil { + return + } + err := func() error { + var buf []byte + rd := bufio.NewReader(conn) + for i := 0; i < crequests; i += opts.Pipeline { + n := opts.Pipeline + if i+n > crequests { + n = crequests - i + } + buf = buf[:0] + for i := 0; i < n; i++ { + buf = fill(buf) + } + atomic.AddUint64(&totalPayload, uint64(len(buf))) + start := time.Now() + _, err := conn.Write(buf) + if err != nil { + return err + } + if err := readResp(rd, n); err != nil { + return err + } + stop := time.Since(start) + for j := 0; j < n; j++ { + durs[client][i+j] = stop / time.Duration(n) + } + atomic.AddInt64(&duration, int64(stop)) + atomic.AddUint64(&count, uint64(n)) + atomic.StoreInt64(&tstop, int64(time.Since(tstart))) + } + return nil + }() + if err != nil { + errs[client] = err + } + }(conns[i], i, crequests) + } + var die bool + for { + remaining := int(atomic.LoadInt64(&remaining)) // active clients + count := int(atomic.LoadUint64(&count)) // completed requests + real := time.Duration(atomic.LoadInt64(&tstop)) // real duration + totalPayload := int(atomic.LoadUint64(&totalPayload)) // size of all bytes sent + more := remaining > 0 + var realrps float64 + if real > 0 { + realrps = float64(count) / (float64(real) / float64(time.Second)) + } + if !opts.CSV { + fmt.Fprintf(opts.Stdout, "\r%s: %.2f", name, realrps) + if more { + fmt.Fprintf(opts.Stdout, "\r") + } else if opts.Quiet { + fmt.Fprintf(opts.Stdout, " requests per second\n") + } else { + fmt.Fprintf(opts.Stdout, "\r====== %s ======\n", name) + fmt.Fprintf(opts.Stdout, " %d requests completed in %.2f seconds\n", opts.Requests, float64(real)/float64(time.Second)) + fmt.Fprintf(opts.Stdout, " %d parallel clients\n", opts.Clients) + fmt.Fprintf(opts.Stdout, " %d bytes payload\n", totalPayload/opts.Requests) + fmt.Fprintf(opts.Stdout, " keep alive: 1\n") + fmt.Fprintf(opts.Stdout, "\n") + var limit time.Duration + var lastper float64 + for { + limit += time.Millisecond + var hits, count int + for i := 0; i < len(durs); i++ { + for j := 0; j < len(durs[i]); j++ { + dur := durs[i][j] + if dur == -1 { + continue + } + if dur < limit { + hits++ + } + count++ + } + } + per := float64(hits) / float64(count) + if math.Floor(per*10000) == math.Floor(lastper*10000) { + continue + } + lastper = per + fmt.Fprintf(opts.Stdout, "%.2f%% <= %d milliseconds\n", per*100, (limit-time.Millisecond)/time.Millisecond) + if per == 1.0 { + break + } + } + fmt.Fprintf(opts.Stdout, "%.2f requests per second\n\n", realrps) + } + } + if !more { + if opts.CSV { + fmt.Fprintf(opts.Stdout, "\"%s\",\"%.2f\"\n", name, realrps) + } + for _, err := range errs { + if err != nil { + fmt.Fprintf(opts.Stderr, "%s\n", err) + die = true + if count == 0 { + break + } + } + } + break + } + time.Sleep(time.Second / 5) + } + + // close clients + for i := 0; i < len(conns); i++ { + if conns[i] != nil { + conns[i].Close() + } + } + if die { + os.Exit(1) + } +} + +// AppendCommand will append a Redis command to the byte slice and +// returns a modifed slice. +func AppendCommand(buf []byte, args ...string) []byte { + buf = append(buf, '*') + buf = strconv.AppendInt(buf, int64(len(args)), 10) + buf = append(buf, '\r', '\n') + for _, arg := range args { + buf = append(buf, '$') + buf = strconv.AppendInt(buf, int64(len(arg)), 10) + buf = append(buf, '\r', '\n') + buf = append(buf, arg...) + buf = append(buf, '\r', '\n') + } + return buf +}