From 2f1d96473570ff710145b6b695872881a752c43a Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Mon, 13 Mar 2023 22:32:12 +0530 Subject: [PATCH] Optimize precomputing of websocket event (#22465) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fmt.Sprintf was badly optimized and in cases of heavy traffic, it was in the hot path and caused a lot of allocations. We sacrifice readability for performance here. ``` 10:46:40-~/mattermost/mattermost-server/model] benchstat base.txt new.txt goos: linux goarch: amd64 pkg: github.com/mattermost/mattermost-server/v6/model cpu: Intel(R) Core(TM) i5-8265U CPU @ 1.60GHz │ base.txt │ new.txt │ │ sec/op │ sec/op vs base │ EncodeJSON-8 2.061µ ± 1% 1.588µ ± 2% -22.93% (p=0.000 n=10) │ base.txt │ new.txt │ │ B/op │ B/op vs base │ EncodeJSON-8 545.0 ± 0% 472.0 ± 0% -13.39% (p=0.000 n=10) │ base.txt │ new.txt │ │ allocs/op │ allocs/op vs base │ EncodeJSON-8 6.000 ± 0% 3.000 ± 0% -50.00% (p=0.000 n=10) ``` ```release-note Writes to websocket now take 13% less memory and also 22% faster per message. ``` --- model/websocket_message.go | 22 +++++++++++++++++----- model/websocket_message_test.go | 17 +++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/model/websocket_message.go b/model/websocket_message.go index dfffbf190e..c92943ecb0 100644 --- a/model/websocket_message.go +++ b/model/websocket_message.go @@ -5,8 +5,8 @@ package model import ( "encoding/json" - "fmt" "io" + "strconv" ) const ( @@ -281,7 +281,7 @@ func (ev *WebSocketEvent) EventType() string { func (ev *WebSocketEvent) ToJSON() ([]byte, error) { if ev.precomputedJSON != nil { - return []byte(fmt.Sprintf(`{"event": %s, "data": %s, "broadcast": %s, "seq": %d}`, ev.precomputedJSON.Event, ev.precomputedJSON.Data, ev.precomputedJSON.Broadcast, ev.GetSequence())), nil + return ev.precomputedJSONBuf(), nil } return json.Marshal(webSocketEventJSON{ ev.event, @@ -294,9 +294,7 @@ func (ev *WebSocketEvent) ToJSON() ([]byte, error) { // Encode encodes the event to the given encoder. func (ev *WebSocketEvent) Encode(enc *json.Encoder) error { if ev.precomputedJSON != nil { - return enc.Encode(json.RawMessage( - fmt.Sprintf(`{"event": %s, "data": %s, "broadcast": %s, "seq": %d}`, ev.precomputedJSON.Event, ev.precomputedJSON.Data, ev.precomputedJSON.Broadcast, ev.sequence), - )) + return enc.Encode(json.RawMessage(ev.precomputedJSONBuf())) } return enc.Encode(webSocketEventJSON{ @@ -307,6 +305,20 @@ func (ev *WebSocketEvent) Encode(enc *json.Encoder) error { }) } +// We write optimal code here sacrificing readability for +// performance. +func (ev *WebSocketEvent) precomputedJSONBuf() []byte { + return []byte(`{"event": ` + + string(ev.precomputedJSON.Event) + + `, "data": ` + + string(ev.precomputedJSON.Data) + + `, "broadcast": ` + + string(ev.precomputedJSON.Broadcast) + + `, "seq": ` + + strconv.Itoa(int(ev.sequence)) + + `}`) +} + func WebSocketEventFromJSON(data io.Reader) (*WebSocketEvent, error) { var ev WebSocketEvent var o webSocketEventJSON diff --git a/model/websocket_message_test.go b/model/websocket_message_test.go index e8f9c17ecd..73cf5dbae3 100644 --- a/model/websocket_message_test.go +++ b/model/websocket_message_test.go @@ -5,6 +5,8 @@ package model import ( "bytes" + "encoding/json" + "io" "testing" "github.com/stretchr/testify/assert" @@ -235,3 +237,18 @@ func TestWebSocketEventDeepCopy(t *testing.T) { }) require.NotEqual(t, ev.data, evCopy.data) } + +var err error + +func BenchmarkEncodeJSON(b *testing.B) { + message := NewWebSocketEvent(WebsocketEventUserAdded, "", "channelID", "", nil, "") + message.Add("user_id", "userID") + message.Add("team_id", "teamID") + + ev := message.PrecomputeJSON() + + enc := json.NewEncoder(io.Discard) + for i := 0; i < b.N; i++ { + err = ev.Encode(enc) + } +}