Skip to content
Open
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
4 changes: 3 additions & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ jobs:
run: go test -race -coverprofile=coverage.txt -covermode=atomic

- name: Upload coverage to Codecov
run: bash <(curl -s https://codecov.io/bash)
uses: codecov/codecov-action@v5
with:
files: ./coverage.txt
11 changes: 10 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ lint:
@golangci-lint run

test:
@go test -v ./...
@go test -race -v ./...

bench:
@go test -bench=. -benchmem -run=^$$ ./...

fuzz:
@go test -fuzz FuzzServerDo -fuzztime 30s
@go test -fuzz FuzzIsArray -fuzztime 30s
@go test -fuzz FuzzConvertToObject -fuzztime 30s
@go test -fuzz FuzzServeHTTP -fuzztime 30s

mod:
@go mod tidy
Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ import (
"os"

"github.com/vmkteam/zenrpc/v2"
"github.com/vmkteam/zenrpc/v2/testdata"
)

type ArithService struct{ zenrpc.Service }
Expand All @@ -74,7 +73,10 @@ type Quotient struct {
Quo, Rem int
}

func (as ArithService) Divide(a, b int) (quo *Quotient, err error) {
// Divide divides two numbers.
//
//zenrpc:401 we do not serve 1
func (as *ArithService) Divide(a, b int) (quo *Quotient, err error) {
if b == 0 {
return nil, errors.New("divide by zero")
} else if b == 1 {
Expand All @@ -90,8 +92,8 @@ func (as ArithService) Divide(a, b int) (quo *Quotient, err error) {
// Pow returns x**y, the base-x exponential of y. If Exp is not set then default value is 2.
//
//zenrpc:exp=2
func (as ArithService) Pow(base float64, exp float64) float64 {
return math.Pow(base, exp)
func (as *ArithService) Pow(base float64, exp *float64) float64 {
return math.Pow(base, *exp)
}

//go:generate zenrpc
Expand All @@ -101,8 +103,8 @@ func main() {
flag.Parse()

rpc := zenrpc.NewServer(zenrpc.Options{ExposeSMD: true})
rpc.Register("arith", testdata.ArithService{})
rpc.Register("", testdata.ArithService{}) // public
rpc.Register("arith", ArithService{})
rpc.Register("", ArithService{}) // public
rpc.Use(zenrpc.Logger(log.New(os.Stderr, "", log.LstdFlags)))

http.Handle("/", rpc)
Expand Down
167 changes: 167 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package zenrpc_test

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"testing"

"github.com/vmkteam/zenrpc/v2"
"github.com/vmkteam/zenrpc/v2/testdata"
)

// --- Low-level utilities ---

func BenchmarkIsArray(b *testing.B) {
object := json.RawMessage(`{"jsonrpc":"2.0","method":"arith.pi","id":1}`)
array := json.RawMessage(`[{"jsonrpc":"2.0","method":"arith.pi","id":1}]`)

b.Run("Object", func(b *testing.B) {
b.ReportAllocs()
b.SetBytes(int64(len(object)))
for b.Loop() {
zenrpc.IsArray(object)
}
})
b.Run("Array", func(b *testing.B) {
b.ReportAllocs()
b.SetBytes(int64(len(array)))
for b.Loop() {
zenrpc.IsArray(array)
}
})
}

func BenchmarkConvertToObject(b *testing.B) {
keys := []string{"a", "b"}
params := json.RawMessage(`[3,2]`)

b.ReportAllocs()
b.SetBytes(int64(len(params)))
for b.Loop() {
_, _ = zenrpc.ConvertToObject(keys, params)
}
}

// --- Core processing via Do() ---

// benchDo is a helper that runs a Do() benchmark with throughput and response size metrics.
func benchDo(b *testing.B, srv *zenrpc.Server, req []byte) {
b.Helper()
b.ReportAllocs()
b.SetBytes(int64(len(req)))
ctx := context.Background()

for b.Loop() {
_, _ = srv.Do(ctx, req)
}
}

func BenchmarkDo_SimpleMethod(b *testing.B) {
benchDo(b, testRPC, []byte(`{"jsonrpc":"2.0","method":"arith.pi","id":1}`))
}

func BenchmarkDo_MethodWithObjectParams(b *testing.B) {
benchDo(b, testRPC, []byte(`{"jsonrpc":"2.0","method":"arith.multiply","params":{"a":3,"b":2},"id":1}`))
}

func BenchmarkDo_MethodWithArrayParams(b *testing.B) {
benchDo(b, testRPC, []byte(`{"jsonrpc":"2.0","method":"arith.multiply","params":[3,2],"id":1}`))
}

func BenchmarkDo_MethodWithDefaultParam(b *testing.B) {
benchDo(b, testRPC, []byte(`{"jsonrpc":"2.0","method":"arith.pow","params":{"base":3},"id":1}`))
}

func BenchmarkDo_MethodNotFound(b *testing.B) {
benchDo(b, testRPC, []byte(`{"jsonrpc":"2.0","method":"arith.nonexistent","id":1}`))
}

func BenchmarkDo_Notification(b *testing.B) {
benchDo(b, testRPC, []byte(`{"jsonrpc":"2.0","method":"arith.multiply","params":{"a":3,"b":2}}`))
}

func BenchmarkDo_InvalidJSON(b *testing.B) {
benchDo(b, testRPC, []byte(`{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]`))
}

// --- Batch processing via Do() ---

func BenchmarkDo_Batch(b *testing.B) {
for _, size := range []int{1, 2, 5} {
b.Run(fmt.Sprintf("Size%d", size), func(b *testing.B) {
req := buildBatchRequest(size)
b.ReportAllocs()
b.SetBytes(int64(len(req)))
ctx := context.Background()

for b.Loop() {
_, _ = testRPC.Do(ctx, req)
}
})
}
}

// --- HTTP transport via ServeHTTP ---

func BenchmarkServeHTTP(b *testing.B) {
ts := httptest.NewServer(http.HandlerFunc(testRPC.ServeHTTP))
defer ts.Close()

body := []byte(`{"jsonrpc":"2.0","method":"arith.multiply","params":{"a":3,"b":2},"id":1}`)
b.ReportAllocs()
b.SetBytes(int64(len(body)))

for b.Loop() {
resp, err := http.Post(ts.URL, "application/json", bytes.NewReader(body))
if err != nil {
b.Fatal(err)
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
}

func BenchmarkServeHTTP_Parallel(b *testing.B) {
ts := httptest.NewServer(http.HandlerFunc(testRPC.ServeHTTP))
defer ts.Close()

body := []byte(`{"jsonrpc":"2.0","method":"arith.multiply","params":{"a":3,"b":2},"id":1}`)
b.ReportAllocs()
b.SetBytes(int64(len(body)))

b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
resp, err := http.Post(ts.URL, "application/json", bytes.NewReader(body))
if err != nil {
b.Fatal(err)
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
})
}

// --- Middleware overhead ---

func BenchmarkDo_WithMiddleware(b *testing.B) {
req := []byte(`{"jsonrpc":"2.0","method":"arith.multiply","params":{"a":3,"b":2},"id":1}`)

b.Run("NoMiddleware", func(b *testing.B) {
srv := zenrpc.NewServer(zenrpc.Options{})
srv.Register("arith", &testdata.ArithService{})
benchDo(b, srv, req)
})

b.Run("WithLogger", func(b *testing.B) {
srv := zenrpc.NewServer(zenrpc.Options{})
srv.Register("arith", &testdata.ArithService{})
srv.Use(zenrpc.Logger(log.New(io.Discard, "", 0)))
benchDo(b, srv, req)
})
}
Loading
Loading