diff --git a/frameworks/fiber/Dockerfile b/frameworks/fiber/Dockerfile new file mode 100644 index 0000000..97a2a91 --- /dev/null +++ b/frameworks/fiber/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/fiber/README.md b/frameworks/fiber/README.md new file mode 100644 index 0000000..dcae5ef --- /dev/null +++ b/frameworks/fiber/README.md @@ -0,0 +1,23 @@ +# Fiber + +[Fiber](https://github.com/gofiber/fiber) is an Express-inspired Go web framework built on [fasthttp](https://github.com/valyala/fasthttp). It's one of the most popular Go web frameworks (~35k stars) known for extreme performance and a familiar API for developers coming from Node.js/Express. + +## Key Features + +- Built on fasthttp (zero-allocation HTTP engine) +- Express-like API for ease of use +- Prefork mode for multi-core utilization +- Zero memory allocation routing +- Built-in middleware ecosystem + +## Implementation Notes + +- Uses Fiber v2 with prefork mode enabled (one process per CPU core) +- Pure Go SQLite via `modernc.org/sqlite` (no CGO) +- Manual compression handling (deflate/gzip) matching HttpArena spec +- Static files pre-loaded into memory at startup +- 25MB body limit for upload endpoint + +## Why This Entry Matters + +HttpArena already has raw fasthttp and two net/http-based frameworks (Gin, Echo). Fiber completes the Go comparison by showing how a framework built *on top of* fasthttp compares — what's the overhead of Fiber's routing and middleware layer over raw fasthttp? And how does fasthttp-based Fiber compare to net/http-based Gin and Echo? diff --git a/frameworks/fiber/go.mod b/frameworks/fiber/go.mod new file mode 100644 index 0000000..cba0651 --- /dev/null +++ b/frameworks/fiber/go.mod @@ -0,0 +1,29 @@ +module fiber-bench + +go 1.24.13 + +require ( + github.com/gofiber/fiber/v2 v2.52.12 + modernc.org/sqlite v1.46.1 +) + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // 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/fiber/go.sum b/frameworks/fiber/go.sum new file mode 100644 index 0000000..3bf8aab --- /dev/null +++ b/frameworks/fiber/go.sum @@ -0,0 +1,73 @@ +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +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/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw= +github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +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/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +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/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +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= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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/fiber/main.go b/frameworks/fiber/main.go new file mode 100644 index 0000000..90491dd --- /dev/null +++ b/frameworks/fiber/main.go @@ -0,0 +1,282 @@ +package main + +import ( + "compress/flate" + "compress/gzip" + "database/sql" + "encoding/json" + "fmt" + "math" + "mime" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + + "github.com/gofiber/fiber/v2" + _ "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() + + app := fiber.New(fiber.Config{ + DisableStartupMessage: true, + BodyLimit: 25 * 1024 * 1024, // 25 MB + }) + + app.Get("/pipeline", func(c *fiber.Ctx) error { + c.Set("Server", "fiber") + return c.SendString("ok") + }) + + baseline11 := func(c *fiber.Ctx) error { + sum := parseQuerySum(c.Context().URI().QueryArgs().String()) + if c.Method() == "POST" { + body := c.Body() + if n, err := strconv.ParseInt(strings.TrimSpace(string(body)), 10, 64); err == nil { + sum += n + } + } + c.Set("Server", "fiber") + return c.SendString(strconv.FormatInt(sum, 10)) + } + app.Get("/baseline11", baseline11) + app.Post("/baseline11", baseline11) + + app.Get("/baseline2", func(c *fiber.Ctx) error { + sum := parseQuerySum(c.Context().URI().QueryArgs().String()) + c.Set("Server", "fiber") + return c.SendString(strconv.FormatInt(sum, 10)) + }) + + app.Get("/json", func(c *fiber.Ctx) error { + 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, + } + } + c.Set("Server", "fiber") + c.Set("Content-Type", "application/json") + data, _ := json.Marshal(ProcessResponse{Items: items, Count: len(items)}) + return c.Send(data) + }) + + app.Get("/compression", func(c *fiber.Ctx) error { + c.Set("Server", "fiber") + ae := c.Get("Accept-Encoding") + if strings.Contains(ae, "deflate") { + c.Set("Content-Type", "application/json") + c.Set("Content-Encoding", "deflate") + w, err := flate.NewWriter(c.Response().BodyWriter(), flate.BestSpeed) + if err == nil { + w.Write(jsonLargeResponse) + w.Close() + } + return nil + } else if strings.Contains(ae, "gzip") { + c.Set("Content-Type", "application/json") + c.Set("Content-Encoding", "gzip") + w, err := gzip.NewWriterLevel(c.Response().BodyWriter(), gzip.BestSpeed) + if err == nil { + w.Write(jsonLargeResponse) + w.Close() + } + return nil + } + c.Set("Content-Type", "application/json") + return c.Send(jsonLargeResponse) + }) + + app.Post("/upload", func(c *fiber.Ctx) error { + body := c.Body() + c.Set("Server", "fiber") + return c.SendString(fmt.Sprintf("%d", len(body))) + }) + + app.Get("/db", func(c *fiber.Ctx) error { + if db == nil { + return c.Status(500).SendString("DB not available") + } + minPrice := 10.0 + maxPrice := 50.0 + if v := c.Query("min"); v != "" { + if f, err := strconv.ParseFloat(v, 64); err == nil { + minPrice = f + } + } + if v := c.Query("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 { + return c.Status(500).SendString("Query failed") + } + 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}, + }) + } + c.Set("Server", "fiber") + c.Set("Content-Type", "application/json") + data, _ := json.Marshal(map[string]interface{}{"items": items, "count": len(items)}) + return c.Send(data) + }) + + app.Get("/static/:filename", func(c *fiber.Ctx) error { + filename := c.Params("filename") + if sf, ok := staticFiles[filename]; ok { + c.Set("Server", "fiber") + c.Set("Content-Type", sf.ContentType) + return c.Send(sf.Data) + } + return c.SendStatus(404) + }) + + app.Listen(":8080") +} diff --git a/frameworks/fiber/meta.json b/frameworks/fiber/meta.json new file mode 100644 index 0000000..d7d90cf --- /dev/null +++ b/frameworks/fiber/meta.json @@ -0,0 +1,18 @@ +{ + "display_name": "fiber", + "language": "Go", + "type": "framework", + "engine": "fiber", + "description": "Fiber is an Express-inspired Go web framework built on fasthttp, featuring zero-allocation routing and extreme performance.", + "repo": "https://github.com/gofiber/fiber", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "limited-conn", + "json", + "compression", + "upload", + "mixed" + ] +}