rpc: improve performance of subscription notification encoding (#28328)
It turns out that encoding json.RawMessage is slow because package json basically parses the message again to ensure it is valid. We can avoid the slowdown by encoding the entire RPC notification once, which yields a 30% speedup.
This commit is contained in:
parent
347fecd881
commit
d6cea4832a
11
rpc/json.go
11
rpc/json.go
@ -46,6 +46,17 @@ type subscriptionResult struct {
|
|||||||
Result json.RawMessage `json:"result,omitempty"`
|
Result json.RawMessage `json:"result,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type subscriptionResultEnc struct {
|
||||||
|
ID string `json:"subscription"`
|
||||||
|
Result any `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonrpcSubscriptionNotification struct {
|
||||||
|
Version string `json:"jsonrpc"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
Params subscriptionResultEnc `json:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
// A value of this type can a JSON-RPC request, notification, successful response or
|
// A value of this type can a JSON-RPC request, notification, successful response or
|
||||||
// error response. Which one it is depends on the fields.
|
// error response. Which one it is depends on the fields.
|
||||||
type jsonrpcMessage struct {
|
type jsonrpcMessage struct {
|
||||||
|
@ -105,7 +105,7 @@ type Notifier struct {
|
|||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
sub *Subscription
|
sub *Subscription
|
||||||
buffer []json.RawMessage
|
buffer []any
|
||||||
callReturned bool
|
callReturned bool
|
||||||
activated bool
|
activated bool
|
||||||
}
|
}
|
||||||
@ -129,12 +129,7 @@ func (n *Notifier) CreateSubscription() *Subscription {
|
|||||||
|
|
||||||
// Notify sends a notification to the client with the given data as payload.
|
// Notify sends a notification to the client with the given data as payload.
|
||||||
// If an error occurs the RPC connection is closed and the error is returned.
|
// If an error occurs the RPC connection is closed and the error is returned.
|
||||||
func (n *Notifier) Notify(id ID, data interface{}) error {
|
func (n *Notifier) Notify(id ID, data any) error {
|
||||||
enc, err := json.Marshal(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
n.mu.Lock()
|
n.mu.Lock()
|
||||||
defer n.mu.Unlock()
|
defer n.mu.Unlock()
|
||||||
|
|
||||||
@ -144,9 +139,9 @@ func (n *Notifier) Notify(id ID, data interface{}) error {
|
|||||||
panic("Notify with wrong ID")
|
panic("Notify with wrong ID")
|
||||||
}
|
}
|
||||||
if n.activated {
|
if n.activated {
|
||||||
return n.send(n.sub, enc)
|
return n.send(n.sub, data)
|
||||||
}
|
}
|
||||||
n.buffer = append(n.buffer, enc)
|
n.buffer = append(n.buffer, data)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,16 +176,16 @@ func (n *Notifier) activate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notifier) send(sub *Subscription, data json.RawMessage) error {
|
func (n *Notifier) send(sub *Subscription, data any) error {
|
||||||
params, _ := json.Marshal(&subscriptionResult{ID: string(sub.ID), Result: data})
|
msg := jsonrpcSubscriptionNotification{
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
msg := &jsonrpcMessage{
|
|
||||||
Version: vsn,
|
Version: vsn,
|
||||||
Method: n.namespace + notificationMethodSuffix,
|
Method: n.namespace + notificationMethodSuffix,
|
||||||
Params: params,
|
Params: subscriptionResultEnc{
|
||||||
|
ID: string(sub.ID),
|
||||||
|
Result: data,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return n.h.conn.writeJSON(ctx, msg, false)
|
return n.h.conn.writeJSON(context.Background(), &msg, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Subscription is created by a notifier and tied to that notifier. The client can use
|
// A Subscription is created by a notifier and tied to that notifier. The client can use
|
||||||
|
@ -17,12 +17,19 @@
|
|||||||
package rpc
|
package rpc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewID(t *testing.T) {
|
func TestNewID(t *testing.T) {
|
||||||
@ -218,3 +225,56 @@ func readAndValidateMessage(in *json.Decoder) (*subConfirmation, *subscriptionRe
|
|||||||
return nil, nil, fmt.Errorf("unrecognized message: %v", msg)
|
return nil, nil, fmt.Errorf("unrecognized message: %v", msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockConn struct {
|
||||||
|
enc *json.Encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSON writes a message to the connection.
|
||||||
|
func (c *mockConn) writeJSON(ctx context.Context, msg interface{}, isError bool) error {
|
||||||
|
return c.enc.Encode(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closed returns a channel which is closed when the connection is closed.
|
||||||
|
func (c *mockConn) closed() <-chan interface{} { return nil }
|
||||||
|
|
||||||
|
// RemoteAddr returns the peer address of the connection.
|
||||||
|
func (c *mockConn) remoteAddr() string { return "" }
|
||||||
|
|
||||||
|
// BenchmarkNotify benchmarks the performance of notifying a subscription.
|
||||||
|
func BenchmarkNotify(b *testing.B) {
|
||||||
|
id := ID("test")
|
||||||
|
notifier := &Notifier{
|
||||||
|
h: &handler{conn: &mockConn{json.NewEncoder(io.Discard)}},
|
||||||
|
sub: &Subscription{ID: id},
|
||||||
|
activated: true,
|
||||||
|
}
|
||||||
|
msg := &types.Header{
|
||||||
|
ParentHash: common.HexToHash("0x01"),
|
||||||
|
Number: big.NewInt(100),
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
notifier.Notify(id, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotify(t *testing.T) {
|
||||||
|
out := new(bytes.Buffer)
|
||||||
|
id := ID("test")
|
||||||
|
notifier := &Notifier{
|
||||||
|
h: &handler{conn: &mockConn{json.NewEncoder(out)}},
|
||||||
|
sub: &Subscription{ID: id},
|
||||||
|
activated: true,
|
||||||
|
}
|
||||||
|
msg := &types.Header{
|
||||||
|
ParentHash: common.HexToHash("0x01"),
|
||||||
|
Number: big.NewInt(100),
|
||||||
|
}
|
||||||
|
notifier.Notify(id, msg)
|
||||||
|
have := strings.TrimSpace(out.String())
|
||||||
|
want := `{"jsonrpc":"2.0","method":"_subscription","params":{"subscription":"test","result":{"parentHash":"0x0000000000000000000000000000000000000000000000000000000000000001","sha3Uncles":"0x0000000000000000000000000000000000000000000000000000000000000000","miner":"0x0000000000000000000000000000000000000000","stateRoot":"0x0000000000000000000000000000000000000000000000000000000000000000","transactionsRoot":"0x0000000000000000000000000000000000000000000000000000000000000000","receiptsRoot":"0x0000000000000000000000000000000000000000000000000000000000000000","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","difficulty":null,"number":"0x64","gasLimit":"0x0","gasUsed":"0x0","timestamp":"0x0","extraData":"0x","mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0000000000000000","baseFeePerGas":null,"withdrawalsRoot":null,"blobGasUsed":null,"excessBlobGas":null,"parentBeaconBlockRoot":null,"hash":"0xe5fb877dde471b45b9742bb4bb4b3d74a761e2fb7cb849a3d2b687eed90fb604"}}}`
|
||||||
|
if have != want {
|
||||||
|
t.Errorf("have:\n%v\nwant:\n%v\n", have, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user