news 2026/10/15 3:18:00

Juptune(D 语言 io_uring 异步库)在 TechEmpower Framework Benchmarks 中的基准测试实现解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Juptune(D 语言 io_uring 异步库)在 TechEmpower Framework Benchmarks 中的基准测试实现解析
  • 性能测试
  • 测试
  • 后端
  • 开发者工具

【免费下载链接】FrameworkBenchmarks

Source for the TechEmpower Framework Benchmarks project

项目地址:https://gitcode.com/gh_mirrors/fr/FrameworkBenchmarks
点击查看免费下载

TechEmpower Framework Benchmarks(TFB)中的frameworks/D/juptune目录,用于在基准测试平台上度量 Juptune——一个围绕 io_uring 构建、尚在开发中的 D 语言异步 I/O 库——的 HTTP 服务能力。本文以该目录的 README 为主体骨架,结合其完整源码、Dockerfile 与基准配置,说明这一基准条目如何从零搭建 HTTP 服务器、如何注册测试端点,以及 TFB 工具链如何对http://localhost:8080/plaintext进行压测与校验,帮助读者理解一个“无路由框架”的底层 I/O 库是如何被纳入标准化的框架基准体系的。

Juptune 是什么,为什么出现在 TFB 里

README 对 Juptune 的定位一句话概括:

Benchmarks for the WIP Juptune library - an upcoming async I/O library based around io_uring.

即这是一个面向“在研(WIP)”库的基准测试条目:Juptune 是基于 Linux io_uring 接口实现的异步 I/O 库,测试目标不是某个 Web 框架,而是这个底层库本身。这一点在 benchmark_config.json 中得到了印证:

{ "framework": "juptune", "maintainers": ["BradleyChatha"], "tests": [ { "default": { "plaintext_url": "/plaintext", "json_url": "/json", "port": 8080, "approach": "Realistic", "classification": "Platform", "database": "None", "framework": "juptune", "language": "D", "flavor": "ldc2", ... "display_name": "Juptune" } } ] }

从这份配置可以读出几个关键事实:

  • classification: "Platform":TFB 将其归类为“平台/底层库”,而不是应用层框架,这与 README 中“async I/O library”的定位一致;
  • flavor: "ldc2":编译后端为 LDC(LLVM 驱动的 D 编译器),且是 2.x 语言版本的 D;
  • database: "None"、webserver: "None"、platform: "None":不依赖外部数据库与反向代理(如 nginx),被测进程直接监听端口;
  • approach: "Realistic":采用贴近真实使用方式的实现,这一点与源码中“手写路由”的取舍相呼应(见下文);
  • 端口为8080,测试 URL 声明了plaintext_url: "/plaintext"与json_url": "/json"。

值得注意的是:README 的“Test Type Implementation Source Code”一节仅列出了 PLAINTEXT 一个实现,但配置与源码中实际同时实现了/json端点(见 json.d)。换言之,JSON 测试的实现已随代码进入仓库,只是 README 尚未同步更新。

构建体系:LDC + Meson + 静态链接的基准镜像

TFB 中每个语言/框架条目通常配套一个 Dockerfile,juptune.dockerfile 即本条目的构建入口。它的完整流程如下:

FROM debian:bookworm ARG LDC_VERSION=1.41.0 ARG JUPTUNE_REF=1b012d22d4e5e29e88fdf760be4cfa899ce3dfb2 # 安装系统依赖与 LDC RUN apt update \ && apt install -y curl xz-utils gnupg libsodium-dev meson unzip pkg-config clang cmake libssl-dev \ && curl -fsS https://dlang.org/install.sh | bash -s ldc-${LDC_VERSION}

几个细节值得展开:

  • 版本锚定:LDC_VERSION=1.41.0与JUPTUNE_REF(一个具体的上游 commit)共同保证基准结果可复现——基准测试条目必须冻结依赖版本,否则不同时间的结果不可比。
  • 系统依赖清单(libsodium-dev meson clang cmake libssl-dev等)反映了 LDC 编译工具链与 Juptune 构建所需的外部库。

