v3: Improve performance of helper functions (#3086)

* Improve performance of helper functions

* Fix issue with PR comments from forks
pull/3076/head
Juan Calderon-Perez 2024-07-23 15:32:22 -04:00 committed by Juan Calderon-Perez
parent 0f046ccf9d
commit d7d91598c5
3 changed files with 58 additions and 31 deletions

View File

@ -65,7 +65,8 @@ jobs:
# Do not save the data (This allows comparing benchmarks)
save-data-file: false
fail-on-alert: true
comment-on-alert: true
# Comment on the PR if the branch is not a fork
comment-on-alert: ${{ github.event.pull_request.head.repo.fork == false }}
github-token: ${{ secrets.GITHUB_TOKEN }}
summary-always: true
alert-threshold: "150%"

View File

@ -298,7 +298,7 @@ func paramsMatch(specParamStr headerParams, offerParams string) bool {
for specParam, specVal := range specParamStr {
foundParam := false
fasthttp.VisitHeaderParams(utils.UnsafeBytes(offerParams), func(key, value []byte) bool {
if utils.EqualFold(specParam, string(key)) {
if utils.EqualFold(specParam, utils.UnsafeString(key)) {
foundParam = true
allSpecParamsMatch = utils.EqualFold(specVal, value)
return false
@ -423,29 +423,31 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head
forEachMediaRange(header, func(accept []byte) {
order++
spec, quality := accept, 1.0
var params headerParams
if i := bytes.IndexByte(accept, ';'); i != -1 {
spec = accept[:i]
// The vast majority of requests will have only the q parameter with
// no whitespace. Check this first to see if we can skip
// the more involved parsing.
if bytes.HasPrefix(accept[i:], []byte(";q=")) && bytes.IndexByte(accept[i+3:], ';') == -1 {
if q, err := fasthttp.ParseUfloat(bytes.TrimRight(accept[i+3:], " ")); err == nil {
// Optimized quality parsing
qIndex := i + 3
if bytes.HasPrefix(accept[i:], []byte(";q=")) && bytes.IndexByte(accept[qIndex:], ';') == -1 {
if q, err := fasthttp.ParseUfloat(accept[qIndex:]); err == nil {
quality = q
}
} else {
params, _ = headerParamPool.Get().(headerParams) //nolint:errcheck // only contains headerParams
for k := range params {
delete(params, k)
}
fasthttp.VisitHeaderParams(accept[i:], func(key, value []byte) bool {
if string(key) == "q" {
if len(key) == 1 && key[0] == 'q' {
if q, err := fasthttp.ParseUfloat(value); err == nil {
quality = q
}
return false
}
params[utils.UnsafeString(utils.ToLowerBytes(key))] = value
lowerKey := utils.UnsafeString(utils.ToLowerBytes(key))
params[lowerKey] = value
return true
})
}
@ -457,13 +459,16 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head
}
}
spec = bytes.TrimRight(spec, " ")
spec = bytes.TrimSpace(spec)
// Get specificity
// Determine specificity
var specificity int
// check for wildcard this could be a mime */* or a wildcard character *
switch {
case string(spec) == "*/*" || string(spec) == "*":
case len(spec) == 1 && spec[0] == '*':
specificity = 1
case bytes.Equal(spec, []byte("*/*")):
specificity = 1
case bytes.HasSuffix(spec, []byte("/*")):
specificity = 2
@ -474,7 +479,13 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head
}
// Add to accepted types
acceptedTypes = append(acceptedTypes, acceptedType{spec: utils.UnsafeString(spec), quality: quality, specificity: specificity, order: order, params: params})
acceptedTypes = append(acceptedTypes, acceptedType{
spec: utils.UnsafeString(spec),
quality: quality,
specificity: specificity,
order: order,
params: params,
})
})
if len(acceptedTypes) > 1 {
@ -483,30 +494,24 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head
}
// Find the first offer that matches the accepted types
ret := ""
done := false
for _, acceptedType := range acceptedTypes {
if !done {
for _, offer := range offers {
if offer == "" {
continue
}
if isAccepted(acceptedType.spec, offer, acceptedType.params) {
ret = offer
done = true
break
for _, offer := range offers {
if offer == "" {
continue
}
if isAccepted(acceptedType.spec, offer, acceptedType.params) {
if acceptedType.params != nil {
headerParamPool.Put(acceptedType.params)
}
return offer
}
}
if acceptedType.params != nil {
for p := range acceptedType.params {
delete(acceptedType.params, p)
}
headerParamPool.Put(acceptedType.params)
}
}
return ret
return ""
}
// sortAcceptedTypes sorts accepted types by quality and specificity, preserving order of equal elements

View File

@ -138,6 +138,8 @@ func Benchmark_Utils_GetOffer(b *testing.B) {
},
}
b.ReportAllocs()
b.ResetTimer()
for _, tc := range testCases {
accept := []byte(tc.accept)
b.Run(tc.description, func(b *testing.B) {
@ -205,6 +207,8 @@ func Benchmark_Utils_ParamsMatch(b *testing.B) {
"appLe": []byte("orange"),
"param": []byte("foo"),
}
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
match = paramsMatch(specParams, `;param=foo; apple=orange`)
}
@ -317,6 +321,8 @@ func Benchmark_Utils_GetSplicedStrList(b *testing.B) {
destination := make([]string, 5)
result := destination
const input = `deflate, gzip,br,brotli,zstd`
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
result = getSplicedStrList(input, destination)
}
@ -359,6 +365,8 @@ func Test_Utils_SortAcceptedTypes(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Utils_SortAcceptedTypes_Sorted -benchmem -count=4
func Benchmark_Utils_SortAcceptedTypes_Sorted(b *testing.B) {
acceptedTypes := make([]acceptedType, 3)
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 1, order: 0}
acceptedTypes[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 1, order: 1}
@ -373,6 +381,8 @@ func Benchmark_Utils_SortAcceptedTypes_Sorted(b *testing.B) {
// go test -v -run=^$ -bench=Benchmark_Utils_SortAcceptedTypes_Unsorted -benchmem -count=4
func Benchmark_Utils_SortAcceptedTypes_Unsorted(b *testing.B) {
acceptedTypes := make([]acceptedType, 11)
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 3, order: 0}
acceptedTypes[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 2, order: 1}
@ -452,9 +462,10 @@ func Test_Utils_getGroupPath(t *testing.T) {
}
// go test -v -run=^$ -bench=Benchmark_Utils_ -benchmem -count=3
func Benchmark_Utils_getGroupPath(b *testing.B) {
var res string
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
_ = getGroupPath("/v1/long/path/john/doe", "/why/this/name/is/so/awesome")
_ = getGroupPath("/v1", "/")
@ -467,7 +478,8 @@ func Benchmark_Utils_getGroupPath(b *testing.B) {
func Benchmark_Utils_Unescape(b *testing.B) {
unescaped := ""
dst := make([]byte, 0)
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
source := "/cr%C3%A9er"
pathBytes := utils.UnsafeBytes(source)
@ -529,6 +541,8 @@ func Test_Utils_IsNoCache(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Utils_IsNoCache -benchmem -count=4
func Benchmark_Utils_IsNoCache(b *testing.B) {
var ok bool
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = isNoCache("public")
_ = isNoCache("no-cache")
@ -544,7 +558,10 @@ func Benchmark_Utils_IsNoCache(b *testing.B) {
func Benchmark_SlashRecognition(b *testing.B) {
search := "wtf/1234"
var result bool
b.Run("indexBytes", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
result = false
for i := 0; i < b.N; i++ {
if strings.IndexByte(search, slashDelimiter) != -1 {
@ -554,6 +571,8 @@ func Benchmark_SlashRecognition(b *testing.B) {
require.True(b, result)
})
b.Run("forEach", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
result = false
c := int32(slashDelimiter)
for i := 0; i < b.N; i++ {
@ -567,6 +586,8 @@ func Benchmark_SlashRecognition(b *testing.B) {
require.True(b, result)
})
b.Run("IndexRune", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
result = false
c := int32(slashDelimiter)
for i := 0; i < b.N; i++ {