Skip to content

better rpcserver example, with some profiling/load testing #52

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
# IDE
.idea/
.vscode/
cert.pem
cert.pem
results.bin
29 changes: 29 additions & 0 deletions examples/rpcserver/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Example RPC Server usage.

- Implement a simple RPC server
- Handle requests with different processing times and gc-heavy operations
- Use pprof for profiling
- Use [Vegeta](https://github.com/tsenart/vegeta) for load testing

Getting started:

```bash
cd examples/rpcserver

# Run the RPC server
go run main.go

# Example requests
curl 'http://localhost:8080' --header 'Content-Type: application/json' --data '{"jsonrpc":"2.0","method":"slow","params":[],"id":2}'

# Using packaged payloads
curl 'http://localhost:8080' --header 'Content-Type: application/json' --data "@rpc-payload-fast.json"
curl 'http://localhost:8080' --header 'Content-Type: application/json' --data "@rpc-payload-slow.json"

# Load testing with Vegeta
vegeta attack -rate=10000 -duration=60s -targets=targets.txt | tee results.bin | vegeta report

# Grab pprof profiles
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
go tool pprof http://localhost:6060/debug/pprof/heap
```
86 changes: 81 additions & 5 deletions examples/rpcserver/main.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,65 @@
package main

//
// This example demonstrates how to use the rpcserver package to create a simple JSON-RPC server.
//
// It includes profiling test handlers, inspired by https://goperf.dev/02-networking/bench-and-load
//

import (
"context"
"flag"
"fmt"
"log/slog"
"math/rand/v2"
"net/http"
"os"
"time"

_ "net/http/pprof"

"github.com/flashbots/go-utils/rpcserver"
)

var listenAddr = ":8080"
var (
// Servers
listenAddr = "localhost:8080"
pprofAddr = "localhost:6060"

// Logger for the server
log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))

// Profiling utilities
fastDelay = flag.Duration("fast-delay", 0, "Fixed delay for fast handler (if any)")
slowMin = flag.Duration("slow-min", 1*time.Millisecond, "Minimum delay for slow handler")
slowMax = flag.Duration("slow-max", 300*time.Millisecond, "Maximum delay for slow handler")
gcMinAlloc = flag.Int("gc-min-alloc", 50, "Minimum number of allocations in GC heavy handler")
gcMaxAlloc = flag.Int("gc-max-alloc", 1000, "Maximum number of allocations in GC heavy handler")
longLivedData [][]byte
)

func main() {
handler, err := rpcserver.NewJSONRPCHandler(
rpcserver.Methods{
"test_foo": HandleTestFoo,
"test_foo": rpcHandlerTestFoo,
"fast": rpcHandlerFast,
"slow": rpcHandlerSlow,
"gc": rpcHandlerGCHeavy,
},
rpcserver.JSONRPCHandlerOpts{
Log: log,
ServerName: "public_server",
GetResponseContent: []byte("Hello world"),
GetResponseContent: []byte("static GET content hurray \\o/\n"),
},
)
if err != nil {
panic(err)
}

// server
// Start separate pprof server
go startPprofServer()

// API server
server := &http.Server{
Addr: listenAddr,
Handler: handler,
Expand All @@ -35,6 +70,47 @@ func main() {
}
}

func HandleTestFoo(ctx context.Context) (string, error) {
func startPprofServer() {
fmt.Println("Starting pprof server.", "pprofAddr:", pprofAddr)
if err := http.ListenAndServe(pprofAddr, nil); err != nil {
fmt.Println("Error starting pprof server:", err)
}
}

func randRange(min, max int) int {
return rand.IntN(max-min) + min
}

func rpcHandlerTestFoo(ctx context.Context) (string, error) {
return "foo", nil
}

func rpcHandlerFast(ctx context.Context) (string, error) {
if *fastDelay > 0 {
time.Sleep(*fastDelay)
}

return "fast response", nil
}

func rpcHandlerSlow(ctx context.Context) (string, error) {
delayRange := int((*slowMax - *slowMin) / time.Millisecond)
delay := time.Duration(randRange(1, delayRange)) * time.Millisecond
time.Sleep(delay)

return fmt.Sprintf("slow response with delay %d ms", delay.Milliseconds()), nil
}

func rpcHandlerGCHeavy(ctx context.Context) (string, error) {
numAllocs := randRange(*gcMinAlloc, *gcMaxAlloc)
var data [][]byte
for i := 0; i < numAllocs; i++ {
// Allocate 10KB slices. Occasionally retain a reference to simulate long-lived objects.
b := make([]byte, 1024*10)
data = append(data, b)
if i%100 == 0 { // every 100 allocations, keep the data alive
longLivedData = append(longLivedData, b)
}
}
return fmt.Sprintf("allocated %d KB\n", len(data)*10), nil
}
6 changes: 6 additions & 0 deletions examples/rpcserver/rpc-payload-fast.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"jsonrpc": "2.0",
"method": "fast",
"params": [],
"id": 83
}
6 changes: 6 additions & 0 deletions examples/rpcserver/rpc-payload-slow.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"jsonrpc": "2.0",
"method": "slow",
"params": [],
"id": 83
}
3 changes: 3 additions & 0 deletions examples/rpcserver/targets.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
POST http://localhost:8080
Content-Type: application/json
@rpc-payload-slow.json