diff --git a/benchmark_test.go b/benchmark_test.go new file mode 100644 index 0000000000000000000000000000000000000000..24402b3218e856a116846f7a90420c6ba4f2ca7c --- /dev/null +++ b/benchmark_test.go @@ -0,0 +1,101 @@ +package jrpc + +import ( + "testing" + + "golang.org/x/sync/errgroup" +) + +func BenchmarkClientHTTPEcho(b *testing.B) { + server := newTestServer() + defer server.Stop() + client, hs := httpTestClient(server, "http", nil) + defer hs.Close() + defer client.Close() + + // Launch concurrent requests. + wantBack := map[string]any{ + "one": map[string]any{"two": "three"}, + "e": map[string]any{"two": "three"}, + "oe": map[string]any{"two": "three"}, + "on": map[string]any{"two": "three"}, + } + + b.StartTimer() + for n := 0; n < b.N; n++ { + eg := &errgroup.Group{} + for i := 0; i < 1000; i++ { + eg.Go(func() error { + return client.Call(nil, "test_echoAny", []any{1, 2, 3, 4, 56, 6, wantBack, wantBack, wantBack}) + }) + } + eg.Wait() + } +} +func BenchmarkClientHTTPEchoEmpty(b *testing.B) { + server := newTestServer() + defer server.Stop() + client, hs := httpTestClient(server, "http", nil) + defer hs.Close() + defer client.Close() + + // Launch concurrent requests. + + b.StartTimer() + for n := 0; n < b.N; n++ { + eg := &errgroup.Group{} + for i := 0; i < 1000; i++ { + eg.Go(func() error { + return client.Call(nil, "test_echoAny", 0) + }) + } + eg.Wait() + } +} +func BenchmarkClientWebsocketEcho(b *testing.B) { + server := newTestServer() + defer server.Stop() + client, hs := httpTestClient(server, "ws", nil) + defer hs.Close() + defer client.Close() + + // Launch concurrent requests. + wantBack := map[string]any{ + "one": map[string]any{"two": "three"}, + "e": map[string]any{"two": "three"}, + "oe": map[string]any{"two": "three"}, + "on": map[string]any{"two": "three"}, + } + + b.StartTimer() + for n := 0; n < b.N; n++ { + eg := &errgroup.Group{} + for i := 0; i < 1000; i++ { + eg.Go(func() error { + return client.Call(nil, "test_echoAny", []any{1, 2, 3, 4, 56, 6, wantBack, wantBack, wantBack}) + }) + } + eg.Wait() + } + +} +func BenchmarkClientWebsocketEchoEmpty(b *testing.B) { + server := newTestServer() + defer server.Stop() + client, hs := httpTestClient(server, "ws", nil) + defer hs.Close() + defer client.Close() + + // Launch concurrent requests. + b.StartTimer() + + for n := 0; n < b.N; n++ { + eg := &errgroup.Group{} + for i := 0; i < 1000; i++ { + eg.Go(func() error { + return client.Call(nil, "test_echoAny", 0) + }) + } + eg.Wait() + } +} diff --git a/client_test.go b/client_test.go index fef6ea33a4cbaa5d31f5365bf0f33169520d145f..72b77a3144a5ba051fd39735480d0d7e887fd5f0 100644 --- a/client_test.go +++ b/client_test.go @@ -18,7 +18,6 @@ package jrpc import ( "context" - "encoding/json" "fmt" "math/rand" "net" @@ -335,86 +334,6 @@ func TestClientHTTP(t *testing.T) { } } -func BenchmarkClientHTTPEcho(b *testing.B) { - server := newTestServer() - defer server.Stop() - client, hs := httpTestClient(server, "http", nil) - defer hs.Close() - defer client.Close() - - // Launch concurrent requests. - b.StartTimer() - wantBack := map[string]any{ - "one": map[string]any{"two": "three"}, - "e": map[string]any{"two": "three"}, - "oe": map[string]any{"two": "three"}, - "on": map[string]any{"two": "three"}, - } - var res json.RawMessage - for n := 0; n < b.N; n++ { - for i := 0; i < 100; i++ { - err := client.Call(&res, "test_echoAny", []any{1, 2, 3, 4, 56, 6, wantBack, wantBack, wantBack}) - if err != nil { - panic(err) - } - } - } -} -func BenchmarkClientHTTPEchoEmpty(b *testing.B) { - server := newTestServer() - defer server.Stop() - client, hs := httpTestClient(server, "http", nil) - defer hs.Close() - defer client.Close() - - // Launch concurrent requests. - b.StartTimer() - var res json.RawMessage - for n := 0; n < b.N; n++ { - for i := 0; i < 100; i++ { - client.Call(&res, "test_echoAny", 0) - } - } -} -func BenchmarkClientWebsocketEcho(b *testing.B) { - server := newTestServer() - defer server.Stop() - client, hs := httpTestClient(server, "ws", nil) - defer hs.Close() - defer client.Close() - - // Launch concurrent requests. - b.StartTimer() - wantBack := map[string]any{ - "one": map[string]any{"two": "three"}, - "e": map[string]any{"two": "three"}, - "oe": map[string]any{"two": "three"}, - "on": map[string]any{"two": "three"}, - } - var res json.RawMessage - for n := 0; n < b.N; n++ { - for i := 0; i < 100; i++ { - client.Call(&res, "test_echoAny", []any{1, 2, 3, 4, 56, 6, wantBack, wantBack, wantBack}) - } - } -} -func BenchmarkClientWebsocketEchoEmpty(b *testing.B) { - server := newTestServer() - defer server.Stop() - client, hs := httpTestClient(server, "ws", nil) - defer hs.Close() - defer client.Close() - - // Launch concurrent requests. - b.StartTimer() - var res json.RawMessage - for n := 0; n < b.N; n++ { - for i := 0; i < 100; i++ { - client.Call(&res, "test_echoAny", 0) - } - } -} - func TestClientReconnect(t *testing.T) { startServer := func(addr string) (*Server, net.Listener) { srv := newTestServer() diff --git a/cpu.out b/cpu.out new file mode 100644 index 0000000000000000000000000000000000000000..fd87d5985d3c404a85daca3b05ccc2b94d2adf17 Binary files /dev/null and b/cpu.out differ diff --git a/cpu2.out b/cpu2.out new file mode 100644 index 0000000000000000000000000000000000000000..e606b70e54f2fd338f4ca392a4629439c695ce51 Binary files /dev/null and b/cpu2.out differ diff --git a/cpu3.out b/cpu3.out new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/cpu4.out b/cpu4.out new file mode 100644 index 0000000000000000000000000000000000000000..8c14d6f6913e1c6a288a292c6d4d0f97adf03a7d Binary files /dev/null and b/cpu4.out differ diff --git a/cpu5.out b/cpu5.out new file mode 100644 index 0000000000000000000000000000000000000000..a46bedd57be082aca18a96f7c105f21c81ffeab7 Binary files /dev/null and b/cpu5.out differ diff --git a/cpu6.out b/cpu6.out new file mode 100644 index 0000000000000000000000000000000000000000..6a7fa3d09f6abf96080572a5ddcda9d5d9029a49 Binary files /dev/null and b/cpu6.out differ diff --git a/cpu7.out b/cpu7.out new file mode 100644 index 0000000000000000000000000000000000000000..524d9517da062cd77f2de5f4dd5c1468ae22af20 Binary files /dev/null and b/cpu7.out differ diff --git a/go.mod b/go.mod index fc2519f64628b86edb02f43433a6e6df78ac7bac..f568902241c5e6f12dc03b85cc80b317a629d45e 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/imdario/mergo v0.3.13 github.com/json-iterator/go v1.1.12 github.com/test-go/testify v1.1.4 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce nhooyr.io/websocket v1.8.7 sigs.k8s.io/yaml v1.3.0 diff --git a/jrpc.test b/jrpc.test new file mode 100755 index 0000000000000000000000000000000000000000..249e5671427aa002d28282089bd23c4eac54662b Binary files /dev/null and b/jrpc.test differ diff --git a/json.go b/json.go index a894887a618025488585ea1dd89e923cfdc217eb..ef07035bcbcbf08e92c1d7aaaa2820dfbc05ae28 100644 --- a/json.go +++ b/json.go @@ -24,11 +24,13 @@ import ( "fmt" "io" "reflect" + "strconv" "strings" "sync" "time" "gfx.cafe/open/jrpc/wsjson" + jsoniter "github.com/json-iterator/go" ) var jzon = wsjson.JZON @@ -143,7 +145,7 @@ type JsonError = jsonError func (err *jsonError) Error() string { if err.Message == "" { - return fmt.Sprintf("json-rpc error %d", err.Code) + return "json-rpc error " + strconv.Itoa(err.Code) } return err.Message } @@ -298,18 +300,13 @@ func isBatch(raw json.RawMessage) bool { // given types. It returns the parsed values or an error when the args could not be // parsed. Missing optional arguments are returned as reflect.Zero values. func parsePositionalArguments(rawArgs json.RawMessage, types []reflect.Type) ([]reflect.Value, error) { - dec := json.NewDecoder(bytes.NewReader(rawArgs)) var args []reflect.Value - tok, err := dec.Token() switch { - case err == io.EOF || tok == nil && err == nil: - // "params" is optional and may be empty. Also allow "params":null even though it's - // not in the spec because our own client used to send it. - case err != nil: - return nil, err - case tok == json.Delim('['): + case len(rawArgs) == 0: + case rawArgs[0] == '[': // Read argument array. - if args, err = parseArgumentArray(dec, types); err != nil { + var err error + if args, err = parseArgumentArray(rawArgs, types); err != nil { return nil, err } default: @@ -325,14 +322,17 @@ func parsePositionalArguments(rawArgs json.RawMessage, types []reflect.Type) ([] return args, nil } -func parseArgumentArray(dec *json.Decoder, types []reflect.Type) ([]reflect.Value, error) { +func parseArgumentArray(p json.RawMessage, types []reflect.Type) ([]reflect.Value, error) { + dec := jsoniter.NewIterator(jzon) + dec.ResetBytes(p) args := make([]reflect.Value, 0, len(types)) - for i := 0; dec.More(); i++ { + for i := 0; dec.ReadArray(); i++ { if i >= len(types) { return args, fmt.Errorf("too many arguments, want at most %d", len(types)) } argval := reflect.New(types[i]) - if err := dec.Decode(argval.Interface()); err != nil { + dec.ReadVal(argval.Interface()) + if err := dec.Error; err != nil { return args, fmt.Errorf("invalid argument %d: %v", i, err) } if argval.IsNil() && types[i].Kind() != reflect.Ptr { @@ -340,9 +340,7 @@ func parseArgumentArray(dec *json.Decoder, types []reflect.Type) ([]reflect.Valu } args = append(args, argval.Elem()) } - // Read end of args array. - _, err := dec.Token() - return args, err + return args, nil } // parseSubscriptionName extracts the subscription name from an encoded argument array.