-
Notifications
You must be signed in to change notification settings - Fork 8.5k
perf: replace regex with custom functions in redirectTrailingSlash #4414
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
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% 99.04% -0.18%
==========================================
Files 42 44 +2
Lines 3182 2928 -254
==========================================
- Hits 3157 2900 -257
Misses 17 17
- Partials 8 11 +3
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) |
Replace regex operations with custom functions (sanitizePathChars and removeRepeatedChar) to improve performance.