Repository: codesenberg/bombardier
Branch: master
Commit: 2d495aca5b23
Files: 50
Total size: 143.3 KB
Directory structure:
gitextract_k5a5acgj/
├── .github/
│ ├── ISSUE_TEMPLATE.md
│ └── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .semaphore/
│ └── semaphore.yml
├── LICENSE
├── README.md
├── args_parser.go
├── args_parser_test.go
├── bombardier.go
├── bombardier_performance_test.go
├── bombardier_test.go
├── build.py
├── client_cert.go
├── client_cert_test.go
├── clients.go
├── clients_test.go
├── cmd/
│ └── utils/
│ └── simplebenchserver/
│ ├── doc.go
│ └── main.go
├── common.go
├── completion_barriers.go
├── completion_barriers_test.go
├── config.go
├── config_test.go
├── dialer.go
├── doc.go
├── docs/
│ └── CONTRIBUTING.md
├── error_map.go
├── error_map_test.go
├── flags.go
├── flags_test.go
├── format.go
├── format_test.go
├── go.mod
├── go.sum
├── headers.go
├── headers_test.go
├── internal/
│ └── test_info.go
├── limiter.go
├── limiter_barrier_test.go
├── limiter_test.go
├── proxy_reader.go
├── rateestimator.go
├── rateestimator_test.go
├── template/
│ └── doc.go
├── templates.go
├── testbody.txt
├── testclient.cert
├── testclient.key
├── testserver.cert
└── testserver.key
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
This is an example of what a **bug report** can look like. Please, feel free to also provide any other information relevant to the issue.
### What version of bombardier are you using?
Hash of the commit, like
[00d7965d6cae34c62042abb0f6c45c45b870dcf3](https://github.com/codesenberg/bombardier/commit/00d7965d6cae34c62042abb0f6c45c45b870dcf3)
in case you've built _bombardier_ yourself or version obtained by
```
bombardier --version
```
in case you are using binaries.
### What operating system and processor architecture are you using (if relevant)?
Examples are `windows/amd64`, `linux/amd64`, `darwin/amd64`, etc.
### What did you do?
Describe steps that can be used to reproduce the error.
### What you expected to happen?
### What actually happened?
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
Before submitting a pull request be sure to check the code with [gometalinter](https://github.com/alecthomas/gometalinter).
This can save a considerable amount of time during code review.
Generally, try to follow this format when writing commit messages:
```
<name of the subsystem of bombardier if applicable or "all">: <short description of changes>
<A more elaborate description of the changes and maybe some explanations go here.>
<Fixes #<number of issue>, updates #<number of issue>, closes #<number of issue>. If applicable.>
```
Examples of such commit messages can be found in the commit log of this project or
[in this section](https://golang.org/doc/contribute.html#commit_changes) of Go's Contribution Guidelines,
from which this format was adopted.
The pull request itself can contain a short description of changes made, questions or provide some other information, etc.
================================================
FILE: .gitignore
================================================
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
cover.*
bombardier
bombardier-*
================================================
FILE: .semaphore/semaphore.yml
================================================
version: v1.0
name: codesenberg/bombardier
agent:
machine:
type: e1-standard-2
os_image: ubuntu2004
blocks:
- name: Test
task:
prologue:
commands:
- checkout
- go install gotest.tools/gotestsum@latest
jobs:
- name: Test go 1.21
commands:
- sem-version go 1.21
- gotestsum --junitfile report.xml ./...
- name: Test go 1.22
commands:
- sem-version go 1.22
- gotestsum --junitfile report.xml ./...
epilogue:
always:
commands:
- '[[ -f report.xml ]] && test-results publish report.xml'
after_pipeline:
task:
jobs:
- name: Publish test results
commands:
- test-results gen-pipeline-report
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2016 Максим Федосеев
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, 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.
================================================
FILE: README.md
================================================
# bombardier [](https://codesenberg.semaphoreci.com/projects/bombardier) [](https://goreportcard.com/report/github.com/codesenberg/bombardier) [](http://godoc.org/github.com/codesenberg/bombardier)

bombardier is a HTTP(S) benchmarking tool. It is written in Go programming language and uses excellent [fasthttp](https://github.com/valyala/fasthttp) instead of Go's default http library, because of its lightning fast performance.
With `bombardier v1.1` and higher you can now use `net/http` client if you need to test HTTP/2.x services or want to use a more RFC-compliant HTTP client.
## Installation
You can grab binaries in the [releases](https://github.com/codesenberg/bombardier/releases) section.
Alternatively, to get latest and greatest run:
Go 1.18+: `go install github.com/codesenberg/bombardier@latest`
## Usage
```
bombardier [<flags>] <url>
```
For a more detailed information about flags consult [GoDoc](http://godoc.org/github.com/codesenberg/bombardier).
## Known issues
AFAIK, it's impossible to pass Host header correctly with `fasthttp`, you can use `net/http`(`--http1`/`--http2` flags) to workaround this issue.
## Examples
Example of running `bombardier` against [this server](https://godoc.org/github.com/codesenberg/bombardier/cmd/utils/simplebenchserver):
```
> bombardier -c 125 -n 10000000 http://localhost:8080
Bombarding http://localhost:8080 with 10000000 requests using 125 connections
10000000 / 10000000 [============================================] 100.00% 37s Done!
Statistics Avg Stdev Max
Reqs/sec 264560.00 10733.06 268434
Latency 471.00us 522.34us 51.00ms
HTTP codes:
1xx - 0, 2xx - 10000000, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 292.92MB/s
```
Or, against a realworld server(with latency distribution):
```
> bombardier -c 200 -d 10s -l http://ya.ru
Bombarding http://ya.ru for 10s using 200 connections
[=========================================================================] 10s Done!
Statistics Avg Stdev Max
Reqs/sec 6607.00 524.56 7109
Latency 29.86ms 5.36ms 305.02ms
Latency Distribution
50% 28.00ms
75% 32.00ms
90% 34.00ms
99% 48.00ms
HTTP codes:
1xx - 0, 2xx - 0, 3xx - 66561, 4xx - 0, 5xx - 0
others - 5
Errors:
dialing to the given TCP address timed out - 5
Throughput: 3.06MB/s
```
================================================
FILE: args_parser.go
================================================
package main
import (
"fmt"
"runtime"
"strconv"
"strings"
"time"
"github.com/alecthomas/kingpin"
"github.com/goware/urlx"
)
type argsParser interface {
parse([]string) (config, error)
}
type kingpinParser struct {
app *kingpin.Application
url string
numReqs *nullableUint64
duration *nullableDuration
headers *headersList
numConns uint64
timeout time.Duration
latencies bool
insecure bool
disableKeepAlives bool
method string
body string
bodyFilePath string
stream bool
certPath string
keyPath string
rate *nullableUint64
clientType clientTyp
printSpec *nullableString
noPrint bool
formatSpec string
}
func newKingpinParser() argsParser {
kparser := &kingpinParser{
numReqs: new(nullableUint64),
duration: new(nullableDuration),
headers: new(headersList),
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
latencies: false,
method: "GET",
body: "",
bodyFilePath: "",
stream: false,
certPath: "",
keyPath: "",
insecure: false,
url: "",
rate: new(nullableUint64),
clientType: fhttp,
printSpec: new(nullableString),
noPrint: false,
formatSpec: "plain-text",
}
app := kingpin.New("", "Fast cross-platform HTTP benchmarking tool").
Version("bombardier version " + version + " " + runtime.GOOS + "/" +
runtime.GOARCH)
app.Flag("connections", "Maximum number of concurrent connections").
Short('c').
PlaceHolder(strconv.FormatUint(defaultNumberOfConns, decBase)).
Uint64Var(&kparser.numConns)
app.Flag("timeout", "Socket/request timeout").
PlaceHolder(defaultTimeout.String()).
Short('t').
DurationVar(&kparser.timeout)
app.Flag("latencies", "Print latency statistics").
Short('l').
BoolVar(&kparser.latencies)
app.Flag("method", "Request method").
PlaceHolder("GET").
Short('m').
StringVar(&kparser.method)
app.Flag("body", "Request body").
Default("").
Short('b').
StringVar(&kparser.body)
app.Flag("body-file", "File to use as request body").
Default("").
Short('f').
StringVar(&kparser.bodyFilePath)
app.Flag("stream", "Specify whether to stream body using "+
"chunked transfer encoding or to serve it from memory").
Short('s').
BoolVar(&kparser.stream)
app.Flag("cert", "Path to the client's TLS Certificate").
Default("").
StringVar(&kparser.certPath)
app.Flag("key", "Path to the client's TLS Certificate Private Key").
Default("").
StringVar(&kparser.keyPath)
app.Flag("insecure",
"Controls whether a client verifies the server's certificate"+
" chain and host name").
Short('k').
BoolVar(&kparser.insecure)
app.Flag("disableKeepAlives",
"Disable HTTP keep-alive. For fasthttp use -H 'Connection: close'").
Short('a').
BoolVar(&kparser.disableKeepAlives)
app.Flag("header", "HTTP headers to use(can be repeated)").
PlaceHolder("\"K: V\"").
Short('H').
SetValue(kparser.headers)
app.Flag("requests", "Number of requests").
PlaceHolder("[pos. int.]").
Short('n').
SetValue(kparser.numReqs)
app.Flag("duration", "Duration of test").
PlaceHolder(defaultTestDuration.String()).
Short('d').
SetValue(kparser.duration)
app.Flag("rate", "Rate limit in requests per second").
PlaceHolder("[pos. int.]").
Short('r').
SetValue(kparser.rate)
app.Flag("fasthttp", "Use fasthttp client").
Action(func(*kingpin.ParseContext) error {
kparser.clientType = fhttp
return nil
}).
Bool()
app.Flag("http1", "Use net/http client with forced HTTP/1.x").
Action(func(*kingpin.ParseContext) error {
kparser.clientType = nhttp1
return nil
}).
Bool()
app.Flag("http2", "Use net/http client with enabled HTTP/2.0").
Action(func(*kingpin.ParseContext) error {
kparser.clientType = nhttp2
return nil
}).
Bool()
app.Flag(
"print", "Specifies what to output. Comma-separated list of values"+
" 'intro' (short: 'i'), 'progress' (short: 'p'),"+
" 'result' (short: 'r'). Examples:"+
"\n\t* i,p,r (prints everything)"+
"\n\t* intro,result (intro & result)"+
"\n\t* r (result only)"+
"\n\t* result (same as above)").
PlaceHolder("<spec>").
Short('p').
SetValue(kparser.printSpec)
app.Flag("no-print", "Don't output anything").
Short('q').
BoolVar(&kparser.noPrint)
app.Flag("format", "Which format to use to output the result. "+
"<spec> is either a name (or its shorthand) of some format "+
"understood by bombardier or a path to the user-defined template, "+
"which uses Go's text/template syntax, prefixed with 'path:' string "+
"(without single quotes), i.e. \"path:/some/path/to/your.template\" "+
" or \"path:C:\\some\\path\\to\\your.template\" in case of Windows. "+
"Formats understood by bombardier are:"+
"\n\t* plain-text (short: pt)"+
"\n\t* json (short: j)").
PlaceHolder("<spec>").
Short('o').
StringVar(&kparser.formatSpec)
app.Arg("url", "Target's URL").Required().
StringVar(&kparser.url)
kparser.app = app
return argsParser(kparser)
}
func (k *kingpinParser) parse(args []string) (config, error) {
k.app.Name = args[0]
_, err := k.app.Parse(args[1:])
if err != nil {
return emptyConf, err
}
pi, pp, pr := true, true, true
if k.printSpec.val != nil {
pi, pp, pr, err = parsePrintSpec(*k.printSpec.val)
if err != nil {
return emptyConf, err
}
}
if k.noPrint {
pi, pp, pr = false, false, false
}
format := formatFromString(k.formatSpec)
if format == nil {
return emptyConf, fmt.Errorf(
"unknown format or invalid format spec %q", k.formatSpec,
)
}
url, err := urlx.Parse(k.url)
if err != nil {
return emptyConf, err
}
return config{
numConns: k.numConns,
numReqs: k.numReqs.val,
duration: k.duration.val,
url: url,
headers: k.headers,
timeout: k.timeout,
method: k.method,
body: k.body,
bodyFilePath: k.bodyFilePath,
stream: k.stream,
keyPath: k.keyPath,
certPath: k.certPath,
printLatencies: k.latencies,
insecure: k.insecure,
disableKeepAlives: k.disableKeepAlives,
rate: k.rate.val,
clientType: k.clientType,
printIntro: pi,
printProgress: pp,
printResult: pr,
format: format,
}, nil
}
func parsePrintSpec(spec string) (bool, bool, bool, error) {
pi, pp, pr := false, false, false
if spec == "" {
return false, false, false, errEmptyPrintSpec
}
parts := strings.Split(spec, ",")
partsCount := 0
for _, p := range parts {
switch p {
case "i", "intro":
pi = true
case "p", "progress":
pp = true
case "r", "result":
pr = true
default:
return false, false, false,
fmt.Errorf("%q is not a valid part of print spec", p)
}
partsCount++
}
if partsCount < 1 || partsCount > 3 {
return false, false, false,
fmt.Errorf(
"spec %q has too many parts, at most 3 are allowed", spec,
)
}
return pi, pp, pr, nil
}
================================================
FILE: args_parser_test.go
================================================
package main
import (
"fmt"
"reflect"
"strconv"
"testing"
"time"
)
const (
programName = "bombardier"
)
func TestInvalidArgsParsing(t *testing.T) {
expectations := []struct {
in []string
out string
}{
{
[]string{programName},
"required argument 'url' not provided",
},
{
[]string{programName, "http://google.com", "http://yahoo.com"},
"unexpected http://yahoo.com",
},
}
for _, e := range expectations {
p := newKingpinParser()
if _, err := p.parse(e.in); err == nil ||
err.Error() != e.out {
t.Error(err, e.out)
}
}
}
func TestUnspecifiedArgParsing(t *testing.T) {
p := newKingpinParser()
args := []string{programName, "--someunspecifiedflag"}
_, err := p.parse(args)
if err == nil {
t.Fail()
}
}
func TestArgsParsing(t *testing.T) {
ten := uint64(10)
expectations := []struct {
in [][]string
out config
}{
{
[][]string{
{programName, "localhost:8080"},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
url: ParseURLOrPanic("http://localhost:8080"),
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{programName, "https://localhost"},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
url: ParseURLOrPanic("https://localhost"),
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{{programName, "https://somehost.somedomain"}},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
url: ParseURLOrPanic("https://somehost.somedomain"),
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"-c", "10",
"-n", strconv.FormatUint(defaultNumberOfReqs, decBase),
"-t", "10s",
"https://somehost.somedomain",
},
{
programName,
"-c10",
"-n" + strconv.FormatUint(defaultNumberOfReqs, decBase),
"-t10s",
"https://somehost.somedomain",
},
{
programName,
"--connections", "10",
"--requests", strconv.FormatUint(defaultNumberOfReqs, decBase),
"--timeout", "10s",
"https://somehost.somedomain",
},
{
programName,
"--connections=10",
"--requests=" + strconv.FormatUint(defaultNumberOfReqs, decBase),
"--timeout=10s",
"https://somehost.somedomain",
},
},
config{
numConns: 10,
timeout: 10 * time.Second,
headers: new(headersList),
method: "GET",
numReqs: &defaultNumberOfReqs,
url: ParseURLOrPanic("https://somehost.somedomain"),
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"--latencies",
"https://somehost.somedomain",
},
{
programName,
"-l",
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
printLatencies: true,
method: "GET",
url: ParseURLOrPanic("https://somehost.somedomain"),
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"--insecure",
"https://somehost.somedomain",
},
{
programName,
"-k",
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
insecure: true,
method: "GET",
url: ParseURLOrPanic("https://somehost.somedomain"),
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"--key", "testclient.key",
"--cert", "testclient.cert",
"https://somehost.somedomain",
},
{
programName,
"--key=testclient.key",
"--cert=testclient.cert",
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
keyPath: "testclient.key",
certPath: "testclient.cert",
url: ParseURLOrPanic("https://somehost.somedomain"),
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"--method", "POST",
"--body", "reqbody",
"https://somehost.somedomain",
},
{
programName,
"--method=POST",
"--body=reqbody",
"https://somehost.somedomain",
},
{
programName,
"-m", "POST",
"-b", "reqbody",
"https://somehost.somedomain",
},
{
programName,
"-mPOST",
"-breqbody",
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "POST",
body: "reqbody",
url: ParseURLOrPanic("https://somehost.somedomain"),
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"--header", "One: Value one",
"--header", "Two: Value two",
"https://somehost.somedomain",
},
{
programName,
"-H", "One: Value one",
"-H", "Two: Value two",
"https://somehost.somedomain",
},
{
programName,
"--header=One: Value one",
"--header=Two: Value two",
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: &headersList{
{"One", "Value one"},
{"Two", "Value two"},
},
method: "GET",
url: ParseURLOrPanic("https://somehost.somedomain"),
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"--rate", "10",
"https://somehost.somedomain",
},
{
programName,
"-r", "10",
"https://somehost.somedomain",
},
{
programName,
"--rate=10",
"https://somehost.somedomain",
},
{
programName,
"-r10",
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
url: ParseURLOrPanic("https://somehost.somedomain"),
rate: &ten,
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"--fasthttp",
"https://somehost.somedomain",
},
{
programName,
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
url: ParseURLOrPanic("https://somehost.somedomain"),
clientType: fhttp,
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"--http1",
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
url: ParseURLOrPanic("https://somehost.somedomain"),
clientType: nhttp1,
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"--http2",
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
url: ParseURLOrPanic("https://somehost.somedomain"),
clientType: nhttp2,
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"--body-file=testbody.txt",
"https://somehost.somedomain",
},
{
programName,
"--body-file", "testbody.txt",
"https://somehost.somedomain",
},
{
programName,
"-f", "testbody.txt",
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
bodyFilePath: "testbody.txt",
url: ParseURLOrPanic("https://somehost.somedomain"),
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"--stream",
"https://somehost.somedomain",
},
{
programName,
"-s",
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
stream: true,
url: ParseURLOrPanic("https://somehost.somedomain"),
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
url: ParseURLOrPanic("https://somehost.somedomain"),
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"--print=r,i,p",
"https://somehost.somedomain",
},
{
programName,
"--print", "r,i,p",
"https://somehost.somedomain",
},
{
programName,
"-p", "r,i,p",
"https://somehost.somedomain",
},
{
programName,
"--print=result,i,p",
"https://somehost.somedomain",
},
{
programName,
"--print", "r,intro,p",
"https://somehost.somedomain",
},
{
programName,
"-p", "r,i,progress",
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
url: ParseURLOrPanic("https://somehost.somedomain"),
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"--print=i,r",
"https://somehost.somedomain",
},
{
programName,
"--print", "i,r",
"https://somehost.somedomain",
},
{
programName,
"-p", "i,r",
"https://somehost.somedomain",
},
{
programName,
"--print=intro,r",
"https://somehost.somedomain",
},
{
programName,
"--print", "i,result",
"https://somehost.somedomain",
},
{
programName,
"-p", "intro,r",
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
url: ParseURLOrPanic("https://somehost.somedomain"),
printIntro: true,
printProgress: false,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"--no-print",
"https://somehost.somedomain",
},
{
programName,
"-q",
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
url: ParseURLOrPanic("https://somehost.somedomain"),
printIntro: false,
printProgress: false,
printResult: false,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"--format", "plain-text",
"https://somehost.somedomain",
},
{
programName,
"--format", "pt",
"https://somehost.somedomain",
},
{
programName,
"--format=plain-text",
"https://somehost.somedomain",
},
{
programName,
"--format=pt",
"https://somehost.somedomain",
},
{
programName,
"-o", "plain-text",
"https://somehost.somedomain",
},
{
programName,
"-o", "pt",
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
url: ParseURLOrPanic("https://somehost.somedomain"),
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
},
},
{
[][]string{
{
programName,
"--format", "json",
"https://somehost.somedomain",
},
{
programName,
"--format", "j",
"https://somehost.somedomain",
},
{
programName,
"--format=json",
"https://somehost.somedomain",
},
{
programName,
"--format=j",
"https://somehost.somedomain",
},
{
programName,
"-o", "json",
"https://somehost.somedomain",
},
{
programName,
"-o", "j",
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
url: ParseURLOrPanic("https://somehost.somedomain"),
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("json"),
},
},
{
[][]string{
{
programName,
"--format", "path:/path/to/tmpl.txt",
"https://somehost.somedomain",
},
{
programName,
"--format=path:/path/to/tmpl.txt",
"https://somehost.somedomain",
},
{
programName,
"-o", "path:/path/to/tmpl.txt",
"https://somehost.somedomain",
},
},
config{
numConns: defaultNumberOfConns,
timeout: defaultTimeout,
headers: new(headersList),
method: "GET",
url: ParseURLOrPanic("https://somehost.somedomain"),
printIntro: true,
printProgress: true,
printResult: true,
format: userDefinedTemplate("/path/to/tmpl.txt"),
},
},
}
for _, e := range expectations {
for _, args := range e.in {
p := newKingpinParser()
cfg, err := p.parse(args)
if err != nil {
t.Error(err)
continue
}
if !reflect.DeepEqual(cfg, e.out) {
t.Logf("Expected: %#v", e.out)
t.Logf("Got: %#v", cfg)
t.Fail()
}
}
}
}
func TestParsePrintSpec(t *testing.T) {
exps := []struct {
spec string
results [3]bool
err error
}{
{
"",
[3]bool{},
errEmptyPrintSpec,
},
{
"a,b,c",
[3]bool{},
fmt.Errorf("%q is not a valid part of print spec", "a"),
},
{
"i,p,r,i",
[3]bool{},
fmt.Errorf(
"spec %q has too many parts, at most 3 are allowed", "i,p,r,i",
),
},
{
"i",
[3]bool{true, false, false},
nil,
},
{
"p",
[3]bool{false, true, false},
nil,
},
{
"r",
[3]bool{false, false, true},
nil,
},
{
"i,p,r",
[3]bool{true, true, true},
nil,
},
}
for _, e := range exps {
var (
act = [3]bool{}
err error
)
act[0], act[1], act[2], err = parsePrintSpec(e.spec)
if !reflect.DeepEqual(err, e.err) {
t.Errorf("For %q, expected err = %q, but got %q",
e.spec, e.err, err,
)
continue
}
if !reflect.DeepEqual(e.results, act) {
t.Errorf("For %q, expected result = %+v, but got %+v",
e.spec, e.results, act,
)
}
}
}
func TestArgsParsingWithEmptyPrintSpec(t *testing.T) {
p := newKingpinParser()
c, err := p.parse(
[]string{programName, "--print=", "somehost.somedomain"})
if err == nil {
t.Fail()
}
if c != emptyConf {
t.Fail()
}
}
func TestArgsParsingWithInvalidPrintSpec(t *testing.T) {
invalidSpecs := [][]string{
{programName, "--format", "noprefix.txt", "somehost.somedomain"},
{programName, "--format=noprefix.txt", "somehost.somedomain"},
{programName, "-o", "noprefix.txt", "somehost.somedomain"},
{programName, "--format", "unknown-format", "somehost.somedomain"},
{programName, "--format=unknown-format", "somehost.somedomain"},
{programName, "-o", "unknown-format", "somehost.somedomain"},
}
p := newKingpinParser()
for _, is := range invalidSpecs {
c, err := p.parse(is)
if err == nil || c != emptyConf {
t.Errorf("invalid print spec %q parsed correctly", is)
}
}
}
func TestEmbeddedURLParsing(t *testing.T) {
p := newKingpinParser()
url := "http://127.0.0.1:8080/to?url=http://10.100.99.41:38667"
c, err := p.parse([]string{programName, url})
if err != nil {
t.Error(err)
}
if c.url.String() != url {
t.Errorf("got %q, wanted %q", c.url, url)
}
}
================================================
FILE: bombardier.go
================================================
package main
import (
"fmt"
"io"
"io/ioutil"
"os"
"os/signal"
"strings"
"sync"
"sync/atomic"
"text/template"
"time"
"github.com/codesenberg/bombardier/internal"
"github.com/cheggaaa/pb"
fhist "github.com/codesenberg/concurrent/float64/histogram"
uhist "github.com/codesenberg/concurrent/uint64/histogram"
uuid "github.com/satori/go.uuid"
)
type bombardier struct {
bytesRead, bytesWritten int64
// HTTP codes
req1xx uint64
req2xx uint64
req3xx uint64
req4xx uint64
req5xx uint64
others uint64
conf config
barrier completionBarrier
ratelimiter limiter
wg sync.WaitGroup
timeTaken time.Duration
latencies *uhist.Histogram
requests *fhist.Histogram
client client
doneChan chan struct{}
// RPS metrics
rpl sync.Mutex
reqs int64
start time.Time
// Errors
errors *errorMap
// Progress bar
bar *pb.ProgressBar
// Output
out io.Writer
template *template.Template
}
func newBombardier(c config) (*bombardier, error) {
if err := c.checkArgs(); err != nil {
return nil, err
}
b := new(bombardier)
b.conf = c
b.latencies = uhist.Default()
b.requests = fhist.Default()
if b.conf.testType() == counted {
b.bar = pb.New64(int64(*b.conf.numReqs))
b.bar.ShowSpeed = true
} else if b.conf.testType() == timed {
b.bar = pb.New64(b.conf.duration.Nanoseconds() / 1e9)
b.bar.ShowCounters = false
b.bar.ShowPercent = false
}
b.bar.ManualUpdate = true
if b.conf.testType() == counted {
b.barrier = newCountingCompletionBarrier(*b.conf.numReqs)
} else {
b.barrier = newTimedCompletionBarrier(*b.conf.duration)
}
if b.conf.rate != nil {
b.ratelimiter = newBucketLimiter(*b.conf.rate)
} else {
b.ratelimiter = &nooplimiter{}
}
b.out = os.Stdout
tlsConfig, err := generateTLSConfig(c)
if err != nil {
return nil, err
}
var (
pbody *string
bsp bodyStreamProducer
)
if c.stream {
if c.bodyFilePath != "" {
bsp = func() (io.ReadCloser, error) {
return os.Open(c.bodyFilePath)
}
} else {
bsp = func() (io.ReadCloser, error) {
return ioutil.NopCloser(
proxyReader{strings.NewReader(c.body)},
), nil
}
}
} else {
pbody = &c.body
if c.bodyFilePath != "" {
var bodyBytes []byte
bodyBytes, err = ioutil.ReadFile(c.bodyFilePath)
if err != nil {
return nil, err
}
sbody := string(bodyBytes)
pbody = &sbody
}
}
cc := &clientOpts{
HTTP2: false,
maxConns: c.numConns,
timeout: c.timeout,
tlsConfig: tlsConfig,
disableKeepAlives: c.disableKeepAlives,
headers: c.headers,
requestURL: c.url,
method: c.method,
body: pbody,
bodProd: bsp,
bytesRead: &b.bytesRead,
bytesWritten: &b.bytesWritten,
}
b.client = makeHTTPClient(c.clientType, cc)
if !b.conf.printProgress {
b.bar.Output = ioutil.Discard
b.bar.NotPrint = true
}
b.template, err = b.prepareTemplate()
if err != nil {
return nil, err
}
b.wg.Add(int(c.numConns))
b.errors = newErrorMap()
b.doneChan = make(chan struct{}, 2)
return b, nil
}
func makeHTTPClient(clientType clientTyp, cc *clientOpts) client {
var cl client
switch clientType {
case nhttp1:
cl = newHTTPClient(cc)
case nhttp2:
cc.HTTP2 = true
cl = newHTTPClient(cc)
case fhttp:
fallthrough
default:
cl = newFastHTTPClient(cc)
}
return cl
}
func (b *bombardier) prepareTemplate() (*template.Template, error) {
var (
templateBytes []byte
err error
)
switch f := b.conf.format.(type) {
case knownFormat:
templateBytes = f.template()
case userDefinedTemplate:
templateBytes, err = ioutil.ReadFile(string(f))
if err != nil {
return nil, err
}
default:
panic("format can't be nil at this point, this is a bug")
}
outputTemplate, err := template.New("output-template").
Funcs(template.FuncMap{
"WithLatencies": func() bool {
return b.conf.printLatencies
},
"FormatBinary": formatBinary,
"FormatTimeUs": formatTimeUs,
"FormatTimeUsUint64": func(us uint64) string {
return formatTimeUs(float64(us))
},
"FloatsToArray": func(ps ...float64) []float64 {
return ps
},
"Multiply": func(num, coeff float64) float64 {
return num * coeff
},
"StringToBytes": func(s string) []byte {
return []byte(s)
},
"UUIDV1": uuid.NewV1,
"UUIDV2": uuid.NewV2,
"UUIDV3": uuid.NewV3,
"UUIDV4": uuid.NewV4,
"UUIDV5": uuid.NewV5,
}).Parse(string(templateBytes))
if err != nil {
return nil, err
}
return outputTemplate, nil
}
func (b *bombardier) writeStatistics(
code int, usTaken uint64,
) {
b.latencies.Increment(usTaken)
b.rpl.Lock()
b.reqs++
b.rpl.Unlock()
var counter *uint64
switch code / 100 {
case 1:
counter = &b.req1xx
case 2:
counter = &b.req2xx
case 3:
counter = &b.req3xx
case 4:
counter = &b.req4xx
case 5:
counter = &b.req5xx
default:
counter = &b.others
}
atomic.AddUint64(counter, 1)
}
func (b *bombardier) performSingleRequest() {
code, usTaken, err := b.client.do()
if err != nil {
b.errors.add(err)
}
b.writeStatistics(code, usTaken)
}
func (b *bombardier) worker() {
done := b.barrier.done()
for b.barrier.tryGrabWork() {
if b.ratelimiter.pace(done) == brk {
break
}
b.performSingleRequest()
b.barrier.jobDone()
}
}
func (b *bombardier) barUpdater() {
done := b.barrier.done()
for {
select {
case <-done:
b.bar.Set64(b.bar.Total)
b.bar.Update()
b.bar.Finish()
if b.conf.printProgress {
fmt.Fprintln(b.out, "Done!")
}
b.doneChan <- struct{}{}
return
default:
current := int64(b.barrier.completed() * float64(b.bar.Total))
b.bar.Set64(current)
b.bar.Update()
time.Sleep(b.bar.RefreshRate)
}
}
}
func (b *bombardier) rateMeter() {
requestsInterval := 10 * time.Millisecond
if b.conf.rate != nil {
requestsInterval, _ = estimate(*b.conf.rate, rateLimitInterval)
}
requestsInterval += 10 * time.Millisecond
ticker := time.NewTicker(requestsInterval)
defer ticker.Stop()
done := b.barrier.done()
for {
select {
case <-ticker.C:
b.recordRps()
continue
case <-done:
b.wg.Wait()
b.recordRps()
b.doneChan <- struct{}{}
return
}
}
}
func (b *bombardier) recordRps() {
b.rpl.Lock()
duration := time.Since(b.start)
reqs := b.reqs
b.reqs = 0
b.start = time.Now()
b.rpl.Unlock()
reqsf := float64(reqs) / duration.Seconds()
b.requests.Increment(reqsf)
}
func (b *bombardier) bombard() {
if b.conf.printIntro {
b.printIntro()
}
b.bar.Start()
bombardmentBegin := time.Now()
b.start = time.Now()
for i := uint64(0); i < b.conf.numConns; i++ {
go func() {
defer b.wg.Done()
b.worker()
}()
}
go b.rateMeter()
go b.barUpdater()
b.wg.Wait()
b.timeTaken = time.Since(bombardmentBegin)
<-b.doneChan
<-b.doneChan
}
func (b *bombardier) printIntro() {
if b.conf.testType() == counted {
fmt.Fprintf(b.out,
"Bombarding %v with %v request(s) using %v connection(s)\n",
b.conf.url, *b.conf.numReqs, b.conf.numConns)
} else if b.conf.testType() == timed {
fmt.Fprintf(b.out, "Bombarding %v for %v using %v connection(s)\n",
b.conf.url, *b.conf.duration, b.conf.numConns)
}
}
func (b *bombardier) gatherInfo() internal.TestInfo {
info := internal.TestInfo{
Spec: internal.Spec{
NumberOfConnections: b.conf.numConns,
Method: b.conf.method,
URL: b.conf.url,
Body: b.conf.body,
BodyFilePath: b.conf.bodyFilePath,
CertPath: b.conf.certPath,
KeyPath: b.conf.keyPath,
Stream: b.conf.stream,
Timeout: b.conf.timeout,
ClientType: internal.ClientType(b.conf.clientType),
Rate: b.conf.rate,
},
Result: internal.Results{
BytesRead: b.bytesRead,
BytesWritten: b.bytesWritten,
TimeTaken: b.timeTaken,
Req1XX: b.req1xx,
Req2XX: b.req2xx,
Req3XX: b.req3xx,
Req4XX: b.req4xx,
Req5XX: b.req5xx,
Others: b.others,
Latencies: b.latencies,
Requests: b.requests,
},
}
testType := b.conf.testType()
info.Spec.TestType = internal.TestType(testType)
if testType == timed {
info.Spec.TestDuration = *b.conf.duration
} else if testType == counted {
info.Spec.NumberOfRequests = *b.conf.numReqs
}
if b.conf.headers != nil {
for _, h := range *b.conf.headers {
info.Spec.Headers = append(info.Spec.Headers,
internal.Header{
Key: h.key,
Value: h.value,
})
}
}
for _, ewc := range b.errors.byFrequency() {
info.Result.Errors = append(info.Result.Errors,
internal.ErrorWithCount{
Error: ewc.error,
Count: ewc.count,
})
}
return info
}
func (b *bombardier) printStats() {
info := b.gatherInfo()
err := b.template.Execute(b.out, info)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
}
func (b *bombardier) redirectOutputTo(out io.Writer) {
b.bar.Output = out
b.out = out
}
func (b *bombardier) disableOutput() {
b.redirectOutputTo(ioutil.Discard)
b.bar.NotPrint = true
}
func main() {
cfg, err := parser.parse(os.Args)
if err != nil {
fmt.Println("Error parsing the arguments:", err)
os.Exit(exitFailure)
}
bombardier, err := newBombardier(cfg)
if err != nil {
fmt.Println("Error initializing bombardier:", err)
os.Exit(exitFailure)
}
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
<-c
bombardier.barrier.cancel()
}()
bombardier.bombard()
if bombardier.conf.printResult {
bombardier.printStats()
}
}
================================================
FILE: bombardier_performance_test.go
================================================
package main
import (
"flag"
"runtime"
"testing"
"time"
)
var (
serverPort = flag.String("port", "8080", "port to use for benchmarks")
clientType = flag.String("client-type", "fasthttp",
"client to use in benchmarks")
)
var (
longDuration = 9001 * time.Hour
highRate = uint64(1000000)
)
func BenchmarkBombardierSingleReqPerf(b *testing.B) {
addr := "localhost:" + *serverPort
benchmarkFireRequest(config{
numConns: defaultNumberOfConns,
numReqs: nil,
duration: &longDuration,
url: ParseURLOrPanic("http://" + addr),
headers: new(headersList),
timeout: defaultTimeout,
method: "GET",
body: "",
printLatencies: false,
clientType: clientTypeFromString(*clientType),
format: knownFormat("json"),
}, b)
}
func BenchmarkBombardierRateLimitPerf(b *testing.B) {
addr := "localhost:" + *serverPort
benchmarkFireRequest(config{
numConns: defaultNumberOfConns,
numReqs: nil,
duration: &longDuration,
url: ParseURLOrPanic("http://" + addr),
headers: new(headersList),
timeout: defaultTimeout,
method: "GET",
body: "",
printLatencies: false,
rate: &highRate,
clientType: clientTypeFromString(*clientType),
format: knownFormat("json"),
}, b)
}
func benchmarkFireRequest(c config, bm *testing.B) {
b, e := newBombardier(c)
if e != nil {
bm.Error(e)
}
b.disableOutput()
bm.SetParallelism(int(defaultNumberOfConns) / runtime.NumCPU())
bm.ResetTimer()
bm.RunParallel(func(pb *testing.PB) {
done := b.barrier.done()
for pb.Next() {
b.ratelimiter.pace(done)
b.performSingleRequest()
}
})
}
================================================
FILE: bombardier_test.go
================================================
package main
import (
"bytes"
"container/ring"
"crypto/tls"
"crypto/x509"
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"reflect"
"sync"
"sync/atomic"
"testing"
"time"
)
func TestBombardierShouldFireSpecifiedNumberOfRequests(t *testing.T) {
testAllClients(t, testBombardierShouldFireSpecifiedNumberOfRequests)
}
func testBombardierShouldFireSpecifiedNumberOfRequests(
clientType clientTyp, t *testing.T,
) {
reqsReceived := uint64(0)
s := httptest.NewServer(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
atomic.AddUint64(&reqsReceived, 1)
}),
)
defer s.Close()
numReqs := uint64(100)
noHeaders := new(headersList)
b, e := newBombardier(config{
numConns: defaultNumberOfConns,
numReqs: &numReqs,
url: ParseURLOrPanic(s.URL),
headers: noHeaders,
timeout: defaultTimeout,
method: "GET",
body: "",
clientType: clientType,
format: knownFormat("plain-text"),
})
if e != nil {
t.Error(e)
}
b.disableOutput()
b.bombard()
if reqsReceived != numReqs {
t.Fail()
}
}
func TestBombardierShouldFinish(t *testing.T) {
testAllClients(t, testBombardierShouldFinish)
}
func testBombardierShouldFinish(clientType clientTyp, t *testing.T) {
reqsReceived := uint64(0)
s := httptest.NewServer(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
atomic.AddUint64(&reqsReceived, 1)
}),
)
defer s.Close()
noHeaders := new(headersList)
desiredTestDuration := 1 * time.Second
b, e := newBombardier(config{
numConns: defaultNumberOfConns,
duration: &desiredTestDuration,
url: ParseURLOrPanic(s.URL),
headers: noHeaders,
timeout: defaultTimeout,
method: "GET",
body: "",
clientType: clientType,
format: knownFormat("plain-text"),
})
if e != nil {
t.Error(e)
}
b.disableOutput()
waitCh := make(chan struct{})
go func() {
b.bombard()
waitCh <- struct{}{}
}()
select {
case <-waitCh:
// Do nothing here
case <-time.After(desiredTestDuration + 5*time.Second):
t.Fail()
}
if reqsReceived == 0 {
t.Fail()
}
}
func TestBombardierShouldSendHeaders(t *testing.T) {
testAllClients(t, testBombardierShouldSendHeaders)
}
func testBombardierShouldSendHeaders(clientType clientTyp, t *testing.T) {
requestHeaders := headersList([]header{
{"Header1", "Value1"},
{"Header-Two", "value-two"},
})
// It's a bit hacky, but FastHTTP can't send Host header correctly
// as of now
if clientType != fhttp {
requestHeaders = append(requestHeaders, header{"Host", "web"})
}
s := httptest.NewServer(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
for _, h := range requestHeaders {
av := r.Header.Get(h.key)
if h.key == "Host" {
av = r.Host
}
if av != h.value {
t.Logf("%q <-> %q", av, h.value)
t.Fail()
}
}
}),
)
defer s.Close()
numReqs := uint64(1)
b, e := newBombardier(config{
numConns: defaultNumberOfConns,
numReqs: &numReqs,
url: ParseURLOrPanic(s.URL),
headers: &requestHeaders,
timeout: defaultTimeout,
method: "GET",
body: "",
clientType: clientType,
format: knownFormat("plain-text"),
})
if e != nil {
t.Error(e)
}
b.disableOutput()
b.bombard()
}
func TestBombardierHTTPCodeRecording(t *testing.T) {
testAllClients(t, testBombardierHTTPCodeRecording)
}
func testBombardierHTTPCodeRecording(clientType clientTyp, t *testing.T) {
cs := []int{200, 302, 404, 505, 606, 707}
codes := ring.New(len(cs))
for _, v := range cs {
codes.Value = v
codes = codes.Next()
}
codes = codes.Next()
var m sync.Mutex
s := httptest.NewServer(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
m.Lock()
nextCode := codes.Value.(int)
codes = codes.Next()
m.Unlock()
if nextCode/100 == 3 {
rw.Header().Set("Location", "http://localhost:666")
}
rw.WriteHeader(nextCode)
}),
)
defer s.Close()
eachCodeCount := uint64(10)
numReqs := uint64(len(cs)) * eachCodeCount
b, e := newBombardier(config{
numConns: defaultNumberOfConns,
numReqs: &numReqs,
url: ParseURLOrPanic(s.URL),
headers: new(headersList),
timeout: defaultTimeout,
method: "GET",
body: "",
clientType: clientType,
format: knownFormat("plain-text"),
})
if e != nil {
t.Error(e)
}
b.disableOutput()
b.bombard()
expectation := []struct {
name string
reqsGot uint64
expected uint64
}{
{"errored", b.others, eachCodeCount * 2},
{"2xx", b.req2xx, eachCodeCount},
{"3xx", b.req3xx, eachCodeCount},
{"4xx", b.req4xx, eachCodeCount},
{"5xx", b.req5xx, eachCodeCount},
}
for _, e := range expectation {
if e.reqsGot != e.expected {
t.Error(e.name, e.reqsGot, e.expected)
}
}
t.Logf("%+v", b.errors.byFrequency())
}
func TestBombardierTimeoutRecoding(t *testing.T) {
testAllClients(t, testBombardierTimeoutRecoding)
}
func testBombardierTimeoutRecoding(clientType clientTyp, t *testing.T) {
shortTimeout := 10 * time.Millisecond
s := httptest.NewServer(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
time.Sleep(shortTimeout * 10)
}),
)
defer s.Close()
numReqs := uint64(10)
b, e := newBombardier(config{
numConns: defaultNumberOfConns,
numReqs: &numReqs,
duration: nil,
url: ParseURLOrPanic(s.URL),
headers: new(headersList),
timeout: shortTimeout,
method: "GET",
body: "",
clientType: clientType,
format: knownFormat("plain-text"),
})
if e != nil {
t.Error(e)
}
b.disableOutput()
b.bombard()
if b.errors.sum() != numReqs {
t.Fail()
}
}
func TestBombardierThroughputRecording(t *testing.T) {
testAllClients(t, testBombardierThroughputRecording)
}
func testBombardierThroughputRecording(clientType clientTyp, t *testing.T) {
responseSize := 1024
response := bytes.Repeat([]byte{'a'}, responseSize)
s := httptest.NewServer(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
_, err := rw.Write(response)
if err != nil {
t.Error(err)
}
}),
)
defer s.Close()
numReqs := uint64(10)
b, e := newBombardier(config{
numConns: defaultNumberOfConns,
numReqs: &numReqs,
url: ParseURLOrPanic(s.URL),
headers: new(headersList),
timeout: defaultTimeout,
method: "GET",
body: "",
clientType: clientType,
format: knownFormat("plain-text"),
})
if e != nil {
t.Error(e)
}
b.disableOutput()
b.bombard()
if b.bytesRead == 0 || b.bytesWritten == 0 {
t.Error(b.bytesRead, b.bytesWritten)
}
}
func TestBombardierStatsPrinting(t *testing.T) {
responseSize := 1024
response := bytes.Repeat([]byte{'a'}, responseSize)
s := httptest.NewServer(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
_, err := rw.Write(response)
if err != nil {
t.Error(err)
}
}),
)
defer s.Close()
numReqs := uint64(10)
b, e := newBombardier(config{
numConns: defaultNumberOfConns,
numReqs: &numReqs,
url: ParseURLOrPanic(s.URL),
headers: new(headersList),
timeout: defaultTimeout,
method: "GET",
body: "",
printLatencies: true,
printIntro: true,
printProgress: true,
printResult: true,
format: knownFormat("plain-text"),
})
if e != nil {
t.Error(e)
return
}
dummy := errors.New("dummy error")
b.errors.add(dummy)
out := new(bytes.Buffer)
b.redirectOutputTo(out)
b.bombard()
b.printStats()
l := out.Len()
// Here we only test if anything is written
if l == 0 {
t.Fail()
}
}
func TestBombardierErrorIfFailToReadClientCert(t *testing.T) {
numReqs := uint64(10)
_, e := newBombardier(config{
numConns: defaultNumberOfConns,
numReqs: &numReqs,
url: ParseURLOrPanic("http://localhost"),
headers: new(headersList),
timeout: defaultTimeout,
method: "GET",
body: "",
printLatencies: true,
certPath: "certPath",
keyPath: "keyPath",
format: knownFormat("plain-text"),
})
if e == nil {
t.Fail()
}
}
func TestBombardierClientCerts(t *testing.T) {
testAllClients(t, testBombardierClientCerts)
}
func testBombardierClientCerts(clientType clientTyp, t *testing.T) {
clientCert, err := tls.LoadX509KeyPair("testclient.cert", "testclient.key")
if err != nil {
t.Error(err)
return
}
clientX509Cert, err := x509.ParseCertificate(clientCert.Certificate[0])
if err != nil {
t.Error(err)
return
}
servertCert, err := tls.LoadX509KeyPair("testserver.cert", "testserver.key")
if err != nil {
t.Error(err)
return
}
tlsConfig := &tls.Config{
ClientAuth: tls.RequireAnyClientCert,
Certificates: []tls.Certificate{servertCert},
}
server := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
certs := r.TLS.PeerCertificates
if numCerts := len(certs); numCerts != 1 {
t.Errorf("expected 1 cert, but got %v", numCerts)
rw.WriteHeader(http.StatusBadRequest)
return
}
cert := certs[0]
if !cert.Equal(clientX509Cert) {
t.Error("certificates don't match")
rw.WriteHeader(http.StatusBadRequest)
return
}
rw.WriteHeader(http.StatusOK)
}))
server.TLS = tlsConfig
server.StartTLS()
singleRequest := uint64(1)
b, e := newBombardier(config{
numConns: defaultNumberOfConns,
numReqs: &singleRequest,
url: ParseURLOrPanic(server.URL),
headers: new(headersList),
timeout: defaultTimeout,
method: "GET",
body: "",
printLatencies: true,
certPath: "testclient.cert",
keyPath: "testclient.key",
insecure: true,
clientType: clientType,
format: knownFormat("plain-text"),
})
if e != nil {
t.Error(e)
return
}
b.disableOutput()
b.bombard()
if b.req2xx != 1 {
t.Error("no 2xx responses, total =", b.reqs, ", 1xx/2xx/3xx/4xx/5xx =", b.req1xx, b.req2xx, b.req3xx, b.req4xx, b.req5xx)
}
server.Close()
}
func TestBombardierRateLimiting(t *testing.T) {
testAllClients(t, testBombardierRateLimiting)
}
func testBombardierRateLimiting(clientType clientTyp, t *testing.T) {
responseSize := 1024
response := bytes.Repeat([]byte{'a'}, responseSize)
s := httptest.NewServer(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
_, err := rw.Write(response)
if err != nil {
t.Error(err)
}
}),
)
defer s.Close()
rate := uint64(5000)
testDuration := 1 * time.Second
b, e := newBombardier(config{
numConns: defaultNumberOfConns,
duration: &testDuration,
url: ParseURLOrPanic(s.URL),
headers: new(headersList),
timeout: defaultTimeout,
method: "GET",
body: "",
rate: &rate,
clientType: clientType,
format: knownFormat("plain-text"),
})
if e != nil {
t.Error(e)
return
}
b.disableOutput()
b.bombard()
if float64(b.req2xx) < float64(rate)*0.75 ||
float64(b.req2xx) > float64(rate)*1.25 {
t.Error(rate, b.req2xx)
}
}
func testAllClients(parent *testing.T, testFun func(clientTyp, *testing.T)) {
clients := []clientTyp{fhttp, nhttp1, nhttp2}
for _, ct := range clients {
parent.Run(ct.String(), func(t *testing.T) {
testFun(ct, t)
})
}
}
func TestBombardierSendsBody(t *testing.T) {
testAllClients(t, testBombardierSendsBody)
}
func testBombardierSendsBody(clientType clientTyp, t *testing.T) {
response := []byte("OK")
requestBody := "abracadabra"
s := httptest.NewServer(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Error(err)
return
}
if string(body) != requestBody {
t.Errorf("Expected %v, but got %v", requestBody, string(body))
}
_, err = rw.Write(response)
if err != nil {
t.Error(err)
}
}),
)
defer s.Close()
one := uint64(1)
b, e := newBombardier(config{
numConns: defaultNumberOfConns,
numReqs: &one,
url: ParseURLOrPanic(s.URL),
headers: new(headersList),
timeout: defaultTimeout,
method: "POST",
body: requestBody,
clientType: clientType,
format: knownFormat("plain-text"),
})
if e != nil {
t.Error(e)
return
}
b.disableOutput()
b.bombard()
}
func TestBombardierSendsBodyFromFile(t *testing.T) {
testAllClients(t, testBombardierSendsBodyFromFile)
}
func testBombardierSendsBodyFromFile(clientType clientTyp, t *testing.T) {
response := []byte("OK")
bodyPath := "testbody.txt"
requestBody, err := ioutil.ReadFile(bodyPath)
if err != nil {
t.Error(err)
return
}
s := httptest.NewServer(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Error(err)
return
}
if string(body) != string(requestBody) {
t.Errorf("Expected %v, but got %v", string(requestBody), string(body))
}
_, err = rw.Write(response)
if err != nil {
t.Error(err)
}
}),
)
defer s.Close()
one := uint64(1)
b, e := newBombardier(config{
numConns: defaultNumberOfConns,
numReqs: &one,
url: ParseURLOrPanic(s.URL),
headers: new(headersList),
timeout: defaultTimeout,
method: "POST",
bodyFilePath: bodyPath,
clientType: clientType,
format: knownFormat("plain-text"),
})
if e != nil {
t.Error(e)
return
}
b.disableOutput()
b.bombard()
}
func TestBombardierFileDoesntExist(t *testing.T) {
bodyPath := "/does/not/exist.forreal"
_, e := newBombardier(config{
numConns: defaultNumberOfConns,
url: ParseURLOrPanic("http://example.com"),
headers: new(headersList),
timeout: defaultTimeout,
method: "POST",
bodyFilePath: bodyPath,
format: knownFormat("plain-text"),
})
_, ok := e.(*os.PathError)
if !ok {
t.Errorf("Expected to get PathError, but got %v", e)
}
}
func TestBombardierStreamsBody(t *testing.T) {
testAllClients(t, testBombardierStreamsBody)
}
func testBombardierStreamsBody(clientType clientTyp, t *testing.T) {
response := []byte("OK")
requestBody := "abracadabra"
s := httptest.NewServer(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if te := r.TransferEncoding; !reflect.DeepEqual(te, []string{"chunked"}) {
t.Errorf("Expected chunked transfer encoding, but got %v", te)
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Error(err)
return
}
if string(body) != requestBody {
t.Errorf("Expected %v, but got %v", requestBody, string(body))
}
_, err = rw.Write(response)
if err != nil {
t.Error(err)
}
}),
)
defer s.Close()
one := uint64(1)
b, e := newBombardier(config{
numConns: defaultNumberOfConns,
numReqs: &one,
url: ParseURLOrPanic(s.URL),
headers: new(headersList),
timeout: defaultTimeout,
method: "POST",
body: requestBody,
stream: true,
clientType: clientType,
format: knownFormat("plain-text"),
})
if e != nil {
t.Error(e)
return
}
b.disableOutput()
b.bombard()
}
func TestBombardierStreamsBodyFromFile(t *testing.T) {
testAllClients(t, testBombardierStreamsBodyFromFile)
}
func testBombardierStreamsBodyFromFile(clientType clientTyp, t *testing.T) {
response := []byte("OK")
bodyPath := "testbody.txt"
requestBody, err := ioutil.ReadFile(bodyPath)
if err != nil {
t.Error(err)
return
}
s := httptest.NewServer(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if te := r.TransferEncoding; !reflect.DeepEqual(te, []string{"chunked"}) {
t.Errorf("Expected chunked transfer encoding, but got %v", te)
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Error(err)
return
}
if string(body) != string(requestBody) {
t.Errorf("Expected %v, but got %v", string(requestBody), string(body))
}
_, err = rw.Write(response)
if err != nil {
t.Error(err)
}
}),
)
defer s.Close()
one := uint64(1)
b, e := newBombardier(config{
numConns: defaultNumberOfConns,
numReqs: &one,
url: ParseURLOrPanic(s.URL),
headers: new(headersList),
timeout: defaultTimeout,
method: "POST",
bodyFilePath: bodyPath,
stream: true,
clientType: clientType,
format: knownFormat("plain-text"),
})
if e != nil {
t.Error(e)
return
}
b.disableOutput()
b.bombard()
}
func TestBombardierShouldSendCustomHostHeader(t *testing.T) {
testAllClients(t, testBombardierShouldSendCustomHostHeader)
}
func testBombardierShouldSendCustomHostHeader(
clientType clientTyp, t *testing.T,
) {
host := "custom-host"
s := httptest.NewServer(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.Host != host {
t.Errorf("Host must be %q, but it's %q", host, r.Host)
}
}),
)
defer s.Close()
numReqs := uint64(100)
headers := headersList([]header{
{"Host", host},
})
b, e := newBombardier(config{
numConns: defaultNumberOfConns,
numReqs: &numReqs,
url: ParseURLOrPanic(s.URL),
headers: &headers,
timeout: defaultTimeout,
method: "GET",
body: "",
clientType: clientType,
format: knownFormat("plain-text"),
})
if e != nil {
t.Error(e)
}
b.disableOutput()
b.bombard()
}
================================================
FILE: build.py
================================================
import argparse
import os
import subprocess
platforms = [
("darwin", "amd64"),
("darwin", "arm64"),
("freebsd", "386"),
("freebsd", "amd64"),
("freebsd", "arm"),
("linux", "386"),
("linux", "amd64"),
("linux", "arm"),
("linux", "arm64"),
("netbsd", "386"),
("netbsd", "amd64"),
("netbsd", "arm"),
("openbsd", "386"),
("openbsd", "amd64"),
("openbsd", "arm"),
("openbsd", "arm64"),
("windows", "386"),
("windows", "amd64"),
("windows", "arm64"),
]
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Auxilary build script.")
parser.add_argument("-v", "--version", default="unspecified",
type=str, help="string used as a version when building binaries")
args = parser.parse_args()
version = args.version
for (build_os, build_arch) in platforms:
ext = ""
if build_os == "windows":
ext = ".exe"
build_env = os.environ.copy()
build_env["GOOS"] = build_os
build_env["GOARCH"] = build_arch
subprocess.run(["go", "build", "-ldflags", "-s -w -X main.version=%s" %
version, "-o", "bombardier-%s-%s%s" % (build_os, build_arch, ext)], env=build_env)
================================================
FILE: client_cert.go
================================================
package main
import (
"crypto/tls"
)
// readClientCert - helper function to read client certificate
// from pem formatted certPath and keyPath files
func readClientCert(certPath, keyPath string) ([]tls.Certificate, error) {
// load keypair
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
return []tls.Certificate{cert}, err
}
// generateTLSConfig - helper function to generate a TLS configuration based on
// config
func generateTLSConfig(c config) (*tls.Config, error) {
var (
certs []tls.Certificate
err error
)
// This assumes that the caller has validated that either both or none of
// the c.certPath and c.keyPath are set.
if c.certPath != "" && c.keyPath != "" {
certs, err = readClientCert(c.certPath, c.keyPath)
if err != nil {
return nil, err
}
}
// Disable gas warning, because InsecureSkipVerify may be set to true
// for the purpose of testing
/* #nosec */
tlsConfig := &tls.Config{
InsecureSkipVerify: c.insecure,
Certificates: certs,
}
return tlsConfig, nil
}
================================================
FILE: client_cert_test.go
================================================
package main
import (
"testing"
)
func TestGenerateTLSConfig(t *testing.T) {
expectations := []struct {
certPath string
keyPath string
errIsNil bool
}{
{
certPath: "testclient.cert",
keyPath: "testclient.key",
errIsNil: true,
},
{
certPath: "doesnotexist.pem",
keyPath: "doesnotexist.pem",
errIsNil: false,
},
{
certPath: "",
keyPath: "",
errIsNil: true,
},
}
for _, e := range expectations {
_, r := generateTLSConfig(
config{
url: ParseURLOrPanic("https://doesnt.exist.com"),
certPath: e.certPath,
keyPath: e.keyPath,
},
)
if (r == nil) != e.errIsNil {
t.Error(e.certPath, e.keyPath, r)
}
}
}
================================================
FILE: clients.go
================================================
package main
import (
"crypto/tls"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/valyala/fasthttp"
)
type client interface {
do() (code int, usTaken uint64, err error)
}
type bodyStreamProducer func() (io.ReadCloser, error)
type clientOpts struct {
HTTP2 bool
maxConns uint64
timeout time.Duration
tlsConfig *tls.Config
disableKeepAlives bool
requestURL *url.URL
headers *headersList
method string
body *string
bodProd bodyStreamProducer
bytesRead, bytesWritten *int64
}
type fasthttpClient struct {
client *fasthttp.Client
headers *fasthttp.RequestHeader
uri *fasthttp.URI
method string
body *string
bodProd bodyStreamProducer
}
func newFastHTTPClient(opts *clientOpts) client {
c := new(fasthttpClient)
uri := fasthttp.AcquireURI()
if err := uri.Parse(
[]byte(opts.requestURL.Host),
[]byte(opts.requestURL.String()),
); err != nil {
// opts.requestURL must always be valid
panic(err)
}
c.uri = uri
c.client = &fasthttp.Client{
MaxConnsPerHost: int(opts.maxConns),
ReadTimeout: opts.timeout,
WriteTimeout: opts.timeout,
DisableHeaderNamesNormalizing: true,
TLSConfig: opts.tlsConfig,
Dial: fasthttpDialFunc(
opts.bytesRead, opts.bytesWritten,
opts.timeout,
),
}
c.headers = headersToFastHTTPHeaders(opts.headers)
c.method, c.body = opts.method, opts.body
c.bodProd = opts.bodProd
return client(c)
}
func (c *fasthttpClient) do() (
code int, usTaken uint64, err error,
) {
// prepare the request
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
if c.headers != nil {
c.headers.CopyTo(&req.Header)
}
req.Header.SetMethod(c.method)
req.SetURI(c.uri)
req.UseHostHeader = true
if c.body != nil {
req.SetBodyString(*c.body)
} else {
bs, bserr := c.bodProd()
if bserr != nil {
return 0, 0, bserr
}
req.SetBodyStream(bs, -1)
}
// fire the request
start := time.Now()
err = c.client.Do(req, resp)
if err != nil {
code = -1
} else {
code = resp.StatusCode()
}
usTaken = uint64(time.Since(start).Nanoseconds() / 1000)
// release resources
fasthttp.ReleaseRequest(req)
fasthttp.ReleaseResponse(resp)
return
}
type httpClient struct {
client *http.Client
headers http.Header
url *url.URL
method string
body *string
bodProd bodyStreamProducer
}
func newHTTPClient(opts *clientOpts) client {
c := new(httpClient)
tr := &http.Transport{
TLSClientConfig: opts.tlsConfig,
MaxIdleConnsPerHost: int(opts.maxConns),
DisableKeepAlives: opts.disableKeepAlives,
ForceAttemptHTTP2: opts.HTTP2,
DialContext: httpDialContextFunc(opts.bytesRead, opts.bytesWritten, opts.timeout),
}
cl := &http.Client{
Transport: tr,
Timeout: opts.timeout,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
c.client = cl
c.headers = headersToHTTPHeaders(opts.headers)
c.method, c.body, c.bodProd = opts.method, opts.body, opts.bodProd
c.url = opts.requestURL
return client(c)
}
func (c *httpClient) do() (
code int, usTaken uint64, err error,
) {
req := &http.Request{}
req.Header = c.headers
req.Method = c.method
req.URL = c.url
if host := req.Header.Get("Host"); host != "" {
req.Host = host
}
if c.body != nil {
br := strings.NewReader(*c.body)
req.ContentLength = int64(len(*c.body))
req.Body = ioutil.NopCloser(br)
} else {
bs, bserr := c.bodProd()
if bserr != nil {
return 0, 0, bserr
}
req.Body = bs
}
start := time.Now()
resp, err := c.client.Do(req)
if err != nil {
code = -1
} else {
code = resp.StatusCode
_, berr := io.Copy(ioutil.Discard, resp.Body)
if berr != nil {
err = berr
}
if cerr := resp.Body.Close(); cerr != nil {
err = cerr
}
}
usTaken = uint64(time.Since(start).Nanoseconds() / 1000)
return
}
func headersToFastHTTPHeaders(h *headersList) *fasthttp.RequestHeader {
if len(*h) == 0 {
return nil
}
res := new(fasthttp.RequestHeader)
for _, header := range *h {
res.Set(header.key, header.value)
}
return res
}
func headersToHTTPHeaders(h *headersList) http.Header {
if len(*h) == 0 {
return http.Header{}
}
headers := http.Header{}
for _, header := range *h {
headers[header.key] = []string{header.value}
}
return headers
}
================================================
FILE: clients_test.go
================================================
package main
import (
"bytes"
"crypto/tls"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"github.com/goware/urlx"
)
func TestShouldReturnNilIfNoHeadersWhereSet(t *testing.T) {
h := new(headersList)
if headersToFastHTTPHeaders(h) != nil {
t.Fail()
}
}
func TestShouldReturnEmptyHeadersIfNoHeaadersWhereSet(t *testing.T) {
h := new(headersList)
if len(headersToHTTPHeaders(h)) != 0 {
t.Fail()
}
}
func TestShouldProperlyConvertToHttpHeaders(t *testing.T) {
h := new(headersList)
for _, hs := range []string{
"Content-Type: application/json", "Custom-Header: xxx42xxx",
} {
if err := h.Set(hs); err != nil {
t.Error(err)
}
}
fh := headersToFastHTTPHeaders(h)
{
e, a := []byte("application/json"), fh.Peek("Content-Type")
if !bytes.Equal(e, a) {
t.Errorf("Expected %v, but got %v", e, a)
}
}
if e, a := []byte("xxx42xxx"), fh.Peek("Custom-Header"); !bytes.Equal(e, a) {
t.Errorf("Expected %v, but got %v", e, a)
}
nh := headersToHTTPHeaders(h)
{
e, a := "application/json", nh.Get("Content-Type")
if e != a {
t.Errorf("Expected %v, but got %v", e, a)
}
}
if e, a := "xxx42xxx", nh.Get("Custom-Header"); e != a {
t.Errorf("Expected %v, but got %v", e, a)
}
}
func TestHTTP2Client(t *testing.T) {
responseSize := 1024
response := bytes.Repeat([]byte{'a'}, responseSize)
s := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !r.ProtoAtLeast(2, 0) {
t.Errorf("invalid HTTP proto version: %v", r.Proto)
}
w.WriteHeader(http.StatusOK)
_, err := w.Write(response)
if err != nil {
t.Error(err)
}
}))
s.EnableHTTP2 = true
s.TLS = &tls.Config{
InsecureSkipVerify: true,
}
s.StartTLS()
defer s.Close()
bytesRead, bytesWritten := int64(0), int64(0)
requestURL, err := urlx.Parse(s.URL)
if err != nil {
t.Fatal(err)
}
c := newHTTPClient(&clientOpts{
HTTP2: true,
headers: new(headersList),
requestURL: requestURL,
method: "GET",
tlsConfig: &tls.Config{
InsecureSkipVerify: true,
},
body: new(string),
bytesRead: &bytesRead,
bytesWritten: &bytesWritten,
})
code, _, err := c.do()
if err != nil {
t.Error(err)
return
}
if code != http.StatusOK {
t.Errorf("invalid response code: %v", code)
}
if atomic.LoadInt64(&bytesRead) == 0 {
t.Errorf("invalid response size: %v", bytesRead)
}
if atomic.LoadInt64(&bytesWritten) == 0 {
t.Errorf("empty request of size: %v", bytesWritten)
}
}
func TestHTTP1Clients(t *testing.T) {
responseSize := 1024
response := bytes.Repeat([]byte{'a'}, responseSize)
s := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if r.ProtoMajor != 1 {
t.Errorf("invalid HTTP proto version: %v", r.Proto)
}
w.WriteHeader(http.StatusOK)
_, err := w.Write(response)
if err != nil {
t.Error(err)
}
},
))
defer s.Close()
bytesRead, bytesWritten := int64(0), int64(0)
requestURL, err := urlx.Parse(s.URL)
if err != nil {
t.Fatal(err)
}
cc := &clientOpts{
HTTP2: false,
headers: new(headersList),
requestURL: requestURL,
method: "GET",
body: new(string),
bytesRead: &bytesRead,
bytesWritten: &bytesWritten,
}
clients := []client{
newHTTPClient(cc),
newFastHTTPClient(cc),
}
for _, c := range clients {
bytesRead, bytesWritten = 0, 0
code, _, err := c.do()
if err != nil {
t.Error(err)
return
}
if code != http.StatusOK {
t.Errorf("invalid response code: %v", code)
}
if bytesRead == 0 {
t.Errorf("invalid response size: %v", bytesRead)
}
if bytesWritten == 0 {
t.Errorf("empty request of size: %v", bytesWritten)
}
}
}
================================================
FILE: cmd/utils/simplebenchserver/doc.go
================================================
/*
Simple HTTP server used for benchmarking.
Following options are available:
--help Show context-sensitive help (also try --help-long and
--help-man).
-p, --port="8080" port to use for benchmarks
-s, --size=1024 size of response in bytes
*/
package main
================================================
FILE: cmd/utils/simplebenchserver/main.go
================================================
package main
import (
"bytes"
"log"
"net/http"
"github.com/alecthomas/kingpin"
"github.com/valyala/fasthttp"
)
var serverPort = kingpin.Flag("port", "port to use for benchmarks").
Default("8080").
Short('p').
String()
var responseSize = kingpin.Flag("size", "size of response in bytes").
Default("1024").
Short('s').
Uint()
var stdHTTP = kingpin.Flag("std-http", "use standard http library").
Default("false").
Bool()
func main() {
kingpin.Parse()
response := bytes.Repeat([]byte("a"), int(*responseSize))
addr := "localhost:" + *serverPort
log.Println("Starting HTTP server on:", addr)
var lserr error
if *stdHTTP {
lserr = http.ListenAndServe(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, werr := w.Write(response)
if werr != nil {
log.Println(werr)
}
}))
} else {
lserr = fasthttp.ListenAndServe(addr, func(c *fasthttp.RequestCtx) {
_, werr := c.Write(response)
if werr != nil {
log.Println(werr)
}
})
}
if lserr != nil {
log.Println(lserr)
}
}
================================================
FILE: common.go
================================================
package main
import (
"errors"
"net/url"
"sort"
"time"
"github.com/goware/urlx"
)
const (
decBase = 10
rateLimitInterval = 10 * time.Millisecond
oneSecond = 1 * time.Second
exitFailure = 1
)
var (
version = "unspecified"
emptyConf = config{}
parser = newKingpinParser()
defaultTestDuration = 10 * time.Second
defaultNumberOfConns = uint64(125)
defaultTimeout = 2 * time.Second
httpMethods = []string{
"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS",
"PATCH",
}
cantHaveBody = []string{"HEAD"}
errUnsupportedScheme = errors.New("unsupported scheme")
errInvalidNumberOfConns = errors.New(
"invalid number of connections(must be > 0)")
errInvalidNumberOfRequests = errors.New(
"invalid number of requests(must be > 0)")
errInvalidTestDuration = errors.New(
"invalid test duration(must be >= 1s)")
errNegativeTimeout = errors.New(
"timeout can't be negative")
errBodyNotAllowed = errors.New(
"HEAD requests cannot have body")
errNoPathToCert = errors.New(
"no Path to TLS Client Certificate")
errNoPathToKey = errors.New(
"no Path to TLS Client Certificate Private Key")
errZeroRate = errors.New(
"rate can't be less than 1")
errBodyProvidedTwice = errors.New("use either --body or --body-file")
errInvalidHeaderFormat = errors.New("invalid header format")
errEmptyPrintSpec = errors.New(
"empty print spec is not a valid print spec")
)
func ParseURLOrPanic(s string) *url.URL {
u, err := urlx.Parse(s)
if err != nil {
panic(err)
}
return u
}
func init() {
sort.Strings(httpMethods)
sort.Strings(cantHaveBody)
}
================================================
FILE: completion_barriers.go
================================================
package main
import (
"sync"
"sync/atomic"
"time"
)
type completionBarrier interface {
completed() float64
tryGrabWork() bool
jobDone()
done() <-chan struct{}
cancel()
}
type countingCompletionBarrier struct {
numReqs, reqsGrabbed, reqsDone uint64
doneChan chan struct{}
closeOnce sync.Once
}
func newCountingCompletionBarrier(numReqs uint64) completionBarrier {
c := new(countingCompletionBarrier)
c.reqsDone, c.reqsGrabbed, c.numReqs = 0, 0, numReqs
c.doneChan = make(chan struct{})
return completionBarrier(c)
}
func (c *countingCompletionBarrier) tryGrabWork() bool {
select {
case <-c.doneChan:
return false
default:
reqsDone := atomic.AddUint64(&c.reqsGrabbed, 1)
return reqsDone <= c.numReqs
}
}
func (c *countingCompletionBarrier) jobDone() {
reqsDone := atomic.AddUint64(&c.reqsDone, 1)
if reqsDone == c.numReqs {
c.closeOnce.Do(func() {
close(c.doneChan)
})
}
}
func (c *countingCompletionBarrier) done() <-chan struct{} {
return c.doneChan
}
func (c *countingCompletionBarrier) cancel() {
c.closeOnce.Do(func() {
close(c.doneChan)
})
}
func (c *countingCompletionBarrier) completed() float64 {
select {
case <-c.doneChan:
return 1.0
default:
reqsDone := atomic.LoadUint64(&c.reqsDone)
return float64(reqsDone) / float64(c.numReqs)
}
}
type timedCompletionBarrier struct {
doneChan chan struct{}
closeOnce sync.Once
start time.Time
duration time.Duration
}
func newTimedCompletionBarrier(duration time.Duration) completionBarrier {
if duration < 0 {
panic("timedCompletionBarrier: negative duration")
}
c := new(timedCompletionBarrier)
c.doneChan = make(chan struct{})
c.start = time.Now()
c.duration = duration
go func() {
time.AfterFunc(duration, func() {
c.closeOnce.Do(func() {
close(c.doneChan)
})
})
}()
return completionBarrier(c)
}
func (c *timedCompletionBarrier) tryGrabWork() bool {
select {
case <-c.doneChan:
return false
default:
return true
}
}
func (c *timedCompletionBarrier) jobDone() {
}
func (c *timedCompletionBarrier) done() <-chan struct{} {
return c.doneChan
}
func (c *timedCompletionBarrier) cancel() {
c.closeOnce.Do(func() {
close(c.doneChan)
})
}
func (c *timedCompletionBarrier) completed() float64 {
select {
case <-c.doneChan:
return 1.0
default:
return float64(time.Since(c.start).Nanoseconds()) /
float64(c.duration.Nanoseconds())
}
}
================================================
FILE: completion_barriers_test.go
================================================
package main
import (
"math"
"testing"
"time"
)
func TestCouintingCompletionBarrierWait(t *testing.T) {
parties := uint64(10)
b := newCountingCompletionBarrier(1000)
for i := uint64(0); i < parties; i++ {
go func() {
for b.tryGrabWork() {
b.jobDone()
}
}()
}
wc := make(chan struct{})
go func() {
<-b.done()
wc <- struct{}{}
}()
select {
case <-wc:
return
case <-time.After(100 * time.Millisecond):
t.Fail()
}
}
func TestTimedCompletionBarrierWait(t *testing.T) {
parties := uint64(10)
duration := 100 * time.Millisecond
timeout := duration * 2
err := 15 * time.Millisecond
sleepDuration := 2 * time.Millisecond
b := newTimedCompletionBarrier(duration)
for i := uint64(0); i < parties; i++ {
go func() {
for b.tryGrabWork() {
time.Sleep(sleepDuration)
b.jobDone()
}
}()
}
wc := make(chan time.Duration)
go func() {
start := time.Now()
<-b.done()
wc <- time.Since(start)
}()
select {
case actual := <-wc:
if !approximatelyEqual(duration, actual, sleepDuration+err) {
t.Errorf("Expected to run %v, but ran %v instead", duration, actual)
}
case <-time.After(timeout):
t.Error("Barrier hanged")
}
}
func TestTimeBarrierCancel(t *testing.T) {
b := newTimedCompletionBarrier(9000 * time.Second)
sleepTime := 100 * time.Millisecond
go func() {
time.Sleep(sleepTime)
b.cancel()
}()
select {
case <-b.done():
if c := b.completed(); c != 1.0 {
t.Error(c)
}
case <-time.After(sleepTime * 2):
t.Fail()
}
}
func TestCountedBarrierCancel(t *testing.T) {
parties := uint64(10)
b := newCountingCompletionBarrier(math.MaxUint64)
sleepTime := 100 * time.Millisecond
for i := uint64(0); i < parties; i++ {
go func() {
for b.tryGrabWork() {
b.jobDone()
}
}()
}
go func() {
time.Sleep(sleepTime)
b.cancel()
}()
select {
case <-b.done():
if c := b.completed(); c != 1.0 {
t.Error(c)
}
case <-time.After(5 * time.Second):
t.Fail()
}
}
func TestTimeBarrierPanicOnBadDuration(t *testing.T) {
defer func() {
r := recover()
if r == nil {
t.Error("shouldn't be empty")
t.Fail()
}
}()
newTimedCompletionBarrier(-1 * time.Second)
t.Error("unreachable")
t.Fail()
}
func approximatelyEqual(expected, actual, err time.Duration) bool {
return expected-err < actual && actual < expected+err
}
================================================
FILE: config.go
================================================
package main
import (
"fmt"
"net/url"
"sort"
"time"
)
type config struct {
numConns uint64
numReqs *uint64
disableKeepAlives bool
duration *time.Duration
url *url.URL
method, certPath, keyPath string
body, bodyFilePath string
stream bool
headers *headersList
timeout time.Duration
// TODO(codesenberg): printLatencies should probably be
// re(named&maked) into printPercentiles or even let
// users provide their own percentiles and not just
// calculate for [0.5, 0.75, 0.9, 0.99]
printLatencies, insecure bool
rate *uint64
clientType clientTyp
printIntro, printProgress, printResult bool
format format
}
type testTyp int
const (
none testTyp = iota
timed
counted
)
type invalidHTTPMethodError struct {
method string
}
func (i *invalidHTTPMethodError) Error() string {
return fmt.Sprintf("Unknown HTTP method: %v", i.method)
}
func (c *config) checkArgs() error {
c.checkOrSetDefaultTestType()
checks := []func() error{
c.checkURL,
c.checkRate,
c.checkRunParameters,
c.checkTimeoutDuration,
c.checkHTTPParameters,
c.checkCertPaths,
}
for _, check := range checks {
if err := check(); err != nil {
return err
}
}
return nil
}
func (c *config) checkOrSetDefaultTestType() {
if c.testType() == none {
c.duration = &defaultTestDuration
}
}
func (c *config) testType() testTyp {
typ := none
if c.numReqs != nil {
typ = counted
} else if c.duration != nil {
typ = timed
}
return typ
}
func (c *config) checkURL() error {
if c.url.Scheme != "http" && c.url.Scheme != "https" {
return errUnsupportedScheme
}
return nil
}
func (c *config) checkRate() error {
if c.rate != nil && *c.rate < 1 {
return errZeroRate
}
return nil
}
func (c *config) checkRunParameters() error {
if c.numConns < uint64(1) {
return errInvalidNumberOfConns
}
if c.testType() == counted && *c.numReqs < uint64(1) {
return errInvalidNumberOfRequests
}
if c.testType() == timed && *c.duration < time.Second {
return errInvalidTestDuration
}
return nil
}
func (c *config) checkTimeoutDuration() error {
if c.timeout < 0 {
return errNegativeTimeout
}
return nil
}
func (c *config) checkHTTPParameters() error {
if !allowedHTTPMethod(c.method) {
return &invalidHTTPMethodError{method: c.method}
}
if !canHaveBody(c.method) && (c.body != "" || c.bodyFilePath != "") {
return errBodyNotAllowed
}
if c.body != "" && c.bodyFilePath != "" {
return errBodyProvidedTwice
}
return nil
}
func (c *config) checkCertPaths() error {
if c.certPath != "" && c.keyPath == "" {
return errNoPathToKey
} else if c.certPath == "" && c.keyPath != "" {
return errNoPathToCert
}
return nil
}
func (c *config) timeoutMillis() uint64 {
return uint64(c.timeout.Nanoseconds() / 1000)
}
func allowedHTTPMethod(method string) bool {
i := sort.SearchStrings(httpMethods, method)
return i < len(httpMethods) && httpMethods[i] == method
}
func canHaveBody(method string) bool {
i := sort.SearchStrings(cantHaveBody, method)
return !(i < len(cantHaveBody) && cantHaveBody[i] == method)
}
type clientTyp int
const (
fhttp clientTyp = iota
nhttp1
nhttp2
)
func (ct clientTyp) String() string {
switch ct {
case fhttp:
return "FastHTTP"
case nhttp1:
return "net/http v1.x"
case nhttp2:
return "net/http v2.0"
}
return "unknown client"
}
================================================
FILE: config_test.go
================================================
package main
import (
"testing"
"time"
)
var (
defaultNumberOfReqs = uint64(10000)
)
func TestCanHaveBody(t *testing.T) {
expectations := []struct {
in string
out bool
}{
{"HEAD", false},
{"GET", true},
{"POST", true},
{"PUT", true},
{"DELETE", true},
{"OPTIONS", true},
}
for _, e := range expectations {
if r := canHaveBody(e.in); r != e.out {
t.Error(e.in, e.out, r)
}
}
}
func TestAllowedHttpMethod(t *testing.T) {
expectations := []struct {
in string
out bool
}{
{"GET", true},
{"POST", true},
{"PUT", true},
{"DELETE", true},
{"HEAD", true},
{"OPTIONS", true},
{"TRUNCATE", false},
}
for _, e := range expectations {
if r := allowedHTTPMethod(e.in); r != e.out {
t.Logf("Expected f(%v) = %v, but got %v", e.in, e.out, r)
t.Fail()
}
}
}
func TestCheckArgs(t *testing.T) {
invalidNumberOfReqs := uint64(0)
smallTestDuration := 99 * time.Millisecond
negativeTimeoutDuration := -1 * time.Second
noHeaders := new(headersList)
zeroRate := uint64(0)
expectations := []struct {
in config
out error
}{
{
config{
numConns: 0,
numReqs: &defaultNumberOfReqs,
duration: &defaultTestDuration,
url: ParseURLOrPanic("http://localhost:8080"),
headers: noHeaders,
timeout: defaultTimeout,
method: "GET",
body: "",
format: knownFormat("plain-text"),
},
errInvalidNumberOfConns,
},
{
config{
numConns: defaultNumberOfConns,
numReqs: &invalidNumberOfReqs,
duration: &defaultTestDuration,
url: ParseURLOrPanic("http://localhost:8080"),
headers: noHeaders,
timeout: defaultTimeout,
method: "GET",
body: "",
format: knownFormat("plain-text"),
},
errInvalidNumberOfRequests,
},
{
config{
numConns: defaultNumberOfConns,
numReqs: nil,
duration: &smallTestDuration,
url: ParseURLOrPanic("http://localhost:8080"),
headers: noHeaders,
timeout: defaultTimeout,
method: "GET",
body: "",
format: knownFormat("plain-text"),
},
errInvalidTestDuration,
},
{
config{
numConns: defaultNumberOfConns,
numReqs: &defaultNumberOfReqs,
duration: &defaultTestDuration,
url: ParseURLOrPanic("http://localhost:8080"),
headers: noHeaders,
timeout: negativeTimeoutDuration,
method: "GET",
body: "",
format: knownFormat("plain-text"),
},
errNegativeTimeout,
},
{
config{
numConns: defaultNumberOfConns,
numReqs: &defaultNumberOfReqs,
duration: &defaultTestDuration,
url: ParseURLOrPanic("http://localhost:8080"),
headers: noHeaders,
timeout: defaultTimeout,
method: "HEAD",
body: "BODY",
format: knownFormat("plain-text"),
},
errBodyNotAllowed,
},
{
config{
numConns: defaultNumberOfConns,
numReqs: &defaultNumberOfReqs,
duration: &defaultTestDuration,
url: ParseURLOrPanic("http://localhost:8080"),
headers: noHeaders,
timeout: defaultTimeout,
method: "HEAD",
bodyFilePath: "testbody.txt",
format: knownFormat("plain-text"),
},
errBodyNotAllowed,
},
{
config{
numConns: defaultNumberOfConns,
numReqs: &defaultNumberOfReqs,
duration: &defaultTestDuration,
url: ParseURLOrPanic("http://localhost:8080"),
headers: noHeaders,
timeout: defaultTimeout,
method: "GET",
body: "BODY",
format: knownFormat("plain-text"),
},
nil,
},
{
config{
numConns: defaultNumberOfConns,
numReqs: &defaultNumberOfReqs,
duration: &defaultTestDuration,
url: ParseURLOrPanic("http://localhost:8080"),
headers: noHeaders,
timeout: defaultTimeout,
method: "GET",
bodyFilePath: "testbody.txt",
format: knownFormat("plain-text"),
},
nil,
},
{
config{
numConns: defaultNumberOfConns,
numReqs: &defaultNumberOfReqs,
duration: &defaultTestDuration,
url: ParseURLOrPanic("http://localhost:8080"),
headers: noHeaders,
timeout: defaultTimeout,
method: "GET",
body: "",
format: knownFormat("plain-text"),
},
nil,
},
{
config{
numConns: defaultNumberOfConns,
numReqs: &defaultNumberOfReqs,
duration: &defaultTestDuration,
url: ParseURLOrPanic("http://localhost:8080"),
headers: noHeaders,
timeout: defaultTimeout,
method: "GET",
body: "",
certPath: "test_cert.pem",
keyPath: "",
format: knownFormat("plain-text"),
},
errNoPathToKey,
},
{
config{
numConns: defaultNumberOfConns,
numReqs: &defaultNumberOfReqs,
duration: &defaultTestDuration,
url: ParseURLOrPanic("http://localhost:8080"),
headers: noHeaders,
timeout: defaultTimeout,
method: "GET",
body: "",
certPath: "",
keyPath: "test_key.pem",
format: knownFormat("plain-text"),
},
errNoPathToCert,
},
{
config{
numConns: defaultNumberOfConns,
numReqs: &defaultNumberOfReqs,
duration: &defaultTestDuration,
url: ParseURLOrPanic("http://localhost:8080"),
headers: noHeaders,
timeout: defaultTimeout,
method: "GET",
rate: &zeroRate,
format: knownFormat("plain-text"),
},
errZeroRate,
},
{
config{
numConns: defaultNumberOfConns,
numReqs: &defaultNumberOfReqs,
duration: &defaultTestDuration,
url: ParseURLOrPanic("http://localhost:8080"),
headers: noHeaders,
timeout: defaultTimeout,
method: "POST",
body: "abracadabra",
bodyFilePath: "testbody.txt",
format: knownFormat("plain-text"),
},
errBodyProvidedTwice,
},
}
for _, e := range expectations {
if r := e.in.checkArgs(); r != e.out {
t.Logf("Expected (%v).checkArgs to return %v, but got %v", e.in, e.out, r)
t.Fail()
}
if _, r := newBombardier(e.in); r != e.out {
t.Logf("Expected newBombardier(%v) to return %v, but got %v", e.in, e.out, r)
t.Fail()
}
}
}
func TestCheckArgsUnsupportedURLScheme(t *testing.T) {
c := config{
numConns: defaultNumberOfConns,
numReqs: &defaultNumberOfReqs,
duration: &defaultTestDuration,
url: ParseURLOrPanic("ftp://localhost:8080"),
headers: nil,
timeout: defaultTimeout,
method: "GET",
body: "",
}
if c.checkArgs() != errUnsupportedScheme {
t.Fail()
}
}
func TestCheckArgsInvalidRequestMethod(t *testing.T) {
c := config{
numConns: defaultNumberOfConns,
numReqs: &defaultNumberOfReqs,
duration: &defaultTestDuration,
url: ParseURLOrPanic("http://localhost:8080"),
headers: nil,
timeout: defaultTimeout,
method: "ABRACADABRA",
body: "",
}
e := c.checkArgs()
if e == nil {
t.Fail()
}
if _, ok := e.(*invalidHTTPMethodError); !ok {
t.Fail()
}
}
func TestCheckArgsTestType(t *testing.T) {
countedConfig := config{
numConns: defaultNumberOfConns,
numReqs: &defaultNumberOfReqs,
duration: nil,
url: ParseURLOrPanic("http://localhost:8080"),
headers: nil,
timeout: defaultTimeout,
method: "GET",
body: "",
}
timedConfig := config{
numConns: defaultNumberOfConns,
numReqs: nil,
duration: &defaultTestDuration,
url: ParseURLOrPanic("http://localhost:8080"),
headers: nil,
timeout: defaultTimeout,
method: "GET",
body: "",
}
both := config{
numConns: defaultNumberOfConns,
numReqs: &defaultNumberOfReqs,
duration: &defaultTestDuration,
url: ParseURLOrPanic("http://localhost:8080"),
headers: nil,
timeout: defaultTimeout,
method: "GET",
body: "",
}
defaultConfig := config{
numConns: defaultNumberOfConns,
numReqs: nil,
duration: nil,
url: ParseURLOrPanic("http://localhost:8080"),
headers: nil,
timeout: defaultTimeout,
method: "GET",
body: "",
}
if err := countedConfig.checkArgs(); err != nil ||
countedConfig.testType() != counted {
t.Fail()
}
if err := timedConfig.checkArgs(); err != nil ||
timedConfig.testType() != timed {
t.Fail()
}
if err := both.checkArgs(); err != nil ||
both.testType() != counted {
t.Fail()
}
if err := defaultConfig.checkArgs(); err != nil ||
defaultConfig.testType() != timed ||
defaultConfig.duration != &defaultTestDuration {
t.Fail()
}
}
func TestTimeoutMillis(t *testing.T) {
defaultConfig := config{
numConns: defaultNumberOfConns,
numReqs: nil,
duration: nil,
url: ParseURLOrPanic("http://localhost:8080"),
headers: nil,
timeout: 2 * time.Second,
method: "GET",
body: "",
}
if defaultConfig.timeoutMillis() != 2000000 {
t.Fail()
}
}
func TestInvalidHTTPMethodError(t *testing.T) {
invalidMethod := "NOSUCHMETHOD"
want := "Unknown HTTP method: " + invalidMethod
err := &invalidHTTPMethodError{invalidMethod}
if got := err.Error(); got != want {
t.Error(got, want)
}
}
func TestClientTypToStringConversion(t *testing.T) {
expectations := []struct {
in clientTyp
out string
}{
{fhttp, "FastHTTP"},
{nhttp1, "net/http v1.x"},
{nhttp2, "net/http v2.0"},
{42, "unknown client"},
}
for _, exp := range expectations {
act := exp.in.String()
if act != exp.out {
t.Errorf("Expected %v, but got %v", exp.out, act)
}
}
}
func clientTypeFromString(s string) clientTyp {
switch s {
case "fasthttp":
return fhttp
case "http1":
return nhttp1
case "http2":
return nhttp2
default:
return fhttp
}
}
================================================
FILE: dialer.go
================================================
package main
import (
"context"
"net"
"sync/atomic"
"time"
)
type countingConn struct {
net.Conn
bytesRead, bytesWritten *int64
}
func (cc *countingConn) Read(b []byte) (n int, err error) {
n, err = cc.Conn.Read(b)
if err == nil {
atomic.AddInt64(cc.bytesRead, int64(n))
}
return
}
func (cc *countingConn) Write(b []byte) (n int, err error) {
n, err = cc.Conn.Write(b)
if err == nil {
atomic.AddInt64(cc.bytesWritten, int64(n))
}
return
}
var fasthttpDialFunc = func(
bytesRead, bytesWritten *int64,
dialTimeout time.Duration,
) func(string) (net.Conn, error) {
return func(address string) (net.Conn, error) {
conn, err := net.DialTimeout("tcp", address, dialTimeout)
if err != nil {
return nil, err
}
wrappedConn := &countingConn{
Conn: conn,
bytesRead: bytesRead,
bytesWritten: bytesWritten,
}
return wrappedConn, nil
}
}
var httpDialContextFunc = func(
bytesRead, bytesWritten *int64,
dialTimeout time.Duration,
) func(context.Context, string, string) (net.Conn, error) {
dialer := &net.Dialer{Timeout: dialTimeout}
return func(ctx context.Context, network, address string) (net.Conn, error) {
conn, err := dialer.DialContext(ctx, network, address)
if err != nil {
return nil, err
}
wrappedConn := &countingConn{
Conn: conn,
bytesRead: bytesRead,
bytesWritten: bytesWritten,
}
return wrappedConn, nil
}
}
================================================
FILE: doc.go
================================================
/*
Command line utility bombardier is a fast cross-platform HTTP
benchmarking tool written in Go.
Installation with Go 1.17+:
go install github.com/codesenberg/bombardier@latest
Installation with older versions of Go:
go get -u github.com/codesenberg/bombardier
Usage:
bombardier [<flags>] <url>
Flags:
--help Show context-sensitive help (also try --help-long
and --help-man).
--version Show application version.
-c, --connections=125 Maximum number of concurrent connections
-t, --timeout=2s Socket/request timeout
-l, --latencies Print latency statistics
-m, --method=GET Request method
-b, --body="" Request body
-f, --body-file="" File to use as request body
-s, --stream Specify whether to stream body using chunked
transfer encoding or to serve it from memory
--cert="" Path to the client's TLS Certificate
--key="" Path to the client's TLS Certificate Private Key
-k, --insecure Controls whether a client verifies the server's
certificate chain and host name
-H, --header="K: V" ... HTTP headers to use(can be repeated)
-n, --requests=[pos. int.] Number of requests
-d, --duration=10s Duration of test
-r, --rate=[pos. int.] Rate limit in requests per second
--fasthttp Use fasthttp client
--http1 Use net/http client with forced HTTP/1.x
--http2 Use net/http client with enabled HTTP/2.0
-p, --print=<spec> Specifies what to output. Comma-separated list of
values 'intro' (short: 'i'), 'progress' (short:
'p'), 'result' (short: 'r'). Examples:
* i,p,r (prints everything)
* intro,result (intro & result)
* r (result only)
* result (same as above)
-q, --no-print Don't output anything
-o, --format=<spec> Which format to use to output the result. <spec>
is either a name (or its shorthand) of some format
understood by bombardier or a path to the
user-defined template, which uses Go's
text/template syntax, prefixed with 'path:' string
(without single quotes), i.e.
"path:/some/path/to/your.template" or
"path:C:\some\path\to\your.template" in case of
Windows. Formats understood by bombardier are:
* plain-text (short: pt)
* json (short: j)
Args:
<url> Target's URL
For detailed documentation on user-defined templates see
documentation for package github.com/codesenberg/bombardier/template.
Link (GoDoc):
https://godoc.org/github.com/codesenberg/bombardier/template
*/
package main
================================================
FILE: docs/CONTRIBUTING.md
================================================
### Contribution Guidelines
For relevant info on how to format commit messages and check the code before submitting pull requests see [PULL_REQUEST_TEMPLATE](https://github.com/codesenberg/bombardier/blob/master/.github/PULL_REQUEST_TEMPLATE.md).
### Reporting issues
Please open an issue if you would like to discuss anything that could be improved, have a suggestion or want to report a bug.
In latter case refer to [ISSUE_TEMPLATE](https://github.com/codesenberg/bombardier/blob/master/.github/ISSUE_TEMPLATE.md).
================================================
FILE: error_map.go
================================================
package main
import (
"sort"
"strconv"
"sync"
"sync/atomic"
)
type errorMap struct {
mu sync.RWMutex
m map[string]*uint64
}
func newErrorMap() *errorMap {
em := new(errorMap)
em.m = make(map[string]*uint64)
return em
}
func (e *errorMap) add(err error) {
s := err.Error()
e.mu.RLock()
c, ok := e.m[s]
e.mu.RUnlock()
if !ok {
e.mu.Lock()
c, ok = e.m[s]
if !ok {
c = new(uint64)
e.m[s] = c
}
e.mu.Unlock()
}
atomic.AddUint64(c, 1)
}
func (e *errorMap) get(err error) uint64 {
s := err.Error()
e.mu.RLock()
defer e.mu.RUnlock()
c := e.m[s]
if c == nil {
return uint64(0)
}
return *c
}
func (e *errorMap) sum() uint64 {
e.mu.RLock()
defer e.mu.RUnlock()
sum := uint64(0)
for _, v := range e.m {
sum += *v
}
return sum
}
type errorWithCount struct {
error string
count uint64
}
func (ewc *errorWithCount) String() string {
return "<" + ewc.error + ":" +
strconv.FormatUint(ewc.count, decBase) + ">"
}
type errorsByFrequency []*errorWithCount
func (ebf errorsByFrequency) Len() int {
return len(ebf)
}
func (ebf errorsByFrequency) Less(i, j int) bool {
return ebf[i].count > ebf[j].count
}
func (ebf errorsByFrequency) Swap(i, j int) {
ebf[i], ebf[j] = ebf[j], ebf[i]
}
func (e *errorMap) byFrequency() errorsByFrequency {
e.mu.RLock()
byFreq := make(errorsByFrequency, 0, len(e.m))
for err, count := range e.m {
byFreq = append(byFreq, &errorWithCount{err, *count})
}
e.mu.RUnlock()
sort.Sort(byFreq)
return byFreq
}
================================================
FILE: error_map_test.go
================================================
package main
import (
"errors"
"reflect"
"testing"
)
func TestErrorMapAdd(t *testing.T) {
m := newErrorMap()
err := errors.New("add")
m.add(err)
if c := m.get(err); c != 1 {
t.Error(c)
}
}
func TestErrorMapGet(t *testing.T) {
m := newErrorMap()
err := errors.New("get")
if c := m.get(err); c != 0 {
t.Error(c)
}
}
func TestByFrequency(t *testing.T) {
m := newErrorMap()
a := errors.New("A")
b := errors.New("B")
c := errors.New("C")
m.add(a)
m.add(a)
m.add(b)
m.add(b)
m.add(b)
m.add(c)
e := errorsByFrequency{
{"B", 3},
{"A", 2},
{"C", 1},
}
if a := m.byFrequency(); !reflect.DeepEqual(a, e) {
t.Logf("Expected: %+v", e)
t.Logf("Got: %+v", a)
t.Fail()
}
}
func TestErrorWithCountToStringConversion(t *testing.T) {
ewc := errorWithCount{"A", 1}
exp := "<A:1>"
if act := ewc.String(); act != exp {
t.Logf("Expected: %+v", exp)
t.Logf("Got: %+v", act)
t.Fail()
}
}
func BenchmarkErrorMapAdd(b *testing.B) {
m := newErrorMap()
err := errors.New("benchmark")
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m.add(err)
}
})
}
func BenchmarkErrorMapGet(b *testing.B) {
m := newErrorMap()
err := errors.New("benchmark")
m.add(err)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m.get(err)
}
})
}
================================================
FILE: flags.go
================================================
package main
import (
"strconv"
"time"
)
const (
nilStr = "nil"
)
type nullableUint64 struct {
val *uint64
}
func (n *nullableUint64) String() string {
if n.val == nil {
return nilStr
}
return strconv.FormatUint(*n.val, 10)
}
func (n *nullableUint64) Set(value string) error {
res, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return err
}
n.val = new(uint64)
*n.val = res
return nil
}
type nullableDuration struct {
val *time.Duration
}
func (n *nullableDuration) String() string {
if n.val == nil {
return nilStr
}
return n.val.String()
}
func (n *nullableDuration) Set(value string) error {
res, err := time.ParseDuration(value)
if err != nil {
return err
}
n.val = &res
return nil
}
type nullableString struct {
val *string
}
func (n *nullableString) String() string {
if n.val == nil {
return nilStr
}
return *n.val
}
func (n *nullableString) Set(value string) error {
n.val = new(string)
*n.val = value
return nil
}
================================================
FILE: flags_test.go
================================================
package main
import (
"math"
"math/big"
"strconv"
"testing"
"time"
)
func TestNullableUint64ConversionToString(t *testing.T) {
nilint := &nullableUint64{val: nil}
if s := nilint.String(); s != "nil" {
t.Errorf("Expected \"nil\", but got %v", s)
}
v := uint64(42)
nonnilint := &nullableUint64{val: &v}
if s, e := nonnilint.String(), strconv.FormatUint(v, 10); s != e {
t.Errorf("Expected %v, but got %v", e, s)
}
}
func TestNullableUint64Parsing(t *testing.T) {
n := &nullableUint64{}
if err := n.Set("-1"); err == nil {
t.Error("Should fail on negative values")
}
if err := n.Set(""); err == nil {
t.Error("Should fail on empty string")
}
b := big.NewInt(0)
b.SetUint64(math.MaxUint64)
b.Add(b, big.NewInt(1))
if err := n.Set(b.String()); err == nil {
t.Error("Should fail on large values")
}
max := strconv.FormatUint(math.MaxUint64, 10)
if err := n.Set(max); err != nil || *n.val != uint64(18446744073709551615) {
t.Error("Shouldn't fail on max value")
}
}
func TestNullableDurationConversionToString(t *testing.T) {
nildur := &nullableDuration{val: nil}
if s := nildur.String(); s != "nil" {
t.Errorf("Expected \"nil\", but got %v", s)
}
d := time.Second
nonnildir := &nullableDuration{val: &d}
if s := nonnildir.String(); s != "1s" {
t.Errorf("Expected 1s, but got %v", s)
}
}
func TestNullableDurationParsing(t *testing.T) {
d := &nullableDuration{}
if err := d.Set(""); err == nil {
t.Error("Should fail on empty string")
}
if err := d.Set("Wubba lubba dub dub!"); err == nil {
t.Error("Should fail on incorrect values")
}
if err := d.Set("1s"); err != nil || *d.val != time.Second {
t.Error("Shouldn't fail on correct values")
}
}
func TestNullableStringConversionToString(t *testing.T) {
ns := new(nullableString)
if act := ns.String(); act != nilStr {
t.Error("Unset nullableString should convert to \"nil\"")
}
someVal := "someval"
if err := ns.Set(someVal); err != nil {
t.Errorf("Couldn't set nullableString to %q", someVal)
}
if act := ns.String(); act != someVal {
t.Errorf("Expected %q, but got %q", someVal, act)
}
}
================================================
FILE: format.go
================================================
package main
import (
"fmt"
)
type units struct {
scale uint64
base string
units []string
}
var (
binaryUnits = &units{
scale: 1024,
base: "",
units: []string{"KB", "MB", "GB", "TB", "PB"},
}
timeUnitsUs = &units{
scale: 1000,
base: "us",
units: []string{"ms", "s"},
}
timeUnitsS = &units{
scale: 60,
base: "s",
units: []string{"m", "h"},
}
)
func formatUnits(n float64, m *units, prec int) string {
amt := n
unit := m.base
scale := float64(m.scale) * 0.85
for i := 0; i < len(m.units) && amt >= scale; i++ {
amt /= float64(m.scale)
unit = m.units[i]
}
return fmt.Sprintf("%.*f%s", prec, amt, unit)
}
func formatBinary(n float64) string {
return formatUnits(n, binaryUnits, 2)
}
func formatTimeUs(n float64) string {
units := timeUnitsUs
if n >= 1000000.0 {
n /= 1000000.0
units = timeUnitsS
}
return formatUnits(n, units, 2)
}
================================================
FILE: format_test.go
================================================
package main
import (
"testing"
)
const (
KB = 1024
MB = KB * 1024
GB = MB * 1024
K = 1000
M = K * 1000
)
func TestShouldFormatBinary(t *testing.T) {
expectations := []struct {
in float64
out string
}{
{10.0, "10.00"},
{10.001, "10.00"},
{1.0 * KB, "1.00KB"},
{1.2 * KB, "1.20KB"},
{1.202 * KB, "1.20KB"},
{5 * KB, "5.00KB"},
{1.0 * MB, "1.00MB"},
{1.3 * MB, "1.30MB"},
{1.302 * MB, "1.30MB"},
{6 * MB, "6.00MB"},
{1.0 * GB, "1.00GB"},
{1.4 * GB, "1.40GB"},
{1.402 * GB, "1.40GB"},
{7 * GB, "7.00GB"},
}
for _, e := range expectations {
actual := formatBinary(e.in)
expected := e.out
if expected != actual {
t.Errorf("Expected \"%v\", but got \"%v\"", expected, actual)
}
}
}
func TestShouldFormatUs(t *testing.T) {
expectations := []struct {
in float64
out string
}{
{20, "20.00us"},
{22.222, "22.22us"},
{20 * K, "20.00ms"},
{20 * M, "20.00s"},
{60 * M, "1.00m"},
{10 * 60 * M, "10.00m"},
{90 * 60 * M, "1.50h"},
}
for _, e := range expectations {
actual := formatTimeUs(e.in)
expected := e.out
if expected != actual {
t.Errorf("Expected \"%v\", but got \"%v\"", expected, actual)
}
}
}
================================================
FILE: go.mod
================================================
module github.com/codesenberg/bombardier
go 1.22
toolchain go1.24.0
require (
github.com/alecthomas/kingpin v2.2.6+incompatible
github.com/cheggaaa/pb v1.0.29
github.com/codesenberg/concurrent v0.0.0-20180531114123-64560cfcf964
github.com/goware/urlx v0.3.2
github.com/juju/ratelimit v1.0.2
github.com/satori/go.uuid v1.2.0
github.com/valyala/fasthttp v1.59.0
)
require (
github.com/PuerkitoBio/purell v1.2.1 // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
)
================================================
FILE: go.sum
================================================
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28=
github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI=
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo=
github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30=
github.com/codesenberg/concurrent v0.0.0-20180531114123-64560cfcf964 h1:9MVnbW3h0Dl4E2oADqwyvODphl9jY1r5HMtcB8U5mGs=
github.com/codesenberg/concurrent v0.0.0-20180531114123-64560cfcf964/go.mod h1:82C6OyVM6eVk7qpBAZXE9uszHUuXWJMHHOeY+b/CSIA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/goware/urlx v0.3.2 h1:gdoo4kBHlkqZNaf6XlQ12LGtQOmpKJrR04Rc3RnpJEo=
github.com/goware/urlx v0.3.2/go.mod h1:h8uwbJy68o+tQXCGZNa9D73WN8n0r9OBae5bUnLcgjw=
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: headers.go
================================================
package main
import (
"fmt"
"strings"
)
type header struct {
key, value string
}
type headersList []header
func (h *headersList) String() string {
return fmt.Sprint(*h)
}
func (h *headersList) IsCumulative() bool {
return true
}
func (h *headersList) Set(value string) error {
res := strings.SplitN(value, ":", 2)
if len(res) != 2 {
return errInvalidHeaderFormat
}
*h = append(*h, header{
res[0], strings.Trim(res[1], " "),
})
return nil
}
================================================
FILE: headers_test.go
================================================
package main
import (
"testing"
)
func TestHeadersToStringConversion(t *testing.T) {
expectations := []struct {
in headersList
out string
}{
{
[]header{},
"[]",
},
{
[]header{
{"Key1", "Value1"},
{"Key2", "Value2"}},
"[{Key1 Value1} {Key2 Value2}]",
},
}
for _, e := range expectations {
actual := e.in.String()
expected := e.out
if expected != actual {
t.Errorf("Expected \"%v\", but got \"%v\"", expected, actual)
}
}
}
func TestShouldErrorOnInvalidFormat(t *testing.T) {
h := new(headersList)
if err := h.Set("Yaba daba do"); err == nil {
t.Error("Should fail on strings without colon")
}
}
func TestShouldProperlyAddValidHeaders(t *testing.T) {
h := new(headersList)
for _, hs := range []string{"Key1: Value1", "Key2: Value2"} {
if err := h.Set(hs); err != nil {
t.Error(err)
}
}
e := []header{{"Key1", "Value1"}, {"Key2", "Value2"}}
for i, v := range *h {
if e[i] != v {
t.Fail()
}
}
}
func TestShouldTrimHeaderValues(t *testing.T) {
h := new(headersList)
if err := h.Set("Key: Value "); err != nil {
t.Error(err)
}
if (*h)[0].key != "Key" || (*h)[0].value != "Value" {
t.Fail()
}
}
================================================
FILE: internal/test_info.go
================================================
package internal
import (
"math"
"net/url"
"sort"
"time"
)
// TestInfo holds information about what specification was used
// to perform the test and results of the test.
type TestInfo struct {
Spec Spec
Result Results
}
// Header represents HTTP header.
type Header struct {
Key, Value string
}
// Spec contains information about test performed.
type Spec struct {
NumberOfConnections uint64
TestType TestType
NumberOfRequests uint64
TestDuration time.Duration
Method string
URL *url.URL
Headers []Header
Body string
BodyFilePath string
CertPath string
KeyPath string
Stream bool
Timeout time.Duration
ClientType ClientType
Rate *uint64
}
// RequestURL returns URL as string.
func (s Spec) RequestURL() string {
return s.URL.String()
}
// IsTimedTest tells if the test was limited by time.
func (s Spec) IsTimedTest() bool {
return s.TestType == ByTime
}
// IsTestWithNumberOfReqs tells if the test was limited by the number
// of requests.
func (s Spec) IsTestWithNumberOfReqs() bool {
return s.TestType == ByNumberOfReqs
}
// IsFastHTTP tells whether fasthttp were used as HTTP client to
// perform the test.
func (s Spec) IsFastHTTP() bool {
return s.ClientType == FastHTTP
}
// IsNetHTTPV1 tells whether Go's default net/http library and
// HTTP/1.x were used to perform the test.
func (s Spec) IsNetHTTPV1() bool {
return s.ClientType == NetHTTP1
}
// IsNetHTTPV2 tells whether Go's default net/http library and
// HTTP/1.x (or HTTP/2.0, if possible) were used to perform the test.
func (s Spec) IsNetHTTPV2() bool {
return s.ClientType == NetHTTP2
}
// Results holds results of the test.
type Results struct {
BytesRead, BytesWritten int64
TimeTaken time.Duration
Req1XX, Req2XX, Req3XX, Req4XX, Req5XX uint64
Others uint64
Errors []ErrorWithCount
Latencies ReadonlyUint64Histogram
Requests ReadonlyFloat64Histogram
}
// ReadonlyUint64Histogram is a readonly histogram with uint64 keys
type ReadonlyUint64Histogram interface {
Get(uint64) uint64
VisitAll(func(uint64, uint64) bool)
Count() uint64
}
// ReadonlyFloat64Histogram is a readonly histogram with float64 keys
type ReadonlyFloat64Histogram interface {
Get(float64) uint64
VisitAll(func(float64, uint64) bool)
Count() uint64
}
// Throughput returns total throughput (read + write) in bytes per
// second
func (r Results) Throughput() float64 {
return float64(r.BytesRead+r.BytesWritten) / r.TimeTaken.Seconds()
}
// LatenciesStats contains statistical information about latencies.
type LatenciesStats struct {
// These are in microseconds
Mean float64
Stddev float64
Max float64
// This is map[0.0 <= p <= 1.0 (percentile)]microseconds
Percentiles map[float64]uint64
}
// LatenciesStats performs various statistical calculations on
// latencies.
func (r Results) LatenciesStats(percentiles []float64) *LatenciesStats {
h := r.Latencies
sum := uint64(0)
count := uint64(0)
max := uint64(0)
pairs := make([]struct{ k, v uint64 }, 0, h.Count())
// Gather all the data
h.VisitAll(func(f uint64, c uint64) bool {
if f > max {
max = f
}
sum += f * c
count += c
pairs = append(pairs, struct{ k, v uint64 }{f, c})
return true
})
if count < 1 {
return nil
}
// Calculate percentiles
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].k < pairs[j].k
})
percentilesMap := map[float64]uint64{}
for _, pc := range percentiles {
if _, calculated := percentilesMap[pc]; calculated {
continue
}
if pc < 0 || pc > 1 {
// Drop percentiles outside of [0, 1] range
continue
}
rank := uint64(pc*float64(count) + 0.5)
total := uint64(0)
for _, p := range pairs {
total += p.v
if total >= rank {
percentilesMap[pc] = p.k
break
}
}
}
// Calculate mean and standard deviation
mean := float64(sum) / float64(count)
sumOfSquares := float64(0)
h.VisitAll(func(f uint64, c uint64) bool {
sumOfSquares += math.Pow(float64(f)-mean, 2)
return true
})
stddev := 0.0
if count > 2 {
stddev = math.Sqrt(sumOfSquares / float64(count))
}
return &LatenciesStats{
Mean: mean,
Stddev: stddev,
Max: float64(max),
Percentiles: percentilesMap,
}
}
// RequestsStats contains statistical information about requests.
type RequestsStats struct {
// These are in requests per second.
Mean float64
Stddev float64
Max float64
// This is map[0.0 <= p <= 1.0 (percentile)](req-s per second)
Percentiles map[float64]float64
}
// RequestsStats performs various statistical calculations on
// latencies.
func (r Results) RequestsStats(percentiles []float64) *RequestsStats {
h := r.Requests
sum := float64(0)
count := uint64(0)
max := float64(0)
pairs := make([]struct {
k float64
v uint64
}, 0, h.Count())
// Gather all the data
h.VisitAll(func(f float64, c uint64) bool {
if math.IsInf(f, 0) || math.IsNaN(f) {
return true
}
if f > max {
max = f
}
sum += f * float64(c)
count += c
pairs = append(pairs, struct {
k float64
v uint64
}{f, c})
return true
})
if count < 1 {
return nil
}
// Calculate percentiles
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].k < pairs[j].k
})
percentilesMap := map[float64]float64{}
for _, pc := range percentiles {
if _, calculated := percentilesMap[pc]; calculated {
continue
}
if pc < 0 || pc > 1 {
// Drop percentiles outside of [0, 1] range
continue
}
rank := uint64(pc*float64(count) + 0.5)
total := uint64(0)
for _, p := range pairs {
total += p.v
if total >= rank {
percentilesMap[pc] = p.k
break
}
}
}
// Calculate mean and standard deviation
mean := sum / float64(count)
sumOfSquares := float64(0)
h.VisitAll(func(f float64, c uint64) bool {
if math.IsInf(f, 0) || math.IsNaN(f) {
return true
}
sumOfSquares += math.Pow(f-mean, 2)
return true
})
stddev := 0.0
if count > 2 {
const besselCorrection = 1.0
stddev = math.Sqrt(sumOfSquares / (float64(count) - besselCorrection))
}
return &RequestsStats{
Mean: mean,
Stddev: stddev,
Max: max,
Percentiles: percentilesMap,
}
}
// ErrorWithCount contains error description alongside with number of
// times this error occurred.
type ErrorWithCount struct {
Error string
Count uint64
}
// TestType represents the type of test that were performed.
type TestType int
const (
_ TestType = iota
// ByTime is a test limited by durations.
ByTime
// ByNumberOfReqs is a test limited by number of requests
// performed.
ByNumberOfReqs
)
// ClientType is the type of HTTP client used in test
type ClientType int
const (
// FastHTTP is fasthttp's HTTP client
FastHTTP ClientType = iota
// NetHTTP1 is Go's default HTTP client with forced HTTP/1.x
NetHTTP1
// NetHTTP2 is Go's default HTTP client with HTTP/2.0 permitted.
NetHTTP2
)
================================================
FILE: limiter.go
================================================
package main
import (
"math"
"sync"
"time"
"github.com/juju/ratelimit"
)
type token uint64
const (
brk token = iota
cont
)
type limiter interface {
pace(<-chan struct{}) token
}
type nooplimiter struct{}
func (n *nooplimiter) pace(<-chan struct{}) token {
return cont
}
type bucketlimiter struct {
limiter *ratelimit.Bucket
timerPool *sync.Pool
}
func newBucketLimiter(rate uint64) limiter {
fillInterval, quantum := estimate(rate, rateLimitInterval)
return &bucketlimiter{
ratelimit.NewBucketWithQuantum(
fillInterval, int64(quantum), int64(quantum),
),
&sync.Pool{
New: func() interface{} {
return time.NewTimer(math.MaxInt64)
},
},
}
}
func (b *bucketlimiter) pace(done <-chan struct{}) (res token) {
wd := b.limiter.Take(1)
if wd <= 0 {
return cont
}
timer := b.timerPool.Get().(*time.Timer)
timer.Reset(wd)
select {
case <-timer.C:
res = cont
case <-done:
res = brk
}
b.timerPool.Put(timer)
return
}
================================================
FILE: limiter_barrier_test.go
================================================
package main
import (
"sync"
"sync/atomic"
"testing"
)
func TestNoopLimiterCounterBarrierCombination(t *testing.T) {
expectations := []uint64{
1, 15, 50, 100, 150, 500, 1000, 1500, 5000,
}
done := make(chan struct{})
for _, count := range expectations {
b := newCountingCompletionBarrier(count)
var lim limiter = &nooplimiter{}
counter := uint64(0)
numParties := 10
var wg sync.WaitGroup
wg.Add(numParties)
for i := 0; i < numParties; i++ {
go func() {
defer wg.Done()
for b.tryGrabWork() {
lim.pace(done)
atomic.AddUint64(&counter, 1)
b.jobDone()
}
}()
}
wg.Wait()
if counter != count {
t.Error(count, counter)
}
}
}
func TestBucketLimiterCounterBarrierCombination(t *testing.T) {
expectations := []struct {
count, rate uint64
}{
{10, 100},
{10, 1000},
{100, 1000},
{100, 10000},
{1000, 10000},
{1000, 100000},
}
done := make(chan struct{})
var expWg sync.WaitGroup
expWg.Add(len(expectations))
for i := range expectations {
exp := expectations[i]
go func() {
defer expWg.Done()
b := newCountingCompletionBarrier(exp.count)
lim := newBucketLimiter(exp.rate)
counter := uint64(0)
numParties := 10
var wg sync.WaitGroup
wg.Add(numParties)
for i := 0; i < numParties; i++ {
go func() {
defer wg.Done()
for b.tryGrabWork() {
lim.pace(done)
atomic.AddUint64(&counter, 1)
b.jobDone()
}
}()
}
wg.Wait()
if counter != exp.count {
t.Error(exp.count, counter)
}
}()
}
expWg.Wait()
}
================================================
FILE: limiter_test.go
================================================
package main
import (
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
)
const maxRps = 10000000
func TestNoopLimiter(t *testing.T) {
var lim limiter = &nooplimiter{}
done := make(chan struct{})
counter := uint64(0)
var wg sync.WaitGroup
wg.Add(int(defaultNumberOfConns))
for i := uint64(0); i < defaultNumberOfConns; i++ {
go func() {
defer wg.Done()
for {
res := lim.pace(done)
if res != cont {
t.Error("nooplimiter should always return cont")
}
atomic.AddUint64(&counter, 1)
select {
case <-done:
return
default:
}
}
}()
}
time.Sleep(100 * time.Millisecond)
close(done)
wg.Wait()
if counter == 0 {
t.Error("no events happened")
}
}
func BenchmarkNoopLimiter(bm *testing.B) {
var lim limiter = &nooplimiter{}
done := make(chan struct{})
bm.SetParallelism(int(defaultNumberOfConns) / runtime.NumCPU())
bm.ResetTimer()
bm.RunParallel(func(pb *testing.PB) {
for pb.Next() {
lim.pace(done)
}
})
}
func TestBucketLimiterLowRates(t *testing.T) {
expectations := []struct {
rate uint64
duration time.Duration
}{
{1, 1 * time.Second},
{10, 1 * time.Second},
{15, 1 * time.Second},
{50, 1 * time.Second},
{100, 1 * time.Second},
{150, 1 * time.Second},
{500, 1 * time.Second},
{1000, 1 * time.Second},
{1500, 1 * time.Second},
{5000, 1 * time.Second},
}
for i := range expectations {
exp := expectations[i]
lim := newBucketLimiter(exp.rate)
done := make(chan struct{})
counter := uint64(0)
waitChan := make(chan struct{})
go func() {
defer func() {
waitChan <- struct{}{}
}()
for lim.pace(done) == cont {
counter++
}
}()
time.Sleep(exp.duration)
close(done)
select {
case <-waitChan:
case <-time.After(exp.duration + 100*time.Millisecond):
t.Error("failed to complete: ", exp)
return
}
expcounter := float64(exp.rate) * exp.duration.Seconds()
var (
lowerBound = 0.5 * expcounter
upperBound = 1.2*expcounter + 5
)
if float64(counter) < lowerBound ||
float64(counter) > upperBound {
t.Errorf("(lower bound, actual, upper bound): (%11.2f, %11d, %11.2f)", lowerBound, counter, upperBound)
}
}
}
func TestBucketLimiterHighRates(t *testing.T) {
expectations := []struct {
rate uint64
duration time.Duration
}{
{100000, 100 * time.Millisecond},
{150000, 100 * time.Millisecond},
{200000, 100 * time.Millisecond},
{500000, 100 * time.Millisecond},
{1000000, 100 * time.Millisecond},
}
for i := range expectations {
exp := expectations[i]
lim := newBucketLimiter(exp.rate)
counter := uint64(0)
done := make(chan struct{})
waitChan := make(chan struct{})
go func() {
defer func() {
waitChan <- struct{}{}
}()
for lim.pace(done) == cont {
counter++
}
}()
time.Sleep(exp.duration)
close(done)
select {
case <-waitChan:
case <-time.After(exp.duration + 50*time.Millisecond):
t.Error("failed to complete: ", exp)
return
}
expcounter := float64(exp.rate) * exp.duration.Seconds()
var (
lowerBound = 0.5 * expcounter
upperBound = 1.2*expcounter + 5
)
if float64(counter) < lowerBound ||
float64(counter) > upperBound {
t.Errorf("(lower bound, actual, upper bound): (%11.2f, %11d, %11.2f)", lowerBound, counter, upperBound)
}
}
}
func BenchmarkBucketLimiter(bm *testing.B) {
lim := newBucketLimiter(maxRps)
done := make(chan struct{})
bm.SetParallelism(int(defaultNumberOfConns) / runtime.NumCPU())
bm.ResetTimer()
bm.RunParallel(func(pb *testing.PB) {
for pb.Next() {
lim.pace(done)
}
})
}
================================================
FILE: proxy_reader.go
================================================
package main
import "io"
type proxyReader struct {
io.Reader
}
================================================
FILE: rateestimator.go
================================================
package main
import (
"math/big"
"time"
)
const (
panicZeroRate = "rate can't be zero"
panicNegativeAdjustTo = "adjustTo can't be negative or zero"
)
func estimate(rate uint64, adjustTo time.Duration) (time.Duration, uint64) {
if rate == 0 {
panic(panicZeroRate)
}
if adjustTo <= 0 {
panic(panicNegativeAdjustTo)
}
br := new(big.Int).SetUint64(rate)
bd := new(big.Int).SetInt64(oneSecond.Nanoseconds())
gcd := new(big.Int).GCD(nil, nil, br, bd).Uint64()
nr, nd := rate/gcd, uint64(oneSecond.Nanoseconds())/gcd
adjustInt := uint64(adjustTo.Nanoseconds())
if nd >= adjustInt {
return time.Duration(nd), nr
}
coef := adjustInt / nd
return time.Duration(coef * nd), coef * nr
}
================================================
FILE: rateestimator_test.go
================================================
package main
import (
"testing"
"time"
)
func TestRateEstimatorPanicWithZeroRate(t *testing.T) {
defer func() {
pv, ok := recover().(string)
if !ok {
t.Error("expected string value")
return
}
if pv != panicZeroRate {
t.Error(panicZeroRate, pv)
}
}()
_, _ = estimate(0, 10*time.Second)
t.Error("should fail with rate == 0")
}
func TestRateEstimatorPanicWithNegativeAdjustTo(t *testing.T) {
defer func() {
pv, ok := recover().(string)
if !ok {
t.Error("expected string value")
return
}
if pv != panicNegativeAdjustTo {
t.Error(panicNegativeAdjustTo, pv)
}
}()
_, _ = estimate(10, -10*time.Second)
t.Error("should fail with adjustTo <= 0")
}
func TestRateEstimatorAccuracy(t *testing.T) {
defer func() {
rv := recover()
if rv != nil {
t.Error(rv)
}
}()
expectations := []struct {
rate uint64
adjustTo time.Duration
expectedQuantum uint64
expectedFillInterval time.Duration
}{
{1, 100 * time.Millisecond, 1, 1 * time.Second},
{1, 1000 * time.Millisecond, 1, 1 * time.Second},
{1, 2000 * time.Millisecond, 2, 2 * time.Second},
{1, 3000 * time.Millisecond, 3, 3 * time.Second},
{4, 3000 * time.Millisecond, 12, 3 * time.Second},
{10000, 100 * time.Millisecond, 1000, 100 * time.Millisecond},
{100000, 100 * time.Millisecond, 10000, 100 * time.Millisecond},
{1000000, 100 * time.Millisecond, 100000, 100 * time.Millisecond},
}
for _, exp := range expectations {
actualFillInterval, actualQuantum := estimate(exp.rate, exp.adjustTo)
if actualFillInterval != exp.expectedFillInterval ||
actualQuantum != exp.expectedQuantum {
t.Log("Expected: ", exp.expectedQuantum, exp.expectedFillInterval)
t.Log("Actual: ", actualQuantum, actualFillInterval)
t.Fail()
}
}
}
================================================
FILE: template/doc.go
================================================
/*
Package template documents the way user-defined output templates are
ment to be used.
User-defined templates use Go's text/template package, so you might
want to check its documentation first.
There are a bunch of helper methods available inside a template
besides those described in aforementioned documentation, namely:
- URLString()
Returns the URL string used for the load test.
- WithLatencies()
Tells whether --latencies flag were activated.
- FormatBinary(numberOfBytes float64) string
Converts bytes to kilo-, mega-, giga-, etc.- bytes, and
appends appropriate suffix "KB", "MB", "GB", etc.
- FormatTimeUs(us float64) string
Converts microseconds to milliseconds, seconds, minutes or
hours and appends appropriate suffix.
- FormatTimeUsUint64(us uint64) string
Same as above, but for uint64, since type conversions are
not available in templates.
- FloatsToArray(ps ...float64) []float64
Converts a bunch of floats into array, since, again,
type conversions are not available in templates.
- Multiply(num, coeff float64) float64
Arithmetics are not available inside of templates either.
- StringToBytes(s string) []byte
Convenience function to convert string to []byte.
- UUIDV1() (UUID, error)
Generates UUID Version 1, based on timestamp and
MAC address (RFC 4122)
- UUIDV2(domain byte) (UUID, error)
Generates UUID Version 2, based on timestamp, MAC address
and POSIX UID/GID (DCE 1.1)
- UUIDV3(ns UUID, name string) UUID
Generates UUID Version 3, based on MD5 hashing (RFC 4122)
- UUIDV4() (UUID, error)
Generates UUID Version 4, based on random numbers (RFC 4122)
- UUIDV5(ns UUID, name string) UUID
Generates UUID Version 5, based on SHA-1 hashing (RFC 4122)
The structure that gets passed to the template is documented in
the package github.com/codesenberg/bombardier/internal. The structure
of interest is TestInfo. It basically consists of Spec and Result
fields, the former contains various information about the test
(number of connections, URL, HTTP method, headers, body, rate, etc.)
performed, while the latter contains results obtained during the
execution of this test (bytes read/written, time taken, RPS, etc.).
Link to GoDoc for the structure used in template:
https://godoc.org/github.com/codesenberg/bombardier/internal#TestInfo
Examples of templates can be found in:
https://github.com/codesenberg/bombardier/blob/master/templates.go
*/
package template
================================================
FILE: templates.go
================================================
package main
import "strings"
var (
templates = map[string][]byte{
"plain-text": []byte(plainTextTemplate),
"json": []byte(jsonTemplate),
}
)
type format interface{}
type knownFormat string
func (kf knownFormat) template() []byte {
return templates[string(kf)]
}
type filePath string
type userDefinedTemplate filePath
func formatFromString(formatSpec string) format {
const prefix = "path:"
if strings.HasPrefix(formatSpec, prefix) {
return userDefinedTemplate(formatSpec[len(prefix):])
}
switch formatSpec {
case "pt", "plain-text":
return knownFormat("plain-text")
case "j", "json":
return knownFormat("json")
}
// nil represents unknown format
return nil
}
const (
plainTextTemplate = `
{{- printf "%10v %10v %10v %10v" "Statistics" "Avg" "Stdev" "Max" }}
{{ with .Result.RequestsStats (FloatsToArray 0.5 0.75 0.9 0.95 0.99) }}
{{- printf " %-10v %10.2f %10.2f %10.2f" "Reqs/sec" .Mean .Stddev .Max -}}
{{ else }}
{{- print " There wasn't enough data to compute statistics for requests." }}
{{ end }}
{{ with .Result.LatenciesStats (FloatsToArray 0.5 0.75 0.9 0.95 0.99) }}
{{- printf " %-10v %10v %10v %10v" "Latency" (FormatTimeUs .Mean) (FormatTimeUs .Stddev) (FormatTimeUs .Max) }}
{{- if WithLatencies }}
{{- "\n Latency Distribution" }}
{{- range $pc, $lat := .Percentiles }}
{{- printf "\n %2.0f%% %10s" (Multiply $pc 100) (FormatTimeUsUint64 $lat) -}}
{{ end -}}
{{ end }}
{{ else }}
{{- print " There wasn't enough data to compute statistics for latencies." }}
{{ end -}}
{{ with .Result -}}
{{ " HTTP codes:" }}
{{ printf " 1xx - %v, 2xx - %v, 3xx - %v, 4xx - %v, 5xx - %v" .Req1XX .Req2XX .Req3XX .Req4XX .Req5XX }}
{{- printf "\n others - %v" .Others }}
{{- with .Errors }}
{{- "\n Errors:"}}
{{- range . }}
{{- printf "\n %10v - %v" .Error .Count }}
{{- end -}}
{{ end -}}
{{ end }}
{{ printf " %-10v %10v/s\n" "Throughput:" (FormatBinary .Result.Throughput)}}`
jsonTemplate = `{"spec":{
{{- with .Spec -}}
"numberOfConnections":{{ .NumberOfConnections }}
{{- if .IsTimedTest -}}
,"testType":"timed","testDurationSeconds":{{ .TestDuration.Seconds }}
{{- else -}}
,"testType":"number-of-requests","numberOfRequests":{{ .NumberOfRequests }}
{{- end -}}
,"method":"{{ .Method }}","url":{{ .RequestURL | printf "%q" }}
{{- with .Headers -}}
,"headers":[
{{- range $index, $header := . -}}
{{- if ne $index 0 -}},{{- end -}}
{"key":{{ .Key | printf "%q" }},"value":{{ .Value | printf "%q" }}}
{{- end -}}
]
{{- end -}}
{{- if .BodyFilePath -}}
,"bodyFilePath":{{ .BodyFilePath | printf "%q" }}
{{- else -}}
,"body":{{ .Body | printf "%q" }}
{{- end -}}
{{- if .CertPath -}}
,"certPath":{{ .CertPath | printf "%q" }}
{{- end -}}
{{- if .KeyPath -}}
,"keyPath":{{ .KeyPath | printf "%q" }}
{{- end -}}
,"stream":{{ .Stream }},"timeoutSeconds":{{ .Timeout.Seconds }}
{{- if .IsFastHTTP -}}
,"client":"fasthttp"
{{- end -}}
{{- if .IsNetHTTPV1 -}}
,"client":"net/http.v1"
{{- end -}}
{{- if .IsNetHTTPV2 -}}
,"client":"net/http.v2"
{{- end -}}
{{- with .Rate -}}
,"rate":{{ . }}
{{- end -}}
{{- end -}}
},
{{- with .Result -}}
"result":{"bytesRead":{{ .BytesRead -}}
,"bytesWritten":{{ .BytesWritten -}}
,"timeTakenSeconds":{{ .TimeTaken.Seconds -}}
,"req1xx":{{ .Req1XX -}}
,"req2xx":{{ .Req2XX -}}
,"req3xx":{{ .Req3XX -}}
,"req4xx":{{ .Req4XX -}}
,"req5xx":{{ .Req5XX -}}
,"others":{{ .Others -}}
{{- with .Errors -}}
,"errors":[
{{- range $index, $error := . -}}
{{- if ne $index 0 -}},{{- end -}}
{"description":{{ .Error | printf "%q" }},"count":{{ .Count }}}
{{- end -}}
]
{{- end -}}
{{- with .LatenciesStats (FloatsToArray 0.5 0.75 0.9 0.95 0.99) -}}
,"latency":{"mean":{{ .Mean -}}
,"stddev":{{ .Stddev -}}
,"max":{{ .Max -}}
{{- if WithLatencies -}}
,"percentiles":{
{{- range $pc, $lat := .Percentiles }}
{{- if ne $pc 0.5 -}},{{- end -}}
{{- printf "\"%2.0f\":%d" (Multiply $pc 100) $lat -}}
{{- end -}}
}
{{- end -}}
}
{{- end -}}
{{- with .RequestsStats (FloatsToArray 0.5 0.75 0.9 0.95 0.99) -}}
,"rps":{"mean":{{ .Mean -}}
,"stddev":{{ .Stddev -}}
,"max":{{ .Max -}}
,"percentiles":{
{{- range $pc, $rps := .Percentiles }}
{{- if ne $pc 0.5 -}},{{- end -}}
{{- printf "\"%2.0f\":%f" (Multiply $pc 100) $rps -}}
{{- end -}}
}}
{{- end -}}
}}
{{- end -}}`
)
================================================
FILE: testbody.txt
================================================
abracadabra
================================================
FILE: testclient.cert
================================================
-----BEGIN CERTIFICATE-----
MIIFITCCAwmgAwIBAgIJAMx2fpQ+fhOZMA0GCSqGSIb3DQEBCwUAMCYxJDAiBgNV
BAMMG0JvbWJhcmRpZXIgQ2xpZW50IFRlc3QgQ2VydDAgFw0xNzAxMjYwNjM2MDda
GA8zMDE2MDUyOTA2MzYwN1owJjEkMCIGA1UEAwwbQm9tYmFyZGllciBDbGllbnQg
VGVzdCBDZXJ0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1TE9F36z
fsSmXbxkfNonYggVA7skrb+10iuLyey5snkOHLszEjmL3Gtux02GaqZ8u3GdfeZ/
Am2KqFnLq/YiZwYJpwpB54PAtKQqCiIcAPCENZdPzuya4bWTP+/bLiLdDY0kJhxM
rReufBL+xBOFrRhLcsW+tECGsu+d39o7JY7oWnm7IQYX7cxK1JgaFL5kmUuaoJN0
iJhB7V3JIQJMC68Yr5dzrYdzwzc1uxm43Y696HYAPkygf41ZoHo5UKWCI9V9M0iz
8oUoWrLdlXHOVaQKpPV9+aQYBiG1KNePvWZ4PCvukXv+zgLP1SvvqNTQKi2HCV37
RZQd0M0Do9aqrtlDrQLeKE39XZQMBKrype7Vr5JcnXMlaC4A8WFF/+cl11V6m4eY
8GLnoTv+l/G2Hbjwm6/oLPCvrErqd0M+6JK9jXnXHwNr05FpEVqwdYZ5K10bYBBU
wY+oZM8sGrT0Hd0N0PHtcs2eZ6yYLNrAaTvZT3w/sFgEqrDKn6c5WJdKO9PKSvbb
E7whD+WkZPeN2ndh+lGYAEnVzyzVgKmNPOPGFa244QEIUpeZv4d+ivPN9eOwgAVH
l4Ms9+u38VjuIE5LNZCiqOlIzaMBD+dPbOpx7rtEacMs8UgyMVIGPiJcsqzw++Ji
pWHOKRAi82TLLcqt30wgIjCfu49hFPbnfIMCAwEAAaNQME4wHQYDVR0OBBYEFAyp
8Do7nsAhXWAPamH+Vn8ntZ3pMB8GA1UdIwQYMBaAFAyp8Do7nsAhXWAPamH+Vn8n
tZ3pMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAKZcYn3AlK77Yg9K
THZym7Cmr07HA9138PnlMLlVTqJnwAU9nGZ41h/Vth1nZLy9SppefuFwXrmpI5W4
lGqY/t+saEgSzwI8cQ1F6AP0XyeQaGcqNpwHnBNF414Um42Emu+lbBJuYFV53Pgq
nhxGUD7/PbUHSCkTj/LOuvVOeXKMl2muuMpk2lwJnQNAv4mB29F+6mwxntxuSk2O
NFk13QXB5ii1NawT16yW/bYSMZsLQKvz/e/9B+tCjGdbQ8ga3OcEzjGgKRhh4Z/E
gdgl7vtu+ZkE+LHzP9V2lUVlbSP4UhQiZJSEawItSwKlIq/hTjra1kby8UbGMtLp
/vjAznZleF5nG4eQbbM3wDtnpWim0ODXH+p1JNPuK/6GzbWh4eykHQ6JElci26Hg
ug+/7iGAxBcCiPPRX1vT92rnKAIicqzZ1UInlun2X01+pxFo+xGKCYDYAJwyS8Y/
Zy4eFbXJJMGznTef7fNhtBLk6jni90YWBbLgaY62T+y1gYsyfYzQE28Idc7LiKOu
DNEfa4nXAB1zIg9JMKzqbDy4WN0B8tKbBBQIwtRn5DyW7YsmgsgZt6Gs+ochvZdh
5Zlr6T6nvFMunVGNUZP901Xa+HYqCTV7PcrvCDVMOfiNkmrlWf6amFTzL8kBELCC
0O6WGsL6rVCeSVy2BPSna0GzH36g
-----END CERTIFICATE-----
================================================
FILE: testclient.key
================================================
-----BEGIN RSA PRIVATE KEY-----
MIIJKQIBAAKCAgEA1TE9F36zfsSmXbxkfNonYggVA7skrb+10iuLyey5snkOHLsz
EjmL3Gtux02GaqZ8u3GdfeZ/Am2KqFnLq/YiZwYJpwpB54PAtKQqCiIcAPCENZdP
zuya4bWTP+/bLiLdDY0kJhxMrReufBL+xBOFrRhLcsW+tECGsu+d39o7JY7oWnm7
IQYX7cxK1JgaFL5kmUuaoJN0iJhB7V3JIQJMC68Yr5dzrYdzwzc1uxm43Y696HYA
Pkygf41ZoHo5UKWCI9V9M0iz8oUoWrLdlXHOVaQKpPV9+aQYBiG1KNePvWZ4PCvu
kXv+zgLP1SvvqNTQKi2HCV37RZQd0M0Do9aqrtlDrQLeKE39XZQMBKrype7Vr5Jc
nXMlaC4A8WFF/+cl11V6m4eY8GLnoTv+l/G2Hbjwm6/oLPCvrErqd0M+6JK9jXnX
HwNr05FpEVqwdYZ5K10bYBBUwY+oZM8sGrT0Hd0N0PHtcs2eZ6yYLNrAaTvZT3w/
sFgEqrDKn6c5WJdKO9PKSvbbE7whD+WkZPeN2ndh+lGYAEnVzyzVgKmNPOPGFa24
4QEIUpeZv4d+ivPN9eOwgAVHl4Ms9+u38VjuIE5LNZCiqOlIzaMBD+dPbOpx7rtE
acMs8UgyMVIGPiJcsqzw++JipWHOKRAi82TLLcqt30wgIjCfu49hFPbnfIMCAwEA
AQKCAgEAm+JU+Uj7lkXUH9YQ4/nfsh6WvxOnziPPns2YeR1O6uD5IKkAvuK1EYa8
iZ52GqWBrs10iwpu9CeEq3R9KE/g99PCWxF0/wOndG5VDvPB5i33ffgVswfud/t8
n9OSQDndyHrbY8JtjmMygiahgl2D8P1CrblJqCNGWrA6j+PSO7Qy0XURDySVeptW
W/yblW9hv3U4qxEmtHogOp/I4Qn88M4nDr1/J/NTAfrsntJACkDFO6SMqQD+mkWQ
s3arUfyzG+COm2EdsscKqsb+nreIV7aK0fNvGYqSxmj/Pc3gnGzAnb7Bwj8YISqN
LSHjK1/wleaURpUhlc6nvnUppDLiuYC8yB3Xk3XqgEIw96Bn7DLWdXZdCHnm02C1
WiV6SDI6TDY0oNw6qhUZ/Aq/hc4gN+MX/g1TJUXfk2R060Ul4ZY4Ywl89K45cUoo
v7uZM0hkZEukTv1LjlbkMHHMyAoHIVBgjGeIUGUOc3/2zsvRHM5MBhuZQRuTxiGg
vcR9/qdX6I0nZlUpjwLJduWn/rF8v2/lFABB36n9frK5wm6HeFp1hEitTljaeRMA
s/RgM9XiHdZpzJT1YK6J6dXzJuUYCiSAeKps4n7xT4F66FTQuXMLqD2eLkiZIsEb
nx6m0muEav+fUg8a/xi4WP/2eMNg7Ayq/PYtY76wIoxsowA7AQECggEBAPT09yFV
wrxV2K3bFgC2+0sMcwoR984j8X82aipMHPKxXPiNE0budIP2OWhMOoIelrR3Pjdm
xizU0bGI3lo5po9t22MhoQIrEREpv5rrUR+sg4DFUPUATRWlVGdr/EdsElQKTKKi
ZpCqrBAMd8D6Vp3EH+LGpy1i37MoY8T+ljZ9wMStsKgN0/k6vHzwDVyUwadvrzNO
v3HuEP+XRRxC/fhuxlti2/AS4Tm9IJIwUzNaVBpio8mLiDWdwHlGZXzxP6vlE3ef
xzHJ71D89qFlxr4EDjiKzYxiiZbGqowv2NDS+Z87rYD5mFCaVUtzrj9PJHLNTh11
P56QizutmAEvazkCggEBAN7NriGkK1+Tkf6hc5KPBgqZaUDSDAHr8DBpSMJy7sTq
yOmd1L+09rNqAXy0wV6isU9v3wYMB38xIoxYMkkW1HQr6tWpcyzbC9HomBKMZGoX
jNmj9MWKlaDN8cXfxx7l7CAf/1oJs7MAFMVu/oKnK35+uZoo9mvo0G6udc7vRHGa
OLPoySq7RuAWT7z9ULd1+Ny+Y4kvWsO/hdjVU4a9r9KQf/eNXt5PWmMb69v7hNGD
2j2CqxdlnRYdUW6y3d8919lNneTXZBbzovw6aK58l124sYjuMyDIoof5ErouWDps
SM1gE+xo48M1cSuZrv+RJu5G6mmtDe1/aXFzeEaBGZsCggEBAKB6R9kf7TcjapPj
nxOSzSjKnCcxxE3ZgGIeDQlu2dwpVEZFbiafG9hEHDH3FrGeRo8uO6ViAFzohAQy
LbGgaT039G2KX4gjHMhIuI1OstP0WiannjUUIGwY5yXmOd20sIE8Sh6WFGmcVqMg
9+eGWe57yYPxLx7t0q31vP8W5uQGGJ8BR2WhwYha8ZdMUQShNAl0gqwzX/rMw3ge
6xjrzqTONcczCfHK/KCuBcOgQzG2cLjkfHcSoYa2tZz+AIkNJ/B+X/WTyJUWvWEq
iI0ON1jPIV3rmWPqPkd4Gc1Dn2CXhw/Jsg539lB/+3c17ybsu202kYF9CdPg0Eal
oJrOLQkCggEADS1U8yBmgEyWAd1CnJRg4xeXpgHGPAbcOcDAUN/DR1orb8Wp43ys
aogGdn2qQhKVMgGHyy/C8b7SMEK3FqOHBSfjx6cx7KE33b5H4DD1b2DdL7IGs/gy
SURk3DMT77vhbzT1QTn5qsiCcfrSip+gbubHy1pI2LD4QtOGnCqCfcWFPP6zhxd0
ZaRsKt1AfNk5UrTf5ikq0RDutZhITFvDnkx1hQqTZcqDqgDoviXuAQYvThwASm30
EG7DdiyV+rIJpgx1Hieu/7yBEzHRJyCvQxe9SD/uPi4fjrMobGJ5TVtCIwNfqke5
0L3EZ7O7KdpH1yfSjVVy0W0Lq24M2v6fqQKCAQA25Xsu2HVCcE3KsEuywGJOs3hA
kXPPJu7vyLRPDPlbDhVMtKSUOAGEHyh8h6o65VZF4322gZ++AoV+EFWlGfQYVYGu
+/uBeTf6y4IuCPvCtsELiERtMAMbrhwSDB83/xIMcKkvs7X7DQ8GRe6n3mDZa6cg
EjZhQRnxnKDR7AO8pM19GuMVPDeMVNdgfUJTSDO7nifuiPEO9rtAwuH/y+RNeLIJ
9a/1zcHXU1uCJPxITlN3ckhWIVEw7ycQ0xXULt4UfcfPHNtfMR0ccUYlP6zI1eBc
CS5K58CWPWjBSiS+SFUIIx6qPtjBDuYBcqnrWqekd7m4yYKOJIaUbXhwm4IN
-----END RSA PRIVATE KEY-----
================================================
FILE: testserver.cert
================================================
-----BEGIN CERTIFICATE-----
MIIFITCCAwmgAwIBAgIJALu5MYN2H+2PMA0GCSqGSIb3DQEBCwUAMCYxJDAiBgNV
BAMMG0JvbWJhcmRpZXIgU2VydmVyIFRlc3QgQ2VydDAgFw0xNzAxMjYwNjM2NTRa
GA8zMDE2MDUyOTA2MzY1NFowJjEkMCIGA1UEAwwbQm9tYmFyZGllciBTZXJ2ZXIg
VGVzdCBDZXJ0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1laXP9o5
eo9YFOdqK/Il1f4AePr7UZ/mq5nbBkA3Pt/5uW1LIq0ECJ3+JzGrzIkYjrj6an0c
3bV5GDW/XA6yH4iMV36ADc+D+SWixQQkkWNrIL3nTHnnxOFbml4uCCDEV5TGWv8Z
GNraUPPSIZAM/OO4uKTMHKiacpnGZ8ZqUuJ0E0F3OHoLwxpJsAhiKv18iy7mHvva
PBCQ/aq8LYUOLRwiL019EVx+LFuMj9Ugc9G5lYHnn0jI/AcMdWeXil01aFEFtGpG
S7b4FV5d3x1C8l1PDDWQfuBzu4wrztnCUnvQyUTZAmBJFXt2V5MxO6p69qvGav//
IdPTxToeaz0oil+O+7cJ4gqjUBsr8xPuGLYjytZ2KJEqTIJJogXFylpwDWIvbXBw
pFhDsuE9J2oz9gPC8R54lb93cuNAeGrWZfoJ8qSzOsO4CKgasdTTrv3uFTgnovh4
qJ4SaRjOtiSmWLZhy7fSG86kB+ZuGkEOUJiFok2aNJL1UI10QuSc095JLFnqZ3LP
DG3gkdwqmYMYFnSKv43Z+azO1+t5BUSNyddSb2ZEF4d5J/UzN/D5XAxgJJoyfMK7
vHfOlglnfBkSdyxw1/BbOPLBFVDWinV33sabtgEismgdDaYjActBuRdl1md4BPRJ
Z714Noquv7qcOYXNuqdhLNWjWh4d0HwA0yECAwEAAaNQME4wHQYDVR0OBBYEFB0Y
it+Qidk1AgL+N00nyYDWgeEFMB8GA1UdIwQYMBaAFB0Yit+Qidk1AgL+N00nyYDW
geEFMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBABjxo7L4N+3lsHya
14dk8ycssuHkJvE2PhbandywV0E5bfqkoVGOmSVw50BIkpQ3WMTwACJZsjeTF1OJ
CtDHSx3EJ3wPOMChAA06QdWSwG6Gxi6NYLrZ+60VjX3f64zwvmasD2xHT20icqAR
H/405qzv5MyELvDzD9u+agdIvf2yXGywGiT8p1yPQZX5pn/1omWZEMSNICOzhFqv
U6FfYOGu/g+FJYmV5JRitMBNcr1sTJI0eHEYWW/d7yxBCtmoN4UoH1TZepijb65n
CT9xl9WX4CRGiG6/T4wcYmL2Q8OwjEn/JNUCSxU9tcyJr66JIIx4BUhUJ012BPXu
BbohBaNA8ygMpt1sweBteVCC5O/O1J7YKYG1o0J2Fd4DLol+bWfM8IijoT23XYF3
I5fcf/Y61iAxx9PqCe7BsRsPi7WXrPxZJlfocXIWbdwVwpQUngjQjiVgdcPH7LnB
NI1E2PDcDVJDVsS7XB/zK2nyY31DlQ8QZYpOzIeHjy4UcepClzk8JqeIQEmh01S7
p7fIIt51N2s2TpvZGC/wGL/0iFn/4mwyXvH9RZn8AUGBWrdB/kY/DD+Nmz8tHtlH
PpM4uDdRwh+Ks62Rw1qde5xIZ82PjLxJ+P79rQ+wYejmfenLr1PPnl1q0W/MT8gT
Puxw1k5iEsR8O9CjyEYP6bhQoZfb
-----END CERTIFICATE-----
================================================
FILE: testserver.key
================================================
-----BEGIN RSA PRIVATE KEY-----
MIIJKgIBAAKCAgEA1laXP9o5eo9YFOdqK/Il1f4AePr7UZ/mq5nbBkA3Pt/5uW1L
Iq0ECJ3+JzGrzIkYjrj6an0c3bV5GDW/XA6yH4iMV36ADc+D+SWixQQkkWNrIL3n
THnnxOFbml4uCCDEV5TGWv8ZGNraUPPSIZAM/OO4uKTMHKiacpnGZ8ZqUuJ0E0F3
OHoLwxpJsAhiKv18iy7mHvvaPBCQ/aq8LYUOLRwiL019EVx+LFuMj9Ugc9G5lYHn
n0jI/AcMdWeXil01aFEFtGpGS7b4FV5d3x1C8l1PDDWQfuBzu4wrztnCUnvQyUTZ
AmBJFXt2V5MxO6p69qvGav//IdPTxToeaz0oil+O+7cJ4gqjUBsr8xPuGLYjytZ2
KJEqTIJJogXFylpwDWIvbXBwpFhDsuE9J2oz9gPC8R54lb93cuNAeGrWZfoJ8qSz
OsO4CKgasdTTrv3uFTgnovh4qJ4SaRjOtiSmWLZhy7fSG86kB+ZuGkEOUJiFok2a
NJL1UI10QuSc095JLFnqZ3LPDG3gkdwqmYMYFnSKv43Z+azO1+t5BUSNyddSb2ZE
F4d5J/UzN/D5XAxgJJoyfMK7vHfOlglnfBkSdyxw1/BbOPLBFVDWinV33sabtgEi
smgdDaYjActBuRdl1md4BPRJZ714Noquv7qcOYXNuqdhLNWjWh4d0HwA0yECAwEA
AQKCAgEAmRXPgTODyh2Hc6a1Fh4lF+oKvF3GEk56miWRYa2Lx8SAwAdnmqSoNN9j
HutDIRrqB0Xm1Rf2/gMXMktxGXcFkbAdTIB1RWfpgpF25/BFjfHMGd6IzP5koyGy
I1cQ2Y1Nrp/77BI3AqGNPDRo6L/SBu0+ieJqRi3F4gQiyQvV9Mz4yqf/Vr8Ul4y3
BJt4Qew6f85HXenTvQK4C/Vd4cUekul9IPvfT/8XvubERhaazx4Dxty5afK6WgdO
xqvueEyKUK9Nu8YL3xgXqGt18F0d66zpQHchdP0qq9E5mMu/FtqIDLi3phLPICDG
LVZb25mvqW6WkOW2e5qnrj4Ma9uKj7q8Pj3Xh69asZaHx71K2Stc7cgEFJxjm0jm
mDZd1huCVwokpWr1hVj353kS/VK85qfN0YoBp1Lz+mi7vqQ+I+o6+IXQblwI0hAA
zYJ91ixGM4Krgc0Jj0g+pIY9AYbESxyvT/wnc2Ub1Fi4e7qvmeoWCSa/gUo6fcro
VlWIcVFj1itNVhvFm29v8aypbpWJepMHPPNAm97bhvjzIiXQMh8ET7cdmcLiKUK7
n1GVEld8UlNThmQ54/d6vSK7+aRtqq/ImVyGZdl7SW8RvVnI/AUQYQXirNdft7Lb
G6HGbLsfk4f/xznKNJt1vGgYpg/8YB1D2allhVClvtA+XbjBYWECggEBAPrOLcsE
y7IbsppDdzNNXSMcgay+24ZD9BEEwd6wAKV4uAc5Hq3JUaene/K8g87g+Vrkx7SS
mDfKUOvepYwflVBwM40Mo8V/zif0riqhYbrGPFgEw6cfiGvW34WUb9KOmrP96hEl
pEQEDetkjjeaiRTWTeTU/Hy6CLXz+4aEfCXib+vKuat8c2E3AXokaTGHsl6BKuEM
GlYod4tc62bcObId8BcwjqS+hsjAD6tWYkeeF6L4iq7rZ1XmHFUD281ZSyW2+BJL
ieSM84ZSg3hXZyTEUPvHKn4QQmNzrkm06VBmvHWDU7avFqfrvOLOcDqPBzpnxAUA
15oKlujae4PVSh8CggEBANrHDkWB4oxSDmFPk27EkzMs83eWJ4sjpT+ZUC1wo4v3
icp4Cc62oow4+tpoA8novx3tSwRfnfKig9w4svlVa2piHv6//NN6LM0jckx1dZdb
TVeAFgGchpzTpCZ+F+vw4Qzgrw2r/Pa+ShyR0FyyRwu2wAQXNl74815b01iAWa3f
UoIkvxlLu6Fy0pmeFQWyFI/WIIpXCyNrDxnrCWjcF28+et7+yngUvQ+4lRn9nF/T
oSjl4uV6RNlI7Q1KI0pqHWIFYuFYFRavgAXIshaZrPpfskE8RrX1b1IofLHi1q6P
A1O55Gxd/WWl5YTu+lpd4HeRPb+QbL0AgRujmZ98ur8CggEAe9Ts2z5k7G2sg2oo
IpZiFAHxLL+XV/WZPgXhSvgPeaPfCQH02c16mZKiKjlVwwFlXLF0wP1YVsN3rN3j
UwoNCQg9C7lf6xWtTiELFVVVEYjrJnJDv/JbwxL2jde6VnW+gHwv44N4VXTDAqRF
a8LLSBR/pSpb96FKx7vNRp+HRJVGuV8AyWDK/wbPneT4Y1IiiXKxHyiAoGWekJqy
R7kYa49IicqZw1Gm7tuVYP1nzQCLnxWkM7Va8hiJiJg9IGikJ9ztIutVDBlj68A1
1WciMA8WBRpTKqcQgFYPiajfQalYB5Vt8dcFEqfcPQe8dc1EvluZdvbxfMcZt6KY
NYFL9QKCAQEAyl7C9sy0kPP+VUlUqWuwdfAorf/5SB2K6A+bOM0um3Q4w07SU6Jh
LbAvawQ4LPbcgoRTlhIUerKVoonYFAdNuzRUU3WoGr6y3nbhbZRhV8ae/kd/E7KE
WmDzQJ/25MsGgfD8PHtRHbTbvR2sTXKjgVRkvePy6VsDU89A6mafjdQ78CKpmm6R
e0BJSswNyhz2JC8AHrdxmCuZ5nGhXJvqGX8EDW5GP1l/oSEu2sHbelC6jKhJf9fg
A9YPYPGpP1Z1I4yz8JqXt0pT9AW3pmw0s8z9iJaHGh2UAb1tyuZ3izTC8RnND+jJ
UtNoQdUFQ73+uttg8OhZjWMACl8E5aBs5QKCAQEAjBAMw41WxOPbF3S9zLOU2mqq
T+sUKhhv36Ri9nKOtyfbOWuMgtYZHDijAkkP5yXDda31kgPkx3qJH4K6F6l+1PUF
w+rbvUck15Vfed0BrzUxXL9m4JAq59TnAWajmoI/5eaeyY6M8Vfz4XQwYNNm353h
zTBbxxO9AKz9I8/mRnctGo9UZNhhBwh30t8oQl2hWLqC1CuY/1R71tkxy6Qz1Tg+
wcnDU8IbyWtuLKBikBOLzTb+EvSMfEZyS9bLUhUZvWznvwJgaLCON+k5gQSjwE98
nvR8VhuFDyq09ChAmFCAOA3plCOHCa/yYEX1BYx0tbP/yOYSePC6sKF1UzLBrw==
-----END RSA PRIVATE KEY-----
gitextract_k5a5acgj/ ├── .github/ │ ├── ISSUE_TEMPLATE.md │ └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .semaphore/ │ └── semaphore.yml ├── LICENSE ├── README.md ├── args_parser.go ├── args_parser_test.go ├── bombardier.go ├── bombardier_performance_test.go ├── bombardier_test.go ├── build.py ├── client_cert.go ├── client_cert_test.go ├── clients.go ├── clients_test.go ├── cmd/ │ └── utils/ │ └── simplebenchserver/ │ ├── doc.go │ └── main.go ├── common.go ├── completion_barriers.go ├── completion_barriers_test.go ├── config.go ├── config_test.go ├── dialer.go ├── doc.go ├── docs/ │ └── CONTRIBUTING.md ├── error_map.go ├── error_map_test.go ├── flags.go ├── flags_test.go ├── format.go ├── format_test.go ├── go.mod ├── go.sum ├── headers.go ├── headers_test.go ├── internal/ │ └── test_info.go ├── limiter.go ├── limiter_barrier_test.go ├── limiter_test.go ├── proxy_reader.go ├── rateestimator.go ├── rateestimator_test.go ├── template/ │ └── doc.go ├── templates.go ├── testbody.txt ├── testclient.cert ├── testclient.key ├── testserver.cert └── testserver.key
SYMBOL INDEX (258 symbols across 32 files)
FILE: args_parser.go
type argsParser (line 14) | type argsParser interface
type kingpinParser (line 18) | type kingpinParser struct
method parse (line 187) | func (k *kingpinParser) parse(args []string) (config, error) {
function newKingpinParser (line 46) | func newKingpinParser() argsParser {
function parsePrintSpec (line 238) | func parsePrintSpec(spec string) (bool, bool, bool, error) {
FILE: args_parser_test.go
constant programName (line 12) | programName = "bombardier"
function TestInvalidArgsParsing (line 15) | func TestInvalidArgsParsing(t *testing.T) {
function TestUnspecifiedArgParsing (line 38) | func TestUnspecifiedArgParsing(t *testing.T) {
function TestArgsParsing (line 47) | func TestArgsParsing(t *testing.T) {
function TestParsePrintSpec (line 732) | func TestParsePrintSpec(t *testing.T) {
function TestArgsParsingWithEmptyPrintSpec (line 796) | func TestArgsParsingWithEmptyPrintSpec(t *testing.T) {
function TestArgsParsingWithInvalidPrintSpec (line 808) | func TestArgsParsingWithInvalidPrintSpec(t *testing.T) {
function TestEmbeddedURLParsing (line 826) | func TestEmbeddedURLParsing(t *testing.T) {
FILE: bombardier.go
type bombardier (line 23) | type bombardier struct
method prepareTemplate (line 178) | func (b *bombardier) prepareTemplate() (*template.Template, error) {
method writeStatistics (line 226) | func (b *bombardier) writeStatistics(
method performSingleRequest (line 251) | func (b *bombardier) performSingleRequest() {
method worker (line 259) | func (b *bombardier) worker() {
method barUpdater (line 270) | func (b *bombardier) barUpdater() {
method rateMeter (line 292) | func (b *bombardier) rateMeter() {
method recordRps (line 315) | func (b *bombardier) recordRps() {
method bombard (line 327) | func (b *bombardier) bombard() {
method printIntro (line 348) | func (b *bombardier) printIntro() {
method gatherInfo (line 359) | func (b *bombardier) gatherInfo() internal.TestInfo {
method printStats (line 425) | func (b *bombardier) printStats() {
method redirectOutputTo (line 433) | func (b *bombardier) redirectOutputTo(out io.Writer) {
method disableOutput (line 438) | func (b *bombardier) disableOutput() {
function newBombardier (line 62) | func newBombardier(c config) (*bombardier, error) {
function makeHTTPClient (line 162) | func makeHTTPClient(clientType clientTyp, cc *clientOpts) client {
function main (line 443) | func main() {
FILE: bombardier_performance_test.go
function BenchmarkBombardierSingleReqPerf (line 21) | func BenchmarkBombardierSingleReqPerf(b *testing.B) {
function BenchmarkBombardierRateLimitPerf (line 38) | func BenchmarkBombardierRateLimitPerf(b *testing.B) {
function benchmarkFireRequest (line 56) | func benchmarkFireRequest(c config, bm *testing.B) {
FILE: bombardier_test.go
function TestBombardierShouldFireSpecifiedNumberOfRequests (line 20) | func TestBombardierShouldFireSpecifiedNumberOfRequests(t *testing.T) {
function testBombardierShouldFireSpecifiedNumberOfRequests (line 24) | func testBombardierShouldFireSpecifiedNumberOfRequests(
function TestBombardierShouldFinish (line 57) | func TestBombardierShouldFinish(t *testing.T) {
function testBombardierShouldFinish (line 61) | func testBombardierShouldFinish(clientType clientTyp, t *testing.T) {
function TestBombardierShouldSendHeaders (line 102) | func TestBombardierShouldSendHeaders(t *testing.T) {
function testBombardierShouldSendHeaders (line 106) | func testBombardierShouldSendHeaders(clientType clientTyp, t *testing.T) {
function TestBombardierHTTPCodeRecording (line 152) | func TestBombardierHTTPCodeRecording(t *testing.T) {
function testBombardierHTTPCodeRecording (line 156) | func testBombardierHTTPCodeRecording(clientType clientTyp, t *testing.T) {
function TestBombardierTimeoutRecoding (line 215) | func TestBombardierTimeoutRecoding(t *testing.T) {
function testBombardierTimeoutRecoding (line 219) | func testBombardierTimeoutRecoding(clientType clientTyp, t *testing.T) {
function TestBombardierThroughputRecording (line 250) | func TestBombardierThroughputRecording(t *testing.T) {
function testBombardierThroughputRecording (line 254) | func testBombardierThroughputRecording(clientType clientTyp, t *testing....
function TestBombardierStatsPrinting (line 288) | func TestBombardierStatsPrinting(t *testing.T) {
function TestBombardierErrorIfFailToReadClientCert (line 334) | func TestBombardierErrorIfFailToReadClientCert(t *testing.T) {
function TestBombardierClientCerts (line 354) | func TestBombardierClientCerts(t *testing.T) {
function testBombardierClientCerts (line 358) | func testBombardierClientCerts(clientType clientTyp, t *testing.T) {
function TestBombardierRateLimiting (line 431) | func TestBombardierRateLimiting(t *testing.T) {
function testBombardierRateLimiting (line 435) | func testBombardierRateLimiting(clientType clientTyp, t *testing.T) {
function testAllClients (line 473) | func testAllClients(parent *testing.T, testFun func(clientTyp, *testing....
function TestBombardierSendsBody (line 482) | func TestBombardierSendsBody(t *testing.T) {
function testBombardierSendsBody (line 486) | func testBombardierSendsBody(clientType clientTyp, t *testing.T) {
function TestBombardierSendsBodyFromFile (line 526) | func TestBombardierSendsBodyFromFile(t *testing.T) {
function testBombardierSendsBodyFromFile (line 530) | func testBombardierSendsBodyFromFile(clientType clientTyp, t *testing.T) {
function TestBombardierFileDoesntExist (line 575) | func TestBombardierFileDoesntExist(t *testing.T) {
function TestBombardierStreamsBody (line 592) | func TestBombardierStreamsBody(t *testing.T) {
function testBombardierStreamsBody (line 596) | func testBombardierStreamsBody(clientType clientTyp, t *testing.T) {
function TestBombardierStreamsBodyFromFile (line 640) | func TestBombardierStreamsBodyFromFile(t *testing.T) {
function testBombardierStreamsBodyFromFile (line 644) | func testBombardierStreamsBodyFromFile(clientType clientTyp, t *testing....
function TestBombardierShouldSendCustomHostHeader (line 693) | func TestBombardierShouldSendCustomHostHeader(t *testing.T) {
function testBombardierShouldSendCustomHostHeader (line 697) | func testBombardierShouldSendCustomHostHeader(
FILE: client_cert.go
function readClientCert (line 9) | func readClientCert(certPath, keyPath string) ([]tls.Certificate, error) {
function generateTLSConfig (line 17) | func generateTLSConfig(c config) (*tls.Config, error) {
FILE: client_cert_test.go
function TestGenerateTLSConfig (line 7) | func TestGenerateTLSConfig(t *testing.T) {
FILE: clients.go
type client (line 15) | type client interface
type bodyStreamProducer (line 19) | type bodyStreamProducer
type clientOpts (line 21) | type clientOpts struct
type fasthttpClient (line 39) | type fasthttpClient struct
method do (line 78) | func (c *fasthttpClient) do() (
function newFastHTTPClient (line 50) | func newFastHTTPClient(opts *clientOpts) client {
type httpClient (line 117) | type httpClient struct
method do (line 154) | func (c *httpClient) do() (
function newHTTPClient (line 128) | func newHTTPClient(opts *clientOpts) client {
function headersToFastHTTPHeaders (line 200) | func headersToFastHTTPHeaders(h *headersList) *fasthttp.RequestHeader {
function headersToHTTPHeaders (line 211) | func headersToHTTPHeaders(h *headersList) http.Header {
FILE: clients_test.go
function TestShouldReturnNilIfNoHeadersWhereSet (line 14) | func TestShouldReturnNilIfNoHeadersWhereSet(t *testing.T) {
function TestShouldReturnEmptyHeadersIfNoHeaadersWhereSet (line 21) | func TestShouldReturnEmptyHeadersIfNoHeaadersWhereSet(t *testing.T) {
function TestShouldProperlyConvertToHttpHeaders (line 28) | func TestShouldProperlyConvertToHttpHeaders(t *testing.T) {
function TestHTTP2Client (line 60) | func TestHTTP2Client(t *testing.T) {
function TestHTTP1Clients (line 117) | func TestHTTP1Clients(t *testing.T) {
FILE: cmd/utils/simplebenchserver/main.go
function main (line 24) | func main() {
FILE: common.go
constant decBase (line 13) | decBase = 10
constant rateLimitInterval (line 15) | rateLimitInterval = 10 * time.Millisecond
constant oneSecond (line 16) | oneSecond = 1 * time.Second
constant exitFailure (line 18) | exitFailure = 1
function ParseURLOrPanic (line 61) | func ParseURLOrPanic(s string) *url.URL {
function init (line 69) | func init() {
FILE: completion_barriers.go
type completionBarrier (line 9) | type completionBarrier interface
type countingCompletionBarrier (line 17) | type countingCompletionBarrier struct
method tryGrabWork (line 30) | func (c *countingCompletionBarrier) tryGrabWork() bool {
method jobDone (line 40) | func (c *countingCompletionBarrier) jobDone() {
method done (line 49) | func (c *countingCompletionBarrier) done() <-chan struct{} {
method cancel (line 53) | func (c *countingCompletionBarrier) cancel() {
method completed (line 59) | func (c *countingCompletionBarrier) completed() float64 {
function newCountingCompletionBarrier (line 23) | func newCountingCompletionBarrier(numReqs uint64) completionBarrier {
type timedCompletionBarrier (line 69) | type timedCompletionBarrier struct
method tryGrabWork (line 94) | func (c *timedCompletionBarrier) tryGrabWork() bool {
method jobDone (line 103) | func (c *timedCompletionBarrier) jobDone() {
method done (line 106) | func (c *timedCompletionBarrier) done() <-chan struct{} {
method cancel (line 110) | func (c *timedCompletionBarrier) cancel() {
method completed (line 116) | func (c *timedCompletionBarrier) completed() float64 {
function newTimedCompletionBarrier (line 76) | func newTimedCompletionBarrier(duration time.Duration) completionBarrier {
FILE: completion_barriers_test.go
function TestCouintingCompletionBarrierWait (line 9) | func TestCouintingCompletionBarrierWait(t *testing.T) {
function TestTimedCompletionBarrierWait (line 32) | func TestTimedCompletionBarrierWait(t *testing.T) {
function TestTimeBarrierCancel (line 63) | func TestTimeBarrierCancel(t *testing.T) {
function TestCountedBarrierCancel (line 80) | func TestCountedBarrierCancel(t *testing.T) {
function TestTimeBarrierPanicOnBadDuration (line 105) | func TestTimeBarrierPanicOnBadDuration(t *testing.T) {
function approximatelyEqual (line 118) | func approximatelyEqual(expected, actual, err time.Duration) bool {
FILE: config.go
type config (line 10) | type config struct
method checkArgs (line 50) | func (c *config) checkArgs() error {
method checkOrSetDefaultTestType (line 71) | func (c *config) checkOrSetDefaultTestType() {
method testType (line 77) | func (c *config) testType() testTyp {
method checkURL (line 87) | func (c *config) checkURL() error {
method checkRate (line 94) | func (c *config) checkRate() error {
method checkRunParameters (line 101) | func (c *config) checkRunParameters() error {
method checkTimeoutDuration (line 114) | func (c *config) checkTimeoutDuration() error {
method checkHTTPParameters (line 121) | func (c *config) checkHTTPParameters() error {
method checkCertPaths (line 134) | func (c *config) checkCertPaths() error {
method timeoutMillis (line 143) | func (c *config) timeoutMillis() uint64 {
type testTyp (line 34) | type testTyp
constant none (line 37) | none testTyp = iota
constant timed (line 38) | timed
constant counted (line 39) | counted
type invalidHTTPMethodError (line 42) | type invalidHTTPMethodError struct
method Error (line 46) | func (i *invalidHTTPMethodError) Error() string {
function allowedHTTPMethod (line 147) | func allowedHTTPMethod(method string) bool {
function canHaveBody (line 152) | func canHaveBody(method string) bool {
type clientTyp (line 157) | type clientTyp
method String (line 165) | func (ct clientTyp) String() string {
constant fhttp (line 160) | fhttp clientTyp = iota
constant nhttp1 (line 161) | nhttp1
constant nhttp2 (line 162) | nhttp2
FILE: config_test.go
function TestCanHaveBody (line 12) | func TestCanHaveBody(t *testing.T) {
function TestAllowedHttpMethod (line 31) | func TestAllowedHttpMethod(t *testing.T) {
function TestCheckArgs (line 52) | func TestCheckArgs(t *testing.T) {
function TestCheckArgsUnsupportedURLScheme (line 262) | func TestCheckArgsUnsupportedURLScheme(t *testing.T) {
function TestCheckArgsInvalidRequestMethod (line 278) | func TestCheckArgsInvalidRequestMethod(t *testing.T) {
function TestCheckArgsTestType (line 298) | func TestCheckArgsTestType(t *testing.T) {
function TestTimeoutMillis (line 358) | func TestTimeoutMillis(t *testing.T) {
function TestInvalidHTTPMethodError (line 374) | func TestInvalidHTTPMethodError(t *testing.T) {
function TestClientTypToStringConversion (line 383) | func TestClientTypToStringConversion(t *testing.T) {
function clientTypeFromString (line 401) | func clientTypeFromString(s string) clientTyp {
FILE: dialer.go
type countingConn (line 10) | type countingConn struct
method Read (line 15) | func (cc *countingConn) Read(b []byte) (n int, err error) {
method Write (line 25) | func (cc *countingConn) Write(b []byte) (n int, err error) {
FILE: error_map.go
type errorMap (line 10) | type errorMap struct
method add (line 21) | func (e *errorMap) add(err error) {
method get (line 38) | func (e *errorMap) get(err error) uint64 {
method sum (line 49) | func (e *errorMap) sum() uint64 {
method byFrequency (line 83) | func (e *errorMap) byFrequency() errorsByFrequency {
function newErrorMap (line 15) | func newErrorMap() *errorMap {
type errorWithCount (line 59) | type errorWithCount struct
method String (line 64) | func (ewc *errorWithCount) String() string {
type errorsByFrequency (line 69) | type errorsByFrequency
method Len (line 71) | func (ebf errorsByFrequency) Len() int {
method Less (line 75) | func (ebf errorsByFrequency) Less(i, j int) bool {
method Swap (line 79) | func (ebf errorsByFrequency) Swap(i, j int) {
FILE: error_map_test.go
function TestErrorMapAdd (line 9) | func TestErrorMapAdd(t *testing.T) {
function TestErrorMapGet (line 18) | func TestErrorMapGet(t *testing.T) {
function TestByFrequency (line 26) | func TestByFrequency(t *testing.T) {
function TestErrorWithCountToStringConversion (line 49) | func TestErrorWithCountToStringConversion(t *testing.T) {
function BenchmarkErrorMapAdd (line 59) | func BenchmarkErrorMapAdd(b *testing.B) {
function BenchmarkErrorMapGet (line 70) | func BenchmarkErrorMapGet(b *testing.B) {
FILE: flags.go
constant nilStr (line 9) | nilStr = "nil"
type nullableUint64 (line 12) | type nullableUint64 struct
method String (line 16) | func (n *nullableUint64) String() string {
method Set (line 23) | func (n *nullableUint64) Set(value string) error {
type nullableDuration (line 33) | type nullableDuration struct
method String (line 37) | func (n *nullableDuration) String() string {
method Set (line 44) | func (n *nullableDuration) Set(value string) error {
type nullableString (line 53) | type nullableString struct
method String (line 57) | func (n *nullableString) String() string {
method Set (line 64) | func (n *nullableString) Set(value string) error {
FILE: flags_test.go
function TestNullableUint64ConversionToString (line 11) | func TestNullableUint64ConversionToString(t *testing.T) {
function TestNullableUint64Parsing (line 23) | func TestNullableUint64Parsing(t *testing.T) {
function TestNullableDurationConversionToString (line 43) | func TestNullableDurationConversionToString(t *testing.T) {
function TestNullableDurationParsing (line 55) | func TestNullableDurationParsing(t *testing.T) {
function TestNullableStringConversionToString (line 68) | func TestNullableStringConversionToString(t *testing.T) {
FILE: format.go
type units (line 7) | type units struct
function formatUnits (line 31) | func formatUnits(n float64, m *units, prec int) string {
function formatBinary (line 44) | func formatBinary(n float64) string {
function formatTimeUs (line 48) | func formatTimeUs(n float64) string {
FILE: format_test.go
constant KB (line 8) | KB = 1024
constant MB (line 9) | MB = KB * 1024
constant GB (line 10) | GB = MB * 1024
constant K (line 12) | K = 1000
constant M (line 13) | M = K * 1000
function TestShouldFormatBinary (line 16) | func TestShouldFormatBinary(t *testing.T) {
function TestShouldFormatUs (line 45) | func TestShouldFormatUs(t *testing.T) {
FILE: headers.go
type header (line 8) | type header struct
type headersList (line 12) | type headersList
method String (line 14) | func (h *headersList) String() string {
method IsCumulative (line 18) | func (h *headersList) IsCumulative() bool {
method Set (line 22) | func (h *headersList) Set(value string) error {
FILE: headers_test.go
function TestHeadersToStringConversion (line 7) | func TestHeadersToStringConversion(t *testing.T) {
function TestShouldErrorOnInvalidFormat (line 32) | func TestShouldErrorOnInvalidFormat(t *testing.T) {
function TestShouldProperlyAddValidHeaders (line 39) | func TestShouldProperlyAddValidHeaders(t *testing.T) {
function TestShouldTrimHeaderValues (line 54) | func TestShouldTrimHeaderValues(t *testing.T) {
FILE: internal/test_info.go
type TestInfo (line 12) | type TestInfo struct
type Header (line 18) | type Header struct
type Spec (line 23) | type Spec struct
method RequestURL (line 49) | func (s Spec) RequestURL() string {
method IsTimedTest (line 54) | func (s Spec) IsTimedTest() bool {
method IsTestWithNumberOfReqs (line 60) | func (s Spec) IsTestWithNumberOfReqs() bool {
method IsFastHTTP (line 66) | func (s Spec) IsFastHTTP() bool {
method IsNetHTTPV1 (line 72) | func (s Spec) IsNetHTTPV1() bool {
method IsNetHTTPV2 (line 78) | func (s Spec) IsNetHTTPV2() bool {
type Results (line 83) | type Results struct
method Throughput (line 112) | func (r Results) Throughput() float64 {
method LatenciesStats (line 129) | func (r Results) LatenciesStats(percentiles []float64) *LatenciesStats {
method RequestsStats (line 207) | func (r Results) RequestsStats(percentiles []float64) *RequestsStats {
type ReadonlyUint64Histogram (line 97) | type ReadonlyUint64Histogram interface
type ReadonlyFloat64Histogram (line 104) | type ReadonlyFloat64Histogram interface
type LatenciesStats (line 117) | type LatenciesStats struct
type RequestsStats (line 195) | type RequestsStats struct
type ErrorWithCount (line 287) | type ErrorWithCount struct
type TestType (line 293) | type TestType
constant _ (line 296) | _ TestType = iota
constant ByTime (line 298) | ByTime
constant ByNumberOfReqs (line 301) | ByNumberOfReqs
type ClientType (line 305) | type ClientType
constant FastHTTP (line 309) | FastHTTP ClientType = iota
constant NetHTTP1 (line 311) | NetHTTP1
constant NetHTTP2 (line 313) | NetHTTP2
FILE: limiter.go
type token (line 11) | type token
constant brk (line 14) | brk token = iota
constant cont (line 15) | cont
type limiter (line 18) | type limiter interface
type nooplimiter (line 22) | type nooplimiter struct
method pace (line 24) | func (n *nooplimiter) pace(<-chan struct{}) token {
type bucketlimiter (line 28) | type bucketlimiter struct
method pace (line 47) | func (b *bucketlimiter) pace(done <-chan struct{}) (res token) {
function newBucketLimiter (line 33) | func newBucketLimiter(rate uint64) limiter {
FILE: limiter_barrier_test.go
function TestNoopLimiterCounterBarrierCombination (line 9) | func TestNoopLimiterCounterBarrierCombination(t *testing.T) {
function TestBucketLimiterCounterBarrierCombination (line 38) | func TestBucketLimiterCounterBarrierCombination(t *testing.T) {
FILE: limiter_test.go
constant maxRps (line 11) | maxRps = 10000000
function TestNoopLimiter (line 13) | func TestNoopLimiter(t *testing.T) {
function BenchmarkNoopLimiter (line 44) | func BenchmarkNoopLimiter(bm *testing.B) {
function TestBucketLimiterLowRates (line 56) | func TestBucketLimiterLowRates(t *testing.T) {
function TestBucketLimiterHighRates (line 106) | func TestBucketLimiterHighRates(t *testing.T) {
function BenchmarkBucketLimiter (line 151) | func BenchmarkBucketLimiter(bm *testing.B) {
FILE: proxy_reader.go
type proxyReader (line 5) | type proxyReader struct
FILE: rateestimator.go
constant panicZeroRate (line 9) | panicZeroRate = "rate can't be zero"
constant panicNegativeAdjustTo (line 10) | panicNegativeAdjustTo = "adjustTo can't be negative or zero"
function estimate (line 13) | func estimate(rate uint64, adjustTo time.Duration) (time.Duration, uint6...
FILE: rateestimator_test.go
function TestRateEstimatorPanicWithZeroRate (line 8) | func TestRateEstimatorPanicWithZeroRate(t *testing.T) {
function TestRateEstimatorPanicWithNegativeAdjustTo (line 23) | func TestRateEstimatorPanicWithNegativeAdjustTo(t *testing.T) {
function TestRateEstimatorAccuracy (line 38) | func TestRateEstimatorAccuracy(t *testing.T) {
FILE: templates.go
type format (line 12) | type format interface
type knownFormat (line 13) | type knownFormat
method template (line 15) | func (kf knownFormat) template() []byte {
type filePath (line 19) | type filePath
type userDefinedTemplate (line 20) | type userDefinedTemplate
function formatFromString (line 22) | func formatFromString(formatSpec string) format {
constant plainTextTemplate (line 38) | plainTextTemplate = `
constant jsonTemplate (line 68) | jsonTemplate = `{"spec":{
Condensed preview — 50 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (166K chars).
[
{
"path": ".github/ISSUE_TEMPLATE.md",
"chars": 765,
"preview": "This is an example of what a **bug report** can look like. Please, feel free to also provide any other information relev"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 891,
"preview": "Before submitting a pull request be sure to check the code with [gometalinter](https://github.com/alecthomas/gometalinte"
},
{
"path": ".gitignore",
"chars": 297,
"preview": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# Architecture spe"
},
{
"path": ".semaphore/semaphore.yml",
"chars": 790,
"preview": "version: v1.0\nname: codesenberg/bombardier\nagent:\n machine:\n type: e1-standard-2\n os_image: ubuntu2004\nblocks:\n "
},
{
"path": "LICENSE",
"chars": 1070,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Максим Федосеев\n\nPermission is hereby granted, free of charge, to any person o"
},
{
"path": "README.md",
"chars": 2847,
"preview": "# bombardier [\n\nconst (\n\tprogramName = \"bombardier\"\n)\n\nfunc Te"
},
{
"path": "bombardier.go",
"chars": 9379,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"text/template\"\n\t"
},
{
"path": "bombardier_performance_test.go",
"chars": 1722,
"preview": "package main\n\nimport (\n\t\"flag\"\n\t\"runtime\"\n\t\"testing\"\n\t\"time\"\n)\n\nvar (\n\tserverPort = flag.String(\"port\", \"8080\", \"port to"
},
{
"path": "bombardier_test.go",
"chars": 17277,
"preview": "package main\n\nimport (\n\t\"bytes\"\n\t\"container/ring\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/"
},
{
"path": "build.py",
"chars": 1266,
"preview": "import argparse\nimport os\nimport subprocess\n\nplatforms = [\n (\"darwin\", \"amd64\"),\n (\"darwin\", \"arm64\"),\n (\"freeb"
},
{
"path": "client_cert.go",
"chars": 1024,
"preview": "package main\n\nimport (\n\t\"crypto/tls\"\n)\n\n// readClientCert - helper function to read client certificate\n// from pem forma"
},
{
"path": "client_cert_test.go",
"chars": 687,
"preview": "package main\n\nimport (\n\t\"testing\"\n)\n\nfunc TestGenerateTLSConfig(t *testing.T) {\n\texpectations := []struct {\n\t\tcertPath s"
},
{
"path": "clients.go",
"chars": 4394,
"preview": "package main\n\nimport (\n\t\"crypto/tls\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/valyala/"
},
{
"path": "clients_test.go",
"chars": 3676,
"preview": "package main\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sync/atomic\"\n\t\"testing\"\n\n\t\"github.com/g"
},
{
"path": "cmd/utils/simplebenchserver/doc.go",
"chars": 296,
"preview": "/*\nSimple HTTP server used for benchmarking.\n\nFollowing options are available:\n\n\t --help Show context-sensiti"
},
{
"path": "cmd/utils/simplebenchserver/main.go",
"chars": 1038,
"preview": "package main\n\nimport (\n\t\"bytes\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/alecthomas/kingpin\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nv"
},
{
"path": "common.go",
"chars": 1609,
"preview": "package main\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/goware/urlx\"\n)\n\nconst (\n\tdecBase = 10\n\n\trateLi"
},
{
"path": "completion_barriers.go",
"chars": 2447,
"preview": "package main\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\ntype completionBarrier interface {\n\tcompleted() float64\n\ttryGra"
},
{
"path": "completion_barriers_test.go",
"chars": 2326,
"preview": "package main\n\nimport (\n\t\"math\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestCouintingCompletionBarrierWait(t *testing.T) {\n\tparties :="
},
{
"path": "config.go",
"chars": 3502,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"sort\"\n\t\"time\"\n)\n\ntype config struct {\n\tnumConns uint64\n\tnumR"
},
{
"path": "config_test.go",
"chars": 9556,
"preview": "package main\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nvar (\n\tdefaultNumberOfReqs = uint64(10000)\n)\n\nfunc TestCanHaveBody(t *testi"
},
{
"path": "dialer.go",
"chars": 1419,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\ntype countingConn struct {\n\tnet.Conn\n\tbytesRead, byte"
},
{
"path": "doc.go",
"chars": 3169,
"preview": "/*\nCommand line utility bombardier is a fast cross-platform HTTP\nbenchmarking tool written in Go.\n\nInstallation with Go "
},
{
"path": "docs/CONTRIBUTING.md",
"chars": 518,
"preview": "### Contribution Guidelines\nFor relevant info on how to format commit messages and check the code before submitting pull"
},
{
"path": "error_map.go",
"chars": 1492,
"preview": "package main\n\nimport (\n\t\"sort\"\n\t\"strconv\"\n\t\"sync\"\n\t\"sync/atomic\"\n)\n\ntype errorMap struct {\n\tmu sync.RWMutex\n\tm map[stri"
},
{
"path": "error_map_test.go",
"chars": 1314,
"preview": "package main\n\nimport (\n\t\"errors\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestErrorMapAdd(t *testing.T) {\n\tm := newErrorMap()\n\terr "
},
{
"path": "flags.go",
"chars": 983,
"preview": "package main\n\nimport (\n\t\"strconv\"\n\t\"time\"\n)\n\nconst (\n\tnilStr = \"nil\"\n)\n\ntype nullableUint64 struct {\n\tval *uint64\n}\n\nfun"
},
{
"path": "flags_test.go",
"chars": 2114,
"preview": "package main\n\nimport (\n\t\"math\"\n\t\"math/big\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestNullableUint64ConversionToString(t "
},
{
"path": "format.go",
"chars": 887,
"preview": "package main\n\nimport (\n\t\"fmt\"\n)\n\ntype units struct {\n\tscale uint64\n\tbase string\n\tunits []string\n}\n\nvar (\n\tbinaryUnits ="
},
{
"path": "format_test.go",
"chars": 1184,
"preview": "package main\n\nimport (\n\t\"testing\"\n)\n\nconst (\n\tKB = 1024\n\tMB = KB * 1024\n\tGB = MB * 1024\n\n\tK = 1000\n\tM = K * 1000\n)\n\nfunc"
},
{
"path": "go.mod",
"chars": 954,
"preview": "module github.com/codesenberg/bombardier\n\ngo 1.22\n\ntoolchain go1.24.0\n\nrequire (\n\tgithub.com/alecthomas/kingpin v2.2.6+i"
},
{
"path": "go.sum",
"chars": 6538,
"preview": "github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=\ngithub.com/PuerkitoBio/purel"
},
{
"path": "headers.go",
"chars": 461,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype header struct {\n\tkey, value string\n}\n\ntype headersList []header\n\nfunc ("
},
{
"path": "headers_test.go",
"chars": 1181,
"preview": "package main\n\nimport (\n\t\"testing\"\n)\n\nfunc TestHeadersToStringConversion(t *testing.T) {\n\texpectations := []struct {\n\t\tin"
},
{
"path": "internal/test_info.go",
"chars": 6873,
"preview": "package internal\n\nimport (\n\t\"math\"\n\t\"net/url\"\n\t\"sort\"\n\t\"time\"\n)\n\n// TestInfo holds information about what specification "
},
{
"path": "limiter.go",
"chars": 969,
"preview": "package main\n\nimport (\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/juju/ratelimit\"\n)\n\ntype token uint64\n\nconst (\n\tbrk token = "
},
{
"path": "limiter_barrier_test.go",
"chars": 1555,
"preview": "package main\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n)\n\nfunc TestNoopLimiterCounterBarrierCombination(t *testing.T) "
},
{
"path": "limiter_test.go",
"chars": 3577,
"preview": "package main\n\nimport (\n\t\"runtime\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\nconst maxRps = 10000000\n\nfunc TestNoopLim"
},
{
"path": "proxy_reader.go",
"chars": 66,
"preview": "package main\n\nimport \"io\"\n\ntype proxyReader struct {\n\tio.Reader\n}\n"
},
{
"path": "rateestimator.go",
"chars": 708,
"preview": "package main\n\nimport (\n\t\"math/big\"\n\t\"time\"\n)\n\nconst (\n\tpanicZeroRate = \"rate can't be zero\"\n\tpanicNegativeAdjust"
},
{
"path": "rateestimator_test.go",
"chars": 1795,
"preview": "package main\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestRateEstimatorPanicWithZeroRate(t *testing.T) {\n\tdefer func() {\n\t\tp"
},
{
"path": "template/doc.go",
"chars": 2493,
"preview": "/*\nPackage template documents the way user-defined output templates are\nment to be used.\n\nUser-defined templates use Go'"
},
{
"path": "templates.go",
"chars": 4284,
"preview": "package main\n\nimport \"strings\"\n\nvar (\n\ttemplates = map[string][]byte{\n\t\t\"plain-text\": []byte(plainTextTemplate),\n\t\t\"json"
},
{
"path": "testbody.txt",
"chars": 11,
"preview": "abracadabra"
},
{
"path": "testclient.cert",
"chars": 1838,
"preview": "-----BEGIN CERTIFICATE-----\nMIIFITCCAwmgAwIBAgIJAMx2fpQ+fhOZMA0GCSqGSIb3DQEBCwUAMCYxJDAiBgNV\nBAMMG0JvbWJhcmRpZXIgQ2xpZW5"
},
{
"path": "testclient.key",
"chars": 3243,
"preview": "-----BEGIN RSA PRIVATE KEY-----\nMIIJKQIBAAKCAgEA1TE9F36zfsSmXbxkfNonYggVA7skrb+10iuLyey5snkOHLsz\nEjmL3Gtux02GaqZ8u3GdfeZ"
},
{
"path": "testserver.cert",
"chars": 1838,
"preview": "-----BEGIN CERTIFICATE-----\nMIIFITCCAwmgAwIBAgIJALu5MYN2H+2PMA0GCSqGSIb3DQEBCwUAMCYxJDAiBgNV\nBAMMG0JvbWJhcmRpZXIgU2VydmV"
},
{
"path": "testserver.key",
"chars": 3247,
"preview": "-----BEGIN RSA PRIVATE KEY-----\nMIIJKgIBAAKCAgEA1laXP9o5eo9YFOdqK/Il1f4AePr7UZ/mq5nbBkA3Pt/5uW1L\nIq0ECJ3+JzGrzIkYjrj6an0"
}
]
About this extraction
This page contains the full source code of the codesenberg/bombardier GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 50 files (143.3 KB), approximately 50.1k tokens, and a symbol index with 258 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.