Reuse bytes when cleaning the URL paths (#2179)
* path: use stack buffer in CleanPath to avoid allocs in common case
Sync from 8222db13db
* path: sync test code from httprouter
* path: update path_test.go to the latest code
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
This commit is contained in:
parent
fd8a65b252
commit
025950afe9
29
path.go
29
path.go
@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
package gin
|
package gin
|
||||||
|
|
||||||
|
const stackBufSize = 128
|
||||||
|
|
||||||
// cleanPath is the URL version of path.Clean, it returns a canonical URL path
|
// cleanPath is the URL version of path.Clean, it returns a canonical URL path
|
||||||
// for p, eliminating . and .. elements.
|
// for p, eliminating . and .. elements.
|
||||||
//
|
//
|
||||||
@ -24,8 +26,11 @@ func cleanPath(p string) string {
|
|||||||
return "/"
|
return "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reasonably sized buffer on stack to avoid allocations in the common case.
|
||||||
|
// If a larger buffer is required, it gets allocated dynamically.
|
||||||
|
buf := make([]byte, 0, stackBufSize)
|
||||||
|
|
||||||
n := len(p)
|
n := len(p)
|
||||||
var buf []byte
|
|
||||||
|
|
||||||
// Invariants:
|
// Invariants:
|
||||||
// reading from path; r is index of next byte to process.
|
// reading from path; r is index of next byte to process.
|
||||||
@ -37,7 +42,12 @@ func cleanPath(p string) string {
|
|||||||
|
|
||||||
if p[0] != '/' {
|
if p[0] != '/' {
|
||||||
r = 0
|
r = 0
|
||||||
|
|
||||||
|
if n+1 > stackBufSize {
|
||||||
buf = make([]byte, n+1)
|
buf = make([]byte, n+1)
|
||||||
|
} else {
|
||||||
|
buf = buf[:n+1]
|
||||||
|
}
|
||||||
buf[0] = '/'
|
buf[0] = '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +79,7 @@ func cleanPath(p string) string {
|
|||||||
// can backtrack
|
// can backtrack
|
||||||
w--
|
w--
|
||||||
|
|
||||||
if buf == nil {
|
if len(buf) == 0 {
|
||||||
for w > 1 && p[w] != '/' {
|
for w > 1 && p[w] != '/' {
|
||||||
w--
|
w--
|
||||||
}
|
}
|
||||||
@ -103,7 +113,7 @@ func cleanPath(p string) string {
|
|||||||
w++
|
w++
|
||||||
}
|
}
|
||||||
|
|
||||||
if buf == nil {
|
if len(buf) == 0 {
|
||||||
return p[:w]
|
return p[:w]
|
||||||
}
|
}
|
||||||
return string(buf[:w])
|
return string(buf[:w])
|
||||||
@ -111,13 +121,20 @@ func cleanPath(p string) string {
|
|||||||
|
|
||||||
// internal helper to lazily create a buffer if necessary.
|
// internal helper to lazily create a buffer if necessary.
|
||||||
func bufApp(buf *[]byte, s string, w int, c byte) {
|
func bufApp(buf *[]byte, s string, w int, c byte) {
|
||||||
if *buf == nil {
|
b := *buf
|
||||||
|
if len(b) == 0 {
|
||||||
if s[w] == c {
|
if s[w] == c {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if l := len(s); l > cap(b) {
|
||||||
*buf = make([]byte, len(s))
|
*buf = make([]byte, len(s))
|
||||||
copy(*buf, s[:w])
|
} else {
|
||||||
|
*buf = (*buf)[:l]
|
||||||
}
|
}
|
||||||
(*buf)[w] = c
|
b = *buf
|
||||||
|
|
||||||
|
copy(b, s[:w])
|
||||||
|
}
|
||||||
|
b[w] = c
|
||||||
}
|
}
|
||||||
|
65
path_test.go
65
path_test.go
@ -6,15 +6,17 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"runtime"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cleanTests = []struct {
|
type cleanPathTest struct {
|
||||||
path, result string
|
path, result string
|
||||||
}{
|
}
|
||||||
|
|
||||||
|
var cleanTests = []cleanPathTest{
|
||||||
// Already clean
|
// Already clean
|
||||||
{"/", "/"},
|
{"/", "/"},
|
||||||
{"/abc", "/abc"},
|
{"/abc", "/abc"},
|
||||||
@ -77,13 +79,62 @@ func TestPathCleanMallocs(t *testing.T) {
|
|||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("skipping malloc count in short mode")
|
t.Skip("skipping malloc count in short mode")
|
||||||
}
|
}
|
||||||
if runtime.GOMAXPROCS(0) > 1 {
|
|
||||||
t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range cleanTests {
|
for _, test := range cleanTests {
|
||||||
allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) })
|
allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) })
|
||||||
assert.EqualValues(t, allocs, 0)
|
assert.EqualValues(t, allocs, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkPathClean(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
for _, test := range cleanTests {
|
||||||
|
cleanPath(test.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func genLongPaths() (testPaths []cleanPathTest) {
|
||||||
|
for i := 1; i <= 1234; i++ {
|
||||||
|
ss := strings.Repeat("a", i)
|
||||||
|
|
||||||
|
correctPath := "/" + ss
|
||||||
|
testPaths = append(testPaths, cleanPathTest{
|
||||||
|
path: correctPath,
|
||||||
|
result: correctPath,
|
||||||
|
}, cleanPathTest{
|
||||||
|
path: ss,
|
||||||
|
result: correctPath,
|
||||||
|
}, cleanPathTest{
|
||||||
|
path: "//" + ss,
|
||||||
|
result: correctPath,
|
||||||
|
}, cleanPathTest{
|
||||||
|
path: "/" + ss + "/b/..",
|
||||||
|
result: correctPath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathCleanLong(t *testing.T) {
|
||||||
|
cleanTests := genLongPaths()
|
||||||
|
|
||||||
|
for _, test := range cleanTests {
|
||||||
|
assert.Equal(t, test.result, cleanPath(test.path))
|
||||||
|
assert.Equal(t, test.result, cleanPath(test.result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPathCleanLong(b *testing.B) {
|
||||||
|
cleanTests := genLongPaths()
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
for _, test := range cleanTests {
|
||||||
|
cleanPath(test.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user