snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-15 20:40:15 +05:00
commit e241b33f26
1365 changed files with 339570 additions and 0 deletions

1022
benchmark/benchmain/main.go Normal file

File diff suppressed because it is too large Load Diff

349
benchmark/benchmark.go Normal file
View File

@@ -0,0 +1,349 @@
/*
*
* Copyright 2014 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 benchmark implements the building blocks to setup end-to-end gRPC benchmarks.
*/
package benchmark
import (
"context"
"fmt"
"io"
"log"
rand "math/rand/v2"
"net"
"strconv"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
)
var logger = grpclog.Component("benchmark")
// Allows reuse of the same testpb.Payload object.
func setPayload(p *testpb.Payload, t testpb.PayloadType, size int) {
if size < 0 {
logger.Fatalf("Requested a response with invalid length %d", size)
}
body := make([]byte, size)
switch t {
case testpb.PayloadType_COMPRESSABLE:
default:
logger.Fatalf("Unsupported payload type: %d", t)
}
p.Type = t
p.Body = body
}
// NewPayload creates a payload with the given type and size.
func NewPayload(t testpb.PayloadType, size int) *testpb.Payload {
p := new(testpb.Payload)
setPayload(p, t, size)
return p
}
type testServer struct {
testgrpc.UnimplementedBenchmarkServiceServer
}
func (s *testServer) UnaryCall(_ context.Context, in *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
return &testpb.SimpleResponse{
Payload: NewPayload(in.ResponseType, int(in.ResponseSize)),
}, nil
}
// UnconstrainedStreamingHeader indicates to the StreamingCall handler that its
// behavior should be unconstrained (constant send/receive in parallel) instead
// of ping-pong.
const UnconstrainedStreamingHeader = "unconstrained-streaming"
// UnconstrainedStreamingDelayHeader is used to pass the maximum amount of time
// the server should sleep between consecutive RPC responses.
const UnconstrainedStreamingDelayHeader = "unconstrained-streaming-delay"
// PreloadMsgSizeHeader indicates that the client is going to ask for
// a fixed response size and passes this size to the server.
// The server is expected to preload the response on startup.
const PreloadMsgSizeHeader = "preload-msg-size"
func (s *testServer) StreamingCall(stream testgrpc.BenchmarkService_StreamingCallServer) error {
preloadMsgSize := 0
if md, ok := metadata.FromIncomingContext(stream.Context()); ok && len(md[PreloadMsgSizeHeader]) != 0 {
val := md[PreloadMsgSizeHeader][0]
var err error
preloadMsgSize, err = strconv.Atoi(val)
if err != nil {
return fmt.Errorf("%q header value is not an integer: %s", PreloadMsgSizeHeader, err)
}
}
if md, ok := metadata.FromIncomingContext(stream.Context()); ok && len(md[UnconstrainedStreamingHeader]) != 0 {
return s.UnconstrainedStreamingCall(stream, preloadMsgSize)
}
response := &testpb.SimpleResponse{
Payload: new(testpb.Payload),
}
preloadedResponse := &grpc.PreparedMsg{}
if preloadMsgSize > 0 {
setPayload(response.Payload, testpb.PayloadType_COMPRESSABLE, preloadMsgSize)
if err := preloadedResponse.Encode(stream, response); err != nil {
return err
}
}
in := new(testpb.SimpleRequest)
for {
// use ServerStream directly to reuse the same testpb.SimpleRequest object
err := stream.(grpc.ServerStream).RecvMsg(in)
if err == io.EOF {
// read done.
return nil
}
if err != nil {
return err
}
if preloadMsgSize > 0 {
err = stream.SendMsg(preloadedResponse)
} else {
setPayload(response.Payload, in.ResponseType, int(in.ResponseSize))
err = stream.Send(response)
}
if err != nil {
return err
}
}
}
func (s *testServer) UnconstrainedStreamingCall(stream testgrpc.BenchmarkService_StreamingCallServer, preloadMsgSize int) error {
maxSleep := 0
if md, ok := metadata.FromIncomingContext(stream.Context()); ok && len(md[UnconstrainedStreamingDelayHeader]) != 0 {
val := md[UnconstrainedStreamingDelayHeader][0]
d, err := time.ParseDuration(val)
if err != nil {
return fmt.Errorf("can't parse %q header: %s", UnconstrainedStreamingDelayHeader, err)
}
maxSleep = int(d)
}
in := new(testpb.SimpleRequest)
// Receive a message to learn response type and size.
err := stream.RecvMsg(in)
if err == io.EOF {
// read done.
return nil
}
if err != nil {
return err
}
response := &testpb.SimpleResponse{
Payload: new(testpb.Payload),
}
setPayload(response.Payload, in.ResponseType, int(in.ResponseSize))
preloadedResponse := &grpc.PreparedMsg{}
if preloadMsgSize > 0 {
if err := preloadedResponse.Encode(stream, response); err != nil {
return err
}
}
go func() {
for {
// Using RecvMsg rather than Recv to prevent reallocation of SimpleRequest.
err := stream.RecvMsg(in)
switch status.Code(err) {
case codes.Canceled:
return
case codes.OK:
default:
log.Fatalf("server recv error: %v", err)
}
}
}()
go func() {
for {
if maxSleep > 0 {
time.Sleep(time.Duration(rand.IntN(maxSleep)))
}
var err error
if preloadMsgSize > 0 {
err = stream.SendMsg(preloadedResponse)
} else {
err = stream.Send(response)
}
switch status.Code(err) {
case codes.Unavailable, codes.Canceled:
return
case codes.OK:
default:
log.Fatalf("server send error: %v", err)
}
}
}()
<-stream.Context().Done()
return stream.Context().Err()
}
// byteBufServer is a gRPC server that sends and receives byte buffer.
// The purpose is to benchmark the gRPC performance without protobuf serialization/deserialization overhead.
type byteBufServer struct {
testgrpc.UnimplementedBenchmarkServiceServer
respSize int32
}
// UnaryCall is an empty function and is not used for benchmark.
// If bytebuf UnaryCall benchmark is needed later, the function body needs to be updated.
func (s *byteBufServer) UnaryCall(context.Context, *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
return &testpb.SimpleResponse{}, nil
}
func (s *byteBufServer) StreamingCall(stream testgrpc.BenchmarkService_StreamingCallServer) error {
for {
var in []byte
err := stream.(grpc.ServerStream).RecvMsg(&in)
if err == io.EOF {
return nil
}
if err != nil {
return err
}
out := make([]byte, s.respSize)
if err := stream.(grpc.ServerStream).SendMsg(&out); err != nil {
return err
}
}
}
// ServerInfo contains the information to create a gRPC benchmark server.
type ServerInfo struct {
// Type is the type of the server.
// It should be "protobuf" or "bytebuf".
Type string
// Metadata is an optional configuration.
// For "protobuf", it's ignored.
// For "bytebuf", it should be an int representing response size.
Metadata any
// Listener is the network listener for the server to use
Listener net.Listener
}
// StartServer starts a gRPC server serving a benchmark service according to info.
// It returns a function to stop the server.
func StartServer(info ServerInfo, opts ...grpc.ServerOption) func() {
s := grpc.NewServer(opts...)
switch info.Type {
case "protobuf":
testgrpc.RegisterBenchmarkServiceServer(s, &testServer{})
case "bytebuf":
respSize, ok := info.Metadata.(int32)
if !ok {
logger.Fatalf("failed to StartServer, invalid metadata: %v, for Type: %v", info.Metadata, info.Type)
}
testgrpc.RegisterBenchmarkServiceServer(s, &byteBufServer{respSize: respSize})
default:
logger.Fatalf("failed to StartServer, unknown Type: %v", info.Type)
}
go s.Serve(info.Listener)
return func() {
s.Stop()
}
}
// DoUnaryCall performs a unary RPC with given stub and request and response sizes.
func DoUnaryCall(tc testgrpc.BenchmarkServiceClient, reqSize, respSize int) error {
pl := NewPayload(testpb.PayloadType_COMPRESSABLE, reqSize)
req := &testpb.SimpleRequest{
ResponseType: pl.Type,
ResponseSize: int32(respSize),
Payload: pl,
}
if _, err := tc.UnaryCall(context.Background(), req); err != nil {
return fmt.Errorf("/BenchmarkService/UnaryCall(_, _) = _, %v, want _, <nil>", err)
}
return nil
}
// DoStreamingRoundTrip performs a round trip for a single streaming rpc.
func DoStreamingRoundTrip(stream testgrpc.BenchmarkService_StreamingCallClient, reqSize, respSize int) error {
pl := NewPayload(testpb.PayloadType_COMPRESSABLE, reqSize)
req := &testpb.SimpleRequest{
ResponseType: pl.Type,
ResponseSize: int32(respSize),
Payload: pl,
}
return DoStreamingRoundTripPreloaded(stream, req)
}
// DoStreamingRoundTripPreloaded performs a round trip for a single streaming rpc with preloaded payload.
func DoStreamingRoundTripPreloaded(stream testgrpc.BenchmarkService_StreamingCallClient, req any) error {
// req could be either *testpb.SimpleRequest or *grpc.PreparedMsg
if err := stream.SendMsg(req); err != nil {
return fmt.Errorf("/BenchmarkService/StreamingCall.Send(_) = %v, want <nil>", err)
}
if _, err := stream.Recv(); err != nil {
// EOF is a valid error here.
if err == io.EOF {
return nil
}
return fmt.Errorf("/BenchmarkService/StreamingCall.Recv(_) = %v, want <nil>", err)
}
return nil
}
// DoByteBufStreamingRoundTrip performs a round trip for a single streaming rpc, using a custom codec for byte buffer.
func DoByteBufStreamingRoundTrip(stream testgrpc.BenchmarkService_StreamingCallClient, reqSize, _ int) error {
out := make([]byte, reqSize)
if err := stream.(grpc.ClientStream).SendMsg(&out); err != nil {
return fmt.Errorf("/BenchmarkService/StreamingCall.(ClientStream).SendMsg(_) = %v, want <nil>", err)
}
var in []byte
if err := stream.(grpc.ClientStream).RecvMsg(&in); err != nil {
// EOF is a valid error here.
if err == io.EOF {
return nil
}
return fmt.Errorf("/BenchmarkService/StreamingCall.(ClientStream).RecvMsg(_) = %v, want <nil>", err)
}
return nil
}
// NewClientConn creates a gRPC client connection to addr.
func NewClientConn(addr string, opts ...grpc.DialOption) *grpc.ClientConn {
return NewClientConnWithContext(context.Background(), addr, opts...)
}
// NewClientConnWithContext creates a gRPC client connection to addr using ctx.
func NewClientConnWithContext(_ context.Context, addr string, opts ...grpc.DialOption) *grpc.ClientConn {
conn, err := grpc.NewClient(addr, opts...)
if err != nil {
logger.Fatalf("grpc.NewClient(%q) = %v", addr, err)
}
return conn
}

View File

@@ -0,0 +1,152 @@
/*
*
* 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.
*
*/
/*
To format the benchmark result:
go run benchmark/benchresult/main.go resultfile
To see the performance change based on an old result:
go run benchmark/benchresult/main.go resultfile_old resultfile
It will print the comparison result of intersection benchmarks between two files.
*/
package main
import (
"encoding/gob"
"fmt"
"log"
"os"
"strings"
"time"
"google.golang.org/grpc/benchmark/stats"
)
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)
}
defer f.Close()
var data []stats.BenchResults
decoder := gob.NewDecoder(f)
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 uint64) string {
return fmt.Sprintf("%20s %12d %12d %8.2f%%\n", title, val1, val2, float64(int64(val2)-int64(val1))*100/float64(val1))
}
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))
}
func strDiff(title, val1, val2 string) string {
return fmt.Sprintf("%20s %12s %12s\n", title, val1, val2)
}
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("%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 += floatChange("Bytes/op", v1.Data.AllocedBytes, v2.Data.AllocedBytes)
changes += floatChange("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)
changes += strDiff("GoVersion", v1.GoVersion, v2.GoVersion)
changes += strDiff("GrpcVersion", v1.GrpcVersion, v2.GrpcVersion)
fmt.Printf("%s\n", changes)
}
}
}
func compareBenchmark(file1, file2 string) {
compareTwoMap(createMap(file1), createMap(file2))
}
func printHeader() {
fmt.Printf("%-80s%12s%12s%12s%18s%18s%18s%18s%12s%12s%12s%12s\n",
"Name", "TotalOps", "SendOps", "RecvOps", "Bytes/op (B)", "Allocs/op (#)",
"RequestT", "ResponseT", "L-50", "L-90", "L-99", "L-Avg")
}
func printline(benchName string, d stats.RunData) {
fmt.Printf("%-80s%12d%12d%12d%18.2f%18.2f%18.2f%18.2f%12v%12v%12v%12v\n",
benchName, d.TotalOps, d.SendOps, d.RecvOps, d.AllocedBytes, d.Allocs,
d.ReqT, d.RespT, d.Fiftieth, d.Ninetieth, d.NinetyNinth, d.Average)
}
func formatBenchmark(fileName string) {
f, err := os.Open(fileName)
if err != nil {
log.Fatalf("Read file %s error: %s\n", fileName, err)
}
defer f.Close()
var results []stats.BenchResults
decoder := gob.NewDecoder(f)
if err = decoder.Decode(&results); err != nil {
log.Fatalf("Decode file %s error: %s\n", fileName, err)
}
if len(results) == 0 {
log.Fatalf("No benchmark results in file %s\n", fileName)
}
fmt.Println("\nShared features:\n" + strings.Repeat("-", 20))
fmt.Print(results[0].Features.SharedFeatures(results[0].SharedFeatures))
fmt.Println(strings.Repeat("-", 35))
wantFeatures := results[0].SharedFeatures
for i := 0; i < len(results[0].SharedFeatures); i++ {
wantFeatures[i] = !wantFeatures[i]
}
printHeader()
for _, r := range results {
printline(r.RunMode+r.Features.PrintableName(wantFeatures), r.Data)
}
}
func main() {
if len(os.Args) == 2 {
formatBenchmark(os.Args[1])
} else {
compareBenchmark(os.Args[1], os.Args[2])
}
}

217
benchmark/client/main.go Normal file
View File

