Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions libxpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
# ── Sub - modules ─────────────────────────────────────────────────────
add_subdirectory(xpp)

# ── End-to-end benchmark servers/clients (wrk/ab/hey) ────────────────
# Controlled by libx's X_BUILD_BENCHMARKS (defined in libx/CMakeLists.txt,
# added before this subdirectory).
if(X_BUILD_BENCHMARKS)
add_subdirectory(bench)
endif()

# ── Aggregated target ───────────────────────────────────────────────
#xpp is defined in xpp / CMakeLists.txt as an INTERFACE library.
#No separate aggregated target needed — just include the subdirectory.
9 changes: 9 additions & 0 deletions libxpp/bench/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# bench/CMakeLists.txt - xpp end-to-end benchmark suite
#
# Standalone servers/clients for driving with external load tools (wrk,
# hey, ab) — not Google Benchmark micro-benchmarks (those live in
# xpp/sync/*_bench.cpp under XPP_BUILD_BENCHMARKS).

add_executable(xpp_http_echo_server http/echo_server.cpp)
target_link_libraries(xpp_http_echo_server PRIVATE xpp)
target_compile_options(xpp_http_echo_server PRIVATE -Wno-error)
94 changes: 94 additions & 0 deletions libxpp/bench/http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# xpp echo server 压测基准

`echo_server.cpp`(xpp)vs `echo_server.go`(Go net/http)——HTTP/1.1 keep-alive
小请求 echo 场景。同一台机器、同一套 wrk 命令直接对比。

## 环境

- 机器:macOS(Apple Silicon M3,12 核)
- 构建:`build-bench`(Release,`-DX_BUILD_BENCHMARKS=ON`,per-module Google Benchmark 关闭)
- 负载工具:wrk 4.2.0(kqueue)
- Go:`go build echo_server.go`,`GOMAXPROCS` 分别设 1(单核)和默认(多核)
- xpp server:单事件循环(单线程)

## 压测命令

```bash
# server
./build-bench/libxpp/bench/xpp_echo_server 8080 # xpp
/tmp/go_echo_server 8081 # Go(GOMAXPROCS=1 或默认)

# 负载(t4/c100 是默认;高并发档位单独指定)
wrk -t4 -c100 -d10s http://127.0.0.1:8080/ping
wrk -t4 -c500 -d10s http://127.0.0.1:8080/ping
wrk -t4 -c1000 -d10s http://127.0.0.1:8080/ping
wrk -t8 -c2000 -d10s http://127.0.0.1:8080/ping
wrk -t4 -c100 -d10s "http://127.0.0.1:8080/echo?size=1024"
wrk -t4 -c100 -d10s -s post.lua http://127.0.0.1:8080/echo
```

`post.lua`:

```lua
wrk.method = "POST"
wrk.body = "hello-world-payload-1234567890"
wrk.headers["Content-Type"] = "text/plain"
```

## 结果(QPS)

### GET /ping

| 并发 | xpp(1 核,CPU ~99%) | Go 单核(GOMAXPROCS=1) | Go 多核(12 核) |
| ------ | ---------------------- | ------------------------ | ----------------- |
| c100 | **77.9k** | 71.2k | 72.9k(CPU 240%) |
| c500 | **70.8k** | 68.4k | 69.4k(CPU 226%) |
| c1000 | **70.3k** | 65.9k | 68.8k |
| c2000 (t8) | **68.5k** | 64.5k(270 read errors) | 63.2k |

### GET /echo?size=1024(c100)

| 指标 | xpp | Go 单核 |
| ------ | ----- | -------- |
| QPS | **73.1k** | 64.9k |
| 吞吐 | **75.9 MB/s** | 70.7 MB/s |

### POST /echo(c100)

| 指标 | xpp | Go 单核 |
| ------ | ----- | -------- |
| QPS | **70.9k** | 66.7k |

## 结论

1. **xpp 单核赢 Go 多核**:xpp 用 1 核跑 77.9k QPS(CPU 99%);Go 用 2.4 核跑 72.9k(CPU 240%)。
等量 QPS 下单核效率约 Go 的 2.6 倍。
2. **Go 多核在 keep-alive 小请求场景零收益**:单核 vs 多核全并发档位差距 <3%,c2000 时
多核反而更低。goroutine 调度 + net/http 中间层开销抵消了多核分摊。
3. **xpp 全档位领先 7%~17%**,且 c2000 时是唯一没有 socket errors 的实现(Go 单核有
270 个 read errors,~0.04%)。
4. **内存稳定**:xpp 30s / 212 万请求压测 RSS 持平(22976→22928 KB),无泄漏。

## 附带发现

