Skip to content

Commit

Permalink
It's premature optimisation time!
Browse files Browse the repository at this point in the history
* Use easyjson instead of std json, this reduces CPU/RAM usage
* Use prepared messages when broadcasting
* Enable compression on websockets
* Prevent unmarshalling data into maps and then into objects
  • Loading branch information
Bios-Marcel committed Aug 20, 2023
1 parent ed4f925 commit bf1a88f
Show file tree
Hide file tree
Showing 12 changed files with 2,469 additions and 458 deletions.
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ require (
github.com/go-chi/chi/v5 v5.0.10
github.com/gofrs/uuid v4.4.0+incompatible
github.com/gorilla/websocket v1.5.0
github.com/mitchellh/mapstructure v1.5.0
github.com/mailru/easyjson v0.7.7
github.com/subosito/gotenv v1.6.0
golang.org/x/text v0.12.0
)

require (
github.com/josharian/intern v1.0.0 // indirect
github.com/stretchr/testify v1.8.0 // indirect
)
18 changes: 15 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc=
github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dustinkirkland/golang-petname v0.0.0-20230626224747-e794b9370d49 h1:6SNWi8VxQeCSwmLuTbEvJd7xvPmdS//zvMBWweZLgck=
Expand All @@ -17,12 +19,22 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
6 changes: 4 additions & 2 deletions internal/api/v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ func postLobby(writer http.ResponseWriter, request *http.Request) {
return
}

lobby.WriteJSON = WriteJSON
lobby.WriteObject = WriteObject
lobby.WritePreparedMessage = WritePreparedMessage
player.SetLastKnownAddress(GetIPAddressFromRequest(request))

