aboutsummaryrefslogtreecommitdiff
path: root/benchmark
diff options
context:
space:
mode:
Diffstat (limited to 'benchmark')
-rw-r--r--benchmark/go.mod8
-rw-r--r--benchmark/go.sum4
-rw-r--r--benchmark/pathrouter_test.go117
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,
+ }))),
+ )),
+ )),
+ )
+}