v3: Improve performance of helper functions (#3086)

* Improve performance of helper functions

* Fix issue with PR comments from forks
pull/3087/head
Juan Calderon-Perez 2024-07-23 15:32:22 -04:00 committed by GitHub
parent 8c3f81e2b7
commit 486304d050
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 59 additions and 32 deletions

View File

@ -65,7 +65,8 @@ jobs:
# Do not save the data (This allows comparing benchmarks) # Do not save the data (This allows comparing benchmarks)
save-data-file: false save-data-file: false
fail-on-alert: true 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 }} github-token: ${{ secrets.GITHUB_TOKEN }}
summary-always: true summary-always: true
alert-threshold: "150%" alert-threshold: "150%"

View File

@ -29,7 +29,7 @@ format:
## format: 🎨 Find markdown format issues (Requires markdownlint-cli) ## format: 🎨 Find markdown format issues (Requires markdownlint-cli)
.PHONY: markdown .PHONY: markdown
format: markdown:
markdownlint-cli2 "**/*.md" "#vendor" markdownlint-cli2 "**/*.md" "#vendor"
## lint: 🚨 Run lint checks ## lint: 🚨 Run lint checks

View File

@ -298,7 +298,7 @@ func paramsMatch(specParamStr headerParams, offerParams string) bool {
for specParam, specVal := range specParamStr { for specParam, specVal := range specParamStr {
foundParam := false foundParam := false
fasthttp.VisitHeaderParams(utils.UnsafeBytes(offerParams), func(key, value []byte) bool { 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 foundParam = true
allSpecParamsMatch = utils.EqualFold(specVal, value) allSpecParamsMatch = utils.EqualFold(specVal, value)
return false return false
@ -423,29 +423,31 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head
forEachMediaRange(header, func(accept []byte) { forEachMediaRange(header, func(accept []byte) {
order++ order++
spec, quality := accept, 1.0 spec, quality := accept, 1.0
var params headerParams var params headerParams
if i := bytes.IndexByte(accept, ';'); i != -1 { if i := bytes.IndexByte(accept, ';'); i != -1 {
spec = accept[:i] spec = accept[:i]
// The vast majority of requests will have only the q parameter with // Optimized quality parsing
// no whitespace. Check this first to see if we can skip qIndex := i + 3
// the more involved parsing. if bytes.HasPrefix(accept[i:], []byte(";q=")) && bytes.IndexByte(accept[qIndex:], ';') == -1 {
if bytes.HasPrefix(accept[i:], []byte(";q=")) && bytes.IndexByte(accept[i+3:], ';') == -1 { if q, err := fasthttp.ParseUfloat(accept[qIndex:]); err == nil {
if q, err := fasthttp.ParseUfloat(bytes.TrimRight(accept[i+3:], " ")); err == nil {
quality = q quality = q
} }
} else { } else {
params, _ = headerParamPool.Get().(headerParams) //nolint:errcheck // only contains headerParams 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 { 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 { if q, err := fasthttp.ParseUfloat(value); err == nil {
quality = q quality = q
} }
return false return false
} }
params[utils.UnsafeString(utils.ToLowerBytes(key))] = value lowerKey := utils.UnsafeString(utils.ToLowerBytes(key))
params[lowerKey] = value
return true 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 var specificity int
// check for wildcard this could be a mime */* or a wildcard character * // check for wildcard this could be a mime */* or a wildcard character *
switch { switch {
case string(spec) == "*/*" || string(spec) == "*": case len(spec) == 1 && spec[0] == '*':
specificity = 1
case bytes.Equal(spec, []byte("*/*")):
specificity = 1 specificity = 1
case bytes.HasSuffix(spec, []byte("/*")): case bytes.HasSuffix(spec, []byte("/*")):
specificity = 2 specificity = 2
@ -474,7 +479,13 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head
} }
// Add to accepted types // 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 { 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 // Find the first offer that matches the accepted types
ret := ""
done := false
for _, acceptedType := range acceptedTypes { for _, acceptedType := range acceptedTypes {
if !done {
for _, offer := range offers { for _, offer := range offers {
if offer == "" { if offer == "" {
continue continue
} }
if isAccepted(acceptedType.spec, offer, acceptedType.params) { if isAccepted(acceptedType.spec, offer, acceptedType.params) {
ret = offer if acceptedType.params != nil {
done = true headerParamPool.Put(acceptedType.params)
break
} }
return offer
} }
} }
if acceptedType.params != nil { if acceptedType.params != nil {
for p := range acceptedType.params {
delete(acceptedType.params, p)
}
headerParamPool.Put(acceptedType.params) headerParamPool.Put(acceptedType.params)
} }
} }
return ret return ""
} }
// sortAcceptedTypes sorts accepted types by quality and specificity, preserving order of equal elements // 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 { for _, tc := range testCases {
accept := []byte(tc.accept) accept := []byte(tc.accept)
b.Run(tc.description, func(b *testing.B) { b.Run(tc.description, func(b *testing.B) {
@ -205,6 +207,8 @@ func Benchmark_Utils_ParamsMatch(b *testing.B) {
"appLe": []byte("orange"), "appLe": []byte("orange"),
"param": []byte("foo"), "param": []byte("foo"),
} }
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
match = paramsMatch(specParams, `;param=foo; apple=orange`) match = paramsMatch(specParams, `;param=foo; apple=orange`)
} }
@ -317,6 +321,8 @@ func Benchmark_Utils_GetSplicedStrList(b *testing.B) {
destination := make([]string, 5) destination := make([]string, 5)
result := destination result := destination
const input = `deflate, gzip,br,brotli,zstd` const input = `deflate, gzip,br,brotli,zstd`
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
result = getSplicedStrList(input, destination) 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 // go test -v -run=^$ -bench=Benchmark_Utils_SortAcceptedTypes_Sorted -benchmem -count=4
func Benchmark_Utils_SortAcceptedTypes_Sorted(b *testing.B) { func Benchmark_Utils_SortAcceptedTypes_Sorted(b *testing.B) {
acceptedTypes := make([]acceptedType, 3) acceptedTypes := make([]acceptedType, 3)
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 1, order: 0} acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 1, order: 0}
acceptedTypes[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 1, order: 1} 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 // go test -v -run=^$ -bench=Benchmark_Utils_SortAcceptedTypes_Unsorted -benchmem -count=4
func Benchmark_Utils_SortAcceptedTypes_Unsorted(b *testing.B) { func Benchmark_Utils_SortAcceptedTypes_Unsorted(b *testing.B) {
acceptedTypes := make([]acceptedType, 11) acceptedTypes := make([]acceptedType, 11)
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 3, order: 0} acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 3, order: 0}
acceptedTypes[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 2, order: 1} 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 // go test -v -run=^$ -bench=Benchmark_Utils_ -benchmem -count=3
func Benchmark_Utils_getGroupPath(b *testing.B) { func Benchmark_Utils_getGroupPath(b *testing.B) {
var res string var res string
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
_ = getGroupPath("/v1/long/path/john/doe", "/why/this/name/is/so/awesome") _ = getGroupPath("/v1/long/path/john/doe", "/why/this/name/is/so/awesome")
_ = getGroupPath("/v1", "/") _ = getGroupPath("/v1", "/")
@ -467,7 +478,8 @@ func Benchmark_Utils_getGroupPath(b *testing.B) {
func Benchmark_Utils_Unescape(b *testing.B) { func Benchmark_Utils_Unescape(b *testing.B) {
unescaped := "" unescaped := ""
dst := make([]byte, 0) dst := make([]byte, 0)
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
source := "/cr%C3%A9er" source := "/cr%C3%A9er"
pathBytes := utils.UnsafeBytes(source) 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 // go test -v -run=^$ -bench=Benchmark_Utils_IsNoCache -benchmem -count=4
func Benchmark_Utils_IsNoCache(b *testing.B) { func Benchmark_Utils_IsNoCache(b *testing.B) {
var ok bool var ok bool
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = isNoCache("public") _ = isNoCache("public")
_ = isNoCache("no-cache") _ = isNoCache("no-cache")
@ -544,7 +558,10 @@ func Benchmark_Utils_IsNoCache(b *testing.B) {
func Benchmark_SlashRecognition(b *testing.B) { func Benchmark_SlashRecognition(b *testing.B) {
search := "wtf/1234" search := "wtf/1234"
var result bool var result bool
b.Run("indexBytes", func(b *testing.B) { b.Run("indexBytes", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
result = false result = false
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
if strings.IndexByte(search, slashDelimiter) != -1 { if strings.IndexByte(search, slashDelimiter) != -1 {
@ -554,6 +571,8 @@ func Benchmark_SlashRecognition(b *testing.B) {
require.True(b, result) require.True(b, result)
}) })
b.Run("forEach", func(b *testing.B) { b.Run("forEach", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
result = false result = false
c := int32(slashDelimiter) c := int32(slashDelimiter)
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -567,6 +586,8 @@ func Benchmark_SlashRecognition(b *testing.B) {
require.True(b, result) require.True(b, result)
}) })
b.Run("IndexRune", func(b *testing.B) { b.Run("IndexRune", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
result = false result = false
c := int32(slashDelimiter) c := int32(slashDelimiter)
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {