Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 71 additions & 23 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"io"
"io/fs"
"log"
"maps"
"math"
"mime/multipart"
"net"
Expand Down Expand Up @@ -54,6 +53,42 @@
// 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 {

Check failure on line 85 in context.go

View workflow job for this annotation

GitHub Actions / lint

unused-parameter: parameter 'key' seems to be unused, consider removing or renaming it as _ (revive)
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 {
Expand All @@ -70,11 +105,9 @@
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
Expand Down Expand Up @@ -105,7 +138,7 @@
c.index = -1

c.fullPath = ""
c.Keys = nil
c.Keys = nil // Reset to nil for backward compatibility

Check failure on line 141 in context.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gci)
c.Errors = c.Errors[:0]
c.Accepted = nil
c.queryCache = nil
Expand All @@ -130,10 +163,14 @@
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))
Expand Down Expand Up @@ -270,24 +307,22 @@
/************************************/

// 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.
Expand Down Expand Up @@ -467,12 +502,25 @@

// 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
}

/************************************/
Expand Down
4 changes: 3 additions & 1 deletion context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
param := LogFormatterParams{
Request: c.Request,
isTerm: isTerm,
Keys: c.Keys,
Keys: c.GetKeysAsMap(),
}

// Stop timer
Expand Down
2 changes: 1 addition & 1 deletion logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
74 changes: 59 additions & 15 deletions tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"strings"
"unicode"
"unicode/utf8"
"unsafe"

"github.com/gin-gonic/gin/internal/bytesconv"
)
Expand Down Expand Up @@ -59,11 +60,30 @@
}

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
}

Expand Down Expand Up @@ -421,13 +441,18 @@
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

Check failure on line 452 in tree.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gci)
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 {
Expand Down Expand Up @@ -460,7 +485,11 @@
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 {
Expand Down Expand Up @@ -489,8 +518,10 @@
// 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++
}

Expand All @@ -509,14 +540,17 @@
// 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,
}
}
Expand Down Expand Up @@ -562,14 +596,16 @@
// 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,
}
}
Expand All @@ -591,7 +627,11 @@
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 {
Expand Down Expand Up @@ -648,7 +688,11 @@
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 {
Expand Down
Loading