Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,258 @@
|
||||
use dever_runtime::wire::serde::{self, Deserialize, Serialize};
|
||||
use dever_runtime::{
|
||||
api::{Request, Response},
|
||||
task::{Limits, Runtime, Scope},
|
||||
};
|
||||
use std::future::Future;
|
||||
|
||||
struct Input<'scope> {
|
||||
request: Request,
|
||||
peer: std::net::SocketAddr,
|
||||
scope: &'scope Scope<'scope>,
|
||||
}
|
||||
|
||||
fn fault(_: impl std::fmt::Display) -> Fault {
|
||||
&FaultInfo {
|
||||
message: "HTTP input failure",
|
||||
source: "api-reference",
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// 此样例不挂起;直接轮询实际生成入口,计时不含网络与调度器启动。
|
||||
fn generated(values: &[Input<'_>]) -> Result<Vec<u8>, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let future = async {
|
||||
if value.request.uri().path() == "/bench/wire/count" {
|
||||
GENERATED_LIST(&value.request, value.peer, value.scope).await
|
||||
} else {
|
||||
GENERATED_HTTP(&value.request, value.peer, value.scope).await
|
||||
}
|
||||
};
|
||||
let mut future = std::pin::pin!(future);
|
||||
let mut context = std::task::Context::from_waker(std::task::Waker::noop());
|
||||
let std::task::Poll::Ready(response) = future.as_mut().poll(&mut context) else {
|
||||
panic!("pure HTTP fixture unexpectedly suspended");
|
||||
};
|
||||
if response.status() != 200 {
|
||||
return Err(fault(response.status()));
|
||||
}
|
||||
Ok(response.into_body().into())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde", deny_unknown_fields)]
|
||||
struct Payload {
|
||||
text: String,
|
||||
value: i64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde", deny_unknown_fields)]
|
||||
struct Arguments {
|
||||
payload: Object<Payload>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde", deny_unknown_fields)]
|
||||
struct ListArguments {
|
||||
values: Integers,
|
||||
}
|
||||
|
||||
struct Integers(Vec<i64>);
|
||||
|
||||
impl<'input> Deserialize<'input> for Integers {
|
||||
fn deserialize<Decoder: serde::Deserializer<'input>>(
|
||||
decoder: Decoder,
|
||||
) -> Result<Self, Decoder::Error> {
|
||||
struct Visitor;
|
||||
impl<'input> serde::de::Visitor<'input> for Visitor {
|
||||
type Value = Integers;
|
||||
fn expecting(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
output.write_str("an integer array")
|
||||
}
|
||||
fn visit_seq<Access: serde::de::SeqAccess<'input>>(
|
||||
self,
|
||||
mut access: Access,
|
||||
) -> Result<Integers, Access::Error> {
|
||||
let mut values = Vec::new();
|
||||
while let Some(value) = access.next_element()? {
|
||||
// 根对象和数组各占一个值;检查与 Serde 解码在同一次遍历中完成。
|
||||
if values.len() == 65536 - 2 {
|
||||
return Err(serde::de::Error::custom("value limit"));
|
||||
}
|
||||
values.push(value);
|
||||
}
|
||||
Ok(Integers(values))
|
||||
}
|
||||
}
|
||||
decoder.deserialize_seq(Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct Object<Value>(Value);
|
||||
|
||||
impl<'input, Value: Deserialize<'input>> Deserialize<'input> for Object<Value> {
|
||||
fn deserialize<Decoder: serde::Deserializer<'input>>(
|
||||
decoder: Decoder,
|
||||
) -> Result<Self, Decoder::Error> {
|
||||
struct Visitor<Value>(std::marker::PhantomData<Value>);
|
||||
impl<'input, Value: Deserialize<'input>> serde::de::Visitor<'input> for Visitor<Value> {
|
||||
type Value = Value;
|
||||
fn expecting(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
output.write_str("an object")
|
||||
}
|
||||
fn visit_map<Access: serde::de::MapAccess<'input>>(
|
||||
self,
|
||||
access: Access,
|
||||
) -> Result<Value, Access::Error> {
|
||||
Value::deserialize(serde::de::value::MapAccessDeserializer::new(access))
|
||||
}
|
||||
}
|
||||
decoder
|
||||
.deserialize_map(Visitor(std::marker::PhantomData))
|
||||
.map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
// 独立使用 Serde;固定记录保证结构预算,字节预算在唯一输入/输出边界处理。
|
||||
fn reference(values: &[Input<'_>]) -> Result<Vec<u8>, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let request = &value.request;
|
||||
let mut headers = request.headers().get_all("content-type").iter();
|
||||
let content = headers.next().ok_or_else(|| fault("content type"))?;
|
||||
let content = std::str::from_utf8(content.as_bytes()).map_err(fault)?;
|
||||
if !content
|
||||
.split(';')
|
||||
.next()
|
||||
.unwrap()
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("application/json")
|
||||
|| headers.next().is_some()
|
||||
{
|
||||
return Err(fault("content type"));
|
||||
}
|
||||
if request.body().len() > 16 * 1024 * 1024 {
|
||||
return Err(fault("input size"));
|
||||
}
|
||||
let text = std::str::from_utf8(request.body()).map_err(fault)?;
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde")]
|
||||
struct Envelope<Value> {
|
||||
code: u16,
|
||||
message: &'static str,
|
||||
data: Value,
|
||||
}
|
||||
struct Output(Vec<u8>);
|
||||
impl std::io::Write for Output {
|
||||
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
|
||||
if bytes.len() > 16 * 1024 * 1024 - self.0.len() {
|
||||
return Err(std::io::Error::other("output size"));
|
||||
}
|
||||
self.0.extend_from_slice(bytes);
|
||||
Ok(bytes.len())
|
||||
}
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
let mut output = Output(Vec::with_capacity(128));
|
||||
if request.uri().path() == "/bench/wire/count" {
|
||||
let arguments: Object<ListArguments> = serde_json::from_str(text).map_err(fault)?;
|
||||
serde_json::to_writer(
|
||||
&mut output,
|
||||
&Envelope {
|
||||
code: 0,
|
||||
message: "ok",
|
||||
data: arguments.0.values.0.len() as i64,
|
||||
},
|
||||
)
|
||||
.map_err(fault)?;
|
||||
} else {
|
||||
let arguments: Object<Arguments> = serde_json::from_str(text).map_err(fault)?;
|
||||
serde_json::to_writer(
|
||||
&mut output,
|
||||
&Envelope {
|
||||
code: 0,
|
||||
message: "ok",
|
||||
data: arguments.0.payload.0,
|
||||
},
|
||||
)
|
||||
.map_err(fault)?;
|
||||
}
|
||||
let mut response = Response::new(output.0.into());
|
||||
response.headers_mut().insert(
|
||||
hyper::header::CONTENT_TYPE,
|
||||
hyper::header::HeaderValue::from_static("application/json; charset=utf-8"),
|
||||
);
|
||||
Ok(response.into_body().into())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
let runtime = Runtime::new(Limits {
|
||||
threads: 1,
|
||||
workers: 1,
|
||||
tasks: 8,
|
||||
})
|
||||
.unwrap();
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
for size in [64, 4096, 65536] {
|
||||
let payload = Payload {
|
||||
text: "雪\\\"".repeat(size / 5),
|
||||
value: 42,
|
||||
};
|
||||
let body = format!(
|
||||
"{{\"payload\":{}}}",
|
||||
serde_json::to_string(&payload).unwrap()
|
||||
)
|
||||
.into_bytes();
|
||||
let values = [Input {
|
||||
request: hyper::Request::builder()
|
||||
.method("POST")
|
||||
.uri("/bench/wire/echo")
|
||||
.header("content-type", "application/json")
|
||||
.body(body.into())
|
||||
.unwrap(),
|
||||
peer: ([127, 0, 0, 1], 12345).into(),
|
||||
scope,
|
||||
}];
|
||||
compare(
|
||||
&format!("http_fixed_record_{size}"),
|
||||
&values,
|
||||
200,
|
||||
generated,
|
||||
reference,
|
||||
|body| body.len() as u64,
|
||||
);
|
||||
}
|
||||
for size in [16, 1024, 8192, 32768] {
|
||||
let body = format!("{{\"values\":[{}]}}", vec!["42"; size].join(",")).into_bytes();
|
||||
let values = [Input {
|
||||
request: hyper::Request::builder()
|
||||
.method("POST")
|
||||
.uri("/bench/wire/count")
|
||||
.header("content-type", "application/json")
|
||||
.body(body.into())
|
||||
.unwrap(),
|
||||
peer: ([127, 0, 0, 1], 12345).into(),
|
||||
scope,
|
||||
}];
|
||||
compare(
|
||||
&format!("http_integer_list_{size}"),
|
||||
&values,
|
||||
200,
|
||||
generated,
|
||||
reference,
|
||||
|body| body.len() as u64,
|
||||
);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
use dever_runtime::wire::{
|
||||
self,
|
||||
serde::{self, Deserialize, Serialize},
|
||||
};
|
||||
|
||||
struct Object<Value>(Value);
|
||||
|
||||
impl<'input, Value: Deserialize<'input>> Deserialize<'input> for Object<Value> {
|
||||
fn deserialize<Decoder: serde::Deserializer<'input>>(
|
||||
decoder: Decoder,
|
||||
) -> Result<Self, Decoder::Error> {
|
||||
wire::object(decoder).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde", deny_unknown_fields)]
|
||||
struct Greeting {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde", deny_unknown_fields)]
|
||||
struct Integer {
|
||||
value: i64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde", deny_unknown_fields)]
|
||||
struct Profile {
|
||||
id: Id,
|
||||
name: String,
|
||||
scores: Vec<i64>,
|
||||
ratio: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde", deny_unknown_fields)]
|
||||
struct Echo {
|
||||
value: Object<Profile>,
|
||||
}
|
||||
|
||||
fn fault(_: String) -> Fault {
|
||||
&FaultInfo {
|
||||
message: "command failure",
|
||||
source: "reference.dever",
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn greeting_generated(values: &[&str]) -> Result<String, Fault> {
|
||||
GENERATED_GREETING(values.first().copied().unwrap_or(r#"{"name":""}"#)).map_err(fault)
|
||||
}
|
||||
|
||||
fn greeting_reference(values: &[&str]) -> Result<String, Fault> {
|
||||
let input: Object<Greeting> =
|
||||
wire::decode(values.first().copied().unwrap_or(r#"{"name":""}"#)).map_err(fault)?;
|
||||
let mut result = String::with_capacity(input.0.name.len() + 9);
|
||||
result.push_str("[Hello, ");
|
||||
result.push_str(&input.0.name);
|
||||
result.push(']');
|
||||
wire::response(&result).map_err(fault)
|
||||
}
|
||||
|
||||
fn echo_generated(values: &[&str]) -> Result<String, Fault> {
|
||||
GENERATED_ECHO(
|
||||
values
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(r#"{"value":{"id":"","name":"","scores":[]}}"#),
|
||||
)
|
||||
.map_err(fault)
|
||||
}
|
||||
|
||||
fn echo_reference(values: &[&str]) -> Result<String, Fault> {
|
||||
let input: Object<Echo> = wire::decode(
|
||||
values
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(r#"{"value":{"id":"","name":"","scores":[]}}"#),
|
||||
)
|
||||
.map_err(fault)?;
|
||||
let result = input.0.value.0;
|
||||
if result.ratio.is_some_and(|value| !value.is_finite()) {
|
||||
return Err(fault("non-finite Float".into()));
|
||||
}
|
||||
wire::response(&result).map_err(fault)
|
||||
}
|
||||
|
||||
fn double_generated(values: &[&str]) -> Result<String, Fault> {
|
||||
GENERATED_DOUBLE(values.first().copied().unwrap_or(r#"{"value":0}"#)).map_err(fault)
|
||||
}
|
||||
|
||||
fn double_reference(values: &[&str]) -> Result<String, Fault> {
|
||||
let input: Object<Integer> =
|
||||
wire::decode(values.first().copied().unwrap_or(r#"{"value":0}"#)).map_err(fault)?;
|
||||
let result = input
|
||||
.0
|
||||
.value
|
||||
.checked_mul(2)
|
||||
.ok_or_else(|| fault("overflow".into()))?;
|
||||
wire::response(&result).map_err(fault)
|
||||
}
|
||||
|
||||
// 独立对照只处理这里的固定标量 record;结构预算由这两个协议本身保证。
|
||||
// 不调用 Dever 的 object/decode/response,也不替换原来的共享运行库对照。
|
||||
fn rust_input<Value: serde::de::DeserializeOwned>(input: &str) -> Result<Value, Fault> {
|
||||
if input.len() > 16 * 1024 * 1024 {
|
||||
return Err(fault("input limit".into()));
|
||||
}
|
||||
struct Record<Value>(std::marker::PhantomData<Value>);
|
||||
impl<'input, Value: Deserialize<'input>> serde::de::Visitor<'input> for Record<Value> {
|
||||
type Value = Value;
|
||||
fn expecting(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
output.write_str("an object")
|
||||
}
|
||||
fn visit_map<Access: serde::de::MapAccess<'input>>(
|
||||
self,
|
||||
access: Access,
|
||||
) -> Result<Value, Access::Error> {
|
||||
Value::deserialize(serde::de::value::MapAccessDeserializer::new(access))
|
||||
}
|
||||
}
|
||||
let mut decoder = serde_json::Deserializer::from_str(input);
|
||||
let value =
|
||||
serde::Deserializer::deserialize_map(&mut decoder, Record(std::marker::PhantomData))
|
||||
.map_err(|error| fault(error.to_string()))?;
|
||||
decoder.end().map_err(|error| fault(error.to_string()))?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn rust_response<Value: Serialize>(data: Value) -> Result<String, Fault> {
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde")]
|
||||
struct Response<Value> {
|
||||
code: u16,
|
||||
message: &'static str,
|
||||
data: Value,
|
||||
}
|
||||
struct Buffer(Vec<u8>);
|
||||
impl std::io::Write for Buffer {
|
||||
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
|
||||
if bytes.len() > 16 * 1024 * 1024 - self.0.len() {
|
||||
return Err(std::io::Error::other("output limit"));
|
||||
}
|
||||
self.0.extend_from_slice(bytes);
|
||||
Ok(bytes.len())
|
||||
}
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// 与 serde_json::to_vec 相同;独立 Rust 参照也避免短输出反复扩容。
|
||||
let mut output = Buffer(Vec::with_capacity(128));
|
||||
serde_json::to_writer(
|
||||
&mut output,
|
||||
&Response {
|
||||
code: 0,
|
||||
message: "ok",
|
||||
data,
|
||||
},
|
||||
)
|
||||
.map_err(|error| fault(error.to_string()))?;
|
||||
Ok(String::from_utf8(output.0).unwrap())
|
||||
}
|
||||
|
||||
fn greeting_rust(values: &[&str]) -> Result<String, Fault> {
|
||||
let input: Greeting = rust_input(values.first().copied().unwrap_or(r#"{"name":""}"#))?;
|
||||
let mut result = String::with_capacity(input.name.len() + 9);
|
||||
result.push_str("[Hello, ");
|
||||
result.push_str(&input.name);
|
||||
result.push(']');
|
||||
rust_response(result)
|
||||
}
|
||||
|
||||
fn double_rust(values: &[&str]) -> Result<String, Fault> {
|
||||
let input: Integer = rust_input(values.first().copied().unwrap_or(r#"{"value":0}"#))?;
|
||||
rust_response(
|
||||
input
|
||||
.value
|
||||
.checked_mul(2)
|
||||
.ok_or_else(|| fault("overflow".into()))?,
|
||||
)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
for input in [
|
||||
r#"{"value":9223372036854775807}"#,
|
||||
r#"{"value":1,"value":2}"#,
|
||||
"[1]",
|
||||
"{}",
|
||||
] {
|
||||
assert_eq!(
|
||||
double_generated(&[input]).is_err(),
|
||||
double_reference(&[input]).is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
double_generated(&[input]).is_err(),
|
||||
double_rust(&[input]).is_err()
|
||||
);
|
||||
}
|
||||
for input in [
|
||||
r#"{"value":["a","b",[],null]}"#,
|
||||
r#"{"value":{"id":"a","name":"b","scores":[],"extra":1}}"#,
|
||||
] {
|
||||
assert_eq!(
|
||||
echo_generated(&[input]).is_err(),
|
||||
echo_reference(&[input]).is_err()
|
||||
);
|
||||
}
|
||||
let long_name = "雪".repeat(2048);
|
||||
let long_greeting = format!("{{\"name\":\"{long_name}\"}}");
|
||||
let echo = format!(
|
||||
"{{\"value\":{{\"id\":\"a\",\"name\":\"{long_name}\",\"scores\":[1,2,3],\"ratio\":0.5}}}}"
|
||||
);
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
compare(
|
||||
"command_greet_short",
|
||||
&[r#"{"name":"Dever"}"#],
|
||||
50000,
|
||||
greeting_generated,
|
||||
greeting_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"command_greet_long",
|
||||
&[long_greeting.as_str()],
|
||||
3000,
|
||||
greeting_generated,
|
||||
greeting_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"command_echo",
|
||||
&[echo.as_str()],
|
||||
3000,
|
||||
echo_generated,
|
||||
echo_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"command_double",
|
||||
&[r#"{"value":21}"#],
|
||||
50000,
|
||||
double_generated,
|
||||
double_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"command_greet_short_independent",
|
||||
&[r#"{"name":"Dever"}"#],
|
||||
50000,
|
||||
greeting_generated,
|
||||
greeting_rust,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"command_greet_long_independent",
|
||||
&[long_greeting.as_str()],
|
||||
3000,
|
||||
greeting_generated,
|
||||
greeting_rust,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"command_double_independent",
|
||||
&[r#"{"value":21}"#],
|
||||
50000,
|
||||
double_generated,
|
||||
double_rust,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// 使用 auth 样例实际生成的 RestCreate0;独立 Rust 直接复用 Serde,合同相同。
|
||||
// 测量仅包含请求对象解码,不包括字段业务约束、认证、SQL 或网络。
|
||||
use dever_runtime::wire::serde::{self, Deserialize};
|
||||
use dever_runtime::wire::serde_json;
|
||||
|
||||
fn fault(_: impl std::fmt::Display) -> Fault {
|
||||
&FaultInfo { message: "REST input failure", source: "rest-reference", line: 1, column: 1 }
|
||||
}
|
||||
|
||||
enum Status { Active, Disabled }
|
||||
|
||||
impl<'input> Deserialize<'input> for Status {
|
||||
fn deserialize<Decoder: serde::Deserializer<'input>>(decoder: Decoder) -> Result<Self, Decoder::Error> {
|
||||
struct Choice;
|
||||
impl<'input> serde::de::Visitor<'input> for Choice {
|
||||
type Value = Status;
|
||||
fn expecting(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { output.write_str("a status name") }
|
||||
fn visit_str<Error: serde::de::Error>(self, value: &str) -> Result<Status, Error> {
|
||||
match value {
|
||||
"Active" => Ok(Status::Active),
|
||||
"Disabled" => Ok(Status::Disabled),
|
||||
_ => Err(Error::custom("unknown status")),
|
||||
}
|
||||
}
|
||||
}
|
||||
decoder.deserialize_str(Choice)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde", deny_unknown_fields)]
|
||||
struct Profile {
|
||||
name: String,
|
||||
bio: Option<String>,
|
||||
#[serde(default = "rank")]
|
||||
rank: i64,
|
||||
#[serde(default = "tag")]
|
||||
tag: Option<String>,
|
||||
#[serde(default = "status")]
|
||||
status: Status,
|
||||
}
|
||||
|
||||
fn rank() -> i64 { 7 }
|
||||
fn tag() -> Option<String> { Some("automatic".into()) }
|
||||
fn status() -> Status { Status::Active }
|
||||
|
||||
fn generated(values: &[String]) -> Result<String, Fault> {
|
||||
let Some(source) = values.first() else { return Ok(String::new()); };
|
||||
let value: RestCreate0 = REST_DECODER(source).map_err(fault)?;
|
||||
Ok(value.field0)
|
||||
}
|
||||
|
||||
fn reference(values: &[String]) -> Result<String, Fault> {
|
||||
let Some(source) = values.first() else { return Ok(String::new()); };
|
||||
if source.len() > 16 * 1024 * 1024 { return Err(fault("byte limit")); }
|
||||
struct Object;
|
||||
impl<'input> serde::de::Visitor<'input> for Object {
|
||||
type Value = Profile;
|
||||
fn expecting(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
output.write_str("an object")
|
||||
}
|
||||
fn visit_map<Access: serde::de::MapAccess<'input>>(self, access: Access) -> Result<Profile, Access::Error> {
|
||||
Profile::deserialize(serde::de::value::MapAccessDeserializer::new(access))
|
||||
}
|
||||
}
|
||||
let mut decoder = serde_json::Deserializer::from_str(source);
|
||||
let value = serde::Deserializer::deserialize_map(&mut decoder, Object).map_err(fault)?;
|
||||
decoder.end().map_err(fault)?;
|
||||
// 固定五个标量字段,类型已证明层数和值数;保留其正常构造与释放。
|
||||
let Profile { name, bio, rank, tag, status } = value;
|
||||
drop((bio, rank, tag, status));
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("workload,sample,implementation,nanos,checksum");
|
||||
for source in [
|
||||
r#"{"name":"Dever"}"#,
|
||||
r#"{"name":"Dever","bio":null,"tag":null,"status":"Disabled"}"#,
|
||||
] {
|
||||
assert_eq!(generated(&[source.into()]).unwrap(), "Dever");
|
||||
assert_eq!(reference(&[source.into()]).unwrap(), "Dever");
|
||||
}
|
||||
for source in [
|
||||
"[]", "null", "{}", r#"{"name":"x","name":"y"}"#,
|
||||
r#"{"name":"x","na\u006de":"y"}"#, r#"{"name":"x","extra":1}"#,
|
||||
r#"{"name":"x","rank":null}"#, r#"{"name":"x","status":"Other"}"#,
|
||||
r#"{"name":"x","status":{"Active":null}}"#,
|
||||
r#"{"name":"x","bio":"\ud800"}"#, r#"{"name":"x"} null"#,
|
||||
] {
|
||||
assert!(generated(&[source.into()]).is_err(), "{source}");
|
||||
assert!(reference(&[source.into()]).is_err(), "{source}");
|
||||
}
|
||||
for (name, bio) in [
|
||||
("short", "x".repeat(16)),
|
||||
("plain_4096", "x".repeat(4096)),
|
||||
("plain_65536", "x".repeat(65536)),
|
||||
("escaped", "雪\"\\\n".repeat(1024)),
|
||||
] {
|
||||
let input = vec![serde_json::json!({"name":"Dever","bio":bio,"rank":7,"tag":"automatic","status":"Active"}).to_string()];
|
||||
compare(&format!("rest_input_{name}"), &input, 1000, generated, reference, |name| name.len() as u64);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
use dever_runtime::wire::serde::{self, Deserialize};
|
||||
use dever_runtime::wire::serde_json::{self, value::RawValue};
|
||||
|
||||
fn fault(_: impl std::fmt::Display) -> Fault {
|
||||
&FaultInfo {
|
||||
message: "REST JSON input",
|
||||
source: "rest-json-reference",
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde", deny_unknown_fields)]
|
||||
struct Fields {
|
||||
value: Box<RawValue>,
|
||||
extra: Option<Box<RawValue>>,
|
||||
}
|
||||
|
||||
fn generated(values: &[String]) -> Result<String, Fault> {
|
||||
let Some(source) = values.first() else {
|
||||
return Ok(String::new());
|
||||
};
|
||||
let value: RAW_ARGS = dever_runtime::wire::deserialize(source).map_err(fault)?;
|
||||
Ok(value.field0.into_text())
|
||||
}
|
||||
|
||||
fn reference(values: &[String]) -> Result<String, Fault> {
|
||||
let Some(source) = values.first() else {
|
||||
return Ok(String::new());
|
||||
};
|
||||
struct Object;
|
||||
impl<'input> serde::de::Visitor<'input> for Object {
|
||||
type Value = Fields;
|
||||
fn expecting(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
output.write_str("an object")
|
||||
}
|
||||
fn visit_map<Access: serde::de::MapAccess<'input>>(
|
||||
self,
|
||||
access: Access,
|
||||
) -> Result<Fields, Access::Error> {
|
||||
Fields::deserialize(serde::de::value::MapAccessDeserializer::new(access))
|
||||
}
|
||||
}
|
||||
let mut decoder = serde_json::Deserializer::from_str(source);
|
||||
let value = serde::Deserializer::deserialize_map(&mut decoder, Object).map_err(fault)?;
|
||||
decoder.end().map_err(fault)?;
|
||||
drop(value.extra);
|
||||
Ok(Box::<str>::from(value.value).into())
|
||||
}
|
||||
|
||||
// 保留原始参照,单独改变入口或字段表示,定位短输入的固定开销。
|
||||
fn generated_decoder(values: &[String]) -> Result<String, Fault> {
|
||||
let Some(source) = values.first() else {
|
||||
return Ok(String::new());
|
||||
};
|
||||
let mut decoder = serde_json::Deserializer::from_str(source);
|
||||
let value = RAW_ARGS::deserialize(&mut decoder).map_err(fault)?;
|
||||
decoder.end().map_err(fault)?;
|
||||
Ok(value.field0.into_text())
|
||||
}
|
||||
|
||||
struct RawFields(Fields);
|
||||
|
||||
impl<'input> Deserialize<'input> for RawFields {
|
||||
fn deserialize<Decoder: serde::Deserializer<'input>>(
|
||||
decoder: Decoder,
|
||||
) -> Result<Self, Decoder::Error> {
|
||||
struct Object;
|
||||
impl<'input> serde::de::Visitor<'input> for Object {
|
||||
type Value = RawFields;
|
||||
fn expecting(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
output.write_str("an object")
|
||||
}
|
||||
fn visit_map<Access: serde::de::MapAccess<'input>>(
|
||||
self,
|
||||
access: Access,
|
||||
) -> Result<RawFields, Access::Error> {
|
||||
Fields::deserialize(serde::de::value::MapAccessDeserializer::new(access))
|
||||
.map(RawFields)
|
||||
}
|
||||
}
|
||||
decoder.deserialize_map(Object)
|
||||
}
|
||||
}
|
||||
|
||||
fn reference_entry(values: &[String]) -> Result<String, Fault> {
|
||||
let Some(source) = values.first() else {
|
||||
return Ok(String::new());
|
||||
};
|
||||
let value: RawFields = serde_json::from_str(source).map_err(fault)?;
|
||||
Ok(Box::<str>::from(value.0.value).into())
|
||||
}
|
||||
|
||||
fn raw_text<'input, Decoder: serde::Deserializer<'input>>(
|
||||
decoder: Decoder,
|
||||
) -> Result<String, Decoder::Error> {
|
||||
Box::<RawValue>::deserialize(decoder).map(|raw| Box::<str>::from(raw).into())
|
||||
}
|
||||
|
||||
fn optional_raw_text<'input, Decoder: serde::Deserializer<'input>>(
|
||||
decoder: Decoder,
|
||||
) -> Result<Option<String>, Decoder::Error> {
|
||||
Option::<Box<RawValue>>::deserialize(decoder)
|
||||
.map(|raw| raw.map(|raw| Box::<str>::from(raw).into()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde", deny_unknown_fields)]
|
||||
struct TextFields {
|
||||
#[serde(deserialize_with = "raw_text")]
|
||||
value: String,
|
||||
#[serde(default, deserialize_with = "optional_raw_text")]
|
||||
extra: Option<String>,
|
||||
}
|
||||
|
||||
fn reference_text(values: &[String]) -> Result<String, Fault> {
|
||||
let Some(source) = values.first() else {
|
||||
return Ok(String::new());
|
||||
};
|
||||
struct Object;
|
||||
impl<'input> serde::de::Visitor<'input> for Object {
|
||||
type Value = TextFields;
|
||||
fn expecting(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
output.write_str("an object")
|
||||
}
|
||||
fn visit_map<Access: serde::de::MapAccess<'input>>(
|
||||
self,
|
||||
access: Access,
|
||||
) -> Result<TextFields, Access::Error> {
|
||||
TextFields::deserialize(serde::de::value::MapAccessDeserializer::new(access))
|
||||
}
|
||||
}
|
||||
let mut decoder = serde_json::Deserializer::from_str(source);
|
||||
let value = serde::Deserializer::deserialize_map(&mut decoder, Object).map_err(fault)?;
|
||||
decoder.end().map_err(fault)?;
|
||||
drop(value.extra);
|
||||
Ok(value.value)
|
||||
}
|
||||
|
||||
fn generated_output(values: &[String]) -> Result<String, Fault> {
|
||||
let Some(source) = values.first() else {
|
||||
return Ok(String::new());
|
||||
};
|
||||
let value: RAW_ARGS = dever_runtime::wire::deserialize(source).map_err(fault)?;
|
||||
GENERATED_RAW_ENCODE(&value.field0).map_err(fault)
|
||||
}
|
||||
|
||||
fn reference_output(values: &[String]) -> Result<String, Fault> {
|
||||
let Some(source) = values.first() else {
|
||||
return Ok(String::new());
|
||||
};
|
||||
let value: RawFields = serde_json::from_str(source).map_err(fault)?;
|
||||
serde_json::to_string(&value.0.value).map_err(fault)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if std::env::args().any(|argument| argument == "--output-only") {
|
||||
println!("workload,sample,implementation,nanos,checksum");
|
||||
output::run();
|
||||
return;
|
||||
}
|
||||
let short_only = std::env::args().any(|argument| argument == "--short");
|
||||
eprintln!(
|
||||
"layout,json={},string={},raw={}",
|
||||
std::mem::size_of::<dever_runtime::wire::json::Json>(),
|
||||
std::mem::size_of::<String>(),
|
||||
std::mem::size_of::<Box<RawValue>>()
|
||||
);
|
||||
println!("workload,sample,implementation,nanos,checksum");
|
||||
for (name, text) in [
|
||||
("short", "x".repeat(16)),
|
||||
("plain_4096", "x".repeat(4096)),
|
||||
("plain_65536", "x".repeat(65536)),
|
||||
("escaped", "雪\"\\\n".repeat(1024)),
|
||||
] {
|
||||
if short_only && name != "short" {
|
||||
continue;
|
||||
}
|
||||
let raw = format!(
|
||||
"{{\"text\":{},\"number\":1e9999}}",
|
||||
serde_json::to_string(&text).unwrap()
|
||||
);
|
||||
let source = format!("{{\"value\":{raw},\"extra\":{raw}}}");
|
||||
// 整体请求校验在两侧计时外完成;这里单独比较已校验请求的字段解码。
|
||||
let checked: RAW_ARGS = dever_runtime::wire::decode(&source).unwrap();
|
||||
assert_eq!(checked.field0.as_str(), raw);
|
||||
assert_eq!(checked.field1.unwrap().as_str(), raw);
|
||||
// 短输入每组至少持续数十毫秒,避免亚毫秒样本的调度噪声主导百分比。
|
||||
let count = if name == "short" { 100000 } else { 1000 };
|
||||
let values = [source];
|
||||
compare(
|
||||
&format!("rest_json_{name}"),
|
||||
&values,
|
||||
count,
|
||||
generated,
|
||||
reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
&format!("rest_json_output_{name}"),
|
||||
&values,
|
||||
count,
|
||||
generated_output,
|
||||
reference_output,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
}
|
||||
let raw = r#"{"text":"xxxxxxxxxxxxxxxx","number":1e9999}"#;
|
||||
let input = [format!("{{\"value\":{raw},\"extra\":{raw}}}")];
|
||||
for (name, implementation) in [
|
||||
(
|
||||
"decoder",
|
||||
generated_decoder as fn(&[String]) -> Result<String, Fault>,
|
||||
),
|
||||
("raw_from_str", reference_entry),
|
||||
("text_decoder", reference_text),
|
||||
] {
|
||||
if short_only && name != "decoder" {
|
||||
continue;
|
||||
}
|
||||
compare(
|
||||
&format!("rest_json_control_{name}"),
|
||||
&input,
|
||||
100000,
|
||||
implementation,
|
||||
reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
use chrono::Datelike;
|
||||
use dever_runtime::wire::json::Json;
|
||||
use dever_runtime::wire::serde::{Serialize, Serializer};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Timestamp(i64);
|
||||
|
||||
impl Serialize for Timestamp {
|
||||
fn serialize<Encoder: Serializer>(
|
||||
&self,
|
||||
encoder: Encoder,
|
||||
) -> Result<Encoder::Ok, Encoder::Error> {
|
||||
let value = chrono::DateTime::from_timestamp_millis(self.0)
|
||||
.filter(|value| (0..=9999).contains(&value.year()))
|
||||
.ok_or_else(|| dever_runtime::wire::serde::ser::Error::custom("invalid timestamp"))?;
|
||||
let pattern = if value.timestamp_subsec_millis() == 0 {
|
||||
"%Y-%m-%dT%H:%M:%SZ"
|
||||
} else {
|
||||
"%Y-%m-%dT%H:%M:%S%.3fZ"
|
||||
};
|
||||
encoder.collect_str(&value.naive_utc().format(pattern))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde")]
|
||||
struct Row {
|
||||
id: i64,
|
||||
created_at: Timestamp,
|
||||
value: Box<RawValue>,
|
||||
extra: Option<Box<RawValue>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde")]
|
||||
struct Reply<Value> {
|
||||
code: u16,
|
||||
message: &'static str,
|
||||
data: Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde")]
|
||||
struct Page<Items> {
|
||||
items: Items,
|
||||
page: i64,
|
||||
size: i64,
|
||||
total: i64,
|
||||
pages: i64,
|
||||
}
|
||||
|
||||
struct Case {
|
||||
dever: Vec<RAW_MODEL>,
|
||||
rust: Vec<Row>,
|
||||
}
|
||||
|
||||
fn detail(values: &[Case]) -> Result<String, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(String::new());
|
||||
};
|
||||
let row = &value.dever[0];
|
||||
REST_DETAIL_ENCODE.map_err(fault)
|
||||
}
|
||||
|
||||
fn page(values: &[Case]) -> Result<String, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(String::new());
|
||||
};
|
||||
let page = Page {
|
||||
items: value.dever.as_slice(),
|
||||
page: 1,
|
||||
size: 20,
|
||||
total: value.dever.len() as i64,
|
||||
pages: 1,
|
||||
};
|
||||
REST_PAGE_ENCODE.map_err(fault)
|
||||
}
|
||||
|
||||
fn encode<Value: Serialize>(data: Value, count: usize) -> Result<String, Fault> {
|
||||
if count > 65_536 {
|
||||
return Err(fault("value budget"));
|
||||
}
|
||||
let text = serde_json::to_string(&Reply {
|
||||
code: 0,
|
||||
message: "ok",
|
||||
data,
|
||||
})
|
||||
.map_err(fault)?;
|
||||
if text.len() > 16 * 1024 * 1024 {
|
||||
return Err(fault("byte budget"));
|
||||
}
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
// 固定夹具中的 Json 是两个标量字段的对象;预算在初次构造后复用。
|
||||
// 独立 Rust 的字段编码、原文输出及响应封装不调用 Dever 运行库。
|
||||
fn row_values(row: &Row) -> usize {
|
||||
if row.extra.is_some() { 9 } else { 7 }
|
||||
}
|
||||
|
||||
fn rust_detail(values: &[Case]) -> Result<String, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(String::new());
|
||||
};
|
||||
encode(&value.rust[0], 3 + row_values(&value.rust[0]))
|
||||
}
|
||||
|
||||
fn rust_page(values: &[Case]) -> Result<String, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(String::new());
|
||||
};
|
||||
let page = Page {
|
||||
items: value.rust.as_slice(),
|
||||
page: 1,
|
||||
size: 20,
|
||||
total: value.rust.len() as i64,
|
||||
pages: 1,
|
||||
};
|
||||
// 五层容器固定小于 64;根、响应字段、分页对象与数组共九个值。
|
||||
encode(page, 9 + value.rust.iter().map(row_values).sum::<usize>())
|
||||
}
|
||||
|
||||
pub(super) fn run() {
|
||||
for (name, text) in [
|
||||
("short", "x".repeat(16)),
|
||||
("plain_4096", "x".repeat(4096)),
|
||||
("plain_65536", "x".repeat(65536)),
|
||||
("escaped", "雪\"\\\n".repeat(1024)),
|
||||
] {
|
||||
let source = format!(
|
||||
"{{\"text\":{},\"number\":1e9999}}",
|
||||
serde_json::to_string(&text).unwrap()
|
||||
);
|
||||
let dever = (0..16)
|
||||
.map(|index| RAW_MODEL {
|
||||
field0: dever_runtime::database::Id::<RAW_MODEL_ID>(index + 1),
|
||||
field1: dever_runtime::time::calendar::DateTime::from_millis(0).unwrap(),
|
||||
field2: Json::parse(&source).unwrap(),
|
||||
field3: (index % 2 == 0).then(|| Json::parse(&source).unwrap()),
|
||||
})
|
||||
.collect();
|
||||
let rust = (0..16)
|
||||
.map(|index| Row {
|
||||
id: index + 1,
|
||||
created_at: Timestamp(0),
|
||||
value: serde_json::from_str(&source).unwrap(),
|
||||
extra: (index % 2 == 0).then(|| serde_json::from_str(&source).unwrap()),
|
||||
})
|
||||
.collect();
|
||||
let values = [Case { dever, rust }];
|
||||
compare_named(
|
||||
&format!("rest_detail_output_{name}"),
|
||||
&values,
|
||||
if name == "short" { 10000 } else { 1000 },
|
||||
detail,
|
||||
rust_detail,
|
||||
|text| text.len() as u64,
|
||||
"rust_bounded",
|
||||
);
|
||||
compare_named(
|
||||
&format!("rest_page_output_{name}"),
|
||||
&values,
|
||||
if name == "short" { 1000 } else { 100 },
|
||||
page,
|
||||
rust_page,
|
||||
|text| text.len() as u64,
|
||||
"rust_bounded",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
//! 新场景沿用同一个 compare 构建器;只替换测试入口名,保留正式生成的认证和 Job 分派。
|
||||
|
||||
use dever::compiler::{emit, model::Program};
|
||||
use std::path::Path;
|
||||
#[path = "../job/build.rs"]
|
||||
mod jobs;
|
||||
|
||||
pub fn run(core: &Path) -> bool {
|
||||
if std::env::args().any(|value| value == "--rest-json") {
|
||||
crate::compare(
|
||||
core,
|
||||
"auth",
|
||||
"rest-json",
|
||||
&(include_str!("../samples.rs").to_owned()
|
||||
+ include_str!("../application/rest_json.rs")
|
||||
+ "\nmod output { use super::*;\n"
|
||||
+ include_str!("../application/rest_output.rs")
|
||||
+ "\n}\n"),
|
||||
&[("GENERATED_RAW_ENCODE", "demo.document.app.encode")],
|
||||
&[],
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if std::env::args().any(|value| value == "--rest-wire") {
|
||||
crate::compare(
|
||||
core,
|
||||
"auth",
|
||||
"rest-wire",
|
||||
&(include_str!("../samples.rs").to_owned() + include_str!("../application/rest.rs")),
|
||||
&[],
|
||||
&[],
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if std::env::args().any(|value| value == "--api-wire") {
|
||||
crate::compare(
|
||||
core,
|
||||
"api-wire",
|
||||
"api-wire",
|
||||
&(include_str!("../samples.rs").to_owned() + include_str!("../application/http.rs")),
|
||||
&[],
|
||||
&[],
|
||||
);
|
||||
return true;
|
||||
}
|
||||
let http = std::env::args().any(|value| value == "--auth-http");
|
||||
if http || std::env::args().any(|value| value == "--auth") {
|
||||
let reference = [
|
||||
include_str!("shared.rs"),
|
||||
include_str!("reference.rs"),
|
||||
include_str!("measure.rs"),
|
||||
"mod protected_http { use super::*;\n",
|
||||
include_str!("http/reference.rs"),
|
||||
include_str!("http/client.rs"),
|
||||
include_str!("http/check.rs"),
|
||||
include_str!("http/measure.rs"),
|
||||
"}\n",
|
||||
&format!("const HTTP_BENCH: bool = {http};\n"),
|
||||
]
|
||||
.concat();
|
||||
crate::compare(
|
||||
core,
|
||||
"auth-performance",
|
||||
if http { "auth-http" } else { "auth" },
|
||||
&reference,
|
||||
&[
|
||||
("GENERATED_SEED", "bench.account.app.seed"),
|
||||
("GENERATED_PERMISSIONS", "bench.account.app.permissions"),
|
||||
],
|
||||
&[],
|
||||
);
|
||||
true
|
||||
} else if std::env::args().any(|value| value == "--jobs") {
|
||||
jobs::run(core);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metadata(name: &str, program: &Program, reference: &str) -> String {
|
||||
match name {
|
||||
"rest-json" => {
|
||||
let index = program
|
||||
.rest
|
||||
.iter()
|
||||
.position(|entry| {
|
||||
program.packages
|
||||
[program.definitions[program.models[entry.model].definition].source]
|
||||
== "demo.document.model"
|
||||
})
|
||||
.unwrap();
|
||||
let source = emit::application::generate(program);
|
||||
let route = source
|
||||
.split_once(&format!("async fn rest{index}("))
|
||||
.unwrap()
|
||||
.1;
|
||||
let detail = route
|
||||
.split_once("let row = rows.pop().ok_or(dever_runtime::api::Failure(404))?; ")
|
||||
.unwrap()
|
||||
.1;
|
||||
let page = route
|
||||
.split_once("let page = executor.page(")
|
||||
.unwrap()
|
||||
.1
|
||||
.split_once(".await.map_err(rest_error)?; ")
|
||||
.unwrap()
|
||||
.1;
|
||||
let definition = program.models[program.rest[index].model].definition;
|
||||
reference
|
||||
.replace("RAW_ARGS", &format!("RestCreate{index}"))
|
||||
.replace("RAW_MODEL_ID", &definition.to_string())
|
||||
.replace("RAW_MODEL", &format!("Type{definition}"))
|
||||
.replace(
|
||||
"REST_DETAIL_ENCODE",
|
||||
detail.split_once(".map_err(Into::into)").unwrap().0,
|
||||
)
|
||||
.replace(
|
||||
"REST_PAGE_ENCODE",
|
||||
page.split_once(".map_err(Into::into)").unwrap().0,
|
||||
)
|
||||
}
|
||||
"rest-wire" => {
|
||||
// 比较实际 REST 输入类型及其解码入口,不把数据库和认证计入 codec 耗时。
|
||||
let source = emit::application::generate(program);
|
||||
let (_, input) = source
|
||||
.split_once("let args: RestCreate0 = input.decode(")
|
||||
.unwrap();
|
||||
let decoder = input.split_once(')').unwrap().0;
|
||||
reference.replace("REST_DECODER", decoder)
|
||||
}
|
||||
"api-wire" => [
|
||||
("GENERATED_HTTP", "/bench/wire/echo"),
|
||||
("GENERATED_LIST", "/bench/wire/count"),
|
||||
]
|
||||
.into_iter()
|
||||
.fold(reference.to_owned(), |source, (marker, path)| {
|
||||
let index = program
|
||||
.http
|
||||
.iter()
|
||||
.position(|entry| entry.path == path && entry.method == "POST")
|
||||
.unwrap();
|
||||
source.replace(marker, &format!("api{index}"))
|
||||
}),
|
||||
"auth" | "auth-http" => {
|
||||
assert_eq!(program.verifiers.len(), 1);
|
||||
let verifier = &program.verifiers[0];
|
||||
let account = program
|
||||
.models
|
||||
.iter()
|
||||
.position(|model| {
|
||||
program.packages[program.definitions[model.definition].source]
|
||||
== "bench.account.model"
|
||||
})
|
||||
.unwrap();
|
||||
reference
|
||||
.replace("ACCOUNT_BINDING", &account.to_string())
|
||||
.replace("AUTH_BINDING", &(program.models.len() + 1).to_string())
|
||||
.replace("AUTH_USER_MODEL", &verifier.user.1.to_string())
|
||||
.replace("AUTH_TENANT_MODEL", &verifier.tenant.1.to_string())
|
||||
.replace(
|
||||
"GENERATED_HTTP",
|
||||
&format!(
|
||||
"api{}",
|
||||
program
|
||||
.http
|
||||
.iter()
|
||||
.position(|entry| entry.path == "/bench/account/admin/permissions"
|
||||
&& entry.method == "GET")
|
||||
.unwrap()
|
||||
),
|
||||
)
|
||||
}
|
||||
"jobs" => jobs::metadata(program, reference),
|
||||
_ => reference.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn application(name: &str, program: &Program) -> String {
|
||||
if matches!(
|
||||
name,
|
||||
"auth" | "auth-http" | "jobs" | "api-wire" | "rest-wire" | "rest-json"
|
||||
) {
|
||||
let source = emit::application::generate(program);
|
||||
assert_eq!(source.matches("fn main() {").count(), 1);
|
||||
source.replacen("fn main() {", "fn application_main() {", 1)
|
||||
} else if !program.commands.is_empty() {
|
||||
emit::application::functions(program)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// 正确性检查在计时外覆盖凭据、Provider、角色、路由及严格输入,不能用空 200 掩盖漏做。
|
||||
fn check_requests(connection: &mut HttpConnection, input: &AuthInput) {
|
||||
for request in &input.requests[..16] {
|
||||
check_reply(&connection.request(&request_bytes(request)), 200);
|
||||
}
|
||||
check_reply(
|
||||
&connection.request(&request_bytes(&input.requests[16])),
|
||||
401,
|
||||
);
|
||||
let valid = &input.requests[0];
|
||||
let mut missing = valid.clone();
|
||||
missing.headers_mut().remove("cookie");
|
||||
let mut invalid = valid.clone();
|
||||
invalid
|
||||
.headers_mut()
|
||||
.insert("cookie", "session=v1.invalid-jwt".parse().unwrap());
|
||||
let mut host = valid.clone();
|
||||
host.headers_mut()
|
||||
.insert("host", "other.test".parse().unwrap());
|
||||
let mut path = valid.clone();
|
||||
*path.uri_mut() = format!("{}-missing", path.uri()).parse().unwrap();
|
||||
let mut query = valid.clone();
|
||||
*query.uri_mut() = format!("{}?unexpected=1", query.uri()).parse().unwrap();
|
||||
for (request, status) in [
|
||||
(missing, 401),
|
||||
(invalid, 401),
|
||||
(host, 404),
|
||||
(path, 404),
|
||||
(query, 400),
|
||||
] {
|
||||
check_reply(&connection.request(&request_bytes(&request)), status);
|
||||
}
|
||||
}
|
||||
|
||||
fn membership(runtime: &Runtime, registry: &Arc<Registry>, input: &AuthInput, grant: bool) {
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let (mut context, claims) = input
|
||||
.service
|
||||
.prepare("admin", &input.requests[0], false, input.now)
|
||||
.unwrap();
|
||||
rust_verify(&mut context, claims.unwrap(), scope, registry)
|
||||
.await
|
||||
.map_err(|error| error.0)
|
||||
.unwrap();
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database.on(AUTH_BINDING, scope).await.unwrap();
|
||||
if grant {
|
||||
authorization::grant(executor, scope, &context, 16, "reader")
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
authorization::revoke(executor, scope, &context, 16, "reader")
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn check(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
config: &Arc<api::Config>,
|
||||
input: &AuthInput,
|
||||
) {
|
||||
for generated in [true, false] {
|
||||
with_server(runtime, registry, config, input, generated, |port| {
|
||||
check_requests(&mut HttpConnection::connect(port), input);
|
||||
});
|
||||
}
|
||||
membership(runtime, registry, input, false);
|
||||
for generated in [true, false] {
|
||||
with_server(runtime, registry, config, input, generated, |port| {
|
||||
let mut connection = HttpConnection::connect(port);
|
||||
check_reply(
|
||||
&connection.request(&request_bytes(&input.requests[15])),
|
||||
403,
|
||||
);
|
||||
});
|
||||
}
|
||||
membership(runtime, registry, input, true);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// 客户端只连接本轮持有的回环监听器,使用同一 HTTP/1.1 长连接顺序发送请求。
|
||||
use std::{
|
||||
io::{BufRead, BufReader, Read, Write},
|
||||
net::TcpStream,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
const ROUTE: &str = "/bench/account/admin/permissions";
|
||||
const CONTENT_TYPE: &str = "application/json; charset=utf-8";
|
||||
const SUCCESS: &str = "{\"code\":0,\"message\":\"ok\",\"data\":[{\"key\":\"bench.account.admin.permissions\",\"component\":\"bench\",\"domain\":\"account\",\"site\":\"admin\",\"action\":\"permissions\",\"method\":\"GET\",\"kind\":\"http\"}]}";
|
||||
|
||||
struct HttpReply {
|
||||
status: u16,
|
||||
content_type: String,
|
||||
body: Vec<u8>,
|
||||
}
|
||||
|
||||
struct HttpConnection(BufReader<TcpStream>);
|
||||
|
||||
impl HttpConnection {
|
||||
fn connect(port: u16) -> Self {
|
||||
let socket = TcpStream::connect(("127.0.0.1", port)).unwrap();
|
||||
socket.set_nodelay(true).unwrap();
|
||||
socket
|
||||
.set_read_timeout(Some(Duration::from_secs(5)))
|
||||
.unwrap();
|
||||
socket
|
||||
.set_write_timeout(Some(Duration::from_secs(5)))
|
||||
.unwrap();
|
||||
Self(BufReader::new(socket))
|
||||
}
|
||||
|
||||
fn request(&mut self, request: &[u8]) -> HttpReply {
|
||||
self.0.get_mut().write_all(request).unwrap();
|
||||
let mut line = String::new();
|
||||
self.0.read_line(&mut line).unwrap();
|
||||
assert!(line.starts_with("HTTP/1.1 "));
|
||||
let status = line.split_whitespace().nth(1).unwrap().parse().unwrap();
|
||||
let mut length = None;
|
||||
let mut content_type = String::new();
|
||||
loop {
|
||||
line.clear();
|
||||
assert!(self.0.read_line(&mut line).unwrap() > 0);
|
||||
if line == "\r\n" {
|
||||
break;
|
||||
}
|
||||
let (name, value) = line.trim_end().split_once(':').unwrap();
|
||||
match name.to_ascii_lowercase().as_str() {
|
||||
"content-length" => {
|
||||
assert!(length.is_none());
|
||||
length = Some(value.trim().parse::<usize>().unwrap());
|
||||
}
|
||||
"content-type" => content_type = value.trim().into(),
|
||||
"connection" => assert_ne!(value.trim(), "close"),
|
||||
"transfer-encoding" | "set-cookie" => panic!("unexpected response header"),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
let mut body = vec![0; length.unwrap()];
|
||||
self.0.read_exact(&mut body).unwrap();
|
||||
HttpReply {
|
||||
status,
|
||||
content_type,
|
||||
body,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn request_bytes(request: &Request) -> Vec<u8> {
|
||||
assert!(request.body().is_empty());
|
||||
let mut bytes = format!("{} {} HTTP/1.1\r\n", request.method(), request.uri()).into_bytes();
|
||||
for (name, value) in request.headers() {
|
||||
bytes.extend_from_slice(name.as_str().as_bytes());
|
||||
bytes.extend_from_slice(b": ");
|
||||
bytes.extend_from_slice(value.as_bytes());
|
||||
bytes.extend_from_slice(b"\r\n");
|
||||
}
|
||||
bytes.extend_from_slice(b"Connection: keep-alive\r\nContent-Length: 0\r\n\r\n");
|
||||
bytes
|
||||
}
|
||||
|
||||
fn check_reply(reply: &HttpReply, status: u16) {
|
||||
assert_eq!(reply.status, status);
|
||||
assert_eq!(reply.content_type, CONTENT_TYPE);
|
||||
let expected = if status == 200 {
|
||||
SUCCESS.as_bytes().to_vec()
|
||||
} else {
|
||||
api::error(status).body().to_vec()
|
||||
};
|
||||
assert_eq!(reply.body, expected);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// 两侧复用相同 HTTP 引擎、限额和调度策略;计时包含真实 JWT 校验、三次数据库读取及响应。
|
||||
fn with_server<Value>(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
config: &Arc<api::Config>,
|
||||
input: &AuthInput,
|
||||
generated: bool,
|
||||
client: impl FnOnce(u16) -> Value,
|
||||
) -> Value {
|
||||
let registry = registry.clone();
|
||||
let config = config.clone();
|
||||
let authentication = input.service.clone();
|
||||
std::thread::scope(|threads| {
|
||||
let (started, port) = std::sync::mpsc::sync_channel(0);
|
||||
let (finished, completion) = tokio::sync::oneshot::channel();
|
||||
let serving = threads.spawn(move || {
|
||||
runtime
|
||||
.run(async move |scope| {
|
||||
let listener = dever_runtime::net::listen("127.0.0.1", 0, scope)
|
||||
.await
|
||||
.unwrap();
|
||||
let port = listener.port().unwrap() as u16;
|
||||
let incoming = listener.clone();
|
||||
let server = scope
|
||||
.spawn(move |child| async move {
|
||||
let limits = dever_runtime::http::Limits {
|
||||
connections: 1,
|
||||
..Default::default()
|
||||
};
|
||||
api::serve::<_, _, true>(
|
||||
&incoming,
|
||||
&limits,
|
||||
&child,
|
||||
None,
|
||||
move |request, scope, peer| {
|
||||
let config = config.clone();
|
||||
let registry = registry.clone();
|
||||
let authentication = authentication.clone();
|
||||
async move {
|
||||
let path = request.uri().path();
|
||||
let response = match (request.method().as_str(), path) {
|
||||
("GET", ROUTE) if config.accepts("admin", &request) => {
|
||||
if generated {
|
||||
GENERATED_HTTP(
|
||||
&request,
|
||||
peer,
|
||||
&scope,
|
||||
®istry,
|
||||
&authentication,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
rust_http(
|
||||
&request,
|
||||
peer,
|
||||
&scope,
|
||||
®istry,
|
||||
&authentication,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
_ => api::error(404),
|
||||
};
|
||||
Ok(response)
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
})
|
||||
.unwrap();
|
||||
// 阻塞客户端独立于运行时线程,也不占用数据库的阻塞池;交接在计时外。
|
||||
started.send(port).unwrap();
|
||||
completion.await.unwrap();
|
||||
listener.close().unwrap();
|
||||
match server.timeout(5000).await.unwrap() {
|
||||
dever_runtime::task::Timed::Ready(result) => result.unwrap(),
|
||||
_ => panic!("owned HTTP benchmark server failed to drain"),
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
});
|
||||
let result = client(port.recv().unwrap());
|
||||
finished.send(()).unwrap();
|
||||
serving.join().unwrap();
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn run(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
config: &Arc<api::Config>,
|
||||
input: &AuthInput,
|
||||
) {
|
||||
check(runtime, registry, config, input);
|
||||
eprintln!(
|
||||
"HTTP/1.1 loopback; one persistent connection; rotating 16 JWT cookies; JWT + Provider + role gate + permission list + JSON response included; TCP connect and response assertions excluded; no TLS/proxy/concurrent-load claim"
|
||||
);
|
||||
let requests = input.requests[..16]
|
||||
.iter()
|
||||
.map(request_bytes)
|
||||
.collect::<Vec<_>>();
|
||||
samples("http_jwt_provider_roles", |generated, count| {
|
||||
with_server(runtime, registry, config, input, generated, |port| {
|
||||
let mut connection = HttpConnection::connect(port);
|
||||
check_reply(&connection.request(&requests[0]), 200);
|
||||
let mut replies = Vec::with_capacity(count);
|
||||
let start = Instant::now();
|
||||
for index in 0..count {
|
||||
replies.push(connection.request(black_box(&requests[index % requests.len()])));
|
||||
}
|
||||
let elapsed = start.elapsed().as_nanos();
|
||||
let mut checksum = 0u64;
|
||||
for reply in &replies {
|
||||
check_reply(reply, 200);
|
||||
checksum += black_box(reply.body.iter().map(|byte| u64::from(*byte)).sum::<u64>());
|
||||
}
|
||||
(elapsed, checksum)
|
||||
})
|
||||
});
|
||||
check(runtime, registry, config, input);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// 手写 Rust 完成同样的请求边界、Provider、权限门禁和业务返回,不调用生成 API。
|
||||
use dever_runtime::{api, authorization, wire};
|
||||
use wire::serde::{self, Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde")]
|
||||
struct RustPermission {
|
||||
key: String,
|
||||
component: String,
|
||||
domain: String,
|
||||
site: String,
|
||||
action: String,
|
||||
method: String,
|
||||
kind: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde", deny_unknown_fields)]
|
||||
struct EmptyFields {}
|
||||
|
||||
struct EmptyInput;
|
||||
|
||||
impl<'input> Deserialize<'input> for EmptyInput {
|
||||
fn deserialize<Decoder: serde::Deserializer<'input>>(
|
||||
decoder: Decoder,
|
||||
) -> Result<Self, Decoder::Error> {
|
||||
let _: EmptyFields = wire::object(decoder)?;
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
fn access_failure(error: authorization::Failure) -> ApiFailure {
|
||||
match error {
|
||||
authorization::Failure::Access(status) => ApiFailure(status),
|
||||
authorization::Failure::Database(_) => ApiFailure(500),
|
||||
}
|
||||
}
|
||||
|
||||
async fn rust_http(
|
||||
request: &Request,
|
||||
peer: std::net::SocketAddr,
|
||||
scope: &Scope<'_>,
|
||||
registry: &Arc<Registry>,
|
||||
authentication: &Arc<auth::Service>,
|
||||
) -> Response {
|
||||
let now = match auth::now() {
|
||||
Ok(now) => now,
|
||||
Err(error) => return api::error(error.status()),
|
||||
};
|
||||
let (mut context, claims) = match authentication.prepare_at("admin", request, false, now, peer)
|
||||
{
|
||||
Ok(prepared) => prepared,
|
||||
Err(error) => return api::error(error.status()),
|
||||
};
|
||||
if let Some(claims) = claims {
|
||||
if let Err(error) = rust_verify(&mut context, claims, scope, registry).await {
|
||||
return error.response();
|
||||
}
|
||||
}
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let outcome = async {
|
||||
authorization::authorize(
|
||||
&mut database,
|
||||
AUTH_BINDING,
|
||||
scope,
|
||||
&context,
|
||||
"bench",
|
||||
"account",
|
||||
"permissions",
|
||||
)
|
||||
.await
|
||||
.map_err(access_failure)?;
|
||||
let input = api::input(request).map_err(ApiFailure)?;
|
||||
let _: EmptyInput = input
|
||||
.decode(dever_runtime::wire::decode)
|
||||
.map_err(|_| ApiFailure(400))?;
|
||||
let values = authorization::permissions(&mut database, AUTH_BINDING, scope, &context, true)
|
||||
.await
|
||||
.map_err(access_failure)?
|
||||
.into_iter()
|
||||
.map(|value| RustPermission {
|
||||
key: value.key,
|
||||
component: value.component,
|
||||
domain: value.domain,
|
||||
site: value.site,
|
||||
action: value.action,
|
||||
method: value.method,
|
||||
kind: value.kind,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
wire::response(&values).map_err(ApiFailure::from)
|
||||
}
|
||||
.await;
|
||||
match outcome {
|
||||
Ok(body) => {
|
||||
let mut response = api::response(body);
|
||||
context.publish(&mut response);
|
||||
response
|
||||
}
|
||||
Err(error) => error.response(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
// JWT 解析在计时前完成;本场景测 Provider 数据库复核及随后实际角色读取。
|
||||
use dever_runtime::api::{Request, Response};
|
||||
|
||||
struct AuthInput {
|
||||
service: Arc<auth::Service>,
|
||||
requests: Vec<Request>,
|
||||
claims: Vec<Claims>,
|
||||
now: i64,
|
||||
}
|
||||
|
||||
fn prepare(path: &Path) -> (Arc<dever_runtime::api::Config>, AuthInput) {
|
||||
let config = Arc::new(dever_runtime::api::Config::load(path, &["admin"]).unwrap());
|
||||
let service = Arc::new(
|
||||
auth::Service::new(
|
||||
config.clone(),
|
||||
&[auth::Verification {
|
||||
name: "bench.account.verify",
|
||||
user_model: AUTH_USER_MODEL,
|
||||
tenant_model: AUTH_TENANT_MODEL,
|
||||
}],
|
||||
&[auth::Route {
|
||||
topic: "admin",
|
||||
required: true,
|
||||
user_models: &[AUTH_USER_MODEL],
|
||||
tenant_models: &[AUTH_TENANT_MODEL],
|
||||
}],
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let now = auth::now().unwrap();
|
||||
let claims = (0..17)
|
||||
.map(|index| Claims {
|
||||
subject: format!("account-{index:02} 中文\""),
|
||||
session: format!("session-{index}"),
|
||||
site: "admin".into(),
|
||||
tenant: None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let requests = claims
|
||||
.iter()
|
||||
.map(|claims| {
|
||||
// 只用公开 Cookie 接口准备凭据,不读取 Secret 的私有字节。
|
||||
let mut bootstrap = Request::new(Default::default());
|
||||
*bootstrap.method_mut() = "POST".parse().unwrap();
|
||||
*bootstrap.uri_mut() = "/login".parse().unwrap();
|
||||
bootstrap
|
||||
.headers_mut()
|
||||
.insert("host", "benchmark.test".parse().unwrap());
|
||||
bootstrap
|
||||
.headers_mut()
|
||||
.insert("origin", "https://benchmark.test".parse().unwrap());
|
||||
let (mut context, _) = service.prepare("admin", &bootstrap, true, now).unwrap();
|
||||
context
|
||||
.issue_cookie(&claims.subject, &claims.session, None, now)
|
||||
.unwrap();
|
||||
let mut response = Response::new(Default::default());
|
||||
context.publish(&mut response);
|
||||
let cookie = response.headers().get("set-cookie").unwrap();
|
||||
let cookie = std::str::from_utf8(cookie.as_bytes())
|
||||
.unwrap()
|
||||
.split(';')
|
||||
.next()
|
||||
.unwrap()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
let mut request = Request::new(Default::default());
|
||||
*request.uri_mut() = "/bench/account/admin/permissions".parse().unwrap();
|
||||
request
|
||||
.headers_mut()
|
||||
.insert("host", "benchmark.test".parse().unwrap());
|
||||
request
|
||||
.headers_mut()
|
||||
.insert("cookie", cookie.try_into().unwrap());
|
||||
request
|
||||
})
|
||||
.collect();
|
||||
(
|
||||
config,
|
||||
AuthInput {
|
||||
service,
|
||||
requests,
|
||||
claims,
|
||||
now,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn measure_auth(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
input: &AuthInput,
|
||||
generated: bool,
|
||||
roles: bool,
|
||||
count: usize,
|
||||
) -> (u128, u64) {
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let mut contexts = input.requests[..16]
|
||||
.iter()
|
||||
.map(|request| {
|
||||
input
|
||||
.service
|
||||
.prepare("admin", request, false, input.now)
|
||||
.unwrap()
|
||||
.0
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut checksum = 0u64;
|
||||
let start = Instant::now();
|
||||
for index in 0..count {
|
||||
let position = index % contexts.len();
|
||||
let context = &mut contexts[position];
|
||||
verify(
|
||||
generated,
|
||||
context,
|
||||
black_box(&input.claims[position]).clone(),
|
||||
scope,
|
||||
registry,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| error.0)
|
||||
.unwrap();
|
||||
checksum += black_box(
|
||||
context.user_id(None).unwrap().unwrap() as u64
|
||||
+ context.id().unwrap().len() as u64,
|
||||
);
|
||||
if roles {
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let values =
|
||||
black_box(permissions(generated, context, scope, &mut database).await);
|
||||
for value in &values {
|
||||
for field in value {
|
||||
checksum += field.bytes().map(u64::from).sum::<u64>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(start.elapsed().as_nanos(), checksum)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn check_auth(runtime: &Runtime, registry: &Arc<Registry>, input: &AuthInput) {
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let expected = vec![
|
||||
[
|
||||
"bench.account.admin.permissions",
|
||||
"bench",
|
||||
"account",
|
||||
"admin",
|
||||
"permissions",
|
||||
"GET",
|
||||
"http",
|
||||
]
|
||||
.map(String::from),
|
||||
];
|
||||
for generated in [true, false] {
|
||||
for index in 0..16 {
|
||||
let (mut context, claims) = input
|
||||
.service
|
||||
.prepare("admin", &input.requests[index], false, input.now)
|
||||
.unwrap();
|
||||
verify(generated, &mut context, claims.unwrap(), scope, registry)
|
||||
.await
|
||||
.map_err(|error| error.0)
|
||||
.unwrap();
|
||||
assert_eq!(context.id().unwrap(), input.claims[index].subject);
|
||||
assert_eq!(
|
||||
context.user_id(Some(AUTH_USER_MODEL)).unwrap(),
|
||||
Some(index as i64 + 1)
|
||||
);
|
||||
assert!(
|
||||
context
|
||||
.tenant_id(Some(AUTH_TENANT_MODEL))
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
assert_eq!(
|
||||
permissions(generated, &mut context, scope, &mut database).await,
|
||||
expected
|
||||
);
|
||||
}
|
||||
for (index, wrong_session) in [(16, false), (0, true)] {
|
||||
let (mut context, claims) = input
|
||||
.service
|
||||
.prepare("admin", &input.requests[index], false, input.now)
|
||||
.unwrap();
|
||||
let mut claims = claims.unwrap();
|
||||
if wrong_session {
|
||||
claims.session.push_str("-revoked");
|
||||
}
|
||||
assert_eq!(
|
||||
verify(generated, &mut context, claims, scope, registry)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.0,
|
||||
401
|
||||
);
|
||||
assert!(context.id().is_err());
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let (directory, path) = directory(
|
||||
"auth",
|
||||
r#""http":{"host":"127.0.0.1","port":18001},"sites":{"admin":{"path":"admin","hosts":["benchmark.test"],"auth":"session","origin":"https://benchmark.test"}},"auth":{"providers":{"session":{"verify":"bench.account.verify","jwtSecret":"benchmark-only-key-not-for-any-deployment","ttlSeconds":3600,"cookie":"session"}}}"#,
|
||||
);
|
||||
let runtime = runtime();
|
||||
let registry = runtime
|
||||
.run(async |scope| database_registry(&path, scope).await)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let (config, input) = prepare(&path);
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
for (index, claims) in input.claims.iter().enumerate() {
|
||||
let account = GENERATED_SEED(
|
||||
&claims.subject,
|
||||
&claims.session,
|
||||
index < 16,
|
||||
scope,
|
||||
&mut database,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(account.field0.0, index as i64 + 1);
|
||||
}
|
||||
let (mut context, claims) = input
|
||||
.service
|
||||
.prepare("admin", &input.requests[0], false, input.now)
|
||||
.unwrap();
|
||||
drop(database);
|
||||
api_verify(&mut context, claims.unwrap(), scope, ®istry)
|
||||
.await
|
||||
.map_err(|error| error.0)
|
||||
.unwrap();
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database.on(AUTH_BINDING, scope).await.unwrap();
|
||||
dever_runtime::authorization::synchronize(
|
||||
executor,
|
||||
scope,
|
||||
&config,
|
||||
&[dever_runtime::authorization::Declaration {
|
||||
topic: "admin",
|
||||
component: "bench",
|
||||
domain: "account",
|
||||
action: "permissions",
|
||||
method: "GET",
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
dever_runtime::authorization::save(
|
||||
&mut database,
|
||||
AUTH_BINDING,
|
||||
scope,
|
||||
&context,
|
||||
"reader",
|
||||
"Benchmark reader",
|
||||
false,
|
||||
&["bench.account.admin.permissions".into()],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
for user in 1..=16 {
|
||||
let executor = database.on(AUTH_BINDING, scope).await.unwrap();
|
||||
dever_runtime::authorization::grant(executor, scope, &context, user, "reader")
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
check_auth(&runtime, ®istry, &input);
|
||||
header();
|
||||
if HTTP_BENCH {
|
||||
protected_http::run(&runtime, ®istry, &config, &input);
|
||||
} else {
|
||||
for (name, roles) in [
|
||||
("auth_provider_verify", false),
|
||||
("auth_provider_role_read", true),
|
||||
] {
|
||||
samples(name, |generated, count| {
|
||||
measure_auth(&runtime, ®istry, &input, generated, roles, count)
|
||||
});
|
||||
}
|
||||
}
|
||||
check_auth(&runtime, ®istry, &input);
|
||||
drop(registry);
|
||||
drop(runtime);
|
||||
drop(directory);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// 手写 Rust 重做 Provider 的查询、完整行解码和身份判断;不调用生成函数。
|
||||
use dever_runtime::{
|
||||
api::Failure as ApiFailure,
|
||||
auth::{self, Claims, Identity},
|
||||
};
|
||||
|
||||
struct RustAccount {
|
||||
id: dever_runtime::database::Id<AUTH_USER_MODEL>,
|
||||
_created_at: dever_runtime::time::calendar::DateTime,
|
||||
subject: String,
|
||||
_session: String,
|
||||
_active: bool,
|
||||
}
|
||||
|
||||
async fn rust_verify(
|
||||
context: &mut auth::Context<'_>,
|
||||
claims: Claims,
|
||||
scope: &Scope<'_>,
|
||||
registry: &Arc<Registry>,
|
||||
) -> Result<(), ApiFailure> {
|
||||
if context.verifier() != Some("bench.account.verify") {
|
||||
return Err(ApiFailure(500));
|
||||
}
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database
|
||||
.on(ACCOUNT_BINDING, scope)
|
||||
.await
|
||||
.map_err(|_| ApiFailure(500))?;
|
||||
let mut rows = executor.query(scope,
|
||||
"SELECT \"id\",\"created_at\",\"subject\",\"session\",\"active\" FROM \"bench_account_model\" WHERE (((\"subject\" IS ?1) AND (\"session\" IS ?2)) AND (\"active\" IS ?3)) ORDER BY \"id\" DESC LIMIT 1",
|
||||
vec![Parameter::Text(claims.subject), Parameter::Text(claims.session), Parameter::Integer(1)], 1,
|
||||
|row| Ok(RustAccount {
|
||||
id: dever_runtime::database::Id(row.get::<_, i64>(0)?),
|
||||
_created_at: dever_runtime::time::calendar::DateTime::from_millis(row.get::<_, i64>(1)?).map_err(|_| dever_runtime::database::Error::new(dever_runtime::database::Kind::InvalidData, "invalid stored DateTime"))?,
|
||||
subject: row.get(2)?, _session: row.get(3)?, _active: row.get(4)?,
|
||||
})
|
||||
).await.map_err(|_| ApiFailure(500))?;
|
||||
let row = rows.pop().ok_or(ApiFailure(401))?;
|
||||
context
|
||||
.establish(Identity {
|
||||
id: row.subject,
|
||||
user_id: Some(row.id.0),
|
||||
tenant_id: None,
|
||||
})
|
||||
.map_err(|error| ApiFailure(error.status()))
|
||||
}
|
||||
|
||||
// 输出所有字段,防止只读长度让优化器删掉 Dever 或 Rust 的记录转换。
|
||||
type PermissionView = [String; 7];
|
||||
|
||||
async fn rust_permissions(
|
||||
context: &mut auth::Context<'_>,
|
||||
scope: &Scope<'_>,
|
||||
database: &mut DbContext,
|
||||
) -> Vec<PermissionView> {
|
||||
dever_runtime::authorization::permissions(database, AUTH_BINDING, scope, context, true)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|value| {
|
||||
[
|
||||
value.key,
|
||||
value.component,
|
||||
value.domain,
|
||||
value.site,
|
||||
value.action,
|
||||
value.method,
|
||||
value.kind,
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn permissions(
|
||||
generated: bool,
|
||||
context: &mut auth::Context<'_>,
|
||||
scope: &Scope<'_>,
|
||||
database: &mut DbContext,
|
||||
) -> Vec<PermissionView> {
|
||||
if generated {
|
||||
GENERATED_PERMISSIONS(scope, database, context)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|value| {
|
||||
[
|
||||
value.field0,
|
||||
value.field1,
|
||||
value.field2,
|
||||
value.field3,
|
||||
value.field4,
|
||||
value.field5,
|
||||
value.field6,
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
rust_permissions(context, scope, database).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify(
|
||||
generated: bool,
|
||||
context: &mut auth::Context<'_>,
|
||||
claims: Claims,
|
||||
scope: &Scope<'_>,
|
||||
registry: &Arc<Registry>,
|
||||
) -> Result<(), ApiFailure> {
|
||||
if generated {
|
||||
api_verify(context, claims, scope, registry).await
|
||||
} else {
|
||||
rust_verify(context, claims, scope, registry).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// SQLite、资源作用域和优化参数两侧一致;只比较生成层,不推断完整语言或 HTTP 吞吐。
|
||||
use dever_runtime::{
|
||||
database::{Context as DbContext, Parameter, Registry},
|
||||
task::{Limits, Runtime, Scope},
|
||||
};
|
||||
use std::{
|
||||
hint::black_box,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
struct BenchDirectory(PathBuf);
|
||||
|
||||
impl Drop for BenchDirectory {
|
||||
fn drop(&mut self) {
|
||||
std::fs::remove_dir_all(&self.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn directory(name: &str, extra: &str) -> (BenchDirectory, PathBuf) {
|
||||
let unique = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let relative = format!("{name}-benchmark-{}-{unique}", std::process::id());
|
||||
let directory = BenchDirectory(
|
||||
std::env::current_exe()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(&relative),
|
||||
);
|
||||
std::fs::create_dir(&directory.0).unwrap();
|
||||
let settings = directory.0.join("setting.json");
|
||||
std::fs::write(&settings, format!(r#"{{"database":{{"default":{{"type":"sqlite","path":"{relative}/case.db","max_connections":1,"max_page_size":2048}}}},{extra}}}"#)).unwrap();
|
||||
(directory, settings)
|
||||
}
|
||||
|
||||
fn runtime() -> Runtime {
|
||||
Runtime::new(Limits {
|
||||
threads: 1,
|
||||
workers: 1,
|
||||
tasks: 16,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn smoke() -> bool {
|
||||
std::env::args().any(|value| value == "--smoke")
|
||||
}
|
||||
|
||||
fn samples(name: &str, mut measure: impl FnMut(bool, usize) -> (u128, u64)) {
|
||||
if let Ok(selected) = std::env::var("DEVER_BENCH_CASES")
|
||||
&& !selected.split(',').any(|case| case == name)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let (rounds, count) = if smoke() { (1, 4) } else { (15, 2000) };
|
||||
if !smoke() {
|
||||
for generated in [true, false] {
|
||||
black_box(measure(generated, 200));
|
||||
}
|
||||
}
|
||||
for sample in 0..rounds {
|
||||
let mut order = [("dever", true), ("rust", false), ("control", true)];
|
||||
order.rotate_left(sample % 3);
|
||||
let mut expected = None;
|
||||
for (implementation, generated) in order {
|
||||
let (elapsed, checksum) = measure(generated, count);
|
||||
if let Some(expected) = expected {
|
||||
assert_eq!(checksum, expected);
|
||||
}
|
||||
expected = Some(checksum);
|
||||
if !smoke() {
|
||||
println!("{name},{sample},{implementation},{elapsed},{checksum}");
|
||||
}
|
||||
}
|
||||
}
|
||||
if smoke() {
|
||||
println!("correctness smoke passed: {name}; no performance result collected");
|
||||
}
|
||||
}
|
||||
|
||||
fn header() {
|
||||
eprintln!(
|
||||
"Rust 1.95.0; release fat LTO; codegen-units=1; overflow-checks=true; SQLite WAL/NORMAL; one connection; one runtime worker; setup/checks excluded"
|
||||
);
|
||||
if !smoke() {
|
||||
println!("case,sample,implementation,nanoseconds,checksum");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
//! 文本和集合对照的构建入口,与其他性能组共用正式生成器和 Cargo 配置。
|
||||
|
||||
use super::compare;
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) fn run(core: &Path) -> bool {
|
||||
if std::env::args().any(|argument| argument == "--text") {
|
||||
compare(
|
||||
core,
|
||||
"text",
|
||||
"text",
|
||||
&(include_str!("../samples.rs").to_owned() + include_str!("../text.rs")),
|
||||
&[
|
||||
("GENERATED_CHAIN", "chain"),
|
||||
("GENERATED_PAIR", "pair"),
|
||||
("GENERATED_TEMPORARY", "temporarySuffix"),
|
||||
("GENERATED_FORWARDED", "forwardedSuffix"),
|
||||
("GENERATED_CONTAINS", "containsAll"),
|
||||
("GENERATED_LIST_CONTAINS", "listContains"),
|
||||
("GENERATED_TEXT_CONCAT", "concatenateText"),
|
||||
("GENERATED_BYTES_CONCAT", "concatenateBytes"),
|
||||
],
|
||||
&[("SEARCH_PAIR", "SearchPair")],
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if std::env::args().any(|argument| argument == "--map-values") {
|
||||
compare(
|
||||
core,
|
||||
"map-values",
|
||||
"map-values",
|
||||
&(include_str!("../samples.rs").to_owned()
|
||||
+ include_str!("../map_values/mod.rs")
|
||||
+ include_str!("../map_values/select.rs")),
|
||||
&[
|
||||
("GENERATED_BUILD_TEXT", "buildText"),
|
||||
("GENERATED_BUILD_NESTED", "buildNested"),
|
||||
("GENERATED_OWNED_TEXT", "ownedText"),
|
||||
("GENERATED_OWNED_NESTED", "ownedNested"),
|
||||
("GENERATED_BORROWED_TEXT", "borrowedText"),
|
||||
("GENERATED_BORROWED_NESTED", "borrowedNested"),
|
||||
("GENERATED_LIST_FIRST", "firstText"),
|
||||
("GENERATED_MAP_GET", "lookupText"),
|
||||
("GENERATED_MAP_FILTER", "filterText"),
|
||||
("GENERATED_MAP_FIND", "findText"),
|
||||
],
|
||||
&[],
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if std::env::args().any(|argument| argument == "--storage") {
|
||||
compare(
|
||||
core,
|
||||
"storage",
|
||||
"storage",
|
||||
&(include_str!("../samples.rs").to_owned() + include_str!("../storage.rs")),
|
||||
&[
|
||||
("GENERATED_LOOKUP", "lookup"),
|
||||
("GENERATED_SCAN", "scan"),
|
||||
("GENERATED_BUILD_MAP", "buildMap"),
|
||||
("GENERATED_ENTRIES", "buildEntries"),
|
||||
("GENERATED_BUILD_LIST", "buildList"),
|
||||
("GENERATED_BUFFER_BUILD", "bufferBuild"),
|
||||
("GENERATED_BUFFER_INSPECT", "bufferInspect"),
|
||||
("GENERATED_RECORD_READ", "readScalarRows"),
|
||||
("GENERATED_TEXT_LOOKUP", "lookupText"),
|
||||
],
|
||||
&[("BUFFER_STATE", "BufferState"), ("SCALAR_ROW", "ScalarRow")],
|
||||
);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
//! 所有性能组共用正式生成器、锁定依赖和 release 构建参数。
|
||||
|
||||
use crate::auth_build;
|
||||
mod collections;
|
||||
pub(crate) use collections::run as collections;
|
||||
use dever::compiler::{emit, project::Project};
|
||||
use dever::process::{child::output::require_success, command};
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) fn compare(
|
||||
core: &Path,
|
||||
fixture: &str,
|
||||
name: &str,
|
||||
reference: &str,
|
||||
functions: &[(&str, &str)],
|
||||
definitions: &[(&str, &str)],
|
||||
) {
|
||||
let path = core.join(format!("test/rust/fixtures/{fixture}"));
|
||||
let path = if path.is_dir() {
|
||||
path
|
||||
} else {
|
||||
path.with_extension("dever")
|
||||
};
|
||||
let project = Project::load(&path).unwrap();
|
||||
let program = project.check().unwrap();
|
||||
let mut comparison = auth_build::metadata(name, &program, reference);
|
||||
if name == "decimal-extra" {
|
||||
for (marker, path) in [
|
||||
("DECIMAL_CODEC_SOURCE", "codec/numeric.rs"),
|
||||
("DECIMAL_SCALAR_SOURCE", "scalar/decimal.rs"),
|
||||
] {
|
||||
comparison = comparison.replace(
|
||||
&format!("\"{marker}\""),
|
||||
&format!("{:?}", core.join("rust/src/runtime/database").join(path)),
|
||||
);
|
||||
}
|
||||
}
|
||||
let memory = std::env::args().any(|argument| argument == "--memory");
|
||||
if memory {
|
||||
assert!(
|
||||
comparison.contains("fn compare_output<"),
|
||||
"memory requires the shared sample harness"
|
||||
);
|
||||
comparison = comparison.replacen("fn compare_output<", "fn timed_compare_output<", 1);
|
||||
comparison.push_str(include_str!("../memory.rs"));
|
||||
}
|
||||
let mut roots = Vec::new();
|
||||
for (marker, name) in functions {
|
||||
if let Some(index) = program
|
||||
.commands
|
||||
.iter()
|
||||
.position(|command| command.name == *name)
|
||||
{
|
||||
comparison = comparison.replace(marker, &format!("command{index}"));
|
||||
roots.push(program.commands[index].function);
|
||||
continue;
|
||||
}
|
||||
let (package, name) = name
|
||||
.rsplit_once('.')
|
||||
.map_or((None, *name), |(package, name)| (Some(package), name));
|
||||
let position = program
|
||||
.functions
|
||||
.iter()
|
||||
.position(|function| {
|
||||
function.signature.name == name
|
||||
&& package.is_none_or(|package| {
|
||||
program.packages[function.signature.source] == package
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
comparison = comparison.replace(marker, &format!("function{position}"));
|
||||
roots.push(position);
|
||||
}
|
||||
for (marker, name) in definitions {
|
||||
let position = program
|
||||
.definitions
|
||||
.iter()
|
||||
.position(|definition| definition.name == *name)
|
||||
.unwrap();
|
||||
comparison = comparison.replace(marker, &format!("Type{position}"));
|
||||
}
|
||||
let task_dependencies = if name == "rest-json" {
|
||||
"chrono = { version = \"=0.4.45\", default-features = false, features = [\"std\"] }\n"
|
||||
} else if name == "tasks" {
|
||||
"tokio = { version = \"=1.53.1\", features = [\"rt-multi-thread\", \"sync\", \"time\", \"macros\"] }\ntokio-util = { version = \"=0.7.19\", features = [\"rt\"] }\nfutures-util = { version = \"=0.3.34\", default-features = false, features = [\"std\"] }\n"
|
||||
} else if matches!(name, "websocket" | "websocket-protocol") {
|
||||
"tokio = { version = \"=1.53.1\", features = [\"rt-multi-thread\", \"net\", \"sync\", \"time\", \"macros\"] }\ntokio-util = { version = \"=0.7.19\", features = [\"rt\"] }\nfutures-util = { version = \"=0.3.34\", default-features = false, features = [\"std\", \"sink\"] }\ntokio-tungstenite = { version = \"=0.30.0\", default-features = false, features = [\"handshake\"] }\n"
|
||||
} else if name == "query" {
|
||||
"axum = { version = \"=0.8.9\", default-features = false, features = [\"query\"] }\nserde = { version = \"=1.0.228\", features = [\"derive\"] }\nserde_urlencoded = \"=0.7.1\"\n"
|
||||
} else if name == "transport" {
|
||||
crate::transport::DEPENDENCIES
|
||||
} else if matches!(name, "auth" | "auth-http") {
|
||||
"tokio = { version = \"=1.53.1\", features = [\"sync\"] }\n"
|
||||
} else if name == "extended-primitives" {
|
||||
"sha2 = \"=0.10.9\"\nuuid = { version = \"=1.28.0\", default-features = false }\n"
|
||||
} else if name == "decimal-extra" {
|
||||
"serde = \"=1.0.228\"\nserde_json = \"=1.0.145\"\nbytes = \"=1.12.1\"\n"
|
||||
} else if name == "api-wire" {
|
||||
"serde_json = \"=1.0.145\"\nhyper = \"=1.11.1\"\n"
|
||||
} else if matches!(name, "database" | "database-postgres" | "application") {
|
||||
"serde_json = \"=1.0.145\"\n"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let name = if memory {
|
||||
format!("{name}-memory")
|
||||
} else {
|
||||
name.to_owned()
|
||||
};
|
||||
let directory = core.join(format!("rust/target/performance/{name}"));
|
||||
std::fs::create_dir_all(&directory).unwrap();
|
||||
roots.extend(program.commands.iter().map(|command| command.function));
|
||||
if matches!(
|
||||
name.trim_end_matches("-memory"),
|
||||
"auth" | "auth-http" | "jobs" | "api-wire" | "rest-wire" | "rest-json"
|
||||
) {
|
||||
roots.extend(program.entries());
|
||||
}
|
||||
let roots = roots.into_iter().collect::<std::collections::BTreeSet<_>>();
|
||||
let functions = program.reachable(roots.iter().copied());
|
||||
let mut source = emit::generate(&program, &project.sources, &roots);
|
||||
source.push_str(&auth_build::application(
|
||||
name.trim_end_matches("-memory"),
|
||||
&program,
|
||||
));
|
||||
source.push_str(&comparison);
|
||||
write(&directory.join("main.rs"), &source);
|
||||
let mut features = dever::compiler::build::runtime_features(&program, &functions);
|
||||
if name.trim_end_matches("-memory") == "query" {
|
||||
features.push("api");
|
||||
}
|
||||
if matches!(
|
||||
name.trim_end_matches("-memory"),
|
||||
"database-postgres" | "decimal-extra"
|
||||
) {
|
||||
features.push("postgres");
|
||||
}
|
||||
let features = format!(", features = {features:?}");
|
||||
write(
|
||||
&directory.join("Cargo.toml"),
|
||||
&format!(
|
||||
"[package]\nname = \"dever-comparison-{name}\"\nversion = \"0.0.1\"\nedition = \"2024\"\n[workspace]\n[[bin]]\nname = \"dever-comparison-{name}\"\npath = \"main.rs\"\n[dependencies]\ndever-runtime = {{ path = {:?}{features} }}\nindexmap = \"=2.14.0\"\ndec = \"=0.4.11\"\n{task_dependencies}[profile.release]\nlto = \"fat\"\ncodegen-units = 1\noverflow-checks = true\n",
|
||||
core.join("rust/src/runtime").to_string_lossy()
|
||||
),
|
||||
);
|
||||
// 基准清单随候选运行库变化;只刷新这个生成项目的离线依赖锁。
|
||||
let arguments = ["+1.95.0", "generate-lockfile", "--offline"];
|
||||
let child = command::start("cargo".as_ref(), &arguments, &directory, true).unwrap();
|
||||
require_success(child.output().unwrap()).unwrap();
|
||||
let result = command::start(
|
||||
"cargo".as_ref(),
|
||||
&[
|
||||
"+1.95.0",
|
||||
"build",
|
||||
"--offline",
|
||||
"--locked",
|
||||
"--release",
|
||||
"--target-dir",
|
||||
"../../programs",
|
||||
],
|
||||
&directory,
|
||||
true,
|
||||
)
|
||||
.unwrap()
|
||||
.output()
|
||||
.unwrap();
|
||||
require_success(result).unwrap();
|
||||
let binary = core.join(format!(
|
||||
"rust/target/programs/release/dever-comparison-{name}"
|
||||
));
|
||||
if memory || std::env::args().any(|argument| argument == "--build-only") {
|
||||
println!("{}", binary.display());
|
||||
return;
|
||||
}
|
||||
let status = command::start(binary.as_os_str(), &[] as &[&str], &directory, false)
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
assert!(status.success());
|
||||
}
|
||||
|
||||
fn write(path: &Path, contents: &str) {
|
||||
if std::fs::read_to_string(path).ok().as_deref() != Some(contents) {
|
||||
std::fs::write(path, contents).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
fn byte_fault() -> Fault {
|
||||
&FaultInfo {
|
||||
message: "byte conversion failed",
|
||||
source: "reference.dever",
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn total_reference(values: &dever_runtime::Bytes) -> Result<i64, Fault> {
|
||||
values.iter().try_fold(0i64, |total, value| {
|
||||
total.checked_add(i64::from(*value)).ok_or_else(byte_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn each_reference(values: &dever_runtime::Bytes) -> Result<Vec<i64>, Fault> {
|
||||
values
|
||||
.iter()
|
||||
.map(|value| i64::from(*value).checked_add(1).ok_or_else(byte_fault))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn encode_generated(values: &[String]) -> Result<dever_runtime::Bytes, Fault> {
|
||||
GENERATED_ENCODE(values.first().map(String::as_str).unwrap_or(""))
|
||||
}
|
||||
|
||||
fn encode_reference(values: &[String]) -> Result<dever_runtime::Bytes, Fault> {
|
||||
Ok(values
|
||||
.first()
|
||||
.map(String::as_str)
|
||||
.unwrap_or("")
|
||||
.as_bytes()
|
||||
.to_vec()
|
||||
.into())
|
||||
}
|
||||
|
||||
fn decode_generated(values: &dever_runtime::Bytes) -> Result<String, Fault> {
|
||||
GENERATED_DECODE(values).map_err(|_| byte_fault())
|
||||
}
|
||||
|
||||
fn decode_reference(values: &dever_runtime::Bytes) -> Result<String, Fault> {
|
||||
std::str::from_utf8(values)
|
||||
.map(str::to_owned)
|
||||
.map_err(|_| byte_fault())
|
||||
}
|
||||
|
||||
fn convert_generated(values: &[i64]) -> Result<dever_runtime::Bytes, Fault> {
|
||||
GENERATED_CONVERT(values).map_err(|_| byte_fault())
|
||||
}
|
||||
|
||||
fn convert_reference(values: &[i64]) -> Result<dever_runtime::Bytes, Fault> {
|
||||
values
|
||||
.iter()
|
||||
.map(|value| u8::try_from(*value).map_err(|_| byte_fault()))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
fn slice_generated(values: &dever_runtime::Bytes) -> Result<dever_runtime::Bytes, Fault> {
|
||||
GENERATED_SLICE(values).map_err(|_| byte_fault())
|
||||
}
|
||||
|
||||
fn slice_reference(values: &dever_runtime::Bytes) -> Result<dever_runtime::Bytes, Fault> {
|
||||
values
|
||||
.get(0..values.len())
|
||||
.map(<[u8]>::to_vec)
|
||||
.map(Into::into)
|
||||
.ok_or_else(byte_fault)
|
||||
}
|
||||
|
||||
fn join_reference(values: &dever_runtime::Bytes) -> Result<dever_runtime::Bytes, Fault> {
|
||||
let mut result = Vec::with_capacity(values.len() + values.len());
|
||||
result.extend_from_slice(values);
|
||||
result.extend_from_slice(values);
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
fn roundtrip_generated(values: &[String]) -> Result<String, Fault> {
|
||||
GENERATED_ROUNDTRIP(values.first().map(String::as_str).unwrap_or("")).map_err(|_| byte_fault())
|
||||
}
|
||||
|
||||
fn roundtrip_reference(values: &[String]) -> Result<String, Fault> {
|
||||
let bytes = values
|
||||
.first()
|
||||
.map(String::as_str)
|
||||
.unwrap_or("")
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
String::from_utf8(bytes).map_err(|_| byte_fault())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let text = "hello 雪🙂 ".repeat(256);
|
||||
let texts = [text];
|
||||
let bytes = dever_runtime::Bytes::copy_from_slice(texts[0].as_bytes());
|
||||
let integers: Vec<i64> = (0..1024).map(|value| value % 256).collect();
|
||||
let invalid = dever_runtime::Bytes::from_static(&[255]);
|
||||
assert!(decode_generated(&invalid).is_err());
|
||||
assert!(decode_reference(&invalid).is_err());
|
||||
assert_eq!(GENERATED_IDENTITY(&bytes).unwrap().as_ptr(), bytes.as_ptr());
|
||||
assert!(convert_generated(&[-1]).is_err());
|
||||
assert!(convert_reference(&[-1]).is_err());
|
||||
assert!(convert_generated(&[256]).is_err());
|
||||
assert!(convert_reference(&[256]).is_err());
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
compare_bytes(
|
||||
"bytes_reduce",
|
||||
&bytes,
|
||||
2000,
|
||||
GENERATED_TOTAL,
|
||||
total_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare_bytes(
|
||||
"bytes_each",
|
||||
&bytes,
|
||||
2000,
|
||||
GENERATED_EACH,
|
||||
each_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"bytes_encode",
|
||||
&texts,
|
||||
20000,
|
||||
encode_generated,
|
||||
encode_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare_bytes(
|
||||
"bytes_decode",
|
||||
&bytes,
|
||||
4000,
|
||||
decode_generated,
|
||||
decode_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"bytes_from_ints",
|
||||
&integers,
|
||||
2000,
|
||||
convert_generated,
|
||||
convert_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare_bytes(
|
||||
"bytes_slice",
|
||||
&bytes,
|
||||
20000,
|
||||
slice_generated,
|
||||
slice_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare_bytes(
|
||||
"bytes_concat",
|
||||
&bytes,
|
||||
10000,
|
||||
GENERATED_JOIN,
|
||||
join_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"bytes_roundtrip",
|
||||
&texts,
|
||||
4000,
|
||||
roundtrip_generated,
|
||||
roundtrip_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
use std::hint::black_box;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ReferenceFailure {
|
||||
Fault(Fault),
|
||||
Failed(String, Fault),
|
||||
}
|
||||
|
||||
fn state_reference(value: &STATE_TYPE) -> Result<i64, Fault> {
|
||||
Ok(match value {
|
||||
STATE_TYPE::Variant0 => 0,
|
||||
STATE_TYPE::Variant1(number) => *number,
|
||||
STATE_TYPE::Variant2(text) => text.chars().count() as i64,
|
||||
})
|
||||
}
|
||||
|
||||
fn nullable_reference(value: &Option<String>) -> Result<i64, Fault> {
|
||||
Ok(match value {
|
||||
None => -1,
|
||||
Some(text) if text == "zero" => 0,
|
||||
Some(text) => text.chars().count() as i64,
|
||||
})
|
||||
}
|
||||
|
||||
fn operation_reference(value: i64) -> Result<i64, ReferenceFailure> {
|
||||
if value >= 0 {
|
||||
Ok(value)
|
||||
} else {
|
||||
Err(ReferenceFailure::Failed(
|
||||
"negative".into(),
|
||||
&FaultInfo {
|
||||
message: "Result.Failed",
|
||||
source: "reference.dever",
|
||||
line: 1,
|
||||
column: 1,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn propagate_reference(value: i64) -> Result<i64, ReferenceFailure> {
|
||||
Ok(operation_reference(value)?)
|
||||
}
|
||||
|
||||
fn capture_reference(value: i64) -> Result<RESULT_TYPE, Fault> {
|
||||
match propagate_reference(value) {
|
||||
Ok(number) => Ok(RESULT_TYPE::Variant0(number)),
|
||||
Err(ReferenceFailure::Failed(message, _origin)) => Ok(RESULT_TYPE::Variant1(message)),
|
||||
Err(ReferenceFailure::Fault(fault)) => Err(fault),
|
||||
}
|
||||
}
|
||||
|
||||
fn change_reference(value: &STATE_TYPE) -> Result<STATE_TYPE, Fault> {
|
||||
Ok(match value {
|
||||
STATE_TYPE::Variant2(text) => {
|
||||
let mut result = String::with_capacity(text.len() + "新".len());
|
||||
result.push_str(text);
|
||||
result.push_str("新");
|
||||
STATE_TYPE::Variant2(result)
|
||||
}
|
||||
_ => value.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn score(value: RESULT_TYPE) -> usize {
|
||||
match value {
|
||||
RESULT_TYPE::Variant0(number) => number as usize,
|
||||
RESULT_TYPE::Variant1(message) => message.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn sample(
|
||||
name: &str,
|
||||
sample: usize,
|
||||
implementation: &str,
|
||||
count: usize,
|
||||
mut operation: impl FnMut(usize) -> usize,
|
||||
) {
|
||||
let start = Instant::now();
|
||||
let mut checksum = 0usize;
|
||||
for position in 0..count {
|
||||
checksum = checksum.wrapping_add(black_box(operation(black_box(position))));
|
||||
}
|
||||
println!(
|
||||
"{name},{sample},{implementation},{},{checksum}",
|
||||
start.elapsed().as_nanos()
|
||||
);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let states = [
|
||||
STATE_TYPE::Variant0,
|
||||
STATE_TYPE::Variant1(42),
|
||||
STATE_TYPE::Variant2("雪山树林".repeat(16)),
|
||||
];
|
||||
let texts = [None, Some("zero".into()), Some("雪山树林".repeat(16))];
|
||||
for state in &states {
|
||||
assert_eq!(
|
||||
GENERATED_STATE(state).unwrap(),
|
||||
state_reference(state).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
GENERATED_CHANGE(state).unwrap(),
|
||||
change_reference(state).unwrap()
|
||||
);
|
||||
}
|
||||
for text in &texts {
|
||||
assert_eq!(
|
||||
GENERATED_NULLABLE(text).unwrap(),
|
||||
nullable_reference(text).unwrap()
|
||||
);
|
||||
}
|
||||
for value in [i64::MIN, -1, 0, 1, i64::MAX] {
|
||||
assert_eq!(
|
||||
GENERATED_PROPAGATE(value).map_err(|_| ()),
|
||||
propagate_reference(value).map_err(|_| ())
|
||||
);
|
||||
assert_eq!(
|
||||
GENERATED_CAPTURE(value).unwrap(),
|
||||
capture_reference(value).unwrap()
|
||||
);
|
||||
}
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
for iteration in 0..9 {
|
||||
for generated in if iteration % 2 == 0 {
|
||||
[true, false]
|
||||
} else {
|
||||
[false, true]
|
||||
} {
|
||||
let implementation = if generated { "dever" } else { "rust" };
|
||||
if generated {
|
||||
sample(
|
||||
"choice_payload",
|
||||
iteration,
|
||||
implementation,
|
||||
1_000_000,
|
||||
|position| GENERATED_STATE(black_box(&states[position % 3])).unwrap() as usize,
|
||||
);
|
||||
sample(
|
||||
"nullable_text",
|
||||
iteration,
|
||||
implementation,
|
||||
1_000_000,
|
||||
|position| {
|
||||
GENERATED_NULLABLE(black_box(&texts[position % 3])).unwrap() as usize
|
||||
},
|
||||
);
|
||||
sample(
|
||||
"default_failure",
|
||||
iteration,
|
||||
implementation,
|
||||
500_000,
|
||||
|position| {
|
||||
black_box(GENERATED_PROPAGATE((position % 8) as i64 - 1)).unwrap_or(7)
|
||||
as usize
|
||||
},
|
||||
);
|
||||
sample(
|
||||
"capture_failure",
|
||||
iteration,
|
||||
implementation,
|
||||
500_000,
|
||||
|position| {
|
||||
score(black_box(
|
||||
GENERATED_CAPTURE((position % 8) as i64 - 1).unwrap(),
|
||||
))
|
||||
},
|
||||
);
|
||||
sample(
|
||||
"choice_copy",
|
||||
iteration,
|
||||
implementation,
|
||||
300_000,
|
||||
|_| match black_box(GENERATED_CHANGE(black_box(&states[2])).unwrap()) {
|
||||
STATE_TYPE::Variant2(text) => text.len(),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
sample(
|
||||
"choice_payload",
|
||||
iteration,
|
||||
implementation,
|
||||
1_000_000,
|
||||
|position| state_reference(black_box(&states[position % 3])).unwrap() as usize,
|
||||
);
|
||||
sample(
|
||||
"nullable_text",
|
||||
iteration,
|
||||
implementation,
|
||||
1_000_000,
|
||||
|position| {
|
||||
nullable_reference(black_box(&texts[position % 3])).unwrap() as usize
|
||||
},
|
||||
);
|
||||
sample(
|
||||
"default_failure",
|
||||
iteration,
|
||||
implementation,
|
||||
500_000,
|
||||
|position| {
|
||||
black_box(propagate_reference((position % 8) as i64 - 1)).unwrap_or(7)
|
||||
as usize
|
||||
},
|
||||
);
|
||||
sample(
|
||||
"capture_failure",
|
||||
iteration,
|
||||
implementation,
|
||||
500_000,
|
||||
|position| {
|
||||
score(black_box(
|
||||
capture_reference((position % 8) as i64 - 1).unwrap(),
|
||||
))
|
||||
},
|
||||
);
|
||||
sample(
|
||||
"choice_copy",
|
||||
iteration,
|
||||
implementation,
|
||||
300_000,
|
||||
|_| match black_box(change_reference(black_box(&states[2])).unwrap()) {
|
||||
STATE_TYPE::Variant2(text) => text.len(),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
fn fault() -> Fault {
|
||||
&FaultInfo {
|
||||
message: "integer arithmetic fault",
|
||||
source: "reference.dever",
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_reference(values: &[i64]) -> Result<Vec<i64>, Fault> {
|
||||
let mut output = Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
output.push(value.checked_mul(2).ok_or_else(fault)?);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn filter_reference(values: &[i64]) -> Result<Vec<i64>, Fault> {
|
||||
let mut output = Vec::new();
|
||||
for value in values {
|
||||
if *value > 0 {
|
||||
output.push(*value);
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn find_reference(values: &[i64]) -> Result<Option<i64>, Fault> {
|
||||
Ok(values.iter().find(|value| **value > 997).copied())
|
||||
}
|
||||
|
||||
fn find_generated(values: &[i64]) -> Result<Option<i64>, Fault> {
|
||||
GENERATED_FIND(values, 997)
|
||||
}
|
||||
|
||||
fn sum_reference(values: &[i64]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
let projected = value.checked_mul(2).ok_or_else(fault)?;
|
||||
total = total.checked_add(projected).ok_or_else(fault)?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn fold_reference(values: &[i64]) -> Result<i64, Fault> {
|
||||
let mut state = 0i64;
|
||||
for value in values {
|
||||
state = state.checked_add(*value).ok_or_else(fault)?;
|
||||
}
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
fn stop_reference(values: &[i64]) -> Result<i64, Fault> {
|
||||
let mut state = 0i64;
|
||||
for value in values {
|
||||
state = state.checked_add(*value).ok_or_else(fault)?;
|
||||
if state >= 3 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
fn rows_reference(values: &[ROW_TYPE]) -> Result<Vec<ROW_TYPE>, Fault> {
|
||||
let mut output = Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
let mut name = String::with_capacity(value.field0.len() + "新".len());
|
||||
name.push_str(&value.field0);
|
||||
name.push_str("新");
|
||||
output.push(ROW_TYPE {
|
||||
field0: name,
|
||||
field1: value.field1.checked_add(1).ok_or_else(fault)?,
|
||||
});
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let values: Vec<i64> = (0..1024).map(|value| (value * 17) % 1000 - 1).collect();
|
||||
let rows: Vec<_> = (0..64)
|
||||
.map(|index| ROW_TYPE {
|
||||
field0: "雪山树林".repeat(4),
|
||||
field1: index,
|
||||
})
|
||||
.collect();
|
||||
for values in [&[i64::MAX][..], &[i64::MAX / 2, 3][..], &[i64::MIN, -1][..]] {
|
||||
assert_eq!(
|
||||
GENERATED_MAP(values).map_err(|_| ()),
|
||||
map_reference(values).map_err(|_| ())
|
||||
);
|
||||
assert_eq!(
|
||||
GENERATED_SUM(values).map_err(|_| ()),
|
||||
sum_reference(values).map_err(|_| ())
|
||||
);
|
||||
assert_eq!(
|
||||
GENERATED_FOLD(values).map_err(|_| ()),
|
||||
fold_reference(values).map_err(|_| ())
|
||||
);
|
||||
assert_eq!(
|
||||
GENERATED_STOP(values).map_err(|_| ()),
|
||||
stop_reference(values).map_err(|_| ())
|
||||
);
|
||||
}
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
compare(
|
||||
"each_int",
|
||||
&values,
|
||||
20000,
|
||||
GENERATED_MAP,
|
||||
map_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"filter_int",
|
||||
&values,
|
||||
20000,
|
||||
GENERATED_FILTER,
|
||||
filter_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"find_int",
|
||||
&values,
|
||||
50000,
|
||||
find_generated,
|
||||
find_reference,
|
||||
|value| value.unwrap_or(0) as u64,
|
||||
);
|
||||
compare(
|
||||
"project_sum",
|
||||
&values,
|
||||
30000,
|
||||
GENERATED_SUM,
|
||||
sum_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"reduce_int",
|
||||
&values,
|
||||
30000,
|
||||
GENERATED_FOLD,
|
||||
fold_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"reduce_until_int",
|
||||
&values,
|
||||
500000,
|
||||
GENERATED_STOP,
|
||||
stop_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"each_record",
|
||||
&rows,
|
||||
5000,
|
||||
GENERATED_ROWS,
|
||||
rows_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
//! 同一负载分别链接改造前后源码;衡量运行库和编译器成本,不冒充独立 Rust 基准。
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use dever::{
|
||||
compiler::check,
|
||||
syntax::{self, source::Source},
|
||||
};
|
||||
use dever_runtime::{
|
||||
database::Parameter,
|
||||
task::{Limits, Runtime},
|
||||
value::decimal::Decimal,
|
||||
};
|
||||
use std::{hint::black_box, time::Instant};
|
||||
use tokio_postgres::types::{ToSql, Type};
|
||||
|
||||
fn measure(name: &str, count: usize, mut operation: impl FnMut() -> u64) {
|
||||
let expected = operation();
|
||||
for _ in 0..10 {
|
||||
assert_eq!(operation(), expected);
|
||||
}
|
||||
for sample in 0..15 {
|
||||
let started = Instant::now();
|
||||
let mut checksum = 0;
|
||||
for _ in 0..count {
|
||||
checksum += black_box(operation());
|
||||
}
|
||||
let elapsed = started.elapsed().as_nanos();
|
||||
assert_eq!(checksum, expected * count as u64);
|
||||
println!("{name},{sample},{count},{elapsed},{checksum}");
|
||||
}
|
||||
}
|
||||
|
||||
fn numeric() {
|
||||
for native in [false, true] {
|
||||
let values = [
|
||||
"0",
|
||||
"-12.345",
|
||||
"1234567890123456789012345.123456789",
|
||||
"0.000000001",
|
||||
]
|
||||
.map(|text| {
|
||||
let value = Decimal::parse(text).unwrap();
|
||||
if native {
|
||||
Parameter::NativeDecimal(value)
|
||||
} else {
|
||||
Parameter::Decimal {
|
||||
value,
|
||||
precision: 34,
|
||||
scale: 9,
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut output = BytesMut::with_capacity(128);
|
||||
measure(
|
||||
if native {
|
||||
"numeric_native"
|
||||
} else {
|
||||
"numeric_column"
|
||||
},
|
||||
100_000,
|
||||
|| {
|
||||
output.clear();
|
||||
for value in black_box(&values) {
|
||||
value.to_sql(&Type::NUMERIC, &mut output).unwrap();
|
||||
}
|
||||
output.iter().map(|value| u64::from(*value)).sum()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn frontend() {
|
||||
for count in [100, 1000, 3000] {
|
||||
let text = (0..count)
|
||||
.map(|index| format!("function{index}() (answer: Int) {{ answer = {index} }}\n"))
|
||||
.collect();
|
||||
let source = Source::new(0, "case.dever", text);
|
||||
let module = syntax::parse(&source).unwrap();
|
||||
measure(&format!("function_check_{count}"), 3, || {
|
||||
check::check(vec![black_box(module.clone())], &["main".into()])
|
||||
.unwrap()
|
||||
.functions
|
||||
.len() as u64
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn command() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let executable = std::env::current_exe().unwrap();
|
||||
let directory = executable.parent().unwrap();
|
||||
measure("command_exit", 20, || {
|
||||
runtime.block_on(async {
|
||||
let mut child =
|
||||
process_worker::Child::command(&executable, &["--child".into()], directory, "")
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(child.wait().await.unwrap().success());
|
||||
1
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn upload() {
|
||||
use dever_runtime::{api::Request, upload};
|
||||
use http_body_util::Full;
|
||||
let directory = std::env::temp_dir().join(format!("dever-cost-upload-{}", std::process::id()));
|
||||
std::fs::create_dir(&directory).unwrap();
|
||||
let mut request = Request::new(Default::default());
|
||||
*request.method_mut() = "POST".parse().unwrap();
|
||||
*request.uri_mut() = "/upload".parse().unwrap();
|
||||
request.headers_mut().insert(
|
||||
"content-type",
|
||||
"multipart/form-data; boundary=boundary".parse().unwrap(),
|
||||
);
|
||||
let mut body = b"--boundary\r\nContent-Disposition: form-data; name=file; filename=test.bin\r\nContent-Type: application/octet-stream\r\n\r\n".to_vec();
|
||||
body.extend(vec![b'x'; 1_048_576]);
|
||||
body.extend_from_slice(b"\r\n--boundary--\r\n");
|
||||
let body = Bytes::from(body);
|
||||
let runtime = Runtime::new(Limits {
|
||||
threads: 1,
|
||||
workers: 1,
|
||||
tasks: 32,
|
||||
})
|
||||
.unwrap();
|
||||
measure("upload_1mib", 3, || {
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let mut multipart = upload::read(
|
||||
&request,
|
||||
Full::new(body.clone()),
|
||||
&directory,
|
||||
&upload::Limits::default(),
|
||||
scope,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let file = multipart.take("file").unwrap();
|
||||
assert_eq!(file.size(), 1_048_576);
|
||||
multipart.finish().unwrap();
|
||||
file.close();
|
||||
1_048_576
|
||||
})
|
||||
.unwrap()
|
||||
});
|
||||
assert_eq!(std::fs::read_dir(directory.join("tmp")).unwrap().count(), 0);
|
||||
std::fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if std::env::args().any(|value| value == "--child") {
|
||||
return;
|
||||
}
|
||||
println!("case,sample,iterations,elapsed_ns,checksum");
|
||||
if std::env::args().any(|value| value == "--warm-build") {
|
||||
warm_build();
|
||||
return;
|
||||
}
|
||||
numeric();
|
||||
frontend();
|
||||
command();
|
||||
upload();
|
||||
query();
|
||||
sites();
|
||||
}
|
||||
|
||||
fn warm_build() {
|
||||
use dever::compiler::build;
|
||||
let directory = std::env::temp_dir().join(format!("dever-cost-build-{}", std::process::id()));
|
||||
std::fs::create_dir(&directory).unwrap();
|
||||
let mut random = 0x9e3779b9u32;
|
||||
let text: String = (0..8 * 1024 * 1024)
|
||||
.map(|_| {
|
||||
random ^= random << 13;
|
||||
random ^= random >> 17;
|
||||
random ^= random << 5;
|
||||
char::from(b'a' + (random % 26) as u8)
|
||||
})
|
||||
.collect();
|
||||
let source = Source::new(
|
||||
0,
|
||||
"warm.dever",
|
||||
format!("main() (answer: Text) {{ answer = {text:?} }}"),
|
||||
);
|
||||
let module = syntax::parse(&source).unwrap();
|
||||
let program = check::check(vec![module], &["main".into()]).unwrap();
|
||||
let sources = [source];
|
||||
let initial = directory.join("initial");
|
||||
build::build(&program, &sources, "main", &initial).unwrap();
|
||||
assert!(std::fs::metadata(&initial).unwrap().len() > 8 * 1024 * 1024);
|
||||
let output = std::process::Command::new(&initial).output().unwrap();
|
||||
assert!(output.status.success());
|
||||
assert!(output.stdout == format!("{text:?}\n").as_bytes());
|
||||
let mut next = 0;
|
||||
measure("warm_build_8mib", 1, || {
|
||||
let output = directory.join(format!("copy-{next}"));
|
||||
next += 1;
|
||||
build::build(&program, &sources, "main", &output).unwrap();
|
||||
assert!(std::fs::metadata(&output).unwrap().len() > 8 * 1024 * 1024);
|
||||
std::fs::remove_file(output).unwrap();
|
||||
1
|
||||
});
|
||||
std::fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
fn query() {
|
||||
use dever_runtime::api::{self, Request};
|
||||
let names = (0..32)
|
||||
.map(|index| format!("field{index}"))
|
||||
.collect::<Vec<_>>();
|
||||
// 原 128 次重复超出 Hyper 的 URI 长度上限;本场景只测可接收的请求。
|
||||
let text = "value%2B%E9%9B%AA".repeat(96);
|
||||
let query = names
|
||||
.iter()
|
||||
.map(|name| format!("{name}={text}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("&");
|
||||
let mut request = Request::new(Default::default());
|
||||
*request.uri_mut() = format!("/test?{query}").parse().unwrap();
|
||||
measure("query_32_fields_96_values", 100, || {
|
||||
let input = api::input(&request).unwrap();
|
||||
let values: std::collections::BTreeMap<String, String> = INPUT_DECODE(input).unwrap();
|
||||
assert_eq!(values["field0"], "value+雪".repeat(96));
|
||||
values.values().map(|value| value.len() as u64).sum()
|
||||
});
|
||||
}
|
||||
|
||||
fn sites() {
|
||||
use dever_runtime::{api::Config, api::Request};
|
||||
let names = (0..512)
|
||||
.map(|index| format!("site{index:04}"))
|
||||
.collect::<Vec<_>>();
|
||||
let sites = names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
(
|
||||
name.clone(),
|
||||
serde_json::json!({"path":name,"hosts":["fixture.test"]}),
|
||||
)
|
||||
})
|
||||
.collect::<serde_json::Map<_, _>>();
|
||||
let path = std::env::temp_dir().join(format!("dever-cost-sites-{}.json", std::process::id()));
|
||||
std::fs::write(
|
||||
&path,
|
||||
serde_json::json!({"http":{"host":"127.0.0.1","port":18080},"sites":sites}).to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
let topics = names.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
let config = Config::load(&path, &topics).unwrap();
|
||||
std::fs::remove_file(path).unwrap();
|
||||
let mut request = Request::new(Default::default());
|
||||
*request.uri_mut() = "/test".parse().unwrap();
|
||||
request
|
||||
.headers_mut()
|
||||
.insert("host", "fixture.test".parse().unwrap());
|
||||
measure("site_select_512", 10_000, || {
|
||||
u64::from(config.accepts(black_box(&names[511]), black_box(&request)))
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
// 两侧共享 SQLite、连接池、截止时间与 worker,单独测量 Dever 生成层的额外成本。
|
||||
// 不把此比较当成未经资源约束的同步 rusqlite、HTTP 吞吐或整个 core 的验收。
|
||||
use dever_runtime::{
|
||||
database::{Context, Id as ModelId, Parameter, Registry},
|
||||
task::{Limits, Runtime, Scope},
|
||||
time::calendar::DateTime,
|
||||
value::uuid::Uuid,
|
||||
};
|
||||
use std::{hint::black_box, path::PathBuf, sync::Arc, time::Instant};
|
||||
|
||||
struct Directory(PathBuf);
|
||||
impl Drop for Directory {
|
||||
fn drop(&mut self) {
|
||||
std::fs::remove_dir_all(&self.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
async fn rust_count(context: &mut Context, scope: &Scope<'_>) -> i64 {
|
||||
context
|
||||
.on(0, scope)
|
||||
.await
|
||||
.unwrap()
|
||||
.query(
|
||||
scope,
|
||||
"SELECT COUNT(*) FROM \"demo_account_model\"",
|
||||
vec![],
|
||||
1,
|
||||
|row| Ok(row.get::<_, i64>(0)?),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.pop()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
struct RustAccount {
|
||||
id: ModelId<0>,
|
||||
created_at: DateTime,
|
||||
name: String,
|
||||
note: Option<String>,
|
||||
uid: Uuid,
|
||||
rank: i64,
|
||||
}
|
||||
|
||||
async fn rust_row(
|
||||
context: &mut Context,
|
||||
scope: &Scope<'_>,
|
||||
update: bool,
|
||||
) -> Result<RustAccount, dever_runtime::database::Error> {
|
||||
let (sql, parameters) = if update {
|
||||
(
|
||||
"UPDATE \"demo_account_model\" SET \"name\" = ?2 WHERE id = ?1 RETURNING \"id\",\"created_at\",\"name\",\"note\",\"uid\",\"rank\"",
|
||||
vec![
|
||||
Parameter::Integer(1),
|
||||
Parameter::Text("benchmark 中文".into()),
|
||||
],
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"SELECT \"id\",\"created_at\",\"name\",\"note\",\"uid\",\"rank\" FROM \"demo_account_model\" WHERE id = ?1",
|
||||
vec![Parameter::Integer(1)],
|
||||
)
|
||||
};
|
||||
let sql = dever_runtime::database::Sql {
|
||||
sqlite: sql,
|
||||
postgres: if update {
|
||||
"UPDATE \"demo_account_model\" SET \"name\" = $2 WHERE id = $1 RETURNING \"id\",\"created_at\",\"name\",\"note\",\"uid\",\"rank\""
|
||||
} else {
|
||||
"SELECT \"id\",\"created_at\",\"name\",\"note\",\"uid\",\"rank\" FROM \"demo_account_model\" WHERE id = $1"
|
||||
},
|
||||
};
|
||||
context
|
||||
.on(0, scope)
|
||||
.await?
|
||||
.query(scope, sql, parameters, 1, |row| {
|
||||
Ok(RustAccount {
|
||||
id: ModelId(row.get::<_, i64>(0)?),
|
||||
created_at: DateTime::from_millis(row.get::<_, i64>(1)?).map_err(|_| {
|
||||
dever_runtime::database::Error::new(
|
||||
dever_runtime::database::Kind::InvalidData,
|
||||
"invalid stored DateTime",
|
||||
)
|
||||
})?,
|
||||
name: row.get::<_, String>(2)?,
|
||||
note: row.get::<_, Option<String>>(3)?,
|
||||
uid: dever_runtime::database::scalar::uuid(row, 4)?,
|
||||
rank: row.get::<_, i64>(5)?,
|
||||
})
|
||||
})
|
||||
.await?
|
||||
.pop()
|
||||
.ok_or_else(|| {
|
||||
dever_runtime::database::Error::new(
|
||||
dever_runtime::database::Kind::NotFound,
|
||||
"account was not found",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Case {
|
||||
Count,
|
||||
Read,
|
||||
Update,
|
||||
}
|
||||
|
||||
fn measure(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
generated: bool,
|
||||
case: Case,
|
||||
count: usize,
|
||||
) -> (u128, i64) {
|
||||
let mut context = Context::new(registry.clone());
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let mut checksum = 0i64;
|
||||
let start = Instant::now();
|
||||
for _ in 0..count {
|
||||
let value = if case != Case::Count {
|
||||
let owner = (case == Case::Update).then(|| context.enter());
|
||||
let row = if generated {
|
||||
let value = if case == Case::Update {
|
||||
GENERATED_UPDATE(
|
||||
dever_runtime::database::Id(1),
|
||||
"benchmark 中文",
|
||||
scope,
|
||||
&mut context,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
} else {
|
||||
GENERATED_READ(dever_runtime::database::Id(1), scope, &mut context)
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
RustAccount {
|
||||
id: value.field0,
|
||||
created_at: value.field1,
|
||||
name: value.field2,
|
||||
note: value.field3,
|
||||
uid: value.field4,
|
||||
rank: value.field5,
|
||||
}
|
||||
} else {
|
||||
rust_row(&mut context, scope, case == Case::Update)
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
if let Some(owner) = owner {
|
||||
context.finish(scope, owner, true).await.unwrap();
|
||||
}
|
||||
// 两侧都把完整记录交给调用者,不在 worker 内提前丢字段。
|
||||
let row = black_box(&row);
|
||||
assert_eq!(row.name, "benchmark 中文");
|
||||
assert!(row.note.is_none());
|
||||
row.id.0 + row.name.len() as i64
|
||||
} else if generated {
|
||||
GENERATED_COUNT(scope, &mut context).await.unwrap()
|
||||
} else {
|
||||
rust_count(&mut context, scope).await
|
||||
};
|
||||
checksum += black_box(value);
|
||||
}
|
||||
(start.elapsed().as_nanos(), checksum)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let unique = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let relative = format!("database-benchmark-{}-{unique}", std::process::id());
|
||||
let directory = Directory(
|
||||
std::env::current_exe()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(&relative),
|
||||
);
|
||||
std::fs::create_dir(&directory.0).unwrap();
|
||||
let settings = directory.0.join("setting.json");
|
||||
let runtime = Runtime::new(Limits {
|
||||
threads: 1,
|
||||
workers: 1,
|
||||
tasks: 16,
|
||||
})
|
||||
.unwrap();
|
||||
let schema = database_settings(&runtime, &directory.0, &settings, &relative);
|
||||
let registry = runtime
|
||||
.run(async |scope| database_registry(&settings, scope).await)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let mut context = Context::new(registry.clone());
|
||||
let created = GENERATED_CREATE("benchmark 中文", scope, &mut context)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(created.field0.0, 1);
|
||||
assert_eq!(
|
||||
GENERATED_COUNT(scope, &mut context).await.unwrap(),
|
||||
rust_count(&mut context, scope).await
|
||||
);
|
||||
let expected = (
|
||||
created.field0,
|
||||
created.field1,
|
||||
created.field2,
|
||||
created.field3,
|
||||
created.field4,
|
||||
created.field5,
|
||||
);
|
||||
let read = GENERATED_READ(ModelId(1), scope, &mut context)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
(
|
||||
read.field0,
|
||||
read.field1,
|
||||
read.field2,
|
||||
read.field3,
|
||||
read.field4,
|
||||
read.field5
|
||||
),
|
||||
expected
|
||||
);
|
||||
let read = rust_row(&mut context, scope, false).await.unwrap();
|
||||
assert_eq!(
|
||||
(
|
||||
read.id,
|
||||
read.created_at,
|
||||
read.name,
|
||||
read.note,
|
||||
read.uid,
|
||||
read.rank
|
||||
),
|
||||
expected
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
println!("case,sample,implementation,nanoseconds,checksum");
|
||||
for (name, case) in [
|
||||
("database_count", Case::Count),
|
||||
("database_get", Case::Read),
|
||||
("database_update_commit", Case::Update),
|
||||
] {
|
||||
if let Ok(selected) = std::env::var("DEVER_BENCH_CASES")
|
||||
&& !selected.split(',').any(|item| item == name)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for generated in [true, false] {
|
||||
black_box(measure(&runtime, ®istry, generated, case, 200));
|
||||
}
|
||||
for sample in 0..15 {
|
||||
let mut order = [("dever", true), ("rust", false), ("control", true)];
|
||||
order.rotate_left(sample % 3);
|
||||
let mut expected = None;
|
||||
for (implementation, generated) in order {
|
||||
let (elapsed, checksum) = measure(&runtime, ®istry, generated, case, 2000);
|
||||
if let Some(expected) = expected {
|
||||
assert_eq!(checksum, expected);
|
||||
}
|
||||
expected = Some(checksum);
|
||||
println!("{name},{sample},{implementation},{elapsed},{checksum}");
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(registry);
|
||||
if let Some(schema) = schema {
|
||||
schema.close(&runtime);
|
||||
}
|
||||
drop(runtime);
|
||||
drop(directory);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// PostgreSQL 只读取显式测试连接;每个比较进程创建并删除独占 schema。
|
||||
struct PostgresSchema {
|
||||
database: dever_runtime::database::Database,
|
||||
drop_sql: &'static str,
|
||||
}
|
||||
|
||||
impl PostgresSchema {
|
||||
fn close(self, runtime: &Runtime) {
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
self.database
|
||||
.executor()
|
||||
.await
|
||||
.unwrap()
|
||||
.initialize(scope, self.drop_sql)
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
self.database.close();
|
||||
}
|
||||
}
|
||||
|
||||
fn database_settings(
|
||||
runtime: &Runtime,
|
||||
directory: &std::path::Path,
|
||||
settings: &std::path::Path,
|
||||
relative: &str,
|
||||
) -> Option<PostgresSchema> {
|
||||
if !POSTGRES {
|
||||
std::fs::write(settings, format!(r#"{{"database":{{"default":{{"type":"sqlite","path":"{relative}/case.db","max_connections":1,"max_page_size":20}}}}}}"#)).unwrap();
|
||||
return None;
|
||||
}
|
||||
let test_settings = std::env::var_os("DEVER_BENCH_POSTGRES_SETTINGS")
|
||||
.expect("PostgreSQL benchmarks require an explicit isolated test configuration");
|
||||
let config: serde_json::Value =
|
||||
serde_json::from_slice(&std::fs::read(test_settings).unwrap()).unwrap();
|
||||
let mut connection = config["database"]["postgres_test"].clone();
|
||||
let options: dever_runtime::database::Options =
|
||||
serde_json::from_value(connection.clone()).unwrap();
|
||||
assert_eq!(options.driver, "postgres");
|
||||
let name = format!("dever_bench_{}", std::process::id());
|
||||
let create_sql: &'static str = Box::leak(format!("CREATE SCHEMA {name}").into_boxed_str());
|
||||
let drop_sql = Box::leak(format!("DROP SCHEMA {name} CASCADE").into_boxed_str());
|
||||
let database = runtime
|
||||
.run(async |scope| {
|
||||
let database = dever_runtime::database::Database::open(directory, options, scope)
|
||||
.await
|
||||
.unwrap();
|
||||
database
|
||||
.executor()
|
||||
.await
|
||||
.unwrap()
|
||||
.initialize(scope, create_sql)
|
||||
.await
|
||||
.unwrap();
|
||||
database
|
||||
})
|
||||
.unwrap();
|
||||
let url = connection["url"].as_str().unwrap();
|
||||
assert!(
|
||||
!url.contains('?'),
|
||||
"test connection must not override schema options"
|
||||
);
|
||||
connection["url"] = format!("{url}?options=-csearch_path%3D{name}").into();
|
||||
connection["max_connections"] = 1.into();
|
||||
connection["max_page_size"] = 20.into();
|
||||
std::fs::write(
|
||||
settings,
|
||||
serde_json::to_vec(&serde_json::json!({"database":{"default":connection}})).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
Some(PostgresSchema { database, drop_sql })
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
use dever_runtime::async_stream::AsyncStream;
|
||||
use dever_runtime::channel::Channel;
|
||||
use dever_runtime::task::{Group, Limits, Runtime, Scope, Task};
|
||||
|
||||
struct ConcurrencyCase {
|
||||
runtime: Runtime,
|
||||
values: Vec<i64>,
|
||||
limit: i64,
|
||||
}
|
||||
|
||||
fn concurrency_fault() -> Fault {
|
||||
&FaultInfo {
|
||||
message: "extended concurrency failed",
|
||||
source: "extended-concurrency-reference",
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
type ChannelOutput = (Vec<Option<i64>>, bool);
|
||||
|
||||
fn channel_generated(values: &[ConcurrencyCase]) -> Result<ChannelOutput, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok((Vec::new(), true));
|
||||
};
|
||||
value
|
||||
.runtime
|
||||
.run(async |scope| EXT_CHANNEL(&value.values, value.limit, scope).await)
|
||||
.map_err(|_| concurrency_fault())?
|
||||
.map_err(|_| concurrency_fault())
|
||||
}
|
||||
|
||||
async fn send_values(values: Vec<i64>, channel: Channel<i64>) -> Result<(), Fault> {
|
||||
for value in values {
|
||||
channel.send(value).await.map_err(|_| concurrency_fault())?;
|
||||
}
|
||||
channel.close().await.map_err(|_| concurrency_fault())
|
||||
}
|
||||
|
||||
fn channel_reference(values: &[ConcurrencyCase]) -> Result<ChannelOutput, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok((Vec::new(), true));
|
||||
};
|
||||
value
|
||||
.runtime
|
||||
.run(async |scope| {
|
||||
let channel = Channel::new(value.limit).map_err(|_| concurrency_fault())?;
|
||||
let producer = channel.clone();
|
||||
let owned = value.values.clone();
|
||||
let pending = scope
|
||||
.spawn_leaf(move || send_values(owned, producer))
|
||||
.map_err(|_| concurrency_fault())?;
|
||||
let mut received = Vec::with_capacity(value.values.len());
|
||||
for _ in &value.values {
|
||||
received.push(channel.receive().await.map_err(|_| concurrency_fault())?);
|
||||
}
|
||||
pending.wait().await.map_err(|_| concurrency_fault())??;
|
||||
let ended = channel
|
||||
.receive()
|
||||
.await
|
||||
.map_err(|_| concurrency_fault())?
|
||||
.is_none();
|
||||
Ok((received, ended))
|
||||
})
|
||||
.map_err(|_| concurrency_fault())?
|
||||
}
|
||||
|
||||
fn channel_score(value: &ChannelOutput) -> u64 {
|
||||
value.0.iter().fold(u64::from(value.1), |score, item| {
|
||||
score
|
||||
.wrapping_mul(31)
|
||||
.wrapping_add(item.unwrap_or(0) as u64)
|
||||
})
|
||||
}
|
||||
|
||||
async fn stream_total(stream: &AsyncStream<i64>, limit: Option<i64>) -> Result<i64, Fault> {
|
||||
let _close = stream.close_on_drop();
|
||||
let mut total = 0i64;
|
||||
while let Some(value) = stream.next().await.map_err(|_| concurrency_fault())? {
|
||||
total = total.checked_add(value).ok_or_else(concurrency_fault)?;
|
||||
if limit.is_some_and(|limit| value >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn pull_generated(values: &[ConcurrencyCase]) -> Result<(i64, i64), Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok((0, 0));
|
||||
};
|
||||
value
|
||||
.runtime
|
||||
.run_leaf(EXT_PULL(&value.values))
|
||||
.map_err(|_| concurrency_fault())?
|
||||
.map_err(|_| concurrency_fault())
|
||||
}
|
||||
|
||||
fn prefix_generated(values: &[ConcurrencyCase]) -> Result<(i64, i64), Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok((0, 0));
|
||||
};
|
||||
value
|
||||
.runtime
|
||||
.run_leaf(EXT_PREFIX(&value.values))
|
||||
.map_err(|_| concurrency_fault())?
|
||||
.map_err(|_| concurrency_fault())
|
||||
}
|
||||
|
||||
fn stream_reference<const EARLY: bool>(values: &[ConcurrencyCase]) -> Result<(i64, i64), Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok((0, 0));
|
||||
};
|
||||
value
|
||||
.runtime
|
||||
.run_leaf(async {
|
||||
let stream = AsyncStream::values(value.values.clone());
|
||||
let alias = stream.clone();
|
||||
let total = stream_total(&stream, EARLY.then_some(8)).await?;
|
||||
let after = stream_total(&alias, None).await?;
|
||||
Ok((total, after))
|
||||
})
|
||||
.map_err(|_| concurrency_fault())?
|
||||
}
|
||||
|
||||
fn stream_score(value: &(i64, i64)) -> u64 {
|
||||
(value.0 as u64)
|
||||
.wrapping_mul(31)
|
||||
.wrapping_add(value.1 as u64)
|
||||
}
|
||||
|
||||
async fn parallel_reference_inner(value: &ConcurrencyCase, scope: &Scope<'_>) -> Result<i64, Fault> {
|
||||
let channel = Channel::new(1).map_err(|_| concurrency_fault())?;
|
||||
let consumer = channel.clone();
|
||||
let pending = scope
|
||||
.spawn_leaf(
|
||||
move || async move { stream_total(&AsyncStream::channel(consumer), None).await },
|
||||
)
|
||||
.map_err(|_| concurrency_fault())?;
|
||||
let mut items = value.values.iter().copied();
|
||||
let group: Group<Task<Result<(), Fault>>, Fault> =
|
||||
Group::new(value.limit).map_err(|_| concurrency_fault())?;
|
||||
group
|
||||
.each(
|
||||
|| std::future::ready(Ok(items.next())),
|
||||
|item| {
|
||||
let target = channel.clone();
|
||||
scope.spawn_leaf(move || async move {
|
||||
target.send(item).await.map_err(|_| concurrency_fault())
|
||||
})
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|_| concurrency_fault())??;
|
||||
channel.close().await.map_err(|_| concurrency_fault())?;
|
||||
pending.wait().await.map_err(|_| concurrency_fault())?
|
||||
}
|
||||
|
||||
fn parallel_generated(values: &[ConcurrencyCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
value
|
||||
.runtime
|
||||
.run(async |scope| EXT_PARALLEL(&value.values, value.limit, scope).await)
|
||||
.map_err(|_| concurrency_fault())?
|
||||
.map_err(|_| concurrency_fault())
|
||||
}
|
||||
|
||||
fn parallel_reference(values: &[ConcurrencyCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
value
|
||||
.runtime
|
||||
.run(async |scope| parallel_reference_inner(value, scope).await)
|
||||
.map_err(|_| concurrency_fault())?
|
||||
}
|
||||
|
||||
fn new_case(limit: i64) -> ConcurrencyCase {
|
||||
ConcurrencyCase {
|
||||
runtime: Runtime::new(Limits {
|
||||
threads: 2,
|
||||
workers: 2,
|
||||
tasks: 8,
|
||||
})
|
||||
.unwrap(),
|
||||
values: (1..=256).collect(),
|
||||
limit,
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// 两侧共用运行库与固定限额;计时含复制、提交、领取及作用域清理。
|
||||
let mut case = new_case(1);
|
||||
assert_eq!(
|
||||
case.runtime.run_leaf(EXT_BOUNDARY()).unwrap().unwrap(),
|
||||
(15, Some(9), 0)
|
||||
);
|
||||
let expected = case.values.iter().sum::<i64>();
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
for capacity in [1, 64] {
|
||||
case.limit = capacity;
|
||||
let values = std::slice::from_ref(&case);
|
||||
let received = case.values.iter().copied().map(Some).collect::<Vec<_>>();
|
||||
assert_eq!(channel_generated(values).unwrap(), (received.clone(), true));
|
||||
assert_eq!(channel_reference(values).unwrap(), (received, true));
|
||||
compare_named(
|
||||
&format!("channel_capacity_{capacity}_shared_runtime"),
|
||||
values,
|
||||
32,
|
||||
channel_generated,
|
||||
channel_reference,
|
||||
channel_score,
|
||||
"rust_runtime",
|
||||
);
|
||||
}
|
||||
case.limit = 8;
|
||||
let values = std::slice::from_ref(&case);
|
||||
assert_eq!(pull_generated(values).unwrap(), (expected, 0));
|
||||
assert_eq!(prefix_generated(values).unwrap(), (36, 0));
|
||||
compare_named(
|
||||
"async_stream_pull_shared_runtime",
|
||||
values,
|
||||
128,
|
||||
pull_generated,
|
||||
stream_reference::<false>,
|
||||
stream_score,
|
||||
"rust_runtime",
|
||||
);
|
||||
compare_named(
|
||||
"async_stream_prefix_shared_runtime",
|
||||
values,
|
||||
128,
|
||||
prefix_generated,
|
||||
stream_reference::<true>,
|
||||
stream_score,
|
||||
"rust_runtime",
|
||||
);
|
||||
for limit in [1, 4] {
|
||||
case.limit = limit;
|
||||
let values = std::slice::from_ref(&case);
|
||||
assert_eq!(parallel_generated(values).unwrap(), expected);
|
||||
assert_eq!(parallel_reference(values).unwrap(), expected);
|
||||
compare_named(
|
||||
&format!("parallel_each_limit_{limit}_shared_runtime"),
|
||||
values,
|
||||
32,
|
||||
parallel_generated,
|
||||
parallel_reference,
|
||||
|value| *value as u64,
|
||||
"rust_runtime",
|
||||
);
|
||||
}
|
||||
// 真正调用空输入 fixture,区别于 samples 的空用例切片。
|
||||
case.values.clear();
|
||||
let empty = std::slice::from_ref(&case);
|
||||
assert_eq!(channel_generated(empty).unwrap(), (Vec::new(), true));
|
||||
assert_eq!(channel_reference(empty).unwrap(), (Vec::new(), true));
|
||||
assert_eq!(pull_generated(empty).unwrap(), (0, 0));
|
||||
assert_eq!(prefix_generated(empty).unwrap(), (0, 0));
|
||||
assert_eq!(stream_reference::<false>(empty).unwrap(), (0, 0));
|
||||
assert_eq!(stream_reference::<true>(empty).unwrap(), (0, 0));
|
||||
assert_eq!(parallel_generated(empty).unwrap(), 0);
|
||||
assert_eq!(parallel_reference(empty).unwrap(), 0);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use std::path::Path;
|
||||
|
||||
pub fn run(core: &Path) -> bool {
|
||||
let all = std::env::args().any(|argument| argument == "--extended");
|
||||
let primitives = all || std::env::args().any(|argument| argument == "--extended-primitives");
|
||||
let concurrency = all || std::env::args().any(|argument| argument == "--extended-concurrency");
|
||||
if primitives {
|
||||
crate::compare(
|
||||
core,
|
||||
"extended-performance/primitives",
|
||||
"extended-primitives",
|
||||
&[include_str!("../samples.rs"), include_str!("primitives.rs")].concat(),
|
||||
&[
|
||||
("EXT_SHA256", "sha256Digest"),
|
||||
("EXT_HMAC", "hmacDigest"),
|
||||
("EXT_DATETIME", "datetimeBatch"),
|
||||
("EXT_DATE", "dateBatch"),
|
||||
("EXT_TIME", "timeBatch"),
|
||||
("EXT_UUID", "uuidBatch"),
|
||||
],
|
||||
&[],
|
||||
);
|
||||
}
|
||||
if concurrency {
|
||||
crate::compare(
|
||||
core,
|
||||
"extended-performance/concurrency",
|
||||
"extended-concurrency",
|
||||
&[
|
||||
include_str!("../samples.rs"),
|
||||
include_str!("concurrency.rs"),
|
||||
]
|
||||
.concat(),
|
||||
&[
|
||||
("EXT_CHANNEL", "channelRoundtrip"),
|
||||
("EXT_PULL", "streamPull"),
|
||||
("EXT_PREFIX", "streamPrefix"),
|
||||
("EXT_BOUNDARY", "streamBoundary"),
|
||||
("EXT_PARALLEL", "parallelBounded"),
|
||||
],
|
||||
&[],
|
||||
);
|
||||
}
|
||||
primitives || concurrency
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
use dever_runtime::time::calendar::{Date, DateTime, Time};
|
||||
use dever_runtime::value::secret::Secret;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
fn primitive_fault() -> Fault {
|
||||
&FaultInfo {
|
||||
message: "extended primitive failed",
|
||||
source: "extended-primitives-reference",
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn digest_score(bytes: &dever_runtime::Bytes) -> u64 {
|
||||
bytes.iter().fold(0u64, |score, byte| {
|
||||
score.wrapping_mul(31).wrapping_add(u64::from(*byte))
|
||||
})
|
||||
}
|
||||
|
||||
fn sha_reference(bytes: &dever_runtime::Bytes) -> Result<dever_runtime::Bytes, Fault> {
|
||||
Ok(Sha256::digest(bytes).to_vec().into())
|
||||
}
|
||||
|
||||
struct HmacCase {
|
||||
key: Secret,
|
||||
bytes: dever_runtime::Bytes,
|
||||
}
|
||||
|
||||
fn hmac_generated(values: &[HmacCase]) -> Result<dever_runtime::Bytes, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(dever_runtime::Bytes::new());
|
||||
};
|
||||
EXT_HMAC(&value.key, &value.bytes)
|
||||
}
|
||||
|
||||
// 共用批准的 Secret 接收点;不读取密钥内部字节。
|
||||
fn hmac_reference(values: &[HmacCase]) -> Result<dever_runtime::Bytes, Fault> {
|
||||
Ok(values
|
||||
.first()
|
||||
.map_or_else(Vec::new, |value| {
|
||||
dever_runtime::crypto::hmac_sha256(&value.key, &value.bytes)
|
||||
})
|
||||
.into())
|
||||
}
|
||||
|
||||
fn datetime_generated(values: &[String]) -> Result<Vec<String>, Fault> {
|
||||
EXT_DATETIME(values).map_err(|_| primitive_fault())
|
||||
}
|
||||
|
||||
fn datetime_reference(values: &[String]) -> Result<Vec<String>, Fault> {
|
||||
values
|
||||
.iter()
|
||||
.map(|value| {
|
||||
DateTime::parse(value)
|
||||
.and_then(DateTime::format)
|
||||
.map_err(|_| primitive_fault())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn date_generated(values: &[String]) -> Result<Vec<String>, Fault> {
|
||||
EXT_DATE(values).map_err(|_| primitive_fault())
|
||||
}
|
||||
|
||||
fn date_reference(values: &[String]) -> Result<Vec<String>, Fault> {
|
||||
values
|
||||
.iter()
|
||||
.map(|value| {
|
||||
Date::parse(value)
|
||||
.and_then(Date::format)
|
||||
.map_err(|_| primitive_fault())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn time_generated(values: &[String]) -> Result<Vec<String>, Fault> {
|
||||
EXT_TIME(values).map_err(|_| primitive_fault())
|
||||
}
|
||||
|
||||
fn time_reference(values: &[String]) -> Result<Vec<String>, Fault> {
|
||||
values
|
||||
.iter()
|
||||
.map(|value| {
|
||||
Time::parse(value)
|
||||
.and_then(Time::format)
|
||||
.map_err(|_| primitive_fault())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn text_score(values: &Vec<String>) -> u64 {
|
||||
values
|
||||
.iter()
|
||||
.flat_map(|value| value.bytes())
|
||||
.fold(0u64, |score, byte| {
|
||||
score.wrapping_mul(31).wrapping_add(u64::from(byte))
|
||||
})
|
||||
}
|
||||
|
||||
fn uuid_generated(values: &[String]) -> Result<Vec<Option<String>>, Fault> {
|
||||
EXT_UUID(values)
|
||||
}
|
||||
|
||||
fn uuid_reference(values: &[String]) -> Result<Vec<Option<String>>, Fault> {
|
||||
Ok(values
|
||||
.iter()
|
||||
.map(|value| {
|
||||
if value.len() != 36 {
|
||||
return None;
|
||||
}
|
||||
uuid::Uuid::try_parse(value)
|
||||
.ok()
|
||||
.map(|value| value.hyphenated().to_string())
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn uuid_score(values: &Vec<Option<String>>) -> u64 {
|
||||
values.iter().fold(0u64, |score, value| {
|
||||
value.as_ref().map_or(score.wrapping_add(1), |value| {
|
||||
value.bytes().fold(score.wrapping_add(2), |score, byte| {
|
||||
score.wrapping_mul(31).wrapping_add(u64::from(byte))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn check_primitives() {
|
||||
assert_eq!(
|
||||
EXT_SHA256(&dever_runtime::Bytes::from_static(b"abc"))
|
||||
.unwrap()
|
||||
.as_ref(),
|
||||
[
|
||||
0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae,
|
||||
0x22, 0x23, 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61,
|
||||
0xf2, 0x00, 0x15, 0xad,
|
||||
]
|
||||
);
|
||||
// 沿用已有密码学验收的公开向量,不使用真实凭据。
|
||||
let vector = [HmacCase {
|
||||
key: Secret::from_input(vec![0x0b; 20]),
|
||||
bytes: dever_runtime::Bytes::from_static(b"Hi There"),
|
||||
}];
|
||||
assert_eq!(
|
||||
hmac_generated(&vector).unwrap().as_ref(),
|
||||
[
|
||||
0xb0, 0x34, 0x4c, 0x61, 0xd8, 0xdb, 0x38, 0x53, 0x5c, 0xa8, 0xaf, 0xce, 0xaf, 0x0b,
|
||||
0xf1, 0x2b, 0x88, 0x1d, 0xc2, 0x00, 0xc9, 0x83, 0x3d, 0xa7, 0x26, 0xe9, 0x37, 0x6c,
|
||||
0x2e, 0x32, 0xcf, 0xf7,
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
hmac_generated(&vector).unwrap(),
|
||||
hmac_reference(&vector).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
datetime_generated(&["2024-02-29T12:34:56.12+08:00".into()]).unwrap(),
|
||||
["2024-02-29T04:34:56.120Z"]
|
||||
);
|
||||
assert_eq!(
|
||||
time_generated(&["12:34:56.000".into()]).unwrap(),
|
||||
["12:34:56"]
|
||||
);
|
||||
for value in ["2023-02-29", "2024-2-29"] {
|
||||
let values = [value.to_owned()];
|
||||
assert!(date_generated(&values).is_err());
|
||||
assert!(date_reference(&values).is_err());
|
||||
}
|
||||
for value in ["24:00:00", "12:34:56.1"] {
|
||||
let values = [value.to_owned()];
|
||||
assert!(time_generated(&values).is_err());
|
||||
assert!(time_reference(&values).is_err());
|
||||
}
|
||||
let values = ["2024-02-29T00:00:00.1234Z".to_owned()];
|
||||
assert!(datetime_generated(&values).is_err());
|
||||
assert!(datetime_reference(&values).is_err());
|
||||
}
|
||||
|
||||
fn main() {
|
||||
check_primitives();
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
for size in [0usize, 64, 4096] {
|
||||
let bytes = dever_runtime::Bytes::from(vec![0x61; size]);
|
||||
compare_bytes(
|
||||
&format!("crypto_sha256_{size}"),
|
||||
&bytes,
|
||||
2000,
|
||||
EXT_SHA256,
|
||||
sha_reference,
|
||||
digest_score,
|
||||
);
|
||||
let values = [HmacCase {
|
||||
key: Secret::from_input(vec![0x0b; 20]),
|
||||
bytes,
|
||||
}];
|
||||
compare_named(
|
||||
&format!("crypto_hmac_{size}_shared_runtime"),
|
||||
&values,
|
||||
2000,
|
||||
hmac_generated,
|
||||
hmac_reference,
|
||||
digest_score,
|
||||
"rust_runtime",
|
||||
);
|
||||
}
|
||||
// 日历参照保留相同精度、年份范围、非法输入和 UTC 规范化规则。
|
||||
let datetimes = [
|
||||
"1970-01-01T00:00:00Z",
|
||||
"2024-02-29T12:34:56.12+08:00",
|
||||
"1969-12-31T23:59:59.999Z",
|
||||
]
|
||||
.map(str::to_owned);
|
||||
let dates = ["2024-02-29", "2000-01-01", "9999-12-31"].map(str::to_owned);
|
||||
let times = ["00:00:00", "12:34:56.000", "23:59:59.999"].map(str::to_owned);
|
||||
compare_named(
|
||||
"calendar_datetime_shared_runtime",
|
||||
&datetimes,
|
||||
2000,
|
||||
datetime_generated,
|
||||
datetime_reference,
|
||||
text_score,
|
||||
"rust_runtime",
|
||||
);
|
||||
compare_named(
|
||||
"calendar_date_shared_runtime",
|
||||
&dates,
|
||||
2000,
|
||||
date_generated,
|
||||
date_reference,
|
||||
text_score,
|
||||
"rust_runtime",
|
||||
);
|
||||
compare_named(
|
||||
"calendar_time_shared_runtime",
|
||||
×,
|
||||
2000,
|
||||
time_generated,
|
||||
time_reference,
|
||||
text_score,
|
||||
"rust_runtime",
|
||||
);
|
||||
let valid = [
|
||||
"550E8400-E29B-41D4-A716-446655440000",
|
||||
"00000000-0000-0000-0000-000000000000",
|
||||
]
|
||||
.map(str::to_owned);
|
||||
assert_eq!(
|
||||
uuid_generated(&valid).unwrap()[0].as_deref(),
|
||||
Some("550e8400-e29b-41d4-a716-446655440000")
|
||||
);
|
||||
let invalid = [
|
||||
"",
|
||||
"550e8400e29b41d4a716446655440000",
|
||||
"00000000-0000-0000-0000-00000000000-",
|
||||
]
|
||||
.map(str::to_owned);
|
||||
assert_eq!(uuid_generated(&invalid).unwrap(), vec![None; invalid.len()]);
|
||||
compare(
|
||||
"uuid_parse_format",
|
||||
&valid,
|
||||
2000,
|
||||
uuid_generated,
|
||||
uuid_reference,
|
||||
uuid_score,
|
||||
);
|
||||
compare(
|
||||
"uuid_parse_invalid",
|
||||
&invalid,
|
||||
2000,
|
||||
uuid_generated,
|
||||
uuid_reference,
|
||||
uuid_score,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
fn fault() -> Fault {
|
||||
&FaultInfo {
|
||||
message: "integer arithmetic fault",
|
||||
source: "reference.dever",
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn integer_reference(values: &[i64]) -> Result<i64, Fault> {
|
||||
values
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.checked_add(1)
|
||||
.ok_or_else(fault)
|
||||
}
|
||||
|
||||
fn integer_generated(values: &[i64]) -> Result<i64, Fault> {
|
||||
GENERATED_INTEGER(values.first().copied().unwrap_or(0))
|
||||
}
|
||||
|
||||
fn map_reference(values: &[i64]) -> Result<Vec<i64>, Fault> {
|
||||
let mut output = Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
output.push(value.checked_add(1).ok_or_else(fault)?);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn fold_reference(values: &[i64]) -> Result<i64, Fault> {
|
||||
let mut state = 0i64;
|
||||
for value in values {
|
||||
state = state.checked_add(*value).ok_or_else(fault)?;
|
||||
}
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
fn text_reference(values: &[String]) -> Result<String, Fault> {
|
||||
let value = values.first().map(String::as_str).unwrap_or("");
|
||||
let mut result = String::with_capacity(value.len() + "新".len());
|
||||
result.push_str(value);
|
||||
result.push_str("新");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn text_generated(values: &[String]) -> Result<String, Fault> {
|
||||
GENERATED_TEXT(values.first().map(String::as_str).unwrap_or(""))
|
||||
}
|
||||
|
||||
fn text_direct_generated(values: &[String]) -> Result<String, Fault> {
|
||||
GENERATED_APPEND(values.first().map(String::as_str).unwrap_or(""))
|
||||
}
|
||||
|
||||
fn append_reference(value: &str) -> Result<String, Fault> {
|
||||
let mut result = String::with_capacity(value.len() + "新".len());
|
||||
result.push_str(value);
|
||||
result.push_str("新");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn text_helper_reference(values: &[String]) -> Result<String, Fault> {
|
||||
append_reference(values.first().map(String::as_str).unwrap_or(""))
|
||||
}
|
||||
|
||||
fn rows_reference(values: &[ROW_TYPE]) -> Result<Vec<ROW_TYPE>, Fault> {
|
||||
let mut result = Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
let mut name = String::with_capacity(value.field0.len() + "新".len());
|
||||
name.push_str(&value.field0);
|
||||
name.push_str("新");
|
||||
result.push(ROW_TYPE {
|
||||
field0: name,
|
||||
field1: value.field1.checked_add(1).ok_or_else(fault)?,
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn capture_reference(values: &[i64]) -> Result<Vec<RESULT_TYPE>, Fault> {
|
||||
let mut result = Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
result.push(if *value < 0 {
|
||||
RESULT_TYPE::Variant1(PROBLEM_TYPE::Variant0(*value))
|
||||
} else {
|
||||
RESULT_TYPE::Variant0(*value)
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn capture_generated(values: &[i64]) -> Result<Vec<RESULT_TYPE>, Fault> {
|
||||
let mut result = Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
result.push(GENERATED_CAPTURE(*value)?);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// 观察部分构造后失败的清理,成功返回的 bool 保持共享计时/内存入口不变。
|
||||
fn map_failed_generated(values: &[i64]) -> Result<bool, Fault> {
|
||||
Ok(GENERATED_MAP(values).is_err())
|
||||
}
|
||||
|
||||
fn map_failed_reference(values: &[i64]) -> Result<bool, Fault> {
|
||||
Ok(map_reference(values).is_err())
|
||||
}
|
||||
|
||||
fn rows_failed_generated(values: &[ROW_TYPE]) -> Result<bool, Fault> {
|
||||
Ok(GENERATED_ROWS(values).is_err())
|
||||
}
|
||||
|
||||
fn rows_failed_reference(values: &[ROW_TYPE]) -> Result<bool, Fault> {
|
||||
Ok(rows_reference(values).is_err())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let values: Vec<i64> = (0..1024).map(|value| (value * 17) % 1000 - 1).collect();
|
||||
let failures: Vec<i64> = (0..128).map(|value| (value % 8) - 1).collect();
|
||||
let rows: Vec<_> = (0..64)
|
||||
.map(|index| ROW_TYPE {
|
||||
field0: "雪山树林".repeat(4),
|
||||
field1: index,
|
||||
})
|
||||
.collect();
|
||||
let texts = ["雪山树林".repeat(16)];
|
||||
eprintln!(
|
||||
"text_forward_equals_direct_address={}",
|
||||
std::ptr::fn_addr_eq(
|
||||
text_generated as fn(&[String]) -> Result<String, Fault>,
|
||||
text_direct_generated as fn(&[String]) -> Result<String, Fault>
|
||||
)
|
||||
);
|
||||
for values in [&[i64::MAX][..], &[i64::MAX, 1][..], &[i64::MIN, -1][..]] {
|
||||
assert_eq!(
|
||||
integer_generated(values).map_err(|_| ()),
|
||||
integer_reference(values).map_err(|_| ())
|
||||
);
|
||||
assert_eq!(
|
||||
GENERATED_MAP(values).map_err(|_| ()),
|
||||
map_reference(values).map_err(|_| ())
|
||||
);
|
||||
assert_eq!(
|
||||
GENERATED_FOLD(values).map_err(|_| ()),
|
||||
fold_reference(values).map_err(|_| ())
|
||||
);
|
||||
assert_eq!(
|
||||
capture_generated(values).unwrap(),
|
||||
capture_reference(values).unwrap()
|
||||
);
|
||||
}
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
compare(
|
||||
"handler_integer",
|
||||
&[41],
|
||||
3000000,
|
||||
integer_generated,
|
||||
integer_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"handler_map",
|
||||
&values,
|
||||
20000,
|
||||
GENERATED_MAP,
|
||||
map_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"handler_fold",
|
||||
&values,
|
||||
30000,
|
||||
GENERATED_FOLD,
|
||||
fold_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"handler_text",
|
||||
&texts,
|
||||
300000,
|
||||
text_generated,
|
||||
text_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare_borrowed(
|
||||
"handler_text",
|
||||
&texts,
|
||||
300000,
|
||||
text_generated,
|
||||
text_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"handler_text_helper",
|
||||
&texts,
|
||||
300000,
|
||||
text_generated,
|
||||
text_helper_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare_named(
|
||||
"handler_text_direct",
|
||||
&texts,
|
||||
300000,
|
||||
text_generated,
|
||||
text_direct_generated,
|
||||
|value| value.len() as u64,
|
||||
"direct",
|
||||
);
|
||||
compare(
|
||||
"handler_record",
|
||||
&rows,
|
||||
5000,
|
||||
GENERATED_ROWS,
|
||||
rows_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"handler_capture",
|
||||
&failures,
|
||||
30000,
|
||||
capture_generated,
|
||||
capture_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"handler_map_failure",
|
||||
&[1, 2, i64::MAX],
|
||||
100000,
|
||||
map_failed_generated,
|
||||
map_failed_reference,
|
||||
|value| u64::from(*value),
|
||||
);
|
||||
let mut failed_rows = rows;
|
||||
failed_rows.last_mut().unwrap().field1 = i64::MAX;
|
||||
compare(
|
||||
"handler_record_failure",
|
||||
&failed_rows,
|
||||
5000,
|
||||
rows_failed_generated,
|
||||
rows_failed_reference,
|
||||
|value| u64::from(*value),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
use indexmap::IndexMap;
|
||||
|
||||
struct Lookup {
|
||||
generated: Map<Id, i64>,
|
||||
reference: IndexMap<Id, i64>,
|
||||
keys: Vec<Id>,
|
||||
}
|
||||
|
||||
struct Comparison {
|
||||
values: Vec<Id>,
|
||||
expected: Id,
|
||||
}
|
||||
|
||||
fn id_fault() -> Fault {
|
||||
&FaultInfo {
|
||||
message: "integer arithmetic fault",
|
||||
source: "reference.dever",
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn roundtrip_generated(values: &[String]) -> Result<String, Fault> {
|
||||
GENERATED_ROUNDTRIP(values.first().map(String::as_str).unwrap_or(""))
|
||||
}
|
||||
|
||||
fn roundtrip_reference(values: &[String]) -> Result<String, Fault> {
|
||||
let id = Id::from(values.first().map(String::as_str).unwrap_or("").to_owned());
|
||||
Ok(id.into_text())
|
||||
}
|
||||
|
||||
fn encode_reference(values: &[String]) -> Result<Vec<Id>, Fault> {
|
||||
Ok(values.iter().map(|text| Id::from(text.clone())).collect())
|
||||
}
|
||||
|
||||
fn compare_generated(values: &[Comparison]) -> Result<i64, Fault> {
|
||||
match values.first() {
|
||||
Some(value) => GENERATED_COMPARE(&value.values, &value.expected),
|
||||
None => Ok(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn compare_reference(values: &[Comparison]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
let expected = value.expected.clone();
|
||||
let mut count = 0i64;
|
||||
for item in &value.values {
|
||||
count = count
|
||||
.checked_add(i64::from(item == &expected))
|
||||
.ok_or_else(id_fault)?;
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn lookup_generated(values: &[Lookup]) -> Result<i64, Fault> {
|
||||
match values.first() {
|
||||
Some(value) => GENERATED_LOOKUP(&value.generated, &value.keys),
|
||||
None => Ok(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_reference(values: &[Lookup]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
let mut count = 0i64;
|
||||
for key in &value.keys {
|
||||
count = count
|
||||
.checked_add(value.reference.get(key).copied().unwrap_or(0))
|
||||
.ok_or_else(id_fault)?;
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn lookup_same_table(values: &[Lookup]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
let mut count = 0i64;
|
||||
for key in &value.keys {
|
||||
count = count
|
||||
.checked_add(value.generated.get(key).copied().unwrap_or(0))
|
||||
.ok_or_else(id_fault)?;
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn map_reference(values: &[String]) -> Result<Vec<(Id, i64)>, Fault> {
|
||||
let mut map = IndexMap::new();
|
||||
for text in values {
|
||||
map.insert(Id::from(text.clone()), text.chars().count() as i64);
|
||||
}
|
||||
Ok(map.into_iter().collect())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let texts: Vec<String> = (0..512)
|
||||
.map(|index| format!("user-{index:04}-雪"))
|
||||
.collect();
|
||||
let short = ["user-0012-雪".to_owned()];
|
||||
let long = ["雪山树林".repeat(512)];
|
||||
let reference = || {
|
||||
texts
|
||||
.iter()
|
||||
.map(|text| (Id::from(text.clone()), text.chars().count() as i64))
|
||||
.collect()
|
||||
};
|
||||
// 建表顺序影响独立表的哈希与分配布局;复测须等量覆盖两种顺序。
|
||||
let (generated, reference) = match std::env::var("DEVER_BENCH_MAP_ORDER")
|
||||
.as_deref()
|
||||
.unwrap_or("dever-first")
|
||||
{
|
||||
"dever-first" => {
|
||||
let generated = GENERATED_BUILD_MAP(&texts).unwrap();
|
||||
(generated, reference())
|
||||
}
|
||||
"rust-first" => {
|
||||
let reference = reference();
|
||||
(GENERATED_BUILD_MAP(&texts).unwrap(), reference)
|
||||
}
|
||||
order => panic!("unknown table construction order: {order}"),
|
||||
};
|
||||
let keys = (0..1024)
|
||||
.map(|index| Id::from(format!("user-{:04}-雪", (index * 17) % 576)))
|
||||
.collect();
|
||||
let lookups = [Lookup {
|
||||
generated,
|
||||
reference,
|
||||
keys,
|
||||
}];
|
||||
let comparisons = [Comparison {
|
||||
values: (0..1024)
|
||||
.map(|index| Id::from(format!("user-{:04}-雪", index % 32)))
|
||||
.collect(),
|
||||
expected: Id::from("user-0012-雪".to_owned()),
|
||||
}];
|
||||
assert_eq!(
|
||||
GENERATED_ENTRIES(&["a".into(), "b".into(), "a".into()]).unwrap(),
|
||||
map_reference(&["a".into(), "b".into(), "a".into()]).unwrap()
|
||||
);
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
compare(
|
||||
"id_roundtrip_short",
|
||||
&short,
|
||||
300000,
|
||||
roundtrip_generated,
|
||||
roundtrip_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"id_roundtrip_long",
|
||||
&long,
|
||||
20000,
|
||||
roundtrip_generated,
|
||||
roundtrip_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"id_each",
|
||||
&texts,
|
||||
300,
|
||||
GENERATED_ENCODE_ALL,
|
||||
encode_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"id_compare",
|
||||
&comparisons,
|
||||
2000,
|
||||
compare_generated,
|
||||
compare_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"id_map_lookup",
|
||||
&lookups,
|
||||
1000,
|
||||
lookup_generated,
|
||||
lookup_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
// 双方读取同一张表,避免将各自随机哈希状态的差异归给生成代码。
|
||||
compare_named(
|
||||
"id_map_lookup_same_table",
|
||||
&lookups,
|
||||
1000,
|
||||
lookup_generated,
|
||||
lookup_same_table,
|
||||
|value| *value as u64,
|
||||
"rust_runtime",
|
||||
);
|
||||
compare(
|
||||
"id_map_build",
|
||||
&texts,
|
||||
300,
|
||||
GENERATED_ENTRIES,
|
||||
map_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//! 固定 payload 指纹来自本基准的公开记录合同;不把生成函数作为 Rust 参考实现。
|
||||
|
||||
use dever::compiler::model::Program;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::path::Path;
|
||||
|
||||
pub fn run(core: &Path) {
|
||||
crate::compare(
|
||||
core,
|
||||
"job-performance",
|
||||
"jobs",
|
||||
&[
|
||||
include_str!("../auth/shared.rs"),
|
||||
include_str!("reference.rs"),
|
||||
include_str!("measure.rs"),
|
||||
include_str!("checks.rs"),
|
||||
]
|
||||
.concat(),
|
||||
&[("GENERATED_SUBMIT", "bench.queue.app.submit")],
|
||||
&[],
|
||||
);
|
||||
}
|
||||
|
||||
pub fn metadata(program: &Program, reference: &str) -> String {
|
||||
assert_eq!(program.jobs.len(), 1);
|
||||
assert_eq!(program.models.len(), 1);
|
||||
assert!(!program.has_roles());
|
||||
assert_eq!(program.jobs[0].name, "bench.queue.job.publish");
|
||||
let schema = format!(
|
||||
"{:x}",
|
||||
Sha256::digest(b"bench.queue.Payload{name:Text,amount:Int}")
|
||||
);
|
||||
reference
|
||||
.replace("JOB_BINDING", &program.models.len().to_string())
|
||||
.replace("RESULT_MODEL", &program.models[0].definition.to_string())
|
||||
.replace("JOB_SCHEMA", &format!("{schema:?}"))
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// 独立 SQL 校验持久队列和业务结果,不能只用函数返回值或随机 ID 长度认定正确。
|
||||
async fn clear(scope: &Scope<'_>, registry: &Arc<Registry>) {
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database.on(JOB_BINDING, scope).await.unwrap();
|
||||
executor
|
||||
.execute(scope, "DELETE FROM _dever_job", vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
executor
|
||||
.execute(scope, "DELETE FROM bench_queue_model", vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn check_rows(scope: &Scope<'_>, registry: &Arc<Registry>, inputs: &[JobInput], handled: bool) {
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database.on(JOB_BINDING, scope).await.unwrap();
|
||||
let rows = executor.query(scope,
|
||||
"SELECT id,target,payload,schema,execution,task_key,state,attempt,retries,timeout_ms,token,lease_until,available_at,created_at FROM _dever_job ORDER BY task_key",
|
||||
vec![], inputs.len() + 1, |row| Ok((
|
||||
row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?,
|
||||
row.get::<_, String>(3)?, row.get::<_, String>(4)?, row.get::<_, String>(5)?,
|
||||
row.get::<_, String>(6)?, row.get::<_, i64>(7)?, row.get::<_, i64>(8)?, row.get::<_, i64>(9)?,
|
||||
row.get::<_, Option<String>>(10)?, row.get::<_, Option<i64>>(11)?, row.get::<_, i64>(12)?, row.get::<_, i64>(13)?,
|
||||
))).await.unwrap();
|
||||
assert_eq!(rows.len(), inputs.len());
|
||||
let mut unique = std::collections::BTreeSet::new();
|
||||
for (row, input) in rows.iter().zip(inputs) {
|
||||
assert!(unique.insert(&row.0));
|
||||
assert_eq!(row.0.len(), 32);
|
||||
assert_eq!(row.1, TARGET);
|
||||
let payload: RustPayload = dever_runtime::wire::decode(&row.2).unwrap();
|
||||
assert_eq!(payload.name, input.name);
|
||||
assert_eq!(payload.amount, input.amount);
|
||||
assert_eq!(row.3, SCHEMA);
|
||||
assert!(matches!(
|
||||
dever_runtime::wire::decode::<Execution>(&row.4).unwrap(),
|
||||
Execution::System { tenant: None }
|
||||
));
|
||||
assert_eq!(row.5, input.key);
|
||||
assert_eq!(row.6, if handled { "completed" } else { "pending" });
|
||||
assert_eq!((row.7, row.8, row.9), (i64::from(handled), 2, 1000));
|
||||
assert!(row.10.is_none() && row.11.is_none());
|
||||
assert!(row.12 >= row.13 && row.13 > 0);
|
||||
}
|
||||
let results = executor
|
||||
.query(
|
||||
scope,
|
||||
"SELECT name,amount FROM bench_queue_model ORDER BY name",
|
||||
vec![],
|
||||
inputs.len() + 1,
|
||||
|row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let expected = if handled {
|
||||
inputs
|
||||
.iter()
|
||||
.map(|input| (input.name.clone(), input.amount * 3 + 7))
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
assert_eq!(results, expected);
|
||||
}
|
||||
|
||||
fn check_jobs(runtime: &Runtime, registry: &Arc<Registry>) {
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
for generated in [true, false] {
|
||||
clear(scope, registry).await;
|
||||
let inputs = inputs(2);
|
||||
for input in &inputs {
|
||||
let id = submit(generated, input, scope, registry).await;
|
||||
assert_eq!(submit(generated, input, scope, registry).await, id);
|
||||
}
|
||||
check_rows(scope, registry, &inputs, false).await;
|
||||
for claim in claims(scope, registry, 2).await {
|
||||
execute(generated, claim, scope, registry).await.unwrap();
|
||||
}
|
||||
check_rows(scope, registry, &inputs, true).await;
|
||||
clear(scope, registry).await;
|
||||
let input = JobInput {
|
||||
name: "rollback".into(),
|
||||
amount: -4,
|
||||
key: "rollback".into(),
|
||||
};
|
||||
submit(generated, &input, scope, registry).await;
|
||||
let claim = claims(scope, registry, 1).await.pop().unwrap();
|
||||
assert!(matches!(
|
||||
execute(generated, claim, scope, registry).await,
|
||||
Err(JobFailure::Handler)
|
||||
));
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database.on(JOB_BINDING, scope).await.unwrap();
|
||||
assert_eq!(
|
||||
executor
|
||||
.query(
|
||||
scope,
|
||||
"SELECT COUNT(*) FROM bench_queue_model",
|
||||
vec![],
|
||||
1,
|
||||
|row| Ok(row.get::<_, i64>(0)?)
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
[0]
|
||||
);
|
||||
assert_eq!(
|
||||
executor
|
||||
.query(scope, "SELECT state FROM _dever_job", vec![], 1, |row| Ok(
|
||||
row.get::<_, String>(0)?
|
||||
))
|
||||
.await
|
||||
.unwrap(),
|
||||
["running"]
|
||||
);
|
||||
drop(database);
|
||||
clear(scope, registry).await;
|
||||
submit(generated, &inputs[0], scope, registry).await;
|
||||
let claim = claims(scope, registry, 1).await.pop().unwrap();
|
||||
let mut claim = Arc::try_unwrap(claim).unwrap();
|
||||
claim.payload = r#"["array-is-not-a-record",1]"#.into();
|
||||
assert!(matches!(
|
||||
execute(generated, Arc::new(claim), scope, registry).await,
|
||||
Err(JobFailure::Blocked)
|
||||
));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
// 入队计时含具体编码和外层提交;执行计时含真实生成分派、任务作用域、解码、业务写入和确认。
|
||||
// 单阶段对照把 claim 放在计时外;完整循环另计入入队、claim、执行和确认。
|
||||
struct JobInput {
|
||||
name: String,
|
||||
amount: i64,
|
||||
key: String,
|
||||
}
|
||||
|
||||
fn inputs(count: usize) -> Vec<JobInput> {
|
||||
(0..count)
|
||||
.map(|index| JobInput {
|
||||
name: format!("job-{index:04} 中文\""),
|
||||
amount: index as i64 % 997,
|
||||
key: format!("key-{index:04}"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn submit(
|
||||
generated: bool,
|
||||
input: &JobInput,
|
||||
scope: &Scope<'_>,
|
||||
registry: &Arc<Registry>,
|
||||
) -> String {
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let owner = database.enter();
|
||||
let execution = Execution::System { tenant: None };
|
||||
let id = if generated {
|
||||
GENERATED_SUBMIT(
|
||||
&input.name,
|
||||
input.amount,
|
||||
&input.key,
|
||||
scope,
|
||||
&mut database,
|
||||
&execution,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
} else {
|
||||
rust_submit(
|
||||
&input.name,
|
||||
input.amount,
|
||||
&input.key,
|
||||
scope,
|
||||
&mut database,
|
||||
&execution,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
database.finish(scope, owner, true).await.unwrap();
|
||||
id.into_text()
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
generated: bool,
|
||||
claim: Arc<Claim>,
|
||||
scope: &Scope<'_>,
|
||||
registry: &Arc<Registry>,
|
||||
) -> Result<(), JobFailure> {
|
||||
let registry = registry.clone();
|
||||
let timeout = claim.policy.timeout_ms;
|
||||
let outcome = if generated {
|
||||
scope
|
||||
.spawn(move |child| job_execute(JOB_BINDING, None, claim, child, registry))
|
||||
.unwrap()
|
||||
.timeout(timeout)
|
||||
.await
|
||||
.unwrap()
|
||||
} else {
|
||||
scope
|
||||
.spawn(move |child| rust_execute(JOB_BINDING, claim, child, registry))
|
||||
.unwrap()
|
||||
.timeout(timeout)
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
match outcome {
|
||||
dever_runtime::task::Timed::Ready(value) => value,
|
||||
_ => panic!("benchmark handler timed out"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn claims(scope: &Scope<'_>, registry: &Arc<Registry>, count: usize) -> Vec<Arc<Claim>> {
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database.on(JOB_BINDING, scope).await.unwrap();
|
||||
let mut claims = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
let claim = job::claim(
|
||||
executor,
|
||||
scope,
|
||||
dever_runtime::database::now().unwrap(),
|
||||
60000,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(claim.schema, SCHEMA);
|
||||
assert_eq!(claim.target, TARGET);
|
||||
assert_eq!(claim.attempt, 1);
|
||||
claims.push(Arc::new(claim));
|
||||
}
|
||||
assert!(
|
||||
job::claim(
|
||||
executor,
|
||||
scope,
|
||||
dever_runtime::database::now().unwrap(),
|
||||
60000
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
claims
|
||||
}
|
||||
|
||||
fn measure_jobs(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
generated: bool,
|
||||
handler: bool,
|
||||
inputs: Vec<JobInput>,
|
||||
) -> (u128, u64) {
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
clear(scope, registry).await;
|
||||
let claims = if handler {
|
||||
for input in &inputs {
|
||||
black_box(submit(false, input, scope, registry).await);
|
||||
}
|
||||
claims(scope, registry, inputs.len()).await
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let mut checksum = 0u64;
|
||||
let start = Instant::now();
|
||||
if handler {
|
||||
for claim in claims {
|
||||
execute(generated, black_box(claim), scope, registry)
|
||||
.await
|
||||
.unwrap();
|
||||
checksum += black_box(1);
|
||||
}
|
||||
} else {
|
||||
for input in &inputs {
|
||||
let id = black_box(submit(generated, black_box(input), scope, registry).await);
|
||||
checksum += id.len() as u64 + input.name.len() as u64 + input.amount as u64;
|
||||
}
|
||||
}
|
||||
let elapsed = start.elapsed().as_nanos();
|
||||
check_rows(scope, registry, &inputs, handler).await;
|
||||
(elapsed, checksum)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let (directory, path) = directory("jobs", r#""runtime":{"mode":"worker"}"#);
|
||||
let runtime = runtime();
|
||||
let registry = runtime
|
||||
.run(async |scope| database_registry(&path, scope).await)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
check_jobs(&runtime, ®istry);
|
||||
header();
|
||||
for (name, handler) in [("job_enqueue_commit", false), ("job_handler_commit", true)] {
|
||||
samples(name, |generated, count| {
|
||||
measure_jobs(&runtime, ®istry, generated, handler, inputs(count))
|
||||
});
|
||||
}
|
||||
samples("job_enqueue_large_payload_commit", |generated, count| {
|
||||
let mut inputs = inputs(count.div_ceil(64));
|
||||
for input in &mut inputs {
|
||||
input.name.push_str(&"a".repeat(64 * 1024));
|
||||
}
|
||||
measure_jobs(&runtime, ®istry, generated, false, inputs)
|
||||
});
|
||||
samples("job_submit_claim_complete", |generated, count| {
|
||||
// 完整循环的每个样本包含 250 项实际任务;初始化及独立 SQL 核验不计时。
|
||||
measure_cycle(&runtime, ®istry, generated, count.div_ceil(8), false)
|
||||
});
|
||||
samples("job_backlog_claim_complete", |generated, count| {
|
||||
measure_cycle(&runtime, ®istry, generated, count.div_ceil(8), true)
|
||||
});
|
||||
samples("job_enqueue_duplicate_commit", |generated, count| {
|
||||
measure_duplicate(&runtime, ®istry, generated, inputs(count))
|
||||
});
|
||||
drop(registry);
|
||||
drop(runtime);
|
||||
drop(directory);
|
||||
}
|
||||
|
||||
fn measure_duplicate(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
generated: bool,
|
||||
inputs: Vec<JobInput>,
|
||||
) -> (u128, u64) {
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
clear(scope, registry).await;
|
||||
let mut expected = Vec::with_capacity(inputs.len());
|
||||
for input in &inputs {
|
||||
expected.push(submit(false, input, scope, registry).await);
|
||||
}
|
||||
let mut actual = Vec::with_capacity(inputs.len());
|
||||
let mut checksum = 0u64;
|
||||
let start = Instant::now();
|
||||
for input in &inputs {
|
||||
let id = submit(generated, black_box(input), scope, registry).await;
|
||||
checksum += black_box(id.len() as u64);
|
||||
actual.push(id);
|
||||
}
|
||||
let elapsed = start.elapsed().as_nanos();
|
||||
assert_eq!(actual, expected);
|
||||
check_rows(scope, registry, &inputs, false).await;
|
||||
(elapsed, checksum)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn measure_cycle(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
generated: bool,
|
||||
count: usize,
|
||||
backlog: bool,
|
||||
) -> (u128, u64) {
|
||||
let inputs = inputs(count);
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
clear(scope, registry).await;
|
||||
if backlog {
|
||||
for input in &inputs {
|
||||
submit(generated, input, scope, registry).await;
|
||||
}
|
||||
}
|
||||
let start = Instant::now();
|
||||
let mut checksum = 0u64;
|
||||
for input in &inputs {
|
||||
if !backlog {
|
||||
submit(generated, black_box(input), scope, registry).await;
|
||||
}
|
||||
let claim = {
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database.on(JOB_BINDING, scope).await.unwrap();
|
||||
job::claim(
|
||||
executor,
|
||||
scope,
|
||||
dever_runtime::database::now().unwrap(),
|
||||
60000,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
};
|
||||
execute(generated, Arc::new(claim), scope, registry)
|
||||
.await
|
||||
.unwrap();
|
||||
checksum += black_box(1);
|
||||
}
|
||||
let elapsed = start.elapsed().as_nanos();
|
||||
check_rows(scope, registry, &inputs, true).await;
|
||||
(elapsed, checksum)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// 手写 Rust 重新实现 payload 编码、业务计算、行解码和事务边界;仅复用同一队列和数据库设施。
|
||||
use dever_runtime::{
|
||||
job::{self, Claim, Execution, Failure as JobFailure},
|
||||
wire::serde::{Deserialize, Serialize},
|
||||
};
|
||||
|
||||
const SCHEMA: &str = JOB_SCHEMA;
|
||||
const TARGET: &str = "bench.queue.job.publish";
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde")]
|
||||
struct RustPayload {
|
||||
name: String,
|
||||
amount: i64,
|
||||
}
|
||||
|
||||
impl<'input> Deserialize<'input> for RustPayload {
|
||||
fn deserialize<Decoder: dever_runtime::wire::serde::Deserializer<'input>>(
|
||||
decoder: Decoder,
|
||||
) -> Result<Self, Decoder::Error> {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde", deny_unknown_fields)]
|
||||
struct Fields {
|
||||
name: String,
|
||||
amount: i64,
|
||||
}
|
||||
// 与 Dever record 一样只接受对象,不能用 serde 默认的数组兼容路径降低校验成本。
|
||||
let fields = dever_runtime::wire::object::<Fields, Decoder>(decoder)?;
|
||||
Ok(Self {
|
||||
name: fields.name,
|
||||
amount: fields.amount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct RustResult {
|
||||
_id: dever_runtime::database::Id<RESULT_MODEL>,
|
||||
_created_at: dever_runtime::time::calendar::DateTime,
|
||||
_name: String,
|
||||
amount: i64,
|
||||
}
|
||||
|
||||
async fn rust_submit(
|
||||
name: &str,
|
||||
amount: i64,
|
||||
key: &str,
|
||||
scope: &Scope<'_>,
|
||||
database: &mut DbContext,
|
||||
execution: &Execution,
|
||||
) -> Result<dever_runtime::value::id::Id, dever_runtime::database::Error> {
|
||||
let payload = dever_runtime::wire::json::Json::encode(&RustPayload {
|
||||
name: name.to_owned(),
|
||||
amount,
|
||||
})
|
||||
.map_err(|_| {
|
||||
dever_runtime::database::Error::new(
|
||||
dever_runtime::database::Kind::InvalidData,
|
||||
"invalid Job payload",
|
||||
)
|
||||
})?;
|
||||
let now = dever_runtime::database::now()?;
|
||||
let executor = database.on(JOB_BINDING, scope).await?;
|
||||
job::enqueue(
|
||||
executor,
|
||||
scope,
|
||||
job::Submission {
|
||||
target: TARGET,
|
||||
payload: Some(&payload),
|
||||
schema: SCHEMA,
|
||||
execution,
|
||||
key,
|
||||
policy: job::Policy {
|
||||
retries: 2,
|
||||
timeout_ms: 1000,
|
||||
},
|
||||
at: now,
|
||||
},
|
||||
now,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn rust_apply(
|
||||
payload: RustPayload,
|
||||
scope: &Scope<'_>,
|
||||
database: &mut DbContext,
|
||||
) -> Result<(), JobFailure> {
|
||||
let amount = payload
|
||||
.amount
|
||||
.checked_mul(3)
|
||||
.and_then(|value| value.checked_add(7))
|
||||
.ok_or(JobFailure::Handler)?;
|
||||
let parameters = vec![
|
||||
Parameter::Text(payload.name),
|
||||
Parameter::Integer(amount),
|
||||
Parameter::Integer(dever_runtime::database::now().map_err(|_| JobFailure::Handler)?),
|
||||
];
|
||||
let executor = database
|
||||
.on(0, scope)
|
||||
.await
|
||||
.map_err(|_| JobFailure::Handler)?;
|
||||
let saved = executor.query(scope,
|
||||
"INSERT INTO \"bench_queue_model\" (\"name\",\"amount\",\"created_at\") VALUES (?1,?2,?3) RETURNING \"id\",\"created_at\",\"name\",\"amount\"",
|
||||
parameters, 1, |row| Ok(RustResult {
|
||||
_id: dever_runtime::database::Id(row.get(0)?),
|
||||
_created_at: dever_runtime::time::calendar::DateTime::from_millis(row.get::<_, i64>(1)?).map_err(|_| dever_runtime::database::Error::new(dever_runtime::database::Kind::InvalidData, "invalid stored DateTime"))?,
|
||||
_name: row.get(2)?, amount: row.get(3)?,
|
||||
})
|
||||
).await.map_err(|_| JobFailure::Handler)?.pop().ok_or(JobFailure::Handler)?;
|
||||
if saved.amount < 0 {
|
||||
return Err(JobFailure::Handler);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rust_execute(
|
||||
binding: usize,
|
||||
claim: Arc<Claim>,
|
||||
scope: Scope<'static>,
|
||||
registry: Arc<Registry>,
|
||||
) -> Result<(), JobFailure> {
|
||||
let execution: Execution =
|
||||
dever_runtime::wire::decode(&claim.execution).map_err(|_| JobFailure::Blocked)?;
|
||||
if claim.target != TARGET || claim.schema != SCHEMA || !registry.same_pool(binding, JOB_BINDING)
|
||||
{
|
||||
return Err(JobFailure::Blocked);
|
||||
}
|
||||
if !matches!(execution, Execution::System { tenant: None }) || claim.execution_tenant != 0 {
|
||||
return Err(JobFailure::Blocked);
|
||||
}
|
||||
let payload: RustPayload =
|
||||
dever_runtime::wire::decode(&claim.payload).map_err(|_| JobFailure::Blocked)?;
|
||||
let mut database = DbContext::new(registry);
|
||||
let owner = database.enter();
|
||||
let outcome = async {
|
||||
rust_apply(payload, &scope, &mut database).await?;
|
||||
let executor = database
|
||||
.on(JOB_BINDING, &scope)
|
||||
.await
|
||||
.map_err(|_| JobFailure::Handler)?;
|
||||
job::complete(
|
||||
executor,
|
||||
&scope,
|
||||
&claim,
|
||||
dever_runtime::database::now().map_err(|_| JobFailure::Handler)?,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| JobFailure::Handler)
|
||||
}
|
||||
.await;
|
||||
database
|
||||
.finish(&scope, owner, outcome.is_ok())
|
||||
.await
|
||||
.map_err(|_| JobFailure::Handler)?;
|
||||
outcome
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// 两侧共享相同的 wire 库和预算,隔离 Dever 生成代码开销,不代替独立 JSON 引擎对比。
|
||||
use dever_runtime::wire::{json::Json, serde::{Serialize, Serializer, ser::SerializeStruct}};
|
||||
|
||||
fn json_fault(_: impl std::fmt::Display) -> Fault {
|
||||
&FaultInfo { message: "JSON failure", source: "json-reference", line: 1, column: 1 }
|
||||
}
|
||||
|
||||
struct Payload<'value> { values: &'value [i64] }
|
||||
|
||||
impl Serialize for Payload<'_> {
|
||||
fn serialize<Encoder: Serializer>(&self, encoder: Encoder) -> Result<Encoder::Ok, Encoder::Error> {
|
||||
let mut record = encoder.serialize_struct("Payload", 1)?;
|
||||
record.serialize_field("values", self.values)?;
|
||||
record.end()
|
||||
}
|
||||
}
|
||||
|
||||
fn generated_encode(values: &[i64]) -> Result<String, Fault> {
|
||||
GENERATED_ENCODE(values).map_err(json_fault)
|
||||
}
|
||||
|
||||
fn reference_encode(values: &[i64]) -> Result<String, Fault> {
|
||||
Json::encode(&Payload { values }).map(|value| value.as_str().to_owned()).map_err(json_fault)
|
||||
}
|
||||
|
||||
// 独立 serde 输出不执行 Dever 的三个预算,只作为额外编码成本的下界。
|
||||
fn reference_serde_encode(values: &[i64]) -> Result<String, Fault> {
|
||||
dever_runtime::wire::serde_json::to_string(&Payload { values }).map_err(json_fault)
|
||||
}
|
||||
|
||||
// 独立 Rust 的同合同实现:两层容器,根和数组占两个值,i64 最长 20 字节。
|
||||
// 值数合格时,含分隔符的最大输出不足 16 MiB,无须逐片段再次检查。
|
||||
fn reference_bounded_encode(values: &[i64]) -> Result<String, Fault> {
|
||||
if values.len() > dever_runtime::wire::document::MAX_VALUES - 2 {
|
||||
return Err(json_fault("JSON value budget"));
|
||||
}
|
||||
reference_serde_encode(values)
|
||||
}
|
||||
|
||||
fn generated_lookup(values: &[String]) -> Result<i64, Fault> {
|
||||
values.first().map_or(Ok(-1), |value| GENERATED_LOOKUP(value).map_err(json_fault))
|
||||
}
|
||||
|
||||
fn reference_lookup(values: &[String]) -> Result<i64, Fault> {
|
||||
let Some(source) = values.first() else { return Ok(-1); };
|
||||
let json = Json::parse(source).map_err(json_fault)?;
|
||||
json.property("count").map_err(json_fault)?.map_or(Ok(-1), |value| value.as_integer().map_err(json_fault))
|
||||
}
|
||||
|
||||
fn generated_update(values: &[String]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| GENERATED_UPDATE(value).map_err(json_fault))
|
||||
}
|
||||
|
||||
fn reference_update(values: &[String]) -> Result<String, Fault> {
|
||||
let Some(source) = values.first() else { return Ok(String::new()); };
|
||||
let value = Json::parse(source).map_err(json_fault)?;
|
||||
let child = Json::integer(43).map_err(json_fault)?;
|
||||
value.set_property("count", &child).map(|value| value.as_str().to_owned()).map_err(json_fault)
|
||||
}
|
||||
|
||||
fn generated_scalar_text(values: &[String]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| GENERATED_SCALAR_TEXT(value).map_err(json_fault))
|
||||
}
|
||||
|
||||
fn reference_scalar_text(values: &[String]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| Json::string(value).map(|value| value.as_str().to_owned()).map_err(json_fault))
|
||||
}
|
||||
|
||||
fn generated_scalar_float(values: &[f64]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| GENERATED_SCALAR_FLOAT(*value).map_err(json_fault))
|
||||
}
|
||||
|
||||
fn reference_scalar_float(values: &[f64]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| Json::float(*value).map(|value| value.as_str().to_owned()).map_err(json_fault))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde")]
|
||||
struct Fixed<'value> {
|
||||
text: &'value str,
|
||||
amount: i64,
|
||||
ratio: f64,
|
||||
}
|
||||
|
||||
fn reference_fixed_value(text: &str, ratio: f64) -> Result<String, Fault> {
|
||||
if !ratio.is_finite() { return Err(json_fault("non-finite Float")); }
|
||||
Json::encode(&Fixed { text, amount: 7, ratio })
|
||||
.map(Json::into_text).map_err(json_fault)
|
||||
}
|
||||
|
||||
fn generated_fixed(values: &[String]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| GENERATED_FIXED_RECORD(value, 4.25).map_err(json_fault))
|
||||
}
|
||||
|
||||
fn reference_fixed(values: &[String]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| reference_fixed_value(value, 4.25))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("workload,sample,implementation,nanos,checksum");
|
||||
for count in [0, 8, 256] {
|
||||
let input = [Json::parse(&serde_json::to_string(&vec!["雪山 "; count]).unwrap()).unwrap()];
|
||||
compare(&format!("json_typed_decode_{count}"), &input, 2000, generated_decode, reference_decode, |values| values.iter().map(|value| value.len() as u64).sum());
|
||||
compare(&format!("json_captured_decode_{count}"), &input, 2000, generated_captured_decode, reference_captured_decode, |value| *value as u64);
|
||||
}
|
||||
let invalid = [Json::parse("[1]").unwrap()];
|
||||
compare("json_captured_decode_invalid", &invalid, 2000, generated_captured_decode, reference_captured_decode, |value| *value as u64);
|
||||
for state in ["Ready", "Busy"] {
|
||||
let input = [Json::parse(&format!("\"{state}\"")).unwrap()];
|
||||
compare(&format!("json_enum_decode_{state}"), &input, 100_000, generated_enum_decode, reference_enum_decode, |value| u64::from(*value));
|
||||
}
|
||||
property_costs();
|
||||
raw_output_costs();
|
||||
let values = (0..256).collect::<Vec<i64>>();
|
||||
compare("json_record", &values, 2000, generated_encode, reference_encode, |value| value.len() as u64);
|
||||
for count in [16, 256, 4096] {
|
||||
let input = (0..count).collect::<Vec<i64>>();
|
||||
compare_named(&format!("json_record_serde_{count}"), &input, 2000, generated_encode, reference_serde_encode, |value| value.len() as u64, "rust_serde");
|
||||
compare_named(&format!("json_record_bounded_{count}"), &input, 2000, generated_encode, reference_bounded_encode, |value| value.len() as u64, "rust_bounded");
|
||||
}
|
||||
let maximum = vec![0; dever_runtime::wire::document::MAX_VALUES - 2];
|
||||
assert_eq!(generated_encode(&maximum).unwrap(), reference_bounded_encode(&maximum).unwrap());
|
||||
let excessive = vec![0; dever_runtime::wire::document::MAX_VALUES - 1];
|
||||
assert!(generated_encode(&excessive).is_err());
|
||||
assert!(reference_bounded_encode(&excessive).is_err());
|
||||
let source = format!(r#"{{"count":42,"ratio":1e9999,"text":"雪","values":{values:?}}}"#);
|
||||
compare("json_lookup", std::slice::from_ref(&source), 2000, generated_lookup, reference_lookup, |value| *value as u64);
|
||||
compare("json_update", &[source], 2000, generated_update, reference_update, |value| value.len() as u64);
|
||||
compare("json_scalar_text", &["雪\"\\".repeat(512)], 2000, generated_scalar_text, reference_scalar_text, |value| value.len() as u64);
|
||||
compare("json_scalar_float", &[12345.6789], 2000, generated_scalar_float, reference_scalar_float, |value| value.len() as u64);
|
||||
for bytes in [64, 4096, 65536] {
|
||||
let input = ["a".repeat(bytes)];
|
||||
assert_eq!(generated_fixed(&input).unwrap(), reference_fixed(&input).unwrap());
|
||||
compare(&format!("json_fixed_record_{bytes}"), &input, 2000, generated_fixed, reference_fixed, |value| value.len() as u64);
|
||||
}
|
||||
for ratio in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
|
||||
assert!(GENERATED_FIXED_RECORD("", ratio).is_err());
|
||||
assert!(reference_fixed_value("", ratio).is_err());
|
||||
}
|
||||
let overhead = reference_fixed_value("", 4.25).unwrap().len();
|
||||
let maximum = "a".repeat(dever_runtime::wire::document::MAX_BYTES - overhead);
|
||||
assert_eq!(GENERATED_FIXED_RECORD(&maximum, 4.25).unwrap().len(), dever_runtime::wire::document::MAX_BYTES);
|
||||
assert!(GENERATED_FIXED_RECORD(&(maximum + "a"), 4.25).is_err());
|
||||
}
|
||||
|
||||
fn generated_decode(values: &[Json]) -> Result<Vec<String>, Fault> {
|
||||
values.first().map_or(Ok(Vec::new()), |value| GENERATED_DECODE(value).map_err(json_fault))
|
||||
}
|
||||
|
||||
fn reference_decode(values: &[Json]) -> Result<Vec<String>, Fault> {
|
||||
// Dever 保留解码失败文本;两侧构造错误后再统一映射为基准 Fault。
|
||||
values.first().map_or(Ok(Vec::new()), |value| serde_json::from_str(value.as_str()).map_err(|error| json_fault(error.to_string())))
|
||||
}
|
||||
|
||||
fn generated_captured_decode(values: &[Json]) -> Result<i64, Fault> {
|
||||
values.first().map_or(Ok(0), |value| GENERATED_CAPTURED_DECODE(value).map_err(json_fault))
|
||||
}
|
||||
|
||||
fn reference_captured_decode(values: &[Json]) -> Result<i64, Fault> {
|
||||
values.first().map_or(Ok(0), |value| {
|
||||
Ok(match serde_json::from_str::<Vec<String>>(value.as_str()) {
|
||||
Ok(values) => values.len() as i64,
|
||||
Err(error) => { let _message = error.to_string(); -1 }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(dever_runtime::wire::serde::Deserialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde")]
|
||||
enum WireState { Ready, Busy }
|
||||
|
||||
fn generated_enum_decode(values: &[Json]) -> Result<bool, Fault> {
|
||||
values.first().map_or(Ok(false), |value| GENERATED_ENUM_DECODE(value).map_err(json_fault))
|
||||
}
|
||||
|
||||
fn reference_enum_decode(values: &[Json]) -> Result<bool, Fault> {
|
||||
values.first().map_or(Ok(false), |value| {
|
||||
serde_json::from_str::<WireState>(value.as_str())
|
||||
.map(|state| matches!(state, WireState::Ready))
|
||||
.map_err(|error| json_fault(error.to_string()))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
use dever_runtime::wire::document;
|
||||
|
||||
fn document_fault(_: impl std::fmt::Display) -> Fault {
|
||||
&FaultInfo { message: "JSON failure", source: "document-reference", line: 1, column: 1 }
|
||||
}
|
||||
|
||||
fn generated_document(values: &[String]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |source| GENERATED_DOCUMENT(source).map_err(document_fault))
|
||||
}
|
||||
|
||||
fn reference_document(values: &[String]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |source| {
|
||||
let value = document::parse_with_depth(source, document::DOCUMENT_MAX_DEPTH).map_err(document_fault)?;
|
||||
document::stringify_with_depth(&value, document::DOCUMENT_MAX_DEPTH).map_err(document_fault)
|
||||
})
|
||||
}
|
||||
|
||||
// 独立手写 Map 适配,解析/写出引擎仍共用;只衡量表示转换与生成层。
|
||||
type ReferenceNode = reference_writer::Node<String, Vec<i64>, Map<String, i64>>;
|
||||
|
||||
fn reference_map_document(values: &[String]) -> Result<String, Fault> {
|
||||
let Some(source) = values.first() else { return Ok(String::new()); };
|
||||
let parsed = document::parse_with_depth(source, document::DOCUMENT_MAX_DEPTH).map_err(document_fault)?;
|
||||
let mut nodes = indexmap::IndexMap::with_capacity(parsed.nodes.len());
|
||||
for (index, value) in parsed.nodes.into_iter().enumerate() {
|
||||
let value = match value {
|
||||
document::Value::Null => ReferenceNode::Null,
|
||||
document::Value::Boolean(value) => ReferenceNode::Boolean(value),
|
||||
document::Value::Number(value) => ReferenceNode::Number(value),
|
||||
document::Value::String(value) => ReferenceNode::String(value),
|
||||
document::Value::Array(values) => ReferenceNode::Array(values),
|
||||
document::Value::Object(values) => ReferenceNode::Object(values.map_or_else(|| Map::with_capacity(0), |values| *values)),
|
||||
};
|
||||
nodes.insert(index as i64, value);
|
||||
}
|
||||
reference_map_write(parsed.root as i64, &nodes)
|
||||
}
|
||||
|
||||
fn reference_map_write(root: i64, nodes: &indexmap::IndexMap<i64, ReferenceNode>) -> Result<String, Fault> {
|
||||
let root = usize::try_from(root).map_err(document_fault)?;
|
||||
if nodes.len() > document::MAX_VALUES { return Err(document_fault("too many nodes")); }
|
||||
let mut ordered = vec![None; nodes.len()];
|
||||
for (id, value) in nodes {
|
||||
if let Ok(index) = usize::try_from(*id) && let Some(slot) = ordered.get_mut(index) {
|
||||
*slot = Some(value);
|
||||
}
|
||||
}
|
||||
let ordered = ordered.into_iter().map(|value| value.ok_or_else(|| document_fault("missing node"))).collect::<Result<Vec<_>, _>>()?;
|
||||
reference_writer::stringify_nodes(root, ordered.len(), |index| {
|
||||
match ordered[index] {
|
||||
ReferenceNode::Null => reference_writer::Node::Null,
|
||||
ReferenceNode::Boolean(value) => reference_writer::Node::Boolean(*value),
|
||||
ReferenceNode::Number(value) => reference_writer::Node::Number(value.as_str()),
|
||||
ReferenceNode::String(value) => reference_writer::Node::String(value.as_str()),
|
||||
ReferenceNode::Array(values) => reference_writer::Node::Array(values.iter().copied()),
|
||||
ReferenceNode::Object(values) => reference_writer::Node::Object(values.iter().map(|(name, id)| (name.as_str(), *id))),
|
||||
}
|
||||
}, document::DOCUMENT_MAX_DEPTH).map_err(document_fault)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ReferenceFailure {
|
||||
Fault(Fault),
|
||||
Parse(i64, String, Fault),
|
||||
Write(i64, String, Fault),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ReferenceFailure {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Fault(fault) | Self::Parse(_, _, fault) | Self::Write(_, _, fault) => fault.fmt(formatter),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reference_map_faults(values: &[String]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |source| reference_map_contract(source).map_err(document_fault))
|
||||
}
|
||||
|
||||
// 原参照继续保留;这里独立保留解析/输出错误的载荷与静态源位置。
|
||||
fn reference_map_contract(source: &str) -> Result<String, ReferenceFailure> {
|
||||
let parse_fault = &FaultInfo { message: "ParseResult.Failed", source: "document-reference", line: 1, column: 1 };
|
||||
let write_fault = &FaultInfo { message: "StringifyResult.Failed", source: "document-reference", line: 2, column: 1 };
|
||||
let fail = |node, message: &str| ReferenceFailure::Write(node, message.into(), write_fault);
|
||||
let parsed = document::parse_with_depth(source, document::DOCUMENT_MAX_DEPTH)
|
||||
.map_err(|error| ReferenceFailure::Parse(error.position as i64, error.message, parse_fault))?;
|
||||
let mut nodes = indexmap::IndexMap::with_capacity(parsed.nodes.len());
|
||||
for (index, value) in parsed.nodes.into_iter().enumerate() {
|
||||
let value = match value {
|
||||
document::Value::Null => ReferenceNode::Null,
|
||||
document::Value::Boolean(value) => ReferenceNode::Boolean(value),
|
||||
document::Value::Number(value) => ReferenceNode::Number(value),
|
||||
document::Value::String(value) => ReferenceNode::String(value),
|
||||
document::Value::Array(values) => ReferenceNode::Array(values),
|
||||
document::Value::Object(values) => ReferenceNode::Object(values.map_or_else(|| Map::with_capacity(0), |values| *values)),
|
||||
};
|
||||
nodes.insert(index as i64, value);
|
||||
}
|
||||
let root = parsed.root as i64;
|
||||
let root_index = usize::try_from(root).map_err(|_| fail(root, "negative JSON root"))?;
|
||||
if nodes.len() > document::MAX_VALUES { return Err(fail(root, "JSON node count exceeds 65536 values")); }
|
||||
let mut ordered = vec![None; nodes.len()];
|
||||
for (id, value) in &nodes {
|
||||
if let Ok(index) = usize::try_from(*id) && let Some(slot) = ordered.get_mut(index) {
|
||||
*slot = Some(value);
|
||||
}
|
||||
}
|
||||
let ordered = ordered.into_iter().enumerate().map(|(index, value)| value.ok_or_else(|| fail(index as i64, "JSON node IDs must be consecutive"))).collect::<Result<Vec<_>, _>>()?;
|
||||
reference_writer::stringify_nodes(root_index, ordered.len(), |index| {
|
||||
match ordered[index] {
|
||||
ReferenceNode::Null => reference_writer::Node::Null,
|
||||
ReferenceNode::Boolean(value) => reference_writer::Node::Boolean(*value),
|
||||
ReferenceNode::Number(value) => reference_writer::Node::Number(value.as_str()),
|
||||
ReferenceNode::String(value) => reference_writer::Node::String(value.as_str()),
|
||||
ReferenceNode::Array(values) => reference_writer::Node::Array(values.iter().copied()),
|
||||
ReferenceNode::Object(values) => reference_writer::Node::Object(values.iter().map(|(name, id)| (name.as_str(), *id))),
|
||||
}
|
||||
}, document::DOCUMENT_MAX_DEPTH).map_err(|error| ReferenceFailure::Write(error.node as i64, error.message, write_fault))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
let text = "雪\\\"\n".repeat(256);
|
||||
let strings = dever_runtime::wire::json::Json::encode(&vec![text; 32]).unwrap().as_str().to_owned();
|
||||
let numbers = format!("[{}]", vec!["1234567890123456789012345678901234e-12"; 256].join(","));
|
||||
let mut nested = "1e9999".to_owned();
|
||||
for _ in 0..48 { nested = format!("{{\"value\":{nested}}}"); }
|
||||
let empty_objects = format!("[{}]", vec!["{}"; 128].join(","));
|
||||
let wide = format!("{{{}}}", (0..256).map(|index| format!("\"key{index}\":1e9999")).collect::<Vec<_>>().join(","));
|
||||
for (name, source) in [("document_text", strings), ("document_numbers", numbers), ("document_nested", nested), ("document_empty", "{}".into()), ("document_empty_objects", empty_objects), ("document_wide", wide)] {
|
||||
compare(name, std::slice::from_ref(&source), 100, generated_document, reference_document, |value| value.bytes().map(u64::from).sum());
|
||||
compare(&format!("{name}_map"), std::slice::from_ref(&source), 100, generated_document, reference_map_document, |value| value.bytes().map(u64::from).sum());
|
||||
compare(&format!("{name}_map_faults"), &[source], 100, generated_document, reference_map_faults, |value| value.bytes().map(u64::from).sum());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// 阶段诊断复用正式入口;shared 标签只测运行库,不是独立 Rust 引擎参照。
|
||||
// map 场景名保留历史编号,Rust 参照仍构造 Map;当前 Dever 直接拥有节点 List。
|
||||
struct PreparedDocument {
|
||||
generated: DOCUMENT_TYPE,
|
||||
reference: (i64, indexmap::IndexMap<i64, ReferenceNode>),
|
||||
raw: document::Document,
|
||||
}
|
||||
|
||||
fn reference_nodes(parsed: document::Document) -> (i64, indexmap::IndexMap<i64, ReferenceNode>) {
|
||||
let mut nodes = indexmap::IndexMap::with_capacity(parsed.nodes.len());
|
||||
for (index, value) in parsed.nodes.into_iter().enumerate() {
|
||||
let value = match value {
|
||||
document::Value::Null => ReferenceNode::Null,
|
||||
document::Value::Boolean(value) => ReferenceNode::Boolean(value),
|
||||
document::Value::Number(value) => ReferenceNode::Number(value),
|
||||
document::Value::String(value) => ReferenceNode::String(value),
|
||||
document::Value::Array(values) => ReferenceNode::Array(values),
|
||||
document::Value::Object(values) => ReferenceNode::Object(values.map_or_else(|| Map::with_capacity(0), |values| *values)),
|
||||
};
|
||||
nodes.insert(index as i64, value);
|
||||
}
|
||||
(parsed.root as i64, nodes)
|
||||
}
|
||||
|
||||
fn phase_raw_parse(values: &[String]) -> Result<usize, Fault> {
|
||||
values.first().map_or(Ok(0), |source| {
|
||||
document::parse_with_depth(source, document::DOCUMENT_MAX_DEPTH)
|
||||
.map(|parsed| black_box(parsed).nodes.len()).map_err(document_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn phase_generated_parse(values: &[String]) -> Result<usize, Fault> {
|
||||
values.first().map_or(Ok(0), |source| {
|
||||
GENERATED_PARSE(source).map(|parsed| black_box(parsed).field1.len()).map_err(document_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn phase_reference_parse(values: &[String]) -> Result<usize, Fault> {
|
||||
values.first().map_or(Ok(0), |source| {
|
||||
document::parse_with_depth(source, document::DOCUMENT_MAX_DEPTH)
|
||||
.map(|parsed| black_box(reference_nodes(parsed)).1.len()).map_err(document_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn phase_generated_write(values: &[PreparedDocument]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| GENERATED_WRITE(&value.generated).map_err(document_fault))
|
||||
}
|
||||
|
||||
fn phase_reference_write(values: &[PreparedDocument]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| reference_map_write(value.reference.0, &value.reference.1))
|
||||
}
|
||||
|
||||
fn phase_raw_write(values: &[PreparedDocument]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| {
|
||||
document::stringify_with_depth(&value.raw, document::DOCUMENT_MAX_DEPTH).map_err(document_fault)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PhaseWriteFailure {
|
||||
Fault(Fault),
|
||||
Write(i64, String, Fault),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PhaseWriteFailure {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Fault(fault) | Self::Write(_, _, fault) => fault.fmt(formatter),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn phase_contract_write(values: &[PreparedDocument]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| reference_contract_write(&value.generated).map_err(document_fault))
|
||||
}
|
||||
|
||||
fn phase_adapter_write(values: &[PreparedDocument]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| runtime_contract_write(&value.generated).map_err(document_fault))
|
||||
}
|
||||
|
||||
// 同一份节点直接调用当前运行库,只衡量生成适配,不作为独立 JSON 引擎参照。
|
||||
fn runtime_contract_write(value: &DOCUMENT_TYPE) -> Result<String, PhaseWriteFailure> {
|
||||
let origin = &FaultInfo { message: "StringifyResult.Failed", source: "document-reference", line: 1, column: 1 };
|
||||
let root = usize::try_from(value.field0).map_err(|_| {
|
||||
PhaseWriteFailure::Write(value.field0, "negative JSON root".into(), origin)
|
||||
})?;
|
||||
document::stringify_slice(root, &value.field1, document::DOCUMENT_MAX_DEPTH)
|
||||
.map_err(|error| PhaseWriteFailure::Write(error.node as i64, error.message, origin))
|
||||
}
|
||||
|
||||
// 同一实参及错误布局,只隔离生成层;解析和输出引擎仍共享。
|
||||
fn reference_contract_write(value: &DOCUMENT_TYPE) -> Result<String, PhaseWriteFailure> {
|
||||
let origin = &FaultInfo { message: "StringifyResult.Failed", source: "document-reference", line: 1, column: 1 };
|
||||
let fail = |node, message: &str| PhaseWriteFailure::Write(node, message.into(), origin);
|
||||
let root = usize::try_from(value.field0).map_err(|_| fail(value.field0, "negative JSON root"))?;
|
||||
reference_writer::stringify_nodes(root, value.field1.len(), |index| {
|
||||
match &value.field1[index] {
|
||||
document::Value::Null => reference_writer::Node::Null,
|
||||
document::Value::Boolean(value) => reference_writer::Node::Boolean(*value),
|
||||
document::Value::Number(value) => reference_writer::Node::Number(value.as_str()),
|
||||
document::Value::String(value) => reference_writer::Node::String(value.as_str()),
|
||||
document::Value::Array(values) => reference_writer::Node::Array(values.iter().copied()),
|
||||
document::Value::Object(values) => reference_writer::Node::Object(values.iter().flat_map(|values| values.iter()).map(|(name, id)| (name.as_str(), *id))),
|
||||
}
|
||||
}, document::DOCUMENT_MAX_DEPTH).map_err(|error| PhaseWriteFailure::Write(error.node as i64, error.message, origin))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
let numbers = format!("[{}]", vec!["1234567890123456789012345678901234e-12"; 256].join(","));
|
||||
let mut nested = "1e9999".to_owned();
|
||||
for _ in 0..48 { nested = format!("{{\"value\":{nested}}}"); }
|
||||
let text = dever_runtime::wire::json::Json::encode(&vec!["雪\\\"\n".repeat(256); 32]).unwrap().as_str().to_owned();
|
||||
let ascii = dever_runtime::wire::serde_json::to_string(&vec!["abcdef".repeat(256); 32]).unwrap();
|
||||
let unicode = dever_runtime::wire::serde_json::to_string(&vec!["雪🙂".repeat(256); 32]).unwrap();
|
||||
for (name, source) in [("numbers", numbers), ("nested", nested), ("text", text), ("ascii", ascii), ("unicode", unicode)] {
|
||||
let prepared = PreparedDocument {
|
||||
generated: GENERATED_PARSE(&source).unwrap(),
|
||||
reference: reference_nodes(document::parse_with_depth(&source, document::DOCUMENT_MAX_DEPTH).unwrap()),
|
||||
raw: document::parse_with_depth(&source, document::DOCUMENT_MAX_DEPTH).unwrap(),
|
||||
};
|
||||
let inputs = std::slice::from_ref(&source);
|
||||
compare_named(&format!("{name}_parse_shared"), inputs, 100, phase_raw_parse, phase_raw_parse, |count| *count as u64, "shared");
|
||||
compare(&format!("{name}_parse_map"), inputs, 100, phase_generated_parse, phase_reference_parse, |count| *count as u64);
|
||||
let values = &[prepared];
|
||||
compare_named(&format!("{name}_write_shared"), values, 100, phase_raw_write, phase_raw_write, |value| value.bytes().map(u64::from).sum(), "shared");
|
||||
compare(&format!("{name}_write_map"), values, 100, phase_generated_write, phase_reference_write, |value| value.bytes().map(u64::from).sum());
|
||||
compare(&format!("{name}_write_map_contract"), values, 100, phase_generated_write, phase_contract_write, |value| value.bytes().map(u64::from).sum());
|
||||
compare_named(&format!("{name}_write_adapter"), values, 100, phase_generated_write, phase_adapter_write, |value| value.bytes().map(u64::from).sum(), "shared_engine");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// 两侧都接收已验证的 JSON,返回独立拥有的原始子树;不在计时内重复验证输入。
|
||||
// 独立参照只使用 Serde,不调用 Dever 的键比较或字段读取实现。
|
||||
use dever_runtime::wire::serde::{
|
||||
Deserializer,
|
||||
de::{DeserializeSeed, IgnoredAny, MapAccess, Visitor},
|
||||
};
|
||||
use dever_runtime::wire::serde_json::{self, value::RawValue};
|
||||
|
||||
struct PropertyCase {
|
||||
source: Json,
|
||||
name: String,
|
||||
}
|
||||
|
||||
struct MatchingKey<'name>(&'name str);
|
||||
|
||||
impl<'input> DeserializeSeed<'input> for MatchingKey<'_> {
|
||||
type Value = bool;
|
||||
|
||||
fn deserialize<Decoder: Deserializer<'input>>(
|
||||
self,
|
||||
decoder: Decoder,
|
||||
) -> Result<bool, Decoder::Error> {
|
||||
decoder.deserialize_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'input> Visitor<'input> for MatchingKey<'_> {
|
||||
type Value = bool;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("an object key")
|
||||
}
|
||||
|
||||
fn visit_str<Failure: dever_runtime::wire::serde::de::Error>(
|
||||
self,
|
||||
value: &str,
|
||||
) -> Result<bool, Failure> {
|
||||
Ok(value == self.0)
|
||||
}
|
||||
}
|
||||
|
||||
struct MatchingProperty<'name>(&'name str);
|
||||
|
||||
impl<'input> Visitor<'input> for MatchingProperty<'_> {
|
||||
type Value = Option<&'input RawValue>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("an object")
|
||||
}
|
||||
|
||||
fn visit_map<Access: MapAccess<'input>>(
|
||||
self,
|
||||
mut fields: Access,
|
||||
) -> Result<Self::Value, Access::Error> {
|
||||
let mut result = None;
|
||||
while let Some(matches) = fields.next_key_seed(MatchingKey(self.0))? {
|
||||
if matches {
|
||||
result = Some(fields.next_value()?);
|
||||
} else {
|
||||
fields.next_value::<IgnoredAny>()?;
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_property(values: &[PropertyCase]) -> Result<Option<String>, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(None);
|
||||
};
|
||||
value
|
||||
.source
|
||||
.property(&value.name)
|
||||
.map(|found| found.map(Json::into_text))
|
||||
.map_err(json_fault)
|
||||
}
|
||||
|
||||
fn native_property(values: &[PropertyCase]) -> Result<Option<String>, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut decoder =
|
||||
dever_runtime::wire::serde_json::Deserializer::from_str(value.source.as_str());
|
||||
decoder
|
||||
.deserialize_map(MatchingProperty(&value.name))
|
||||
.map(|found| found.map(|raw| raw.get().to_owned()))
|
||||
.map_err(json_fault)
|
||||
}
|
||||
|
||||
fn property_costs() {
|
||||
for (kind, suffix) in [("plain", ""), ("escaped", "雪\\\"\n")] {
|
||||
let names = (0..256)
|
||||
.map(|index| format!("key_{index:03}_{suffix}"))
|
||||
.collect::<Vec<_>>();
|
||||
let fields = names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
format!(
|
||||
"{}: [ 1e9999, -0, \"\\u96ea\" ]",
|
||||
serde_json::to_string(name).unwrap()
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let source = format!("{{{}}}", fields.join(","));
|
||||
for (position, name) in [
|
||||
("first", names[0].as_str()),
|
||||
("middle", names[128].as_str()),
|
||||
("last", names[255].as_str()),
|
||||
("missing", "absent"),
|
||||
] {
|
||||
let values = [PropertyCase {
|
||||
source: Json::parse(&source).unwrap(),
|
||||
name: name.into(),
|
||||
}];
|
||||
let expected =
|
||||
(position != "missing").then(|| r#"[ 1e9999, -0, "\u96ea" ]"#.to_owned());
|
||||
assert_eq!(runtime_property(&values).unwrap(), expected);
|
||||
assert_eq!(native_property(&values).unwrap(), expected);
|
||||
compare_named(
|
||||
&format!("json_property_{kind}_{position}"),
|
||||
&values,
|
||||
1000,
|
||||
runtime_property,
|
||||
native_property,
|
||||
|value| value.as_ref().map_or(0, |text| text.len() as u64),
|
||||
"rust_serde",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
// 两侧从已验证的原始值开始,独立参照直接序列化 Serde RawValue。
|
||||
// 此组只比较原文输出;构造与更新成本由现有 json 组同时检查。
|
||||
struct RawOutputCase {
|
||||
dever: Json,
|
||||
rust: Box<RawValue>,
|
||||
values: usize,
|
||||
depth: usize,
|
||||
}
|
||||
|
||||
fn runtime_raw_output(values: &[RawOutputCase]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| {
|
||||
Ok(value.dever.as_str().trim_ascii().to_owned())
|
||||
})
|
||||
}
|
||||
|
||||
fn native_raw_output(values: &[RawOutputCase]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| {
|
||||
serde_json::to_string(&value.rust).map_err(json_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn runtime_nested_output(values: &[RawOutputCase]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| {
|
||||
Json::array(std::slice::from_ref(&value.dever)).map(Json::into_text).map_err(json_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn native_nested_output(values: &[RawOutputCase]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| {
|
||||
if value.values + 1 > 65_536 || value.depth + 1 > 64 || value.rust.get().len() + 2 > 16 * 1024 * 1024 {
|
||||
return Err(json_fault("JSON budget"));
|
||||
}
|
||||
serde_json::to_string(&[&value.rust]).map_err(json_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn generated_nested_array_output(values: &[RawOutputCase]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| {
|
||||
GENERATED_ARRAY(std::slice::from_ref(&value.dever)).map_err(json_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn runtime_from_text(values: &[String]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| {
|
||||
Json::parse(value).map(Json::into_text).map_err(json_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn native_from_text(values: &[String]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| {
|
||||
serde_json::from_str::<Box<RawValue>>(value)
|
||||
.map(|raw| Box::<str>::from(raw).into()).map_err(json_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn runtime_from_text_nested(values: &[String]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| {
|
||||
let raw = Json::parse(value).map_err(json_fault)?;
|
||||
Json::array(&[raw]).map(Json::into_text).map_err(json_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn native_from_text_nested(values: &[String]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| {
|
||||
let raw: Box<RawValue> = serde_json::from_str(value).map_err(json_fault)?;
|
||||
serde_json::to_string(&[raw]).map_err(json_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn from_text_costs(name: &str, source: &str, values: &[RawOutputCase]) {
|
||||
let count = if name == "short" { 100_000 } else { 1000 };
|
||||
compare_named(
|
||||
&format!("json_nested_output_{name}"), values, count,
|
||||
runtime_nested_output, native_nested_output,
|
||||
|value| value.len() as u64, "rust_serde",
|
||||
);
|
||||
compare_named(
|
||||
&format!("json_dever_array_output_{name}"), values, count,
|
||||
generated_nested_array_output, native_nested_output,
|
||||
|value| value.len() as u64, "rust_serde",
|
||||
);
|
||||
// 原生参照仅检查 JSON 语法,不包含 Dever 的去重和预算,不能据此宣称合同追平。
|
||||
let sources = [source.to_owned()];
|
||||
compare_named(
|
||||
&format!("json_from_text_{name}"), &sources, count,
|
||||
runtime_from_text, native_from_text,
|
||||
|value| value.len() as u64, "rust_syntax_only",
|
||||
);
|
||||
compare_named(
|
||||
&format!("json_from_text_nested_{name}"), &sources, count,
|
||||
runtime_from_text_nested, native_from_text_nested,
|
||||
|value| value.len() as u64, "rust_syntax_only",
|
||||
);
|
||||
}
|
||||
|
||||
fn raw_output_costs() {
|
||||
nested_output_costs();
|
||||
array_output_costs();
|
||||
for (name, text) in [
|
||||
("short", "a".repeat(64)),
|
||||
("plain_4096", "a".repeat(4096)),
|
||||
("plain_65536", "a".repeat(65536)),
|
||||
("escaped", "雪\"\\\n".repeat(1024)),
|
||||
] {
|
||||
let source = format!(
|
||||
"{{\"text\":{},\"number\":1e9999}}",
|
||||
serde_json::to_string(&text).unwrap()
|
||||
);
|
||||
let values = [RawOutputCase {
|
||||
dever: Json::parse(&source).unwrap(),
|
||||
rust: serde_json::from_str(&source).unwrap(),
|
||||
// 上面构造的对象恰好包含 text、number 两个标量字段。
|
||||
values: 3,
|
||||
depth: 1,
|
||||
}];
|
||||
assert_eq!(runtime_raw_output(&values).unwrap(), source);
|
||||
assert_eq!(native_raw_output(&values).unwrap(), source);
|
||||
from_text_costs(name, &source, &values);
|
||||
compare_named(
|
||||
&format!("json_raw_output_{name}"),
|
||||
&values,
|
||||
1000,
|
||||
runtime_raw_output,
|
||||
native_raw_output,
|
||||
|value| value.len() as u64,
|
||||
"rust_serde",
|
||||
);
|
||||
// 输入解码已经取得 RawValue;输出不应丢掉它后再从 String 解析。
|
||||
let decoded = [RawOutputCase {
|
||||
dever: serde_json::from_str(&source).unwrap(),
|
||||
rust: serde_json::from_str(&source).unwrap(),
|
||||
values: 3,
|
||||
depth: 1,
|
||||
}];
|
||||
compare_named(
|
||||
&format!("json_decoded_output_{name}"),
|
||||
&decoded,
|
||||
1000,
|
||||
runtime_raw_output,
|
||||
native_raw_output,
|
||||
|value| value.len() as u64,
|
||||
"rust_serde",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct ArrayOutputCase {
|
||||
dever: Vec<Json>,
|
||||
rust: Vec<NativeArrayValue>,
|
||||
}
|
||||
|
||||
struct NativeArrayValue {
|
||||
raw: Box<RawValue>,
|
||||
values: usize,
|
||||
depth: usize,
|
||||
}
|
||||
|
||||
struct NativeArray<'value>(&'value [NativeArrayValue]);
|
||||
|
||||
impl Serialize for NativeArray<'_> {
|
||||
fn serialize<Encoder: Serializer>(&self, encoder: Encoder) -> Result<Encoder::Ok, Encoder::Error> {
|
||||
use dever_runtime::wire::serde::ser::{Error, SerializeSeq};
|
||||
let mut sequence = encoder.serialize_seq(Some(self.0.len()))?;
|
||||
let mut count = 1;
|
||||
let mut bytes = 2;
|
||||
for (index, value) in self.0.iter().enumerate() {
|
||||
count += value.values;
|
||||
bytes += value.raw.get().len() + usize::from(index != 0);
|
||||
if count > 65_536 || value.depth + 1 > 64 || bytes > 16 * 1024 * 1024 {
|
||||
return Err(Encoder::Error::custom("JSON budget"));
|
||||
}
|
||||
sequence.serialize_element(&value.raw)?;
|
||||
}
|
||||
sequence.end()
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_array_output(values: &[ArrayOutputCase]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| {
|
||||
Json::array(&value.dever).map(Json::into_text).map_err(json_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn native_array_output(values: &[ArrayOutputCase]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| {
|
||||
serde_json::to_string(&NativeArray(&value.rust)).map_err(json_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn generated_array_output(values: &[ArrayOutputCase]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| {
|
||||
GENERATED_ARRAY(&value.dever).map_err(json_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn array_output_costs() {
|
||||
for count in [0, 1, 4, 16] {
|
||||
let sources: Vec<_> = [" null ", " [1,-0] ", r#" {"雪":"\u96ea"} "#, " [] "]
|
||||
.into_iter().cycle().take(count).collect();
|
||||
// 两侧每个子值都已验证且保留结构统计;计时包含合并预算和完整写出。
|
||||
let values = [ArrayOutputCase {
|
||||
dever: sources.iter().map(|source| Json::parse(source).unwrap()).collect(),
|
||||
rust: sources.iter().map(|source| {
|
||||
let (values, depth) = reference_shape(&serde_json::from_str(source).unwrap());
|
||||
NativeArrayValue { raw: serde_json::from_str(source).unwrap(), values, depth }
|
||||
}).collect(),
|
||||
}];
|
||||
compare_named(&format!("json_array_mixed_{count}"), &values, 100_000,
|
||||
runtime_array_output, native_array_output, |value| value.len() as u64, "rust_bounded");
|
||||
compare_named(&format!("json_dever_array_mixed_{count}"), &values, 100_000,
|
||||
generated_array_output, native_array_output, |value| value.len() as u64, "rust_bounded");
|
||||
}
|
||||
}
|
||||
|
||||
struct NestedCase {
|
||||
dever: Json,
|
||||
rust: Box<RawValue>,
|
||||
values: usize,
|
||||
depth: usize,
|
||||
}
|
||||
|
||||
fn runtime_nested_record(values: &[NestedCase]) -> Result<String, Fault> {
|
||||
values.first().map_or(Ok(String::new()), |value| GENERATED_NESTED(&value.dever).map_err(json_fault))
|
||||
}
|
||||
|
||||
fn native_nested_record(values: &[NestedCase]) -> Result<String, Fault> {
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde")]
|
||||
struct Nested<'value> { value: &'value RawValue }
|
||||
let Some(value) = values.first() else { return Ok(String::new()); };
|
||||
if value.values + 1 > 65_536 || value.depth + 1 > 64 || value.rust.get().len() + 10 > 16 * 1024 * 1024 {
|
||||
return Err(json_fault("JSON budget"));
|
||||
}
|
||||
serde_json::to_string(&Nested { value: &value.rust }).map_err(json_fault)
|
||||
}
|
||||
|
||||
fn reference_shape(value: &serde_json::Value) -> (usize, usize) {
|
||||
let children: Vec<_> = match value {
|
||||
serde_json::Value::Array(values) => values.iter().map(reference_shape).collect(),
|
||||
serde_json::Value::Object(values) => values.values().map(reference_shape).collect(),
|
||||
_ => return (1, 0),
|
||||
};
|
||||
(1 + children.iter().map(|child| child.0).sum::<usize>(), 1 + children.iter().map(|child| child.1).max().unwrap_or(0))
|
||||
}
|
||||
|
||||
fn nested_output_costs() {
|
||||
for (name, text) in [
|
||||
("short", "a".repeat(64)),
|
||||
("plain_4096", "a".repeat(4096)),
|
||||
("plain_65536", "a".repeat(65536)),
|
||||
("escaped", "雪\"\\\n".repeat(1024)),
|
||||
] {
|
||||
let source = format!("{{\"text\":{},\"number\":-0}}", serde_json::to_string(&text).unwrap());
|
||||
// 两侧都从已验证原文及结构信息开始;独立 Rust 不调用 Dever 的验证或输出。
|
||||
let (count, depth) = reference_shape(&serde_json::from_str(&source).unwrap());
|
||||
let values = [NestedCase {
|
||||
dever: Json::parse(&source).unwrap(),
|
||||
rust: serde_json::from_str(&source).unwrap(), values: count, depth,
|
||||
}];
|
||||
compare_named(&format!("json_dever_nested_{name}"), &values,
|
||||
if name == "short" { 100_000 } else { 1000 },
|
||||
runtime_nested_record, native_nested_record, |value| value.len() as u64, "rust_bounded");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
//! 冻结原写出算法供前后对照;生产运行库不依赖此通用节点适配。
|
||||
use dever_runtime::wire::document::{MAX_BYTES, MAX_VALUES, WriteError};
|
||||
use dever_runtime::wire::serde_json;
|
||||
|
||||
pub enum Node<Text, Children, Members> {
|
||||
Null,
|
||||
Boolean(bool),
|
||||
Number(Text),
|
||||
String(Text),
|
||||
Array(Children),
|
||||
Object(Members),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Size {
|
||||
bytes: usize,
|
||||
values: usize,
|
||||
depth: usize,
|
||||
}
|
||||
|
||||
/// 节点直接按下标借用,编号和文档结构在 measure 中检查;不再给取节点包装错误通道。
|
||||
pub fn stringify_nodes<'node, Children, Members>(
|
||||
root: usize,
|
||||
count: usize,
|
||||
node: impl Fn(usize) -> Node<&'node str, Children, Members>,
|
||||
max_depth: usize,
|
||||
) -> Result<String, WriteError>
|
||||
where
|
||||
Children: Iterator<Item = i64>,
|
||||
Members: Iterator<Item = (&'node str, i64)>,
|
||||
{
|
||||
let bytes = measure(root, count, &node, max_depth)?;
|
||||
let mut output = String::with_capacity(bytes);
|
||||
write_node(&node, root, &mut output);
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn measure<'node, Children, Members>(
|
||||
root: usize,
|
||||
count: usize,
|
||||
node: &impl Fn(usize) -> Node<&'node str, Children, Members>,
|
||||
max_depth: usize,
|
||||
) -> Result<usize, WriteError>
|
||||
where
|
||||
Children: Iterator<Item = i64>,
|
||||
Members: Iterator<Item = (&'node str, i64)>,
|
||||
{
|
||||
let fail = |node, message: &str| WriteError {
|
||||
node,
|
||||
message: message.into(),
|
||||
};
|
||||
if count > MAX_VALUES {
|
||||
return Err(fail(root, "JSON node count exceeds 65536 values"));
|
||||
}
|
||||
if root >= count {
|
||||
return Err(fail(root, "JSON root does not refer to a document node"));
|
||||
}
|
||||
let mut sizes = Vec::<Size>::with_capacity(count);
|
||||
for index in 0..count {
|
||||
let mut size = Size {
|
||||
bytes: 0,
|
||||
values: 1,
|
||||
depth: 0,
|
||||
};
|
||||
match node(index) {
|
||||
Node::Null => size.bytes = 4,
|
||||
Node::Boolean(value) => size.bytes = if value { 4 } else { 5 },
|
||||
Node::Number(raw) => {
|
||||
if !matches!(raw.as_bytes().first(), Some(b'-' | b'0'..=b'9'))
|
||||
|| !serde_json::from_str::<&serde_json::value::RawValue>(raw)
|
||||
.is_ok_and(|value| value.get() == raw)
|
||||
{
|
||||
return Err(fail(index, "invalid JSON number"));
|
||||
}
|
||||
size.bytes = raw.len();
|
||||
}
|
||||
Node::String(text) => size.bytes = string_size(text),
|
||||
Node::Array(children) => {
|
||||
size.bytes = 2;
|
||||
size.depth = 1;
|
||||
for (position, child) in children.enumerate() {
|
||||
size.bytes += usize::from(position != 0);
|
||||
add_child(&mut size, child, &sizes, index, max_depth)?;
|
||||
}
|
||||
}
|
||||
Node::Object(fields) => {
|
||||
size.bytes = 2;
|
||||
size.depth = 1;
|
||||
for (position, (name, child)) in fields.enumerate() {
|
||||
size.bytes += string_size(name) + 1 + usize::from(position != 0);
|
||||
add_child(&mut size, child, &sizes, index, max_depth)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
check(size, index, max_depth)?;
|
||||
sizes.push(size);
|
||||
}
|
||||
Ok(sizes[root].bytes)
|
||||
}
|
||||
|
||||
fn add_child(
|
||||
size: &mut Size,
|
||||
child: i64,
|
||||
sizes: &[Size],
|
||||
node: usize,
|
||||
max_depth: usize,
|
||||
) -> Result<(), WriteError> {
|
||||
let child = usize::try_from(child).map_err(|_| WriteError {
|
||||
node,
|
||||
message: "negative JSON node ID".into(),
|
||||
})?;
|
||||
let child = sizes.get(child).ok_or_else(|| WriteError {
|
||||
node,
|
||||
message: "JSON child must precede parent".into(),
|
||||
})?;
|
||||
size.bytes += child.bytes;
|
||||
size.values += child.values;
|
||||
size.depth = size.depth.max(child.depth + 1);
|
||||
check(*size, node, max_depth)
|
||||
}
|
||||
|
||||
fn check(size: Size, node: usize, max_depth: usize) -> Result<(), WriteError> {
|
||||
let message = if size.bytes > MAX_BYTES {
|
||||
"JSON exceeds 16 MiB"
|
||||
} else if size.values > MAX_VALUES {
|
||||
"JSON contains more than 65536 values"
|
||||
} else if size.depth > max_depth {
|
||||
"JSON nesting exceeds configured limit"
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
Err(WriteError {
|
||||
node,
|
||||
message: message.into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn string_size(text: &str) -> usize {
|
||||
text.len()
|
||||
+ 2
|
||||
+ text
|
||||
.bytes()
|
||||
.map(|byte| match byte {
|
||||
b'"' | b'\\' | 8 | 12 | b'\n' | b'\r' | b'\t' => 1,
|
||||
0..=31 => 5,
|
||||
_ => 0,
|
||||
})
|
||||
.sum::<usize>()
|
||||
}
|
||||
|
||||
fn write_node<'node, Children, Members>(
|
||||
node: &impl Fn(usize) -> Node<&'node str, Children, Members>,
|
||||
index: usize,
|
||||
output: &mut String,
|
||||
) where
|
||||
Children: Iterator<Item = i64>,
|
||||
Members: Iterator<Item = (&'node str, i64)>,
|
||||
{
|
||||
match node(index) {
|
||||
Node::Null => output.push_str("null"),
|
||||
Node::Boolean(value) => output.push_str(if value { "true" } else { "false" }),
|
||||
Node::Number(raw) => output.push_str(raw),
|
||||
Node::String(text) => write_string(text, output),
|
||||
Node::Array(children) => {
|
||||
output.push('[');
|
||||
for (position, child) in children.enumerate() {
|
||||
if position != 0 {
|
||||
output.push(',');
|
||||
}
|
||||
write_node(node, child as usize, output);
|
||||
}
|
||||
output.push(']');
|
||||
}
|
||||
Node::Object(fields) => {
|
||||
output.push('{');
|
||||
for (position, (name, child)) in fields.enumerate() {
|
||||
if position != 0 {
|
||||
output.push(',');
|
||||
}
|
||||
write_string(name, output);
|
||||
output.push(':');
|
||||
write_node(node, child as usize, output);
|
||||
}
|
||||
output.push('}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_string(text: &str, output: &mut String) {
|
||||
output.push('"');
|
||||
for character in text.chars() {
|
||||
match character {
|
||||
'"' => output.push_str("\\\""),
|
||||
'\\' => output.push_str("\\\\"),
|
||||
'\u{08}' => output.push_str("\\b"),
|
||||
'\u{0c}' => output.push_str("\\f"),
|
||||
'\n' => output.push_str("\\n"),
|
||||
'\r' => output.push_str("\\r"),
|
||||
'\t' => output.push_str("\\t"),
|
||||
character if character < '\u{20}' => {
|
||||
use std::fmt::Write;
|
||||
write!(output, "\\u{:04x}", character as u32).unwrap();
|
||||
}
|
||||
character => output.push(character),
|
||||
}
|
||||
}
|
||||
output.push('"');
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use std::hint::black_box;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[inline(never)]
|
||||
fn generated_list(values: &[i64]) -> Result<i64, Fault> {
|
||||
GENERATED_LIST(values)
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn reference_list(values: &[i64]) -> Result<i64, Fault> {
|
||||
let failure = &FaultInfo { source: "reference", line: 1, column: 1, message: "integer overflow" };
|
||||
values.iter().try_fold(0i64, |sum, value| sum.checked_add(*value)).ok_or(failure)
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn measure(operation: fn(&[i64]) -> Result<i64, Fault>, values: &[i64], count: usize) -> (Duration, usize) {
|
||||
let operation = black_box(operation);
|
||||
let start = Instant::now();
|
||||
let mut checksum = 0usize;
|
||||
for _ in 0..count {
|
||||
checksum = checksum.wrapping_add(black_box(operation(black_box(values)).unwrap()) as usize);
|
||||
}
|
||||
(start.elapsed(), checksum)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let values: Vec<i64> = (0..1024).map(|position| (position * 37 % 100) as i64).collect();
|
||||
for values in [values.as_slice(), &[], &[i64::MAX, 1], &[i64::MIN, -1], &[i64::MAX, -1]] {
|
||||
assert_eq!(generated_list(values).map_err(|_| ()), reference_list(values).map_err(|_| ()));
|
||||
}
|
||||
let expected = generated_list(&values).unwrap() as usize * 100_000;
|
||||
measure(generated_list, &values, 10_000);
|
||||
measure(reference_list, &values, 10_000);
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
for iteration in 0..21 {
|
||||
for control in if iteration % 2 == 0 { [false, true] } else { [true, false] } {
|
||||
let case = if control { "list_same_function" } else { "list_shared_caller" };
|
||||
let mut results = Vec::new();
|
||||
for first in if iteration % 2 == 0 { [true, false] } else { [false, true] } {
|
||||
let operation = if first || control { generated_list } else { reference_list };
|
||||
let (elapsed, checksum) = measure(operation, &values, 100_000);
|
||||
assert_eq!(checksum, expected);
|
||||
let implementation = if first { "dever" } else if control { "same" } else { "rust" };
|
||||
results.push((implementation, elapsed, checksum));
|
||||
}
|
||||
for (implementation, elapsed, checksum) in results {
|
||||
println!("{case},{iteration},{implementation},{},{checksum}", elapsed.as_nanos());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
use indexmap::IndexMap;
|
||||
|
||||
fn map_fault() -> Fault {
|
||||
&FaultInfo {
|
||||
message: "integer overflow",
|
||||
source: "map-reference",
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn add(total: i64, value: i64) -> Result<i64, Fault> {
|
||||
total.checked_add(value).ok_or_else(map_fault)
|
||||
}
|
||||
|
||||
fn build_text(values: &[String]) -> IndexMap<String, String> {
|
||||
let mut map = IndexMap::with_capacity(values.len());
|
||||
for value in values {
|
||||
map.insert(value.clone(), value.clone());
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
fn build_nested(values: &[String]) -> IndexMap<String, Vec<String>> {
|
||||
let mut map = IndexMap::with_capacity(values.len());
|
||||
for value in values {
|
||||
map.insert(value.clone(), vec![value.clone(), value.clone()]);
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
fn text_weight(key: &str, value: &str) -> Result<i64, Fault> {
|
||||
add(key.chars().count() as i64, value.chars().count() as i64)
|
||||
}
|
||||
|
||||
fn nested_weight(key: &str, values: &[String]) -> Result<i64, Fault> {
|
||||
let count = values
|
||||
.iter()
|
||||
.try_fold(0, |total, value| add(total, value.chars().count() as i64))?;
|
||||
add(key.chars().count() as i64, count)
|
||||
}
|
||||
|
||||
fn owned_text(values: &[String]) -> Result<i64, Fault> {
|
||||
build_text(values)
|
||||
.into_iter()
|
||||
.try_fold(0, |total, (key, value)| {
|
||||
add(total, text_weight(&key, &value)?)
|
||||
})
|
||||
}
|
||||
|
||||
fn owned_nested(values: &[String]) -> Result<i64, Fault> {
|
||||
build_nested(values)
|
||||
.into_iter()
|
||||
.try_fold(0, |total, (key, value)| {
|
||||
add(total, nested_weight(&key, &value)?)
|
||||
})
|
||||
}
|
||||
|
||||
struct Borrowed {
|
||||
text: Map<String, String>,
|
||||
nested: Map<String, Vec<String>>,
|
||||
reference_text: IndexMap<String, String>,
|
||||
reference_nested: IndexMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
fn borrowed_text(values: &[Borrowed]) -> Result<i64, Fault> {
|
||||
values
|
||||
.first()
|
||||
.map_or(Ok(0), |value| GENERATED_BORROWED_TEXT(&value.text))
|
||||
}
|
||||
|
||||
fn borrowed_nested(values: &[Borrowed]) -> Result<i64, Fault> {
|
||||
values
|
||||
.first()
|
||||
.map_or(Ok(0), |value| GENERATED_BORROWED_NESTED(&value.nested))
|
||||
}
|
||||
|
||||
fn reference_borrowed_text(values: &[Borrowed]) -> Result<i64, Fault> {
|
||||
values.first().map_or(Ok(0), |value| {
|
||||
value
|
||||
.reference_text
|
||||
.iter()
|
||||
.try_fold(0, |total, (key, value)| {
|
||||
add(total, text_weight(key, value)?)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 原始参照合并了两个错误来源;分别保留故障位置,并用同一张表排除输入地址差异。
|
||||
fn reference_text_same_faults<const SHARED: bool>(values: &[Borrowed]) -> Result<i64, Fault> {
|
||||
values.first().map_or(Ok(0), |value| {
|
||||
let mut entries = if SHARED {
|
||||
value.text.iter()
|
||||
} else {
|
||||
value.reference_text.iter()
|
||||
};
|
||||
entries.try_fold(0i64, |total, (key, value)| {
|
||||
let count = (key.chars().count() as i64)
|
||||
.checked_add(value.chars().count() as i64)
|
||||
.ok_or(const {
|
||||
&FaultInfo {
|
||||
message: "integer arithmetic fault",
|
||||
source: "map-reference",
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
})?;
|
||||
total.checked_add(count).ok_or(const {
|
||||
&FaultInfo {
|
||||
message: "integer overflow",
|
||||
source: "map-reference",
|
||||
line: 2,
|
||||
column: 1,
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn reference_borrowed_nested(values: &[Borrowed]) -> Result<i64, Fault> {
|
||||
values.first().map_or(Ok(0), |value| {
|
||||
value
|
||||
.reference_nested
|
||||
.iter()
|
||||
.try_fold(0, |total, (key, value)| {
|
||||
add(total, nested_weight(key, value)?)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn reference_nested_same_order(values: &[Borrowed]) -> Result<i64, Fault> {
|
||||
values.first().map_or(Ok(0), |value| {
|
||||
value
|
||||
.reference_nested
|
||||
.iter()
|
||||
.try_fold(0, |total, (key, values)| {
|
||||
let key_count = key.chars().count() as i64;
|
||||
let count = values
|
||||
.iter()
|
||||
.try_fold(0, |total, value| add(total, value.chars().count() as i64))?;
|
||||
add(total, add(key_count, count)?)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn reference_nested_same_faults<const SHARED: bool>(values: &[Borrowed]) -> Result<i64, Fault> {
|
||||
values.first().map_or(Ok(0), |value| {
|
||||
let mut entries = if SHARED {
|
||||
value.nested.iter()
|
||||
} else {
|
||||
value.reference_nested.iter()
|
||||
};
|
||||
entries.try_fold(0i64, |total, (key, values)| {
|
||||
let key_count = key.chars().count() as i64;
|
||||
let count = values.iter().try_fold(0i64, |total, value| {
|
||||
total.checked_add(value.chars().count() as i64).ok_or(
|
||||
const {
|
||||
&FaultInfo {
|
||||
message: "integer overflow",
|
||||
source: "map-reference",
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
},
|
||||
)
|
||||
})?;
|
||||
let count = key_count.checked_add(count).ok_or(
|
||||
const {
|
||||
&FaultInfo {
|
||||
message: "integer arithmetic fault",
|
||||
source: "map-reference",
|
||||
line: 2,
|
||||
column: 1,
|
||||
}
|
||||
},
|
||||
)?;
|
||||
total.checked_add(count).ok_or(
|
||||
const {
|
||||
&FaultInfo {
|
||||
message: "integer overflow",
|
||||
source: "map-reference",
|
||||
line: 3,
|
||||
column: 1,
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn list_first(values: &[String]) -> Result<Option<String>, Fault> {
|
||||
values
|
||||
.first()
|
||||
.map_or(Ok(None), |value| GENERATED_LIST_FIRST(value))
|
||||
}
|
||||
|
||||
// 两个实现逐个读取同一批表,避免把某一份输入的分配位置固定给一个标签。
|
||||
fn nested_corpus<const GENERATED: bool>(values: &[Borrowed]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
let input = std::slice::from_ref(value);
|
||||
let count = if GENERATED {
|
||||
borrowed_nested(input)?
|
||||
} else {
|
||||
reference_nested_same_faults::<true>(input)?
|
||||
};
|
||||
total = add(total, count)?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
// 已知字面量的选取等价于复制输入,独立 Rust 直接实现这一结果。
|
||||
fn reference_owned_text(values: &[String]) -> Result<Option<String>, Fault> {
|
||||
values.first().map_or(Ok(None), |value| {
|
||||
let value = value.as_str();
|
||||
Ok(Some(value.to_owned()))
|
||||
})
|
||||
}
|
||||
|
||||
fn map_get(values: &[String]) -> Result<Option<String>, Fault> {
|
||||
values
|
||||
.first()
|
||||
.map_or(Ok(None), |value| GENERATED_MAP_GET(value))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
let reference_first = std::env::var_os("DEVER_MAP_REFERENCE_FIRST").is_some();
|
||||
for size in [32, 4096] {
|
||||
let values: Vec<String> = (0..64)
|
||||
.map(|index| format!("{index}:{}", "雪".repeat(size / 3)))
|
||||
.collect();
|
||||
// 两种建表顺序都计时;同表参照另行保留,排除独立分配地址的影响。
|
||||
let (text, nested, reference_text, reference_nested) = if reference_first {
|
||||
let reference_text = build_text(&values);
|
||||
let reference_nested = build_nested(&values);
|
||||
(GENERATED_BUILD_TEXT(&values).unwrap(), GENERATED_BUILD_NESTED(&values).unwrap(), reference_text, reference_nested)
|
||||
} else {
|
||||
(GENERATED_BUILD_TEXT(&values).unwrap(), GENERATED_BUILD_NESTED(&values).unwrap(), build_text(&values), build_nested(&values))
|
||||
};
|
||||
let maps = [Borrowed { text, nested, reference_text, reference_nested }];
|
||||
selection_comparisons(size);
|
||||
compare(
|
||||
&format!("map_text_owned_{size}"),
|
||||
&values,
|
||||
200,
|
||||
GENERATED_OWNED_TEXT,
|
||||
owned_text,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
&format!("map_text_borrowed_{size}"),
|
||||
&maps,
|
||||
200,
|
||||
borrowed_text,
|
||||
reference_borrowed_text,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
&format!("map_text_same_faults_{size}"),
|
||||
&maps,
|
||||
200,
|
||||
borrowed_text,
|
||||
reference_text_same_faults::<false>,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
&format!("map_text_same_table_{size}"),
|
||||
&maps,
|
||||
200,
|
||||
borrowed_text,
|
||||
reference_text_same_faults::<true>,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
&format!("map_nested_owned_{size}"),
|
||||
&values,
|
||||
200,
|
||||
GENERATED_OWNED_NESTED,
|
||||
owned_nested,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
&format!("map_nested_borrowed_{size}"),
|
||||
&maps,
|
||||
200,
|
||||
borrowed_nested,
|
||||
reference_borrowed_nested,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
&format!("list_first_owned_{size}"),
|
||||
&values,
|
||||
2000,
|
||||
list_first,
|
||||
reference_owned_text,
|
||||
|value| value.as_ref().map_or(0, |text| text.len() as u64),
|
||||
);
|
||||
compare(
|
||||
&format!("map_get_owned_{size}"),
|
||||
&values,
|
||||
2000,
|
||||
map_get,
|
||||
reference_owned_text,
|
||||
|value| value.as_ref().map_or(0, |text| text.len() as u64),
|
||||
);
|
||||
compare(
|
||||
&format!("map_nested_same_order_{size}"),
|
||||
&maps,
|
||||
200,
|
||||
borrowed_nested,
|
||||
reference_nested_same_order,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
&format!("map_nested_same_faults_{size}"),
|
||||
&maps,
|
||||
200,
|
||||
borrowed_nested,
|
||||
reference_nested_same_faults::<false>,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
&format!("map_nested_same_table_{size}"),
|
||||
&maps,
|
||||
200,
|
||||
borrowed_nested,
|
||||
reference_nested_same_faults::<true>,
|
||||
|value| *value as u64,
|
||||
);
|
||||
let mut corpus: Vec<_> = (0..4)
|
||||
.map(|offset| {
|
||||
let mut input = values.clone();
|
||||
input.rotate_left(offset * 13);
|
||||
Borrowed {
|
||||
text: Map::with_capacity(0),
|
||||
nested: GENERATED_BUILD_NESTED(&input).unwrap(),
|
||||
reference_text: IndexMap::new(),
|
||||
reference_nested: IndexMap::new(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
for reverse in [false, true] {
|
||||
if reverse { corpus.reverse(); }
|
||||
compare(
|
||||
&format!("map_nested_corpus_{size}_{}", if reverse { "reverse" } else { "forward" }),
|
||||
&corpus,
|
||||
50,
|
||||
nested_corpus::<true>,
|
||||
nested_corpus::<false>,
|
||||
|value| *value as u64,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// 独立 Rust 直接遍历 IndexMap;只复制命中项,保持结果顺序和拥有值语义。
|
||||
struct Selection {
|
||||
generated: Map<String, String>,
|
||||
reference: IndexMap<String, String>,
|
||||
target: String,
|
||||
}
|
||||
|
||||
fn selected_generated(input: &Selection) -> Result<Vec<(String, String)>, Fault> {
|
||||
GENERATED_MAP_FILTER(&input.generated, &input.target)
|
||||
}
|
||||
|
||||
fn selected_reference(input: &Selection) -> Result<Vec<(String, String)>, Fault> {
|
||||
let mut output = Vec::new();
|
||||
for (key, value) in &input.reference {
|
||||
if *value == input.target {
|
||||
output.push((key.clone(), value.clone()));
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn found_generated(input: &Selection) -> Result<Option<(String, String)>, Fault> {
|
||||
GENERATED_MAP_FIND(&input.generated, &input.target)
|
||||
}
|
||||
|
||||
fn found_reference(input: &Selection) -> Result<Option<(String, String)>, Fault> {
|
||||
for (key, value) in &input.reference {
|
||||
if *value == input.target {
|
||||
return Ok(Some((key.clone(), value.clone())));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// 使用相同物理表与参数类型,单独检查生成层;独立 IndexMap 参照仍保留。
|
||||
fn selected_same_table(input: &Selection) -> Result<Vec<(String, String)>, Fault> {
|
||||
let mut output = Vec::new();
|
||||
let target = input.target.as_str();
|
||||
for (key, value) in input.generated.iter() {
|
||||
if value == target {
|
||||
output.push((key.clone(), value.clone()));
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn found_same_table(input: &Selection) -> Result<Option<(String, String)>, Fault> {
|
||||
let target = input.target.as_str();
|
||||
for (key, value) in input.generated.iter() {
|
||||
if value == target {
|
||||
return Ok(Some((key.clone(), value.clone())));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn selection_comparisons(size: usize) {
|
||||
let reference_first = std::env::var_os("DEVER_MAP_REFERENCE_FIRST").is_some();
|
||||
for mode in ["last", "first", "missing", "all"] {
|
||||
let suffix = "雪".repeat(size / 3);
|
||||
let target = format!(
|
||||
"{}:{suffix}",
|
||||
match mode {
|
||||
"last" => 63,
|
||||
"missing" => 64,
|
||||
_ => 0,
|
||||
}
|
||||
);
|
||||
let (mut generated, mut reference) = if reference_first {
|
||||
let reference = IndexMap::with_capacity(64);
|
||||
(Map::with_capacity(64), reference)
|
||||
} else {
|
||||
(Map::with_capacity(64), IndexMap::with_capacity(64))
|
||||
};
|
||||
for index in 0..64 {
|
||||
let key = format!("{index}:{suffix}");
|
||||
let value = if mode == "all" {
|
||||
target.clone()
|
||||
} else {
|
||||
key.clone()
|
||||
};
|
||||
if reference_first {
|
||||
reference.insert(key.clone(), value.clone());
|
||||
generated.insert(key, value);
|
||||
} else {
|
||||
generated.insert(key.clone(), value.clone());
|
||||
reference.insert(key, value);
|
||||
}
|
||||
}
|
||||
let values = Selection {
|
||||
generated,
|
||||
reference,
|
||||
target,
|
||||
};
|
||||
// 直接传入表和目标,空表断言复用同一入口,不额外包单元素数组。
|
||||
let empty = Selection {
|
||||
generated: Map::with_capacity(0),
|
||||
reference: IndexMap::new(),
|
||||
target: String::new(),
|
||||
};
|
||||
if mode != "first" {
|
||||
for (suffix, reference) in [
|
||||
("", selected_reference as fn(&Selection) -> _),
|
||||
("_same_table", selected_same_table as fn(&Selection) -> _),
|
||||
] {
|
||||
compare_output::<false, _, _>(
|
||||
&format!("map_filter_{mode}_{size}{suffix}"),
|
||||
&values,
|
||||
&empty,
|
||||
500,
|
||||
selected_generated,
|
||||
reference,
|
||||
|entries| {
|
||||
entries
|
||||
.iter()
|
||||
.map(|(key, value)| (key.len() + value.len()) as u64)
|
||||
.sum()
|
||||
},
|
||||
"rust",
|
||||
);
|
||||
}
|
||||
}
|
||||
if mode != "all" {
|
||||
for (suffix, reference) in [
|
||||
("", found_reference as fn(&Selection) -> _),
|
||||
("_same_table", found_same_table as fn(&Selection) -> _),
|
||||
] {
|
||||
compare_output::<false, _, _>(
|
||||
&format!("map_find_{mode}_{size}{suffix}"),
|
||||
&values,
|
||||
&empty,
|
||||
500,
|
||||
found_generated,
|
||||
reference,
|
||||
|entry| {
|
||||
entry
|
||||
.as_ref()
|
||||
.map_or(0, |(key, value)| (key.len() + value.len()) as u64)
|
||||
},
|
||||
"rust",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* 校准探针:正常释放、失败 realloc、零大小释放和已知的 37 字节残留。 */
|
||||
static void *volatile retained;
|
||||
static pthread_barrier_t start;
|
||||
|
||||
static void exercise(void) {
|
||||
for (int index = 0; index < 1000; index++) {
|
||||
void *first = malloc(10);
|
||||
assert(first);
|
||||
first = realloc(first, 20);
|
||||
assert(first);
|
||||
void *second = calloc(2, 16);
|
||||
assert(second);
|
||||
volatile size_t impossible = SIZE_MAX;
|
||||
assert(realloc(first, impossible) == NULL);
|
||||
free(second);
|
||||
assert(realloc(first, 0) == NULL);
|
||||
void *aligned = NULL;
|
||||
assert(posix_memalign(&aligned, 64, 96) == 0);
|
||||
assert((uintptr_t)aligned % 64 == 0);
|
||||
void *original = aligned;
|
||||
errno = E2BIG;
|
||||
assert(posix_memalign(&aligned, 3, 96) == EINVAL);
|
||||
assert(aligned == original && errno == E2BIG);
|
||||
assert(posix_memalign(&aligned, 64, impossible) == ENOMEM);
|
||||
assert(aligned == original && errno == E2BIG);
|
||||
free(aligned);
|
||||
}
|
||||
}
|
||||
|
||||
static void *worker(void *input) {
|
||||
pthread_barrier_wait(&start);
|
||||
free(input);
|
||||
exercise();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(int count, char **arguments) {
|
||||
int concurrent = count > 1 && strcmp(arguments[1], "threads") == 0;
|
||||
if (concurrent) {
|
||||
pthread_t workers[4];
|
||||
assert(pthread_barrier_init(&start, NULL, 5) == 0);
|
||||
for (size_t index = 0; index < 4; index++) {
|
||||
void *input = malloc(16);
|
||||
assert(input);
|
||||
assert(pthread_create(&workers[index], NULL, worker, input) == 0);
|
||||
}
|
||||
pthread_barrier_wait(&start);
|
||||
for (size_t index = 0; index < 4; index++) {
|
||||
assert(pthread_join(workers[index], NULL) == 0);
|
||||
}
|
||||
assert(pthread_barrier_destroy(&start) == 0);
|
||||
} else {
|
||||
exercise();
|
||||
}
|
||||
if (count > (concurrent ? 2 : 1)) retained = malloc(37);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* glibc 验收探针,不进入运行库;插桩耗时与 RSS 不用于性能比较。
|
||||
* 只跟踪这些入口实际返回的请求字节,忽略 loader 内部产生的未知指针。
|
||||
* 固定表不调用分配器,表满直接使验收失败,不能悄悄漏记。 */
|
||||
extern void *__libc_malloc(size_t size);
|
||||
extern void *__libc_calloc(size_t count, size_t size);
|
||||
extern void *__libc_realloc(void *pointer, size_t size);
|
||||
extern void __libc_free(void *pointer);
|
||||
extern void *__libc_memalign(size_t alignment, size_t size);
|
||||
|
||||
#define CAPACITY 131072
|
||||
#define DELETED ((uintptr_t)1)
|
||||
|
||||
static struct { uintptr_t address; size_t size; } entries[CAPACITY];
|
||||
static size_t allocations, releases, allocated_bytes, released_bytes, live_bytes, peak_bytes;
|
||||
static int recording = 1;
|
||||
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static size_t slot(uintptr_t address, int insert) {
|
||||
size_t first_deleted = CAPACITY;
|
||||
size_t start = (address >> 4) % CAPACITY;
|
||||
for (size_t step = 0; step < CAPACITY; step++) {
|
||||
size_t index = (start + step) % CAPACITY;
|
||||
if (entries[index].address == address) return index;
|
||||
if (entries[index].address == DELETED && first_deleted == CAPACITY) first_deleted = index;
|
||||
if (!entries[index].address) return insert && first_deleted != CAPACITY ? first_deleted : index;
|
||||
}
|
||||
if (insert && first_deleted != CAPACITY) return first_deleted;
|
||||
abort();
|
||||
}
|
||||
|
||||
static void added(void *pointer, size_t size) {
|
||||
if (!recording || !pointer) return;
|
||||
uintptr_t address = (uintptr_t)pointer;
|
||||
size_t index = slot(address, 1);
|
||||
if (entries[index].address == address) abort();
|
||||
entries[index].address = address;
|
||||
entries[index].size = size;
|
||||
allocations++;
|
||||
allocated_bytes += size;
|
||||
live_bytes += size;
|
||||
if (live_bytes > peak_bytes) peak_bytes = live_bytes;
|
||||
}
|
||||
|
||||
static void removed(uintptr_t address) {
|
||||
if (!recording || !address) return;
|
||||
size_t index = slot(address, 0);
|
||||
if (entries[index].address != address) return;
|
||||
releases++;
|
||||
released_bytes += entries[index].size;
|
||||
live_bytes -= entries[index].size;
|
||||
entries[index].address = DELETED;
|
||||
}
|
||||
|
||||
void *malloc(size_t size) {
|
||||
pthread_mutex_lock(&lock);
|
||||
void *pointer = __libc_malloc(size);
|
||||
added(pointer, size);
|
||||
pthread_mutex_unlock(&lock);
|
||||
return pointer;
|
||||
}
|
||||
|
||||
void *calloc(size_t count, size_t size) {
|
||||
pthread_mutex_lock(&lock);
|
||||
void *pointer = __libc_calloc(count, size);
|
||||
if (pointer) added(pointer, count * size);
|
||||
pthread_mutex_unlock(&lock);
|
||||
return pointer;
|
||||
}
|
||||
|
||||
void *realloc(void *pointer, size_t size) {
|
||||
pthread_mutex_lock(&lock);
|
||||
uintptr_t address = (uintptr_t)pointer;
|
||||
void *result = __libc_realloc(pointer, size);
|
||||
/* 失败时旧块仍存活;glibc 的零大小 realloc 则释放旧块。 */
|
||||
if (result || size == 0) {
|
||||
removed(address);
|
||||
added(result, size);
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
int posix_memalign(void **result, size_t alignment, size_t size) {
|
||||
if (alignment < sizeof(void *) || (alignment & (alignment - 1))) return EINVAL;
|
||||
int previous_errno = errno;
|
||||
pthread_mutex_lock(&lock);
|
||||
void *pointer = __libc_memalign(alignment, size);
|
||||
if (pointer) {
|
||||
*result = pointer;
|
||||
added(pointer, size);
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
errno = previous_errno;
|
||||
return pointer ? 0 : ENOMEM;
|
||||
}
|
||||
|
||||
void free(void *pointer) {
|
||||
pthread_mutex_lock(&lock);
|
||||
removed((uintptr_t)pointer);
|
||||
__libc_free(pointer);
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
__attribute__((destructor)) static void report(void) {
|
||||
pthread_mutex_lock(&lock);
|
||||
recording = 0;
|
||||
pthread_mutex_unlock(&lock);
|
||||
fprintf(stderr, "heap_stats,%zu,%zu,%zu,%zu,%zu,%zu\n",
|
||||
allocations, releases, allocated_bytes, released_bytes, live_bytes, peak_bytes);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 只编入 --memory 产物,复用原输入与两侧函数;普通计时产物不改变。
|
||||
// 全进程初始化和输入也会被探针计数,零次调用作为相同场景的基线。
|
||||
fn compare_output<const BORROW: bool, Input: ?Sized, Output: PartialEq + std::fmt::Debug>(name: &str, values: &Input, _: &Input, _: usize, generated: fn(&Input) -> Result<Output, Fault>, reference: fn(&Input) -> Result<Output, Fault>, score: fn(&Output) -> u64, reference_name: &str) {
|
||||
let selected = std::env::var("DEVER_HEAP_CASE").expect("DEVER_HEAP_CASE");
|
||||
if selected != name { return; }
|
||||
let implementation = std::env::var("DEVER_HEAP_IMPL").expect("DEVER_HEAP_IMPL");
|
||||
let count = std::env::var("DEVER_HEAP_COUNT").expect("DEVER_HEAP_COUNT").parse::<usize>().unwrap();
|
||||
let operation = if implementation == "dever" { generated } else {
|
||||
assert_eq!(implementation, reference_name);
|
||||
reference
|
||||
};
|
||||
let operation = black_box(operation);
|
||||
let mut checksum = 0u64;
|
||||
for _ in 0..count {
|
||||
let value = black_box(operation(black_box(values)).unwrap());
|
||||
checksum = checksum.wrapping_add(black_box(score(&value)));
|
||||
drop(value);
|
||||
}
|
||||
println!("heap,{name},{implementation},{count},{checksum}");
|
||||
// RSS 单独在未加载分配探针的进程读取,避免把探针静态表算进语言成本。
|
||||
if std::env::var_os("DEVER_HEAP_RSS").is_some() {
|
||||
let status = std::fs::read_to_string("/proc/self/status").unwrap();
|
||||
for line in status.lines().filter(|line| line.starts_with("VmRSS:") || line.starts_with("VmHWM:")) {
|
||||
println!("heap_rss,{line}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// 独立 Rust 参照直接使用相同版本的 dec,逐项清理和检查状态。
|
||||
// 两侧使用同一输入、Decimal128 精度、舍入和顺序,不用二进制浮点代替。
|
||||
|
||||
fn generated(values: &[Decimal]) -> Result<Decimal, Fault> {
|
||||
GENERATED_DECIMAL_SUM(values)
|
||||
}
|
||||
|
||||
fn reference(values: &[Decimal]) -> Result<Decimal, Fault> {
|
||||
let mut context = dec::Context::<dec::Decimal128>::default();
|
||||
let mut total = dec::Decimal128::ZERO;
|
||||
for value in values {
|
||||
context.clear_status();
|
||||
total = context.add(total, dec::Decimal128::from_be_bytes(value.to_be_bytes()));
|
||||
if !total.is_finite() || context.status().overflow() || context.status().invalid_operation()
|
||||
{
|
||||
return Err(&FaultInfo {
|
||||
message: "Decimal fault",
|
||||
source: "Rust reference",
|
||||
line: 0,
|
||||
column: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(Decimal::from_be_bytes(total.to_be_bytes()))
|
||||
}
|
||||
|
||||
fn score(value: &Decimal) -> u64 {
|
||||
u64::from_be_bytes(value.to_be_bytes()[8..].try_into().unwrap())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let maximum = Decimal::parse("9.999999999999999999999999999999999E+6144").unwrap();
|
||||
let zero = Decimal::from_int(0);
|
||||
for values in [
|
||||
vec![maximum, maximum, -maximum],
|
||||
vec![-maximum, -maximum, maximum],
|
||||
vec![zero],
|
||||
Vec::new(),
|
||||
] {
|
||||
let actual = generated(&values);
|
||||
let expected = reference(&values);
|
||||
assert_eq!(actual.is_err(), expected.is_err());
|
||||
if let (Ok(actual), Ok(expected)) = (actual, expected) {
|
||||
assert_eq!(actual.to_be_bytes(), expected.to_be_bytes());
|
||||
}
|
||||
}
|
||||
let short = [
|
||||
"0.1", "0.2", "-0.05", "1.25", "-0.00", "10", "-2.50", "0.001",
|
||||
]
|
||||
.map(|text| Decimal::parse(text).unwrap());
|
||||
let integers = (0..256).map(Decimal::from_int).collect::<Vec<_>>();
|
||||
let palette = [
|
||||
"-1234567890123456789012345678901234",
|
||||
"1.000000000000000000000000000000001",
|
||||
"-0.001",
|
||||
"-1E-6176",
|
||||
"-0.00",
|
||||
"0E+100",
|
||||
"1E-6176",
|
||||
"0.9999999999999999999999999999999999",
|
||||
"1234567890123456789012345678901234",
|
||||
]
|
||||
.map(|text| Decimal::parse(text).unwrap());
|
||||
let mixed = (0..256)
|
||||
.map(|index| palette[index % palette.len()])
|
||||
.collect::<Vec<_>>();
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
for (name, values, count) in [
|
||||
("decimal_sum_short", short.as_slice(), 10000),
|
||||
("decimal_sum_integers", integers.as_slice(), 1000),
|
||||
("decimal_sum_mixed", mixed.as_slice(), 1000),
|
||||
] {
|
||||
assert_eq!(
|
||||
generated(values).unwrap().to_be_bytes(),
|
||||
reference(values).unwrap().to_be_bytes()
|
||||
);
|
||||
compare(name, values, count, generated, reference, score);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// 保留直接 Rust 参照,分别观察值参数、借用参数和 Result 对循环优化的影响。
|
||||
|
||||
fn generated(values: &[Option<f64>]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
total += GENERATED_FLOAT(value)?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn reference(values: &[Option<f64>]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
total += match value {
|
||||
None => -1,
|
||||
Some(value) if *value == 1.0 => 1,
|
||||
Some(value) if *value == 0.0 => 0,
|
||||
Some(_) => 2,
|
||||
};
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn generated_batch(values: &[Option<f64>]) -> Result<i64, Fault> {
|
||||
GENERATED_FLOAT_TOTAL(values)
|
||||
}
|
||||
|
||||
fn reference_batch(values: &[Option<f64>]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
let projected = match value {
|
||||
None => -1,
|
||||
Some(value) if *value == 1.0 => 1,
|
||||
Some(value) if *value == 0.0 => 0,
|
||||
Some(_) => 2,
|
||||
};
|
||||
total = total.checked_add(projected).ok_or(&FaultInfo {
|
||||
message: "integer overflow",
|
||||
source: "Rust reference",
|
||||
line: 0,
|
||||
column: 0,
|
||||
})?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn classify(value: Option<f64>) -> i64 {
|
||||
match value {
|
||||
None => -1,
|
||||
Some(value) if value == 1.0 => 1,
|
||||
Some(value) if value == 0.0 => 0,
|
||||
Some(_) => 2,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_result(value: Option<f64>) -> Result<i64, Fault> {
|
||||
Ok(classify(value))
|
||||
}
|
||||
|
||||
fn classify_borrowed(value: &Option<f64>) -> i64 {
|
||||
match value {
|
||||
None => -1,
|
||||
Some(value) if *value == 1.0 => 1,
|
||||
Some(value) if *value == 0.0 => 0,
|
||||
Some(_) => 2,
|
||||
}
|
||||
}
|
||||
|
||||
fn value_helper(values: &[Option<f64>]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
total += classify(*value);
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn result_helper(values: &[Option<f64>]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
total += classify_result(*value)?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn borrowed_helper(values: &[Option<f64>]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
total += classify_borrowed(value);
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let special = [
|
||||
None,
|
||||
Some(0.0),
|
||||
Some(-0.0),
|
||||
Some(1.0),
|
||||
Some(-1.0),
|
||||
Some(f64::NAN),
|
||||
Some(f64::INFINITY),
|
||||
Some(f64::NEG_INFINITY),
|
||||
];
|
||||
let values = (0..256)
|
||||
.map(|index| special[index % special.len()])
|
||||
.collect::<Vec<_>>();
|
||||
let mut state = 0x123456789abcdef0u64;
|
||||
let mixed = (0..256)
|
||||
.map(|index| {
|
||||
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
if index % 3 == 0 {
|
||||
Some(f64::from_bits(state))
|
||||
} else {
|
||||
special[((state >> 32) as usize) % special.len()]
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let references = [
|
||||
(
|
||||
"direct",
|
||||
reference as fn(&[Option<f64>]) -> Result<i64, Fault>,
|
||||
),
|
||||
("value", value_helper),
|
||||
("result", result_helper),
|
||||
("borrowed", borrowed_helper),
|
||||
];
|
||||
for (name, operation) in references {
|
||||
for value in values.iter().chain(&mixed) {
|
||||
assert_eq!(generated(&[*value]).unwrap(), operation(&[*value]).unwrap());
|
||||
}
|
||||
eprintln!(
|
||||
"{name}_equals_generated={}",
|
||||
std::ptr::fn_addr_eq(
|
||||
operation,
|
||||
generated as fn(&[Option<f64>]) -> Result<i64, Fault>
|
||||
)
|
||||
);
|
||||
}
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
for (name, values) in [("float_batch", &values), ("float_batch_mixed", &mixed)] {
|
||||
compare(
|
||||
name,
|
||||
values,
|
||||
10000,
|
||||
generated_batch,
|
||||
reference_batch,
|
||||
|value| *value as u64,
|
||||
);
|
||||
}
|
||||
compare(
|
||||
"float_direct",
|
||||
&values,
|
||||
10000,
|
||||
generated,
|
||||
reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"float_mixed",
|
||||
&mixed,
|
||||
10000,
|
||||
generated,
|
||||
reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"float_value_helper",
|
||||
&values,
|
||||
10000,
|
||||
generated,
|
||||
value_helper,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"float_result_helper",
|
||||
&values,
|
||||
10000,
|
||||
generated,
|
||||
result_helper,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"float_borrowed_helper",
|
||||
&values,
|
||||
10000,
|
||||
generated,
|
||||
borrowed_helper,
|
||||
|value| *value as u64,
|
||||
);
|
||||
// 同一混合序列也经过各参照,避免把函数边界或布局差异归给 Dever。
|
||||
for (name, operation) in [
|
||||
(
|
||||
"float_mixed_value_helper",
|
||||
value_helper as fn(&[Option<f64>]) -> Result<i64, Fault>,
|
||||
),
|
||||
("float_mixed_result_helper", result_helper),
|
||||
("float_mixed_borrowed_helper", borrowed_helper),
|
||||
] {
|
||||
compare(name, &mixed, 10000, generated, operation, |value| {
|
||||
*value as u64
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
fn decimal_generated(values: &[Option<Decimal>]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
total += GENERATED_DECIMAL(value)?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn decimal_reference(values: &[Option<Decimal>]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
let zero = Decimal::from_int(0);
|
||||
for value in values {
|
||||
total += match value {
|
||||
None => 2,
|
||||
Some(value) if *value < zero => -1,
|
||||
Some(value) if *value == zero => 0,
|
||||
Some(_) => 1,
|
||||
};
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn edge_generated(values: &[Decimal]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
total += GENERATED_EDGE(*value)?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn edge_reference(values: &[Decimal]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
let one = Decimal::from_int(1);
|
||||
for value in values {
|
||||
total += i64::from(*value > one);
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn float_generated(values: &[Option<f64>]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
total += GENERATED_FLOAT(value)?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn float_reference(values: &[Option<f64>]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
total += match value {
|
||||
None => -1,
|
||||
Some(value) if *value == 1.0 => 1,
|
||||
Some(value) if *value == 0.0 => 0,
|
||||
Some(_) => 2,
|
||||
};
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let decimals = (0..256).map(|index| match index % 4 {
|
||||
0 => None,
|
||||
1 => Some(Decimal::from_int(-(index as i64))),
|
||||
2 => Some(Decimal::from_int(0)),
|
||||
_ => Some(Decimal::from_int(index as i64)),
|
||||
}).collect::<Vec<_>>();
|
||||
let edges = ["0", "1", "1.000000000000000000000000000000001", "2"];
|
||||
let edges = (0..256).map(|index| Decimal::parse(edges[index % 4]).unwrap()).collect::<Vec<_>>();
|
||||
let special = [None, Some(0.0), Some(-0.0), Some(1.0), Some(-1.0), Some(f64::NAN), Some(f64::INFINITY), Some(f64::NEG_INFINITY)];
|
||||
let floats = (0..256).map(|index| special[index % special.len()]).collect::<Vec<_>>();
|
||||
for value in &decimals { assert_eq!(GENERATED_DECIMAL(value).unwrap(), decimal_reference(&[*value]).unwrap()); }
|
||||
for value in &edges { assert_eq!(GENERATED_EDGE(*value).unwrap(), edge_reference(&[*value]).unwrap()); }
|
||||
for value in &floats { assert_eq!(GENERATED_FLOAT(value).unwrap(), float_reference(&[*value]).unwrap()); }
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
compare("decimal_clauses", &decimals, 10000, decimal_generated, decimal_reference, |value| *value as u64);
|
||||
compare("decimal_adjacent", &edges, 10000, edge_generated, edge_reference, |value| *value as u64);
|
||||
compare("float_clauses", &floats, 10000, float_generated, float_reference, |value| *value as u64);
|
||||
|
||||
// 独立保留原三组;混合输入覆盖非整数、不同小数位、负零和指数极限。
|
||||
let spellings = [
|
||||
"-9.999999999999999999999999999999999E+6144",
|
||||
"-1234567890123456789012345678901234",
|
||||
"-1.000000000000000000000000000000001",
|
||||
"-1.00", "-0.001", "-1E-6176", "-0.00", "0", "0E+100",
|
||||
"1E-6176", "0.001", "0.9999999999999999999999999999999999",
|
||||
"1", "1.000", "1.000000000000000000000000000000001",
|
||||
"1234567890123456789012345678901234",
|
||||
"9.999999999999999999999999999999999E+6144",
|
||||
];
|
||||
let palette = spellings.map(|text| Decimal::parse(text).unwrap());
|
||||
let mut state = 0x123456789abcdef0u64;
|
||||
let mixed = (0..256).map(|_| {
|
||||
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
palette[((state >> 32) as usize) % palette.len()]
|
||||
}).collect::<Vec<_>>();
|
||||
let nullable = mixed.iter().enumerate().map(|(index, value)| {
|
||||
if index % 5 == 0 { None } else { Some(*value) }
|
||||
}).collect::<Vec<_>>();
|
||||
for value in &nullable { assert_eq!(GENERATED_DECIMAL(value).unwrap(), decimal_reference(&[*value]).unwrap()); }
|
||||
for value in &mixed { assert_eq!(GENERATED_EDGE(*value).unwrap(), edge_reference(&[*value]).unwrap()); }
|
||||
compare("decimal_mixed_clauses", &nullable, 10000, decimal_generated, decimal_reference, |value| *value as u64);
|
||||
compare("decimal_mixed_adjacent", &mixed, 10000, edge_generated, edge_reference, |value| *value as u64);
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
// 算术参照直接调用 dec,逐步检查同样的错误;不降低精度或重排运算。
|
||||
use dec::{Context, Decimal128};
|
||||
|
||||
const TWO: Decimal128 = Decimal128::from_ne_bytes(
|
||||
u128::from_be_bytes([34, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]).to_ne_bytes(),
|
||||
);
|
||||
|
||||
// 两套表示在计时前准备,Rust 循环不经过 Dever 的输入/输出转换。
|
||||
struct Inputs<Generated, Native> {
|
||||
generated: Vec<Generated>,
|
||||
native: Vec<Native>,
|
||||
}
|
||||
|
||||
fn inputs<Generated: Copy, Native>(
|
||||
values: &[Generated],
|
||||
convert: impl FnMut(Generated) -> Native,
|
||||
) -> [Inputs<Generated, Native>; 1] {
|
||||
[Inputs {
|
||||
generated: values.to_vec(),
|
||||
native: values.iter().copied().map(convert).collect(),
|
||||
}]
|
||||
}
|
||||
|
||||
fn native_bytes(value: Decimal128) -> [u8; 16] {
|
||||
u128::from_ne_bytes(value.to_ne_bytes()).to_be_bytes()
|
||||
}
|
||||
|
||||
fn raw(value: Decimal) -> Decimal128 {
|
||||
Decimal128::from_be_bytes(value.to_be_bytes())
|
||||
}
|
||||
|
||||
fn checked(value: Decimal128, context: &Context<Decimal128>) -> Result<Decimal128, Fault> {
|
||||
if value.is_finite() && !context.status().overflow() && !context.status().invalid_operation() {
|
||||
Ok(value)
|
||||
} else {
|
||||
Err(&FaultInfo {
|
||||
message: "Decimal arithmetic fault",
|
||||
source: "Rust reference",
|
||||
line: 0,
|
||||
column: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn operation<const KIND: usize>(
|
||||
context: &mut Context<Decimal128>,
|
||||
left: Decimal128,
|
||||
right: Decimal128,
|
||||
) -> Result<Decimal128, Fault> {
|
||||
context.clear_status();
|
||||
let value = match KIND {
|
||||
0 => context.add(left, right),
|
||||
1 => context.sub(left, right),
|
||||
2 => context.mul(left, right),
|
||||
3 => context.div(left, right),
|
||||
4 => {
|
||||
let product = operation::<2>(context, left, right)?;
|
||||
let added = operation::<0>(context, product, left)?;
|
||||
let divided = operation::<3>(context, added, right)?;
|
||||
return operation::<1>(context, divided, left);
|
||||
}
|
||||
5 => context.mul(left, TWO),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
checked(value, context)
|
||||
}
|
||||
|
||||
fn generated_one<const KIND: usize>(left: Decimal, right: Decimal) -> Result<Decimal, Fault> {
|
||||
match KIND {
|
||||
0 => GENERATED_ADD(left, right),
|
||||
1 => GENERATED_SUBTRACT(left, right),
|
||||
2 => GENERATED_MULTIPLY(left, right),
|
||||
3 => GENERATED_DIVIDE(left, right),
|
||||
4 => GENERATED_CALCULATE(left, right),
|
||||
5 => GENERATED_SCALE(left),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn score(bytes: [u8; 16]) -> u64 {
|
||||
u64::from_be_bytes(bytes[8..].try_into().unwrap())
|
||||
}
|
||||
|
||||
fn generated<const KIND: usize>(
|
||||
inputs: &[Inputs<(Decimal, Decimal), (Decimal128, Decimal128)>],
|
||||
) -> Result<u64, Fault> {
|
||||
let values = inputs.first().map_or(&[][..], |input| input.generated.as_slice());
|
||||
let mut total = 0u64;
|
||||
for (left, right) in values {
|
||||
total = total.wrapping_add(score(generated_one::<KIND>(*left, *right)?.to_be_bytes()));
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn reference<const KIND: usize>(
|
||||
inputs: &[Inputs<(Decimal, Decimal), (Decimal128, Decimal128)>],
|
||||
) -> Result<u64, Fault> {
|
||||
let values = inputs.first().map_or(&[][..], |input| input.native.as_slice());
|
||||
let mut context = Context::<Decimal128>::default();
|
||||
let mut total = 0u64;
|
||||
for (left, right) in values {
|
||||
let value = operation::<KIND>(&mut context, *left, *right)?;
|
||||
total = total.wrapping_add(score(native_bytes(value)));
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn compare_operation<const KIND: usize>(name: &str, values: &[(Decimal, Decimal)]) {
|
||||
let mut context = Context::<Decimal128>::default();
|
||||
let maximum = Decimal::parse("9.999999999999999999999999999999999E+6144").unwrap();
|
||||
for (left, right) in values
|
||||
.iter()
|
||||
.copied()
|
||||
.chain([(maximum, maximum), (maximum, Decimal::from_int(0))])
|
||||
{
|
||||
let actual = generated_one::<KIND>(left, right);
|
||||
let expected = operation::<KIND>(&mut context, raw(left), raw(right));
|
||||
assert_eq!(actual.is_err(), expected.is_err());
|
||||
if let (Ok(actual), Ok(expected)) = (actual, expected) {
|
||||
assert_eq!(actual.to_be_bytes(), expected.to_be_bytes());
|
||||
}
|
||||
}
|
||||
let inputs = inputs(values, |(left, right)| (raw(left), raw(right)));
|
||||
compare(
|
||||
name,
|
||||
&inputs,
|
||||
1000,
|
||||
generated::<KIND>,
|
||||
reference::<KIND>,
|
||||
|value| *value,
|
||||
);
|
||||
}
|
||||
|
||||
fn projected(inputs: &[Inputs<Decimal, Decimal128>]) -> Result<[u8; 16], Fault> {
|
||||
let values = inputs.first().map_or(&[][..], |input| input.generated.as_slice());
|
||||
GENERATED_SCALED(values).map(Decimal::to_be_bytes)
|
||||
}
|
||||
|
||||
fn projected_reference(inputs: &[Inputs<Decimal, Decimal128>]) -> Result<[u8; 16], Fault> {
|
||||
let values = inputs.first().map_or(&[][..], |input| input.native.as_slice());
|
||||
let mut context = Context::<Decimal128>::default();
|
||||
let mut total = Decimal128::ZERO;
|
||||
for value in values {
|
||||
let next = operation::<5>(&mut context, *value, Decimal128::ZERO)?;
|
||||
total = operation::<0>(&mut context, total, next)?;
|
||||
}
|
||||
Ok(native_bytes(total))
|
||||
}
|
||||
|
||||
fn converted(values: &[i64]) -> Result<u64, Fault> {
|
||||
let mut total = 0u64;
|
||||
for value in values {
|
||||
total = total.wrapping_add(score(GENERATED_CONVERT(*value)?.to_be_bytes()));
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn converted_reference(values: &[i64]) -> Result<u64, Fault> {
|
||||
let mut total = 0u64;
|
||||
for value in values {
|
||||
let decimal = if let Ok(narrow) = i32::try_from(*value) {
|
||||
Decimal128::from(narrow)
|
||||
} else if let Ok(narrow) = u32::try_from(*value) {
|
||||
Decimal128::from(narrow)
|
||||
} else {
|
||||
Decimal128::from(*value)
|
||||
};
|
||||
total = total.wrapping_add(score(native_bytes(decimal)));
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn converted_direct_reference(values: &[i64]) -> Result<u64, Fault> {
|
||||
let mut total = 0u64;
|
||||
for value in values {
|
||||
total = total.wrapping_add(score(native_bytes(Decimal128::from(*value))));
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let constants = GENERATED_CONSTANTS().unwrap();
|
||||
let expected = [
|
||||
Decimal128::ZERO,
|
||||
Context::<Decimal128>::default().parse("-0.00").unwrap(),
|
||||
Decimal128::from(i64::MIN),
|
||||
Decimal128::from(i64::MAX),
|
||||
Decimal128::from(3i64),
|
||||
];
|
||||
assert_eq!(constants.len(), expected.len());
|
||||
for (actual, expected) in constants.iter().zip(expected) {
|
||||
assert_eq!(actual.to_be_bytes(), expected.to_be_bytes());
|
||||
}
|
||||
let left = [
|
||||
"-0.00",
|
||||
"1.25",
|
||||
"-7.5",
|
||||
"1E-60",
|
||||
"9999999999999999999999999999999999",
|
||||
"-123.456",
|
||||
"0.9999999999999999999999999999999999",
|
||||
"1234567.890",
|
||||
]
|
||||
.map(|text| Decimal::parse(text).unwrap());
|
||||
let right = [
|
||||
"3.25",
|
||||
"-2",
|
||||
"0.1",
|
||||
"1.000000000000000000000000000000001",
|
||||
"2",
|
||||
"10",
|
||||
"-3",
|
||||
"1",
|
||||
]
|
||||
.map(|text| Decimal::parse(text).unwrap());
|
||||
let values = (0..256)
|
||||
.map(|index| (left[index % left.len()], right[index % right.len()]))
|
||||
.collect::<Vec<_>>();
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
compare_operation::<0>("decimal_add", &values);
|
||||
compare_operation::<1>("decimal_subtract", &values);
|
||||
compare_operation::<2>("decimal_multiply", &values);
|
||||
compare_operation::<3>("decimal_divide", &values);
|
||||
compare_operation::<4>("decimal_calculate", &values);
|
||||
compare_operation::<5>("decimal_literal_scale", &values);
|
||||
let items = values.iter().map(|(left, _)| *left).collect::<Vec<_>>();
|
||||
let prepared = inputs(&items, raw);
|
||||
assert_eq!(projected(&prepared).unwrap(), projected_reference(&prepared).unwrap());
|
||||
let large = Decimal::parse("4E+6144").unwrap();
|
||||
let maximum = Decimal::parse("9.999999999999999999999999999999999E+6144").unwrap();
|
||||
for items in [vec![], vec![large, large, maximum], vec![maximum, large]] {
|
||||
let prepared = inputs(&items, raw);
|
||||
let actual = projected(&prepared);
|
||||
let expected = projected_reference(&prepared);
|
||||
assert_eq!(actual.is_err(), expected.is_err());
|
||||
match (actual, expected) {
|
||||
(Ok(actual), Ok(expected)) => assert_eq!(actual, expected),
|
||||
(Err(actual), Err(_)) => {
|
||||
let source = std::fs::read_to_string(actual.source).unwrap();
|
||||
let needle = if items[0] == maximum {
|
||||
"value * 2"
|
||||
} else {
|
||||
"sum(scale, values)"
|
||||
};
|
||||
assert_eq!(
|
||||
actual.line,
|
||||
source[..source.find(needle).unwrap()]
|
||||
.bytes()
|
||||
.filter(|byte| *byte == b'\n')
|
||||
.count()
|
||||
+ 1
|
||||
);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
compare(
|
||||
"decimal_projected_sum",
|
||||
&prepared,
|
||||
1000,
|
||||
projected,
|
||||
projected_reference,
|
||||
|value| score(*value),
|
||||
);
|
||||
let numbers = [
|
||||
i64::MIN,
|
||||
i64::MAX,
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
i32::MIN as i64,
|
||||
i32::MAX as i64,
|
||||
1234567890123456789,
|
||||
];
|
||||
for value in numbers {
|
||||
assert_eq!(
|
||||
GENERATED_CONVERT(value).unwrap().to_be_bytes(),
|
||||
Decimal128::from(value).to_be_bytes()
|
||||
);
|
||||
}
|
||||
let integers = (0..256)
|
||||
.map(|index| numbers[index % numbers.len()])
|
||||
.collect::<Vec<_>>();
|
||||
compare(
|
||||
"decimal_int_conversion",
|
||||
&integers,
|
||||
1000,
|
||||
converted,
|
||||
converted_reference,
|
||||
|value| *value,
|
||||
);
|
||||
|
||||
for (name, numbers) in [
|
||||
("decimal_int_conversion_narrow", [
|
||||
i64::from(i32::MIN), -1, 0, 1, i64::from(i32::MAX),
|
||||
i64::from(i32::MAX) + 1, i64::from(u32::MAX) - 1, i64::from(u32::MAX),
|
||||
]),
|
||||
("decimal_int_conversion_wide", [
|
||||
i64::MIN, i64::MAX, i64::from(i32::MIN) - 1, i64::from(u32::MAX) + 1,
|
||||
-1234567890123456789, 1234567890123456789, -4294967296, 4294967296,
|
||||
]),
|
||||
] {
|
||||
let values = (0..256).map(|index| numbers[index % numbers.len()]).collect::<Vec<_>>();
|
||||
compare(name, &values, 1000, converted, converted_reference, |value| *value);
|
||||
if name.ends_with("wide") {
|
||||
compare_named("decimal_int_conversion_wide_direct", &values, 1000, converted, converted_direct_reference, |value| *value, "rust_direct");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
fn reference_ports(values: &[PORT_TYPE]) -> Result<i64, Fault> {
|
||||
let fault = &FaultInfo { message: "integer arithmetic fault", source: "Rust reference", line: 0, column: 0 };
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
let value = value.field0.checked_add(1).ok_or(fault)?;
|
||||
total = total.checked_add(value).ok_or(fault)?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
// 两处溢出保留各自来源;原单一错误参照仍用于观察诊断语义的成本。
|
||||
fn reference_ports_distinct_faults(values: &[PORT_TYPE]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
let adjusted = value.field0.checked_add(1).ok_or(&FaultInfo {
|
||||
message: "integer arithmetic fault", source: "Rust reference", line: 1, column: 1,
|
||||
})?;
|
||||
total = total.checked_add(adjusted).ok_or(&FaultInfo {
|
||||
message: "integer overflow", source: "Rust reference", line: 2, column: 1,
|
||||
})?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
// Port 只允许 1..=65535,加一不可能溢出;累计仍按 Int 合同检查。
|
||||
fn reference_ports_bounded(values: &[PORT_TYPE]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
total = total.checked_add(value.field0.wrapping_add(1)).ok_or(&FaultInfo {
|
||||
message: "integer overflow", source: "Rust reference", line: 2, column: 1,
|
||||
})?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
const SCALE: [u8; 16] = [34, 7, 192, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0];
|
||||
|
||||
fn reference_rates(values: &[RATE_TYPE]) -> Result<Decimal, Fault> {
|
||||
let fault = &FaultInfo { message: "decimal arithmetic fault", source: "Rust reference", line: 0, column: 0 };
|
||||
let scale = dec::Decimal128::from_be_bytes(SCALE);
|
||||
let mut context = dec::Context::<dec::Decimal128>::default();
|
||||
let mut total = dec::Decimal128::ZERO;
|
||||
for value in values {
|
||||
context.clear_status();
|
||||
let value = context.mul(dec::Decimal128::from_be_bytes(value.field0.to_be_bytes()), scale);
|
||||
if !value.is_finite() || context.status().overflow() || context.status().invalid_operation() { return Err(fault); }
|
||||
context.clear_status();
|
||||
total = context.add(total, value);
|
||||
if !total.is_finite() || context.status().overflow() || context.status().invalid_operation() { return Err(fault); }
|
||||
}
|
||||
Ok(Decimal::from_be_bytes(total.to_be_bytes()))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
assert_eq!(Decimal::parse("100.0").unwrap().to_be_bytes(), SCALE);
|
||||
for values in [vec![], vec![1], vec![65535], vec![1, 65535], vec![32768, 32767]] {
|
||||
let ports = values.into_iter().map(|field0| PORT_TYPE { field0 }).collect::<Vec<_>>();
|
||||
assert_eq!(GENERATED_TOTAL(&ports).map_err(|fault| fault.message), reference_ports_distinct_faults(&ports).map_err(|fault| fault.message));
|
||||
assert_eq!(GENERATED_TOTAL(&ports).map_err(|fault| fault.message), reference_ports_bounded(&ports).map_err(|fault| fault.message));
|
||||
}
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
let ports = (0..64).map(|index| PORT_TYPE { field0: index * 1000 + 1 }).collect::<Vec<_>>();
|
||||
compare("range_int", &ports, 100_000, GENERATED_TOTAL, reference_ports, |value| *value as u64);
|
||||
compare("range_int_distinct_faults", &ports, 100_000, GENERATED_TOTAL, reference_ports_distinct_faults, |value| *value as u64);
|
||||
compare("range_int_bounded", &ports, 100_000, GENERATED_TOTAL, reference_ports_bounded, |value| *value as u64);
|
||||
let rates = (0..32).map(|index| RATE_TYPE { field0: Decimal::parse(&format!("0.{index:02}")).unwrap() }).collect::<Vec<_>>();
|
||||
assert_eq!(GENERATED_RATES(&rates).unwrap().to_be_bytes(), reference_rates(&rates).unwrap().to_be_bytes());
|
||||
compare("range_decimal", &rates, 5_000, GENERATED_RATES, reference_rates, |value| u64::from(value.to_be_bytes()[15]));
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
// 标准库入口来自实际 Dever 源码;参照只使用 dec,不调用 Dever 运行库。
|
||||
use dec::{Context, Decimal128};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
static NATIVE_CONTEXT: LazyLock<Context<Decimal128>> = LazyLock::new(Context::default);
|
||||
|
||||
struct DecimalInput {
|
||||
text: String,
|
||||
value: Decimal,
|
||||
native: Decimal128,
|
||||
places: i64,
|
||||
}
|
||||
|
||||
fn native_parse(text: &str) -> Option<Decimal128> {
|
||||
let mut context = NATIVE_CONTEXT.clone();
|
||||
let value = context.parse(text).ok()?;
|
||||
(value.is_finite() && !context.status().inexact()).then_some(value)
|
||||
}
|
||||
|
||||
fn parsed<const NATIVE: bool>(values: &[String]) -> Result<Vec<Option<[u8; 16]>>, Fault> {
|
||||
values
|
||||
.iter()
|
||||
.map(|text| {
|
||||
Ok(if NATIVE {
|
||||
native_parse(text).map(|value| value.to_be_bytes())
|
||||
} else {
|
||||
DECIMAL_PARSE(text)?.map(Decimal::to_be_bytes)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct NativeDisplay(Decimal128);
|
||||
impl std::fmt::Display for NativeDisplay {
|
||||
fn fmt(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let value = Context::<Decimal128>::default().reduce(self.0);
|
||||
if value.is_zero() {
|
||||
output.write_str("0")
|
||||
} else {
|
||||
write!(output, "{value}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn formatted<const NATIVE: bool>(values: &[DecimalInput]) -> Result<Vec<String>, Fault> {
|
||||
values
|
||||
.iter()
|
||||
.map(|input| {
|
||||
if NATIVE {
|
||||
Ok(NativeDisplay(input.native).to_string())
|
||||
} else {
|
||||
DECIMAL_TEXT(input.value)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn native_round(value: Decimal128, places: i64) -> Option<Decimal128> {
|
||||
let exponent = i32::try_from(places.checked_neg()?).ok()?;
|
||||
if !(-6176..=6111).contains(&exponent) {
|
||||
return None;
|
||||
}
|
||||
let mut context = NATIVE_CONTEXT.clone();
|
||||
let mut quantum = Decimal128::ONE;
|
||||
context.set_exponent(&mut quantum, exponent);
|
||||
let value = context.quantize(value, quantum);
|
||||
(value.is_finite() && !context.status().underflow() && !context.status().invalid_context())
|
||||
.then_some(value)
|
||||
}
|
||||
|
||||
fn rounded<const NATIVE: bool>(values: &[DecimalInput]) -> Result<Vec<Option<[u8; 16]>>, Fault> {
|
||||
Ok(values
|
||||
.iter()
|
||||
.map(|input| {
|
||||
if NATIVE {
|
||||
native_round(input.native, input.places).map(|value| value.to_be_bytes())
|
||||
} else {
|
||||
DECIMAL_ROUND(input.value, input.places)
|
||||
.ok()
|
||||
.map(Decimal::to_be_bytes)
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn negated<const NATIVE: bool>(values: &[DecimalInput]) -> Result<Vec<[u8; 16]>, Fault> {
|
||||
Ok(values
|
||||
.iter()
|
||||
.map(|input| {
|
||||
if NATIVE {
|
||||
(-input.native).to_be_bytes()
|
||||
} else {
|
||||
(-input.value).to_be_bytes()
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn compared<const NATIVE: bool>(
|
||||
values: &[DecimalInput],
|
||||
) -> Result<Vec<(bool, Option<std::cmp::Ordering>)>, Fault> {
|
||||
Ok(values
|
||||
.windows(2)
|
||||
.map(|pair| {
|
||||
if NATIVE {
|
||||
(
|
||||
pair[0].native == pair[1].native,
|
||||
pair[0].native.partial_cmp(&pair[1].native),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
pair[0].value == pair[1].value,
|
||||
pair[0].value.partial_cmp(&pair[1].value),
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn byte_score(values: &[Option<[u8; 16]>]) -> u64 {
|
||||
values
|
||||
.iter()
|
||||
.flatten()
|
||||
.flat_map(|value| value.iter())
|
||||
.map(|value| u64::from(*value))
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn text_score(values: &[String]) -> u64 {
|
||||
values
|
||||
.iter()
|
||||
.flat_map(|value| value.bytes())
|
||||
.map(u64::from)
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("case,sample,implementation,nanoseconds,checksum");
|
||||
let spellings = [
|
||||
"0",
|
||||
"-0",
|
||||
"1.2300",
|
||||
"-12.345",
|
||||
"1E-6176",
|
||||
"1E6111",
|
||||
"9.999999999999999999999999999999999E6144",
|
||||
"1234567890123456789012345678901234",
|
||||
"1.25",
|
||||
"1.35",
|
||||
"-250",
|
||||
"+.125",
|
||||
"1e-34",
|
||||
"1.000000000000000000000000000000000",
|
||||
];
|
||||
let texts = spellings
|
||||
.iter()
|
||||
.cycle()
|
||||
.take(112)
|
||||
.map(|text| (*text).to_owned())
|
||||
.collect::<Vec<_>>();
|
||||
let inputs = texts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, text)| DecimalInput {
|
||||
text: text.clone(),
|
||||
value: Decimal::parse(text).unwrap(),
|
||||
native: native_parse(text).unwrap(),
|
||||
places: [0, 1, 2, -2, 6176, -6111][index % 6],
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
compare(
|
||||
"decimal_parse_valid",
|
||||
&texts,
|
||||
1000,
|
||||
parsed::<false>,
|
||||
parsed::<true>,
|
||||
|value| byte_score(value),
|
||||
);
|
||||
let invalid = [
|
||||
"",
|
||||
"+",
|
||||
"NaN",
|
||||
"-sNaN",
|
||||
"Infinity",
|
||||
"nan123",
|
||||
" 1",
|
||||
"1 ",
|
||||
"1_000",
|
||||
"1e",
|
||||
"1\0",
|
||||
"雪",
|
||||
"1e9999",
|
||||
"1e-9999",
|
||||
"12345678901234567890123456789012345",
|
||||
]
|
||||
.iter()
|
||||
.cycle()
|
||||
.take(120)
|
||||
.map(|value| (*value).to_owned())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(
|
||||
parsed::<false>(&invalid)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(Option::is_none)
|
||||
);
|
||||
compare(
|
||||
"decimal_parse_invalid",
|
||||
&invalid,
|
||||
1000,
|
||||
parsed::<false>,
|
||||
parsed::<true>,
|
||||
|value| byte_score(value),
|
||||
);
|
||||
compare(
|
||||
"decimal_format",
|
||||
&inputs,
|
||||
1000,
|
||||
formatted::<false>,
|
||||
formatted::<true>,
|
||||
|value| text_score(value),
|
||||
);
|
||||
compare(
|
||||
"decimal_round",
|
||||
&inputs,
|
||||
1000,
|
||||
rounded::<false>,
|
||||
rounded::<true>,
|
||||
|value| byte_score(value),
|
||||
);
|
||||
compare(
|
||||
"decimal_negate",
|
||||
&inputs,
|
||||
1000,
|
||||
negated::<false>,
|
||||
negated::<true>,
|
||||
|value| value.iter().flatten().map(|value| u64::from(*value)).sum(),
|
||||
);
|
||||
compare(
|
||||
"decimal_compare",
|
||||
&inputs,
|
||||
1000,
|
||||
compared::<false>,
|
||||
compared::<true>,
|
||||
|value| {
|
||||
value
|
||||
.iter()
|
||||
.map(|(equal, order)| {
|
||||
u64::from(*equal) + u64::from(order == &Some(std::cmp::Ordering::Less))
|
||||
})
|
||||
.sum()
|
||||
},
|
||||
);
|
||||
wire_cases(&inputs);
|
||||
storage_cases();
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
// 在基准中编译同一份私有 PostgreSQL codec,不为测试扩大生产 API。
|
||||
use dever_runtime::value;
|
||||
mod database {
|
||||
pub use dever_runtime::database::{Error, Kind};
|
||||
pub mod scalar {
|
||||
pub use super::{Error, Kind};
|
||||
#[path = "DECIMAL_SCALAR_SOURCE"]
|
||||
pub mod decimal;
|
||||
}
|
||||
pub mod codec {
|
||||
#[path = "DECIMAL_CODEC_SOURCE"]
|
||||
mod implementation;
|
||||
pub fn encode(value: crate::Decimal, fixed: bool, output: &mut bytes::BytesMut) {
|
||||
if fixed {
|
||||
implementation::encode(value, 34, 4, output).unwrap();
|
||||
} else {
|
||||
implementation::encode_native(value, output);
|
||||
}
|
||||
}
|
||||
pub fn decode(bytes: &[u8]) -> crate::Decimal {
|
||||
implementation::decode(bytes).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
use database::codec as postgres_codec;
|
||||
|
||||
struct StoredDecimal {
|
||||
value: Decimal,
|
||||
native: Decimal128,
|
||||
key: String,
|
||||
postgres: Vec<u8>,
|
||||
}
|
||||
|
||||
fn native_parts(coefficient: i128, exponent: i32) -> Option<Decimal128> {
|
||||
let mut context = NATIVE_CONTEXT.clone();
|
||||
// i128 的六次乘加以及 String 收缩均更慢;借用路径复用 CString 的容量特化。
|
||||
let value = context
|
||||
.parse(format!("{coefficient}E{exponent}").as_str())
|
||||
.ok()?;
|
||||
(value.is_finite() && !context.status().inexact()).then_some(value)
|
||||
}
|
||||
|
||||
fn native_scaled(value: Decimal128, precision: u32, scale: u32) -> Option<i128> {
|
||||
if !(1..=34).contains(&precision) || scale > precision {
|
||||
return None;
|
||||
}
|
||||
let coefficient = value.coefficient();
|
||||
let shift = value.exponent() + scale as i32;
|
||||
let scaled = if coefficient == 0 {
|
||||
0
|
||||
} else if shift >= 0 {
|
||||
coefficient.checked_mul(10_i128.checked_pow(shift as u32)?)?
|
||||
} else {
|
||||
let divisor = 10_i128.checked_pow(shift.unsigned_abs())?;
|
||||
if coefficient % divisor != 0 {
|
||||
return None;
|
||||
}
|
||||
coefficient / divisor
|
||||
};
|
||||
(scaled.unsigned_abs() < 10_u128.pow(precision)).then_some(scaled)
|
||||
}
|
||||
|
||||
fn sqlite_encoded<const NATIVE: bool>(values: &[StoredDecimal]) -> Result<Vec<String>, Fault> {
|
||||
Ok(values
|
||||
.iter()
|
||||
.map(|input| {
|
||||
if NATIVE {
|
||||
format!(
|
||||
"{:035}",
|
||||
native_scaled(input.native, 34, 4).unwrap() + 10_i128.pow(34)
|
||||
)
|
||||
} else {
|
||||
dever_runtime::database::scalar::decimal::encode(input.value, 34, 4).unwrap()
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn sqlite_decoded<const NATIVE: bool>(
|
||||
values: &[StoredDecimal],
|
||||
) -> Result<Vec<Option<[u8; 16]>>, Fault> {
|
||||
Ok(values
|
||||
.iter()
|
||||
.map(|input| {
|
||||
if NATIVE {
|
||||
let text = &input.key;
|
||||
if text.len() != 35 || !text.bytes().all(|byte| byte.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
let shifted = text.parse::<i128>().ok()?;
|
||||
if shifted <= 0 || shifted >= 2 * 10_i128.pow(34) {
|
||||
return None;
|
||||
}
|
||||
native_parts(shifted - 10_i128.pow(34), -4).map(|value| value.to_be_bytes())
|
||||
} else {
|
||||
dever_runtime::database::scalar::decimal::decode(&input.key, 34, 4)
|
||||
.ok()
|
||||
.map(Decimal::to_be_bytes)
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
// 独立按协议生成分组;两侧均用栈数组,不给参照添加额外分配。
|
||||
fn native_postgres(coefficient: i128, exponent: i32, scale: u16, output: &mut bytes::BytesMut) {
|
||||
use bytes::BufMut;
|
||||
let mut number = coefficient.unsigned_abs() * 10_u128.pow(exponent.rem_euclid(4) as u32);
|
||||
let mut groups = [0u16; 10];
|
||||
let mut count = 0;
|
||||
while number != 0 {
|
||||
groups[count] = (number % 10000) as u16;
|
||||
count += 1;
|
||||
number /= 10000;
|
||||
}
|
||||
let weight = exponent.div_euclid(4) + count as i32 - 1;
|
||||
let skip = groups[..count]
|
||||
.iter()
|
||||
.take_while(|value| **value == 0)
|
||||
.count();
|
||||
output.put_i16((count - skip) as i16);
|
||||
output.put_i16(if count == 0 { 0 } else { weight as i16 });
|
||||
output.put_u16(if coefficient < 0 { 0x4000 } else { 0 });
|
||||
output.put_u16(scale);
|
||||
for value in groups[skip..count].iter().rev() {
|
||||
output.put_u16(*value);
|
||||
}
|
||||
}
|
||||
|
||||
fn native_postgres_decode(bytes: &[u8]) -> Option<Decimal128> {
|
||||
if bytes.len() < 8 || !bytes.len().is_multiple_of(2) {
|
||||
return None;
|
||||
}
|
||||
let word = |index: usize| i16::from_be_bytes([bytes[index * 2], bytes[index * 2 + 1]]);
|
||||
let count = word(0);
|
||||
let weight = word(1);
|
||||
let sign = word(2);
|
||||
if count < 0
|
||||
|| bytes.len() != 8 + count as usize * 2
|
||||
|| !matches!(sign, 0 | 0x4000)
|
||||
|| word(3) < 0
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut coefficient = 0i128;
|
||||
for index in 0..count as usize {
|
||||
let digit = word(index + 4);
|
||||
if !(0..10000).contains(&digit) {
|
||||
return None;
|
||||
}
|
||||
coefficient = coefficient
|
||||
.checked_mul(10000)?
|
||||
.checked_add(i128::from(digit))?;
|
||||
}
|
||||
if coefficient == 0 {
|
||||
return Some(Decimal128::ZERO);
|
||||
}
|
||||
let mut exponent = (i32::from(weight) + 1 - i32::from(count)) * 4;
|
||||
while coefficient % 10 == 0 {
|
||||
coefficient /= 10;
|
||||
exponent += 1;
|
||||
}
|
||||
if coefficient >= 10_i128.pow(34) {
|
||||
return None;
|
||||
}
|
||||
native_parts(if sign == 0 { coefficient } else { -coefficient }, exponent)
|
||||
}
|
||||
|
||||
fn postgres_encoded<const NATIVE: bool, const FIXED: bool>(
|
||||
values: &[StoredDecimal],
|
||||
) -> Result<Vec<u8>, Fault> {
|
||||
let mut output = bytes::BytesMut::with_capacity(values.len() * 28);
|
||||
for input in values {
|
||||
if NATIVE {
|
||||
if FIXED {
|
||||
native_postgres(
|
||||
native_scaled(input.native, 34, 4).unwrap(),
|
||||
-4,
|
||||
4,
|
||||
&mut output,
|
||||
);
|
||||
} else {
|
||||
native_postgres(
|
||||
input.native.coefficient(),
|
||||
input.native.exponent(),
|
||||
(-input.native.exponent()).max(0) as u16,
|
||||
&mut output,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
postgres_codec::encode(input.value, FIXED, &mut output);
|
||||
}
|
||||
}
|
||||
Ok(output.to_vec())
|
||||
}
|
||||
|
||||
fn postgres_decoded<const NATIVE: bool>(values: &[StoredDecimal]) -> Result<Vec<[u8; 16]>, Fault> {
|
||||
Ok(values
|
||||
.iter()
|
||||
.map(|input| {
|
||||
if NATIVE {
|
||||
native_postgres_decode(&input.postgres)
|
||||
.unwrap()
|
||||
.to_be_bytes()
|
||||
} else {
|
||||
postgres_codec::decode(&input.postgres).to_be_bytes()
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn storage_cases() {
|
||||
let inputs = [
|
||||
"0",
|
||||
"-0",
|
||||
"1.2300",
|
||||
"-9999.0001",
|
||||
"999999999999999999999999999999.9999",
|
||||
"-999999999999999999999999999999.9999",
|
||||
]
|
||||
.iter()
|
||||
.cycle()
|
||||
.take(120)
|
||||
.map(|text| {
|
||||
let value = Decimal::parse(*text).unwrap();
|
||||
let native = native_parse(text).unwrap();
|
||||
let mut postgres = bytes::BytesMut::new();
|
||||
native_postgres(
|
||||
native.coefficient(),
|
||||
native.exponent(),
|
||||
(-native.exponent()).max(0) as u16,
|
||||
&mut postgres,
|
||||
);
|
||||
StoredDecimal {
|
||||
value,
|
||||
native,
|
||||
key: format!(
|
||||
"{:035}",
|
||||
native_scaled(native, 34, 4).unwrap() + 10_i128.pow(34)
|
||||
),
|
||||
postgres: postgres.to_vec(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
compare(
|
||||
"decimal_sqlite_encode",
|
||||
&inputs,
|
||||
1000,
|
||||
sqlite_encoded::<false>,
|
||||
sqlite_encoded::<true>,
|
||||
|value| text_score(value),
|
||||
);
|
||||
compare(
|
||||
"decimal_sqlite_decode",
|
||||
&inputs,
|
||||
1000,
|
||||
sqlite_decoded::<false>,
|
||||
sqlite_decoded::<true>,
|
||||
|value| byte_score(value),
|
||||
);
|
||||
compare(
|
||||
"decimal_postgres_encode",
|
||||
&inputs,
|
||||
1000,
|
||||
postgres_encoded::<false, true>,
|
||||
postgres_encoded::<true, true>,
|
||||
|value| value.iter().map(|value| u64::from(*value)).sum(),
|
||||
);
|
||||
compare(
|
||||
"decimal_postgres_native",
|
||||
&inputs,
|
||||
1000,
|
||||
postgres_encoded::<false, false>,
|
||||
postgres_encoded::<true, false>,
|
||||
|value| value.iter().map(|value| u64::from(*value)).sum(),
|
||||
);
|
||||
compare(
|
||||
"decimal_postgres_decode",
|
||||
&inputs,
|
||||
1000,
|
||||
postgres_decoded::<false>,
|
||||
postgres_decoded::<true>,
|
||||
|value| value.iter().flatten().map(|value| u64::from(*value)).sum(),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// JSON 参照独立实现 Serde 的 Decimal 字符串合同,复用同一个 serde_json 解析器。
|
||||
impl serde::Serialize for NativeDisplay {
|
||||
fn serialize<Encoder: serde::Serializer>(
|
||||
&self,
|
||||
encoder: Encoder,
|
||||
) -> Result<Encoder::Ok, Encoder::Error> {
|
||||
encoder.collect_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'input> serde::Deserialize<'input> for NativeDisplay {
|
||||
fn deserialize<Decoder: serde::Deserializer<'input>>(
|
||||
decoder: Decoder,
|
||||
) -> Result<Self, Decoder::Error> {
|
||||
struct Visitor;
|
||||
impl<'input> serde::de::Visitor<'input> for Visitor {
|
||||
type Value = NativeDisplay;
|
||||
fn expecting(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
output.write_str("an exact Decimal string")
|
||||
}
|
||||
fn visit_str<Error: serde::de::Error>(self, text: &str) -> Result<Self::Value, Error> {
|
||||
native_parse(text)
|
||||
.map(NativeDisplay)
|
||||
.ok_or_else(|| Error::custom("invalid Decimal"))
|
||||
}
|
||||
}
|
||||
decoder.deserialize_str(Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
fn json_encoded<const NATIVE: bool>(values: &[DecimalInput]) -> Result<Vec<String>, Fault> {
|
||||
Ok(values
|
||||
.iter()
|
||||
.map(|input| {
|
||||
if NATIVE {
|
||||
serde_json::to_string(&NativeDisplay(input.native)).unwrap()
|
||||
} else {
|
||||
serde_json::to_string(&input.value).unwrap()
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn json_decoded<const NATIVE: bool>(values: &[String]) -> Result<Vec<Option<[u8; 16]>>, Fault> {
|
||||
Ok(values
|
||||
.iter()
|
||||
.map(|text| {
|
||||
if NATIVE {
|
||||
serde_json::from_str::<NativeDisplay>(text)
|
||||
.ok()
|
||||
.map(|value| value.0.to_be_bytes())
|
||||
} else {
|
||||
serde_json::from_str::<Decimal>(text)
|
||||
.ok()
|
||||
.map(Decimal::to_be_bytes)
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn wire_cases(inputs: &[DecimalInput]) {
|
||||
let texts = inputs
|
||||
.iter()
|
||||
.map(|input| serde_json::to_string(&input.text).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
compare(
|
||||
"decimal_json_encode",
|
||||
inputs,
|
||||
1000,
|
||||
json_encoded::<false>,
|
||||
json_encoded::<true>,
|
||||
|value| text_score(value),
|
||||
);
|
||||
compare(
|
||||
"decimal_json_decode",
|
||||
&texts,
|
||||
1000,
|
||||
json_decoded::<false>,
|
||||
json_decoded::<true>,
|
||||
|value| byte_score(value),
|
||||
);
|
||||
let invalid = [
|
||||
"1.25",
|
||||
"null",
|
||||
"[]",
|
||||
"\"NaN\"",
|
||||
"\"1e9999\"",
|
||||
"\" 1\"",
|
||||
"\"1\\u0000\"",
|
||||
"\"1\" true",
|
||||
]
|
||||
.iter()
|
||||
.cycle()
|
||||
.take(112)
|
||||
.map(|value| (*value).to_owned())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(
|
||||
json_decoded::<false>(&invalid)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(Option::is_none)
|
||||
);
|
||||
compare(
|
||||
"decimal_json_invalid",
|
||||
&invalid,
|
||||
1000,
|
||||
json_decoded::<false>,
|
||||
json_decoded::<true>,
|
||||
|value| byte_score(value),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
fn fault() -> Fault {
|
||||
&FaultInfo {
|
||||
message: "integer arithmetic fault",
|
||||
source: "reference.dever",
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_reference(values: &[i64]) -> Result<Vec<i64>, Fault> {
|
||||
let mut output = Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
output.push(value.checked_add(1).ok_or_else(fault)?);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn total_reference(values: &[i64]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
let adjusted = value.checked_add(1).ok_or_else(fault)?;
|
||||
total = total.checked_add(adjusted).ok_or_else(fault)?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
// 与跨包调用一致,投影失败和累计失败必须能定位到不同来源。
|
||||
fn total_reference_distinct_faults(values: &[i64]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
let adjusted = value.checked_add(1).ok_or(&FaultInfo {
|
||||
message: "integer arithmetic fault",
|
||||
source: "reference/value.rs",
|
||||
line: 1,
|
||||
column: 1,
|
||||
})?;
|
||||
total = total.checked_add(adjusted).ok_or(&FaultInfo {
|
||||
message: "integer arithmetic fault",
|
||||
source: "reference/batch.rs",
|
||||
line: 2,
|
||||
column: 1,
|
||||
})?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn dispatch_generated(values: &[i64]) -> Result<i64, Fault> {
|
||||
GENERATED_DISPATCH(values.first().copied().unwrap_or(0))
|
||||
}
|
||||
|
||||
fn dispatch_reference(values: &[i64]) -> Result<i64, Fault> {
|
||||
values
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.checked_mul(2)
|
||||
.ok_or_else(fault)
|
||||
}
|
||||
|
||||
fn dispatch_total_reference(values: &[i64]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
let doubled = value.checked_mul(2).ok_or_else(fault)?;
|
||||
total = total.checked_add(doubled).ok_or(&FaultInfo {
|
||||
message: "integer overflow",
|
||||
source: "reference/dispatch.rs",
|
||||
line: 2,
|
||||
column: 1,
|
||||
})?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let values: Vec<i64> = (0..1024).map(|value| value % 17).collect();
|
||||
for values in [&[i64::MAX][..], &[i64::MAX - 1, 1][..], &[i64::MIN][..]] {
|
||||
assert_eq!(
|
||||
GENERATED_MAP(values).map_err(|_| ()),
|
||||
map_reference(values).map_err(|_| ())
|
||||
);
|
||||
assert_eq!(
|
||||
GENERATED_TOTAL(values).map_err(|_| ()),
|
||||
total_reference(values).map_err(|_| ())
|
||||
);
|
||||
assert_eq!(
|
||||
GENERATED_TOTAL(values).map_err(|fault| fault.message),
|
||||
total_reference_distinct_faults(values).map_err(|fault| fault.message)
|
||||
);
|
||||
assert_eq!(
|
||||
dispatch_generated(values).map_err(|_| ()),
|
||||
dispatch_reference(values).map_err(|_| ())
|
||||
);
|
||||
}
|
||||
let projection = GENERATED_TOTAL(&[i64::MAX]).unwrap_err();
|
||||
let sum = GENERATED_TOTAL(&[i64::MAX - 1, 1]).unwrap_err();
|
||||
assert_ne!(projection.source, sum.source);
|
||||
assert_ne!(
|
||||
total_reference_distinct_faults(&[i64::MAX])
|
||||
.unwrap_err()
|
||||
.source,
|
||||
total_reference_distinct_faults(&[i64::MAX - 1, 1])
|
||||
.unwrap_err()
|
||||
.source
|
||||
);
|
||||
for values in [&[i64::MAX][..], &[i64::MAX / 2, 1][..], &[i64::MIN][..]] {
|
||||
assert_eq!(
|
||||
GENERATED_DISPATCH_TOTAL(values).map_err(|fault| fault.message),
|
||||
dispatch_total_reference(values).map_err(|fault| fault.message)
|
||||
);
|
||||
}
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
compare(
|
||||
"package_map",
|
||||
&values,
|
||||
20000,
|
||||
GENERATED_MAP,
|
||||
map_reference,
|
||||
|value| value.iter().map(|value| *value as u64).sum(),
|
||||
);
|
||||
compare(
|
||||
"package_total",
|
||||
&values,
|
||||
20000,
|
||||
GENERATED_TOTAL,
|
||||
total_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"package_total_distinct_faults",
|
||||
&values,
|
||||
20000,
|
||||
GENERATED_TOTAL,
|
||||
total_reference_distinct_faults,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"package_dispatch",
|
||||
&[21],
|
||||
3000000,
|
||||
dispatch_generated,
|
||||
dispatch_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"package_dispatch_batch",
|
||||
&values,
|
||||
20000,
|
||||
GENERATED_DISPATCH_TOTAL,
|
||||
dispatch_total_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
// 隔离同一受检翻倍在内联表达式、Rust 函数边界和 Dever 生成路径上的差异。
|
||||
// 共享故障元数据以排除错误对象大小和来源内容,原 packages 参照保持不变。
|
||||
|
||||
fn shared_fault() -> Fault {
|
||||
GENERATED_DISPATCH(i64::MAX).unwrap_err()
|
||||
}
|
||||
|
||||
fn dispatch_generated(values: &[i64]) -> Result<i64, Fault> {
|
||||
GENERATED_DISPATCH(values.first().copied().unwrap_or(0))
|
||||
}
|
||||
|
||||
fn adjust_generated(values: &[i64]) -> Result<i64, Fault> {
|
||||
GENERATED_ADJUST(values.first().copied().unwrap_or(0))
|
||||
}
|
||||
|
||||
fn inline_reference(values: &[i64]) -> Result<i64, Fault> {
|
||||
values
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.checked_mul(2)
|
||||
.ok_or_else(shared_fault)
|
||||
}
|
||||
|
||||
fn propagated_reference(values: &[i64]) -> Result<i64, Fault> {
|
||||
let answer = values
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.checked_mul(2)
|
||||
.ok_or_else(shared_fault)?;
|
||||
Ok(answer)
|
||||
}
|
||||
|
||||
fn helper(value: i64) -> Result<i64, Fault> {
|
||||
value.checked_mul(2).ok_or_else(shared_fault)
|
||||
}
|
||||
|
||||
fn helper_reference(values: &[i64]) -> Result<i64, Fault> {
|
||||
helper(values.first().copied().unwrap_or(0))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn early_helper(value: i64) -> Result<i64, Fault> {
|
||||
value.checked_mul(2).ok_or_else(shared_fault)
|
||||
}
|
||||
|
||||
fn early_helper_reference(values: &[i64]) -> Result<i64, Fault> {
|
||||
early_helper(values.first().copied().unwrap_or(0))
|
||||
}
|
||||
|
||||
fn independent_helper(value: i64) -> Result<i64, Fault> {
|
||||
value.checked_mul(2).ok_or(&FaultInfo {
|
||||
message: "integer arithmetic fault",
|
||||
source: "reference.dever",
|
||||
line: 1,
|
||||
column: 1,
|
||||
})
|
||||
}
|
||||
|
||||
fn independent_helper_reference(values: &[i64]) -> Result<i64, Fault> {
|
||||
independent_helper(values.first().copied().unwrap_or(0))
|
||||
}
|
||||
|
||||
fn propagated_helper(value: i64) -> Result<i64, Fault> {
|
||||
let answer = value.checked_mul(2).ok_or_else(shared_fault)?;
|
||||
Ok(answer)
|
||||
}
|
||||
|
||||
fn propagated_helper_reference(values: &[i64]) -> Result<i64, Fault> {
|
||||
propagated_helper(values.first().copied().unwrap_or(0))
|
||||
}
|
||||
|
||||
fn batch_generated(values: &[i64]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
total = total
|
||||
.checked_add(GENERATED_DISPATCH(*value)?)
|
||||
.ok_or_else(shared_fault)?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn batch_reference(values: &[i64]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for value in values {
|
||||
let doubled = value.checked_mul(2).ok_or_else(shared_fault)?;
|
||||
total = total.checked_add(doubled).ok_or_else(shared_fault)?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let operations = [
|
||||
dispatch_generated as fn(&[i64]) -> Result<i64, Fault>,
|
||||
adjust_generated,
|
||||
inline_reference,
|
||||
propagated_reference,
|
||||
helper_reference,
|
||||
early_helper_reference,
|
||||
propagated_helper_reference,
|
||||
];
|
||||
for value in [
|
||||
i64::MIN,
|
||||
i64::MIN / 2 - 1,
|
||||
i64::MIN / 2,
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
21,
|
||||
i64::MAX / 2,
|
||||
i64::MAX / 2 + 1,
|
||||
i64::MAX,
|
||||
] {
|
||||
for operation in operations {
|
||||
assert_eq!(
|
||||
operation(&[value]).map_err(|fault| fault.to_string()),
|
||||
inline_reference(&[value]).map_err(|fault| fault.to_string())
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
independent_helper_reference(&[value]).map_err(|_| ()),
|
||||
inline_reference(&[value]).map_err(|_| ())
|
||||
);
|
||||
}
|
||||
for (name, operation) in [
|
||||
(
|
||||
"dispatch",
|
||||
dispatch_generated as fn(&[i64]) -> Result<i64, Fault>,
|
||||
),
|
||||
("adjust", adjust_generated),
|
||||
("propagated", propagated_reference),
|
||||
("helper", helper_reference),
|
||||
("early_helper", early_helper_reference),
|
||||
("independent_helper", independent_helper_reference),
|
||||
("propagated_helper", propagated_helper_reference),
|
||||
] {
|
||||
eprintln!(
|
||||
"{name}_equals_dispatch={}",
|
||||
std::ptr::fn_addr_eq(
|
||||
operation,
|
||||
dispatch_generated as fn(&[i64]) -> Result<i64, Fault>
|
||||
)
|
||||
);
|
||||
}
|
||||
let values = (0..256).map(|index| index * 17 - 2000).collect::<Vec<_>>();
|
||||
for input in [
|
||||
vec![],
|
||||
vec![i64::MAX],
|
||||
vec![i64::MIN],
|
||||
vec![i64::MAX / 2, 1],
|
||||
values.clone(),
|
||||
] {
|
||||
assert_eq!(
|
||||
batch_generated(&input).map_err(|fault| fault.to_string()),
|
||||
batch_reference(&input).map_err(|fault| fault.to_string())
|
||||
);
|
||||
}
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
compare(
|
||||
"scalar_matched_hot_path",
|
||||
&[21],
|
||||
3000000,
|
||||
adjust_generated,
|
||||
independent_helper_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"scalar_inline",
|
||||
&[21],
|
||||
3000000,
|
||||
dispatch_generated,
|
||||
inline_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare_named(
|
||||
"scalar_direct",
|
||||
&[21],
|
||||
3000000,
|
||||
dispatch_generated,
|
||||
adjust_generated,
|
||||
|value| *value as u64,
|
||||
"direct",
|
||||
);
|
||||
compare(
|
||||
"scalar_propagated",
|
||||
&[21],
|
||||
3000000,
|
||||
dispatch_generated,
|
||||
propagated_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"scalar_helper",
|
||||
&[21],
|
||||
3000000,
|
||||
dispatch_generated,
|
||||
helper_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"scalar_early_helper",
|
||||
&[21],
|
||||
3000000,
|
||||
dispatch_generated,
|
||||
early_helper_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"scalar_independent_helper",
|
||||
&[21],
|
||||
3000000,
|
||||
dispatch_generated,
|
||||
independent_helper_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"scalar_propagated_helper",
|
||||
&[21],
|
||||
3000000,
|
||||
dispatch_generated,
|
||||
propagated_helper_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"scalar_batch",
|
||||
&values,
|
||||
30000,
|
||||
batch_generated,
|
||||
batch_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// 使用实际三条调用路径记录接受/拒绝,不把 Axum 默认行为误当成 Dever 合同。
|
||||
fn query_checks() {
|
||||
let suffix = "&count=-7&enabled=true&ratio=1.25";
|
||||
let mut cases = vec![
|
||||
("plain", format!("name=Alice{suffix}")),
|
||||
("empty_text", format!("name={suffix}")),
|
||||
("unicode", format!("name=%E9%9B%AA+test{suffix}")),
|
||||
("escaped_separator", format!("name=a%26b%3Dc{suffix}")),
|
||||
("escaped_percent", format!("name=%25{suffix}")),
|
||||
(
|
||||
"valid_replacement_character",
|
||||
format!("name=%EF%BF%BD{suffix}"),
|
||||
),
|
||||
("duplicate_required", format!("name=a&name=b{suffix}")),
|
||||
("duplicate_optional", format!("name=a{suffix}&tag=x&tag=y")),
|
||||
("escaped_duplicate", format!("name=a&%6Eame=b{suffix}")),
|
||||
("unknown", format!("name=a{suffix}&extra=x")),
|
||||
("missing_required", "name=a&enabled=true&ratio=1.25".into()),
|
||||
("optional_present", format!("name=a{suffix}&tag=x")),
|
||||
("empty_pairs", format!("&&name=a&&{suffix}&")),
|
||||
("missing_equals", format!("name{suffix}")),
|
||||
("truncated_percent", format!("name=%{suffix}")),
|
||||
("short_percent", format!("name=%A{suffix}")),
|
||||
("invalid_hex", format!("name=%GG{suffix}")),
|
||||
("invalid_utf8", format!("name=%FF{suffix}")),
|
||||
("overlong_utf8", format!("name=%C0%AF{suffix}")),
|
||||
("encoded_surrogate", format!("name=%ED%A0%80{suffix}")),
|
||||
("nul", format!("name=%00{suffix}")),
|
||||
];
|
||||
for (name, value) in [
|
||||
("int_min", "-9223372036854775808"),
|
||||
("int_max", "9223372036854775807"),
|
||||
("int_overflow", "9223372036854775808"),
|
||||
("int_fraction", "1.5"),
|
||||
("int_leading_zero", "007"),
|
||||
("int_encoded_plus", "%2B7"),
|
||||
] {
|
||||
cases.push((
|
||||
name,
|
||||
format!("name=a&count={value}&enabled=true&ratio=1.25"),
|
||||
));
|
||||
}
|
||||
for (name, value) in [
|
||||
("float_nan", "NaN"),
|
||||
("float_inf", "inf"),
|
||||
("float_overflow", "1e999"),
|
||||
("float_negative_zero", "-0"),
|
||||
("float_exponent", "1.5e3"),
|
||||
] {
|
||||
cases.push((name, format!("name=a&count=7&enabled=true&ratio={value}")));
|
||||
}
|
||||
cases.push(("invalid_bool", "name=a&count=7&enabled=1&ratio=1.25".into()));
|
||||
println!("case,dever,axum,serde_urlencoded,equal_values");
|
||||
for (name, query) in cases {
|
||||
let input = [query_input(&query)];
|
||||
let actual = runtime_query::<QueryArguments>(&input);
|
||||
let axum = axum_query::<QueryArguments>(&input);
|
||||
let serde = serde_query::<QueryArguments>(&input);
|
||||
let equal = match (&actual, &axum, &serde) {
|
||||
(Ok(actual), Ok(axum), Ok(serde)) => actual == axum && actual == serde,
|
||||
(Err(_), Err(_), Err(_)) => true,
|
||||
_ => false,
|
||||
};
|
||||
println!(
|
||||
"{name},{},{},{},{equal}",
|
||||
actual.is_ok(),
|
||||
axum.is_ok(),
|
||||
serde.is_ok()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// 对比实际运行库、Axum Query 和其底层 serde_urlencoded;不包含网络或路由耗时。
|
||||
// 默认库的错误输入行为另行检查,合法输入计时不能证明完整边界语义等价。
|
||||
use axum::{extract::Query, http::Uri};
|
||||
use dever_runtime::api::{self, Request};
|
||||
use serde::{Deserialize, de::DeserializeOwned};
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct QueryArguments {
|
||||
name: String,
|
||||
count: i64,
|
||||
enabled: bool,
|
||||
ratio: f64,
|
||||
tag: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct WideArguments {
|
||||
field0: String,
|
||||
field1: String,
|
||||
field2: String,
|
||||
field3: String,
|
||||
field4: String,
|
||||
field5: String,
|
||||
field6: String,
|
||||
field7: String,
|
||||
field8: String,
|
||||
field9: String,
|
||||
field10: String,
|
||||
field11: String,
|
||||
field12: String,
|
||||
field13: String,
|
||||
field14: String,
|
||||
field15: String,
|
||||
field16: String,
|
||||
field17: String,
|
||||
field18: String,
|
||||
field19: String,
|
||||
field20: String,
|
||||
field21: String,
|
||||
field22: String,
|
||||
field23: String,
|
||||
field24: String,
|
||||
field25: String,
|
||||
field26: String,
|
||||
field27: String,
|
||||
field28: String,
|
||||
field29: String,
|
||||
field30: String,
|
||||
field31: String,
|
||||
}
|
||||
|
||||
struct QueryInput {
|
||||
request: Request,
|
||||
uri: Uri,
|
||||
}
|
||||
|
||||
fn query_fault(_: impl std::fmt::Display) -> Fault {
|
||||
&FaultInfo {
|
||||
message: "invalid query",
|
||||
source: "query-comparison",
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_query<Arguments: DeserializeOwned>(
|
||||
values: &[QueryInput],
|
||||
) -> Result<Option<Arguments>, Fault> {
|
||||
let Some(input) = values.first() else {
|
||||
return Ok(None);
|
||||
};
|
||||
api::input(&input.request)
|
||||
.map_err(query_fault)?
|
||||
.decode(dever_runtime::wire::decode)
|
||||
.map(Some)
|
||||
.map_err(query_fault)
|
||||
}
|
||||
|
||||
fn axum_query<Arguments: DeserializeOwned>(
|
||||
values: &[QueryInput],
|
||||
) -> Result<Option<Arguments>, Fault> {
|
||||
let Some(input) = values.first() else {
|
||||
return Ok(None);
|
||||
};
|
||||
Query::<Arguments>::try_from_uri(&input.uri)
|
||||
.map(|Query(arguments)| Some(arguments))
|
||||
.map_err(query_fault)
|
||||
}
|
||||
|
||||
fn serde_query<Arguments: DeserializeOwned>(
|
||||
values: &[QueryInput],
|
||||
) -> Result<Option<Arguments>, Fault> {
|
||||
let Some(input) = values.first() else {
|
||||
return Ok(None);
|
||||
};
|
||||
serde_urlencoded::from_str(input.uri.query().unwrap_or_default())
|
||||
.map(Some)
|
||||
.map_err(query_fault)
|
||||
}
|
||||
|
||||
fn query_input(query: &str) -> QueryInput {
|
||||
let target = format!("/test?{query}");
|
||||
QueryInput {
|
||||
uri: target.parse().unwrap(),
|
||||
request: axum::http::Request::builder()
|
||||
.uri(target)
|
||||
.body(Default::default())
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn query_comparison<Arguments: DeserializeOwned + PartialEq + std::fmt::Debug>(
|
||||
name: &str,
|
||||
query: &str,
|
||||
count: usize,
|
||||
) {
|
||||
let input = [query_input(query)];
|
||||
for (label, decoder) in [
|
||||
("axum", axum_query::<Arguments> as fn(&[QueryInput]) -> _),
|
||||
(
|
||||
"serde_urlencoded",
|
||||
serde_query::<Arguments> as fn(&[QueryInput]) -> _,
|
||||
),
|
||||
] {
|
||||
compare_named(
|
||||
&format!("query_{name}_{label}"),
|
||||
&input,
|
||||
count,
|
||||
runtime_query::<Arguments>,
|
||||
decoder,
|
||||
|output| u64::from(output.is_some()),
|
||||
label,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if std::env::args().any(|argument| argument == "--check") {
|
||||
query_checks();
|
||||
return;
|
||||
}
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
for (name, value, count) in [
|
||||
("plain", "Alice".to_owned(), 20_000),
|
||||
("unicode", "%E9%9B%AA+test".to_owned(), 20_000),
|
||||
("escaped", "%22%5C%00%26%3D%25".repeat(8), 10_000),
|
||||
("long", "value%2B%E9%9B%AA".repeat(256), 1_000),
|
||||
] {
|
||||
query_comparison::<QueryArguments>(
|
||||
name,
|
||||
&format!("name={value}&count=-7&enabled=true&ratio=1.25&tag=demo"),
|
||||
count,
|
||||
);
|
||||
}
|
||||
for (name, value, count) in [
|
||||
("wide_plain", "testvalue".to_owned(), 2_000),
|
||||
("wide_escaped", "value%2B%E9%9B%AA".repeat(8), 500),
|
||||
] {
|
||||
let query = (0..32)
|
||||
.map(|index| format!("field{index}={value}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("&");
|
||||
query_comparison::<WideArguments>(name, &query, count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
fn integer_reference(value: i64) -> Result<i64, Fault> {
|
||||
let failure = &FaultInfo {
|
||||
source: "reference",
|
||||
line: 1,
|
||||
column: 1,
|
||||
message: "integer arithmetic fault",
|
||||
};
|
||||
value
|
||||
.checked_mul(3)
|
||||
.ok_or(failure)?
|
||||
.checked_add(7)
|
||||
.ok_or(failure)?
|
||||
.checked_div(2)
|
||||
.ok_or(failure)
|
||||
}
|
||||
|
||||
fn list_reference(values: &[i64]) -> Result<i64, Fault> {
|
||||
let failure = &FaultInfo {
|
||||
source: "reference",
|
||||
line: 1,
|
||||
column: 1,
|
||||
message: "integer overflow",
|
||||
};
|
||||
values
|
||||
.iter()
|
||||
.try_fold(0i64, |sum, value| sum.checked_add(*value))
|
||||
.ok_or(failure)
|
||||
}
|
||||
|
||||
fn text_reference(first: &str, second: &str) -> Result<String, Fault> {
|
||||
let mut result = String::with_capacity(first.len() + second.len());
|
||||
result.push_str(first);
|
||||
result.push_str(second);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn sample(
|
||||
name: &str,
|
||||
sample: usize,
|
||||
implementation: &str,
|
||||
count: usize,
|
||||
mut operation: impl FnMut(usize) -> usize,
|
||||
) {
|
||||
let start = Instant::now();
|
||||
let mut checksum = 0usize;
|
||||
for position in 0..count {
|
||||
checksum = checksum.wrapping_add(black_box(operation(black_box(position))));
|
||||
}
|
||||
println!(
|
||||
"{name},{sample},{implementation},{},{checksum}",
|
||||
start.elapsed().as_nanos()
|
||||
);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let values: Vec<i64> = (0..1024)
|
||||
.map(|position| (position * 37 % 100) as i64)
|
||||
.collect();
|
||||
let text = "雪山远处有树林,近处有人撑伞。".repeat(4);
|
||||
for value in [i64::MIN, -123, -1, 0, 100, i64::MAX] {
|
||||
assert_eq!(
|
||||
GENERATED_INTEGER(value).map_err(|_| ()),
|
||||
integer_reference(value).map_err(|_| ())
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
GENERATED_LIST(&values).unwrap(),
|
||||
list_reference(&values).unwrap()
|
||||
);
|
||||
assert!(GENERATED_LIST(&[i64::MAX, 1]).is_err());
|
||||
assert!(list_reference(&[i64::MAX, 1]).is_err());
|
||||
assert_eq!(
|
||||
GENERATED_TEXT(&text, &text).unwrap(),
|
||||
text_reference(&text, &text).unwrap()
|
||||
);
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
// 求和两侧共用计时函数,保留同函数 control,避免把调用端布局计入生成器差距。
|
||||
compare(
|
||||
"list_sum_1024",
|
||||
&values,
|
||||
40_000,
|
||||
GENERATED_LIST,
|
||||
list_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
for iteration in 0..9 {
|
||||
for generated in if iteration % 2 == 0 {
|
||||
[true, false]
|
||||
} else {
|
||||
[false, true]
|
||||
} {
|
||||
let implementation = if generated { "dever" } else { "rust" };
|
||||
if generated {
|
||||
sample(
|
||||
"checked_int",
|
||||
iteration,
|
||||
implementation,
|
||||
2_000_000,
|
||||
|position| GENERATED_INTEGER(position as i64).unwrap() as usize,
|
||||
);
|
||||
sample("text_join", iteration, implementation, 300_000, |_| {
|
||||
black_box(GENERATED_TEXT(black_box(&text), black_box(&text)).unwrap()).len()
|
||||
});
|
||||
} else {
|
||||
sample(
|
||||
"checked_int",
|
||||
iteration,
|
||||
implementation,
|
||||
2_000_000,
|
||||
|position| integer_reference(position as i64).unwrap() as usize,
|
||||
);
|
||||
sample("text_join", iteration, implementation, 300_000, |_| {
|
||||
black_box(text_reference(black_box(&text), black_box(&text)).unwrap()).len()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// 文件创建和清理位于计时之外;同一文件先预热,不把页缓存当作编译器收益。
|
||||
struct ResourceCase {
|
||||
path: String,
|
||||
limit: i64,
|
||||
}
|
||||
|
||||
struct ResourceDirectory(std::path::PathBuf);
|
||||
|
||||
impl Drop for ResourceDirectory {
|
||||
fn drop(&mut self) {
|
||||
fs::remove_dir_all(&self.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_fault(_: impl std::fmt::Display) -> Fault {
|
||||
&FaultInfo {
|
||||
source: "resource-reference",
|
||||
line: 1,
|
||||
column: 1,
|
||||
message: "resource failure",
|
||||
}
|
||||
}
|
||||
|
||||
fn generated_open(values: &[ResourceCase]) -> Result<bool, Fault> {
|
||||
values.first().map_or(Ok(true), |value| {
|
||||
GENERATED_OPEN(&value.path).map_err(resource_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn reference_open(values: &[ResourceCase]) -> Result<bool, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(true);
|
||||
};
|
||||
let file = ReferenceFile::open(&value.path).map_err(resource_fault)?;
|
||||
let alias = file.clone();
|
||||
alias.close().map_err(resource_fault)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn generated_read(values: &[ResourceCase]) -> Result<Vec<u8>, Fault> {
|
||||
values.first().map_or(Ok(Vec::new()), |value| {
|
||||
GENERATED_READ(&value.path, value.limit).map_err(resource_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn reference_read(values: &[ResourceCase]) -> Result<Vec<u8>, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let file = ReferenceFile::open(&value.path).map_err(resource_fault)?;
|
||||
let bytes = file
|
||||
.read(value.limit)
|
||||
.map_err(resource_fault)?
|
||||
.unwrap_or_default();
|
||||
file.close().map_err(resource_fault)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn generated_stream(values: &[ResourceCase]) -> Result<i64, Fault> {
|
||||
values.first().map_or(Ok(0), |value| {
|
||||
GENERATED_STREAM(&value.path, value.limit).map_err(resource_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn reference_stream(values: &[ResourceCase]) -> Result<i64, Fault> {
|
||||
values.first().map_or(Ok(0), |value| {
|
||||
reference_consume(&value.path, value.limit, false).map_err(resource_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn generated_partial(values: &[ResourceCase]) -> Result<i64, Fault> {
|
||||
values.first().map_or(Ok(0), |value| {
|
||||
GENERATED_PARTIAL(&value.path, value.limit).map_err(resource_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn reference_partial(values: &[ResourceCase]) -> Result<i64, Fault> {
|
||||
values.first().map_or(Ok(0), |value| {
|
||||
reference_consume(&value.path, value.limit, true).map_err(resource_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn generated_closed(values: &[ResourceCase]) -> Result<bool, Fault> {
|
||||
Ok(values
|
||||
.first()
|
||||
.is_none_or(|value| GENERATED_CLOSED(&value.path).is_err()))
|
||||
}
|
||||
|
||||
fn reference_closed(values: &[ResourceCase]) -> Result<bool, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(true);
|
||||
};
|
||||
let file = ReferenceFile::open(&value.path).map_err(resource_fault)?;
|
||||
let stream = ReferenceStream::new(&file, 64).map_err(resource_fault)?;
|
||||
file.close().map_err(resource_fault)?;
|
||||
Ok(stream.next().unwrap().is_err())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let path =
|
||||
std::env::temp_dir().join(format!("dever-resource-performance-{}", std::process::id()));
|
||||
fs::create_dir(&path).unwrap();
|
||||
let directory = ResourceDirectory(path);
|
||||
let path = directory.0.join("input");
|
||||
let bytes = (0..65537)
|
||||
.map(|index| (index % 251) as u8)
|
||||
.collect::<Vec<_>>();
|
||||
fs::write(&path, &bytes).unwrap();
|
||||
let path = path.to_str().unwrap().to_owned();
|
||||
assert_eq!(GENERATED_READ(&path, 1_000_000).unwrap(), bytes);
|
||||
for limit in [0, -1, i64::MAX] {
|
||||
assert!(GENERATED_READ(&path, limit).is_err());
|
||||
assert!(ReferenceFile::open(&path).unwrap().read(limit).is_err());
|
||||
}
|
||||
let expected: i64 = bytes.iter().map(|byte| i64::from(*byte)).sum();
|
||||
for limit in [64, 4096] {
|
||||
assert_eq!(GENERATED_STREAM(&path, limit).unwrap(), expected);
|
||||
assert_eq!(GENERATED_PARTIAL(&path, limit).unwrap(), expected);
|
||||
}
|
||||
let closed = GENERATED_CLOSED(&path).unwrap_err();
|
||||
assert!(
|
||||
closed.to_string().contains("ReadEvent.Failed")
|
||||
&& format!("{closed:?}").contains("file is closed"),
|
||||
"{closed}"
|
||||
);
|
||||
let file = ReferenceFile::open(&path).unwrap();
|
||||
let alias = file.clone();
|
||||
assert_eq!(file.read(2).unwrap().unwrap(), bytes[..2]);
|
||||
assert_eq!(alias.read(2).unwrap().unwrap(), bytes[2..4]);
|
||||
let stream = ReferenceStream::new(&file, 2).unwrap();
|
||||
alias.close().unwrap();
|
||||
assert!(stream.next().unwrap().is_err());
|
||||
assert!(stream.next().is_none());
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
let values = [ResourceCase {
|
||||
path: path.clone(),
|
||||
limit: 64,
|
||||
}];
|
||||
compare(
|
||||
"file_open_close",
|
||||
&values,
|
||||
2000,
|
||||
generated_open,
|
||||
reference_open,
|
||||
|value| u64::from(*value),
|
||||
);
|
||||
compare(
|
||||
"file_read_64",
|
||||
&values,
|
||||
2000,
|
||||
generated_read,
|
||||
reference_read,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"stream_read_64",
|
||||
&values,
|
||||
50,
|
||||
generated_stream,
|
||||
reference_stream,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"stream_resume_64",
|
||||
&values,
|
||||
50,
|
||||
generated_partial,
|
||||
reference_partial,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"stream_closed",
|
||||
&values,
|
||||
2000,
|
||||
generated_closed,
|
||||
reference_closed,
|
||||
|value| u64::from(*value),
|
||||
);
|
||||
let values = [ResourceCase { path, limit: 4096 }];
|
||||
compare(
|
||||
"file_read_4096",
|
||||
&values,
|
||||
1000,
|
||||
generated_read,
|
||||
reference_read,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"stream_read_4096",
|
||||
&values,
|
||||
200,
|
||||
generated_stream,
|
||||
reference_stream,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"stream_resume_4096",
|
||||
&values,
|
||||
200,
|
||||
generated_partial,
|
||||
reference_partial,
|
||||
|value| *value as u64,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// 独立 Rust 参照保留文件的跨线程别名及同步流的共享游标,不调用 Dever I/O。
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
fs,
|
||||
io::Read,
|
||||
rc::Rc,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ReferenceFile(Arc<Mutex<Option<fs::File>>>);
|
||||
|
||||
impl ReferenceFile {
|
||||
fn open(path: &str) -> Result<Self, String> {
|
||||
fs::File::open(path)
|
||||
.map(|file| Self(Arc::new(Mutex::new(Some(file)))))
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn read(&self, limit: i64) -> Result<Option<Vec<u8>>, String> {
|
||||
let limit = usize::try_from(limit)
|
||||
.ok()
|
||||
.filter(|limit| *limit > 0)
|
||||
.ok_or("read limit must be positive")?;
|
||||
let mut guard = self.0.lock().map_err(|_| "file lock poisoned")?;
|
||||
let file = guard.as_mut().ok_or("file is closed")?;
|
||||
let mut bytes = Vec::new();
|
||||
bytes
|
||||
.try_reserve_exact(limit)
|
||||
.map_err(|error| error.to_string())?;
|
||||
bytes.resize(limit, 0);
|
||||
let count = file.read(&mut bytes).map_err(|error| error.to_string())?;
|
||||
bytes.truncate(count);
|
||||
Ok((count != 0).then_some(bytes))
|
||||
}
|
||||
|
||||
fn close(&self) -> Result<(), String> {
|
||||
drop(
|
||||
self.0
|
||||
.lock()
|
||||
.map_err(|_| "file lock poisoned")?
|
||||
.take()
|
||||
.ok_or("file is closed")?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ReferenceStream(Rc<RefCell<Option<(ReferenceFile, i64)>>>);
|
||||
|
||||
impl ReferenceStream {
|
||||
fn new(file: &ReferenceFile, limit: i64) -> Result<Self, String> {
|
||||
usize::try_from(limit)
|
||||
.ok()
|
||||
.filter(|limit| *limit > 0)
|
||||
.ok_or("read limit must be positive")?;
|
||||
if file.0.lock().map_err(|_| "file lock poisoned")?.is_none() {
|
||||
return Err("file is closed".into());
|
||||
}
|
||||
Ok(Self(Rc::new(RefCell::new(Some((file.clone(), limit))))))
|
||||
}
|
||||
|
||||
fn next(&self) -> Option<Result<Vec<u8>, String>> {
|
||||
let mut source = self.0.borrow_mut();
|
||||
let (file, limit) = source.as_ref()?;
|
||||
match file.read(*limit) {
|
||||
Ok(Some(bytes)) => Some(Ok(bytes)),
|
||||
Ok(None) => {
|
||||
*source = None;
|
||||
None
|
||||
}
|
||||
Err(error) => {
|
||||
*source = None;
|
||||
Some(Err(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&self) {
|
||||
*self.0.borrow_mut() = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn reference_count(bytes: &[u8], total: i64) -> Result<i64, String> {
|
||||
bytes.iter().try_fold(total, |sum, byte| {
|
||||
sum.checked_add(i64::from(*byte))
|
||||
.ok_or_else(|| "integer overflow".into())
|
||||
})
|
||||
}
|
||||
|
||||
fn reference_consume(path: &str, limit: i64, partial: bool) -> Result<i64, String> {
|
||||
let file = ReferenceFile::open(path)?;
|
||||
let stream = ReferenceStream::new(&file, limit)?;
|
||||
let alias = partial.then(|| stream.clone());
|
||||
let mut total = 0;
|
||||
if partial {
|
||||
if let Some(bytes) = stream.next() {
|
||||
total = reference_count(&bytes?, total)?;
|
||||
}
|
||||
}
|
||||
while let Some(bytes) = alias.as_ref().unwrap_or(&stream).next() {
|
||||
total = reference_count(&bytes?, total)?;
|
||||
}
|
||||
if partial {
|
||||
stream.close();
|
||||
alias.as_ref().unwrap().close();
|
||||
}
|
||||
file.close()?;
|
||||
Ok(total)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use std::hint::black_box;
|
||||
use std::time::Instant;
|
||||
|
||||
#[inline(never)]
|
||||
fn measure<const BORROW: bool, Input: ?Sized, Output: std::fmt::Debug>(operation: fn(&Input) -> Result<Output, Fault>, values: &Input, count: usize, score: fn(&Output) -> u64) -> (u128, u64) {
|
||||
let operation = black_box(operation);
|
||||
let start = Instant::now();
|
||||
let mut checksum = 0u64;
|
||||
for _ in 0..count {
|
||||
if BORROW {
|
||||
// 保留 Result,通过借用观察输出,隔离取出所有权时的元数据搬运。
|
||||
// 屏障只接收计分值;传入内部引用会使计时环重读 Result 标签并额外访存。
|
||||
let value = operation(black_box(values));
|
||||
checksum = checksum.wrapping_add(black_box(score(value.as_ref().unwrap())));
|
||||
} else {
|
||||
let value = black_box(operation(black_box(values)).unwrap());
|
||||
checksum = checksum.wrapping_add(score(&value));
|
||||
}
|
||||
}
|
||||
(start.elapsed().as_nanos(), checksum)
|
||||
}
|
||||
|
||||
fn compare<Input, Output: PartialEq + std::fmt::Debug>(name: &str, values: &[Input], count: usize, generated: fn(&[Input]) -> Result<Output, Fault>, reference: fn(&[Input]) -> Result<Output, Fault>, score: fn(&Output) -> u64) {
|
||||
compare_named(name, values, count, generated, reference, score, "rust");
|
||||
}
|
||||
|
||||
fn compare_named<Input, Output: PartialEq + std::fmt::Debug>(name: &str, values: &[Input], count: usize, generated: fn(&[Input]) -> Result<Output, Fault>, reference: fn(&[Input]) -> Result<Output, Fault>, score: fn(&Output) -> u64, reference_name: &str) {
|
||||
compare_output::<false, _, _>(name, values, &[], count, generated, reference, score, reference_name);
|
||||
}
|
||||
|
||||
fn compare_borrowed<Input, Output: PartialEq + std::fmt::Debug>(name: &str, values: &[Input], count: usize, generated: fn(&[Input]) -> Result<Output, Fault>, reference: fn(&[Input]) -> Result<Output, Fault>, score: fn(&Output) -> u64) {
|
||||
compare_output::<true, _, _>(&format!("{name}_borrowed"), values, &[], count, generated, reference, score, "rust");
|
||||
}
|
||||
|
||||
fn compare_bytes<Output: PartialEq + std::fmt::Debug>(name: &str, values: &dever_runtime::Bytes, count: usize, generated: fn(&dever_runtime::Bytes) -> Result<Output, Fault>, reference: fn(&dever_runtime::Bytes) -> Result<Output, Fault>, score: fn(&Output) -> u64) {
|
||||
compare_output::<false, _, _>(name, values, &dever_runtime::Bytes::new(), count, generated, reference, score, "rust");
|
||||
}
|
||||
|
||||
fn compare_output<const BORROW: bool, Input: ?Sized, Output: PartialEq + std::fmt::Debug>(name: &str, values: &Input, empty: &Input, count: usize, generated: fn(&Input) -> Result<Output, Fault>, reference: fn(&Input) -> Result<Output, Fault>, score: fn(&Output) -> u64, reference_name: &str) {
|
||||
assert_eq!(generated(values).unwrap(), reference(values).unwrap());
|
||||
assert_eq!(generated(empty).unwrap(), reference(empty).unwrap());
|
||||
for operation in [generated, reference] { black_box(measure::<BORROW, _, _>(operation, values, 2000, score)); }
|
||||
for sample in 0..15 {
|
||||
// 每个标签各占首、中、末位五次,并交替正反顺序,避免固定前后关系。
|
||||
let mut order = [("dever", generated), (reference_name, reference), ("control", generated)];
|
||||
order.rotate_left(sample % 3);
|
||||
if sample / 3 % 2 == 1 { order.swap(0, 1); }
|
||||
let mut expected = None;
|
||||
for (implementation, operation) in order {
|
||||
let (elapsed, checksum) = measure::<BORROW, _, _>(operation, values, count, score);
|
||||
if let Some(expected) = expected { assert_eq!(checksum, expected); }
|
||||
expected = Some(checksum);
|
||||
println!("{name},{sample},{implementation},{elapsed},{checksum}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
use indexmap::IndexMap;
|
||||
|
||||
struct Lookup {
|
||||
generated: Map<i64, i64>,
|
||||
reference: IndexMap<i64, i64>,
|
||||
keys: Vec<i64>,
|
||||
}
|
||||
|
||||
struct TextLookup {
|
||||
generated: Map<String, i64>,
|
||||
reference: IndexMap<String, i64>,
|
||||
keys: Vec<String>,
|
||||
}
|
||||
|
||||
fn text_lookup_generated(values: &[TextLookup]) -> Result<i64, Fault> {
|
||||
values.first().map_or(Ok(0), |value| {
|
||||
GENERATED_TEXT_LOOKUP(&value.generated, &value.keys)
|
||||
})
|
||||
}
|
||||
|
||||
fn text_lookup_reference(values: &[TextLookup]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
let mut total = 0i64;
|
||||
for key in &value.keys {
|
||||
total = total
|
||||
.checked_add(value.reference.get(key.as_str()).copied().unwrap_or(0))
|
||||
.ok_or_else(overflow)?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn text_lookup_same_table(values: &[TextLookup]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
let mut total = 0i64;
|
||||
for key in &value.keys {
|
||||
total = total
|
||||
.checked_add(value.generated.get(key.as_str()).copied().unwrap_or(0))
|
||||
.ok_or_else(overflow)?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn text_lookup_comparisons() {
|
||||
let reference_first = match std::env::var("DEVER_BENCH_MAP_ORDER")
|
||||
.as_deref()
|
||||
.unwrap_or("dever-first")
|
||||
{
|
||||
"dever-first" => false,
|
||||
"rust-first" => true,
|
||||
order => panic!("unknown table construction order: {order}"),
|
||||
};
|
||||
for count in [512, 1024, 2048, 4096] {
|
||||
let keys: Vec<_> = (0..count)
|
||||
.map(|index| format!("lookup-key-{index}"))
|
||||
.collect();
|
||||
let (mut generated, mut reference) = if reference_first {
|
||||
let reference = IndexMap::with_capacity(count);
|
||||
(Map::with_capacity(count), reference)
|
||||
} else {
|
||||
(Map::with_capacity(count), IndexMap::with_capacity(count))
|
||||
};
|
||||
for key in &keys {
|
||||
if reference_first {
|
||||
reference.insert(key.clone(), 120);
|
||||
generated.insert(key.clone(), 120);
|
||||
} else {
|
||||
generated.insert(key.clone(), 120);
|
||||
reference.insert(key.clone(), 120);
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
GENERATED_TEXT_LOOKUP(&generated, &["missing".to_owned()]).unwrap(),
|
||||
0
|
||||
);
|
||||
let values = [TextLookup {
|
||||
generated,
|
||||
reference,
|
||||
keys,
|
||||
}];
|
||||
compare(
|
||||
&format!("map_text_lookup_{count}"),
|
||||
&values,
|
||||
200,
|
||||
text_lookup_generated,
|
||||
text_lookup_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
// 同表只隔离生成层;独立 IndexMap 参照不能被这个控制组替代。
|
||||
compare_named(
|
||||
&format!("map_text_lookup_{count}_same_table"),
|
||||
&values,
|
||||
200,
|
||||
text_lookup_generated,
|
||||
text_lookup_same_table,
|
||||
|value| *value as u64,
|
||||
"rust_runtime",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn overflow() -> Fault {
|
||||
&FaultInfo {
|
||||
message: "integer overflow",
|
||||
source: "reference.dever",
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_generated(values: &[Lookup]) -> Result<i64, Fault> {
|
||||
match values.first() {
|
||||
Some(value) => GENERATED_LOOKUP(&value.generated, &value.keys),
|
||||
None => Ok(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_reference(values: &[Lookup]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
let mut answer = 0i64;
|
||||
for key in &value.keys {
|
||||
answer = answer
|
||||
.checked_add(value.reference.get(key).copied().unwrap_or(0))
|
||||
.ok_or_else(overflow)?;
|
||||
}
|
||||
Ok(answer)
|
||||
}
|
||||
|
||||
fn lookup_same_table(values: &[Lookup]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
let mut answer = 0i64;
|
||||
for key in &value.keys {
|
||||
answer = answer
|
||||
.checked_add(value.generated.get(key).copied().unwrap_or(0))
|
||||
.ok_or_else(overflow)?;
|
||||
}
|
||||
Ok(answer)
|
||||
}
|
||||
|
||||
fn scan_generated(values: &[Lookup]) -> Result<i64, Fault> {
|
||||
match values.first() {
|
||||
Some(value) => GENERATED_SCAN(&value.generated),
|
||||
None => Ok(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_reference(values: &[Lookup]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
value.reference.values().try_fold(0i64, |total, item| {
|
||||
total.checked_add(*item).ok_or_else(overflow)
|
||||
})
|
||||
}
|
||||
|
||||
fn map_generated(values: &[i64]) -> Result<Vec<(i64, i64)>, Fault> {
|
||||
GENERATED_ENTRIES(values)
|
||||
}
|
||||
|
||||
fn map_reference(values: &[i64]) -> Result<Vec<(i64, i64)>, Fault> {
|
||||
let mut result = IndexMap::new();
|
||||
for value in values {
|
||||
result.insert(*value, *value);
|
||||
}
|
||||
Ok(result.into_iter().collect())
|
||||
}
|
||||
|
||||
fn list_reference(values: &[i64]) -> Result<Vec<i64>, Fault> {
|
||||
let mut result = Vec::new();
|
||||
for value in values {
|
||||
result.push(*value);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn buffer_reference(values: &dever_runtime::Bytes) -> Result<BUFFER_STATE, Fault> {
|
||||
let mut items = Vec::new();
|
||||
let mut count = 0i64;
|
||||
for value in values.iter() {
|
||||
items.push(i64::from(*value));
|
||||
count = count.checked_add(1).ok_or_else(overflow)?;
|
||||
}
|
||||
Ok(BUFFER_STATE {
|
||||
field0: items,
|
||||
field1: count,
|
||||
})
|
||||
}
|
||||
|
||||
fn buffer_inspect_reference(values: &dever_runtime::Bytes) -> Result<BUFFER_STATE, Fault> {
|
||||
let mut items = Vec::new();
|
||||
let mut count = 0i64;
|
||||
for value in values.iter() {
|
||||
count = (items.len() as i64).checked_add(1).ok_or_else(overflow)?;
|
||||
items.push(i64::from(*value));
|
||||
}
|
||||
Ok(BUFFER_STATE {
|
||||
field0: items,
|
||||
field1: count,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_records_reference(values: &[SCALAR_ROW]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for row in values {
|
||||
let projected = row
|
||||
.field0
|
||||
.checked_add(row.field1)
|
||||
.ok_or_else(overflow)?
|
||||
.checked_add(row.field2)
|
||||
.ok_or_else(overflow)?;
|
||||
total = total.checked_add(projected).ok_or_else(overflow)?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let values: Vec<i64> = (0..1024).collect();
|
||||
let lookup = Lookup {
|
||||
generated: GENERATED_BUILD_MAP(&values).unwrap(),
|
||||
reference: values.iter().map(|value| (*value, *value)).collect(),
|
||||
keys: (0..1024).map(|index| (index * 17) % 1152).collect(),
|
||||
};
|
||||
let lookups = [lookup];
|
||||
let overflow_case = [Lookup {
|
||||
generated: GENERATED_BUILD_MAP(&[i64::MAX, 1]).unwrap(),
|
||||
reference: [(i64::MAX, i64::MAX), (1, 1)].into_iter().collect(),
|
||||
keys: vec![i64::MAX, 1],
|
||||
}];
|
||||
assert!(scan_generated(&overflow_case).is_err());
|
||||
assert!(scan_reference(&overflow_case).is_err());
|
||||
assert!(lookup_generated(&overflow_case).is_err());
|
||||
assert!(lookup_reference(&overflow_case).is_err());
|
||||
assert_eq!(
|
||||
map_generated(&[3, 1, 3, 2]).unwrap(),
|
||||
map_reference(&[3, 1, 3, 2]).unwrap()
|
||||
);
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
text_lookup_comparisons();
|
||||
for count in [512, 1024, 2048, 4096] {
|
||||
let bytes = dever_runtime::Bytes::from(vec![120u8; count]);
|
||||
let rows: Vec<_> = bytes
|
||||
.iter()
|
||||
.map(|value| SCALAR_ROW {
|
||||
field0: i64::from(*value),
|
||||
field1: 0,
|
||||
field2: 0,
|
||||
})
|
||||
.collect();
|
||||
compare(
|
||||
&format!("list_record_read_{count}"),
|
||||
&rows,
|
||||
1000,
|
||||
GENERATED_RECORD_READ,
|
||||
read_records_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare_bytes(
|
||||
&format!("record_update_{count}"),
|
||||
&bytes,
|
||||
1000,
|
||||
GENERATED_BUFFER_BUILD,
|
||||
buffer_reference,
|
||||
|value| value.field1 as u64,
|
||||
);
|
||||
compare_bytes(
|
||||
&format!("record_inspect_{count}"),
|
||||
&bytes,
|
||||
1000,
|
||||
GENERATED_BUFFER_INSPECT,
|
||||
buffer_inspect_reference,
|
||||
|value| value.field1 as u64,
|
||||
);
|
||||
}
|
||||
compare(
|
||||
"map_lookup",
|
||||
&lookups,
|
||||
2000,
|
||||
lookup_generated,
|
||||
lookup_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
// 同一张表隔离随机哈希布局;独立 IndexMap 参照仍保留。
|
||||
compare_named(
|
||||
"map_lookup_same_table",
|
||||
&lookups,
|
||||
2000,
|
||||
lookup_generated,
|
||||
lookup_same_table,
|
||||
|value| *value as u64,
|
||||
"rust_runtime",
|
||||
);
|
||||
compare(
|
||||
"map_scan",
|
||||
&lookups,
|
||||
20000,
|
||||
scan_generated,
|
||||
scan_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"map_build",
|
||||
&values[..512],
|
||||
400,
|
||||
map_generated,
|
||||
map_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
"list_build",
|
||||
&values,
|
||||
1000,
|
||||
GENERATED_BUILD_LIST,
|
||||
list_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// 独立使用 Tokio 的取消与排空参照;不调用 Dever 的 Scope、Task 或生成函数。
|
||||
use tokio_util::task::{AbortOnDropHandle, task_tracker::TaskTrackerToken};
|
||||
|
||||
impl NativeTasks {
|
||||
fn tracked_leaf<Value, Work>(
|
||||
&self,
|
||||
tasks: &TaskTracker,
|
||||
call: impl FnOnce() -> Work + Send + 'static,
|
||||
) -> Result<AbortOnDropHandle<(Value, TaskTrackerToken)>, Fault>
|
||||
where
|
||||
Value: Send + 'static,
|
||||
Work: std::future::Future<Output = Value> + Send + 'static,
|
||||
{
|
||||
if tasks.is_closed() {
|
||||
return Err(task_fault("closed task scope"));
|
||||
}
|
||||
let permit = self.tasks.clone().try_acquire_owned().map_err(task_fault)?;
|
||||
let tracking = tasks.token();
|
||||
let future = async move {
|
||||
let result = call().await;
|
||||
drop(permit);
|
||||
result
|
||||
};
|
||||
// Future 先释放;未领取的输出也必须先于跟踪令牌释放。
|
||||
Ok(AbortOnDropHandle::new(tokio::spawn(
|
||||
future.map(move |result| (result, tracking)),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn reference_cancel_scoped(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else { return Ok(0); };
|
||||
value.native.worker_scope(async |tasks| {
|
||||
let task = value.native.tracked_leaf(tasks, || native_fallible_sleep(60000))?;
|
||||
task.abort();
|
||||
match task.await {
|
||||
Err(error) if error.is_cancelled() => (),
|
||||
Err(error) => return Err(NativeAsyncFailure::Fault(task_fault(error))),
|
||||
Ok((result, _tracking)) => result?,
|
||||
}
|
||||
Ok::<_, NativeAsyncFailure>(1)
|
||||
})?.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn cancel_scoped(cases: &[TaskCase]) {
|
||||
compare_named("task_cancel_scoped", cases, 1000,
|
||||
generated_cancel, reference_cancel_scoped, |value| *value as u64, "rust_scoped");
|
||||
}
|
||||
|
||||
fn check_cancel_cleanup() {
|
||||
let native = NativeTasks::new();
|
||||
native.worker_scope(async |tasks| {
|
||||
let task = native.tracked_leaf(tasks, || async { 42 }).unwrap();
|
||||
let (value, tracking) = task.await.unwrap();
|
||||
assert_eq!(value, 42);
|
||||
drop(tracking);
|
||||
let permits = native.tasks.clone().acquire_many_owned(8).await.unwrap();
|
||||
assert!(native.tracked_leaf(tasks, || async {}).is_err());
|
||||
drop(permits);
|
||||
tasks.close();
|
||||
assert!(native.tracked_leaf(tasks, || async {}).is_err());
|
||||
}).unwrap();
|
||||
|
||||
// 立即放弃、已启动 Future、未领取输出分别验证,根 panic 也必须排空。
|
||||
for stage in 0..3 {
|
||||
for root_panics in [false, true] {
|
||||
let dropped = Arc::new(AtomicBool::new(false));
|
||||
let observed = dropped.clone();
|
||||
let owned = WorkerOutput(dropped);
|
||||
let outcome = native.worker_scope(async |tasks| {
|
||||
let (started, ready) = tokio::sync::oneshot::channel();
|
||||
let task = native.tracked_leaf(tasks, move || async move {
|
||||
if stage != 0 { started.send(()).unwrap(); }
|
||||
if stage != 2 { std::future::pending::<()>().await; }
|
||||
owned
|
||||
}).unwrap();
|
||||
if stage != 0 { ready.await.unwrap(); }
|
||||
if stage == 2 {
|
||||
while !task.is_finished() { tokio::task::yield_now().await; }
|
||||
assert!(!observed.load(Ordering::SeqCst));
|
||||
}
|
||||
if root_panics { panic!("root failure after leaf submission"); }
|
||||
drop(task);
|
||||
});
|
||||
assert_eq!(outcome.is_err(), root_panics);
|
||||
assert!(observed.load(Ordering::SeqCst));
|
||||
assert_eq!(native.tasks.available_permits(), 8);
|
||||
}
|
||||
}
|
||||
|
||||
native.worker_scope(async |tasks| {
|
||||
let task = native.tracked_leaf(tasks, || async { Err::<(), _>("business failure") }).unwrap();
|
||||
while !task.is_finished() { tokio::task::yield_now().await; }
|
||||
task.abort();
|
||||
let (result, tracking) = task.await.unwrap();
|
||||
assert_eq!(result, Err("business failure"));
|
||||
drop(tracking);
|
||||
let task = native.tracked_leaf(tasks, || async { panic!("leaf failure") }).unwrap();
|
||||
assert!(task.await.unwrap_err().is_panic());
|
||||
}).unwrap();
|
||||
println!("leaf cleanup: capacity, closed scope, inputs, future, unclaimed output, root panic, business error, task panic and re-entry passed");
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// 只由显式检查入口运行;同步通道控制 worker 时点,不用睡眠猜测退出顺序。
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
struct WorkerOutput(Arc<AtomicBool>);
|
||||
|
||||
impl Drop for WorkerOutput {
|
||||
fn drop(&mut self) {
|
||||
self.0.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_worker_cleanup() {
|
||||
assert!(matches!(native_blocking_sleep(-1), Err(NativeAsyncFailure::Sleep(message, _))
|
||||
if message == "sleep duration must be nonnegative"));
|
||||
let native = NativeTasks::new();
|
||||
native.worker_scope(async |tasks| {
|
||||
assert_eq!(native.tracked_blocking(tasks, || 42).await.unwrap(), 42);
|
||||
let failure = native.tracked_blocking(tasks, || Err::<(), _>("business failure")).await;
|
||||
assert_eq!(failure.unwrap(), Err("business failure"));
|
||||
tasks.close();
|
||||
assert!(native.tracked_blocking(tasks, || panic!("closed scope ran work")).await.is_err());
|
||||
}).unwrap();
|
||||
|
||||
let called = Arc::new(AtomicBool::new(false));
|
||||
native.worker_scope(async |tasks| {
|
||||
let permits = native.workers.clone().acquire_many_owned(2).await.unwrap();
|
||||
let observed = called.clone();
|
||||
let queued = native.tracked_blocking(tasks, move || observed.store(true, Ordering::SeqCst));
|
||||
let mut queued = Box::pin(queued);
|
||||
assert!(futures_util::poll!(queued.as_mut()).is_pending());
|
||||
drop(queued);
|
||||
drop(permits);
|
||||
}).unwrap();
|
||||
assert!(!called.load(Ordering::SeqCst));
|
||||
|
||||
// 调用者取消、worker panic、根入口 panic 都必须排空真实工作和未领取结果。
|
||||
for worker_panics in [false, true] {
|
||||
for root_panics in [false, true] {
|
||||
let dropped = Arc::new(AtomicBool::new(false));
|
||||
let observed = dropped.clone();
|
||||
let (release, blocked) = std::sync::mpsc::channel();
|
||||
let (started, ready) = tokio::sync::oneshot::channel();
|
||||
let worker = move || {
|
||||
started.send(()).unwrap();
|
||||
blocked.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
let output = WorkerOutput(dropped);
|
||||
if worker_panics { std::panic::panic_any(output); }
|
||||
output
|
||||
};
|
||||
let result = native.worker_scope(async |tasks| {
|
||||
let mut pending = Box::pin(native.tracked_blocking(tasks, worker));
|
||||
assert!(futures_util::poll!(pending.as_mut()).is_pending());
|
||||
ready.await.unwrap();
|
||||
drop(pending);
|
||||
assert_eq!(native.workers.available_permits(), 1);
|
||||
assert!(!tasks.is_empty());
|
||||
assert!(!observed.load(Ordering::SeqCst));
|
||||
release.send(()).unwrap();
|
||||
if root_panics { panic!("root failure after worker submission"); }
|
||||
});
|
||||
assert_eq!(result.is_err(), root_panics);
|
||||
assert!(observed.load(Ordering::SeqCst));
|
||||
assert_eq!(native.workers.available_permits(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
let claimed = native.worker_scope(async |tasks| {
|
||||
let result = native.tracked_blocking(tasks, || String::from("owned output")).await.unwrap();
|
||||
tasks.close();
|
||||
tasks.wait().await;
|
||||
result
|
||||
}).unwrap();
|
||||
assert_eq!(claimed, "owned output");
|
||||
assert_eq!(native.worker_scope(async |tasks| native.tracked_blocking(tasks, || 7).await).unwrap().unwrap(), 7);
|
||||
println!("worker cleanup: success, business error, closed scope, queued cancellation, running cancellation, panic, result drop and re-entry passed");
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
// 保留原单次入口参照,另外区分入口成本和同一入口内连续执行的任务成本。
|
||||
// 每个 steady 样本仍逐次创建、等待并清理真实任务,只复用父入口。
|
||||
|
||||
const CONTINUOUS_CALLS: usize = 16;
|
||||
|
||||
// 与 Dever async_total 保持同样的可失败计时分支和携带文本的错误合同,不加填充字段。
|
||||
enum NativeAsyncFailure {
|
||||
Fault(Fault),
|
||||
Sleep(String, Fault),
|
||||
}
|
||||
|
||||
impl From<Fault> for NativeAsyncFailure {
|
||||
fn from(value: Fault) -> Self {
|
||||
Self::Fault(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NativeAsyncFailure> for Fault {
|
||||
fn from(value: NativeAsyncFailure) -> Self {
|
||||
match value {
|
||||
NativeAsyncFailure::Fault(fault) => fault,
|
||||
NativeAsyncFailure::Sleep(message, fault) => {
|
||||
drop(message);
|
||||
fault
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn native_fallible_sleep(millis: i64) -> Result<(), NativeAsyncFailure> {
|
||||
let fail = |message: &str| NativeAsyncFailure::Sleep(message.to_owned(), task_fault("timer"));
|
||||
let millis = u64::try_from(millis).map_err(|_| fail("sleep duration must be nonnegative"))?;
|
||||
let deadline = tokio::time::Instant::now()
|
||||
.checked_add(Duration::from_millis(millis))
|
||||
.ok_or_else(|| fail("sleep deadline is out of range"))?;
|
||||
tokio::time::sleep_until(deadline).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn native_async_total(values: Vec<i64>, delayed: bool) -> Result<i64, NativeAsyncFailure> {
|
||||
if delayed {
|
||||
native_fallible_sleep(0).await?;
|
||||
}
|
||||
native_total(&values).map_err(Into::into)
|
||||
}
|
||||
|
||||
// 仅作成本诊断:保留原始 Rust 参照,另测附带完成跟踪的相同双任务负载。
|
||||
// 元组先放返回值再放令牌,未领取结果的析构顺序与生产运行库相同。
|
||||
fn native_tracked<Failure, Work>(
|
||||
native: &NativeTasks,
|
||||
work: Work,
|
||||
cancel: CancellationToken,
|
||||
tracking: tokio_util::task::task_tracker::TaskTrackerToken,
|
||||
) -> JoinHandle<(
|
||||
Result<i64, Failure>,
|
||||
tokio_util::task::task_tracker::TaskTrackerToken,
|
||||
)>
|
||||
where
|
||||
Failure: From<Fault> + Send + 'static,
|
||||
Work: std::future::Future<Output = Result<i64, Failure>> + Send + 'static,
|
||||
{
|
||||
let admission = native.tasks.clone().try_acquire_owned().unwrap();
|
||||
let workers = native.workers.clone();
|
||||
tokio::spawn(async move {
|
||||
let tracking = tracking;
|
||||
let result = tokio::select! {
|
||||
biased;
|
||||
result = work => result,
|
||||
() = cancel.cancelled() => Err(task_fault("cancelled").into()),
|
||||
};
|
||||
drop(workers);
|
||||
drop(admission);
|
||||
(result, tracking)
|
||||
})
|
||||
}
|
||||
|
||||
fn reference_async_tracked(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
reference_tracked(values, |values| async move { native_total(&values) })
|
||||
}
|
||||
|
||||
fn reference_async_typed(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
reference_tracked(values, |values| native_async_total(values, false))
|
||||
}
|
||||
|
||||
fn reference_tracked<Failure, Work>(
|
||||
values: &[TaskCase],
|
||||
call: impl Fn(Vec<i64>) -> Work,
|
||||
) -> Result<i64, Fault>
|
||||
where
|
||||
Failure: From<Fault> + Into<Fault> + Send + 'static,
|
||||
Work: std::future::Future<Output = Result<i64, Failure>> + Send + 'static,
|
||||
{
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
value.native.runtime.block_on(async {
|
||||
let cancel = CancellationToken::new();
|
||||
let tasks = tokio_util::task::TaskTracker::new();
|
||||
let outcome = async {
|
||||
let mut total = 0i64;
|
||||
for _ in 0..CONTINUOUS_CALLS {
|
||||
let first = native_tracked(
|
||||
&value.native,
|
||||
call(value.values.clone()),
|
||||
cancel.child_token(),
|
||||
tasks.token(),
|
||||
);
|
||||
let second = native_tracked(
|
||||
&value.native,
|
||||
call(value.values.clone()),
|
||||
cancel.child_token(),
|
||||
tasks.token(),
|
||||
);
|
||||
let first = first
|
||||
.await
|
||||
.map_err(task_fault)
|
||||
.and_then(|(result, _tracking)| result.map_err(Into::into));
|
||||
if first.is_err() {
|
||||
cancel.cancel();
|
||||
}
|
||||
let second = second
|
||||
.await
|
||||
.map_err(task_fault)
|
||||
.and_then(|(result, _tracking)| result.map_err(Into::into));
|
||||
let result = first?
|
||||
.checked_add(second?)
|
||||
.ok_or_else(|| task_fault("integer overflow"))?;
|
||||
total = total
|
||||
.checked_add(result)
|
||||
.ok_or_else(|| task_fault("integer overflow"))?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
.await;
|
||||
cancel.cancel();
|
||||
tasks.close();
|
||||
tasks.wait().await;
|
||||
outcome
|
||||
})
|
||||
}
|
||||
|
||||
fn generated_entry(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
value.generated.run(async |_| 1i64).map_err(task_fault)
|
||||
}
|
||||
|
||||
fn reference_entry(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
Ok(value.native.runtime.block_on(async { 1i64 }))
|
||||
}
|
||||
|
||||
fn generated_leaf_entry(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else { return Ok(0); };
|
||||
value.generated.run_leaf(async { 1i64 }).map_err(task_fault)
|
||||
}
|
||||
|
||||
fn generated_async_steady(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
value
|
||||
.generated
|
||||
.run(async |scope| {
|
||||
let mut total = 0i64;
|
||||
for _ in 0..CONTINUOUS_CALLS {
|
||||
let result = GENERATED_ASYNC(&value.values, scope)
|
||||
.await
|
||||
.map_err(task_fault)?;
|
||||
total = total
|
||||
.checked_add(result)
|
||||
.ok_or_else(|| task_fault("integer overflow"))?;
|
||||
}
|
||||
Ok(total)
|
||||
})
|
||||
.map_err(task_fault)?
|
||||
}
|
||||
|
||||
fn reference_async_steady(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
value.native.runtime.block_on(async {
|
||||
let cancel = CancellationToken::new();
|
||||
let mut total = 0i64;
|
||||
for _ in 0..CONTINUOUS_CALLS {
|
||||
let first = value
|
||||
.native
|
||||
.start(value.values.clone(), true, cancel.child_token());
|
||||
let second = value
|
||||
.native
|
||||
.start(value.values.clone(), true, cancel.child_token());
|
||||
let first = first.await.map_err(task_fault).and_then(|result| result);
|
||||
if first.is_err() {
|
||||
cancel.cancel();
|
||||
}
|
||||
let second = second.await.map_err(task_fault).and_then(|result| result);
|
||||
let result = first?
|
||||
.checked_add(second?)
|
||||
.ok_or_else(|| task_fault("integer overflow"))?;
|
||||
total = total
|
||||
.checked_add(result)
|
||||
.ok_or_else(|| task_fault("integer overflow"))?;
|
||||
}
|
||||
cancel.cancel();
|
||||
Ok(total)
|
||||
})
|
||||
}
|
||||
|
||||
fn generated_cancel_steady(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
value
|
||||
.generated
|
||||
.run(async |scope| {
|
||||
let mut total = 0i64;
|
||||
for _ in 0..CONTINUOUS_CALLS {
|
||||
let result = GENERATED_CANCEL(scope).await.map_err(task_fault)?;
|
||||
total = total
|
||||
.checked_add(result)
|
||||
.ok_or_else(|| task_fault("integer overflow"))?;
|
||||
}
|
||||
Ok(total)
|
||||
})
|
||||
.map_err(task_fault)?
|
||||
}
|
||||
|
||||
fn reference_cancel_steady(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
value.native.runtime.block_on(async {
|
||||
let root = CancellationToken::new();
|
||||
let mut total = 0i64;
|
||||
for _ in 0..CONTINUOUS_CALLS {
|
||||
let admission = value.native.tasks.clone().try_acquire_owned().unwrap();
|
||||
let cancel = root.child_token();
|
||||
let child = cancel.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
let deadline = tokio::time::Instant::now()
|
||||
.checked_add(Duration::from_millis(60000))
|
||||
.unwrap();
|
||||
tokio::select! {
|
||||
biased;
|
||||
() = tokio::time::sleep_until(deadline) => panic!("cancelled timer completed"),
|
||||
() = child.cancelled() => (),
|
||||
}
|
||||
drop(admission);
|
||||
});
|
||||
cancel.cancel();
|
||||
task.await.map_err(task_fault)?;
|
||||
total = total
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| task_fault("integer overflow"))?;
|
||||
}
|
||||
root.cancel();
|
||||
Ok(total)
|
||||
})
|
||||
}
|
||||
|
||||
// 取消机制采用标准 JoinHandle::abort 的独立 Rust 对照;原 token 对照继续保留。
|
||||
fn reference_cancel_abort(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else { return Ok(0); };
|
||||
value.native.runtime.block_on(async {
|
||||
let admission = value.native.tasks.clone().try_acquire_owned().unwrap();
|
||||
let task = tokio::spawn(async move {
|
||||
let _admission = admission;
|
||||
let deadline = tokio::time::Instant::now()
|
||||
.checked_add(Duration::from_millis(60000))
|
||||
.ok_or_else(|| task_fault("sleep deadline"))?;
|
||||
tokio::time::sleep_until(deadline).await;
|
||||
Ok::<(), Fault>(())
|
||||
});
|
||||
task.abort();
|
||||
match task.await {
|
||||
Err(error) if error.is_cancelled() => Ok(1),
|
||||
Err(error) => Err(task_fault(error)),
|
||||
Ok(result) => { result?; Err(task_fault("cancelled timer completed")) }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn lifecycle(cases: &[TaskCase]) {
|
||||
let expected = native_total(&cases[0].values).unwrap() * 2 * CONTINUOUS_CALLS as i64;
|
||||
assert_eq!(generated_async_steady(cases).unwrap(), expected);
|
||||
assert_eq!(reference_async_steady(cases).unwrap(), expected);
|
||||
assert_eq!(reference_async_tracked(cases).unwrap(), expected);
|
||||
assert_eq!(reference_async_typed(cases).unwrap(), expected);
|
||||
let native = &cases[0].native;
|
||||
assert_eq!(
|
||||
native
|
||||
.runtime
|
||||
.block_on(native_async_total(cases[0].values.clone(), true))
|
||||
.map_err(Fault::from)
|
||||
.unwrap(),
|
||||
expected / CONTINUOUS_CALLS as i64 / 2
|
||||
);
|
||||
assert!(native.runtime.block_on(native_fallible_sleep(-1)).is_err());
|
||||
eprintln!(
|
||||
"result-layout: rust_typed_async={}",
|
||||
std::mem::size_of::<Result<i64, NativeAsyncFailure>>()
|
||||
);
|
||||
assert_eq!(
|
||||
generated_cancel_steady(cases).unwrap(),
|
||||
CONTINUOUS_CALLS as i64
|
||||
);
|
||||
assert_eq!(
|
||||
reference_cancel_steady(cases).unwrap(),
|
||||
CONTINUOUS_CALLS as i64
|
||||
);
|
||||
cases[0]
|
||||
.generated
|
||||
.run(async |scope| {
|
||||
eprintln!(
|
||||
"task-layout: task={} optional_task={}",
|
||||
std::mem::size_of::<dever_runtime::task::Task<()>>(),
|
||||
std::mem::size_of::<Option<dever_runtime::task::Task<()>>>(),
|
||||
);
|
||||
eprintln!(
|
||||
"future-layout: fault={} dever_pair={} rust_pair={} dever_cancel={} rust_cancel={}",
|
||||
std::mem::size_of::<Fault>(),
|
||||
std::mem::size_of_val(&GENERATED_ASYNC(&cases[0].values, scope)),
|
||||
std::mem::size_of_val(&handwritten_pair(scope, &cases[0].values)),
|
||||
std::mem::size_of_val(&GENERATED_CANCEL(scope)),
|
||||
std::mem::size_of_val(&handwritten_cancel(scope)),
|
||||
);
|
||||
let result = GENERATED_ASYNC(&cases[0].values, scope).await;
|
||||
eprintln!(
|
||||
"result-layout: dever_async={} rust_async={}",
|
||||
std::mem::size_of_val(&result),
|
||||
std::mem::size_of::<Result<i64, Fault>>()
|
||||
);
|
||||
let result = GENERATED_CANCEL(scope).await;
|
||||
eprintln!(
|
||||
"result-layout: dever_cancel={} rust_cancel={}",
|
||||
std::mem::size_of_val(&result),
|
||||
std::mem::size_of::<Result<i64, Fault>>()
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
compare(
|
||||
"task_entry_ready",
|
||||
cases,
|
||||
100000,
|
||||
generated_entry,
|
||||
reference_entry,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"task_leaf_entry_ready",
|
||||
cases,
|
||||
100000,
|
||||
generated_leaf_entry,
|
||||
reference_entry,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"task_async_steady",
|
||||
cases,
|
||||
64,
|
||||
generated_async_steady,
|
||||
reference_async_steady,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"task_cancel_steady",
|
||||
cases,
|
||||
64,
|
||||
generated_cancel_steady,
|
||||
reference_cancel_steady,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare_named(
|
||||
"task_async_tracked_diagnostic",
|
||||
cases,
|
||||
64,
|
||||
generated_async_steady,
|
||||
reference_async_tracked,
|
||||
|value| *value as u64,
|
||||
"rust_tracked",
|
||||
);
|
||||
compare_named(
|
||||
"task_async_typed_diagnostic",
|
||||
cases,
|
||||
64,
|
||||
generated_async_steady,
|
||||
reference_async_typed,
|
||||
|value| *value as u64,
|
||||
"rust_typed",
|
||||
);
|
||||
compare_named(
|
||||
"task_cancel_abort_diagnostic",
|
||||
cases,
|
||||
1000,
|
||||
generated_cancel,
|
||||
reference_cancel_abort,
|
||||
|value| *value as u64,
|
||||
"rust_abort",
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// 调度器保持存活,计时包含输入复制、提交、结果领取和作用域清理。
|
||||
// 独立参照针对同样的一层双任务/worker/计时器,不能代表任意嵌套并发已验收。
|
||||
use dever_runtime::task::{Limits, Runtime};
|
||||
|
||||
struct TaskCase {
|
||||
generated: Runtime,
|
||||
native: NativeTasks,
|
||||
values: Vec<i64>,
|
||||
}
|
||||
|
||||
fn new_runtime() -> Runtime {
|
||||
Runtime::new(Limits {
|
||||
threads: 2,
|
||||
workers: 2,
|
||||
tasks: 8,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn generated_sync(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
value
|
||||
.generated
|
||||
.run(async |scope| { GENERATED_SYNC(&value.values, scope).await })
|
||||
.map_err(task_fault)?
|
||||
.map_err(task_fault)
|
||||
}
|
||||
|
||||
fn reference_sync(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
values
|
||||
.first()
|
||||
.map_or(Ok(0), |value| value.native.pair(&value.values, false))
|
||||
}
|
||||
|
||||
fn generated_async(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
value
|
||||
.generated
|
||||
.run(async |scope| { GENERATED_ASYNC(&value.values, scope).await })
|
||||
.map_err(task_fault)?
|
||||
.map_err(task_fault)
|
||||
}
|
||||
|
||||
fn reference_async(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
values
|
||||
.first()
|
||||
.map_or(Ok(0), |value| value.native.pair(&value.values, true))
|
||||
}
|
||||
|
||||
fn generated_parallel(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
value
|
||||
.generated
|
||||
.run(async |scope| { GENERATED_PARALLEL(&value.values, scope).await })
|
||||
.map_err(task_fault)?
|
||||
.map_err(task_fault)
|
||||
}
|
||||
|
||||
fn reference_parallel(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
values
|
||||
.first()
|
||||
.map_or(Ok(0), |value| value.native.blocking(&value.values, false))
|
||||
}
|
||||
|
||||
fn generated_blocking(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
value
|
||||
.generated
|
||||
.run(async |scope| { GENERATED_BLOCKING(&value.values, scope).await })
|
||||
.map_err(task_fault)?
|
||||
.map_err(task_fault)
|
||||
}
|
||||
|
||||
fn reference_blocking(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
values
|
||||
.first()
|
||||
.map_or(Ok(0), |value| value.native.blocking(&value.values, true))
|
||||
}
|
||||
|
||||
fn generated_cancel(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
value
|
||||
.generated
|
||||
.run(async |scope| { GENERATED_CANCEL(scope).await })
|
||||
.map_err(task_fault)?
|
||||
.map_err(task_fault)
|
||||
}
|
||||
|
||||
fn reference_cancel(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
values.first().map_or(Ok(0), |value| value.native.cancel())
|
||||
}
|
||||
|
||||
fn generated_timer(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else {
|
||||
return Ok(0);
|
||||
};
|
||||
value
|
||||
.generated
|
||||
.run_leaf(GENERATED_TIMER())
|
||||
.map_err(task_fault)?
|
||||
.map_err(task_fault)
|
||||
}
|
||||
|
||||
fn reference_timer(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
values.first().map_or(Ok(0), |value| value.native.timer())
|
||||
}
|
||||
|
||||
fn generated_fault(values: &[TaskCase]) -> Result<bool, Fault> {
|
||||
Ok(generated_sync(values).is_err())
|
||||
}
|
||||
fn reference_fault(values: &[TaskCase]) -> Result<bool, Fault> {
|
||||
Ok(reference_sync(values).is_err())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if std::env::args().any(|argument| argument == "--check-cancel-cleanup") {
|
||||
check_cancel_cleanup();
|
||||
return;
|
||||
}
|
||||
if std::env::args().any(|argument| argument == "--check-worker-cleanup") {
|
||||
check_worker_cleanup();
|
||||
return;
|
||||
}
|
||||
let cases = [TaskCase {
|
||||
generated: new_runtime(),
|
||||
native: NativeTasks::new(),
|
||||
values: (0..128).collect(),
|
||||
}];
|
||||
let expected = native_total(&cases[0].values).unwrap();
|
||||
assert_eq!(generated_sync(&cases).unwrap(), expected * 2);
|
||||
assert_eq!(generated_async(&cases).unwrap(), expected * 2);
|
||||
assert_eq!(generated_parallel(&cases).unwrap(), expected);
|
||||
assert_eq!(generated_blocking(&cases).unwrap(), expected);
|
||||
assert_eq!(generated_cancel(&cases).unwrap(), 1);
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
compare(
|
||||
"task_sync_pair",
|
||||
&cases,
|
||||
500,
|
||||
generated_sync,
|
||||
reference_sync,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"task_async_pair",
|
||||
&cases,
|
||||
1000,
|
||||
generated_async,
|
||||
reference_async,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"task_parallel",
|
||||
&cases,
|
||||
500,
|
||||
generated_parallel,
|
||||
reference_parallel,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"task_blocking",
|
||||
&cases,
|
||||
500,
|
||||
generated_blocking,
|
||||
reference_blocking,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"task_cancel",
|
||||
&cases,
|
||||
1000,
|
||||
generated_cancel,
|
||||
reference_cancel,
|
||||
|value| *value as u64,
|
||||
);
|
||||
compare(
|
||||
"task_timer_zero",
|
||||
&cases,
|
||||
10,
|
||||
generated_timer,
|
||||
reference_timer,
|
||||
|value| *value as u64,
|
||||
);
|
||||
let failures = [TaskCase {
|
||||
generated: new_runtime(),
|
||||
native: NativeTasks::new(),
|
||||
values: vec![i64::MAX, 1],
|
||||
}];
|
||||
assert!(generated_fault(&failures).unwrap());
|
||||
assert!(generated_async(&failures).is_err());
|
||||
assert!(reference_async(&failures).is_err());
|
||||
assert!(shared_async(&failures).is_err());
|
||||
assert!(shared_worker::<false>(&failures).is_err());
|
||||
assert!(shared_worker::<true>(&failures).is_err());
|
||||
assert!(generated_nested(&failures).is_err());
|
||||
assert!(reference_nested(&failures).is_err());
|
||||
compare(
|
||||
"task_sync_fault",
|
||||
&failures,
|
||||
500,
|
||||
generated_fault,
|
||||
reference_fault,
|
||||
|value| u64::from(*value),
|
||||
);
|
||||
assert!(generated_async_steady(&failures).is_err());
|
||||
assert!(reference_async_steady(&failures).is_err());
|
||||
assert!(reference_async_tracked(&failures).is_err());
|
||||
assert!(reference_async_typed(&failures).is_err());
|
||||
drop(failures);
|
||||
lifecycle(&cases);
|
||||
shared_runtime(&cases);
|
||||
worker_steady(&cases);
|
||||
worker_scoped(&cases);
|
||||
cancel_scoped(&cases);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// 独立 Tokio 参照只实现本组一层任务工作负载,不调用 Dever 调度器。
|
||||
// 任务和 worker 分别限流;遇到失败或取消仍领取所有 JoinHandle,已启动阻塞工作不强停。
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tokio::{runtime, sync::Semaphore, task::JoinHandle};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
struct NativeTasks {
|
||||
runtime: runtime::Runtime,
|
||||
workers: Arc<Semaphore>,
|
||||
tasks: Arc<Semaphore>,
|
||||
}
|
||||
|
||||
fn task_fault(_: impl std::fmt::Debug) -> Fault {
|
||||
&FaultInfo {
|
||||
source: "task-reference",
|
||||
line: 1,
|
||||
column: 1,
|
||||
message: "task failure",
|
||||
}
|
||||
}
|
||||
|
||||
fn native_total(values: &[i64]) -> Result<i64, Fault> {
|
||||
values.iter().try_fold(0i64, |total, value| {
|
||||
total
|
||||
.checked_add(*value)
|
||||
.ok_or_else(|| task_fault("integer overflow"))
|
||||
})
|
||||
}
|
||||
|
||||
impl NativeTasks {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
runtime: runtime::Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
.max_blocking_threads(2)
|
||||
.enable_time()
|
||||
.build()
|
||||
.unwrap(),
|
||||
workers: Arc::new(Semaphore::new(2)),
|
||||
tasks: Arc::new(Semaphore::new(8)),
|
||||
}
|
||||
}
|
||||
|
||||
fn start(
|
||||
&self,
|
||||
values: Vec<i64>,
|
||||
asynchronous: bool,
|
||||
cancel: CancellationToken,
|
||||
) -> JoinHandle<Result<i64, Fault>> {
|
||||
let admission = self.tasks.clone().try_acquire_owned().unwrap();
|
||||
let workers = self.workers.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = if asynchronous {
|
||||
tokio::select! {
|
||||
biased;
|
||||
result = async { native_total(&values) } => result,
|
||||
() = cancel.cancelled() => Err(task_fault("cancelled")),
|
||||
}
|
||||
} else {
|
||||
let worker = tokio::select! {
|
||||
biased;
|
||||
() = cancel.cancelled() => return Err(task_fault("cancelled")),
|
||||
worker = workers.acquire_owned() => worker.unwrap(),
|
||||
};
|
||||
let result = tokio::task::spawn_blocking(move || native_total(&values))
|
||||
.await
|
||||
.map_err(task_fault)?;
|
||||
drop(worker);
|
||||
result
|
||||
};
|
||||
drop(admission);
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
fn pair(&self, values: &[i64], asynchronous: bool) -> Result<i64, Fault> {
|
||||
self.runtime.block_on(async {
|
||||
let cancel = CancellationToken::new();
|
||||
let first = self.start(values.to_vec(), asynchronous, cancel.child_token());
|
||||
let second = self.start(values.to_vec(), asynchronous, cancel.child_token());
|
||||
let first = first.await.map_err(task_fault).and_then(|value| value);
|
||||
if first.is_err() {
|
||||
cancel.cancel();
|
||||
}
|
||||
let second = second.await.map_err(task_fault).and_then(|value| value);
|
||||
cancel.cancel();
|
||||
first?
|
||||
.checked_add(second?)
|
||||
.ok_or_else(|| task_fault("integer overflow"))
|
||||
})
|
||||
}
|
||||
|
||||
fn blocking(&self, values: &[i64], sleep: bool) -> Result<i64, Fault> {
|
||||
let values = values.to_vec();
|
||||
self.runtime.block_on(async {
|
||||
let worker = self.workers.clone().acquire_owned().await.unwrap();
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
if sleep {
|
||||
std::time::Instant::now()
|
||||
.checked_add(Duration::ZERO)
|
||||
.ok_or_else(|| task_fault("sleep deadline"))?;
|
||||
std::thread::sleep(Duration::ZERO);
|
||||
}
|
||||
native_total(&values)
|
||||
})
|
||||
.await
|
||||
.map_err(task_fault)?;
|
||||
drop(worker);
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
fn cancel(&self) -> Result<i64, Fault> {
|
||||
self.runtime.block_on(async {
|
||||
let admission = self.tasks.clone().try_acquire_owned().unwrap();
|
||||
let cancel = CancellationToken::new();
|
||||
let child = cancel.child_token();
|
||||
let task = tokio::spawn(async move {
|
||||
let deadline = tokio::time::Instant::now()
|
||||
.checked_add(Duration::from_millis(60000))
|
||||
.unwrap();
|
||||
tokio::select! {
|
||||
biased;
|
||||
() = tokio::time::sleep_until(deadline) => panic!("cancelled timer completed"),
|
||||
() = child.cancelled() => (),
|
||||
}
|
||||
drop(admission);
|
||||
});
|
||||
cancel.cancel();
|
||||
task.await.map_err(task_fault)?;
|
||||
Ok(1)
|
||||
})
|
||||
}
|
||||
|
||||
fn timer(&self) -> Result<i64, Fault> {
|
||||
self.runtime.block_on(async {
|
||||
let deadline = tokio::time::Instant::now()
|
||||
.checked_add(Duration::ZERO)
|
||||
.ok_or_else(|| task_fault("sleep deadline"))?;
|
||||
tokio::time::sleep_until(deadline).await;
|
||||
Ok(1)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// 独立 Tokio 参照补齐当前阻塞工作负载的收尾合同;不调用 Dever 运行库。
|
||||
// 原始轻量参照继续保留,不将本组结果冒充原参照的性能提升。
|
||||
use futures_util::FutureExt;
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
impl NativeTasks {
|
||||
fn worker_scope<Value>(
|
||||
&self,
|
||||
call: impl AsyncFnOnce(&TaskTracker) -> Value,
|
||||
) -> Result<Value, Fault> {
|
||||
let tasks = TaskTracker::new();
|
||||
self.runtime.block_on(async {
|
||||
let result = AssertUnwindSafe(async { call(&tasks).await }).catch_unwind().await;
|
||||
tasks.close();
|
||||
tasks.wait().await;
|
||||
result.map_err(task_fault)
|
||||
})
|
||||
}
|
||||
|
||||
async fn tracked_blocking<Value: Send + 'static>(
|
||||
&self,
|
||||
tasks: &TaskTracker,
|
||||
call: impl FnOnce() -> Value + Send + 'static,
|
||||
) -> Result<Value, Fault> {
|
||||
if tasks.is_closed() {
|
||||
return Err(task_fault("closed worker scope"));
|
||||
}
|
||||
let permit = self.workers.clone().acquire_owned().await.unwrap();
|
||||
let token = tasks.token();
|
||||
let (result, _token) = tokio::task::spawn_blocking(move || {
|
||||
let _permit = permit;
|
||||
let result = catch_unwind(AssertUnwindSafe(call)).map_err(task_fault);
|
||||
// 输出先析构,跟踪才结束;panic 载荷也在通知排空前释放。
|
||||
(result, token)
|
||||
}).await.map_err(task_fault)?;
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn reference_worker_scoped<const BLOCKING: bool, const CALLS: usize>(
|
||||
values: &[TaskCase],
|
||||
) -> Result<i64, Fault> {
|
||||
if BLOCKING {
|
||||
reference_worker_calls::<CALLS, _>(values, |owned| {
|
||||
native_blocking_sleep(0)?;
|
||||
native_total(&owned).map_err(NativeAsyncFailure::from)
|
||||
})
|
||||
} else {
|
||||
reference_worker_calls::<CALLS, _>(values, |owned| native_total(&owned))
|
||||
}
|
||||
}
|
||||
|
||||
fn native_blocking_sleep(millis: i64) -> Result<(), NativeAsyncFailure> {
|
||||
let fail = |message: &str| NativeAsyncFailure::Sleep(message.to_owned(), task_fault("sleep"));
|
||||
let millis = u64::try_from(millis).map_err(|_| fail("sleep duration must be nonnegative"))?;
|
||||
let duration = Duration::from_millis(millis);
|
||||
std::time::Instant::now().checked_add(duration)
|
||||
.ok_or_else(|| fail("sleep deadline is out of range"))?;
|
||||
std::thread::sleep(duration);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 保留实际业务错误及文本负载;不以填充字段强凑与生成代码相同的布局。
|
||||
fn reference_worker_calls<const CALLS: usize, Failure: From<Fault> + Into<Fault> + Send + 'static>(
|
||||
values: &[TaskCase],
|
||||
call: impl FnOnce(Vec<i64>) -> Result<i64, Failure> + Copy + Send + 'static,
|
||||
) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else { return Ok(0); };
|
||||
value.native.worker_scope(async |tasks| {
|
||||
let mut total = 0i64;
|
||||
for _ in 0..CALLS {
|
||||
let owned = value.values.clone();
|
||||
let result = value.native.tracked_blocking(tasks, move || call(owned))
|
||||
.await.map_err(Failure::from)??;
|
||||
total = total.checked_add(result)
|
||||
.ok_or_else(|| Failure::from(task_fault("integer overflow")))?;
|
||||
}
|
||||
Ok::<_, Failure>(total)
|
||||
})?.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn worker_scoped(cases: &[TaskCase]) {
|
||||
let expected = native_total(&cases[0].values).unwrap();
|
||||
assert_eq!(reference_worker_scoped::<false, 1>(cases).unwrap(), expected);
|
||||
assert_eq!(reference_worker_scoped::<true, 1>(cases).unwrap(), expected);
|
||||
assert_eq!(reference_worker_scoped::<false, CONTINUOUS_CALLS>(cases).unwrap(), expected * CONTINUOUS_CALLS as i64);
|
||||
assert_eq!(reference_worker_scoped::<true, CONTINUOUS_CALLS>(cases).unwrap(), expected * CONTINUOUS_CALLS as i64);
|
||||
let failures = [TaskCase {
|
||||
generated: new_runtime(), native: NativeTasks::new(), values: vec![i64::MAX, 1],
|
||||
}];
|
||||
assert!(reference_worker_scoped::<false, 1>(&failures).is_err());
|
||||
assert!(reference_worker_scoped::<true, 1>(&failures).is_err());
|
||||
assert!(reference_worker_scoped::<false, CONTINUOUS_CALLS>(&failures).is_err());
|
||||
assert!(reference_worker_scoped::<true, CONTINUOUS_CALLS>(&failures).is_err());
|
||||
compare_named("task_parallel_scoped", cases, 500,
|
||||
generated_parallel, reference_worker_scoped::<false, 1>, |value| *value as u64, "rust_scoped");
|
||||
compare_named("task_blocking_scoped", cases, 500,
|
||||
generated_blocking, reference_worker_scoped::<true, 1>, |value| *value as u64, "rust_scoped");
|
||||
compare_named("task_parallel_scoped_steady", cases, 64,
|
||||
generated_worker_steady::<false>, reference_worker_scoped::<false, CONTINUOUS_CALLS>, |value| *value as u64, "rust_scoped");
|
||||
compare_named("task_blocking_scoped_steady", cases, 64,
|
||||
generated_worker_steady::<true>, reference_worker_scoped::<true, CONTINUOUS_CALLS>, |value| *value as u64, "rust_scoped");
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// 手写 Rust 与 Dever 生成代码使用同一个运行时,隔离生成代码与运行时策略的成本。
|
||||
// 原始直接 Tokio 参照继续保留;这里不调用任何生成函数或填充错误布局。
|
||||
use dever_runtime::task::Scope;
|
||||
|
||||
async fn handwritten_pair(scope: &Scope<'_>, values: &[i64]) -> Result<i64, NativeAsyncFailure> {
|
||||
let first_values = values.to_vec();
|
||||
let first = scope
|
||||
.spawn_leaf(move || native_async_total(first_values, false))
|
||||
.map_err(|error| NativeAsyncFailure::Fault(task_fault(error)))?;
|
||||
let second_values = values.to_vec();
|
||||
let second = match scope.spawn_leaf(move || native_async_total(second_values, false)) {
|
||||
Ok(task) => task,
|
||||
Err(error) => {
|
||||
first.cancel();
|
||||
let _ = first.wait().await;
|
||||
return Err(task_fault(error).into());
|
||||
}
|
||||
};
|
||||
let first = first.wait().await
|
||||
.map_err(|error| NativeAsyncFailure::Fault(task_fault(error)))
|
||||
.and_then(|result| result);
|
||||
if first.is_err() { second.cancel(); }
|
||||
let second = second.wait().await
|
||||
.map_err(|error| NativeAsyncFailure::Fault(task_fault(error)))
|
||||
.and_then(|result| result);
|
||||
first?.checked_add(second?)
|
||||
.ok_or_else(|| task_fault("integer overflow").into())
|
||||
}
|
||||
|
||||
async fn handwritten_cancel(scope: &Scope<'_>) -> Result<i64, NativeAsyncFailure> {
|
||||
let task = scope.spawn_leaf(|| native_fallible_sleep(60000))
|
||||
.map_err(|error| NativeAsyncFailure::Fault(task_fault(error)))?;
|
||||
if let Some(result) = task.stop().await
|
||||
.map_err(|error| NativeAsyncFailure::Fault(task_fault(error)))? {
|
||||
result?;
|
||||
}
|
||||
Ok(1)
|
||||
}
|
||||
|
||||
fn shared_async(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else { return Ok(0); };
|
||||
value.generated.run(async |scope| handwritten_pair(scope, &value.values).await)
|
||||
.map_err(task_fault)?.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn shared_cancel(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else { return Ok(0); };
|
||||
value.generated.run(async |scope| handwritten_cancel(scope).await)
|
||||
.map_err(task_fault)?.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn shared_worker<const SLEEP: bool>(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else { return Ok(0); };
|
||||
value.generated.run(async |scope| {
|
||||
let owned = value.values.clone();
|
||||
scope.blocking(move || {
|
||||
if SLEEP {
|
||||
std::time::Instant::now()
|
||||
.checked_add(std::time::Duration::ZERO)
|
||||
.ok_or_else(|| task_fault("sleep deadline"))?;
|
||||
std::thread::sleep(std::time::Duration::ZERO);
|
||||
}
|
||||
native_total(&owned)
|
||||
}).await.map_err(task_fault)?
|
||||
}).map_err(task_fault)?
|
||||
}
|
||||
|
||||
// 两侧都在真实父任务中执行双子任务;同样复制输入,不改用更大的错误或状态。
|
||||
fn nested<Work>(
|
||||
values: &[TaskCase],
|
||||
call: impl FnOnce(Scope<'static>, Vec<i64>) -> Work + Send + 'static,
|
||||
) -> Result<i64, Fault>
|
||||
where
|
||||
Work: std::future::Future<Output = Result<i64, Fault>> + Send + 'static,
|
||||
{
|
||||
let Some(value) = values.first() else { return Ok(0); };
|
||||
let owned = value.values.clone();
|
||||
value.generated.run(async move |scope| {
|
||||
let parent = scope.spawn(move |child| call(child, owned)).map_err(task_fault)?;
|
||||
parent.wait().await.map_err(task_fault)?
|
||||
}).map_err(task_fault)?
|
||||
}
|
||||
|
||||
fn generated_nested(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
nested(values, |scope, owned| async move {
|
||||
GENERATED_ASYNC(&owned, &scope).await.map_err(task_fault)
|
||||
})
|
||||
}
|
||||
|
||||
fn reference_nested(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
nested(values, |scope, owned| async move {
|
||||
handwritten_pair(&scope, &owned).await.map_err(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
fn shared_runtime(cases: &[TaskCase]) {
|
||||
compare_named(
|
||||
"task_parallel_shared_runtime", cases, 500,
|
||||
generated_parallel, shared_worker::<false>, |value| *value as u64, "rust_runtime",
|
||||
);
|
||||
compare_named(
|
||||
"task_blocking_shared_runtime", cases, 500,
|
||||
generated_blocking, shared_worker::<true>, |value| *value as u64, "rust_runtime",
|
||||
);
|
||||
compare_named(
|
||||
"task_async_shared_runtime", cases, 1000,
|
||||
generated_async, shared_async, |value| *value as u64, "rust_runtime",
|
||||
);
|
||||
compare_named(
|
||||
"task_cancel_shared_runtime", cases, 1000,
|
||||
generated_cancel, shared_cancel, |value| *value as u64, "rust_runtime",
|
||||
);
|
||||
compare_named(
|
||||
"task_nested_shared_runtime", cases, 1000,
|
||||
generated_nested, reference_nested, |value| *value as u64, "rust_runtime",
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// 每个入口连续提交并等待真实 worker;双方逐次复制输入、获取许可,不预先完成工作。
|
||||
fn generated_worker_steady<const BLOCKING: bool>(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else { return Ok(0); };
|
||||
value.generated.run(async |scope| {
|
||||
let mut total = 0i64;
|
||||
for _ in 0..CONTINUOUS_CALLS {
|
||||
let result = if BLOCKING {
|
||||
GENERATED_BLOCKING(&value.values, scope).await.map_err(task_fault)?
|
||||
} else {
|
||||
GENERATED_PARALLEL(&value.values, scope).await?
|
||||
};
|
||||
total = total.checked_add(result).ok_or_else(|| task_fault("integer overflow"))?;
|
||||
}
|
||||
Ok(total)
|
||||
}).map_err(task_fault)?
|
||||
}
|
||||
|
||||
fn reference_worker_steady<const BLOCKING: bool>(values: &[TaskCase]) -> Result<i64, Fault> {
|
||||
let Some(value) = values.first() else { return Ok(0); };
|
||||
value.native.runtime.block_on(async {
|
||||
let mut total = 0i64;
|
||||
for _ in 0..CONTINUOUS_CALLS {
|
||||
let owned = value.values.clone();
|
||||
let permit = value.native.workers.clone().acquire_owned().await.unwrap();
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
if BLOCKING {
|
||||
std::time::Instant::now().checked_add(Duration::ZERO)
|
||||
.ok_or_else(|| task_fault("sleep deadline"))?;
|
||||
std::thread::sleep(Duration::ZERO);
|
||||
}
|
||||
native_total(&owned)
|
||||
}).await.map_err(task_fault)?;
|
||||
drop(permit);
|
||||
total = total.checked_add(result?).ok_or_else(|| task_fault("integer overflow"))?;
|
||||
}
|
||||
Ok(total)
|
||||
})
|
||||
}
|
||||
|
||||
fn worker_steady(cases: &[TaskCase]) {
|
||||
let expected = native_total(&cases[0].values).unwrap() * CONTINUOUS_CALLS as i64;
|
||||
assert_eq!(generated_worker_steady::<false>(cases).unwrap(), expected);
|
||||
assert_eq!(reference_worker_steady::<false>(cases).unwrap(), expected);
|
||||
assert_eq!(generated_worker_steady::<true>(cases).unwrap(), expected);
|
||||
assert_eq!(reference_worker_steady::<true>(cases).unwrap(), expected);
|
||||
let failures = [TaskCase {
|
||||
generated: new_runtime(), native: NativeTasks::new(), values: vec![i64::MAX, 1],
|
||||
}];
|
||||
assert!(generated_worker_steady::<false>(&failures).is_err());
|
||||
assert!(reference_worker_steady::<false>(&failures).is_err());
|
||||
assert!(generated_worker_steady::<true>(&failures).is_err());
|
||||
assert!(reference_worker_steady::<true>(&failures).is_err());
|
||||
compare("task_parallel_steady", cases, 64,
|
||||
generated_worker_steady::<false>, reference_worker_steady::<false>, |value| *value as u64);
|
||||
compare("task_blocking_steady", cases, 64,
|
||||
generated_worker_steady::<true>, reference_worker_steady::<true>, |value| *value as u64);
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
fn chain_reference(values: &[String]) -> Result<String, Fault> {
|
||||
let value = values.first().map(String::as_str).unwrap_or("");
|
||||
let mut output = String::with_capacity(value.len() + value.len() + 3);
|
||||
output.push('[');
|
||||
output.push_str(value);
|
||||
output.push(':');
|
||||
output.push_str(value);
|
||||
output.push(']');
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn chain_generated(values: &[String]) -> Result<String, Fault> {
|
||||
GENERATED_CHAIN(values.first().map(String::as_str).unwrap_or(""))
|
||||
}
|
||||
|
||||
// 与 Dever 的 str 函数边界一致;原切片入口参照仍保留。
|
||||
fn chain_native(value: &str) -> Result<String, Fault> {
|
||||
let mut output = String::with_capacity(value.len() + value.len() + 3);
|
||||
output.push('[');
|
||||
output.push_str(value);
|
||||
output.push(':');
|
||||
output.push_str(value);
|
||||
output.push(']');
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn chain_boundary(values: &[String]) -> Result<String, Fault> {
|
||||
chain_native(values.first().map(String::as_str).unwrap_or(""))
|
||||
}
|
||||
|
||||
fn pair_reference(values: &[String]) -> Result<String, Fault> {
|
||||
let value = values.first().map(String::as_str).unwrap_or("");
|
||||
let mut output = String::with_capacity(value.len() + value.len());
|
||||
output.push_str(value);
|
||||
output.push_str(value);
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn pair_generated(values: &[String]) -> Result<String, Fault> {
|
||||
GENERATED_PAIR(values.first().map(String::as_str).unwrap_or(""))
|
||||
}
|
||||
|
||||
fn temporary_generated(values: &[String]) -> Result<String, Fault> {
|
||||
GENERATED_TEMPORARY(values.first().map(String::as_str).unwrap_or(""))
|
||||
}
|
||||
|
||||
fn temporary_reference(values: &[String]) -> Result<String, Fault> {
|
||||
let value = values.first().map(String::as_str).unwrap_or("");
|
||||
let mut output = String::with_capacity(value.len() + "后".len());
|
||||
output.push_str(value);
|
||||
output.push_str("后");
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn forwarded_generated(values: &[String]) -> Result<String, Fault> {
|
||||
GENERATED_FORWARDED(values.first().map(String::as_str).unwrap_or(""))
|
||||
}
|
||||
|
||||
fn contains_reference(values: &[SEARCH_PAIR]) -> Result<i64, Fault> {
|
||||
let mut total = 0i64;
|
||||
for pair in values {
|
||||
total = total
|
||||
.checked_add(if pair.field0.contains(pair.field1.as_str()) {
|
||||
120
|
||||
} else {
|
||||
0
|
||||
})
|
||||
.ok_or(
|
||||
const {
|
||||
&FaultInfo {
|
||||
message: "integer arithmetic fault",
|
||||
source: "reference",
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
},
|
||||
)?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn concatenate_text_reference(values: &[String]) -> Result<String, Fault> {
|
||||
let mut output = String::new();
|
||||
for value in values {
|
||||
output.push_str(value);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn concatenate_bytes_reference(
|
||||
values: &[dever_runtime::Bytes],
|
||||
) -> Result<dever_runtime::Bytes, Fault> {
|
||||
let mut output = Vec::new();
|
||||
for value in values {
|
||||
output.extend_from_slice(value);
|
||||
}
|
||||
Ok(output.into())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
for count in [8, 256, 4096] {
|
||||
let values = vec!["雪山".to_owned(); count];
|
||||
compare(
|
||||
&format!("list_contains_{count}"),
|
||||
&values,
|
||||
2000,
|
||||
list_contains_generated,
|
||||
list_contains_reference,
|
||||
|found| u64::from(*found),
|
||||
);
|
||||
}
|
||||
// 双方使用同一批 String;独立 Rust 实现只调用标准库,排除分配布局和内置时钟差异。
|
||||
for count in [512, 1024, 2048, 4096] {
|
||||
let values: Vec<_> = (0..count)
|
||||
.map(|_| {
|
||||
let text = "compare-value-120".to_owned();
|
||||
let pattern = text.to_uppercase().to_lowercase();
|
||||
SEARCH_PAIR {
|
||||
field0: text,
|
||||
field1: pattern,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
compare(
|
||||
&format!("text_contains_{count}"),
|
||||
&values,
|
||||
1000,
|
||||
GENERATED_CONTAINS,
|
||||
contains_reference,
|
||||
|value| *value as u64,
|
||||
);
|
||||
}
|
||||
let values: Vec<_> = [
|
||||
("雪山🙂", "山"),
|
||||
("abc", "z"),
|
||||
("abc", ""),
|
||||
("", "x"),
|
||||
("", ""),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(text, pattern)| SEARCH_PAIR {
|
||||
field0: text.into(),
|
||||
field1: pattern.into(),
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(GENERATED_CONTAINS(&values).unwrap(), 360);
|
||||
assert_eq!(contains_reference(&values).unwrap(), 360);
|
||||
for count in [512, 2048, 8192, 32768] {
|
||||
let texts = vec!["x".to_owned(); count];
|
||||
let bytes = (0..count)
|
||||
.map(|_| dever_runtime::Bytes::from(vec![b'x']))
|
||||
.collect::<Vec<_>>();
|
||||
compare(
|
||||
&format!("text_concat_{count}"),
|
||||
&texts,
|
||||
200,
|
||||
GENERATED_TEXT_CONCAT,
|
||||
concatenate_text_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
&format!("bytes_concat_{count}"),
|
||||
&bytes,
|
||||
200,
|
||||
GENERATED_BYTES_CONCAT,
|
||||
concatenate_bytes_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
}
|
||||
for (name, value, count) in [
|
||||
("text_chain_empty", String::new(), 300000),
|
||||
("text_chain_small", "雪山".repeat(2), 300000),
|
||||
("text_chain_large", "雪山".repeat(1024), 30000),
|
||||
] {
|
||||
let values = [value];
|
||||
compare(
|
||||
name,
|
||||
&values,
|
||||
count,
|
||||
chain_generated,
|
||||
chain_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
&format!("{name}_boundary"),
|
||||
&values,
|
||||
count,
|
||||
chain_generated,
|
||||
chain_boundary,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare_borrowed(
|
||||
name,
|
||||
&values,
|
||||
count,
|
||||
chain_generated,
|
||||
chain_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
}
|
||||
compare(
|
||||
"text_pair",
|
||||
&["雪山".repeat(32)],
|
||||
300000,
|
||||
pair_generated,
|
||||
pair_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
for (name, value, count) in [
|
||||
("text_temporary_empty", String::new(), 300000),
|
||||
("text_temporary_small", "雪山".repeat(2), 300000),
|
||||
("text_temporary_medium", "雪山".repeat(32), 300000),
|
||||
("text_temporary_large", "雪山".repeat(1024), 30000),
|
||||
] {
|
||||
let values = [value];
|
||||
compare(
|
||||
name,
|
||||
&values,
|
||||
count,
|
||||
temporary_generated,
|
||||
temporary_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare(
|
||||
&format!("{name}_forwarded"),
|
||||
&values,
|
||||
count,
|
||||
forwarded_generated,
|
||||
temporary_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare_borrowed(
|
||||
name,
|
||||
&values,
|
||||
count,
|
||||
temporary_generated,
|
||||
temporary_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
compare_borrowed(
|
||||
&format!("{name}_forwarded"),
|
||||
&values,
|
||||
count,
|
||||
forwarded_generated,
|
||||
temporary_reference,
|
||||
|value| value.len() as u64,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn list_contains_generated(values: &[String]) -> Result<bool, Fault> {
|
||||
GENERATED_LIST_CONTAINS(values, "absent")
|
||||
}
|
||||
|
||||
fn list_contains_reference(values: &[String]) -> Result<bool, Fault> {
|
||||
Ok(values.iter().any(|item| item.as_str() == "absent"))
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use std::path::Path;
|
||||
|
||||
pub const DEPENDENCIES: &str =
|
||||
"futures-util = { version = \"=0.3.34\", default-features = false, features = [\"std\"] }\n";
|
||||
|
||||
pub fn run(core: &Path) -> bool {
|
||||
if !std::env::args().any(|argument| argument == "--transport") {
|
||||
return false;
|
||||
}
|
||||
assert!(
|
||||
!std::env::args().any(|argument| argument == "--memory"),
|
||||
"transport uses asynchronous samples; --memory is unsupported"
|
||||
);
|
||||
let reference = [
|
||||
include_str!("reference.rs"),
|
||||
include_str!("session.rs"),
|
||||
include_str!("samples.rs"),
|
||||
]
|
||||
.concat()
|
||||
.replace(
|
||||
"TRANSPORT_TLS_DIRECTORY",
|
||||
&format!("{:?}", core.join("test/dever-tests/fixtures/tls")),
|
||||
);
|
||||
crate::compare(
|
||||
core,
|
||||
"transport-performance",
|
||||
"transport",
|
||||
&reference,
|
||||
&[
|
||||
("GENERATED_TRANSPORT_PLAIN", "main.servePlain"),
|
||||
("GENERATED_TRANSPORT_TLS", "main.serveSecure"),
|
||||
("GENERATED_TRANSPORT_EXCHANGE", "main.exchange"),
|
||||
],
|
||||
&[],
|
||||
);
|
||||
true
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use dever_runtime::{
|
||||
http, net,
|
||||
task::{Runtime, Scope, Timed},
|
||||
tls,
|
||||
};
|
||||
use std::{collections::BTreeSet, hint::black_box, sync::Arc, time::Instant};
|
||||
|
||||
async fn rust_transport_route(
|
||||
request: http::Request,
|
||||
_: Arc<Scope<'static>>,
|
||||
) -> Result<http::Response, String> {
|
||||
if request.method != "POST" || request.target != "/transport" {
|
||||
return Ok(http::Response {
|
||||
status: 404,
|
||||
headers: vec![],
|
||||
body: dever_runtime::Bytes::new(),
|
||||
});
|
||||
}
|
||||
let mut body = Vec::with_capacity(6 + request.body.len());
|
||||
body.extend_from_slice(b"reply:");
|
||||
body.extend_from_slice(&request.body);
|
||||
Ok(http::Response {
|
||||
status: 201,
|
||||
headers: vec![],
|
||||
body: body.into(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn rust_transport_serve(
|
||||
listener: &net::Listener,
|
||||
limits: &http::Limits,
|
||||
scope: &Scope<'_>,
|
||||
secure: Option<&tls::ServerTls>,
|
||||
) -> Result<(), String> {
|
||||
let result = if let Some(secure) = secure {
|
||||
http::serve_tls::<_, _, false>(listener, limits, scope, secure, rust_transport_route).await
|
||||
} else {
|
||||
http::serve::<_, _, false>(listener, limits, scope, rust_transport_route).await
|
||||
};
|
||||
result.map_err(|error| error.message().to_owned())
|
||||
}
|
||||
|
||||
async fn rust_transport_exchange(
|
||||
client: &http::HttpClient,
|
||||
payload: &dever_runtime::Bytes,
|
||||
) -> Result<(i64, dever_runtime::Bytes), String> {
|
||||
let response = client
|
||||
.request(http::Request {
|
||||
method: "POST".into(),
|
||||
target: "/transport".into(),
|
||||
headers: vec![],
|
||||
body: payload.clone(),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| error.into_owned())?;
|
||||
Ok((response.status, response.body))
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
fn transport_samples(
|
||||
runtime: &Runtime,
|
||||
certificates: &Arc<TransportCertificates>,
|
||||
case: TransportCase,
|
||||
smoke: bool,
|
||||
) {
|
||||
let name = case.name();
|
||||
if let Ok(selected) = std::env::var("DEVER_BENCH_CASES")
|
||||
&& !selected.split(',').any(|item| item == name)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let batches = if case.cold || smoke { 2 } else { 16 };
|
||||
if !smoke {
|
||||
for generated in [true, false] {
|
||||
black_box(transport_measure(
|
||||
runtime,
|
||||
certificates,
|
||||
case,
|
||||
generated,
|
||||
2,
|
||||
false,
|
||||
));
|
||||
}
|
||||
}
|
||||
eprintln!(
|
||||
"{name}: concurrency={}, connections={}, requests_per_sample={}",
|
||||
case.concurrency,
|
||||
case.connections(),
|
||||
batches * case.concurrency,
|
||||
);
|
||||
for sample in 0..if smoke { 1 } else { 15 } {
|
||||
let mut order = [("dever", true), ("rust_runtime", false), ("control", true)];
|
||||
order.rotate_left(sample % 3);
|
||||
let mut expected = None;
|
||||
for (implementation, generated) in order {
|
||||
let (elapsed, checksum) =
|
||||
transport_measure(runtime, certificates, case, generated, batches, !smoke);
|
||||
if let Some(expected) = expected {
|
||||
assert_eq!(checksum, expected);
|
||||
}
|
||||
expected = Some(checksum);
|
||||
if !smoke {
|
||||
println!("{name},{sample},{implementation},{elapsed},{checksum}");
|
||||
}
|
||||
}
|
||||
}
|
||||
if smoke {
|
||||
println!("correctness smoke passed: {name}; no performance result collected");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let smoke = std::env::args().any(|argument| argument == "--smoke");
|
||||
let fixture = std::path::Path::new(TRANSPORT_TLS_DIRECTORY);
|
||||
let certificates = Arc::new(TransportCertificates {
|
||||
root: std::fs::read(fixture.join("root.pem")).unwrap(),
|
||||
server: tls::server(
|
||||
&std::fs::read(fixture.join("server.pem")).unwrap(),
|
||||
&std::fs::read(fixture.join("server-key.pem")).unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
});
|
||||
let threads = std::thread::available_parallelism().map_or(1, |count| count.get().min(4));
|
||||
let runtime = Runtime::new(dever_runtime::task::Limits {
|
||||
threads,
|
||||
workers: 1,
|
||||
tasks: 256,
|
||||
})
|
||||
.unwrap();
|
||||
eprintln!(
|
||||
"transport: shared Dever runtime/Hyper/TLS; generation-layer comparison only; \
|
||||
Linux dev Pod loopback; {threads} async workers (maximum 4), one blocking worker; \
|
||||
HTTP/1 uses c connections; persistent HTTP/2 uses one connection and c streams; \
|
||||
cold uses c fresh clients and fresh TLS session caches; \
|
||||
cold timing includes connect/DNS (TLS localhost), full handshake and first response; \
|
||||
server readiness is primed outside timing for both modes; \
|
||||
persistent timing excludes the priming batch; payloads are fixed buffered bytes; \
|
||||
elapsed_ns sums request batches, including request/handler/body collection; \
|
||||
certificate/pool/server setup, exact-byte/socket checks and teardown are excluded"
|
||||
);
|
||||
if !smoke {
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
}
|
||||
for http2 in [false, true] {
|
||||
for secure in [false, true] {
|
||||
for concurrency in [1, 8, 32] {
|
||||
for (cold, bytes) in [(true, 64), (false, 64), (false, 65536)] {
|
||||
transport_samples(
|
||||
&runtime,
|
||||
&certificates,
|
||||
TransportCase {
|
||||
http2,
|
||||
secure,
|
||||
cold,
|
||||
concurrency,
|
||||
bytes,
|
||||
},
|
||||
smoke,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
#[derive(Clone, Copy)]
|
||||
struct TransportCase {
|
||||
http2: bool,
|
||||
secure: bool,
|
||||
cold: bool,
|
||||
concurrency: usize,
|
||||
bytes: usize,
|
||||
}
|
||||
|
||||
impl TransportCase {
|
||||
fn connections(self) -> usize {
|
||||
if self.cold || !self.http2 {
|
||||
self.concurrency
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
fn limits(self, connections: usize, streams: usize) -> http::Limits {
|
||||
http::Limits {
|
||||
connections: connections as i64,
|
||||
http2: self.http2.then_some(http::Http2Limits {
|
||||
streams: streams as i64,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn name(self) -> String {
|
||||
format!(
|
||||
"http{}_{}_{}_c{}_b{}",
|
||||
if self.http2 { 2 } else { 1 },
|
||||
if self.secure { "tls" } else { "plain" },
|
||||
if self.cold {
|
||||
"cold_first_response"
|
||||
} else {
|
||||
"persistent"
|
||||
},
|
||||
self.concurrency,
|
||||
self.bytes,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct TransportCertificates {
|
||||
root: Vec<u8>,
|
||||
server: tls::ServerTls,
|
||||
}
|
||||
|
||||
fn transport_clients(
|
||||
case: TransportCase,
|
||||
port: i64,
|
||||
certificates: &TransportCertificates,
|
||||
scope: &Scope<'_>,
|
||||
) -> Vec<http::HttpClient> {
|
||||
let origin = if case.secure {
|
||||
format!("https://localhost:{port}")
|
||||
} else {
|
||||
format!("http://127.0.0.1:{port}")
|
||||
};
|
||||
let count = if case.cold { case.concurrency } else { 1 };
|
||||
let limits = if case.cold {
|
||||
case.limits(1, 1)
|
||||
} else {
|
||||
case.limits(case.connections(), case.concurrency)
|
||||
};
|
||||
(0..count)
|
||||
.map(|_| {
|
||||
let fresh_tls = tls::client(&certificates.root).unwrap();
|
||||
http::client(
|
||||
&origin,
|
||||
&fresh_tls,
|
||||
&limits,
|
||||
&http::PoolLimits::default(),
|
||||
scope,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn transport_batch(
|
||||
generated: bool,
|
||||
clients: &[http::HttpClient],
|
||||
payloads: &[dever_runtime::Bytes],
|
||||
) -> Vec<(i64, dever_runtime::Bytes)> {
|
||||
futures_util::future::join_all(payloads.iter().enumerate().map(|(index, payload)| {
|
||||
let client = &clients[index % clients.len()];
|
||||
async move {
|
||||
if generated {
|
||||
GENERATED_TRANSPORT_EXCHANGE(black_box(client), black_box(payload))
|
||||
.await
|
||||
.map_err(|error| error.to_string())
|
||||
} else {
|
||||
rust_transport_exchange(black_box(client), black_box(payload)).await
|
||||
}
|
||||
}
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
.map(Result::unwrap)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn transport_check(
|
||||
replies: &[(i64, dever_runtime::Bytes)],
|
||||
payloads: &[dever_runtime::Bytes],
|
||||
) -> u64 {
|
||||
assert_eq!(replies.len(), payloads.len());
|
||||
let mut checksum = 0u64;
|
||||
for ((status, body), payload) in replies.iter().zip(payloads) {
|
||||
assert_eq!(*status, 201);
|
||||
assert_eq!(body.len(), 6 + payload.len());
|
||||
assert_eq!(&body[..6], b"reply:");
|
||||
assert_eq!(&body[6..], payload.as_ref());
|
||||
checksum += *status as u64 + body.iter().map(|byte| u64::from(*byte)).sum::<u64>();
|
||||
}
|
||||
black_box(checksum)
|
||||
}
|
||||
|
||||
fn transport_connections(port: i64) -> BTreeSet<(String, String)> {
|
||||
let endpoint = format!("0100007F:{port:04X}");
|
||||
std::fs::read_to_string("/proc/self/net/tcp")
|
||||
.expect("transport checks require the Linux dev Pod TCP table")
|
||||
.lines()
|
||||
.skip(1)
|
||||
.filter_map(|line| {
|
||||
let fields = line.split_whitespace().collect::<Vec<_>>();
|
||||
assert!(fields.len() >= 10, "malformed Linux TCP table");
|
||||
(fields[1] == endpoint && fields[3] == "01")
|
||||
.then(|| (fields[2].to_owned(), fields[9].to_owned()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn transport_close(clients: Vec<http::HttpClient>) {
|
||||
for client in clients {
|
||||
client.close().await.unwrap();
|
||||
assert!(rust_transport_exchange(&client, &dever_runtime::Bytes::from_static(b"closed")).await.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
async fn transport_session(
|
||||
scope: &Scope<'_>,
|
||||
certificates: &TransportCertificates,
|
||||
case: TransportCase,
|
||||
generated: bool,
|
||||
batches: usize,
|
||||
timed: bool,
|
||||
) -> (u128, u64) {
|
||||
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
|
||||
let port = listener.port().unwrap();
|
||||
let incoming = listener.clone();
|
||||
let server_tls = certificates.server.clone();
|
||||
let server = scope
|
||||
.spawn(move |child| async move {
|
||||
let connections = case.connections() as i64;
|
||||
let streams = case.concurrency as i64;
|
||||
if generated && case.secure {
|
||||
GENERATED_TRANSPORT_TLS(
|
||||
&incoming,
|
||||
case.http2,
|
||||
connections,
|
||||
streams,
|
||||
&server_tls,
|
||||
&child,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| error.to_string())
|
||||
} else if generated {
|
||||
GENERATED_TRANSPORT_PLAIN(&incoming, case.http2, connections, streams, &child)
|
||||
.await
|
||||
.map_err(|error| error.to_string())
|
||||
} else {
|
||||
rust_transport_serve(
|
||||
&incoming,
|
||||
&case.limits(case.connections(), case.concurrency),
|
||||
&child,
|
||||
case.secure.then_some(&server_tls),
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let payloads = (0..case.concurrency)
|
||||
.map(|slot| {
|
||||
(0..case.bytes)
|
||||
.map(|index| ((index + slot) % 251) as u8)
|
||||
.collect::<Vec<_>>()
|
||||
.into()
|
||||
})
|
||||
.collect::<Vec<dever_runtime::Bytes>>();
|
||||
let mut clients = transport_clients(case, port, certificates, scope);
|
||||
let replies = transport_batch(generated, &clients, &payloads).await;
|
||||
transport_check(&replies, &payloads);
|
||||
let connections = transport_connections(port);
|
||||
assert_eq!(connections.len(), case.connections());
|
||||
let persistent = if case.cold {
|
||||
transport_close(clients).await;
|
||||
clients = transport_clients(case, port, certificates, scope);
|
||||
BTreeSet::new()
|
||||
} else {
|
||||
connections
|
||||
};
|
||||
let mut elapsed = 0;
|
||||
let mut checksum = 0;
|
||||
for batch in 0..batches {
|
||||
if case.cold && batch != 0 {
|
||||
clients = transport_clients(case, port, certificates, scope);
|
||||
}
|
||||
let start = timed.then(Instant::now);
|
||||
let replies = transport_batch(generated, &clients, &payloads).await;
|
||||
elapsed += start.map_or(0, |start| start.elapsed().as_nanos());
|
||||
checksum += transport_check(&replies, &payloads);
|
||||
let connections = transport_connections(port);
|
||||
assert_eq!(connections.len(), case.connections());
|
||||
if case.cold {
|
||||
transport_close(std::mem::take(&mut clients)).await;
|
||||
} else {
|
||||
assert_eq!(connections, persistent, "persistent transport reconnected");
|
||||
}
|
||||
}
|
||||
transport_close(clients).await;
|
||||
listener.close().unwrap();
|
||||
match server.timeout(5000).await.unwrap() {
|
||||
Timed::Ready(result) => result.unwrap(),
|
||||
Timed::Expired(_) => panic!("transport server did not drain"),
|
||||
}
|
||||
assert!(transport_connections(port).is_empty());
|
||||
(elapsed, checksum)
|
||||
}
|
||||
|
||||
fn transport_measure(
|
||||
runtime: &Runtime,
|
||||
certificates: &Arc<TransportCertificates>,
|
||||
case: TransportCase,
|
||||
generated: bool,
|
||||
batches: usize,
|
||||
timed: bool,
|
||||
) -> (u128, u64) {
|
||||
let certificates = certificates.clone();
|
||||
runtime
|
||||
.run(async move |scope| {
|
||||
let session = scope
|
||||
.spawn(move |child| async move {
|
||||
transport_session(&child, &certificates, case, generated, batches, timed).await
|
||||
})
|
||||
.unwrap();
|
||||
match session.timeout(30000).await.unwrap() {
|
||||
Timed::Ready(result) => result,
|
||||
Timed::Expired(_) => panic!("transport sample exceeded 30 seconds after cleanup"),
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
//! 仅定位成功收发的同步成本,不作为完整资源合同的 Rust 参照。
|
||||
|
||||
use futures_util::{Sink, Stream};
|
||||
use std::{
|
||||
future::{Future, poll_fn},
|
||||
pin::Pin,
|
||||
sync::Mutex,
|
||||
task::{Context, Poll, ready},
|
||||
};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_tungstenite::{
|
||||
WebSocketStream,
|
||||
tungstenite::{Error, Message},
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub trait Transport: AsyncRead + AsyncWrite + Unpin + Send {}
|
||||
impl<Value: AsyncRead + AsyncWrite + Unpin + Send> Transport for Value {}
|
||||
pub type Boxed = Box<dyn Transport>;
|
||||
type Wire = WebSocketStream<Boxed>;
|
||||
|
||||
pub struct Shared<const CANCEL: bool> {
|
||||
wire: Mutex<Wire>,
|
||||
read: tokio::sync::Mutex<()>,
|
||||
write: tokio::sync::Mutex<()>,
|
||||
closed: CancellationToken,
|
||||
closing: CancellationToken,
|
||||
}
|
||||
|
||||
impl<const CANCEL: bool> Shared<CANCEL> {
|
||||
pub fn new(wire: Wire) -> Self {
|
||||
Self {
|
||||
wire: Mutex::new(wire),
|
||||
read: tokio::sync::Mutex::new(()),
|
||||
write: tokio::sync::Mutex::new(()),
|
||||
closed: CancellationToken::new(),
|
||||
closing: CancellationToken::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll<Value>(
|
||||
&self,
|
||||
context: &mut Context<'_>,
|
||||
call: impl FnOnce(Pin<&mut Wire>, &mut Context<'_>) -> Poll<Value>,
|
||||
) -> Poll<Value> {
|
||||
let mut wire = self.wire.lock().unwrap();
|
||||
call(Pin::new(&mut *wire), context)
|
||||
}
|
||||
|
||||
async fn active<Value>(
|
||||
&self,
|
||||
work: impl Future<Output = Result<Value, Error>>,
|
||||
) -> Result<Value, Error> {
|
||||
if CANCEL {
|
||||
tokio::select! {
|
||||
biased;
|
||||
() = self.closed.cancelled() => Err(Error::ConnectionClosed),
|
||||
() = self.closing.cancelled() => Err(Error::ConnectionClosed),
|
||||
result = work => result,
|
||||
}
|
||||
} else {
|
||||
work.await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send(&self, frame: Message) -> Result<(), Error> {
|
||||
self.active(async {
|
||||
let _direction = self.write.lock().await;
|
||||
let mut frame = Some(frame);
|
||||
poll_fn(|context| {
|
||||
self.poll(context, |mut wire, context| {
|
||||
ready!(wire.as_mut().poll_ready(context))?;
|
||||
Poll::Ready(wire.start_send(frame.take().unwrap()))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
poll_fn(|context| self.poll(context, |wire, context| wire.poll_flush(context))).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn receive(&self) -> Result<Option<Message>, Error> {
|
||||
self.active(async {
|
||||
let _direction = self.read.lock().await;
|
||||
poll_fn(|context| self.poll(context, |wire, context| wire.poll_next(context)))
|
||||
.await
|
||||
.transpose()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn close(&mut self) {
|
||||
self.wire.get_mut().unwrap().close(None).await.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// 对照真实发送的生成适配层;双方共用运行库,分别记录发送与完整收发,逐字节校验在计时外。
|
||||
use dever_runtime::{
|
||||
http::websocket::{self as ws, Message, WebSocket},
|
||||
task::{Limits, Runtime},
|
||||
};
|
||||
use std::{hint::black_box, time::Instant};
|
||||
|
||||
async fn send(generated: bool, borrowed: bool, socket: &WebSocket, message: &MESSAGE_TYPE) {
|
||||
if generated && borrowed {
|
||||
WS_SEND_MESSAGE(socket, message).await.unwrap();
|
||||
return;
|
||||
}
|
||||
match message {
|
||||
MESSAGE_TYPE::Variant0(text) if generated => WS_SEND_TEXT(socket, text).await.unwrap(),
|
||||
MESSAGE_TYPE::Variant1(bytes) if generated => WS_SEND_BINARY(socket, bytes).await.unwrap(),
|
||||
MESSAGE_TYPE::Variant0(text) => socket.send(Message::Text(text.clone())).await.unwrap(),
|
||||
MESSAGE_TYPE::Variant1(bytes) => socket.send(Message::Binary(bytes.clone())).await.unwrap(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn payload_size(message: &MESSAGE_TYPE) -> usize {
|
||||
match message {
|
||||
MESSAGE_TYPE::Variant0(text) => text.len(),
|
||||
MESSAGE_TYPE::Variant1(bytes) => bytes.len(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn check(received: Option<Message>, expected: &MESSAGE_TYPE) {
|
||||
match (received, expected) {
|
||||
(Some(Message::Text(actual)), MESSAGE_TYPE::Variant0(expected)) => {
|
||||
assert_eq!(&actual, expected)
|
||||
}
|
||||
(Some(Message::Binary(actual)), MESSAGE_TYPE::Variant1(expected)) => {
|
||||
assert_eq!(&actual, expected)
|
||||
}
|
||||
_ => panic!("WebSocket message changed"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn ready(socket: &WebSocket, input: &MESSAGE_TYPE, count: usize) {
|
||||
socket
|
||||
.send(Message::Text(format!("roundtrip:{count}")))
|
||||
.await
|
||||
.unwrap();
|
||||
send(false, false, socket, input).await;
|
||||
assert_eq!(
|
||||
socket.receive().await.unwrap(),
|
||||
Some(Message::Text("ready".to_owned()))
|
||||
);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if std::env::args().any(|argument| argument == "--peer") {
|
||||
peer::run();
|
||||
return;
|
||||
}
|
||||
let mut peer = peer::Peer::start(1);
|
||||
let runtime = Runtime::new(Limits {
|
||||
threads: 1,
|
||||
workers: 1,
|
||||
tasks: 128,
|
||||
})
|
||||
.unwrap();
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
runtime.run(async |scope| {
|
||||
// 双方轮流使用同一连接;固定协议对端在另一 CPU,隔离连接差异与线程争抢。
|
||||
let socket = ws::connect("127.0.0.1", i64::from(peer.port), "/bench", &ws::Limits::default(), scope).await.unwrap();
|
||||
for bytes in [64, 65536] {
|
||||
for binary in [false, true] {
|
||||
let input = if binary {
|
||||
MESSAGE_TYPE::Variant1((0..bytes).map(|index| (index % 251) as u8).collect::<Vec<_>>().into())
|
||||
} else {
|
||||
MESSAGE_TYPE::Variant0("雪a".repeat(bytes / 4))
|
||||
};
|
||||
for borrowed in [false, true] {
|
||||
let name = format!("ws_{}_{}_{}", if binary { "binary" } else { "text" }, if borrowed { "borrowed" } else { "temporary" }, bytes);
|
||||
for generated in [true, false] {
|
||||
ready(&socket, &input, 20).await;
|
||||
for _ in 0..20 {
|
||||
send(generated, borrowed, &socket, &input).await;
|
||||
check(socket.receive().await.unwrap(), &input);
|
||||
}
|
||||
}
|
||||
for sample in 0..15 {
|
||||
let mut order = [("dever", true), ("rust_runtime", false), ("control", true)];
|
||||
order.rotate_left(sample % 3);
|
||||
for (implementation, generated) in order {
|
||||
ready(&socket, &input, 200).await;
|
||||
let mut sent = 0;
|
||||
let mut roundtrip = 0;
|
||||
let mut checksum = 0;
|
||||
for _ in 0..200 {
|
||||
let start = Instant::now();
|
||||
send(generated, borrowed, &socket, black_box(&input)).await;
|
||||
sent += start.elapsed().as_nanos();
|
||||
let received = socket.receive().await.unwrap();
|
||||
roundtrip += start.elapsed().as_nanos();
|
||||
check(received, &input);
|
||||
checksum += payload_size(black_box(&input));
|
||||
}
|
||||
assert_eq!(checksum, bytes * 200);
|
||||
println!("{name}_send,{sample},{implementation},{sent},{checksum}");
|
||||
println!("{name}_roundtrip,{sample},{implementation},{roundtrip},{checksum}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
socket.close(1000, "").await.unwrap();
|
||||
}).unwrap();
|
||||
peer.finish();
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// 独立协议对端,不调用 Dever HTTP/WebSocket;绑到另一个 CPU,避免双方争抢工作线程。
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use std::{
|
||||
io::{BufRead, BufReader, Write},
|
||||
process::{Child, Command, Stdio},
|
||||
};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio_tungstenite::{
|
||||
WebSocketStream,
|
||||
tungstenite::{Message, protocol::WebSocketConfig},
|
||||
};
|
||||
|
||||
pub fn config() -> WebSocketConfig {
|
||||
WebSocketConfig::default()
|
||||
.read_buffer_size(4096)
|
||||
.write_buffer_size(0)
|
||||
.max_write_buffer_size(1048576 + 4096)
|
||||
.max_message_size(Some(1048576))
|
||||
.max_frame_size(Some(1048576))
|
||||
}
|
||||
|
||||
pub struct Peer {
|
||||
child: Child,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
impl Peer {
|
||||
pub fn start(connections: usize) -> Self {
|
||||
let cpu = std::env::var("DEVER_WS_PEER_CPU").expect("set a separate peer CPU");
|
||||
let executable = std::env::var_os("DEVER_WS_PEER_BINARY")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|| std::env::current_exe().unwrap());
|
||||
let mut child = Command::new("taskset")
|
||||
.args(["-c", &cpu])
|
||||
.arg(executable)
|
||||
.arg("--peer")
|
||||
.env("DEVER_WS_PEER_CONNECTIONS", connections.to_string())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let mut ready = String::new();
|
||||
BufReader::new(child.stdout.take().unwrap())
|
||||
.read_line(&mut ready)
|
||||
.unwrap();
|
||||
let port = ready.trim().parse().expect("peer did not report its port");
|
||||
Self { child, port }
|
||||
}
|
||||
|
||||
pub fn finish(&mut self) {
|
||||
assert!(self.child.wait().unwrap().success());
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Peer {
|
||||
fn drop(&mut self) {
|
||||
// 断言失败也回收本次测试创建的子进程。
|
||||
let _ = self.child.kill();
|
||||
let _ = self.child.wait();
|
||||
}
|
||||
}
|
||||
|
||||
async fn serve(mut socket: WebSocketStream<TcpStream>) {
|
||||
while let Some(command) = socket.next().await {
|
||||
let command = command.unwrap();
|
||||
if command.is_close() {
|
||||
socket.flush().await.unwrap();
|
||||
return;
|
||||
}
|
||||
let (mode, count) = command.to_text().unwrap().split_once(':').unwrap();
|
||||
let count: usize = count.parse().unwrap();
|
||||
assert!(mode == "batch" || mode == "roundtrip");
|
||||
let expected = socket.next().await.unwrap().unwrap();
|
||||
socket.send(Message::Text("ready".into())).await.unwrap();
|
||||
for _ in 0..count {
|
||||
let actual = socket.next().await.unwrap().unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
if mode == "roundtrip" {
|
||||
socket.send(actual).await.unwrap();
|
||||
}
|
||||
}
|
||||
if mode == "batch" {
|
||||
socket.send(expected).await.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
let connections: usize = std::env::var("DEVER_WS_PEER_CONNECTIONS")
|
||||
.expect("set expected peer connections")
|
||||
.parse()
|
||||
.unwrap();
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
runtime.block_on(async {
|
||||
tokio::time::timeout(std::time::Duration::from_secs(120), async {
|
||||
let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
|
||||
println!("{}", listener.local_addr().unwrap().port());
|
||||
std::io::stdout().flush().unwrap();
|
||||
let mut sessions = Vec::new();
|
||||
for _ in 0..connections {
|
||||
let (stream, _) = listener.accept().await.unwrap();
|
||||
stream.set_nodelay(true).unwrap();
|
||||
sessions.push(tokio::spawn(async move {
|
||||
let socket =
|
||||
tokio_tungstenite::accept_async_with_config(stream, Some(config()))
|
||||
.await
|
||||
.unwrap();
|
||||
serve(socket).await;
|
||||
}));
|
||||
}
|
||||
for session in sessions {
|
||||
session.await.unwrap();
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("protocol peer timed out");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
// 成功会话的独立协议参照。原共享运行库基准保留;这里不声称覆盖别名、取消和资源归属合同。
|
||||
use dever_runtime::{
|
||||
http::websocket::{self as ws, Message, WebSocket},
|
||||
task::{Limits, Runtime},
|
||||
};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use std::{
|
||||
hint::black_box,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_tungstenite::{WebSocketStream, tungstenite::Message as Frame};
|
||||
|
||||
enum Client {
|
||||
Dever(WebSocket),
|
||||
Rust(WebSocketStream<TcpStream>),
|
||||
Boxed(WebSocketStream<layers::Boxed>),
|
||||
Locked(layers::Shared<false>),
|
||||
Cancelled(layers::Shared<true>),
|
||||
Shared {
|
||||
socket: shared::Socket,
|
||||
_owner: shared::Owner,
|
||||
},
|
||||
}
|
||||
|
||||
impl Client {
|
||||
async fn send(&mut self, value: &MESSAGE_TYPE) {
|
||||
if let Self::Dever(socket) = self {
|
||||
match value {
|
||||
MESSAGE_TYPE::Variant0(text) => WS_SEND_TEXT(socket, text).await.unwrap(),
|
||||
MESSAGE_TYPE::Variant1(bytes) => WS_SEND_BINARY(socket, bytes).await.unwrap(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 所有参照采用同样的借用输入、拥有消息及写期限。
|
||||
let frame = match value {
|
||||
MESSAGE_TYPE::Variant0(text) => Frame::Text(text.clone().into()),
|
||||
MESSAGE_TYPE::Variant1(bytes) => Frame::Binary(bytes.clone()),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if let Self::Shared { socket, .. } = self {
|
||||
socket.send(frame).await.unwrap();
|
||||
return;
|
||||
}
|
||||
assert!(frame.len() <= 1048576);
|
||||
tokio::time::timeout(Duration::from_millis(5000), async {
|
||||
match self {
|
||||
Self::Rust(socket) => socket.send(frame).await,
|
||||
Self::Boxed(socket) => socket.send(frame).await,
|
||||
Self::Locked(socket) => socket.send(frame).await,
|
||||
Self::Cancelled(socket) => socket.send(frame).await,
|
||||
Self::Dever(_) => unreachable!(),
|
||||
Self::Shared { .. } => unreachable!(),
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn receive(&mut self) -> Message {
|
||||
if let Self::Dever(socket) = self {
|
||||
return socket.receive().await.unwrap().unwrap();
|
||||
}
|
||||
let frame = if let Self::Shared { socket, .. } = self {
|
||||
socket.receive().await.unwrap().unwrap()
|
||||
} else {
|
||||
tokio::time::timeout(Duration::from_millis(60000), async {
|
||||
match self {
|
||||
Self::Rust(socket) => socket.next().await.transpose(),
|
||||
Self::Boxed(socket) => socket.next().await.transpose(),
|
||||
Self::Locked(socket) => socket.receive().await,
|
||||
Self::Cancelled(socket) => socket.receive().await,
|
||||
Self::Dever(_) => unreachable!(),
|
||||
Self::Shared { .. } => unreachable!(),
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
};
|
||||
match frame {
|
||||
Frame::Text(value) => Message::Text(value.to_string()),
|
||||
Frame::Binary(value) => Message::Binary(value),
|
||||
_ => panic!("unexpected protocol frame"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn close(&mut self) {
|
||||
match self {
|
||||
Self::Dever(socket) => socket.close(1000, "").await.unwrap(),
|
||||
Self::Rust(socket) => socket.close(None).await.unwrap(),
|
||||
Self::Boxed(socket) => socket.close(None).await.unwrap(),
|
||||
Self::Locked(socket) => socket.close().await,
|
||||
Self::Cancelled(socket) => socket.close().await,
|
||||
Self::Shared { socket, .. } => socket.close(1000, "").await.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check(actual: Message, expected: &MESSAGE_TYPE) {
|
||||
match (actual, expected) {
|
||||
(Message::Text(actual), MESSAGE_TYPE::Variant0(expected)) => assert_eq!(&actual, expected),
|
||||
(Message::Binary(actual), MESSAGE_TYPE::Variant1(expected)) => {
|
||||
assert_eq!(&actual, expected)
|
||||
}
|
||||
_ => panic!("payload changed"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn measure(client: &mut Client, input: &MESSAGE_TYPE, mode: &str, count: usize) -> u128 {
|
||||
client
|
||||
.send(&MESSAGE_TYPE::Variant0(format!("{mode}:{count}")))
|
||||
.await;
|
||||
client.send(input).await;
|
||||
check(
|
||||
client.receive().await,
|
||||
&MESSAGE_TYPE::Variant0("ready".to_owned()),
|
||||
);
|
||||
if mode == "batch" {
|
||||
// 批量发送后单独收确认,避免把每条消息的回声等待计入发送成本。
|
||||
let start = Instant::now();
|
||||
for _ in 0..count {
|
||||
client.send(black_box(input)).await;
|
||||
}
|
||||
let elapsed = start.elapsed().as_nanos();
|
||||
check(client.receive().await, input);
|
||||
elapsed
|
||||
} else {
|
||||
let mut elapsed = 0;
|
||||
for _ in 0..count {
|
||||
let start = Instant::now();
|
||||
client.send(black_box(input)).await;
|
||||
let actual = client.receive().await;
|
||||
elapsed += start.elapsed().as_nanos();
|
||||
check(actual, input);
|
||||
}
|
||||
elapsed
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if std::env::args().any(|argument| argument == "--peer") {
|
||||
peer::run();
|
||||
return;
|
||||
}
|
||||
let mut peer = peer::Peer::start(2);
|
||||
let reference = std::env::var("DEVER_WS_REFERENCE").unwrap_or_else(|_| "raw".to_owned());
|
||||
let label = match reference.as_str() {
|
||||
"raw" => "rust_protocol",
|
||||
"boxed" => "rust_boxed_diagnostic",
|
||||
"locked" => "rust_locked_diagnostic",
|
||||
"cancelled" => "rust_cancelled_diagnostic",
|
||||
"shared" => "rust_shared",
|
||||
_ => panic!("unknown WebSocket reference"),
|
||||
};
|
||||
let runtime = Runtime::new(Limits {
|
||||
threads: 1,
|
||||
workers: 1,
|
||||
tasks: 128,
|
||||
})
|
||||
.unwrap();
|
||||
println!("case,sample,implementation,elapsed_ns,checksum");
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
tokio::time::timeout(Duration::from_secs(120), async {
|
||||
let socket = ws::connect(
|
||||
"127.0.0.1",
|
||||
i64::from(peer.port),
|
||||
"/bench",
|
||||
&ws::Limits::default(),
|
||||
scope,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let stream = TcpStream::connect(("127.0.0.1", peer.port)).await.unwrap();
|
||||
stream.set_nodelay(true).unwrap();
|
||||
let url = format!("ws://127.0.0.1:{}/bench", peer.port);
|
||||
let control = if reference == "raw" || reference == "shared" {
|
||||
let (wire, _) = tokio_tungstenite::client_async_with_config(
|
||||
url,
|
||||
stream,
|
||||
Some(peer::config()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
if reference == "shared" {
|
||||
let (socket, owner) = shared::Socket::new(
|
||||
wire,
|
||||
shared::Limits {
|
||||
message: 1048576,
|
||||
idle: Duration::from_millis(60000),
|
||||
write: Duration::from_millis(5000),
|
||||
},
|
||||
);
|
||||
Client::Shared {
|
||||
socket,
|
||||
_owner: owner,
|
||||
}
|
||||
} else {
|
||||
Client::Rust(wire)
|
||||
}
|
||||
} else {
|
||||
let stream: layers::Boxed = Box::new(stream);
|
||||
let (wire, _) = tokio_tungstenite::client_async_with_config(
|
||||
url,
|
||||
stream,
|
||||
Some(peer::config()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
match reference.as_str() {
|
||||
"boxed" => Client::Boxed(wire),
|
||||
"locked" => Client::Locked(layers::Shared::new(wire)),
|
||||
"cancelled" => Client::Cancelled(layers::Shared::new(wire)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
};
|
||||
let mut clients = [Client::Dever(socket), control];
|
||||
for bytes in [64, 65536] {
|
||||
for binary in [false, true] {
|
||||
let input = if binary {
|
||||
MESSAGE_TYPE::Variant1(
|
||||
(0..bytes)
|
||||
.map(|index| (index % 251) as u8)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
MESSAGE_TYPE::Variant0("雪a".repeat(bytes / 4))
|
||||
};
|
||||
for mode in ["batch", "roundtrip"] {
|
||||
let name = format!(
|
||||
"ws_protocol_{}_{bytes}_{mode}",
|
||||
if binary { "binary" } else { "text" }
|
||||
);
|
||||
for client in &mut clients {
|
||||
black_box(measure(client, &input, mode, 20).await);
|
||||
}
|
||||
for sample in 0..15 {
|
||||
let mut order = [("dever", 0), (label, 1), ("control", 0)];
|
||||
order.rotate_left(sample % 3);
|
||||
if sample / 3 % 2 == 1 {
|
||||
order.swap(0, 1);
|
||||
}
|
||||
for (implementation, index) in order {
|
||||
let elapsed =
|
||||
measure(&mut clients[index], &input, mode, 200).await;
|
||||
println!(
|
||||
"{name},{sample},{implementation},{elapsed},{}",
|
||||
bytes * 200
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for client in &mut clients {
|
||||
client.close().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("protocol comparison timed out");
|
||||
})
|
||||
.unwrap();
|
||||
peer.finish();
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
// 独立 Rust 共享会话参照,不调用 Dever 运行库;覆盖收发、期限、关闭和所有权退出。
|
||||
// TCP/WebSocket 握手在计时外完成;此参照不代表 TLS、HTTP 升级或整个 Scope 系统。
|
||||
mod wire;
|
||||
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
future::Future,
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{net::TcpStream, sync::Mutex as Direction};
|
||||
use tokio_tungstenite::{
|
||||
WebSocketStream,
|
||||
tungstenite::{
|
||||
Message,
|
||||
protocol::{CloseFrame, frame::coding::CloseCode},
|
||||
},
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use wire::Wire;
|
||||
|
||||
pub type Error = Cow<'static, str>;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Limits {
|
||||
pub message: usize,
|
||||
pub idle: Duration,
|
||||
pub write: Duration,
|
||||
}
|
||||
|
||||
struct State {
|
||||
socket: Mutex<Option<WebSocketStream<TcpStream>>>,
|
||||
reader: Direction<()>,
|
||||
writer: Direction<()>,
|
||||
closing: CancellationToken,
|
||||
ended: AtomicBool,
|
||||
limits: Limits,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn abort(&self) {
|
||||
self.closing.cancel();
|
||||
self.socket.lock().unwrap().take();
|
||||
}
|
||||
|
||||
async fn active<Value>(
|
||||
&self,
|
||||
work: impl Future<Output = Result<Value, Error>>,
|
||||
) -> Result<Value, Error> {
|
||||
tokio::select! {
|
||||
biased;
|
||||
() = self.closing.cancelled() => Err("WebSocket is closing".into()),
|
||||
result = work => result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有者退出同步释放协议连接;保留别名不能延长连接生命。
|
||||
pub struct Owner(Arc<State>);
|
||||
impl Drop for Owner {
|
||||
fn drop(&mut self) {
|
||||
self.0.abort();
|
||||
}
|
||||
}
|
||||
|
||||
struct Writing<'socket>(Option<&'socket State>);
|
||||
impl Drop for Writing<'_> {
|
||||
fn drop(&mut self) {
|
||||
if let Some(state) = self.0 {
|
||||
state.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Socket(Arc<State>);
|
||||
|
||||
impl Socket {
|
||||
pub fn new(socket: WebSocketStream<TcpStream>, limits: Limits) -> (Self, Owner) {
|
||||
let state = Arc::new(State {
|
||||
socket: Mutex::new(Some(socket)),
|
||||
reader: Direction::new(()),
|
||||
writer: Direction::new(()),
|
||||
closing: CancellationToken::new(),
|
||||
ended: AtomicBool::new(false),
|
||||
limits,
|
||||
});
|
||||
(Self(state.clone()), Owner(state))
|
||||
}
|
||||
|
||||
pub async fn send(&self, message: Message) -> Result<(), Error> {
|
||||
let limit = match message {
|
||||
Message::Text(_) | Message::Binary(_) => self.0.limits.message,
|
||||
Message::Ping(_) | Message::Pong(_) => 125,
|
||||
_ => return Err("use close for the closing handshake".into()),
|
||||
};
|
||||
if message.len() > limit {
|
||||
return Err("WebSocket message exceeds byte limit".into());
|
||||
}
|
||||
self.0
|
||||
.active(async {
|
||||
tokio::time::timeout(self.0.limits.write, async {
|
||||
let _writer = self.0.writer.lock().await;
|
||||
let mut writing = Writing(Some(&self.0));
|
||||
Wire(&self.0.socket).send(message).await.map_err(failure)?;
|
||||
writing.0 = None;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|_| Error::from("WebSocket write timed out"))?
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn receive(&self) -> Result<Option<Message>, Error> {
|
||||
if self.0.ended.load(Ordering::Acquire) {
|
||||
return Ok(None);
|
||||
}
|
||||
let result = self
|
||||
.0
|
||||
.active(async {
|
||||
tokio::time::timeout(self.0.limits.idle, async {
|
||||
let _reader = self.0.reader.lock().await;
|
||||
if self.0.ended.load(Ordering::Acquire) {
|
||||
return Ok(None);
|
||||
}
|
||||
let message = Wire(&self.0.socket)
|
||||
.next()
|
||||
.await
|
||||
.transpose()
|
||||
.map_err(failure)?;
|
||||
match message {
|
||||
Some(Message::Ping(_)) => {
|
||||
self.reply().await?;
|
||||
Ok(message)
|
||||
}
|
||||
Some(Message::Close(_)) | None => {
|
||||
let _closing = Writing(Some(&self.0));
|
||||
self.reply().await?;
|
||||
self.0.ended.store(true, Ordering::Release);
|
||||
Ok(None)
|
||||
}
|
||||
_ => Ok(message),
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| Error::from("WebSocket receive timed out"))?
|
||||
})
|
||||
.await;
|
||||
if result.is_err() && !self.0.closing.is_cancelled() {
|
||||
self.0.abort();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
async fn reply(&self) -> Result<(), Error> {
|
||||
let _writer = self.0.writer.lock().await;
|
||||
let mut writing = Writing(Some(&self.0));
|
||||
tokio::time::timeout(self.0.limits.write, Wire(&self.0.socket).flush())
|
||||
.await
|
||||
.map_err(|_| Error::from("WebSocket reply timed out"))?
|
||||
.map_err(failure)?;
|
||||
writing.0 = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn close(&self, code: u16, reason: &str) -> Result<(), Error> {
|
||||
let code = CloseCode::from(code);
|
||||
if !code.is_allowed() || reason.len() > 123 {
|
||||
return Err("invalid WebSocket close frame".into());
|
||||
}
|
||||
if self.0.closing.is_cancelled() {
|
||||
return Err("WebSocket is closed".into());
|
||||
}
|
||||
self.0.closing.cancel();
|
||||
let _closing = Writing(Some(&self.0));
|
||||
tokio::time::timeout(self.0.limits.write, async {
|
||||
let writer = self.0.writer.lock().await;
|
||||
Wire(&self.0.socket)
|
||||
.send(Message::Close(Some(CloseFrame {
|
||||
code,
|
||||
reason: reason.to_owned().into(),
|
||||
})))
|
||||
.await
|
||||
.map_err(failure)?;
|
||||
drop(writer);
|
||||
let _reader = self.0.reader.lock().await;
|
||||
loop {
|
||||
let message = Wire(&self.0.socket)
|
||||
.next()
|
||||
.await
|
||||
.transpose()
|
||||
.map_err(failure)?;
|
||||
Wire(&self.0.socket).flush().await.map_err(failure)?;
|
||||
if matches!(message, Some(Message::Close(_)) | None) {
|
||||
self.0.ended.store(true, Ordering::Release);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| Error::from("WebSocket close timed out"))?
|
||||
}
|
||||
}
|
||||
|
||||
fn failure(error: tokio_tungstenite::tungstenite::Error) -> Error {
|
||||
error.to_string().into()
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use futures_util::{Sink, Stream};
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::Mutex,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_tungstenite::{
|
||||
WebSocketStream,
|
||||
tungstenite::{Error, Message},
|
||||
};
|
||||
|
||||
// 共享引用仅在一次协议调用内借用连接;send/next 的状态机由 futures-util 提供。
|
||||
pub(super) struct Wire<'socket>(pub &'socket Mutex<Option<WebSocketStream<TcpStream>>>);
|
||||
|
||||
impl Wire<'_> {
|
||||
fn with<Value>(
|
||||
&self,
|
||||
call: impl FnOnce(Pin<&mut WebSocketStream<TcpStream>>) -> Value,
|
||||
) -> Result<Value, Error> {
|
||||
let mut socket = self.0.lock().unwrap();
|
||||
socket
|
||||
.as_mut()
|
||||
.map(|socket| call(Pin::new(socket)))
|
||||
.ok_or(Error::ConnectionClosed)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sink<Message> for Wire<'_> {
|
||||
type Error = Error;
|
||||
|
||||
fn poll_ready(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
self.with(|socket| socket.poll_ready(context))?
|
||||
}
|
||||
|
||||
fn start_send(self: Pin<&mut Self>, message: Message) -> Result<(), Error> {
|
||||
self.with(|socket| socket.start_send(message))?
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
self.with(|socket| socket.poll_flush(context))?
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
self.with(|socket| socket.poll_close(context))?
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for Wire<'_> {
|
||||
type Item = Result<Message, Error>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match self.with(|socket| socket.poll_next(context)) {
|
||||
Ok(result) => result,
|
||||
Err(error) => Poll::Ready(Some(Err(error))),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user