bench: port wire_vs_bast from alktty, add union + validate_bytes groups
The wire_vs_bast bench originated in alktty as the uncommitted curiosity probe that surfaced review #004's 400x read gap (the driver for the 0.3.0 compiled-forms release). It now lives here so alktype owns its perf story; the alktty-only async roundtrip group (tokio ChunkReader/ ChunkWriter over a duplex pipe) was dropped — that measures alktty's I/O stack, not this engine. The alktty copy is deleted. Groups: - read_chunk_stream / write_chunk_stream — the original ChunkHeader shape, byte-identical methodology, so numbers stay comparable with the historical series (400x → ~18x on read p64). - read_packet_stream (new) — SFTP-shaped byte-discriminator union (Read/Write variants, Write carries a length-prefixed bytes field): exercises CompositePlan::Union dispatch + variant walks + variable reads, the case ADR-011's framing argument was about. The alktype consumer pattern follows the documented FieldValue::Union contract; a pre-measurement parity check locks the pattern (variant walk size + disc size == packet size) so the stream loop can't drift silently. - validate_stream (new) — engine.validate_bytes per buffer (materialize + ValidationPlan walk), the read+validate-on-untrusted-stream shape alkcall cares about; closes the phase-7/8 bench deferral. - one-shots — engine_compile, sequential_reader_new, layout_build. criterion 0.7 dev-dep (default-features off). Benches don't affect the wasm gate (bench targets never compile under wasm32-unknown-unknown). Numbers (1024 chunks/iter, Xeon D-1521, shared box — ±10% noise): - read p64: hand 5.7 µs / alktype 104.8 µs (~18x; parity with the phase-2/8 record of 98-99 ns/chunk) - read p4k: hand 12.1 µs / alktype 107.5 µs - write p64: hand 14.0 µs / alktype 37.5 µs; p4k: 324/362 µs - packet read p64: hand 3.3 µs / alktype 622.6 µs (~189x — dominated by per-field String allocs + variant reader construction; the read_next (String, FieldValue) signature is pinned by the semver contract) - validate: header 458 ns/chunk, packet p64 2.23 µs, packet p4k 47 µs - one-shots: compile 615 µs (meta-schema dominated), reader_new 15.7 ns, layout_build 343 ns Verification: cargo test --release (566 tests green), clippy --all-targets -D warnings, wasm32-unknown-unknown build clean.
This commit is contained in:
1 parent
557a0d791e
commit
dea96f0195
3 files changed
+839
-1
No files matched your search
Generated
+187
@@ -29,6 +29,7 @@ dependencies = [
|
||||
name = "alktype"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"jsonschema",
|
||||
"serde_json",
|
||||
]
|
||||
@@ -39,6 +40,18 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
@@ -84,12 +97,107 @@ version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928"
|
||||
dependencies = [
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"itertools",
|
||||
"num-traits",
|
||||
"oorandom",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.11.0"
|
||||
@@ -107,6 +215,12 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
||||
|
||||
[[package]]
|
||||
name = "email_address"
|
||||
version = "0.2.9"
|
||||
@@ -174,6 +288,17 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
@@ -304,6 +429,15 @@ dependencies = [
|
||||
"hashbrown 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -479,6 +613,12 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "outref"
|
||||
version = "0.5.2"
|
||||
@@ -628,6 +768,15 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -733,6 +882,16 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinytemplate"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-general-category"
|
||||
version = "1.1.0"
|
||||
@@ -773,6 +932,16 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.4+wasi-0.2.12"
|
||||
@@ -827,12 +996,30 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
|
||||
+9
-1
@@ -19,4 +19,12 @@ default = []
|
||||
|
||||
[dependencies]
|
||||
jsonschema = { version = "0.46", default-features = false }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1"
|
||||
criterion = { version = "0.7", default-features = false }
|
||||
|
||||
[[bench]]
|
||||
name = "wire_vs_bast"
|
||||
harness = false
|
||||
@@ -0,0 +1,643 @@
|
||||
//! Informal speed comparison: hand-rolled codec logic vs alktype-driven
|
||||
//! codec over the same wire shapes.
|
||||
//!
|
||||
//! History: this bench originated (uncommitted) in `alktty` as the
|
||||
//! curiosity probe that surfaced review #004's 400x read gap — the
|
||||
//! finding that drove the 0.3.0 compiled-forms release (ADR-011/012).
|
||||
//! It now lives here so alktype owns its perf story. The alktty-only
|
||||
//! async roundtrip group (tokio `ChunkReader`/`ChunkWriter` over a
|
||||
//! duplex pipe) was dropped — that measures alktty's I/O stack, not
|
||||
//! this engine.
|
||||
//!
|
||||
//! The alktype engine / layout / plans are built **once outside** the
|
||||
//! measured routine, per the "build cost is paid once" framing.
|
||||
//!
|
||||
//! Shapes:
|
||||
//!
|
||||
//! - **ChunkHeader** — a 5-byte header (`stream_type: uint8`,
|
||||
//! `length: uint32` big-endian). The original shape, kept so numbers
|
||||
//! stay comparable with the historical series (review #004: 400x →
|
||||
//! 0.3.0: ~18x on read p64).
|
||||
//! - **Read** — the hand-rolled path mirrors
|
||||
//! `ChunkReader::read_chunk_after_peek` minus the tokio I/O
|
||||
//! (identical overhead on both sides): validate `stream_type <= 4`,
|
||||
//! parse `u32::from_be_bytes`, slice the payload. The alktype path
|
||||
//! drives `SequentialReader::read_next` over the `ChunkHeader`
|
||||
//! struct, then slices the payload at the parsed length. Both
|
||||
//! return a `&[u8]` payload view — no allocation in either measured
|
||||
//! path.
|
||||
//! - **Write** — serialize the 5-byte header. Hand-rolled mirrors
|
||||
//! `ChunkWriter::write_chunk`'s header writes; alktype uses
|
||||
//! `PackedLayout` offsets (built once) and
|
||||
//! `data_access::write_u8`/`write_u32` at those offsets.
|
||||
//! - **Packet** — a byte-offset-discriminator union
|
||||
//! (`Read {handle, length}` / `Write {handle, length, data: bytes}`),
|
||||
//! the SFTP-shaped case ADR-011's framing argument was about:
|
||||
//! exercises `CompositePlan::Union` dispatch, variant walks, and
|
||||
//! length-prefixed variable reads. The alktype consumer pattern is
|
||||
//! the documented one: `read_next` on the root yields
|
||||
//! `FieldValue::Union { discriminator, variant_start }`, the consumer
|
||||
//! selects the pre-built reader for that variant and walks it over
|
||||
//! `&buf[variant_start..]`.
|
||||
//! - **validate_bytes** — `engine.validate_bytes` per buffer
|
||||
//! (materialize + `ValidationPlan` walk, ADR-010/ADR-012 §3): the
|
||||
//! read+validate-on-untrusted-stream shape `alkcall` cares about. No
|
||||
//! hand comparator: a hand-rolled codec validates inline during the
|
||||
//! (already measured) parse, while `validate_bytes` additionally
|
||||
//! materializes a `Value` tree per buffer — the honest reading is the
|
||||
//! absolute per-chunk cost.
|
||||
//!
|
||||
//! One-shots (paid once at startup, not per chunk):
|
||||
//! `alktype_engine_compile` (dominated by BAST meta-schema
|
||||
//! validation), `alktype_sequential_reader_new` (an `Arc::clone`),
|
||||
//! `alktype_layout_build`.
|
||||
//!
|
||||
//! Two payload sizes (64 B, 4 KiB) so per-chunk fixed overhead is
|
||||
//! visible separately from payload-copy cost.
|
||||
//!
|
||||
//! Run: `cargo bench --bench wire_vs_bast`
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use std::hint::black_box;
|
||||
|
||||
use alktype::{
|
||||
data_access, AlkTypeEngine, Endian, FieldValue, LayoutBuilder, LayoutMode, PackedLayout,
|
||||
ReadPlan, SequentialReader,
|
||||
};
|
||||
|
||||
/// Mirrors `alktty::wire::MAX_CHUNK_LEN` — the hand-rolled comparator
|
||||
/// validates against the same cap the real codec enforces.
|
||||
const MAX_CHUNK_LEN: u32 = 16 * 1024 * 1024;
|
||||
|
||||
/// The `ChunkHeader` BAST definition. The `StreamType` enum is
|
||||
/// intentionally NOT used — BAST enums encode as `u32`, but the
|
||||
/// on-wire `stream_type` is a `uint8`; both sides read it as `uint8`.
|
||||
const CHUNK_HEADER_BAST: &str = r#"{
|
||||
"$schema": "https://alk.dev/bast/v1/schema",
|
||||
"$defs": {
|
||||
"ChunkHeader": {
|
||||
"kind": "struct",
|
||||
"endian": "big",
|
||||
"fields": [
|
||||
{ "name": "stream_type", "kind": "uint8" },
|
||||
{ "name": "length", "kind": "uint32" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
/// SFTP-shaped byte-discriminator union: one byte selects the variant,
|
||||
/// `Write` carries a trailing length-prefixed `bytes` field. The root
|
||||
/// struct wraps the union (`AlkTypeEngine::compile` requires a struct
|
||||
/// root); mapping keys are the stringified `uint8` discriminator
|
||||
/// values.
|
||||
const PACKET_BAST: &str = r##"{
|
||||
"$schema": "https://alk.dev/bast/v1/schema",
|
||||
"$defs": {
|
||||
"Packet": {
|
||||
"kind": "struct",
|
||||
"endian": "big",
|
||||
"fields": [
|
||||
{ "name": "event", "kind": { "$ref": "#/$defs/Event" } }
|
||||
]
|
||||
},
|
||||
"Event": {
|
||||
"kind": "union",
|
||||
"discriminator": { "kind": "byte", "offset": 0, "type": "uint8" },
|
||||
"mapping": {
|
||||
"5": { "$ref": "#/$defs/Read" },
|
||||
"6": { "$ref": "#/$defs/Write" }
|
||||
}
|
||||
},
|
||||
"Read": {
|
||||
"kind": "struct",
|
||||
"endian": "big",
|
||||
"fields": [
|
||||
{ "name": "handle", "kind": "uint32" },
|
||||
{ "name": "length", "kind": "uint32" }
|
||||
]
|
||||
},
|
||||
"Write": {
|
||||
"kind": "struct",
|
||||
"endian": "big",
|
||||
"fields": [
|
||||
{ "name": "handle", "kind": "uint32" },
|
||||
{ "name": "length", "kind": "uint32" },
|
||||
{ "name": "data", "kind": "bytes" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}"##;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ChunkHeader fixtures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// One chunk's worth of bytes on the wire: 5-byte header + payload.
|
||||
fn make_chunk_bytes(stream_type: u8, payload: &[u8]) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(5 + payload.len());
|
||||
buf.push(stream_type);
|
||||
buf.extend_from_slice(&(payload.len() as u32).to_be_bytes());
|
||||
buf.extend_from_slice(payload);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Concatenate `n` chunks into one buffer, each with `payload_len` bytes.
|
||||
fn make_chunk_stream(n: usize, payload_len: usize) -> Vec<u8> {
|
||||
let payload = vec![0xA5u8; payload_len];
|
||||
let mut buf = Vec::with_capacity(n * (5 + payload_len));
|
||||
for i in 0..n {
|
||||
let st = (i % 5) as u8;
|
||||
buf.extend_from_slice(&make_chunk_bytes(st, &payload));
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hand-rolled chunk read: mirrors ChunkReader::read_chunk_after_peek minus
|
||||
// the tokio I/O. Returns (stream_type, payload) so the compiler can't
|
||||
// elide the work. Validates stream_type <= 4 and length <= MAX_CHUNK_LEN.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[inline]
|
||||
fn hand_read_header(buf: &[u8]) -> Option<(u8, u32)> {
|
||||
if buf.len() < 5 {
|
||||
return None;
|
||||
}
|
||||
let stream_type = buf[0];
|
||||
if stream_type > 4 {
|
||||
return None;
|
||||
}
|
||||
let length = u32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]);
|
||||
if length > MAX_CHUNK_LEN {
|
||||
return None;
|
||||
}
|
||||
Some((stream_type, length))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn hand_read_chunk(buf: &[u8]) -> Option<(u8, &[u8])> {
|
||||
let (st, len) = hand_read_header(buf)?;
|
||||
let end = 5usize.checked_add(len as usize)?;
|
||||
if buf.len() < end {
|
||||
return None;
|
||||
}
|
||||
Some((st, &buf[5..end]))
|
||||
}
|
||||
|
||||
/// Drive `hand_read_chunk` across `n` contiguous chunks in `buf`.
|
||||
/// Returns the total payload bytes consumed (so the loop body is
|
||||
/// meaningfully used and not optimized away).
|
||||
fn hand_read_stream(buf: &[u8], n: usize) -> usize {
|
||||
let mut pos = 0usize;
|
||||
let mut total = 0usize;
|
||||
for _ in 0..n {
|
||||
let (st, payload) = match hand_read_chunk(&buf[pos..]) {
|
||||
Some(v) => v,
|
||||
None => break,
|
||||
};
|
||||
total += payload.len();
|
||||
pos += 5 + payload.len();
|
||||
black_box(st);
|
||||
}
|
||||
black_box(total)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// alktype chunk read: SequentialReader over ChunkHeader. The reader is
|
||||
// constructed once per benchmark group and reset() between chunks. After
|
||||
// the header read, the payload is sliced at the parsed length — same as
|
||||
// the hand-rolled path. We do NOT re-read a length prefix for the payload
|
||||
// (that would be the double-prefix problem).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn alktype_read_stream(buf: &[u8], n: usize, reader: &mut SequentialReader) -> usize {
|
||||
let mut pos = 0usize;
|
||||
let mut total = 0usize;
|
||||
for _ in 0..n {
|
||||
reader.reset();
|
||||
let st = match reader.read_next(&buf[pos..]) {
|
||||
Ok(Some((_, FieldValue::U8(v)))) => v,
|
||||
_ => break,
|
||||
};
|
||||
let len = match reader.read_next(&buf[pos..]) {
|
||||
Ok(Some((_, FieldValue::U32(v)))) => v,
|
||||
_ => break,
|
||||
};
|
||||
if len > MAX_CHUNK_LEN {
|
||||
break;
|
||||
}
|
||||
let end = match 5usize.checked_add(len as usize) {
|
||||
Some(e) if e <= buf.len() - pos => e,
|
||||
_ => break,
|
||||
};
|
||||
let payload = &buf[pos + 5..pos + end];
|
||||
total += payload.len();
|
||||
pos += end;
|
||||
black_box(st);
|
||||
black_box(payload.as_ptr());
|
||||
}
|
||||
black_box(total)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hand-rolled chunk write: mirrors ChunkWriter::write_chunk's header
|
||||
// writes into a caller-provided buffer. Writes `n` contiguous chunks.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn hand_write_stream(out: &mut Vec<u8>, n: usize, payload_len: usize) {
|
||||
let payload = vec![0xA5u8; payload_len];
|
||||
for i in 0..n {
|
||||
let st = (i % 5) as u8;
|
||||
let start = out.len();
|
||||
out.resize(start + 5 + payload_len, 0);
|
||||
out[start] = st;
|
||||
out[start + 1..start + 5].copy_from_slice(&(payload_len as u32).to_be_bytes());
|
||||
out[start + 5..start + 5 + payload_len].copy_from_slice(&payload);
|
||||
}
|
||||
black_box(out.len());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// alktype chunk write: data_access::write_u8 / write_u32 at the
|
||||
// PackedLayout offsets. The layout is built once per group and reused.
|
||||
// Payload bytes are copied with the same slice copy as the hand-rolled
|
||||
// path so the comparison isolates the header-encoding overhead.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn alktype_write_stream(out: &mut Vec<u8>, n: usize, payload_len: usize, layout: &PackedLayout) {
|
||||
let payload = vec![0xA5u8; payload_len];
|
||||
let st_pos = layout.get("stream_type").expect("stream_type field").offset;
|
||||
let len_pos = layout.get("length").expect("length field").offset;
|
||||
for i in 0..n {
|
||||
let start = out.len();
|
||||
out.resize(start + 5 + payload_len, 0);
|
||||
let _ = data_access::write_u8(out, start + st_pos, (i % 5) as u8, "stream_type");
|
||||
let _ = data_access::write_u32(
|
||||
out,
|
||||
start + len_pos,
|
||||
payload_len as u32,
|
||||
"length",
|
||||
Endian::Big,
|
||||
);
|
||||
out[start + 5..start + 5 + payload_len].copy_from_slice(&payload);
|
||||
}
|
||||
black_box(out.len());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Packet fixtures: byte-disc union stream, alternating Read/Write
|
||||
// variants. Wire layout per packet (packed, big-endian):
|
||||
// Read: disc(1) + handle(4) + length(4) = 9 bytes
|
||||
// Write: disc(1) + handle(4) + length(4) + len(4)+data = 13 + payload
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn make_packet_bytes(disc: u8, payload: &[u8]) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(13 + 4 + payload.len());
|
||||
buf.push(disc);
|
||||
buf.extend_from_slice(&0x0102_0304u32.to_be_bytes());
|
||||
buf.extend_from_slice(&(payload.len() as u32).to_be_bytes());
|
||||
if disc == 6 {
|
||||
buf.extend_from_slice(&(payload.len() as u32).to_be_bytes());
|
||||
buf.extend_from_slice(payload);
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
fn make_packet_stream(n: usize, payload_len: usize) -> Vec<u8> {
|
||||
let payload = vec![0xA5u8; payload_len];
|
||||
let mut buf = Vec::new();
|
||||
for i in 0..n {
|
||||
let disc = if i % 2 == 0 { 5u8 } else { 6u8 };
|
||||
buf.extend_from_slice(&make_packet_bytes(disc, &payload));
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hand-rolled packet read: read the discriminator byte, match the
|
||||
// variant, parse its fields directly. Returns bytes consumed.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn hand_read_packet(buf: &[u8]) -> Option<usize> {
|
||||
let disc = *buf.first()?;
|
||||
match disc {
|
||||
5 => {
|
||||
if buf.len() < 9 {
|
||||
return None;
|
||||
}
|
||||
let handle = u32::from_be_bytes(buf[1..5].try_into().ok()?);
|
||||
let length = u32::from_be_bytes(buf[5..9].try_into().ok()?);
|
||||
black_box((handle, length));
|
||||
Some(9)
|
||||
}
|
||||
6 => {
|
||||
if buf.len() < 13 {
|
||||
return None;
|
||||
}
|
||||
let handle = u32::from_be_bytes(buf[1..5].try_into().ok()?);
|
||||
let length = u32::from_be_bytes(buf[5..9].try_into().ok()?);
|
||||
let data_len = u32::from_be_bytes(buf[9..13].try_into().ok()?);
|
||||
let end = 13usize.checked_add(data_len as usize)?;
|
||||
if buf.len() < end {
|
||||
return None;
|
||||
}
|
||||
black_box((handle, length));
|
||||
black_box(&buf[13..end].as_ptr());
|
||||
Some(end)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn hand_read_packet_stream(buf: &[u8], n: usize) -> usize {
|
||||
let mut pos = 0usize;
|
||||
let mut total = 0usize;
|
||||
for _ in 0..n {
|
||||
let Some(consumed) = hand_read_packet(&buf[pos..]) else {
|
||||
break;
|
||||
};
|
||||
total += consumed;
|
||||
pos += consumed;
|
||||
}
|
||||
black_box(total)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// alktype packet read: the documented union consumer contract. The root
|
||||
// reader walks the wrapping struct; `read_next` returns
|
||||
// `FieldValue::Union { discriminator, variant_start }`; the consumer
|
||||
// selects the pre-built reader for that variant and walks it over
|
||||
// `&buf[pos + variant_start..]` until exhausted.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Walk one variant's fields to exhaustion; returns bytes consumed.
|
||||
fn alktype_walk_variant(reader: &mut SequentialReader, buf: &[u8]) -> Option<usize> {
|
||||
reader.reset();
|
||||
loop {
|
||||
match reader.read_next(buf) {
|
||||
Ok(Some((name, value))) => {
|
||||
black_box(name);
|
||||
black_box(&value);
|
||||
}
|
||||
Ok(None) => return Some(reader.position()),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn alktype_read_packet_stream(
|
||||
buf: &[u8],
|
||||
n: usize,
|
||||
packet: &mut SequentialReader,
|
||||
read: &mut SequentialReader,
|
||||
write: &mut SequentialReader,
|
||||
) -> usize {
|
||||
let mut pos = 0usize;
|
||||
let mut total = 0usize;
|
||||
for _ in 0..n {
|
||||
packet.reset();
|
||||
let disc = match packet.read_next(&buf[pos..]) {
|
||||
Ok(Some((_, FieldValue::Union {
|
||||
discriminator,
|
||||
variant_start,
|
||||
}))) => {
|
||||
pos += variant_start;
|
||||
discriminator
|
||||
}
|
||||
_ => break,
|
||||
};
|
||||
let vbuf = &buf[pos..];
|
||||
let consumed = match disc.as_str() {
|
||||
"5" => alktype_walk_variant(read, vbuf),
|
||||
"6" => alktype_walk_variant(write, vbuf),
|
||||
_ => break,
|
||||
};
|
||||
let Some(consumed) = consumed else {
|
||||
break;
|
||||
};
|
||||
total += consumed;
|
||||
pos += consumed;
|
||||
}
|
||||
black_box(total)
|
||||
}
|
||||
|
||||
/// One-time sanity check (outside the measured loops): the union
|
||||
/// consumer pattern the stream loop relies on — root reader reports the
|
||||
/// mapping key and the variant start; the variant reader's walk to
|
||||
/// exhaustion reports exactly the variant's byte size, so
|
||||
/// `variant_start + consumed` lands on the next packet.
|
||||
fn assert_packet_reader_parity(
|
||||
payload_len: usize,
|
||||
packet: &mut SequentialReader,
|
||||
read: &mut SequentialReader,
|
||||
write: &mut SequentialReader,
|
||||
) {
|
||||
let payload = vec![0u8; payload_len];
|
||||
|
||||
let read_pkt = make_packet_bytes(5, &payload);
|
||||
packet.reset();
|
||||
match packet.read_next(&read_pkt) {
|
||||
Ok(Some((_, FieldValue::Union {
|
||||
discriminator,
|
||||
variant_start,
|
||||
}))) => {
|
||||
assert_eq!(discriminator, "5");
|
||||
assert_eq!(variant_start, 1, "variant starts after the 1-byte disc");
|
||||
}
|
||||
_ => panic!("expected union value for Read packet"),
|
||||
}
|
||||
let consumed = alktype_walk_variant(read, &read_pkt[1..]).expect("read variant walk");
|
||||
assert_eq!(consumed, 8, "Read = handle(4) + length(4)");
|
||||
assert_eq!(1 + consumed, read_pkt.len(), "Read packet fully consumed");
|
||||
|
||||
let write_pkt = make_packet_bytes(6, &payload);
|
||||
packet.reset();
|
||||
match packet.read_next(&write_pkt) {
|
||||
Ok(Some((_, FieldValue::Union {
|
||||
discriminator,
|
||||
variant_start,
|
||||
}))) => {
|
||||
assert_eq!(discriminator, "6");
|
||||
assert_eq!(variant_start, 1);
|
||||
}
|
||||
_ => panic!("expected union value for Write packet"),
|
||||
}
|
||||
let consumed = alktype_walk_variant(write, &write_pkt[1..]).expect("write variant walk");
|
||||
assert_eq!(
|
||||
consumed,
|
||||
12 + payload_len,
|
||||
"Write = handle(4) + length(4) + len-prefix(4) + data"
|
||||
);
|
||||
assert_eq!(1 + consumed, write_pkt.len(), "Write packet fully consumed");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Benchmarks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn bench_read(c: &mut Criterion) {
|
||||
let bast: serde_json::Value = serde_json::from_str(CHUNK_HEADER_BAST).expect("bast json");
|
||||
let engine =
|
||||
AlkTypeEngine::compile(&bast, "ChunkHeader", LayoutMode::Packed, None).expect("compile");
|
||||
let mut reader = engine.sequential_reader().expect("packed reader");
|
||||
|
||||
let mut group = c.benchmark_group("read_chunk_stream");
|
||||
for (payload_len, label) in [(64usize, "p64"), (4096usize, "p4k")] {
|
||||
let n = 1024;
|
||||
let buf = make_chunk_stream(n, payload_len);
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("hand_rolled", label), &n, |b, &n| {
|
||||
b.iter(|| hand_read_stream(black_box(&buf), n));
|
||||
});
|
||||
group.bench_with_input(BenchmarkId::new("alktype", label), &n, |b, &n| {
|
||||
b.iter(|| alktype_read_stream(black_box(&buf), n, &mut reader));
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_write(c: &mut Criterion) {
|
||||
let bast: serde_json::Value = serde_json::from_str(CHUNK_HEADER_BAST).expect("bast json");
|
||||
let builder = LayoutBuilder::new(&bast, "ChunkHeader").expect("builder");
|
||||
let layout = builder
|
||||
.build(&std::collections::HashMap::new())
|
||||
.expect("layout");
|
||||
|
||||
let mut group = c.benchmark_group("write_chunk_stream");
|
||||
for (payload_len, label) in [(64usize, "p64"), (4096usize, "p4k")] {
|
||||
let n = 1024;
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("hand_rolled", label),
|
||||
&(n, payload_len),
|
||||
|b, &(n, pl)| {
|
||||
b.iter(|| {
|
||||
let mut out = Vec::with_capacity(n * (5 + pl));
|
||||
hand_write_stream(&mut out, n, pl);
|
||||
});
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("alktype", label),
|
||||
&(n, payload_len),
|
||||
|b, &(n, pl)| {
|
||||
b.iter(|| {
|
||||
let mut out = Vec::with_capacity(n * (5 + pl));
|
||||
alktype_write_stream(&mut out, n, pl, &layout);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_packet_read(c: &mut Criterion) {
|
||||
let bast: serde_json::Value = serde_json::from_str(PACKET_BAST).expect("bast json");
|
||||
let engine =
|
||||
AlkTypeEngine::compile(&bast, "Packet", LayoutMode::Packed, None).expect("compile");
|
||||
let mut packet_reader = engine.sequential_reader().expect("packed reader");
|
||||
let read_plan = std::sync::Arc::new(ReadPlan::compile(&bast, "Read").expect("read plan"));
|
||||
let write_plan = std::sync::Arc::new(ReadPlan::compile(&bast, "Write").expect("write plan"));
|
||||
let mut read_reader = SequentialReader::new(read_plan);
|
||||
let mut write_reader = SequentialReader::new(write_plan);
|
||||
|
||||
// One-time parity check of the union consumer pattern (not measured).
|
||||
assert_packet_reader_parity(64, &mut packet_reader, &mut read_reader, &mut write_reader);
|
||||
|
||||
let mut group = c.benchmark_group("read_packet_stream");
|
||||
for (payload_len, label) in [(64usize, "p64"), (4096usize, "p4k")] {
|
||||
let n = 1024;
|
||||
let buf = make_packet_stream(n, payload_len);
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("hand_rolled", label), &n, |b, &n| {
|
||||
b.iter(|| hand_read_packet_stream(black_box(&buf), n));
|
||||
});
|
||||
group.bench_with_input(BenchmarkId::new("alktype", label), &n, |b, &n| {
|
||||
b.iter(|| {
|
||||
alktype_read_packet_stream(
|
||||
black_box(&buf),
|
||||
n,
|
||||
&mut packet_reader,
|
||||
&mut read_reader,
|
||||
&mut write_reader,
|
||||
)
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_validate(c: &mut Criterion) {
|
||||
let header_bast: serde_json::Value =
|
||||
serde_json::from_str(CHUNK_HEADER_BAST).expect("bast json");
|
||||
let header_engine = AlkTypeEngine::compile(&header_bast, "ChunkHeader", LayoutMode::Packed, None)
|
||||
.expect("compile");
|
||||
let packet_bast: serde_json::Value = serde_json::from_str(PACKET_BAST).expect("bast json");
|
||||
let packet_engine =
|
||||
AlkTypeEngine::compile(&packet_bast, "Packet", LayoutMode::Packed, None).expect("compile");
|
||||
|
||||
let mut group = c.benchmark_group("validate_stream");
|
||||
let n = 1024;
|
||||
|
||||
let headers: Vec<Vec<u8>> = (0..n)
|
||||
.map(|i| make_chunk_bytes((i % 5) as u8, &[0xA5u8; 64]))
|
||||
.collect();
|
||||
group.bench_function("alktype_chunk_header", |b| {
|
||||
b.iter(|| {
|
||||
for h in &headers {
|
||||
header_engine.validate_bytes(black_box(h)).expect("validate");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
for (payload_len, label) in [(64usize, "p64"), (4096usize, "p4k")] {
|
||||
let payload = vec![0xA5u8; payload_len];
|
||||
let packets: Vec<Vec<u8>> = (0..n)
|
||||
.map(|i| make_packet_bytes(if i % 2 == 0 { 5 } else { 6 }, &payload))
|
||||
.collect();
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("alktype_packet", label),
|
||||
&packets,
|
||||
|b, packets| {
|
||||
b.iter(|| {
|
||||
for p in packets {
|
||||
packet_engine.validate_bytes(black_box(p)).expect("validate");
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// One-shot costs paid once at startup, not per chunk.
|
||||
fn bench_oneshot(c: &mut Criterion) {
|
||||
let bast: serde_json::Value = serde_json::from_str(CHUNK_HEADER_BAST).expect("bast json");
|
||||
c.bench_function("alktype_engine_compile", |b| {
|
||||
b.iter(|| {
|
||||
let _ =
|
||||
AlkTypeEngine::compile(black_box(&bast), "ChunkHeader", LayoutMode::Packed, None)
|
||||
.expect("compile");
|
||||
});
|
||||
});
|
||||
c.bench_function("alktype_sequential_reader_new", |b| {
|
||||
let engine = AlkTypeEngine::compile(&bast, "ChunkHeader", LayoutMode::Packed, None)
|
||||
.expect("compile");
|
||||
b.iter(|| engine.sequential_reader());
|
||||
});
|
||||
c.bench_function("alktype_layout_build", |b| {
|
||||
let builder = LayoutBuilder::new(&bast, "ChunkHeader").expect("builder");
|
||||
b.iter(|| builder.build(&std::collections::HashMap::new()));
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_read,
|
||||
bench_write,
|
||||
bench_packet_read,
|
||||
bench_validate,
|
||||
bench_oneshot
|
||||
);
|
||||
criterion_main!(benches);
|
||||
Reference in new issue
Block a user