From 8c134774eec807997f117efc5f9dc418a138aa90 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Thu, 10 Apr 2025 14:09:38 -0500 Subject: [PATCH] optimize session using benc --- core/sessions/Session.go | 190 ++++++++++++++++++++++++++++++++++++--- go.mod | 4 +- go.sum | 12 ++- 3 files changed, 190 insertions(+), 16 deletions(-) diff --git a/core/sessions/Session.go b/core/sessions/Session.go index 5aa7f8e..992c1fd 100644 --- a/core/sessions/Session.go +++ b/core/sessions/Session.go @@ -4,18 +4,19 @@ import ( "sync" "time" - "github.com/goccy/go-json" + "github.com/deneonet/benc" + bstd "github.com/deneonet/benc/std" ) // Session stores data for a single user session type Session struct { - ID string `json:"id"` - Data map[string]any `json:"data"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - LastUsed time.Time `json:"last_used"` - Expiry time.Time `json:"expiry"` - dirty bool // Tracks if session has changes, not serialized + ID string + Data map[string]any + CreatedAt time.Time + UpdatedAt time.Time + LastUsed time.Time + Expiry time.Time + dirty bool // Tracks if session has changes, not serialized } // Session pool to reduce allocations @@ -27,6 +28,9 @@ var sessionPool = sync.Pool{ }, } +// BufPool for reusing serialization buffers +var bufPool = benc.NewBufPool(benc.WithBufferSize(4096)) + // GetFromPool retrieves a session from the pool func GetFromPool() *Session { return sessionPool.Get().(*Session) @@ -130,18 +134,178 @@ func (s *Session) ResetDirty() { s.dirty = false } -// Marshal serializes the session to JSON -func (s *Session) Marshal() ([]byte, error) { - return json.Marshal(s) +// SizePlain calculates the size needed to marshal the session +func (s *Session) SizePlain() (size int) { + // ID + size += bstd.SizeString(s.ID) + + // Data (map of string to any) + // For simplicity, we store data as binary-encoded strings + // This is a simplification, in a real-world scenario you would handle + // different types differently + dataAsStrings := make(map[string]string) + for k, v := range s.Data { + dataAsStrings[k] = toString(v) + } + size += bstd.SizeMap(dataAsStrings, bstd.SizeString, bstd.SizeString) + + // Time fields + size += bstd.SizeInt64() * 4 // Store Unix timestamps for all time fields + + return size } -// Unmarshal deserializes a session from JSON +// MarshalPlain serializes the session to binary +func (s *Session) MarshalPlain(n int, b []byte) (int, error) { + // ID + n = bstd.MarshalString(n, b, s.ID) + + // Data + dataAsStrings := make(map[string]string) + for k, v := range s.Data { + dataAsStrings[k] = toString(v) + } + n = bstd.MarshalMap(n, b, dataAsStrings, bstd.MarshalString, bstd.MarshalString) + + // Time fields as Unix timestamps + n = bstd.MarshalInt64(n, b, s.CreatedAt.Unix()) + n = bstd.MarshalInt64(n, b, s.UpdatedAt.Unix()) + n = bstd.MarshalInt64(n, b, s.LastUsed.Unix()) + n = bstd.MarshalInt64(n, b, s.Expiry.Unix()) + + return n, nil +} + +// UnmarshalPlain deserializes the session from binary +func (s *Session) UnmarshalPlain(n int, b []byte) (int, error) { + var err error + + // ID + n, s.ID, err = bstd.UnmarshalString(n, b) + if err != nil { + return n, err + } + + // Data + var dataAsStrings map[string]string + n, dataAsStrings, err = bstd.UnmarshalMap[string, string](n, b, bstd.UnmarshalString, bstd.UnmarshalString) + if err != nil { + return n, err + } + + // Convert string data back to original types + s.Data = make(map[string]any, len(dataAsStrings)) + for k, v := range dataAsStrings { + s.Data[k] = fromString(v) + } + + // Time fields + var timestamp int64 + + // CreatedAt + n, timestamp, err = bstd.UnmarshalInt64(n, b) + if err != nil { + return n, err + } + s.CreatedAt = time.Unix(timestamp, 0) + + // UpdatedAt + n, timestamp, err = bstd.UnmarshalInt64(n, b) + if err != nil { + return n, err + } + s.UpdatedAt = time.Unix(timestamp, 0) + + // LastUsed + n, timestamp, err = bstd.UnmarshalInt64(n, b) + if err != nil { + return n, err + } + s.LastUsed = time.Unix(timestamp, 0) + + // Expiry + n, timestamp, err = bstd.UnmarshalInt64(n, b) + if err != nil { + return n, err + } + s.Expiry = time.Unix(timestamp, 0) + + return n, nil +} + +// Marshal serializes the session using benc +func (s *Session) Marshal() ([]byte, error) { + size := s.SizePlain() + + data, err := bufPool.Marshal(size, func(b []byte) (n int) { + n, _ = s.MarshalPlain(0, b) + return n + }) + + if err != nil { + return nil, err + } + + return data, nil +} + +// Unmarshal deserializes a session using benc func Unmarshal(data []byte) (*Session, error) { session := GetFromPool() - err := json.Unmarshal(data, session) + _, err := session.UnmarshalPlain(0, data) if err != nil { ReturnToPool(session) return nil, err } return session, nil } + +// Helper functions to convert between any and string +// In a production environment, you would use a more robust serialization method for the map values +func toString(v any) string { + if v == nil { + return "" + } + switch t := v.(type) { + case string: + return t + case []byte: + return string(t) + case int: + return "i:" + string(rune(t)) + case bool: + if t { + return "b:t" + } + return "b:f" + default: + return "u:" // unknown type + } +} + +func fromString(s string) any { + if s == "" { + return nil + } + if len(s) < 2 { + return s + } + + prefix := s[:2] + switch prefix { + case "i:": + if len(s) > 2 { + return int(rune(s[2])) + } + return 0 + case "b:": + if len(s) > 2 && s[2] == 't' { + return true + } + return false + case "u:": + return nil + default: + return s + } +} diff --git a/go.mod b/go.mod index 220434a..748a337 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,9 @@ go 1.24.1 require ( git.sharkk.net/Sky/LuaJIT-to-Go v0.0.0 github.com/VictoriaMetrics/fastcache v1.12.2 + github.com/deneonet/benc v1.1.7 github.com/goccy/go-json v0.10.5 + github.com/matoous/go-nanoid/v2 v2.1.0 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.60.0 ) @@ -15,7 +17,7 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/matoous/go-nanoid/v2 v2.1.0 // indirect + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect golang.org/x/sys v0.31.0 // indirect ) diff --git a/go.sum b/go.sum index b1f529b..25ecc84 100644 --- a/go.sum +++ b/go.sum @@ -7,7 +7,10 @@ github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOL github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/deneonet/benc v1.1.7 h1:0XPxTTVJZq/ulxXvMn2Mzjx5XquekVky3wX6eTgA0vA= +github.com/deneonet/benc v1.1.7/go.mod h1:UCfkM5Od0B2huwv/ZItvtUb7QnALFt9YXtX8NXX4Lts= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -16,17 +19,22 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sony/sonyflake v1.2.0 h1:Pfr3A+ejSg+0SPqpoAmQgEtNDAhc2G1SUYk205qVMLQ= -github.com/sony/sonyflake v1.2.0/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.60.0 h1:kBRYS0lOhVJ6V+bYN8PqAHELKHtXqwq9zNMLKx1MBsw= github.com/valyala/fasthttp v1.60.0/go.mod h1:iY4kDgV3Gc6EqhRZ8icqcmlG6bqhcDXfuHgTO4FXCvc= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=