- xpp 单核上限 ~78k QPS,受限于单事件循环架构;要突破只能多进程/多 loop 横向扩展。
- xpp 单核上限 ~78k QPS,受限于单事件循环架构;要突破只能多进程/多 loop 横向扩展。
- 同一负载下 C 层 bench(`libx/bench/http/http_bench_server.cpp`,已重写到新 API)为
**85.4k QPS** —— xpp 层(77.9k)约 **10% 封装开销**,来自每请求的 Arc/ReqState/
channel/spawn 分配 + `Router::compose`(见 `todos/http-server-per-request-allocations.md`)。
- 重写 C 层 bench 时发现并修复了一个 libx bug:h2 下 `on_data` 回调的 arg 用
route 级 `route_info->arg` 而非 `stream->user`,per-request 状态(`xHttpCtxSetUser`)
不可见 → POST body 处理解引用 NULL 崩溃(见 `issues/http-h2-ondata-user-arg.md`)。

## 复现

```bash
cmake -B build-bench -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DX_BUILD_BENCHMARKS=ON \
-DXBASE_BUILD_BENCHMARKS=OFF -DXBUF_BUILD_BENCHMARKS=OFF -DXJSON_BUILD_BENCHMARKS=OFF \
-DX_BUILD_TESTS=OFF
cmake --build build-bench --target xpp_echo_server -j
./build-bench/libxpp/bench/xpp_echo_server 8080
```

Go 侧:`go build -o /tmp/go_echo_server libxpp/bench/http/echo_server.go`
99 changes: 99 additions & 0 deletions libxpp/bench/http/echo_server.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright 2025 The libx++ Authors. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
* echo_server.cpp - xpp::http::Server end-to-end benchmark server
*
* Usage: echo_server [port]
* Default port: 8080
*
* Routes:
* GET /ping → 200 "pong" (minimal response — pure QPS)
* GET /echo?size=N → 200 with N bytes of 'x' (payload throughput)
* POST /echo → 200 echoing the request body
*
* Designed to be benchmarked with wrk:
* wrk -t4 -c100 -d10s http://127.0.0.1:8080/ping
* wrk -t4 -c100 -d10s http://127.0.0.1:8080/echo?size=1024
* wrk -t4 -c100 -d10s -s post.lua http://127.0.0.1:8080/echo
*/

#include <cstdio>
#include <cstdlib>
#include <string>

#include <xpp/event.h>
#include <xpp/http/server.h>

namespace http = xpp::http;

static size_t parse_size(const xpp::String &url) {
size_t size = 64; // default
auto q = url.find("size=");
if (q.is_some()) {
auto b = url.as_bytes();
size_t v = 0;
for (size_t i = q.unwrap() + 5; i < b.size(); ++i) {
uint8_t c = b.data()[i];
if (c < '0' || c > '9') break;
v = v * 10 + static_cast<size_t>(c - '0');
}
if (v > 0) size = v;
if (size > 1048576) size = 1048576; // cap at 1 MB
}
return size;
}

int main(int argc, char *argv[]) {
uint16_t port = 8080;
if (argc > 1) port = static_cast<uint16_t>(atoi(argv[1]));

xpp::EventLoop loop;
if (!loop) {
fprintf(stderr, "Failed to create event loop\n");
return 1;
}
xpp::WaitScope scope(loop);

auto server_r =
http::Server::builder()
.route("GET /ping", [](http::Request) { return http::Response::ok("pong"); })
.route("GET /echo",
[](http::Request req) {
std::string body(parse_size(req.url()), 'x');
return http::Response::ok(xpp::Bytes::copy(body.data(), body.size()));
})
.route("POST /echo",
[](http::Request req) -> xpp::Promise<http::Result<http::Response>> {
return req.into_body().bytes().then(
[](http::Result<xpp::Bytes> b) -> http::Result<http::Response> {
if (b.is_err()) {
return http::Result<http::Response>(
xpp::err, http::Error(http::Error::Kind::Io,
xpp::String::from_utf8("read body failed").unwrap()));
}
return http::Result<http::Response>(xpp::ok, http::Response::ok(b.unwrap()));
});
})
.bind("0.0.0.0", port)
.build();

if (server_r.is_err()) {
fprintf(stderr, "Failed to build server\n");
return 1;
}
http::Server server = std::move(server_r).unwrap();

auto running = server.serve();

fprintf(stdout, "xpp echo server listening on 0.0.0.0:%u\n", server.port());
fprintf(stdout, " GET /ping → 200 \"pong\"\n");
fprintf(stdout, " GET /echo?size=N → 200 with N bytes\n");
fprintf(stdout, " POST /echo → 200 echo body\n");
fflush(stdout);

loop.run();

return 0;
}
78 changes: 78 additions & 0 deletions libxpp/bench/http/echo_server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2025 The libx++ Authors. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
* echo_server.go - Go net/http echo server for comparison benchmarking
*
* Mirrors echo_server.cpp (xpp::http::Server) route-for-route so wrk
* numbers are directly comparable:
* GET /ping → 200 "pong" (minimal response — pure QPS)
* GET /echo?size=N → 200 with N bytes of 'x' (payload throughput)
* POST /echo → 200 echoing the request body
*
* Usage: go run echo_server.go [port]
* Default port: 8080
*/