@@ -0,0 +1,217 @@
/*
*
* 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 main provides a client used for benchmarking. Before running the
client, the user would need to launch the grpc server.
To start the server before running the client, you can run look for the command
under the following file:
benchmark/server/main.go
After starting the server, the client can be run. An example of how to run this
command is:
go run benchmark/client/main.go -test_name=grpc_test
If the server is running on a different port than 50051, then use the port flag
for the client to hit the server on the correct port.
An example for how to run this command on a different port can be found here:
go run benchmark/client/main.go -test_name=grpc_test -port=8080
*/
package main
import (
"context"
"flag"
"fmt"
"os"
"runtime"
"runtime/pprof"
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/benchmark"
"google.golang.org/grpc/benchmark/stats"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/internal/syscall"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
)
var (
port = flag.String("port", "50051", "Localhost port to connect to.")
numRPC = flag.Int("r", 1, "The number of concurrent RPCs on each connection.")
numConn = flag.Int("c", 1, "The number of parallel connections.")
warmupDur = flag.Int("w", 10, "Warm-up duration in seconds")
duration = flag.Int("d", 60, "Benchmark duration in seconds")
rqSize = flag.Int("req", 1, "Request message size in bytes.")
rspSize = flag.Int("resp", 1, "Response message size in bytes.")
rpcType = flag.String("rpc_type", "unary",
`Configure different client rpc type. Valid options are:
unary;
streaming.`)
testName = flag.String("test_name", "", "Name of the test used for creating profiles.")
wg sync.WaitGroup
hopts = stats.HistogramOptions{
NumBuckets: 2495,
GrowthFactor: .01,
}
mu sync.Mutex
hists []*stats.Histogram
logger = grpclog.Component("benchmark")
)
func main() {
flag.Parse()
if *testName == "" {
logger.Fatal("-test_name not set")
}
req := &testpb.SimpleRequest{
ResponseType: testpb.PayloadType_COMPRESSABLE,
ResponseSize: int32(*rspSize),
Payload: &testpb.Payload{
Type: testpb.PayloadType_COMPRESSABLE,
Body: make([]byte, *rqSize),
},
}
connectCtx, connectCancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
defer connectCancel()
ccs := buildConnections(connectCtx)
warmDeadline := time.Now().Add(time.Duration(*warmupDur) * time.Second)
endDeadline := warmDeadline.Add(time.Duration(*duration) * time.Second)
cf, err := os.Create("/tmp/" + *testName + ".cpu")
if err != nil {
logger.Fatalf("Error creating file: %v", err)
}
defer cf.Close()
pprof.StartCPUProfile(cf)
cpuBeg := syscall.GetCPUTime()
for _, cc := range ccs {
runWithConn(cc, req, warmDeadline, endDeadline)
}
wg.Wait()
cpu := time.Duration(syscall.GetCPUTime() - cpuBeg)
pprof.StopCPUProfile()
mf, err := os.Create("/tmp/" + *testName + ".mem")
if err != nil {
logger.Fatalf("Error creating file: %v", err)
}
defer mf.Close()
runtime.GC() // materialize all statistics
if err := pprof.WriteHeapProfile(mf); err != nil {
logger.Fatalf("Error writing memory profile: %v", err)
}
hist := stats.NewHistogram(hopts)
for _, h := range hists {
hist.Merge(h)
}
parseHist(hist)
fmt.Println("Client CPU utilization:", cpu)
fmt.Println("Client CPU profile:", cf.Name())
fmt.Println("Client Mem Profile:", mf.Name())
}
func buildConnections(ctx context.Context) []*grpc.ClientConn {
ccs := make([]*grpc.ClientConn, *numConn)
for i := range ccs {
ccs[i] = benchmark.NewClientConnWithContext(ctx, "localhost:"+*port,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
grpc.WithWriteBufferSize(128*1024),
grpc.WithReadBufferSize(128*1024),
)
}
return ccs
}
func runWithConn(cc *grpc.ClientConn, req *testpb.SimpleRequest, warmDeadline, endDeadline time.Time) {
for i := 0; i < *numRPC; i++ {
wg.Add(1)
go func() {
defer wg.Done()
caller := makeCaller(cc, req)
hist := stats.NewHistogram(hopts)
for {
start := time.Now()
if start.After(endDeadline) {
mu.Lock()
hists = append(hists, hist)
mu.Unlock()
return
}
caller()
elapsed := time.Since(start)
if start.After(warmDeadline) {
hist.Add(elapsed.Nanoseconds())
}
}
}()
}
}
func makeCaller(cc *grpc.ClientConn, req *testpb.SimpleRequest) func() {
client := testgrpc.NewBenchmarkServiceClient(cc)
if *rpcType == "unary" {
return func() {
if _, err := client.UnaryCall(context.Background(), req); err != nil {
logger.Fatalf("RPC failed: %v", err)
}
}
}
stream, err := client.StreamingCall(context.Background())
if err != nil {
logger.Fatalf("RPC failed: %v", err)
}
return func() {
if err := stream.Send(req); err != nil {
logger.Fatalf("Streaming RPC failed to send: %v", err)
}
if _, err := stream.Recv(); err != nil {
logger.Fatalf("Streaming RPC failed to read: %v", err)
}
}
}
func parseHist(hist *stats.Histogram) {
fmt.Println("qps:", float64(hist.Count)/float64(*duration))
fmt.Printf("Latency: (50/90/99 %%ile): %v/%v/%v\n",
time.Duration(median(.5, hist)),
time.Duration(median(.9, hist)),
time.Duration(median(.99, hist)))
}
func median(percentile float64, h *stats.Histogram) int64 {
need := int64(float64(h.Count) * percentile)
have := int64(0)
for _, bucket := range h.Buckets {
count := bucket.Count
if have+count >= need {
percent := float64(need-have) / float64(count)
return int64((1.0-percent)*bucket.LowBound + percent*bucket.LowBound*(1.0+hopts.GrowthFactor))
}
have += bucket.Count
}
panic("should have found a bound")
}

178
benchmark/flags/flags.go Normal file
View File

@@ -0,0 +1,178 @@
/*
*
* Copyright 2019 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 flags provide convenience types and routines to accept specific types
of flag values on the command line.
*/
package flags
import (
"bytes"
"encoding/csv"
"flag"
"fmt"
"strconv"
"strings"
"time"
)
// stringFlagWithAllowedValues represents a string flag which can only take a
// predefined set of values.
type stringFlagWithAllowedValues struct {
val string
allowed []string
}
// StringWithAllowedValues returns a flag variable of type
// stringFlagWithAllowedValues configured with the provided parameters.
// 'allowed` is the set of values that this flag can be set to.
func StringWithAllowedValues(name, defaultVal, usage string, allowed []string) *string {
as := &stringFlagWithAllowedValues{defaultVal, allowed}
flag.CommandLine.Var(as, name, usage)
return &as.val
}
// String implements the flag.Value interface.
func (as *stringFlagWithAllowedValues) String() string {
return as.val
}
// Set implements the flag.Value interface.
func (as *stringFlagWithAllowedValues) Set(val string) error {
for _, a := range as.allowed {
if a == val {
as.val = val
return nil
}
}
return fmt.Errorf("want one of: %v", strings.Join(as.allowed, ", "))
}
type durationSliceValue []time.Duration
// DurationSlice returns a flag representing a slice of time.Duration objects.
func DurationSlice(name string, defaultVal []time.Duration, usage string) *[]time.Duration {
ds := make([]time.Duration, len(defaultVal))
copy(ds, defaultVal)
dsv := (*durationSliceValue)(&ds)
flag.CommandLine.Var(dsv, name, usage)
return &ds
}
// Set implements the flag.Value interface.
func (dsv *durationSliceValue) Set(s string) error {
ds := strings.Split(s, ",")
var dd []time.Duration
for _, n := range ds {
d, err := time.ParseDuration(n)
if err != nil {
return err
}
dd = append(dd, d)
}
*dsv = durationSliceValue(dd)
return nil
}
// String implements the flag.Value interface.
func (dsv *durationSliceValue) String() string {
var b bytes.Buffer
for i, d := range *dsv {
if i > 0 {
b.WriteRune(',')
}
b.WriteString(d.String())
}
return b.String()
}
type intSliceValue []int
// IntSlice returns a flag representing a slice of ints.
func IntSlice(name string, defaultVal []int, usage string) *[]int {
is := make([]int, len(defaultVal))
copy(is, defaultVal)
isv := (*intSliceValue)(&is)
flag.CommandLine.Var(isv, name, usage)
return &is
}
// Set implements the flag.Value interface.
func (isv *intSliceValue) Set(s string) error {
is := strings.Split(s, ",")
var ret []int
for _, n := range is {
i, err := strconv.Atoi(n)
if err != nil {
return err
}
ret = append(ret, i)
}
*isv = intSliceValue(ret)
return nil
}
// String implements the flag.Value interface.
func (isv *intSliceValue) String() string {
var b bytes.Buffer
for i, n := range *isv {
if i > 0 {
b.WriteRune(',')
}
b.WriteString(strconv.Itoa(n))
}
return b.String()
}
type stringSliceValue []string
// StringSlice returns a flag representing a slice of strings.
func StringSlice(name string, defaultVal []string, usage string) *[]string {
ss := make([]string, len(defaultVal))
copy(ss, defaultVal)
ssv := (*stringSliceValue)(&ss)
flag.CommandLine.Var(ssv, name, usage)
return &ss
}
// escapedCommaSplit splits a comma-separated list of strings in the same way
// CSV files work (escaping a comma requires double-quotes).
func escapedCommaSplit(str string) ([]string, error) {
r := csv.NewReader(strings.NewReader(str))
ret, err := r.Read()
if err != nil {
return nil, err
}
return ret, nil
}
// Set implements the flag.Value interface.
func (ss *stringSliceValue) Set(str string) error {
var err error
*ss, err = escapedCommaSplit(str)
if err != nil {
return err
}
return nil
}
// String implements the flag.Value interface.
func (ss *stringSliceValue) String() string {
return strings.Join(*ss, ",")
}

View File

