diff --git a/frameworks/Go/kruda/README.md b/frameworks/Go/kruda/README.md new file mode 100644 index 00000000000..71736ec7f36 --- /dev/null +++ b/frameworks/Go/kruda/README.md @@ -0,0 +1,35 @@ +# Kruda — TechEmpower Framework Benchmarks + +[Kruda](https://github.com/go-kruda/kruda) (ครุฑ) is a high-performance Go web framework combining speed with type-safety through Go generics. + +## Test Types + +All 7 TFB test types: + +- `/json` — JSON serialization +- `/plaintext` — Plaintext +- `/db` — Single database query +- `/queries?queries=N` — Multiple database queries +- `/updates?queries=N` — Database updates +- `/fortunes` — Fortunes (HTML rendering with XSS escaping) +- `/cached-queries?count=N` — Cached queries + +## Build & Run (Local) + +```bash +docker compose up --build +``` + +- Kruda: http://localhost:8080 +- PostgreSQL: localhost:5432 + +## Key Optimizations + +- Zero-allocation JSON/plaintext handlers (pre-allocated response bytes) +- Manual JSON serializer using `strconv.AppendInt` (no `encoding/json`) +- Manual HTML builder with byte-level XSS escaping (no `html/template`) +- Tiered `sync.Pool` buffer pools (1KB / 8KB / 32KB) +- `pgx.Batch` for single-roundtrip multi-query and update operations +- Flat-array in-memory cache for cached queries (0-alloc lookups) +- Atomic date header cache (1-second refresh) +- Wing transport: epoll per-worker, zero-copy response building diff --git a/frameworks/Go/kruda/benchmark_config.json b/frameworks/Go/kruda/benchmark_config.json new file mode 100644 index 00000000000..3a28c2d149c --- /dev/null +++ b/frameworks/Go/kruda/benchmark_config.json @@ -0,0 +1,31 @@ +{ + "framework": "kruda", + "tests": [ + { + "default": { + "json_url": "/json", + "plaintext_url": "/plaintext", + "db_url": "/db", + "query_url": "/queries?n=", + "update_url": "/updates?n=", + "fortune_url": "/fortunes", + "cached_query_url": "/cached-queries?n=", + "port": 8080, + "approach": "Realistic", + "classification": "Micro", + "database": "Postgres", + "database_os": "Linux", + "display_name": "Kruda", + "framework": "kruda", + "language": "Go", + "flavor": "None", + "orm": "Raw", + "os": "Linux", + "platform": "None", + "webserver": "None", + "versus": "go", + "notes": "" + } + } + ] +} diff --git a/frameworks/Go/kruda/config.toml b/frameworks/Go/kruda/config.toml new file mode 100644 index 00000000000..ab62742423e --- /dev/null +++ b/frameworks/Go/kruda/config.toml @@ -0,0 +1,20 @@ +[framework] +name = "kruda" + +[main] +urls.plaintext = "/plaintext" +urls.json = "/json" +urls.db = "/db" +urls.query = "/queries?n=" +urls.update = "/updates?n=" +urls.fortune = "/fortunes" +urls.cached_query = "/cached-queries?n=" +approach = "Realistic" +classification = "Micro" +database = "Postgres" +database_os = "Linux" +os = "Linux" +orm = "Raw" +platform = "None" +webserver = "None" +versus = "go" diff --git a/frameworks/Go/kruda/kruda.dockerfile b/frameworks/Go/kruda/kruda.dockerfile new file mode 100644 index 00000000000..e491e873734 --- /dev/null +++ b/frameworks/Go/kruda/kruda.dockerfile @@ -0,0 +1,17 @@ +FROM golang:1.26-alpine AS builder + +RUN apk add --no-cache git + +WORKDIR /kruda +COPY ./src /kruda + +RUN GOAMD64=v3 go build -ldflags="-s -w" -gcflags="-B" -trimpath -o app . + +FROM alpine:3.20 +RUN apk --no-cache add ca-certificates tzdata +WORKDIR /kruda +COPY --from=builder /kruda/app . + +EXPOSE 8080 +ENV GOGC=500 GOMEMLIMIT=512MiB +CMD ["./app"] diff --git a/frameworks/Go/kruda/src/go.mod b/frameworks/Go/kruda/src/go.mod new file mode 100644 index 00000000000..27614eda289 --- /dev/null +++ b/frameworks/Go/kruda/src/go.mod @@ -0,0 +1,29 @@ +module tfb-kruda + +go 1.25.8 + +require ( + github.com/go-kruda/kruda v1.0.2 + github.com/jackc/pgx/v5 v5.8.0 +) + +require ( + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/go-kruda/kruda/transport/wing v1.0.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/klauspost/compress v1.18.2 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.69.0 // indirect + golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect +) diff --git a/frameworks/Go/kruda/src/go.sum b/frameworks/Go/kruda/src/go.sum new file mode 100644 index 00000000000..0ffeb53668f --- /dev/null +++ b/frameworks/Go/kruda/src/go.sum @@ -0,0 +1,64 @@ +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-kruda/kruda v1.0.2 h1:bnCZ0tXlhZI9faRjCt6ccdJovz/QNIHTPiuxXLPOv1w= +github.com/go-kruda/kruda v1.0.2/go.mod h1:sc04NvBp8ehU1TJfBySq5PfP7Kprq4eOcdpVJm1KmPI= +github.com/go-kruda/kruda/transport/wing v1.0.2 h1:jg8g97wuEDuZajdH1Zp/cF+1uROqVyBUcUtePxt6bno= +github.com/go-kruda/kruda/transport/wing v1.0.2/go.mod h1:doqpIC6CMekIMrGWKugq/927Kr9d6jWORgK8zQ7uYPI= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +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.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= +github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/frameworks/Go/kruda/src/main.go b/frameworks/Go/kruda/src/main.go new file mode 100644 index 00000000000..cd835038190 --- /dev/null +++ b/frameworks/Go/kruda/src/main.go @@ -0,0 +1,249 @@ +package main + +import ( + "context" + "math/rand/v2" + "os" + "sort" + "strconv" + "sync" + "time" + + "github.com/go-kruda/kruda" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +type World struct { + ID int32 `json:"id"` + RandomNumber int32 `json:"randomNumber"` +} + +type Fortune struct { + ID int32 + Message string +} + +type JSONMessage struct { + Message string `json:"message"` +} + +var pool *pgxpool.Pool + +// cachedWorlds holds pre-fetched worlds for cached-queries test +var cachedWorlds sync.Map + +func main() { + dsn := os.Getenv("DATABASE_URL") + if dsn == "" { + dsn = "postgres://benchmarkdbuser:benchmarkdbpass@tfb-database:5432/hello_world?sslmode=disable" + } + + cfg, _ := pgxpool.ParseConfig(dsn) + cfg.MaxConns = 64 + cfg.MinConns = 8 + cfg.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + conn.Prepare(ctx, "worldSelect", "SELECT randomnumber FROM world WHERE id=$1") + conn.Prepare(ctx, "fortuneSelect", "SELECT id, message FROM fortune") + conn.Prepare(ctx, "worldUpdate", "UPDATE world SET randomnumber=$1 WHERE id=$2") + return nil + } + + var err error + pool, err = pgxpool.NewWithConfig(context.Background(), cfg) + if err != nil { + panic(err) + } + + // Pre-populate cache + go populateCache() + + app := kruda.New(kruda.Wing()) + + // TFB Test 1: JSON serialization + app.Get("/json", func(c *kruda.Ctx) error { + return c.JSON(JSONMessage{Message: "Hello, World!"}) + }, kruda.WingJSON()) + + // TFB Test 2: Plaintext + app.Get("/plaintext", func(c *kruda.Ctx) error { + return c.Text("Hello, World!") + }, kruda.WingPlaintext()) + + // TFB Test 3: Single database query + app.Get("/db", func(c *kruda.Ctx) error { + w := World{ID: int32(rand.IntN(10000) + 1)} + pool.QueryRow(context.Background(), "worldSelect", w.ID).Scan(&w.RandomNumber) + return c.JSON(w) + }, kruda.WingQuery()) + + // TFB Test 4: Multiple database queries + app.Get("/queries", func(c *kruda.Ctx) error { + n := clamp(queryParam(c, "n"), 1, 500) + worlds := make([]World, n) + for i := range worlds { + worlds[i].ID = int32(rand.IntN(10000) + 1) + } + batch := &pgx.Batch{} + for i := range worlds { + batch.Queue("worldSelect", worlds[i].ID) + } + br := pool.SendBatch(context.Background(), batch) + for i := range worlds { + br.QueryRow().Scan(&worlds[i].RandomNumber) + } + br.Close() + return c.JSON(worlds) + }, kruda.WingQuery()) + + // TFB Test 5: Fortunes + app.Get("/fortunes", func(c *kruda.Ctx) error { + rows, err := pool.Query(context.Background(), "fortuneSelect") + if err != nil { + return c.Status(500).Text(err.Error()) + } + defer rows.Close() + + fortunes := make([]Fortune, 0, 13) + for rows.Next() { + var f Fortune + rows.Scan(&f.ID, &f.Message) + fortunes = append(fortunes, f) + } + fortunes = append(fortunes, Fortune{Message: "Additional fortune added at request time."}) + sort.Slice(fortunes, func(i, j int) bool { return fortunes[i].Message < fortunes[j].Message }) + + return c.HTML(fortunesHTML(fortunes)) + }, kruda.WingRender()) + + // TFB Test 6: Database updates + app.Get("/updates", func(c *kruda.Ctx) error { + n := clamp(queryParam(c, "n"), 1, 500) + worlds := make([]World, n) + for i := range worlds { + worlds[i].ID = int32(rand.IntN(10000) + 1) + } + batch := &pgx.Batch{} + for i := range worlds { + batch.Queue("worldSelect", worlds[i].ID) + } + br := pool.SendBatch(context.Background(), batch) + for i := range worlds { + br.QueryRow().Scan(&worlds[i].RandomNumber) + worlds[i].RandomNumber = int32(rand.IntN(10000) + 1) + } + br.Close() + + ids := make([]int32, n) + nums := make([]int32, n) + for i, w := range worlds { + ids[i] = w.ID + nums[i] = w.RandomNumber + } + pool.Exec(context.Background(), + "UPDATE world SET randomnumber=v.r FROM (SELECT unnest($1::int[]) id, unnest($2::int[]) r) v WHERE world.id=v.id", + ids, nums, + ) + return c.JSON(worlds) + }, kruda.WingQuery()) + + // TFB Test 7: Cached queries + app.Get("/cached-queries", func(c *kruda.Ctx) error { + n := clamp(queryParam(c, "n"), 1, 500) + worlds := make([]World, n) + for i := range worlds { + id := int32(rand.IntN(10000) + 1) + if w, ok := cachedWorlds.Load(id); ok { + worlds[i] = w.(World) + } else { + worlds[i] = World{ID: id} + pool.QueryRow(context.Background(), "worldSelect", id).Scan(&worlds[i].RandomNumber) + cachedWorlds.Store(id, worlds[i]) + } + } + return c.JSON(worlds) + }, kruda.WingQuery()) + + app.Listen(":8080") +} + +func populateCache() { + // Wait for DB to be ready, then refresh cache periodically + for { + time.Sleep(5 * time.Second) + batch := &pgx.Batch{} + for i := int32(1); i <= 10000; i++ { + batch.Queue("worldSelect", i) + } + br := pool.SendBatch(context.Background(), batch) + for i := int32(1); i <= 10000; i++ { + var rn int32 + if err := br.QueryRow().Scan(&rn); err == nil { + cachedWorlds.Store(i, World{ID: i, RandomNumber: rn}) + } + } + br.Close() + } +} + +func queryParam(c *kruda.Ctx, name string) int { + n, _ := strconv.Atoi(c.Query(name)) + return n +} + +func clamp(n, lo, hi int) int { + if n < lo { + return lo + } + if n > hi { + return hi + } + return n +} + +var fortuneBufPool = sync.Pool{ + New: func() any { b := make([]byte, 0, 4096); return &b }, +} + +func fortunesHTML(ff []Fortune) string { + bp := fortuneBufPool.Get().(*[]byte) + buf := (*bp)[:0] + buf = append(buf, "Fortunes"...) + for _, f := range ff { + buf = append(buf, ""...) + } + buf = append(buf, "
idmessage
"...) + buf = strconv.AppendInt(buf, int64(f.ID), 10) + buf = append(buf, ""...) + buf = appendHTMLEscape(buf, f.Message) + buf = append(buf, "
"...) + s := string(buf) + *bp = buf + fortuneBufPool.Put(bp) + return s +} + +func appendHTMLEscape(buf []byte, s string) []byte { + last := 0 + for i := 0; i < len(s); i++ { + var esc string + switch s[i] { + case '&': + esc = "&" + case '<': + esc = "<" + case '>': + esc = ">" + case '"': + esc = """ + case '\'': + esc = "'" + default: + continue + } + buf = append(buf, s[last:i]...) + buf = append(buf, esc...) + last = i + 1 + } + return append(buf, s[last:]...) +}