mirror of
https://github.com/hanzoai/benchmarks.git
synced 2026-10-30 14:59:11 +00:00
Separate, orthogonal axis from the zaphttp framing-CPU test (that carries the body opaque and never touches JSON). Isolates ZAP's real structural win: typed binary fields read zero-copy (no scan/reflect/per-field alloc) vs HTTP+JSON (de)serializing text every call. One eth-style record, same compute, same field set both sides; JSON held to its fastest libs (goccy, sonic) + stdlib. New shared `record` package (DRY: one record + codecs used by the microbench, the /rpc server, and the ZAP load generator). serde_test.go: decode/encode/ round-trip/handler benches + wire sizes + round-trip equivalence. main.go: POST /rpc route (zap transport = zero-copy decode; http transport = JSON codec). zapload: -scenario rpc sends the identical ZAP-typed body. Numbers (GB10 arm64, go1.26, median of 3, default GOGC/GOMAXPROCS): - Decode: ZAP 13.4ns/0-alloc vs goccy 473ns/4, sonic 845ns/8, stdlib 2334ns/13 (35–174x, 0 vs many allocs) — the crush. - Encode: honest wash — ZAP 629ns/12-alloc vs goccy 424ns/4 (v1.3.0 SetBytes defers tail via copy + offsets slice; SetBytesFixed is luxfi/zap-only). - Round trip: ZAP 648ns vs goccy 904 / sonic 1509 / stdlib 3107. - Handler (per-req): ZAP 271ns/5-alloc vs goccy 431/4, sonic 736/7, stdlib 1444/9. - Wire: total ZAP 162B vs JSON 233B. - End-to-end /rpc c125 d6s: ZAP 534,945 rps vs sonic 483k (1.11x), goccy 452k (1.18x), stdlib 407k (1.31x). Reproducible off published tags (zap-proto/go v1.3.0, no local replaces).
95 lines
3.4 KiB
Go
95 lines
3.4 KiB
Go
// Command zipbench stands up a minimal server on either the zip framework or
|
|
// raw fasthttp so `hey`/`wrk`/`bombardier` (HTTP) and `./zapload` (ZAP binary
|
|
// transport) can measure END-TO-END max req/sec (accept → parse → respond over
|
|
// TCP) — the number the in-process Go benches (handler cost only) don't capture.
|
|
//
|
|
// Two scenarios:
|
|
//
|
|
// - hello: trivial "ok" handler — the framing/transport tax (existing test).
|
|
//
|
|
// - rpc: the eth-style /rpc record from the record package — HTTP+JSON vs
|
|
// native ZAP, same logical work both sides, isolating the JSON-skip advantage.
|
|
//
|
|
// go run . -fw zip -addr :8091 -scenario hello
|
|
// go run . -fw zip -addr :8091 -scenario rpc -transport http -json goccy # bombardier
|
|
// go run . -fw zip -addr :8091 -scenario rpc -transport zap # ./zapload
|
|
// go run . -fw fasthttp -addr :8092
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"log"
|
|
|
|
rec "github.com/hanzoai/benchmarks/framework/zip/record"
|
|
"github.com/valyala/fasthttp"
|
|
"github.com/zap-proto/zip"
|
|
)
|
|
|
|
func main() {
|
|
fw := flag.String("fw", "zip", "framework: zip | fasthttp")
|
|
addr := flag.String("addr", ":8091", "listen address")
|
|
transport := flag.String("transport", "http", "zip transport: http | zap")
|
|
scenario := flag.String("scenario", "hello", "handler: hello | rpc")
|
|
jsonLib := flag.String("json", "stdlib", "rpc JSON codec on the http transport: stdlib | goccy | sonic")
|
|
flag.Parse()
|
|
|
|
switch *fw {
|
|
case "zip":
|
|
// zip is ZAP-native: a bare addr binds the ZAP binary transport; "http://"
|
|
// binds fasthttp HTTP. Same handler either way — only the wire differs.
|
|
app := zip.New(zip.Config{ServerHeader: "-"})
|
|
app.Get("/health", func(c *zip.Ctx) error { return c.String(200, "ok") })
|
|
|
|
if *scenario == "rpc" {
|
|
h, desc := rpcHandler(*transport, *jsonLib)
|
|
app.Post("/rpc", h)
|
|
log.Printf("rpc handler: %s", desc)
|
|
}
|
|
|
|
laddr := *addr // bare = ZAP (driven by ./zapload)
|
|
if *transport == "http" {
|
|
laddr = "http://" + *addr // HTTP (driven by hey/bombardier)
|
|
}
|
|
log.Printf("zip listening (%s transport, scenario=%s) on %s", *transport, *scenario, *addr)
|
|
log.Fatal(app.Listen(laddr))
|
|
case "fasthttp":
|
|
h := func(ctx *fasthttp.RequestCtx) {
|
|
ctx.SetStatusCode(200)
|
|
ctx.SetBodyString("ok")
|
|
}
|
|
log.Printf("fasthttp listening on %s", *addr)
|
|
log.Fatal(fasthttp.ListenAndServe(*addr, h))
|
|
default:
|
|
log.Fatalf("unknown -fw %q", *fw)
|
|
}
|
|
}
|
|
|
|
// rpcHandler builds the /rpc handler for the configured wire. On the zap
|
|
// transport the body is a native ZAP-typed message (decoded zero-copy); on the
|
|
// http transport it is JSON decoded with the selected codec. Both run the same
|
|
// serverCompute* path from rpc.go — same fields in, same compute, same fields
|
|
// out — so the only difference measured end-to-end is the serialization wire.
|
|
func rpcHandler(transport, jsonLib string) (zip.Handler, string) {
|
|
if transport == "zap" {
|
|
return func(c *zip.Ctx) error {
|
|
out, err := rec.ServerComputeZAP(c.Body())
|
|
if err != nil {
|
|
return c.String(400, "bad zap body")
|
|
}
|
|
return c.Bytes(200, out)
|
|
}, "zap-typed (zero-copy decode)"
|
|
}
|
|
|
|
codec, ok := rec.LookupJSONCodec(jsonLib)
|
|
if !ok {
|
|
log.Fatalf("unknown/unavailable -json %q", jsonLib)
|
|
}
|
|
return func(c *zip.Ctx) error {
|
|
out, err := rec.ServerComputeJSON(codec, c.Body())
|
|
if err != nil {
|
|
return c.String(400, "bad json body")
|
|
}
|
|
return c.Bytes(200, out)
|
|
}, "http+json codec=" + codec.Name
|
|
}
|