@@ -0,0 +1,153 @@
/*
*
* Copyright 2019 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 flags
import (
"flag"
"reflect"
"testing"
"time"
"google.golang.org/grpc/internal/grpctest"
)
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
func (s) TestStringWithAllowedValues(t *testing.T) {
const defaultVal = "default"
tests := []struct {
args string
allowed []string
wantVal string
wantErr bool
}{
{"-workloads=all", []string{"unary", "streaming", "all"}, "all", false},
{"-workloads=disallowed", []string{"unary", "streaming", "all"}, defaultVal, true},
}
for _, test := range tests {
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
var w = StringWithAllowedValues("workloads", defaultVal, "usage", test.allowed)
err := flag.CommandLine.Parse([]string{test.args})
switch {
case !test.wantErr && err != nil:
t.Errorf("failed to parse command line args {%v}: %v", test.args, err)
case test.wantErr && err == nil:
t.Errorf("flag.Parse(%v) = nil, want non-nil error", test.args)
default:
if *w != test.wantVal {
t.Errorf("flag value is %v, want %v", *w, test.wantVal)
}
}
}
}
func (s) TestDurationSlice(t *testing.T) {
defaultVal := []time.Duration{time.Second, time.Nanosecond}
tests := []struct {
args string
wantVal []time.Duration
wantErr bool
}{
{"-latencies=1s", []time.Duration{time.Second}, false},
{"-latencies=1s,2s,3s", []time.Duration{time.Second, 2 * time.Second, 3 * time.Second}, false},
{"-latencies=bad", defaultVal, true},
}
for _, test := range tests {
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
var w = DurationSlice("latencies", defaultVal, "usage")
err := flag.CommandLine.Parse([]string{test.args})
switch {
case !test.wantErr && err != nil:
t.Errorf("failed to parse command line args {%v}: %v", test.args, err)
case test.wantErr && err == nil:
t.Errorf("flag.Parse(%v) = nil, want non-nil error", test.args)
default:
if !reflect.DeepEqual(*w, test.wantVal) {
t.Errorf("flag value is %v, want %v", *w, test.wantVal)
}
}
}
}
func (s) TestIntSlice(t *testing.T) {
defaultVal := []int{1, 1024}
tests := []struct {
args string
wantVal []int
wantErr bool
}{
{"-kbps=1", []int{1}, false},
{"-kbps=1,2,3", []int{1, 2, 3}, false},
{"-kbps=20e4", defaultVal, true},
}
for _, test := range tests {
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
var w = IntSlice("kbps", defaultVal, "usage")
err := flag.CommandLine.Parse([]string{test.args})
switch {
case !test.wantErr && err != nil:
t.Errorf("failed to parse command line args {%v}: %v", test.args, err)
case test.wantErr && err == nil:
t.Errorf("flag.Parse(%v) = nil, want non-nil error", test.args)
default:
if !reflect.DeepEqual(*w, test.wantVal) {
t.Errorf("flag value is %v, want %v", *w, test.wantVal)
}
}
}
}
func (s) TestStringSlice(t *testing.T) {
defaultVal := []string{"bar", "baz"}
tests := []struct {
args string
wantVal []string
wantErr bool
}{
{"-name=foobar", []string{"foobar"}, false},
{"-name=foo,bar", []string{"foo", "bar"}, false},
{`-name="foo,bar",baz`, []string{"foo,bar", "baz"}, false},
{`-name="foo,bar""",baz`, []string{`foo,bar"`, "baz"}, false},
}
for _, test := range tests {
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
var w = StringSlice("name", defaultVal, "usage")
err := flag.CommandLine.Parse([]string{test.args})
switch {
case !test.wantErr && err != nil:
t.Errorf("failed to parse command line args {%v}: %v", test.args, err)
case test.wantErr && err == nil:
t.Errorf("flag.Parse(%v) = nil, want non-nil error", test.args)
default:
if !reflect.DeepEqual(*w, test.wantVal) {
t.Errorf("flag value is %v, want %v", *w, test.wantVal)
}
}
}
}

View File

@@ -0,0 +1,334 @@
/*
*
* 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 latency provides wrappers for net.Conn, net.Listener, and
// net.Dialers, designed to interoperate to inject real-world latency into
// network connections.
package latency
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"io"
"net"
"time"
)
// Dialer is a function matching the signature of net.Dial.
type Dialer func(network, address string) (net.Conn, error)
// TimeoutDialer is a function matching the signature of net.DialTimeout.
type TimeoutDialer func(network, address string, timeout time.Duration) (net.Conn, error)
// ContextDialer is a function matching the signature of
// net.Dialer.DialContext.
type ContextDialer func(ctx context.Context, network, address string) (net.Conn, error)
// Network represents a network with the given bandwidth, latency, and MTU
// (Maximum Transmission Unit) configuration, and can produce wrappers of
// net.Listeners, net.Conn, and various forms of dialing functions. The
// Listeners and Dialers/Conns on both sides of connections must come from this
// package, but need not be created from the same Network. Latency is computed
// when sending (in Write), and is injected when receiving (in Read). This
// allows senders' Write calls to be non-blocking, as in real-world
// applications.
//
// Note: Latency is injected by the sender specifying the absolute time data
// should be available, and the reader delaying until that time arrives to
// provide the data. This package attempts to counter-act the effects of clock
// drift and existing network latency by measuring the delay between the
// sender's transmission time and the receiver's reception time during startup.
// No attempt is made to measure the existing bandwidth of the connection.
type Network struct {
Kbps int // Kilobits per second; if non-positive, infinite
Latency time.Duration // One-way latency (sending); if non-positive, no delay
MTU int // Bytes per packet; if non-positive, infinite
}
var (
// Local simulates local network.
Local = Network{0, 0, 0}
// LAN simulates local area network.
LAN = Network{100 * 1024, 2 * time.Millisecond, 1500}
// WAN simulates wide area network.
WAN = Network{20 * 1024, 30 * time.Millisecond, 1500}
// Longhaul simulates bad network.
Longhaul = Network{1000 * 1024, 200 * time.Millisecond, 9000}
)
func (n *Network) isLocal() bool {
return *n == Local
}
// Conn returns a net.Conn that wraps c and injects n's latency into that
// connection. This function also imposes latency for connection creation.
// If n's Latency is lower than the measured latency in c, an error is
// returned.
func (n *Network) Conn(c net.Conn) (net.Conn, error) {
if n.isLocal() {
return c, nil
}
start := now()
nc := &conn{Conn: c, network: n, readBuf: new(bytes.Buffer)}
if err := nc.sync(); err != nil {
return nil, err
}
sleep(start.Add(nc.delay).Sub(now()))
return nc, nil
}
type conn struct {
net.Conn
network *Network
readBuf *bytes.Buffer // one packet worth of data received
lastSendEnd time.Time // time the previous Write should be fully on the wire
delay time.Duration // desired latency - measured latency
}
// header is sent before all data transmitted by the application.
type header struct {
ReadTime int64 // Time the reader is allowed to read this packet (UnixNano)
Sz int32 // Size of the data in the packet
}
func (c *conn) Write(p []byte) (n int, err error) {
tNow := now()
if c.lastSendEnd.Before(tNow) {
c.lastSendEnd = tNow
}
for len(p) > 0 {
pkt := p
if c.network.MTU > 0 && len(pkt) > c.network.MTU {
pkt = pkt[:c.network.MTU]
p = p[c.network.MTU:]
} else {
p = nil
}
if c.network.Kbps > 0 {
if congestion := c.lastSendEnd.Sub(tNow) - c.delay; congestion > 0 {
// The network is full; sleep until this packet can be sent.
sleep(congestion)
tNow = tNow.Add(congestion)
}
}
c.lastSendEnd = c.lastSendEnd.Add(c.network.pktTime(len(pkt)))
hdr := header{ReadTime: c.lastSendEnd.Add(c.delay).UnixNano(), Sz: int32(len(pkt))}
if err := binary.Write(c.Conn, binary.BigEndian, hdr); err != nil {
return n, err
}
x, err := c.Conn.Write(pkt)
n += x
if err != nil {
return n, err
}
}
return n, nil
}
func (c *conn) Read(p []byte) (n int, err error) {
if c.readBuf.Len() == 0 {
var hdr header
if err := binary.Read(c.Conn, binary.BigEndian, &hdr); err != nil {
return 0, err
}
defer func() { sleep(time.Unix(0, hdr.ReadTime).Sub(now())) }()
if _, err := io.CopyN(c.readBuf, c.Conn, int64(hdr.Sz)); err != nil {
return 0, err
}
}
// Read from readBuf.
return c.readBuf.Read(p)
}
// sync does a handshake and then measures the latency on the network in
// coordination with the other side.
func (c *conn) sync() error {
const (
pingMsg = "syncPing"
warmup = 10 // minimum number of iterations to measure latency
giveUp = 50 // maximum number of iterations to measure latency
accuracy = time.Millisecond // req'd accuracy to stop early
goodRun = 3 // stop early if latency within accuracy this many times
)
type syncMsg struct {
SendT int64 // Time sent. If zero, stop.
RecvT int64 // Time received. If zero, fill in and respond.
}
// A trivial handshake
if err := binary.Write(c.Conn, binary.BigEndian, []byte(pingMsg)); err != nil {
return err
}
var ping [8]byte
if err := binary.Read(c.Conn, binary.BigEndian, &ping); err != nil {
return err
} else if string(ping[:]) != pingMsg {
return fmt.Errorf("malformed handshake message: %v (want %q)", ping, pingMsg)
}
// Both sides are alive and syncing. Calculate network delay / clock skew.
att := 0
good := 0
var latency time.Duration
localDone, remoteDone := false, false
send := true
for !localDone || !remoteDone {
if send {
if err := binary.Write(c.Conn, binary.BigEndian, syncMsg{SendT: now().UnixNano()}); err != nil {
return err
}
att++
send = false
}
// Block until we get a syncMsg
m := syncMsg{}
if err := binary.Read(c.Conn, binary.BigEndian, &m); err != nil {
return err
}
if m.RecvT == 0 {
// Message initiated from other side.
if m.SendT == 0 {
remoteDone = true
continue
}
// Send response.
m.RecvT = now().UnixNano()
if err := binary.Write(c.Conn, binary.BigEndian, m); err != nil {
return err
}
continue
}
lag := time.Duration(m.RecvT - m.SendT)
latency += lag
avgLatency := latency / time.Duration(att)
if e := lag - avgLatency; e > -accuracy && e < accuracy {
good++
} else {
good = 0
}
if att < giveUp && (att < warmup || good < goodRun) {
send = true
continue
}
localDone = true
latency = avgLatency
// Tell the other side we're done.
if err := binary.Write(c.Conn, binary.BigEndian, syncMsg{}); err != nil {
return err
}
}
if c.network.Latency <= 0 {
return nil
}
c.delay = c.network.Latency - latency
if c.delay < 0 {
return fmt.Errorf("measured network latency (%v) higher than desired latency (%v)", latency, c.network.Latency)
}
return nil
}
// Listener returns a net.Listener that wraps l and injects n's latency in its
// connections.
func (n *Network) Listener(l net.Listener) net.Listener {
if n.isLocal() {
return l
}
return &listener{Listener: l, network: n}
}
type listener struct {
net.Listener
network *Network
}
func (l *listener) Accept() (net.Conn, error) {
c, err := l.Listener.Accept()
if err != nil {
return nil, err
}
return l.network.Conn(c)
}
// Dialer returns a Dialer that wraps d and injects n's latency in its
// connections. n's Latency is also injected to the connection's creation.
func (n *Network) Dialer(d Dialer) Dialer {
if n.isLocal() {
return d
}
return func(network, address string) (net.Conn, error) {
conn, err := d(network, address)
if err != nil {
return nil, err
}
return n.Conn(conn)
}
}
// TimeoutDialer returns a TimeoutDialer that wraps d and injects n's latency
// in its connections. n's Latency is also injected to the connection's
// creation.
func (n *Network) TimeoutDialer(d TimeoutDialer) TimeoutDialer {
if n.isLocal() {
return d
}
return func(network, address string, timeout time.Duration) (net.Conn, error) {
conn, err := d(network, address, timeout)
if err != nil {
return nil, err
}
return n.Conn(conn)
}
}
// ContextDialer returns a ContextDialer that wraps d and injects n's latency
// in its connections. n's Latency is also injected to the connection's
// creation.
func (n *Network) ContextDialer(d ContextDialer) ContextDialer {
if n.isLocal() {
return d
}
return func(ctx context.Context, network, address string) (net.Conn, error) {
conn, err := d(ctx, network, address)
if err != nil {
return nil, err
}
return n.Conn(conn)
}
}
// pktTime returns the time it takes to transmit one packet of data of size b
// in bytes.
func (n *Network) pktTime(b int) time.Duration {
if n.Kbps <= 0 {
return time.Duration(0)
}
return time.Duration(b) * time.Second / time.Duration(n.Kbps*(1024/8))
}
// Wrappers for testing
var now = time.Now
var sleep = time.Sleep

View File

@@ -0,0 +1,363 @@
/*
*
* 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 latency
import (
"bytes"
"fmt"
"net"
"reflect"
"sync"
"testing"
"time"
"google.golang.org/grpc/internal/grpctest"
)
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
// bufConn is a net.Conn implemented by a bytes.Buffer (which is a ReadWriter).
type bufConn struct {
*bytes.Buffer
}
func (bufConn) Close() error { panic("unimplemented") }
func (bufConn) LocalAddr() net.Addr { panic("unimplemented") }
func (bufConn) RemoteAddr() net.Addr { panic("unimplemented") }
func (bufConn) SetDeadline(time.Time) error { panic("unimplemented") }
func (bufConn) SetReadDeadline(time.Time) error { panic("unimplemented") }
func (bufConn) SetWriteDeadline(time.Time) error { panic("unimplemented") }
func restoreHooks() func() {
s := sleep
n := now
return func() {
sleep = s
now = n
}
}
func (s) TestConn(t *testing.T) {
defer restoreHooks()()
// Constant time.
now = func() time.Time { return time.Unix(123, 456) }
// Capture sleep times for checking later.
var sleepTimes []time.Duration
sleep = func(t time.Duration) { sleepTimes = append(sleepTimes, t) }
wantSleeps := func(want ...time.Duration) {
if !reflect.DeepEqual(want, sleepTimes) {
t.Fatalf("sleepTimes = %v; want %v", sleepTimes, want)
}
sleepTimes = nil
}
// Use a fairly high latency to cause a large BDP and avoid sleeps while
// writing due to simulation of full buffers.
latency := 1 * time.Second
c, err := (&Network{Kbps: 1, Latency: latency, MTU: 5}).Conn(bufConn{&bytes.Buffer{}})
if err != nil {
t.Fatalf("Unexpected error creating connection: %v", err)
}
wantSleeps(latency) // Connection creation delay.
// 1 kbps = 128 Bps. Divides evenly by 1 second using nanos.
byteLatency := time.Second / 128
write := func(b []byte) {
n, err := c.Write(b)
if n != len(b) || err != nil {
t.Fatalf("c.Write(%v) = %v, %v; want %v, nil", b, n, err, len(b))
}
}
write([]byte{1, 2, 3, 4, 5}) // One full packet
pkt1Time := latency + byteLatency*5
write([]byte{6}) // One partial packet
pkt2Time := pkt1Time + byteLatency
write([]byte{7, 8, 9, 10, 11, 12, 13}) // Two packets
pkt3Time := pkt2Time + byteLatency*5
pkt4Time := pkt3Time + byteLatency*2
// No reads, so no sleeps yet.
wantSleeps()
read := func(n int, want []byte) {
b := make([]byte, n)
if rd, err := c.Read(b); err != nil || rd != len(want) {
t.Fatalf("c.Read(<%v bytes>) = %v, %v; want %v, nil", n, rd, err, len(want))
}
if !reflect.DeepEqual(b[:len(want)], want) {
t.Fatalf("read %v; want %v", b, want)
}
}
read(1, []byte{1})
wantSleeps(pkt1Time)
read(1, []byte{2})
wantSleeps()
read(3, []byte{3, 4, 5})
wantSleeps()
read(2, []byte{6})
wantSleeps(pkt2Time)
read(2, []byte{7, 8})
wantSleeps(pkt3Time)
read(10, []byte{9, 10, 11})
wantSleeps()
read(10, []byte{12, 13})
wantSleeps(pkt4Time)
}
func (s) TestSync(t *testing.T) {
defer restoreHooks()()
// Infinitely fast CPU: time doesn't pass unless sleep is called.
tn := time.Unix(123, 0)
now = func() time.Time { return tn }
sleep = func(d time.Duration) { tn = tn.Add(d) }
// Simulate a 20ms latency network, then run sync across that and expect to
// measure 20ms latency, or 10ms additional delay for a 30ms network.
slowConn, err := (&Network{Kbps: 0, Latency: 20 * time.Millisecond, MTU: 5}).Conn(bufConn{&bytes.Buffer{}})
if err != nil {
t.Fatalf("Unexpected error creating connection: %v", err)
}
c, err := (&Network{Latency: 30 * time.Millisecond}).Conn(slowConn)
if err != nil {
t.Fatalf("Unexpected error creating connection: %v", err)
}
if c.(*conn).delay != 10*time.Millisecond {
t.Fatalf("c.delay = %v; want 10ms", c.(*conn).delay)
}
}
func (s) TestSyncTooSlow(t *testing.T) {
defer restoreHooks()()
// Infinitely fast CPU: time doesn't pass unless sleep is called.
tn := time.Unix(123, 0)
now = func() time.Time { return tn }
sleep = func(d time.Duration) { tn = tn.Add(d) }
// Simulate a 10ms latency network, then attempt to simulate a 5ms latency
// network and expect an error.
slowConn, err := (&Network{Kbps: 0, Latency: 10 * time.Millisecond, MTU: 5}).Conn(bufConn{&bytes.Buffer{}})
if err != nil {
t.Fatalf("Unexpected error creating connection: %v", err)
}
errWant := "measured network latency (10ms) higher than desired latency (5ms)"
if _, err := (&Network{Latency: 5 * time.Millisecond}).Conn(slowConn); err == nil || err.Error() != errWant {
t.Fatalf("Conn() = _, %q; want _, %q", err, errWant)
}
}
func (s) TestListenerAndDialer(t *testing.T) {
defer restoreHooks()()
tn := time.Unix(123, 0)
startTime := tn
mu := &sync.Mutex{}
now = func() time.Time {
mu.Lock()
defer mu.Unlock()
return tn
}
// Use a fairly high latency to cause a large BDP and avoid sleeps while
// writing due to simulation of full buffers.
n := &Network{Kbps: 2, Latency: 1 * time.Second, MTU: 10}
// 2 kbps = .25 kBps = 256 Bps
byteLatency := func(n int) time.Duration {
return time.Duration(n) * time.Second / 256
}
// Create a real listener and wrap it.
l, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("Unexpected error creating listener: %v", err)
}
defer l.Close()
l = n.Listener(l)
var serverConn net.Conn
var scErr error
scDone := make(chan struct{})
go func() {
serverConn, scErr = l.Accept()
close(scDone)
}()
// Create a dialer and use it.
clientConn, err := n.TimeoutDialer(net.DialTimeout)("tcp", l.Addr().String(), 2*time.Second)
if err != nil {
t.Fatalf("Unexpected error dialing: %v", err)
}
defer clientConn.Close()
// Block until server's Conn is available.
<-scDone
if scErr != nil {
t.Fatalf("Unexpected error listening: %v", scErr)
}
defer serverConn.Close()
// sleep (only) advances tn. Done after connections established so sync detects zero delay.
sleep = func(d time.Duration) {
mu.Lock()
defer mu.Unlock()
if d > 0 {
tn = tn.Add(d)
}
}
seq := func(a, b int) []byte {
buf := make([]byte, b-a)
for i := 0; i < b-a; i++ {
buf[i] = byte(i + a)
}
return buf
}
pkt1 := seq(0, 10)
pkt2 := seq(10, 30)
pkt3 := seq(30, 35)
write := func(c net.Conn, b []byte) {
n, err := c.Write(b)
if n != len(b) || err != nil {
t.Fatalf("c.Write(%v) = %v, %v; want %v, nil", b, n, err, len(b))
}
}
write(serverConn, pkt1)
write(serverConn, pkt2)
write(serverConn, pkt3)
write(clientConn, pkt3)
write(clientConn, pkt1)
write(clientConn, pkt2)
if tn != startTime {
t.Fatalf("unexpected sleep in write; tn = %v; want %v", tn, startTime)
}
read := func(c net.Conn, n int, want []byte, timeWant time.Time) {
b := make([]byte, n)
if rd, err := c.Read(b); err != nil || rd != len(want) {
t.Fatalf("c.Read(<%v bytes>) = %v, %v; want %v, nil (read: %v)", n, rd, err, len(want), b[:rd])
}
if !reflect.DeepEqual(b[:len(want)], want) {
t.Fatalf("read %v; want %v", b, want)
}
if !tn.Equal(timeWant) {
t.Errorf("tn after read(%v) = %v; want %v", want, tn, timeWant)
}
}
read(clientConn, len(pkt1)+1, pkt1, startTime.Add(n.Latency+byteLatency(len(pkt1))))
read(serverConn, len(pkt3)+1, pkt3, tn) // tn was advanced by the above read; pkt3 is shorter than pkt1
read(clientConn, len(pkt2), pkt2[:10], startTime.Add(n.Latency+byteLatency(len(pkt1)+10)))
read(clientConn, len(pkt2), pkt2[10:], startTime.Add(n.Latency+byteLatency(len(pkt1)+len(pkt2))))
read(clientConn, len(pkt3), pkt3, startTime.Add(n.Latency+byteLatency(len(pkt1)+len(pkt2)+len(pkt3))))
read(serverConn, len(pkt1), pkt1, tn) // tn already past the arrival time due to prior reads
read(serverConn, len(pkt2), pkt2[:10], tn)
read(serverConn, len(pkt2), pkt2[10:], tn)
// Sleep awhile and make sure the read happens disregarding previous writes
// (lastSendEnd handling).
sleep(10 * time.Second)
write(clientConn, pkt1)
read(serverConn, len(pkt1), pkt1, tn.Add(n.Latency+byteLatency(len(pkt1))))
// Send, sleep longer than the network delay, then make sure the read happens
// instantly.
write(serverConn, pkt1)
sleep(10 * time.Second)
read(clientConn, len(pkt1), pkt1, tn)
}
func (s) TestBufferBloat(t *testing.T) {
defer restoreHooks()()
// Infinitely fast CPU: time doesn't pass unless sleep is called.
tn := time.Unix(123, 0)
now = func() time.Time { return tn }
// Capture sleep times for checking later.
var sleepTimes []time.Duration
sleep = func(d time.Duration) {
sleepTimes = append(sleepTimes, d)
tn = tn.Add(d)
}
wantSleeps := func(want ...time.Duration) error {
if !reflect.DeepEqual(want, sleepTimes) {
return fmt.Errorf("sleepTimes = %v; want %v", sleepTimes, want)
}
sleepTimes = nil
return nil
}
n := &Network{Kbps: 8 /* 1KBps */, Latency: time.Second, MTU: 8}
bdpBytes := (n.Kbps * 1024 / 8) * int(n.Latency/time.Second) // 1024
c, err := n.Conn(bufConn{&bytes.Buffer{}})
if err != nil {
t.Fatalf("Unexpected error creating connection: %v", err)
}
wantSleeps(n.Latency) // Connection creation delay.
write := func(n int, sleeps ...time.Duration) {
if wt, err := c.Write(make([]byte, n)); err != nil || wt != n {
t.Fatalf("c.Write(<%v bytes>) = %v, %v; want %v, nil", n, wt, err, n)
}
if err := wantSleeps(sleeps...); err != nil {
t.Fatalf("After writing %v bytes: %v", n, err)
}
}
read := func(n int, sleeps ...time.Duration) {
if rd, err := c.Read(make([]byte, n)); err != nil || rd != n {
t.Fatalf("c.Read(_) = %v, %v; want %v, nil", rd, err, n)
}
if err := wantSleeps(sleeps...); err != nil {
t.Fatalf("After reading %v bytes: %v", n, err)
}
}
write(8) // No reads and buffer not full, so no sleeps yet.
read(8, time.Second+n.pktTime(8))
write(bdpBytes) // Fill the buffer.
write(1) // We can send one extra packet even when the buffer is full.
write(n.MTU, n.pktTime(1)) // Make sure we sleep to clear the previous write.
write(1, n.pktTime(n.MTU))
write(n.MTU+1, n.pktTime(1), n.pktTime(n.MTU))
tn = tn.Add(10 * time.Second) // Wait long enough for the buffer to clear.
write(bdpBytes) // No sleeps required.
}

