A few more improvements to the benchmark code. (#2840)
* A few more improvements to the benchmark code. * In benchmain/main.go: * Define types for function arguments to make code more readable * Significantly simplify the code as a result of stats package refactor. * In benchresult/main.go * Simplify code as a result of stats package refactor. * In stats/stats.go * Define and expose featureIndex enum. * Refactor the types used to store features, results, stats etc. * Provide easy to use methods to add/modify/read/dump stats info. * Delete stats/util.go - dead code.
This commit is contained in:

committed by
GitHub

parent
f2967c2f83
commit
b681a11d08
@ -55,7 +55,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
@ -66,6 +65,7 @@ import (
|
||||
"google.golang.org/grpc/benchmark/stats"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
"google.golang.org/grpc/internal/channelz"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"google.golang.org/grpc/test/bufconn"
|
||||
)
|
||||
|
||||
@ -96,6 +96,8 @@ var (
|
||||
cpuProfile = flag.String("cpuProfile", "", "Enables CPU profiling output to the filename provided")
|
||||
benchmarkResultFile = flag.String("resultFile", "", "Save the benchmark result into a binary file")
|
||||
useBufconn = flag.Bool("bufconn", false, "Use in-memory connection instead of system network I/O")
|
||||
enableKeepalive = flag.Bool("enable_keepalive", false, "Enable client keepalive. \n"+
|
||||
"Keepalive.Time is set to 10s, Keepalive.Timeout is set to 1s, Keepalive.PermitWithoutStream is set to true.")
|
||||
)
|
||||
|
||||
const (
|
||||
@ -120,6 +122,8 @@ const (
|
||||
networkLongHaul = "Longhaul"
|
||||
|
||||
numStatsBuckets = 10
|
||||
warmupCallCount = 10
|
||||
warmuptime = time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
@ -139,6 +143,8 @@ var (
|
||||
networkModeWAN: latency.WAN,
|
||||
networkLongHaul: latency.Longhaul,
|
||||
}
|
||||
keepaliveTime = 10 * time.Second // this is the minimum allowed
|
||||
keepaliveTimeout = 1 * time.Second
|
||||
)
|
||||
|
||||
// runModes indicates the workloads to run. This is initialized with a call to
|
||||
@ -169,76 +175,86 @@ func runModesFromWorkloads(workload string) runModes {
|
||||
return r
|
||||
}
|
||||
|
||||
func unaryBenchmark(startTimer func(), stopTimer func(uint64), benchFeatures stats.Features, benchTime time.Duration, s *stats.Stats) uint64 {
|
||||
caller, cleanup := makeFuncUnary(benchFeatures)
|
||||
type startFunc func(mode string, bf stats.Features)
|
||||
type stopFunc func(count uint64)
|
||||
type ucStopFunc func(req uint64, resp uint64)
|
||||
type rpcCallFunc func(pos int)
|
||||
type rpcSendFunc func(pos int)
|
||||
type rpcRecvFunc func(pos int)
|
||||
type rpcCleanupFunc func()
|
||||
|
||||
func unaryBenchmark(start startFunc, stop stopFunc, bf stats.Features, s *stats.Stats) {
|
||||
caller, cleanup := makeFuncUnary(bf)
|
||||
defer cleanup()
|
||||
return runBenchmark(caller, startTimer, stopTimer, benchFeatures, benchTime, s)
|
||||
runBenchmark(caller, start, stop, bf, s, workloadsUnary)
|
||||
}
|
||||
|
||||
func streamBenchmark(startTimer func(), stopTimer func(uint64), benchFeatures stats.Features, benchTime time.Duration, s *stats.Stats) uint64 {
|
||||
caller, cleanup := makeFuncStream(benchFeatures)
|
||||
func streamBenchmark(start startFunc, stop stopFunc, bf stats.Features, s *stats.Stats) {
|
||||
caller, cleanup := makeFuncStream(bf)
|
||||
defer cleanup()
|
||||
return runBenchmark(caller, startTimer, stopTimer, benchFeatures, benchTime, s)
|
||||
runBenchmark(caller, start, stop, bf, s, workloadsStreaming)
|
||||
}
|
||||
|
||||
func unconstrainedStreamBenchmark(benchFeatures stats.Features, warmuptime, benchTime time.Duration) (uint64, uint64) {
|
||||
var sender, recver func(int)
|
||||
var cleanup func()
|
||||
if benchFeatures.EnablePreloader {
|
||||
sender, recver, cleanup = makeFuncUnconstrainedStreamPreloaded(benchFeatures)
|
||||
func unconstrainedStreamBenchmark(start startFunc, stop ucStopFunc, bf stats.Features, s *stats.Stats) {
|
||||
var sender rpcSendFunc
|
||||
var recver rpcRecvFunc
|
||||
var cleanup rpcCleanupFunc
|
||||
if bf.EnablePreloader {
|
||||
sender, recver, cleanup = makeFuncUnconstrainedStreamPreloaded(bf)
|
||||
} else {
|
||||
sender, recver, cleanup = makeFuncUnconstrainedStream(benchFeatures)
|
||||
sender, recver, cleanup = makeFuncUnconstrainedStream(bf)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
requestCount uint64
|
||||
responseCount uint64
|
||||
)
|
||||
wg.Add(2 * benchFeatures.MaxConcurrentCalls)
|
||||
|
||||
// Resets the counters once warmed up
|
||||
var req, resp uint64
|
||||
go func() {
|
||||
// Resets the counters once warmed up
|
||||
<-time.NewTimer(warmuptime).C
|
||||
atomic.StoreUint64(&requestCount, 0)
|
||||
atomic.StoreUint64(&responseCount, 0)
|
||||
atomic.StoreUint64(&req, 0)
|
||||
atomic.StoreUint64(&resp, 0)
|
||||
start(workloadsUnconstrained, bf)
|
||||
}()
|
||||
|
||||
bmEnd := time.Now().Add(benchTime + warmuptime)
|
||||
for i := 0; i < benchFeatures.MaxConcurrentCalls; i++ {
|
||||
bmEnd := time.Now().Add(bf.BenchTime + warmuptime)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2 * bf.MaxConcurrentCalls)
|
||||
for i := 0; i < bf.MaxConcurrentCalls; i++ {
|
||||
go func(pos int) {
|
||||
defer wg.Done()
|
||||
for {
|
||||
t := time.Now()
|
||||
if t.After(bmEnd) {
|
||||
break
|
||||
return
|
||||
}
|
||||
sender(pos)
|
||||
atomic.AddUint64(&requestCount, 1)
|
||||
atomic.AddUint64(&req, 1)
|
||||
}
|
||||
wg.Done()
|
||||
}(i)
|
||||
go func(pos int) {
|
||||
defer wg.Done()
|
||||
for {
|
||||
t := time.Now()
|
||||
if t.After(bmEnd) {
|
||||
break
|
||||
return
|
||||
}
|
||||
recver(pos)
|
||||
atomic.AddUint64(&responseCount, 1)
|
||||
atomic.AddUint64(&resp, 1)
|
||||
}
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
return requestCount, responseCount
|
||||
stop(req, resp)
|
||||
}
|
||||
|
||||
func makeClient(benchFeatures stats.Features) (testpb.BenchmarkServiceClient, func()) {
|
||||
nw := &latency.Network{Kbps: benchFeatures.Kbps, Latency: benchFeatures.Latency, MTU: benchFeatures.Mtu}
|
||||
// makeClient returns a gRPC client for the grpc.testing.BenchmarkService
|
||||
// service. The client is configured using the different options in the passed
|
||||
// 'bf'. Also returns a cleanup function to close the client and release
|
||||
// resources.
|
||||
func makeClient(bf stats.Features) (testpb.BenchmarkServiceClient, func()) {
|
||||
nw := &latency.Network{Kbps: bf.Kbps, Latency: bf.Latency, MTU: bf.MTU}
|
||||
opts := []grpc.DialOption{}
|
||||
sopts := []grpc.ServerOption{}
|
||||
if benchFeatures.ModeCompressor == compModeNop {
|
||||
if bf.ModeCompressor == compModeNop {
|
||||
sopts = append(sopts,
|
||||
grpc.RPCCompressor(nopCompressor{}),
|
||||
grpc.RPCDecompressor(nopDecompressor{}),
|
||||
@ -248,7 +264,7 @@ func makeClient(benchFeatures stats.Features) (testpb.BenchmarkServiceClient, fu
|
||||
grpc.WithDecompressor(nopDecompressor{}),
|
||||
)
|
||||
}
|
||||
if benchFeatures.ModeCompressor == compModeGzip {
|
||||
if bf.ModeCompressor == compModeGzip {
|
||||
sopts = append(sopts,
|
||||
grpc.RPCCompressor(grpc.NewGZIPCompressor()),
|
||||
grpc.RPCDecompressor(grpc.NewGZIPDecompressor()),
|
||||
@ -258,11 +274,20 @@ func makeClient(benchFeatures stats.Features) (testpb.BenchmarkServiceClient, fu
|
||||
grpc.WithDecompressor(grpc.NewGZIPDecompressor()),
|
||||
)
|
||||
}
|
||||
sopts = append(sopts, grpc.MaxConcurrentStreams(uint32(benchFeatures.MaxConcurrentCalls+1)))
|
||||
if bf.EnableKeepalive {
|
||||
opts = append(opts,
|
||||
grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: keepaliveTime,
|
||||
Timeout: keepaliveTimeout,
|
||||
PermitWithoutStream: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
sopts = append(sopts, grpc.MaxConcurrentStreams(uint32(bf.MaxConcurrentCalls+1)))
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
|
||||
var lis net.Listener
|
||||
if benchFeatures.UseBufConn {
|
||||
if bf.UseBufConn {
|
||||
bcLis := bufconn.Listen(256 * 1024)
|
||||
lis = bcLis
|
||||
opts = append(opts, grpc.WithContextDialer(func(ctx context.Context, address string) (net.Conn, error) {
|
||||
@ -289,18 +314,18 @@ func makeClient(benchFeatures stats.Features) (testpb.BenchmarkServiceClient, fu
|
||||
}
|
||||
}
|
||||
|
||||
func makeFuncUnary(benchFeatures stats.Features) (func(int), func()) {
|
||||
tc, cleanup := makeClient(benchFeatures)
|
||||
func makeFuncUnary(bf stats.Features) (rpcCallFunc, rpcCleanupFunc) {
|
||||
tc, cleanup := makeClient(bf)
|
||||
return func(int) {
|
||||
unaryCaller(tc, benchFeatures.ReqSizeBytes, benchFeatures.RespSizeBytes)
|
||||
unaryCaller(tc, bf.ReqSizeBytes, bf.RespSizeBytes)
|
||||
}, cleanup
|
||||
}
|
||||
|
||||
func makeFuncStream(benchFeatures stats.Features) (func(int), func()) {
|
||||
tc, cleanup := makeClient(benchFeatures)
|
||||
func makeFuncStream(bf stats.Features) (rpcCallFunc, rpcCleanupFunc) {
|
||||
tc, cleanup := makeClient(bf)
|
||||
|
||||
streams := make([]testpb.BenchmarkService_StreamingCallClient, benchFeatures.MaxConcurrentCalls)
|
||||
for i := 0; i < benchFeatures.MaxConcurrentCalls; i++ {
|
||||
streams := make([]testpb.BenchmarkService_StreamingCallClient, bf.MaxConcurrentCalls)
|
||||
for i := 0; i < bf.MaxConcurrentCalls; i++ {
|
||||
stream, err := tc.StreamingCall(context.Background())
|
||||
if err != nil {
|
||||
grpclog.Fatalf("%v.StreamingCall(_) = _, %v", tc, err)
|
||||
@ -309,12 +334,12 @@ func makeFuncStream(benchFeatures stats.Features) (func(int), func()) {
|
||||
}
|
||||
|
||||
return func(pos int) {
|
||||
streamCaller(streams[pos], benchFeatures.ReqSizeBytes, benchFeatures.RespSizeBytes)
|
||||
streamCaller(streams[pos], bf.ReqSizeBytes, bf.RespSizeBytes)
|
||||
}, cleanup
|
||||
}
|
||||
|
||||
func makeFuncUnconstrainedStreamPreloaded(benchFeatures stats.Features) (func(int), func(int), func()) {
|
||||
streams, req, cleanup := setupUnconstrainedStream(benchFeatures)
|
||||
func makeFuncUnconstrainedStreamPreloaded(bf stats.Features) (rpcSendFunc, rpcRecvFunc, rpcCleanupFunc) {
|
||||
streams, req, cleanup := setupUnconstrainedStream(bf)
|
||||
|
||||
preparedMsg := make([]*grpc.PreparedMsg, len(streams))
|
||||
for i, stream := range streams {
|
||||
@ -332,8 +357,8 @@ func makeFuncUnconstrainedStreamPreloaded(benchFeatures stats.Features) (func(in
|
||||
}, cleanup
|
||||
}
|
||||
|
||||
func makeFuncUnconstrainedStream(benchFeatures stats.Features) (func(int), func(int), func()) {
|
||||
streams, req, cleanup := setupUnconstrainedStream(benchFeatures)
|
||||
func makeFuncUnconstrainedStream(bf stats.Features) (rpcSendFunc, rpcRecvFunc, rpcCleanupFunc) {
|
||||
streams, req, cleanup := setupUnconstrainedStream(bf)
|
||||
|
||||
return func(pos int) {
|
||||
streams[pos].Send(req)
|
||||
@ -342,11 +367,11 @@ func makeFuncUnconstrainedStream(benchFeatures stats.Features) (func(int), func(
|
||||
}, cleanup
|
||||
}
|
||||
|
||||
func setupUnconstrainedStream(benchFeatures stats.Features) ([]testpb.BenchmarkService_StreamingCallClient, *testpb.SimpleRequest, func()) {
|
||||
tc, cleanup := makeClient(benchFeatures)
|
||||
func setupUnconstrainedStream(bf stats.Features) ([]testpb.BenchmarkService_StreamingCallClient, *testpb.SimpleRequest, rpcCleanupFunc) {
|
||||
tc, cleanup := makeClient(bf)
|
||||
|
||||
streams := make([]testpb.BenchmarkService_StreamingCallClient, benchFeatures.MaxConcurrentCalls)
|
||||
for i := 0; i < benchFeatures.MaxConcurrentCalls; i++ {
|
||||
streams := make([]testpb.BenchmarkService_StreamingCallClient, bf.MaxConcurrentCalls)
|
||||
for i := 0; i < bf.MaxConcurrentCalls; i++ {
|
||||
stream, err := tc.UnconstrainedStreamingCall(context.Background())
|
||||
if err != nil {
|
||||
grpclog.Fatalf("%v.UnconstrainedStreamingCall(_) = _, %v", tc, err)
|
||||
@ -354,16 +379,18 @@ func setupUnconstrainedStream(benchFeatures stats.Features) ([]testpb.BenchmarkS
|
||||
streams[i] = stream
|
||||
}
|
||||
|
||||
pl := bm.NewPayload(testpb.PayloadType_COMPRESSABLE, benchFeatures.ReqSizeBytes)
|
||||
pl := bm.NewPayload(testpb.PayloadType_COMPRESSABLE, bf.ReqSizeBytes)
|
||||
req := &testpb.SimpleRequest{
|
||||
ResponseType: pl.Type,
|
||||
ResponseSize: int32(benchFeatures.RespSizeBytes),
|
||||
ResponseSize: int32(bf.RespSizeBytes),
|
||||
Payload: pl,
|
||||
}
|
||||
|
||||
return streams, req, cleanup
|
||||
}
|
||||
|
||||
// Makes a UnaryCall gRPC request using the given BenchmarkServiceClient and
|
||||
// request and response sizes.
|
||||
func unaryCaller(client testpb.BenchmarkServiceClient, reqSize, respSize int) {
|
||||
if err := bm.DoUnaryCall(client, reqSize, respSize); err != nil {
|
||||
grpclog.Fatalf("DoUnaryCall failed: %v", err)
|
||||
@ -376,41 +403,36 @@ func streamCaller(stream testpb.BenchmarkService_StreamingCallClient, reqSize, r
|
||||
}
|
||||
}
|
||||
|
||||
func runBenchmark(caller func(int), startTimer func(), stopTimer func(uint64), benchFeatures stats.Features, benchTime time.Duration, s *stats.Stats) uint64 {
|
||||
func runBenchmark(caller rpcCallFunc, start startFunc, stop stopFunc, bf stats.Features, s *stats.Stats, mode string) {
|
||||
// Warm up connection.
|
||||
for i := 0; i < 10; i++ {
|
||||
for i := 0; i < warmupCallCount; i++ {
|
||||
caller(0)
|
||||
}
|
||||
|
||||
// Run benchmark.
|
||||
startTimer()
|
||||
var (
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
wg.Add(benchFeatures.MaxConcurrentCalls)
|
||||
bmEnd := time.Now().Add(benchTime)
|
||||
start(mode, bf)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(bf.MaxConcurrentCalls)
|
||||
bmEnd := time.Now().Add(bf.BenchTime)
|
||||
var count uint64
|
||||
for i := 0; i < benchFeatures.MaxConcurrentCalls; i++ {
|
||||
for i := 0; i < bf.MaxConcurrentCalls; i++ {
|
||||
go func(pos int) {
|
||||
defer wg.Done()
|
||||
for {
|
||||
t := time.Now()
|
||||
if t.After(bmEnd) {
|
||||
break
|
||||
return
|
||||
}
|
||||
start := time.Now()
|
||||
caller(pos)
|
||||
elapse := time.Since(start)
|
||||
atomic.AddUint64(&count, 1)
|
||||
mu.Lock()
|
||||
s.Add(elapse)
|
||||
mu.Unlock()
|
||||
s.AddDuration(elapse)
|
||||
}
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
stopTimer(count)
|
||||
return count
|
||||
stop(count)
|
||||
}
|
||||
|
||||
// benchOpts represents all configurable options available while running this
|
||||
@ -424,6 +446,7 @@ type benchOpts struct {
|
||||
networkMode string
|
||||
benchmarkResultFile string
|
||||
useBufconn bool
|
||||
enableKeepalive bool
|
||||
features *featureOpts
|
||||
}
|
||||
|
||||
@ -432,39 +455,18 @@ type benchOpts struct {
|
||||
// features through command line flags. We generate all possible combinations
|
||||
// for the provided values and run the benchmarks for each combination.
|
||||
type featureOpts struct {
|
||||
enableTrace []bool // Feature index 0
|
||||
readLatencies []time.Duration // Feature index 1
|
||||
readKbps []int // Feature index 2
|
||||
readMTU []int // Feature index 3
|
||||
maxConcurrentCalls []int // Feature index 4
|
||||
reqSizeBytes []int // Feature index 5
|
||||
respSizeBytes []int // Feature index 6
|
||||
compModes []string // Feature index 7
|
||||
enableChannelz []bool // Feature index 8
|
||||
enablePreloader []bool // Feature index 9
|
||||
enableTrace []bool
|
||||
readLatencies []time.Duration
|
||||
readKbps []int
|
||||
readMTU []int
|
||||
maxConcurrentCalls []int
|
||||
reqSizeBytes []int
|
||||
respSizeBytes []int
|
||||
compModes []string
|
||||
enableChannelz []bool
|
||||
enablePreloader []bool
|
||||
}
|
||||
|
||||
// featureIndex is an enum for the different features that could be configured
|
||||
// by the user through command line flags.
|
||||
type featureIndex int
|
||||
|
||||
const (
|
||||
enableTraceIndex featureIndex = iota
|
||||
readLatenciesIndex
|
||||
readKbpsIndex
|
||||
readMTUIndex
|
||||
maxConcurrentCallsIndex
|
||||
reqSizeBytesIndex
|
||||
respSizeBytesIndex
|
||||
compModesIndex
|
||||
enableChannelzIndex
|
||||
enablePreloaderIndex
|
||||
|
||||
// This is a place holder to indicate the total number of feature indices we
|
||||
// have. Any new feature indices should be added above this.
|
||||
maxFeatureIndex
|
||||
)
|
||||
|
||||
// makeFeaturesNum returns a slice of ints of size 'maxFeatureIndex' where each
|
||||
// element of the slice (indexed by 'featuresIndex' enum) contains the number
|
||||
// of features to be exercised by the benchmark code.
|
||||
@ -472,31 +474,31 @@ const (
|
||||
// enableTrace feature, while index 1 contains the number of value of
|
||||
// readLatencies feature and so on.
|
||||
func makeFeaturesNum(b *benchOpts) []int {
|
||||
featuresNum := make([]int, maxFeatureIndex)
|
||||
featuresNum := make([]int, stats.MaxFeatureIndex)
|
||||
for i := 0; i < len(featuresNum); i++ {
|
||||
switch featureIndex(i) {
|
||||
case enableTraceIndex:
|
||||
switch stats.FeatureIndex(i) {
|
||||
case stats.EnableTraceIndex:
|
||||
featuresNum[i] = len(b.features.enableTrace)
|
||||
case readLatenciesIndex:
|
||||
case stats.ReadLatenciesIndex:
|
||||
featuresNum[i] = len(b.features.readLatencies)
|
||||
case readKbpsIndex:
|
||||
case stats.ReadKbpsIndex:
|
||||
featuresNum[i] = len(b.features.readKbps)
|
||||
case readMTUIndex:
|
||||
case stats.ReadMTUIndex:
|
||||
featuresNum[i] = len(b.features.readMTU)
|
||||
case maxConcurrentCallsIndex:
|
||||
case stats.MaxConcurrentCallsIndex:
|
||||
featuresNum[i] = len(b.features.maxConcurrentCalls)
|
||||
case reqSizeBytesIndex:
|
||||
case stats.ReqSizeBytesIndex:
|
||||
featuresNum[i] = len(b.features.reqSizeBytes)
|
||||
case respSizeBytesIndex:
|
||||
case stats.RespSizeBytesIndex:
|
||||
featuresNum[i] = len(b.features.respSizeBytes)
|
||||
case compModesIndex:
|
||||
case stats.CompModesIndex:
|
||||
featuresNum[i] = len(b.features.compModes)
|
||||
case enableChannelzIndex:
|
||||
case stats.EnableChannelzIndex:
|
||||
featuresNum[i] = len(b.features.enableChannelz)
|
||||
case enablePreloaderIndex:
|
||||
case stats.EnablePreloaderIndex:
|
||||
featuresNum[i] = len(b.features.enablePreloader)
|
||||
default:
|
||||
log.Fatalf("Unknown feature index %v in generateFeatures. maxFeatureIndex is %v", i, maxFeatureIndex)
|
||||
log.Fatalf("Unknown feature index %v in generateFeatures. maxFeatureIndex is %v", i, stats.MaxFeatureIndex)
|
||||
}
|
||||
}
|
||||
return featuresNum
|
||||
@ -537,26 +539,28 @@ func (b *benchOpts) generateFeatures(featuresNum []int) []stats.Features {
|
||||
// all options.
|
||||
var result []stats.Features
|
||||
var curPos []int
|
||||
initialPos := make([]int, maxFeatureIndex)
|
||||
initialPos := make([]int, stats.MaxFeatureIndex)
|
||||
for !reflect.DeepEqual(initialPos, curPos) {
|
||||
if curPos == nil {
|
||||
curPos = make([]int, maxFeatureIndex)
|
||||
curPos = make([]int, stats.MaxFeatureIndex)
|
||||
}
|
||||
result = append(result, stats.Features{
|
||||
// These features stay the same for each iteration.
|
||||
NetworkMode: b.networkMode,
|
||||
UseBufConn: b.useBufconn,
|
||||
EnableKeepalive: b.enableKeepalive,
|
||||
BenchTime: b.benchTime,
|
||||
// These features can potentially change for each iteration.
|
||||
EnableTrace: b.features.enableTrace[curPos[enableTraceIndex]],
|
||||
Latency: b.features.readLatencies[curPos[readLatenciesIndex]],
|
||||
Kbps: b.features.readKbps[curPos[readKbpsIndex]],
|
||||
Mtu: b.features.readMTU[curPos[readMTUIndex]],
|
||||
MaxConcurrentCalls: b.features.maxConcurrentCalls[curPos[maxConcurrentCallsIndex]],
|
||||
ReqSizeBytes: b.features.reqSizeBytes[curPos[reqSizeBytesIndex]],
|
||||
RespSizeBytes: b.features.respSizeBytes[curPos[respSizeBytesIndex]],
|
||||
ModeCompressor: b.features.compModes[curPos[compModesIndex]],
|
||||
EnableChannelz: b.features.enableChannelz[curPos[enableChannelzIndex]],
|
||||
EnablePreloader: b.features.enablePreloader[curPos[enablePreloaderIndex]],
|
||||
EnableTrace: b.features.enableTrace[curPos[stats.EnableTraceIndex]],
|
||||
Latency: b.features.readLatencies[curPos[stats.ReadLatenciesIndex]],
|
||||
Kbps: b.features.readKbps[curPos[stats.ReadKbpsIndex]],
|
||||
MTU: b.features.readMTU[curPos[stats.ReadMTUIndex]],
|
||||
MaxConcurrentCalls: b.features.maxConcurrentCalls[curPos[stats.MaxConcurrentCallsIndex]],
|
||||
ReqSizeBytes: b.features.reqSizeBytes[curPos[stats.ReqSizeBytesIndex]],
|
||||
RespSizeBytes: b.features.respSizeBytes[curPos[stats.RespSizeBytesIndex]],
|
||||
ModeCompressor: b.features.compModes[curPos[stats.CompModesIndex]],
|
||||
EnableChannelz: b.features.enableChannelz[curPos[stats.EnableChannelzIndex]],
|
||||
EnablePreloader: b.features.enablePreloader[curPos[stats.EnablePreloaderIndex]],
|
||||
})
|
||||
addOne(curPos, featuresNum)
|
||||
}
|
||||
@ -597,6 +601,7 @@ func processFlags() *benchOpts {
|
||||
networkMode: *networkMode,
|
||||
benchmarkResultFile: *benchmarkResultFile,
|
||||
useBufconn: *useBufconn,
|
||||
enableKeepalive: *enableKeepalive,
|
||||
features: &featureOpts{
|
||||
enableTrace: setToggleMode(*traceMode),
|
||||
readLatencies: append([]time.Duration(nil), *readLatency...),
|
||||
@ -648,76 +653,36 @@ func setCompressorMode(val string) []string {
|
||||
}
|
||||
}
|
||||
|
||||
func printThroughput(requestCount uint64, requestSize int, responseCount uint64, responseSize int, benchTime time.Duration) {
|
||||
requestThroughput := float64(requestCount) * float64(requestSize) * 8 / benchTime.Seconds()
|
||||
responseThroughput := float64(responseCount) * float64(responseSize) * 8 / benchTime.Seconds()
|
||||
fmt.Printf("Number of requests: %v\tRequest throughput: %v bit/s\n", requestCount, requestThroughput)
|
||||
fmt.Printf("Number of responses: %v\tResponse throughput: %v bit/s\n", responseCount, responseThroughput)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func main() {
|
||||
opts := processFlags()
|
||||
before(opts)
|
||||
s := stats.NewStats(numStatsBuckets)
|
||||
s.SortLatency()
|
||||
var memStats runtime.MemStats
|
||||
var results testing.BenchmarkResult
|
||||
var startAllocs, startBytes uint64
|
||||
var startTime time.Time
|
||||
var startTimer = func() {
|
||||
runtime.ReadMemStats(&memStats)
|
||||
startAllocs = memStats.Mallocs
|
||||
startBytes = memStats.TotalAlloc
|
||||
startTime = time.Now()
|
||||
}
|
||||
var stopTimer = func(count uint64) {
|
||||
runtime.ReadMemStats(&memStats)
|
||||
results = testing.BenchmarkResult{
|
||||
N: int(count),
|
||||
T: time.Since(startTime),
|
||||
Bytes: 0,
|
||||
MemAllocs: memStats.Mallocs - startAllocs,
|
||||
MemBytes: memStats.TotalAlloc - startBytes,
|
||||
}
|
||||
}
|
||||
|
||||
// Run benchmarks
|
||||
resultSlice := []stats.BenchResults{}
|
||||
s := stats.NewStats(numStatsBuckets)
|
||||
featuresNum := makeFeaturesNum(opts)
|
||||
sharedPos := sharedFeatures(featuresNum)
|
||||
for _, benchFeature := range opts.generateFeatures(featuresNum) {
|
||||
grpc.EnableTracing = benchFeature.EnableTrace
|
||||
if benchFeature.EnableChannelz {
|
||||
sf := sharedFeatures(featuresNum)
|
||||
|
||||
var (
|
||||
start = func(mode string, bf stats.Features) { s.StartRun(mode, bf, sf) }
|
||||
stop = func(count uint64) { s.EndRun(count) }
|
||||
ucStop = func(req uint64, resp uint64) { s.EndUnconstrainedRun(req, resp) }
|
||||
)
|
||||
|
||||
for _, bf := range opts.generateFeatures(featuresNum) {
|
||||
grpc.EnableTracing = bf.EnableTrace
|
||||
if bf.EnableChannelz {
|
||||
channelz.TurnOn()
|
||||
}
|
||||
if opts.rModes.unary {
|
||||
count := unaryBenchmark(startTimer, stopTimer, benchFeature, opts.benchTime, s)
|
||||
s.SetBenchmarkResult("Unary", benchFeature, results.N,
|
||||
results.AllocedBytesPerOp(), results.AllocsPerOp(), sharedPos)
|
||||
fmt.Println(s.BenchString())
|
||||
fmt.Println(s.String())
|
||||
printThroughput(count, benchFeature.ReqSizeBytes, count, benchFeature.RespSizeBytes, opts.benchTime)
|
||||
resultSlice = append(resultSlice, s.GetBenchmarkResults())
|
||||
s.Clear()
|
||||
unaryBenchmark(start, stop, bf, s)
|
||||
}
|
||||
if opts.rModes.streaming {
|
||||
count := streamBenchmark(startTimer, stopTimer, benchFeature, opts.benchTime, s)
|
||||
s.SetBenchmarkResult("Stream", benchFeature, results.N,
|
||||
results.AllocedBytesPerOp(), results.AllocsPerOp(), sharedPos)
|
||||
fmt.Println(s.BenchString())
|
||||
fmt.Println(s.String())
|
||||
printThroughput(count, benchFeature.ReqSizeBytes, count, benchFeature.RespSizeBytes, opts.benchTime)
|
||||
resultSlice = append(resultSlice, s.GetBenchmarkResults())
|
||||
s.Clear()
|
||||
streamBenchmark(start, stop, bf, s)
|
||||
}
|
||||
if opts.rModes.unconstrained {
|
||||
requestCount, responseCount := unconstrainedStreamBenchmark(benchFeature, time.Second, opts.benchTime)
|
||||
fmt.Printf("Unconstrained Stream-%v\n", benchFeature)
|
||||
printThroughput(requestCount, benchFeature.ReqSizeBytes, responseCount, benchFeature.RespSizeBytes, opts.benchTime)
|
||||
unconstrainedStreamBenchmark(start, ucStop, bf, s)
|
||||
}
|
||||
}
|
||||
after(opts, resultSlice)
|
||||
after(opts, s.GetResults())
|
||||
}
|
||||
|
||||
func before(opts *benchOpts) {
|
||||
|
@ -32,14 +32,13 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/benchmark/stats"
|
||||
)
|
||||
|
||||
func createMap(fileName string, m map[string]stats.BenchResults) {
|
||||
func createMap(fileName string) map[string]stats.BenchResults {
|
||||
f, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
log.Fatalf("Read file %s error: %s\n", fileName, err)
|
||||
@ -50,18 +49,22 @@ func createMap(fileName string, m map[string]stats.BenchResults) {
|
||||
if err = decoder.Decode(&data); err != nil {
|
||||
log.Fatalf("Decode file %s error: %s\n", fileName, err)
|
||||
}
|
||||
m := make(map[string]stats.BenchResults)
|
||||
for _, d := range data {
|
||||
m[d.RunMode+"-"+d.Features.String()] = d
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func intChange(title string, val1, val2 int64) string {
|
||||
return fmt.Sprintf("%10s %12s %12s %8.2f%%\n", title, strconv.FormatInt(val1, 10),
|
||||
strconv.FormatInt(val2, 10), float64(val2-val1)*100/float64(val1))
|
||||
func intChange(title string, val1, val2 uint64) string {
|
||||
return fmt.Sprintf("%20s %12d %12d %8.2f%%\n", title, val1, val2, float64(int64(val2)-int64(val1))*100/float64(val1))
|
||||
}
|
||||
|
||||
func timeChange(title int, val1, val2 time.Duration) string {
|
||||
return fmt.Sprintf("%10s %12s %12s %8.2f%%\n", strconv.Itoa(title)+" latency", val1.String(),
|
||||
func floatChange(title string, val1, val2 float64) string {
|
||||
return fmt.Sprintf("%20s %12.2f %12.2f %8.2f%%\n", title, val1, val2, float64(int64(val2)-int64(val1))*100/float64(val1))
|
||||
}
|
||||
func timeChange(title string, val1, val2 time.Duration) string {
|
||||
return fmt.Sprintf("%20s %12s %12s %8.2f%%\n", title, val1.String(),
|
||||
val2.String(), float64(val2-val1)*100/float64(val1))
|
||||
}
|
||||
|
||||
@ -69,30 +72,30 @@ func compareTwoMap(m1, m2 map[string]stats.BenchResults) {
|
||||
for k2, v2 := range m2 {
|
||||
if v1, ok := m1[k2]; ok {
|
||||
changes := k2 + "\n"
|
||||
changes += fmt.Sprintf("%10s %12s %12s %8s\n", "Title", "Before", "After", "Percentage")
|
||||
changes += intChange("Bytes/op", v1.AllocedBytesPerOp, v2.AllocedBytesPerOp)
|
||||
changes += intChange("Allocs/op", v1.AllocsPerOp, v2.AllocsPerOp)
|
||||
changes += timeChange(v1.Latency[1].Percent, v1.Latency[1].Value, v2.Latency[1].Value)
|
||||
changes += timeChange(v1.Latency[2].Percent, v1.Latency[2].Value, v2.Latency[2].Value)
|
||||
changes += fmt.Sprintf("%20s %12s %12s %8s\n", "Title", "Before", "After", "Percentage")
|
||||
changes += intChange("TotalOps", v1.Data.TotalOps, v2.Data.TotalOps)
|
||||
changes += intChange("SendOps", v1.Data.SendOps, v2.Data.SendOps)
|
||||
changes += intChange("RecvOps", v1.Data.RecvOps, v2.Data.RecvOps)
|
||||
changes += intChange("Bytes/op", v1.Data.AllocedBytes, v2.Data.AllocedBytes)
|
||||
changes += intChange("Allocs/op", v1.Data.Allocs, v2.Data.Allocs)
|
||||
changes += floatChange("ReqT/op", v1.Data.ReqT, v2.Data.ReqT)
|
||||
changes += floatChange("RespT/op", v1.Data.RespT, v2.Data.RespT)
|
||||
changes += timeChange("50th-Lat", v1.Data.Fiftieth, v2.Data.Fiftieth)
|
||||
changes += timeChange("90th-Lat", v1.Data.Ninetieth, v2.Data.Ninetieth)
|
||||
changes += timeChange("99th-Lat", v1.Data.NinetyNinth, v2.Data.NinetyNinth)
|
||||
changes += timeChange("Avg-Lat", v1.Data.Average, v2.Data.Average)
|
||||
fmt.Printf("%s\n", changes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func compareBenchmark(file1, file2 string) {
|
||||
var BenchValueFile1 map[string]stats.BenchResults
|
||||
var BenchValueFile2 map[string]stats.BenchResults
|
||||
BenchValueFile1 = make(map[string]stats.BenchResults)
|
||||
BenchValueFile2 = make(map[string]stats.BenchResults)
|
||||
|
||||
createMap(file1, BenchValueFile1)
|
||||
createMap(file2, BenchValueFile2)
|
||||
|
||||
compareTwoMap(BenchValueFile1, BenchValueFile2)
|
||||
compareTwoMap(createMap(file1), createMap(file2))
|
||||
}
|
||||
|
||||
func printline(benchName, ltc50, ltc90, allocByte, allocsOp interface{}) {
|
||||
fmt.Printf("%-80v%12v%12v%12v%12v\n", benchName, ltc50, ltc90, allocByte, allocsOp)
|
||||
func printline(benchName, total, send, recv, allocB, allocN, reqT, respT, ltc50, ltc90, l99, lAvg interface{}) {
|
||||
fmt.Printf("%-80v%12v%12v%12v%12v%12v%18v%18v%12v%12v%12v%12v\n",
|
||||
benchName, total, send, recv, allocB, allocN, reqT, respT, ltc50, ltc90, l99, lAvg)
|
||||
}
|
||||
|
||||
func formatBenchmark(fileName string) {
|
||||
@ -101,26 +104,30 @@ func formatBenchmark(fileName string) {
|
||||
log.Fatalf("Read file %s error: %s\n", fileName, err)
|
||||
}
|
||||
defer f.Close()
|
||||
var data []stats.BenchResults
|
||||
var results []stats.BenchResults
|
||||
decoder := gob.NewDecoder(f)
|
||||
if err = decoder.Decode(&data); err != nil {
|
||||
if err = decoder.Decode(&results); err != nil {
|
||||
log.Fatalf("Decode file %s error: %s\n", fileName, err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
log.Fatalf("No data in file %s\n", fileName)
|
||||
if len(results) == 0 {
|
||||
log.Fatalf("No benchmark results in file %s\n", fileName)
|
||||
}
|
||||
printPos := data[0].SharedPosion
|
||||
|
||||
fmt.Println("\nShared features:\n" + strings.Repeat("-", 20))
|
||||
fmt.Print(stats.PartialPrintString(printPos, data[0].Features, true))
|
||||
fmt.Print(results[0].Features.SharedFeatures(results[0].SharedFeatures))
|
||||
fmt.Println(strings.Repeat("-", 35))
|
||||
for i := 0; i < len(data[0].SharedPosion); i++ {
|
||||
printPos[i] = !printPos[i]
|
||||
|
||||
wantFeatures := results[0].SharedFeatures
|
||||
for i := 0; i < len(results[0].SharedFeatures); i++ {
|
||||
wantFeatures[i] = !wantFeatures[i]
|
||||
}
|
||||
printline("Name", "latency-50", "latency-90", "Alloc (B)", "Alloc (#)")
|
||||
for _, d := range data {
|
||||
name := d.RunMode + stats.PartialPrintString(printPos, d.Features, false)
|
||||
printline(name, d.Latency[1].Value.String(), d.Latency[2].Value.String(),
|
||||
d.AllocedBytesPerOp, d.AllocsPerOp)
|
||||
|
||||
printline("Name", "TotalOps", "SendOps", "RecvOps", "Alloc (B)", "Alloc (#)",
|
||||
"RequestT", "ResponseT", "L-50", "L-90", "L-99", "L-Avg")
|
||||
for _, r := range results {
|
||||
d := r.Data
|
||||
printline(r.RunMode+r.Features.PrintableName(wantFeatures), d.TotalOps, d.SendOps, d.RecvOps,
|
||||
d.AllocedBytes, d.Allocs, d.ReqT, d.RespT, d.Fiftieth, d.Ninetieth, d.NinetyNinth, d.Average)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,293 +16,401 @@
|
||||
*
|
||||
*/
|
||||
|
||||
// Package stats registers stats used for creating benchmarks
|
||||
// Package stats tracks the statistics associated with benchmark runs.
|
||||
package stats
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Features contains most fields for a benchmark
|
||||
// FeatureIndex is an enum for features that usually differ across individual
|
||||
// benchmark runs in a single execution. These are usually configured by the
|
||||
// user through command line flags.
|
||||
type FeatureIndex int
|
||||
|
||||
// FeatureIndex enum values corresponding to individually settable features.
|
||||
const (
|
||||
EnableTraceIndex FeatureIndex = iota
|
||||
ReadLatenciesIndex
|
||||
ReadKbpsIndex
|
||||
ReadMTUIndex
|
||||
MaxConcurrentCallsIndex
|
||||
ReqSizeBytesIndex
|
||||
RespSizeBytesIndex
|
||||
CompModesIndex
|
||||
EnableChannelzIndex
|
||||
EnablePreloaderIndex
|
||||
|
||||
// MaxFeatureIndex is a place holder to indicate the total number of feature
|
||||
// indices we have. Any new feature indices should be added above this.
|
||||
MaxFeatureIndex
|
||||
)
|
||||
|
||||
// Features represent configured options for a specific benchmark run. This is
|
||||
// usually constructed from command line arguments passed by the caller. See
|
||||
// benchmark/benchmain/main.go for defined command line flags. This is also
|
||||
// part of the BenchResults struct which is serialized and written to a file.
|
||||
type Features struct {
|
||||
// Network mode used for this benchmark run. Could be one of Local, LAN, WAN
|
||||
// or Longhaul.
|
||||
NetworkMode string
|
||||
// UseBufCon indicates whether an in-memory connection was used for this
|
||||
// benchmark run instead of system network I/O.
|
||||
UseBufConn bool
|
||||
// EnableKeepalive indicates if keepalives were enabled on the connections
|
||||
// used in this benchmark run.
|
||||
EnableKeepalive bool
|
||||
// BenchTime indicates the duration of the benchmark run.
|
||||
BenchTime time.Duration
|
||||
|
||||
// Features defined above are usually the same for all benchmark runs in a
|
||||
// particular invocation, while the features defined below could vary from
|
||||
// run to run based on the configured command line. These features have a
|
||||
// corresponding featureIndex value which is used for a variety of reasons.
|
||||
|
||||
// EnableTrace indicates if tracing was enabled.
|
||||
EnableTrace bool
|
||||
// Latency is the simulated one-way network latency used.
|
||||
Latency time.Duration
|
||||
// Kbps is the simulated network throughput used.
|
||||
Kbps int
|
||||
Mtu int
|
||||
// MTU is the simulated network MTU used.
|
||||
MTU int
|
||||
// MaxConcurrentCalls is the number of concurrent RPCs made during this
|
||||
// benchmark run.
|
||||
MaxConcurrentCalls int
|
||||
// ReqSizeBytes is the request size in bytes used in this benchmark run.
|
||||
ReqSizeBytes int
|
||||
// RespSizeBytes is the response size in bytes used in this benchmark run.
|
||||
RespSizeBytes int
|
||||
// ModeCompressor represents the compressor mode used.
|
||||
ModeCompressor string
|
||||
// EnableChannelz indicates if channelz was turned on.
|
||||
EnableChannelz bool
|
||||
// EnablePreloader indicates if preloading was turned on.
|
||||
EnablePreloader bool
|
||||
}
|
||||
|
||||
// String returns the textual output of the Features as string.
|
||||
// String returns all the feature values as a string.
|
||||
func (f Features) String() string {
|
||||
return fmt.Sprintf("traceMode_%t-latency_%s-kbps_%#v-MTU_%#v-maxConcurrentCalls_"+
|
||||
"%#v-reqSize_%#vB-respSize_%#vB-Compressor_%s-Preloader_%t", f.EnableTrace,
|
||||
f.Latency.String(), f.Kbps, f.Mtu, f.MaxConcurrentCalls, f.ReqSizeBytes, f.RespSizeBytes, f.ModeCompressor, f.EnablePreloader)
|
||||
return fmt.Sprintf("networkMode_%v-bufConn_%v-keepalive_%v-benchTime_%v-"+
|
||||
"trace_%v-latency_%v-kbps_%v-MTU_%v-maxConcurrentCalls_%v-"+
|
||||
"reqSize_%vB-respSize_%vB-compressor_%v-channelz_%v-preloader_%v",
|
||||
f.NetworkMode, f.UseBufConn, f.EnableKeepalive, f.BenchTime,
|
||||
f.EnableTrace, f.Latency, f.Kbps, f.MTU, f.MaxConcurrentCalls,
|
||||
f.ReqSizeBytes, f.RespSizeBytes, f.ModeCompressor, f.EnableChannelz, f.EnablePreloader)
|
||||
}
|
||||
|
||||
// ConciseString returns the concise textual output of the Features as string, skipping
|
||||
// setting with default value.
|
||||
func (f Features) ConciseString() string {
|
||||
noneEmptyPos := []bool{f.EnableTrace, f.Latency != 0, f.Kbps != 0, f.Mtu != 0, true, true, true, f.ModeCompressor != "off", f.EnableChannelz, f.EnablePreloader}
|
||||
return PartialPrintString(noneEmptyPos, f, false)
|
||||
// SharedFeatures returns the shared features as a pretty printable string.
|
||||
// 'wantFeatures' is a bitmask of wanted features, indexed by FeaturesIndex.
|
||||
func (f Features) SharedFeatures(wantFeatures []bool) string {
|
||||
var b bytes.Buffer
|
||||
if f.NetworkMode != "" {
|
||||
b.WriteString(fmt.Sprintf("Network: %v\n", f.NetworkMode))
|
||||
}
|
||||
if f.UseBufConn {
|
||||
b.WriteString(fmt.Sprintf("UseBufConn: %v\n", f.UseBufConn))
|
||||
}
|
||||
if f.EnableKeepalive {
|
||||
b.WriteString(fmt.Sprintf("EnableKeepalive: %v\n", f.EnableKeepalive))
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("BenchTime: %v\n", f.BenchTime))
|
||||
f.partialString(&b, wantFeatures, ": ", "\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// PartialPrintString can print certain features with different format.
|
||||
func PartialPrintString(noneEmptyPos []bool, f Features, shared bool) string {
|
||||
s := ""
|
||||
var (
|
||||
prefix, suffix, linker string
|
||||
isNetwork bool
|
||||
)
|
||||
if shared {
|
||||
suffix = "\n"
|
||||
linker = ": "
|
||||
} else {
|
||||
prefix = "-"
|
||||
linker = "_"
|
||||
}
|
||||
if noneEmptyPos[0] {
|
||||
s += fmt.Sprintf("%sTrace%s%t%s", prefix, linker, f.EnableTrace, suffix)
|
||||
}
|
||||
if shared && f.NetworkMode != "" {
|
||||
s += fmt.Sprintf("Network: %s \n", f.NetworkMode)
|
||||
isNetwork = true
|
||||
}
|
||||
if !isNetwork {
|
||||
if noneEmptyPos[1] {
|
||||
s += fmt.Sprintf("%slatency%s%s%s", prefix, linker, f.Latency.String(), suffix)
|
||||
}
|
||||
if noneEmptyPos[2] {
|
||||
s += fmt.Sprintf("%skbps%s%#v%s", prefix, linker, f.Kbps, suffix)
|
||||
}
|
||||
if noneEmptyPos[3] {
|
||||
s += fmt.Sprintf("%sMTU%s%#v%s", prefix, linker, f.Mtu, suffix)
|
||||
}
|
||||
}
|
||||
if noneEmptyPos[4] {
|
||||
s += fmt.Sprintf("%sCallers%s%#v%s", prefix, linker, f.MaxConcurrentCalls, suffix)
|
||||
}
|
||||
if noneEmptyPos[5] {
|
||||
s += fmt.Sprintf("%sreqSize%s%#vB%s", prefix, linker, f.ReqSizeBytes, suffix)
|
||||
}
|
||||
if noneEmptyPos[6] {
|
||||
s += fmt.Sprintf("%srespSize%s%#vB%s", prefix, linker, f.RespSizeBytes, suffix)
|
||||
}
|
||||
if noneEmptyPos[7] {
|
||||
s += fmt.Sprintf("%sCompressor%s%s%s", prefix, linker, f.ModeCompressor, suffix)
|
||||
}
|
||||
if noneEmptyPos[8] {
|
||||
s += fmt.Sprintf("%sChannelz%s%t%s", prefix, linker, f.EnableChannelz, suffix)
|
||||
}
|
||||
if noneEmptyPos[9] {
|
||||
s += fmt.Sprintf("%sPreloader%s%t%s", prefix, linker, f.EnablePreloader, suffix)
|
||||
}
|
||||
return s
|
||||
// PrintableName returns a one line name which includes the features specified
|
||||
// by 'wantFeatures' which is a bitmask of wanted features, indexed by
|
||||
// FeaturesIndex.
|
||||
func (f Features) PrintableName(wantFeatures []bool) string {
|
||||
var b bytes.Buffer
|
||||
f.partialString(&b, wantFeatures, "_", "-")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type percentLatency struct {
|
||||
Percent int
|
||||
Value time.Duration
|
||||
// partialString writes features specified by 'wantFeatures' to the provided
|
||||
// bytes.Buffer.
|
||||
func (f Features) partialString(b *bytes.Buffer, wantFeatures []bool, sep, delim string) {
|
||||
for i, sf := range wantFeatures {
|
||||
if sf {
|
||||
switch FeatureIndex(i) {
|
||||
case EnableTraceIndex:
|
||||
b.WriteString(fmt.Sprintf("Trace%v%v%v", sep, f.EnableTrace, delim))
|
||||
case ReadLatenciesIndex:
|
||||
b.WriteString(fmt.Sprintf("Latency%v%v%v", sep, f.Latency, delim))
|
||||
case ReadKbpsIndex:
|
||||
b.WriteString(fmt.Sprintf("Kbps%v%v%v", sep, f.Kbps, delim))
|
||||
case ReadMTUIndex:
|
||||
b.WriteString(fmt.Sprintf("MTU%v%v%v", sep, f.MTU, delim))
|
||||
case MaxConcurrentCallsIndex:
|
||||
b.WriteString(fmt.Sprintf("Callers%v%v%v", sep, f.MaxConcurrentCalls, delim))
|
||||
case ReqSizeBytesIndex:
|
||||
b.WriteString(fmt.Sprintf("ReqSize%v%vB%v", sep, f.ReqSizeBytes, delim))
|
||||
case RespSizeBytesIndex:
|
||||
b.WriteString(fmt.Sprintf("RespSize%v%vB%v", sep, f.RespSizeBytes, delim))
|
||||
case CompModesIndex:
|
||||
b.WriteString(fmt.Sprintf("Compressor%v%v%v", sep, f.ModeCompressor, delim))
|
||||
case EnableChannelzIndex:
|
||||
b.WriteString(fmt.Sprintf("Channelz%v%v%v", sep, f.EnableChannelz, delim))
|
||||
case EnablePreloaderIndex:
|
||||
b.WriteString(fmt.Sprintf("Preloader%v%v%v", sep, f.EnablePreloader, delim))
|
||||
default:
|
||||
log.Fatalf("Unknown feature index %v. maxFeatureIndex is %v", i, MaxFeatureIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchResults records features and result of a benchmark.
|
||||
// BenchResults records features and results of a benchmark run. A collection
|
||||
// of these structs is usually serialized and written to a file after a
|
||||
// benchmark execution, and could later be read for pretty-printing or
|
||||
// comparison with other benchmark results.
|
||||
type BenchResults struct {
|
||||
// RunMode is the workload mode for this benchmark run. This could be unary,
|
||||
// stream or unconstrained.
|
||||
RunMode string
|
||||
// Features represents the configured feature options for this run.
|
||||
Features Features
|
||||
Latency []percentLatency
|
||||
Operations int
|
||||
NsPerOp int64
|
||||
AllocedBytesPerOp int64
|
||||
AllocsPerOp int64
|
||||
SharedPosion []bool
|
||||
// SharedFeatures represents the features which were shared across all
|
||||
// benchmark runs during one execution. It is a slice indexed by
|
||||
// 'FeaturesIndex' and a value of true indicates that the associated
|
||||
// feature is shared across all runs.
|
||||
SharedFeatures []bool
|
||||
// Data contains the statistical data of interest from the benchmark run.
|
||||
Data RunData
|
||||
}
|
||||
|
||||
// SetBenchmarkResult sets features of benchmark and basic results.
|
||||
func (stats *Stats) SetBenchmarkResult(mode string, features Features, o int, allocdBytes, allocs int64, sharedPos []bool) {
|
||||
stats.result.RunMode = mode
|
||||
stats.result.Features = features
|
||||
stats.result.Operations = o
|
||||
stats.result.AllocedBytesPerOp = allocdBytes
|
||||
stats.result.AllocsPerOp = allocs
|
||||
stats.result.SharedPosion = sharedPos
|
||||
}
|
||||
// RunData contains statistical data of interest from a benchmark run.
|
||||
type RunData struct {
|
||||
// TotalOps is the number of operations executed during this benchmark run.
|
||||
// Only makes sense for unary and streaming workloads.
|
||||
TotalOps uint64
|
||||
// SendOps is the number of send operations executed during this benchmark
|
||||
// run. Only makes sense for unconstrained workloads.
|
||||
SendOps uint64
|
||||
// RecvOps is the number of receive operations executed during this benchmark
|
||||
// run. Only makes sense for unconstrained workloads.
|
||||
RecvOps uint64
|
||||
// AllocedBytes is the average memory allocation in bytes per operation.
|
||||
AllocedBytes uint64
|
||||
// Allocs is the average number of memory allocations per operation.
|
||||
Allocs uint64
|
||||
// ReqT is the average request throughput associated with this run.
|
||||
ReqT float64
|
||||
// RespT is the average response throughput associated with this run.
|
||||
RespT float64
|
||||
|
||||
// GetBenchmarkResults returns the result of the benchmark including features and result.
|
||||
func (stats *Stats) GetBenchmarkResults() BenchResults {
|
||||
return stats.result
|
||||
}
|
||||
// We store different latencies associated with each run. These latencies are
|
||||
// only computed for unary and stream workloads as they are not very useful
|
||||
// for unconstrained workloads.
|
||||
|
||||
// BenchString output latency stats as the format as time + unit.
|
||||
func (stats *Stats) BenchString() string {
|
||||
stats.maybeUpdate()
|
||||
s := stats.result
|
||||
res := s.RunMode + "-" + s.Features.String() + ": \n"
|
||||
if len(s.Latency) != 0 {
|
||||
var statsUnit = s.Latency[0].Value
|
||||
var timeUnit = fmt.Sprintf("%v", statsUnit)[1:]
|
||||
for i := 1; i < len(s.Latency)-1; i++ {
|
||||
res += fmt.Sprintf("%d_Latency: %s %s \t", s.Latency[i].Percent,
|
||||
strconv.FormatFloat(float64(s.Latency[i].Value)/float64(statsUnit), 'f', 4, 64), timeUnit)
|
||||
}
|
||||
res += fmt.Sprintf("Avg latency: %s %s \t",
|
||||
strconv.FormatFloat(float64(s.Latency[len(s.Latency)-1].Value)/float64(statsUnit), 'f', 4, 64), timeUnit)
|
||||
}
|
||||
res += fmt.Sprintf("Count: %v \t", s.Operations)
|
||||
res += fmt.Sprintf("%v Bytes/op\t", s.AllocedBytesPerOp)
|
||||
res += fmt.Sprintf("%v Allocs/op\t", s.AllocsPerOp)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// Stats is a simple helper for gathering additional statistics like histogram
|
||||
// during benchmarks. This is not thread safe.
|
||||
type Stats struct {
|
||||
numBuckets int
|
||||
unit time.Duration
|
||||
min, max int64
|
||||
histogram *Histogram
|
||||
|
||||
durations durationSlice
|
||||
dirty bool
|
||||
|
||||
sortLatency bool
|
||||
result BenchResults
|
||||
// Fiftieth is the 50th percentile latency.
|
||||
Fiftieth time.Duration
|
||||
// Ninetieth is the 90th percentile latency.
|
||||
Ninetieth time.Duration
|
||||
// Ninetyninth is the 99th percentile latency.
|
||||
NinetyNinth time.Duration
|
||||
// Average is the average latency.
|
||||
Average time.Duration
|
||||
}
|
||||
|
||||
type durationSlice []time.Duration
|
||||
|
||||
// NewStats creates a new Stats instance. If numBuckets is not positive,
|
||||
// the default value (16) will be used.
|
||||
func (a durationSlice) Len() int { return len(a) }
|
||||
func (a durationSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a durationSlice) Less(i, j int) bool { return a[i] < a[j] }
|
||||
|
||||
// Stats is a helper for gathering statistics about individual benchmark runs.
|
||||
type Stats struct {
|
||||
mu sync.Mutex
|
||||
numBuckets int
|
||||
hw *histWrapper
|
||||
results []BenchResults
|
||||
startMS runtime.MemStats
|
||||
stopMS runtime.MemStats
|
||||
}
|
||||
|
||||
type histWrapper struct {
|
||||
unit time.Duration
|
||||
histogram *Histogram
|
||||
durations durationSlice
|
||||
}
|
||||
|
||||
// NewStats creates a new Stats instance. If numBuckets is not positive, the
|
||||
// default value (16) will be used.
|
||||
func NewStats(numBuckets int) *Stats {
|
||||
if numBuckets <= 0 {
|
||||
numBuckets = 16
|
||||
}
|
||||
return &Stats{
|
||||
// Use one more bucket for the last unbounded bucket.
|
||||
numBuckets: numBuckets + 1,
|
||||
durations: make(durationSlice, 0, 100000),
|
||||
}
|
||||
s := &Stats{numBuckets: numBuckets + 1}
|
||||
s.hw = &histWrapper{}
|
||||
return s
|
||||
}
|
||||
|
||||
// Add adds an elapsed time per operation to the stats.
|
||||
func (stats *Stats) Add(d time.Duration) {
|
||||
stats.durations = append(stats.durations, d)
|
||||
stats.dirty = true
|
||||
// StartRun is to be invoked to indicate the start of a new benchmark run.
|
||||
func (s *Stats) StartRun(mode string, f Features, sf []bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
runtime.ReadMemStats(&s.startMS)
|
||||
s.results = append(s.results, BenchResults{RunMode: mode, Features: f, SharedFeatures: sf})
|
||||
}
|
||||
|
||||
// Clear resets the stats, removing all values.
|
||||
func (stats *Stats) Clear() {
|
||||
stats.durations = stats.durations[:0]
|
||||
stats.histogram = nil
|
||||
stats.dirty = false
|
||||
stats.result = BenchResults{}
|
||||
// EndRun is to be invoked to indicate the end of the ongoing benchmark run. It
|
||||
// computes a bunch of stats and dumps them to stdout.
|
||||
func (s *Stats) EndRun(count uint64) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
runtime.ReadMemStats(&s.stopMS)
|
||||
r := &s.results[len(s.results)-1]
|
||||
r.Data = RunData{
|
||||
TotalOps: count,
|
||||
AllocedBytes: s.stopMS.TotalAlloc - s.startMS.TotalAlloc,
|
||||
Allocs: s.stopMS.Mallocs - s.startMS.Mallocs,
|
||||
ReqT: float64(count) * float64(r.Features.ReqSizeBytes) * 8 / r.Features.BenchTime.Seconds(),
|
||||
RespT: float64(count) * float64(r.Features.RespSizeBytes) * 8 / r.Features.BenchTime.Seconds(),
|
||||
}
|
||||
s.computeLatencies(r)
|
||||
s.dump(r)
|
||||
s.hw = &histWrapper{}
|
||||
}
|
||||
|
||||
// EndUnconstrainedRun is similar to EndRun, but is to be used for
|
||||
// unconstrained workloads.
|
||||
func (s *Stats) EndUnconstrainedRun(req uint64, resp uint64) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
runtime.ReadMemStats(&s.stopMS)
|
||||
r := &s.results[len(s.results)-1]
|
||||
r.Data = RunData{
|
||||
SendOps: req,
|
||||
RecvOps: resp,
|
||||
AllocedBytes: (s.stopMS.TotalAlloc - s.startMS.TotalAlloc) / ((req + resp) / 2),
|
||||
Allocs: (s.stopMS.Mallocs - s.startMS.Mallocs) / ((req + resp) / 2),
|
||||
ReqT: float64(req) * float64(r.Features.ReqSizeBytes) * 8 / r.Features.BenchTime.Seconds(),
|
||||
RespT: float64(resp) * float64(r.Features.RespSizeBytes) * 8 / r.Features.BenchTime.Seconds(),
|
||||
}
|
||||
s.computeLatencies(r)
|
||||
s.dump(r)
|
||||
s.hw = &histWrapper{}
|
||||
}
|
||||
|
||||
// AddDuration adds an elapsed duration per operation to the stats. This is
|
||||
// used by unary and stream modes where request and response stats are equal.
|
||||
func (s *Stats) AddDuration(d time.Duration) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.hw.durations = append(s.hw.durations, d)
|
||||
}
|
||||
|
||||
// GetResults returns the results from all benchmark runs.
|
||||
func (s *Stats) GetResults() []BenchResults {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return s.results
|
||||
}
|
||||
|
||||
// computeLatencies computes percentile latencies based on durations stored in
|
||||
// the stats object and updates the corresponding fields in the result object.
|
||||
func (s *Stats) computeLatencies(result *BenchResults) {
|
||||
if len(s.hw.durations) == 0 {
|
||||
return
|
||||
}
|
||||
sort.Sort(s.hw.durations)
|
||||
minDuration := int64(s.hw.durations[0])
|
||||
maxDuration := int64(s.hw.durations[len(s.hw.durations)-1])
|
||||
|
||||
// Use the largest unit that can represent the minimum time duration.
|
||||
s.hw.unit = time.Nanosecond
|
||||
for _, u := range []time.Duration{time.Microsecond, time.Millisecond, time.Second} {
|
||||
if minDuration <= int64(u) {
|
||||
break
|
||||
}
|
||||
s.hw.unit = u
|
||||
}
|
||||
|
||||
numBuckets := s.numBuckets
|
||||
if n := int(maxDuration - minDuration + 1); n < numBuckets {
|
||||
numBuckets = n
|
||||
}
|
||||
s.hw.histogram = NewHistogram(HistogramOptions{
|
||||
NumBuckets: numBuckets,
|
||||
// max-min(lower bound of last bucket) = (1 + growthFactor)^(numBuckets-2) * baseBucketSize.
|
||||
GrowthFactor: math.Pow(float64(maxDuration-minDuration), 1/float64(numBuckets-2)) - 1,
|
||||
BaseBucketSize: 1.0,
|
||||
MinValue: minDuration,
|
||||
})
|
||||
for _, d := range s.hw.durations {
|
||||
s.hw.histogram.Add(int64(d))
|
||||
}
|
||||
result.Data.Fiftieth = s.hw.durations[max(s.hw.histogram.Count*int64(50)/100-1, 0)]
|
||||
result.Data.Ninetieth = s.hw.durations[max(s.hw.histogram.Count*int64(90)/100-1, 0)]
|
||||
result.Data.NinetyNinth = s.hw.durations[max(s.hw.histogram.Count*int64(99)/100-1, 0)]
|
||||
result.Data.Average = time.Duration(float64(s.hw.histogram.Sum) / float64(s.hw.histogram.Count))
|
||||
}
|
||||
|
||||
// dump returns a printable version.
|
||||
func (s *Stats) dump(result *BenchResults) {
|
||||
var b bytes.Buffer
|
||||
// This prints the run mode and all features of the bench on a line.
|
||||
b.WriteString(fmt.Sprintf("%s-%s:\n", result.RunMode, result.Features.String()))
|
||||
|
||||
unit := s.hw.unit
|
||||
tUnit := fmt.Sprintf("%v", unit)[1:] // stores one of s, ms, μs, ns
|
||||
|
||||
if l := result.Data.Fiftieth; l != 0 {
|
||||
b.WriteString(fmt.Sprintf("50_Latency: %s%s\t", strconv.FormatFloat(float64(l)/float64(unit), 'f', 4, 64), tUnit))
|
||||
}
|
||||
if l := result.Data.Ninetieth; l != 0 {
|
||||
b.WriteString(fmt.Sprintf("90_Latency: %s%s\t", strconv.FormatFloat(float64(l)/float64(unit), 'f', 4, 64), tUnit))
|
||||
}
|
||||
if l := result.Data.NinetyNinth; l != 0 {
|
||||
b.WriteString(fmt.Sprintf("99_Latency: %s%s\t", strconv.FormatFloat(float64(l)/float64(unit), 'f', 4, 64), tUnit))
|
||||
}
|
||||
if l := result.Data.Average; l != 0 {
|
||||
b.WriteString(fmt.Sprintf("Avg_Latency: %s%s\t", strconv.FormatFloat(float64(l)/float64(unit), 'f', 4, 64), tUnit))
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("Bytes/op: %v\t", result.Data.AllocedBytes))
|
||||
b.WriteString(fmt.Sprintf("Allocs/op: %v\t\n", result.Data.Allocs))
|
||||
|
||||
// This prints the histogram stats for the latency.
|
||||
if s.hw.histogram == nil {
|
||||
b.WriteString("Histogram (empty)\n")
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf("Histogram (unit: %s)\n", tUnit))
|
||||
s.hw.histogram.PrintWithUnit(&b, float64(unit))
|
||||
}
|
||||
|
||||
// Print throughput data.
|
||||
req := result.Data.SendOps
|
||||
if req == 0 {
|
||||
req = result.Data.TotalOps
|
||||
}
|
||||
resp := result.Data.RecvOps
|
||||
if resp == 0 {
|
||||
resp = result.Data.TotalOps
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("Number of requests: %v\tRequest throughput: %v bit/s\n", req, result.Data.ReqT))
|
||||
b.WriteString(fmt.Sprintf("Number of responses: %v\tResponse throughput: %v bit/s\n", resp, result.Data.RespT))
|
||||
fmt.Println(b.String())
|
||||
}
|
||||
|
||||
//Sort method for durations
|
||||
func (a durationSlice) Len() int { return len(a) }
|
||||
func (a durationSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a durationSlice) Less(i, j int) bool { return a[i] < a[j] }
|
||||
func max(a, b int64) int64 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// maybeUpdate updates internal stat data if there was any newly added
|
||||
// stats since this was updated.
|
||||
func (stats *Stats) maybeUpdate() {
|
||||
if !stats.dirty {
|
||||
return
|
||||
}
|
||||
|
||||
if stats.sortLatency {
|
||||
sort.Sort(stats.durations)
|
||||
stats.min = int64(stats.durations[0])
|
||||
stats.max = int64(stats.durations[len(stats.durations)-1])
|
||||
}
|
||||
|
||||
stats.min = math.MaxInt64
|
||||
stats.max = 0
|
||||
for _, d := range stats.durations {
|
||||
if stats.min > int64(d) {
|
||||
stats.min = int64(d)
|
||||
}
|
||||
if stats.max < int64(d) {
|
||||
stats.max = int64(d)
|
||||
}
|
||||
}
|
||||
|
||||
// Use the largest unit that can represent the minimum time duration.
|
||||
stats.unit = time.Nanosecond
|
||||
for _, u := range []time.Duration{time.Microsecond, time.Millisecond, time.Second} {
|
||||
if stats.min <= int64(u) {
|
||||
break
|
||||
}
|
||||
stats.unit = u
|
||||
}
|
||||
|
||||
numBuckets := stats.numBuckets
|
||||
if n := int(stats.max - stats.min + 1); n < numBuckets {
|
||||
numBuckets = n
|
||||
}
|
||||
stats.histogram = NewHistogram(HistogramOptions{
|
||||
NumBuckets: numBuckets,
|
||||
// max-min(lower bound of last bucket) = (1 + growthFactor)^(numBuckets-2) * baseBucketSize.
|
||||
GrowthFactor: math.Pow(float64(stats.max-stats.min), 1/float64(numBuckets-2)) - 1,
|
||||
BaseBucketSize: 1.0,
|
||||
MinValue: stats.min})
|
||||
|
||||
for _, d := range stats.durations {
|
||||
stats.histogram.Add(int64(d))
|
||||
}
|
||||
|
||||
stats.dirty = false
|
||||
|
||||
if stats.durations.Len() != 0 {
|
||||
var percentToObserve = []int{50, 90, 99}
|
||||
// First data record min unit from the latency result.
|
||||
stats.result.Latency = append(stats.result.Latency, percentLatency{Percent: -1, Value: stats.unit})
|
||||
for _, position := range percentToObserve {
|
||||
stats.result.Latency = append(stats.result.Latency, percentLatency{Percent: position, Value: stats.durations[max(stats.histogram.Count*int64(position)/100-1, 0)]})
|
||||
}
|
||||
// Last data record the average latency.
|
||||
avg := float64(stats.histogram.Sum) / float64(stats.histogram.Count)
|
||||
stats.result.Latency = append(stats.result.Latency, percentLatency{Percent: -1, Value: time.Duration(avg)})
|
||||
}
|
||||
}
|
||||
|
||||
// SortLatency blocks the output
|
||||
func (stats *Stats) SortLatency() {
|
||||
stats.sortLatency = true
|
||||
}
|
||||
|
||||
// Print writes textual output of the Stats.
|
||||
func (stats *Stats) Print(w io.Writer) {
|
||||
stats.maybeUpdate()
|
||||
if stats.histogram == nil {
|
||||
fmt.Fprint(w, "Histogram (empty)\n")
|
||||
} else {
|
||||
fmt.Fprintf(w, "Histogram (unit: %s)\n", fmt.Sprintf("%v", stats.unit)[1:])
|
||||
stats.histogram.PrintWithUnit(w, float64(stats.unit))
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the textual output of the Stats as string.
|
||||
func (stats *Stats) String() string {
|
||||
var b bytes.Buffer
|
||||
stats.Print(&b)
|
||||
return b.String()
|
||||
}
|
||||
|
@ -1,208 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2017 gRPC 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 stats
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
curB *testing.B
|
||||
curBenchName string
|
||||
curStats map[string]*Stats
|
||||
|
||||
orgStdout *os.File
|
||||
nextOutPos int
|
||||
|
||||
injectCond *sync.Cond
|
||||
injectDone chan struct{}
|
||||
)
|
||||
|
||||
// AddStats adds a new unnamed Stats instance to the current benchmark. You need
|
||||
// to run benchmarks by calling RunTestMain() to inject the stats to the
|
||||
// benchmark results. If numBuckets is not positive, the default value (16) will
|
||||
// be used. Please note that this calls b.ResetTimer() since it may be blocked
|
||||
// until the previous benchmark stats is printed out. So AddStats() should
|
||||
// typically be called at the very beginning of each benchmark function.
|
||||
func AddStats(b *testing.B, numBuckets int) *Stats {
|
||||
return AddStatsWithName(b, "", numBuckets)
|
||||
}
|
||||
|
||||
// AddStatsWithName adds a new named Stats instance to the current benchmark.
|
||||
// With this, you can add multiple stats in a single benchmark. You need
|
||||
// to run benchmarks by calling RunTestMain() to inject the stats to the
|
||||
// benchmark results. If numBuckets is not positive, the default value (16) will
|
||||
// be used. Please note that this calls b.ResetTimer() since it may be blocked
|
||||
// until the previous benchmark stats is printed out. So AddStatsWithName()
|
||||
// should typically be called at the very beginning of each benchmark function.
|
||||
func AddStatsWithName(b *testing.B, name string, numBuckets int) *Stats {
|
||||
var benchName string
|
||||
for i := 1; ; i++ {
|
||||
pc, _, _, ok := runtime.Caller(i)
|
||||
if !ok {
|
||||
panic("benchmark function not found")
|
||||
}
|
||||
p := strings.Split(runtime.FuncForPC(pc).Name(), ".")
|
||||
benchName = p[len(p)-1]
|
||||
if strings.HasPrefix(benchName, "run") {
|
||||
break
|
||||
}
|
||||
}
|
||||
procs := runtime.GOMAXPROCS(-1)
|
||||
if procs != 1 {
|
||||
benchName = fmt.Sprintf("%s-%d", benchName, procs)
|
||||
}
|
||||
|
||||
stats := NewStats(numBuckets)
|
||||
|
||||
if injectCond != nil {
|
||||
// We need to wait until the previous benchmark stats is printed out.
|
||||
injectCond.L.Lock()
|
||||
for curB != nil && curBenchName != benchName {
|
||||
injectCond.Wait()
|
||||
}
|
||||
|
||||
curB = b
|
||||
curBenchName = benchName
|
||||
curStats[name] = stats
|
||||
|
||||
injectCond.L.Unlock()
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
return stats
|
||||
}
|
||||
|
||||
// RunTestMain runs the tests with enabling injection of benchmark stats. It
|
||||
// returns an exit code to pass to os.Exit.
|
||||
func RunTestMain(m *testing.M) int {
|
||||
startStatsInjector()
|
||||
defer stopStatsInjector()
|
||||
return m.Run()
|
||||
}
|
||||
|
||||
// startStatsInjector starts stats injection to benchmark results.
|
||||
func startStatsInjector() {
|
||||
orgStdout = os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
nextOutPos = 0
|
||||
|
||||
resetCurBenchStats()
|
||||
|
||||
injectCond = sync.NewCond(&sync.Mutex{})
|
||||
injectDone = make(chan struct{})
|
||||
go func() {
|
||||
defer close(injectDone)
|
||||
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Split(splitLines)
|
||||
for scanner.Scan() {
|
||||
injectStatsIfFinished(scanner.Text())
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// stopStatsInjector stops stats injection and restores os.Stdout.
|
||||
func stopStatsInjector() {
|
||||
os.Stdout.Close()
|
||||
<-injectDone
|
||||
injectCond = nil
|
||||
os.Stdout = orgStdout
|
||||
}
|
||||
|
||||
// splitLines is a split function for a bufio.Scanner that returns each line
|
||||
// of text, teeing texts to the original stdout even before each line ends.
|
||||
func splitLines(data []byte, eof bool) (advance int, token []byte, err error) {
|
||||
if eof && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
if i := bytes.IndexByte(data, '\n'); i >= 0 {
|
||||
orgStdout.Write(data[nextOutPos : i+1])
|
||||
nextOutPos = 0
|
||||
return i + 1, data[0:i], nil
|
||||
}
|
||||
|
||||
orgStdout.Write(data[nextOutPos:])
|
||||
nextOutPos = len(data)
|
||||
|
||||
if eof {
|
||||
// This is a final, non-terminated line. Return it.
|
||||
return len(data), data, nil
|
||||
}
|
||||
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// injectStatsIfFinished prints out the stats if the current benchmark finishes.
|
||||
func injectStatsIfFinished(line string) {
|
||||
injectCond.L.Lock()
|
||||
defer injectCond.L.Unlock()
|
||||
// We assume that the benchmark results start with "Benchmark".
|
||||
if curB == nil || !strings.HasPrefix(line, "Benchmark") {
|
||||
return
|
||||
}
|
||||
|
||||
if !curB.Failed() {
|
||||
// Output all stats in alphabetical order.
|
||||
names := make([]string, 0, len(curStats))
|
||||
for name := range curStats {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
stats := curStats[name]
|
||||
// The output of stats starts with a header like "Histogram (unit: ms)"
|
||||
// followed by statistical properties and the buckets. Add the stats name
|
||||
// if it is a named stats and indent them as Go testing outputs.
|
||||
lines := strings.Split(stats.String(), "\n")
|
||||
if n := len(lines); n > 0 {
|
||||
if name != "" {
|
||||
name = ": " + name
|
||||
}
|
||||
fmt.Fprintf(orgStdout, "--- %s%s\n", lines[0], name)
|
||||
for _, line := range lines[1 : n-1] {
|
||||
fmt.Fprintf(orgStdout, "\t%s\n", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetCurBenchStats()
|
||||
injectCond.Signal()
|
||||
}
|
||||
|
||||
// resetCurBenchStats resets the current benchmark stats.
|
||||
func resetCurBenchStats() {
|
||||
curB = nil
|
||||
curBenchName = ""
|
||||
curStats = make(map[string]*Stats)
|
||||
}
|
Reference in New Issue
Block a user