From 0f92a2afef8ff7a31068e82954ccc0cc839e6039 Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:54:00 +0000 Subject: [PATCH] =?UTF-8?q?Add=20chi:=20lightweight=20idiomatic=20Go=20rou?= =?UTF-8?q?ter=20(~22k=20=E2=AD=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frameworks/chi/Dockerfile | 11 ++ frameworks/chi/README.md | 22 +++ frameworks/chi/go.mod | 21 +++ frameworks/chi/go.sum | 55 ++++++++ frameworks/chi/main.go | 284 ++++++++++++++++++++++++++++++++++++++ frameworks/chi/meta.json | 19 +++ 6 files changed, 412 insertions(+) create mode 100644 frameworks/chi/Dockerfile create mode 100644 frameworks/chi/README.md create mode 100644 frameworks/chi/go.mod create mode 100644 frameworks/chi/go.sum create mode 100644 frameworks/chi/main.go create mode 100644 frameworks/chi/meta.json diff --git a/frameworks/chi/Dockerfile b/frameworks/chi/Dockerfile new file mode 100644 index 00000000..97a2a916 --- /dev/null +++ b/frameworks/chi/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.24-alpine AS build +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY main.go ./ +RUN CGO_ENABLED=0 go build -o server main.go + +FROM alpine:3.19 +COPY --from=build /app/server /server +EXPOSE 8080 +CMD ["/server"] diff --git a/frameworks/chi/README.md b/frameworks/chi/README.md new file mode 100644 index 00000000..a7707845 --- /dev/null +++ b/frameworks/chi/README.md @@ -0,0 +1,22 @@ +# Chi + +[Chi](https://github.com/go-chi/chi) is a lightweight, idiomatic and composable router for building Go HTTP services. It's built on top of Go's standard `net/http` package with zero external dependencies for routing. + +## Key Features + +- **100% compatible with net/http** — Chi router implements `http.Handler`, so it works with any `net/http` middleware or server +- **Lightweight** — no framework magic, just a router with middleware support +- **Context-based** — uses `context.Context` for request-scoped values +- **Middleware stack** — composable middleware with `Use()`, inline middleware, and sub-routers + +## Why It's Interesting for HttpArena + +Chi sits between raw `net/http` and full frameworks like Gin/Echo/Fiber. It adds routing and middleware composition but stays close to the standard library. The key comparison: + +- **go-fasthttp** — raw fasthttp, custom HTTP implementation +- **Fiber** — framework on top of fasthttp +- **Gin** — framework with httprouter, custom Context +- **Echo** — framework with custom radix tree router +- **Chi** — thin router layer on stdlib `net/http` + +Chi's approach means handlers are just `http.HandlerFunc` — no custom context, no framework lock-in. The performance question: how much does staying close to stdlib cost vs. custom abstractions? diff --git a/frameworks/chi/go.mod b/frameworks/chi/go.mod new file mode 100644 index 00000000..4b3c5128 --- /dev/null +++ b/frameworks/chi/go.mod @@ -0,0 +1,21 @@ +module chi-server + +go 1.24.13 + +require ( + github.com/go-chi/chi/v5 v5.2.5 + modernc.org/sqlite v1.46.1 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/sys v0.37.0 // indirect + modernc.org/libc v1.67.6 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) diff --git a/frameworks/chi/go.sum b/frameworks/chi/go.sum new file mode 100644 index 00000000..5570fac9 --- /dev/null +++ b/frameworks/chi/go.sum @@ -0,0 +1,55 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= +modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= +modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= +modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/frameworks/chi/main.go b/frameworks/chi/main.go new file mode 100644 index 00000000..d04a7bae --- /dev/null +++ b/frameworks/chi/main.go @@ -0,0 +1,284 @@ +package main + +import ( + "compress/flate" + "compress/gzip" + "database/sql" + "encoding/json" + "fmt" + "io" + "math" + "mime" + "net/http" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + + "github.com/go-chi/chi/v5" + _ "modernc.org/sqlite" +) + +type Rating struct { + Score float64 `json:"score"` + Count int `json:"count"` +} + +type DatasetItem struct { + ID int `json:"id"` + Name string `json:"name"` + Category string `json:"category"` + Price float64 `json:"price"` + Quantity int `json:"quantity"` + Active bool `json:"active"` + Tags []string `json:"tags"` + Rating Rating `json:"rating"` +} + +type ProcessedItem struct { + ID int `json:"id"` + Name string `json:"name"` + Category string `json:"category"` + Price float64 `json:"price"` + Quantity int `json:"quantity"` + Active bool `json:"active"` + Tags []string `json:"tags"` + Rating Rating `json:"rating"` + Total float64 `json:"total"` +} + +type ProcessResponse struct { + Items []ProcessedItem `json:"items"` + Count int `json:"count"` +} + +var dataset []DatasetItem +var jsonLargeResponse []byte +var db *sql.DB + +type StaticFile struct { + Data []byte + ContentType string +} + +var staticFiles map[string]StaticFile + +func loadDataset() { + path := os.Getenv("DATASET_PATH") + if path == "" { + path = "/data/dataset.json" + } + data, err := os.ReadFile(path) + if err != nil { + return + } + json.Unmarshal(data, &dataset) +} + +func loadDatasetLarge() { + data, err := os.ReadFile("/data/dataset-large.json") + if err != nil { + return + } + var raw []DatasetItem + if json.Unmarshal(data, &raw) != nil { + return + } + items := make([]ProcessedItem, len(raw)) + for i, d := range raw { + items[i] = ProcessedItem{ + ID: d.ID, Name: d.Name, Category: d.Category, + Price: d.Price, Quantity: d.Quantity, Active: d.Active, + Tags: d.Tags, Rating: d.Rating, + Total: math.Round(d.Price*float64(d.Quantity)*100) / 100, + } + } + jsonLargeResponse, _ = json.Marshal(ProcessResponse{Items: items, Count: len(items)}) +} + +func loadDB() { + d, err := sql.Open("sqlite", "file:/data/benchmark.db?mode=ro&immutable=1") + if err != nil { + return + } + d.SetMaxOpenConns(runtime.NumCPU()) + db = d +} + +func loadStaticFiles() { + staticFiles = make(map[string]StaticFile) + entries, err := os.ReadDir("/data/static") + if err != nil { + return + } + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + data, err := os.ReadFile(filepath.Join("/data/static", name)) + if err != nil { + continue + } + ct := mime.TypeByExtension(filepath.Ext(name)) + if ct == "" { + ct = "application/octet-stream" + } + staticFiles[name] = StaticFile{Data: data, ContentType: ct} + } +} + +func parseQuerySum(query string) int64 { + var sum int64 + for _, pair := range strings.Split(query, "&") { + parts := strings.SplitN(pair, "=", 2) + if len(parts) == 2 { + if n, err := strconv.ParseInt(parts[1], 10, 64); err == nil { + sum += n + } + } + } + return sum +} + +func main() { + loadDataset() + loadDatasetLarge() + loadDB() + loadStaticFiles() + + r := chi.NewRouter() + + r.Get("/pipeline", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Server", "chi") + w.Write([]byte("ok")) + }) + + baseline11 := func(w http.ResponseWriter, r *http.Request) { + sum := parseQuerySum(r.URL.RawQuery) + if r.Method == "POST" { + body, _ := io.ReadAll(r.Body) + if n, err := strconv.ParseInt(strings.TrimSpace(string(body)), 10, 64); err == nil { + sum += n + } + } + w.Header().Set("Server", "chi") + w.Write([]byte(strconv.FormatInt(sum, 10))) + } + r.Get("/baseline11", baseline11) + r.Post("/baseline11", baseline11) + + baseline2 := func(w http.ResponseWriter, r *http.Request) { + sum := parseQuerySum(r.URL.RawQuery) + w.Header().Set("Server", "chi") + w.Write([]byte(strconv.FormatInt(sum, 10))) + } + r.Get("/baseline2", baseline2) + + r.Get("/json", func(w http.ResponseWriter, r *http.Request) { + items := make([]ProcessedItem, len(dataset)) + for i, d := range dataset { + items[i] = ProcessedItem{ + ID: d.ID, Name: d.Name, Category: d.Category, + Price: d.Price, Quantity: d.Quantity, Active: d.Active, + Tags: d.Tags, Rating: d.Rating, + Total: math.Round(d.Price*float64(d.Quantity)*100) / 100, + } + } + w.Header().Set("Server", "chi") + w.Header().Set("Content-Type", "application/json") + data, _ := json.Marshal(ProcessResponse{Items: items, Count: len(items)}) + w.Write(data) + }) + + r.Get("/compression", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Server", "chi") + ae := r.Header.Get("Accept-Encoding") + if strings.Contains(ae, "deflate") { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Encoding", "deflate") + fw, err := flate.NewWriter(w, flate.BestSpeed) + if err == nil { + fw.Write(jsonLargeResponse) + fw.Close() + } + } else if strings.Contains(ae, "gzip") { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Encoding", "gzip") + gw, err := gzip.NewWriterLevel(w, gzip.BestSpeed) + if err == nil { + gw.Write(jsonLargeResponse) + gw.Close() + } + } else { + w.Header().Set("Content-Type", "application/json") + w.Write(jsonLargeResponse) + } + }) + + r.Post("/upload", func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + w.Header().Set("Server", "chi") + w.Write([]byte(fmt.Sprintf("%d", len(body)))) + }) + + r.Get("/db", func(w http.ResponseWriter, r *http.Request) { + if db == nil { + http.Error(w, "DB not available", http.StatusInternalServerError) + return + } + minPrice := 10.0 + maxPrice := 50.0 + if v := r.URL.Query().Get("min"); v != "" { + if f, err := strconv.ParseFloat(v, 64); err == nil { + minPrice = f + } + } + if v := r.URL.Query().Get("max"); v != "" { + if f, err := strconv.ParseFloat(v, 64); err == nil { + maxPrice = f + } + } + rows, err := db.Query("SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ? AND ? LIMIT 50", minPrice, maxPrice) + if err != nil { + http.Error(w, "Query failed", http.StatusInternalServerError) + return + } + defer rows.Close() + var items []map[string]interface{} + for rows.Next() { + var id, quantity, active, ratingCount int + var name, category, tags string + var price, ratingScore float64 + if err := rows.Scan(&id, &name, &category, &price, &quantity, &active, &tags, &ratingScore, &ratingCount); err != nil { + continue + } + var tagsArr []string + json.Unmarshal([]byte(tags), &tagsArr) + items = append(items, map[string]interface{}{ + "id": id, "name": name, "category": category, + "price": price, "quantity": quantity, "active": active == 1, + "tags": tagsArr, + "rating": map[string]interface{}{"score": ratingScore, "count": ratingCount}, + }) + } + w.Header().Set("Server", "chi") + w.Header().Set("Content-Type", "application/json") + data, _ := json.Marshal(map[string]interface{}{"items": items, "count": len(items)}) + w.Write(data) + }) + + r.Get("/static/{filename}", func(w http.ResponseWriter, r *http.Request) { + filename := chi.URLParam(r, "filename") + if sf, ok := staticFiles[filename]; ok { + w.Header().Set("Server", "chi") + w.Header().Set("Content-Type", sf.ContentType) + w.Write(sf.Data) + } else { + http.NotFound(w, r) + } + }) + + http.ListenAndServe(":8080", r) +} diff --git a/frameworks/chi/meta.json b/frameworks/chi/meta.json new file mode 100644 index 00000000..b7a9c0e5 --- /dev/null +++ b/frameworks/chi/meta.json @@ -0,0 +1,19 @@ +{ + "display_name": "chi", + "language": "Go", + "type": "framework", + "engine": "chi", + "description": "Chi is a lightweight, idiomatic and composable router for building Go HTTP services, built on top of net/http.", + "repo": "https://github.com/go-chi/chi", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "noisy", + "limited-conn", + "json", + "compression", + "upload", + "mixed" + ] +}