View File

@@ -0,0 +1,125 @@
/*
*
* 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 primitives_test
import (
"strconv"
"testing"
"google.golang.org/grpc/codes"
)
type codeBench uint32
const (
OK codeBench = iota
Canceled
Unknown
InvalidArgument
DeadlineExceeded
NotFound
AlreadyExists
PermissionDenied
ResourceExhausted
FailedPrecondition
Aborted
OutOfRange
Unimplemented
Internal
Unavailable
DataLoss
Unauthenticated
)
// The following String() function was generated by stringer.
const codeName = "OKCanceledUnknownInvalidArgumentDeadlineExceededNotFoundAlreadyExistsPermissionDeniedResourceExhaustedFailedPreconditionAbortedOutOfRangeUnimplementedInternalUnavailableDataLossUnauthenticated"
var codeIndex = [...]uint8{0, 2, 10, 17, 32, 48, 56, 69, 85, 102, 120, 127, 137, 150, 158, 169, 177, 192}
func (i codeBench) String() string {
if i >= codeBench(len(codeIndex)-1) {
return "Code(" + strconv.FormatInt(int64(i), 10) + ")"
}
return codeName[codeIndex[i]:codeIndex[i+1]]
}
var nameMap = map[codeBench]string{
OK: "OK",
Canceled: "Canceled",
Unknown: "Unknown",
InvalidArgument: "InvalidArgument",
DeadlineExceeded: "DeadlineExceeded",
NotFound: "NotFound",
AlreadyExists: "AlreadyExists",
PermissionDenied: "PermissionDenied",
ResourceExhausted: "ResourceExhausted",
FailedPrecondition: "FailedPrecondition",
Aborted: "Aborted",
OutOfRange: "OutOfRange",
Unimplemented: "Unimplemented",
Internal: "Internal",
Unavailable: "Unavailable",
DataLoss: "DataLoss",
Unauthenticated: "Unauthenticated",
}
func (i codeBench) StringUsingMap() string {
if s, ok := nameMap[i]; ok {
return s
}
return "Code(" + strconv.FormatInt(int64(i), 10) + ")"
}
func BenchmarkCodeStringStringer(b *testing.B) {
for i := 0; i < b.N; i++ {
c := codeBench(uint32(i % 17))
_ = c.String()
}
}
func BenchmarkCodeStringMap(b *testing.B) {
for i := 0; i < b.N; i++ {
c := codeBench(uint32(i % 17))
_ = c.StringUsingMap()
}
}
// codes.Code.String() does a switch.
func BenchmarkCodeStringSwitch(b *testing.B) {
for i := 0; i < b.N; i++ {
c := codes.Code(uint32(i % 17))
_ = c.String()
}
}
// Testing all codes (0<=c<=16) and also one overflow (17).
func BenchmarkCodeStringStringerWithOverflow(b *testing.B) {
for i := 0; i < b.N; i++ {
c := codeBench(uint32(i % 18))
_ = c.String()
}
}
// Testing all codes (0<=c<=16) and also one overflow (17).
func BenchmarkCodeStringSwitchWithOverflow(b *testing.B) {
for i := 0; i < b.N; i++ {
c := codes.Code(uint32(i % 18))
_ = c.String()
}
}

View File

@@ -0,0 +1,152 @@
/*
*
* 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 primitives_test
import (
"context"
"testing"
"time"
)
const defaultTestTimeout = 10 * time.Second
func BenchmarkCancelContextErrNoErr(b *testing.B) {
ctx, cancel := context.WithCancel(context.Background())
for i := 0; i < b.N; i++ {
if err := ctx.Err(); err != nil {
b.Fatal("error")
}
}
cancel()
}
func BenchmarkCancelContextErrGotErr(b *testing.B) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
for i := 0; i < b.N; i++ {
if err := ctx.Err(); err == nil {
b.Fatal("error")
}
}
}
func BenchmarkCancelContextChannelNoErr(b *testing.B) {
ctx, cancel := context.WithCancel(context.Background())
for i := 0; i < b.N; i++ {
select {
case <-ctx.Done():
b.Fatal("error: ctx.Done():", ctx.Err())
default:
}
}
cancel()
}
func BenchmarkCancelContextChannelGotErr(b *testing.B) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
for i := 0; i < b.N; i++ {
select {
case <-ctx.Done():
if err := ctx.Err(); err == nil {
b.Fatal("error")
}
default:
b.Fatal("error: !ctx.Done()")
}
}
}
func BenchmarkTimerContextErrNoErr(b *testing.B) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
for i := 0; i < b.N; i++ {
if err := ctx.Err(); err != nil {
b.Fatal("error")
}
}
cancel()
}
func BenchmarkTimerContextErrGotErr(b *testing.B) {
ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond)
cancel()
for i := 0; i < b.N; i++ {
if err := ctx.Err(); err == nil {
b.Fatal("error")
}
}
}
func BenchmarkTimerContextChannelNoErr(b *testing.B) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
for i := 0; i < b.N; i++ {
select {
case <-ctx.Done():
b.Fatal("error: ctx.Done():", ctx.Err())
default:
}
}
cancel()
}
func BenchmarkTimerContextChannelGotErr(b *testing.B) {
ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond)
cancel()
for i := 0; i < b.N; i++ {
select {
case <-ctx.Done():
if err := ctx.Err(); err == nil {
b.Fatal("error")
}
default:
b.Fatal("error: !ctx.Done()")
}
}
}
type ctxKey struct{}
func newContextWithLocalKey(parent context.Context) context.Context {
return context.WithValue(parent, ctxKey{}, nil)
}
var ck = ctxKey{}
func newContextWithGlobalKey(parent context.Context) context.Context {
return context.WithValue(parent, ck, nil)
}
func BenchmarkContextWithValue(b *testing.B) {
benches := []struct {
name string
f func(context.Context) context.Context
}{
{"newContextWithLocalKey", newContextWithLocalKey},
{"newContextWithGlobalKey", newContextWithGlobalKey},
}
pCtx := context.Background()
for _, bench := range benches {
b.Run(bench.name, func(b *testing.B) {
for j := 0; j < b.N; j++ {
bench.f(pCtx)
}
})
}
}

View File

@@ -0,0 +1,466 @@
/*
*
* 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 primitives_test contains benchmarks for various synchronization primitives
// available in Go.
package primitives_test
import (
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
"unsafe"
)
func BenchmarkSelectClosed(b *testing.B) {
c := make(chan struct{})
close(c)
x := 0
b.ResetTimer()
for i := 0; i < b.N; i++ {
select {
case <-c:
x++
default:
}
}
b.StopTimer()
if x != b.N {
b.Fatal("error")
}
}
func BenchmarkSelectOpen(b *testing.B) {
c := make(chan struct{})
x := 0
b.ResetTimer()
for i := 0; i < b.N; i++ {
select {
case <-c:
default:
x++
}
}
b.StopTimer()
if x != b.N {
b.Fatal("error")
}
}
func BenchmarkAtomicBool(b *testing.B) {
c := int32(0)
x := 0
b.ResetTimer()
for i := 0; i < b.N; i++ {
if atomic.LoadInt32(&c) == 0 {
x++
}
}
b.StopTimer()
if x != b.N {
b.Fatal("error")
}
}
func BenchmarkAtomicValueLoad(b *testing.B) {
c := atomic.Value{}
c.Store(0)
x := 0
b.ResetTimer()
for i := 0; i < b.N; i++ {
if c.Load().(int) == 0 {
x++
}
}
b.StopTimer()
if x != b.N {
b.Fatal("error")
}
}
func BenchmarkAtomicValueStore(b *testing.B) {
c := atomic.Value{}
v := 123
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Store(v)
}
b.StopTimer()
}
func BenchmarkMutex(b *testing.B) {
c := sync.Mutex{}
x := 0
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Lock()
x++
c.Unlock()
}
b.StopTimer()
if x != b.N {
b.Fatal("error")
}
}
func BenchmarkRWMutex(b *testing.B) {
c := sync.RWMutex{}
x := 0
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.RLock()
x++
c.RUnlock()
}
b.StopTimer()
if x != b.N {
b.Fatal("error")
}
}
func BenchmarkRWMutexW(b *testing.B) {
c := sync.RWMutex{}
x := 0
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Lock()
x++
c.Unlock()
}
b.StopTimer()
if x != b.N {
b.Fatal("error")
}
}
func BenchmarkMutexWithDefer(b *testing.B) {
c := sync.Mutex{}
x := 0
b.ResetTimer()
for i := 0; i < b.N; i++ {
func() {
c.Lock()
defer c.Unlock()
x++
}()
}
b.StopTimer()
if x != b.N {
b.Fatal("error")
}
}
func BenchmarkMutexWithClosureDefer(b *testing.B) {
c := sync.Mutex{}
x := 0
b.ResetTimer()
for i := 0; i < b.N; i++ {
func() {
c.Lock()
defer func() { c.Unlock() }()
x++
}()
}
b.StopTimer()
if x != b.N {
b.Fatal("error")
}
}
func BenchmarkMutexWithoutDefer(b *testing.B) {
c := sync.Mutex{}
x := 0
b.ResetTimer()
for i := 0; i < b.N; i++ {
func() {
c.Lock()
x++
c.Unlock()
}()
}
b.StopTimer()
if x != b.N {
b.Fatal("error")
}
}
func BenchmarkAtomicAddInt64(b *testing.B) {
var c int64
b.ResetTimer()
for i := 0; i < b.N; i++ {
atomic.AddInt64(&c, 1)
}
b.StopTimer()
if c != int64(b.N) {
b.Fatal("error")
}
}
func BenchmarkAtomicTimeValueStore(b *testing.B) {
var c atomic.Value
t := time.Now()
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Store(t)
}
b.StopTimer()
}
func BenchmarkAtomic16BValueStore(b *testing.B) {
var c atomic.Value
t := struct {
a int64
b int64
}{
123, 123,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Store(t)
}
b.StopTimer()
}
func BenchmarkAtomic32BValueStore(b *testing.B) {
var c atomic.Value
t := struct {
a int64
b int64
c int64
d int64
}{
123, 123, 123, 123,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Store(t)
}
b.StopTimer()
}
func BenchmarkAtomicPointerStore(b *testing.B) {
t := 123
var up unsafe.Pointer
b.ResetTimer()
for i := 0; i < b.N; i++ {
atomic.StorePointer(&up, unsafe.Pointer(&t))
}
b.StopTimer()
}
func BenchmarkAtomicTimePointerStore(b *testing.B) {
t := time.Now()
var up unsafe.Pointer
b.ResetTimer()
for i := 0; i < b.N; i++ {
atomic.StorePointer(&up, unsafe.Pointer(&t))
}
b.StopTimer()
}
func BenchmarkStoreContentionWithAtomic(b *testing.B) {
t := 123
var c unsafe.Pointer
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
atomic.StorePointer(&c, unsafe.Pointer(&t))
}
})
}
func BenchmarkStoreContentionWithMutex(b *testing.B) {
t := 123
var mu sync.Mutex
var c int
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mu.Lock()
c = t
mu.Unlock()
}
})
_ = c
}
type dummyStruct struct {
a int64
b time.Time
}
func BenchmarkStructStoreContention(b *testing.B) {
d := dummyStruct{}
dp := unsafe.Pointer(&d)
t := time.Now()
for _, j := range []int{100000000, 10000, 0} {
for _, i := range []int{100000, 10} {
b.Run(fmt.Sprintf("CAS/%v/%v", j, i), func(b *testing.B) {
b.SetParallelism(i)
b.RunParallel(func(pb *testing.PB) {
n := &dummyStruct{
b: t,
}
for pb.Next() {
for y := 0; y < j; y++ {
}
for {
v := (*dummyStruct)(atomic.LoadPointer(&dp))
n.a = v.a + 1
if atomic.CompareAndSwapPointer(&dp, unsafe.Pointer(v), unsafe.Pointer(n)) {
n = v
break
}
}
}
})
})
}
}
var mu sync.Mutex
for _, j := range []int{100000000, 10000, 0} {
for _, i := range []int{100000, 10} {
b.Run(fmt.Sprintf("Mutex/%v/%v", j, i), func(b *testing.B) {
b.SetParallelism(i)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
for y := 0; y < j; y++ {
}
mu.Lock()
d.a++
d.b = t
mu.Unlock()
}
})
})
}
}
}
type myFooer struct{}
func (myFooer) Foo() {}
type fooer interface {
Foo()
}
func BenchmarkInterfaceTypeAssertion(b *testing.B) {
// Call a separate function to avoid compiler optimizations.
runInterfaceTypeAssertion(b, myFooer{})
}
func runInterfaceTypeAssertion(b *testing.B, fer any) {
x := 0
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, ok := fer.(fooer); ok {
x++
}
}
b.StopTimer()
if x != b.N {
b.Fatal("error")
}
}
func BenchmarkStructTypeAssertion(b *testing.B) {
// Call a separate function to avoid compiler optimizations.
runStructTypeAssertion(b, myFooer{})
}
func runStructTypeAssertion(b *testing.B, fer any) {
x := 0
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, ok := fer.(myFooer); ok {
x++
}
}
b.StopTimer()
if x != b.N {
b.Fatal("error")
}
}
func BenchmarkWaitGroupAddDone(b *testing.B) {
wg := sync.WaitGroup{}
b.RunParallel(func(pb *testing.PB) {
i := 0
for ; pb.Next(); i++ {
wg.Add(1)
}
for ; i > 0; i-- {
wg.Done()
}
})
}
func BenchmarkRLockUnlock(b *testing.B) {
mu := sync.RWMutex{}
b.RunParallel(func(pb *testing.PB) {
i := 0
for ; pb.Next(); i++ {
mu.RLock()
}
for ; i > 0; i-- {
mu.RUnlock()
}
})
}
type ifNop interface {
nop()
}
type alwaysNop struct{}
func (alwaysNop) nop() {}
type concreteNop struct {
isNop atomic.Bool
i int
}
func (c *concreteNop) nop() {
if c.isNop.Load() {
return
}
c.i++
}
func BenchmarkInterfaceNop(b *testing.B) {
n := ifNop(alwaysNop{})
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
n.nop()
}
})
}
func BenchmarkConcreteNop(b *testing.B) {
n := &concreteNop{}
n.isNop.Store(true)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
n.nop()
}
})
}

View File

@@ -0,0 +1,114 @@
/*
*
* 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.
*
*/
// Benchmark options for safe config selector type.
package primitives_test
import (
"sync"
"sync/atomic"
"testing"
"time"
"unsafe"
)
type safeUpdaterAtomicAndCounter struct {
ptr unsafe.Pointer // *countingFunc
}
type countingFunc struct {
mu sync.RWMutex
f func()
}
func (s *safeUpdaterAtomicAndCounter) call() {
cfPtr := atomic.LoadPointer(&s.ptr)
var cf *countingFunc
for {
cf = (*countingFunc)(cfPtr)
cf.mu.RLock()
cfPtr2 := atomic.LoadPointer(&s.ptr)
if cfPtr == cfPtr2 {
// Use cf with confidence!
break
}
// cf changed; try to use the new one instead, because the old one is
// no longer valid to use.
cf.mu.RUnlock()
cfPtr = cfPtr2
}
defer cf.mu.RUnlock()
cf.f()
}
func (s *safeUpdaterAtomicAndCounter) update(f func()) {
newCF := &countingFunc{f: f}
oldCFPtr := atomic.SwapPointer(&s.ptr, unsafe.Pointer(newCF))
if oldCFPtr == nil {
return
}
(*countingFunc)(oldCFPtr).mu.Lock()
(*countingFunc)(oldCFPtr).mu.Unlock() //lint:ignore SA2001 necessary to unlock after locking to unblock any RLocks
}
type safeUpdaterRWMutex struct {
mu sync.RWMutex
f func()
}
func (s *safeUpdaterRWMutex) call() {
s.mu.RLock()
defer s.mu.RUnlock()
s.f()
}
func (s *safeUpdaterRWMutex) update(f func()) {
s.mu.Lock()
defer s.mu.Unlock()
s.f = f
}
type updater interface {
call()
update(f func())
}
func benchmarkSafeUpdater(b *testing.B, u updater) {
t := time.NewTicker(time.Second)
go func() {
for range t.C {
u.update(func() {})
}
}()
b.RunParallel(func(pb *testing.PB) {
u.update(func() {})
for pb.Next() {
u.call()
}
})
t.Stop()
}
func BenchmarkSafeUpdaterAtomicAndCounter(b *testing.B) {
benchmarkSafeUpdater(b, &safeUpdaterAtomicAndCounter{})
}
func BenchmarkSafeUpdaterRWMutex(b *testing.B) {
benchmarkSafeUpdater(b, &safeUpdaterRWMutex{})
}