接着是安装 Juptune 库本身的阶段,其中包含多处“为兼容较老发行版做的降级与裁剪”:

# Bookworm 的 meson 偏旧,因此需要重命名 meson.options; # 部分依赖版本比 Juptune 的 targets 旧,但对本基准的代码而言降级是可接受的 RUN curl -fsSL https://github.com/Juptune/juptune/archive/${JUPTUNE_REF}.zip -o code.zip \ && unzip code.zip \ && cd juptune* \ && . ~/dlang/ldc-${LDC_VERSION}/activate \ && mv meson.options meson_options.txt \ && sed -i 's/1.0.20/1.0.18/' meson.build \ && sed -i 's/3.5.0/1.0.18/' meson.build \ && sed -iE "s|.+./examples|#|" meson.build \ && sed -iE 's|^subdir|#|' meson.build \ && sed -iEz 's|juptune_all_unittest_exe[^)]+)|#|' meson.build \ && meson setup build --buildtype release -Dlightweight-results=true -Ddefault_library=static \ && meson install -C build

这里的注释信息量很大(来自 Dockerfile 原文):

  1. mv meson.options meson_options.txt——Debian Bookworm 自带 meson 版本较旧,不识别新文件名,故改名以兼容;
  2. 两处sed将依赖版本要求从1.0.20/3.5.0降到1.0.18,与 Bookworm 中较旧的依赖兼容;
  3. 若干sed注释掉subdir、examples 目录与juptune_all_unittest_exe目标——按注释解释,这是“把一些可执行文件的构建 dummy 掉以加快整体构建”,作者也注明理想做法是上游增加相应构建选项;
  4. 构建参数--buildtype release -Dlightweight-results=true -Ddefault_library=static:发布优化 + 精简 Result 类型 +静态链接,保证最终二进制不依赖动态库环境。

最后编译基准应用本体:

WORKDIR /app COPY ./src/ . RUN . ~/dlang/ldc-${LDC_VERSION}/activate \ && meson setup build --buildtype release -Ddefault_library=static \ && meson compile -C build ENTRYPOINT [ "/app/build/juptune-tfb" ]

应用侧的构建脚本是 src/meson.build,它声明了 4 个 D 源文件与对 Juptune 的依赖:

project('juptune-tfb', 'd') srcs = files( './main.d', './tests/common.d', './tests/json.d', './tests/plaintext.d', ) juptune_dep = dependency('juptune') dep = declare_dependency( include_directories: include_directories('.'), sources: srcs, dependencies: [juptune_dep], d_module_versions: [ 'Juptune_Result_NoLineInfo', 'Juptune_Result_NoContext' ] ) main_exe = executable('juptune-tfb', dependencies: [dep])

两个实现细节:

  • 产物名为juptune-tfb,与 Dockerfile 的ENTRYPOINT [ "/app/build/juptune-tfb" ]对应——容器启动即运行基准服务器;
  • d_module_versions启用了Juptune_Result_NoLineInfo与Juptune_Result_NoContext。从命名可推断,这两个编译开关用于裁减 JuptuneResult错误类型中携带的行号/上下文信息,以换取更小的错误对象与更低的运行开销——对一个以Result为返回通道的高频 I/O 库来说,这是典型的基准级调优手段。

服务器启动流程:EventLoop、Fiber 配置与无 GC 引导线程

main.d 是整个基准应用的入口,也是理解 Juptune 编程模型的核心样本。先看顶部的常量配置区:

enum SOCKET_BACKLOG_PER_THREAD = 1000; enum FIBER_CALL_STACK_BYTES = 1024 * 100; enum HTTP_READ_BUFFER_BYTES = 1024; enum HTTP_WRITE_BUFFER_BYTES = 1024; enum HTTP_CONFIG = Http1Config() .withReadTimeout(1000.msecs) .withWriteTimeout(1000.msecs); static assert( HTTP_READ_BUFFER_BYTES + HTTP_WRITE_BUFFER_BYTES < FIBER_CALL_STACK_BYTES / 4, "To be safe, please ensure the buffer bytes are only a quarter the size of a fiber call stack." );

