Skip to content

Commit 0ddd2a3

Browse files
committed
bench: 自包含性能基线 + BENCHMARKS.md
bench/http_bench.c: 用 asyncweb 自身 server + 裸 async 客户端在同一 loop 里压测 (host 无 wrk/ab, valgrind 与工具链不兼容, 故自包含)。量顺序 keep-alive 吞吐 + µs 级延迟分布, 与并发 (C 连接 x M 请求) 吞吐; RSS 由外层 time -v 量。 基线 (环回, Release, 明文): 顺序 ~28.7K req/s (p50 34µs / p99 ~50µs), 并发 50x400 ~13.8K req/s, 峰值 RSS ~3.7MB。均为相对基线, 供发布后回归对比, 非网络吞吐上限。经 find_package 构建路径验证。
1 parent b0f1e99 commit 0ddd2a3

3 files changed

Lines changed: 308 additions & 0 deletions

File tree

BENCHMARKS.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# asyncweb 性能基线
2+
3+
一组**本地环回**基线数字, 用于发布参照与回归对比。**不是**网络吞吐上限:
4+
所有请求走 127.0.0.1, 测的是库自身开销 (协程调度 + 解析 + syscall), 不含真实
5+
网络的 RTT/带宽。
6+
7+
## 方法
8+
9+
压测程序 `bench/http_bench.c` 是自包含的 —— 用 asyncweb 自己的 HTTP server +
10+
裸 async socket 客户端, 在同一进程/同一 event loop 里跑, 无外部压测工具依赖
11+
(host 上没有 wrk/ab, 且本机 valgrind 与工具链不兼容)。handler 回固定 4 字节
12+
`pong`, 隔离掉业务开销。
13+
14+
两个场景:
15+
- **顺序 (keep-alive)**: 单连接连续发 N 个请求, 无连接重建。量库的最优路径吞吐
16+
+ 每请求延迟分布 (µs 级单调时钟)。
17+
- **并发**: C 条连接各发 M 个请求, 全部并发推进。量 event loop 在多连接交织下
18+
的吞吐。
19+
20+
计时: 延迟用 `clock_gettime(CLOCK_MONOTONIC)` (µs); 峰值内存用 `/usr/bin/time -v`
21+
的 Maximum RSS。
22+
23+
## 复现
24+
25+
```bash
26+
# 装好 libcoro + asyncweb (openssl 后端) 到某 prefix 后:
27+
cmake -S bench -B bench/build -DCMAKE_PREFIX_PATH=/your/prefix
28+
cmake --build bench/build
29+
/usr/bin/time -v ./bench/build/http_bench # 默认 20000 / 50x400
30+
./bench/build/http_bench 50000 100 500 # 自定义 seq_n / conns / per
31+
```
32+
33+
## 基线数字
34+
35+
环境: Linux 7.0 / GCC (Release `-O2`) / 明文 HTTP / 环回。取 3 次运行的代表值。
36+
37+
| 场景 | 请求数 | 吞吐 | 延迟 p50 | 延迟 p99 |
38+
|------|--------|------|----------|----------|
39+
| 顺序 keep-alive | 20000 | ~28,700 req/s | 34 µs | ~50 µs |
40+
| 并发 50×400 | 20000 | ~13,800 req/s |||
41+
42+
峰值 RSS (整个进程, 含 server + 客户端 + OpenSSL): **~3.7 MB**
43+
44+
## 解读
45+
46+
- **顺序 > 并发** 是符合预期的: 环回下顺序 keep-alive 无连接重建、缓存局部性最好;
47+
并发场景额外付 50 次连接建立 + 协程交织调度。两者都是单线程 loop 的数字。
48+
- **p99 与 p50 接近** (34µs vs ~50µs): 环回下无长尾, 说明协程调度开销稳定、无明显
49+
抖动或锁竞争。
50+
- **RSS ~3.7MB** 包含 OpenSSL 静态数据; 库自身每连接状态很小 (per-conn buffer 起始
51+
8KB, 按需翻倍到 64KB 上限)。
52+
53+
这些是**相对基线**: 用于发布后监测回归 (同机器同参数跑, 对比数字是否劣化), 不宜
54+
跨机器/跨场景绝对比较。