package main

import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
)

func handlePing(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
}

func handleEcho(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read body failed", http.StatusInternalServerError)
return
}
w.Write(body)
return
}

// GET: parse ?size=N from URL
size := 64 // default
if s := r.URL.Query().Get("size"); s != "" {
if n, err := strconv.Atoi(s); err == nil && n > 0 {
size = n
if size > 1048576 { // cap at 1MB
size = 1048576
}
}
}

w.Write(bytes.Repeat([]byte("x"), size))
}

func main() {
port := "8080"
if len(os.Args) > 1 {
port = os.Args[1]
}

mux := http.NewServeMux()
mux.HandleFunc("/ping", handlePing)
mux.HandleFunc("/echo", handleEcho)

addr := "0.0.0.0:" + port
fmt.Printf("Go echo server listening on %s\n", addr)
fmt.Printf(" GET /ping → 200 \"pong\"\n")
fmt.Printf(" GET /echo?size=N → 200 with N bytes\n")
fmt.Printf(" POST /echo → 200 echo body\n")

if err := http.ListenAndServe(addr, mux); err != nil {
fmt.Fprintf(os.Stderr, "Failed to listen: %v\n", err)
os.Exit(1)
}
}
53 changes: 53 additions & 0 deletions todos/http-server-per-request-allocations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# TODO: xpp HTTP server 每请求堆分配优化

> 来源:`libxpp/bench` 压测(2026-08-26)——xpp echo server 单核 77.9k QPS,但
> 已赢 Go 多核(72.9k @ 240% CPU),分配开销不是当前瓶颈
> 优先级:低——正确性优先;在出现下述"触发条件"时再做

## 动机

`srv_on_request_cb`(`libxpp/xpp/http/server.h`)对**每个请求**固定执行多次堆分配:

1. `sync::mpsc::channel<Bytes>(256)` —— 每请求建一条 body channel
2. `Own<ReqState>(new ReqState())` —— per-request 状态
3. `impl->m_reqs.emplace(ctx_key, ...)` —— map 插入(`Own` 转移)
4. `Router::compose(req)` —— **每次请求重新做路由匹配 + middleware 链构建**
5. `Arc<Request>::make(std::move(req))` —— 请求本体搬上堆
6. `Arc<ConnLifetime>::make()` + `m_conns.emplace(...)` —— UAF 修复引入的连接生命周期
7. `xpp::spawn(...)` —— defer node 堆分配

78k QPS ≈ 每秒 ~80 万次分配。压测结论(`libxpp/bench/http/README.md`)显示
当前单核效率已碾压 Go(等量 QPS 下核用量约 Go 的 1/2.6),分配开销**尚未**成为
吞吐瓶颈——但它是单核 QPS 上限(~78k)的构成因素之一,且会随路由/middleware
复杂度线性放大。

## 内容

- **`Router::compose` 结果缓存**:静态路由表 + 固定 middleware 链在 server 构建时
预组合成最终 endpoint,请求到达时免匹配直接调用。需处理动态部分(path params、
按请求状态变化的 layer)——只缓存结构,参数注入保留在调用时。
- **固定尺寸对象池**:`ReqState`、`ConnLifetime`(若保留独立对象)尺寸已知,可用
xbase slab 池化,消除每请求 `new`/Arc 分配。
- **请求路径分配盘点**:用 `malloc` 追踪/采样量化每请求实际分配次数与字节数,
找出大头,避免凭感觉优化。mpsc channel 是否可复用、`Arc<Request>` 是否可用
更轻的持有方式(如 arena)逐一评估。

### 与单核瓶颈的关系

单核 ~78k 是**单事件循环**架构上限,需多进程/多 loop 才能突破——与本项正交。
本项只压低单核内的每请求成本,不改变"单核有上限"这一事实。

## 非目标

- 不改单事件循环架构(多进程/多 loop 是独立的扩展方向,另立 todo)
- 不动 handler 层 API(`route`/`layer`/`compose` 语义保持)
- 不为优化牺牲正确性(尤其 ConnLifetime 的连接关闭防护)
- 不引入第三方内存管理库

## 触发条件

- 单机吞吐真实需求逼近单核上限(>70k QPS),且实测确认分配占 CPU 大头
- 路由表 / middleware 链变大后 QPS 明显随复杂度下降
- 或有用户报告高 QPS 下 CPU profile 显示 `srv_on_request_cb` 分配路径为热点

此时先做"分配盘点"(内容第 3 项)确定方向,再动手缓存/池化。
Loading
Loading