perf(path): replace regex with custom functions in redirectTrailingSlash#4414
perf(path): replace regex with custom functions in redirectTrailingSlash#4414appleboy merged 2 commits intogin-gonic:masterfrom
Conversation
sanitizePathChars test codepackage redirectslash
import (
"regexp"
"strings"
"testing"
)
var regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
func filterSafeCharsRegexp(s string) string {
return regSafePrefix.ReplaceAllString(s, "")
}
func filterSafeCharsMap(s string) string {
return strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '/' || r == '-' {
return r
}
return -1
}, s)
}
func filterSafeCharsCustom(s string) string {
var sb strings.Builder
sb.Grow(len(s))
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '/' || r == '-' {
sb.WriteRune(r)
}
}
return sb.String()
}
func benchmarkFilterChars(b *testing.B, prefix string) {
testCases := []struct {
name string
fn func(string) string
}{
{
name: "regexp",
fn: filterSafeCharsRegexp,
},
{
name: "map",
fn: filterSafeCharsMap,
},
{
name: "custom",
fn: filterSafeCharsCustom,
},
}
for _, tc := range testCases {
b.Run(prefix+" "+tc.name, func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
tc.fn(prefix)
}
})
}
}
func BenchmarkSafe(b *testing.B) {
benchmarkFilterChars(b, "safe-PREFIX/123")
}
func BenchmarkUnsafe(b *testing.B) {
benchmarkFilterChars(b, "?unsafe-PREFIX/123@_++__")
}sanitizePathChars benchmark output |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4414 +/- ##
==========================================
- Coverage 99.21% 98.91% -0.30%
==========================================
Files 42 44 +2
Lines 3182 2953 -229
==========================================
- Hits 3157 2921 -236
- Misses 17 19 +2
- Partials 8 13 +5
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
removeRepeatedSlash test codepackage redirectslash
import (
"regexp"
"strings"
"testing"
)
var regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
func removeRepeatedSlashRegexp(s string) string {
return regRemoveRepeatedChar.ReplaceAllString(s, "/")
}
func removeRepeatedSlashLoopReplace(s string) string {
for strings.Contains(s, "//") {
s = strings.ReplaceAll(s, "//", "/")
}
return s
}
func removeRepeatedSlashStringBuilder(s string) string {
if !strings.Contains(s, "//") {
return s
}
var sb strings.Builder
sb.Grow(len(s) - 1)
prevChar := rune(0)
for _, r := range s {
if r == '/' && prevChar == '/' {
continue
}
sb.WriteRune(r)
prevChar = r
}
return sb.String()
}
// removeRepeatedSlash removes multiple consecutive slashes from a string.
func removeRepeatedSlash(s string) string {
return removeRepeatedChar(s, '/')
}
// removeRepeatedChar removes multiple consecutive 'char's from a string.
// if s == "/a//b///c////" && char == '/', the func returns "/a/b/c/"
func removeRepeatedChar(s string, char byte) string {
// Check if there are any consecutive chars
hasRepeatedChar := false
for i := 1; i < len(s); i++ {
if s[i] == char && s[i-1] == char {
hasRepeatedChar = true
break
}
}
if !hasRepeatedChar {
return s
}
const stackBufSize = 128
// Reasonably sized buffer on stack to avoid allocations in the common case.
buf := make([]byte, 0, stackBufSize)
// Invariants:
// reading from s; r is index of next byte to process.
// writing to buf; w is index of next byte to write.
r := 0
w := 0
for n := len(s); r < n; {
if s[r] == char {
// Write the first char
bufApp(&buf, s, w, char)
w++
r++
// Skip all consecutive chars
for r < n && s[r] == char {
r++
}
} else {
// Copy non-char character
bufApp(&buf, s, w, s[r])
w++
r++
}
}
// If the original string was not modified (or only shortened at the end),
// return the respective substring of the original string.
// Otherwise, return a new string from the buffer.
if len(buf) == 0 {
return s[:w]
}
return string(buf[:w])
}
func bufApp(buf *[]byte, s string, w int, c byte) {
b := *buf
if len(b) == 0 {
if s[w] == c {
return
}
length := len(s)
if length > cap(b) {
*buf = make([]byte, length)
} else {
*buf = (*buf)[:length]
}
b = *buf
copy(b, s[:w])
}
b[w] = c
}
func benchmarkRemoveRepeatedSlash(b *testing.B, prefix string) {
testCases := []struct {
name string
fn func(string) string
}{
{
name: "regexp",
fn: removeRepeatedSlashRegexp,
},
{
name: "loopReplace",
fn: removeRepeatedSlashLoopReplace,
},
{
name: "stringBuilder",
fn: removeRepeatedSlashStringBuilder,
},
{
name: "buff",
fn: removeRepeatedSlash,
},
}
for _, tc := range testCases {
b.Run(prefix+" "+tc.name, func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
tc.fn(prefix)
}
})
}
}
func BenchmarkRemoveRepeatedSlash_MultipleSlashes(b *testing.B) {
prefix := "/somePrefix/more//text///more////"
benchmarkRemoveRepeatedSlash(b, prefix)
}
func BenchmarkRemoveRepeatedSlash_TwoSlashes(b *testing.B) {
prefix := "/somePrefix/more//"
benchmarkRemoveRepeatedSlash(b, prefix)
}
func BenchmarkRemoveNoRepeatedSlash(b *testing.B) {
prefix := "/somePrefix/more/text/"
benchmarkRemoveRepeatedSlash(b, prefix)
}
func BenchmarkRemoveNoSlash(b *testing.B) {
prefix := "/somePrefixmoretext"
benchmarkRemoveRepeatedSlash(b, prefix)
}removeRepeatedSlash benchmark output |
|
Can you provide the final benchmark report? |
For this pull request, the previous two comments actually represent the most recently updated benchmark results. |
a489adc to
d661e32
Compare
|
I just fixed the |
The following are performance tests conducted using the hey tool.server codepackage main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
gin.SetMode(gin.ReleaseMode)
router.GET("/api/users/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
})
if err := router.Run(":8080"); err != nil {
panic(err)
}
}hey command (simple X-Forwarded-Prefix)hey -n 100000 -c 100 -H "X-Forwarded-Prefix: api/v1" http://localhost:8080/api/usershey output (before optimization)hey output (after optimization)hey command (complex X-Forwarded-Prefix)hey -n 100000 -c 100 -H "X-Forwarded-Prefix: api;v1//users///test=123" http://localhost:8080/api/usershey output (before optimization)hey output (after optimization) |
d661e32 to
d5ee6e1
Compare
|
@1911860538 Thanks. |
Replace regex operations with custom functions (sanitizePathChars and removeRepeatedChar) to improve performance.