bench/CMakeLists.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# asyncweb 性能基线 —— 用安装后的 find_package 方式构建。
2+
# cmake -B build -DCMAKE_PREFIX_PATH=/your/prefix
3+
# cmake --build build
4+
# /usr/bin/time -v ./build/http_bench # -v 附带峰值 RSS
5+
cmake_minimum_required(VERSION 3.16)
6+
project(asyncweb_bench C)
7+
8+
set(CMAKE_C_STANDARD 11)
9+
10+
find_package(asyncweb REQUIRED)
11+
12+
add_executable(http_bench http_bench.c)
13+
target_link_libraries(http_bench PRIVATE asyncweb::asyncweb)

bench/http_bench.c

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/*
2+
* asyncweb 性能基线 (自包含, 无外部压测工具依赖)。
3+
*
4+
* 在同一进程/同一 loop 里起 asyncweb HTTP server, 再用裸 async socket 客户端
5+
* 压它, 量三个数:
6+
* 1. 顺序吞吐 (keep-alive 单连接连续 N 个请求) -> req/s + 平均延迟
7+
* 2. 并发吞吐 (C 条连接各发 M 个请求, 全部并发) -> req/s
8+
* 3. 延迟分布 (顺序场景收集每请求耗时, 报 p50/p99)
9+
*
10+
* 计时用 libcoro loop_time_ms (毫秒精度)。内存 RSS 由外层 /usr/bin/time -v 量。
11+
*
12+
* 用法: ./bench [seq_n] [conc_conns] [conc_per]
13+
* 默认 seq_n=20000, conc_conns=50, conc_per=400
14+
*/
15+
#include <stdio.h>
16+
#include <stdlib.h>
17+
#include <string.h>
18+
#include <time.h>
19+
#include <libcoro/libcoro.h>
20+
#include <asyncweb/asyncweb.h>
21+
22+
/* 微秒级单调时钟 (loop_time_ms 只有毫秒精度, 高吞吐下延迟多为 0)。 */
23+
static uint64_t now_us(void) {
24+
struct timespec ts;
25+
clock_gettime(CLOCK_MONOTONIC, &ts);
26+
return (uint64_t)ts.tv_sec * 1000000ULL + (uint64_t)(ts.tv_nsec / 1000ULL);
27+
}
28+
29+
static uint16_t g_port;
30+
static anet_http_server_t *g_server;
31+
32+
static int SEQ_N = 20000;
33+
static int CONC_CONNS = 50;
34+
static int CONC_PER = 400;
35+
36+
/* --- 并发场景共享状态 --- */
37+
static int g_conc_pending;
38+
static int g_conc_done_reqs;
39+
40+
static void bench_handler(const anet_http_server_request_t *req,
41+
anet_http_server_response_t *resp, void *ud) {
42+
(void)ud; (void)req;
43+
resp->status_code = 200;
44+
resp->status_text = "OK";
45+
resp->content_type = "text/plain";
46+
resp->body = "pong";
47+
resp->body_len = 4;
48+
}
49+
50+
static anet_socket_t *conn_client(void) {
51+
anet_palsock_t cs = anet_palsock_create(AF_INET, SOCK_STREAM, 0, 1);
52+
if (!anet_palsock_is_valid(cs)) return NULL;
53+
return anet_socket_create(cs);
54+
}
55+
56+
static int cmp_u64(const void *a, const void *b) {
57+
uint64_t x = *(const uint64_t*)a, y = *(const uint64_t*)b;
58+
return (x > y) - (x < y);
59+
}
60+
61+
/* 顺序 keep-alive: 单连接连续 SEQ_N 个请求, 记录总耗时与每请求延迟(us 粗略)。 */
62+
task_t* task(seq_bench_task) {
63+
gen_dec_vars(
64+
anet_socket_t *cli;
65+
future_t *fut;
66+
int i;
67+
char rbuf[4096];
68+
uint64_t t0;
69+
uint64_t t1;
70+
uint64_t rt0; /* 本请求开始时刻 us (必须跨 yield 存活) */
71+
uint64_t *lat; /* 每请求延迟 (us) */
72+
int ok;
73+
);
74+
gen_begin(ctx);
75+
76+
gen_var(cli) = conn_client();
77+
{
78+
struct sockaddr_in sin; memset(&sin,0,sizeof(sin));
79+
sin.sin_family=AF_INET; sin.sin_addr.s_addr=htonl(INADDR_LOOPBACK); sin.sin_port=htons(g_port);
80+
gen_var(fut) = anet_socket_connect(gen_var(cli),(struct sockaddr*)&sin,sizeof(sin));
81+
}
82+
gen_yield(gen_var(fut));
83+
if (future_is_rejected(gen_var(fut))) { printf("seq connect failed\n"); exit(1); }
84+
85+
gen_var(lat) = malloc(sizeof(uint64_t) * SEQ_N);
86+
gen_var(ok) = 0;
87+
gen_var(t0) = now_us();
88+
89+
for (gen_var(i) = 0; gen_var(i) < SEQ_N; gen_var(i)++) {
90+
gen_var(rt0) = now_us();
91+
static const char *REQ = "GET /p HTTP/1.1\r\nHost: x\r\n\r\n";
92+
gen_var(fut) = anet_socket_send(gen_var(cli), REQ, strlen(REQ));
93+
gen_yield(gen_var(fut));
94+
if (future_is_rejected(gen_var(fut))) break;
95+
96+
gen_var(fut) = anet_socket_recv(gen_var(cli), gen_var(rbuf), sizeof(gen_var(rbuf))-1);
97+
gen_yield(gen_var(fut));
98+
if (future_is_rejected(gen_var(fut)) || anet_code_of(future_result(gen_var(fut))) <= 0) break;
99+
gen_var(lat)[gen_var(ok)] = now_us() - gen_var(rt0);
100+
gen_var(ok)++;
101+
}
102+
103+
gen_var(t1) = now_us();
104+
{
105+
uint64_t elapsed_us = gen_var(t1) - gen_var(t0);
106+
if (elapsed_us == 0) elapsed_us = 1;
107+
double rps = (double)gen_var(ok) * 1000000.0 / (double)elapsed_us;
108+
qsort(gen_var(lat), gen_var(ok), sizeof(uint64_t), cmp_u64);
109+
uint64_t p50 = gen_var(ok) ? gen_var(lat)[gen_var(ok)/2] : 0;
110+
uint64_t p99 = gen_var(ok) ? gen_var(lat)[(gen_var(ok)*99)/100] : 0;
111+
printf("[seq] reqs=%d elapsed=%llums throughput=%.0f req/s lat p50=%lluus p99=%lluus\n",
112+
gen_var(ok), (unsigned long long)(elapsed_us/1000), rps,
113+
(unsigned long long)p50, (unsigned long long)p99);
114+
}
115+
free(gen_var(lat));
116+
117+
anet_socket_close(gen_var(cli));
118+
free(gen_var(cli));
119+
gen_var(cli) = NULL;
120+
121+
gen_return(0); /* 子任务: 由 master 收尾, 不自行 loop_stop */
122+
gen_end(NULL);
123+
}
124+
125+
/* 一条并发连接: keep-alive 连发 CONC_PER 个请求, 完成后递减 pending。 */
126+
task_t* task(conc_conn_task) {
127+
gen_dec_vars(
128+
anet_socket_t *cli;
129+
future_t *fut;
130+
int j;
131+
char rbuf[4096];
132+
);
133+
gen_begin(ctx);
134+
135+
gen_var(cli) = conn_client();
136+
if (!gen_var(cli)) { g_conc_pending--; gen_return(NULL); }
137+
{
138+
struct sockaddr_in sin; memset(&sin,0,sizeof(sin));
139+
sin.sin_family=AF_INET; sin.sin_addr.s_addr=htonl(INADDR_LOOPBACK); sin.sin_port=htons(g_port);
140+
gen_var(fut) = anet_socket_connect(gen_var(cli),(struct sockaddr*)&sin,sizeof(sin));
141+
}
142+
gen_yield(gen_var(fut));
143+
if (future_is_rejected(gen_var(fut))) { goto done; }
144+
145+
for (gen_var(j) = 0; gen_var(j) < CONC_PER; gen_var(j)++) {
146+
static const char *REQ = "GET /p HTTP/1.1\r\nHost: x\r\n\r\n";
147+
gen_var(fut) = anet_socket_send(gen_var(cli), REQ, strlen(REQ));
148+
gen_yield(gen_var(fut));
149+
if (future_is_rejected(gen_var(fut))) break;
150+
gen_var(fut) = anet_socket_recv(gen_var(cli), gen_var(rbuf), sizeof(gen_var(rbuf))-1);
151+
gen_yield(gen_var(fut));
152+
if (future_is_rejected(gen_var(fut)) || anet_code_of(future_result(gen_var(fut))) <= 0) break;
153+
g_conc_done_reqs++;
154+
}
155+
156+
done:
157+
anet_socket_close(gen_var(cli));
158+
free(gen_var(cli));
159+
gen_var(cli) = NULL;
160+
g_conc_pending--;
161+
gen_return(NULL);
162+
gen_end(NULL);
163+
}
164+
165+
task_t* task(conc_bench_task) {
166+
gen_dec_vars(
167+
int i;
168+
uint64_t t0;
169+
task_t *ct;
170+
);
171+
gen_begin(ctx);
172+
173+
g_conc_pending = CONC_CONNS;
174+
g_conc_done_reqs = 0;
175+
gen_var(t0) = now_us();
176+
177+
for (gen_var(i) = 0; gen_var(i) < CONC_CONNS; gen_var(i)++) {
178+
gen_var(ct) = conc_conn_task();
179+
if (!gen_var(ct)) { g_conc_pending--; continue; }
180+
task_run(gen_var(ct));
181+
}
182+
183+
while (g_conc_pending > 0) {
184+
gen_yield(async_sleep(2));
185+
}
186+
187+
{
188+
uint64_t elapsed_us = now_us() - gen_var(t0);
189+
if (elapsed_us == 0) elapsed_us = 1;
190+
double rps = (double)g_conc_done_reqs * 1000000.0 / (double)elapsed_us;
191+
printf("[conc] conns=%d x %d = %d reqs elapsed=%llums throughput=%.0f req/s\n",
192+
CONC_CONNS, CONC_PER, g_conc_done_reqs, (unsigned long long)(elapsed_us/1000), rps);
193+
}
194+
195+
gen_return(0); /* 子任务: 由 master 收尾 */
196+
gen_end(NULL);
197+
}
198+
199+
/* master: 依次跑 seq 与 conc 两个子场景, 全程一个 accept 循环。 */
200+
task_t* task(bench_master_task) {
201+
gen_dec_vars(
202+
task_t *accept_task;
203+
task_t *phase;
204+
);
205+
gen_begin(ctx);
206+
207+
gen_var(accept_task) = anet_http_server_run(g_server);
208+
task_run(gen_var(accept_task));
209+
210+
gen_var(phase) = seq_bench_task();
211+
gen_yield_from_task(gen_var(phase));
212+
213+
gen_var(phase) = conc_bench_task();
214+
gen_yield_from_task(gen_var(phase));
215+
216+
anet_http_server_stop(g_server);
217+
gen_yield(async_sleep(10));
218+
219+
loop_stop();
220+
gen_return(0);
221+
gen_end(NULL);
222+
}
223+
224+
int main(int argc, char **argv) {
225+
if (argc > 1) SEQ_N = atoi(argv[1]);
226+
if (argc > 2) CONC_CONNS = atoi(argv[2]);
227+
if (argc > 3) CONC_PER = atoi(argv[3]);
228+
229+
if (anet_init() != ANET_OK) { printf("init failed\n"); return 1; }
230+
g_server = anet_http_server_create(0, bench_handler, NULL);
231+
if (!g_server) { printf("server create failed\n"); return 1; }
232+
g_port = anet_http_server_port(g_server);
233+
printf("bench server on 127.0.0.1:%u\n", g_port);
234+
235+
/* 单个 accept 循环, master 依次驱动 seq 与 conc 两阶段。 */
236+
loop_run(bench_master_task());
237+
238+
anet_http_server_destroy(g_server);
239+
anet_cleanup();
240+
return 0;
241+
}

0 commit comments

Comments
 (0)