Initial Dever 0.0.1

This commit is contained in:
Codex committed 2026-11-03 03:32:07 +00:00
commit 81233d098c
1479 files changed
+163782

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
# 性能测试
主入口是 `test/rust/performance.rs`,比较真实 Dever 生成程序和 Rust 实现。Go 与 Web 框架对照使用本目录的 Python 驱动。先测改动涉及的功能,完整矩阵用于阶段检查。
## 运行
在仓库的 `rust/` 目录执行,使用锁定工具链:
```sh
cargo +1.95.0 bench --locked --offline --bench performance -- --storage
cargo +1.95.0 bench --locked --offline --bench performance -- --map-values
cargo +1.95.0 bench --locked --offline --bench performance -- --json
```
| 参数 | 工作负载 |
| --- | --- |
| 不传参数 | 基础内核 |
| `--storage`、`--map-values` | Map/List 的构造、查找、借用和筛选 |
| `--collections`、`--handlers`、`--text`、`--bytes` | 集合处理、函数传递、文本和字节 |
| `--numeric`、`--decimal-ops`、`--decimal-sum` | 数值与十进制计算 |
| `--application`、`--query` | CMD、JSON 输入输出和 query 解码 |
| `--json`、`--json-document` | JSON 与动态文档 |
| `--resources`、`--tasks` | 文件、流、任务调度和取消 |
| `--database`、`--database-postgres` | 数据库操作,PostgreSQL 需独立测试配置 |
| `--websocket-protocol` | 独立协议对端的消息传输 |
| `--websocket` | 共享运行库的 WebSocket 生成适配 |
其他诊断开关见主入口及 `performance/extended/`、`performance/transport/`。比较程序保存在 `rust/target/programs/release/`,使用 `--build-only` 可只构建,再单独运行生成的程序。
Python 驱动从仓库根使用,先查看参数:
```sh
python3 test/performance/kernel_go.py --help
python3 test/performance/http_go.py --help
python3 test/performance/cms.py --help
```
这些测试可能编译程序、启动回环服务或访问测试数据库;地址和工具路径使用现有测试配置,不能指向业务数据。
## 公平比较
- 使用相同语义、输入、精度、依赖、编译选项和资源限制。Dever 的错误处理、复制和任务清理,参照也须完成。
- 绑定可用 CPU,避免同时构建或跑其他基准。记录频率、节流和负载,保留正向与反向执行顺序。
- 提前固定轮次和预热次数,保留全部样本、绝对耗时、失败次数及源码/二进制摘要,不测到通过才停。
- 独立 Rust 参照用于比较实现;同表、同函数或共享运行库仅用于诊断生成层与测量波动。
- 报告修改前后及相对参照的变化。一次快结果不能代表稳定提速,一个样例不能代表整门语言。
## 网络与内存
网络结果须同时检查响应内容、错误、丢弃请求和实际测量时长,不能只看 QPS。客户端与服务端分别限制 CPU;并发数相同不等于负载相同。
`--memory` 构建分配诊断产物,探针位于 `test/rust/performance/memory.c`。分配次数、存活字节、RSS/PSS 和 cgroup 内存分别报告;插桩结果不作为普通性能耗时。
## 保存结果
原始数据放在忽略的 `target/performance/` 或测试输出目录,与源码提交和编译器摘要对应。README 只维护测试方法,不追加逐轮成绩和修改日志。
+292
View File
@@ -0,0 +1,292 @@
"""Measure real typed/raw JSON API requests with isolated native applications."""
import argparse
import http.client
import json
import os
from pathlib import Path
import platform
import secrets
import shutil
import signal
import socket
import statistics
import subprocess
import tempfile
import time
from build import fingerprint
from process import process_sample
from settings import write_private_json
FIXTURE = Path(__file__).parent / "fixtures/api-json"
def configuration(port):
return {
"http": {"host": "127.0.0.1", "port": port},
"runtime": {"mode": "api", "shutdown_ms": 2000},
"log": {"level": "error"},
"database": {"default": {"type": "sqlite", "path": "data/control.db",
"max_connections": 2}},
"auth": {"providers": {"session": {
"verify": "benchmark.auth.verify", "jwtSecret": secrets.token_hex(32),
"cookie": "benchmark_session"}}},
"sites": {"test": {"path": "", "auth": "session", "hosts": ["localhost"],
"origin": "https://localhost"}},
}
def build(arguments):
output = arguments.output.resolve()
output.mkdir(parents=True, exist_ok=False)
project = output / "project"
shutil.copytree(FIXTURE / "benchmark", project / "module/benchmark")
(project / "config").mkdir()
write_private_json(project / "config/setting.json", configuration(8080))
artifacts = {}
for label, compiler in (("before", arguments.baseline_compiler),
("after", arguments.compiler)):
compiler = compiler.resolve()
executable = output / label
started = time.monotonic()
with (output / f"{label}.build.log").open("x") as log:
subprocess.run([compiler, "build", project, "--output", executable],
stdout=log, stderr=subprocess.STDOUT, check=True, timeout=900)
artifacts[label] = {"path": str(executable), **fingerprint(executable),
"compiler": {"path": str(compiler), **fingerprint(compiler)},
"build_seconds": time.monotonic() - started,
"runtime_manifests": {
str(path.relative_to(compiler.parent)): fingerprint(path)
for path in sorted((compiler.parent / "runtime").glob("*/manifest.json"))}}
if arguments.with_peers:
go_binary = output / "go"
subprocess.run(["go", "build", "-trimpath", "-ldflags=-s -w", "-o", go_binary,
FIXTURE.resolve() / "peer.go"], check=True, timeout=180)
artifacts["go"] = {"path": str(go_binary), **fingerprint(go_binary),
"compiler": subprocess.check_output(["go", "version"], text=True).strip(),
"source": fingerprint(FIXTURE / "peer.go"),
"scope": "native codec/transport baseline; not full Dever API semantics"}
built = subprocess.run(["cargo", "build", "--offline", "--locked", "--release",
"-p", "dever-tests", "--example", "api-json-peer",
"--message-format=json"], capture_output=True, text=True,
timeout=600)
(output / "rust.build.log").write_text(built.stderr)
built.check_returncode()
executables = [message["executable"] for line in built.stdout.splitlines()
if (message := json.loads(line)).get("reason") == "compiler-artifact"
and message.get("executable") and message["target"]["name"] == "api-json-peer"]
if len(executables) != 1:
raise RuntimeError("expected one native Rust API peer")
rust_binary = output / "rust"
shutil.copy2(executables[0], rust_binary)
artifacts["rust"] = {"path": str(rust_binary), **fingerprint(rust_binary),
"compiler": subprocess.check_output(["rustc", "--version"], text=True).strip(),
"source": fingerprint(FIXTURE / "peer.rs"),
"profile": "release: opt-level=3, fat LTO, codegen-units=1",
"scope": "native codec/transport baseline; not full Dever API semantics"}
if arguments.with_runtime_peer:
runtime = arguments.compiler.resolve().parent / "runtime/linux-x86_64"
if not (runtime / "libc.a").is_file() or not (runtime / "crt1.o").is_file():
raise ValueError("Rust runtime peer requires the compiler's Linux x86_64 runtime inputs")
command = ["cargo", "rustc", "--offline", "--locked", "--profile", "runtime-pack",
"-p", "dever-runtime", "--features", "api,crypto,external,sqlite",
"--example", "api-json-runtime-peer", "--message-format=json", "--",
"-C", "target-feature=+crt-static", "-C", "relocation-model=static",
"-C", f"link-arg=-B{runtime}/", "-C", f"link-arg=-L{runtime}/",
"-C", f"link-arg=-Wl,-Map,{output / 'rust-runtime.link.map'}"]
built = subprocess.run(command, capture_output=True, text=True, timeout=600)
(output / "rust-runtime.build.log").write_text(built.stderr)
built.check_returncode()
executables = [message["executable"] for line in built.stdout.splitlines()
if (message := json.loads(line)).get("reason") == "compiler-artifact"
and message.get("executable") and message["target"]["name"] == "api-json-runtime-peer"]
if len(executables) != 1:
raise RuntimeError("expected one Rust runtime API peer")
runtime_binary = output / "rust-runtime"
shutil.copy2(executables[0], runtime_binary)
artifacts["rust_runtime"] = {
"path": str(runtime_binary), **fingerprint(runtime_binary),
"compiler": subprocess.check_output(["rustc", "--version"], text=True).strip(),
"source": fingerprint(FIXTURE / "runtime-peer.rs"), "command": command,
"runtime_inputs": {name: fingerprint(runtime / name) for name in
["crt1.o", "crti.o", "crtbeginT.o", "crtend.o", "crtn.o", "libc.a", "libm.a", "libgcc.a", "libgcc_eh.a"]},
"profile": "runtime-pack: opt-level=3, fat LTO, static executable, codegen-units=1",
"scope": "shared Dever runtime contract with native Rust values and codecs; not an independent runtime",
}
manifest = {
"artifacts": artifacts,
"sources": {str(path.relative_to(FIXTURE)): fingerprint(path)
for path in sorted(FIXTURE.rglob("*.dever"))},
}
write_private_json(output / "manifest.json", manifest)
print(output / "manifest.json", flush=True)
def request(client, route, body, expected):
started = time.perf_counter_ns()
client.request("POST", f"/benchmark/echo/{route}", body,
{"Host": "localhost", "Content-Type": "application/json"})
response = client.getresponse()
encoded = response.read()
elapsed = time.perf_counter_ns() - started
if response.status != 200 or json.loads(encoded) != {
"code": 0, "message": "ok", "data": expected}:
raise AssertionError(f"API response mismatch: HTTP {response.status}")
return elapsed
def percentile(values, fraction):
return sorted(values)[min(len(values) - 1, int(len(values) * fraction))] / 1000
def measure(artifact, route, payload, arguments):
expected = payload["payload" if route == "echo" else "source"]
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode()
with socket.socket() as listener:
listener.bind(("127.0.0.1", 0))
port = listener.getsockname()[1]
with tempfile.TemporaryDirectory(prefix="api-json-", dir=arguments.output) as temporary:
directory = Path(temporary)
executable = directory / "app"
shutil.copy2(artifact["path"], executable)
(directory / "config").mkdir()
write_private_json(directory / "config/setting.json", configuration(port))
with (directory / "stdout").open("w+") as stdout, \
(directory / "stderr").open("w+") as stderr:
process = subprocess.Popen(["taskset", "-c", str(arguments.server_cpu), executable],
cwd=directory, stdout=stdout, stderr=stderr,
start_new_session=True)
client = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
try:
deadline = time.monotonic() + 20
while True:
if process.poll() is not None:
raise RuntimeError(f"API exited before readiness: {process.returncode}")
try:
request(client, route, body, expected)
break
except (ConnectionError, http.client.HTTPException):
client.close()
if time.monotonic() >= deadline:
raise TimeoutError("API readiness timeout")
time.sleep(0.02)
for _ in range(50):
request(client, route, body, expected)
before = process_sample(process.pid)
started = time.monotonic()
latencies = [request(client, route, body, expected)
for _ in range(arguments.requests)]
elapsed = time.monotonic() - started
after = process_sample(process.pid)
result = {
"requests": len(latencies), "request_bytes": len(body),
"elapsed_seconds": elapsed,
"p50_us": percentile(latencies, 0.5),
"p95_us": percentile(latencies, 0.95),
"p99_us": percentile(latencies, 0.99),
"server_cpu_us_per_request":
(after["cpu_seconds"] - before["cpu_seconds"]) * 1e6 / len(latencies),
"rss_bytes": after["rss_bytes"],
"rss_growth_bytes": after["rss_bytes"] - before["rss_bytes"],
}
except BaseException as error:
stderr.flush()
stderr.seek(0)
error.add_note(stderr.read()[-4000:])
raise
finally:
client.close()
if process.poll() is None:
os.killpg(process.pid, signal.SIGTERM)
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL)
process.wait(timeout=5)
raise TimeoutError("API did not drain after SIGTERM")
if process.returncode:
raise RuntimeError(f"API shutdown failed: {process.returncode}")
return result
def run(arguments):
allowed = os.sched_getaffinity(0)
if (arguments.server_cpu == arguments.client_cpu or
not {arguments.server_cpu, arguments.client_cpu} <= allowed):
raise ValueError("server and client require distinct allowed CPUs")
if arguments.requests < 1 or arguments.repeats < 1:
raise ValueError("requests and repeats must be positive")
manifest = json.loads(arguments.manifest.read_text())
for artifact in manifest["artifacts"].values():
if fingerprint(Path(artifact["path"])) != {
key: artifact[key] for key in ("bytes", "sha256")}:
raise ValueError("API benchmark binary changed after build")
arguments.output.mkdir(parents=True, exist_ok=False)
os.sched_setaffinity(0, {arguments.client_cpu})
labels = list(manifest["artifacts"])
report = {"manifest": manifest, "server_cpu": arguments.server_cpu,
"client_cpu": arguments.client_cpu, "samples": [], "summary": [],
"environment": {"kernel": platform.release(), "machine": platform.machine(),
"allowed_cpus": sorted(allowed), "python": platform.python_version()},
"runner": fingerprint(Path(__file__)),
"requests_per_process": arguments.requests, "repeats": arguments.repeats}
try:
for count in (1, 16, 128):
row = {"id": 9007199254740993, "text": "雪😀\n\"" + "x" * 80,
"weight": 1.25, "note": None}
value = {"rows": [row] * count, "raw": {"large": 123456789012345678901234567890}}
for route, argument in (("echo", "payload"), ("raw", "source")):
for repeat in range(arguments.repeats):
offset = repeat % len(labels)
order = labels[offset:] + labels[:offset]
for label in order:
sample = measure(manifest["artifacts"][label], route,
{argument: value}, arguments)
report["samples"].append({"label": label, "route": route,
"rows": count, "repeat": repeat, **sample})
for label in labels:
samples = [row for row in report["samples"]
if row["label"] == label and row["route"] == route
and row["rows"] == count]
summary = {"label": label, "route": route, "rows": count}
for metric in ("p50_us", "p95_us", "p99_us",
"server_cpu_us_per_request", "rss_bytes"):
summary[metric] = statistics.median(sample[metric] for sample in samples)
report["summary"].append(summary)
print(json.dumps(summary), flush=True)
for artifact in manifest["artifacts"].values():
if fingerprint(Path(artifact["path"])) != {
key: artifact[key] for key in ("bytes", "sha256")}:
raise ValueError("API benchmark binary changed during measurement")
report["passed"] = True
finally:
write_private_json(arguments.output / "report.json", report)
def main():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
prepare = commands.add_parser("build")
prepare.add_argument("--compiler", type=Path, required=True)
prepare.add_argument("--baseline-compiler", type=Path, required=True)
prepare.add_argument("--output", type=Path, required=True)
prepare.add_argument("--with-peers", action="store_true")
prepare.add_argument("--with-runtime-peer", action="store_true")
prepare.set_defaults(action=build)
measure = commands.add_parser("run")
measure.add_argument("--manifest", type=Path, required=True)
measure.add_argument("--output", type=Path, required=True)
measure.add_argument("--server-cpu", type=int, required=True)
measure.add_argument("--client-cpu", type=int, required=True)
measure.add_argument("--requests", type=int, default=1000)
measure.add_argument("--repeats", type=int, default=7)
measure.set_defaults(action=run)
arguments = parser.parse_args()
arguments.output = arguments.output.resolve()
arguments.action(arguments)
if __name__ == "__main__":
main()
+174
View File
@@ -0,0 +1,174 @@
"""Verify the API JSON fixture's observable contract before runtime comparison."""
import argparse
import base64
from contextlib import ExitStack, contextmanager
from copy import deepcopy
import hashlib
import hmac
import http.client
import json
from pathlib import Path
import shutil
import signal
import socket
import subprocess
import tempfile
import time
from api_json import configuration, request, write_private_json
from build import fingerprint
@contextmanager
def server(binary, output, settings):
with tempfile.TemporaryDirectory(prefix="api-contract-", dir=output) as temporary:
directory = Path(temporary)
executable = directory / "app"
shutil.copy2(binary, executable)
with socket.socket() as listener:
listener.bind(("127.0.0.1", 0))
port = listener.getsockname()[1]
settings = deepcopy(settings)
settings["http"]["port"] = port
(directory / "config").mkdir()
write_private_json(directory / "config/setting.json", settings)
with (directory / "stdout").open("w+") as stdout, (directory / "stderr").open("w+") as stderr:
process = subprocess.Popen([executable], cwd=directory, stdout=stdout,
stderr=stderr, start_new_session=True)
client = http.client.HTTPConnection("127.0.0.1", port, timeout=20)
try:
deadline = time.monotonic() + 30
while True:
if process.poll() is not None:
raise RuntimeError(f"API contract server exited: {process.returncode}")
try:
request(client, "raw", b'{"source":0}', 0)
break
except (ConnectionError, http.client.HTTPException):
client.close()
if time.monotonic() >= deadline:
raise TimeoutError("API contract server readiness timeout")
time.sleep(0.05)
yield client
finally:
client.close()
if process.poll() is None:
process.send_signal(signal.SIGTERM)
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=5)
raise
if process.returncode:
raise RuntimeError(f"API contract server shutdown failed: {process.returncode}")
def credential(settings):
def encode(value):
return base64.urlsafe_b64encode(json.dumps(value, separators=(",", ":")).encode()).rstrip(b"=")
now = int(time.time())
payload = encode({"alg": "HS256", "typ": "JWT"}) + b"." + encode({
"iss": "session", "sub": "fixture", "sid": "fixture-session", "aud": "test",
"tenant": None, "iat": now, "exp": now + 600,
})
key = settings["auth"]["providers"]["session"]["jwtSecret"].encode()
signature = base64.urlsafe_b64encode(hmac.digest(key, payload, "sha256")).rstrip(b"=")
return (payload + b"." + signature).decode()
def cases(settings):
result = []
row = '{"id":9007199254740993,"text":"雪😀\\n\\\"","weight":1.25,"note":null}'
typed = '{"payload":{"rows":[' + row + '],"raw":{"large":123456789012345678901234567890}}}'
def add(name, body, status=200, route="raw", headers=None, method="POST", query=""):
result.append({"name": name, "body": body.encode() if isinstance(body, str) else body,
"status": status, "path": "/benchmark/echo/" + route + query,
"headers": headers or [("Host", "localhost"), ("Content-Type", "application/json")],
"method": method})
add("typed-unicode-exact-number", typed, route="echo")
add("typed-optional-absent", typed.replace(',"note":null', ""), route="echo")
add("typed-optional-text", typed.replace('"note":null', '"note":"雪"'), route="echo")
for spelling in ["-0", "-9223372036854775808", "9223372036854775807"]:
add("typed-int-" + spelling, typed.replace("9007199254740993", spelling), route="echo")
for spelling in ["1.0", "1e0", "9223372036854775808", "-9223372036854775809", "true"]:
add("typed-invalid-int-" + spelling, typed.replace("9007199254740993", spelling), 400, "echo")
add("typed-nonfinite", typed.replace("1.25", "1e999"), 400, "echo")
add("typed-unknown-field", typed.replace('"weight":', '"extra":0,"weight":'), 400, "echo")
add("typed-missing-field", typed.replace('"weight":1.25,', ""), 400, "echo")
add("typed-duplicate-escaped-field", typed.replace('"weight":', '"we\\u0069ght":0,"weight":'), 400, "echo")
add("typed-invalid-optional", typed.replace('"note":null', '"note":false'), 400, "echo")
for name, value in [("number", "1e999"), ("null", "null"), ("array", "[1,true,null]"),
("unicode", '"\\ud83d\\ude00"'), ("object", '{"large":123456789012345678901234567890}')]:
add("raw-" + name, '{"source":' + value + '}')
for name, body in [("duplicate", '{"source":{"key":1,"k\\u0065y":2}}'),
("surrogate", '{"source":"\\ud800"}'),
("trailing-comma", '{"source":[0,]}'),
("root-duplicate", '{"source":0,"source":1}'),
("root-unknown", '{"source":0,"extra":1}'),
("root-missing", '{}'), ("trailing-root", '{"source":0}[]'),
("invalid-utf8", b'{"source":"\xff"}')]:
add(name, body, 400)
for depth, status in [(63, 200), (64, 400)]:
add(f"depth-{depth}", '{"source":' + '[' * depth + '0' + ']' * depth + '}', status)
for count, status in [(65534, 200), (65535, 400)]:
add(f"elements-{count}", '{"source":[' + ','.join(['0'] * count) + ']}', status)
add("wrong-method", '{}', 405, method="GET")
add("unknown-route", '{}', 404, route="missing")
add("query-on-post", '{"source":0}', 400, query="?value=1")
add("wrong-content-type", '{}', 400, headers=[("Host", "localhost"), ("Content-Type", "text/plain")])
add("duplicate-content-type", '{}', 400, headers=[("Host", "localhost"), ("Content-Type", "application/json"), ("Content-Type", "application/json")])
add("wrong-host", '{}', 401, headers=[("Host", "other.invalid"), ("Content-Type", "application/json")])
add("invalid-bearer", '{}', 401, headers=[("Host", "localhost"), ("Content-Type", "application/json"), ("Authorization", "Bearer invalid")])
add("valid-bearer", typed, route="echo", headers=[("Host", "localhost"), ("Content-Type", "application/json"), ("Authorization", "Bearer " + credential(settings))])
add("public-expired-cookie", '{"source":0}', headers=[("Host", "localhost"), ("Content-Type", "application/json"), ("Cookie", "benchmark_session=invalid")])
return result
def verify(dever, rust, output):
output.mkdir(parents=True, exist_ok=False)
artifacts = {label: {"path": str(binary.resolve()), **fingerprint(binary)}
for label, binary in [("dever", dever), ("rust_runtime", rust)]}
settings = configuration(8080)
report = {"artifacts": artifacts, "cases": [], "passed": False}
try:
with ExitStack() as stack:
clients = {label: stack.enter_context(server(Path(artifact["path"]), output, settings))
for label, artifact in artifacts.items()}
for case in cases(settings):
observed = {}
for label, client in clients.items():
client.putrequest(case["method"], case["path"], skip_host=True, skip_accept_encoding=True)
for name, value in case["headers"]:
client.putheader(name, value)
client.putheader("Content-Length", str(len(case["body"])))
client.endheaders(case["body"])
response = client.getresponse()
body = response.read()
observed[label] = (response.status, response.getheader("Content-Type"), response.getheader("Allow"), body)
client.close()
passed = all(value[0] == case["status"] for value in observed.values()) and len(set(observed.values())) == 1
report["cases"].append({"name": case["name"], "expected_status": case["status"], "passed": passed,
"observed": {label: {"status": value[0], "body_sha256": hashlib.sha256(value[3]).hexdigest(), "bytes": len(value[3])}
for label, value in observed.items()}})
if not passed:
raise AssertionError(f"API contract differs for {case['name']}: " + repr({label: (value[0], value[3][:240]) for label, value in observed.items()}))
for artifact in artifacts.values():
assert fingerprint(Path(artifact["path"])) == {key: artifact[key] for key in ["bytes", "sha256"]}
report["passed"] = True
finally:
write_private_json(output / "report.json", report)
print(json.dumps({"passed": True, "cases": len(report["cases"])}), flush=True)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--dever", type=Path, required=True)
parser.add_argument("--rust", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
arguments = parser.parse_args()
verify(arguments.dever, arguments.rust, arguments.output)
+478
View File
@@ -0,0 +1,478 @@
"""构建可搬运的基准产物;构建耗时与运行测量分开。"""
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", "idleSampleMs", "pending", "channelCapacity", "input", "pendingInput"),
"http": ("lifetime", "limits", "http2Limits", "largeBody", "certificate", "key"),
"live": (),
"orm": ("lifetime", "ormInput", "ormRows", "ormConcurrency", "ormCancelWork"),
}
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"])),
"idleSampleMs": ("Int", str(settings["idle_sample_ms"])),
"pending": ("Int", str(settings["pending"])),
"channelCapacity": ("Int", str(settings["channel_capacity"])),
"input": ("Bytes", f'dever.bytes.fromText({json.dumps("x" * settings["iterations"])})'),
"pendingInput": ("Bytes", f'dever.bytes.fromText({json.dumps("x" * settings["pending"])})'),
"ormInput": ("Bytes", f'dever.bytes.fromText({json.dumps("x" * settings["orm_iterations"])})'),
"ormRows": ("Bytes", f'dever.bytes.fromText({json.dumps("r" * settings["orm_rows"])})'),
"ormConcurrency": ("Int", str(settings["orm_concurrency"])),
"ormCancelWork": ("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}}'),
"http2Limits": ("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.fromText({json.dumps((TLS / "server.pem").read_text())})')
if "key" in names:
functions["key"] = ("Bytes", f'dever.bytes.fromText({json.dumps((TLS / "server-key.pem").read_text())})')
source = ""
for name in names:
if name == "largeBody":
continue
kind, expression = functions[name]
source += f"\n{name}() (result: {kind}) {{\nresult = {expression}\n}}\n"
if "largeBody" in names:
source += '\ndouble(value: Int, text: Text) (result: Text) { result = text + text }\n'
source += '\nlargeBody() (result: Bytes) {\nresult = dever.bytes.fromText(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.ormIdle")
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):
has_protocols = args.selection in ("all", "protocols")
if args.with_go and not has_protocols:
raise ValueError("--with-go requires protocol fixtures")
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)
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; workers also sets Go GOMAXPROCS when selected",
"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()
method = "ormSeed" if entry == "orm_http" else "orm" + entry.removeprefix("orm_").capitalize()
command_entry = f"benchmark.item.{method}"
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",),
"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"
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)
if args.with_go:
binary = destination / "go_http"
source = FIXTURES / "http/peer.go"
with (destination / "go.build.log").open("w") as log:
subprocess.run(["go", "build", "-trimpath", "-ldflags=-s -w", "-o", binary, source],
stdout=log, stderr=log, check=True, timeout=120)
manifest["binaries"]["go_http"] = fingerprint(binary)
manifest["go"] = {"compiler": subprocess.check_output(["go", "version"], text=True).strip(),
"source": fingerprint(source), "profile": "go build -trimpath -ldflags=-s -w"}
(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",)
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}"
site["hosts"] = ["127.0.0.1"]
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, "--tenant", identity["tenant_id"], "dever.tenant.migrate", "null"],
directory, f"migrate-{tenant_key}")
for site in ("admin", "front"):
execute([executable, "--tenant", identity["tenant_id"], "dever.auth.owner",
json.dumps({"site": site, "user_id": 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)
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()
+200
View File
@@ -0,0 +1,200 @@
"""同 Pod 两个隔离容器:server 执行固定矩阵,client 只负责施压。
构建产物与本文件、process.py、settings.py 放入 /work;两个角色通过 emptyDir
交接有限测试命令。测量采用闭环长连接,延迟含收发和正文验证,不含建连时间。
"""
import argparse
import hashlib
import http.client
import json
import os
from pathlib import Path
import subprocess
import time
from process import Process, summarize
ROOT = Path("/work")
def write(path, value):
temporary = path.with_suffix(".tmp")
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n")
temporary.replace(path)
def counters():
return {name: (Path("/sys/fs/cgroup") / name).read_text()
for name in ("cpu.max", "cpu.stat", "memory.max", "memory.events")}
def client():
request = ROOT / "request.json"
while not (ROOT / "finished").exists():
if not request.exists():
time.sleep(0.02)
continue
item = json.loads(request.read_text())
request.unlink()
directory = ROOT / item["name"]
before = counters()
with Process([ROOT / "bin/rust-http", "load", item["port"], item["path"],
item["concurrency"], item["seconds"]], directory, cpus={2, 3, 4, 5}) as process:
deadline = time.monotonic() + item["seconds"] + 30
while process.child.poll() is None and time.monotonic() < deadline:
process.sample()
time.sleep(0.2)
if process.child.poll() is None:
raise TimeoutError(f"load client timed out: {item}")
result = {"exit_code": process.child.returncode, "stdout": process.text(),
"stderr": (directory / "stderr.log").read_text(),
"resources": summarize(process.samples), "samples": process.samples,
"cgroup_before": before, "cgroup_after": counters()}
write(directory / "result.json", result)
def load(process, name, port, path, concurrency, seconds):
result_path = ROOT / name / "result.json"
write(ROOT / "request.json", {"name": name, "port": port, "path": path,
"concurrency": concurrency, "seconds": seconds})
deadline = time.monotonic() + seconds + 40
while not result_path.exists() and time.monotonic() < deadline:
process.sample()
time.sleep(0.2)
result = json.loads(result_path.read_text())
if result["exit_code"] == 0:
result["measurement"] = json.loads(result["stdout"])
return result
def probe(port):
connection = http.client.HTTPConnection("127.0.0.1", port, timeout=5)
try:
connection.request("GET", "/json")
response = connection.getresponse()
return response.status == 200 and response.read() == b'{"ok":true}'
except OSError:
return False
finally:
connection.close()
def command(language, capacity=2048):
if language == "dever":
return [ROOT / "bin" / ("dever-http" if capacity == 2048 else "dever-overload")]
if language == "rust":
return [ROOT / "bin/rust-http", "serve", capacity]
binary = {"go": "go-http", "axum": "axum-http", "fiber": "fiber-http"}[language]
return [ROOT / "bin" / binary, capacity]
def measure(language, path, concurrency, seconds, name):
with Process(command(language), ROOT / name / "server", cpus={0, 1}) as process:
port = process.ready()
if not probe(port):
raise RuntimeError(f"smoke failed: {name}")
warmup = load(process, name + "/warmup", port, path, concurrency, 2)
process.samples.clear()
before = counters()
result = load(process, name + "/load", port, path, concurrency, seconds)
after = counters()
recovered = probe(port)
record = {"name": name, "language": language, "path": path,
"concurrency": concurrency, "seconds": seconds,
"warmup": warmup, "client": result,
"server": summarize(process.samples), "samples": process.samples,
"server_alive": process.child.poll() is None, "recovery_probe": recovered,
"stderr": (process.directory / "stderr.log").read_text(),
"cgroup_before": before, "cgroup_after": after}
write(ROOT / name / "result.json", record)
with (ROOT / "results.jsonl").open("a") as output:
output.write(json.dumps(record) + "\n")
print(json.dumps({"name": name, "measurement": result.get("measurement"),
"rss_mib": record["server"]["rss_high_water_bytes"] / 1048576,
"server_cores": record["server"]["cpu_cores_sampled"],
"client_cores": result["resources"]["cpu_cores_sampled"],
"recovery": recovered}), flush=True)
for phase in (warmup, result):
measurement = phase.get("measurement", {})
if phase["exit_code"] != 0 or measurement.get("errors") != 0 or not measurement.get("succeeded"):
raise RuntimeError(f"load failed; retained result: {name}")
if not record["server_alive"] or not recovered:
raise RuntimeError(f"server did not recover: {name}")
def overload(language):
name = "overload/" + language
with Process(command(language, 8192), ROOT / name / "server", cpus={0, 1}) as process:
port = process.ready()
connections = []
successful = 0
failed = 0
started = time.monotonic()
try:
for _ in range(4500):
connection = http.client.HTTPConnection("127.0.0.1", port, timeout=2)
connections.append(connection)
connection.request("GET", "/json")
response = connection.getresponse()
if response.status != 200 or response.read() != b'{"ok":true}':
failed += 1
break
successful += 1
except (OSError, http.client.HTTPException):
failed += 1
held_seconds = time.monotonic() - started
process.sample()
for connection in connections:
connection.close()
time.sleep(6)
recovered = probe(port)
record = {"language": language, "connections_target": 4500,
"successful_connections": successful, "failed": failed,
"open_seconds": held_seconds, "server_alive": process.child.poll() is None,
"recovered_after_release": recovered, "stdout": process.text(),
"stderr": (process.directory / "stderr.log").read_text(), "samples": process.samples}
write(ROOT / name / "result.json", record)
print(json.dumps(record), flush=True)
def server(mode, suite):
languages = ["dever", "axum", "fiber"] if suite == "frameworks" else ["dever", "rust", "go"]
if mode == "smoke":
for language in languages:
for path in ("/json", "/echo"):
measure(language, path, 4, 1, f"smoke/{language}/{path[1:]}")
return
manifest = {"suite": suite, "languages": languages,
"protocol": "HTTP/1.1", "client": "closed loop; one outstanding request per connection",
"server_cpus": [0, 1], "client_cpus": [2, 3, 4, 5],
"replicates": 3, "seconds": 12, "warmup_seconds": 2,
"server_capacity": 2048, "concurrency": [64, 256, 1024],
"paths": {"/json": "GET; 11-byte JSON", "/echo": "POST; 65536-byte body returned unchanged"},
"hashes": {path.name: hashlib.sha256(path.read_bytes()).hexdigest() for path in (ROOT / "bin").iterdir()}}
write(ROOT / "manifest.json", manifest)
for replicate in range(3):
for path in ("/json", "/echo"):
for concurrency in (64, 256, 1024):
for language in languages[replicate:] + languages[:replicate]:
name = f"matrix/{replicate}/{path[1:]}/{concurrency}/{language}"
measure(language, path, concurrency, 12, name)
for language in languages:
measure(language, "/json", 1024, 60, "sustained/" + language)
if suite == "core":
overload(language)
(ROOT / "finished").touch()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("role", choices=("server", "client"))
parser.add_argument("--mode", choices=("smoke", "matrix"), default="matrix")
parser.add_argument("--suite", choices=("core", "frameworks"), default="core")
arguments = parser.parse_args()
if arguments.role == "client":
client()
else:
try:
server(arguments.mode, arguments.suite)
finally:
(ROOT / "finished").touch()
@@ -0,0 +1,13 @@
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
}
}
@@ -0,0 +1,3 @@
global type Auth {
label: Text(1, 64)
}
@@ -0,0 +1,3 @@
public post echo = app.echo
public post raw = app.raw
@@ -0,0 +1,19 @@
type Row {
id: Int
text: Text
weight: Float
note: Text?
}
type Payload {
rows: List<Row>
raw: Json
}
echo(payload: Payload) (value: Payload) {
value = payload
}
raw(source: Json) (value: Json) {
value = source
}
@@ -0,0 +1,51 @@
use std::io::Write;
use std::path::Path;
use dever_backend_bridge::{Target, emit_object};
use dever_core::source::SourceMap;
use dever_runtime::config::Settings;
fn main() -> Result<(), String> {
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
let (project, output, ir) = match arguments.as_slice() {
[project, output] => (project, output, None),
[project, output, ir] => (project, output, Some(ir)),
_ => {
return Err("usage: api-json-object <project> <new-object-file> [new-ir-file]".into());
}
};
let project = Path::new(project);
let sources =
SourceMap::load(&project.join("module")).map_err(|errors| format!("{errors:?}"))?;
let settings = Settings::load_project(project).map_err(|error| error.to_string())?;
let program = dever_core::check_with_settings(&sources, &settings).map_err(|errors| {
errors
.iter()
.map(|error| error.render(&sources))
.collect::<String>()
})?;
let errors = program.application_errors()?;
if !errors.is_empty() {
return Err(errors
.iter()
.map(|error| error.render(&sources))
.collect::<String>());
}
let module = dever_core::llvm::emit_executable(&program, &sources)?;
if let Some(ir) = ir {
let mut ir = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(ir)
.map_err(|error| error.to_string())?;
ir.write_all(module.as_bytes())
.map_err(|error| error.to_string())?;
}
let object = emit_object(&module, Target::LinuxX86_64)?;
let mut output = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(output)
.map_err(|error| error.to_string())?;
output.write_all(&object).map_err(|error| error.to_string())
}
+115
View File
@@ -0,0 +1,115 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"os/signal"
"runtime"
"syscall"
"time"
)
type row struct {
ID int64 `json:"id"`
Text string `json:"text"`
Weight float64 `json:"weight"`
Note *string `json:"note"`
}
type payload struct {
Rows []row `json:"rows"`
Raw json.RawMessage `json:"raw"`
}
type envelope struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data"`
}
func handle(writer http.ResponseWriter, request *http.Request) {
typed := request.URL.Path == "/benchmark/echo/echo"
if request.Method != http.MethodPost || (!typed && request.URL.Path != "/benchmark/echo/raw") {
http.NotFound(writer, request)
return
}
body := http.MaxBytesReader(writer, request.Body, 16*1024*1024)
defer body.Close()
decoder := json.NewDecoder(body)
decoder.DisallowUnknownFields()
var result any
var err error
if typed {
var input struct {
Payload payload `json:"payload"`
}
err = decoder.Decode(&input)
result = input.Payload
} else {
var input struct {
Source json.RawMessage `json:"source"`
}
err = decoder.Decode(&input)
result = input.Source
}
var extra any
if err != nil || decoder.Decode(&extra) != io.EOF {
http.Error(writer, "invalid JSON", http.StatusBadRequest)
return
}
encoded, err := json.Marshal(envelope{0, "ok", result})
if err != nil {
http.Error(writer, "invalid response", http.StatusInternalServerError)
return
}
writer.Header().Set("Content-Type", "application/json")
writer.Write(encoded)
}
func run() error {
runtime.GOMAXPROCS(1)
content, err := os.ReadFile("config/setting.json")
if err != nil {
return err
}
var settings struct {
HTTP struct {
Port uint16 `json:"port"`
} `json:"http"`
}
if err := json.Unmarshal(content, &settings); err != nil {
return err
}
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", settings.HTTP.Port))
if err != nil {
return err
}
server := &http.Server{Handler: http.HandlerFunc(handle), ReadHeaderTimeout: 10 * time.Second, IdleTimeout: 10 * time.Second}
stopping := make(chan os.Signal, 1)
signal.Notify(stopping, syscall.SIGTERM)
defer signal.Stop(stopping)
closed := make(chan error, 1)
go func() {
<-stopping
context, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
closed <- server.Shutdown(context)
}()
if err := server.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
return err
}
return <-closed
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
+131
View File
@@ -0,0 +1,131 @@
use std::convert::Infallible;
use std::time::Duration;
use bytes::Bytes;
use http_body_util::{BodyExt, Full, Limited};
use hyper::{
Request, Response, StatusCode, body::Incoming, server::conn::http1, service::service_fn,
};
use hyper_util::rt::TokioIo;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use tokio::net::TcpListener;
use tokio::signal::unix::{SignalKind, signal};
use tokio::sync::watch;
use tokio::task::JoinSet;
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Row {
id: i64,
text: String,
weight: f64,
note: Option<String>,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Payload {
rows: Vec<Row>,
raw: Box<RawValue>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct TypedRequest {
payload: Payload,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawRequest {
source: Box<RawValue>,
}
#[derive(Serialize)]
struct Envelope<Value> {
code: u8,
message: &'static str,
data: Value,
}
async fn handle(request: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
let typed = request.uri().path() == "/benchmark/echo/echo";
if request.method() != hyper::Method::POST
|| !typed && request.uri().path() != "/benchmark/echo/raw"
{
return Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Full::new(Bytes::new()))
.unwrap());
}
let body = Limited::new(request.into_body(), 16 * 1024 * 1024)
.collect()
.await;
let encoded = body.map_err(|error| error.to_string()).and_then(|body| {
let bytes = body.to_bytes();
if typed {
serde_json::from_slice::<TypedRequest>(&bytes).and_then(|value| {
serde_json::to_vec(&Envelope {
code: 0,
message: "ok",
data: value.payload,
})
})
} else {
serde_json::from_slice::<RawRequest>(&bytes).and_then(|value| {
serde_json::to_vec(&Envelope {
code: 0,
message: "ok",
data: value.source,
})
})
}
.map_err(|error| error.to_string())
});
let (status, body) = match encoded {
Ok(body) => (StatusCode::OK, body),
Err(_) => (StatusCode::BAD_REQUEST, Vec::new()),
};
Ok(Response::builder()
.status(status)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(body)))
.unwrap())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
tokio::runtime::Builder::new_current_thread().enable_all().build()?.block_on(async {
let settings: serde_json::Value = serde_json::from_slice(&std::fs::read("config/setting.json")?)?;
let port = u16::try_from(settings["http"]["port"].as_u64().ok_or("missing port")?)?;
let listener = TcpListener::bind(("127.0.0.1", port)).await?;
let mut terminate = signal(SignalKind::terminate())?;
let (shutdown, stopping) = watch::channel(false);
let mut connections = JoinSet::new();
loop {
tokio::select! {
_ = terminate.recv() => break,
Some(result) = connections.join_next(), if !connections.is_empty() => { result?; },
accepted = listener.accept() => {
let (stream, _) = accepted?;
stream.set_nodelay(true)?;
let mut stopping = stopping.clone();
connections.spawn(async move {
let connection = http1::Builder::new().serve_connection(TokioIo::new(stream), service_fn(handle));
tokio::pin!(connection);
tokio::select! {
_ = &mut connection => {},
_ = stopping.changed() => {
connection.as_mut().graceful_shutdown();
let _ = tokio::time::timeout(Duration::from_secs(2), connection).await;
}
}
});
}
}
}
shutdown.send(true)?;
while let Some(result) = connections.join_next().await { result?; }
Ok(())
})
}
@@ -0,0 +1,246 @@
use dever_runtime::api;
use dever_runtime::application::Session;
use dever_runtime::auth;
use dever_runtime::config::RuntimeProfile;
use dever_runtime::http::{Request, Response};
use dever_runtime::wire::decode::{self, Fields, Reader};
use dever_runtime::wire::{Encoded, Encoder};
use dever_runtime::{lifecycle, task};
#[derive(Default)]
struct Row {
id: i64,
text: String,
weight: f64,
note: Option<String>,
}
impl Fields for Row {
fn count(&self) -> usize {
4
}
fn index(&self, name: &str) -> Result<usize, String> {
match name {
"id" => Ok(0),
"text" => Ok(1),
"weight" => Ok(2),
"note" => Ok(3),
_ => Err("unknown wire record field".into()),
}
}
fn required(&self, index: usize) -> bool {
index != 3
}
fn read(&mut self, index: usize, reader: &mut Reader<'_, '_>) -> Result<(), String> {
match index {
0 => self.id = reader.int()?,
1 => self.text = reader.text()?.into_owned(),
2 => self.weight = reader.float()?,
3 => {
reader.nullable(&mut |reader| {
self.note = Some(reader.text()?.into_owned());
Ok(())
})?;
}
_ => return Err("invalid wire field index".into()),
}
Ok(())
}
}
#[derive(Default)]
struct Payload {
rows: Vec<Row>,
raw: Option<Encoded>,
}
impl Fields for Payload {
fn count(&self) -> usize {
2
}
fn index(&self, name: &str) -> Result<usize, String> {
match name {
"rows" => Ok(0),
"raw" => Ok(1),
_ => Err("unknown wire record field".into()),
}
}
fn required(&self, _: usize) -> bool {
true
}
fn read(&mut self, index: usize, reader: &mut Reader<'_, '_>) -> Result<(), String> {
match index {
0 => reader.list(&mut |reader| {
let mut row = Row::default();
reader.record(&mut row)?;
self.rows.push(row);
Ok(())
}),
1 => {
self.raw = Some(reader.json()?);
Ok(())
}
_ => Err("invalid wire field index".into()),
}
}
}
impl Payload {
fn encode(self) -> Result<Encoded, String> {
let mut encoder = Encoder::default();
encoder.compiled_record(2, |encoder| {
encoder.compiled_key("\"rows\":")?;
encoder.begin_array()?;
for row in self.rows {
encoder.compiled_record(4, |encoder| {
encoder.compiled_key("\"id\":")?;
encoder.int(row.id)?;
encoder.compiled_key(",\"text\":")?;
encoder.text(&row.text)?;
encoder.compiled_key(",\"weight\":")?;
encoder.float(row.weight)?;
encoder.compiled_key(",\"note\":")?;
match row.note {
Some(note) => encoder.text(&note),
None => encoder.null(),
}
})?;
}
encoder.end()?;
encoder.compiled_key(",\"raw\":")?;
encoder.encoded(&self.raw.ok_or("missing required wire field")?)
})?;
encoder.finish()
}
}
enum Value {
Typed(Payload),
Raw(Encoded),
}
struct Arguments {
typed: bool,
value: Option<Value>,
}
impl Fields for Arguments {
fn count(&self) -> usize {
1
}
fn index(&self, name: &str) -> Result<usize, String> {
if name == if self.typed { "payload" } else { "source" } {
Ok(0)
} else {
Err("unknown wire record field".into())
}
}
fn required(&self, _: usize) -> bool {
true
}
fn read(&mut self, _: usize, reader: &mut Reader<'_, '_>) -> Result<(), String> {
self.value = Some(if self.typed {
let mut payload = Payload::default();
reader.record(&mut payload)?;
Value::Typed(payload)
} else {
Value::Raw(reader.json()?)
});
Ok(())
}
}
async fn call(mut request: Request, typed: bool) -> Response {
if let Err(error) = auth::require_components(&[], &[]).await {
return api::auth_error(error);
}
if let Err(error) = api::upload::read_body(&mut request).await {
return api::invalid_input(error);
}
let body = match api::Inputs::json_body(&request) {
Ok(body) => body,
Err(error) => return api::invalid_input(error),
};
let mut arguments = Arguments { typed, value: None };
if let Err(error) = decode::read(body, &mut |reader| reader.record(&mut arguments)) {
return api::invalid_input(api::InputError(error));
}
let encoded = match arguments.value {
Some(Value::Typed(payload)) => payload.encode(),
Some(Value::Raw(value)) => Ok(value),
None => Err("missing required wire field".into()),
};
match encoded {
Ok(value) => {
if api::commit_response_metadata().is_err() {
return api::internal_error();
}
api::success(value)
}
Err(_) => api::internal_error(),
}
}
async fn route(request: Request) -> Result<Response, String> {
let typed = match request.target.split('?').next().unwrap_or("") {
"/benchmark/echo/echo" => true,
"/benchmark/echo/raw" => false,
_ => return Ok(api::not_found()),
};
if request.method != "POST" {
return Ok(api::method_not_allowed(&["POST"]));
}
let prepared = match auth::prepare(&[], &request, true) {
Ok(prepared) => prepared,
Err(error) => return Ok(api::auth_error(error)),
};
let identity = prepared.claims().map(|claims| auth::Identity {
id: claims.subject.clone(),
user_id: None,
tenant_id: None,
});
Ok(
match auth::scope(prepared, identity, call(request, typed)).await {
Ok(response) => response,
Err(error) => api::auth_error(error),
},
)
}
fn main() -> Result<(), String> {
let session = Session::load(
RuntimeProfile {
sqlite: true,
postgres: false,
},
&[(None, "benchmark", false)],
&[],
)?;
session.initialize_authorization(false)?;
let cleanup = session.clone();
task::run_entry_with_typed_cleanup(
task::RuntimeConfig::default(),
session.clone().scope(async move {
session
.databases()
.prepare()
.await
.map_err(|error| error.to_string())?;
lifecycle::configure(true, false)?;
lifecycle::run(api::serve(route)).await
}),
|result| async move {
let closed = cleanup.close().await;
result.and(closed)
},
)
}
@@ -0,0 +1,112 @@
#define _POSIX_C_SOURCE 200809L
#include "runtime.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
extern long long dever_test_allocations(void);
static void text_clone(const void *source, void *target) {
*(void **)target = dever_rt_v1_text_retain(*(void *const *)source);
}
static void text_drop(void *value) {
dever_rt_v1_text_release(*(void **)value);
*(void **)value = NULL;
}
static uint8_t text_equal(const void *left, const void *right) {
return dever_rt_v1_text_equal(*(void *const *)left, *(void *const *)right);
}
static const DeverRtType text_type = {
sizeof(void *), _Alignof(void *), text_clone, text_drop, text_equal, NULL
};
static uint32_t input_field(void *inputs, void *out, DeverRtBuffer *error) {
const DeverRtWireName name = {(const uint8_t *)"source", 6};
uint8_t present = 0;
return dever_rt_v1_cmd_input_field(inputs, &name, 1, out, &present, error);
}
static uint32_t decode(const void *reader, void *out, DeverRtBuffer *error) {
return dever_rt_v1_wire_read_raw(reader, out, error);
}
static uint32_t encode(const void *value, void *encoder, DeverRtBuffer *error) {
return dever_rt_v1_wire_encoder_json(encoder, *(void *const *)value, error);
}
static uint32_t operation(unsigned phase, void *body, void *source, void **out, DeverRtBuffer *error) {
if (phase == 0) return dever_rt_v1_cmd_decode(body, &text_type, input_field, out, error);
if (phase == 1) return dever_rt_v1_wire_decode(source, &text_type, decode, out, error);
if (phase == 2) return dever_rt_v1_wire_encode(&source, &text_type, encode, out, error);
return dever_rt_v1_json_from_text(source, out, error);
}
int dever_test_run(void) {
const char *setting = getenv("DEVER_WIRE_BENCH_ROWS");
size_t rows = setting == NULL ? 128 : (size_t)strtoul(setting, NULL, 10);
int wide = getenv("DEVER_WIRE_BENCH_WIDE") != NULL;
if (rows != 1 && rows != 16 && rows != 128 && !(wide && rows == 1024)) return 1;
const char *row = "{\"id\":9007199254740993,\"text\":\"雪😀\\n\\\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",\"weight\":1.25,\"note\":null}";
const char *suffix = "],\"raw\":{\"large\":123456789012345678901234567890}}";
size_t capacity = rows * (strlen(row) + 1) + strlen(suffix) + 64;
char *raw = malloc(capacity);
if (raw == NULL) return 2;
strcpy(raw, wide ? "{" : "{\"rows\":[");
for (size_t index = 0; index < rows; ++index) {
if (index != 0) strcat(raw, ",");
if (wide) {
char field[64];
snprintf(field, sizeof(field), "\"field_%zu\":%zu", index, index);
strcat(raw, field);
} else {
strcat(raw, row);
}
}
strcat(raw, wide ? "}" : suffix);
size_t length = strlen(raw);
DeverRtBuffer error = {0};
void *source = NULL, *body = NULL;
if (dever_rt_v1_text_new((const uint8_t *)raw, length, &source, &error)) return 3;
if (getenv("DEVER_WIRE_BENCH_VALIDATED") != NULL) {
void *validated = NULL;
if (dever_rt_v1_wire_decode(source, &text_type, decode, &validated, &error)) return 11;
dever_rt_v1_text_release(source);
source = validated;
}
char *request = malloc(length + 12);
if (request == NULL) return 4;
snprintf(request, length + 12, "{\"source\":%s}", raw);
if (dever_rt_v1_text_new((const uint8_t *)request, strlen(request), &body, &error)) return 5;
free(request);
for (unsigned phase = 0; phase < 4; ++phase) {
void *output = NULL;
if (operation(phase, body, source, &output, &error)) return 6;
DeverRtBuffer bytes = {0};
if (dever_rt_v1_text_export(output, &bytes) || bytes.len != length || memcmp(bytes.ptr, raw, length) != 0) return 7;
dever_rt_v1_buffer_free(bytes.ptr, bytes.len);
dever_rt_v1_text_release(output);
struct timespec started, finished;
if (clock_gettime(CLOCK_MONOTONIC, &started)) return 8;
long long allocations = dever_test_allocations();
const size_t iterations = 500;
for (size_t iteration = 0; iteration < iterations; ++iteration) {
if (operation(phase, body, source, &output, &error)) return 9;
dever_rt_v1_text_release(output);
}
allocations = dever_test_allocations() - allocations;
if (clock_gettime(CLOCK_MONOTONIC, &finished)) return 10;
int64_t elapsed = (int64_t)(finished.tv_sec - started.tv_sec) * INT64_C(1000000000)
+ finished.tv_nsec - started.tv_nsec;
printf("{\"phase\":%u,\"rows\":%zu,\"iterations\":%zu,\"elapsed_ns\":%lld,\"allocations\":%lld}\n",
phase, rows, iterations, (long long)elapsed, allocations);
}
dever_rt_v1_text_release(source);
dever_rt_v1_text_release(body);
free(raw);
return 0;
}
@@ -0,0 +1,135 @@
#define _POSIX_C_SOURCE 200809L
#include "runtime.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
extern long long dever_test_allocations(void);
#ifndef WIRE_TEXT_KEY
#define WIRE_TEXT_KEY "text"
#endif
#ifndef WIRE_NUMBER_KEY
#define WIRE_NUMBER_KEY "number"
#endif
#ifndef WIRE_RECORD
#define WIRE_RECORD 0
#endif
typedef struct {
void *text;
int64_t rows;
} Payload;
#if WIRE_RECORD != 2
static const DeverRtWireName fields[] = {
{(const uint8_t *)WIRE_TEXT_KEY, sizeof(WIRE_TEXT_KEY) - 1},
{(const uint8_t *)WIRE_NUMBER_KEY, sizeof(WIRE_NUMBER_KEY) - 1}
};
#endif
static void payload_clone(const void *source, void *destination) {
const Payload *input = source;
Payload *output = destination;
*output = *input;
output->text = dever_rt_v1_text_retain(input->text);
}
static void payload_drop(void *value) {
Payload *payload = value;
dever_rt_v1_text_release(payload->text);
payload->text = NULL;
}
static uint8_t payload_equal(const void *left, const void *right) {
const Payload *first = left;
const Payload *second = right;
return first->rows == second->rows && dever_rt_v1_text_equal(first->text, second->text);
}
static const DeverRtType payload_type = {
sizeof(Payload), _Alignof(Payload), payload_clone, payload_drop, payload_equal, NULL
};
#if WIRE_RECORD == 2
static const DeverRtWireName prefixes[] = {
{(const uint8_t *)"\"" WIRE_TEXT_KEY "\":", sizeof("\"" WIRE_TEXT_KEY "\":") - 1},
{(const uint8_t *)",\"" WIRE_NUMBER_KEY "\":", sizeof(",\"" WIRE_NUMBER_KEY "\":") - 1}
};
static uint32_t encode_fields(const void *value, void *writer, DeverRtBuffer *error) {
const Payload *payload = value;
uint32_t status = dever_rt_v1_wire_encoder_compiled_key(writer, &prefixes[0], error);
if (status == 0) status = dever_rt_v1_wire_encoder_text(writer, payload->text, error);
if (status == 0) status = dever_rt_v1_wire_encoder_compiled_key(writer, &prefixes[1], error);
if (status == 0) status = dever_rt_v1_wire_encoder_int(writer, INT64_C(9007199254740993), error);
return status;
}
#elif WIRE_RECORD
static uint32_t encode_field(uint64_t index, const void *value, void *writer, DeverRtBuffer *error) {
const Payload *payload = value;
if (index == 0) return dever_rt_v1_wire_encoder_text(writer, payload->text, error);
if (index == 1) return dever_rt_v1_wire_encoder_int(writer, INT64_C(9007199254740993), error);
return 2;
}
#endif
static uint32_t encode(const void *value, void *writer, DeverRtBuffer *error) {
const Payload *payload = value;
uint32_t status = dever_rt_v1_wire_encoder_begin_array(writer, error);
for (int64_t index = 0; index < payload->rows && status == 0; ++index) {
#if WIRE_RECORD == 2
status = dever_rt_v1_wire_encoder_compiled_record(writer, value, &payload_type, 2, encode_fields, error);
#elif WIRE_RECORD
status = dever_rt_v1_wire_encoder_record(writer, value, &payload_type, fields, 2, encode_field, error);
#else
status = dever_rt_v1_wire_encoder_begin_object(writer, error);
if (status == 0) status = dever_rt_v1_wire_encoder_key(writer, &fields[0], error);
if (status == 0) status = dever_rt_v1_wire_encoder_text(writer, payload->text, error);
if (status == 0) status = dever_rt_v1_wire_encoder_key(writer, &fields[1], error);
if (status == 0) status = dever_rt_v1_wire_encoder_int(writer, INT64_C(9007199254740993), error);
if (status == 0) status = dever_rt_v1_wire_encoder_end(writer, error);
#endif
}
if (status == 0) status = dever_rt_v1_wire_encoder_end(writer, error);
return status;
}
int dever_test_run(void) {
const char *configured = getenv("DEVER_WIRE_BENCH_ROWS");
int64_t rows = configured == NULL ? 128 : strtoll(configured, NULL, 10);
if (rows != 4 && rows != 128) return 1;
const char *text = "Unicode 雪😀 and newline\n";
const char *row = "{\"" WIRE_TEXT_KEY "\":\"Unicode 雪😀 and newline\\n\",\"" WIRE_NUMBER_KEY "\":9007199254740993}";
Payload payload = {NULL, rows};
DeverRtBuffer error = {0};
if (dever_rt_v1_text_new((const uint8_t *)text, strlen(text), &payload.text, &error)) return 2;
void *output = NULL;
if (dever_rt_v1_wire_encode(&payload, &payload_type, encode, &output, &error)) return 3;
DeverRtBuffer bytes = {0};
if (dever_rt_v1_text_export(output, &bytes)) return 4;
size_t row_length = strlen(row);
if (bytes.len != 1 + (row_length + 1) * (size_t)rows || bytes.ptr[0] != '[' || bytes.ptr[bytes.len - 1] != ']') return 5;
for (int64_t index = 0; index < rows; ++index) {
if (memcmp(bytes.ptr + 1 + (size_t)index * (row_length + 1), row, row_length) != 0) return 6;
}
dever_rt_v1_buffer_free(bytes.ptr, bytes.len);
dever_rt_v1_text_release(output);
struct timespec started, finished;
if (clock_gettime(CLOCK_MONOTONIC, &started)) return 7;
long long allocations = dever_test_allocations();
const int64_t iterations = 500;
for (int64_t iteration = 0; iteration < iterations; ++iteration) {
if (dever_rt_v1_wire_encode(&payload, &payload_type, encode, &output, &error)) return 8;
dever_rt_v1_text_release(output);
}
allocations = dever_test_allocations() - allocations;
if (clock_gettime(CLOCK_MONOTONIC, &finished)) return 9;
int64_t elapsed = (int64_t)(finished.tv_sec - started.tv_sec) * INT64_C(1000000000)
+ finished.tv_nsec - started.tv_nsec;
payload_drop(&payload);
printf("{\"rows\":%lld,\"iterations\":%lld,\"elapsed_ns\":%lld,\"allocations\":%lld}\n",
(long long)rows, (long long)iterations, (long long)elapsed, allocations);
return 0;
}
+21
View File
@@ -0,0 +1,21 @@
ready(port: Int) () {
dever.io.println("READY|" + int.toText(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.toText(count) + "|" + int.toText(finish - begin) + "|" + int.toText(
checksum
)
)
}
@@ -0,0 +1,3 @@
long cmd run = app.run
long cmd continuous = app.continuous
@@ -0,0 +1,39 @@
type InputResult {
Parsed(value: Decimal)
error Invalid
}
type State {
total: Decimal
factor: Decimal
}
type Sample {
elapsed_ns: Int
checksum: Text
}
parsed(value: Decimal) (result: Decimal) { result = value }
parsed(value: null) (result: Decimal) { fail(InputResult.Invalid) }
step(value: Int, state: State) (next: State) {
next = State {
total = state.total + decimal.fromInt(value) * state.factor
factor = state.factor
}
}
run(ticks: Text, initial: Text, factor: Text) (sample: Sample) {
bytes = dever.bytes.fromText(ticks)
state = State { total = parsed(decimal.parse(initial))
factor = parsed(decimal.parse(factor)) }
begin = dever.time.monotonicNanos()
final = reduce(step, bytes, state)
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = decimal.toText(final.total) }
}
continuous(ticks: Text, initial: Text, factor: Text) (samples: List<Sample>) {
samples = [run(ticks, initial, factor), run(ticks, initial, factor), run(ticks, initial, factor), run(ticks, initial, factor), run(ticks, initial, factor), run(ticks, initial, factor), run(ticks, initial, factor), run(ticks, initial, factor), run(ticks, initial, factor), run(ticks, initial, factor)]
}
+71
View File
@@ -0,0 +1,71 @@
use dec::{Context, Decimal128, Rounding};
use dever_runtime::number::DecimalValue;
use std::hint::black_box;
use std::time::Instant;
fn finite(context: &Context<Decimal128>, value: Decimal128) -> Decimal128 {
let status = context.status();
assert!(!status.division_by_zero() && !status.division_undefined());
assert!(!status.overflow() && !status.underflow());
assert!(!status.invalid_operation() && !status.invalid_context() && value.is_finite());
value
}
fn measure(mode: &str, ticks: &str, initial: &str, factor: &str) -> serde_json::Value {
let bytes = black_box(ticks.as_bytes());
let (checksum, elapsed) = if mode == "runtime" {
let mut total = DecimalValue::parse(initial).expect("initial");
let factor = DecimalValue::parse(factor).expect("factor");
let begin = Instant::now();
for value in bytes {
let product = DecimalValue::from_int(i64::from(*value))
.checked_mul(factor)
.expect("multiply");
total = total.checked_add(product).expect("add");
}
black_box(total);
let elapsed = begin.elapsed();
(total.to_string(), elapsed)
} else {
assert_eq!(mode, "decimal128");
let mut context = Context::<Decimal128>::default();
let mut total = context.parse(initial).expect("initial");
assert!(!context.status().inexact() && total.is_finite());
context.clear_status();
let factor = context.parse(factor).expect("factor");
assert!(!context.status().inexact() && factor.is_finite());
context.set_rounding(Rounding::HalfEven);
let begin = Instant::now();
for value in bytes {
let item = Decimal128::from(i64::from(*value));
context.clear_status();
let product = context.mul(item, factor);
let product = finite(&context, product);
context.clear_status();
let sum = context.add(total, product);
total = finite(&context, sum);
}
black_box(total);
let elapsed = begin.elapsed();
(context.reduce(total).to_string(), elapsed)
};
serde_json::json!({"checksum": checksum, "elapsed_ns": elapsed.as_nanos()})
}
fn main() {
let arguments = std::env::args().collect::<Vec<_>>();
let input: serde_json::Value = serde_json::from_str(&arguments[3]).expect("input object");
let ticks = input["ticks"].as_str().expect("ticks");
let initial = input["initial"].as_str().expect("initial");
let factor = input["factor"].as_str().expect("factor");
let output = match arguments[2].as_str() {
"run" => measure(&arguments[1], ticks, initial, factor),
"continuous" => serde_json::Value::Array(
(0..10)
.map(|_| measure(&arguments[1], ticks, initial, factor))
.collect(),
),
_ => panic!("unknown measurement"),
};
println!("{output}");
}
@@ -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))
}
serverTls(result: dever.tls.ServerResult.Ready(tls)) (value: dever.system.ServerTls) {
value = tls
}
serverTls(result: dever.tls.ServerResult.Failed(message)) (
value: dever.system.ServerTls
) {
fail(app.WorkerFailure.Failed(message))
}
responseValue(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 = responseValue(select(request.target, large))
}
select(path: "/plain", large: Bytes) (result: Content) {
result = Content {
body = dever.bytes.fromText("hello")
kind = "text/plain"
}
}
select(path: "/json", large: Bytes) (result: Content) {
result = Content {
body = dever.bytes.fromText("{\"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.fromText("unknown benchmark path")
kind = "text/plain"
}
}
serve(
listener: dever.system.Listener,
tls: null,
limits: dever.http.Limits,
large: Bytes
) () {
dever.system.httpServe(route, listener, limits, large)
}
serve(
listener: dever.system.Listener,
tls: dever.system.ServerTls,
limits: dever.http.Limits,
large: Bytes
) () {
dever.system.httpServeTls(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.toText(port)))
paused(result(dever.task.sleep(lifetime)))
closed(result(dever.net.closeListener(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(
serverTls(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.largeBody(), config.lifetime())
result = true
}
https() (result: Bool) {
port.secure(
config.limits(),
config.largeBody(),
config.lifetime(),
config.certificate(),
config.key()
)
result = true
}
http2() (result: Bool) {
port.plain(config.http2Limits(), config.largeBody(), config.lifetime())
result = true
}
https2() (result: Bool) {
port.secure(
config.http2Limits(),
config.largeBody(),
config.lifetime(),
config.certificate(),
config.key()
)
result = true
}
+66
View File
@@ -0,0 +1,66 @@
//! 独立 Axum 路由;用官方 Hyper 入口统一连接、报文和超时上限。
use axum::{
Router,
body::Bytes,
extract::DefaultBodyLimit,
http::header,
routing::{get, post},
};
use hyper_util::{
rt::{TokioIo, TokioTimer},
service::TowerToHyperService,
};
use std::{sync::Arc, time::Duration};
use tokio::{net::TcpListener, sync::Semaphore};
async fn serve(capacity: usize) -> Result<(), Box<dyn std::error::Error>> {
let router = Router::new()
.route(
"/json",
get(async |_: Bytes| {
(
[(header::CONTENT_TYPE, "application/json")],
"{\"ok\":true}",
)
}),
)
.route(
"/echo",
post(async |body: Bytes| ([(header::CONTENT_TYPE, "application/octet-stream")], body)),
)
.layer(DefaultBodyLimit::max(65536));
let listener = TcpListener::bind("127.0.0.1:0").await?;
let slots = Arc::new(Semaphore::new(capacity));
println!("READY|{}", listener.local_addr()?.port());
loop {
let permit = slots.clone().acquire_owned().await?;
let (stream, _) = listener.accept().await?;
stream.set_nodelay(true)?;
let service = TowerToHyperService::new(router.clone());
tokio::spawn(async move {
let _permit = permit;
let mut builder = hyper::server::conn::http1::Builder::new();
builder
.timer(TokioTimer::new())
.header_read_timeout(Duration::from_secs(5))
.max_buf_size(8192);
if let Err(error) = builder
.serve_connection(TokioIo::new(stream), service)
.await
{
if !error.is_incomplete_message() && !error.is_closed() {
eprintln!("connection: {error}");
}
}
});
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let capacity = std::env::args().nth(1).unwrap().parse()?;
tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()?
.block_on(serve(capacity))
}
@@ -0,0 +1,31 @@
route(request: dever.http.Request) (response: dever.http.Response) {
response = selected(request.target, request.body)
}
selected(path: "/json", body: Bytes) (response: dever.http.Response) {
response = dever.http.Response { status = 200, headers = [dever.http.header("content-type", "application/json")], body = dever.bytes.fromText("{\"ok\":true}") }
}
selected(path: "/echo", body: Bytes) (response: dever.http.Response) {
response = dever.http.Response { status = 200, headers = [dever.http.header("content-type", "application/octet-stream")], body = body }
}
selected(path: other, body: Bytes) (response: dever.http.Response) {
response = dever.http.Response { status = 404, headers = [], body = dever.bytes.fromText("") }
}
serve(connections: Int) () {
listener = dever.net.listen("127.0.0.1", 0)
port = dever.net.port(listener)
limits = dever.http.defaultLimits()
limits.connections = connections
limits.body_bytes = 65536
blocking(dever.io.println("READY|" + int.toText(port)))
dever.http.serve(route, listener, limits)
}
main() () {
serve(2048)
}
overload() () {
serve(8192)
}
@@ -0,0 +1,78 @@
package main
import (
"fmt"
"io"
"net"
"net/http"
"os"
"runtime"
"strconv"
"sync"
"time"
)
type listener struct {
net.Listener
slots chan struct{}
}
type connection struct {
net.Conn
slots chan struct{}
once sync.Once
}
func (value *connection) Close() error {
err := value.Conn.Close()
value.once.Do(func() { <-value.slots })
return err
}
func (value *listener) Accept() (net.Conn, error) {
value.slots <- struct{}{}
stream, err := value.Listener.Accept()
if err != nil {
<-value.slots
return nil, err
}
return &connection{Conn: stream, slots: value.slots}, nil
}
func main() {
capacity, err := strconv.Atoi(os.Args[1])
if err != nil {
panic(err)
}
runtime.GOMAXPROCS(2)
bound, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(err)
}
server := http.Server{
ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second,
IdleTimeout: 5 * time.Second, MaxHeaderBytes: 8192,
Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
defer request.Body.Close()
body, err := io.ReadAll(http.MaxBytesReader(writer, request.Body, 65536))
if err != nil {
http.Error(writer, err.Error(), http.StatusBadRequest)
return
}
switch request.URL.RequestURI() {
case "/json":
writer.Header().Set("Content-Type", "application/json")
writer.Write([]byte(`{"ok":true}`))
case "/echo":
writer.Header().Set("Content-Type", "application/octet-stream")
writer.Write(body)
default:
writer.WriteHeader(http.StatusNotFound)
}
}),
}
fmt.Printf("READY|%d\n", bound.Addr().(*net.TCPAddr).Port)
if err := server.Serve(&listener{Listener: bound, slots: make(chan struct{}, capacity)}); err != nil {
panic(err)
}
}
@@ -0,0 +1,121 @@
//! 独立 Hyper 服务及三种语言共用的 HTTP/1.1 客户端,不链接 Dever 运行库。
use bytes::Bytes;
use http_body_util::{BodyExt, Full, Limited};
use hyper::{Request, Response, body::Incoming, service::service_fn};
use hyper_util::rt::{TokioIo, TokioTimer};
use std::{convert::Infallible, sync::Arc, time::Duration};
use tokio::{net::TcpListener, sync::{Barrier, Semaphore}, time::Instant};
async fn route(request: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
let path = request.uri().path().to_owned();
let body = match Limited::new(request.into_body(), 65536).collect().await {
Ok(body) => body.to_bytes(),
Err(_) => return Ok(Response::builder().status(400).body(Full::new(Bytes::new())).unwrap()),
};
let (status, kind, body) = match path.as_str() {
"/json" => (200, "application/json", Bytes::from_static(b"{\"ok\":true}")),
"/echo" => (200, "application/octet-stream", body),
_ => (404, "text/plain", Bytes::new()),
};
Ok(Response::builder().status(status).header("content-type", kind).body(Full::new(body)).unwrap())
}
async fn serve(capacity: usize) -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:0").await?;
let slots = Arc::new(Semaphore::new(capacity));
println!("READY|{}", listener.local_addr()?.port());
loop {
let permit = slots.clone().acquire_owned().await?;
let (stream, _) = listener.accept().await?;
stream.set_nodelay(true)?;
tokio::spawn(async move {
let _permit = permit;
let mut builder = hyper::server::conn::http1::Builder::new();
builder.timer(TokioTimer::new()).header_read_timeout(Duration::from_secs(5)).max_buf_size(8192);
if let Err(error) = builder.serve_connection(TokioIo::new(stream), service_fn(route)).await {
if !error.is_incomplete_message() && !error.is_closed() {
eprintln!("connection: {error}");
}
}
});
}
}
async fn load(port: u16, path: String, concurrency: usize, seconds: f64) -> Result<(), Box<dyn std::error::Error>> {
let payload = if path == "/echo" { Bytes::from(vec![b'x'; 65536]) } else { Bytes::new() };
let expected = if path == "/echo" { payload.clone() } else { Bytes::from_static(b"{\"ok\":true}") };
let kind = if path == "/echo" { "application/octet-stream" } else { "application/json" };
let barrier = Arc::new(Barrier::new(concurrency + 1));
let mut workers = Vec::new();
for _ in 0..concurrency {
let stream = tokio::net::TcpStream::connect(("127.0.0.1", port)).await?;
stream.set_nodelay(true)?;
let (mut sender, connection) = hyper::client::conn::http1::handshake(TokioIo::new(stream)).await?;
let driver = tokio::spawn(connection);
let barrier = barrier.clone();
let payload = payload.clone();
let expected = expected.clone();
let path = path.clone();
workers.push(tokio::spawn(async move {
barrier.wait().await;
let deadline = Instant::now() + Duration::from_secs_f64(seconds);
let mut samples = Vec::new();
let mut errors = 0_u64;
while Instant::now() < deadline {
let request = Request::builder().method(if path == "/echo" { "POST" } else { "GET" })
.uri(&path).header("host", "localhost").body(Full::new(payload.clone())).unwrap();
let started = Instant::now();
let exchange = async {
let response = sender.send_request(request).await.map_err(|error| error.to_string())?;
if response.status() != 200 || response.headers().get("content-type").and_then(|value| value.to_str().ok()) != Some(kind) {
return Err("status or content type differs".to_owned());
}
let body = response.into_body().collect().await.map_err(|error| error.to_string())?.to_bytes();
if body != expected { return Err("response body differs".to_owned()); }
Ok(())
};
match tokio::time::timeout(Duration::from_secs(5), exchange).await {
Ok(Ok(())) => samples.push(started.elapsed().as_micros() as u64),
result => {
eprintln!("request: {result:?}");
errors += 1;
break;
}
}
}
drop(sender);
driver.abort();
let _ = driver.await;
(samples, errors)
}));
}
let started = Instant::now();
barrier.wait().await;
let mut samples = Vec::new();
let mut errors = 0;
for worker in workers {
let (mut values, failed) = worker.await?;
samples.append(&mut values);
errors += failed;
}
let elapsed = started.elapsed().as_secs_f64();
samples.sort_unstable();
let percentile = |percent: usize| -> Option<f64> {
if samples.is_empty() { None } else { Some(samples[(samples.len() * percent).div_ceil(100) - 1] as f64 / 1000.0) }
};
println!("{}", serde_json::json!({"concurrency":concurrency,"seconds":seconds,"elapsed_seconds":elapsed,
"succeeded":samples.len(),"errors":errors,"qps":samples.len() as f64 / elapsed,
"p50_ms":percentile(50),"p95_ms":percentile(95),"p99_ms":percentile(99)}));
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let arguments = std::env::args().skip(1).collect::<Vec<_>>();
let server = arguments[0] == "serve";
let runtime = tokio::runtime::Builder::new_multi_thread().worker_threads(if server { 2 } else { 4 }).enable_all().build()?;
if server {
runtime.block_on(serve(arguments[1].parse()?))
} else {
runtime.block_on(load(arguments[1].parse()?, arguments[2].clone(), arguments[3].parse()?, arguments[4].parse()?))
}
}
+46
View File
@@ -0,0 +1,46 @@
package main
import (
"fmt"
"net"
"os"
"runtime"
"strconv"
"time"
"github.com/gofiber/fiber/v3"
)
func main() {
capacity, err := strconv.Atoi(os.Args[1])
if err != nil {
panic(err)
}
runtime.GOMAXPROCS(2)
app := fiber.New(fiber.Config{
Concurrency: capacity,
BodyLimit: 65536,
ReadBufferSize: 8192,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
IdleTimeout: 5 * time.Second,
CaseSensitive: true,
StrictRouting: true,
})
app.Get("/json", func(context fiber.Ctx) error {
context.Set("Content-Type", "application/json")
return context.SendString(`{"ok":true}`)
})
app.Post("/echo", func(context fiber.Ctx) error {
context.Set("Content-Type", "application/octet-stream")
return context.Send(context.BodyRaw())
})
bound, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(err)
}
fmt.Printf("READY|%d\n", bound.Addr().(*net.TCPAddr).Port)
if err := app.Listener(bound, fiber.ListenConfig{DisableStartupMessage: true}); err != nil {
panic(err)
}
}
+142
View File
@@ -0,0 +1,142 @@
package main
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"runtime"
"sync"
"time"
)
type settings struct {
Workers int `json:"workers"`
Connections int `json:"connections"`
TimeoutMS int `json:"timeout_ms"`
LifetimeMS int `json:"lifetime_ms"`
Certificate string `json:"cert_path"`
Key string `json:"key_path"`
}
type limitedListener struct {
net.Listener
slots chan struct{}
closed chan struct{}
once sync.Once
}
type limitedConnection struct {
net.Conn
slots chan struct{}
once sync.Once
}
func (connection *limitedConnection) Close() error {
err := connection.Conn.Close()
connection.once.Do(func() { <-connection.slots })
return err
}
func (listener *limitedListener) Accept() (net.Conn, error) {
connection, err := listener.Listener.Accept()
if err != nil {
return nil, err
}
select {
case listener.slots <- struct{}{}:
return &limitedConnection{Conn: connection, slots: listener.slots}, nil
case <-listener.closed:
connection.Close()
return nil, net.ErrClosed
}
}
func (listener *limitedListener) Close() error {
listener.once.Do(func() { close(listener.closed) })
return listener.Listener.Close()
}
func run() error {
if len(os.Args) != 2 || (os.Args[1] != "http" && os.Args[1] != "https") {
return errors.New("usage: go_http http|https")
}
executable, err := os.Executable()
if err != nil {
return err
}
content, err := os.ReadFile(filepath.Join(filepath.Dir(executable), "config", "setting.json"))
if err != nil {
return err
}
var document struct {
Benchmark settings `json:"benchmark"`
}
if err := json.Unmarshal(content, &document); err != nil {
return err
}
config := document.Benchmark
if config.Workers < 1 || config.Connections < 1 || config.TimeoutMS < 1 || config.LifetimeMS < 1 {
return errors.New("benchmark requires positive workers, connections, timeout_ms and lifetime_ms")
}
runtime.GOMAXPROCS(config.Workers)
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return err
}
bounded := &limitedListener{Listener: listener, slots: make(chan struct{}, config.Connections), closed: make(chan struct{})}
defer bounded.Close()
large := bytes.Repeat([]byte{'x'}, 65536)
deadline := time.Duration(config.TimeoutMS) * time.Millisecond
server := &http.Server{
ReadTimeout: deadline, ReadHeaderTimeout: deadline, WriteTimeout: deadline,
IdleTimeout: deadline, MaxHeaderBytes: 8192,
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
body := http.MaxBytesReader(writer, request.Body, 65536)
defer body.Close()
if _, err := io.Copy(io.Discard, body); err != nil {
http.Error(writer, "invalid request body", http.StatusBadRequest)
return
}
switch request.URL.RequestURI() {
case "/plain":
writer.Header().Set("Content-Type", "text/plain")
io.WriteString(writer, "hello")
case "/json":
writer.Header().Set("Content-Type", "application/json")
io.WriteString(writer, `{"ok":true}`)
case "/bytes":
writer.Header().Set("Content-Type", "application/octet-stream")
writer.Write(large)
default:
http.NotFound(writer, request)
}
}),
}
timer := time.AfterFunc(time.Duration(config.LifetimeMS)*time.Millisecond, func() { server.Close() })
defer timer.Stop()
fmt.Printf("READY|%d\n", listener.Addr().(*net.TCPAddr).Port)
if os.Args[1] == "https" {
err = server.ServeTLS(bounded, config.Certificate, config.Key)
} else {
err = server.Serve(bounded)
}
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
@@ -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
+1
View File
@@ -0,0 +1 @@
long cmd run = app.run
+56
View File
@@ -0,0 +1,56 @@
type State {
source: Json
update: Bool
checksum: Int
}
run(source: Json, update: Bool, ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
initial = State {
source = source
update = update
checksum = 0
}
output = transform(source, update)
begin = dever.time.monotonicNanos()
final = reduce(step, input, initial)
end = dever.time.monotonicNanos()
sample = Sample {
elapsed_ns = end - begin
checksum = final.checksum
output = output
}
}
type Sample {
elapsed_ns: Int
checksum: Int
output: Text
}
step(unused: Int, state: State) (next: State) {
output = transform(state.source, state.update)
next = State {
source = state.source
update = state.update
checksum = state.checksum + length(dever.bytes.fromText(output))
}
}
transform(source: Json, update: false) (output: Text) {
output = selected(dever.json.propertyValue(source, "data"))
}
transform(source: Json, update: true) (output: Text) {
output = dever.json.toText(
dever.json.setProperty(source, "active", dever.json.boolean(true))
)
}
selected(value: Json) (output: Text) {
output = dever.json.toText(value)
}
selected(value: null) (output: Text) {
output = "null"
}
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"os"
"time"
)
type input struct {
Source json.RawMessage `json:"source"`
Update bool `json:"update"`
Ticks string `json:"ticks"`
}
type sample struct {
Elapsed int64 `json:"elapsed_ns"`
Checksum int `json:"checksum"`
Output string `json:"output"`
}
func transform(source []byte, update bool) []byte {
decoder := json.NewDecoder(bytes.NewReader(source))
decoder.UseNumber()
var object map[string]any
if err := decoder.Decode(&object); err != nil {
panic(err)
}
var selected any = object["data"]
if update {
object["active"] = true
selected = object
}
output, err := json.Marshal(selected)
if err != nil {
panic(err)
}
return output
}
func main() {
var arguments input
if len(os.Args) != 2 {
panic("expected one JSON input")
}
if err := json.Unmarshal([]byte(os.Args[1]), &arguments); err != nil {
panic(err)
}
if !json.Valid(arguments.Source) {
panic("invalid source")
}
output := transform(arguments.Source, arguments.Update)
checksum := 0
begin := time.Now()
for range []byte(arguments.Ticks) {
checksum += len(transform(arguments.Source, arguments.Update))
}
result := sample{time.Since(begin).Nanoseconds(), checksum, string(output)}
encoded, err := json.Marshal(result)
if err != nil {
panic(err)
}
fmt.Println(string(encoded))
}
+46
View File
@@ -0,0 +1,46 @@
use serde::{Deserialize, Serialize};
use serde_json::{Value, value::RawValue};
use std::hint::black_box;
use std::time::Instant;
#[derive(Deserialize)]
struct Input {
source: Box<RawValue>,
update: bool,
ticks: String,
}
#[derive(Serialize)]
struct Sample {
elapsed_ns: u128,
checksum: usize,
output: String,
}
fn transform(source: &str, update: bool) -> String {
let mut value: Value = serde_json::from_str(source).expect("valid JSON input");
let object = value.as_object_mut().expect("object input");
if update {
object.insert("active".into(), Value::Bool(true));
serde_json::to_string(&value).expect("serializable object")
} else {
serde_json::to_string(object.get("data").expect("data field")).expect("serializable value")
}
}
fn main() {
let argument = std::env::args().nth(1).expect("one JSON argument");
let input: Input = serde_json::from_str(&argument).expect("valid input fields");
let output = transform(input.source.get(), input.update);
let mut checksum = 0;
let started = Instant::now();
for _ in input.ticks.bytes() {
checksum += transform(black_box(input.source.get()), input.update).len();
}
let sample = Sample {
elapsed_ns: started.elapsed().as_nanos(),
checksum,
output,
};
println!("{}", serde_json::to_string(&sample).unwrap());
}
@@ -0,0 +1,67 @@
long cmd record_update = app.recordUpdate
long cmd continuous_record_update = app.continuousRecordUpdate
long cmd integer = app.integer
long cmd record_inspect = app.recordInspect
long cmd continuous_record_inspect = app.continuousRecordInspect
long cmd map_lookup = app.mapLookup
long cmd continuous_map_lookup = app.continuousMapLookup
long cmd map_text_lookup = app.mapTextLookup
long cmd continuous_map_text_lookup = app.continuousMapTextLookup
long cmd text_equal = app.textEqual
long cmd continuous_text_equal = app.continuousTextEqual
long cmd text_contains = app.textContains
long cmd continuous_text_contains = app.continuousTextContains
long cmd float = app.float
long cmd bytes_concat = app.bytesConcat
long cmd text_concat = app.textConcat
long cmd list = app.list
long cmd map = app.map
long cmd list_read = app.listRead
long cmd map_read = app.mapRead
long cmd list_record = app.listRecord
long cmd list_record_read = app.listRecordRead
long cmd continuous_integer = app.continuousInteger
long cmd continuous_float = app.continuousFloat
long cmd continuous_list = app.continuousList
long cmd continuous_map = app.continuousMap
long cmd continuous_list_read = app.continuousListRead
long cmd continuous_map_read = app.continuousMapRead
long cmd continuous_list_record = app.continuousListRecord
long cmd continuous_list_record_read = app.continuousListRecordRead
long cmd continuous_text_concat = app.continuousTextConcat
long cmd continuous_bytes_concat = app.continuousBytesConcat
long cmd call_read = app.callRead
long cmd continuous_call_read = app.continuousCallRead
+393
View File
@@ -0,0 +1,393 @@
type Lookup {
values: Map<Int, Int>
index: Int
total: Int
}
selected(value: Int) (answer: Int) { answer = value }
selected(value: null) (answer: Int) { answer = 0 }
lookup(value: Int, state: Lookup) (next: Lookup) {
found = selected(get(state.values, state.index))
next = Lookup { values = state.values
index = state.index + 1
total = state.total + found }
}
mapLookup(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
values = reduce(putValue, input, {})
initial = Lookup { values = values
index = 0
total = 0 }
begin = dever.time.monotonicNanos()
final = reduce(lookup, input, initial)
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = final.total }
}
continuousMapLookup(ticks: Text) (samples: List<Sample>) {
samples = each(mapLookup, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
type TextLookup {
values: Map<Text, Int>
total: Int
}
type TextPair {
left: Text
right: Text
}
textEntry(entry: MapEntry<Int, Int>, state: Map<Text, Int>) (next: Map<Text, Int>) {
next = put(state, "lookup-key-" + int.toText(entry.key), entry.value)
}
textKey(entry: MapEntry<Text, Int>) (key: Text) { key = entry.key }
textLookup(key: Text, state: TextLookup) (next: TextLookup) {
found = selected(get(state.values, key))
next = TextLookup { values = state.values
total = state.total + found }
}
mapTextLookup(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
values = reduce(textEntry, entries(reduce(putValue, input, {})), {})
keys = each(textKey, entries(values))
initial = TextLookup { values = values
total = 0 }
begin = dever.time.monotonicNanos()
final = reduce(textLookup, keys, initial)
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = final.total + length(keys) - length(input) }
}
continuousMapTextLookup(ticks: Text) (samples: List<Sample>) {
samples = each(mapTextLookup, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
textPair(value: Int) (pair: TextPair) {
left = "compare-value-" + int.toText(value)
pair = TextPair { left = left
right = text.lower(text.upper(left)) }
}
matchedValue(matched: true) (value: Int) { value = 120 }
matchedValue(matched: false) (value: Int) { value = 0 }
equalTextPair(row: TextPair, total: Int) (next: Int) {
next = total + matchedValue(row.left == row.right)
}
containsTextPair(row: TextPair, total: Int) (next: Int) {
next = total + matchedValue(text.contains(row.left, row.right))
}
textEqual(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
rows = each(textPair, input)
begin = dever.time.monotonicNanos()
total = reduce(equalTextPair, rows, 0)
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = total + length(rows) - length(input) }
}
continuousTextEqual(ticks: Text) (samples: List<Sample>) {
samples = each(textEqual, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
textContains(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
rows = each(textPair, input)
begin = dever.time.monotonicNanos()
total = reduce(containsTextPair, rows, 0)
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = total + length(rows) - length(input) }
}
continuousTextContains(ticks: Text) (samples: List<Sample>) {
samples = each(textContains, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
type Sample {
elapsed_ns: Int
checksum: Int
}
type FloatSample {
elapsed_ns: Int
checksum: Float
}
addFloat(value: Int, state: Float) (next: Float) {
next = state + float.fromInt(value)
}
float(ticks: Text) (sample: FloatSample) {
bytes = dever.bytes.fromText(ticks)
zero = float.fromInt(0)
begin = dever.time.monotonicNanos()
total = reduce(addFloat, bytes, zero)
end = dever.time.monotonicNanos()
sample = FloatSample { elapsed_ns = end - begin
checksum = total }
}
joinBytes(value: Bytes, state: Bytes) (next: Bytes) {
next = dever.bytes.concat(state, value)
}
bytesConcat(ticks: Text) (sample: Sample) {
chunks = each(dever.bytes.fromText, text.split(ticks, ""))
empty = dever.bytes.fromText("")
begin = dever.time.monotonicNanos()
output = reduce(joinBytes, chunks, empty)
end = dever.time.monotonicNanos()
total = reduce(add, output, 0)
sample = Sample { elapsed_ns = end - begin
checksum = total }
}
joinText(value: Text, state: Text) (next: Text) {
next = state + value
}
textConcat(ticks: Text) (sample: Sample) {
chunks = text.split(ticks, ",")
begin = dever.time.monotonicNanos()
output = reduce(joinText, chunks, "")
end = dever.time.monotonicNanos()
sample = Sample {
elapsed_ns = end - begin
checksum = reduce(add, dever.bytes.fromText(output), 0)
}
}
add(value: Int, state: Int) (next: Int) {
next = state + value
}
appendValue(value: Int, state: List<Int>) (next: List<Int>) {
next = append(state, value)
}
putValue(value: Int, state: Map<Int, Int>) (next: Map<Int, Int>) {
key = length(state)
next = put(state, key, value)
}
mapped(entry: MapEntry<Int, Int>) (value: Int) {
value = entry.value
}
integer(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
begin = dever.time.monotonicNanos()
value = reduce(add, input, 0)
end = dever.time.monotonicNanos()
sample = Sample {
elapsed_ns = end - begin
checksum = value
}
}
list(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
begin = dever.time.monotonicNanos()
value = reduce(appendValue, input, [])
end = dever.time.monotonicNanos()
sample = Sample {
elapsed_ns = end - begin
checksum = sum(value)
}
}
map(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
begin = dever.time.monotonicNanos()
value = reduce(putValue, input, {})
end = dever.time.monotonicNanos()
sample = Sample {
elapsed_ns = end - begin
checksum = sum(each(mapped, entries(value)))
}
}
listRead(ticks: Text) (sample: Sample) {
values = reduce(appendValue, dever.bytes.fromText(ticks), [])
begin = dever.time.monotonicNanos()
total = sum(values)
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = total }
}
mapRead(ticks: Text) (sample: Sample) {
values = reduce(putValue, dever.bytes.fromText(ticks), {})
begin = dever.time.monotonicNanos()
total = sum(mapped, entries(values))
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = total }
}
type Record {
first: Int
second: Int
third: Int
}
appendRecord(value: Int, state: List<Record>) (next: List<Record>) {
next = append(state, Record { first = value
second = 0
third = 0 })
}
recordValue(row: Record) (value: Int) {
value = row.first + row.second + row.third
}
listRecord(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
begin = dever.time.monotonicNanos()
values = reduce(appendRecord, input, [])
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = sum(recordValue, values) }
}
listRecordRead(ticks: Text) (sample: Sample) {
values = reduce(appendRecord, dever.bytes.fromText(ticks), [])
begin = dever.time.monotonicNanos()
total = sum(recordValue, values)
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = total }
}
continuousInteger(ticks: Text) (samples: List<Sample>) {
samples = each(integer, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousFloat(ticks: Text) (samples: List<FloatSample>) {
samples = each(float, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousList(ticks: Text) (samples: List<Sample>) {
samples = each(list, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousMap(ticks: Text) (samples: List<Sample>) {
samples = each(map, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousListRead(ticks: Text) (samples: List<Sample>) {
samples = each(listRead, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousMapRead(ticks: Text) (samples: List<Sample>) {
samples = each(mapRead, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousListRecord(ticks: Text) (samples: List<Sample>) {
samples = each(listRecord, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousListRecordRead(ticks: Text) (samples: List<Sample>) {
samples = each(listRecordRead, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousTextConcat(ticks: Text) (samples: List<Sample>) {
samples = each(textConcat, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousBytesConcat(ticks: Text) (samples: List<Sample>) {
samples = each(bytesConcat, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
type OwnedRecord {
name: Text
tags: List<Text>
count: Int
}
ownedRecord(value: Int) (row: OwnedRecord) {
name = "row-value-" + int.toText(value)
row = OwnedRecord { name = name
tags = [name]
count = value - length(name) - 1 }
}
inspectOwned(row: OwnedRecord) (value: Int) {
value = row.count + length(row.name) + length(row.tags)
}
forwardOwned(row: OwnedRecord) (value: Int) {
value = inspectOwned(row)
}
callRead(ticks: Text) (sample: Sample) {
rows = each(ownedRecord, dever.bytes.fromText(ticks))
begin = dever.time.monotonicNanos()
total = sum(forwardOwned, rows)
end = dever.time.monotonicNanos()
preserved = length(rows)
sample = Sample { elapsed_ns = end - begin
checksum = total + preserved - length(rows) }
}
continuousCallRead(ticks: Text) (samples: List<Sample>) {
samples = each(callRead, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
type Accumulator {
values: List<Int>
count: Int
}
appendField(value: Int, state: Accumulator) (next: Accumulator) {
values = append(state.values, value)
next = Accumulator { values = values
count = state.count + 1 }
}
recordUpdate(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
begin = dever.time.monotonicNanos()
state = reduce(appendField, input, Accumulator { values = []
count = 0 })
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = sum(state.values) + state.count - length(state.values) }
}
continuousRecordUpdate(ticks: Text) (samples: List<Sample>) {
samples = each(recordUpdate, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
appendInspected(value: Int, state: Accumulator) (next: Accumulator) {
count = length(state.values) + 1
values = append(state.values, value)
next = Accumulator { values = values
count = count }
}
recordInspect(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
begin = dever.time.monotonicNanos()
state = reduce(appendInspected, input, Accumulator { values = []
count = 0 })
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = sum(state.values) + state.count - length(state.values) }
}
continuousRecordInspect(ticks: Text) (samples: List<Sample>) {
samples = each(recordInspect, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
+214
View File
@@ -0,0 +1,214 @@
use std::hint::black_box;
use std::time::{Duration, Instant};
#[derive(Debug, PartialEq)]
struct Fault {
message: String,
locations: Vec<&'static str>,
causes: Vec<String>,
}
impl Fault {
fn overflow(location: &'static str) -> Self {
Self {
message: "Int overflow".into(),
locations: vec![location],
causes: Vec::new(),
}
}
fn at(mut self, location: &'static str) -> Self {
self.locations.push(location);
self
}
}
struct Record {
first: i64,
second: i64,
third: i64,
}
struct OwnedRecord {
name: String,
tags: Vec<String>,
count: i64,
}
fn inspect_owned(row: &OwnedRecord) -> Result<i64, Fault> {
let partial = row
.count
.checked_add(row.name.chars().count() as i64)
.ok_or_else(|| Fault::overflow("inspect_owned"))?;
partial
.checked_add(row.tags.len() as i64)
.ok_or_else(|| Fault::overflow("inspect_owned"))
}
fn forward_owned(row: &OwnedRecord) -> Result<i64, Fault> {
inspect_owned(row).map_err(|error| error.at("forward_owned"))
}
fn owned_sum(rows: &[OwnedRecord]) -> Result<i64, Fault> {
let mut total = 0_i64;
for row in rows {
let projected = forward_owned(row).map_err(|error| error.at("call_read"))?;
total = total
.checked_add(projected)
.ok_or_else(|| Fault::overflow("call_read"))?;
}
Ok(total)
}
fn add(total: i64, value: i64) -> Result<i64, Fault> {
total
.checked_add(value)
.ok_or_else(|| Fault::overflow("add"))
}
fn record_value(row: &Record) -> Result<i64, Fault> {
let partial = row
.first
.checked_add(row.second)
.ok_or_else(|| Fault::overflow("record_value"))?;
partial
.checked_add(row.third)
.ok_or_else(|| Fault::overflow("record_value"))
}
fn record_sum(values: &[Record]) -> Result<i64, Fault> {
let mut total = 0i64;
for row in values {
let projected = record_value(row).map_err(|error| error.at("record_sum"))?;
total = total
.checked_add(projected)
.ok_or_else(|| Fault::overflow("record_sum"))?;
}
Ok(total)
}
fn measure(operation: &str, ticks: &str) -> Result<(i64, Duration), Fault> {
let source = black_box(ticks.as_bytes());
match operation {
"call_read" => {
let rows = source
.iter()
.map(|value| {
let name = format!("row-value-{value}");
OwnedRecord {
count: i64::from(*value) - name.chars().count() as i64 - 1,
tags: vec![name.clone()],
name,
}
})
.collect::<Vec<_>>();
let borrowed = black_box(&rows);
let begin = Instant::now();
let total = owned_sum(borrowed)?;
black_box(total);
let elapsed = begin.elapsed();
black_box(&rows);
Ok((total, elapsed))
}
"integer" => {
let begin = Instant::now();
let mut total = 0;
for value in source {
total = add(total, i64::from(*value)).map_err(|error| error.at("integer"))?;
}
Ok((black_box(total), begin.elapsed()))
}
"list_record_read" => {
let mut values = Vec::new();
for value in source {
values.push(Record {
first: i64::from(*value),
second: 0,
third: 0,
});
}
let borrowed = black_box(&values);
let begin = Instant::now();
let total = record_sum(borrowed)?;
black_box(total);
drop(values);
Ok((total, begin.elapsed()))
}
_ => panic!("unknown checked kernel"),
}
}
fn verify_errors() {
let fault = owned_sum(&[OwnedRecord {
name: "overflow".into(),
tags: vec!["preserved".into()],
count: i64::MAX,
}])
.unwrap_err();
assert_eq!(fault.message, "Int overflow");
assert_eq!(
fault.locations,
["inspect_owned", "forward_owned", "call_read"]
);
assert!(fault.causes.is_empty());
assert_eq!(add(i64::MAX, 0).unwrap(), i64::MAX);
assert_eq!(add(i64::MIN, 0).unwrap(), i64::MIN);
for (left, right) in [(i64::MAX, 1), (i64::MIN, -1)] {
let fault = add(left, right).unwrap_err().at("integer");
assert_eq!(fault.message, "Int overflow");
assert_eq!(fault.locations, ["add", "integer"]);
assert!(fault.causes.is_empty());
}
let fault = record_sum(&[Record {
first: i64::MAX,
second: 1,
third: 0,
}])
.unwrap_err();
assert_eq!(fault.locations, ["record_value", "record_sum"]);
let fault = record_sum(&[
Record {
first: i64::MAX,
second: 0,
third: 0,
},
Record {
first: 1,
second: 0,
third: 0,
},
])
.unwrap_err();
assert_eq!(fault.locations, ["record_sum"]);
}
fn main() {
let arguments = std::env::args().collect::<Vec<_>>();
if arguments[1] == "--verify-errors" {
verify_errors();
println!("checked overflow and error frames verified");
return;
}
let input: serde_json::Value = serde_json::from_str(&arguments[2]).expect("input object");
let ticks = input["ticks"].as_str().expect("ticks string");
let (operation, count) = arguments[1]
.strip_prefix("continuous_")
.map_or((arguments[1].as_str(), 1), |operation| (operation, 10));
let mut samples = Vec::new();
for _ in 0..count {
match measure(operation, ticks) {
Ok((checksum, elapsed)) => samples
.push(serde_json::json!({"checksum": checksum, "elapsed_ns": elapsed.as_nanos()})),
Err(error) => {
eprintln!("{error:?}");
std::process::exit(1);
}
}
}
let output = if count == 1 {
samples.pop().unwrap()
} else {
serde_json::Value::Array(samples)
};
println!("{output}");
}
+278
View File
@@ -0,0 +1,278 @@
package main
import (
"encoding/json"
"fmt"
"math"
"os"
"runtime"
"strconv"
"strings"
"time"
"unicode/utf8"
)
type input struct {
Ticks string `json:"ticks"`
}
type sample struct {
Elapsed int64 `json:"elapsed_ns"`
Checksum int64 `json:"checksum"`
}
type record struct {
First int64
Second int64
Third int64
}
type ownedRecord struct {
Name string
Tags []string
Count int64
}
func inspectOwned(row ownedRecord) int64 {
return checkedAdd(checkedAdd(row.Count, int64(utf8.RuneCountInString(row.Name))), int64(len(row.Tags)))
}
func forwardOwned(row ownedRecord) int64 {
return inspectOwned(row)
}
func checkedAdd(total, value int64) int64 {
if value > 0 && total > math.MaxInt64-value {
panic("overflow")
}
return total + value
}
func measure(operation string, ticks string) sample {
source := []byte(ticks)
total := int64(0)
elapsed := int64(0)
switch operation {
case "record_update":
var values []int64
count := int64(0)
begin := time.Now()
for _, value := range source {
values = append(values, int64(value))
count = checkedAdd(count, 1)
}
elapsed = time.Since(begin).Nanoseconds()
for _, value := range values {
total = checkedAdd(total, value)
}
total = checkedAdd(total, count) - int64(len(values))
case "record_inspect":
var values []int64
count := int64(0)
begin := time.Now()
for _, value := range source {
count = checkedAdd(int64(len(values)), 1)
values = append(values, int64(value))
}
elapsed = time.Since(begin).Nanoseconds()
for _, value := range values {
total = checkedAdd(total, value)
}
total = checkedAdd(total, count) - int64(len(values))
case "map_lookup":
values := make(map[int64]int64, len(source))
for index, value := range source {
values[int64(index)] = int64(value)
}
index := int64(0)
begin := time.Now()
for range source {
total = checkedAdd(total, values[index])
index = checkedAdd(index, 1)
}
elapsed = time.Since(begin).Nanoseconds()
case "text_equal", "text_contains":
rows := make([][2]string, len(source))
for index, value := range source {
left := "compare-value-" + strconv.Itoa(int(value))
rows[index] = [2]string{left, strings.ToLower(strings.ToUpper(left))}
}
begin := time.Now()
if operation == "text_equal" {
for _, row := range rows {
matched := int64(0)
if row[0] == row[1] {
matched = 120
}
total = checkedAdd(total, matched)
}
} else {
for _, row := range rows {
matched := int64(0)
if strings.Contains(row[0], row[1]) {
matched = 120
}
total = checkedAdd(total, matched)
}
}
elapsed = time.Since(begin).Nanoseconds()
runtime.KeepAlive(rows)
total = checkedAdd(total, int64(len(rows))) - int64(len(source))
case "map_text_lookup":
keys := make([]string, len(source))
values := make(map[string]int64, len(source))
for index, value := range source {
key := "lookup-key-" + strconv.Itoa(index)
keys[index] = key
values[key] = int64(value)
}
begin := time.Now()
for _, key := range keys {
total = checkedAdd(total, values[key])
}
elapsed = time.Since(begin).Nanoseconds()
runtime.KeepAlive(values)
runtime.KeepAlive(keys)
total = checkedAdd(total, int64(len(keys))) - int64(len(source))
case "call_read":
rows := make([]ownedRecord, 0, len(source))
for _, value := range source {
name := "row-value-" + strconv.Itoa(int(value))
rows = append(rows, ownedRecord{name, []string{name}, int64(value) - int64(utf8.RuneCountInString(name)) - 1})
}
begin := time.Now()
for _, row := range rows {
total = checkedAdd(total, forwardOwned(row))
}
elapsed = time.Since(begin).Nanoseconds()
case "float":
floating := 0.0
begin := time.Now()
for _, value := range source {
floating += float64(value)
}
elapsed = time.Since(begin).Nanoseconds()
total = int64(floating)
case "bytes_concat":
chunks := make([][]byte, len(source))
for index := range source {
chunks[index] = source[index : index+1]
}
var output []byte
begin := time.Now()
for _, chunk := range chunks {
output = append(output, chunk...)
}
elapsed = time.Since(begin).Nanoseconds()
for _, value := range output {
total = checkedAdd(total, int64(value))
}
case "text_concat":
chunks := strings.Split(ticks, ",")
begin := time.Now()
var output strings.Builder
for _, chunk := range chunks {
output.WriteString(chunk)
}
joined := output.String()
elapsed = time.Since(begin).Nanoseconds()
for index := 0; index < len(joined); index++ {
total = checkedAdd(total, int64(joined[index]))
}
case "integer":
begin := time.Now()
for _, value := range source {
total = checkedAdd(total, int64(value))
}
elapsed = time.Since(begin).Nanoseconds()
case "list":
var values []int64
begin := time.Now()
for _, value := range source {
values = append(values, int64(value))
}
elapsed = time.Since(begin).Nanoseconds()
for _, value := range values {
total = checkedAdd(total, value)
}
case "map":
values := make(map[int64]int64)
var order []int64
begin := time.Now()
for _, value := range source {
key := int64(len(values))
values[key] = int64(value)
order = append(order, key)
}
elapsed = time.Since(begin).Nanoseconds()
for _, key := range order {
total = checkedAdd(total, values[key])
}
case "list_read":
var values []int64
for _, value := range source {
values = append(values, int64(value))
}
begin := time.Now()
for _, value := range values {
total = checkedAdd(total, value)
}
elapsed = time.Since(begin).Nanoseconds()
case "map_read":
values := make(map[int64]int64)
var order []int64
for _, value := range source {
key := int64(len(values))
values[key] = int64(value)
order = append(order, key)
}
begin := time.Now()
for _, key := range order {
total = checkedAdd(total, values[key])
}
elapsed = time.Since(begin).Nanoseconds()
case "list_record":
var values []record
begin := time.Now()
for _, value := range source {
values = append(values, record{int64(value), 0, 0})
}
elapsed = time.Since(begin).Nanoseconds()
for _, value := range values {
total = checkedAdd(total, checkedAdd(checkedAdd(value.First, value.Second), value.Third))
}
case "list_record_read":
var values []record
for _, value := range source {
values = append(values, record{int64(value), 0, 0})
}
begin := time.Now()
for _, value := range values {
total = checkedAdd(total, checkedAdd(checkedAdd(value.First, value.Second), value.Third))
}
elapsed = time.Since(begin).Nanoseconds()
default:
panic("unknown kernel")
}
return sample{elapsed, total}
}
func main() {
var arguments input
if err := json.Unmarshal([]byte(os.Args[2]), &arguments); err != nil {
panic(err)
}
var result any
if operation, continuous := strings.CutPrefix(os.Args[1], "continuous_"); continuous {
samples := make([]sample, 0, 10)
for range 10 {
samples = append(samples, measure(operation, arguments.Ticks))
}
result = samples
} else {
result = measure(os.Args[1], arguments.Ticks)
}
output, err := json.Marshal(result)
if err != nil {
panic(err)
}
fmt.Println(string(output))
}
+306
View File
@@ -0,0 +1,306 @@
use indexmap::IndexMap;
use std::hint::black_box;
use std::time::Instant;
struct Record {
first: i64,
second: i64,
third: i64,
}
struct OwnedRecord {
name: String,
tags: Vec<String>,
count: i64,
}
fn inspect_owned(row: &OwnedRecord) -> i64 {
add(
add(row.count, row.name.chars().count() as i64),
row.tags.len() as i64,
)
}
fn forward_owned(row: &OwnedRecord) -> i64 {
inspect_owned(row)
}
fn add(total: i64, value: i64) -> i64 {
total.checked_add(value).expect("integer overflow")
}
fn record_value(row: &Record) -> i64 {
add(add(row.first, row.second), row.third)
}
fn measure(operation: &str, ticks: &str) -> serde_json::Value {
let source = black_box(ticks.as_bytes());
let (checksum, elapsed) = match operation {
"record_update" => {
let begin = Instant::now();
let mut values = Vec::new();
let mut count = 0;
for value in source {
values.push(i64::from(*value));
count = add(count, 1);
}
black_box((&values, count));
let elapsed = begin.elapsed();
let total = values.iter().fold(0, |total, value| add(total, *value));
let checksum = add(total, count)
.checked_sub(values.len() as i64)
.expect("integer overflow");
(checksum, elapsed)
}
"record_inspect" => {
let begin = Instant::now();
let mut values = Vec::new();
let mut count = 0;
for value in source {
count = add(values.len() as i64, 1);
values.push(i64::from(*value));
}
black_box((&values, count));
let elapsed = begin.elapsed();
let total = values.iter().fold(0, |total, value| add(total, *value));
let checksum = add(total, count)
.checked_sub(values.len() as i64)
.expect("integer overflow");
(checksum, elapsed)
}
"map_lookup" => {
let values = source
.iter()
.enumerate()
.map(|(index, value)| (index as i64, i64::from(*value)))
.collect::<IndexMap<_, _>>();
let borrowed = black_box(&values);
let mut index = 0;
let mut total = 0;
let begin = Instant::now();
for _ in source {
total = add(total, borrowed.get(&index).copied().unwrap_or(0));
index = add(index, 1);
}
black_box(total);
let elapsed = begin.elapsed();
black_box(&values);
(total, elapsed)
}
"text_equal" | "text_contains" => {
let rows = source
.iter()
.map(|value| {
let left = format!("compare-value-{value}");
let right = left.to_uppercase().to_lowercase();
(left, right)
})
.collect::<Vec<_>>();
let borrowed = black_box(&rows);
let mut total = 0;
let begin = Instant::now();
if operation == "text_equal" {
for (left, right) in borrowed {
total = add(total, if left == right { 120 } else { 0 });
}
} else {
for (left, right) in borrowed {
total = add(
total,
if left.contains(right.as_str()) {
120
} else {
0
},
);
}
}
black_box(total);
let elapsed = begin.elapsed();
black_box(&rows);
let checksum = add(total, rows.len() as i64)
.checked_sub(source.len() as i64)
.expect("integer overflow");
(checksum, elapsed)
}
"map_text_lookup" => {
// 与 Dever 使用相同的拥有键和插入顺序;HashMap<&str, _> 是不同的数据结构。
let values = source
.iter()
.enumerate()
.map(|(index, value)| (format!("lookup-key-{index}"), i64::from(*value)))
.collect::<IndexMap<_, _>>();
let keys = values.keys().cloned().collect::<Vec<_>>();
let borrowed = black_box(&values);
let mut total = 0;
let begin = Instant::now();
for key in &keys {
total = add(total, borrowed.get(key.as_str()).copied().unwrap_or(0));
}
black_box(total);
let elapsed = begin.elapsed();
black_box((&keys, &values));
let checksum = add(total, keys.len() as i64)
.checked_sub(source.len() as i64)
.expect("integer overflow");
(checksum, elapsed)
}
"call_read" => {
let rows = source
.iter()
.map(|value| {
let name = format!("row-value-{value}");
OwnedRecord {
count: i64::from(*value) - name.chars().count() as i64 - 1,
tags: vec![name.clone()],
name,
}
})
.collect::<Vec<_>>();
let borrowed = black_box(&rows);
let begin = Instant::now();
let total = borrowed
.iter()
.fold(0, |total, row| add(total, forward_owned(row)));
black_box(total);
let elapsed = begin.elapsed();
black_box(&rows);
(total, elapsed)
}
"float" => {
let begin = Instant::now();
let total = source
.iter()
.fold(0.0, |total, value| total + f64::from(*value));
black_box(total);
let elapsed = begin.elapsed();
(total as i64, elapsed)
}
"bytes_concat" => {
let chunks = source.chunks(1).map(<[u8]>::to_vec).collect::<Vec<_>>();
let begin = Instant::now();
let mut output = Vec::new();
for chunk in &chunks {
output.extend_from_slice(chunk);
}
black_box(&output);
let elapsed = begin.elapsed();
drop(chunks);
let total = output
.iter()
.fold(0, |total, byte| add(total, i64::from(*byte)));
(total, elapsed)
}
"text_concat" => {
let chunks = std::str::from_utf8(source)
.unwrap()
.split(',')
.map(str::to_owned)
.collect::<Vec<_>>();
let begin = Instant::now();
let mut output = String::new();
for chunk in &chunks {
output.push_str(chunk);
}
black_box(&output);
let elapsed = begin.elapsed();
drop(chunks);
let total = output
.bytes()
.fold(0, |total, byte| add(total, i64::from(byte)));
(total, elapsed)
}
"integer" => {
let begin = Instant::now();
let total = source
.iter()
.fold(0, |total, value| add(total, i64::from(*value)));
(black_box(total), begin.elapsed())
}
"list" | "list_read" => {
let mut values = Vec::new();
let begin = Instant::now();
values.extend(source.iter().map(|value| i64::from(*value)));
let construction = begin.elapsed();
let borrowed = black_box(&values);
let begin = Instant::now();
let total = borrowed.iter().fold(0, |total, value| add(total, *value));
black_box(total);
let traversal = begin.elapsed();
drop(values);
(
total,
if operation == "list" {
construction
} else {
traversal
},
)
}
"list_record" | "list_record_read" => {
let mut values = Vec::new();
let begin = Instant::now();
for value in source {
values.push(Record {
first: i64::from(*value),
second: 0,
third: 0,
});
}
let construction = begin.elapsed();
let borrowed = black_box(&values);
let begin = Instant::now();
let total = borrowed
.iter()
.fold(0, |total, row| add(total, record_value(row)));
black_box(total);
let traversal = begin.elapsed();
drop(values);
(
total,
if operation == "list_record" {
construction
} else {
traversal
},
)
}
"map" | "map_read" => {
let mut values = IndexMap::new();
let begin = Instant::now();
for value in source {
let key = i64::try_from(values.len()).expect("map size");
values.insert(key, i64::from(*value));
}
let construction = begin.elapsed();
let borrowed = black_box(&values);
let begin = Instant::now();
let total = borrowed.values().fold(0, |total, value| add(total, *value));
black_box(total);
drop(values);
let traversal = begin.elapsed();
(
total,
if operation == "map" {
construction
} else {
traversal
},
)
}
_ => panic!("unknown kernel"),
};
serde_json::json!({"checksum": checksum, "elapsed_ns": elapsed.as_nanos()})
}
fn main() {
let arguments = std::env::args().collect::<Vec<_>>();
let input: serde_json::Value = serde_json::from_str(&arguments[2]).expect("input object");
let ticks = input["ticks"].as_str().expect("ticks string");
let output = if let Some(operation) = arguments[1].strip_prefix("continuous_") {
serde_json::Value::Array((0..10).map(|_| measure(operation, ticks)).collect())
} else {
measure(&arguments[1], ticks)
};
println!("{output}");
}
@@ -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))
}
tcpChunk(event: dever.io.ReadEvent.Chunk(bytes), socket: dever.system.Socket) () recover("TCP 连接错误由运行器检查") {
written(result(dever.net.write(socket, bytes)))
}
tcpChunk(event: dever.io.ReadEvent.Failed(message), socket: dever.system.Socket) () recover("TCP 连接错误由运行器检查") {
blocking(dever.io.println("ERROR|" + message))
}
tcpStream(
result: dever.net.ReadStreamResult.Streaming(stream),
socket: dever.system.Socket
) () {
each(tcpChunk, stream, socket)
}
tcpStream(
result: dever.net.ReadStreamResult.Failed(message),
socket: dever.system.Socket
) () {
fail(app.WorkerFailure.Failed(message))
}
tcpConnection(
event: dever.net.ConnectResult.Connected(socket),
limits: dever.http.Limits
) () recover("TCP 连接错误由运行器检查") {
written(result(dever.net.timeout(socket, limits.timeout_ms)))
tcpStream(result(dever.net.chunks(socket, 4096)), socket)
closed(result(dever.net.close(socket)))
}
tcpConnection(
event: dever.net.ConnectResult.Failed(message),
limits: dever.http.Limits
) () recover("TCP 连接错误由运行器检查") {
blocking(dever.io.println("ERROR|" + message))
}
tcpServer(listener: dever.system.Listener, limits: dever.http.Limits) () {
stream = dever.net.connections(listener)
parallelEach(tcpConnection, stream, limits.connections, limits)
}
websocketMessage(
event: dever.websocket.MessageEvent.Read(message),
socket: dever.system.WebSocket
) () recover("WebSocket 连接错误由运行器检查") {
websocketEcho(message, socket)
}
websocketMessage(
event: dever.websocket.MessageEvent.Failed(message),
socket: dever.system.WebSocket
) () recover("WebSocket 连接错误由运行器检查") {
blocking(dever.io.println("ERROR|" + message))
}
websocketEcho(
message: dever.websocket.Message.Text(value),
socket: dever.system.WebSocket
) () recover("WebSocket 连接错误由运行器检查") {
written(result(dever.websocket.send(socket, dever.websocket.Message.Text(value))))
}
websocketEcho(
message: dever.websocket.Message.Binary(value),
socket: dever.system.WebSocket
) () recover("WebSocket 连接错误由运行器检查") {
written(result(dever.websocket.send(socket, dever.websocket.Message.Binary(value))))
}
websocketEcho(message: other, socket: dever.system.WebSocket) () recover("WebSocket 连接错误由运行器检查") {}
websocketLimits() (result: dever.websocket.Limits) {
result = dever.websocket.Limits {
message_bytes = 4096
idle_ms = setting.lifetime_ms
write_ms = setting.limits.timeout_ms
}
}
websocketConnection(result: dever.websocket.ConnectResult.Connected(socket)) () {
each(websocketMessage, dever.websocket.messages(socket), socket)
}
websocketConnection(result: dever.websocket.ConnectResult.Failed(message)) () {
fail(app.WorkerFailure.Failed(message))
}
route(request: dever.http.Request, reply: dever.system.HttpReply) () {
livePath(request.target, reply)
}
livePath(target: "/ws", reply: dever.system.HttpReply) () recover("实时连接错误由运行器检查") {
websocketConnection(result(dever.websocket.accept(reply, websocketLimits())))
}
livePath(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)))
}
livePath(target: other, reply: dever.system.HttpReply) () recover("实时连接错误由运行器检查") {
written(
result(
dever.http.respond(
reply,
dever.http.Response {
status = 404
headers = []
body = dever.bytes.fromText("unknown live benchmark path")
}
)
)
)
}
liveLimits() (result: dever.http.LiveLimits) {
result = dever.http.LiveLimits {
chunk_bytes = 4096
idle_ms = setting.lifetime_ms
heartbeat_ms = 100
}
}
liveServer(listener: dever.system.Listener) () {
dever.system.httpServeLive(route, listener, setting.limits, liveLimits())
}
serve(service: Service.Tcp, listener: dever.system.Listener) () {
tcpServer(listener, setting.limits)
}
serve(service: Service.Live, listener: dever.system.Listener) () {
liveServer(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.toText(port)))
paused(result(dever.task.sleep(setting.lifetime_ms)))
stop(server)
closed(result(dever.net.closeListener(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))
}
populateOne(value: Int) () {
created = model.create({
value = value
label = "fixture"
})
}
populate() () {
each(populateOne, config.ormRows())
}
crudOnce(value: Int) () {
begin = dever.time.monotonicNanos()
created = model.create({
value = value
label = "created"
})
changed = model.update(created.id, { label = "updated" })
removed = model.delete(created.id)
end = dever.time.monotonicNanos()
blocking(bench.report(begin, end, "crud", 1, changed + removed))
}
listOnce(value: Int) () {
begin = dever.time.monotonicNanos()
page = model.list({
order = id.asc
page = 1
size = 32
})
end = dever.time.monotonicNanos()
blocking(bench.report(begin, end, "list", 1, page.total + length(page.items)))
}
cursorOnce(value: Int) () {
begin = dever.time.monotonicNanos()
page = model.cursor({
order = id.asc
size = 32
})
end = dever.time.monotonicNanos()
blocking(bench.report(begin, end, "cursor", 1, length(page.items)))
}
countItem(value: model.Item, total: Int) (next: Int) {
next = total + 1
}
streamOnce(value: Int) () {
begin = dever.time.monotonicNanos()
rows = model.stream({
order = id.asc
size = 16
})
total = reduce(countItem, rows, 0)
end = dever.time.monotonicNanos()
blocking(bench.report(begin, end, "stream", 1, total))
}
poolOnce(value: Int) () {
begin = dever.time.monotonicNanos()
total = model.count()
end = dever.time.monotonicNanos()
blocking(bench.report(begin, end, "pool", 1, total))
}
transaction fillCancelledTransaction() () {
created = model.create({
value = 1
label = "cancelled"
})
delayed = model.delay(config.ormCancelWork())
}
ormIdle() (result: Bool) {
total = model.count()
blocking(bench.ready(total))
bench.pause(config.lifetime())
result = true
}
ormCrud() (result: Bool) {
ready()
each(crudOnce, config.ormInput())
result = true
}
ormList() (result: Bool) {
populate()
ready()
each(listOnce, config.ormInput())
result = true
}
ormCursor() (result: Bool) {
populate()
ready()
each(cursorOnce, config.ormInput())
result = true
}
ormStream() (result: Bool) {
ready()
each(streamOnce, config.ormInput())
result = true
}
ormPool() (result: Bool) {
ready()
parallelEach(poolOnce, config.ormInput(), config.ormConcurrency())
result = true
}
ormSeed() (result: Bool) {
populate()
result = true
}
count() (total: Int) {
total = model.count()
}
cancel() (result: Bool) {
fillCancelledTransaction()
result = true
}
@@ -0,0 +1,9 @@
public get count = app.count
public post cancel = app.cancel
cmd idle = app.ormIdle
cmd crud = app.ormCrud
cmd list = app.ormList
cmd cursor = app.ormCursor
cmd stream = app.ormStream
cmd pool = app.ormPool
cmd seed = app.ormSeed
@@ -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.blockingPause(millis: Int) () {
dever.time.sleep(millis)
}
port.reportError(message: Text) () {
dever.io.println("ERROR|" + message)
}
@@ -0,0 +1,11 @@
cmd sync_idle = app.syncIdle
cmd async_idle = app.asyncIdle
cmd tasks = app.tasks
cmd tasks_on_worker = app.tasksOnWorker
cmd channels = app.channels
cmd cancel = app.cancel
+132
View File
@@ -0,0 +1,132 @@
type Parking {
ready: Channel<Int>
gate: Channel<Int>
}
syncIdle() (result: Bool) {
bench.ready(0)
port.blockingPause(config.lifetime())
result = true
}
idle() () {
blocking(bench.ready(0))
bench.pause(config.lifetime())
}
asyncIdle() (result: Bool) {
idle()
result = true
}
identity(value: Int) (result: Int) {
result = value
}
taskSum(value: Int, state: Int) (result: Int) {
job = run(identity(value))
result = state + wait(job)
}
sum(value: Int, state: Int) (result: Int) {
result = state + value
}
taskSample(index: Int, input: Bytes) () {
begin = dever.time.monotonicNanos()
checksum = reduce(taskSum, input, 0)
end = dever.time.monotonicNanos()
blocking(bench.report(begin, end, "tasks", length(input), checksum))
}
sendByte(value: Int, output: Channel<Int>) () {
send(output, value)
}
produce(input: Bytes, output: Channel<Int>) () {
each(sendByte, input, output)
close(output)
}
channelSample(index: Int, input: Bytes) () {
output = channel(Int, config.channelCapacity())
begin = dever.time.monotonicNanos()
producer = run(produce(input, output))
checksum = reduce(sum, stream(output), 0)
wait(producer)
end = dever.time.monotonicNanos()
blocking(bench.report(begin, end, "channels", length(input), checksum))
}
waitOne(value: Int, parking: Parking) () {
send(parking.ready, value)
signaled(receive(parking.gate))
}
signaled(value: Int) () {}
signaled(value: null) () {
blocking(port.reportError("channel closed before expected signal"))
}
hold(parking: Parking) () {
parallelEach(waitOne, config.pendingInput(), config.pending(), parking)
}
started(value: Int, ready: Channel<Int>) () {
signaled(receive(ready))
}
cancelWorker() () {
parking = Parking {
ready = channel(Int, config.pending())
gate = channel(Int, 1)
}
job = run(hold(parking))
each(started, config.pendingInput(), parking.ready)
blocking(bench.ready(config.pending()))
bench.pause(config.idleSampleMs())
begin = dever.time.monotonicNanos()
stop(job)
end = dever.time.monotonicNanos()
close(parking.ready)
close(parking.gate)
blocking(bench.report(begin, end, "cancel", config.pending(), config.pending()))
}
taskWorker() () {
input = config.input()
blocking(bench.ready(0))
each(taskSample, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], input)
}
channelWorker() () {
input = config.input()
blocking(bench.ready(0))
each(channelSample, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], input)
}
tasks() (result: Bool) {
taskWorker()
result = true
}
nestedTaskWorker() () {
job = run(taskWorker())
wait(job)
}
tasksOnWorker() (result: Bool) {
nestedTaskWorker()
result = true
}
channels() (result: Bool) {
channelWorker()
result = true
}
cancel() (result: Bool) {
cancelWorker()
result = true
}
@@ -0,0 +1,3 @@
blockingPause(millis: Int) () fails dever.time.SleepResult
reportError(message: Text) () fails dever.io.WriteResult
+196
View File
@@ -0,0 +1,196 @@
"""Compare native Dever JSON operations with Go and Rust; build separately."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import statistics
import subprocess
import tempfile
import time
FIXTURE = Path(__file__).parent / "fixtures/json"
def fingerprint(path):
with path.open("rb") as source:
digest = hashlib.file_digest(source, "sha256").hexdigest()
return {"path": str(path), "bytes": path.stat().st_size, "sha256": digest}
def build(arguments):
output = arguments.output.resolve()
output.mkdir(parents=True, exist_ok=False)
module = output / "module/benchmark/json"
module.mkdir(parents=True)
for name in ("app.dever", "api.dever"):
shutil.copy2(FIXTURE / name, module / name)
(output / "config").mkdir()
(output / "config/setting.json").write_text("{}\n")
artifacts = {}
for label, compiler in (("dever", arguments.compiler),
("dever_before", arguments.baseline_compiler)):
if compiler is None:
continue
binary = output / label
with (output / (label + ".build.log")).open("w") as log:
subprocess.run([compiler, "build", str(output), "--output", str(binary)],
stdout=log, stderr=subprocess.STDOUT, check=True)
artifacts[label] = fingerprint(binary)
artifacts[label]["compiler"] = fingerprint(Path(compiler).resolve())
binary = output / "go"
subprocess.run(["go", "build", "-trimpath", "-ldflags=-s -w", "-o", str(binary),
str(FIXTURE.resolve() / "peer.go")], check=True)
artifacts["go"] = fingerprint(binary)
artifacts["go"]["compiler"] = subprocess.check_output(["go", "version"], text=True).strip()
command = ["cargo", "build", "--offline", "--locked", "--release", "-p", "dever-tests",
"--example", "json-peer", "--message-format=json"]
process = subprocess.run(command, cwd=FIXTURE.resolve().parents[3],
capture_output=True, text=True, check=True, timeout=300)
(output / "rust.build.log").write_text(process.stderr)
executables = [message["executable"] for line in process.stdout.splitlines()
if (message := json.loads(line)).get("reason") == "compiler-artifact"
and message.get("executable") and message["target"]["name"] == "json-peer"]
if len(executables) != 1:
raise RuntimeError("expected exactly one native Rust JSON peer")
binary = output / "rust"
shutil.copy2(executables[0], binary)
artifacts["rust"] = fingerprint(binary)
artifacts["rust"]["compiler"] = subprocess.check_output(["rustc", "--version"], text=True).strip()
artifacts["rust"]["source"] = fingerprint(FIXTURE.resolve() / "peer.rs")
artifacts["rust"]["profile"] = "release: opt-level=3, fat LTO, codegen-units=1"
(output / "manifest.json").write_text(json.dumps(artifacts, indent=2) + "\n")
print(output / "manifest.json")
def measure(command):
with tempfile.TemporaryFile() as stdout, tempfile.TemporaryFile() as stderr:
process = subprocess.Popen(command, stdout=stdout, stderr=stderr, close_fds=False)
deadline = time.monotonic() + 120
high_water = 0
try:
while True:
try:
process_root = Path("/proc") / str(process.pid)
if (process_root / "exe").resolve() == Path(command[0]).resolve():
for line in (process_root / "status").read_text().splitlines():
if line.startswith("VmHWM:"):
high_water = max(high_water, int(line.split()[1]))
except (FileNotFoundError, ProcessLookupError):
pass
waited, status, _ = os.wait4(process.pid, os.WNOHANG)
if waited:
process.returncode = os.waitstatus_to_exitcode(status)
break
if time.monotonic() >= deadline:
raise TimeoutError("benchmark exceeded 120 seconds")
time.sleep(0.005)
finally:
if process.returncode is None:
process.kill()
process.wait()
stdout.seek(0)
stderr.seek(0)
errors = stderr.read().decode()
if process.returncode or errors:
raise RuntimeError(errors or "benchmark failed")
result = json.loads(stdout.read())
if "code" in result:
if result["code"] != 0:
raise RuntimeError("Dever benchmark returned failure")
result = result["data"]
if high_water == 0:
raise RuntimeError("process ended before native RSS could be sampled")
result["sampled_hwm_kib"] = high_water
return result
def run(arguments):
if arguments.output.exists():
raise FileExistsError(arguments.output)
if arguments.cpu is not None:
if arguments.cpu not in os.sched_getaffinity(0):
raise ValueError("requested CPU is outside the allowed affinity")
os.sched_setaffinity(0, {arguments.cpu})
manifest = json.loads(arguments.manifest.read_text())
for artifact in manifest.values():
actual = fingerprint(Path(artifact["path"]))
if any(actual[field] != artifact[field] for field in ("bytes", "sha256")):
raise ValueError("benchmark artifact changed after build")
report = {"artifacts": manifest, "cpu_affinity": sorted(os.sched_getaffinity(0)),
"order": "rotate implementations each repeat within each workload",
"samples": [], "summary": []}
row = {"zeta": "field", "id": 9007199254740993, "alpha": "x" * 80,
"enabled": True, "values": [1, None, "escaped\nvalue"]}
for rows in (4, 40, 160):
source = {"zeta": {"caption": "form"}, "data": {"rows": [row] * rows},
"active": False, "alpha": "end"}
payload_bytes = len(json.dumps(source, separators=(",", ":")).encode())
for update in (False, True):
expected = dict(source, active=True) if update else source["data"]
groups = {label: [] for label in manifest}
labels = list(manifest)
for repeat in range(arguments.repeats):
offset = repeat % len(labels)
for label in labels[offset:] + labels[:offset]:
artifact = manifest[label]
iterations = (arguments.baseline_iterations if label == "dever_before"
else arguments.iterations)
payload = json.dumps({"source": source, "update": update,
"ticks": "x" * iterations}, separators=(",", ":"))
command = [artifact["path"]]
if label.startswith("dever"):
command.append("benchmark.json.run")
sample = measure(command + [payload])
if (json.loads(sample["output"]) != expected or
sample["checksum"] != len(sample["output"].encode()) * iterations or
sample["elapsed_ns"] <= 0):
raise AssertionError("JSON output or checksum mismatch")
sample.pop("output")
sample.update(label=label, update=update, bytes=payload_bytes,
iterations=iterations, repeat=repeat,
us_per_operation=sample["elapsed_ns"] / iterations / 1000)
groups[label].append(sample)
report["samples"].append(sample)
for label, samples in groups.items():
summary = {"label": label, "operation": "set" if update else "get",
"bytes": payload_bytes,
"median_us": statistics.median(item["us_per_operation"] for item in samples),
"sampled_hwm_kib": max(item["sampled_hwm_kib"] for item in samples)}
report["summary"].append(summary)
print(json.dumps(summary), flush=True)
with arguments.output.open("x") as output:
json.dump(report, output, indent=2)
output.write("\n")
def positive(value):
parsed = int(value)
if not 1 <= parsed <= 10000:
raise argparse.ArgumentTypeError("expected 1..10000")
return parsed
def main():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
building = commands.add_parser("build")
building.add_argument("--compiler", required=True)
building.add_argument("--baseline-compiler")
building.add_argument("--output", type=Path, required=True)
running = commands.add_parser("run")
running.add_argument("--manifest", type=Path, required=True)
running.add_argument("--output", type=Path, required=True)
running.add_argument("--iterations", type=positive, default=2000)
running.add_argument("--baseline-iterations", type=positive, default=1)
running.add_argument("--repeats", type=positive, default=5)
running.add_argument("--cpu", type=int)
arguments = parser.parse_args()
(build if arguments.command == "build" else run)(arguments)
if __name__ == "__main__":
main()
+304
View File
@@ -0,0 +1,304 @@
"""Compare current Dever collection kernels with independent Go and Rust programs."""
import argparse
import errno
import hashlib
import json
import os
from pathlib import Path
import shutil
import statistics
import subprocess
import tempfile
import time
FIXTURE = Path(__file__).parent / "fixtures/kernel"
CORE = Path(__file__).resolve().parents[2]
CASES = {"integer": (16384, 65536), "float": (16384, 65536),
"list": (512, 1024, 2048, 4096),
"map": (512, 1024, 2048, 4096),
"list_read": (512, 1024, 2048, 4096),
"list_record": (512, 1024, 2048, 4096),
"list_record_read": (512, 1024, 2048, 4096),
"record_update": (512, 1024, 2048, 4096),
"record_inspect": (512, 1024, 2048, 4096),
"call_read": (512, 1024, 2048, 4096),
"map_read": (512, 1024, 2048, 4096),
"map_lookup": (512, 1024, 2048, 4096),
"map_text_lookup": (512, 1024, 2048, 4096),
"text_equal": (512, 1024, 2048, 4096),
"text_contains": (512, 1024, 2048, 4096),
"text_concat": (512, 2048, 8192, 32768),
"bytes_concat": (512, 2048, 8192, 32768)}
def fingerprint(path):
with path.open("rb") as source:
digest = hashlib.file_digest(source, "sha256").hexdigest()
return {"path": str(path), "bytes": path.stat().st_size, "sha256": digest}
def source_identity():
files = [CORE / "rust" / name for name in
("Cargo.toml", "Cargo.lock", "rust-toolchain.toml")]
for directory in ("rust/src", "lib", "sdk"):
files.extend(path for path in (CORE / directory).rglob("*")
if path.is_file() and path.suffix in
{".rs", ".toml", ".md", ".dever", ".py", ".c", ".h", ".js", ".mjs", ".json", ".go"})
digest = hashlib.sha256()
for path in sorted(files):
digest.update(str(path.relative_to(CORE)).encode() + b"\0")
digest.update(fingerprint(path)["sha256"].encode() + b"\n")
return {"sha256": digest.hexdigest(), "files": len(files)}
def build(arguments):
tree = source_identity()
output = arguments.output.resolve()
output.mkdir(parents=True, exist_ok=False)
module = output / "module/benchmark/kernel"
module.mkdir(parents=True)
for name in ("app.dever", "api.dever"):
shutil.copy2(FIXTURE / name, module / name)
(output / "config").mkdir()
(output / "config/setting.json").write_text("{}\n")
artifacts = {}
for label, compiler in (("dever", arguments.compiler),
("dever_before", arguments.baseline_compiler)):
if compiler is None:
continue
binary = output / label
with (output / (label + ".build.log")).open("w") as log:
subprocess.run([compiler, "build", str(output), "--output", str(binary)],
stdout=log, stderr=subprocess.STDOUT, check=True, timeout=300)
artifacts[label] = fingerprint(binary)
artifacts[label]["compiler"] = fingerprint(Path(compiler).resolve())
artifacts[label]["source_tree"] = tree
generated = output / (label + ".rs")
with generated.open("w") as source:
subprocess.run([compiler, "emit", str(output)], stdout=source,
check=True, timeout=120)
artifacts[label]["generated"] = fingerprint(generated)
binary = output / "go"
subprocess.run(["go", "build", "-trimpath", "-ldflags=-s -w", "-o", str(binary),
str(FIXTURE.resolve() / "peer.go")], check=True, timeout=120)
artifacts["go"] = fingerprint(binary)
artifacts["go"]["compiler"] = subprocess.check_output(["go", "version"], text=True).strip()
peer = output / "rust-peer"
peer.mkdir()
shutil.copy2(FIXTURE / "peer.rs", peer / "main.rs")
core = FIXTURE.resolve().parents[3]
shutil.copy2(core / "rust/rust-toolchain.toml", peer / "rust-toolchain.toml")
(peer / "Cargo.toml").write_text(
'[package]\nname = "kernel-peer"\nversion = "0.0.1"\nedition = "2024"\n'
'[workspace]\n[[bin]]\nname = "kernel-peer"\npath = "main.rs"\n'
'[dependencies]\nserde_json = "=1.0.145"\nindexmap = "=2.14.0"\n'
'[profile.release]\nopt-level = 3\nlto = "fat"\ncodegen-units = 1\n'
'overflow-checks = true\nstrip = "symbols"\n')
subprocess.run(["cargo", "generate-lockfile", "--offline"], cwd=peer,
check=True, timeout=120)
command = ["cargo", "build", "--offline", "--locked", "--release",
"--bin", "kernel-peer", "--message-format=json"]
process = subprocess.run(command, cwd=peer,
capture_output=True, text=True, check=True, timeout=300)
(output / "rust.build.log").write_text(process.stderr)
executables = [message["executable"] for line in process.stdout.splitlines()
if (message := json.loads(line)).get("reason") == "compiler-artifact"
and message.get("executable") and message["target"]["name"] == "kernel-peer"]
if len(executables) != 1:
raise RuntimeError("expected exactly one native Rust peer")
binary = output / "rust"
shutil.copy2(executables[0], binary)
artifacts["rust"] = fingerprint(binary)
artifacts["rust"]["compiler"] = subprocess.check_output(
["rustc", "--version"], cwd=peer, text=True).strip()
artifacts["rust"]["profile"] = "release: opt-level=3, fat LTO, codegen-units=1"
artifacts["rust"]["source"] = fingerprint(peer / "main.rs")
artifacts["rust"]["lock"] = fingerprint(peer / "Cargo.lock")
artifacts["rust"]["map"] = "ordered IndexMap with owned keys, matching Dever"
if source_identity() != tree:
raise RuntimeError("sources changed during build; rebuild the comparison")
(output / "manifest.json").write_text(json.dumps(artifacts, indent=2) + "\n")
print(output / "manifest.json")
def run(arguments):
if arguments.output.exists():
raise FileExistsError(arguments.output)
if arguments.cpu is not None:
if arguments.cpu not in os.sched_getaffinity(0):
raise ValueError("requested CPU is outside the allowed affinity")
os.sched_setaffinity(0, {arguments.cpu})
manifest = json.loads(arguments.manifest.read_text())
if arguments.candidate not in manifest:
raise ValueError("candidate is missing from the manifest")
if arguments.parity_with is not None and arguments.parity_with not in manifest:
raise ValueError("parity comparator is missing from the manifest")
for artifact in manifest.values():
actual = fingerprint(Path(artifact["path"]))
if any(actual[field] != artifact[field] for field in ("bytes", "sha256")):
raise ValueError("benchmark artifact changed after build")
identities = [artifact["source_tree"] for artifact in manifest.values()
if "source_tree" in artifact]
if identities and any(identity != source_identity() for identity in identities):
raise ValueError("current sources differ from the build; rebuild before measuring")
report = {"artifacts": manifest, "cpu_affinity": sorted(os.sched_getaffinity(0)),
"measured_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"manifest": fingerprint(arguments.manifest), "repeats": arguments.repeats,
"samples_per_process": 10 if arguments.continuous else 1,
"measurement": "continuous" if arguments.continuous else "cold",
"samples": [], "growth": {}}
operations = getattr(arguments, "operations", None) or list(CASES)
if len(set(operations)) != len(operations) or any(operation not in CASES for operation in operations):
raise ValueError("kernel selection contains duplicate or unknown operations")
if arguments.check_growth and not any(operation in operations for operation in ("list", "map", "list_record")):
raise ValueError("growth checking requires a collection construction operation")
report["operations"] = operations
with tempfile.TemporaryDirectory(prefix="kernel-run-", dir=arguments.output.resolve().parent) as temporary:
execution = stage_execution(manifest, Path(temporary))
report["execution"] = execution
measure(arguments, report)
def stage_execution(manifest, directory):
for label, artifact in manifest.items():
if label.startswith("dever"):
configuration = Path(artifact["path"]).parent / "config/setting.json"
if json.loads(configuration.read_text()) != {}:
raise ValueError("kernel benchmark requires an empty application configuration")
configuration = directory / "config/setting.json"
configuration.parent.mkdir()
configuration.write_text("{}\n")
artifacts = {}
for index, (label, artifact) in enumerate(manifest.items()):
binary = directory / f"program-{index:04d}"
method = "hardlink"
try:
os.link(artifact["path"], binary)
except OSError as error:
if error.errno != errno.EXDEV:
raise
shutil.copy2(artifact["path"], binary)
method = "copy"
actual = fingerprint(binary)
if any(actual[field] != artifact[field] for field in ("bytes", "sha256")):
raise ValueError("benchmark artifact changed during staging")
artifacts[label] = {**actual, "method": method}
return {"artifacts": artifacts, "configuration": fingerprint(configuration)}
def measure(arguments, report):
manifest = report["artifacts"]
operations = report["operations"]
executables = report["execution"]["artifacts"]
for operation in operations:
sizes = CASES[operation]
for count in sizes:
ticks = ",".join(["x"] * count) if operation == "text_concat" else "x" * count
continuous = arguments.continuous
payload = json.dumps({"ticks": ticks})
timings = {label: [] for label in manifest}
process_timings = {label: [] for label in manifest}
process_elapsed = {label: [] for label in manifest}
for repeat in range(arguments.repeats):
labels = list(manifest)
offset = repeat % len(labels)
labels = labels[offset:] + labels[:offset]
for label in labels:
action = (("benchmark.kernel." if label.startswith("dever") else "")
+ ("continuous_" if continuous else "") + operation)
started = time.perf_counter_ns()
process = subprocess.run([executables[label]["path"], action, payload],
capture_output=True, text=True, timeout=120, check=True)
process_elapsed[label].append((time.perf_counter_ns() - started) / 1_000_000)
if process.stderr:
raise RuntimeError(process.stderr)
value = json.loads(process.stdout)
if label.startswith("dever"):
if value["code"] != 0:
raise RuntimeError("Dever benchmark returned failure")
value = value["data"]
values = value if continuous else [value]
if not isinstance(values, list) or len(values) != (10 if continuous else 1):
raise AssertionError("kernel sample count mismatch")
if any(sample["checksum"] != count * 120 or sample["elapsed_ns"] <= 0
for sample in values):
raise AssertionError("kernel output or checksum mismatch")
measured = [sample["elapsed_ns"] / 1_000_000 for sample in values]
process_timings[label].append(measured)
timings[label].extend(measured[1:] if continuous else measured)
item = {"operation": operation, "count": count, "samples_ms": timings,
"process_samples_ms": process_timings,
"process_elapsed_ms": process_elapsed,
"first_sample_median_ms": {
label: statistics.median(values[0] for values in processes)
for label, processes in process_timings.items()},
"median_ms": {label: statistics.median(values) for label, values in timings.items()}}
report["samples"].append(item)
print(json.dumps(item), flush=True)
for operation in ("list", "map", "list_record"):
if operation not in operations:
continue
samples = [item for item in report["samples"] if item["operation"] == operation]
report["growth"][operation] = {
label: samples[-1]["median_ms"][label] / samples[0]["median_ms"][label]
for label in manifest}
if arguments.parity_with is not None:
cases = [{
"operation": item["operation"],
"count": item["count"],
"ratio": item["median_ms"][arguments.candidate] / item["median_ms"][arguments.parity_with],
"passed": item["median_ms"][arguments.candidate] <= item["median_ms"][arguments.parity_with],
} for item in report["samples"]]
report["parity"] = {
"candidate": arguments.candidate,
"comparator": arguments.parity_with,
"passed": all(item["passed"] for item in cases),
"cases": cases,
}
with arguments.output.open("x") as output:
json.dump(report, output, indent=2)
output.write("\n")
if arguments.check_growth and any(values[arguments.candidate] > 24 for values in report["growth"].values()):
raise AssertionError("8x input exceeded 24x collection construction time; inspect raw samples")
if arguments.parity_with is not None and not report["parity"]["passed"]:
failures = [item for item in report["parity"]["cases"] if not item["passed"]]
raise AssertionError(
f"{len(failures)} of {len(cases)} cases did not reach {arguments.parity_with} parity; "
f"full samples and ratios saved to {arguments.output}"
)
def repeats(value):
parsed = int(value)
if not 3 <= parsed <= 25:
raise argparse.ArgumentTypeError("expected 3..25 repetitions")
return parsed
def main():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
building = commands.add_parser("build")
building.add_argument("--compiler", required=True)
building.add_argument("--baseline-compiler")
building.add_argument("--output", type=Path, required=True)
running = commands.add_parser("run")
running.add_argument("--manifest", type=Path, required=True)
running.add_argument("--output", type=Path, required=True)
running.add_argument("--repeats", type=repeats, default=5)
running.add_argument("--continuous", action="store_true",
help="retain ten calls per process; compare calls 2..10 separately from the first")
running.add_argument("--cpu", type=int)
running.add_argument("--check-growth", action="store_true")
running.add_argument("--operations", nargs="+", choices=tuple(CASES),
help="select diagnostic kernels; omitted runs the complete matrix")
running.add_argument("--parity-with", choices=("go", "rust"))
running.add_argument("--candidate", default="dever")
arguments = parser.parse_args()
(build if arguments.command == "build" else run)(arguments)
if __name__ == "__main__":
main()
+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}
+237
View File
@@ -0,0 +1,237 @@
"""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
except PermissionError as failure:
try:
self.child.wait(timeout=0.1)
except subprocess.TimeoutExpired:
raise failure
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)
+272
View File
@@ -0,0 +1,272 @@
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,
provision_identity, publish_workload, run_postgres_case, run_postgres_rbac)
from settings import write_private_json
class CmsMeasurementTests(unittest.TestCase):
def test_identity_provisioning_uses_current_tenant_commands(self):
identity = {"tenant_id": 19, "user_id": 31, "credential_id": 7}
executable = Path("owned-app")
with patch("cms.execute", side_effect=[identity, None, None, None]) as execute_command:
actual, password = provision_identity(executable, Path("owned-output"), "example")
self.assertEqual(actual, identity)
self.assertTrue(password)
calls = execute_command.call_args_list
self.assertEqual(calls[0].args[0][1], "user.account.bootstrap")
self.assertEqual(calls[1].args[0],
[executable, "--tenant", 19, "dever.tenant.migrate", "null"])
for call, site in zip(calls[2:], ("admin", "front")):
self.assertEqual(call.args[0][:4],
[executable, "--tenant", 19, "dever.auth.owner"])
self.assertEqual(json.loads(call.args[0][4]), {"site": site, "user_id": 31})
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.assertEqual(staged["sites"]["admin"]["hosts"], ["127.0.0.1"])
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()
File diff suppressed because it is too large. Load diff