diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index e65d956b..8973f90a 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -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%" diff --git a/Makefile b/Makefile index 4f533db5..08bf38a2 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ format: ## format: 🎨 Find markdown format issues (Requires markdownlint-cli) .PHONY: markdown -format: +markdown: markdownlint-cli2 "**/*.md" "#vendor" ## lint: 🚨 Run lint checks diff --git a/helpers.go b/helpers.go index 68d8193a..804ab78f 100644 --- a/helpers.go +++ b/helpers.go @@ -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 diff --git a/helpers_test.go b/helpers_test.go index caf4983d..ac07a77a 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -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++ {