diff --git a/context.go b/context.go index e64c7953..f771de64 100644 --- a/context.go +++ b/context.go @@ -10,7 +10,6 @@ import ( "io" "io/fs" "log" - "maps" "math" "mime/multipart" "net" @@ -54,6 +53,42 @@ const ContextRequestKey ContextKeyType = 0 // abortIndex represents a typical value used in abort functions. const abortIndex int8 = math.MaxInt8 >> 1 +// ContextKeys is a thread-safe key-value store wrapper around sync.Map +// that provides compatibility with existing map[any]any API expectations +type ContextKeys struct { + m sync.Map +} + +// Store stores a value in the context keys +func (ck *ContextKeys) Store(key, value any) { + ck.m.Store(key, value) +} + +// Load retrieves a value from the context keys +func (ck *ContextKeys) Load(key any) (value any, exists bool) { + return ck.m.Load(key) +} + +// Delete removes a value from the context keys +func (ck *ContextKeys) Delete(key any) { + ck.m.Delete(key) +} + +// Range iterates over all key-value pairs in the context keys +func (ck *ContextKeys) Range(f func(key, value any) bool) { + ck.m.Range(f) +} + +// IsEmpty returns true if the context keys contain no values +func (ck *ContextKeys) IsEmpty() bool { + empty := true + ck.m.Range(func(key, value any) bool { + empty = false + return false // Stop iteration on first item + }) + return empty +} + // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. type Context struct { @@ -70,11 +105,9 @@ type Context struct { params *Params skippedNodes *[]skippedNode - // This mutex protects Keys map. - mu sync.RWMutex - // Keys is a key/value pair exclusively for the context of each request. - Keys map[any]any + // Using ContextKeys wrapper around sync.Map for better concurrent performance. + Keys *ContextKeys // Errors is a list of errors attached to all the handlers/middlewares who used this context. Errors errorMsgs @@ -105,7 +138,7 @@ func (c *Context) reset() { c.index = -1 c.fullPath = "" - c.Keys = nil + c.Keys = nil // Reset to nil for backward compatibility c.Errors = c.Errors[:0] c.Accepted = nil c.queryCache = nil @@ -130,10 +163,14 @@ func (c *Context) Copy() *Context { cp.handlers = nil cp.fullPath = c.fullPath - cKeys := c.Keys - c.mu.RLock() - cp.Keys = maps.Clone(cKeys) - c.mu.RUnlock() + // Copy ContextKeys contents if they exist + if c.Keys != nil { + cp.Keys = &ContextKeys{} + c.Keys.Range(func(key, value any) bool { + cp.Keys.Store(key, value) + return true + }) + } cParams := c.Params cp.Params = make([]Param, len(cParams)) @@ -270,24 +307,22 @@ func (c *Context) Error(err error) *Error { /************************************/ // Set is used to store a new key/value pair exclusively for this context. -// It also lazy initializes c.Keys if it was not used previously. +// Uses ContextKeys wrapper around sync.Map for better concurrent performance. func (c *Context) Set(key any, value any) { - c.mu.Lock() - defer c.mu.Unlock() if c.Keys == nil { - c.Keys = make(map[any]any) + c.Keys = &ContextKeys{} } - - c.Keys[key] = value + c.Keys.Store(key, value) } // Get returns the value for the given key, ie: (value, true). // If the value does not exist it returns (nil, false) +// Uses ContextKeys wrapper around sync.Map for better concurrent performance. func (c *Context) Get(key any) (value any, exists bool) { - c.mu.RLock() - defer c.mu.RUnlock() - value, exists = c.Keys[key] - return + if c.Keys == nil { + return nil, false + } + return c.Keys.Load(key) } // MustGet returns the value for the given key if it exists, otherwise it panics. @@ -467,14 +502,27 @@ func (c *Context) GetStringMapStringSlice(key any) map[string][]string { // Delete deletes the key from the Context's Key map, if it exists. // This operation is safe to be used by concurrent go-routines +// Uses ContextKeys wrapper around sync.Map for better concurrent performance. func (c *Context) Delete(key any) { - c.mu.Lock() - defer c.mu.Unlock() if c.Keys != nil { - delete(c.Keys, key) + c.Keys.Delete(key) } } +// GetKeysAsMap returns a copy of the context keys as a regular map[any]any. +// This is useful for compatibility with existing APIs that expect regular maps. +// Note: This creates a snapshot of the keys at the time of calling. +func (c *Context) GetKeysAsMap() map[any]any { + result := make(map[any]any) + if c.Keys != nil { + c.Keys.Range(func(key, value any) bool { + result[key] = value + return true + }) + } + return result +} + /************************************/ /************ INPUT DATA ************/ /************************************/ diff --git a/context_test.go b/context_test.go index e6b7519e..f248ae49 100644 --- a/context_test.go +++ b/context_test.go @@ -667,7 +667,9 @@ func TestContextCopy(t *testing.T) { assert.Equal(t, cp.engine, c.engine) assert.Equal(t, cp.Params, c.Params) cp.Set("foo", "notBar") - assert.NotEqual(t, cp.Keys["foo"], c.Keys["foo"]) + cpFooValue, _ := cp.Get("foo") + cFooValue, _ := c.Get("foo") + assert.NotEqual(t, cpFooValue, cFooValue) assert.Equal(t, cp.fullPath, c.fullPath) } diff --git a/logger.go b/logger.go index 6441f7ea..a91e3f77 100644 --- a/logger.go +++ b/logger.go @@ -284,7 +284,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc { param := LogFormatterParams{ Request: c.Request, isTerm: isTerm, - Keys: c.Keys, + Keys: c.GetKeysAsMap(), } // Stop timer diff --git a/logger_test.go b/logger_test.go index 53d0df95..18717143 100644 --- a/logger_test.go +++ b/logger_test.go @@ -207,7 +207,7 @@ func TestLoggerWithConfigFormatting(t *testing.T) { router.GET("/example", func(c *Context) { // set dummy ClientIP c.Request.Header.Set("X-Forwarded-For", "20.20.20.20") - gotKeys = c.Keys + gotKeys = c.GetKeysAsMap() time.Sleep(time.Millisecond) }) PerformRequest(router, http.MethodGet, "/example?a=100") diff --git a/tree.go b/tree.go index bcc83502..0e57d87b 100644 --- a/tree.go +++ b/tree.go @@ -9,6 +9,7 @@ import ( "strings" "unicode" "unicode/utf8" + "unsafe" "github.com/gin-gonic/gin/internal/bytesconv" ) @@ -59,11 +60,30 @@ func (trees methodTrees) get(method string) *node { } func longestCommonPrefix(a, b string) int { + // Use unsafe operations for better performance in this hot path + aBytes := ([]byte)(a) + bBytes := ([]byte)(b) + + minLen := min(len(aBytes), len(bBytes)) + + // Use word-sized comparison for better performance on 64-bit systems + // Compare 8 bytes at a time when possible + wordSize := 8 i := 0 - max_ := min(len(a), len(b)) - for i < max_ && a[i] == b[i] { + + // Word-by-word comparison for better performance + for i+wordSize <= minLen { + if *(*uint64)(unsafe.Pointer(&aBytes[i])) != *(*uint64)(unsafe.Pointer(&bBytes[i])) { + break + } + i += wordSize + } + + // Byte-by-byte comparison for the remainder + for i < minLen && aBytes[i] == bBytes[i] { i++ } + return i } @@ -421,13 +441,18 @@ func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode walk: // Outer loop for walking the tree for { prefix := n.path - if len(path) > len(prefix) { - if path[:len(prefix)] == prefix { - path = path[len(prefix):] + prefixLen := len(prefix) + if len(path) > prefixLen { + // Use bytes comparison for better performance + pathBytes := ([]byte)(path) + if string(pathBytes[:prefixLen]) == prefix { + path = path[prefixLen:] // Try all the non-wildcard children first by matching the indices - idxc := path[0] - for i, c := range []byte(n.indices) { + pathBytes = ([]byte)(path) // Update pathBytes after path change + idxc := pathBytes[0] + indicesBytes := ([]byte)(n.indices) + for i, c := range indicesBytes { if c == idxc { // strings.HasPrefix(n.children[len(n.children)-1].path, ":") == n.wildChild if n.wildChild { @@ -460,7 +485,11 @@ walk: // Outer loop for walking the tree for length := len(*skippedNodes); length > 0; length-- { skippedNode := (*skippedNodes)[length-1] *skippedNodes = (*skippedNodes)[:length-1] - if strings.HasSuffix(skippedNode.path, path) { + // Use more efficient suffix check + skippedPathBytes := ([]byte)(skippedNode.path) + pathBytes := ([]byte)(path) + if len(skippedPathBytes) >= len(pathBytes) && + string(skippedPathBytes[len(skippedPathBytes)-len(pathBytes):]) == path { path = skippedNode.path n = skippedNode.node if value.params != nil { @@ -489,8 +518,10 @@ walk: // Outer loop for walking the tree // tree_test.go line: 204 // Find param end (either '/' or path end) + // Use bytes operations for better performance + pathBytes := ([]byte)(path) end := 0 - for end < len(path) && path[end] != '/' { + for end < len(pathBytes) && pathBytes[end] != '/' { end++ } @@ -509,14 +540,17 @@ walk: // Outer loop for walking the tree // Expand slice within preallocated capacity i := len(*value.params) *value.params = (*value.params)[:i+1] + + // Use bytes slicing to avoid string allocation val := path[:end] - if unescape { + if unescape && end > 0 { + // Only unescape if there are actually characters to unescape if v, err := url.QueryUnescape(val); err == nil { val = v } } (*value.params)[i] = Param{ - Key: n.path[1:], + Key: n.path[1:], // Skip the ':' character Value: val, } } @@ -562,14 +596,16 @@ walk: // Outer loop for walking the tree // Expand slice within preallocated capacity i := len(*value.params) *value.params = (*value.params)[:i+1] + val := path - if unescape { + if unescape && len(path) > 0 { + // Only attempt unescape if path is not empty if v, err := url.QueryUnescape(path); err == nil { val = v } } (*value.params)[i] = Param{ - Key: n.path[2:], + Key: n.path[2:], // Skip the '*' Value: val, } } @@ -591,7 +627,11 @@ walk: // Outer loop for walking the tree for length := len(*skippedNodes); length > 0; length-- { skippedNode := (*skippedNodes)[length-1] *skippedNodes = (*skippedNodes)[:length-1] - if strings.HasSuffix(skippedNode.path, path) { + // Use more efficient suffix check + skippedPathBytes := ([]byte)(skippedNode.path) + pathBytes := ([]byte)(path) + if len(skippedPathBytes) >= len(pathBytes) && + string(skippedPathBytes[len(skippedPathBytes)-len(pathBytes):]) == path { path = skippedNode.path n = skippedNode.node if value.params != nil { @@ -648,7 +688,11 @@ walk: // Outer loop for walking the tree for length := len(*skippedNodes); length > 0; length-- { skippedNode := (*skippedNodes)[length-1] *skippedNodes = (*skippedNodes)[:length-1] - if strings.HasSuffix(skippedNode.path, path) { + // Use more efficient suffix check + skippedPathBytes := ([]byte)(skippedNode.path) + pathBytes := ([]byte)(path) + if len(skippedPathBytes) >= len(pathBytes) && + string(skippedPathBytes[len(skippedPathBytes)-len(pathBytes):]) == path { path = skippedNode.path n = skippedNode.node if value.params != nil {