View File

@@ -0,0 +1,165 @@
/*
*
* Copyright 2019 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 primitives_test
import (
"sync"
"sync/atomic"
"testing"
)
type incrementUint64Map interface {
increment(string)
result(string) uint64
}
type mapWithLock struct {
mu sync.Mutex
m map[string]uint64
}
func newMapWithLock() incrementUint64Map {
return &mapWithLock{
m: make(map[string]uint64),
}
}
func (mwl *mapWithLock) increment(c string) {
mwl.mu.Lock()
mwl.m[c]++
mwl.mu.Unlock()
}
func (mwl *mapWithLock) result(c string) uint64 {
return mwl.m[c]
}
type mapWithAtomicFastpath struct {
mu sync.RWMutex
m map[string]*uint64
}
func newMapWithAtomicFastpath() incrementUint64Map {
return &mapWithAtomicFastpath{
m: make(map[string]*uint64),
}
}
func (mwaf *mapWithAtomicFastpath) increment(c string) {
mwaf.mu.RLock()
if p, ok := mwaf.m[c]; ok {
atomic.AddUint64(p, 1)
mwaf.mu.RUnlock()
return
}
mwaf.mu.RUnlock()
mwaf.mu.Lock()
if p, ok := mwaf.m[c]; ok {
atomic.AddUint64(p, 1)
mwaf.mu.Unlock()
return
}
var temp uint64 = 1
mwaf.m[c] = &temp
mwaf.mu.Unlock()
}
func (mwaf *mapWithAtomicFastpath) result(c string) uint64 {
return atomic.LoadUint64(mwaf.m[c])
}
type mapWithSyncMap struct {
m sync.Map
}
func newMapWithSyncMap() incrementUint64Map {
return &mapWithSyncMap{}
}
func (mwsm *mapWithSyncMap) increment(c string) {
p, ok := mwsm.m.Load(c)
if !ok {
tp := new(uint64)
p, _ = mwsm.m.LoadOrStore(c, tp)
}
atomic.AddUint64(p.(*uint64), 1)
}
func (mwsm *mapWithSyncMap) result(c string) uint64 {
p, _ := mwsm.m.Load(c)
return atomic.LoadUint64(p.(*uint64))
}
func benchmarkIncrementUint64Map(b *testing.B, f func() incrementUint64Map) {
const cat = "cat"
benches := []struct {
name string
goroutineCount int
}{
{
name: " 1",
goroutineCount: 1,
},
{
name: " 10",
goroutineCount: 10,
},
{
name: " 100",
goroutineCount: 100,
},
{
name: "1000",
goroutineCount: 1000,
},
}
for _, bb := range benches {
b.Run(bb.name, func(b *testing.B) {
m := f()
var wg sync.WaitGroup
wg.Add(bb.goroutineCount)
b.ResetTimer()
for i := 0; i < bb.goroutineCount; i++ {
go func() {
for j := 0; j < b.N; j++ {
m.increment(cat)
}
wg.Done()
}()
}
wg.Wait()
b.StopTimer()
if m.result(cat) != uint64(bb.goroutineCount*b.N) {
b.Fatalf("result is %d, want %d", m.result(cat), b.N)
}
})
}
}
func BenchmarkMapWithSyncMutexContention(b *testing.B) {
benchmarkIncrementUint64Map(b, newMapWithLock)
}
func BenchmarkMapWithAtomicFastpath(b *testing.B) {
benchmarkIncrementUint64Map(b, newMapWithAtomicFastpath)
}
func BenchmarkMapWithSyncMap(b *testing.B) {
benchmarkIncrementUint64Map(b, newMapWithSyncMap)
}

188
benchmark/run_bench.sh Executable file
View File

@@ -0,0 +1,188 @@
#!/bin/bash
rpcs=(1)
conns=(1)
warmup=10
dur=10
reqs=(1)
resps=(1)
rpc_types=(unary)
# idx[0] = idx value for rpcs
# idx[1] = idx value for conns
# idx[2] = idx value for reqs
# idx[3] = idx value for resps
# idx[4] = idx value for rpc_types
idx=(0 0 0 0 0)
idx_max=(1 1 1 1 1)
inc()
{
for i in $(seq $((${#idx[@]}-1)) -1 0); do
idx[${i}]=$((${idx[${i}]}+1))
if [ ${idx[${i}]} == ${idx_max[${i}]} ]; then
idx[${i}]=0
else
break
fi
done
local fin
fin=1
# Check to see if we have looped back to the beginning.
for v in ${idx[@]}; do
if [ ${v} != 0 ]; then
fin=0
break
fi
done
if [ ${fin} == 1 ]; then
rm -Rf ${out_dir}
clean_and_die 0
fi
}
clean_and_die() {
rm -Rf ${out_dir}
exit $1
}
run(){
local nr
nr=${rpcs[${idx[0]}]}
local nc
nc=${conns[${idx[1]}]}
req_sz=${reqs[${idx[2]}]}
resp_sz=${resps[${idx[3]}]}
r_type=${rpc_types[${idx[4]}]}
# Following runs one benchmark
base_port=50051
delta=0
test_name="r_"${nr}"_c_"${nc}"_req_"${req_sz}"_resp_"${resp_sz}"_"${r_type}"_"$(date +%s)
echo "================================================================================"
echo ${test_name}
while :
do
port=$((${base_port}+${delta}))
# Launch the server in background
${out_dir}/server --port=${port} --test_name="Server_"${test_name}&
server_pid=$(echo $!)
# Launch the client
${out_dir}/client --port=${port} --d=${dur} --w=${warmup} --r=${nr} --c=${nc} --req=${req_sz} --resp=${resp_sz} --rpc_type=${r_type} --test_name="client_"${test_name}
client_status=$(echo $?)
kill -INT ${server_pid}
wait ${server_pid}
if [ ${client_status} == 0 ]; then
break
fi
delta=$((${delta}+1))
if [ ${delta} == 10 ]; then
echo "Continuous 10 failed runs. Exiting now."
rm -Rf ${out_dir}
clean_and_die 1
fi
done
}
set_param(){
local argname=$1
shift
local idx=$1
shift
if [ $# -eq 0 ]; then
echo "${argname} not specified"
exit 1
fi
PARAM=($(echo $1 | sed 's/,/ /g'))
if [ ${idx} -lt 0 ]; then
return
fi
idx_max[${idx}]=${#PARAM[@]}
}
while [ $# -gt 0 ]; do
case "$1" in
-r)
shift
set_param "number of rpcs" 0 $1
rpcs=(${PARAM[@]})
shift
;;
-c)
shift
set_param "number of connections" 1 $1
conns=(${PARAM[@]})
shift
;;
-w)
shift
set_param "warm-up period" -1 $1
warmup=${PARAM}
shift
;;
-d)
shift
set_param "duration" -1 $1
dur=${PARAM}
shift
;;
-req)
shift
set_param "request size" 2 $1
reqs=(${PARAM[@]})
shift
;;
-resp)
shift
set_param "response size" 3 $1
resps=(${PARAM[@]})
shift
;;
-rpc_type)
shift
set_param "rpc type" 4 $1
rpc_types=(${PARAM[@]})
shift
;;
-h|--help)
echo "Following are valid options:"
echo
echo "-h, --help show brief help"
echo "-w warm-up duration in seconds, default value is 10"
echo "-d benchmark duration in seconds, default value is 60"
echo ""
echo "Each of the following can have multiple comma separated values."
echo ""
echo "-r number of RPCs, default value is 1"
echo "-c number of Connections, default value is 1"
echo "-req req size in bytes, default value is 1"
echo "-resp resp size in bytes, default value is 1"
echo "-rpc_type valid values are unary|streaming, default is unary"
exit 0
;;
*)
echo "Incorrect option $1"
exit 1
;;
esac
done
# Build server and client
out_dir=$(mktemp -d oss_benchXXX)
go build -o ${out_dir}/server $GOPATH/src/google.golang.org/grpc/benchmark/server/main.go && go build -o ${out_dir}/client $GOPATH/src/google.golang.org/grpc/benchmark/client/main.go
if [ $? != 0 ]; then
clean_and_die 1
fi
while :
do
run
inc
done

97
benchmark/server/main.go Normal file
View File

@@ -0,0 +1,97 @@
/*
*
* 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 main provides a server used for benchmarking. It launches a server
which is listening on port 50051. An example to start the server can be found
at:
go run benchmark/server/main.go -test_name=grpc_test
After starting the server, the client can be run separately and used to test
qps and latency.
*/
package main
import (
"flag"
"fmt"
"net"
_ "net/http/pprof"
"os"
"os/signal"
"runtime"
"runtime/pprof"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/benchmark"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/internal/syscall"
)
var (
port = flag.String("port", "50051", "Localhost port to listen on.")
testName = flag.String("test_name", "", "Name of the test used for creating profiles.")
logger = grpclog.Component("benchmark")
)
func main() {
flag.Parse()
if *testName == "" {
logger.Fatal("-test_name not set")
}
lis, err := net.Listen("tcp", ":"+*port)
if err != nil {
logger.Fatalf("Failed to listen: %v", err)
}
defer lis.Close()
cf, err := os.Create("/tmp/" + *testName + ".cpu")
if err != nil {
logger.Fatalf("Failed to create file: %v", err)
}
defer cf.Close()
pprof.StartCPUProfile(cf)
cpuBeg := syscall.GetCPUTime()
// Launch server in a separate goroutine.
stop := benchmark.StartServer(benchmark.ServerInfo{Type: "protobuf", Listener: lis},
grpc.WriteBufferSize(128*1024),
grpc.ReadBufferSize(128*1024),
)
// Wait on OS terminate signal.
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
<-ch
cpu := time.Duration(syscall.GetCPUTime() - cpuBeg)
stop()
pprof.StopCPUProfile()
mf, err := os.Create("/tmp/" + *testName + ".mem")
if err != nil {
logger.Fatalf("Failed to create file: %v", err)
}
defer mf.Close()
runtime.GC() // materialize all statistics
if err := pprof.WriteHeapProfile(mf); err != nil {
logger.Fatalf("Failed to write memory profile: %v", err)
}
fmt.Println("Server CPU utilization:", cpu)
fmt.Println("Server CPU profile:", cf.Name())
fmt.Println("Server Mem Profile:", mf.Name())
}

