feat: publish Dever core with project scaffolding and versioned AI skills

This commit is contained in:
Antigravity committed 2026-10-04 13:37:30 +00:00
1 parent 70fa73cdc2
commit ac458ffa7f
960 files changed
+221319 -1163

No files matched your search

+349
View File
@@ -0,0 +1,349 @@
# 独立二进制性能基准
这里测常驻内存、异步调度、HTTP/1.1、HTTP/2 和 TCP/WebSocket/SSE 长连接;不改运行时。构建单独执行,运行阶段只启动已编译产物。全部程序绑定自己创建的 loopback 随机端口,不接触已有服务,不进入默认 `cargo test`。
## 构建与短时验证
在仓库根目录运行,使用已经安装的 Rust 工具链与锁定的离线依赖:
```bash
cargo build --offline -p dever-cli --bin dever
cargo build --offline --locked --release --target-dir target/native-runtime -p dever-tests --example network_bench --example live_bench --example async_bench
python3 test/performance/run.py build --output target/performance/build-current --workers 1 --connections 512
python3 -m unittest discover -s test/performance -p test_runner.py -v
python3 test/performance/test_peer_settings.py -v
python3 test/performance/test_network_bench.py -v
python3 test/performance/test_live_bench.py -v
python3 test/performance/run.py run --artifacts target/performance/build-current --output target/performance/smoke-current --suite all --duration 0.5 --warmup 0.2 --rates 100 --paths /plain --repeats 1
```
只复验当前 Dever/Markdown 双源码 CMS 和 base/SQLite/PostgreSQL/both 四个 profile 时,可使用
`python3 test/performance/run.py build --selection cms-profiles --output target/performance/cms-build-current`。
该选择不构建无关的 HTTP、长连接、ORM 场景和 peer;生成的产物仍可用
`run --suite cms` 分别测量 `cms`(Dever)和 `cms-md`(Markdown)的真实登录、文章创建/替换、发布和前台查询;旧 `READY|3` 计数 fixture 已删除。仅做四 profile 构建可选 `--selection profiles`。PostgreSQL profile 使用不连接数据库的编译专用声明,不需要真实 URL;真实 PostgreSQL ORM/压力测试仍必须显式配置根 `config/setting.json` 的 `database.postgres_test`。
仅复验 runtime/HTTP/TLS/HTTP2/TCP/WebSocket/SSE 时使用 `--selection protocols`,不构建 ORM/CMS 或重复四 profile;三种 Rust peer 可用 `--network-bench`、`--live-bench`、`--async-bench` 指定明确准备的路径。
输出目录必须是新路径,保留先前报告。当前 Dever 产物统一经公开 `dever build` 使用 LLVM O2、LLD 和明确准备的 runtime archive;构建日志及二进制 SHA-256、大小、耗时保存在 `manifest.json`。runtime/http/live 每组构建一个包含该组 CMD 的应用,各测量通过不同命令启动,大小不能解读为仅一个函数的程序。`profile-report.json` 仍使用同一份最小 CMD 源码生成 base、SQLite、PostgreSQL 和 both 产物,记录 bridge 的锁定依赖。作者必须准备匹配源码的四个 archive;编译器自身的 debug/release 不决定 archive 优化级别。Rust 对照程序由上面的 Cargo release 命令另行准备并记录二进制摘要。Dever 使用生产默认调度,`--workers` 和 `task_capacity` 只配置 Rust 对照,不声称两者调度配置相同;可用专属 CPU affinity 统一核数。运行器要求 `application_configuration=llvm-cmd-v1`,拒绝旧 Rust 后端产物冒充当前 LLVM 测量。测量不调用 Cargo。
双源码 CMS 也有独立入口,不构建网络 peer 和无关场景:
```text
python3 -B test/performance/cms.py build --output target/performance/cms-publish-build
python3 -B test/performance/cms.py run --manifest target/performance/cms-publish-build/manifest.json --output target/performance/cms-publish-128 --cgroup-parent /sys/fs/cgroup --memory-mib 128
python3 -B test/performance/cms.py run --manifest target/performance/cms-publish-build/manifest.json --output target/performance/cms-publish-64 --cgroup-parent /sys/fs/cgroup --memory-mib 64
python3 -B -m unittest discover -s test/performance -p test_cms.py -v
```
运行使用仅含二进制和私有 `config/setting.json` 的副本,不保留 module。先在自有 SQLite 平台库 bootstrap、迁移租户库并给 admin/front 分别建立 Owner,再启动随机 loopback 端口。服务启动与登录/发布/查询在专属 cgroup 内,bootstrap 和部署命令不在该预算内。保留 Cookie/Origin、真实身份和租户验证,不伪造登录身份。逐篇创建、替换、发布并核对全部前台结果;重复发布必须返回 409,两套源码结果必须一致。默认 16 篇顺序业务基线报告启动时间、每种请求样本数/中位/最大延迟、RSS/PSS 与 cgroup peak/OOM,不称为并发饱和吞吐或 p99。测试手动传送服务返回的 Secure Cookie,只验证 owned HTTP 协议,不代表浏览器 HTTPS 验收。日志阈值固定 error,出现服务错误、超时、OOM 或清理失败均失败。
追加 `--concurrency 8 --read-seconds 10` 会在发布后运行八个独立连接的已登录前台列表查询,每次都核对完整业务结果。报告另列 `concurrent_reads` 的实际成功 QPS、p50/p95/p99、测量期资源趋势和断连 FD 回落;这是闭环读负载,不能解释为写入吞吐或预设请求到达率。并发数最多64、时长最多3600秒,同一服务进程内支持30分钟持续读取;客户端使用固定2048桶的对数延迟直方图,分位数报告包含请求及结果核对的桶上界,避免随请求数保存无限样本。首次失败停止其他客户端,所有连接按原请求期限关闭;不记录 Cookie 或密码。完整Linux门及64/128MiB长测入口见 [test/ci/README.md](../ci/README.md)。
真实 PostgreSQL 的 Owner HTTP/双物理租户验收另由共享 Rust fixture 管理数据库生命周期:
```text
cargo test --offline --locked -p dever-tests --features api,postgres --test postgres_api postgres_cms_http_isolates_two_tenant_databases -- --ignored --exact --nocapture
```
根 `database.postgres_test` 必须指向专属测试实例,URL 数据库名包含 `{case}`,账号允许创建/删除测试库;默认不运行、不探测外部服务。fixture 使用随机控制库及租户前缀,只清理本轮拥有的库,失败也清理,清理失败不能记录 passed。`cms.py postgres-case` 始终先复制传入项目到自有临时目录,端口/密钥/配置/产物仅写副本,输出目录保留日志和不含凭据的结果。Rust 目标额外注入仅在 `test/` 的普通成员 CMD,并显式启用 `--rbac-fixture`:覆盖 Owner、两个租户各两篇发布、相同 slug 独立数据、跨站 Cookie/跨租户登录拒绝,以及非 Owner 多角色、跨站同名角色和原会话撤销立即失效。单独调用不启用该测试 fixture 的工具仍仅证明 Owner 合同。权限键分别读取实际站点目录,GET/DELETE 参数统一走 query,生产 CMS 和 Owner 保留角色规则不改。
runtime/HTTP/live 夹具已使用当前 App→Port→Adapter 结构,CMD 根有真实 Bool 输出;live 的 typed Adapter setting 在每个产物的私有配置中独立绑定。ORM seed 是测量前的 CMD 部署步骤,count/cancel 是普通 API,取消测试使用单连接池并核对计数和连接归还,不能把 handler 超时冒充客户端取消。
2026-10-02 LLVM 基线使用 `protocols-llvm-10-02`、`cms-llvm-10-02` 及 `profiles-llvm-10-02` 的准确产物摘要,私有作者编译器来源记录在 `target/closure-current-compiler/provenance.json`。四个最小 CMD profile 大小分别为 base 2,083,880、SQLite 4,139,976、PostgreSQL 2,265,224、both 4,238,568 字节。Dever/Markdown CMS 分别 8,323,880 / 8,326,120 字节。
| 当前测量 | 条件 | 结果与报告目录(均位于 target/performance) |
| --- | --- | --- |
| 双 CMS 发布 | 64/128 MiB、每组顺序16篇 | 零错误/OOM,启动44.6–51.9ms,RSS峰值28.20–28.33MiB;`cms-llvm-{64,128}-10-02` |
| 双 CMS 登录后列表读取 | 64/128 MiB、8并发、10秒、不绑核 | 1210.6–1322.2 QPS,p99 11.48–13.59ms,RSS峰值30.81–31.04MiB,零错误/OOM;`cms-read-llvm-{64,128}-10-02` |
| HTTP/2 明文及 TLS 稳态 | 64/128 MiB、单核、4连接×16流、目标10k、60秒 | 成功/计划99.718–99.794%,9971.6–9979.2 QPS,p99 2.912–3.424ms,RSS峰值4.88–5.72MiB,零错误/OOM/断开后FD差;`http2-steady-llvm-{64,128}-10-02` |
| HTTP/2 连接恢复 | 64 MiB、单核、两种传输各100轮 | 合计19,887个完整成功请求,200次断开后FD差均0,零错误/OOM;`http2-recovery-llvm-64-10-02-v2` |
| TCP/WS/SSE 连接恢复 | 64/128 MiB、单核、32/128连接、各10轮 | 12个case、120次断开后FD差均0,完整消息校验通过,零错误/OOM;`live-llvm-{64,128}-10-02` |
这些是本机 loopback 与热文件缓存的有限测量。CMS使用闭环客户端,HTTP/2使用固定速率且保留少量未派发计数,两者不能直接比较。15k TLS短时校准未达到99%计划成功门,所以当前一分钟基线选择10k,不把下文历史20k/饱和结果当作本次结果。首次恢复因运行器重复复制peer耗尽磁盘的失败目录保留,最终恢复证据仅用v2。历史 `cms-publish-*-09-30-*`、`orm-runtime-64-09-30-v1` 与 `live-tcp-64-09-30-v1` 不代表本次重跑。
共享编译的本机基线记录于 `target/closure-shared-compile-measurement.json`:私有debug核心、热OS缓存下,首次请求7.318秒、命中5.564秒;两个同步客户端提交同一份新源码分别7.463/7.838秒且产物一致。时间包含签名输入复核及IPC,不能当纯编译耗时或正式优化版CLI成绩。每个产物4,239,368字节,两条缓存合计8,478,736字节,校验无损坏;跨真实UID共享另有独立定向验收。
产物目录含二进制、测试证书、构建配置,可整体复制到兼容的 Linux 机器;运行器仅依赖 Python 3.11+ 标准库和 `/proc`。TLS 证书来自 `test/dever-tests/fixtures/tls/`,仅用于测试,执行时校验证书链和 localhost 名称。测试证书过期后需更新 fixture,不得关闭验证。
两个独立 peer 测试从仓库 `config/setting.json` 的 `performance` 读取 `network_peer`、`live_peer`,例如:
```json
{"performance":{"network_peer":"target/performance/build-current/network_bench","live_peer":"target/performance/build-current/live_bench"}}
```
把该 `performance` 对象合并进已有设置,保留原有数据库配置。基准进程配置不读取环境变量。运行器在同文件系统硬链接已验证的只读 peer 二进制,仅跨文件系统时复制;每轮仍在其旁边以 `0600` 写入独立 `config/setting.json` 的 `benchmark` 对象。测量期间不得修改输入二进制。字段为 `workers`、`connections`、`task_capacity`、`timeout_ms`、`lifetime_ms`、HTTP/2 参数和 TLS 证书路径。CLI 负载参数仍显式传递。缺少配置、非法类型或越界值会报错;构建器通过不启动网络的 `check-config` 检查 peer 的 JSON 配置协议。旧协议的 manifest 不再接受,应重新构建产物,旧报告仍保留历史意义。
## 场景与统计口径
| 组 | 场景 | 计时/验证 |
| --- | --- | --- |
| memory | 同步空闲、异步空闲、256 个等待 Channel 的任务、HTTP/HTTPS 监听 | READY 后采样 RSS/PSS、VmHWM、线程/FD;parked 用取消场景的等待阶段,不另建实现 |
| async | 16384 次 run/wait、16384 次有界 Channel 发送与消费、取消并排空 256 个已启动任务 | 源码内单调时钟;计算结果/数量必须正确,打印在计时外;前两项每进程 10 轮、去掉首轮,取消保留完整排空耗时 |
| http | HTTP/HTTPS × Dever/Rust-runtime/原生 Hyper × 三种响应 | 固定速率、有限并发;状态码、Content-Type 和完整正文必须匹配;连接复用与新建连接可分别运行 |
| http2 | h2c prior knowledge/TLS ALPN × Dever/Rust-runtime/原生 Hyper × 物理连接/流拓扑 | 计时前建立固定物理连接;校验握手数、状态码、Content-Type、完整正文、吞吐、延迟和资源 |
| http2-recovery | 同一服务进程内重复建立、使用、关闭 HTTP/2 连接 | 每轮校验请求总账和握手数,断开后 FD 必须回到 READY 基线,保留 RSS 序列 |
| live | TCP/WebSocket/SSE × 连接档位 × 多轮连接/保持/断开 | TCP/WS 校验带轮次和连接标识的回显;SSE 校验初始事件与心跳;对比各轮断开后的 FD/RSS |
| orm | SQLite/PostgreSQL 的 idle、CRUD、list/cursor/stream、pool saturation、transaction cancel、HTTP+JSON+DB | 每个 case 使用独立 bundle 和数据库;校验精确结果,再报告数据库操作延迟、吞吐、资源和连接回收 |
| cms | `examples/cms/dever` 和 `examples/cms/md` 的 Model、Seed、用户注册和发布事务 | 相同入口、单独 SQLite bundle,业务初始化与事务完成后分别采样 RSS/PSS/VmHWM 和可用时的 cgroup peak |
## ORM 基准
所有 Dever 产物在 `build` 阶段通过公开 `dever` 项目路径生成;无数据库声明的 runtime/http/live 使用 base archive。`run` 阶段只复制和执行已有二进制。每个数据库 case 都创建独立 bundle;SQLite 使用该 bundle 自己的 `data/db/orm.db`,重复轮次不会复用数据库状态。
构建器把测试源码暂存到 `module/benchmark/<domain>/app.dever`,共享能力位于 `benchmark/bench`、`benchmark/config`,由 `api.dever` 声明 CMD。fixture 不生成 main/package/exposes,也不向 `dever build` 传任意 entry。
CMS 从原始双源码项目构建,通过真实登录/租户/发布 HTTP 流程测量,不注入新的业务入口。普通 `build` 只构建 Dever CMS,`cms-profiles` 构建双源码。下方历史 `READY|3` 报告不代表当前实现。
2026-09-18 的当前源码专项报告保存在 `target/performance/cms-dual-build-09-18-v2/{manifest.json,profile-report.json}` 和 `target/performance/cms-dual-rss-09-18/report.json`。base/SQLite/PostgreSQL/both 原生程序分别为 411,352 / 2,558,200 / 2,708,112 / 4,672,888 字节。CMS Dever/Markdown 产物分别为 3,151,352 / 3,151,416 字节;各运行 3 次,READY 全部为 3,完成时间中位数分别为 19.87 / 19.69 ms,空闲 RSS 中位数同为 5.37 MiB,PSS 分别为 3.28 / 3.33 MiB,VmHWM 中位数同为 5.37 MiB。采用本机热缓存、独立 SQLite bundle、每次 READY 后 2 秒采样;无吞吐负载,不能把启动时间或空闲内存解释为业务吞吐/峰值容量。当前 cgroup 父目录不可写,未施加 64/128 MiB 硬限额,也未记录 cgroup peak。
默认只构建和运行 SQLite。真实 PostgreSQL 统一读取仓库根 `config/setting.json` 的 `database.postgres_test`,不接受环境变量或命令行连接覆盖;该本地文件已被 Git 忽略。其 `url` 必须在数据库名中包含唯一 `{case}`,例如 `postgres://bench:secret@127.0.0.1/dever_{case}`,查询参数或用户名中的占位符不算隔离。运行器按“输出目录短哈希 + driver + entry + repeat”展开 case,因此不同报告不会复用数据库;构建器和运行器以 create-new 和 `0600` 权限原子写入 bundle 的 `config/setting.json`,构建结束会删除含连接信息的临时配置,URL 不写入 manifest 或 report。运行器不会创建或清理 PostgreSQL 数据库,因此模板必须指向预先准备的专用测试数据库,不能指向已有业务库。
```json
{
"database": {
"default": {
"type": "sqlite",
"path": "data/db/test.db"
},
"postgres_test": {
"type": "postgres",
"url": "postgres://bench:secret@127.0.0.1/dever_{case}",
"tls": "disabled",
"min_connections": 0,
"max_connections": 4,
"max_page_size": 100,
"wait_timeout_ms": 5000,
"io_timeout_ms": 30000
}
}
}
```
```bash
python3 test/performance/run.py run \
--artifacts target/performance/build-current \
--output target/performance/orm-smoke \
--suite orm --duration 1 --warmup 0.2 --repeats 1
python3 test/performance/run.py run \
--artifacts target/performance/build-current \
--output target/performance/orm-postgres-smoke \
--suite orm --duration 1 --warmup 0.2 --repeats 1
```
ORM 报告包含 binary size、RSS/PSS/VmHWM、cgroup current/peak/OOM、线程/FD、吞吐和 p50/p95/p99。当前生产 runtime 没有低成本的 pool wait、SQL 数和数据库连接数观测点,报告将这些字段明确标为 `unavailable`,同时记录配置的 pool capacity 和进程 FD,不用推算值冒充真实指标。transaction cancel 先验证单连接 pool 可用,再由长事务占满 pool、在响应完成前断开客户端,并要求连接在 HTTP handler timeout 前归还、计数仍为 0;这样不会把 handler timeout 回滚误报为客户端取消,也不会绕过语言对 transaction 内并发任务的静态限制。
run/wait 同时跑 Dever 与 Rust-runtime 的 root/worker 两种位置。root 在 `run_entry_with` 入口执行;worker 在一个计时外创建的结构化外层任务内执行。二者内部都用原有 `task::run/wait`、Bytes 输入和 checked Int 加法,保留子任务额度、取消和清理;没有以裸 Tokio task 代替语言任务。这个实验不修改默认调度。Channel/取消暂时仍是 Dever 单方基线。`/proc` 上下文切换计数汇总采样时仍存活的线程,线程退出会使累计值减少;总量分析用独立 `perf stat`,不能将采样差值当作精确任务计数。
live 用 `--connection-tiers 32,128,512 --cycles 3 --duration 2` 配置,每个档位独立启动服务,每轮保持约 2 秒,再关闭全部客户端并等待 300 ms。Rust 对端发布零基轮次的 `connecting/connected/closing/disconnected` 四阶段;Python 只用 connected/disconnected 保持窗口比较资源,记录最后一次断开样本和相对基线的 FD/RSS 差值。采样前后阶段变化的过渡样本不归属任何阶段,也不写入阶段报告。正文不匹配、阶段缺失、SSE 保持期无心跳或未回收全部客户端都失败;RSS 保留可能来自分配器缓存,不能单凭不回落断言泄漏。默认只测 32 条,档位不能超过构建时的 connections;`--duration` 为 0.3–60 秒,采样间隔最多 100 ms。live 的 SSE/WS 是明文 HTTP/1 场景,不包含 WSS 或 HTTP/2。
HTTP/2 用 `物理连接数 x 每连接并发流数` 描述拓扑。当前固定每连接 16 流、单流接收窗口 65535 字节、连接接收窗口 262144 字节、frame/send-buffer 上限 16384 字节。客户端在计时前完成精确数量的 h2c 或 TLS+h2 握手,每个连接的 `SendRequest` clone 由其 16 个请求槽共享;测量期间不重试、不替换连接、不自动扩容。`request_slots` 必须等于两个维度的乘积,`successful_handshakes` 必须等于物理连接数。断开后等待 300 ms,再以 READY 基线核对 FD。
三个路径为 `/plain`(`hello`)、`/json`(预制 `{"ok":true}`)、`/bytes`(65536 个 `x`)。JSON 路径测 JSON 响应传输,不代表动态 JSON 解析/序列化性能。`/bytes` 跨越 HTTP/2 默认单流接收窗口,用于验证流控下的大正文传输。大正文在服务启动时准备,各服务均无业务日志。
网络记录 `scheduled/sent/completed/succeeded/errors/dropped`;`dropped_capacity` 表示无可用请求槽,`dispatch_expired` 表示负载发生器未能在窗口内发出。`latency_ms` 从**计划发送时间**算到成功完成,包含排队/调度;`request_latency_ms` 单独测实际发出到成功完成,`dispatch_lag_ms` 记录调度延迟。不能用两个 p99 相减估算请求耗时。没有成功样本时成功延迟为 null,失败不会稀释成功分位数。Tokio 定时器会让高频请求成批到期,这个发生器用于可复现的有界负载和相对比较,不适合独立证明微秒级服务延迟或极限吞吐。检查负载端 CPU、lag、丢弃,再解释服务结果。
延迟使用有界直方图:1 ms 内按微秒分桶,之后每个二倍区间分 64 桶,分位数取桶上界。每轮保留原始计数和资源采样,不把多个 p99 再平均成整体 p99。异步的 `p95_ns_per_operation_sample` 是批次平均耗时的分位数,**不是逐任务 p95**。`ready_seconds` 是父进程启动到观察 READY 的耗时,包含创建进程和最多约 5 ms 的观察误差。
## 小机器运行预算
建议先测 1 核/128 MiB,再试 64 MiB 和 2 核/256 MiB。这里是应用预算,不包含操作系统自身内存。`--server-cpu` 固定服务端 CPU,`--client-cpus` 指定逗号分隔的客户端 CPU 集合;`--client-workers` 指定 HTTP/1、HTTP/2 和 live 负载的 worker 数。客户端 CPU 不能包含服务端 CPU;不指定客户端集合时继承当前允许的 CPU。`--cpu-quota` 是计算份额上限,不能代替绑核。
```bash
python3 test/performance/run.py run \
--artifacts target/performance/build-current \
--output target/performance/budget-128m-1 \
--suite all --duration 10 --warmup 2 --repeats 3 \
--rates 100,1000 --paths /plain,/json,/bytes \
--concurrency 16 --connection-mode both \
--http2-connections 1,4 --recovery-connections 1 \
--connection-tiers 32,128,512 --cycles 3 \
--server-cpu 0 --client-cpus 1 --client-workers 1 \
--cgroup-parent /sys/fs/cgroup/system.slice \
--memory-mib 128 --cpu-quota 1
```
上例 cgroup 路径只适用于有相应权限且父组已启用 cpu/memory 控制器的机器。工具为每个被测进程创建新的 `dever-perf-*` 子组,仅把自己的子进程放进去;不会修改父组、其他服务或全局 swap。指定内存限额时把该子组 swap 限额设为零。没有权限会明确失败,不回退成“无限额通过”。负载端和 Python 采样进程留在预算之外。
`memory.current/peak` 包含该组被计费的用户内存、内核/网络资源等;不能与进程 RSS 混用。报告同时保留两者、OOM 事件和 CPU 节流统计。进程由 Python 子进程 exec 启动,cgroup 峰值也可能包含 exec 前短暂的 Python 启动页,空闲基线应看 READY 后的 current/RSS/PSS。50 ms 默认采样可能漏掉短暂峰值,VmHWM 和 cgroup peak 补充整个生命周期峰值。
构建和指纹读取会预热文件缓存,已缓存的可执行文件/共享库页可能已计费到父组或其他组,因此本工具的 cgroup current 可以小于进程 RSS。它不是冷启动总 RAM 成本;三者应交叉观察。本阶段测热文件缓存下的新进程,不操作系统 `drop_caches`,也不据此宣称整台机器最低内存。
运行器在结束/异常/中断时终止并回收自己创建的进程,删除自己的空 cgroup。memory/http/http2/live 服务最终由运行器结束进程,**不把这个过程当作语言优雅退出验证**;http2-recovery 和 live 单独验证连接多轮断开后的回收。小时级 soak、故障网络和真实小机器矩阵需另行执行。
每轮 Rust 负载程序使用同一已验摘要的不可变二进制硬链接,各自保留独立 `config/setting.json` 和日志;跨文件系统才复制。测量期间不得改写输入二进制。这样 100 轮连接恢复不会累积 100 份相同程序,也不通过符号链接让配置解析回到源程序目录。
## 对照边界与报告
- Rust-runtime 服务使用与 Dever 相同的 HTTP/TLS/task runtime,帮助定位源代码桥接开销。
- 原生 Hyper 服务匹配本测试正常 GET 的响应、连接数、头/正文上限和 TLS 配置;没有复刻 runtime 的完整 WriteTimeout、程序故障传播和结构化 Scope。不能由这个对照推断慢读/慢上传、异常报文的能力等价。
- 每轮倒转实现顺序,减轻固定执行顺序造成的偏置。构建配置、CPU 型号、CPU 亲和性、父 cgroup、二进制指纹、原始日志、资源采样、负载端占用写入报告。
- `cases.jsonl` 在每个场景完成后追加;`report.json` 即使中途失败也保留已完成结果。`status=complete` 表示测量完整,不表示没有请求错误或已经达到性能目标。先查看 errors/dropped/OOM,再查看延迟和内存。
- 不预设绝对 QPS 或最低内存。已有计算热路径的 1.15 对照门槛仍在 `test/dever-tests/tests/performance.rs`,不推广成 HTTP 或整个语言的性能承诺。
## 首轮结果:2026-09-12
环境为 Linux x86_64、Intel Xeon Processor (Skylake, IBRS) 虚拟 CPU。服务绑定 CPU 0,负载端绑定 CPU 1;服务 runtime 1 worker、2 blocking slots、272 task slots、32 connections,负载端 16 个请求槽。主测每场景预热 0.5 秒、测量 2 秒,重复 3 个独立进程,目标速率 1000 请求/秒。不是持续压力或吞吐上限测量。
| Dever 空闲场景 | 三轮 RSS 中位值 |
| --- | ---: |
| 同步程序 | 1.90 MiB |
| 异步运行时 | 2.55 MiB |
| 256 个已启动、等待 Channel 的任务 | 3.38 MiB |
| HTTP 监听 | 3.45 MiB |
| HTTPS 监听 | 4.37 MiB |
以上为 128 MiB 专属 cgroup 下的 READY 后采样。另用 64 MiB 限额重复 3 轮空闲场景,全部完成、无 OOM。READY 的一次输出会启动异步程序的 blocking worker,异步场景观察到 3 个线程;这是该 fixture 的基线,不是“完全不打印”的最小运行时。
异步三轮中位值:完整 `run/wait` 一轮约 15.90 µs,Channel 生产与消费平均每项约 0.89 µs;停止并排空 256 个已启动等待任务约 0.61–0.67 ms。前两项每进程校验 10×16384 项,去掉首批预热;包含结构化任务语义,不能把 run/wait 数字称为纯任务分配耗时。尚未提供这几组异步操作的同语义 Rust 对照。
HTTP/HTTPS 三实现、三路径共 54 组正式测量,108000 个计划请求中 107962 个发出且成功,0 请求错误。38 个未发出:32 个超过发送窗口、6 个无空闲并发槽;预热另有 43 个未发出,原始记录保留。各组成功速率约 999 请求/秒,受目标速率限制,不能宣称最大吞吐为 1000。
| Dever 路径 | HTTP 每轮实际请求 p99 范围 | HTTPS 每轮实际请求 p99 范围 |
| --- | ---: | ---: |
| /plain | 0.243–1.248 ms | 0.580–0.909 ms |
| /json | 0.280–0.342 ms | 0.897–2.752 ms |
| /bytes | 0.392–0.500 ms | 0.839–2.400 ms |
表中是 dispatched→completed,每列列出三个独立 p99 的范围,没有把它们合并成全局 p99;计划发送延迟和负载端 lag 另在 JSON 中。短时虚拟机抖动明显,这些结果不用于宣称哪种实现更快。Dever 在这组负载的 RSS 中位值约 HTTP 3.9 MiB、HTTPS 4.9–5.1 MiB。
另以每次新建连接、100 请求/秒测试 HTTP/HTTPS 全路径三实现,共 18 组、900 个正式请求,全部成功且无丢弃。主测 78 组(15 内存、9 异步、54 网络)、新连接 18 组、64 MiB 内存 15 组均无 OOM。
原始结果及对应产物保存在仓库 `target/performance/`:
- `baseline-128m/report.json`:最终三轮基线,对应 `build-2/manifest.json`。
- `connections-new-128m/report.json`:新连接模式,对应 build-2。
- `memory-64m/report.json`:64 MiB 空闲基线,对应 build-1;该组使用的 Dever 二进制与 build-2 一致。
- `smoke-1/report.json`:早期连通检查,保留当时指纹,不用于最终网络比较。
以上首轮结果保留当时的测试范围;后续测量见下节。
## 第二轮:高负载与调度定位
`http-load-p2-final/report.json` 复用 build-2 产物,1 worker、32 服务连接和 32 客户端请求槽、服务 1 CPU/128 MiB。仅 `/plain`,预热 0.5 秒、测量 2 秒;HTTP/HTTPS、三个实现、5000/15000/30000 请求每秒,均重复三轮,共 54 组。
正式窗口共计划 1800000 个请求,1500830 个实际发出且成功,错误 0。未发出 299170 个:容量丢弃 298476、发送窗口过期 694;所有预算组无 OOM。下表是三个独立进程的成功 QPS 中位值,不是最大吞吐。
| 目标请求/秒 | Dever HTTP | Rust-runtime HTTP | Hyper HTTP | Dever HTTPS | Rust-runtime HTTPS | Hyper HTTPS |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 5000 | 4993 | 4994 | 4993 | 4970 | 4972 | 4970 |
| 15000 | 14904 | 14897 | 14922 | 14839 | 14756 | 14726 |
| 30000 | 21866 | 22187 | 22324 | 21217 | 21320 | 22236 |
最高输入档位 Dever 服务 CPU 中位值为 HTTP 0.53 核、HTTPS 0.62 核,客户端为 0.77/0.86 核;Dever RSS 约 4.12/5.32 MiB。32 请求槽与定时器成批发送已经影响结果,不能据此断言服务端上限是每秒两万。提高并发槽、使用独立负载机器应作为下一次吞吐上限测量的条件。
`http-load-p2/report.json` 保留初次中断:Linux 退出时 `/proc` 内存字段先于 waitpid 消失,导致采样工具失败。修复后才完整重跑;不能把初次中断算成语言服务失败,也不将两次样本混合。
`async-p2/report.json` 使用 build-p2、1 worker/1 CPU/128 MiB、1040 task slots、每批 16384 次操作,重复 3 个进程;每进程十批、去掉首批。共 18 组,校验全部通过且无 OOM。下表为进程内中位数再取三进程中位数:
| run/wait 位置 | Dever | 同 runtime、同优化参数的 Rust |
| --- | ---: | ---: |
| 入口 root | 15.50 µs/次 | 15.59 µs/次 |
| 外层结构化任务 worker | 2.003 µs/次 | 1.984 µs/次 |
这个串行微任务场景中,调度位置使 Dever 平均单次耗时降低约 87%,约 7.7 倍速度;不推广为所有业务程序加速。两种语言路径数值接近。源码路径显示 `run_entry_with` 用入口线程 block_on,run body 和 supervisor 在 Tokio worker 执行;单核下逐项等待造成频繁 OS 线程切换。`profile-p2/final-runtime-*.stat` 是独立 perf stat 的整体软件计数,对应 build-p2/async_bench;未加 cgroup、固定 CPU 0。虚拟机不支持 cycles/instructions,未报告硬件 IPC 或采样火焰图。
同组 Channel 约 0.912 µs/项;256 个任务停止并排空三次共 0.688/0.916/1.045 ms。这里只新增对照和证据,生产入口调度尚未优化。后续优化入口调度时需确认入口 future 的 Send 条件,根任务不应挤占子任务配额,并验证入口故障、子任务取消和 blocking 清理;不应直接照搬实验外层 run 或删除结构化任务语义。
## 第二轮:长连接容量与回收
`live-p2-final/report.json` 使用 build-p2-final、1 worker/1 CPU/128 MiB、512 connection slots、1040 task slots;32/128/512 条连接各重复三进程,每进程三轮连接/保持 2 秒/断开。TCP/WS 每约 100 ms 校验一次小消息回显,SSE 校验初始事件和 100 ms 心跳。
27 组共建立并关闭 18144 条连接,校验 377061 条消息,无协议错误和 OOM。81 次断开后的服务 FD 均回到启动基线。下面是第三轮保持窗口 RSS 的三个独立进程中位值,包含分配器在此前两轮保留的内存:
| 同时连接数 | TCP RSS | WebSocket RSS | SSE RSS |
| ---: | ---: | ---: | ---: |
| 32 | 3.99 MiB | 5.11 MiB | 5.03 MiB |
| 128 | 4.90 MiB | 9.44 MiB | 9.16 MiB |
| 512 | 8.70 MiB | 17.00 MiB | 24.97 MiB |
512 条连接保持时服务 CPU 约 TCP 0.15–0.16 核、WS 0.18–0.20 核、SSE 0.12–0.14 核。这里测的是持续小消息和心跳连接,不是大消息吞吐或完全不读的客户端。
初次 `live-p2/report.json` 也完成了全部消息校验,但有一条下一轮的采样被误归属上轮 disconnected,产生假的 FD +15。最终报告使用采样前后阶段一致性判断,完整重跑;资源回收结论只引用 final 报告。
随后 `live-cycles-p2/report.json` 对每种协议保持 512 条连接,十轮重连、每轮保持 3 秒,单场景约 34 秒。共 15360 次连接、471207 条校验消息,错误/OOM 为零,30 次 FD 回落均为零差值。TCP 断开后的 RSS 最后稳定在约 8.6–8.8 MiB,WS 约 17 MiB;SSE 前三轮 21.33/24.48/25.12 MiB,后七轮为 25.24–25.36 MiB,未观察到按轮持续增长。这是短时重复回收证据,不是小时级泄漏或生产网络稳定性证明。
复现本轮长连接两组命令(输出目录另取新名称):
```bash
python3 test/performance/run.py run --artifacts target/performance/build-p2-final --output target/performance/live-repeat --suite live --duration 2 --connection-tiers 32,128,512 --cycles 3 --repeats 3 --server-cpu 0 --client-cpus 1 --client-workers 1 --cgroup-parent /sys/fs/cgroup/system.slice --memory-mib 128 --cpu-quota 1
python3 test/performance/run.py run --artifacts target/performance/build-p2-final --output target/performance/live-more-cycles --suite live --duration 3 --connection-tiers 512 --cycles 10 --repeats 1 --server-cpu 0 --client-cpus 1 --client-workers 1 --cgroup-parent /sys/fs/cgroup/system.slice --memory-mib 128 --cpu-quota 1
```
本轮 12 个运行器检查、4 个独立 TCP/SSE 对端检查通过,11 个 Dever 入口及两个新 Rust example 离线构建通过。Rustfmt/Clippy 未安装,未运行;Dever 格式、Python 解析、源码空白与最终范围检查通过。生产编译器/运行时没有修改,未运行全量测试。
## 第三轮:HTTP/2 吞吐、流控与连接回收
2026-09-13 使用 `build-http2-final` 产物完成正式矩阵。环境仍为 Linux x86_64、Intel Xeon Processor (Skylake, IBRS) 虚拟 CPU;服务绑定 CPU 0,负载端绑定 CPU 1。服务使用 1 worker、128 MiB cgroup、1 CPU quota、swap 0;负载端不计入服务预算。每个 case 预热 0.5 秒、测量 2 秒、启动独立服务进程,三实现与两种传输的执行顺序逐轮倒转。
`/plain` 共 108 个 case、3600000 个计划请求,其中 2729242 个发出且完整成功,错误 0。870758 个未发出请求中,869368 个受请求槽限制,1390 个超过调度窗口;全部计数守恒。108 次正式负载和 108 次预热的握手数均与物理连接数一致,断开后的 FD 均回到 READY 基线,无 OOM。下表是 30000 请求/秒输入档位的三轮中位数;p99 是实际发出到完整响应的请求延迟。
| 实现 | 传输 | 1x16 成功 QPS | 4x16 成功 QPS | 4x16 请求 p99 | 4x16 服务 RSS |
| --- | --- | ---: | ---: | ---: | ---: |
| Dever | h2c | 10578 | 29575 | 2.208 ms | 4.60 MiB |
| Rust-runtime | h2c | 10804 | 28996 | 3.392 ms | 5.00 MiB |
| Hyper | h2c | 11956 | 29108 | 1.984 ms | 4.59 MiB |
| Dever | TLS+h2 | 9957 | 29814 | 1.840 ms | 5.59 MiB |
| Rust-runtime | TLS+h2 | 10122 | 28681 | 2.880 ms | 6.07 MiB |
| Hyper | TLS+h2 | 11555 | 28719 | 3.200 ms | 5.68 MiB |
Dever 4x16 在 5000、15000、30000 三档的 h2c 成功 QPS 中位值分别为 4995、14983、29575,TLS+h2 分别为 4994、14977、29814。1x16 在高输入下只有约 10k–10.6k 成功 QPS,而 4x16 接近 30k,说明这次单连接结果受固定 16 个请求槽明显约束;不能把任一数字解释成服务端极限。最高档 4x16 的 Dever 服务 CPU 中位值为 h2c 0.89 核、TLS+h2 0.93 核,负载端也约 0.9 核,进一步限制了极限吞吐结论。三种实现的短时差异不具备跨机器性能承诺意义。
`/bytes` 共 108 个 case、115200 个计划请求,115180 个完整成功、错误 0、丢弃 20、无 OOM,握手和 FD 检查全部通过。三种实现、两种传输、两种拓扑在 100/500/1000 请求每秒档均维持目标速率;这相当于单 case 最高约 62.5 MiB/s 的响应正文。Dever 的 RSS 中位值为 h2c 4.04–4.23 MiB、TLS+h2 5.05–5.30 MiB。短 case 的 p99 有明显虚拟机抖动,原始三轮数据比单个汇总值更适合定位异常。
`http2-recovery-128m` 对三种实现、h2c/TLS 各在同一服务进程中运行 10 轮 1x16 连接周期,共 60 轮、30000 个计划请求;29984 个完整成功、错误 0、丢弃 16。每轮握手数正确,60 次断开后的 FD 差值均为 0,无 OOM。六组最后一轮相对第一轮的断开后 RSS 变化为 8–100 KiB,短时序列未观察到按轮持续增长;这不是小时级泄漏证明。
另在 64 MiB、1 CPU、swap 0 下运行 Dever 1x16、5000 请求每秒的 h2c/TLS 代表场景。20000 个计划请求中 19991 个完整成功、错误 0、丢弃 9、无 OOM;h2c/TLS 成功 QPS 为 4997/4996,RSS 为 4.04/5.17 MiB,PSS 为 2.00/3.10 MiB。cgroup peak 只有 1.30/1.35 MiB,是热文件页可能已计费给其它组所致,不能代替进程 RSS/PSS 或冷启动总内存。
正式结果和构建清单位于:
- `build-http2-final/manifest.json`:16 个二进制的指纹、大小、构建配置和固定 HTTP/2 limits。
- `http2-plain-128m/report.json`:`/plain` 128 MiB 正式矩阵。
- `http2-bytes-128m/report.json`:`/bytes` 128 MiB 正式矩阵。
- `http2-recovery-128m/report.json`:128 MiB 十轮连接恢复矩阵。
- `http2-active-64m/report.json`:64 MiB Dever 活跃代表场景。
历史 `build-http2-final` 产物已不在当前工作树;以下是当时使用的命令记录,精确复现历史二进制须先恢复匹配的旧产物。本机当前可直接运行的命令见下方第四轮,复现时必须换新输出目录:
```bash
python3 test/performance/run.py run --artifacts target/performance/build-http2-final --output target/performance/http2-plain-repeat --suite http2 --duration 2 --warmup 0.5 --rates 5000,15000,30000 --paths /plain --http2-connections 1,4 --repeats 3 --server-cpu 0 --client-cpus 1 --client-workers 1 --cgroup-parent /sys/fs/cgroup/system.slice --memory-mib 128 --cpu-quota 1
python3 test/performance/run.py run --artifacts target/performance/build-http2-final --output target/performance/http2-bytes-repeat --suite http2 --duration 2 --warmup 0.5 --rates 100,500,1000 --paths /bytes --http2-connections 1,4 --repeats 3 --server-cpu 0 --client-cpus 1 --client-workers 1 --cgroup-parent /sys/fs/cgroup/system.slice --memory-mib 128 --cpu-quota 1
python3 test/performance/run.py run --artifacts target/performance/build-http2-final --output target/performance/http2-recovery-repeat --suite http2-recovery --duration 0.5 --warmup 0.1 --cycles 10 --recovery-rate 1000 --recovery-path /plain --recovery-connections 1 --repeats 1 --server-cpu 0 --client-cpus 1 --client-workers 1 --cgroup-parent /sys/fs/cgroup/system.slice --memory-mib 128 --cpu-quota 1
```
N5 的接口与约束见 [LANGUAGE.md](../../LANGUAGE.md)。这轮只建立 loopback、热文件缓存、短时固定速率下的相对证据,不包含跨主机网络、故障注入、小时级 soak 或多平台矩阵。HTTP/2 不保证所有负载都比 HTTP/1.1 快;普通 WebSocket 继续走 HTTP/1,RFC 8441 扩展 CONNECT 尚未实现。
## 第四轮:多核负载端、HTTP/2 饱和与稳态
2026-09-18 使用 `typed-orm-build-v2/manifest.json` 中校验过指纹的现存 HTTP/2 二进制和测试证书;原 `build-http2-final` 不在当前工作树,未重建产物。服务端绑定 CPU 0、1 worker、1 CPU quota、无 swap;负载端为 CPU 1–4、4 workers,位于服务 cgroup 外。机器允许 CPU 0–4。服务预算为 128 MiB,稳态另测 64 MiB。所有场景使用 `/plain`、16 条物理连接、每连接 16 流,只有自有 loopback 端口;这是同一虚拟机的热缓存观测,不是跨机器或生产容量。
128 MiB 扫描覆盖 h2c/TLS+h2 的 30k/45k/60k/90k 输入,每档预热 1 秒、计时 5 秒、2 个重复。16/16 case 计数守恒,请求错误、OOM 和断开后的 FD 差均为 0;每次握手数为 16。下表为成功 QPS 的两轮中位值:
| 传输 | 30k | 45k | 60k | 90k |
| --- | ---: | ---: | ---: | ---: |
| h2c | 29,899 | 36,690 | 36,027 | 39,486 |
| TLS+h2 | 29,770 | 38,205 | 37,410 | 38,136 |
45k 以上服务 CPU 约 0.96–1.00 核,客户端约 1.43–1.97 核;未发送请求主要因 256 个请求槽满,调度窗口过期数量相对很少。因此观测到当前拓扑约 36k–40k req/s 的吞吐平台,不能把它归结为纯服务端极限或把输入 90k 当成实际吞吐。28k 的首轮 60 秒成功率为 h2c 98.68%、TLS+h2 97.91%,未达 99% 验收门槛;24k 的 10 秒校准 TLS+h2 为 99.18%,余量仍小。20k 的 10 秒校准分别为 99.96% 和 99.92%,所以正式稳态统一采用 20k,低于较低观测平台的 80%。这些探索报告保留,不算正式稳态通过。
正式每个 case 预热 1 秒、测量 60 秒、计划 1,200,000 次请求。成功率是成功请求除以计划请求,容量丢弃仍在原始报告中,不称为零丢弃:
| 服务预算 | 传输 | 成功/计划 | 成功 QPS | 请求 p99 | 服务 RSS 高水位 | OOM/错误/FD 差 |
| --- | --- | ---: | ---: | ---: | ---: | --- |
| 128 MiB | h2c | 1,197,220/1,200,000 (99.77%) | 19,953 | 5.376 ms | 6.78 MiB | 0/0/0 |
| 128 MiB | TLS+h2 | 1,199,036/1,200,000 (99.92%) | 19,983 | 4.800 ms | 7.83 MiB | 0/0/0 |
| 64 MiB | h2c | 1,199,221/1,200,000 (99.94%) | 19,986 | 4.672 ms | 6.43 MiB | 0/0/0 |
| 64 MiB | TLS+h2 | 1,197,545/1,200,000 (99.80%) | 19,958 | 5.184 ms | 7.90 MiB | 0/0/0 |
128 MiB 下在各自的同一服务进程上完成 h2c/TLS+h2 各 100 轮 16x16 恢复;每轮 0.1 秒、1000 req/s,随后观察断开 0.3 秒。两组各计划 10,000 次、成功 9,944 次,其余各 56 次为调度窗口过期,请求错误和 OOM 为 0。每轮至少成功 95(h2c)/98(TLS+h2)次,握手始终 16,200 次断开后的 FD 差全部为 0。断开后 RSS 从首轮到末轮为 h2c 4,845,568→5,357,568 B,TLS+h2 5,959,680→6,647,808 B;保留完整逐轮序列,不将分钟级增长或回落解释成小时级泄漏结论。邻接的 HTTP/HTTPS 各 30/30 成功,TCP/WS/SSE 各 16/16 连接完成关闭,零错误。
复现命令(输出目录必须换新名称;运行前确认 CPU 0–4 和 cgroup 权限,不接触现有服务):
```bash
python3 test/performance/run.py run --artifacts target/performance/typed-orm-build-v2 --output target/performance/http2-scan-next --suite http2 --duration 5 --warmup 1 --rates 30000,45000,60000,90000 --paths /plain --http2-connections 16 --repeats 2 --implementations dever --server-cpu 0 --client-cpus 1,2,3,4 --client-workers 4 --cgroup-parent /sys/fs/cgroup/system.slice --memory-mib 128 --cpu-quota 1
python3 test/performance/run.py run --artifacts target/performance/typed-orm-build-v2 --output target/performance/http2-steady-128-next --suite http2 --duration 60 --warmup 1 --rates 20000 --paths /plain --http2-connections 16 --repeats 1 --implementations dever --server-cpu 0 --client-cpus 1,2,3,4 --client-workers 4 --cgroup-parent /sys/fs/cgroup/system.slice --memory-mib 128 --cpu-quota 1
python3 test/performance/run.py run --artifacts target/performance/typed-orm-build-v2 --output target/performance/http2-steady-64-next --suite http2 --duration 60 --warmup 1 --rates 20000 --paths /plain --http2-connections 16 --repeats 1 --implementations dever --server-cpu 0 --client-cpus 1,2,3,4 --client-workers 4 --cgroup-parent /sys/fs/cgroup/system.slice --memory-mib 64 --cpu-quota 1
python3 test/performance/run.py run --artifacts target/performance/typed-orm-build-v2 --output target/performance/http2-recovery-next --suite http2-recovery --duration 0.1 --cycles 100 --recovery-rate 1000 --recovery-connections 16 --repeats 1 --implementations dever --server-cpu 0 --client-cpus 1,2,3,4 --client-workers 4 --cgroup-parent /sys/fs/cgroup/system.slice --memory-mib 128 --cpu-quota 1
```
本轮原始结果位于 `target/performance/http2-ceiling-{scan,steady-128m-20k,steady-64m-20k,recovery}-09-18/`;28k 首轮及 24k/20k 校准在相邻命名目录。吞吐平台、20k 持续负载和 100 轮恢复是不同问题,不能互相替代;服务与负载仍共享虚拟机和热缓存,结果不外推到真实网络、小物理机或小时级运行。
+466
View File
@@ -0,0 +1,466 @@
"""构建可搬运的基准产物;构建耗时与运行测量分开。"""
from copy import deepcopy
import hashlib
import json
from pathlib import Path
import secrets
import shutil
import subprocess
import tempfile
import time
from urllib.parse import urlsplit
from settings import PEER_CONFIGURATION, stage_peer, write_private_json
ROOT = Path(__file__).resolve().parents[2]
FIXTURES = Path(__file__).parent / "fixtures"
TLS = ROOT / "test/dever-tests/fixtures/tls"
SETTING_PATH = ROOT / "config/setting.json"
POSTGRES_TEST_CONNECTION = "postgres_test"
APPLICATION_CONFIGURATION = "llvm-cmd-v1"
ENTRIES = {
"runtime": ("sync_idle", "async_idle", "tasks", "tasks_on_worker", "channels", "cancel"),
"http": ("http", "https", "http2", "https2"),
"live": ("tcp", "ws", "sse"),
}
ORM_ENTRIES = ("orm_idle", "orm_crud", "orm_list", "orm_cursor", "orm_stream",
"orm_pool", "orm_http")
RUNTIME_PROFILES = {
"base": ("runtime-api", "runtime-external"),
"sqlite": ("runtime-api", "runtime-external", "runtime-sqlite"),
"postgres": ("runtime-api", "runtime-external", "runtime-postgres"),
"both": ("runtime-api", "runtime-external", "runtime-sqlite", "runtime-postgres"),
}
BENCHMARK_COMMAND = "benchmark.command.run"
CONFIG_FUNCTIONS = {
"runtime": ("lifetime", "idle_sample_ms", "pending", "channel_capacity", "input", "pending_input"),
"http": ("lifetime", "limits", "http2_limits", "large_body", "certificate", "key"),
"live": (),
"orm": ("lifetime", "orm_input", "orm_rows", "orm_concurrency", "orm_cancel_work"),
}
def fingerprint(path):
with path.open("rb") as binary:
digest = hashlib.file_digest(binary, "sha256").hexdigest()
return {"sha256": digest, "bytes": path.stat().st_size}
def postgres_test_connection(document):
try:
connection = document["database"][POSTGRES_TEST_CONNECTION]
except (KeyError, TypeError) as error:
raise ValueError(
f"config/setting.json must define database.{POSTGRES_TEST_CONNECTION}"
) from error
if not isinstance(connection, dict):
raise ValueError(f"database.{POSTGRES_TEST_CONNECTION} must be an object")
if connection.get("type") != "postgres":
raise ValueError(f"database.{POSTGRES_TEST_CONNECTION} must use type 'postgres'")
max_connections = connection.get("max_connections")
if type(max_connections) is not int or not 1 <= max_connections <= 256:
raise ValueError(
f"database.{POSTGRES_TEST_CONNECTION}.max_connections must be an explicit 1..256 integer"
)
return connection
def postgres_case_url(document, case_name):
template = postgres_test_connection(document).get("url")
if not isinstance(template, str):
raise ValueError(f"database.{POSTGRES_TEST_CONNECTION}.url must be Text")
parsed = urlsplit(template)
database = parsed.path.removeprefix("/")
if (template.count("{case}") != 1 or "{case}" not in database
or not database or "/" in database):
raise ValueError(
f"database.{POSTGRES_TEST_CONNECTION}.url must contain one {{case}} placeholder "
"in its database name"
)
if parsed.scheme not in ("postgres", "postgresql"):
raise ValueError(
f"database.{POSTGRES_TEST_CONNECTION}.url must use postgres:// or postgresql://"
)
return template.replace("{case}", case_name)
def postgres_database_case(output, case_name):
run = hashlib.sha256(str(output.resolve()).encode()).hexdigest()[:12]
return f"{run}_{case_name.replace('-', '_')}"
def load_postgres_setting(path=SETTING_PATH):
if not path.is_file():
return None
try:
document = json.loads(path.read_text())
except (OSError, json.JSONDecodeError) as error:
raise ValueError(f"cannot load '{path}': {error}") from error
if not isinstance(document, dict):
raise ValueError(f"'{path}' must contain a JSON object")
connections = document.get("database")
if connections is None:
return None
if not isinstance(connections, dict):
raise ValueError(f"'{path}' database must be an object")
if POSTGRES_TEST_CONNECTION not in connections:
return None
postgres_case_url(document, "validation")
return document
def configuration(args, postgres_setting=None):
settings = {
"workers": args.workers,
"connections": args.connections,
"pending": args.pending,
"iterations": args.iterations,
"channel_capacity": 64,
"timeout_ms": 2000,
"lifetime_ms": 3_600_000,
"idle_sample_ms": 3000,
"task_capacity": max(args.pending + 16, 2 * args.connections + 16),
"max_blocking_threads": 2,
"http2_streams": args.http2_streams,
"http2_stream_window_bytes": args.http2_stream_window_bytes,
"http2_connection_window_bytes": args.http2_connection_window_bytes,
"orm_iterations": args.orm_iterations,
"orm_rows": args.orm_rows,
"orm_concurrency": args.orm_concurrency,
"orm_cancel_work": args.orm_cancel_work,
"orm_sqlite_connections": args.orm_sqlite_connections,
"orm_postgres_connections": (
postgres_test_connection(postgres_setting)["max_connections"]
if postgres_setting is not None else None
),
}
if settings["task_capacity"] > 65536:
raise ValueError("task capacity exceeds the language limit")
return settings
def config_source(settings, fixture):
names = CONFIG_FUNCTIONS[fixture]
functions = {
"lifetime": ("Int", str(settings["lifetime_ms"])),
"idle_sample_ms": ("Int", str(settings["idle_sample_ms"])),
"pending": ("Int", str(settings["pending"])),
"channel_capacity": ("Int", str(settings["channel_capacity"])),
"input": ("Bytes", f'dever.bytes.from_text({json.dumps("x" * settings["iterations"])})'),
"pending_input": ("Bytes", f'dever.bytes.from_text({json.dumps("x" * settings["pending"])})'),
"orm_input": ("Bytes", f'dever.bytes.from_text({json.dumps("x" * settings["orm_iterations"])})'),
"orm_rows": ("Bytes", f'dever.bytes.from_text({json.dumps("r" * settings["orm_rows"])})'),
"orm_concurrency": ("Int", str(settings["orm_concurrency"])),
"orm_cancel_work": ("Int", str(settings["orm_cancel_work"])),
"limits": ("dever.http.Limits", "dever.http.Limits {\nheader_bytes = 8192\n"
f'body_bytes = 65536\ntimeout_ms = {settings["timeout_ms"]}\n'
f'connections = {settings["connections"]}\nhttp2 = null\n}}'),
"http2_limits": ("dever.http.Limits", "dever.http.Limits {\nheader_bytes = 8192\n"
f'body_bytes = 65536\ntimeout_ms = {settings["timeout_ms"]}\n'
f'connections = {settings["connections"]}\nhttp2 = dever.http.Http2Limits {{\n'
f'streams = {settings["http2_streams"]}\n'
f'stream_window_bytes = {settings["http2_stream_window_bytes"]}\n'
f'connection_window_bytes = {settings["http2_connection_window_bytes"]}\n}}\n}}'),
}
if "certificate" in names:
functions["certificate"] = ("Bytes", f'dever.bytes.from_text({json.dumps((TLS / "server.pem").read_text())})')
if "key" in names:
functions["key"] = ("Bytes", f'dever.bytes.from_text({json.dumps((TLS / "server-key.pem").read_text())})')
source = ""
for name in names:
if name == "large_body":
continue
kind, expression = functions[name]
source += f"\n{name}() (result: {kind}) {{\nresult = {expression}\n}}\n"
if "large_body" in names:
source += '\ndouble(value: Int, text: Text) (result: Text) { result = text + text }\n'
source += '\nlarge_body() (result: Bytes) {\nresult = dever.bytes.from_text(reduce(double, '
source += '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "x"))\n}\n'
return source
def stage_application_command(module, capability):
command = module / "benchmark/command"
command.mkdir(parents=True, exist_ok=True)
(command / "app.dever").write_text(
f"run() (result: Bool) {{\n result = {capability}()\n}}\n"
)
(command / "api.dever").write_text("cmd run = app.run\n")
def stage_benchmark_support(module, settings, fixture):
if fixture in ("runtime", "orm"):
bench = module / "benchmark/bench/app.dever"
bench.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(FIXTURES / "bench.dever", bench)
if CONFIG_FUNCTIONS[fixture]:
config = module / "benchmark/config/app.dever"
config.parent.mkdir(parents=True, exist_ok=True)
config.write_text(config_source(settings, fixture))
def database_setting(driver, settings, postgres_setting=None, case_name=None):
if driver == "sqlite":
database = {
"type": "sqlite",
"path": "data/db/orm.db",
"max_connections": settings["orm_sqlite_connections"],
"max_page_size": max(32, min(settings["orm_rows"], 10_000)),
}
elif driver == "postgres":
if postgres_setting is None or case_name is None:
raise ValueError("PostgreSQL requires config/setting.json")
database = deepcopy(postgres_test_connection(postgres_setting))
database["url"] = postgres_case_url(postgres_setting, case_name)
else:
raise ValueError(f"unknown database driver: {driver}")
document = ({name: deepcopy(postgres_setting[name])
for name in ("http", "log") if name in postgres_setting}
if driver == "postgres" else {})
document.setdefault("http", {})
document.setdefault("log", {})
document["database"] = {"default": database}
document["auth"] = {"providers": {"benchmark": {
"verify": "benchmark.item.verify",
"jwtSecret": secrets.token_urlsafe(48),
"ttlSeconds": 3600,
}}}
document["sites"] = {"benchmark": {"path": "", "auth": "benchmark"}}
return document
def profile_database_setting(profile):
if profile == "base":
return {}
sqlite = {
"type": "sqlite",
"path": "data/db/profile.db",
"max_connections": 1,
"max_page_size": 100,
}
if profile == "sqlite":
return {"database": {"default": sqlite}}
if profile not in ("postgres", "both"):
raise ValueError(f"unknown runtime profile: {profile}")
postgres = {
"type": "postgres",
"url": "postgres://build-only.invalid/dever_profile",
"tls": "disabled",
"max_connections": 1,
}
if profile == "postgres":
return {"database": {"default": postgres}}
return {"database": {"default": sqlite, "postgres_profile": postgres}}
def dependency_packages(text):
return sorted({line.removesuffix(" (*)") for line in text.splitlines() if line})
def runtime_dependencies(cargo, features):
command = [cargo, "tree", "--offline", "--locked", "-p", "dever-backend-bridge",
"--no-default-features", "--prefix", "none"]
if features:
command.extend(("--features", ",".join(features)))
return dependency_packages(subprocess.check_output(command, cwd=ROOT, text=True))
def prepare_native_project(destination, fixture, settings):
module = destination / "module"
domain = module / "benchmark" / fixture
domain.mkdir(parents=True)
shutil.copyfile(FIXTURES / fixture / "app.dever", domain / "app.dever")
for role in ("port", "adapter", "api"):
shutil.copyfile(FIXTURES / fixture / f"{role}.dever",
domain / f"{role}.dever")
stage_benchmark_support(module, settings, fixture)
config = destination / "config"
config.mkdir()
write_private_json(config / "setting.json",
native_runtime_setting(settings) if fixture == "live" else {})
def prepare_project(destination, fixture, settings, driver, postgres_setting=None):
module = destination / "module"
domain = module / "benchmark/item"
domain.mkdir(parents=True)
shutil.copyfile(FIXTURES / fixture / "app.dever", domain / "app.dever")
shutil.copyfile(FIXTURES / fixture / "app/model/item.dever", domain / "model.dever")
if fixture == "orm":
shutil.copyfile(FIXTURES / fixture / "app/api.dever", domain / "api.dever")
stage_benchmark_support(module, settings, fixture)
stage_application_command(module, "benchmark.item.orm_idle")
config = destination / "config"
config.mkdir()
write_private_json(
config / "setting.json",
database_setting(driver, settings, postgres_setting, "build_only"),
)
(destination / "data").mkdir()
def prepare_profile_project(destination, setting):
shutil.copytree(FIXTURES / "profile", destination / "module")
config = destination / "config"
config.mkdir()
write_private_json(config / "setting.json", setting)
(destination / "data").mkdir()
def prepare_cms_project(destination, source):
shutil.copytree(ROOT / "examples/cms" / source, destination)
def native_runtime_setting(settings):
return {"adapter": {"benchmark.live": {"setting": {
"limits": {
"header_bytes": 8192,
"body_bytes": 65536,
"timeout_ms": settings["timeout_ms"],
"connections": settings["connections"],
"http2": None,
},
"lifetime_ms": settings["lifetime_ms"],
}}}}
def record_binary(manifest, name, binary, started, **metadata):
manifest["binaries"][name] = {
**fingerprint(binary),
"build_seconds": time.monotonic() - started,
**metadata,
}
def build_profile_report(args, manifest, log):
report = {"source": "profile/benchmark/command/app.dever", "profiles": {}}
for profile, features in RUNTIME_PROFILES.items():
item = {"runtime_features": list(features),
"dependencies": runtime_dependencies(args.cargo, features)}
print(f"build profile-{profile}", flush=True)
binary_name = f"profile-{profile}"
binary = args.output.resolve() / binary_name
started = time.monotonic()
project = args.output.resolve() / "source" / f"profile-{profile}"
setting = profile_database_setting(profile)
prepare_profile_project(project, setting)
subprocess.run(
[args.compiler.resolve(), "build", project, "--output", binary],
stdout=log,
stderr=log,
check=True,
)
record_binary(manifest, binary_name, binary, started,
profile=profile, fixture="profile")
item.update({"status": "built", "binary": fingerprint(binary)})
report["profiles"][profile] = item
path = args.output.resolve() / "profile-report.json"
path.write_text(json.dumps(report, indent=2) + "\n")
manifest["files"]["profile-report.json"] = fingerprint(path)
def build(args):
postgres_setting = load_postgres_setting() if args.selection == "all" else None
settings = configuration(args, postgres_setting)
destination = args.output.resolve()
destination.mkdir(parents=True, exist_ok=False)
compiler = args.compiler.resolve()
cargo = shutil.which(str(args.cargo))
if cargo is None:
raise ValueError(f"cannot find Cargo command: {args.cargo}")
args.cargo = Path(cargo)
has_protocols = args.selection in ("all", "protocols")
peers = ({name: getattr(args, name).resolve()
for name in ("network_bench", "live_bench", "async_bench")}
if has_protocols else {})
required_tools = (compiler, *peers.values())
for executable in required_tools:
if not executable.is_file():
raise ValueError(f"build the executable first: {executable}")
# 在昂贵构建之前拒绝旧环境变量协议产物;这里只读配置,不启动网络。
for name, peer in peers.items():
if name == "async_bench":
continue # The finite Rust reference takes explicit positional arguments.
with tempfile.TemporaryDirectory(prefix="dever-peer-config-") as temporary:
executable = stage_peer(peer, Path(temporary), {"workers": 2})
result = subprocess.run([executable, "check-config"], capture_output=True,
text=True, check=True, timeout=5)
if json.loads(result.stdout) != {"workers": 2}:
raise ValueError(f"benchmark peer does not implement {PEER_CONFIGURATION}: {peer}")
manifest = {
"peer_configuration": PEER_CONFIGURATION,
"application_configuration": APPLICATION_CONFIGURATION,
"selection": args.selection,
"settings": settings,
"cargo": subprocess.check_output([cargo, "-V"], text=True),
"compiler": fingerprint(compiler),
"optimization": "LLVM O2 application objects, LLD static link, selected prebuilt runtime archive",
"runtime_scheduling": "Dever uses production runtime defaults; workers/task_capacity configure Rust references only",
"binaries": {},
"files": {},
}
with (destination / "build.log").open("w") as log:
for fixture, entries in (ENTRIES.items() if has_protocols else ()):
source = destination / "source" / fixture
prepare_native_project(source, fixture, settings)
print(f"build {fixture}", flush=True)
started = time.monotonic()
executable = source / "app"
subprocess.run([compiler, "build", source, "--output", executable],
stdout=log, stderr=log, check=True)
for entry in entries:
binary = destination / entry
shutil.copy2(executable, binary)
record_binary(manifest, entry, binary, started, profile="base", fixture=fixture,
command=f"benchmark.{fixture}.{entry}", shared_application=fixture)
drivers = ()
if args.selection == "all":
drivers = ("sqlite", "postgres") if postgres_setting is not None else ("sqlite",)
for driver in drivers:
project = destination / "source" / f"orm-{driver}"
try:
prepare_project(project, "orm", settings, driver, postgres_setting)
subprocess.run([compiler, "fmt", project], stdout=log, stderr=log, check=True)
for entry in ORM_ENTRIES:
name = f"{driver}-{entry.removeprefix('orm_')}"
print(f"build {name}", flush=True)
binary = destination / name
started = time.monotonic()
command_entry = "benchmark.item.orm_seed" if entry == "orm_http" else f"benchmark.item.{entry}"
stage_application_command(project / "module", command_entry)
subprocess.run([compiler, "build", project, "--output", binary],
stdout=log, stderr=log, check=True)
record_binary(manifest, name, binary, started, profile=driver, fixture="orm",
entry=entry)
finally:
if driver == "postgres":
(project / "config/setting.json").unlink(missing_ok=True)
cms_sources = {"all": ("dever",), "cms-profiles": ("dever", "md"),
"profiles": (), "protocols": ()}[args.selection]
for source in cms_sources:
cms_project = destination / "source" / f"cms-{source}"
prepare_cms_project(cms_project, source)
config_name = f"source/cms-{source}/config/setting.json"
manifest["files"][config_name] = fingerprint(cms_project / "config/setting.json")
binary_name = "cms" if source == "dever" else "cms-md"
print(f"build cms-{source}", flush=True)
cms_binary = destination / binary_name
started = time.monotonic()
subprocess.run([compiler, "build", cms_project, "--output", cms_binary],
stdout=log, stderr=log, check=True)
record_binary(manifest, binary_name, cms_binary, started,
profile="sqlite", fixture="cms", source=source)
if args.selection != "protocols":
build_profile_report(args, manifest, log)
for name, peer in peers.items():
shutil.copy2(peer, destination / name)
manifest["binaries"][name] = fingerprint(destination / name)
if has_protocols:
shutil.copytree(TLS, destination / "tls")
for path in sorted((destination / "tls").rglob("*")):
if path.is_file():
name = path.relative_to(destination).as_posix()
manifest["files"][name] = fingerprint(path)
(destination / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
print(f"artifacts: {destination}", flush=True)
+675
View File
@@ -0,0 +1,675 @@
"""显式验收双源码 CMS 的真实身份、租户、发布链及专属内存预算。"""
import argparse
from collections import Counter
from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy
import http.client
from http.cookies import SimpleCookie
import json
import math
import os
from pathlib import Path
import secrets
import shutil
import signal
import socket
import subprocess
import tempfile
from threading import Event
import time
from urllib.parse import urlencode
from build import ROOT, fingerprint
from process import Budget, Process, observe, summarize, resource_trend
from settings import write_private_json
FORMATS = ("dever", "md")
ARTICLE_PATH = "/news/article/admin/manage"
ARTICLE_LIST_PATH = f"{ARTICLE_PATH}/list?size=100"
REVISION_PATH = "/content/revision/admin/manage/create"
SESSION_PATH = "/user/account/{site}/session/login"
PUBLICATIONS_PATH = "/news/article/front/browse/list?size=100"
def require(condition, message):
if not condition:
raise RuntimeError(message)
def execute(command, directory, log_name, *, envelope=False, timeout=180):
child = subprocess.Popen([str(value) for value in command], cwd=directory,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, start_new_session=True)
failure = None
try:
stdout, stderr = child.communicate(timeout=timeout)
except BaseException as error:
failure = error
try:
signal_group(child.pid, signal.SIGTERM)
stdout, stderr = child.communicate(timeout=3)
except subprocess.TimeoutExpired:
signal_group(child.pid, signal.SIGKILL)
stdout, stderr = child.communicate(timeout=3)
(directory / f"{log_name}.stdout.log").write_bytes(stdout)
(directory / f"{log_name}.stderr.log").write_bytes(stderr)
if failure is not None:
raise failure
require(child.returncode == 0, f"{log_name} exited {child.returncode}: {directory}")
if envelope:
return decode_response(stdout, 200)
def signal_group(pid, signal_number):
try:
os.killpg(pid, signal_number)
except ProcessLookupError:
pass
def decode_response(body, status):
document = json.loads(body)
require(isinstance(document, dict) and set(document) == {"code", "message", "data"},
f"unexpected response envelope (HTTP {status})")
require(status == 200 and document["code"] == 0,
f"request failed: HTTP {status}, code={document['code']}, {document['message']}")
return document["data"]
def copy_cms_project(source, destination):
destination.mkdir()
for name in ("config", "module", "test"):
shutil.copytree(source / name, destination / name)
def build_cases(compiler, output):
output.mkdir(parents=True, exist_ok=False)
cases = {}
for source_format in FORMATS:
source = ROOT / "examples/cms" / source_format
project = output / source_format
copy_cms_project(source, project)
started = time.monotonic()
execute([compiler, "check", project], project, "check")
execute([compiler, "build", project, "--output", project / "cms-app"], project, "build", timeout=900)
executable = project / "cms-app"
cases[source_format] = {
"executable": str(executable), "config": str(project / "config/setting.json"),
"fingerprint": fingerprint(executable),
"build_seconds": time.monotonic() - started,
}
manifest = {"format": "dever-cms-publish-v1", "cases": cases}
write_private_json(output / "manifest.json", manifest)
return manifest
def available_port():
with socket.socket() as reservation:
reservation.bind(("127.0.0.1", 0))
return reservation.getsockname()[1]
def isolated_settings(settings, port):
settings = deepcopy(settings)
settings["runtime"]["mode"] = "api"
settings["http"] = {"host": "127.0.0.1", "port": port}
settings["auth"]["providers"]["session"]["jwtSecret"] = secrets.token_hex(32)
for site in settings["sites"].values():
site["origin"] = f"http://127.0.0.1:{port}"
database = settings["database"]["default"]
if database["type"] == "sqlite":
database["path"] = "data/db/platform.db"
database["tenant_directory"] = "data/tenants"
elif database["type"] == "postgres":
require(bool(database.get("url")) and bool(database.get("tenant_database_prefix")),
"PostgreSQL CMS acceptance requires an explicit isolated control database and tenant prefix")
else:
raise RuntimeError("CMS acceptance supports only SQLite and PostgreSQL")
settings["log"]["level"] = "error"
return settings
class CmsClient:
def __init__(self, port):
self.connection = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
self.origin = f"http://127.0.0.1:{port}"
self.cookies = {}
self.timings = {}
self.record_timings = True
def request(self, method, path, payload=None, *, site=None, expected=200):
headers = {"Content-Type": "application/json", "Origin": self.origin}
if site in self.cookies:
headers["Cookie"] = self.cookies[site]
body = None
if payload is not None:
if method in ("GET", "DELETE"):
query = {name: value if isinstance(value, str) else json.dumps(value)
for name, value in payload.items()}
path += ("&" if "?" in path else "?") + urlencode(query)
else:
body = json.dumps(payload)
started = time.monotonic()
self.connection.request(method, path, body, headers)
response = self.connection.getresponse()
body = response.read()
route = path.split("?")[0]
label = f"{method} {route}" + (f" expected {expected}" if expected != 200 else "")
if self.record_timings:
self.timings.setdefault(label, []).append(
(time.monotonic() - started) * 1000)
require(response.status == expected,
f"{method} {path}: HTTP {response.status}, expected {expected}: {body!r}")
if expected != 200:
document = json.loads(body)
require(document.get("code") != 0, "rejected request returned success")
return None
data = decode_response(body, response.status)
if path.endswith("/login"):
cookie = SimpleCookie()
for name, value in response.getheaders():
if name.lower() == "set-cookie":
cookie.load(value)
require(bool(cookie), "login did not return a session cookie")
self.cookies[site] = "; ".join(f"{name}={value.value}" for name, value in cookie.items())
return data
def close(self):
self.connection.close()
def wait_ready(process, client):
deadline = time.monotonic() + 20
while time.monotonic() < deadline:
process.sample()
require(process.child.poll() is None, f"CMS exited before readiness: {process.directory}")
try:
client.request("GET", ARTICLE_LIST_PATH, expected=401)
return time.monotonic() - process.started
except (ConnectionError, http.client.HTTPException, OSError):
client.close()
time.sleep(0.02)
raise TimeoutError(f"CMS readiness timeout: {process.directory}")
def login_client(client, identity, password, tenant_key):
for site in ("admin", "front"):
data = client.request("POST", SESSION_PATH.format(site=site), {
"credential_id": identity["credential_id"], "password": password, "tenant": tenant_key,
}, site=site)
require(data["account"]["id"] == identity["user_id"]
and data["tenant_id"] == identity["tenant_id"] and data["site"] == site,
"login changed the trusted identity or tenant/site")
def publish_workload(client, process, identity, password, articles, *,
tenant_key="bench", title_prefix="Article"):
login_client(client, identity, password, tenant_key)
titles = []
ids = []
for index in range(articles):
slug, title = f"bench-{index}", f"{title_prefix} {index}"
created = client.request("POST", f"{ARTICLE_PATH}/create", {
"slug": f" {slug.upper()} ", "title": f" {title} ", "body": "Draft body",
}, site="admin")
require(created["slug"] == slug and created["title"] == title,
"article creation did not normalize input")
require("author_id" not in created and "status" not in created,
"article creation leaked private owner or publication state")
changed = client.request("POST", REVISION_PATH, {
"selected_article_id": created["id"], "expected_version": created["version"],
"title": f" {title} published ", "body": "Published body",
}, site="admin")
expected_title = f"{title} published"
require(changed["title"] == expected_title
and changed["article_id"] == created["id"]
and changed["version"] == created["version"] + 1,
"revision did not preserve normalized content and advance the article version")
published = client.request("POST", f"{ARTICLE_PATH}/publish", {
"article_id": created["id"],
}, site="admin")
require(published["slug"] == slug and published["title"] == expected_title
and published["body"] == "Published body",
"publication did not reflect the stored article")
titles.append(expected_title)
ids.append(created["id"])
process.sample()
listing = client.request("GET", PUBLICATIONS_PATH, site="front")
require(listing["total"] == articles
and sorted(value["title"] for value in listing["items"]) == sorted(titles),
"front query did not return every published article exactly once")
client.request("POST", f"{ARTICLE_PATH}/publish", {"article_id": ids[0]},
site="admin", expected=409)
return {"articles": articles, "titles": sorted(titles), "duplicate_publish_status": 409}
def provision_identity(executable, directory, tenant_key):
password = secrets.token_urlsafe(24)
identity = execute([executable, "user.account.bootstrap", json.dumps({
"tenant_key": tenant_key, "tenant_name": f"Benchmark {tenant_key}",
"email": f"{tenant_key}@example.invalid", "display_name": "Benchmark",
"password": password,
})], directory, f"bootstrap-{tenant_key}", envelope=True)
execute([executable, "--dever-tenant-migrate", identity["tenant_id"]],
directory, f"migrate-{tenant_key}")
for site in ("admin", "front"):
execute([executable, "--dever-tenant-owner", identity["tenant_id"], site,
identity["user_id"]], directory, f"owner-{tenant_key}-{site}")
return identity, password
def latency_summary(timings):
result = {}
for action, samples in timings.items():
values = sorted(samples)
result[action] = {"samples": len(values), "p50_ms": values[len(values) // 2],
"max_ms": values[-1]}
return result
def catalog_key(catalog, component, domain, site, action, method):
matches = [entry["key"] for entry in catalog
if (entry["component"], entry["domain"], entry["site"],
entry["action"], entry["method"]) == (component, domain, site, action, method)]
require(len(matches) == 1 and bool(matches[0]),
f"permission catalog must uniquely identify {component}.{domain}.{site}.{action}")
return matches[0]
def run_postgres_rbac(executable, directory, port, owner, identity):
catalog = owner.request("GET", "/user/authorization/admin/manage/permissions", site="admin")
front_catalog = owner.request("GET", "/user/authorization/front/manage/permissions", site="front")
permissions = {
"admin_read": catalog_key(catalog, "news", "article", "admin", "list", "GET"),
"admin_permissions": catalog_key(catalog, "user", "authorization", "admin", "permissions", "GET"),
"front_list": catalog_key(front_catalog, "news", "article", "front", "list", "GET"),
}
require(len(set(permissions.values())) == 3, "RBAC acceptance reused a permission key")
password = secrets.token_urlsafe(24)
member = execute([executable, "user.account.fixture_member", json.dumps({
"selected_tenant_id": identity["tenant_id"], "email": f"member-{secrets.token_hex(8)}@example.invalid",
"display_name": "Acceptance member", "password": password,
})], directory, "rbac-member", envelope=True)
require(isinstance(member["user_id"], int) and not isinstance(member["user_id"], bool)
and member["user_id"] > 0 and member["user_id"] != identity["user_id"]
and member["tenant_id"] == identity["tenant_id"],
"RBAC acceptance did not create a distinct member in the owner's tenant")
probes = (("admin", ARTICLE_LIST_PATH),
("admin", "/user/authorization/admin/manage/permissions"),
("front", PUBLICATIONS_PATH))
client = CmsClient(port)
try:
login_client(client, member, password, "tenant_a")
for site, path in probes:
client.request("GET", path, site=site, expected=403)
roles = (("admin", "fixture-reader", "admin_read"),
("admin", "fixture-catalog", "admin_permissions"),
("front", "fixture-reader", "front_list"))
for site, role_id, permission in roles:
base = f"/user/authorization/{site}/manage"
owner.request("POST", f"{base}/save", {
"id": role_id, "name": role_id, "all_permissions": False,
"permission_keys": [permissions[permission]],
}, site=site)
owner.request("POST", f"{base}/grant", {
"user_id": member["user_id"], "role_id": role_id,
}, site=site)
for site, path in probes:
client.request("GET", path, site=site)
owner.request("DELETE", "/user/authorization/admin/manage/revoke", {
"user_id": member["user_id"], "role_id": "fixture-reader",
}, site="admin")
client.request("GET", ARTICLE_LIST_PATH, site="admin", expected=403)
client.request("GET", probes[1][1], site="admin")
client.request("GET", PUBLICATIONS_PATH, site="front")
for site, role_id, path in (("admin", "fixture-catalog", probes[1][1]),
("front", "fixture-reader", PUBLICATIONS_PATH)):
owner.request("DELETE", f"/user/authorization/{site}/manage/revoke", {
"user_id": member["user_id"], "role_id": role_id,
}, site=site)
client.request("GET", path, site=site, expected=403)
finally:
client.close()
return {"member_user_id": member["user_id"], "permissions": permissions,
"before_grant_denied": True, "both_admin_roles_allowed": True,
"revoked_role_denied": True, "second_role_still_allowed": True,
"front_same_named_role_still_allowed": True}
class ReadLatency:
"""固定内存的对数直方图;分位数报告桶上界,最大相邻桶比例约1.011。"""
def __init__(self):
self.buckets = [0] * 2048
self.count = 0
def add(self, milliseconds):
index = math.ceil(math.log2(1 + milliseconds) * 64)
require(0 <= index < len(self.buckets), "CMS latency exceeds histogram bounds")
self.buckets[index] += 1
self.count += 1
def merge(self, other):
self.buckets = [left + right for left, right in zip(self.buckets, other.buckets)]
self.count += other.count
def percentile(self, fraction):
threshold = math.ceil(self.count * fraction)
count = 0
for index, samples in enumerate(self.buckets):
count += samples
if count >= threshold:
return 2 ** (index / 64) - 1
raise ValueError("empty latency histogram")
def concurrent_reads(port, cookies, expected, process, *, concurrency, seconds, interval):
"""Each closed-loop client keeps its own connection and verifies every reply."""
stop = Event()
started = time.monotonic()
deadline = started + seconds
first = len(process.samples)
def read_publications():
client = CmsClient(port)
client.cookies = cookies.copy()
client.record_timings = False
latency = ReadLatency()
try:
while not stop.is_set() and time.monotonic() < deadline:
request_started = time.monotonic()
listing = client.request("GET", PUBLICATIONS_PATH, site="front")
require(listing == expected, "concurrent CMS read changed publication results")
latency.add((time.monotonic() - request_started) * 1000)
return latency
except BaseException:
stop.set()
raise
finally:
client.close()
with ThreadPoolExecutor(max_workers=concurrency) as clients:
futures = [clients.submit(read_publications) for _ in range(concurrency)]
try:
while any(not future.done() for future in futures):
process.check()
observe([process], interval, interval)
elapsed = time.monotonic() - started
latency = ReadLatency()
for future in futures:
latency.merge(future.result())
finally:
stop.set()
require(latency.count > 0, "concurrent CMS read completed without a successful request")
require(elapsed >= seconds, "concurrent CMS read ended before the measurement window")
process.sample()
return {"mode": "closed_loop_authenticated_read", "concurrency": concurrency,
"requested_seconds": seconds, "elapsed_seconds": elapsed,
"succeeded": latency.count, "errors": 0, "result_checked_on_every_request": True,
"actual_success_qps": latency.count / elapsed,
"latency_measurement": "request and reply validation; logarithmic bucket upper bound, 64 buckets per octave",
"request_latency_ms": {name: latency.percentile(fraction)
for name, fraction in (("p50", .5), ("p95", .95), ("p99", .99))},
"resources": summarize(process.samples[first:]),
"trend": resource_trend(process.samples[first:]) if len(process.samples[first:]) >= 4 else None}
def sqlite_pool_descriptor_limits(settings, directory):
database = settings["database"]["default"]
require(database["type"] == "sqlite", "CMS read benchmark requires isolated SQLite")
databases = [directory / database["path"]]
databases.extend((directory / database["tenant_directory"]).glob("*.db"))
# 每个连接可持有主文件与WAL;同进程各连接共用一个SHM句柄。
return {str(path.resolve()) + suffix: limit for path in databases
for suffix, limit in (("", database["max_connections"]),
("-wal", database["max_connections"]), ("-shm", 1))}
def validate_read_descriptor_recovery(load):
before = Counter(load["descriptors_before"].values())
after = Counter(load["descriptors_after"].values())
for path, limit in load["pooled_descriptor_limits"].items():
require(after[path] <= limit, f"CMS SQLite descriptor count exceeds pool limit: {path}")
before.pop(path, None)
after.pop(path, None)
require(before == after,
f"CMS read descriptors did not recover: added={dict(after - before)}, missing={dict(before - after)}")
def run_case(case, directory, *, articles, cgroup_parent, memory_mib,
idle_seconds=0.2, interval=0.05, server_cpu=None, cpu_quota=None,
concurrency=0, read_seconds=5):
executable = Path(case["executable"])
require(fingerprint(executable) == case["fingerprint"], "CMS artifact fingerprint changed")
directory.mkdir(parents=True, exist_ok=False)
target = directory / "cms-app"
shutil.copy2(executable, target)
(directory / "config").mkdir()
(directory / "data").mkdir()
port = available_port()
settings = isolated_settings(json.loads(Path(case["config"]).read_text()), port)
write_private_json(directory / "config/setting.json", settings)
identity, password = provision_identity(target, directory, "bench")
budget = Budget(cgroup_parent, memory_mib, cpu_quota)
client = CmsClient(port)
failure = None
result = {}
try:
with Process([target], directory / "server", budget=budget,
cpus=(server_cpu,) if server_cpu is not None else None) as process:
result["raw"] = process.samples
ready_seconds = wait_ready(process, client)
started = time.monotonic()
contract = publish_workload(client, process, identity, password, articles)
elapsed = time.monotonic() - started
result.update(contract=contract, ready_seconds=ready_seconds,
workload_seconds=elapsed, published_articles_per_second=articles / elapsed,
latency=latency_summary(client.timings))
if concurrency:
expected = client.request("GET", PUBLICATIONS_PATH, site="front")
# 控制连接不参与长读负载,先关闭,避免其空闲超时改变基线。
client.close()
observe([process], 0.3, interval)
descriptors_before = process.descriptors()
pooled_limits = sqlite_pool_descriptor_limits(settings, directory)
read_load = {"descriptors_before": descriptors_before,
"pooled_descriptor_limits": pooled_limits}
result["concurrent_reads"] = read_load
read_load.update(concurrent_reads(port, client.cookies, expected, process,
concurrency=concurrency, seconds=read_seconds, interval=interval))
observe([process], 0.3, interval)
descriptors_after = process.descriptors()
read_load.update(descriptors_after=descriptors_after,
fd_delta_after_disconnect=len(descriptors_after) - len(descriptors_before))
validate_read_descriptor_recovery(read_load)
observe([process], idle_seconds, interval)
process.sample()
process.check()
require(process.child.poll() is None, "CMS exited during the publish workload")
resources = summarize(process.samples)
result.update(resources=resources, budget_before_stop=budget.sample())
except BaseException as error:
failure = error
finally:
client.close()
try:
budget.close()
except BaseException as error:
if failure is None:
failure = error
else:
failure.add_note(f"cannot close owned CMS budget: {error}")
result["cgroup"] = budget.final
if failure is None and budget.final is not None and (
budget.final["events"]["oom"] or budget.final["events"]["oom_kill"]):
failure = RuntimeError("CMS exceeded its memory budget")
result["status"] = "failed" if failure else "passed"
if failure is not None:
result["error"] = str(failure)
write_private_json(directory / "report.json", result)
if failure is not None:
raise failure
return result
def run_postgres_case(project, output, compiler, *, articles, rbac_fixture=False):
# 构建、临时密钥和端口只写测试副本,调用者的项目配置始终保持不变。
with tempfile.TemporaryDirectory(prefix="dever-cms-postgres-") as temporary:
staged = Path(temporary) / "project"
copy_cms_project(project, staged)
return run_staged_postgres_case(staged, output, compiler, articles=articles,
rbac_fixture=rbac_fixture)
def run_staged_postgres_case(project, output, compiler, *, articles, rbac_fixture):
require(1 <= articles <= 100, "PostgreSQL CMS acceptance requires 1..100 articles")
output.mkdir(parents=True, exist_ok=False)
report = {"status": "running", "source": "dever", "tenants": {},
"scope": "PostgreSQL HTTP owner flow and physical database tenant isolation; not non-owner RBAC revocation"}
if rbac_fixture:
report["scope"] = "PostgreSQL HTTP owner flow, physical database tenant isolation and non-owner multi-role/site grant-revoke"
clients = {}
try:
setting_path = project / "config/setting.json"
initial = json.loads(setting_path.read_text())
require(initial["database"]["default"]["type"] == "postgres",
"postgres-case requires a PostgreSQL project configuration")
port = available_port()
write_private_json(setting_path, isolated_settings(initial, port), replace=True)
execute([compiler, "check", project], output, "check")
executable = project / "cms-app"
execute([compiler, "build", project, "--output", executable], output, "build", timeout=900)
report["binary"] = fingerprint(executable)
identities = {key: provision_identity(executable, output, key)
for key in ("tenant_a", "tenant_b")}
require(identities["tenant_a"][0]["tenant_id"] != identities["tenant_b"][0]["tenant_id"],
"PostgreSQL acceptance reused a tenant identity")
clients = {key: CmsClient(port) for key in identities}
with Process([executable], output / "server") as process:
report["ready_seconds"] = wait_ready(process, clients["tenant_a"])
for key, (identity, password) in identities.items():
contract = publish_workload(clients[key], process, identity, password, articles,
tenant_key=key, title_prefix=key)
permissions = clients[key].request(
"GET", "/user/authorization/admin/manage/permissions", site="admin")
require(bool(permissions), "CMS permission catalog is empty")
report["tenants"][key] = {"tenant_id": identity["tenant_id"],
"user_id": identity["user_id"], "contract": contract}
for key, client in clients.items():
listing = client.request("GET", PUBLICATIONS_PATH, site="front")
contract = report["tenants"][key]["contract"]
require(listing["total"] == articles
and sorted(value["title"] for value in listing["items"]) == contract["titles"],
"PostgreSQL tenant query leaked another tenant's articles")
require(sorted(value["slug"] for value in listing["items"])
== sorted(f"bench-{index}" for index in range(articles)),
"PostgreSQL tenants did not preserve the same slug namespace")
report["tenants"][key]["slugs"] = sorted(value["slug"] for value in listing["items"])
identity, password = identities["tenant_a"]
clients["tenant_a"].request("POST", SESSION_PATH.format(site="admin"), {
"credential_id": identity["credential_id"], "password": password, "tenant": "tenant_b",
}, site="admin", expected=401)
front_cookie = clients["tenant_a"].cookies["front"]
clients["tenant_a"].cookies["front"] = clients["tenant_a"].cookies["admin"]
clients["tenant_a"].request("GET", PUBLICATIONS_PATH, site="front", expected=401)
clients["tenant_a"].cookies["front"] = front_cookie
if rbac_fixture:
report["rbac"] = run_postgres_rbac(executable, output, port,
clients["tenant_a"], identity)
process.sample()
process.check()
require(process.child.poll() is None, "PostgreSQL CMS exited during tenant acceptance")
report["resources"] = summarize(process.samples)
report["cross_site_cookie_rejected"] = True
report["cross_tenant_login_rejected"] = True
report["status"] = "passed"
except BaseException as error:
report["status"] = "failed"
report["error"] = str(error)
raise
finally:
for client in clients.values():
client.close()
write_private_json(output / "report.json", report)
return report
def run_cases(manifest, output, *, articles, cgroup_parent, memory_mib,
concurrency=0, read_seconds=5):
require(manifest["format"] == "dever-cms-publish-v1", "unexpected CMS manifest format")
require(1 <= articles <= 100, "articles must be 1..100")
require(0 <= concurrency <= 64, "read concurrency must be 0..64")
require(math.isfinite(read_seconds) and 0.1 <= read_seconds <= 3600,
"read seconds must be finite and within 0.1..3600")
require((memory_mib is None) == (cgroup_parent is None),
"memory budget and cgroup parent must be provided together")
output.mkdir(parents=True, exist_ok=False)
report = {"format": manifest["format"], "memory_mib": memory_mib,
"scope": "owned loopback HTTP/1, sequential publish, isolated SQLite; not a saturation result",
"cases": {}}
if concurrency:
report["scope"] = "owned loopback HTTP/1, sequential publish and closed-loop authenticated reads, isolated SQLite"
try:
for source_format in FORMATS:
report["cases"][source_format] = run_case(manifest["cases"][source_format],
output / source_format, articles=articles,
cgroup_parent=cgroup_parent, memory_mib=memory_mib,
concurrency=concurrency, read_seconds=read_seconds)
require(report["cases"]["dever"]["contract"] == report["cases"]["md"]["contract"],
"Dever and Markdown CMS changed the publish contract")
report["status"] = "passed"
except BaseException as error:
report["status"] = "failed"
report["error"] = str(error)
raise
finally:
for source_format in FORMATS:
case_report = output / source_format / "report.json"
if source_format not in report["cases"] and case_report.is_file():
report["cases"][source_format] = json.loads(case_report.read_text())
write_private_json(output / "report.json", report)
return report
def main():
parser = argparse.ArgumentParser(description=__doc__)
actions = parser.add_subparsers(dest="action", required=True)
build = actions.add_parser("build")
build.add_argument("--compiler", type=Path, default=ROOT / "target/debug/dever")
build.add_argument("--output", type=Path, required=True)
run = actions.add_parser("run")
run.add_argument("--manifest", type=Path, required=True)
run.add_argument("--output", type=Path, required=True)
run.add_argument("--articles", type=int, default=16)
run.add_argument("--cgroup-parent", type=Path)
run.add_argument("--memory-mib", type=int, choices=(64, 128))
run.add_argument("--concurrency", type=int, default=0,
help="0 disables load; otherwise 1..64 independent authenticated read clients")
run.add_argument("--read-seconds", type=float, default=5)
postgres = actions.add_parser("postgres-case")
postgres.add_argument("--project", type=Path, required=True)
postgres.add_argument("--output", type=Path, required=True)
postgres.add_argument("--compiler", type=Path, default=ROOT / "target/debug/dever")
postgres.add_argument("--articles", type=int, default=2)
postgres.add_argument("--rbac-fixture", action="store_true",
help="run test-only member CMD provided by the Rust PostgreSQL fixture")
args = parser.parse_args()
if args.action == "build":
result = build_cases(args.compiler.resolve(strict=True), args.output.resolve())
elif args.action == "postgres-case":
report = run_postgres_case(args.project.resolve(strict=True), args.output.resolve(),
args.compiler.resolve(strict=True), articles=args.articles,
rbac_fixture=args.rbac_fixture)
result = {"status": report["status"], "report": str(args.output.resolve() / "report.json")}
else:
result = run_cases(json.loads(args.manifest.read_text()), args.output.resolve(),
articles=args.articles, cgroup_parent=args.cgroup_parent,
memory_mib=args.memory_mib, concurrency=args.concurrency,
read_seconds=args.read_seconds)
result = {"status": result["status"], "memory_mib": result["memory_mib"],
"report": str(args.output / "report.json")}
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
+21
View File
@@ -0,0 +1,21 @@
ready(port: Int) () {
dever.io.println("READY|" + int.to_text(port))
}
pause(millis: Int) () {
dever.task.sleep(millis)
}
report(
begin: Int,
finish: Int,
name: Text,
count: Int,
checksum: Int
) () recover("基准失败输出由运行器检查") {
dever.io.println(
"SAMPLE|" + name + "|" + int.to_text(count) + "|" + int.to_text(finish - begin) + "|" + int.to_text(
checksum
)
)
}
@@ -0,0 +1,136 @@
type Content {
body: Bytes
kind: Text
}
listening(result: dever.net.ListenResult.Listening(listener)) (
value: dever.system.Listener
) {
value = listener
}
listening(result: dever.net.ListenResult.Failed(message)) (value: dever.system.Listener) {
fail(app.WorkerFailure.Failed(message))
}
bound(result: dever.net.PortResult.Bound(port)) (value: Int) {
value = port
}
bound(result: dever.net.PortResult.Failed(message)) (value: Int) {
fail(app.WorkerFailure.Failed(message))
}
paused(result: dever.time.SleepResult.Done) () {}
paused(result: dever.time.SleepResult.Failed(message)) () {
fail(app.WorkerFailure.Failed(message))
}
closed(result: dever.io.CloseResult.Closed) () {}
closed(result: dever.io.CloseResult.Failed(message)) () {
fail(app.WorkerFailure.Failed(message))
}
server_tls(result: dever.tls.ServerResult.Ready(tls)) (value: dever.system.ServerTls) {
value = tls
}
server_tls(result: dever.tls.ServerResult.Failed(message)) (
value: dever.system.ServerTls
) {
fail(app.WorkerFailure.Failed(message))
}
response_value(content: Content) (result: dever.http.Response) {
result = dever.http.Response {
status = 200
headers = [dever.http.header("Content-Type", content.kind)]
body = content.body
}
}
route(request: dever.http.Request, large: Bytes) (response: dever.http.Response) {
response = response_value(select(request.target, large))
}
select(path: "/plain", large: Bytes) (result: Content) {
result = Content {
body = dever.bytes.from_text("hello")
kind = "text/plain"
}
}
select(path: "/json", large: Bytes) (result: Content) {
result = Content {
body = dever.bytes.from_text("{\"ok\":true}")
kind = "application/json"
}
}
select(path: "/bytes", large: Bytes) (result: Content) {
result = Content {
body = large
kind = "application/octet-stream"
}
}
select(path: other, large: Bytes) (result: Content) {
result = Content {
body = dever.bytes.from_text("unknown benchmark path")
kind = "text/plain"
}
}
serve(
listener: dever.system.Listener,
tls: null,
limits: dever.http.Limits,
large: Bytes
) () {
dever.system.http_serve(route, listener, limits, large)
}
serve(
listener: dever.system.Listener,
tls: dever.system.ServerTls,
limits: dever.http.Limits,
large: Bytes
) () {
dever.system.http_serve_tls(route, listener, limits, tls, large)
}
worker(
tls: dever.system.ServerTls?,
limits: dever.http.Limits,
large: Bytes,
lifetime: Int
) () {
listener = listening(result(dever.net.listen("127.0.0.1", 0)))
port = bound(result(dever.net.port(listener)))
server = run(serve(listener, tls, limits, large))
blocking(dever.io.println("READY|" + int.to_text(port)))
paused(result(dever.task.sleep(lifetime)))
closed(result(dever.net.close_listener(listener)))
stop(server)
}
port.plain(limits: dever.http.Limits, large: Bytes, lifetime: Int) () {
worker(null, limits, large, lifetime)
}
port.secure(
limits: dever.http.Limits,
large: Bytes,
lifetime: Int,
certificate: Bytes,
key: Bytes
) () {
worker(
server_tls(result(dever.tls.server(certificate, key))),
limits,
large,
lifetime
)
}
+7
View File
@@ -0,0 +1,7 @@
cmd http = app.http
cmd https = app.https
cmd http2 = app.http2
cmd https2 = app.https2
+35
View File
@@ -0,0 +1,35 @@
type WorkerFailure {
error Failed(message: Text)
}
http() (result: Bool) {
port.plain(config.limits(), config.large_body(), config.lifetime())
result = true
}
https() (result: Bool) {
port.secure(
config.limits(),
config.large_body(),
config.lifetime(),
config.certificate(),
config.key()
)
result = true
}
http2() (result: Bool) {
port.plain(config.http2_limits(), config.large_body(), config.lifetime())
result = true
}
https2() (result: Bool) {
port.secure(
config.http2_limits(),
config.large_body(),
config.lifetime(),
config.certificate(),
config.key()
)
result = true
}
@@ -0,0 +1,9 @@
plain(limits: dever.http.Limits, large: Bytes, lifetime: Int) () fails app.WorkerFailure
secure(
limits: dever.http.Limits,
large: Bytes,
lifetime: Int,
certificate: Bytes,
key: Bytes
) () fails app.WorkerFailure
@@ -0,0 +1,213 @@
setting {
limits: dever.http.Limits
lifetime_ms: Int
}
type Service {
Tcp
Live
}
listening(result: dever.net.ListenResult.Listening(listener)) (
value: dever.system.Listener
) {
value = listener
}
listening(result: dever.net.ListenResult.Failed(message)) (value: dever.system.Listener) {
fail(app.WorkerFailure.Failed(message))
}
bound(result: dever.net.PortResult.Bound(port)) (value: Int) {
value = port
}
bound(result: dever.net.PortResult.Failed(message)) (value: Int) {
fail(app.WorkerFailure.Failed(message))
}
written(result: dever.io.WriteResult.Done) () {}
written(result: dever.io.WriteResult.Failed(message)) () {
fail(app.WorkerFailure.Failed(message))
}
closed(result: dever.io.CloseResult.Closed) () {}
closed(result: dever.io.CloseResult.Failed(message)) () {
fail(app.WorkerFailure.Failed(message))
}
paused(result: dever.time.SleepResult.Done) () {}
paused(result: dever.time.SleepResult.Failed(message)) () {
fail(app.WorkerFailure.Failed(message))
}
tcp_chunk(event: dever.io.ReadEvent.Chunk(bytes), socket: dever.system.Socket) () recover("TCP 连接错误由运行器检查") {
written(result(dever.net.write(socket, bytes)))
}
tcp_chunk(event: dever.io.ReadEvent.Failed(message), socket: dever.system.Socket) () recover("TCP 连接错误由运行器检查") {
blocking(dever.io.println("ERROR|" + message))
}
tcp_stream(
result: dever.net.ReadStreamResult.Streaming(stream),
socket: dever.system.Socket
) () {
each(tcp_chunk, stream, socket)
}
tcp_stream(
result: dever.net.ReadStreamResult.Failed(message),
socket: dever.system.Socket
) () {
fail(app.WorkerFailure.Failed(message))
}
tcp_connection(
event: dever.net.ConnectResult.Connected(socket),
limits: dever.http.Limits
) () recover("TCP 连接错误由运行器检查") {
written(result(dever.net.timeout(socket, limits.timeout_ms)))
tcp_stream(result(dever.net.chunks(socket, 4096)), socket)
closed(result(dever.net.close(socket)))
}
tcp_connection(
event: dever.net.ConnectResult.Failed(message),
limits: dever.http.Limits
) () recover("TCP 连接错误由运行器检查") {
blocking(dever.io.println("ERROR|" + message))
}
tcp_server(listener: dever.system.Listener, limits: dever.http.Limits) () {
stream = dever.net.connections(listener)
parallel_each(tcp_connection, stream, limits.connections, limits)
}
websocket_message(
event: dever.websocket.MessageEvent.Read(message),
socket: dever.system.WebSocket
) () recover("WebSocket 连接错误由运行器检查") {
websocket_echo(message, socket)
}
websocket_message(
event: dever.websocket.MessageEvent.Failed(message),
socket: dever.system.WebSocket
) () recover("WebSocket 连接错误由运行器检查") {
blocking(dever.io.println("ERROR|" + message))
}
websocket_echo(
message: dever.websocket.Message.Text(value),
socket: dever.system.WebSocket
) () recover("WebSocket 连接错误由运行器检查") {
written(result(dever.websocket.send(socket, dever.websocket.Message.Text(value))))
}
websocket_echo(
message: dever.websocket.Message.Binary(value),
socket: dever.system.WebSocket
) () recover("WebSocket 连接错误由运行器检查") {
written(result(dever.websocket.send(socket, dever.websocket.Message.Binary(value))))
}
websocket_echo(message: other, socket: dever.system.WebSocket) () recover("WebSocket 连接错误由运行器检查") {}
websocket_limits() (result: dever.websocket.Limits) {
result = dever.websocket.Limits {
message_bytes = 4096
idle_ms = setting.lifetime_ms
write_ms = setting.limits.timeout_ms
}
}
websocket_connection(result: dever.websocket.ConnectResult.Connected(socket)) () {
each(websocket_message, dever.websocket.messages(socket), socket)
}
websocket_connection(result: dever.websocket.ConnectResult.Failed(message)) () {
fail(app.WorkerFailure.Failed(message))
}
route(request: dever.http.Request, reply: dever.system.HttpReply) () {
live_path(request.target, reply)
}
live_path(target: "/ws", reply: dever.system.HttpReply) () recover("实时连接错误由运行器检查") {
websocket_connection(result(dever.websocket.accept(reply, websocket_limits())))
}
live_path(target: "/events", reply: dever.system.HttpReply) () recover("实时连接错误由运行器检查") {
written(result(dever.sse.start(reply, [])))
written(
result(
dever.sse.send(
reply,
dever.sse.Event {
event = "live"
data = "ready"
id = "1"
retry_ms = null
}
)
)
)
paused(result(dever.task.sleep(setting.lifetime_ms)))
}
live_path(target: other, reply: dever.system.HttpReply) () recover("实时连接错误由运行器检查") {
written(
result(
dever.http.respond(
reply,
dever.http.Response {
status = 404
headers = []
body = dever.bytes.from_text("unknown live benchmark path")
}
)
)
)
}
live_limits() (result: dever.http.LiveLimits) {
result = dever.http.LiveLimits {
chunk_bytes = 4096
idle_ms = setting.lifetime_ms
heartbeat_ms = 100
}
}
live_server(listener: dever.system.Listener) () {
dever.system.http_serve_live(route, listener, setting.limits, live_limits())
}
serve(service: Service.Tcp, listener: dever.system.Listener) () {
tcp_server(listener, setting.limits)
}
serve(service: Service.Live, listener: dever.system.Listener) () {
live_server(listener)
}
worker(service: Service) () {
listener = listening(result(dever.net.listen("127.0.0.1", 0)))
port = bound(result(dever.net.port(listener)))
server = run(serve(service, listener))
blocking(dever.io.println("READY|" + int.to_text(port)))
paused(result(dever.task.sleep(setting.lifetime_ms)))
stop(server)
closed(result(dever.net.close_listener(listener)))
}
port.tcp() () {
worker(Service.Tcp)
}
port.live() () {
worker(Service.Live)
}
+5
View File
@@ -0,0 +1,5 @@
cmd tcp = app.tcp
cmd ws = app.ws
cmd sse = app.sse
+18
View File
@@ -0,0 +1,18 @@
type WorkerFailure {
error Failed(message: Text)
}
tcp() (result: Bool) {
port.tcp()
result = true
}
ws() (result: Bool) {
port.live()
result = true
}
sse() (result: Bool) {
port.live()
result = true
}
@@ -0,0 +1,3 @@
tcp() () fails app.WorkerFailure
live() () fails app.WorkerFailure
+144
View File
@@ -0,0 +1,144 @@
type Identity {
id: Text
user_id: model.id?
tenant_id: model.id?
}
verify(claims: dever.auth.Claims) (identity: Identity) {
identity = Identity {
id = claims.subject
user_id = null
tenant_id = null
}
}
ready() () {
blocking(bench.ready(0))
}
populate_one(value: Int) () {
created = model.create({
value = value
label = "fixture"
})
}
populate() () {
each(populate_one, config.orm_rows())
}
crud_once(value: Int) () {
begin = dever.time.monotonic_nanos()
created = model.create({
value = value
label = "created"
})
changed = model.update(created.id, { label = "updated" })
removed = model.delete(created.id)
end = dever.time.monotonic_nanos()
blocking(bench.report(begin, end, "crud", 1, changed + removed))
}
list_once(value: Int) () {
begin = dever.time.monotonic_nanos()
page = model.list({
order = id.asc
page = 1
size = 32
})
end = dever.time.monotonic_nanos()
blocking(bench.report(begin, end, "list", 1, page.total + length(page.items)))
}
cursor_once(value: Int) () {
begin = dever.time.monotonic_nanos()
page = model.cursor({
order = id.asc
size = 32
})
end = dever.time.monotonic_nanos()
blocking(bench.report(begin, end, "cursor", 1, length(page.items)))
}
count_item(value: model.Item, total: Int) (next: Int) {
next = total + 1
}
stream_once(value: Int) () {
begin = dever.time.monotonic_nanos()
rows = model.stream({
order = id.asc
size = 16
})
total = reduce(count_item, rows, 0)
end = dever.time.monotonic_nanos()
blocking(bench.report(begin, end, "stream", 1, total))
}
pool_once(value: Int) () {
begin = dever.time.monotonic_nanos()
total = model.count()
end = dever.time.monotonic_nanos()
blocking(bench.report(begin, end, "pool", 1, total))
}
transaction fill_cancelled_transaction() () {
created = model.create({
value = 1
label = "cancelled"
})
delayed = model.delay(config.orm_cancel_work())
}
orm_idle() (result: Bool) {
total = model.count()
blocking(bench.ready(total))
bench.pause(config.lifetime())
result = true
}
orm_crud() (result: Bool) {
ready()
each(crud_once, config.orm_input())
result = true
}
orm_list() (result: Bool) {
populate()
ready()
each(list_once, config.orm_input())
result = true
}
orm_cursor() (result: Bool) {
populate()
ready()
each(cursor_once, config.orm_input())
result = true
}
orm_stream() (result: Bool) {
ready()
each(stream_once, config.orm_input())
result = true
}
orm_pool() (result: Bool) {
ready()
parallel_each(pool_once, config.orm_input(), config.orm_concurrency())
result = true
}
orm_seed() (result: Bool) {
populate()
result = true
}
count() (total: Int) {
total = model.count()
}
cancel() (result: Bool) {
fill_cancelled_transaction()
result = true
}
@@ -0,0 +1,9 @@
public get count = app.count
public post cancel = app.cancel
cmd idle = app.orm_idle
cmd crud = app.orm_crud
cmd list = app.orm_list
cmd cursor = app.orm_cursor
cmd stream = app.orm_stream
cmd pool = app.orm_pool
cmd seed = app.orm_seed
@@ -0,0 +1,13 @@
type DelayResult {
total: Int
}
global type Item {
value: Int index
label: Text(1, 32)
}
sql delay(work: Int) (result: DelayResult) {
sqlite = "WITH RECURSIVE counter(value) AS (SELECT 1 UNION ALL SELECT value + 1 FROM counter WHERE value < ?1) SELECT SUM(value) AS total FROM counter"
postgres = "SELECT SUM(value)::bigint AS total FROM generate_series(1::bigint, $1::bigint) AS value"
}
@@ -0,0 +1 @@
cmd run = app.run
@@ -0,0 +1,4 @@
run() (result: Bool) {
dever.io.println("profile")
result = true
}
@@ -0,0 +1,7 @@
port.blocking_pause(millis: Int) () {
dever.time.sleep(millis)
}
port.report_error(message: Text) () {
dever.io.println("ERROR|" + message)
}
@@ -0,0 +1,11 @@
cmd sync_idle = app.sync_idle
cmd async_idle = app.async_idle
cmd tasks = app.tasks
cmd tasks_on_worker = app.tasks_on_worker
cmd channels = app.channels
cmd cancel = app.cancel
+132
View File
@@ -0,0 +1,132 @@
type Parking {
ready: Channel<Int>
gate: Channel<Int>
}
sync_idle() (result: Bool) {
bench.ready(0)
port.blocking_pause(config.lifetime())
result = true
}
idle() () {
blocking(bench.ready(0))
bench.pause(config.lifetime())
}
async_idle() (result: Bool) {
idle()
result = true
}
identity(value: Int) (result: Int) {
result = value
}
task_sum(value: Int, state: Int) (result: Int) {
job = run(identity(value))
result = state + wait(job)
}
sum(value: Int, state: Int) (result: Int) {
result = state + value
}
task_sample(index: Int, input: Bytes) () {
begin = dever.time.monotonic_nanos()
checksum = reduce(task_sum, input, 0)
end = dever.time.monotonic_nanos()
blocking(bench.report(begin, end, "tasks", length(input), checksum))
}
send_byte(value: Int, output: Channel<Int>) () {
send(output, value)
}
produce(input: Bytes, output: Channel<Int>) () {
each(send_byte, input, output)
close(output)
}
channel_sample(index: Int, input: Bytes) () {
output = channel(Int, config.channel_capacity())
begin = dever.time.monotonic_nanos()
producer = run(produce(input, output))
checksum = reduce(sum, stream(output), 0)
wait(producer)
end = dever.time.monotonic_nanos()
blocking(bench.report(begin, end, "channels", length(input), checksum))
}
wait_one(value: Int, parking: Parking) () {
send(parking.ready, value)
signaled(receive(parking.gate))
}
signaled(value: Int) () {}
signaled(value: null) () {
blocking(port.report_error("channel closed before expected signal"))
}
hold(parking: Parking) () {
parallel_each(wait_one, config.pending_input(), config.pending(), parking)
}
started(value: Int, ready: Channel<Int>) () {
signaled(receive(ready))
}
cancel_worker() () {
parking = Parking {
ready = channel(Int, config.pending())
gate = channel(Int, 1)
}
job = run(hold(parking))
each(started, config.pending_input(), parking.ready)
blocking(bench.ready(config.pending()))
bench.pause(config.idle_sample_ms())
begin = dever.time.monotonic_nanos()
stop(job)
end = dever.time.monotonic_nanos()
close(parking.ready)
close(parking.gate)
blocking(bench.report(begin, end, "cancel", config.pending(), config.pending()))
}
task_worker() () {
input = config.input()
blocking(bench.ready(0))
each(task_sample, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], input)
}
channel_worker() () {
input = config.input()
blocking(bench.ready(0))
each(channel_sample, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], input)
}
tasks() (result: Bool) {
task_worker()
result = true
}
nested_task_worker() () {
job = run(task_worker())
wait(job)
}
tasks_on_worker() (result: Bool) {
nested_task_worker()
result = true
}
channels() (result: Bool) {
channel_worker()
result = true
}
cancel() (result: Bool) {
cancel_worker()
result = true
}
@@ -0,0 +1,3 @@
blocking_pause(millis: Int) () fails dever.time.SleepResult
report_error(message: Text) () fails dever.io.WriteResult
+97
View File
@@ -0,0 +1,97 @@
"""长连接对端的阶段校验与采样;进程预算由统一运行器管理。"""
import json
import time
from process import Process, summarize
PHASES = ("connecting", "connected", "closing", "disconnected")
SETTLE_SECONDS = 0.3
PROCESS_MARGIN_SECONDS = 10
def phase_sequence(cycles):
return [(cycle, phase) for cycle in range(cycles) for phase in PHASES]
def lifecycle_seconds(cycles, hold_seconds, timeout_ms):
timeout_seconds = timeout_ms / 1000
# Each cycle has independent connect, hold-overrun and close deadlines.
return cycles * (hold_seconds + 3 * timeout_seconds + SETTLE_SECONDS) + PROCESS_MARGIN_SECONDS
def stable_phase(before, after):
return before[-1] if before and before == after else None
def parse_progress(text, cycles):
phases = []
result = None
# stdout is sampled while its writer is alive; only consume complete lines.
for line in text.splitlines(keepends=True):
if not line.endswith("\n"):
continue
if result is not None:
raise ValueError("live benchmark output continued after its final result")
if line.startswith("PHASE|"):
_, cycle, phase = line.strip().split("|")
phases.append((int(cycle), phase))
elif line.startswith("{"):
result = json.loads(line)
else:
raise ValueError(f"unexpected live benchmark output: {line.strip()}")
if phases != phase_sequence(cycles)[:len(phases)]:
raise ValueError("live benchmark phases are missing, duplicated or out of order")
return phases, result
def validate_result(result, protocol, connections, cycles):
counts = ("attempted_connections", "connected", "disconnected", "messages", "errors")
if result is None or any(type(result[key]) is not int or result[key] < 0 for key in counts):
raise ValueError("missing or invalid live counters")
expected = connections * cycles
if (result["protocol"] != protocol or result["connections"] != connections or result["cycles"] != cycles
or any(result[key] != expected for key in counts[:3])
or result["messages"] < expected or result["errors"] != 0 or result["elapsed_ms"] <= 0):
raise ValueError("live benchmark did not validate and close all requested connections")
def load_live(args, process, port, protocol, connections, timeout_ms):
scheme, path = {"tcp": ("tcp", "/"), "ws": ("ws", "/ws"), "sse": ("http", "/events")}[protocol]
hold_ms = round(args.duration * 1000)
command = [args.artifacts / "live_bench", protocol, f"{scheme}://127.0.0.1:{port}{path}",
connections, args.cycles, hold_ms, timeout_ms]
phases = {phase: [] for phase in phase_sequence(args.cycles)}
with Process(command, process.directory / "load", cpus=args.client_cpus,
peer_settings={"workers": args.client_workers}) as client:
deadline = time.monotonic() + lifecycle_seconds(args.cycles, args.duration, timeout_ms)
while client.child.poll() is None:
if time.monotonic() >= deadline:
raise TimeoutError("long connection client exceeded its bounded lifecycle")
progress_before, _ = parse_progress(client.text(), args.cycles)
first = len(process.samples)
process.sample()
process.check()
if process.child.poll() is not None:
raise RuntimeError("server exited during long connection load")
progress_after, _ = parse_progress(client.text(), args.cycles)
if phase := stable_phase(progress_before, progress_after):
phases[phase].extend(process.samples[first:])
client.sample()
time.sleep(args.interval)
client.check()
progress, result = parse_progress(client.text(), args.cycles)
if progress != phase_sequence(args.cycles):
raise ValueError("long connection client ended before all phases")
validate_result(result, protocol, connections, args.cycles)
# Connection setup/close can finish between polls. Only the two explicit
# hold windows require observations; retain setup samples when present.
observations = []
for (cycle, phase), samples in phases.items():
if phase in ("connected", "disconnected") and len(samples) < 2:
raise ValueError(f"sampling interval missed {cycle}/{phase} window")
observations.append({"cycle": cycle, "phase": phase,
"resources": summarize(samples) if samples else None,
"last": samples[-1] if samples else None, "raw": samples})
return {"result": result, "phases": observations,
"client_resources": summarize(client.samples), "client_raw": client.samples}
+231
View File
@@ -0,0 +1,231 @@
"""Linux 进程与可选专属 cgroup 的生命周期和采样。"""
import os
from pathlib import Path
import signal
import statistics
import subprocess
import time
import uuid
from settings import stage_peer
def key_values(path):
return {fields[0].rstrip(":"): fields[1] for line in path.read_text().splitlines()
if len(fields := line.split()) >= 2}
def process_sample(pid):
root = Path("/proc") / str(pid)
status = key_values(root / "status")
if "VmRSS" not in status:
raise ProcessLookupError(f"process {pid} exited during sampling")
memory = key_values(root / "smaps_rollup")
# comm 字段可含空格和括号;从最后一个右括号之后解析固定 stat 字段。
stat = (root / "stat").read_text().rsplit(")", 1)[1].split()
switches = {"voluntary_ctxt_switches": 0, "nonvoluntary_ctxt_switches": 0}
for thread in (root / "task").iterdir():
try:
thread_status = key_values(thread / "status")
except FileNotFoundError:
continue # A completed blocking thread can disappear during a sample.
for name in switches:
switches[name] += int(thread_status[name])
return {
"time": time.monotonic(),
"rss_bytes": int(status["VmRSS"]) * 1024,
"rss_high_water_bytes": int(status["VmHWM"]) * 1024,
"pss_bytes": int(memory["Pss"]) * 1024,
"threads": int(status["Threads"]),
"fds": len(list((root / "fd").iterdir())),
"cpu_seconds": (int(stat[11]) + int(stat[12])) / os.sysconf("SC_CLK_TCK"),
**switches,
}
class Budget:
def __init__(self, parent, memory_mib, cpu_quota):
self.path = None
self.final = None
if parent is None:
if memory_mib is not None or cpu_quota is not None:
raise ValueError("memory/cpu quotas require --cgroup-parent")
return
self.path = parent.resolve() / ("dever-perf-" + uuid.uuid4().hex)
self.path.mkdir()
try:
if memory_mib is not None:
(self.path / "memory.max").write_text(str(memory_mib * 1024 * 1024))
(self.path / "memory.swap.max").write_text("0")
if cpu_quota is not None:
(self.path / "cpu.max").write_text(f"{round(cpu_quota * 100000)} 100000")
except BaseException:
self.path.rmdir()
raise
def sample(self):
if self.path is None:
return None
return {
"current_bytes": int((self.path / "memory.current").read_text()),
"peak_bytes": int((self.path / "memory.peak").read_text()),
"events": {k: int(v) for k, v in key_values(self.path / "memory.events").items()},
"cpu": {k: int(v) for k, v in key_values(self.path / "cpu.stat").items()},
"max": (self.path / "memory.max").read_text().strip(),
"cpu_max": (self.path / "cpu.max").read_text().strip(),
}
def close(self):
if self.path is not None:
try:
self.final = self.sample()
except OSError as error:
try:
self.path.rmdir()
except OSError as cleanup_error:
error.add_note(f"cannot remove owned cgroup {self.path}: {cleanup_error}")
raise
else:
self.path.rmdir()
class Process:
def __init__(self, command, directory, *, peer_settings=None, cpus=None, budget=None):
self.command = [str(arg) for arg in command]
self.directory = directory
self.budget = budget
self.samples = []
self.directory.mkdir(parents=True, exist_ok=False)
if peer_settings is not None:
self.command[0] = str(stage_peer(command[0], directory, peer_settings))
self.stdout = (directory / "stdout.log").open("w")
self.stderr = (directory / "stderr.log").open("w")
self.started = time.monotonic()
def prepare_child():
# 运行器单线程;只对即将 exec 的自有子进程设置 CPU/cgroup。
if cpus is not None:
os.sched_setaffinity(0, cpus)
if budget is not None and budget.path is not None:
(budget.path / "cgroup.procs").write_text(str(os.getpid()))
try:
self.child = subprocess.Popen(self.command, stdout=self.stdout, stderr=self.stderr,
preexec_fn=prepare_child, start_new_session=True)
except BaseException:
self.stdout.close()
self.stderr.close()
raise
def text(self):
return (self.directory / "stdout.log").read_text()
def descriptors(self):
descriptors = {}
for path in (Path("/proc") / str(self.child.pid) / "fd").iterdir():
try:
descriptors[path.name] = os.readlink(path)
except FileNotFoundError:
continue # A descriptor may close while /proc is being read.
return descriptors
def sample(self):
if self.child.poll() is not None:
return
try:
sample = process_sample(self.child.pid)
except (FileNotFoundError, ProcessLookupError):
# Linux can release /proc memory fields before waitpid reports exit.
# This observation is unavailable; check() still owns exit status.
return
sample["elapsed_seconds"] = sample.pop("time") - self.started
if self.budget is not None:
sample["cgroup"] = self.budget.sample()
self.samples.append(sample)
def ready(self, timeout=10):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
self.sample()
for line in self.text().splitlines():
if line.startswith("ERROR|"):
raise RuntimeError(line)
if line.startswith("READY|"):
self.ready_seconds = time.monotonic() - self.started
return int(line.split("|")[1])
if self.child.poll() is not None:
raise RuntimeError(f"process exited before readiness: {self.directory}")
time.sleep(0.005)
raise TimeoutError(f"readiness timeout: {self.directory}")
def check(self):
code = self.child.poll()
if code is not None and code != 0:
raise RuntimeError(f"process exited {code}: {self.directory}")
if "ERROR|" in self.text() or (self.directory / "stderr.log").stat().st_size:
raise RuntimeError(f"benchmark reported an error: {self.directory}")
def close(self):
try:
if self.child.poll() is None:
try:
os.killpg(self.child.pid, signal.SIGTERM)
except ProcessLookupError:
# 子进程可能恰好在 poll 与 signal 之间自然退出。
pass
try:
self.child.wait(timeout=3)
except subprocess.TimeoutExpired:
os.killpg(self.child.pid, signal.SIGKILL)
self.child.wait(timeout=3)
finally:
self.stdout.close()
self.stderr.close()
def __enter__(self):
return self
def __exit__(self, *_):
self.close()
def observe(processes, seconds, interval):
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
for process in processes:
process.sample()
process.check()
time.sleep(min(interval, max(0, deadline - time.monotonic())))
def summarize(samples):
if not samples:
raise ValueError("process ended before any resource sample")
result = {"samples": len(samples)}
for metric in ("rss_bytes", "pss_bytes", "threads", "fds"):
values = sorted(sample[metric] for sample in samples)
result[metric + "_median"] = values[len(values) // 2]
result[metric + "_max"] = max(values)
result["rss_high_water_bytes"] = max(sample["rss_high_water_bytes"] for sample in samples)
elapsed = samples[-1]["elapsed_seconds"] - samples[0]["elapsed_seconds"]
cpu = samples[-1]["cpu_seconds"] - samples[0]["cpu_seconds"]
result["cpu_seconds_sampled"] = cpu
result["cpu_cores_sampled"] = cpu / elapsed if elapsed > 0 else None
for name in ("voluntary_ctxt_switches", "nonvoluntary_ctxt_switches"):
result[name + "_sampled"] = samples[-1][name] - samples[0][name]
return result
def resource_trend(samples):
"""报告四段中位数与后半程增长,避免将启动分配误记为长期泄漏。"""
if len(samples) < 4:
raise ValueError("resource trend requires at least four observations")
windows = [samples[len(samples) * index // 4:len(samples) * (index + 1) // 4]
for index in range(4)]
medians = [statistics.median(sample["rss_bytes"] for sample in window) for window in windows]
elapsed = samples[-1]["elapsed_seconds"] - samples[0]["elapsed_seconds"]
return {"observed_seconds": elapsed, "rss_quarter_medians_bytes": medians,
"rss_late_growth_bytes": medians[-1] - medians[2],
"rss_late_growth_bytes_per_second": (medians[-1] - medians[2]) / (elapsed / 4) if elapsed > 0 else None,
"fds_first": samples[0]["fds"], "fds_last": samples[-1]["fds"]}
File diff suppressed because it is too large. Load diff
+59
View File
@@ -0,0 +1,59 @@
"""基准配置与隔离 peer 布置;配置只来自 config/setting.json。"""
import errno
import json
import os
from pathlib import Path
import shutil
import tempfile
ROOT = Path(__file__).resolve().parents[2]
PEER_CONFIGURATION = "setting-json-v1"
def write_private_json(path, value, *, replace=False):
path = Path(path)
temporary = None
if replace:
descriptor, temporary = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}-")
else:
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
try:
with os.fdopen(descriptor, "w") as output:
os.fchmod(output.fileno(), 0o600)
json.dump(value, output, indent=2)
output.write("\n")
if temporary is not None:
os.replace(temporary, path)
finally:
if temporary is not None:
Path(temporary).unlink(missing_ok=True)
def stage_peer(executable, directory, settings):
"""独立配置共用不可变程序;测量期间不得重写输入二进制。"""
executable = Path(executable).resolve(strict=True)
destination = directory / executable.name
(directory / "config").mkdir()
write_private_json(directory / "config/setting.json", {"benchmark": settings})
try:
os.link(executable, destination)
except OSError as error:
if error.errno != errno.EXDEV:
raise
# 配置目录可以位于另一文件系统;这种情况下硬链接不可用。
with destination.open("xb") as output, executable.open("rb") as source:
shutil.copyfileobj(source, output)
shutil.copymode(executable, destination)
return destination.resolve()
def configured_peer(name, setting_path=ROOT / "config/setting.json"):
document = json.loads(setting_path.read_text())
section = document.get("performance") if isinstance(document, dict) else None
value = section.get(name) if isinstance(section, dict) else None
if not isinstance(value, str) or not value.strip():
raise ValueError(f"config/setting.json must define performance.{name} as a binary path")
# 仓库设置中的相对路径以项目根目录为基准。
path = Path(value)
return (setting_path.parent.parent / path).resolve(strict=True)
+255
View File
@@ -0,0 +1,255 @@
import json
from pathlib import Path
import subprocess
import sys
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import Mock, patch
from cms import (CmsClient, ReadLatency, concurrent_reads, decode_response, execute, isolated_settings, latency_summary,
publish_workload, run_postgres_case, run_postgres_rbac)
from settings import write_private_json
class CmsMeasurementTests(unittest.TestCase):
def test_long_read_histogram_is_bounded_and_reports_upper_bounds(self):
latency = ReadLatency()
for _ in range(10000):
latency.add(3)
other = ReadLatency()
other.add(100)
latency.merge(other)
self.assertEqual(len(latency.buckets), 2048)
self.assertEqual(latency.count, 10001)
self.assertGreaterEqual(latency.percentile(.99), 3)
self.assertLess(latency.percentile(.99), 3.05)
client = CmsClient(12345)
client.record_timings = False
client.connection = SimpleNamespace(request=Mock(), getresponse=Mock(return_value=SimpleNamespace(
status=200, read=Mock(return_value=b'{"code":0,"message":"ok","data":1}'))), close=Mock())
try:
self.assertEqual(client.request("GET", "/test"), 1)
self.assertEqual(client.timings, {})
finally:
client.close()
def test_concurrent_read_failure_closes_clients_and_cannot_be_counted_as_success(self):
expected = {"total": 1, "items": [{"title": "published"}]}
for reply in ({"total": 0, "items": []}, RuntimeError("HTTP 403")):
with self.subTest(reply=reply):
opened = []
def client(_port):
request = Mock(side_effect=reply) if isinstance(reply, Exception) else Mock(return_value=reply)
instance = SimpleNamespace(cookies={}, timings={}, request=request, close=Mock())
opened.append(instance)
return instance
process = SimpleNamespace(samples=[], check=Mock())
with patch("cms.CmsClient", side_effect=client), patch("cms.observe"):
with self.assertRaises(RuntimeError):
concurrent_reads(12345, {"front": "test-only-cookie"}, expected, process,
concurrency=2, seconds=.1, interval=.01)
self.assertEqual(len(opened), 2)
for instance in opened:
instance.close.assert_called_once()
def test_delete_inputs_use_query_instead_of_json_body(self):
client = CmsClient(12345)
response = SimpleNamespace(status=200, read=Mock(return_value=b'{"code":0,"message":"ok","data":true}'))
connection = SimpleNamespace(request=Mock(), getresponse=Mock(return_value=response), close=Mock())
client.connection = connection
try:
self.assertIs(client.request("DELETE", "/revoke", {"user_id": 3, "role_id": "fixture reader"}), True)
sent = connection.request.call_args.args
self.assertEqual(sent[:3], ("DELETE", "/revoke?user_id=3&role_id=fixture+reader", None))
finally:
client.close()
def test_postgres_rbac_checks_catalog_roles_and_same_session_revocation(self):
catalog = [
{"component": "news", "domain": "article", "site": "admin", "action": "list",
"method": "GET", "key": "news.article.admin.list"},
{"component": "user", "domain": "authorization", "site": "admin", "action": "permissions",
"method": "GET", "key": "user.authorization.admin.permissions"},
{"component": "news", "domain": "article", "site": "front", "action": "list",
"method": "GET", "key": "news.article.front.list"},
]
identity = {"user_id": 1, "tenant_id": 2}
member = {"user_id": 3, "tenant_id": 2, "credential_id": 4}
login = [{"account": {"id": 3}, "tenant_id": 2, "site": site}
for site in ("admin", "front")]
for failure in (False, True):
with self.subTest(failure=failure):
owner = SimpleNamespace(request=Mock(side_effect=[
[entry for entry in catalog if entry["site"] == "admin"],
[entry for entry in catalog if entry["site"] == "front"],
] + [True] * 9))
responses = login + ([RuntimeError("missing denial")] if failure else [None] * 11)
client = SimpleNamespace(request=Mock(side_effect=responses), close=Mock())
with patch("cms.execute", return_value=member), patch("cms.CmsClient", return_value=client):
if failure:
with self.assertRaisesRegex(RuntimeError, "missing denial"):
run_postgres_rbac(Path("owned-app"), Path("owned-output"), 12345, owner, identity)
else:
result = run_postgres_rbac(Path("owned-app"), Path("owned-output"), 12345, owner, identity)
self.assertEqual(result["member_user_id"], 3)
self.assertEqual(set(result["permissions"]), {"admin_read", "admin_permissions", "front_list"})
self.assertNotIn("password", result)
calls = client.request.call_args_list[2:]
self.assertEqual([call.kwargs.get("expected", 200) for call in calls],
[403, 403, 403, 200, 200, 200, 403, 200, 200, 403, 403])
saves = [call for call in owner.request.call_args_list if call.args[1].endswith("/save")]
self.assertEqual(owner.request.call_args_list[1].kwargs["site"], "front")
self.assertEqual(saves[0].args[2]["id"], saves[2].args[2]["id"])
self.assertNotEqual(saves[0].kwargs["site"], saves[2].kwargs["site"])
client.close.assert_called_once()
def test_postgres_case_keeps_source_configuration_on_build_failure(self):
with tempfile.TemporaryDirectory(prefix="dever-cms-source-") as temporary:
directory = Path(temporary)
source = directory / "project"
source.mkdir()
for name in ("config", "module", "test"):
(source / name).mkdir()
original = {
"runtime": {"mode": "all"},
"database": {"default": {
"type": "postgres", "url": "postgres://fixture@127.0.0.1:12345/control",
"tenant_database_prefix": "dever_test_owned",
}},
"auth": {"providers": {"session": {"jwtSecret": "original"}}},
"sites": {}, "log": {"level": "info"},
}
setting = source / "config/setting.json"
write_private_json(setting, original)
before = setting.read_bytes()
staged_paths = []
def fail_check(command, *_args, **_kwargs):
staged = command[2]
staged_paths.append(staged)
self.assertNotEqual(staged, source)
changed = json.loads((staged / "config/setting.json").read_text())
self.assertEqual(changed["runtime"]["mode"], "api")
self.assertNotEqual(changed["auth"]["providers"]["session"]["jwtSecret"], "original")
raise RuntimeError("owned check failed")
output = directory / "result"
with patch("cms.execute", side_effect=fail_check):
with self.assertRaisesRegex(RuntimeError, "owned check failed"):
run_postgres_case(source, output, directory / "compiler", articles=2)
self.assertEqual(setting.read_bytes(), before)
self.assertFalse((source / "cms-app").exists())
self.assertTrue(staged_paths)
self.assertFalse(staged_paths[0].exists())
self.assertEqual(json.loads((output / "report.json").read_text())["status"], "failed")
def test_private_config_replacement_is_atomic_and_keeps_exclusive_creation(self):
with tempfile.TemporaryDirectory(prefix="dever-cms-setting-") as temporary:
directory = Path(temporary)
setting = directory / "setting.json"
write_private_json(setting, {"original": True})
with self.assertRaises(FileExistsError):
write_private_json(setting, {"overwrite": True})
with self.assertRaises(TypeError):
write_private_json(setting, {"invalid": object()}, replace=True)
self.assertEqual(json.loads(setting.read_text()), {"original": True})
self.assertEqual(list(directory.iterdir()), [setting])
write_private_json(setting, {"postgres": True}, replace=True)
self.assertEqual(json.loads(setting.read_text()), {"postgres": True})
self.assertEqual(setting.stat().st_mode & 0o777, 0o600)
self.assertEqual(list(directory.iterdir()), [setting])
def test_envelope_failure_cannot_count_as_publication(self):
self.assertEqual(decode_response(b'{"code":0,"message":"ok","data":{"id":1}}', 200),
{"id": 1})
for document, status in (({"code": 0, "message": "ok", "data": {}}, 500),
({"code": 409, "message": "conflict", "data": None}, 200),
({"id": 1}, 200)):
with self.assertRaises(RuntimeError):
decode_response(json.dumps(document).encode(), status)
def test_isolation_preserves_site_contract_without_mutating_input(self):
source = {
"runtime": {"mode": "all"}, "http": {"port": 8080},
"database": {"default": {"type": "sqlite", "path": "old.db"}},
"auth": {"providers": {"session": {"jwtSecret": "example", "verify": "user.account.verify"}}},
"sites": {"admin": {"path": "admin", "auth": "session", "origin": "https://example.invalid"}},
"log": {"level": "info"},
}
staged = isolated_settings(source, 12345)
self.assertEqual(source["runtime"]["mode"], "all")
self.assertEqual(source["database"]["default"]["path"], "old.db")
self.assertEqual(staged["runtime"]["mode"], "api")
self.assertEqual(staged["sites"]["admin"]["path"], "admin")
self.assertEqual(staged["sites"]["admin"]["origin"], "http://127.0.0.1:12345")
self.assertNotEqual(staged["auth"]["providers"]["session"]["jwtSecret"], "example")
self.assertEqual(staged["auth"]["providers"]["session"]["verify"], "user.account.verify")
self.assertEqual(source["log"]["level"], "info")
self.assertEqual(staged["log"]["level"], "error")
source["database"]["default"]["type"] = "postgres"
with self.assertRaises(RuntimeError):
isolated_settings(source, 12345)
def test_postgres_isolation_preserves_explicit_control_database_and_prefix(self):
source = {
"runtime": {"mode": "all"},
"database": {"default": {
"type": "postgres", "url": "postgres://fixture@127.0.0.1:12345/control",
"tenant_database_prefix": "dever_test_owned", "tls": "disabled",
}},
"auth": {"providers": {"session": {"jwtSecret": "example"}}},
"sites": {"admin": {"origin": "https://example.invalid"}},
"log": {"level": "info"},
}
staged = isolated_settings(source, 23456)
self.assertEqual(staged["database"], source["database"])
self.assertEqual(staged["http"], {"host": "127.0.0.1", "port": 23456})
self.assertNotIn("path", staged["database"]["default"])
source["database"]["default"].pop("tenant_database_prefix")
with self.assertRaises(RuntimeError):
isolated_settings(source, 23456)
def test_publish_workload_uses_tenant_and_title_parameters(self):
identity = {"credential_id": 11, "user_id": 3, "tenant_id": 2}
publication = {"slug": "bench-0", "title": "Tenant A 0 published",
"body": "Published body"}
client = SimpleNamespace(request=Mock(side_effect=[
{"account": {"id": 3}, "tenant_id": 2, "site": "admin"},
{"account": {"id": 3}, "tenant_id": 2, "site": "front"},
{"id": 1, "slug": "bench-0", "title": "Tenant A 0", "version": 1},
{"id": 7, "article_id": 1, "title": "Tenant A 0 published", "version": 2},
publication, {"total": 1, "items": [publication]}, None,
]))
process = SimpleNamespace(sample=Mock())
contract = publish_workload(client, process, identity, "fixture-password", 1,
tenant_key="tenant_a", title_prefix="Tenant A")
self.assertEqual(contract, {"articles": 1, "titles": ["Tenant A 0 published"],
"duplicate_publish_status": 409})
for call in client.request.call_args_list[:2]:
self.assertEqual(call.args[2]["tenant"], "tenant_a")
revision = client.request.call_args_list[3]
self.assertEqual(revision.args[:2], ("POST", "/content/revision/admin/manage/create"))
self.assertEqual(revision.args[2]["expected_version"], 1)
self.assertEqual(revision.args[2]["selected_article_id"], 1)
self.assertEqual(process.sample.call_count, 1)
def test_small_workload_reports_samples_without_implying_p99(self):
self.assertEqual(latency_summary({"publish": [4, 1, 3]}),
{"publish": {"samples": 3, "p50_ms": 3, "max_ms": 4}})
def test_command_timeout_retains_output_and_reaps_its_owned_process(self):
with tempfile.TemporaryDirectory(prefix="dever-cms-timeout-") as temporary:
directory = Path(temporary)
command = [sys.executable, "-u", "-c",
"import time,sys; print('starting'); print('diagnostic',file=sys.stderr); time.sleep(10)"]
with self.assertRaises(subprocess.TimeoutExpired):
execute(command, directory, "timeout", timeout=0.2)
self.assertEqual((directory / "timeout.stdout.log").read_text(), "starting\n")
self.assertEqual((directory / "timeout.stderr.log").read_text(), "diagnostic\n")
if __name__ == "__main__":
unittest.main()
+122
View File
@@ -0,0 +1,122 @@
"""用独立 Python 对端检查 Rust 长连接客户端的正文与生命周期判定。"""
from contextlib import contextmanager
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
from pathlib import Path
import socketserver
import subprocess
import tempfile
import threading
import time
import unittest
from live import parse_progress, phase_sequence, validate_result
from settings import configured_peer, stage_peer
@contextmanager
def serving(server):
with server:
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield server.server_address[1]
finally:
server.shutdown()
thread.join(timeout=2)
if thread.is_alive():
raise RuntimeError("owned test listener did not stop")
class Echo(socketserver.BaseRequestHandler):
def handle(self):
self.request.settimeout(2)
while payload := self.request.recv(4096):
response = b"!" * len(payload) if self.server.wrong_body else payload
self.request.sendall(response)
if self.server.wrong_body:
return
class InvalidEvent(BaseHTTPRequestHandler):
def log_message(self, *_):
pass
def do_GET(self):
body = b"event: live\nid: 1\ndata: wrong\n\n"
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
try:
self.wfile.write(body)
except (BrokenPipeError, ConnectionResetError):
# The client aborts sibling sessions after the first deliberate mismatch.
return
class SilentEvent(BaseHTTPRequestHandler):
def log_message(self, *_):
pass
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.end_headers()
self.wfile.write(b"event: live\nid: 1\ndata: ready\n\n")
self.wfile.flush()
time.sleep(1)
class LiveClientTests(unittest.TestCase):
def setUp(self):
directory = tempfile.TemporaryDirectory(prefix="dever-live-client-")
self.addCleanup(directory.cleanup)
self.executable = stage_peer(configured_peer("live_peer"), Path(directory.name), {"workers": 1})
def execute(self, protocol, port, path):
scheme = "http" if protocol == "sse" else protocol
return subprocess.run([self.executable, protocol, f"{scheme}://127.0.0.1:{port}{path}", "2", "2", "300", "1000"],
capture_output=True, text=True, timeout=8)
def test_tcp_reconnect_and_exact_echo(self):
listener = socketserver.ThreadingTCPServer(("127.0.0.1", 0), Echo)
listener.wrong_body = False
with serving(listener) as port:
result = self.execute("tcp", port, "/")
self.assertEqual(result.returncode, 0, result.stderr)
phases, counts = parse_progress(result.stdout, 2)
self.assertEqual(phases, phase_sequence(2))
validate_result(counts, "tcp", 2, 2)
def test_tcp_wrong_body_cannot_be_counted_as_held(self):
listener = socketserver.ThreadingTCPServer(("127.0.0.1", 0), Echo)
listener.wrong_body = True
with serving(listener) as port:
result = self.execute("tcp", port, "/")
self.assert_failure(result)
def test_sse_wrong_event_cannot_pass_connection_readiness(self):
with serving(ThreadingHTTPServer(("127.0.0.1", 0), InvalidEvent)) as port:
result = self.execute("sse", port, "/events")
self.assert_failure(result)
def test_sse_silent_hold_cannot_pass_heartbeat_validation(self):
with serving(ThreadingHTTPServer(("127.0.0.1", 0), SilentEvent)) as port:
result = self.execute("sse", port, "/events")
self.assertNotEqual(result.returncode, 0)
counts = json.loads(result.stdout.splitlines()[-1])
self.assertGreater(counts["errors"], 0)
self.assertIn("PHASE|0|connected\n", result.stdout)
self.assertNotIn("PHASE|0|closing\n", result.stdout)
def assert_failure(self, result):
self.assertNotEqual(result.returncode, 0)
counts = json.loads(result.stdout.splitlines()[-1])
self.assertGreater(counts["errors"], 0)
self.assertNotIn("PHASE|0|connected\n", result.stdout)
if __name__ == "__main__":
unittest.main()
+120
View File
@@ -0,0 +1,120 @@
"""用独立的 Python HTTP 对端验证负载计数;只占用自有随机回环端口。"""
from contextlib import contextmanager
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
from pathlib import Path
import subprocess
import tempfile
import threading
import unittest
from process import Process
from run import validate_load
from settings import configured_peer, stage_peer
ROOT = Path(__file__).resolve().parents[2]
TLS = ROOT / "test/dever-tests/fixtures/tls"
@contextmanager
def peer(body):
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *_):
pass
server = HTTPServer(("127.0.0.1", 0), Handler)
worker = threading.Thread(target=server.serve_forever, kwargs={"poll_interval": 0.01})
worker.start()
try:
yield server.server_port
finally:
server.shutdown()
server.server_close()
worker.join(timeout=2)
if worker.is_alive():
raise RuntimeError("owned HTTP peer did not stop")
@contextmanager
def benchmark_service(executable, mode):
settings = {"workers": 1, "connections": 4, "task_capacity": 128,
"timeout_ms": 1000, "lifetime_ms": 5000, "http2_streams": 16,
"http2_stream_window_bytes": 65535, "http2_connection_window_bytes": 262144,
"cert_path": str(TLS / "server.pem"), "key_path": str(TLS / "server-key.pem")}
with tempfile.TemporaryDirectory(prefix="dever-h2-bench-") as directory:
with Process([executable, mode], Path(directory) / "server", peer_settings=settings) as process:
yield process.ready()
process.check()
class NetworkMeasurementTests(unittest.TestCase):
def setUp(self):
directory = tempfile.TemporaryDirectory(prefix="dever-network-client-")
self.addCleanup(directory.cleanup)
self.executable = stage_peer(configured_peer("network_peer"), Path(directory.name), {"workers": 1})
def load(self, port):
executable = self.executable
output = subprocess.run([str(executable), "load", "h1", f"http://localhost:{port}",
"/plain", "20", "0.2", "1", "1", "500", "true", "-"],
capture_output=True, text=True, timeout=5, check=True)
self.assertEqual(output.stderr, "")
result = json.loads(output.stdout)
validate_load(result)
self.assertEqual(result["scheduled"], 4)
self.assertGreaterEqual(result["elapsed_seconds"], 0.2)
return result
def test_success_requires_exact_body_and_full_window(self):
with peer(b"hello") as port:
result = self.load(port)
self.assertEqual(result["succeeded"], 4)
self.assertEqual(result["errors"], 0)
self.assertEqual(result["dropped"], 0)
self.assertEqual(result["latency_samples"], 4)
self.assertEqual(result["http_version"], "h1")
self.assertEqual(result["physical_connections"], 1)
self.assertEqual(result["streams_per_connection"], 1)
self.assertEqual(result["request_slots"], 1)
def test_wrong_body_is_an_error_with_no_success_latency(self):
with peer(b"wrong") as port:
result = self.load(port)
self.assertEqual(result["succeeded"], 0)
self.assertEqual(result["errors"], 4)
self.assertEqual(result["latency_samples"], 0)
self.assertIsNone(result["latency_ms"])
def test_http2_uses_one_physical_connection_for_sixteen_request_slots(self):
executable = self.executable
for mode, transport in (("runtime-http2", "http"), ("hyper-http2", "http"),
("runtime-https2", "https"), ("hyper-https2", "https")):
with self.subTest(mode=mode), benchmark_service(executable, mode) as port:
output = subprocess.run(
[str(executable), "load", "h2", f"{transport}://localhost:{port}",
"/bytes", "200", "0.1", "1", "16", "1000", "true", str(TLS / "root.pem")],
capture_output=True, text=True, timeout=5, check=True)
self.assertEqual(output.stderr, "")
result = json.loads(output.stdout)
validate_load(result)
self.assertEqual(result["errors"], 0)
self.assertGreater(result["succeeded"], 0)
self.assertEqual(result["physical_connections"], 1)
self.assertEqual(result["streams_per_connection"], 16)
self.assertEqual(result["request_slots"], 16)
self.assertEqual(result["successful_handshakes"], 1)
if __name__ == "__main__":
unittest.main()
+47
View File
@@ -0,0 +1,47 @@
"""只执行 peer 的配置检查入口,不创建网络或连接数据库。"""
import json
from pathlib import Path
import subprocess
import tempfile
import unittest
from settings import configured_peer, stage_peer
class PeerSettingsTests(unittest.TestCase):
def check(self, peer, document):
with tempfile.TemporaryDirectory(prefix="dever-peer-config-test-") as temporary:
root = Path(temporary)
executable = stage_peer(configured_peer(peer), root, {})
setting = root / "config/setting.json"
if document is None:
setting.unlink()
else:
setting.write_text(json.dumps(document))
# 不在 executable 目录启动,验证读取位置不受工作目录影响。
return subprocess.run([executable, "check-config"], cwd=root.parent,
capture_output=True, text=True, timeout=5)
def test_both_peers_use_the_external_integer_setting(self):
for peer in ("network_peer", "live_peer"):
for workers in (1, 4, 64):
with self.subTest(peer=peer, workers=workers):
output = self.check(peer, {"benchmark": {"workers": workers}})
self.assertEqual(output.returncode, 0, output.stderr)
self.assertEqual(json.loads(output.stdout), {"workers": workers})
def test_both_peers_reject_missing_malformed_and_out_of_range_configuration(self):
invalid = [None, {}, [], {"benchmark": []}, {"benchmark": {"unknown": 1}}]
invalid.extend({"benchmark": {"workers": value}} for value in (0, 65, -1, 1.5, True, "4", None))
for peer in ("network_peer", "live_peer"):
for document in invalid:
with self.subTest(peer=peer, document=document):
output = self.check(peer, document)
self.assertNotEqual(output.returncode, 0)
self.assertEqual(output.stdout, "")
self.assertTrue(output.stderr.strip())
if __name__ == "__main__":
unittest.main()
+803
View File
@@ -0,0 +1,803 @@
"""基准运行器的最小正确性检查,不启动网络压测或编译器。"""
import argparse
import errno
import json
import os
from pathlib import Path
from types import SimpleNamespace
import sys
import tempfile
import unittest
from unittest.mock import MagicMock, Mock, patch
import cms
from process import Budget, Process, observe, summarize
from build import (APPLICATION_CONFIGURATION, BENCHMARK_COMMAND, ENTRIES, configuration, dependency_packages, fingerprint,
load_postgres_setting, postgres_case_url, postgres_database_case,
native_runtime_setting, prepare_cms_project, prepare_native_project,
prepare_profile_project, prepare_project,
profile_database_setting, stage_application_command)
from run import (HTTP_BODY_BYTES, application_command, fixture_command, async_measurements, csv_positive_ints, http2_summary,
cms_cases, cms_summary, expected_orm_checksum, orm_measurements, orm_summary,
orm_count, parse_client_cpus, parse_implementations, parser, percentile,
positive, report_arguments, required_binaries, require_no_oom, require_no_request_errors,
require_successful_requests, server, stage_orm_bundle, transaction_cancel_measurement,
validate_cpu_placement, validate_load)
from run import stage_live_bundle, verify_artifacts
from live import lifecycle_seconds, parse_progress, phase_sequence, stable_phase, validate_result
from settings import PEER_CONFIGURATION, configured_peer, stage_peer
class MeasurementTests(unittest.TestCase):
def test_application_command_matches_generated_declaration(self):
self.assertEqual(application_command(Path("app")),
[Path("app"), BENCHMARK_COMMAND, "{}"])
with tempfile.TemporaryDirectory(prefix="dever-perf-command-") as temporary:
module = Path(temporary) / "module"
stage_application_command(module, "benchmark.item.orm_list")
self.assertEqual((module / "benchmark/command/api.dever").read_text(),
"cmd run = app.run\n")
self.assertIn("benchmark.item.orm_list()",
(module / "benchmark/command/app.dever").read_text())
self.assertFalse((module / "main.dever").exists())
def test_generated_projects_use_stock_cms_and_cmd_for_orm_profiles(self):
settings = configuration(parser().parse_args(["build", "--output", "unused"]))
with tempfile.TemporaryDirectory(prefix="dever-perf-project-") as temporary:
root = Path(temporary)
for source in ("dever", "md"):
project = root / f"cms-{source}"
prepare_cms_project(project, source)
self.assertFalse((project / "module/main.dever").exists())
self.assertFalse((project / "module/benchmark").exists())
self.assertEqual((project / "config/setting.json").read_bytes(),
(Path(__file__).parents[2] / "examples/cms" / source
/ "config/setting.json").read_bytes())
self.assertTrue((project / "test/news/article").is_dir())
orm = root / "orm"
prepare_project(orm, "orm", settings, "sqlite")
self.assertFalse((orm / "module/main.dever").exists())
self.assertTrue((orm / "module/benchmark/command/api.dever").is_file())
self.assertTrue((orm / "module/benchmark/item/api.dever").is_file())
for profile in ("base", "sqlite", "postgres", "both"):
project = root / f"profile-{profile}"
prepare_profile_project(project, profile_database_setting(profile))
self.assertFalse((project / "module/main.dever").exists())
self.assertTrue((project / "module/benchmark/command/api.dever").is_file())
def test_profile_free_generation_copies_roles_and_only_used_helpers(self):
settings = configuration(parser().parse_args(["build", "--output", "unused"]))
expected_config = {
"runtime": ("lifetime", "idle_sample_ms", "pending", "channel_capacity",
"input", "pending_input"),
"http": ("lifetime", "limits", "http2_limits", "large_body",
"certificate", "key"),
"live": (),
}
with tempfile.TemporaryDirectory(prefix="dever-perf-sources-") as temporary:
for fixture, functions in expected_config.items():
destination = Path(temporary) / fixture
prepare_native_project(destination, fixture, settings)
domain = destination / "module/benchmark" / fixture
self.assertTrue(all((domain / f"{role}.dever").is_file()
for role in ("app", "port", "adapter", "api")))
config = destination / "module/benchmark/config/app.dever"
self.assertEqual(config.is_file(), bool(functions))
if functions:
source = config.read_text()
self.assertTrue(all(f"\n{name}()" in source for name in functions))
self.assertNotIn("orm_input()", source)
self.assertEqual((destination / "module/benchmark/bench/app.dever").is_file(),
fixture == "runtime")
setting = json.loads((destination / "config/setting.json").read_text())
self.assertEqual(setting, native_runtime_setting(settings) if fixture == "live" else {})
declarations = (domain / "api.dever").read_text()
for entry in ENTRIES[fixture]:
self.assertIn(f"cmd {entry} = app.{entry}", declarations)
self.assertEqual(fixture_command(Path("app"), entry),
[Path("app"), f"benchmark.{fixture}.{entry}", "{}"])
live = native_runtime_setting(settings)["adapter"]["benchmark.live"]["setting"]
self.assertEqual(live["lifetime_ms"], settings["lifetime_ms"])
self.assertEqual(live["limits"]["timeout_ms"], settings["timeout_ms"])
self.assertNotIn("timeout_ms", live)
def test_live_adapter_setting_is_private_to_each_executable(self):
settings = {"lifetime_ms": 4000, "timeout_ms": 2000, "connections": 8}
with tempfile.TemporaryDirectory(prefix="dever-live-bundle-") as temporary:
root = Path(temporary)
artifacts = root / "artifacts"
artifacts.mkdir()
for protocol in ("tcp", "ws"):
(artifacts / protocol).write_bytes(protocol.encode())
output = root / "output"
output.mkdir()
args = SimpleNamespace(artifacts=artifacts, output=output)
for protocol in ("tcp", "ws"):
executable = stage_live_bundle(args, settings, f"live-{protocol}-0", protocol)
self.assertEqual(executable.read_bytes(), protocol.encode())
setting = executable.parent / "config/setting.json"
self.assertEqual(json.loads(setting.read_text()), native_runtime_setting(settings))
self.assertEqual(setting.stat().st_mode & 0o777, 0o600)
self.assertFalse((artifacts / "config").exists())
def test_cms_cases_use_stock_config_and_publish_result(self):
with tempfile.TemporaryDirectory(prefix="dever-perf-cms-case-") as temporary:
root = Path(temporary)
artifacts = root / "artifacts"
output = root / "output"
artifacts.mkdir()
output.mkdir()
for source, name in (("dever", "cms"), ("md", "cms-md")):
(artifacts / name).write_bytes(source.encode())
config = artifacts / "source" / f"cms-{source}" / "config"
config.mkdir(parents=True)
(config / "setting.json").write_text("{}")
args = SimpleNamespace(artifacts=artifacts, output=output, repeats=1,
_cms_sources=("dever", "md"), cgroup_parent=None,
memory_mib=None, idle_seconds=0.1, interval=0.01,
server_cpu=None, cpu_quota=None)
result = {"contract": {"articles": 16, "titles": [f"Article {i}" for i in range(16)],
"duplicate_publish_status": 409},
"ready_seconds": 0.2, "workload_seconds": 0.5,
"published_articles_per_second": 32,
"latency": {}, "resources": {"rss_bytes_median": 1,
"pss_bytes_median": 1,
"rss_high_water_bytes": 2},
"raw": [], "budget_before_stop": None, "cgroup": None}
with patch("cms.run_case", return_value=result) as run_case:
cases = cms_cases(args, {}, {})
self.assertEqual(len(cases), 2)
self.assertEqual(run_case.call_count, 2)
for call, source in zip(run_case.call_args_list, ("dever", "md")):
identity = call.args[0]
self.assertEqual(identity["config"], str(
artifacts / "source" / f"cms-{source}" / "config/setting.json"))
self.assertEqual(identity["fingerprint"], fingerprint(Path(identity["executable"])))
self.assertEqual(call.kwargs["articles"], 16)
self.assertEqual(cms_summary(cases)["dever"]["articles"], 16)
self.assertTrue(all(case["workload_mode"] == "sequential_publish" for case in cases))
self.assertTrue(all("ready_value" not in case for case in cases))
def test_focused_cms_build_selection_keeps_the_full_build_as_default(self):
self.assertEqual(parser().parse_args(["build", "--output", "unused"]).selection, "all")
self.assertEqual(parser().parse_args(
["build", "--output", "unused", "--selection", "cms-profiles"]
).selection, "cms-profiles")
self.assertEqual(parser().parse_args(
["build", "--output", "unused", "--selection", "profiles"]
).selection, "profiles")
def test_cms_descriptor_recovery_bounds_pool_and_preserves_other_resources(self):
before = {"0": "/dev/null", "3": "socket:[listener]", "4": "/fixture/platform.db"}
after = {**before, "5": "/fixture/platform.db", "6": "/fixture/platform.db-wal"}
load = {"descriptors_before": before, "descriptors_after": after,
"pooled_descriptor_limits": {"/fixture/platform.db": 4,
"/fixture/platform.db-wal": 4,
"/fixture/platform.db-shm": 1}}
cms.validate_read_descriptor_recovery(load)
invalid = [
{**after, "9": "socket:[request]"},
{**after, "9": "socket:[listener]"},
{**after, "3": "socket:[replacement]"},
{**after, "0": "/unexpected/file"},
{**after, **{str(index): "/fixture/platform.db" for index in range(10, 15)}},
{**after, **{str(index): "/fixture/platform.db-wal" for index in range(10, 15)}},
{**after, "10": "/fixture/platform.db-shm", "11": "/fixture/platform.db-shm"},
]
for descriptors in invalid:
with self.subTest(descriptors=descriptors), self.assertRaises(RuntimeError):
cms.validate_read_descriptor_recovery({**load, "descriptors_after": descriptors})
def test_cms_failed_recovery_keeps_case_and_aggregate_evidence(self):
with tempfile.TemporaryDirectory(prefix="dever-cms-failed-report-") as temporary:
root = Path(temporary)
executable = root / "app"
executable.write_bytes(b"fixture")
config = root / "setting.json"
config.write_text("{}")
identity = {"executable": str(executable), "fingerprint": fingerprint(executable),
"config": str(config)}
manifest = {"format": "dever-cms-publish-v1", "cases": {"dever": identity, "md": identity}}
client = Mock(cookies={}, timings={})
client.request.return_value = []
snapshots = iter(({"3": "socket:[listener]"},
{"3": "socket:[listener]", "4": "socket:[leaked]"}))
def descriptors():
self.assertTrue(client.close.called, "control client must close before the baseline")
return next(snapshots)
process = Mock(samples=[{"fds": 1}], descriptors=descriptors)
context = MagicMock()
context.__enter__.return_value = process
context.__exit__.return_value = False
budget = Mock(final={"events": {"oom": 0, "oom_kill": 0}})
settings = {"database": {"default": {"type": "sqlite", "path": "data/platform.db",
"tenant_directory": "data/tenants", "max_connections": 4}}}
load = {"elapsed_seconds": 75, "succeeded": 100, "errors": 0}
with (patch("cms.isolated_settings", return_value=settings),
patch("cms.provision_identity", return_value=("actor", "password")),
patch("cms.CmsClient", return_value=client), patch("cms.Budget", return_value=budget),
patch("cms.Process", return_value=context), patch("cms.wait_ready", return_value=0.1),
patch("cms.publish_workload", return_value={"articles": 1}), patch("cms.observe"),
patch("cms.concurrent_reads", return_value=load) as reads):
with self.assertRaisesRegex(RuntimeError, "read descriptors did not recover"):
cms.run_cases(manifest, root / "output", articles=1, cgroup_parent=None,
memory_mib=None, concurrency=8, read_seconds=75)
snapshots = iter(({"3": "socket:[listener]"},))
reads.side_effect = RuntimeError("read fixture failed")
with self.assertRaisesRegex(RuntimeError, "read fixture failed"):
cms.run_cases(manifest, root / "read-failure", articles=1, cgroup_parent=None,
memory_mib=None, concurrency=8, read_seconds=75)
case = json.loads((root / "output/dever/report.json").read_text())
report = json.loads((root / "output/report.json").read_text())
self.assertEqual(case["status"], "failed")
self.assertEqual(case["raw"], [{"fds": 1}])
self.assertEqual(case["concurrent_reads"]["succeeded"], 100)
self.assertEqual(case["concurrent_reads"]["descriptors_after"]["4"], "socket:[leaked]")
self.assertEqual(case["cgroup"], budget.final)
self.assertEqual(report["cases"], {"dever": case})
self.assertEqual(report["status"], "failed")
early = json.loads((root / "read-failure/report.json").read_text())["cases"]["dever"]
self.assertEqual(early["contract"], {"articles": 1})
self.assertEqual(early["concurrent_reads"]["descriptors_before"], {"3": "socket:[listener]"})
self.assertNotIn("succeeded", early["concurrent_reads"])
self.assertEqual(context.__exit__.call_count, 2)
self.assertEqual(budget.close.call_count, 2)
def test_cms_requires_every_declared_source_binary(self):
args = SimpleNamespace(suite="cms", _postgres_setting=None,
_cms_sources=("dever", "md"))
self.assertEqual(required_binaries(args), {"cms", "cms-md"})
args._cms_sources = ("dever",)
self.assertEqual(required_binaries(args), {"cms"})
def test_peer_staging_preserves_binary_and_isolates_settings(self):
with tempfile.TemporaryDirectory(prefix="dever-peer-setting-") as temporary:
root = Path(temporary)
source = root / "peer"
source.write_bytes(b"owned executable")
source.chmod(0o700)
for workers in (1, 4):
directory = root / str(workers)
directory.mkdir()
executable = stage_peer(source, directory, {"workers": workers})
self.assertEqual(executable.read_bytes(), source.read_bytes())
self.assertTrue(executable.samefile(source))
self.assertEqual(executable.stat().st_mode & 0o777, 0o700)
setting = directory / "config/setting.json"
self.assertEqual(json.loads(setting.read_text()), {"benchmark": {"workers": workers}})
self.assertEqual(setting.stat().st_mode & 0o777, 0o600)
with self.assertRaises(FileExistsError):
stage_peer(source, directory, {"workers": 8})
self.assertFalse((root / "config").exists())
def test_peer_staging_copies_only_across_filesystems(self):
with tempfile.TemporaryDirectory(prefix="dever-peer-filesystem-") as temporary:
root = Path(temporary)
source = root / "peer"
source.write_bytes(b"owned executable")
source.chmod(0o700)
directory = root / "cross-filesystem"
directory.mkdir()
with patch("settings.os.link", side_effect=OSError(errno.EXDEV, "different device")):
executable = stage_peer(source, directory, {})
self.assertEqual(executable.read_bytes(), source.read_bytes())
self.assertEqual(executable.stat().st_mode & 0o777, 0o700)
self.assertFalse(executable.samefile(source))
denied = root / "denied"
denied.mkdir()
with patch("settings.os.link", side_effect=PermissionError(errno.EACCES, "denied")):
with self.assertRaises(PermissionError):
stage_peer(source, denied, {})
self.assertFalse((denied / "peer").exists())
def test_peer_paths_come_from_project_setting(self):
with tempfile.TemporaryDirectory(prefix="dever-peer-path-") as temporary:
root = Path(temporary)
(root / "config").mkdir()
(root / "peer").write_bytes(b"binary")
setting = root / "config/setting.json"
setting.write_text(json.dumps({"performance": {"network_peer": "peer"}}))
self.assertEqual(configured_peer("network_peer", setting), root / "peer")
for value in ({}, {"performance": []}, {"performance": {"network_peer": 123}}):
setting.write_text(json.dumps(value))
with self.assertRaisesRegex(ValueError, "performance.network_peer"):
configured_peer("network_peer", setting)
def test_process_stages_peer_before_starting_child(self):
with tempfile.TemporaryDirectory(prefix="dever-peer-process-") as temporary:
root = Path(temporary)
command = [sys.executable, "-c", (
"import json, pathlib, sys; "
"print((pathlib.Path(sys.executable).parent / 'config/setting.json').read_text())"
)]
with Process(command, root / "child", peer_settings={"workers": 3}) as process:
self.assertEqual(process.child.wait(timeout=5), 0)
process.check()
self.assertEqual(json.loads(process.text()), {"benchmark": {"workers": 3}})
@staticmethod
def postgres_setting():
return {
"database": {
"postgres_test": {
"type": "postgres",
"url": "postgres://user:secret@localhost/dever_{case}",
"tls": "disabled",
"max_connections": 4,
}
}
}
def test_orm_samples_are_bounded_and_report_percentiles(self):
result = orm_measurements(
"SAMPLE|crud|1|1000000|2\nSAMPLE|crud|1|3000000|2\n",
"crud", 2, 2, 0.5,
)
self.assertEqual(result["operations"], 2)
self.assertEqual(result["throughput_operations_per_second"], 4)
self.assertEqual(result["latency_ms"], {"p50": 1, "p95": 3, "p99": 3})
with self.assertRaises(ValueError):
orm_measurements("SAMPLE|crud|1|1|2\nSAMPLE|crud|1|1|2\n", "crud", 1, 2, 1)
with self.assertRaises(ValueError):
orm_measurements("SAMPLE|list|1|1|2\n", "crud", 1, 2, 1)
with self.assertRaises(ValueError):
orm_measurements("SAMPLE|crud|1|1|1\n", "crud", 1, 2, 1)
def test_orm_checksums_cover_empty_cancel_and_bounded_pages(self):
settings = {"orm_rows": 20}
self.assertEqual(expected_orm_checksum("crud", settings), 2)
self.assertEqual(expected_orm_checksum("list", settings), 40)
self.assertEqual(expected_orm_checksum("cursor", settings), 20)
self.assertEqual(expected_orm_checksum("stream", settings), 20)
self.assertEqual(expected_orm_checksum("pool", settings), 20)
result = transaction_cancel_measurement(0, 0.01)
self.assertEqual(result["raw"][0]["checksum"], 0)
with self.assertRaises(RuntimeError):
transaction_cancel_measurement(1, 0.01)
def test_postgres_cases_require_explicit_isolation_template(self):
setting = self.postgres_setting()
self.assertEqual(
postgres_case_url(setting, "orm_postgres_crud_0"),
"postgres://user:secret@localhost/dever_orm_postgres_crud_0",
)
for value in ("postgres://localhost/shared", "sqlite:///{case}",
"postgres://localhost/shared?application_name={case}"):
setting["database"]["postgres_test"]["url"] = value
with self.assertRaises(ValueError):
postgres_case_url(setting, "case")
def test_postgres_setting_is_loaded_from_the_project_config(self):
with tempfile.TemporaryDirectory(prefix="dever-setting-") as directory:
path = Path(directory) / "setting.json"
path.write_text(json.dumps(self.postgres_setting()))
self.assertEqual(load_postgres_setting(path), self.postgres_setting())
path.write_text(json.dumps({
"database": {
"default": {
"type": "sqlite",
"path": "data/db/dever.db",
"max_connections": 1,
"max_page_size": 100,
}
}
}))
self.assertIsNone(load_postgres_setting(path))
path.write_text(json.dumps({
"database": {"postgres_test": {"type": "sqlite"}}
}))
with self.assertRaises(ValueError):
load_postgres_setting(path)
self.assertIsNone(load_postgres_setting(path.with_name("missing.json")))
def test_profile_settings_cover_each_database_capability(self):
base = profile_database_setting("base")
sqlite = profile_database_setting("sqlite")
postgres = profile_database_setting("postgres")
both = profile_database_setting("both")
self.assertEqual(base, {})
self.assertEqual(tuple(sqlite["database"]), ("default",))
self.assertEqual(sqlite["database"]["default"]["type"], "sqlite")
self.assertEqual(postgres["database"]["default"]["type"], "postgres")
self.assertEqual(postgres["database"]["default"]["url"],
"postgres://build-only.invalid/dever_profile")
self.assertEqual(tuple(both["database"]), ("default", "postgres_profile"))
self.assertEqual(both["database"]["postgres_profile"]["type"], "postgres")
self.assertNotIn("postgres_test", both["database"])
def test_postgres_cancel_fixture_binds_int_as_bigint(self):
fixture = (Path(__file__).parent / "fixtures/orm/app/model/item.dever").read_text()
self.assertIn("generate_series(1::bigint, $1::bigint)", fixture)
def test_dependency_report_is_unique_and_stable(self):
self.assertEqual(
dependency_packages("z v1\na v1\nz v1 (*)\n"),
["a v1", "z v1"],
)
def test_report_omits_private_postgres_configuration(self):
arguments = report_arguments(SimpleNamespace(
_postgres_setting=self.postgres_setting(),
artifacts=Path("artifacts"),
client_workers=4,
client_cpus=(1, 2, 3, 4),
))
self.assertNotIn("_postgres_setting", arguments)
self.assertEqual(arguments["artifacts"], "artifacts")
self.assertEqual(json.loads(json.dumps(arguments))["client_cpus"], [1, 2, 3, 4])
self.assertEqual(arguments["client_workers"], 4)
def test_orm_bundle_uses_a_fresh_external_setting_per_case(self):
settings = {"orm_sqlite_connections": 1, "orm_postgres_connections": 4,
"orm_rows": 128}
with tempfile.TemporaryDirectory(prefix="dever-orm-bundle-") as directory:
root = Path(directory)
artifacts = root / "artifacts"
artifacts.mkdir()
(artifacts / "sqlite-crud").write_bytes(b"binary")
output = root / "output"
output.mkdir()
args = SimpleNamespace(output=output, artifacts=artifacts, _postgres_setting=None)
first = stage_orm_bundle(args, settings, "sqlite", "crud", 0)
second = stage_orm_bundle(args, settings, "sqlite", "crud", 1)
self.assertNotEqual(first[1].parent, second[1].parent)
first_setting = json.loads((first[1].parent / "config/setting.json").read_text())
second_setting = json.loads((second[1].parent / "config/setting.json").read_text())
self.assertEqual(first_setting["database"], second_setting["database"])
self.assertNotEqual(first_setting["auth"], second_setting["auth"])
self.assertEqual(first_setting["database"]["default"]["path"], "data/db/orm.db")
self.assertEqual((first[1].parent / "config/setting.json").stat().st_mode & 0o777,
0o600)
def test_orm_http_bundles_use_isolated_ports_and_private_auth(self):
settings = {"orm_sqlite_connections": 1, "orm_rows": 17}
with tempfile.TemporaryDirectory(prefix="dever-orm-http-bundle-") as directory:
root = Path(directory)
artifacts = root / "artifacts"
artifacts.mkdir()
(artifacts / "sqlite-http").write_bytes(b"binary")
output = root / "output"
output.mkdir()
args = SimpleNamespace(output=output, artifacts=artifacts, _postgres_setting=None)
bundles = [stage_orm_bundle(args, settings, "sqlite", entry, 0,
binary_entry="http" if entry == "cancel" else None)
for entry in ("http", "cancel")]
documents = [json.loads((bundle[1].parent / "config/setting.json").read_text())
for bundle in bundles]
self.assertTrue(all(0 < document["http"]["port"] < 65536
for document in documents))
self.assertTrue(all(document["http"]["host"] == "127.0.0.1"
for document in documents))
self.assertNotEqual(documents[0]["auth"], documents[1]["auth"])
self.assertTrue(all(document["sites"]["benchmark"]["path"] == ""
for document in documents))
self.assertTrue(all((bundle[1].parent / "config/setting.json").stat().st_mode & 0o777
== 0o600 for bundle in bundles))
def test_orm_count_requires_the_current_api_envelope(self):
with patch("run.http.client.HTTPConnection") as connection:
response = connection.return_value.getresponse.return_value
response.status = 200
response.getheader.return_value = "application/json; charset=utf-8"
response.read.return_value = b'{"code":0,"message":"ok","data":17}'
self.assertEqual(orm_count(12345, 0.2), 17)
connection.return_value.request.assert_called_with(
"GET", "/benchmark/item/count", headers={"Connection": "close"}
)
for invalid in (b'{"ok":true}', b'{"code":0,"message":"ok","data":true}',
b'{"code":0,"message":"ok","data":-1}'):
response.read.return_value = invalid
with self.assertRaises(ValueError):
orm_count(12345, 0.2)
def test_postgres_bundle_uses_the_named_project_setting(self):
settings = {"orm_sqlite_connections": 1, "orm_postgres_connections": 4,
"orm_rows": 128}
with tempfile.TemporaryDirectory(prefix="dever-postgres-bundle-") as directory:
root = Path(directory)
artifacts = root / "artifacts"
artifacts.mkdir()
(artifacts / "postgres-crud").write_bytes(b"binary")
output = root / "output"
output.mkdir()
args = SimpleNamespace(
output=output,
artifacts=artifacts,
_postgres_setting=self.postgres_setting(),
)
_, executable, _, _ = stage_orm_bundle(args, settings, "postgres", "crud", 2)
setting_path = executable.parent / "config/setting.json"
setting = json.loads(setting_path.read_text())
self.assertEqual(tuple(setting["database"]), ("default",))
case = postgres_database_case(output, "orm-postgres-crud-2")
self.assertEqual(
setting["database"]["default"]["url"],
f"postgres://user:secret@localhost/dever_{case}",
)
self.assertEqual(setting_path.stat().st_mode & 0o777, 0o600)
def test_postgres_database_names_isolate_separate_reports(self):
first = postgres_database_case(Path("first"), "orm-postgres-crud-0")
second = postgres_database_case(Path("second"), "orm-postgres-crud-0")
self.assertNotEqual(first, second)
self.assertTrue(first.endswith("_orm_postgres_crud_0"))
def test_artifact_verification_rejects_missing_and_changed_files(self):
with tempfile.TemporaryDirectory(prefix="dever-artifacts-") as directory:
root = Path(directory)
binary = root / "app"
certificate = root / "tls/root.pem"
certificate.parent.mkdir()
binary.write_bytes(b"binary")
certificate.write_bytes(b"certificate")
manifest = {
"peer_configuration": PEER_CONFIGURATION,
"application_configuration": APPLICATION_CONFIGURATION,
"binaries": {"app": fingerprint(binary)},
"files": {"tls/root.pem": fingerprint(certificate)},
}
verify_artifacts(root, manifest, {"app"})
with self.assertRaisesRegex(ValueError, "obsolete configuration contract"):
verify_artifacts(root, {**manifest, "peer_configuration": "old"}, {"app"})
with self.assertRaisesRegex(ValueError, "obsolete application contract"):
verify_artifacts(root, {**manifest, "application_configuration": "rust-native"}, {"app"})
with self.assertRaises(ValueError):
verify_artifacts(root, manifest, {"app", "missing"})
certificate.write_bytes(b"changed")
with self.assertRaises(ValueError):
verify_artifacts(root, manifest, {"app"})
def test_server_cleanup_does_not_mask_primary_failure(self):
args = SimpleNamespace(cgroup_parent=None, memory_mib=None, cpu_quota=None,
server_cpu=None)
with tempfile.TemporaryDirectory(prefix="dever-server-cleanup-") as directory:
with patch("run.Budget") as budget_type, patch("run.Process"):
budget_type.return_value.close.side_effect = OSError("cleanup")
with self.assertRaisesRegex(ValueError, "primary") as caught:
with server(args, ["unused"], Path(directory) / "case", {}):
raise ValueError("primary")
self.assertTrue(any("cannot close benchmark budget" in note
for note in caught.exception.__notes__))
def test_orm_summary_keeps_driver_and_operation_separate(self):
def case(driver, entry, latency):
return {"suite": "orm", "driver": driver, "entry": entry,
"binary": {"bytes": 10},
"resources": {"rss_bytes_median": 20, "pss_bytes_median": 15,
"rss_high_water_bytes": 25},
"measurement": {"throughput_operations_per_second": 100,
"latency_ms": {"p50": latency, "p95": latency + 1,
"p99": latency + 2}}}
summary = orm_summary([case("sqlite", "crud", 1), case("postgres", "crud", 2)])
self.assertEqual([(value["driver"], value["entry"]) for value in summary],
[("postgres", "crud"), ("sqlite", "crud")])
self.assertEqual(summary[1]["latency_p99_ms_median"], 3)
def test_cms_summary_reports_business_baseline(self):
cases = [{
"suite": "cms",
"source": "dever",
"binary": {"bytes": 30},
"ready_seconds": 0.5,
"contract": {"articles": 16},
"workload_seconds": 0.25,
"published_articles_per_second": 64,
"resources": {"rss_bytes_median": 20, "pss_bytes_median": 15,
"rss_high_water_bytes": 25},
"cgroup": {"peak_bytes": 40},
}]
self.assertEqual(
cms_summary(cases),
{"dever": {"repeats": 1, "workload_mode": "sequential_publish",
"binary_bytes": 30, "articles": 16,
"ready_seconds_median": 0.5, "workload_seconds_median": 0.25,
"published_articles_per_second_median": 64,
"rss_bytes_median": 20,
"pss_bytes_median": 15, "rss_high_water_bytes_median": 25,
"cgroup_peak_bytes_median": 40}},
)
def test_cms_summary_separates_source_variants(self):
cases = [
{"suite": "cms", "source": source, "binary": {"bytes": size},
"ready_seconds": 0.5, "contract": {"articles": 16},
"workload_seconds": 0.25, "published_articles_per_second": 64,
"resources": {"rss_bytes_median": 20, "pss_bytes_median": 15,
"rss_high_water_bytes": 25}, "cgroup": None}
for source, size in (("dever", 30), ("md", 31))
]
summary = cms_summary(cases)
self.assertEqual(summary["dever"]["binary_bytes"], 30)
self.assertEqual(summary["md"]["binary_bytes"], 31)
def test_live_lifecycle_includes_all_three_phase_deadlines(self):
self.assertAlmostEqual(lifecycle_seconds(2, 0.3, 2000), 23.2)
def test_live_phase_order_and_partial_writes(self):
text = "PHASE|0|connecting\nPHASE|0|connected\nPHASE|0|clos"
self.assertEqual(parse_progress(text, 1), (phase_sequence(1)[:2], None))
with self.assertRaises(ValueError):
parse_progress("PHASE|0|connected\n", 1)
with self.assertRaises(ValueError):
parse_progress("PHASE|0|connecting\nPHASE|0|connecting\n", 1)
def test_live_sample_is_only_attributed_to_a_stable_phase(self):
connecting = phase_sequence(1)[:1]
connected = phase_sequence(1)[:2]
self.assertEqual(stable_phase(connected, connected), (0, "connected"))
self.assertIsNone(stable_phase(connecting, connected))
self.assertIsNone(stable_phase([], connecting))
def test_live_incomplete_cleanup_cannot_pass(self):
result = {"protocol": "tcp", "connections": 4, "cycles": 3,
"attempted_connections": 12, "connected": 12, "disconnected": 12,
"messages": 24, "errors": 0, "elapsed_ms": 3000}
validate_result(result, "tcp", 4, 3)
for key, value in (("disconnected", 11), ("errors", 1), ("messages", 0)):
with self.assertRaises(ValueError):
validate_result({**result, key: value}, "tcp", 4, 3)
def test_missing_cgroup_controller_still_removes_owned_empty_group(self):
with tempfile.TemporaryDirectory(prefix="dever-perf-test-") as directory:
budget = Budget(Path(directory), None, None)
owned = budget.path
with self.assertRaises(FileNotFoundError):
budget.close()
self.assertFalse(owned.exists())
def test_dropped_and_failed_requests_cannot_disappear_from_report(self):
result = {"scheduled": 10, "sent": 4, "completed": 4, "succeeded": 0,
"errors": 4, "dropped": 6, "dropped_capacity": 5, "dispatch_expired": 1,
"latency_samples": 0, "request_latency_samples": 0, "dispatch_lag_samples": 0,
"latency_ms": None, "request_latency_ms": None, "dispatch_lag_ms": None,
"elapsed_seconds": 1, "actual_success_qps": 0, "http_version": "h2",
"physical_connections": 1, "streams_per_connection": 16,
"request_slots": 16, "successful_handshakes": 1}
validate_load(result)
with self.assertRaises(ValueError):
validate_load({**result, "errors": 3})
with self.assertRaises(ValueError):
validate_load({**result, "latency_ms": {"p50": 0, "p95": 0, "p99": 0}})
with self.assertRaises(ValueError):
validate_load({**result, "dispatch_lag_samples": 1,
"dispatch_lag_ms": {"sample_count": 2, "p50": 0,
"p95": 0, "p99": 0}})
with self.assertRaises(ValueError):
validate_load({**result, "request_slots": 15})
with self.assertRaises(ValueError):
validate_load({**result, "successful_handshakes": 2})
def test_http2_run_fails_on_request_errors_or_oom(self):
require_no_request_errors({"errors": 0}, "load")
with self.assertRaises(RuntimeError):
require_no_request_errors({"errors": 1}, "load")
require_no_oom(None)
require_no_oom({"events": {"oom": 0, "oom_kill": 0, "oom_group_kill": 0}})
with self.assertRaises(RuntimeError):
require_no_oom({"events": {"oom": 1, "oom_kill": 0, "oom_group_kill": 0}})
def test_orm_http_requires_at_least_one_success_without_a_qps_threshold(self):
require_successful_requests({"succeeded": 1}, "ORM HTTP load")
with self.assertRaises(RuntimeError):
require_successful_requests({"succeeded": 0}, "ORM HTTP load")
def test_http2_summary_keeps_topologies_separate(self):
def case(connections, repeat, qps, p99, rss):
return {"suite": "http2", "implementation": "dever", "transport": "http",
"path": "/plain", "rate": 5000.0, "physical_connections": connections,
"streams_per_connection": 16, "repeat": repeat,
"load": {"actual_success_qps": qps, "dropped": 0,
"request_latency_ms": {"p99": p99}},
"resources": {"rss_bytes_median": rss, "pss_bytes_median": rss - 10,
"rss_high_water_bytes": rss + 10}}
summary = http2_summary([case(1, 0, 90, 2, 100), case(1, 1, 110, 4, 120),
case(4, 0, 200, 1, 140)])
self.assertEqual(len(summary), 2)
self.assertEqual(summary[0]["physical_connections"], 1)
self.assertEqual(summary[0]["actual_success_qps_median"], 100)
self.assertEqual(summary[0]["request_latency_p99_ms_median"], 3)
self.assertEqual(summary[0]["server_rss_bytes_median"], 110)
self.assertEqual(summary[1]["physical_connections"], 4)
def test_warmup_and_checksum_are_checked_before_aggregation(self):
lines = ["SAMPLE|tasks|4|400000|480"]
lines += [f"SAMPLE|tasks|4|{nanos}|480" for nanos in range(40, 76, 4)]
result = async_measurements("\n".join(lines), {"iterations": 4, "pending": 2})
self.assertEqual(result["median_ns_per_operation"], 14)
self.assertEqual(len(result["raw"]), 10)
with self.assertRaises(ValueError):
async_measurements("\n".join(lines).replace("|480", "|479"), {"iterations": 4})
with self.assertRaises(ValueError):
async_measurements("\n".join(lines[:-1]), {"iterations": 4})
def test_cancel_reports_full_group_drain_and_has_no_fake_warmup(self):
result = async_measurements("SAMPLE|cancel|32|32000|32", {"pending": 32})
self.assertFalse(result["warmup_samples"])
self.assertEqual(result["median_ns_per_operation"], 1000)
def test_percentile_uses_nearest_rank(self):
self.assertEqual(percentile([9, 2, 5, 1], 0.5), 2)
self.assertEqual(percentile([9, 2, 5, 1], 0.99), 9)
with self.assertRaises(argparse.ArgumentTypeError):
positive("nan")
def test_http2_matrix_lists_reject_empty_duplicate_and_unknown_values(self):
self.assertEqual(csv_positive_ints("1,4", "connections"), (1, 4))
self.assertEqual(parse_implementations("dever,hyper"), ("dever", "hyper"))
for value in ("", "1,", "1,1"):
with self.assertRaises(ValueError):
csv_positive_ints(value, "connections")
for value in ("", "dever,", "dever,dever", "dever,other"):
with self.assertRaises(ValueError):
parse_implementations(value)
def test_client_cpu_configuration_rejects_invalid_sets_and_placement(self):
self.assertEqual(parse_client_cpus("0,1,2"), (0, 1, 2))
for value in ("", "1,", "1,,2", "1,1", "-1", "other"):
with self.subTest(value=value), self.assertRaises(ValueError):
parse_client_cpus(value)
args = parser().parse_args(["run", "--artifacts", "artifacts", "--output", "output",
"--client-workers", "4", "--client-cpus", "1,2,3,4"])
self.assertEqual((args.client_workers, args.client_cpus), (4, (1, 2, 3, 4)))
with self.assertRaises(ValueError):
validate_cpu_placement(0, (1, 5), {0, 1, 2, 3, 4})
with self.assertRaises(ValueError):
validate_cpu_placement(0, (0, 1), {0, 1, 2, 3, 4})
with self.assertRaises(ValueError):
validate_cpu_placement(-1, (1,), {0, 1})
validate_cpu_placement(0, (1, 2), {0, 1, 2})
def test_process_affinity_is_applied_only_to_owned_child(self):
allowed = os.sched_getaffinity(0)
cpus = tuple(sorted(allowed)[:2])
with tempfile.TemporaryDirectory(prefix="dever-perf-test-") as directory:
command = [sys.executable, "-c",
"import os; print(sorted(os.sched_getaffinity(0)), flush=True)"]
with Process(command, Path(directory) / "affinity", cpus=cpus) as process:
self.assertEqual(process.child.wait(timeout=5), 0)
process.check()
self.assertEqual(json.loads(process.text()), list(cpus))
self.assertEqual(os.sched_getaffinity(0), allowed)
def test_http_body_size_metadata_matches_fixtures(self):
self.assertEqual(HTTP_BODY_BYTES, {"/plain": 5, "/json": 11, "/bytes": 65_536})
def test_live_process_sampling_and_owned_cleanup(self):
with tempfile.TemporaryDirectory(prefix="dever-perf-test-") as directory:
command = [sys.executable, "-c", "import time; print('READY|0', flush=True); time.sleep(10)"]
with Process(command, Path(directory) / "process") as process:
self.assertEqual(process.ready(), 0)
observe([process], 0.04, 0.01)
result = summarize(process.samples)
self.assertGreater(result["rss_bytes_median"], 0)
self.assertGreater(result["pss_bytes_median"], 0)
self.assertGreaterEqual(result["threads_median"], 1)
self.assertGreaterEqual(result["fds_median"], 3)
self.assertIsNotNone(process.child.poll())
def test_early_exit_does_not_become_successful_readiness(self):
with tempfile.TemporaryDirectory(prefix="dever-perf-test-") as directory:
with Process([sys.executable, "-c", "raise SystemExit(7)"], Path(directory) / "failed") as process:
with self.assertRaises(RuntimeError):
process.ready()
def test_memory_teardown_can_precede_waitpid_exit(self):
with tempfile.TemporaryDirectory(prefix="dever-perf-test-") as directory:
command = [sys.executable, "-c", "import time; print('READY|0', flush=True); time.sleep(10)"]
with Process(command, Path(directory) / "teardown") as process:
process.ready()
count = len(process.samples)
with patch("process.process_sample", side_effect=ProcessLookupError):
process.sample()
self.assertEqual(len(process.samples), count)
self.assertIsNone(process.child.poll())
if __name__ == "__main__":
unittest.main()