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
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:
1022
benchmark/benchmain/main.go
Normal file
1022
benchmark/benchmain/main.go
Normal file
File diff suppressed because it is too large
Load Diff
349
benchmark/benchmark.go
Normal file
349
benchmark/benchmark.go
Normal 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
|
||||
}
|
||||
152
benchmark/benchresult/main.go
Normal file
152
benchmark/benchresult/main.go
Normal 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
217
benchmark/client/main.go
Normal 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
178
benchmark/flags/flags.go
Normal 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, ",")
|
||||
}
|
||||
153
benchmark/flags/flags_test.go
Normal file
153
benchmark/flags/flags_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
334
benchmark/latency/latency.go
Normal file
334
benchmark/latency/latency.go
Normal 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
|
||||
363
benchmark/latency/latency_test.go
Normal file
363
benchmark/latency/latency_test.go
Normal 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.
|
||||
}
|
||||
125
benchmark/primitives/code_string_test.go
Normal file
125
benchmark/primitives/code_string_test.go
Normal 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()
|
||||
}
|
||||
}
|
||||
152
benchmark/primitives/context_test.go
Normal file
152
benchmark/primitives/context_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
466
benchmark/primitives/primitives_test.go
Normal file
466
benchmark/primitives/primitives_test.go
Normal 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()
|
||||
}
|
||||
})
|
||||
}
|
||||
114
benchmark/primitives/safe_config_selector_test.go
Normal file
114
benchmark/primitives/safe_config_selector_test.go
Normal 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{})
|
||||
}
|
||||
165
benchmark/primitives/syncmap_test.go
Normal file
165
benchmark/primitives/syncmap_test.go
Normal 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
188
benchmark/run_bench.sh
Executable 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
97
benchmark/server/main.go
Normal 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
179
benchmark/stats/curve.go
Normal 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]
|
||||
}
|
||||
221
benchmark/stats/histogram.go
Normal file
221
benchmark/stats/histogram.go
Normal 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
496
benchmark/stats/stats.go
Normal 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())
|
||||
}
|
||||
428
benchmark/worker/benchmark_client.go
Normal file
428
benchmark/worker/benchmark_client.go
Normal 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()
|
||||
}
|
||||
187
benchmark/worker/benchmark_server.go
Normal file
187
benchmark/worker/benchmark_server.go
Normal 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
239
benchmark/worker/main.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user