From 3d5f225fb25376e00637cbe655c2d35cf2ff11e2 Mon Sep 17 00:00:00 2001 From: k2safe Date: Mon, 21 Sep 2026 02:35:48 +0800 Subject: [PATCH] test: compare native concurrency and server memory --- CHANGELOG.md | 3 +- PERFORMANCE.md | 62 +- README.md | 2 +- ROADMAP.md | 5 +- internal/perfcheck/main.go | 586 ++++++++++ internal/perfcheck/main_test.go | 63 + performance/tcpBaseline.json | 1735 ++++++++++++++++++++++++++++ scripts/verifyPerformance.py | 16 +- transport/benchmarkTCP_test.go | 125 ++ transport/native/benchmark_test.go | 95 -- 10 files changed, 2577 insertions(+), 115 deletions(-) create mode 100644 internal/perfcheck/main.go create mode 100644 internal/perfcheck/main_test.go create mode 100644 performance/tcpBaseline.json create mode 100644 transport/benchmarkTCP_test.go delete mode 100644 transport/native/benchmark_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c0d82e..100e910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ## Unreleased -- 新增 native 真实 TCP Keep-Alive 并发 benchmark,纳入稳定性与性能验收脚本,用于评估后续事件状态机优化。 +- 统一标准库与 native 的真实 TCP benchmark,增加固定并发档位、响应状态与读写错误检查。 +- 新增独立进程并发与长连接内存测量工具,记录成功吞吐、P95/P99、服务端分配、空闲连接堆内存和回收情况;修正并行 `ns/op` 与客户端/服务端混合分配的解释。 ## v0.5.0 diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 8b6c888..3de87f8 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -1,22 +1,62 @@ # GoOmni 性能基线 -这份基线用于比较未来自研网络层,不代表生产容量承诺。标准库 benchmark 测进程内请求分发;native benchmark 额外覆盖真实 TCP、Keep-Alive 和 GoOmni 请求链,但不包含 TLS、数据库或业务存储延迟。 +这轮测量显示 native 与标准库处于相同吞吐量级,在本机短时测试中,native 的空闲长连接堆内存更少。当前 native 仅实现 HTTP 的部分能力,尚不能据此判断生产容量或替代完整标准库服务端。 -运行: +## 运行方式 ```sh +# race、vet、build,以及进程内和真实 TCP benchmark python3 scripts/verifyPerformance.py + +# 另加独立服务进程的并发、延迟和空闲长连接内存矩阵 +python3 scripts/verifyPerformance.py --load --output /tmp/goOmniPerf.json + +# 只运行独立进程矩阵,默认每档 2 秒、3 轮 +go run ./internal/perfcheck --duration 2s --count 3 --output /tmp/goOmniPerf.json ``` -本次记录环境:Go 1.25.0、darwin/arm64、Apple M2 Max;每项连续运行 3 次: +可通过 `--concurrency 1,32,128` / `--idle 128,1024` 调整矩阵;传空字符串可跳过对应场景。工具只监听本机随机端口,每个场景启动独立服务进程,并在结束后关闭客户端、检查连接排空、关闭服务。响应状态、完整响应体、requestId 或传输错误均计入失败,不能把快速返回的错误页算成成功吞吐。 -```text -BenchmarkTransportRouteDispatch/nethttp 568–671 ns/op 1344 B/op 13 allocs/op -BenchmarkTransportApplicationRequest/nethttp 2017–2208 ns/op 3091 B/op 34 allocs/op -BenchmarkNativeApplicationRequest 15438–16711 ns/op 7855–7874 B/op 100 allocs/op -``` +## 2026-09-21 本机测量 -`BenchmarkTransportRouteDispatch` 只覆盖标准库路由、路径参数和 `http.Handler` 调用;`BenchmarkTransportApplicationRequest` 还包含 GoOmni Context、RequestID、统一 JSON 响应和默认中间件链。 -`BenchmarkNativeApplicationRequest` 启动 native TCP listener,通过复用连接的 HTTP client 并发请求健康接口,包含 socket、请求解析、连接状态管理、响应写回和对象池路径。它用于观察网络层的相对变化,不应直接换算为生产 QPS。 +环境:Go 1.25.0、darwin/arm64、Apple M2 Max、GOMAXPROCS=12。网络实现来自 `c36bb70` 工作树(native 与 v0.5.0 相同);本次新增测量工具,不修改服务端实现。原始测量值见 [tcpBaseline.json](performance/tcpBaseline.json)。报告时间为 UTC,北京时间是 9 月 21 日。 -比较新网络层时,应使用相同 Go 版本、机器和 benchmark 命令,并同时运行 `-race` 全量测试。性能下降需要结合分配次数、取消传播、错误响应和业务契约结果一起判断。 +每个并发档位持续约 2 秒、重复 3 轮,每轮交替标准库/native 的先后顺序。下表为各指标的三轮中位数;全部流量场景共完成 **2,660,828** 次请求,错误 **0**。另有 128 / 1,024 条空闲长连接的建立、保持和回收测试。 + +| 并发请求 | 标准库成功请求/秒 | native 成功请求/秒 | 标准库 P95 | native P95 | 标准库 P99 | native P99 | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| 1 | 19,731 | 21,497 | 0.074 ms | 0.070 ms | 0.139 ms | 0.119 ms | +| 32 | 90,099 | 88,812 | 1.016 ms | 0.983 ms | 1.507 ms | 1.507 ms | +| 128 | 112,422 | 112,105 | 3.277 ms | 3.408 ms | 4.719 ms | 4.981 ms | + +128 并发时,两者吞吐差约 0.3%,native 的尾延迟略高。基于这组结果,暂时没有理由仅为健康接口吞吐就改成手写事件循环。 + +| 空闲 Keep-Alive 连接 | 标准库新增堆内存 | native 新增堆内存 | 标准库约每连接 | native 约每连接 | +| --- | ---: | ---: | ---: | ---: | +| 128 | 1.275 MiB | 0.658 MiB | 10,444 B | 5,389 B | +| 1,024 | 9.871 MiB | 5.137 MiB | 10,108 B | 5,260 B | + +1,024 条空闲连接时,native 的新增堆内存约少 48%。这是**服务端 Go 堆内存增量**,不包括 goroutine 栈、进程其他内存和内核 socket 缓冲区,不能解释为整个进程内存减半。同档服务端 `StackInuse` 分别约为标准库 8.75 MiB、native 8.69 MiB,原始报告单独记录。 + +连接断开后,两者连接数都回到 0,goroutine 分别回到 2 / 3 的初始基线。一次 GC 后仍有对象池缓存;第二次 GC 后,总堆内存中位数回到约 2.10 / 2.08 MiB(启动基线约 1.54 MiB)。堆内存不会立刻精确回到启动数值,不把缓存保留解释为泄漏,也不把这轮短测解释为长期无泄漏证明。 + +128 并发时,**服务端单独累计分配**约为标准库 4,454 B/请求、47 次/请求,native 4,297 B/请求、55 次/请求。native 的分配字节较少、次数较多;具体来源需用分配 profile 确认,不能仅凭总量归因于 HTTP 桥接或 JSON。 + +## 统计边界 + +- 同一台机器上,压测客户端和服务端是两个独立进程。两者使用同一健康接口、相同小型 JSON 响应、固定 `requestId=perfCheck` 和 Keep-Alive;不启用访问日志、TLS 或数据库。 +- 并发模型为闭环:每个 worker 等上一个请求完成后发下一个。实际经过时间包含最后请求排空;吞吐只计算成功响应。不能据此推断开放到达流量下的排队或过载表现。 +- 延迟从客户端发起请求到完整响应校验完成,包含失败请求。直方图使用每个 2 的幂区间内 16 档,报告桶上界,舍入误差至多约 6.25%。小幅差别不应过度解释。 +- 服务端 `runtime.MemStats` 通过独立 stdin/stdout 控制通道读取,客户端内存不计入。累计分配差值包含少量采样开销。堆峰值每 200 ms 采样,可能错过瞬时峰值;这些指标不是进程 RSS。 +- 空闲场景在所有连接完成健康请求后做 GC 再采样。新增堆内存包括连接、缓冲区及分摊的运行时开销。`after` 为客户端断开并排空后一次 GC,`afterReclaim` 为第二次 GC,用于观察对象池缓存释放。 +- native 配置上限为 8,192 连接 / 4,096 活跃请求;本轮并发低于上限,未验证过载保护容量。标准库没有同样的并发闸门;比较包括各自真实实现的开销。 + +## benchmark 口径修正 + +`BenchmarkTransportTCPApplicationRequest/{nethttp,native}/concurrency{1,32,128}` 使用同一测试入口、客户端、应用与响应检查。原先独立的 `BenchmarkNativeApplicationRequest` 已合并,避免两个入口配置漂移。`verifyPerformance.py` 会一并运行。 + +并行 benchmark 的 **`ns/op` 是总经过时间除以请求数,反映吞吐成本,不是单请求延迟**。`B/op` / `allocs/op` 统计同一进程中的客户端与服务端总分配。此前报告的 native 15.4–16.7 µs/op、约 7.8 KB/op、100 allocs/op 应按这个含义理解,不能作为服务端延迟或独立内存指标。 + +`BenchmarkTransportRouteDispatch` 和 `BenchmarkTransportApplicationRequest` 继续保留,分别测进程内路由分发和完整框架处理链;不包含 TCP,不能直接与上述网络延迟比较。 + +后续优先补 Linux 目标环境、持续压测、大请求体/慢客户端、连接反复建立回收和协议边界验证,再通过 profile 决定是否优化对象分配或事件状态机。 diff --git a/README.md b/README.md index 760c6fa..2523b03 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ python3 scripts/verifyBusiness.py --version v0.5.0 该验收会在临时目录中先固定 `v0.2.0`,再升级到 `v0.5.0`,检查无本地 replace、配置加载、结构化日志、requestId、CRUD、校验错误、业务错误和优雅退出。 -标准库路由和完整 GoOmni 请求链的可重复性能基线见 [PERFORMANCE.md](PERFORMANCE.md),运行 `python3 scripts/verifyPerformance.py` 可重新采样。 +标准库与 native 的并发、延迟和长连接内存对照见 [PERFORMANCE.md](PERFORMANCE.md)。`python3 scripts/verifyPerformance.py` 运行检查与 benchmark;增加 `--load --output /tmp/goOmniPerf.json` 可测量独立服务进程并保存报告。 在框架根目录运行: diff --git a/ROADMAP.md b/ROADMAP.md index bf31071..75611e5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -39,9 +39,10 @@ - 已加入标准库路由和完整 GoOmni 请求链 benchmark,输出分配次数和耗时,作为未来自研网络层的对照基线。 - 新增 `transport/native` 第一版自研 HTTP/1.1 服务端:自行管理 TCP listener、Keep-Alive、Content-Length 请求解析、响应写回、读写超时和优雅关闭;通过 `App.RunWith` 接入,业务 Handler / Service 保持不变。 -- 新增 native 真实 TCP Keep-Alive 并发 benchmark,记录 socket、解析、响应和对象池路径的耗时与分配,后续压测以此作为事件状态机改造前的基线。 +- 标准库与 native 共用 TCP benchmark,固定 1 / 32 / 128 并发;相同应用、客户端、超时和响应检查,避免两个测试入口逐渐失去可比性。 +- 新增独立服务进程压测 `internal/perfcheck`,记录成功吞吐、P50/P95/P99、服务端分配、采样堆峰值,以及 128 / 1,024 条空闲长连接的堆内存、goroutine 和断开后的回收情况。原始报告保存在 `performance/tcpBaseline.json`。 - native 当前拒绝 chunked 请求体,仍使用标准 `http.Request` / `http.ResponseWriter` 作为桥接;解析器、连接抽象和生产压测将在后续阶段继续下沉。 - 已具备全量 race、vet、build 验收;生产压测、长连接规模和真实部署验证仍待完成。 -- 自动验收入口:`python3 scripts/verifyPerformance.py`。 +- 自动验收入口:`python3 scripts/verifyPerformance.py`;增加 `--load --output /tmp/goOmniPerf.json` 可重复运行独立进程并发与内存矩阵。 以上阶段以验收结果判断完成。当前仍是基础框架试用阶段;MySQL、适配层契约和一次业务模块试用已完成,自研网络层已有可运行预览,稳定性与生产规模验证尚未完成。 diff --git a/internal/perfcheck/main.go b/internal/perfcheck/main.go new file mode 100644 index 0000000..2cac2f0 --- /dev/null +++ b/internal/perfcheck/main.go @@ -0,0 +1,586 @@ +// perfcheck compares real TCP servers in separate processes. It is a local +// measurement tool, not part of the framework's public CLI or server API. +package main + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "math/bits" + "net" + "net/http" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + omni "git.yuntuo.hk/k2safe/go-omni" + "git.yuntuo.hk/k2safe/go-omni/transport/native" +) + +const expectedBody = `{"code":"OK","message":"success","data":{"status":"ok"},"requestId":"perfCheck"}` + +type snapshot struct { + Address string `json:"address,omitempty"` + HeapAlloc uint64 `json:"heapAlloc"` + HeapInuse uint64 `json:"heapInuse"` + StackInuse uint64 `json:"stackInuse"` + TotalAlloc uint64 `json:"totalAlloc"` + Mallocs uint64 `json:"mallocs"` + NumGC uint32 `json:"numGc"` + Goroutines int `json:"goroutines"` + Connections int64 `json:"connections"` + Accepted int64 `json:"accepted"` +} + +type result struct { + Adapter string `json:"adapter"` + Scenario string `json:"scenario"` + Concurrency int `json:"concurrency"` + Run int `json:"run"` + Requests uint64 `json:"requests,omitempty"` + Errors uint64 `json:"errors"` + FirstError string `json:"firstError,omitempty"` + ElapsedSeconds float64 `json:"elapsedSeconds,omitempty"` + RequestsPerSecond float64 `json:"requestsPerSecond,omitempty"` + P50Millis float64 `json:"p50Millis,omitempty"` + P95Millis float64 `json:"p95Millis,omitempty"` + P99Millis float64 `json:"p99Millis,omitempty"` + ServerBytesPerRequest float64 `json:"serverBytesPerRequest,omitempty"` + ServerAllocsPerRequest float64 `json:"serverAllocsPerRequest,omitempty"` + SampledPeakHeapAlloc uint64 `json:"sampledPeakHeapAlloc,omitempty"` + IdleHeapDelta int64 `json:"idleHeapDelta,omitempty"` + IdleHeapPerConnection float64 `json:"idleHeapPerConnection,omitempty"` + Before snapshot `json:"before"` + During snapshot `json:"during"` + After snapshot `json:"after"` + AfterReclaim snapshot `json:"afterReclaim"` +} + +type report struct { + CreatedAt string `json:"createdAt"` + GoVersion string `json:"goVersion"` + Platform string `json:"platform"` + GOMAXPROCS int `json:"gomaxprocs"` + Duration string `json:"duration"` + Notes []string `json:"notes"` + Results []result `json:"results"` +} + +type trackedListener struct { + net.Listener + active, accepted atomic.Int64 +} + +type trackedConn struct { + net.Conn + once sync.Once + active *atomic.Int64 +} + +func (l *trackedListener) Accept() (net.Conn, error) { + c, err := l.Listener.Accept() + if err != nil { + return nil, err + } + l.active.Add(1) + l.accepted.Add(1) + return &trackedConn{Conn: c, active: &l.active}, nil +} + +func (c *trackedConn) Close() error { + err := c.Conn.Close() + c.once.Do(func() { c.active.Add(-1) }) + return err +} + +func main() { + serverMode := flag.String("server", "", "internal server subprocess: nethttp or native") + duration := flag.Duration("duration", 2*time.Second, "measured duration per traffic case") + concurrencyText := flag.String("concurrency", "1,32,128", "traffic concurrency levels") + idleText := flag.String("idle", "128,1024", "established idle Keep-Alive connection levels") + runs := flag.Int("count", 3, "repetitions; adapter order alternates each repetition") + output := flag.String("output", "", "optional JSON report path") + flag.Parse() + var err error + if *serverMode != "" { + err = serve(*serverMode) + } else { + err = compare(*duration, *concurrencyText, *idleText, *runs, *output) + } + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func serve(adapter string) error { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return err + } + tracked := &trackedListener{Listener: listener} + application := omni.New() + application.GET("/health", func(c *omni.Context) error { + return c.OK(map[string]string{"status": "ok"}) + }) + done := make(chan error, 1) + var shutdown func(context.Context) error + switch adapter { + case "native": + server := native.New(application) + server.MaxConnections = 8192 + server.MaxConcurrentRequests = 4096 + shutdown = server.Shutdown + go func() { done <- server.Serve(context.Background(), tracked) }() + case "nethttp": + server := &http.Server{ + Handler: application, ReadHeaderTimeout: 5 * time.Second, + ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, + IdleTimeout: time.Minute, MaxHeaderBytes: 1 << 20, + } + shutdown = server.Shutdown + go func() { + err := server.Serve(tracked) + if errors.Is(err, http.ErrServerClosed) { + err = nil + } + done <- err + }() + default: + listener.Close() + return fmt.Errorf("unknown server %q", adapter) + } + stop := func() error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := shutdown(ctx); err != nil { + return err + } + select { + case err := <-done: + return err + case <-ctx.Done(): + return ctx.Err() + } + } + encoder := json.NewEncoder(os.Stdout) + if err := encoder.Encode(snapshot{Address: listener.Addr().String()}); err != nil { + return err + } + decoder := json.NewDecoder(os.Stdin) + for { + var command string + if err := decoder.Decode(&command); err != nil || command == "stop" { + return stop() + } + if command == "gc" { + runtime.GC() + } + var memory runtime.MemStats + runtime.ReadMemStats(&memory) + if err := encoder.Encode(snapshot{ + HeapAlloc: memory.HeapAlloc, HeapInuse: memory.HeapInuse, + StackInuse: memory.StackInuse, TotalAlloc: memory.TotalAlloc, + Mallocs: memory.Mallocs, NumGC: memory.NumGC, + Goroutines: runtime.NumGoroutine(), Connections: tracked.active.Load(), Accepted: tracked.accepted.Load(), + }); err != nil { + return err + } + } +} + +type child struct { + cmd *exec.Cmd + input io.WriteCloser + encoder *json.Encoder + decoder *json.Decoder + address string + cancel context.CancelFunc + closed bool +} + +func startChild(adapter string) (*child, error) { + executable, err := os.Executable() + if err != nil { + return nil, err + } + // This bounds even a broken server or protocol read; it is not a load duration. + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + c := &child{cmd: exec.CommandContext(ctx, executable, "--server", adapter), cancel: cancel} + c.cmd.Stderr = os.Stderr + if c.input, err = c.cmd.StdinPipe(); err != nil { + cancel() + return nil, err + } + output, err := c.cmd.StdoutPipe() + if err != nil { + cancel() + c.input.Close() + return nil, err + } + if err := c.cmd.Start(); err != nil { + cancel() + c.input.Close() + output.Close() + return nil, err + } + c.encoder, c.decoder = json.NewEncoder(c.input), json.NewDecoder(output) + var ready snapshot + if err := c.decoder.Decode(&ready); err != nil { + cancel() + c.cmd.Wait() + return nil, err + } + c.address = ready.Address + return c, nil +} + +func (c *child) snapshot(gc bool) (snapshot, error) { + command := "snapshot" + if gc { + command = "gc" + } + var value snapshot + if err := c.encoder.Encode(command); err != nil { + return value, err + } + err := c.decoder.Decode(&value) + return value, err +} + +func (c *child) close() error { + if c.closed { + return nil + } + c.closed = true + defer c.cancel() + err := c.encoder.Encode("stop") + c.input.Close() + return errors.Join(err, c.cmd.Wait()) +} + +// Histograms use 16 subdivisions per power of two. Percentiles are bucket +// upper bounds (at most about 6.25% rounding), with bounded client memory. +type histogram [1024]uint64 + +func (h *histogram) add(d time.Duration) { + n := uint64(max(d.Nanoseconds(), 1)) + exponent := bits.Len64(n) - 1 + base := uint64(1) << exponent + sub := (n - base) * 16 / base + h[exponent*16+int(sub)]++ +} + +func (h *histogram) percentile(count uint64, percent uint64) float64 { + target := (count*percent + 99) / 100 + if target == 0 { + return 0 + } + var seen uint64 + for bucket, n := range h { + seen += n + if seen >= target { + base := uint64(1) << (bucket / 16) + return float64(base+uint64(bucket%16+1)*base/16) / float64(time.Millisecond) + } + } + return 0 +} + +func exchange(client *http.Client, address string) error { + req, err := http.NewRequest(http.MethodGet, "http://"+address+"/health", nil) + if err != nil { + return err + } + req.Header.Set("X-Request-ID", "perfCheck") + response, err := client.Do(req) + if err != nil { + return err + } + body, readErr := io.ReadAll(io.LimitReader(response.Body, 4096)) + closeErr := response.Body.Close() + if response.StatusCode != 200 || response.Header.Get("X-Request-ID") != "perfCheck" || strings.TrimSpace(string(body)) != expectedBody { + return fmt.Errorf("invalid response status=%d body=%q", response.StatusCode, body) + } + return errors.Join(readErr, closeErr) +} + +func traffic(c *child, concurrency int, duration time.Duration) (result, error) { + r := result{Scenario: "traffic", Concurrency: concurrency} + client := &http.Client{Transport: &http.Transport{ + MaxIdleConns: concurrency, MaxIdleConnsPerHost: concurrency, + MaxConnsPerHost: concurrency, IdleConnTimeout: time.Minute, + }, Timeout: 5 * time.Second} + defer client.CloseIdleConnections() + // Warm every worker before forcing a baseline GC in the server. + warmErrors := make(chan error, concurrency) + for i := 0; i < concurrency; i++ { + go func() { warmErrors <- exchange(client, c.address) }() + } + for i := 0; i < concurrency; i++ { + if err := <-warmErrors; err != nil { + return r, err + } + } + var err error + if r.Before, err = c.snapshot(true); err != nil { + return r, err + } + type workerResult struct { + latencies histogram + count, errors uint64 + firstError string + } + workers := make([]workerResult, concurrency) + start := make(chan struct{}) + var group sync.WaitGroup + var deadline time.Time + for i := range workers { + group.Add(1) + go func(w *workerResult) { + defer group.Done() + <-start + for time.Now().Before(deadline) { + began := time.Now() + err := exchange(client, c.address) + w.latencies.add(time.Since(began)) + w.count++ + if err != nil { + w.errors++ + if w.firstError == "" { + w.firstError = err.Error() + } + } + } + }(&workers[i]) + } + done := make(chan struct{}) + go func() { group.Wait(); close(done) }() + begin := time.Now() + deadline = begin.Add(duration) + close(start) + ticker := time.NewTicker(200 * time.Millisecond) + defer ticker.Stop() + var sampleErr error + loading := true + for loading { + select { + case <-done: + loading = false + case <-ticker.C: + s, err := c.snapshot(false) + if err != nil { + sampleErr = err + } else if s.HeapAlloc > r.SampledPeakHeapAlloc { + r.SampledPeakHeapAlloc = s.HeapAlloc + } + } + } + r.ElapsedSeconds = time.Since(begin).Seconds() + if sampleErr != nil { + return r, sampleErr + } + var latencies histogram + for _, w := range workers { + r.Requests += w.count + r.Errors += w.errors + if r.FirstError == "" { + r.FirstError = w.firstError + } + for i, n := range w.latencies { + latencies[i] += n + } + } + if r.During, err = c.snapshot(false); err != nil { + return r, err + } + r.SampledPeakHeapAlloc = max(r.SampledPeakHeapAlloc, r.During.HeapAlloc) + if r.Requests == 0 { + return r, errors.New("no requests completed") + } + r.RequestsPerSecond = float64(r.Requests-r.Errors) / r.ElapsedSeconds + r.P50Millis = latencies.percentile(r.Requests, 50) + r.P95Millis = latencies.percentile(r.Requests, 95) + r.P99Millis = latencies.percentile(r.Requests, 99) + r.ServerBytesPerRequest = float64(r.During.TotalAlloc-r.Before.TotalAlloc) / float64(r.Requests) + r.ServerAllocsPerRequest = float64(r.During.Mallocs-r.Before.Mallocs) / float64(r.Requests) + client.CloseIdleConnections() + r.After, err = drained(c) + if err == nil { + r.AfterReclaim, err = c.snapshot(true) + } + return r, err +} + +func idle(c *child, count int) (result, error) { + r := result{Scenario: "idle", Concurrency: count} + var err error + if r.Before, err = c.snapshot(true); err != nil { + return r, err + } + connections := make([]net.Conn, 0, count) + defer func() { + for _, conn := range connections { + conn.Close() + } + }() + for i := 0; i < count; i++ { + conn, err := net.DialTimeout("tcp", c.address, 5*time.Second) + if err != nil { + return r, err + } + connections = append(connections, conn) + conn.SetDeadline(time.Now().Add(5 * time.Second)) + if _, err := io.WriteString(conn, "GET /health HTTP/1.1\r\nHost: localhost\r\nX-Request-ID: perfCheck\r\n\r\n"); err != nil { + return r, err + } + response, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: http.MethodGet}) + if err != nil { + return r, err + } + body, readErr := io.ReadAll(response.Body) + response.Body.Close() + if readErr != nil || response.StatusCode != 200 || strings.TrimSpace(string(body)) != expectedBody || response.Close { + return r, fmt.Errorf("invalid Keep-Alive response: status=%d error=%v", response.StatusCode, readErr) + } + conn.SetDeadline(time.Time{}) + } + if r.During, err = c.snapshot(true); err != nil { + return r, err + } + if r.During.Connections != int64(count) { + return r, fmt.Errorf("idle connections: want %d, have %d", count, r.During.Connections) + } + r.IdleHeapDelta = int64(r.During.HeapAlloc) - int64(r.Before.HeapAlloc) + r.IdleHeapPerConnection = float64(r.IdleHeapDelta) / float64(count) + for _, conn := range connections { + conn.Close() + } + connections = nil + r.After, err = drained(c) + if err == nil { + r.AfterReclaim, err = c.snapshot(true) + } + return r, err +} + +func drained(c *child) (snapshot, error) { + deadline := time.Now().Add(5 * time.Second) + for { + s, err := c.snapshot(false) + if err != nil { + return s, err + } + if s.Connections == 0 { + return c.snapshot(true) + } + if time.Now().After(deadline) { + return s, fmt.Errorf("%d connections remained after clients closed", s.Connections) + } + time.Sleep(10 * time.Millisecond) + } +} + +func levels(text string) ([]int, error) { + var values []int + if text == "" { + return values, nil + } + for _, item := range strings.Split(text, ",") { + n, err := strconv.Atoi(strings.TrimSpace(item)) + if err != nil || n < 1 || n > 4096 { + return nil, fmt.Errorf("invalid level %q (expected 1..4096)", item) + } + values = append(values, n) + } + return values, nil +} + +func compare(duration time.Duration, concurrencyText, idleText string, count int, output string) error { + concurrency, err := levels(concurrencyText) + if err != nil { + return err + } + idleLevels, err := levels(idleText) + if err != nil { + return err + } + if count < 1 || duration < 200*time.Millisecond || duration > 30*time.Second || len(concurrency)+len(idleLevels) == 0 { + return errors.New("count must be positive, duration 200ms..30s, and at least one scenario selected") + } + report := report{ + CreatedAt: time.Now().UTC().Format(time.RFC3339), GoVersion: runtime.Version(), + Platform: runtime.GOOS + "/" + runtime.GOARCH, GOMAXPROCS: runtime.GOMAXPROCS(0), Duration: duration.String(), + Notes: []string{ + "Loopback HTTP/1.1 health response, fixed requestId, no TLS, access logs or database.", + "Each case starts a fresh server process; load client runs in the parent process on the same machine.", + "Closed-loop concurrency; throughput is successful responses / actual elapsed time, including drain.", + "Latency percentiles include failures and are histogram upper bounds with <=6.25% rounding.", + "Memory is server-only runtime.MemStats (not RSS or kernel socket memory); sampling interval 200ms.", + "Idle memory measured after a forced GC; delta includes heap growth amortized across connections, excluding stacks.", + "After is after closing clients and one forced GC; afterReclaim is a second GC to observe pooled-cache release.", + "Native limits: 8192 connections / 4096 active requests. These cases do not benchmark overload rejection.", + }, + } + var caseErrors []error + for run := 1; run <= count; run++ { + adapters := []string{"nethttp", "native"} + if run%2 == 0 { + adapters[0], adapters[1] = adapters[1], adapters[0] + } + for _, scenario := range []struct { + name string + levels []int + }{{"traffic", concurrency}, {"idle", idleLevels}} { + for _, level := range scenario.levels { + for _, adapter := range adapters { + c, err := startChild(adapter) + if err != nil { + return err + } + var value result + if scenario.name == "traffic" { + value, err = traffic(c, level, duration) + } else { + value, err = idle(c, level) + } + err = errors.Join(err, c.close()) + value.Adapter, value.Run = adapter, run + if err != nil { + value.Errors++ + value.FirstError = err.Error() + } + report.Results = append(report.Results, value) + if value.Errors > 0 { + caseErrors = append(caseErrors, fmt.Errorf("%s %s/%d run %d: %s", adapter, scenario.name, level, run, value.FirstError)) + } + if scenario.name == "traffic" { + fmt.Printf("%s concurrency=%d run=%d: %.0f req/s p95=%.3fms p99=%.3fms server=%.0f B/req errors=%d\n", adapter, level, run, value.RequestsPerSecond, value.P95Millis, value.P99Millis, value.ServerBytesPerRequest, value.Errors) + } else { + fmt.Printf("%s idle=%d run=%d: server heap delta=%.2f MiB (%.0f B/conn), goroutines=%d->%d->%d errors=%d\n", adapter, level, run, float64(value.IdleHeapDelta)/(1<<20), value.IdleHeapPerConnection, value.Before.Goroutines, value.During.Goroutines, value.After.Goroutines, value.Errors) + } + } + } + } + } + if output != "" { + data, err := json.MarshalIndent(report, "", " ") + if err != nil { + return err + } + if err := os.WriteFile(output, append(data, '\n'), 0644); err != nil { + return err + } + fmt.Printf("Report: %s\n", output) + } + return errors.Join(caseErrors...) +} diff --git a/internal/perfcheck/main_test.go b/internal/perfcheck/main_test.go new file mode 100644 index 0000000..a4c87e8 --- /dev/null +++ b/internal/perfcheck/main_test.go @@ -0,0 +1,63 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +// A load generator must never report a fast failure page as good throughput. +func TestExchangeRejectsInvalidResponses(t *testing.T) { + for _, tt := range []struct { + name string + status int + body string + requestID string + truncated bool + wantOK bool + }{ + {"valid", 200, expectedBody, "perfCheck", false, true}, + {"overload", 503, "server busy", "perfCheck", false, false}, + {"wrongPayload", 200, `{"data":"wrong"}`, "perfCheck", false, false}, + {"missingRequestId", 200, expectedBody, "", false, false}, + {"truncated", 200, expectedBody, "perfCheck", true, false}, + } { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("X-Request-ID") != "perfCheck" { + t.Error("client did not send fixed requestId") + } + w.Header().Set("X-Request-ID", tt.requestID) + if tt.truncated { + w.Header().Set("Content-Length", "4096") + } + w.WriteHeader(tt.status) + _, _ = w.Write([]byte(tt.body)) + })) + defer server.Close() + err := exchange(server.Client(), strings.TrimPrefix(server.URL, "http://")) + if (err == nil) != tt.wantOK { + t.Fatalf("valid=%v, error=%v", tt.wantOK, err) + } + }) + } +} + +func TestPercentileBounds(t *testing.T) { + var h histogram + // Known ranks, including a tail much slower than the median. + for i := 1; i <= 100; i++ { + h.add(time.Duration(i) * time.Millisecond) + } + for _, p := range []uint64{50, 95, 99} { + value := h.percentile(100, p) + if value < float64(p) || value > float64(p)*1.0625 { + t.Fatalf("p%d=%gms outside advertised bucket rounding", p, value) + } + } + if value := h.percentile(0, 95); value != 0 { + t.Fatalf("empty histogram returned %g", value) + } +} diff --git a/performance/tcpBaseline.json b/performance/tcpBaseline.json new file mode 100644 index 0000000..ac4b691 --- /dev/null +++ b/performance/tcpBaseline.json @@ -0,0 +1,1735 @@ +{ + "createdAt": "2026-09-20T18:30:09Z", + "goVersion": "go1.25.0", + "platform": "darwin/arm64", + "gomaxprocs": 12, + "duration": "2s", + "notes": [ + "Loopback HTTP/1.1 health response, fixed requestId, no TLS, access logs or database.", + "Each case starts a fresh server process; load client runs in the parent process on the same machine.", + "Closed-loop concurrency; throughput is successful responses / actual elapsed time, including drain.", + "Latency percentiles include failures and are histogram upper bounds with \u003c=6.25% rounding.", + "Memory is server-only runtime.MemStats (not RSS or kernel socket memory); sampling interval 200ms.", + "Idle memory measured after a forced GC; delta includes heap growth amortized across connections, excluding stacks.", + "After is after closing clients and one forced GC; afterReclaim is a second GC to observe pooled-cache release.", + "Native limits: 8192 connections / 4096 active requests. These cases do not benchmark overload rejection." + ], + "results": [ + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 1, + "run": 1, + "requests": 39462, + "errors": 0, + "elapsedSeconds": 2.000005, + "requestsPerSecond": 19730.95067262332, + "p50Millis": 0.047104, + "p95Millis": 0.077824, + "p99Millis": 0.131072, + "serverBytesPerRequest": 4454.37697024986, + "serverAllocsPerRequest": 47.08965587147129, + "sampledPeakHeapAlloc": 3492736, + "before": { + "heapAlloc": 1636640, + "heapInuse": 3588096, + "stackInuse": 557056, + "totalAlloc": 3078640, + "mallocs": 23478, + "numGc": 2, + "goroutines": 3, + "connections": 1, + "accepted": 1 + }, + "during": { + "heapAlloc": 1944200, + "heapInuse": 3997696, + "stackInuse": 851968, + "totalAlloc": 178857264, + "mallocs": 1881730, + "numGc": 101, + "goroutines": 3, + "connections": 1, + "accepted": 1 + }, + "after": { + "heapAlloc": 1719816, + "heapInuse": 3702784, + "stackInuse": 786432, + "totalAlloc": 178861424, + "mallocs": 1881740, + "numGc": 102, + "goroutines": 2, + "connections": 0, + "accepted": 1 + }, + "afterReclaim": { + "heapAlloc": 1692416, + "heapInuse": 3571712, + "stackInuse": 786432, + "totalAlloc": 178863608, + "mallocs": 1881746, + "numGc": 103, + "goroutines": 2, + "connections": 0, + "accepted": 1 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 1, + "run": 1, + "requests": 43512, + "errors": 0, + "elapsedSeconds": 2.000188292, + "requestsPerSecond": 21753.95195243949, + "p50Millis": 0.043008, + "p95Millis": 0.069632, + "p99Millis": 0.114688, + "serverBytesPerRequest": 4295.838757124471, + "serverAllocsPerRequest": 55.07106085677514, + "sampledPeakHeapAlloc": 2835696, + "before": { + "heapAlloc": 1633112, + "heapInuse": 3604480, + "stackInuse": 622592, + "totalAlloc": 3074824, + "mallocs": 23466, + "numGc": 2, + "goroutines": 4, + "connections": 1, + "accepted": 1 + }, + "during": { + "heapAlloc": 2466608, + "heapInuse": 4390912, + "stackInuse": 688128, + "totalAlloc": 189995360, + "mallocs": 2419718, + "numGc": 108, + "goroutines": 4, + "connections": 1, + "accepted": 1 + }, + "after": { + "heapAlloc": 1658936, + "heapInuse": 3776512, + "stackInuse": 622592, + "totalAlloc": 189999360, + "mallocs": 2419730, + "numGc": 109, + "goroutines": 3, + "connections": 0, + "accepted": 1 + }, + "afterReclaim": { + "heapAlloc": 1645576, + "heapInuse": 3604480, + "stackInuse": 622592, + "totalAlloc": 190001288, + "mallocs": 2419735, + "numGc": 110, + "goroutines": 3, + "connections": 0, + "accepted": 1 + } + }, + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 32, + "run": 1, + "requests": 176070, + "errors": 0, + "elapsedSeconds": 2.00058625, + "requestsPerSecond": 88009.20230257505, + "p50Millis": 0.278528, + "p95Millis": 1.048576, + "p99Millis": 1.441792, + "serverBytesPerRequest": 4468.189061168853, + "serverAllocsPerRequest": 47.11240983699665, + "sampledPeakHeapAlloc": 4113888, + "before": { + "heapAlloc": 1993880, + "heapInuse": 4628480, + "stackInuse": 1146880, + "totalAlloc": 3604480, + "mallocs": 25939, + "numGc": 3, + "goroutines": 34, + "connections": 32, + "accepted": 32 + }, + "during": { + "heapAlloc": 3577608, + "heapInuse": 6414336, + "stackInuse": 2293760, + "totalAlloc": 790318528, + "mallocs": 8321021, + "numGc": 471, + "goroutines": 34, + "connections": 32, + "accepted": 32 + }, + "after": { + "heapAlloc": 2244384, + "heapInuse": 4669440, + "stackInuse": 1966080, + "totalAlloc": 790324576, + "mallocs": 8321053, + "numGc": 472, + "goroutines": 2, + "connections": 0, + "accepted": 32 + }, + "afterReclaim": { + "heapAlloc": 1934808, + "heapInuse": 3801088, + "stackInuse": 1966080, + "totalAlloc": 790326760, + "mallocs": 8321060, + "numGc": 473, + "goroutines": 2, + "connections": 0, + "accepted": 32 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 32, + "run": 1, + "requests": 170185, + "errors": 0, + "elapsedSeconds": 2.000530708, + "requestsPerSecond": 85069.92635476206, + "p50Millis": 0.278528, + "p95Millis": 1.048576, + "p99Millis": 1.572864, + "serverBytesPerRequest": 4305.3017833534095, + "serverAllocsPerRequest": 55.133631048564794, + "sampledPeakHeapAlloc": 3410920, + "before": { + "heapAlloc": 1814112, + "heapInuse": 4349952, + "stackInuse": 950272, + "totalAlloc": 3390600, + "mallocs": 25714, + "numGc": 2, + "goroutines": 35, + "connections": 32, + "accepted": 32 + }, + "during": { + "heapAlloc": 2055752, + "heapInuse": 5341184, + "stackInuse": 1146880, + "totalAlloc": 736088384, + "mallocs": 9408631, + "numGc": 555, + "goroutines": 35, + "connections": 32, + "accepted": 32 + }, + "after": { + "heapAlloc": 1896016, + "heapInuse": 4407296, + "stackInuse": 786432, + "totalAlloc": 736095248, + "mallocs": 9408674, + "numGc": 556, + "goroutines": 3, + "connections": 0, + "accepted": 32 + }, + "afterReclaim": { + "heapAlloc": 1728488, + "heapInuse": 3743744, + "stackInuse": 786432, + "totalAlloc": 736097432, + "mallocs": 9408680, + "numGc": 557, + "goroutines": 3, + "connections": 0, + "accepted": 32 + } + }, + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 128, + "run": 1, + "requests": 224900, + "errors": 0, + "elapsedSeconds": 2.000502334, + "requestsPerSecond": 112421.76336296141, + "p50Millis": 0.8192, + "p95Millis": 3.2768, + "p99Millis": 4.718592, + "serverBytesPerRequest": 4452.596282792352, + "serverAllocsPerRequest": 47.1292352156514, + "sampledPeakHeapAlloc": 6644048, + "before": { + "heapAlloc": 2944776, + "heapInuse": 5505024, + "stackInuse": 1802240, + "totalAlloc": 5016680, + "mallocs": 32887, + "numGc": 4, + "goroutines": 128, + "connections": 126, + "accepted": 126 + }, + "during": { + "heapAlloc": 4749256, + "heapInuse": 7782400, + "stackInuse": 3080192, + "totalAlloc": 1006405584, + "mallocs": 10632252, + "numGc": 371, + "goroutines": 130, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 3130128, + "heapInuse": 5816320, + "stackInuse": 1933312, + "totalAlloc": 1006417136, + "mallocs": 10632332, + "numGc": 372, + "goroutines": 2, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1997352, + "heapInuse": 3850240, + "stackInuse": 1933312, + "totalAlloc": 1006419320, + "mallocs": 10632338, + "numGc": 373, + "goroutines": 2, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 128, + "run": 1, + "requests": 223237, + "errors": 0, + "elapsedSeconds": 2.0005375, + "requestsPerSecond": 111588.51058777953, + "p50Millis": 0.786432, + "p95Millis": 3.407872, + "p99Millis": 4.980736, + "serverBytesPerRequest": 4297.285019956369, + "serverAllocsPerRequest": 55.094782674915, + "sampledPeakHeapAlloc": 4607856, + "before": { + "heapAlloc": 2246848, + "heapInuse": 4734976, + "stackInuse": 1736704, + "totalAlloc": 4255872, + "mallocs": 32172, + "numGc": 3, + "goroutines": 115, + "connections": 112, + "accepted": 112 + }, + "during": { + "heapAlloc": 3420024, + "heapInuse": 6258688, + "stackInuse": 1966080, + "totalAlloc": 963568888, + "mallocs": 12331366, + "numGc": 460, + "goroutines": 131, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 2435264, + "heapInuse": 4907008, + "stackInuse": 819200, + "totalAlloc": 963580728, + "mallocs": 12331438, + "numGc": 461, + "goroutines": 3, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1810152, + "heapInuse": 3710976, + "stackInuse": 819200, + "totalAlloc": 963582912, + "mallocs": 12331445, + "numGc": 462, + "goroutines": 3, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "nethttp", + "scenario": "idle", + "concurrency": 128, + "run": 1, + "errors": 0, + "idleHeapDelta": 1336888, + "idleHeapPerConnection": 10444.4375, + "before": { + "heapAlloc": 1612376, + "heapInuse": 3547136, + "stackInuse": 491520, + "totalAlloc": 3036888, + "mallocs": 23305, + "numGc": 1, + "goroutines": 2, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 2949264, + "heapInuse": 5251072, + "stackInuse": 1769472, + "totalAlloc": 4969088, + "mallocs": 32512, + "numGc": 3, + "goroutines": 130, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 2805744, + "heapInuse": 5062656, + "stackInuse": 753664, + "totalAlloc": 4994936, + "mallocs": 32623, + "numGc": 4, + "goroutines": 2, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1724472, + "heapInuse": 3760128, + "stackInuse": 753664, + "totalAlloc": 4997424, + "mallocs": 32633, + "numGc": 5, + "goroutines": 2, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "native", + "scenario": "idle", + "concurrency": 128, + "run": 1, + "errors": 0, + "idleHeapDelta": 696072, + "idleHeapPerConnection": 5438.0625, + "before": { + "heapAlloc": 1606384, + "heapInuse": 3407872, + "stackInuse": 458752, + "totalAlloc": 3030960, + "mallocs": 23288, + "numGc": 1, + "goroutines": 3, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 2302456, + "heapInuse": 4186112, + "stackInuse": 1605632, + "totalAlloc": 4289200, + "mallocs": 31756, + "numGc": 2, + "goroutines": 131, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 2329080, + "heapInuse": 4300800, + "stackInuse": 655360, + "totalAlloc": 4325544, + "mallocs": 31874, + "numGc": 3, + "goroutines": 3, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1715600, + "heapInuse": 3514368, + "stackInuse": 655360, + "totalAlloc": 4328048, + "mallocs": 31885, + "numGc": 4, + "goroutines": 3, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "nethttp", + "scenario": "idle", + "concurrency": 1024, + "run": 1, + "errors": 0, + "idleHeapDelta": 10350192, + "idleHeapPerConnection": 10107.609375, + "before": { + "heapAlloc": 1612120, + "heapInuse": 3473408, + "stackInuse": 491520, + "totalAlloc": 3036664, + "mallocs": 23304, + "numGc": 1, + "goroutines": 2, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 11962312, + "heapInuse": 14548992, + "stackInuse": 9175040, + "totalAlloc": 18019736, + "mallocs": 95457, + "numGc": 5, + "goroutines": 1026, + "connections": 1024, + "accepted": 1024 + }, + "after": { + "heapAlloc": 10805352, + "heapInuse": 13303808, + "stackInuse": 819200, + "totalAlloc": 18086560, + "mallocs": 95671, + "numGc": 6, + "goroutines": 2, + "connections": 0, + "accepted": 1024 + }, + "afterReclaim": { + "heapAlloc": 2200016, + "heapInuse": 4202496, + "stackInuse": 819200, + "totalAlloc": 18089048, + "mallocs": 95681, + "numGc": 7, + "goroutines": 2, + "connections": 0, + "accepted": 1024 + } + }, + { + "adapter": "native", + "scenario": "idle", + "concurrency": 1024, + "run": 1, + "errors": 0, + "idleHeapDelta": 5373232, + "idleHeapPerConnection": 5247.296875, + "before": { + "heapAlloc": 1617360, + "heapInuse": 3465216, + "stackInuse": 524288, + "totalAlloc": 3041840, + "mallocs": 23300, + "numGc": 1, + "goroutines": 3, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 6990592, + "heapInuse": 9207808, + "stackInuse": 9109504, + "totalAlloc": 12887640, + "mallocs": 90134, + "numGc": 5, + "goroutines": 1027, + "connections": 1024, + "accepted": 1024 + }, + "after": { + "heapAlloc": 7047376, + "heapInuse": 9469952, + "stackInuse": 720896, + "totalAlloc": 12954384, + "mallocs": 90350, + "numGc": 6, + "goroutines": 3, + "connections": 0, + "accepted": 1024 + }, + "afterReclaim": { + "heapAlloc": 2181080, + "heapInuse": 4104192, + "stackInuse": 720896, + "totalAlloc": 12956872, + "mallocs": 90360, + "numGc": 7, + "goroutines": 3, + "connections": 0, + "accepted": 1024 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 1, + "run": 2, + "requests": 42996, + "errors": 0, + "elapsedSeconds": 2.000068417, + "requestsPerSecond": 21497.26461082356, + "p50Millis": 0.043008, + "p95Millis": 0.069632, + "p99Millis": 0.118784, + "serverBytesPerRequest": 4295.052935156758, + "serverAllocsPerRequest": 55.067494650665175, + "sampledPeakHeapAlloc": 3492952, + "before": { + "heapAlloc": 1641992, + "heapInuse": 3637248, + "stackInuse": 655360, + "totalAlloc": 3078328, + "mallocs": 23471, + "numGc": 2, + "goroutines": 4, + "connections": 1, + "accepted": 1 + }, + "during": { + "heapAlloc": 3492952, + "heapInuse": 5332992, + "stackInuse": 688128, + "totalAlloc": 187748424, + "mallocs": 2391153, + "numGc": 103, + "goroutines": 4, + "connections": 1, + "accepted": 1 + }, + "after": { + "heapAlloc": 1658088, + "heapInuse": 3801088, + "stackInuse": 622592, + "totalAlloc": 187752424, + "mallocs": 2391165, + "numGc": 104, + "goroutines": 3, + "connections": 0, + "accepted": 1 + }, + "afterReclaim": { + "heapAlloc": 1644968, + "heapInuse": 3588096, + "stackInuse": 622592, + "totalAlloc": 187754608, + "mallocs": 2391171, + "numGc": 105, + "goroutines": 3, + "connections": 0, + "accepted": 1 + } + }, + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 1, + "run": 2, + "requests": 39687, + "errors": 0, + "elapsedSeconds": 2.000020666, + "requestsPerSecond": 19843.294959233186, + "p50Millis": 0.047104, + "p95Millis": 0.073728, + "p99Millis": 0.139264, + "serverBytesPerRequest": 4457.090735001386, + "serverAllocsPerRequest": 47.096757124499206, + "sampledPeakHeapAlloc": 3423696, + "before": { + "heapAlloc": 1638832, + "heapInuse": 3612672, + "stackInuse": 557056, + "totalAlloc": 3080176, + "mallocs": 23476, + "numGc": 2, + "goroutines": 3, + "connections": 1, + "accepted": 1 + }, + "during": { + "heapAlloc": 2571392, + "heapInuse": 4505600, + "stackInuse": 786432, + "totalAlloc": 179968736, + "mallocs": 1892605, + "numGc": 103, + "goroutines": 3, + "connections": 1, + "accepted": 1 + }, + "after": { + "heapAlloc": 1715896, + "heapInuse": 3710976, + "stackInuse": 753664, + "totalAlloc": 179973024, + "mallocs": 1892618, + "numGc": 104, + "goroutines": 2, + "connections": 0, + "accepted": 1 + }, + "afterReclaim": { + "heapAlloc": 1683648, + "heapInuse": 3555328, + "stackInuse": 753664, + "totalAlloc": 179974952, + "mallocs": 1892623, + "numGc": 105, + "goroutines": 2, + "connections": 0, + "accepted": 1 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 32, + "run": 2, + "requests": 177949, + "errors": 0, + "elapsedSeconds": 2.000303583, + "requestsPerSecond": 88960.99647690328, + "p50Millis": 0.278528, + "p95Millis": 0.98304, + "p99Millis": 1.507328, + "serverBytesPerRequest": 4304.238135645606, + "serverAllocsPerRequest": 55.12900325374124, + "sampledPeakHeapAlloc": 3368856, + "before": { + "heapAlloc": 1823912, + "heapInuse": 4300800, + "stackInuse": 1015808, + "totalAlloc": 3402320, + "mallocs": 25726, + "numGc": 2, + "goroutines": 35, + "connections": 32, + "accepted": 32 + }, + "during": { + "heapAlloc": 2226712, + "heapInuse": 5480448, + "stackInuse": 1179648, + "totalAlloc": 769337192, + "mallocs": 9835877, + "numGc": 548, + "goroutines": 35, + "connections": 32, + "accepted": 32 + }, + "after": { + "heapAlloc": 1904896, + "heapInuse": 4292608, + "stackInuse": 819200, + "totalAlloc": 769343464, + "mallocs": 9835909, + "numGc": 549, + "goroutines": 3, + "connections": 0, + "accepted": 32 + }, + "afterReclaim": { + "heapAlloc": 1736120, + "heapInuse": 3661824, + "stackInuse": 819200, + "totalAlloc": 769345392, + "mallocs": 9835914, + "numGc": 550, + "goroutines": 3, + "connections": 0, + "accepted": 32 + } + }, + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 32, + "run": 2, + "requests": 180248, + "errors": 0, + "elapsedSeconds": 2.00056525, + "requestsPerSecond": 90098.53590129089, + "p50Millis": 0.278528, + "p95Millis": 1.015808, + "p99Millis": 1.507328, + "serverBytesPerRequest": 4470.327282410901, + "serverAllocsPerRequest": 47.11734942967467, + "sampledPeakHeapAlloc": 4070832, + "before": { + "heapAlloc": 2008240, + "heapInuse": 4915200, + "stackInuse": 1081344, + "totalAlloc": 3589376, + "mallocs": 25901, + "numGc": 2, + "goroutines": 34, + "connections": 32, + "accepted": 32 + }, + "during": { + "heapAlloc": 4024272, + "heapInuse": 6840320, + "stackInuse": 2490368, + "totalAlloc": 809356928, + "mallocs": 8518709, + "numGc": 479, + "goroutines": 34, + "connections": 32, + "accepted": 32 + }, + "after": { + "heapAlloc": 2221984, + "heapInuse": 4505600, + "stackInuse": 2195456, + "totalAlloc": 809364400, + "mallocs": 8518759, + "numGc": 481, + "goroutines": 2, + "connections": 0, + "accepted": 32 + }, + "afterReclaim": { + "heapAlloc": 1929720, + "heapInuse": 3833856, + "stackInuse": 2195456, + "totalAlloc": 809366328, + "mallocs": 8518764, + "numGc": 482, + "goroutines": 2, + "connections": 0, + "accepted": 32 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 128, + "run": 2, + "requests": 224333, + "errors": 0, + "elapsedSeconds": 2.00109875, + "requestsPerSecond": 112104.91236377014, + "p50Millis": 0.753664, + "p95Millis": 3.407872, + "p99Millis": 4.980736, + "serverBytesPerRequest": 4297.053148667383, + "serverAllocsPerRequest": 55.09475645580454, + "sampledPeakHeapAlloc": 4655184, + "before": { + "heapAlloc": 2339440, + "heapInuse": 4653056, + "stackInuse": 1671168, + "totalAlloc": 4342800, + "mallocs": 32315, + "numGc": 3, + "goroutines": 131, + "connections": 128, + "accepted": 128 + }, + "during": { + "heapAlloc": 4337936, + "heapInuse": 6758400, + "stackInuse": 1998848, + "totalAlloc": 968313624, + "mallocs": 12391887, + "numGc": 468, + "goroutines": 131, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 2454464, + "heapInuse": 5029888, + "stackInuse": 819200, + "totalAlloc": 968326008, + "mallocs": 12391973, + "numGc": 469, + "goroutines": 3, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1827784, + "heapInuse": 3710976, + "stackInuse": 819200, + "totalAlloc": 968328192, + "mallocs": 12391980, + "numGc": 470, + "goroutines": 3, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 128, + "run": 2, + "requests": 224658, + "errors": 0, + "elapsedSeconds": 2.000817709, + "requestsPerSecond": 112283.09255233605, + "p50Millis": 0.8192, + "p95Millis": 3.2768, + "p99Millis": 4.718592, + "serverBytesPerRequest": 4453.685655529738, + "serverAllocsPerRequest": 47.128546501793835, + "sampledPeakHeapAlloc": 7418920, + "before": { + "heapAlloc": 2955384, + "heapInuse": 5586944, + "stackInuse": 1802240, + "totalAlloc": 5029472, + "mallocs": 32921, + "numGc": 4, + "goroutines": 130, + "connections": 128, + "accepted": 128 + }, + "during": { + "heapAlloc": 4259776, + "heapInuse": 7544832, + "stackInuse": 3211264, + "totalAlloc": 1005585584, + "mallocs": 10620726, + "numGc": 372, + "goroutines": 130, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 3135984, + "heapInuse": 5906432, + "stackInuse": 2064384, + "totalAlloc": 1005598608, + "mallocs": 10620814, + "numGc": 373, + "goroutines": 2, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 2010472, + "heapInuse": 3883008, + "stackInuse": 2064384, + "totalAlloc": 1005600792, + "mallocs": 10620820, + "numGc": 374, + "goroutines": 2, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "native", + "scenario": "idle", + "concurrency": 128, + "run": 2, + "errors": 0, + "idleHeapDelta": 689768, + "idleHeapPerConnection": 5388.8125, + "before": { + "heapAlloc": 1617504, + "heapInuse": 3457024, + "stackInuse": 524288, + "totalAlloc": 3042080, + "mallocs": 23302, + "numGc": 1, + "goroutines": 3, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 2307272, + "heapInuse": 4218880, + "stackInuse": 1572864, + "totalAlloc": 4293424, + "mallocs": 31770, + "numGc": 2, + "goroutines": 131, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 2327464, + "heapInuse": 4308992, + "stackInuse": 622592, + "totalAlloc": 4323144, + "mallocs": 31873, + "numGc": 3, + "goroutines": 3, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1715264, + "heapInuse": 3538944, + "stackInuse": 622592, + "totalAlloc": 4325648, + "mallocs": 31884, + "numGc": 4, + "goroutines": 3, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "nethttp", + "scenario": "idle", + "concurrency": 128, + "run": 2, + "errors": 0, + "idleHeapDelta": 1319256, + "idleHeapPerConnection": 10306.6875, + "before": { + "heapAlloc": 1617432, + "heapInuse": 3571712, + "stackInuse": 524288, + "totalAlloc": 3041784, + "mallocs": 23308, + "numGc": 1, + "goroutines": 2, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 2936688, + "heapInuse": 5120000, + "stackInuse": 1638400, + "totalAlloc": 4958824, + "mallocs": 32504, + "numGc": 3, + "goroutines": 130, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 2795104, + "heapInuse": 4898816, + "stackInuse": 655360, + "totalAlloc": 4987808, + "mallocs": 32596, + "numGc": 4, + "goroutines": 2, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1715096, + "heapInuse": 3694592, + "stackInuse": 655360, + "totalAlloc": 4990392, + "mallocs": 32607, + "numGc": 5, + "goroutines": 2, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "native", + "scenario": "idle", + "concurrency": 1024, + "run": 2, + "errors": 0, + "idleHeapDelta": 5389360, + "idleHeapPerConnection": 5263.046875, + "before": { + "heapAlloc": 1601072, + "heapInuse": 3497984, + "stackInuse": 425984, + "totalAlloc": 3025616, + "mallocs": 23282, + "numGc": 1, + "goroutines": 3, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 6990432, + "heapInuse": 9404416, + "stackInuse": 9109504, + "totalAlloc": 12886536, + "mallocs": 90180, + "numGc": 5, + "goroutines": 1027, + "connections": 1024, + "accepted": 1024 + }, + "after": { + "heapAlloc": 7047376, + "heapInuse": 9519104, + "stackInuse": 753664, + "totalAlloc": 12955776, + "mallocs": 90406, + "numGc": 6, + "goroutines": 3, + "connections": 0, + "accepted": 1024 + }, + "afterReclaim": { + "heapAlloc": 2183336, + "heapInuse": 4177920, + "stackInuse": 753664, + "totalAlloc": 12957704, + "mallocs": 90411, + "numGc": 7, + "goroutines": 3, + "connections": 0, + "accepted": 1024 + } + }, + { + "adapter": "nethttp", + "scenario": "idle", + "concurrency": 1024, + "run": 2, + "errors": 0, + "idleHeapDelta": 10342088, + "idleHeapPerConnection": 10099.6953125, + "before": { + "heapAlloc": 1611912, + "heapInuse": 3473408, + "stackInuse": 491520, + "totalAlloc": 3036504, + "mallocs": 23303, + "numGc": 1, + "goroutines": 2, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 11954000, + "heapInuse": 14442496, + "stackInuse": 9076736, + "totalAlloc": 18004552, + "mallocs": 95428, + "numGc": 5, + "goroutines": 1026, + "connections": 1024, + "accepted": 1024 + }, + "after": { + "heapAlloc": 10809800, + "heapInuse": 13328384, + "stackInuse": 786432, + "totalAlloc": 18086432, + "mallocs": 95667, + "numGc": 6, + "goroutines": 2, + "connections": 0, + "accepted": 1024 + }, + "afterReclaim": { + "heapAlloc": 2199616, + "heapInuse": 4186112, + "stackInuse": 786432, + "totalAlloc": 18088360, + "mallocs": 95672, + "numGc": 7, + "goroutines": 2, + "connections": 0, + "accepted": 1024 + } + }, + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 1, + "run": 3, + "requests": 38650, + "errors": 0, + "elapsedSeconds": 2.000164708, + "requestsPerSecond": 19323.40864000486, + "p50Millis": 0.049152, + "p95Millis": 0.073728, + "p99Millis": 0.139264, + "serverBytesPerRequest": 4456.037050452781, + "serverAllocsPerRequest": 47.096765847347996, + "sampledPeakHeapAlloc": 3494472, + "before": { + "heapAlloc": 1649600, + "heapInuse": 3751936, + "stackInuse": 622592, + "totalAlloc": 3091392, + "mallocs": 23493, + "numGc": 2, + "goroutines": 3, + "connections": 1, + "accepted": 1 + }, + "during": { + "heapAlloc": 2591800, + "heapInuse": 4653056, + "stackInuse": 851968, + "totalAlloc": 175317224, + "mallocs": 1843783, + "numGc": 100, + "goroutines": 3, + "connections": 1, + "accepted": 1 + }, + "after": { + "heapAlloc": 1723328, + "heapInuse": 3842048, + "stackInuse": 786432, + "totalAlloc": 175321256, + "mallocs": 1843795, + "numGc": 101, + "goroutines": 2, + "connections": 0, + "accepted": 1 + }, + "afterReclaim": { + "heapAlloc": 1693408, + "heapInuse": 3702784, + "stackInuse": 786432, + "totalAlloc": 175323440, + "mallocs": 1843801, + "numGc": 102, + "goroutines": 2, + "connections": 0, + "accepted": 1 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 1, + "run": 3, + "requests": 42891, + "errors": 0, + "elapsedSeconds": 2.000075208, + "requestsPerSecond": 21444.6935937421, + "p50Millis": 0.045056, + "p95Millis": 0.069632, + "p99Millis": 0.12288, + "serverBytesPerRequest": 4295.194982630389, + "serverAllocsPerRequest": 55.0668205451027, + "sampledPeakHeapAlloc": 3197328, + "before": { + "heapAlloc": 1638536, + "heapInuse": 3629056, + "stackInuse": 622592, + "totalAlloc": 3078360, + "mallocs": 23471, + "numGc": 2, + "goroutines": 4, + "connections": 1, + "accepted": 1 + }, + "during": { + "heapAlloc": 1716328, + "heapInuse": 4079616, + "stackInuse": 655360, + "totalAlloc": 187303568, + "mallocs": 2385342, + "numGc": 105, + "goroutines": 4, + "connections": 1, + "accepted": 1 + }, + "after": { + "heapAlloc": 1659704, + "heapInuse": 3760128, + "stackInuse": 589824, + "totalAlloc": 187307568, + "mallocs": 2385354, + "numGc": 106, + "goroutines": 3, + "connections": 0, + "accepted": 1 + }, + "afterReclaim": { + "heapAlloc": 1645768, + "heapInuse": 3620864, + "stackInuse": 589824, + "totalAlloc": 187309752, + "mallocs": 2385360, + "numGc": 107, + "goroutines": 3, + "connections": 0, + "accepted": 1 + } + }, + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 32, + "run": 3, + "requests": 182070, + "errors": 0, + "elapsedSeconds": 2.000483125, + "requestsPerSecond": 91013.0146686441, + "p50Millis": 0.278528, + "p95Millis": 0.98304, + "p99Millis": 1.507328, + "serverBytesPerRequest": 4470.001120448179, + "serverAllocsPerRequest": 47.11588949305212, + "sampledPeakHeapAlloc": 4037832, + "before": { + "heapAlloc": 1992024, + "heapInuse": 4562944, + "stackInuse": 1081344, + "totalAlloc": 3573640, + "mallocs": 25843, + "numGc": 2, + "goroutines": 33, + "connections": 31, + "accepted": 31 + }, + "during": { + "heapAlloc": 2638056, + "heapInuse": 6004736, + "stackInuse": 2359296, + "totalAlloc": 817426744, + "mallocs": 8604233, + "numGc": 487, + "goroutines": 34, + "connections": 32, + "accepted": 32 + }, + "after": { + "heapAlloc": 2239528, + "heapInuse": 4653056, + "stackInuse": 2031616, + "totalAlloc": 817432888, + "mallocs": 8604269, + "numGc": 488, + "goroutines": 2, + "connections": 0, + "accepted": 32 + }, + "afterReclaim": { + "heapAlloc": 1931720, + "heapInuse": 3874816, + "stackInuse": 2031616, + "totalAlloc": 817434816, + "mallocs": 8604274, + "numGc": 489, + "goroutines": 2, + "connections": 0, + "accepted": 32 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 32, + "run": 3, + "requests": 177639, + "errors": 0, + "elapsedSeconds": 2.000176, + "requestsPerSecond": 88811.68457175768, + "p50Millis": 0.278528, + "p95Millis": 0.98304, + "p99Millis": 1.507328, + "serverBytesPerRequest": 4303.6634072472825, + "serverAllocsPerRequest": 55.12404370661848, + "sampledPeakHeapAlloc": 3737072, + "before": { + "heapAlloc": 1811496, + "heapInuse": 4235264, + "stackInuse": 851968, + "totalAlloc": 3387360, + "mallocs": 25686, + "numGc": 2, + "goroutines": 35, + "connections": 32, + "accepted": 32 + }, + "during": { + "heapAlloc": 3257280, + "heapInuse": 6094848, + "stackInuse": 1081344, + "totalAlloc": 767885824, + "mallocs": 9817866, + "numGc": 545, + "goroutines": 35, + "connections": 32, + "accepted": 32 + }, + "after": { + "heapAlloc": 1898800, + "heapInuse": 4390912, + "stackInuse": 753664, + "totalAlloc": 767892400, + "mallocs": 9817901, + "numGc": 546, + "goroutines": 3, + "connections": 0, + "accepted": 32 + }, + "afterReclaim": { + "heapAlloc": 1721880, + "heapInuse": 3620864, + "stackInuse": 753664, + "totalAlloc": 767894328, + "mallocs": 9817906, + "numGc": 547, + "goroutines": 3, + "connections": 0, + "accepted": 32 + } + }, + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 128, + "run": 3, + "requests": 225710, + "errors": 0, + "elapsedSeconds": 2.000534625, + "requestsPerSecond": 112824.84050982122, + "p50Millis": 0.8192, + "p95Millis": 3.2768, + "p99Millis": 4.718592, + "serverBytesPerRequest": 4453.873873554561, + "serverAllocsPerRequest": 47.13242213459749, + "sampledPeakHeapAlloc": 6310984, + "before": { + "heapAlloc": 2983744, + "heapInuse": 5758976, + "stackInuse": 1835008, + "totalAlloc": 5057400, + "mallocs": 33052, + "numGc": 4, + "goroutines": 130, + "connections": 128, + "accepted": 128 + }, + "during": { + "heapAlloc": 5150088, + "heapInuse": 8052736, + "stackInuse": 3145728, + "totalAlloc": 1010341272, + "mallocs": 10671311, + "numGc": 372, + "goroutines": 130, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 3134480, + "heapInuse": 5857280, + "stackInuse": 2064384, + "totalAlloc": 1010353432, + "mallocs": 10671395, + "numGc": 373, + "goroutines": 2, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 2012600, + "heapInuse": 3964928, + "stackInuse": 2064384, + "totalAlloc": 1010355616, + "mallocs": 10671401, + "numGc": 374, + "goroutines": 2, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 128, + "run": 3, + "requests": 226631, + "errors": 0, + "elapsedSeconds": 2.001603042, + "requestsPerSecond": 113224.74798676888, + "p50Millis": 0.753664, + "p95Millis": 3.407872, + "p99Millis": 4.980736, + "serverBytesPerRequest": 4296.581879795792, + "serverAllocsPerRequest": 55.092396891863864, + "sampledPeakHeapAlloc": 5363864, + "before": { + "heapAlloc": 2325992, + "heapInuse": 4759552, + "stackInuse": 1802240, + "totalAlloc": 4333848, + "mallocs": 32320, + "numGc": 3, + "goroutines": 131, + "connections": 128, + "accepted": 128 + }, + "during": { + "heapAlloc": 3131560, + "heapInuse": 6258688, + "stackInuse": 1933312, + "totalAlloc": 978072496, + "mallocs": 12517965, + "numGc": 465, + "goroutines": 131, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 2423744, + "heapInuse": 4964352, + "stackInuse": 786432, + "totalAlloc": 978083408, + "mallocs": 12518041, + "numGc": 466, + "goroutines": 3, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1799736, + "heapInuse": 3743744, + "stackInuse": 786432, + "totalAlloc": 978085592, + "mallocs": 12518048, + "numGc": 467, + "goroutines": 3, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "nethttp", + "scenario": "idle", + "concurrency": 128, + "run": 3, + "errors": 0, + "idleHeapDelta": 1344288, + "idleHeapPerConnection": 10502.25, + "before": { + "heapAlloc": 1606824, + "heapInuse": 3555328, + "stackInuse": 458752, + "totalAlloc": 3031240, + "mallocs": 23298, + "numGc": 1, + "goroutines": 2, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 2951112, + "heapInuse": 5152768, + "stackInuse": 1769472, + "totalAlloc": 4966488, + "mallocs": 32511, + "numGc": 3, + "goroutines": 130, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 2796160, + "heapInuse": 4907008, + "stackInuse": 720896, + "totalAlloc": 4985888, + "mallocs": 32605, + "numGc": 4, + "goroutines": 2, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1715608, + "heapInuse": 3686400, + "stackInuse": 720896, + "totalAlloc": 4987816, + "mallocs": 32610, + "numGc": 5, + "goroutines": 2, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "native", + "scenario": "idle", + "concurrency": 128, + "run": 3, + "errors": 0, + "idleHeapDelta": 689768, + "idleHeapPerConnection": 5388.8125, + "before": { + "heapAlloc": 1616480, + "heapInuse": 3473408, + "stackInuse": 524288, + "totalAlloc": 3041072, + "mallocs": 23296, + "numGc": 1, + "goroutines": 3, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 2306248, + "heapInuse": 4227072, + "stackInuse": 1605632, + "totalAlloc": 4291856, + "mallocs": 31759, + "numGc": 2, + "goroutines": 131, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 2333208, + "heapInuse": 4358144, + "stackInuse": 688128, + "totalAlloc": 4328088, + "mallocs": 31876, + "numGc": 3, + "goroutines": 3, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1719584, + "heapInuse": 3571712, + "stackInuse": 688128, + "totalAlloc": 4330688, + "mallocs": 31888, + "numGc": 4, + "goroutines": 3, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "nethttp", + "scenario": "idle", + "concurrency": 1024, + "run": 3, + "errors": 0, + "idleHeapDelta": 10351144, + "idleHeapPerConnection": 10108.5390625, + "before": { + "heapAlloc": 1612664, + "heapInuse": 3473408, + "stackInuse": 491520, + "totalAlloc": 3037176, + "mallocs": 23307, + "numGc": 1, + "goroutines": 2, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 11963808, + "heapInuse": 14958592, + "stackInuse": 9175040, + "totalAlloc": 18043328, + "mallocs": 95502, + "numGc": 6, + "goroutines": 1026, + "connections": 1024, + "accepted": 1024 + }, + "after": { + "heapAlloc": 10810232, + "heapInuse": 13475840, + "stackInuse": 819200, + "totalAlloc": 18115240, + "mallocs": 95734, + "numGc": 7, + "goroutines": 2, + "connections": 0, + "accepted": 1024 + }, + "afterReclaim": { + "heapAlloc": 2199680, + "heapInuse": 4268032, + "stackInuse": 819200, + "totalAlloc": 18117728, + "mallocs": 95744, + "numGc": 8, + "goroutines": 2, + "connections": 0, + "accepted": 1024 + } + }, + { + "adapter": "native", + "scenario": "idle", + "concurrency": 1024, + "run": 3, + "errors": 0, + "idleHeapDelta": 5386192, + "idleHeapPerConnection": 5259.953125, + "before": { + "heapAlloc": 1611120, + "heapInuse": 3440640, + "stackInuse": 491520, + "totalAlloc": 3035632, + "mallocs": 23289, + "numGc": 1, + "goroutines": 3, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 6997312, + "heapInuse": 9289728, + "stackInuse": 9109504, + "totalAlloc": 12894344, + "mallocs": 90200, + "numGc": 5, + "goroutines": 1027, + "connections": 1024, + "accepted": 1024 + }, + "after": { + "heapAlloc": 7047888, + "heapInuse": 9396224, + "stackInuse": 688128, + "totalAlloc": 12958448, + "mallocs": 90415, + "numGc": 6, + "goroutines": 3, + "connections": 0, + "accepted": 1024 + }, + "afterReclaim": { + "heapAlloc": 2179288, + "heapInuse": 4112384, + "stackInuse": 688128, + "totalAlloc": 12960936, + "mallocs": 90425, + "numGc": 7, + "goroutines": 3, + "connections": 0, + "accepted": 1024 + } + } + ] +} diff --git a/scripts/verifyPerformance.py b/scripts/verifyPerformance.py index f227f9c..4fcc6dc 100755 --- a/scripts/verifyPerformance.py +++ b/scripts/verifyPerformance.py @@ -11,7 +11,12 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--go", default="go") parser.add_argument("--count", type=int, default=3) + parser.add_argument("--load", action="store_true", help="Also compare isolated servers and idle connection memory") + parser.add_argument("--duration", default="2s", help="Duration per load scenario") + parser.add_argument("--output", help="JSON output path for --load") args = parser.parse_args() + if args.count < 1: + parser.error("--count must be positive") root = Path(__file__).resolve().parent.parent env = dict(os.environ, GOWORK="off") checks = [ @@ -23,12 +28,13 @@ def main(): "-benchmem", "-count", str(args.count), "./transport"], "transport benchmarks", ), - ( - [args.go, "test", "-run", "^$", "-bench", "^BenchmarkNative", - "-benchmem", "-count", str(args.count), "./transport/native"], - "native TCP benchmarks", - ), ] + if args.load: + command = [args.go, "run", "./internal/perfcheck", "--duration", args.duration, + "--count", str(args.count)] + if args.output: + command += ["--output", str(Path(args.output).resolve())] + checks.append((command, "isolated server concurrency and idle-memory matrix")) for command, name in checks: print(f"Running {name}", flush=True) result = subprocess.run(command, cwd=root, env=env, text=True, diff --git a/transport/benchmarkTCP_test.go b/transport/benchmarkTCP_test.go new file mode 100644 index 0000000..775ba1a --- /dev/null +++ b/transport/benchmarkTCP_test.go @@ -0,0 +1,125 @@ +package transport_test + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "sync" + "testing" + "time" + + omni "git.yuntuo.hk/k2safe/go-omni" + "git.yuntuo.hk/k2safe/go-omni/transport/native" +) + +// Both servers use the same application, client, request validation and fixed +// concurrency. B/op includes client AND server allocations in this process; +// ns/op is aggregate throughput cost, not individual request latency. +func BenchmarkTransportTCPApplicationRequest(b *testing.B) { + for _, adapter := range []string{"nethttp", "native"} { + for _, concurrency := range []int{1, 32, 128} { + b.Run(fmt.Sprintf("%s/concurrency%d", adapter, concurrency), func(b *testing.B) { + benchmarkTCP(b, adapter, concurrency) + }) + } + } +} + +func benchmarkTCP(b *testing.B, adapter string, concurrency int) { + application := omni.New() + application.GET("/health", func(ctx *omni.Context) error { + return ctx.OK(map[string]string{"status": "ok"}) + }) + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + b.Fatal(err) + } + done := make(chan error, 1) + var shutdown func(context.Context) error + if adapter == "native" { + server := native.New(application) + server.MaxConnections = 1024 + server.MaxConcurrentRequests = 1024 + shutdown = server.Shutdown + go func() { done <- server.Serve(context.Background(), listener) }() + } else { + server := &http.Server{ + Handler: application, ReadHeaderTimeout: 5 * time.Second, + ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, + IdleTimeout: time.Minute, MaxHeaderBytes: 1 << 20, + } + shutdown = server.Shutdown + go func() { + err := server.Serve(listener) + if err == http.ErrServerClosed { + err = nil + } + done <- err + }() + } + client := &http.Client{Transport: &http.Transport{ + MaxIdleConns: 1024, + MaxIdleConnsPerHost: 1024, + MaxConnsPerHost: concurrency, + IdleConnTimeout: time.Minute, + }, Timeout: 5 * time.Second} + b.Cleanup(func() { + client.CloseIdleConnections() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := shutdown(ctx); err != nil { + b.Error(err) + } + select { + case err := <-done: + if err != nil { + b.Error(err) + } + case <-ctx.Done(): + b.Error("server did not stop") + } + }) + endpoint := "http://" + listener.Addr().String() + "/health" + request := func() error { + response, err := client.Get(endpoint) + if err != nil { + return err + } + _, readErr := io.Copy(io.Discard, response.Body) + closeErr := response.Body.Close() + if response.StatusCode != http.StatusOK || response.Header.Get("X-Request-ID") == "" { + return fmt.Errorf("unexpected response: status=%d", response.StatusCode) + } + if readErr != nil { + return readErr + } + return closeErr + } + if err := request(); err != nil { + b.Fatal(err) + } + errors := make(chan error, concurrency) + var workers sync.WaitGroup + b.ReportAllocs() + b.ResetTimer() + for worker := 0; worker < concurrency; worker++ { + workers.Add(1) + go func(worker int) { + defer workers.Done() + for index := worker; index < b.N; index += concurrency { + if err := request(); err != nil { + errors <- err + return + } + } + }(worker) + } + workers.Wait() + b.StopTimer() + close(errors) + for err := range errors { + b.Error(err) + } +} diff --git a/transport/native/benchmark_test.go b/transport/native/benchmark_test.go deleted file mode 100644 index 1ba994c..0000000 --- a/transport/native/benchmark_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package native_test - -import ( - "context" - "io" - "net" - "net/http" - "sync/atomic" - "testing" - "time" - - omni "git.yuntuo.hk/k2safe/go-omni" - "git.yuntuo.hk/k2safe/go-omni/transport/native" - netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp" -) - -// BenchmarkNativeApplicationRequest measures the real TCP path with a reused -// client connection. It is intentionally separate from the in-process router -// benchmarks in transport/benchmark_test.go. -func BenchmarkNativeApplicationRequest(b *testing.B) { - application := omni.NewWithRouter(netrouter.New()) - application.GET("/health", func(ctx *omni.Context) error { - return ctx.OK(map[string]string{"status": "ok"}) - }) - server := native.New(application) - server.MaxConnections = 1024 - server.MaxConcurrentRequests = 1024 - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - b.Fatal(err) - } - done := make(chan error, 1) - go func() { done <- server.Serve(context.Background(), listener) }() - waitForBenchmarkServer(b, listener.Addr().String()) - - client := &http.Client{Transport: &http.Transport{ - MaxIdleConns: 1024, - MaxIdleConnsPerHost: 1024, - IdleConnTimeout: time.Minute, - }} - endpoint := "http://" + listener.Addr().String() + "/health" - response, err := client.Get(endpoint) - if err != nil { - b.Fatal(err) - } - _, _ = io.Copy(io.Discard, response.Body) - response.Body.Close() - - var failed atomic.Bool - var failure atomic.Value - b.ReportAllocs() - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - response, err := client.Get(endpoint) - if err != nil { - if failed.CompareAndSwap(false, true) { - failure.Store(err.Error()) - } - continue - } - _, _ = io.Copy(io.Discard, response.Body) - response.Body.Close() - } - }) - b.StopTimer() - client.CloseIdleConnections() - shutdownContext, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - if err := server.Shutdown(shutdownContext); err != nil { - b.Fatal(err) - } - if err := <-done; err != nil { - b.Fatal(err) - } - if failed.Load() { - b.Fatalf("native request failed: %s", failure.Load().(string)) - } -} - -func waitForBenchmarkServer(b *testing.B, address string) { - b.Helper() - deadline := time.Now().Add(2 * time.Second) - for { - conn, err := net.DialTimeout("tcp", address, 50*time.Millisecond) - if err == nil { - conn.Close() - return - } - if time.Now().After(deadline) { - b.Fatalf("server did not start: %v", err) - } - time.Sleep(10 * time.Millisecond) - } -}