mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-06 15:08:39 +00:00
Merge 50c4ff53a7 into 12eabbd76d
This commit is contained in:
commit
7e8ac114b6
1 changed files with 210 additions and 0 deletions
210
node/rpcstack_bench_test.go
Normal file
210
node/rpcstack_bench_test.go
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// createAndStartServerTB is will create a server instance with the given config and returns it.
|
||||||
|
func createAndStartServerTB(tb testing.TB, httpConfig *httpConfig, disableHTTP2 bool, wsConfig *wsConfig, apis []rpc.API) *httpServer {
|
||||||
|
tb.Helper()
|
||||||
|
srv := &httpServer{
|
||||||
|
log: log.New("rpc-bench"),
|
||||||
|
timeouts: rpc.DefaultHTTPTimeouts,
|
||||||
|
disableHTTP2: disableHTTP2,
|
||||||
|
}
|
||||||
|
if err := srv.setListenAddr("127.0.0.1", 0); err != nil {
|
||||||
|
tb.Fatal("failed to set listen address:", err)
|
||||||
|
}
|
||||||
|
if err := srv.enableRPC(apis, *httpConfig); err != nil {
|
||||||
|
tb.Fatal("failed to enable RPC:", err)
|
||||||
|
}
|
||||||
|
if err := srv.enableWS(apis, *wsConfig); err != nil {
|
||||||
|
tb.Fatal("failed to enable WS:", err)
|
||||||
|
}
|
||||||
|
if err := srv.start(); err != nil {
|
||||||
|
tb.Fatal("failed to start HTTP server:", err)
|
||||||
|
}
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
|
// newHTTP1Client returns a vanilla HTTP/1.1 client.
|
||||||
|
func newHTTP1Client() *http.Client {
|
||||||
|
tr := &http.Transport{
|
||||||
|
MaxIdleConns: 2048,
|
||||||
|
MaxIdleConnsPerHost: 2048,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
}
|
||||||
|
tr.Protocols = new(http.Protocols)
|
||||||
|
tr.Protocols.SetHTTP1(true)
|
||||||
|
return &http.Client{Transport: tr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newH2CClient returns a client that speaks unencrypted HTTP/2 (H2C).
|
||||||
|
func newH2CClient() *http.Client {
|
||||||
|
tr := &http.Transport{}
|
||||||
|
tr.Protocols = new(http.Protocols)
|
||||||
|
tr.Protocols.SetUnencryptedHTTP2(true)
|
||||||
|
return &http.Client{Transport: tr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rpcPayload is a minimal valid JSON-RPC 2.0 request.
|
||||||
|
const rpcPayload = `{"jsonrpc":"2.0","id":1,"method":"rpc_modules","params":[]}`
|
||||||
|
|
||||||
|
// doRequest sends one JSON-RPC POST and discards the body. It returns the
|
||||||
|
// round-trip latency in nanoseconds and any error.
|
||||||
|
func doRequest(client *http.Client, url string) (int64, error) {
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := client.Post(url, "application/json",
|
||||||
|
strings.NewReader(rpcPayload))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
_, _ = io.Copy(io.Discard, resp.Body) // drain so the connection is reusable
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return 0, fmt.Errorf("unexpected status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return time.Since(start).Nanoseconds(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// latencyHistogram is a lock-free accumulator for percentile reporting.
|
||||||
|
type latencyHistogram struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
samples []int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *latencyHistogram) record(ns int64) {
|
||||||
|
h.mu.Lock()
|
||||||
|
h.samples = append(h.samples, ns)
|
||||||
|
h.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *latencyHistogram) report(b *testing.B) {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
if len(h.samples) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort.Slice(h.samples, func(i, j int) bool { return h.samples[i] < h.samples[j] })
|
||||||
|
n := len(h.samples)
|
||||||
|
p := func(pct float64) float64 {
|
||||||
|
idx := int(math.Ceil(pct/100.0*float64(n))) - 1
|
||||||
|
if idx < 0 {
|
||||||
|
idx = 0
|
||||||
|
}
|
||||||
|
if idx >= n {
|
||||||
|
idx = n - 1
|
||||||
|
}
|
||||||
|
return float64(h.samples[idx]) / 1e6 // ns → ms
|
||||||
|
}
|
||||||
|
b.ReportMetric(p(50), "p50_ms")
|
||||||
|
b.ReportMetric(p(95), "p95_ms")
|
||||||
|
b.ReportMetric(p(99), "p99_ms")
|
||||||
|
}
|
||||||
|
|
||||||
|
// benchConcurrent is the shared driver used by every sub-benchmark.
|
||||||
|
//
|
||||||
|
// client – pre-configured HTTP client (HTTP/1 or H2C)
|
||||||
|
// url – full RPC endpoint URL
|
||||||
|
// concurrency – number of parallel goroutines hammering the server
|
||||||
|
func benchConcurrent(b *testing.B, client *http.Client, url string, concurrency int) {
|
||||||
|
b.Helper()
|
||||||
|
|
||||||
|
hist := &latencyHistogram{}
|
||||||
|
var errors atomic.Int64
|
||||||
|
|
||||||
|
b.SetParallelism(concurrency)
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
for pb.Next() {
|
||||||
|
ns, err := doRequest(client, url)
|
||||||
|
if err != nil {
|
||||||
|
errors.Add(1)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hist.record(ns)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.StopTimer()
|
||||||
|
hist.report(b)
|
||||||
|
|
||||||
|
if e := errors.Load(); e > 0 {
|
||||||
|
b.Logf("WARN: %d requests failed", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var concurrencyLevels = []int{1, 10, 50, 100, 500}
|
||||||
|
|
||||||
|
// BenchmarkRPCHTTP1 measures the HTTP/1.1 baseline.
|
||||||
|
func BenchmarkRPCHTTP1(b *testing.B) {
|
||||||
|
srv := createAndStartServerTB(b, &httpConfig{}, true /*disableHTTP2*/, &wsConfig{}, nil)
|
||||||
|
defer srv.stop()
|
||||||
|
|
||||||
|
url := "http://" + srv.listenAddr()
|
||||||
|
client := newHTTP1Client()
|
||||||
|
|
||||||
|
for _, c := range concurrencyLevels {
|
||||||
|
c := c
|
||||||
|
b.Run(fmt.Sprintf("concurrency=%d", c), func(b *testing.B) {
|
||||||
|
benchConcurrent(b, client, url, c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkRPCH2C measures HTTP/2 cleartext (H2C).
|
||||||
|
func BenchmarkRPCH2C(b *testing.B) {
|
||||||
|
srv := createAndStartServerTB(b, &httpConfig{}, false /*disableHTTP2*/, &wsConfig{}, nil)
|
||||||
|
defer srv.stop()
|
||||||
|
|
||||||
|
url := "http://" + srv.listenAddr()
|
||||||
|
client := newH2CClient()
|
||||||
|
|
||||||
|
for _, c := range concurrencyLevels {
|
||||||
|
c := c
|
||||||
|
b.Run(fmt.Sprintf("concurrency=%d", c), func(b *testing.B) {
|
||||||
|
benchConcurrent(b, client, url, c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkRPCH2CvsHTTP1SameServer exercises both protocols against the same
|
||||||
|
// server instance (H2C enabled) to isolate protocol overhead from any
|
||||||
|
// server-startup variance.
|
||||||
|
func BenchmarkRPCH2CvsHTTP1SameServer(b *testing.B) {
|
||||||
|
srv := createAndStartServerTB(b, &httpConfig{}, false /*disableHTTP2*/, &wsConfig{}, nil)
|
||||||
|
defer srv.stop()
|
||||||
|
|
||||||
|
url := "http://" + srv.listenAddr()
|
||||||
|
|
||||||
|
protocols := []struct {
|
||||||
|
name string
|
||||||
|
client *http.Client
|
||||||
|
}{
|
||||||
|
{"HTTP1", newHTTP1Client()},
|
||||||
|
{"H2C", newH2CClient()},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, proto := range protocols {
|
||||||
|
proto := proto
|
||||||
|
for _, c := range concurrencyLevels {
|
||||||
|
c := c
|
||||||
|
b.Run(fmt.Sprintf("%s/concurrency=%d", proto.name, c), func(b *testing.B) {
|
||||||
|
benchConcurrent(b, proto.client, url, c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue