diff options
Diffstat (limited to 'benchmark')
| -rw-r--r-- | benchmark/go.mod | 8 | ||||
| -rw-r--r-- | benchmark/go.sum | 4 | ||||
| -rw-r--r-- | benchmark/pathrouter_test.go | 117 |
3 files changed, 129 insertions, 0 deletions
diff --git a/benchmark/go.mod b/benchmark/go.mod new file mode 100644 index 0000000..a8125f8 --- /dev/null +++ b/benchmark/go.mod @@ -0,0 +1,8 @@ +module go.xyrillian.de/gg/benchmark + +go 1.26 + +require ( + github.com/gorilla/mux v1.8.1 + go.xyrillian.de/gg v1.10.1 +) diff --git a/benchmark/go.sum b/benchmark/go.sum new file mode 100644 index 0000000..e5138fd --- /dev/null +++ b/benchmark/go.sum @@ -0,0 +1,4 @@ +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +go.xyrillian.de/gg v1.10.1 h1:V6oSU+tl25vaRQaMy6Y3jl/0kNoY/a25x4WIk5zQFAw= +go.xyrillian.de/gg v1.10.1/go.mod h1:DoO4fQSWIrBRlNlCjVyrYM0kAEBt/Jg2GkMH+cGRZ0k= diff --git a/benchmark/pathrouter_test.go b/benchmark/pathrouter_test.go new file mode 100644 index 0000000..361cc2f --- /dev/null +++ b/benchmark/pathrouter_test.go @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +package benchmark_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "go.xyrillian.de/gg/assert" + pr "go.xyrillian.de/gg/pathrouter" +) + +func BenchmarkRouterWithDistributionAPI(b *testing.B) { + // This benchmark uses the OCI Distribution API as a moderately complex + // real-world example of an HTTP API with parametrized endpoint paths. + + testWith := func(h http.Handler) func(b *testing.B) { + return func(b *testing.B) { + for b.Loop() { + req := httptest.NewRequest(http.MethodGet, "/v2/foo/bar/manifests/latest", http.NoBody) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + assert.Equal(b, rec.Code, http.StatusForbidden) + assert.Equal(b, rec.Body.String(), "GetOrHeadManifest is forbidden\n") + if b.Failed() { + b.FailNow() + } + } + } + } + + b.Run("platform=gorilla-mux", testWith(buildMuxRouterForDistributionAPI())) + b.Run("platform=gg-pathrouter", testWith(buildPathrouterForDistributionAPI())) +} + +func buildMuxRouterForDistributionAPI() http.Handler { + handle := func(endpointName string) http.HandlerFunc { + msg := endpointName + " is forbidden" + return func(w http.ResponseWriter, r *http.Request) { + http.Error(w, msg, http.StatusForbidden) + } + } + r := mux.NewRouter() + r.Methods("GET").Path("/v2/").HandlerFunc(handle("GetToplevel")) + r.Methods("GET").Path("/v2/_catalog").HandlerFunc(handle("GetCatalog")) + r.Methods("DELETE").Path("/v2/{repository:.+}/blobs/{digest}").HandlerFunc(handle("DeleteBlob")) + r.Methods("GET", "HEAD").Path("/v2/{repository:.+}/blobs/{digest}").HandlerFunc(handle("GetOrHeadBlob")) + r.Methods("POST").Path("/v2/{repository:.+}/blobs/uploads/").HandlerFunc(handle("StartBlobUpload")) + r.Methods("DELETE").Path("/v2/{repository:.+}/blobs/uploads/{uuid}").HandlerFunc(handle("DeleteBlobUpload")) + r.Methods("GET").Path("/v2/{repository:.+}/blobs/uploads/{uuid}").HandlerFunc(handle("GetBlobUpload")) + r.Methods("PATCH").Path("/v2/{repository:.+}/blobs/uploads/{uuid}").HandlerFunc(handle("ContinueBlobUpload")) + r.Methods("PUT").Path("/v2/{repository:.+}/blobs/uploads/{uuid}").HandlerFunc(handle("FinishBlobUpload")) + r.Methods("DELETE").Path("/v2/{repository:.+}/manifests/{reference}").HandlerFunc(handle("DeleteManifest")) + r.Methods("GET", "HEAD").Path("/v2/{repository:.+}/manifests/{reference}").HandlerFunc(handle("GetOrHeadManifest")) + r.Methods("PUT").Path("/v2/{repository:.+}/manifests/{reference}").HandlerFunc(handle("PutManifest")) + r.Methods("GET").Path("/v2/{repository:.+}/referrers/{reference}").HandlerFunc(handle("GetReferrers")) + r.Methods("GET").Path("/v2/{repository:.+}/tags/list").HandlerFunc(handle("ListTags")) + return r +} + +func buildPathrouterForDistributionAPI() http.Handler { + handle := func(endpointName string) pr.HandlerFunc { + msg := endpointName + " is forbidden" + return func(w http.ResponseWriter, r *http.Request, vars map[string]string) { + http.Error(w, msg, http.StatusForbidden) + } + } + + return pr.Choice( + pr.Element("v2", pr.Choice( + pr.Element("/", pr.Handlers(pr.ByMethod{ + http.MethodGet: handle("GetToplevel"), + http.MethodHead: nil, + })), + pr.Element("_catalog", pr.Handlers(pr.ByMethod{ + http.MethodGet: handle("GetCatalog"), + http.MethodHead: nil, + })), + pr.CatchAllVariable("repository", pr.Choice( + pr.Element("blobs", pr.Choice( + pr.Variable("digest", pr.Handlers(pr.ByMethod{ + http.MethodDelete: handle("DeleteBlob"), + http.MethodGet: handle("GetOrHeadBlob"), + })), + pr.Element("uploads", pr.Choice( + pr.Element("/", pr.Handlers(pr.ByMethod{ + http.MethodPost: handle("StartBlobUpload"), + })), + pr.Variable("uuid", pr.Handlers(pr.ByMethod{ + http.MethodDelete: handle("DeleteBlobUpload"), + http.MethodGet: handle("GetBlobUpload"), + http.MethodHead: nil, + http.MethodPatch: handle("ContinueBlobUpload"), + http.MethodPut: handle("FinishBlobUpload"), + })), + )), + )), + pr.Element("manifests", pr.Variable("reference", pr.Handlers(pr.ByMethod{ + http.MethodDelete: handle("DeleteManifest"), + http.MethodGet: handle("GetOrHeadManifest"), + http.MethodPut: handle("PutManifest"), + }))), + pr.Element("referrers", pr.Variable("reference", pr.Handlers(pr.ByMethod{ + http.MethodGet: handle("GetReferrers"), + http.MethodHead: nil, + }))), + pr.Element("tags", pr.Element("list", pr.Handlers(pr.ByMethod{ + http.MethodGet: handle("ListTags"), + http.MethodHead: nil, + }))), + )), + )), + ) +} |
