mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Port nginx and move parsers to own package.
This change: - Ports the nginx benchmark. - Switches the Httpd benchmark to use 'hey' as a client. - Moves all parsers to their own package 'tools'. Parsers are moved to their own package because 1) parsing output of a command is often dependent on the format of the command (e.g. 'fio --json'), 2) to enable easier reuse, and 3) clean up and simplify actual running benchmarks (no TestParser functions and ugly sample output in benchmark files). PiperOrigin-RevId: 324144165
This commit is contained in:
committed by
gVisor bot
parent
6a4bcbdb28
commit
98f9527c04
@@ -0,0 +1 @@
|
||||
FROM nginx:1.15.10
|
||||
@@ -24,5 +24,6 @@ go_test(
|
||||
deps = [
|
||||
"//pkg/test/dockerutil",
|
||||
"//test/benchmarks/harness",
|
||||
"//test/benchmarks/tools",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -16,15 +16,12 @@ package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/test/dockerutil"
|
||||
"gvisor.dev/gvisor/test/benchmarks/harness"
|
||||
"gvisor.dev/gvisor/test/benchmarks/tools"
|
||||
)
|
||||
|
||||
// All possible operations from redis. Note: "ping" will
|
||||
@@ -99,16 +96,10 @@ func BenchmarkRedis(b *testing.B) {
|
||||
b.Fatalf("failed to start redis with: %v", err)
|
||||
}
|
||||
|
||||
// runs redis benchmark -t operation for 100K requests against server.
|
||||
cmd := strings.Split(
|
||||
fmt.Sprintf("redis-benchmark --csv -t %s -h %s -p %d", operation, ip, serverPort), " ")
|
||||
|
||||
// There is no -t PING_BULK for redis-benchmark, so adjust the command in that case.
|
||||
// Note that "ping" will run both PING_INLINE and PING_BULK.
|
||||
if operation == "PING_BULK" {
|
||||
cmd = strings.Split(
|
||||
fmt.Sprintf("redis-benchmark --csv -t ping -h %s -p %d", ip, serverPort), " ")
|
||||
redis := tools.Redis{
|
||||
Operation: operation,
|
||||
}
|
||||
|
||||
// Reset profiles and timer to begin the measurement.
|
||||
server.RestartProfiles()
|
||||
b.ResetTimer()
|
||||
@@ -117,81 +108,16 @@ func BenchmarkRedis(b *testing.B) {
|
||||
defer client.CleanUp(ctx)
|
||||
out, err := client.Run(ctx, dockerutil.RunOpts{
|
||||
Image: "benchmarks/redis",
|
||||
}, cmd...)
|
||||
}, redis.MakeCmd(ip, serverPort)...)
|
||||
if err != nil {
|
||||
b.Fatalf("redis-benchmark failed with: %v", err)
|
||||
}
|
||||
|
||||
// Stop time while we parse results.
|
||||
b.StopTimer()
|
||||
result, err := parseOperation(operation, out)
|
||||
if err != nil {
|
||||
b.Fatalf("parsing result %s failed with err: %v", out, err)
|
||||
}
|
||||
b.ReportMetric(result, operation) // operations per second
|
||||
redis.Report(b, out)
|
||||
b.StartTimer()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// parseOperation grabs the metric operations per second from redis-benchmark output.
|
||||
func parseOperation(operation, data string) (float64, error) {
|
||||
re := regexp.MustCompile(fmt.Sprintf(`"%s( .*)?","(\d*\.\d*)"`, operation))
|
||||
match := re.FindStringSubmatch(data)
|
||||
// If no match, simply don't add it to the result map.
|
||||
if len(match) < 3 {
|
||||
return 0.0, fmt.Errorf("could not find %s in %s", operation, data)
|
||||
}
|
||||
return strconv.ParseFloat(match[2], 64)
|
||||
}
|
||||
|
||||
// TestParser tests the parser on sample data.
|
||||
func TestParser(t *testing.T) {
|
||||
sampleData := `
|
||||
"PING_INLINE","48661.80"
|
||||
"PING_BULK","50301.81"
|
||||
"SET","48923.68"
|
||||
"GET","49382.71"
|
||||
"INCR","49975.02"
|
||||
"LPUSH","49875.31"
|
||||
"RPUSH","50276.52"
|
||||
"LPOP","50327.12"
|
||||
"RPOP","50556.12"
|
||||
"SADD","49504.95"
|
||||
"HSET","49504.95"
|
||||
"SPOP","50025.02"
|
||||
"LPUSH (needed to benchmark LRANGE)","48875.86"
|
||||
"LRANGE_100 (first 100 elements)","33955.86"
|
||||
"LRANGE_300 (first 300 elements)","16550.81"
|
||||
"LRANGE_500 (first 450 elements)","13653.74"
|
||||
"LRANGE_600 (first 600 elements)","11219.57"
|
||||
"MSET (10 keys)","44682.75"
|
||||
`
|
||||
wants := map[string]float64{
|
||||
"PING_INLINE": 48661.80,
|
||||
"PING_BULK": 50301.81,
|
||||
"SET": 48923.68,
|
||||
"GET": 49382.71,
|
||||
"INCR": 49975.02,
|
||||
"LPUSH": 49875.31,
|
||||
"RPUSH": 50276.52,
|
||||
"LPOP": 50327.12,
|
||||
"RPOP": 50556.12,
|
||||
"SADD": 49504.95,
|
||||
"HSET": 49504.95,
|
||||
"SPOP": 50025.02,
|
||||
"LRANGE_100": 33955.86,
|
||||
"LRANGE_300": 16550.81,
|
||||
"LRANGE_500": 13653.74,
|
||||
"LRANGE_600": 11219.57,
|
||||
"MSET": 44682.75,
|
||||
}
|
||||
for op, want := range wants {
|
||||
if got, err := parseOperation(op, sampleData); err != nil {
|
||||
t.Fatalf("failed to parse %s: %v", op, err)
|
||||
} else if want != got {
|
||||
t.Fatalf("wanted %f for op %s, got %f", want, op, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ go_test(
|
||||
deps = [
|
||||
"//pkg/test/dockerutil",
|
||||
"//test/benchmarks/harness",
|
||||
"//test/benchmarks/tools",
|
||||
"@com_github_docker_docker//api/types/mount:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
+29
-228
@@ -15,72 +15,47 @@ package fs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"gvisor.dev/gvisor/pkg/test/dockerutil"
|
||||
"gvisor.dev/gvisor/test/benchmarks/harness"
|
||||
"gvisor.dev/gvisor/test/benchmarks/tools"
|
||||
)
|
||||
|
||||
type fioTestCase struct {
|
||||
test string // test to run: read, write, randread, randwrite.
|
||||
size string // total size to be read/written of format N[GMK] (e.g. 5G).
|
||||
blocksize string // blocksize to be read/write of format N[GMK] (e.g. 4K).
|
||||
iodepth int // iodepth for reads/writes.
|
||||
time int // time to run the test in seconds, usually for rand(read/write).
|
||||
}
|
||||
|
||||
// makeCmdFromTestcase makes a fio command.
|
||||
func (f *fioTestCase) makeCmdFromTestcase(filename string) []string {
|
||||
cmd := []string{"fio", "--output-format=json", "--ioengine=sync"}
|
||||
cmd = append(cmd, fmt.Sprintf("--name=%s", f.test))
|
||||
cmd = append(cmd, fmt.Sprintf("--size=%s", f.size))
|
||||
cmd = append(cmd, fmt.Sprintf("--blocksize=%s", f.blocksize))
|
||||
cmd = append(cmd, fmt.Sprintf("--filename=%s", filename))
|
||||
cmd = append(cmd, fmt.Sprintf("--iodepth=%d", f.iodepth))
|
||||
cmd = append(cmd, fmt.Sprintf("--rw=%s", f.test))
|
||||
if f.time != 0 {
|
||||
cmd = append(cmd, "--time_based")
|
||||
cmd = append(cmd, fmt.Sprintf("--runtime=%d", f.time))
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// BenchmarkFio runs fio on the runtime under test. There are 4 basic test
|
||||
// cases each run on a tmpfs mount and a bind mount. Fio requires root so that
|
||||
// caches can be dropped.
|
||||
func BenchmarkFio(b *testing.B) {
|
||||
testCases := []fioTestCase{
|
||||
fioTestCase{
|
||||
test: "write",
|
||||
size: "5G",
|
||||
blocksize: "1M",
|
||||
iodepth: 4,
|
||||
testCases := []tools.Fio{
|
||||
tools.Fio{
|
||||
Test: "write",
|
||||
Size: "5G",
|
||||
Blocksize: "1M",
|
||||
Iodepth: 4,
|
||||
},
|
||||
fioTestCase{
|
||||
test: "read",
|
||||
size: "5G",
|
||||
blocksize: "1M",
|
||||
iodepth: 4,
|
||||
tools.Fio{
|
||||
Test: "read",
|
||||
Size: "5G",
|
||||
Blocksize: "1M",
|
||||
Iodepth: 4,
|
||||
},
|
||||
fioTestCase{
|
||||
test: "randwrite",
|
||||
size: "5G",
|
||||
blocksize: "4K",
|
||||
iodepth: 4,
|
||||
time: 30,
|
||||
tools.Fio{
|
||||
Test: "randwrite",
|
||||
Size: "5G",
|
||||
Blocksize: "4K",
|
||||
Iodepth: 4,
|
||||
Time: 30,
|
||||
},
|
||||
fioTestCase{
|
||||
test: "randread",
|
||||
size: "5G",
|
||||
blocksize: "4K",
|
||||
iodepth: 4,
|
||||
time: 30,
|
||||
tools.Fio{
|
||||
Test: "randread",
|
||||
Size: "5G",
|
||||
Blocksize: "4K",
|
||||
Iodepth: 4,
|
||||
Time: 30,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -92,7 +67,7 @@ func BenchmarkFio(b *testing.B) {
|
||||
|
||||
for _, fsType := range []mount.Type{mount.TypeBind, mount.TypeTmpfs} {
|
||||
for _, tc := range testCases {
|
||||
testName := strings.Title(tc.test) + strings.Title(string(fsType))
|
||||
testName := strings.Title(tc.Test) + strings.Title(string(fsType))
|
||||
b.Run(testName, func(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
container := machine.GetContainer(ctx, b)
|
||||
@@ -109,7 +84,6 @@ func BenchmarkFio(b *testing.B) {
|
||||
b.Fatalf("failed to make mount: %v", err)
|
||||
}
|
||||
defer mountCleanup()
|
||||
cmd := tc.makeCmdFromTestcase(outfile)
|
||||
|
||||
// Start the container with the mount.
|
||||
if err := container.Spawn(
|
||||
@@ -127,8 +101,8 @@ func BenchmarkFio(b *testing.B) {
|
||||
}
|
||||
|
||||
// For reads, we need a file to read so make one inside the container.
|
||||
if strings.Contains(tc.test, "read") {
|
||||
fallocateCmd := fmt.Sprintf("fallocate -l %s %s", tc.size, outfile)
|
||||
if strings.Contains(tc.Test, "read") {
|
||||
fallocateCmd := fmt.Sprintf("fallocate -l %s %s", tc.Size, outfile)
|
||||
if out, err := container.Exec(ctx, dockerutil.ExecOpts{},
|
||||
strings.Split(fallocateCmd, " ")...); err != nil {
|
||||
b.Fatalf("failed to create readable file on mount: %v, %s", err, out)
|
||||
@@ -139,6 +113,7 @@ func BenchmarkFio(b *testing.B) {
|
||||
if err := harness.DropCaches(machine); err != nil {
|
||||
b.Skipf("failed to drop caches with %v. You probably need root.", err)
|
||||
}
|
||||
cmd := tc.MakeCmd(outfile)
|
||||
container.RestartProfiles()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -148,19 +123,7 @@ func BenchmarkFio(b *testing.B) {
|
||||
b.Fatalf("failed to run cmd %v: %v", cmd, err)
|
||||
}
|
||||
b.StopTimer()
|
||||
// Parse the output and report the metrics.
|
||||
isRead := strings.Contains(tc.test, "read")
|
||||
bw, err := parseBandwidth(data, isRead)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to parse bandwidth from %s with: %v", data, err)
|
||||
}
|
||||
b.ReportMetric(bw, "bandwidth") // in b/s.
|
||||
|
||||
iops, err := parseIOps(data, isRead)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to parse iops from %s with: %v", data, err)
|
||||
}
|
||||
b.ReportMetric(iops, "iops")
|
||||
tc.Report(b, data)
|
||||
// If b.N is used (i.e. we run for an hour), we should drop caches
|
||||
// after each run.
|
||||
if err := harness.DropCaches(machine); err != nil {
|
||||
@@ -205,165 +168,3 @@ func makeMount(machine harness.Machine, mountType mount.Type, target string) (mo
|
||||
return mount.Mount{}, func() {}, fmt.Errorf("illegal mount time not supported: %v", mountType)
|
||||
}
|
||||
}
|
||||
|
||||
// parseBandwidth reports the bandwidth in b/s.
|
||||
func parseBandwidth(data string, isRead bool) (float64, error) {
|
||||
if isRead {
|
||||
result, err := parseFioJSON(data, "read", "bw")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 1024 * result, nil
|
||||
}
|
||||
result, err := parseFioJSON(data, "write", "bw")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 1024 * result, nil
|
||||
}
|
||||
|
||||
// parseIOps reports the write IO per second metric.
|
||||
func parseIOps(data string, isRead bool) (float64, error) {
|
||||
if isRead {
|
||||
return parseFioJSON(data, "read", "iops")
|
||||
}
|
||||
return parseFioJSON(data, "write", "iops")
|
||||
}
|
||||
|
||||
// fioResult is for parsing FioJSON.
|
||||
type fioResult struct {
|
||||
Jobs []fioJob
|
||||
}
|
||||
|
||||
// fioJob is for parsing FioJSON.
|
||||
type fioJob map[string]json.RawMessage
|
||||
|
||||
// fioMetrics is for parsing FioJSON.
|
||||
type fioMetrics map[string]json.RawMessage
|
||||
|
||||
// parseFioJSON parses data and grabs "op" (read or write) and "metric"
|
||||
// (bw or iops) from the JSON.
|
||||
func parseFioJSON(data, op, metric string) (float64, error) {
|
||||
var result fioResult
|
||||
if err := json.Unmarshal([]byte(data), &result); err != nil {
|
||||
return 0, fmt.Errorf("could not unmarshal data: %v", err)
|
||||
}
|
||||
|
||||
if len(result.Jobs) < 1 {
|
||||
return 0, fmt.Errorf("no jobs present to parse")
|
||||
}
|
||||
|
||||
var metrics fioMetrics
|
||||
if err := json.Unmarshal(result.Jobs[0][op], &metrics); err != nil {
|
||||
return 0, fmt.Errorf("could not unmarshal jobs: %v", err)
|
||||
}
|
||||
|
||||
if _, ok := metrics[metric]; !ok {
|
||||
return 0, fmt.Errorf("no metric found for op: %s", op)
|
||||
}
|
||||
return strconv.ParseFloat(string(metrics[metric]), 64)
|
||||
}
|
||||
|
||||
// TestParsers tests that the parsers work on sampleData.
|
||||
func TestParsers(t *testing.T) {
|
||||
sampleData := `
|
||||
{
|
||||
"fio version" : "fio-3.1",
|
||||
"timestamp" : 1554837456,
|
||||
"timestamp_ms" : 1554837456621,
|
||||
"time" : "Tue Apr 9 19:17:36 2019",
|
||||
"jobs" : [
|
||||
{
|
||||
"jobname" : "test",
|
||||
"groupid" : 0,
|
||||
"error" : 0,
|
||||
"eta" : 2147483647,
|
||||
"elapsed" : 1,
|
||||
"job options" : {
|
||||
"name" : "test",
|
||||
"ioengine" : "sync",
|
||||
"size" : "1073741824",
|
||||
"filename" : "/disk/file.dat",
|
||||
"iodepth" : "4",
|
||||
"bs" : "4096",
|
||||
"rw" : "write"
|
||||
},
|
||||
"read" : {
|
||||
"io_bytes" : 0,
|
||||
"io_kbytes" : 0,
|
||||
"bw" : 123456,
|
||||
"iops" : 1234.5678,
|
||||
"runtime" : 0,
|
||||
"total_ios" : 0,
|
||||
"short_ios" : 0,
|
||||
"bw_min" : 0,
|
||||
"bw_max" : 0,
|
||||
"bw_agg" : 0.000000,
|
||||
"bw_mean" : 0.000000,
|
||||
"bw_dev" : 0.000000,
|
||||
"bw_samples" : 0,
|
||||
"iops_min" : 0,
|
||||
"iops_max" : 0,
|
||||
"iops_mean" : 0.000000,
|
||||
"iops_stddev" : 0.000000,
|
||||
"iops_samples" : 0
|
||||
},
|
||||
"write" : {
|
||||
"io_bytes" : 1073741824,
|
||||
"io_kbytes" : 1048576,
|
||||
"bw" : 1753471,
|
||||
"iops" : 438367.892977,
|
||||
"runtime" : 598,
|
||||
"total_ios" : 262144,
|
||||
"bw_min" : 1731120,
|
||||
"bw_max" : 1731120,
|
||||
"bw_agg" : 98.725328,
|
||||
"bw_mean" : 1731120.000000,
|
||||
"bw_dev" : 0.000000,
|
||||
"bw_samples" : 1,
|
||||
"iops_min" : 432780,
|
||||
"iops_max" : 432780,
|
||||
"iops_mean" : 432780.000000,
|
||||
"iops_stddev" : 0.000000,
|
||||
"iops_samples" : 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
// WriteBandwidth.
|
||||
got, err := parseBandwidth(sampleData, false)
|
||||
var want float64 = 1753471.0 * 1024
|
||||
if err != nil {
|
||||
t.Fatalf("parse failed with err: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("got: %f, want: %f", got, want)
|
||||
}
|
||||
|
||||
// ReadBandwidth.
|
||||
got, err = parseBandwidth(sampleData, true)
|
||||
want = 123456 * 1024
|
||||
if err != nil {
|
||||
t.Fatalf("parse failed with err: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("got: %f, want: %f", got, want)
|
||||
}
|
||||
|
||||
// WriteIOps.
|
||||
got, err = parseIOps(sampleData, false)
|
||||
want = 438367.892977
|
||||
if err != nil {
|
||||
t.Fatalf("parse failed with err: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("got: %f, want: %f", got, want)
|
||||
}
|
||||
|
||||
// ReadIOps.
|
||||
got, err = parseIOps(sampleData, true)
|
||||
want = 1234.5678
|
||||
if err != nil {
|
||||
t.Fatalf("parse failed with err: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("got: %f, want: %f", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ go_test(
|
||||
srcs = [
|
||||
"httpd_test.go",
|
||||
"iperf_test.go",
|
||||
"nginx_test.go",
|
||||
"node_test.go",
|
||||
],
|
||||
library = ":network",
|
||||
@@ -27,5 +28,6 @@ go_test(
|
||||
"//pkg/test/dockerutil",
|
||||
"//pkg/test/testutil",
|
||||
"//test/benchmarks/harness",
|
||||
"//test/benchmarks/tools",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -16,12 +16,11 @@ package network
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/test/dockerutil"
|
||||
"gvisor.dev/gvisor/test/benchmarks/harness"
|
||||
"gvisor.dev/gvisor/test/benchmarks/tools"
|
||||
)
|
||||
|
||||
// see Dockerfile '//images/benchmarks/httpd'.
|
||||
@@ -52,13 +51,16 @@ func BenchmarkHttpdConcurrency(b *testing.B) {
|
||||
defer serverMachine.CleanUp()
|
||||
|
||||
// The test iterates over client concurrency, so set other parameters.
|
||||
requests := 10000
|
||||
concurrency := []int{1, 5, 10, 25}
|
||||
doc := docs["10Kb"]
|
||||
|
||||
for _, c := range concurrency {
|
||||
b.Run(fmt.Sprintf("%d", c), func(b *testing.B) {
|
||||
runHttpd(b, clientMachine, serverMachine, doc, requests, c)
|
||||
hey := &tools.Hey{
|
||||
Requests: 10000,
|
||||
Concurrency: c,
|
||||
Doc: docs["10Kb"],
|
||||
}
|
||||
runHttpd(b, clientMachine, serverMachine, hey)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -78,18 +80,20 @@ func BenchmarkHttpdDocSize(b *testing.B) {
|
||||
}
|
||||
defer serverMachine.CleanUp()
|
||||
|
||||
requests := 10000
|
||||
concurrency := 1
|
||||
|
||||
for name, filename := range docs {
|
||||
b.Run(name, func(b *testing.B) {
|
||||
runHttpd(b, clientMachine, serverMachine, filename, requests, concurrency)
|
||||
hey := &tools.Hey{
|
||||
Requests: 10000,
|
||||
Concurrency: 1,
|
||||
Doc: filename,
|
||||
}
|
||||
runHttpd(b, clientMachine, serverMachine, hey)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// runHttpd runs a single test run.
|
||||
func runHttpd(b *testing.B, clientMachine, serverMachine harness.Machine, doc string, requests, concurrency int) {
|
||||
func runHttpd(b *testing.B, clientMachine, serverMachine harness.Machine, hey *tools.Hey) {
|
||||
b.Helper()
|
||||
|
||||
// Grab a container from the server.
|
||||
@@ -98,11 +102,11 @@ func runHttpd(b *testing.B, clientMachine, serverMachine harness.Machine, doc st
|
||||
defer server.CleanUp(ctx)
|
||||
|
||||
// Copy the docs to /tmp and serve from there.
|
||||
cmd := "mkdir -p /tmp/html; cp -r /local /tmp/html/.; apache2 -X"
|
||||
cmd := "mkdir -p /tmp/html; cp -r /local/* /tmp/html/.; apache2 -X"
|
||||
port := 80
|
||||
|
||||
// Start the server.
|
||||
server.Spawn(ctx, dockerutil.RunOpts{
|
||||
if err := server.Spawn(ctx, dockerutil.RunOpts{
|
||||
Image: "benchmarks/httpd",
|
||||
Ports: []int{port},
|
||||
Env: []string{
|
||||
@@ -113,7 +117,9 @@ func runHttpd(b *testing.B, clientMachine, serverMachine harness.Machine, doc st
|
||||
"APACHE_LOG_DIR=/tmp",
|
||||
"APACHE_PID_FILE=/tmp/apache.pid",
|
||||
},
|
||||
}, "sh", "-c", cmd)
|
||||
}, "sh", "-c", cmd); err != nil {
|
||||
b.Fatalf("failed to start server: %v")
|
||||
}
|
||||
|
||||
ip, err := serverMachine.IPAddress()
|
||||
if err != nil {
|
||||
@@ -132,146 +138,18 @@ func runHttpd(b *testing.B, clientMachine, serverMachine harness.Machine, doc st
|
||||
client := clientMachine.GetNativeContainer(ctx, b)
|
||||
defer client.CleanUp(ctx)
|
||||
|
||||
path := fmt.Sprintf("http://%s:%d/%s", ip, servingPort, doc)
|
||||
// See apachebench (ab) for flags.
|
||||
cmd = fmt.Sprintf("ab -n %d -c %d %s", requests, concurrency, path)
|
||||
|
||||
b.ResetTimer()
|
||||
server.RestartProfiles()
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err := client.Run(ctx, dockerutil.RunOpts{
|
||||
Image: "benchmarks/ab",
|
||||
}, "sh", "-c", cmd)
|
||||
Image: "benchmarks/hey",
|
||||
}, hey.MakeCmd(ip, servingPort)...)
|
||||
if err != nil {
|
||||
b.Fatalf("run failed with: %v", err)
|
||||
}
|
||||
|
||||
b.StopTimer()
|
||||
|
||||
// Parse and report custom metrics.
|
||||
transferRate, err := parseTransferRate(out)
|
||||
if err != nil {
|
||||
b.Logf("failed to parse transferrate: %v", err)
|
||||
}
|
||||
b.ReportMetric(transferRate*1024, "transfer_rate") // Convert from Kb/s to b/s.
|
||||
|
||||
latency, err := parseLatency(out)
|
||||
if err != nil {
|
||||
b.Logf("failed to parse latency: %v", err)
|
||||
}
|
||||
b.ReportMetric(latency/1000, "mean_latency") // Convert from ms to s.
|
||||
|
||||
reqPerSecond, err := parseRequestsPerSecond(out)
|
||||
if err != nil {
|
||||
b.Logf("failed to parse requests per second: %v", err)
|
||||
}
|
||||
b.ReportMetric(reqPerSecond, "requests_per_second")
|
||||
|
||||
hey.Report(b, out)
|
||||
b.StartTimer()
|
||||
}
|
||||
}
|
||||
|
||||
var transferRateRE = regexp.MustCompile(`Transfer rate:\s+(\d+\.?\d+?)\s+\[Kbytes/sec\]\s+received`)
|
||||
|
||||
// parseTransferRate parses transfer rate from apachebench output.
|
||||
func parseTransferRate(data string) (float64, error) {
|
||||
match := transferRateRE.FindStringSubmatch(data)
|
||||
if len(match) < 2 {
|
||||
return 0, fmt.Errorf("failed get bandwidth: %s", data)
|
||||
}
|
||||
return strconv.ParseFloat(match[1], 64)
|
||||
}
|
||||
|
||||
var latencyRE = regexp.MustCompile(`Total:\s+\d+\s+(\d+)\s+(\d+\.?\d+?)\s+\d+\s+\d+\s`)
|
||||
|
||||
// parseLatency parses latency from apachebench output.
|
||||
func parseLatency(data string) (float64, error) {
|
||||
match := latencyRE.FindStringSubmatch(data)
|
||||
if len(match) < 2 {
|
||||
return 0, fmt.Errorf("failed get bandwidth: %s", data)
|
||||
}
|
||||
return strconv.ParseFloat(match[1], 64)
|
||||
}
|
||||
|
||||
var requestsPerSecondRE = regexp.MustCompile(`Requests per second:\s+(\d+\.?\d+?)\s+`)
|
||||
|
||||
// parseRequestsPerSecond parses requests per second from apachebench output.
|
||||
func parseRequestsPerSecond(data string) (float64, error) {
|
||||
match := requestsPerSecondRE.FindStringSubmatch(data)
|
||||
if len(match) < 2 {
|
||||
return 0, fmt.Errorf("failed get bandwidth: %s", data)
|
||||
}
|
||||
return strconv.ParseFloat(match[1], 64)
|
||||
}
|
||||
|
||||
// Sample output from apachebench.
|
||||
const sampleData = `This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
|
||||
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
|
||||
Licensed to The Apache Software Foundation, http://www.apache.org/
|
||||
|
||||
Benchmarking 10.10.10.10 (be patient).....done
|
||||
|
||||
|
||||
Server Software: Apache/2.4.38
|
||||
Server Hostname: 10.10.10.10
|
||||
Server Port: 80
|
||||
|
||||
Document Path: /latin10k.txt
|
||||
Document Length: 210 bytes
|
||||
|
||||
Concurrency Level: 1
|
||||
Time taken for tests: 0.180 seconds
|
||||
Complete requests: 100
|
||||
Failed requests: 0
|
||||
Non-2xx responses: 100
|
||||
Total transferred: 38800 bytes
|
||||
HTML transferred: 21000 bytes
|
||||
Requests per second: 556.44 [#/sec] (mean)
|
||||
Time per request: 1.797 [ms] (mean)
|
||||
Time per request: 1.797 [ms] (mean, across all concurrent requests)
|
||||
Transfer rate: 210.84 [Kbytes/sec] received
|
||||
|
||||
Connection Times (ms)
|
||||
min mean[+/-sd] median max
|
||||
Connect: 0 0 0.2 0 2
|
||||
Processing: 1 2 1.0 1 8
|
||||
Waiting: 1 1 1.0 1 7
|
||||
Total: 1 2 1.2 1 10
|
||||
|
||||
Percentage of the requests served within a certain time (ms)
|
||||
50% 1
|
||||
66% 2
|
||||
75% 2
|
||||
80% 2
|
||||
90% 2
|
||||
95% 3
|
||||
98% 7
|
||||
99% 10
|
||||
100% 10 (longest request)`
|
||||
|
||||
// TestParsers checks the parsers work.
|
||||
func TestParsers(t *testing.T) {
|
||||
want := 210.84
|
||||
got, err := parseTransferRate(sampleData)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse transfer rate with error: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("parseTransferRate got: %f, want: %f", got, want)
|
||||
}
|
||||
|
||||
want = 2.0
|
||||
got, err = parseLatency(sampleData)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse transfer rate with error: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("parseLatency got: %f, want: %f", got, want)
|
||||
}
|
||||
|
||||
want = 556.44
|
||||
got, err = parseRequestsPerSecond(sampleData)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse transfer rate with error: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("parseRequestsPerSecond got: %f, want: %f", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,19 +15,18 @@ package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/test/dockerutil"
|
||||
"gvisor.dev/gvisor/pkg/test/testutil"
|
||||
"gvisor.dev/gvisor/test/benchmarks/harness"
|
||||
"gvisor.dev/gvisor/test/benchmarks/tools"
|
||||
)
|
||||
|
||||
func BenchmarkIperf(b *testing.B) {
|
||||
const time = 10 // time in seconds to run the client.
|
||||
iperf := tools.Iperf{
|
||||
Time: 10, // time in seconds to run client.
|
||||
}
|
||||
|
||||
clientMachine, err := h.GetMachine()
|
||||
if err != nil {
|
||||
@@ -92,10 +91,6 @@ func BenchmarkIperf(b *testing.B) {
|
||||
if err := harness.WaitUntilServing(ctx, clientMachine, ip, servingPort); err != nil {
|
||||
b.Fatalf("failed to wait for server: %v", err)
|
||||
}
|
||||
|
||||
// iperf report in Kb realtime
|
||||
cmd := fmt.Sprintf("iperf -f K --realtime --time %d -c %s -p %d", time, ip.String(), servingPort)
|
||||
|
||||
// Run the client.
|
||||
b.ResetTimer()
|
||||
|
||||
@@ -105,46 +100,14 @@ func BenchmarkIperf(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err := client.Run(ctx, dockerutil.RunOpts{
|
||||
Image: "benchmarks/iperf",
|
||||
}, strings.Split(cmd, " ")...)
|
||||
}, iperf.MakeCmd(ip, servingPort)...)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to run client: %v", err)
|
||||
}
|
||||
b.StopTimer()
|
||||
|
||||
// Parse bandwidth and report it.
|
||||
bW, err := bandwidth(out)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to parse bandwitdth from %s: %v", out, err)
|
||||
}
|
||||
b.ReportMetric(bW*1024, "bandwidth") // Convert from Kb/s to b/s.
|
||||
iperf.Report(b, out)
|
||||
b.StartTimer()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// bandwidth parses the Bandwidth number from an iperf report. A sample is below.
|
||||
func bandwidth(data string) (float64, error) {
|
||||
re := regexp.MustCompile(`\[\s*\d+\][^\n]+\s+(\d+\.?\d*)\s+KBytes/sec`)
|
||||
match := re.FindStringSubmatch(data)
|
||||
if len(match) < 1 {
|
||||
return 0, fmt.Errorf("failed get bandwidth: %s", data)
|
||||
}
|
||||
return strconv.ParseFloat(match[1], 64)
|
||||
}
|
||||
|
||||
func TestParser(t *testing.T) {
|
||||
sampleData := `
|
||||
------------------------------------------------------------
|
||||
Client connecting to 10.138.15.215, TCP port 32779
|
||||
TCP window size: 45.0 KByte (default)
|
||||
------------------------------------------------------------
|
||||
[ 3] local 10.138.15.216 port 32866 connected with 10.138.15.215 port 32779
|
||||
[ ID] Interval Transfer Bandwidth
|
||||
[ 3] 0.0-10.0 sec 459520 KBytes 45900 KBytes/sec
|
||||
`
|
||||
bandwidth, err := bandwidth(sampleData)
|
||||
if err != nil || bandwidth != 45900 {
|
||||
t.Fatalf("failed with: %v and %f", err, bandwidth)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2020 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/test/dockerutil"
|
||||
"gvisor.dev/gvisor/test/benchmarks/harness"
|
||||
"gvisor.dev/gvisor/test/benchmarks/tools"
|
||||
)
|
||||
|
||||
// BenchmarkNginxConcurrency iterates the concurrency argument and tests
|
||||
// how well the runtime under test handles requests in parallel.
|
||||
// TODO(zkoopmans): Update with different doc sizes like Httpd.
|
||||
func BenchmarkNginxConcurrency(b *testing.B) {
|
||||
// Grab a machine for the client and server.
|
||||
clientMachine, err := h.GetMachine()
|
||||
if err != nil {
|
||||
b.Fatalf("failed to get client: %v", err)
|
||||
}
|
||||
defer clientMachine.CleanUp()
|
||||
|
||||
serverMachine, err := h.GetMachine()
|
||||
if err != nil {
|
||||
b.Fatalf("failed to get server: %v", err)
|
||||
}
|
||||
defer serverMachine.CleanUp()
|
||||
|
||||
concurrency := []int{1, 5, 10, 25}
|
||||
for _, c := range concurrency {
|
||||
b.Run(fmt.Sprintf("%d", c), func(b *testing.B) {
|
||||
hey := &tools.Hey{
|
||||
Requests: 10000,
|
||||
Concurrency: c,
|
||||
}
|
||||
runNginx(b, clientMachine, serverMachine, hey)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// runHttpd runs a single test run.
|
||||
func runNginx(b *testing.B, clientMachine, serverMachine harness.Machine, hey *tools.Hey) {
|
||||
b.Helper()
|
||||
|
||||
// Grab a container from the server.
|
||||
ctx := context.Background()
|
||||
server := serverMachine.GetContainer(ctx, b)
|
||||
defer server.CleanUp(ctx)
|
||||
|
||||
port := 80
|
||||
// Start the server.
|
||||
if err := server.Spawn(ctx,
|
||||
dockerutil.RunOpts{
|
||||
Image: "benchmarks/nginx",
|
||||
Ports: []int{port},
|
||||
}); err != nil {
|
||||
b.Fatalf("server failed to start: %v", err)
|
||||
}
|
||||
|
||||
ip, err := serverMachine.IPAddress()
|
||||
if err != nil {
|
||||
b.Fatalf("failed to find server ip: %v", err)
|
||||
}
|
||||
|
||||
servingPort, err := server.FindPort(ctx, port)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to find server port %d: %v", port, err)
|
||||
}
|
||||
|
||||
// Check the server is serving.
|
||||
harness.WaitUntilServing(ctx, clientMachine, ip, servingPort)
|
||||
|
||||
// Grab a client.
|
||||
client := clientMachine.GetNativeContainer(ctx, b)
|
||||
defer client.CleanUp(ctx)
|
||||
|
||||
b.ResetTimer()
|
||||
server.RestartProfiles()
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err := client.Run(ctx, dockerutil.RunOpts{
|
||||
Image: "benchmarks/hey",
|
||||
}, hey.MakeCmd(ip, servingPort)...)
|
||||
if err != nil {
|
||||
b.Fatalf("run failed with: %v", err)
|
||||
}
|
||||
b.StopTimer()
|
||||
hey.Report(b, out)
|
||||
b.StartTimer()
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,12 @@ package network
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/test/dockerutil"
|
||||
"gvisor.dev/gvisor/test/benchmarks/harness"
|
||||
"gvisor.dev/gvisor/test/benchmarks/tools"
|
||||
)
|
||||
|
||||
// BenchmarkNode runs 10K requests using 'hey' against a Node server run on
|
||||
@@ -36,13 +34,17 @@ func BenchmarkNode(b *testing.B) {
|
||||
|
||||
for _, c := range concurrency {
|
||||
b.Run(fmt.Sprintf("Concurrency%d", c), func(b *testing.B) {
|
||||
runNode(b, requests, c)
|
||||
hey := &tools.Hey{
|
||||
Requests: requests,
|
||||
Concurrency: c,
|
||||
}
|
||||
runNode(b, hey)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// runNode runs the test for a given # of requests and concurrency.
|
||||
func runNode(b *testing.B, requests, concurrency int) {
|
||||
func runNode(b *testing.B, hey *tools.Hey) {
|
||||
b.Helper()
|
||||
|
||||
// The machine to hold Redis and the Node Server.
|
||||
@@ -106,7 +108,7 @@ func runNode(b *testing.B, requests, concurrency int) {
|
||||
// Wait until the Client sees the server as up.
|
||||
harness.WaitUntilServing(ctx, clientMachine, servingIP, servingPort)
|
||||
|
||||
heyCmd := strings.Split(fmt.Sprintf("hey -n %d -c %d http://%s:%d/", requests, concurrency, servingIP, servingPort), " ")
|
||||
heyCmd := hey.MakeCmd(servingIP, servingPort)
|
||||
|
||||
nodeApp.RestartProfiles()
|
||||
b.ResetTimer()
|
||||
@@ -123,139 +125,7 @@ func runNode(b *testing.B, requests, concurrency int) {
|
||||
|
||||
// Stop the timer to parse the data and report stats.
|
||||
b.StopTimer()
|
||||
requests, err := parseHeyRequestsPerSecond(out)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to parse requests per second: %v", err)
|
||||
}
|
||||
b.ReportMetric(requests, "requests_per_second")
|
||||
|
||||
bw, err := parseHeyBandwidth(out)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to parse bandwidth: %v", err)
|
||||
}
|
||||
b.ReportMetric(bw, "bandwidth")
|
||||
|
||||
ave, err := parseHeyAverageLatency(out)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to parse average latency: %v", err)
|
||||
}
|
||||
b.ReportMetric(ave, "average_latency")
|
||||
hey.Report(b, out)
|
||||
b.StartTimer()
|
||||
}
|
||||
}
|
||||
|
||||
var heyReqPerSecondRE = regexp.MustCompile(`Requests/sec:\s*(\d+\.?\d+?)\s+`)
|
||||
|
||||
// parseHeyRequestsPerSecond finds requests per second from hey output.
|
||||
func parseHeyRequestsPerSecond(data string) (float64, error) {
|
||||
match := heyReqPerSecondRE.FindStringSubmatch(data)
|
||||
if len(match) < 2 {
|
||||
return 0, fmt.Errorf("failed get bandwidth: %s", data)
|
||||
}
|
||||
return strconv.ParseFloat(match[1], 64)
|
||||
}
|
||||
|
||||
var heyAverageLatencyRE = regexp.MustCompile(`Average:\s*(\d+\.?\d+?)\s+secs`)
|
||||
|
||||
// parseHeyAverageLatency finds Average Latency in seconds form hey output.
|
||||
func parseHeyAverageLatency(data string) (float64, error) {
|
||||
match := heyAverageLatencyRE.FindStringSubmatch(data)
|
||||
if len(match) < 2 {
|
||||
return 0, fmt.Errorf("failed get average latency match%d : %s", len(match), data)
|
||||
}
|
||||
return strconv.ParseFloat(match[1], 64)
|
||||
}
|
||||
|
||||
var heySizePerRequestRE = regexp.MustCompile(`Size/request:\s*(\d+\.?\d+?)\s+bytes`)
|
||||
|
||||
// parseHeyBandwidth computes bandwidth from request/sec * bytes/request
|
||||
// and reports in bytes/second.
|
||||
func parseHeyBandwidth(data string) (float64, error) {
|
||||
match := heyReqPerSecondRE.FindStringSubmatch(data)
|
||||
if len(match) < 2 {
|
||||
return 0, fmt.Errorf("failed get requests per second: %s", data)
|
||||
}
|
||||
reqPerSecond, err := strconv.ParseFloat(match[1], 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to convert %s to float", match[1])
|
||||
}
|
||||
|
||||
match = heySizePerRequestRE.FindStringSubmatch(data)
|
||||
if len(match) < 2 {
|
||||
return 0, fmt.Errorf("failed get average latency: %s", data)
|
||||
}
|
||||
requestSize, err := strconv.ParseFloat(match[1], 64)
|
||||
return requestSize * reqPerSecond, err
|
||||
}
|
||||
|
||||
// TestHeyParsers tests that the parsers work with sample output.
|
||||
func TestHeyParsers(t *testing.T) {
|
||||
sampleData := `
|
||||
Summary:
|
||||
Total: 2.2391 secs
|
||||
Slowest: 1.6292 secs
|
||||
Fastest: 0.0066 secs
|
||||
Average: 0.5351 secs
|
||||
Requests/sec: 89.3202
|
||||
|
||||
Total data: 841200 bytes
|
||||
Size/request: 4206 bytes
|
||||
|
||||
Response time histogram:
|
||||
0.007 [1] |
|
||||
0.169 [0] |
|
||||
0.331 [149] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
|
||||
0.493 [0] |
|
||||
0.656 [0] |
|
||||
0.818 [0] |
|
||||
0.980 [0] |
|
||||
1.142 [0] |
|
||||
1.305 [0] |
|
||||
1.467 [49] |■■■■■■■■■■■■■
|
||||
1.629 [1] |
|
||||
|
||||
|
||||
Latency distribution:
|
||||
10% in 0.2149 secs
|
||||
25% in 0.2449 secs
|
||||
50% in 0.2703 secs
|
||||
75% in 1.3315 secs
|
||||
90% in 1.4045 secs
|
||||
95% in 1.4232 secs
|
||||
99% in 1.4362 secs
|
||||
|
||||
Details (average, fastest, slowest):
|
||||
DNS+dialup: 0.0002 secs, 0.0066 secs, 1.6292 secs
|
||||
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
|
||||
req write: 0.0000 secs, 0.0000 secs, 0.0012 secs
|
||||
resp wait: 0.5225 secs, 0.0064 secs, 1.4346 secs
|
||||
resp read: 0.0122 secs, 0.0001 secs, 0.2006 secs
|
||||
|
||||
Status code distribution:
|
||||
[200] 200 responses
|
||||
`
|
||||
want := 89.3202
|
||||
got, err := parseHeyRequestsPerSecond(sampleData)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse request per second with: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("got: %f, want: %f", got, want)
|
||||
}
|
||||
|
||||
want = 89.3202 * 4206
|
||||
got, err = parseHeyBandwidth(sampleData)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse bandwidth with: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("got: %f, want: %f", got, want)
|
||||
}
|
||||
|
||||
want = 0.5351
|
||||
got, err = parseHeyAverageLatency(sampleData)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse average latency with: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("got: %f, want: %f", got, want)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
load("//tools:defs.bzl", "go_library", "go_test")
|
||||
|
||||
package(licenses = ["notice"])
|
||||
|
||||
go_library(
|
||||
name = "tools",
|
||||
srcs = [
|
||||
"ab.go",
|
||||
"fio.go",
|
||||
"hey.go",
|
||||
"iperf.go",
|
||||
"redis.go",
|
||||
"tools.go",
|
||||
],
|
||||
visibility = ["//:sandbox"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "tools_test",
|
||||
size = "small",
|
||||
srcs = [
|
||||
"ab_test.go",
|
||||
"fio_test.go",
|
||||
"hey_test.go",
|
||||
"iperf_test.go",
|
||||
"redis_test.go",
|
||||
],
|
||||
library = ":tools",
|
||||
)
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright 2020 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ApacheBench is for the client application ApacheBench.
|
||||
type ApacheBench struct {
|
||||
Requests int
|
||||
Concurrency int
|
||||
Doc string
|
||||
// TODO(zkoopmans): support KeepAlive and pass option to enable.
|
||||
}
|
||||
|
||||
// MakeCmd makes an ApacheBench command.
|
||||
func (a *ApacheBench) MakeCmd(ip net.IP, port int) []string {
|
||||
path := fmt.Sprintf("http://%s:%d/%s", ip, port, a.Doc)
|
||||
// See apachebench (ab) for flags.
|
||||
cmd := fmt.Sprintf("ab -n %d -c %d %s", a.Requests, a.Concurrency, path)
|
||||
return []string{"sh", "-c", cmd}
|
||||
}
|
||||
|
||||
// Report parses and reports metrics from ApacheBench output.
|
||||
func (a *ApacheBench) Report(b *testing.B, output string) {
|
||||
// Parse and report custom metrics.
|
||||
transferRate, err := a.parseTransferRate(output)
|
||||
if err != nil {
|
||||
b.Logf("failed to parse transferrate: %v", err)
|
||||
}
|
||||
b.ReportMetric(transferRate*1024, "transfer_rate_b/s") // Convert from Kb/s to b/s.
|
||||
|
||||
latency, err := a.parseLatency(output)
|
||||
if err != nil {
|
||||
b.Logf("failed to parse latency: %v", err)
|
||||
}
|
||||
b.ReportMetric(latency/1000, "mean_latency_secs") // Convert from ms to s.
|
||||
|
||||
reqPerSecond, err := a.parseRequestsPerSecond(output)
|
||||
if err != nil {
|
||||
b.Logf("failed to parse requests per second: %v", err)
|
||||
}
|
||||
b.ReportMetric(reqPerSecond, "requests_per_second")
|
||||
}
|
||||
|
||||
var transferRateRE = regexp.MustCompile(`Transfer rate:\s+(\d+\.?\d+?)\s+\[Kbytes/sec\]\s+received`)
|
||||
|
||||
// parseTransferRate parses transfer rate from ApacheBench output.
|
||||
func (a *ApacheBench) parseTransferRate(data string) (float64, error) {
|
||||
match := transferRateRE.FindStringSubmatch(data)
|
||||
if len(match) < 2 {
|
||||
return 0, fmt.Errorf("failed get bandwidth: %s", data)
|
||||
}
|
||||
return strconv.ParseFloat(match[1], 64)
|
||||
}
|
||||
|
||||
var latencyRE = regexp.MustCompile(`Total:\s+\d+\s+(\d+)\s+(\d+\.?\d+?)\s+\d+\s+\d+\s`)
|
||||
|
||||
// parseLatency parses latency from ApacheBench output.
|
||||
func (a *ApacheBench) parseLatency(data string) (float64, error) {
|
||||
match := latencyRE.FindStringSubmatch(data)
|
||||
if len(match) < 2 {
|
||||
return 0, fmt.Errorf("failed get bandwidth: %s", data)
|
||||
}
|
||||
return strconv.ParseFloat(match[1], 64)
|
||||
}
|
||||
|
||||
var requestsPerSecondRE = regexp.MustCompile(`Requests per second:\s+(\d+\.?\d+?)\s+`)
|
||||
|
||||
// parseRequestsPerSecond parses requests per second from ApacheBench output.
|
||||
func (a *ApacheBench) parseRequestsPerSecond(data string) (float64, error) {
|
||||
match := requestsPerSecondRE.FindStringSubmatch(data)
|
||||
if len(match) < 2 {
|
||||
return 0, fmt.Errorf("failed get bandwidth: %s", data)
|
||||
}
|
||||
return strconv.ParseFloat(match[1], 64)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2020 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tools
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestApacheBench checks the ApacheBench parsers on sample output.
|
||||
func TestApacheBench(t *testing.T) {
|
||||
// Sample output from apachebench.
|
||||
sampleData := `This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
|
||||
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
|
||||
Licensed to The Apache Software Foundation, http://www.apache.org/
|
||||
|
||||
Benchmarking 10.10.10.10 (be patient).....done
|
||||
|
||||
|
||||
Server Software: Apache/2.4.38
|
||||
Server Hostname: 10.10.10.10
|
||||
Server Port: 80
|
||||
|
||||
Document Path: /latin10k.txt
|
||||
Document Length: 210 bytes
|
||||
|
||||
Concurrency Level: 1
|
||||
Time taken for tests: 0.180 seconds
|
||||
Complete requests: 100
|
||||
Failed requests: 0
|
||||
Non-2xx responses: 100
|
||||
Total transferred: 38800 bytes
|
||||
HTML transferred: 21000 bytes
|
||||
Requests per second: 556.44 [#/sec] (mean)
|
||||
Time per request: 1.797 [ms] (mean)
|
||||
Time per request: 1.797 [ms] (mean, across all concurrent requests)
|
||||
Transfer rate: 210.84 [Kbytes/sec] received
|
||||
|
||||
Connection Times (ms)
|
||||
min mean[+/-sd] median max
|
||||
Connect: 0 0 0.2 0 2
|
||||
Processing: 1 2 1.0 1 8
|
||||
Waiting: 1 1 1.0 1 7
|
||||
Total: 1 2 1.2 1 10
|
||||
|
||||
Percentage of the requests served within a certain time (ms)
|
||||
50% 1
|
||||
66% 2
|
||||
75% 2
|
||||
80% 2
|
||||
90% 2
|
||||
95% 3
|
||||
98% 7
|
||||
99% 10
|
||||
100% 10 (longest request)`
|
||||
|
||||
ab := ApacheBench{}
|
||||
want := 210.84
|
||||
got, err := ab.parseTransferRate(sampleData)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse transfer rate with error: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("parseTransferRate got: %f, want: %f", got, want)
|
||||
}
|
||||
|
||||
want = 2.0
|
||||
got, err = ab.parseLatency(sampleData)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse transfer rate with error: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("parseLatency got: %f, want: %f", got, want)
|
||||
}
|
||||
|
||||
want = 556.44
|
||||
got, err = ab.parseRequestsPerSecond(sampleData)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse transfer rate with error: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("parseRequestsPerSecond got: %f, want: %f", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright 2020 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Fio makes 'fio' commands and parses their output.
|
||||
type Fio struct {
|
||||
Test string // test to run: read, write, randread, randwrite.
|
||||
Size string // total size to be read/written of format N[GMK] (e.g. 5G).
|
||||
Blocksize string // blocksize to be read/write of format N[GMK] (e.g. 4K).
|
||||
Iodepth int // iodepth for reads/writes.
|
||||
Time int // time to run the test in seconds, usually for rand(read/write).
|
||||
}
|
||||
|
||||
// MakeCmd makes a 'fio' command.
|
||||
func (f *Fio) MakeCmd(filename string) []string {
|
||||
cmd := []string{"fio", "--output-format=json", "--ioengine=sync"}
|
||||
cmd = append(cmd, fmt.Sprintf("--name=%s", f.Test))
|
||||
cmd = append(cmd, fmt.Sprintf("--size=%s", f.Size))
|
||||
cmd = append(cmd, fmt.Sprintf("--blocksize=%s", f.Blocksize))
|
||||
cmd = append(cmd, fmt.Sprintf("--filename=%s", filename))
|
||||
cmd = append(cmd, fmt.Sprintf("--iodepth=%d", f.Iodepth))
|
||||
cmd = append(cmd, fmt.Sprintf("--rw=%s", f.Test))
|
||||
if f.Time != 0 {
|
||||
cmd = append(cmd, "--time_based")
|
||||
cmd = append(cmd, fmt.Sprintf("--runtime=%d", f.Time))
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Report reports metrics based on output from an 'fio' command.
|
||||
func (f *Fio) Report(b *testing.B, output string) {
|
||||
b.Helper()
|
||||
// Parse the output and report the metrics.
|
||||
isRead := strings.Contains(f.Test, "read")
|
||||
bw, err := f.parseBandwidth(output, isRead)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to parse bandwidth from %s with: %v", output, err)
|
||||
}
|
||||
b.ReportMetric(bw, "bandwidth_b/s") // in b/s.
|
||||
|
||||
iops, err := f.parseIOps(output, isRead)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to parse iops from %s with: %v", output, err)
|
||||
}
|
||||
b.ReportMetric(iops, "iops")
|
||||
}
|
||||
|
||||
// parseBandwidth reports the bandwidth in b/s.
|
||||
func (f *Fio) parseBandwidth(data string, isRead bool) (float64, error) {
|
||||
if isRead {
|
||||
result, err := f.parseFioJSON(data, "read", "bw")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 1024 * result, nil
|
||||
}
|
||||
result, err := f.parseFioJSON(data, "write", "bw")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 1024 * result, nil
|
||||
}
|
||||
|
||||
// parseIOps reports the write IO per second metric.
|
||||
func (f *Fio) parseIOps(data string, isRead bool) (float64, error) {
|
||||
if isRead {
|
||||
return f.parseFioJSON(data, "read", "iops")
|
||||
}
|
||||
return f.parseFioJSON(data, "write", "iops")
|
||||
}
|
||||
|
||||
// fioResult is for parsing FioJSON.
|
||||
type fioResult struct {
|
||||
Jobs []fioJob
|
||||
}
|
||||
|
||||
// fioJob is for parsing FioJSON.
|
||||
type fioJob map[string]json.RawMessage
|
||||
|
||||
// fioMetrics is for parsing FioJSON.
|
||||
type fioMetrics map[string]json.RawMessage
|
||||
|
||||
// parseFioJSON parses data and grabs "op" (read or write) and "metric"
|
||||
// (bw or iops) from the JSON.
|
||||
func (f *Fio) parseFioJSON(data, op, metric string) (float64, error) {
|
||||
var result fioResult
|
||||
if err := json.Unmarshal([]byte(data), &result); err != nil {
|
||||
return 0, fmt.Errorf("could not unmarshal data: %v", err)
|
||||
}
|
||||
|
||||
if len(result.Jobs) < 1 {
|
||||
return 0, fmt.Errorf("no jobs present to parse")
|
||||
}
|
||||
|
||||
var metrics fioMetrics
|
||||
if err := json.Unmarshal(result.Jobs[0][op], &metrics); err != nil {
|
||||
return 0, fmt.Errorf("could not unmarshal jobs: %v", err)
|
||||
}
|
||||
|
||||
if _, ok := metrics[metric]; !ok {
|
||||
return 0, fmt.Errorf("no metric found for op: %s", op)
|
||||
}
|
||||
return strconv.ParseFloat(string(metrics[metric]), 64)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright 2020 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tools
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestFio checks the Fio parsers on sample output.
|
||||
func TestFio(t *testing.T) {
|
||||
sampleData := `
|
||||
{
|
||||
"fio version" : "fio-3.1",
|
||||
"timestamp" : 1554837456,
|
||||
"timestamp_ms" : 1554837456621,
|
||||
"time" : "Tue Apr 9 19:17:36 2019",
|
||||
"jobs" : [
|
||||
{
|
||||
"jobname" : "test",
|
||||
"groupid" : 0,
|
||||
"error" : 0,
|
||||
"eta" : 2147483647,
|
||||
"elapsed" : 1,
|
||||
"job options" : {
|
||||
"name" : "test",
|
||||
"ioengine" : "sync",
|
||||
"size" : "1073741824",
|
||||
"filename" : "/disk/file.dat",
|
||||
"iodepth" : "4",
|
||||
"bs" : "4096",
|
||||
"rw" : "write"
|
||||
},
|
||||
"read" : {
|
||||
"io_bytes" : 0,
|
||||
"io_kbytes" : 0,
|
||||
"bw" : 123456,
|
||||
"iops" : 1234.5678,
|
||||
"runtime" : 0,
|
||||
"total_ios" : 0,
|
||||
"short_ios" : 0,
|
||||
"bw_min" : 0,
|
||||
"bw_max" : 0,
|
||||
"bw_agg" : 0.000000,
|
||||
"bw_mean" : 0.000000,
|
||||
"bw_dev" : 0.000000,
|
||||
"bw_samples" : 0,
|
||||
"iops_min" : 0,
|
||||
"iops_max" : 0,
|
||||
"iops_mean" : 0.000000,
|
||||
"iops_stddev" : 0.000000,
|
||||
"iops_samples" : 0
|
||||
},
|
||||
"write" : {
|
||||
"io_bytes" : 1073741824,
|
||||
"io_kbytes" : 1048576,
|
||||
"bw" : 1753471,
|
||||
"iops" : 438367.892977,
|
||||
"runtime" : 598,
|
||||
"total_ios" : 262144,
|
||||
"bw_min" : 1731120,
|
||||
"bw_max" : 1731120,
|
||||
"bw_agg" : 98.725328,
|
||||
"bw_mean" : 1731120.000000,
|
||||
"bw_dev" : 0.000000,
|
||||
"bw_samples" : 1,
|
||||
"iops_min" : 432780,
|
||||
"iops_max" : 432780,
|
||||
"iops_mean" : 432780.000000,
|
||||
"iops_stddev" : 0.000000,
|
||||
"iops_samples" : 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
fio := Fio{}
|
||||
// WriteBandwidth.
|
||||
got, err := fio.parseBandwidth(sampleData, false)
|
||||
var want float64 = 1753471.0 * 1024
|
||||
if err != nil {
|
||||
t.Fatalf("parse failed with err: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("got: %f, want: %f", got, want)
|
||||
}
|
||||
|
||||
// ReadBandwidth.
|
||||
got, err = fio.parseBandwidth(sampleData, true)
|
||||
want = 123456 * 1024
|
||||
if err != nil {
|
||||
t.Fatalf("parse failed with err: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("got: %f, want: %f", got, want)
|
||||
}
|
||||
|
||||
// WriteIOps.
|
||||
got, err = fio.parseIOps(sampleData, false)
|
||||
want = 438367.892977
|
||||
if err != nil {
|
||||
t.Fatalf("parse failed with err: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("got: %f, want: %f", got, want)
|
||||
}
|
||||
|
||||
// ReadIOps.
|
||||
got, err = fio.parseIOps(sampleData, true)
|
||||
want = 1234.5678
|
||||
if err != nil {
|
||||
t.Fatalf("parse failed with err: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("got: %f, want: %f", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2020 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Hey is for the client application 'hey'.
|
||||
type Hey struct {
|
||||
Requests int
|
||||
Concurrency int
|
||||
Doc string
|
||||
}
|
||||
|
||||
// MakeCmd returns a 'hey' command.
|
||||
func (h *Hey) MakeCmd(ip net.IP, port int) []string {
|
||||
return strings.Split(fmt.Sprintf("hey -n %d -c %d http://%s:%d/%s",
|
||||
h.Requests, h.Concurrency, ip, port, h.Doc), " ")
|
||||
}
|
||||
|
||||
// Report parses output from 'hey' and reports metrics.
|
||||
func (h *Hey) Report(b *testing.B, output string) {
|
||||
b.Helper()
|
||||
requests, err := h.parseRequestsPerSecond(output)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to parse requests per second: %v", err)
|
||||
}
|
||||
b.ReportMetric(requests, "requests_per_second")
|
||||
|
||||
ave, err := h.parseAverageLatency(output)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to parse average latency: %v", err)
|
||||
}
|
||||
b.ReportMetric(ave, "average_latency_secs")
|
||||
}
|
||||
|
||||
var heyReqPerSecondRE = regexp.MustCompile(`Requests/sec:\s*(\d+\.?\d+?)\s+`)
|
||||
|
||||
// parseRequestsPerSecond finds requests per second from 'hey' output.
|
||||
func (h *Hey) parseRequestsPerSecond(data string) (float64, error) {
|
||||
match := heyReqPerSecondRE.FindStringSubmatch(data)
|
||||
if len(match) < 2 {
|
||||
return 0, fmt.Errorf("failed get bandwidth: %s", data)
|
||||
}
|
||||
return strconv.ParseFloat(match[1], 64)
|
||||
}
|
||||
|
||||
var heyAverageLatencyRE = regexp.MustCompile(`Average:\s*(\d+\.?\d+?)\s+secs`)
|
||||
|
||||
// parseHeyAverageLatency finds Average Latency in seconds form 'hey' output.
|
||||
func (h *Hey) parseAverageLatency(data string) (float64, error) {
|
||||
match := heyAverageLatencyRE.FindStringSubmatch(data)
|
||||
if len(match) < 2 {
|
||||
return 0, fmt.Errorf("failed get average latency match%d : %s", len(match), data)
|
||||
}
|
||||
return strconv.ParseFloat(match[1], 64)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2020 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tools
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestHey checks the Hey parsers on sample output.
|
||||
func TestHey(t *testing.T) {
|
||||
sampleData := `
|
||||
Summary:
|
||||
Total: 2.2391 secs
|
||||
Slowest: 1.6292 secs
|
||||
Fastest: 0.0066 secs
|
||||
Average: 0.5351 secs
|
||||
Requests/sec: 89.3202
|
||||
|
||||
Total data: 841200 bytes
|
||||
Size/request: 4206 bytes
|
||||
|
||||
Response time histogram:
|
||||
0.007 [1] |
|
||||
0.169 [0] |
|
||||
0.331 [149] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
|
||||
0.493 [0] |
|
||||
0.656 [0] |
|
||||
0.818 [0] |
|
||||
0.980 [0] |
|
||||
1.142 [0] |
|
||||
1.305 [0] |
|
||||
1.467 [49] |■■■■■■■■■■■■■
|
||||
1.629 [1] |
|
||||
|
||||
|
||||
Latency distribution:
|
||||
10% in 0.2149 secs
|
||||
25% in 0.2449 secs
|
||||
50% in 0.2703 secs
|
||||
75% in 1.3315 secs
|
||||
90% in 1.4045 secs
|
||||
95% in 1.4232 secs
|
||||
99% in 1.4362 secs
|
||||
|
||||
Details (average, fastest, slowest):
|
||||
DNS+dialup: 0.0002 secs, 0.0066 secs, 1.6292 secs
|
||||
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
|
||||
req write: 0.0000 secs, 0.0000 secs, 0.0012 secs
|
||||
resp wait: 0.5225 secs, 0.0064 secs, 1.4346 secs
|
||||
resp read: 0.0122 secs, 0.0001 secs, 0.2006 secs
|
||||
|
||||
Status code distribution:
|
||||
[200] 200 responses
|
||||
`
|
||||
hey := Hey{}
|
||||
want := 89.3202
|
||||
got, err := hey.parseRequestsPerSecond(sampleData)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse request per second with: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("got: %f, want: %f", got, want)
|
||||
}
|
||||
|
||||
want = 0.5351
|
||||
got, err = hey.parseAverageLatency(sampleData)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse average latency with: %v", err)
|
||||
} else if got != want {
|
||||
t.Fatalf("got: %f, want: %f", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2020 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Iperf is for the client side of `iperf`.
|
||||
type Iperf struct {
|
||||
Time int
|
||||
}
|
||||
|
||||
// MakeCmd returns a iperf client command.
|
||||
func (i *Iperf) MakeCmd(ip net.IP, port int) []string {
|
||||
// iperf report in Kb realtime
|
||||
return strings.Split(fmt.Sprintf("iperf -f K --realtime --time %d -c %s -p %d", i.Time, ip, port), " ")
|
||||
}
|
||||
|
||||
// Report parses output from iperf client and reports metrics.
|
||||
func (i *Iperf) Report(b *testing.B, output string) {
|
||||
b.Helper()
|
||||
// Parse bandwidth and report it.
|
||||
bW, err := i.bandwidth(output)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to parse bandwitdth from %s: %v", output, err)
|
||||
}
|
||||
b.ReportMetric(bW*1024, "bandwidth_b/s") // Convert from Kb/s to b/s.
|
||||
}
|
||||
|
||||
// bandwidth parses the Bandwidth number from an iperf report. A sample is below.
|
||||
func (i *Iperf) bandwidth(data string) (float64, error) {
|
||||
re := regexp.MustCompile(`\[\s*\d+\][^\n]+\s+(\d+\.?\d*)\s+KBytes/sec`)
|
||||
match := re.FindStringSubmatch(data)
|
||||
if len(match) < 1 {
|
||||
return 0, fmt.Errorf("failed get bandwidth: %s", data)
|
||||
}
|
||||
return strconv.ParseFloat(match[1], 64)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2020 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package tools
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestIperf checks the Iperf parsers on sample output.
|
||||
func TestIperf(t *testing.T) {
|
||||
sampleData := `
|
||||
------------------------------------------------------------
|
||||
Client connecting to 10.138.15.215, TCP port 32779
|
||||
TCP window size: 45.0 KByte (default)
|
||||
------------------------------------------------------------
|
||||
[ 3] local 10.138.15.216 port 32866 connected with 10.138.15.215 port 32779
|
||||
[ ID] Interval Transfer Bandwidth
|
||||
[ 3] 0.0-10.0 sec 459520 KBytes 45900 KBytes/sec
|
||||
`
|
||||
i := Iperf{}
|
||||
bandwidth, err := i.bandwidth(sampleData)
|
||||
if err != nil || bandwidth != 45900 {
|
||||
t.Fatalf("failed with: %v and %f", err, bandwidth)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2020 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Redis is for the client 'redis-benchmark'.
|
||||
type Redis struct {
|
||||
Operation string
|
||||
}
|
||||
|
||||
// MakeCmd returns a redis-benchmark client command.
|
||||
func (r *Redis) MakeCmd(ip net.IP, port int) []string {
|
||||
// There is no -t PING_BULK for redis-benchmark, so adjust the command in that case.
|
||||
// Note that "ping" will run both PING_INLINE and PING_BULK.
|
||||
if r.Operation == "PING_BULK" {
|
||||
return strings.Split(
|
||||
fmt.Sprintf("redis-benchmark --csv -t ping -h %s -p %d", ip, port), " ")
|
||||
}
|
||||
|
||||
// runs redis-benchmark -t operation for 100K requests against server.
|
||||
return strings.Split(
|
||||
fmt.Sprintf("redis-benchmark --csv -t %s -h %s -p %d", r.Operation, ip, port), " ")
|
||||
}
|
||||
|
||||
// Report parses output from redis-benchmark client and reports metrics.
|
||||
func (r *Redis) Report(b *testing.B, output string) {
|
||||
b.Helper()
|
||||
result, err := r.parseOperation(output)
|
||||
if err != nil {
|
||||
b.Fatalf("parsing result %s failed with err: %v", output, err)
|
||||
}
|
||||
b.ReportMetric(result, r.Operation) // operations per second
|
||||
}
|
||||
|
||||
// parseOperation grabs the metric operations per second from redis-benchmark output.
|
||||
func (r *Redis) parseOperation(data string) (float64, error) {
|
||||
re := regexp.MustCompile(fmt.Sprintf(`"%s( .*)?","(\d*\.\d*)"`, r.Operation))
|
||||
match := re.FindStringSubmatch(data)
|
||||
// If no match, simply don't add it to the result map.
|
||||
if len(match) < 3 {
|
||||
return 0.0, fmt.Errorf("could not find %s in %s", r.Operation, data)
|
||||
}
|
||||
return strconv.ParseFloat(match[2], 64)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user