SetUsersessionCookie(writer, player)
Expand Down Expand Up @@ -180,9 +181,10 @@ func postPlayer(writer http.ResponseWriter, request *http.Request) {

// SetUsersessionCookie takes the players usersession and sets it as a cookie.
func SetUsersessionCookie(w http.ResponseWriter, player *game.Player) {
session := player.GetUserSession().String()
http.SetCookie(w, &http.Cookie{
Name: "usersession",
Value: player.GetUserSession().String(),
Value: session,
Path: "/",
SameSite: http.SameSiteStrictMode,
})
Expand Down
42 changes: 29 additions & 13 deletions internal/api/ws.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package api

import (
"encoding/json"
"errors"
"fmt"
"log"
Expand All @@ -12,6 +11,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/gofrs/uuid"
"github.com/gorilla/websocket"
"github.com/mailru/easyjson"

"github.com/scribble-rs/scribble.rs/internal/game"
"github.com/scribble-rs/scribble.rs/internal/state"
Expand All @@ -21,9 +21,10 @@ var (
ErrPlayerNotConnected = errors.New("player not connected")

upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(_ *http.Request) bool { return true },
EnableCompression: true,
}
)

Expand Down Expand Up @@ -86,6 +87,8 @@ func wsListen(lobby *game.Lobby, player *game.Player, socket *websocket.Conn) {
}
}()

var event game.EventTypeOnly

for {
messageType, data, err := socket.ReadMessage()
if err != nil {
Expand All @@ -109,11 +112,9 @@ func wsListen(lobby *game.Lobby, player *game.Player, socket *websocket.Conn) {
}

if messageType == websocket.TextMessage {
received := &game.Event{}

if err := json.Unmarshal(data, received); err != nil {
if err := easyjson.Unmarshal(data, &event); err != nil {
log.Printf("Error unmarshalling message: %s\n", err)
err := WriteJSON(player, game.Event{
err := WriteObject(player, game.Event{
Type: game.EventTypeSystemMessage,
Data: fmt.Sprintf("error parsing message, please report this issue via Github: %s!", err),
})
Expand All @@ -123,16 +124,31 @@ func wsListen(lobby *game.Lobby, player *game.Player, socket *websocket.Conn) {
continue
}

if err := lobby.HandleEvent(received, player); err != nil {
if err := lobby.HandleEvent(event.Type, data, player); err != nil {
log.Printf("Error handling event: %s\n", err)
}
}
}
}

// WriteJSON marshals the given input into a JSON string and sends it to the
// player using the currently established websocket connection.
func WriteJSON(player *game.Player, object any) error {
func WriteObject(player *game.Player, object easyjson.Marshaler) error {
player.GetWebsocketMutex().Lock()
defer player.GetWebsocketMutex().Unlock()

socket := player.GetWebsocket()
if socket == nil || !player.Connected {
return ErrPlayerNotConnected
}

bytes, err := easyjson.Marshal(object)
if err != nil {
return fmt.Errorf("error marshalling payload: %w", err)
}

return socket.WriteMessage(websocket.TextMessage, bytes)
}

func WritePreparedMessage(player *game.Player, message *websocket.PreparedMessage) error {
player.GetWebsocketMutex().Lock()
defer player.GetWebsocketMutex().Unlock()

Expand All @@ -141,5 +157,5 @@ func WriteJSON(player *game.Player, object any) error {
return ErrPlayerNotConnected
}

return socket.WriteJSON(object)
return socket.WritePreparedMessage(message)
}
3 changes: 2 additions & 1 deletion internal/frontend/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ func ssrCreateLobby(writer http.ResponseWriter, request *http.Request) {
return
}

lobby.WriteJSON = api.WriteJSON
lobby.WriteObject = api.WriteObject
lobby.WritePreparedMessage = api.WritePreparedMessage
player.SetLastKnownAddress(api.GetIPAddressFromRequest(request))

api.SetUsersessionCookie(writer, player)
Expand Down
118 changes: 5 additions & 113 deletions internal/game/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
discordemojimap "github.com/Bios-Marcel/discordemojimap/v2"
"github.com/gofrs/uuid"
"github.com/gorilla/websocket"
easyjson "github.com/mailru/easyjson"
"golang.org/x/text/cases"
)

Expand Down Expand Up @@ -61,9 +62,9 @@ type Lobby struct {
// wordChoice represents the current choice of words present to the drawer.
wordChoice []string
Wordpack string
// RoundEndTime represents the time at which the current round will end.
// roundEndTime represents the time at which the current round will end.
// This is a UTC unix-timestamp in milliseconds.
RoundEndTime int64
roundEndTime int64

timeLeftTicker *time.Ticker
scoreEarnedByGuessers int
Expand Down Expand Up @@ -91,117 +92,14 @@ type Lobby struct {

mutex *sync.Mutex

WriteJSON func(player *Player, object any) error
}

// EditableLobbySettings represents all lobby settings that are editable by
// the lobby owner after the lobby has already been opened.
type EditableLobbySettings struct {
// MaxPlayers defines the maximum amount of players in a single lobby.
MaxPlayers int `json:"maxPlayers"`
// CustomWords are additional words that will be used in addition to the
// predefined words.
// Public defines whether the lobby is being broadcast to clients asking
// for available lobbies.
Public bool `json:"public"`
// EnableVotekick decides whether players are allowed to kick eachother
// by casting majority votes.
EnableVotekick bool `json:"enableVotekick"`
// CustomWordsChance determines the chance of each word being a custom
// word on the next word prompt. This needs to be an integer between
// 0 and 100. The value represents a percentage.
CustomWordsChance int `json:"customWordsChance"`
// ClientsPerIPLimit helps preventing griefing by reducing each player
// to one tab per IP address.
ClientsPerIPLimit int `json:"clientsPerIpLimit"`
// DrawingTime is the amount of seconds that each player has available to
// finish their drawing.
DrawingTime int `json:"drawingTime"`
// Rounds defines how many iterations a lobby does before the game ends.
// One iteration means every participant does one drawing.
Rounds int `json:"rounds"`
}

type State string

const (
// Unstarted means the lobby has been opened but never started.
Unstarted State = "unstarted"
// Ongoing means the lobby has already been started.
Ongoing State = "ongoing"
// GameOver means that the lobby had been start, but the max round limit
// has already been reached.
GameOver State = "gameOver"
)

// WordHint describes a character of the word that is to be guessed, whether
// the character should be shown and whether it should be underlined on the
// UI.
type WordHint struct {
Character rune `json:"character"`
Underline bool `json:"underline"`
}

// RGBColor represents a 24-bit color consisting of red, green and blue.
type RGBColor struct {
R uint8 `json:"r"`
G uint8 `json:"g"`
B uint8 `json:"b"`
}

// Line is the struct that a client sends when drawing.
type Line struct {
FromX float32 `json:"fromX"`
FromY float32 `json:"fromY"`
ToX float32 `json:"toX"`
ToY float32 `json:"toY"`
Color RGBColor `json:"color"`
LineWidth float32 `json:"lineWidth"`
}

// Fill represents the usage of the fill bucket.
type Fill struct {
X float32 `json:"x"`
Y float32 `json:"y"`
Color RGBColor `json:"color"`
WriteObject func(*Player, easyjson.Marshaler) error
WritePreparedMessage func(*Player, *websocket.PreparedMessage) error
}

// MaxPlayerNameLength defines how long a string can be at max when used
// as the playername.
const MaxPlayerNameLength int = 30

// Player represents a participant in a Lobby.
type Player struct {
// userSession uniquely identifies the player.
userSession uuid.UUID
ws *websocket.Conn
socketMutex *sync.Mutex
lastKnownAddress string
// disconnectTime is used to kick a player in case the lobby doesn't have
// space for new players. The player with the oldest disconnect.Time will
// get kicked.
disconnectTime *time.Time

votedForKick map[uuid.UUID]bool

// ID uniquely identified the Player.
ID uuid.UUID `json:"id"`
// Name is the players displayed name
Name string `json:"name"`
// Score is the points that the player got in the current Lobby.
Score int `json:"score"`
// Connected defines whether the players websocket connection is currently
// established. This has previously been in state but has been moved out
// in order to avoid losing the state on refreshing the page.
// While checking the websocket against nil would be enough, we still need
// this field for sending it via the APIs.
Connected bool `json:"connected"`
// Rank is the current ranking of the player in his Lobby
LastScore int `json:"lastScore"`
Rank int `json:"rank"`
State PlayerState `json:"state"`
}

// GetLastKnownAddress returns the last known IP-Address used for an HTTP request.
func (player *Player) GetLastKnownAddress() string {
return player.lastKnownAddress
Expand Down Expand Up @@ -307,12 +205,6 @@ func SanitizeName(name string) string {
return generatePlayerName()
}

// Event contains an eventtype and optionally any data.
type Event struct {
Data any `json:"data"`
Type string `json:"type"`
}

// GetConnectedPlayerCount returns the amount of player that have currently
// established a socket connection.
func (lobby *Lobby) GetConnectedPlayerCount() int {
Expand Down
38 changes: 0 additions & 38 deletions internal/game/events.go

This file was deleted.

Loading

0 comments on commit bf1a88f

Please sign in to comment.