这组参数体现了基准作者对资源布局的显式控制:

参数取值作用
SOCKET_BACKLOG_PER_THREAD1000每个线程的 listen backlog 上限,实际监听 backlog 为1000 * totalCPUs
FIBER_CALL_STACK_BYTES102400(100KB)每个 Fiber 调用栈的块大小,通过FiberAllocatorConfig注入 EventLoop
HTTP_READ_BUFFER_BYTES/HTTP_WRITE_BUFFER_BYTES各 1024每连接 HTTP 读/写缓冲大小
HTTP_CONFIG读/写超时各 1000ms通过Http1Config().withReadTimeout(...).withWriteTimeout(...)配置

尤其值得一提的是那条static assert:编译期强制要求“读缓冲 + 写缓冲 < 单个 Fiber 栈的四分之一”,并以断言消息说明理由——“为了安全,缓冲字节数应仅为 Fiber 调用栈大小的四分之一”。这是因为每个连接的读/写缓冲会被声明为栈上数组并置于连接处理的 Fiber 栈内(见下文路由循环),若不约束缓冲与栈的比例,高并发下可能出现 Fiber 栈溢出。这是把“内存布局约束”下沉到编译期的一个典型例子。

main()的启动分两个阶段:

void main() { auto loop = EventLoop( EventLoopConfig() .withFiberAllocatorConfig( FiberAllocatorConfig() .withBlockStackSize(FIBER_CALL_STACK_BYTES) ) ); // open() 和 listen() 不能在 event loop 线程之外执行, // 所以目前只能用这种“别扭”的方式来搭建服务器。 loop.addNoGCThread(() @nogc nothrow { server.open().resultAssert; server.listen("0.0.0.0:8080", SOCKET_BACKLOG_PER_THREAD*totalCPUs).resultAssert; juptuneEventLoopCancelThread(); }); loop.join(); // 然后再设置真正的 loop 线程。 foreach(i; 0..totalCPUs) loop.addGCThread(&router); loop.join(); }

从这段代码与源码注释可以还原出 Juptune 的并发模型与当前限制:

  1. 全局 server 套接字:__gshared TcpSocket server;上的注释写道——“目前没有直接把数据传递给新线程的机制,所以不得不使用全局状态”。也就是说,在引导阶段创建、在后续路由线程中使用的TcpSocket通过__gshared全局变量跨线程传递;
  2. 两阶段启动:由于open()/listen()只能在 event loop 线程内执行,作者先addNoGCThread(...)起一个无 GC 线程完成server.open()与server.listen("0.0.0.0:8080", 1000*totalCPUs),随后调用juptuneEventLoopCancelThread()取消该线程;loop.join()等待其结束;
  3. 每 CPU 一个路由线程:foreach(i; 0..totalCPUs) loop.addGCThread(&router);——按物理 CPU 数拉起一组带 GC 的事件循环线程,每个线程跑同一份router函数。多核下多个监听/接受线程共享同一个监听套接字,是基准常见的水平扩展方式(源码层面的具体调度细节由 Juptune 库负责,此处仅描述调用关系)。

手写路由:accept 循环 + 每连接异步 Fiber

Juptune 当前不提供高层的“服务器/路由”特性。main.d 中的注释直接说明了这一基准实现的定位:

Juptune currently does not provide higher-level server features out of the box, so we have to hand-make a custom router.

This is realistic in the sense that building a custom router is a completely valid, supported pattern for people who want/need something very specialised.

This is unrealistic in the sense that once Juptune has a native router, the native router would almost certainly be used in a case like this (but since that's a TODO, this will have to do for now).

翻译过来:Juptune 没有开箱即用的路由层,必须手写;手写自定义路由本身是“现实的(Realistic)”,因为它是完全合法且被支持的专用场景做法;但不太现实之处在于,一旦 Juptune 提供原生路由(目前是 TODO),真实项目大概率会用原生路由。这与 benchmark_config.json 中"approach": "Realistic"的声明相互呼应。

router()的主体是一个 accept 循环,每个连接被包装进一个异步 Fiber:

while(!juptuneEventLoopIsThreadCanceled()) { TcpSocket client; auto result = server.accept(client); if(result.isError) { log("error accepting socket: ", result); continue; } result = async(function () nothrow { auto client = juptuneEventLoopGetContext!TcpSocket; scope(exit) if(client.isOpen) auto _ = client.close(); // ... 逐连接处理循环 ... }, client, &asyncMoveSetter!TcpSocket); if(result.isError) { log("error calling async(): ", result); continue; } }

结构要点:

  • 循环以juptuneEventLoopIsThreadCanceled()为退出条件,与启动阶段juptuneEventLoopCancelThread()的用法构成线程生命周期的对称接口;
  • 每个 accept 到的client通过async(...)提交给事件循环,回调通过juptuneEventLoopGetContext!TcpSocket取回自己的套接字上下文,scope(exit)保证连接处理结束时关闭套接字;
  • Fiber 内部是一个do { ... } while(!readSummary.connectionClosed && !writeSummary.connectionClosed);循环,即支持同一 TCP 连接上的连续请求(HTTP keep-alive 语义),直到读/写摘要表明连接已关闭。

HTTP/1.1 请求解析流程

连接 Fiber 内先构造读/写器(缓冲为栈上数组,受上文static assert的约束):

ubyte[HTTP_READ_BUFFER_BYTES] readBuffer; ubyte[HTTP_WRITE_BUFFER_BYTES] writeBuffer; auto reader = Http1Reader(client, readBuffer, HTTP_CONFIG); auto writer = Http1Writer(client, writeBuffer, HTTP_CONFIG);

随后按“请求行 → 头 → 体 → 收尾”的顺序解析:

  1. 请求行与路由:reader.readRequestLine(requestLine)之后,用requestLine.access(...)无拷贝地取出方法与 URI,做两级 switch 路由:
switch(methodString) { case "GET": method = Method.get; break; default: setError(405, "Unexpected method"); break; } switch(uri.path) { case "/plaintext": route = Route.plaintext; break; case "/json": route = Route.json; break; default: setError(404, "Not found"); break; }

路由表是一个Route枚举(FAILSAFE / plaintext / json)加一个Method枚举(FAILSAFE / get),未匹配则走FAILSAFE分支并记录 404/405 错误码。

  1. 请求头:先checkEndOfHeaders探测是否到头,否则逐个readHeader。由于是自定义路由,作者选择“在路由过程中直接决定处理/忽略每个头”,而不是把所有头塞进 hashmap 再后处理——这是手写路由的性能优势所在,源码注释明确点出了这一点:
// Since we're using a custom router, we have the luxury of handling/ignoring // headers during routing rather than stuffing them all into a hashmap... header.access((scope name, scope value) { /* 按 route 分发 */ });
  1. 请求体:readBody循环 +chunk.access(...)逐块访问,同样按路由决定是否处理;
  2. 收尾与分发:reader.finishMessage(readSummary)汇总本次读操作的结果;若路由阶段设置了errorMsg(404/405),直接写错误响应后continue;否则final switch(route)分发到handlePlainText或handleJson。

错误路径全部以log(...)记录后返回,日志函数由 common.d 提供:

enum ENABLE_LOGGING = false; void log(Args...)(scope auto ref Args args) { static if(ENABLE_LOGGING) writeln(args).assumeWontThrow; }

ENABLE_LOGGING = false意味着发布构建下log()是零开销的空操作(static if在编译期消除整个函数体),这是基准代码控制日志开销的常用手法。

PLAINTEXT 实现:README 列出的核心端点

README 的 “Test Type Implementation Source Code” 一节指向 src/tests/plaintext.d,其完整实现如下:

void handlePlainText( scope ref PlainTextHeaderInput input, scope ref Http1Writer writer, scope ref Http1MessageSummary summary, ) nothrow { import juptune.core.util : then; import juptune.core.util.conv : IntToCharBuffer, toBase10; immutable RESPONSE = "Hello, World!"; IntToCharBuffer contentLengthBuffer; const contentLength = toBase10(RESPONSE.length, contentLengthBuffer); auto result = writer.putResponseLine(Http1Version.http11, 200, "OK").then!( () => writer.putServerAndDate(), () => writer.putHeader("Content-Length", contentLength), () => writer.putHeader("Content-Type", "text/plain"), () => writer.finishHeaders(), () => writer.putBody("Hello, World!"), () => writer.finishBody(), () => writer.finishTrailers(), () => writer.finishMessage(summary) ); if(result.isError) { log("writing response [plaintext] failed: ", result); return; } }

实现特征:

  • then!链式调用:Juptune 的Result类型通过then!(来自juptune.core.util)把“响应行 → Server/Date 头 → 自定义头 → 结束头 → 写体 → 结束体/尾节 → 完成消息”串成惰性执行链,任一步失败即中止并返回错误,避免每步都手写错误判断;
  • 零堆分配的数字转换:Content-Length不是先itoa成动态字符串,而是toBase10(RESPONSE.length, contentLengthBuffer)写入IntToCharBuffer栈缓冲——对热路径而言,这类“不分配”的细节正是底层库基准要度量的对象;
  • 固定响应体:"Hello, World!"(13 字节),与 TFB plaintext 用例的校验要求完全吻合(见下文“TFB 校验规则”一节)。

JSON 端点的对应实现

虽然 README 未列出,json.d 中的handleJson与 plaintext 同构:

ArrayNonShrink!char buffer; buffer.reserve(256); ubyte[8] jsonDepth; scope put = (scope const(char)[] slice) { buffer.put(slice); return Result.noError; }; scope builder = JsonBuilder!(typeof(put))(put, jsonDepth); auto result = builder.jsonSerialise(Message("Hello, World!"));

要点:用 Juptune 自带的JsonBuilder+ 回调式写入器把Message("Hello, World!")(@Json string message;)序列化到预分配的ArrayNonShrink!char栈缓冲中(reserve(256)),随后同样以then!链写出Content-Type: application/json响应;序列化失败则回写 500。json_depth使用 8 字节的栈数组控制嵌套深度,避免了递归/堆分配。

共用工具:手写 GMT 日期头

common.d 的putServerAndDate负责写入 TFB 要求的两个响应头:

return writer.putHeader("Server", "Juptune (Linux)").then!( () => writer.putHeader("Date", dateBuffer[0..dateCursor]) );

Date头是手工逐字段拼装到char[256] dateBuffer上的:星期缩写(final switch(currTime.dayOfWeek))、日、月份缩写(final switch(currTime.month))、年、时分秒,最后加" GMT"。两处final switch利用枚举穷尽性让编译器证明分支完备,配合全程栈缓冲,保持了与整体“无分配热路径”一致的风格。

测试 URL 与 TFB 工具链的校验逻辑

README 的 “Test URLs” 一节给出:

PLAINTEXT

http://localhost:8080/plaintext

容器内服务监听0.0.0.0:8080(见 main.d 的server.listen("0.0.0.0:8080", ...)),TFB 的测试驱动在宿主机访问该 URL 验证连通性与响应正确性。plaintext 用例的校验规则实现在 toolset/test_types/plaintext/plaintext.py 中,与本条目相关的检查包括:

  • URL 长度约束:plaintext_url至少 10 个字符,否则记为 fail。本条目的/plaintext恰好 10 个字符(README 中写作/plaintext,配置为"/plaintext"),满足下限;
  • 响应体校验:大小写不敏感地检查 body 中是否包含b"hello, world!";handlePlainText返回的Hello, World!正好命中;
  • 额外字节告警:若响应体比Hello, World!多字节,会提示 “Server is returning N more bytes than are required. This may negatively affect benchmark performance.”——本实现的响应体恰好 13 字节,不触发告警;
  • 状态码与头校验:verify_status与verify_headers(..., should_be='plaintext')检查 200 状态和Content-Type: text/plain,与handlePlainText写入的200 OK/text/plain对应。

实际压测阶段,TFB 使用 wrk 脚本执行流水线压测:toolset/wrk/pipeline.sh 配合 toolset/wrk/pipeline.lua,按benchmark_config.json中声明的并发级别对http://localhost:8080/plaintext发起请求——这部分属于 TFB 通用工具链,各框架条目共享。

小结:一个底层库如何进入框架基准

把 README 的三个信息点(Juptune 是 io_uring 异步 I/O 库、PLAINTEXT 实现源码、测试 URL)放回仓库上下文后,可以归纳出该基准条目的设计逻辑:

  1. 冻结依赖:juptune.dockerfile 用LDC_VERSION=1.41.0+ 固定上游 commit 保证可复现,并针对 Debian Bookworm 的旧工具链做版本降级与构建裁剪;
  2. 显式资源布局:main.d 中的常量区 +static assert把 Fiber 栈、HTTP 缓冲、backlog 的配比变成编译期可验证的约束;
  3. 手写 HTTP 层:在没有原生路由的库里,用Http1Reader/Http1Writer+Result.then!链手工实现路由与响应,作为“Realistic”方案提交基准;
  4. 零开销日志与栈上缓冲:ENABLE_LOGGING=false、IntToCharBuffer、ArrayNonShrink等细节保证热路径贴近库的真实能力。

对希望在 TFB 中新增 D 语言底层库条目的开发者,这个目录提供了完整的参考结构:README.md(说明与实现入口)、benchmark_config.json(用例与元数据声明)、<name>.dockerfile(构建与 ENTRYPOINT)、src/main.d+src/tests/*.d(服务器与各用例实现)。

  • 性能测试
  • 测试
  • 后端
  • 开发者工具

【免费下载链接】FrameworkBenchmarks

Source for the TechEmpower Framework Benchmarks project

项目地址:https://gitcode.com/gh_mirrors/fr/FrameworkBenchmarks
点击查看免费下载
上一篇:猫抓浏览器资源嗅探扩展:三步解决网页视频下载难题
下一篇:HC开源社区物业管理系统:10分钟快速上手完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 3:17:54

Claudecode报错402 insufficient balance(1008)

我们在终端使用claude code中遇到如下报错&#xff1a;由于我们的claude是通过cc-switch接入minimax供应商的&#xff0c;那么我们点击编辑minimax供应商我们点击 获取api key,进入到minimaxi官网&#xff0c;进行充值&#xff0c;并且重新获取api key后填充到这个api key的输入…

作者头像 李华
网站建设 2026/10/15 3:17:24

复旦-华盛顿大学EMBA:2026年AI课程矩阵解析

管理者学习人工智能&#xff0c;难点通常不在于记住多少技术名词&#xff0c;而在于能否理解数据基础、识别业务场景、判断战略价值&#xff0c;并把创新设想转化为可以讨论和验证的项目。判断一个EMBA项目的专业实力&#xff0c;也不能只看课程名称里有没有“AI”&#xff0c;…

作者头像 李华
网站建设 2026/10/15 3:17:17

NL in the Middle: Code Translation with LLMs and Intermediate Representations

一、文章主要内容和创新点 主要内容 本文聚焦于利用大型语言模型(LLMs)进行代码翻译的任务,旨在通过中间表示(自然语言NL和抽象语法树AST)结合提示工程技术(如零样本、单样本、思维链CoT等)提升翻译准确性。 实验设计:比较了多种提示方式(零样本提示、单样本提示、两…

作者头像 李华
网站建设 2026/10/15 3:14:50

C#调用zlgcan DLL实现CAN上位机通信:从P/Invoke到避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华