From 3563e56a77012b43df362d5ea7d9bb65c1e934d8 Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Wed, 8 Nov 2023 12:15:24 +0530 Subject: [PATCH] MM-54998: Optimize JSON marshalling in websocket broadcast (#25286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marshalling a json.RawMessage is not zero overhead. Instead, it compacts the raw message which starts to have an overhead at scale. https://github.com/golang/go/issues/33422 Since we have full control over the message constructed, we can simply write the byte slice into the network stream. This gives considerable performance boost. ``` goos: linux goarch: amd64 pkg: github.com/mattermost/mattermost/server/public/model cpu: Intel(R) Core(TM) i5-8265U CPU @ 1.60GHz │ old.txt │ new_2.txt │ │ sec/op │ sec/op vs base │ EncodeJSON-8 1640.5n ± 2% 289.6n ± 1% -82.35% (p=0.000 n=10) │ old.txt │ new_2.txt │ │ B/op │ B/op vs base │ EncodeJSON-8 528.0 ± 0% 503.0 ± 0% -4.73% (p=0.000 n=10) │ old.txt │ new_2.txt │ │ allocs/op │ allocs/op vs base │ EncodeJSON-8 5.000 ± 0% 4.000 ± 0% -20.00% (p=0.000 n=10) ``` P.S. No concerns over changing the model API because we are still using 0.x https://mattermost.atlassian.net/browse/MM-54998 ```release-note Improve websocket event marshalling performance ``` --- server/channels/app/platform/web_conn.go | 4 ++-- server/public/model/websocket_message.go | 5 +++-- server/public/model/websocket_message_test.go | 5 ++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/server/channels/app/platform/web_conn.go b/server/channels/app/platform/web_conn.go index ddd4eb1c96..cce589989f 100644 --- a/server/channels/app/platform/web_conn.go +++ b/server/channels/app/platform/web_conn.go @@ -463,7 +463,7 @@ func (wc *WebConn) writePump() { var err error if evtOk { evt = evt.SetSequence(wc.Sequence) - err = evt.Encode(enc) + err = evt.Encode(enc, &buf) wc.Sequence++ } else { err = enc.Encode(msg) @@ -530,7 +530,7 @@ func (wc *WebConn) writeMessage(msg *model.WebSocketEvent) error { // We don't use the encoder from the write pump because it's unwieldy to pass encoders // around, and this is only called during initialization of the webConn. var buf bytes.Buffer - err := msg.Encode(json.NewEncoder(&buf)) + err := msg.Encode(json.NewEncoder(&buf), &buf) if err != nil { mlog.Warn("Error in encoding websocket message", mlog.Err(err)) return nil diff --git a/server/public/model/websocket_message.go b/server/public/model/websocket_message.go index 6a8bf5ae31..80119ef1ca 100644 --- a/server/public/model/websocket_message.go +++ b/server/public/model/websocket_message.go @@ -294,9 +294,10 @@ func (ev *WebSocketEvent) ToJSON() ([]byte, error) { } // Encode encodes the event to the given encoder. -func (ev *WebSocketEvent) Encode(enc *json.Encoder) error { +func (ev *WebSocketEvent) Encode(enc *json.Encoder, buf io.Writer) error { if ev.precomputedJSON != nil { - return enc.Encode(json.RawMessage(ev.precomputedJSONBuf())) + _, err := buf.Write(ev.precomputedJSONBuf()) + return err } return enc.Encode(webSocketEventJSON{ diff --git a/server/public/model/websocket_message_test.go b/server/public/model/websocket_message_test.go index b6092a26eb..3fa743bee0 100644 --- a/server/public/model/websocket_message_test.go +++ b/server/public/model/websocket_message_test.go @@ -247,8 +247,11 @@ func BenchmarkEncodeJSON(b *testing.B) { ev := message.PrecomputeJSON() + var seq int64 enc := json.NewEncoder(io.Discard) for i := 0; i < b.N; i++ { - err = ev.Encode(enc) + ev = ev.SetSequence(seq) + err = ev.Encode(enc, io.Discard) + seq++ } }