179
benchmark/stats/curve.go Normal file
View File

@@ -0,0 +1,179 @@
/*
*
* Copyright 2019 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 (
"crypto/sha256"
"encoding/csv"
"encoding/hex"
"fmt"
"math"
rand "math/rand/v2"
"os"
"sort"
"strconv"
)
// payloadCurveRange represents a line within a payload curve CSV file.
type payloadCurveRange struct {
from, to int32
weight float64
}
// newPayloadCurveRange receives a line from a payload curve CSV file and
// returns a *payloadCurveRange if the values are acceptable.
func newPayloadCurveRange(line []string) (*payloadCurveRange, error) {
if len(line) != 3 {
return nil, fmt.Errorf("invalid number of entries in line %v (expected 3)", line)
}
var from, to int64
var weight float64
var err error
if from, err = strconv.ParseInt(line[0], 10, 32); err != nil {
return nil, err
}
if from <= 0 {
return nil, fmt.Errorf("line %v: field (%d) must be in (0, %d]", line, from, math.MaxInt32)
}
if to, err = strconv.ParseInt(line[1], 10, 32); err != nil {
return nil, err
}
if to <= 0 {
return nil, fmt.Errorf("line %v: field %d must be in (0, %d]", line, to, math.MaxInt32)
}
if from > to {
return nil, fmt.Errorf("line %v: from (%d) > to (%d)", line, from, to)
}
if weight, err = strconv.ParseFloat(line[2], 64); err != nil {
return nil, err
}
return &payloadCurveRange{from: int32(from), to: int32(to), weight: weight}, nil
}
// chooseRandom picks a payload size (in bytes) for a particular range. This is
// done with a uniform distribution.
func (pcr *payloadCurveRange) chooseRandom() int {
if pcr.from == pcr.to { // fast path
return int(pcr.from)
}
return int(rand.Int32N(pcr.to-pcr.from+1) + pcr.from)
}
// sha256file is a helper function that returns a hex string matching the
// SHA-256 sum of the input file.
func sha256file(file string) (string, error) {
data, err := os.ReadFile(file)
if err != nil {
return "", err
}
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:]), nil
}
// PayloadCurve is an internal representation of a weighted random distribution
// CSV file. Once a *PayloadCurve is created with NewPayloadCurve, the
// ChooseRandom function should be called to generate random payload sizes.
type PayloadCurve struct {
pcrs []*payloadCurveRange
// Sha256 must be a public field so that the gob encoder can write it to
// disk. This will be needed at decode-time by the Hash function.
Sha256 string
}
// NewPayloadCurve parses a .csv file and returns a *PayloadCurve if no errors
// were encountered in parsing and initialization.
func NewPayloadCurve(file string) (*PayloadCurve, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
defer f.Close()
r := csv.NewReader(f)
lines, err := r.ReadAll()
if err != nil {
return nil, err
}
ret := &PayloadCurve{}
var total float64
for _, line := range lines {
pcr, err := newPayloadCurveRange(line)
if err != nil {
return nil, err
}
ret.pcrs = append(ret.pcrs, pcr)
total += pcr.weight
}
ret.Sha256, err = sha256file(file)
if err != nil {
return nil, err
}
for _, pcr := range ret.pcrs {
pcr.weight /= total
}
sort.Slice(ret.pcrs, func(i, j int) bool {
if ret.pcrs[i].from == ret.pcrs[j].from {
return ret.pcrs[i].to < ret.pcrs[j].to
}
return ret.pcrs[i].from < ret.pcrs[j].from
})
var lastTo int32
for _, pcr := range ret.pcrs {
if lastTo >= pcr.from {
return nil, fmt.Errorf("[%d, %d] overlaps with a different line", pcr.from, pcr.to)
}
lastTo = pcr.to
}
return ret, nil
}
// ChooseRandom picks a random payload size (in bytes) that follows the
// underlying weighted random distribution.
func (pc *PayloadCurve) ChooseRandom() int {
target := rand.Float64()
var seen float64
for _, pcr := range pc.pcrs {
seen += pcr.weight
if seen >= target {
return pcr.chooseRandom()
}
}
// This should never happen, but if it does, return a sane default.
return 1
}
// Hash returns a string uniquely identifying a payload curve file for feature
// matching purposes.
func (pc *PayloadCurve) Hash() string {
return pc.Sha256
}
// ShortHash returns a shortened version of Hash for display purposes.
func (pc *PayloadCurve) ShortHash() string {
return pc.Sha256[:8]
}

View File

@@ -0,0 +1,221 @@
/*
*
* 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 (
"bytes"
"fmt"
"io"
"log"
"math"
"strconv"
"strings"
)
// Histogram accumulates values in the form of a histogram with
// exponentially increased bucket sizes.
type Histogram struct {
// Count is the total number of values added to the histogram.
Count int64
// Sum is the sum of all the values added to the histogram.
Sum int64
// SumOfSquares is the sum of squares of all values.
SumOfSquares int64
// Min is the minimum of all the values added to the histogram.
Min int64
// Max is the maximum of all the values added to the histogram.
Max int64
// Buckets contains all the buckets of the histogram.
Buckets []HistogramBucket
opts HistogramOptions
logBaseBucketSize float64
oneOverLogOnePlusGrowthFactor float64
}
// HistogramOptions contains the parameters that define the histogram's buckets.
// The first bucket of the created histogram (with index 0) contains [min, min+n)
// where n = BaseBucketSize, min = MinValue.
// Bucket i (i>=1) contains [min + n * m^(i-1), min + n * m^i), where m = 1+GrowthFactor.
// The type of the values is int64.
type HistogramOptions struct {
// NumBuckets is the number of buckets.
NumBuckets int
// GrowthFactor is the growth factor of the buckets. A value of 0.1
// indicates that bucket N+1 will be 10% larger than bucket N.
GrowthFactor float64
// BaseBucketSize is the size of the first bucket.
BaseBucketSize float64
// MinValue is the lower bound of the first bucket.
MinValue int64
}
// HistogramBucket represents one histogram bucket.
type HistogramBucket struct {
// LowBound is the lower bound of the bucket.
LowBound float64
// Count is the number of values in the bucket.
Count int64
}
// NewHistogram returns a pointer to a new Histogram object that was created
// with the provided options.
func NewHistogram(opts HistogramOptions) *Histogram {
if opts.NumBuckets == 0 {
opts.NumBuckets = 32
}
if opts.BaseBucketSize == 0.0 {
opts.BaseBucketSize = 1.0
}
h := Histogram{
Buckets: make([]HistogramBucket, opts.NumBuckets),
Min: math.MaxInt64,
Max: math.MinInt64,
opts: opts,
logBaseBucketSize: math.Log(opts.BaseBucketSize),
oneOverLogOnePlusGrowthFactor: 1 / math.Log(1+opts.GrowthFactor),
}
m := 1.0 + opts.GrowthFactor
delta := opts.BaseBucketSize
h.Buckets[0].LowBound = float64(opts.MinValue)
for i := 1; i < opts.NumBuckets; i++ {
h.Buckets[i].LowBound = float64(opts.MinValue) + delta
delta = delta * m
}
return &h
}
// Print writes textual output of the histogram values.
func (h *Histogram) Print(w io.Writer) {
h.PrintWithUnit(w, 1)
}
// PrintWithUnit writes textual output of the histogram values .
// Data in histogram is divided by a Unit before print.
func (h *Histogram) PrintWithUnit(w io.Writer, unit float64) {
avg := float64(h.Sum) / float64(h.Count)
fmt.Fprintf(w, "Count: %d Min: %5.1f Max: %5.1f Avg: %.2f\n", h.Count, float64(h.Min)/unit, float64(h.Max)/unit, avg/unit)
fmt.Fprintf(w, "%s\n", strings.Repeat("-", 60))
if h.Count <= 0 {
return
}
maxBucketDigitLen := len(strconv.FormatFloat(h.Buckets[len(h.Buckets)-1].LowBound, 'f', 6, 64))
maxCountDigitLen := len(strconv.FormatInt(h.Count, 10))
percentMulti := 100 / float64(h.Count)
accCount := int64(0)
for i, b := range h.Buckets {
fmt.Fprintf(w, "[%*f, ", maxBucketDigitLen, b.LowBound/unit)
if i+1 < len(h.Buckets) {
fmt.Fprintf(w, "%*f)", maxBucketDigitLen, h.Buckets[i+1].LowBound/unit)
} else {
upperBound := float64(h.opts.MinValue) + (b.LowBound-float64(h.opts.MinValue))*(1.0+h.opts.GrowthFactor)
fmt.Fprintf(w, "%*f)", maxBucketDigitLen, upperBound/unit)
}
accCount += b.Count
fmt.Fprintf(w, " %*d %5.1f%% %5.1f%%", maxCountDigitLen, b.Count, float64(b.Count)*percentMulti, float64(accCount)*percentMulti)
const barScale = 0.1
barLength := int(float64(b.Count)*percentMulti*barScale + 0.5)
fmt.Fprintf(w, " %s\n", strings.Repeat("#", barLength))
}
}
// String returns the textual output of the histogram values as string.
func (h *Histogram) String() string {
var b bytes.Buffer
h.Print(&b)
return b.String()
}
// Clear resets all the content of histogram.
func (h *Histogram) Clear() {
h.Count = 0
h.Sum = 0
h.SumOfSquares = 0
h.Min = math.MaxInt64
h.Max = math.MinInt64
for i := range h.Buckets {
h.Buckets[i].Count = 0
}
}
// Opts returns a copy of the options used to create the Histogram.
func (h *Histogram) Opts() HistogramOptions {
return h.opts
}
// Add adds a value to the histogram.
func (h *Histogram) Add(value int64) error {
bucket, err := h.findBucket(value)
if err != nil {
return err
}
h.Buckets[bucket].Count++
h.Count++
h.Sum += value
h.SumOfSquares += value * value
if value < h.Min {
h.Min = value
}
if value > h.Max {
h.Max = value
}
return nil
}
func (h *Histogram) findBucket(value int64) (int, error) {
delta := float64(value - h.opts.MinValue)
if delta < 0 {
return 0, fmt.Errorf("no bucket for value: %d", value)
}
var b int
if delta >= h.opts.BaseBucketSize {
// b = log_{1+growthFactor} (delta / baseBucketSize) + 1
// = log(delta / baseBucketSize) / log(1+growthFactor) + 1
// = (log(delta) - log(baseBucketSize)) * (1 / log(1+growthFactor)) + 1
b = int((math.Log(delta)-h.logBaseBucketSize)*h.oneOverLogOnePlusGrowthFactor + 1)
}
if b >= len(h.Buckets) {
return 0, fmt.Errorf("no bucket for value: %d", value)
}
return b, nil
}
// Merge takes another histogram h2, and merges its content into h.
// The two histograms must be created by equivalent HistogramOptions.
func (h *Histogram) Merge(h2 *Histogram) {
if h.opts != h2.opts {
log.Fatalf("failed to merge histograms, created by inequivalent options")
}
h.Count += h2.Count
h.Sum += h2.Sum
h.SumOfSquares += h2.SumOfSquares
if h2.Min < h.Min {
h.Min = h2.Min
}
if h2.Max > h.Max {
h.Max = h2.Max
}
for i, b := range h2.Buckets {
h.Buckets[i].Count += b.Count
}
}

496
benchmark/stats/stats.go Normal file
View File

@@ -0,0 +1,496 @@
/*
*
* 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 tracks the statistics associated with benchmark runs.
package stats
import (
"bytes"
"fmt"
"log"
"math"
"runtime"
"sort"
"strconv"
"sync"
"time"
"google.golang.org/grpc"
)
// 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
ReqPayloadCurveIndex
RespPayloadCurveIndex
CompModesIndex
EnableChannelzIndex
EnablePreloaderIndex
ClientReadBufferSize
ClientWriteBufferSize
ServerReadBufferSize
ServerWriteBufferSize
SleepBetweenRPCs
RecvBufferPool
SharedWriteBuffer
// 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
// Connections configures the number of grpc connections between client and server.
Connections int
// 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 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.
// Unused if ReqPayloadCurve is non-nil.
ReqSizeBytes int
// RespSizeBytes is the response size in bytes used in this benchmark run.
// Unused if RespPayloadCurve is non-nil.
RespSizeBytes int
// ReqPayloadCurve is a histogram representing the shape a random
// distribution request payloads should take.
ReqPayloadCurve *PayloadCurve
// RespPayloadCurve is a histogram representing the shape a random
// distribution request payloads should take.
RespPayloadCurve *PayloadCurve
// 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
// ClientReadBufferSize is the size of the client read buffer in bytes. If negative, use the default buffer size.
ClientReadBufferSize int
// ClientWriteBufferSize is the size of the client write buffer in bytes. If negative, use the default buffer size.
ClientWriteBufferSize int
// ServerReadBufferSize is the size of the server read buffer in bytes. If negative, use the default buffer size.
ServerReadBufferSize int
// ServerWriteBufferSize is the size of the server write buffer in bytes. If negative, use the default buffer size.
ServerWriteBufferSize int
// SleepBetweenRPCs configures optional delay between RPCs.
SleepBetweenRPCs time.Duration
// RecvBufferPool represents the shared recv buffer pool used.
RecvBufferPool string
// SharedWriteBuffer configures whether both client and server share per-connection write buffer
SharedWriteBuffer bool
}
// String returns all the feature values as a string.
func (f Features) String() string {
var reqPayloadString, respPayloadString string
if f.ReqPayloadCurve != nil {
reqPayloadString = fmt.Sprintf("reqPayloadCurve_%s", f.ReqPayloadCurve.ShortHash())
} else {
reqPayloadString = fmt.Sprintf("reqSize_%vB", f.ReqSizeBytes)
}
if f.RespPayloadCurve != nil {
respPayloadString = fmt.Sprintf("respPayloadCurve_%s", f.RespPayloadCurve.ShortHash())
} else {
respPayloadString = fmt.Sprintf("respSize_%vB", f.RespSizeBytes)
}
return fmt.Sprintf("networkMode_%v-bufConn_%v-keepalive_%v-benchTime_%v-"+
"trace_%v-latency_%v-kbps_%v-MTU_%v-maxConcurrentCalls_%v-%s-%s-"+
"compressor_%v-channelz_%v-preloader_%v-clientReadBufferSize_%v-"+
"clientWriteBufferSize_%v-serverReadBufferSize_%v-serverWriteBufferSize_%v-"+
"sleepBetweenRPCs_%v-connections_%v-recvBufferPool_%v-sharedWriteBuffer_%v",
f.NetworkMode, f.UseBufConn, f.EnableKeepalive, f.BenchTime, f.EnableTrace,
f.Latency, f.Kbps, f.MTU, f.MaxConcurrentCalls, reqPayloadString,
respPayloadString, f.ModeCompressor, f.EnableChannelz, f.EnablePreloader,
f.ClientReadBufferSize, f.ClientWriteBufferSize, f.ServerReadBufferSize,
f.ServerWriteBufferSize, f.SleepBetweenRPCs, f.Connections,
f.RecvBufferPool, f.SharedWriteBuffer)
}
// 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()
}
// 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()
}
// 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 ReqPayloadCurveIndex:
if f.ReqPayloadCurve != nil {
b.WriteString(fmt.Sprintf("ReqPayloadCurve%vSHA-256:%v%v", sep, f.ReqPayloadCurve.Hash(), delim))
}
case RespPayloadCurveIndex:
if f.RespPayloadCurve != nil {
b.WriteString(fmt.Sprintf("RespPayloadCurve%vSHA-256:%v%v", sep, f.RespPayloadCurve.Hash(), 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))
case ClientReadBufferSize:
b.WriteString(fmt.Sprintf("ClientReadBufferSize%v%v%v", sep, f.ClientReadBufferSize, delim))
case ClientWriteBufferSize:
b.WriteString(fmt.Sprintf("ClientWriteBufferSize%v%v%v", sep, f.ClientWriteBufferSize, delim))
case ServerReadBufferSize:
b.WriteString(fmt.Sprintf("ServerReadBufferSize%v%v%v", sep, f.ServerReadBufferSize, delim))
case ServerWriteBufferSize:
b.WriteString(fmt.Sprintf("ServerWriteBufferSize%v%v%v", sep, f.ServerWriteBufferSize, delim))
case SleepBetweenRPCs:
b.WriteString(fmt.Sprintf("SleepBetweenRPCs%v%v%v", sep, f.SleepBetweenRPCs, delim))
case RecvBufferPool:
b.WriteString(fmt.Sprintf("RecvBufferPool%v%v%v", sep, f.RecvBufferPool, delim))
case SharedWriteBuffer:
b.WriteString(fmt.Sprintf("SharedWriteBuffer%v%v%v", sep, f.SharedWriteBuffer, delim))
default:
log.Fatalf("Unknown feature index %v. maxFeatureIndex is %v", i, MaxFeatureIndex)
}
}
}
}
// 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 {
// GoVersion is the version of the compiler the benchmark was compiled with.
GoVersion string
// GrpcVersion is the gRPC version being benchmarked.
GrpcVersion string
// 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
// 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
}
// 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 float64
// Allocs is the average number of memory allocations per operation.
Allocs float64
// ReqT is the average request throughput associated with this run.
ReqT float64
// RespT is the average response throughput associated with this run.
RespT float64
// 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.
// 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
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
}
// Use one more bucket for the last unbounded bucket.
s := &Stats{numBuckets: numBuckets + 1}
s.hw = &histWrapper{}
return s
}
// 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{
GoVersion: runtime.Version(),
GrpcVersion: grpc.Version,
RunMode: mode,
Features: f,
SharedFeatures: sf,
})
}
// 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: float64(s.stopMS.TotalAlloc-s.startMS.TotalAlloc) / float64(count),
Allocs: float64(s.stopMS.Mallocs-s.startMS.Mallocs) / float64(count),
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: float64(s.stopMS.TotalAlloc-s.startMS.TotalAlloc) / float64((req+resp)/2),
Allocs: float64(s.stopMS.Mallocs-s.startMS.Mallocs) / float64((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
// Go and gRPC version information.
b.WriteString(fmt.Sprintf("%s/grpc%s\n", result.GoVersion, result.GrpcVersion))
// 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())
}

View File

@@ -0,0 +1,428 @@
/*
*
* Copyright 2016 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 main
import (
"context"
"flag"
"math"
rand "math/rand/v2"
"runtime"
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/benchmark"
"google.golang.org/grpc/benchmark/stats"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/internal/syscall"
"google.golang.org/grpc/status"
"google.golang.org/grpc/testdata"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
_ "google.golang.org/grpc/xds" // To install the xds resolvers and balancers.
)
var caFile = flag.String("ca_file", "", "The file containing the CA root cert file")
type lockingHistogram struct {
mu sync.Mutex
histogram *stats.Histogram
}
func (h *lockingHistogram) add(value int64) {
h.mu.Lock()
defer h.mu.Unlock()
h.histogram.Add(value)
}
// swap sets h.histogram to o and returns its old value.
func (h *lockingHistogram) swap(o *stats.Histogram) *stats.Histogram {
h.mu.Lock()
defer h.mu.Unlock()
old := h.histogram
h.histogram = o
return old
}
func (h *lockingHistogram) mergeInto(merged *stats.Histogram) {
h.mu.Lock()
defer h.mu.Unlock()
merged.Merge(h.histogram)
}
type benchmarkClient struct {
closeConns func()
lastResetTime time.Time
histogramOptions stats.HistogramOptions
lockingHistograms []lockingHistogram
rusageLastReset *syscall.Rusage
}
func printClientConfig(config *testpb.ClientConfig) {
// Some config options are ignored:
// - client type:
// will always create sync client
// - async client threads.
// - core list
logger.Infof(" * client type: %v (ignored, always creates sync client)", config.ClientType)
logger.Infof(" * async client threads: %v (ignored)", config.AsyncClientThreads)
// TODO: use cores specified by CoreList when setting list of cores is supported in go.
logger.Infof(" * core list: %v (ignored)", config.CoreList)
logger.Infof(" - security params: %v", config.SecurityParams)
logger.Infof(" - core limit: %v", config.CoreLimit)
logger.Infof(" - payload config: %v", config.PayloadConfig)
logger.Infof(" - rpcs per chann: %v", config.OutstandingRpcsPerChannel)
logger.Infof(" - channel number: %v", config.ClientChannels)
logger.Infof(" - load params: %v", config.LoadParams)
logger.Infof(" - rpc type: %v", config.RpcType)
logger.Infof(" - histogram params: %v", config.HistogramParams)
logger.Infof(" - server targets: %v", config.ServerTargets)
}
func setupClientEnv(config *testpb.ClientConfig) {
// Use all cpu cores available on machine by default.
// TODO: Revisit this for the optimal default setup.
if config.CoreLimit > 0 {
runtime.GOMAXPROCS(int(config.CoreLimit))
} else {
runtime.GOMAXPROCS(runtime.NumCPU())
}
}
// createConns creates connections according to given config.
// It returns the connections and corresponding function to close them.
// It returns non-nil error if there is anything wrong.
func createConns(config *testpb.ClientConfig) ([]*grpc.ClientConn, func(), error) {
opts := []grpc.DialOption{
grpc.WithWriteBufferSize(128 * 1024),
grpc.WithReadBufferSize(128 * 1024),
}
// Sanity check for client type.
switch config.ClientType {
case testpb.ClientType_SYNC_CLIENT:
case testpb.ClientType_ASYNC_CLIENT:
default:
return nil, nil, status.Errorf(codes.InvalidArgument, "unknown client type: %v", config.ClientType)
}
// Check and set security options.
if config.SecurityParams != nil {
if *caFile == "" {
*caFile = testdata.Path("ca.pem")
}
creds, err := credentials.NewClientTLSFromFile(*caFile, config.SecurityParams.ServerHostOverride)
if err != nil {
return nil, nil, status.Errorf(codes.InvalidArgument, "failed to create TLS credentials: %v", err)
}
opts = append(opts, grpc.WithTransportCredentials(creds))
} else {
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
}
// Use byteBufCodec if it is required.
if config.PayloadConfig != nil {
switch config.PayloadConfig.Payload.(type) {
case *testpb.PayloadConfig_BytebufParams:
opts = append(opts, grpc.WithDefaultCallOptions(grpc.CallCustomCodec(byteBufCodec{})))
case *testpb.PayloadConfig_SimpleParams:
default:
return nil, nil, status.Errorf(codes.InvalidArgument, "unknown payload config: %v", config.PayloadConfig)
}
}
// Create connections.
connCount := int(config.ClientChannels)
conns := make([]*grpc.ClientConn, connCount)
for connIndex := 0; connIndex < connCount; connIndex++ {
conns[connIndex] = benchmark.NewClientConn(config.ServerTargets[connIndex%len(config.ServerTargets)], opts...)
}
return conns, func() {
for _, conn := range conns {
conn.Close()
}
}, nil
}
func performRPCs(ctx context.Context, config *testpb.ClientConfig, conns []*grpc.ClientConn, bc *benchmarkClient) error {
// Read payload size and type from config.
var (
payloadReqSize, payloadRespSize int
payloadType string
)
if config.PayloadConfig != nil {
switch c := config.PayloadConfig.Payload.(type) {
case *testpb.PayloadConfig_BytebufParams:
payloadReqSize = int(c.BytebufParams.ReqSize)
payloadRespSize = int(c.BytebufParams.RespSize)
payloadType = "bytebuf"
case *testpb.PayloadConfig_SimpleParams:
payloadReqSize = int(c.SimpleParams.ReqSize)
payloadRespSize = int(c.SimpleParams.RespSize)
payloadType = "protobuf"
default:
return status.Errorf(codes.InvalidArgument, "unknown payload config: %v", config.PayloadConfig)
}
}
// If set, perform an open loop, if not perform a closed loop. An open loop
// asynchronously starts RPCs based on random start times derived from a
// Poisson distribution. A closed loop performs RPCs in a blocking manner,
// and runs the next RPC after the previous RPC completes and returns.
var poissonLambda *float64
switch t := config.LoadParams.Load.(type) {
case *testpb.LoadParams_ClosedLoop:
case *testpb.LoadParams_Poisson:
if t.Poisson == nil {
return status.Errorf(codes.InvalidArgument, "poisson is nil, needs to be set")
}
if t.Poisson.OfferedLoad <= 0 {
return status.Errorf(codes.InvalidArgument, "poisson.offered is <= 0: %v, needs to be >0", t.Poisson.OfferedLoad)
}
poissonLambda = &t.Poisson.OfferedLoad
default:
return status.Errorf(codes.InvalidArgument, "unknown load params: %v", config.LoadParams)
}
rpcCountPerConn := int(config.OutstandingRpcsPerChannel)
switch config.RpcType {
case testpb.RpcType_UNARY:
bc.unaryLoop(ctx, conns, rpcCountPerConn, payloadReqSize, payloadRespSize, poissonLambda)
case testpb.RpcType_STREAMING:
bc.streamingLoop(ctx, conns, rpcCountPerConn, payloadReqSize, payloadRespSize, payloadType, poissonLambda)
default:
return status.Errorf(codes.InvalidArgument, "unknown rpc type: %v", config.RpcType)
}
return nil
}
func startBenchmarkClient(ctx context.Context, config *testpb.ClientConfig) (*benchmarkClient, error) {
printClientConfig(config)
// Set running environment like how many cores to use.
setupClientEnv(config)
conns, closeConns, err := createConns(config)
if err != nil {
return nil, err
}
rpcCountPerConn := int(config.OutstandingRpcsPerChannel)
bc := &benchmarkClient{
histogramOptions: stats.HistogramOptions{
NumBuckets: int(math.Log(config.HistogramParams.MaxPossible)/math.Log(1+config.HistogramParams.Resolution)) + 1,
GrowthFactor: config.HistogramParams.Resolution,
BaseBucketSize: (1 + config.HistogramParams.Resolution),
MinValue: 0,
},
lockingHistograms: make([]lockingHistogram, rpcCountPerConn*len(conns)),
lastResetTime: time.Now(),
closeConns: closeConns,
rusageLastReset: syscall.GetRusage(),
}
if err = performRPCs(ctx, config, conns, bc); err != nil {
// Close all connections if performRPCs failed.
closeConns()
return nil, err
}
return bc, nil
}
func (bc *benchmarkClient) unaryLoop(ctx context.Context, conns []*grpc.ClientConn, rpcCountPerConn int, reqSize int, respSize int, poissonLambda *float64) {
for ic, conn := range conns {
client := testgrpc.NewBenchmarkServiceClient(conn)
// For each connection, create rpcCountPerConn goroutines to do rpc.
for j := range rpcCountPerConn {
// Create histogram for each goroutine.
idx := ic*rpcCountPerConn + j
bc.lockingHistograms[idx].histogram = stats.NewHistogram(bc.histogramOptions)
// Start goroutine on the created mutex and histogram.
go func(idx int) {
// TODO: do warm up if necessary.
// Now relying on worker client to reserve time to do warm up.
// The worker client needs to wait for some time after client is created,
// before starting benchmark.
if poissonLambda == nil { // Closed loop.
for {
if ctx.Err() != nil {
break
}
start := time.Now()
if err := benchmark.DoUnaryCall(client, reqSize, respSize); err != nil {
continue
}
elapse := time.Since(start)
bc.lockingHistograms[idx].add(int64(elapse))
}
} else { // Open loop.
timeBetweenRPCs := time.Duration((rand.ExpFloat64() / *poissonLambda) * float64(time.Second))
time.AfterFunc(timeBetweenRPCs, func() {
bc.poissonUnary(client, idx, reqSize, respSize, *poissonLambda)
})
}
}(idx)
}
}
}
func (bc *benchmarkClient) streamingLoop(ctx context.Context, conns []*grpc.ClientConn, rpcCountPerConn int, reqSize int, respSize int, payloadType string, poissonLambda *float64) {
var doRPC func(testgrpc.BenchmarkService_StreamingCallClient, int, int) error
if payloadType == "bytebuf" {
doRPC = benchmark.DoByteBufStreamingRoundTrip
} else {
doRPC = benchmark.DoStreamingRoundTrip
}
for ic, conn := range conns {
// For each connection, create rpcCountPerConn goroutines to do rpc.
for j := 0; j < rpcCountPerConn; j++ {
c := testgrpc.NewBenchmarkServiceClient(conn)
stream, err := c.StreamingCall(context.Background())
if err != nil {
logger.Fatalf("%v.StreamingCall(_) = _, %v", c, err)
}
idx := ic*rpcCountPerConn + j
bc.lockingHistograms[idx].histogram = stats.NewHistogram(bc.histogramOptions)
if poissonLambda == nil { // Closed loop.
// Start goroutine on the created mutex and histogram.
go func(idx int) {
// TODO: do warm up if necessary.
// Now relying on worker client to reserve time to do warm up.
// The worker client needs to wait for some time after client is created,
// before starting benchmark.
for {
start := time.Now()
if err := doRPC(stream, reqSize, respSize); err != nil {
return
}
elapse := time.Since(start)
bc.lockingHistograms[idx].add(int64(elapse))
if ctx.Err() != nil {
return
}
}
}(idx)
} else { // Open loop.
timeBetweenRPCs := time.Duration((rand.ExpFloat64() / *poissonLambda) * float64(time.Second))
time.AfterFunc(timeBetweenRPCs, func() {
bc.poissonStreaming(stream, idx, reqSize, respSize, *poissonLambda, doRPC)
})
}
}
}
}
func (bc *benchmarkClient) poissonUnary(client testgrpc.BenchmarkServiceClient, idx int, reqSize int, respSize int, lambda float64) {
go func() {
start := time.Now()
if err := benchmark.DoUnaryCall(client, reqSize, respSize); err != nil {
return
}
elapse := time.Since(start)
bc.lockingHistograms[idx].add(int64(elapse))
}()
timeBetweenRPCs := time.Duration((rand.ExpFloat64() / lambda) * float64(time.Second))
time.AfterFunc(timeBetweenRPCs, func() {
bc.poissonUnary(client, idx, reqSize, respSize, lambda)
})
}
func (bc *benchmarkClient) poissonStreaming(stream testgrpc.BenchmarkService_StreamingCallClient, idx int, reqSize int, respSize int, lambda float64, doRPC func(testgrpc.BenchmarkService_StreamingCallClient, int, int) error) {
go func() {
start := time.Now()
if err := doRPC(stream, reqSize, respSize); err != nil {
return
}
elapse := time.Since(start)
bc.lockingHistograms[idx].add(int64(elapse))
}()
timeBetweenRPCs := time.Duration((rand.ExpFloat64() / lambda) * float64(time.Second))
time.AfterFunc(timeBetweenRPCs, func() {
bc.poissonStreaming(stream, idx, reqSize, respSize, lambda, doRPC)
})
}
// getStats returns the stats for benchmark client.
// It resets lastResetTime and all histograms if argument reset is true.
func (bc *benchmarkClient) getStats(reset bool) *testpb.ClientStats {
var wallTimeElapsed, uTimeElapsed, sTimeElapsed float64
mergedHistogram := stats.NewHistogram(bc.histogramOptions)
if reset {
// Merging histogram may take some time.
// Put all histograms aside and merge later.
toMerge := make([]*stats.Histogram, len(bc.lockingHistograms))
for i := range bc.lockingHistograms {
toMerge[i] = bc.lockingHistograms[i].swap(stats.NewHistogram(bc.histogramOptions))
}
for i := 0; i < len(toMerge); i++ {
mergedHistogram.Merge(toMerge[i])
}
wallTimeElapsed = time.Since(bc.lastResetTime).Seconds()
latestRusage := syscall.GetRusage()
uTimeElapsed, sTimeElapsed = syscall.CPUTimeDiff(bc.rusageLastReset, latestRusage)
bc.rusageLastReset = latestRusage
bc.lastResetTime = time.Now()
} else {
// Merge only, not reset.
for i := range bc.lockingHistograms {
bc.lockingHistograms[i].mergeInto(mergedHistogram)
}
wallTimeElapsed = time.Since(bc.lastResetTime).Seconds()
uTimeElapsed, sTimeElapsed = syscall.CPUTimeDiff(bc.rusageLastReset, syscall.GetRusage())
}
b := make([]uint32, len(mergedHistogram.Buckets))
for i, v := range mergedHistogram.Buckets {
b[i] = uint32(v.Count)
}
return &testpb.ClientStats{
Latencies: &testpb.HistogramData{
Bucket: b,
MinSeen: float64(mergedHistogram.Min),
MaxSeen: float64(mergedHistogram.Max),
Sum: float64(mergedHistogram.Sum),
SumOfSquares: float64(mergedHistogram.SumOfSquares),
Count: float64(mergedHistogram.Count),
},
TimeElapsed: wallTimeElapsed,
TimeUser: uTimeElapsed,
TimeSystem: sTimeElapsed,
}
}
func (bc *benchmarkClient) shutdown() {
bc.closeConns()
}

View File

@@ -0,0 +1,187 @@
/*
*
* Copyright 2016 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 main
import (
"flag"
"fmt"
"net"
"runtime"
"strconv"
"strings"
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/benchmark"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/internal/syscall"
testpb "google.golang.org/grpc/interop/grpc_testing"
"google.golang.org/grpc/status"
"google.golang.org/grpc/testdata"
)
var (
certFile = flag.String("tls_cert_file", "", "The TLS cert file")
keyFile = flag.String("tls_key_file", "", "The TLS key file")
)
type benchmarkServer struct {
port int
cores int
closeFunc func()
mu sync.Mutex
lastResetTime time.Time
rusageLastReset *syscall.Rusage
}
func printServerConfig(config *testpb.ServerConfig) {
// Some config options are ignored:
// - server type:
// will always start sync server
// - async server threads
// - core list
logger.Infof(" * server type: %v (ignored, always starts sync server)", config.ServerType)
logger.Infof(" * async server threads: %v (ignored)", config.AsyncServerThreads)
// TODO: use cores specified by CoreList when setting list of cores is supported in go.
logger.Infof(" * core list: %v (ignored)", config.CoreList)
logger.Infof(" - security params: %v", config.SecurityParams)
logger.Infof(" - core limit: %v", config.CoreLimit)
logger.Infof(" - port: %v", config.Port)
logger.Infof(" - payload config: %v", config.PayloadConfig)
}
func startBenchmarkServer(config *testpb.ServerConfig, serverPort int) (*benchmarkServer, error) {
printServerConfig(config)
// Use all cpu cores available on machine by default.
// TODO: Revisit this for the optimal default setup.
numOfCores := runtime.NumCPU()
if config.CoreLimit > 0 {
numOfCores = int(config.CoreLimit)
}
runtime.GOMAXPROCS(numOfCores)
opts := []grpc.ServerOption{
grpc.WriteBufferSize(128 * 1024),
grpc.ReadBufferSize(128 * 1024),
}
// Sanity check for server type.
switch config.ServerType {
case testpb.ServerType_SYNC_SERVER:
case testpb.ServerType_ASYNC_SERVER:
case testpb.ServerType_ASYNC_GENERIC_SERVER:
default:
return nil, status.Errorf(codes.InvalidArgument, "unknown server type: %v", config.ServerType)
}
// Set security options.
if config.SecurityParams != nil {
if *certFile == "" {
*certFile = testdata.Path("server1.pem")
}
if *keyFile == "" {
*keyFile = testdata.Path("server1.key")
}
creds, err := credentials.NewServerTLSFromFile(*certFile, *keyFile)
if err != nil {
logger.Fatalf("failed to generate credentials: %v", err)
}
opts = append(opts, grpc.Creds(creds))
}
// Priority: config.Port > serverPort > default (0).
port := int(config.Port)
if port == 0 {
port = serverPort
}
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
logger.Fatalf("Failed to listen: %v", err)
}
addr := lis.Addr().String()
// Create different benchmark server according to config.
var closeFunc func()
if config.PayloadConfig != nil {
switch payload := config.PayloadConfig.Payload.(type) {
case *testpb.PayloadConfig_BytebufParams:
opts = append(opts, grpc.CustomCodec(byteBufCodec{}))
closeFunc = benchmark.StartServer(benchmark.ServerInfo{
Type: "bytebuf",
Metadata: payload.BytebufParams.RespSize,
Listener: lis,
}, opts...)
case *testpb.PayloadConfig_SimpleParams:
closeFunc = benchmark.StartServer(benchmark.ServerInfo{
Type: "protobuf",
Listener: lis,
}, opts...)
case *testpb.PayloadConfig_ComplexParams:
return nil, status.Errorf(codes.Unimplemented, "unsupported payload config: %v", config.PayloadConfig)
default:
return nil, status.Errorf(codes.InvalidArgument, "unknown payload config: %v", config.PayloadConfig)
}
} else {
// Start protobuf server if payload config is nil.
closeFunc = benchmark.StartServer(benchmark.ServerInfo{
Type: "protobuf",
Listener: lis,
}, opts...)
}
logger.Infof("benchmark server listening at %v", addr)
addrSplitted := strings.Split(addr, ":")
p, err := strconv.Atoi(addrSplitted[len(addrSplitted)-1])
if err != nil {
logger.Fatalf("failed to get port number from server address: %v", err)
}
return &benchmarkServer{
port: p,
cores: numOfCores,
closeFunc: closeFunc,
lastResetTime: time.Now(),
rusageLastReset: syscall.GetRusage(),
}, nil
}
// getStats returns the stats for benchmark server.
// It resets lastResetTime if argument reset is true.
func (bs *benchmarkServer) getStats(reset bool) *testpb.ServerStats {
bs.mu.Lock()
defer bs.mu.Unlock()
wallTimeElapsed := time.Since(bs.lastResetTime).Seconds()
rusageLatest := syscall.GetRusage()
uTimeElapsed, sTimeElapsed := syscall.CPUTimeDiff(bs.rusageLastReset, rusageLatest)
if reset {
bs.lastResetTime = time.Now()
bs.rusageLastReset = rusageLatest
}
return &testpb.ServerStats{
TimeElapsed: wallTimeElapsed,
TimeUser: uTimeElapsed,
TimeSystem: sTimeElapsed,
}
}

239
benchmark/worker/main.go Normal file
View File

@@ -0,0 +1,239 @@
/*
*
* Copyright 2016 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.
*
*/
// Binary worker implements the benchmark worker that can turn into a benchmark
// client or server.
package main
import (
"context"
"flag"
"fmt"
"io"
"net"
"net/http"
_ "net/http/pprof"
"runtime"
"strconv"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/status"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
)
var (
driverPort = flag.Int("driver_port", 10000, "port for communication with driver")
serverPort = flag.Int("server_port", 0, "port for benchmark server if not specified by server config message")
pprofPort = flag.Int("pprof_port", -1, "Port for pprof debug server to listen on. Pprof server doesn't start if unset")
blockProfRate = flag.Int("block_prof_rate", 0, "fraction of goroutine blocking events to report in blocking profile")
logger = grpclog.Component("benchmark")
)
type byteBufCodec struct {
}
func (byteBufCodec) Marshal(v any) ([]byte, error) {
b, ok := v.(*[]byte)
if !ok {
return nil, fmt.Errorf("failed to marshal: %v is not type of *[]byte", v)
}
return *b, nil
}
func (byteBufCodec) Unmarshal(data []byte, v any) error {
b, ok := v.(*[]byte)
if !ok {
return fmt.Errorf("failed to marshal: %v is not type of *[]byte", v)
}
*b = data
return nil
}
func (byteBufCodec) String() string {
return "bytebuffer"
}
// workerServer implements WorkerService rpc handlers.
// It can create benchmarkServer or benchmarkClient on demand.
type workerServer struct {
testgrpc.UnimplementedWorkerServiceServer
stop chan<- bool
serverPort int
}
func (s *workerServer) RunServer(stream testgrpc.WorkerService_RunServerServer) error {
var bs *benchmarkServer
defer func() {
// Close benchmark server when stream ends.
logger.Infof("closing benchmark server")
if bs != nil {
bs.closeFunc()
}
}()
for {
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
var out *testpb.ServerStatus
switch argtype := in.Argtype.(type) {
case *testpb.ServerArgs_Setup:
logger.Infof("server setup received:")
if bs != nil {
logger.Infof("server setup received when server already exists, closing the existing server")
bs.closeFunc()
}
bs, err = startBenchmarkServer(argtype.Setup, s.serverPort)
if err != nil {
return err
}
out = &testpb.ServerStatus{
Stats: bs.getStats(false),
Port: int32(bs.port),
Cores: int32(bs.cores),
}
case *testpb.ServerArgs_Mark:
logger.Infof("server mark received:")
logger.Infof(" - %v", argtype)
if bs == nil {
return status.Error(codes.InvalidArgument, "server does not exist when mark received")
}
out = &testpb.ServerStatus{
Stats: bs.getStats(argtype.Mark.Reset_),
Port: int32(bs.port),
Cores: int32(bs.cores),
}
}
if err := stream.Send(out); err != nil {
return err
}
}
}
func (s *workerServer) RunClient(stream testgrpc.WorkerService_RunClientServer) error {
var bc *benchmarkClient
ctx, cancel := context.WithCancel(stream.Context())
defer func() {
cancel()
// Shut down benchmark client when stream ends.
logger.Infof("shutting down benchmark client")
if bc != nil {
bc.shutdown()
}
}()
for {
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
var out *testpb.ClientStatus
switch t := in.Argtype.(type) {
case *testpb.ClientArgs_Setup:
logger.Infof("client setup received:")
if bc != nil {
logger.Infof("client setup received when client already exists, shutting down the existing client")
bc.shutdown()
}
bc, err = startBenchmarkClient(ctx, t.Setup)
if err != nil {
return err
}
out = &testpb.ClientStatus{
Stats: bc.getStats(false),
}
case *testpb.ClientArgs_Mark:
logger.Infof("client mark received:")
logger.Infof(" - %v", t)
if bc == nil {
return status.Error(codes.InvalidArgument, "client does not exist when mark received")
}
out = &testpb.ClientStatus{
Stats: bc.getStats(t.Mark.Reset_),
}
}
if err := stream.Send(out); err != nil {
return err
}
}
}
func (s *workerServer) CoreCount(context.Context, *testpb.CoreRequest) (*testpb.CoreResponse, error) {
logger.Infof("core count: %v", runtime.NumCPU())
return &testpb.CoreResponse{Cores: int32(runtime.NumCPU())}, nil
}
func (s *workerServer) QuitWorker(context.Context, *testpb.Void) (*testpb.Void, error) {
logger.Infof("quitting worker")
s.stop <- true
return &testpb.Void{}, nil
}
func main() {
grpc.EnableTracing = false
flag.Parse()
lis, err := net.Listen("tcp", ":"+strconv.Itoa(*driverPort))
if err != nil {
logger.Fatalf("failed to listen: %v", err)
}
logger.Infof("worker listening at port %v", *driverPort)
s := grpc.NewServer()
stop := make(chan bool)
testgrpc.RegisterWorkerServiceServer(s, &workerServer{
stop: stop,
serverPort: *serverPort,
})
go func() {
<-stop
// Wait for 1 second before stopping the server to make sure the return value of QuitWorker is sent to client.
// TODO revise this once server graceful stop is supported in gRPC.
time.Sleep(time.Second)
s.Stop()
}()
runtime.SetBlockProfileRate(*blockProfRate)
if *pprofPort >= 0 {
go func() {
logger.Infoln("Starting pprof server on port " + strconv.Itoa(*pprofPort))
logger.Infoln(http.ListenAndServe("localhost:"+strconv.Itoa(*pprofPort), nil))
}()
}
s.Serve(lis)
}