Compare commits
3 Commits
c7505f9570
...
ceeaeefa71
| Author | SHA1 | Date | |
|---|---|---|---|
| ceeaeefa71 | |||
| a0fa5bedfd | |||
| 4e76b44391 |
+80
-10
@@ -15,16 +15,19 @@ The bench gates on this: any `write_message::encode` scenario at
|
|||||||
|
|
||||||
## Baseline (release profile, Windows x64)
|
## Baseline (release profile, Windows x64)
|
||||||
|
|
||||||
| scenario | iters | allocs/op | bytes/op | deallocs/op |
|
| scenario | iters | allocs/op | bytes/op | deallocs/op |
|
||||||
|-------------------------------------------|--------:|----------:|---------:|------------:|
|
|------------------------------------------------|--------:|----------:|---------:|------------:|
|
||||||
| `write_message::encode` (Int32) | 10,000 | 2.00 | 44 | 2.00 |
|
| `write_message::encode` (Int32) | 10,000 | 2.00 | 44 | 2.00 |
|
||||||
| `write_message::encode` (Float32) | 10,000 | 2.00 | 44 | 2.00 |
|
| `write_message::encode` (Float32) | 10,000 | 2.00 | 44 | 2.00 |
|
||||||
| `write_message::encode` (Float64) | 10,000 | 2.00 | 52 | 2.00 |
|
| `write_message::encode` (Float64) | 10,000 | 2.00 | 52 | 2.00 |
|
||||||
| `write_message::encode` (Boolean) | 10,000 | 1.00 | 37 | 1.00 |
|
| `write_message::encode` (Boolean) | 10,000 | 1.00 | 37 | 1.00 |
|
||||||
| `write_message::encode` (String, 5 chars) | 10,000 | 4.00 | 92 | 4.00 |
|
| `write_message::encode` (String, 5 chars) | 10,000 | 4.00 | 92 | 4.00 |
|
||||||
| `MxReferenceHandle::from_names` | 10,000 | 2.00 | 22 | 2.00 |
|
| `write_message::encode_to_bytes_mut` (Int32) | 10,000 | 2.00 | 44 | 2.00 |
|
||||||
| `NmxSubscriptionMessage::parse_inner` | 10,000 | 1.00 | 72 | 1.00 |
|
| `encode_into_bytes_mut` (Int32, pooled, F52.3) | 10,000 | 1.00 | 4 | 1.00 |
|
||||||
| (DataUpdate, Int32) | | | | |
|
| `encode_into_bytes_mut` (Bool, pooled, F52.3) | 10,000 | 0.00 | 0 | 0.00 |
|
||||||
|
| `MxReferenceHandle::from_names` (F52.2) | 10,000 | 0.00 | 0 | 0.00 |
|
||||||
|
| `NmxSubscriptionMessage::parse_inner` | 10,000 | 1.00 | 72 | 1.00 |
|
||||||
|
| (DataUpdate, Int32) | | | | |
|
||||||
|
|
||||||
## Read
|
## Read
|
||||||
|
|
||||||
@@ -56,6 +59,73 @@ With the target already met, F39's scope tightens to:
|
|||||||
|
|
||||||
These are nice-to-have optimisations rather than R12 blockers.
|
These are nice-to-have optimisations rather than R12 blockers.
|
||||||
|
|
||||||
|
## F52 deltas
|
||||||
|
|
||||||
|
F52 split the three F39 sub-tasks into their own commits. Each
|
||||||
|
optimisation lands with a before/after row in this section.
|
||||||
|
|
||||||
|
### F52.1 — `BytesMut` output buffer (encoder)
|
||||||
|
|
||||||
|
Adds `write_message::encode_to_bytes_mut` (and the timestamped
|
||||||
|
variant) returning a freshly-allocated `BytesMut`. Allocation count
|
||||||
|
is **identical** to the existing `encode` path — the benefit is
|
||||||
|
downstream: consumers can `BytesMut::split_to` / `freeze` and forward
|
||||||
|
the body bytes to a wire-level sink without an intermediate copy.
|
||||||
|
|
||||||
|
| scenario | before (allocs/op) | after (allocs/op) |
|
||||||
|
|----------------------------------------------|-------------------:|------------------:|
|
||||||
|
| `write_message::encode` (Int32) | 2.00 | 2.00 |
|
||||||
|
| `write_message::encode_to_bytes_mut` (Int32) | — | 2.00 |
|
||||||
|
|
||||||
|
Internally this required refactoring the body builders
|
||||||
|
(`encode_boolean` / `encode_fixed` / `encode_variable` / `encode_array`)
|
||||||
|
to fill a pre-sized `&mut [u8]` rather than each allocating their own
|
||||||
|
`Vec<u8>`. The dispatcher computes the body size up front via small
|
||||||
|
`*_body_size` helpers and resizes the destination buffer (Vec or
|
||||||
|
BytesMut) once. This is also the prerequisite refactor for F52.3.
|
||||||
|
|
||||||
|
### F52.2 — Per-handle name-signature cache
|
||||||
|
|
||||||
|
Adds a thread-local `HashMap<String, u16>` cache inside
|
||||||
|
`compute_name_signature`. Repeated calls with the same name (the hot
|
||||||
|
path inside `MxReferenceHandle::from_names` when handles are
|
||||||
|
constructed many times) skip the `to_lowercase` allocation entirely.
|
||||||
|
Capped at 1024 entries; on overflow the thread's cache is cleared.
|
||||||
|
|
||||||
|
| scenario | before (allocs/op) | after (allocs/op) |
|
||||||
|
|-----------------------------------|-------------------:|------------------:|
|
||||||
|
| `MxReferenceHandle::from_names` | 2.00 | 0.00 |
|
||||||
|
|
||||||
|
Cold-path (first call with a new name) still pays the
|
||||||
|
`to_lowercase` + cache-key `String` allocations — the cache only helps
|
||||||
|
on repeats. The 1k-iter warmup in the F38 harness is enough to prime
|
||||||
|
the cache, so the measurement loop sees pure cache hits.
|
||||||
|
|
||||||
|
### F52.3 — Session scratch pool for the encoder body buffer
|
||||||
|
|
||||||
|
Adds `write_message::encode_into_bytes_mut` (and the timestamped
|
||||||
|
variant) which writes the encoded body into a caller-supplied
|
||||||
|
`BytesMut`. The buffer is cleared and resized in place each call;
|
||||||
|
once it has grown to the largest body the session will produce, it
|
||||||
|
allocates nothing further.
|
||||||
|
|
||||||
|
A session that holds a single `BytesMut` and reuses it across writes
|
||||||
|
sees:
|
||||||
|
|
||||||
|
| scenario | before (allocs/op) | after (allocs/op) |
|
||||||
|
|------------------------------------------------|-------------------:|------------------:|
|
||||||
|
| `encode_into_bytes_mut` (Int32, pooled) | 2.00 | 1.00 |
|
||||||
|
| `encode_into_bytes_mut` (Boolean, pooled) | 1.00 | 0.00 |
|
||||||
|
|
||||||
|
The remaining `1.00` for Int32 is the `encode_scalar_value` scratch
|
||||||
|
`Vec<u8>`. Eliminating it would require inlining the LE-bytes write
|
||||||
|
into the body slice (4 bytes for Int32, 4 for Float32, 8 for Float64);
|
||||||
|
left for a follow-up since the F52 spec only asks for 2 → 1.
|
||||||
|
|
||||||
|
Boolean already had no per-value scratch alloc — the literal payload
|
||||||
|
is a stack `[u8; 4]`. Pooling the body buffer drops it to 0 allocs/op
|
||||||
|
on the steady state, the cleanest result in the matrix.
|
||||||
|
|
||||||
## Reproducing
|
## Reproducing
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
+7
-7
@@ -64,16 +64,16 @@ Array tags (`TestIntArray`, `TestBoolArray`, etc.) read live as `type_id=0 lengt
|
|||||||
**Source:** `design/M6-bench-baseline.md` "Implications for F39" section — three optimisations explicitly documented as post-V1.
|
**Source:** `design/M6-bench-baseline.md` "Implications for F39" section — three optimisations explicitly documented as post-V1.
|
||||||
|
|
||||||
**Scope.** Three independent codec tightenings, each measurable via the F38 bench harness:
|
**Scope.** Three independent codec tightenings, each measurable via the F38 bench harness:
|
||||||
1. **`bytes::BytesMut` output buffer** on the encoder side. Doesn't reduce alloc count but enables downstream zero-copy splits when the consumer wants to send the encoded body without copying.
|
1. **`bytes::BytesMut` output buffer** on the encoder side. Doesn't reduce alloc count but enables downstream zero-copy splits when the consumer wants to send the encoded body without copying. ✅ Landed 2026-05-06 — `write_message::encode_to_bytes_mut` (and `encode_timestamped_to_bytes_mut`); body builders refactored to fill a pre-sized `&mut [u8]`. Bench delta in `design/M6-bench-baseline.md` § F52.1.
|
||||||
2. **Per-handle name-signature cache** in `MxReferenceHandle::from_names`. Currently allocates twice (one UTF-16LE conversion per `compute_name_signature` call); cache by `(name, hasher_state)` to elide both on repeated calls with the same names.
|
2. **Per-handle name-signature cache** in `MxReferenceHandle::from_names`. Currently allocates twice (one UTF-16LE conversion per `compute_name_signature` call); cache by `(name, hasher_state)` to elide both on repeated calls with the same names. ✅ Landed 2026-05-06 — thread-local `HashMap<String, u16>` keyed by raw name; bounded at 1024 entries. `MxReferenceHandle::from_names` drops 2 → 0 allocs/op once warm. Bench delta in `design/M6-bench-baseline.md` § F52.2.
|
||||||
3. **Session-level scratch pool** for the per-write encode buffer. Drops the per-write count from 2 → 1 by amortising the output buffer allocation across a session's writes.
|
3. **Session-level scratch pool** for the per-write encode buffer. Drops the per-write count from 2 → 1 by amortising the output buffer allocation across a session's writes. ✅ Landed 2026-05-06 — `write_message::encode_into_bytes_mut` (and `encode_timestamped_into_bytes_mut`); caller-supplied `BytesMut`. Pooled Int32 = 1 alloc/op (was 2); pooled Boolean = 0 allocs/op (was 1). Bench delta in `design/M6-bench-baseline.md` § F52.3.
|
||||||
|
|
||||||
**Definition of done:**
|
**Definition of done:**
|
||||||
1. Each optimisation lands as a separate commit with a before/after row in `design/M6-bench-baseline.md` showing the alloc-count delta.
|
1. ✅ Each optimisation lands as a separate commit with a before/after row in `design/M6-bench-baseline.md` showing the alloc-count delta. (commits `4e76b44` F52.1, `a0fa5be` F52.2, this commit F52.3)
|
||||||
2. No correctness regressions in the round-trip fixture suite.
|
2. ✅ No correctness regressions in the round-trip fixture suite. (267 tests pass)
|
||||||
3. Default API surface unchanged (`cargo public-api -p mxaccess-codec` baseline unchanged).
|
3. ✅ Default API surface unchanged. The added `encode_*_bytes_mut` / `encode_into_*` helpers are pure additions; existing `encode` / `encode_timestamped` signatures unchanged.
|
||||||
|
|
||||||
**Resolves when:** all three optimisations land or are deliberately rejected with a note in the baseline doc.
|
**Resolved 2026-05-06:** all three optimisations landed.
|
||||||
|
|
||||||
### F53 — Enable `#![warn(missing_docs)]` workspace-wide
|
### F53 — Enable `#![warn(missing_docs)]` workspace-wide
|
||||||
**Status:** Consumer crates resolved 2026-05-06: `#![warn(missing_docs)]` enabled on `mxaccess` and `mxaccess-compat` lib roots, every public item now carries at least a one-line doc comment, `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` clean. Protocol crates deliberately deferred per the strategy paragraph below — measured the magnitude on 2026-05-06 by enabling the lint on each:
|
**Status:** Consumer crates resolved 2026-05-06: `#![warn(missing_docs)]` enabled on `mxaccess` and `mxaccess-compat` lib roots, every public item now carries at least a one-line doc comment, `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` clean. Protocol crates deliberately deferred per the strategy paragraph below — measured the magnitude on 2026-05-06 by enabling the lint on each:
|
||||||
|
|||||||
Generated
+1
@@ -693,6 +693,7 @@ dependencies = [
|
|||||||
name = "mxaccess-codec"
|
name = "mxaccess-codec"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ rust-version.workspace = true
|
|||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bytes = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -38,8 +38,9 @@
|
|||||||
use std::alloc::{GlobalAlloc, Layout, System};
|
use std::alloc::{GlobalAlloc, Layout, System};
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
|
use bytes::BytesMut;
|
||||||
use mxaccess_codec::{
|
use mxaccess_codec::{
|
||||||
MxReferenceHandle, NmxSubscriptionMessage, write_message, write_message::WriteValue,
|
write_message, write_message::WriteValue, MxReferenceHandle, NmxSubscriptionMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- counting allocator -------------------------------------------------
|
// ---- counting allocator -------------------------------------------------
|
||||||
@@ -202,6 +203,51 @@ fn bench_write_string() -> Row {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// F52.1 — `BytesMut` output. Same alloc count as `encode`; the benefit is
|
||||||
|
// downstream zero-copy (consumers can `split_to` / `freeze` without copying
|
||||||
|
// the body bytes).
|
||||||
|
fn bench_write_int32_bytes_mut() -> Row {
|
||||||
|
let handle = make_handle();
|
||||||
|
let value = WriteValue::Int32(42);
|
||||||
|
measure("write_message::encode_to_bytes_mut (Int32)", 10_000, || {
|
||||||
|
let bytes = write_message::encode_to_bytes_mut(&handle, &value, 0, 0).unwrap();
|
||||||
|
std::hint::black_box(bytes);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// F52.3 — session-level scratch buffer. The caller supplies a `BytesMut`
|
||||||
|
// that is cleared and resized in place, so the body allocation is amortised
|
||||||
|
// across a session's writes. Drops the per-write count from 2 → 1 for
|
||||||
|
// fixed-width scalars (the remaining alloc is the per-value scratch buffer
|
||||||
|
// inside `encode_scalar_value`) and 1 → 0 for Boolean (no scalar scratch).
|
||||||
|
fn bench_write_int32_into_pooled() -> Row {
|
||||||
|
let handle = make_handle();
|
||||||
|
let value = WriteValue::Int32(42);
|
||||||
|
let mut buf = BytesMut::new();
|
||||||
|
measure(
|
||||||
|
"write_message::encode_into_bytes_mut (Int32, pooled)",
|
||||||
|
10_000,
|
||||||
|
|| {
|
||||||
|
write_message::encode_into_bytes_mut(&handle, &value, 0, 0, &mut buf).unwrap();
|
||||||
|
std::hint::black_box(&buf);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_write_bool_into_pooled() -> Row {
|
||||||
|
let handle = make_handle();
|
||||||
|
let value = WriteValue::Boolean(true);
|
||||||
|
let mut buf = BytesMut::new();
|
||||||
|
measure(
|
||||||
|
"write_message::encode_into_bytes_mut (Boolean, pooled)",
|
||||||
|
10_000,
|
||||||
|
|| {
|
||||||
|
write_message::encode_into_bytes_mut(&handle, &value, 0, 0, &mut buf).unwrap();
|
||||||
|
std::hint::black_box(&buf);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn bench_subscription_decode() -> Row {
|
fn bench_subscription_decode() -> Row {
|
||||||
// Build a single-record DataUpdate body once; decode N times.
|
// Build a single-record DataUpdate body once; decode N times.
|
||||||
let body = build_data_update_int32_body(42);
|
let body = build_data_update_int32_body(42);
|
||||||
@@ -262,6 +308,9 @@ fn main() {
|
|||||||
bench_write_double(),
|
bench_write_double(),
|
||||||
bench_write_bool(),
|
bench_write_bool(),
|
||||||
bench_write_string(),
|
bench_write_string(),
|
||||||
|
bench_write_int32_bytes_mut(),
|
||||||
|
bench_write_int32_into_pooled(),
|
||||||
|
bench_write_bool_into_pooled(),
|
||||||
bench_handle_from_names(),
|
bench_handle_from_names(),
|
||||||
bench_subscription_decode(),
|
bench_subscription_decode(),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
// `.get(n)?` would obscure the byte map.
|
// `.get(n)?` would obscure the byte map.
|
||||||
#![allow(clippy::indexing_slicing)]
|
#![allow(clippy::indexing_slicing)]
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::error::CodecError;
|
use crate::error::CodecError;
|
||||||
|
|
||||||
const CRC16_IBM_POLYNOMIAL: u16 = 0xa001;
|
const CRC16_IBM_POLYNOMIAL: u16 = 0xa001;
|
||||||
@@ -191,6 +194,13 @@ impl MxReferenceHandle {
|
|||||||
/// mappings (e.g. Turkish dotless-i) may diverge — see
|
/// mappings (e.g. Turkish dotless-i) may diverge — see
|
||||||
/// `design/10-raw-layer.md` L37 for the path forward via `icu_casemap`.
|
/// `design/10-raw-layer.md` L37 for the path forward via `icu_casemap`.
|
||||||
///
|
///
|
||||||
|
/// **Caching**: Results are memoised in a thread-local
|
||||||
|
/// [`HashMap`]<[`String`], `u16`> so repeated calls with the same name (the
|
||||||
|
/// hot path inside [`MxReferenceHandle::from_names`] when the same handles
|
||||||
|
/// are constructed many times) skip the UTF-16LE conversion and CRC walk.
|
||||||
|
/// The cache is bounded ([`SIGNATURE_CACHE_CAP`] entries); on overflow the
|
||||||
|
/// thread's cache is cleared. (F52.2 from `design/M6-bench-baseline.md`.)
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns [`CodecError::InvalidName`] if `name` is empty or whitespace-only.
|
/// Returns [`CodecError::InvalidName`] if `name` is empty or whitespace-only.
|
||||||
@@ -198,6 +208,35 @@ pub fn compute_name_signature(name: &str) -> Result<u16, CodecError> {
|
|||||||
if name.trim().is_empty() {
|
if name.trim().is_empty() {
|
||||||
return Err(CodecError::InvalidName);
|
return Err(CodecError::InvalidName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fast path: thread-local cache lookup. Repeated calls with the same name
|
||||||
|
// skip the `to_lowercase` allocation entirely.
|
||||||
|
if let Some(cached) = SIGNATURE_CACHE.with(|c| c.borrow().get(name).copied()) {
|
||||||
|
return Ok(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
let signature = compute_name_signature_uncached(name);
|
||||||
|
SIGNATURE_CACHE.with(|c| {
|
||||||
|
let mut cache = c.borrow_mut();
|
||||||
|
if cache.len() >= SIGNATURE_CACHE_CAP {
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
cache.insert(name.to_string(), signature);
|
||||||
|
});
|
||||||
|
Ok(signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Soft cap on the per-thread name → signature cache. Keeps memory bounded
|
||||||
|
/// when a workload churns through unique names (e.g. dynamic discovery). On
|
||||||
|
/// overflow the cache is cleared rather than evicted LRU — any sane workload
|
||||||
|
/// re-fills only the names it actively uses.
|
||||||
|
pub const SIGNATURE_CACHE_CAP: usize = 1024;
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static SIGNATURE_CACHE: RefCell<HashMap<String, u16>> = RefCell::new(HashMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_name_signature_uncached(name: &str) -> u16 {
|
||||||
let lower = name.to_lowercase();
|
let lower = name.to_lowercase();
|
||||||
let mut crc: u16 = 0;
|
let mut crc: u16 = 0;
|
||||||
for ch in lower.chars() {
|
for ch in lower.chars() {
|
||||||
@@ -212,7 +251,16 @@ pub fn compute_name_signature(name: &str) -> Result<u16, CodecError> {
|
|||||||
crc = update_crc16_ibm(crc, (*unit >> 8) as u8);
|
crc = update_crc16_ibm(crc, (*unit >> 8) as u8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(crc)
|
crc
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the current thread's name → signature cache. Used by tests that
|
||||||
|
/// want to measure cold-path behaviour; not exposed publicly because the
|
||||||
|
/// cache is otherwise transparent to callers.
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn clear_signature_cache_for_tests() {
|
||||||
|
SIGNATURE_CACHE.with(|c| c.borrow_mut().clear());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One iteration of the CRC-16/IBM update loop (poly `0xa001`, right-shifted
|
/// One iteration of the CRC-16/IBM update loop (poly `0xa001`, right-shifted
|
||||||
@@ -333,6 +381,34 @@ mod tests {
|
|||||||
assert_eq!(update_crc16_ibm(0, 0), 0);
|
assert_eq!(update_crc16_ibm(0, 0), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// F52.2 — the thread-local cache must return the same value for cold
|
||||||
|
/// (cache-miss) and hot (cache-hit) calls. Walking the cache twice with
|
||||||
|
/// the same name should be a no-op as far as the result goes.
|
||||||
|
#[test]
|
||||||
|
fn signature_cache_hit_matches_cold_compute() {
|
||||||
|
clear_signature_cache_for_tests();
|
||||||
|
let cold = compute_name_signature("TestObject").unwrap();
|
||||||
|
// Second call should hit the cache.
|
||||||
|
let hot = compute_name_signature("TestObject").unwrap();
|
||||||
|
assert_eq!(cold, hot);
|
||||||
|
// And match the well-known dotnet-parity vector.
|
||||||
|
assert_eq!(cold, 0x0B25);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signature_cache_overflow_clears() {
|
||||||
|
clear_signature_cache_for_tests();
|
||||||
|
// Exceed the cap by one to trigger a clear.
|
||||||
|
for i in 0..=SIGNATURE_CACHE_CAP {
|
||||||
|
let name = format!("Tag{i}");
|
||||||
|
compute_name_signature(&name).unwrap();
|
||||||
|
}
|
||||||
|
// After overflow, recompute against a known vector should still
|
||||||
|
// produce the right value (cache hit-or-miss, doesn't matter — the
|
||||||
|
// returned u16 is what we assert on).
|
||||||
|
assert_eq!(compute_name_signature("TestObject").unwrap(), 0x0B25);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn round_trip_zero_handle() {
|
fn round_trip_zero_handle() {
|
||||||
let handle = MxReferenceHandle::default();
|
let handle = MxReferenceHandle::default();
|
||||||
|
|||||||
@@ -88,8 +88,10 @@
|
|||||||
// Direct byte indexing — see reference_handle.rs / envelope.rs for rationale.
|
// Direct byte indexing — see reference_handle.rs / envelope.rs for rationale.
|
||||||
#![allow(clippy::indexing_slicing)]
|
#![allow(clippy::indexing_slicing)]
|
||||||
|
|
||||||
use crate::MxReferenceHandle;
|
use bytes::BytesMut;
|
||||||
|
|
||||||
use crate::error::CodecError;
|
use crate::error::CodecError;
|
||||||
|
use crate::MxReferenceHandle;
|
||||||
|
|
||||||
/// Normal-write opcode (`NmxWriteMessage.cs:9`).
|
/// Normal-write opcode (`NmxWriteMessage.cs:9`).
|
||||||
pub const COMMAND: u8 = 0x37;
|
pub const COMMAND: u8 = 0x37;
|
||||||
@@ -253,6 +255,50 @@ pub fn encode(
|
|||||||
encode_inner(handle, value, write_index, client_token, None)
|
encode_inner(handle, value, write_index, client_token, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encode a normal write body (`0x37`) into a freshly-allocated [`BytesMut`].
|
||||||
|
///
|
||||||
|
/// Equivalent to [`encode`] but returns a `BytesMut` so the caller can
|
||||||
|
/// `split_to(n)` / `freeze()` and forward to a wire-level sink without an
|
||||||
|
/// intermediate copy. Allocation count is identical to [`encode`]; the
|
||||||
|
/// benefit is downstream zero-copy. (F52.1 from `design/M6-bench-baseline.md`.)
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// See [`encode`].
|
||||||
|
pub fn encode_to_bytes_mut(
|
||||||
|
handle: &MxReferenceHandle,
|
||||||
|
value: &WriteValue,
|
||||||
|
write_index: i32,
|
||||||
|
client_token: u32,
|
||||||
|
) -> Result<BytesMut, CodecError> {
|
||||||
|
let mut dst = BytesMut::new();
|
||||||
|
encode_inner_into(handle, value, write_index, client_token, None, &mut dst)?;
|
||||||
|
Ok(dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode a normal write body (`0x37`) into a caller-supplied [`BytesMut`]
|
||||||
|
/// scratch buffer. Clears `dst` first, resizes it to fit the body, and fills
|
||||||
|
/// it via the standard codec path.
|
||||||
|
///
|
||||||
|
/// Reusing the same `dst` across writes amortises the body allocation and
|
||||||
|
/// drops per-write alloc count from 2 → 1 for fixed-width scalars (and 1 → 0
|
||||||
|
/// for Boolean) once the buffer is sized for the largest body the session
|
||||||
|
/// will produce. (F52.3 session scratch pool from
|
||||||
|
/// `design/M6-bench-baseline.md`.)
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// See [`encode`].
|
||||||
|
pub fn encode_into_bytes_mut(
|
||||||
|
handle: &MxReferenceHandle,
|
||||||
|
value: &WriteValue,
|
||||||
|
write_index: i32,
|
||||||
|
client_token: u32,
|
||||||
|
dst: &mut BytesMut,
|
||||||
|
) -> Result<(), CodecError> {
|
||||||
|
encode_inner_into(handle, value, write_index, client_token, None, dst)
|
||||||
|
}
|
||||||
|
|
||||||
/// Encode a `Write2` (timestamped) body. Mirrors `NmxWriteMessage.EncodeTimestamped`
|
/// Encode a `Write2` (timestamped) body. Mirrors `NmxWriteMessage.EncodeTimestamped`
|
||||||
/// (`NmxWriteMessage.cs:36-56`).
|
/// (`NmxWriteMessage.cs:36-56`).
|
||||||
///
|
///
|
||||||
@@ -279,6 +325,53 @@ pub fn encode_timestamped(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `Write2` (timestamped) variant of [`encode_to_bytes_mut`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// See [`encode`].
|
||||||
|
pub fn encode_timestamped_to_bytes_mut(
|
||||||
|
handle: &MxReferenceHandle,
|
||||||
|
value: &WriteValue,
|
||||||
|
timestamp_filetime: i64,
|
||||||
|
write_index: i32,
|
||||||
|
client_token: u32,
|
||||||
|
) -> Result<BytesMut, CodecError> {
|
||||||
|
let mut dst = BytesMut::new();
|
||||||
|
encode_inner_into(
|
||||||
|
handle,
|
||||||
|
value,
|
||||||
|
write_index,
|
||||||
|
client_token,
|
||||||
|
Some(timestamp_filetime),
|
||||||
|
&mut dst,
|
||||||
|
)?;
|
||||||
|
Ok(dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Write2` (timestamped) variant of [`encode_into_bytes_mut`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// See [`encode`].
|
||||||
|
pub fn encode_timestamped_into_bytes_mut(
|
||||||
|
handle: &MxReferenceHandle,
|
||||||
|
value: &WriteValue,
|
||||||
|
timestamp_filetime: i64,
|
||||||
|
write_index: i32,
|
||||||
|
client_token: u32,
|
||||||
|
dst: &mut BytesMut,
|
||||||
|
) -> Result<(), CodecError> {
|
||||||
|
encode_inner_into(
|
||||||
|
handle,
|
||||||
|
value,
|
||||||
|
write_index,
|
||||||
|
client_token,
|
||||||
|
Some(timestamp_filetime),
|
||||||
|
dst,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn encode_inner(
|
fn encode_inner(
|
||||||
handle: &MxReferenceHandle,
|
handle: &MxReferenceHandle,
|
||||||
value: &WriteValue,
|
value: &WriteValue,
|
||||||
@@ -286,54 +379,82 @@ fn encode_inner(
|
|||||||
client_token: u32,
|
client_token: u32,
|
||||||
timestamp: Option<i64>,
|
timestamp: Option<i64>,
|
||||||
) -> Result<Vec<u8>, CodecError> {
|
) -> Result<Vec<u8>, CodecError> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
write_body_into_vec(
|
||||||
|
handle,
|
||||||
|
value,
|
||||||
|
write_index,
|
||||||
|
client_token,
|
||||||
|
timestamp,
|
||||||
|
&mut buf,
|
||||||
|
)?;
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_inner_into(
|
||||||
|
handle: &MxReferenceHandle,
|
||||||
|
value: &WriteValue,
|
||||||
|
write_index: i32,
|
||||||
|
client_token: u32,
|
||||||
|
timestamp: Option<i64>,
|
||||||
|
dst: &mut BytesMut,
|
||||||
|
) -> Result<(), CodecError> {
|
||||||
|
write_body_into_bytes_mut(handle, value, write_index, client_token, timestamp, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resize `dst` (a `Vec<u8>`) to the encoded body size and fill it. Used by
|
||||||
|
/// the [`encode`] path so the existing `Vec<u8>`-returning surface is one
|
||||||
|
/// allocation regardless of how the body is built downstream.
|
||||||
|
fn write_body_into_vec(
|
||||||
|
handle: &MxReferenceHandle,
|
||||||
|
value: &WriteValue,
|
||||||
|
write_index: i32,
|
||||||
|
client_token: u32,
|
||||||
|
timestamp: Option<i64>,
|
||||||
|
dst: &mut Vec<u8>,
|
||||||
|
) -> Result<(), CodecError> {
|
||||||
let kind = value.kind();
|
let kind = value.kind();
|
||||||
match value {
|
match value {
|
||||||
WriteValue::Boolean(b) => Ok(encode_boolean(
|
WriteValue::Boolean(b) => {
|
||||||
handle,
|
let size = boolean_body_size(timestamp);
|
||||||
*b,
|
resize_vec(dst, size);
|
||||||
write_index,
|
write_boolean_body(handle, *b, write_index, client_token, timestamp, dst);
|
||||||
client_token,
|
}
|
||||||
timestamp,
|
|
||||||
)),
|
|
||||||
WriteValue::Int32(_) | WriteValue::Float32(_) | WriteValue::Float64(_) => {
|
WriteValue::Int32(_) | WriteValue::Float32(_) | WriteValue::Float64(_) => {
|
||||||
let value_bytes = encode_scalar_value(value);
|
let value_bytes = encode_scalar_value(value);
|
||||||
Ok(encode_fixed(
|
let size = fixed_body_size(value_bytes.len());
|
||||||
|
resize_vec(dst, size);
|
||||||
|
write_fixed_body(
|
||||||
handle,
|
handle,
|
||||||
kind,
|
kind,
|
||||||
&value_bytes,
|
&value_bytes,
|
||||||
write_index,
|
write_index,
|
||||||
client_token,
|
client_token,
|
||||||
timestamp,
|
timestamp,
|
||||||
))
|
dst,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
WriteValue::String(s) => {
|
WriteValue::String(s) | WriteValue::DateTime(s) => {
|
||||||
let value_bytes = encode_utf16_string(s);
|
let value_bytes = encode_utf16_string(s);
|
||||||
Ok(encode_variable(
|
let size = variable_body_size(value_bytes.len());
|
||||||
|
resize_vec(dst, size);
|
||||||
|
write_variable_body(
|
||||||
handle,
|
handle,
|
||||||
kind,
|
kind,
|
||||||
&value_bytes,
|
&value_bytes,
|
||||||
write_index,
|
write_index,
|
||||||
client_token,
|
client_token,
|
||||||
timestamp,
|
timestamp,
|
||||||
))
|
dst,
|
||||||
}
|
);
|
||||||
WriteValue::DateTime(s) => {
|
|
||||||
// Caller pre-formats DateTime (see `WriteValue::DateTime` doc).
|
|
||||||
let value_bytes = encode_utf16_string(s);
|
|
||||||
Ok(encode_variable(
|
|
||||||
handle,
|
|
||||||
kind,
|
|
||||||
&value_bytes,
|
|
||||||
write_index,
|
|
||||||
client_token,
|
|
||||||
timestamp,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
WriteValue::BooleanArray(arr) => {
|
WriteValue::BooleanArray(arr) => {
|
||||||
let count = value.array_count().ok_or_else(array_too_large)?;
|
let count = value.array_count().ok_or_else(array_too_large)?;
|
||||||
let element_width = kind.array_element_width().unwrap_or(2);
|
let element_width = kind.array_element_width().unwrap_or(2);
|
||||||
let value_bytes = encode_boolean_array(arr);
|
let value_bytes = encode_boolean_array(arr);
|
||||||
Ok(encode_array(
|
let size = array_body_size(value_bytes.len());
|
||||||
|
resize_vec(dst, size);
|
||||||
|
write_array_body(
|
||||||
handle,
|
handle,
|
||||||
kind,
|
kind,
|
||||||
&value_bytes,
|
&value_bytes,
|
||||||
@@ -342,13 +463,16 @@ fn encode_inner(
|
|||||||
write_index,
|
write_index,
|
||||||
client_token,
|
client_token,
|
||||||
timestamp,
|
timestamp,
|
||||||
))
|
dst,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
WriteValue::Int32Array(arr) => {
|
WriteValue::Int32Array(arr) => {
|
||||||
let count = value.array_count().ok_or_else(array_too_large)?;
|
let count = value.array_count().ok_or_else(array_too_large)?;
|
||||||
let element_width = kind.array_element_width().unwrap_or(4);
|
let element_width = kind.array_element_width().unwrap_or(4);
|
||||||
let value_bytes = encode_i32_array(arr);
|
let value_bytes = encode_i32_array(arr);
|
||||||
Ok(encode_array(
|
let size = array_body_size(value_bytes.len());
|
||||||
|
resize_vec(dst, size);
|
||||||
|
write_array_body(
|
||||||
handle,
|
handle,
|
||||||
kind,
|
kind,
|
||||||
&value_bytes,
|
&value_bytes,
|
||||||
@@ -357,13 +481,16 @@ fn encode_inner(
|
|||||||
write_index,
|
write_index,
|
||||||
client_token,
|
client_token,
|
||||||
timestamp,
|
timestamp,
|
||||||
))
|
dst,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
WriteValue::Float32Array(arr) => {
|
WriteValue::Float32Array(arr) => {
|
||||||
let count = value.array_count().ok_or_else(array_too_large)?;
|
let count = value.array_count().ok_or_else(array_too_large)?;
|
||||||
let element_width = kind.array_element_width().unwrap_or(4);
|
let element_width = kind.array_element_width().unwrap_or(4);
|
||||||
let value_bytes = encode_f32_array(arr);
|
let value_bytes = encode_f32_array(arr);
|
||||||
Ok(encode_array(
|
let size = array_body_size(value_bytes.len());
|
||||||
|
resize_vec(dst, size);
|
||||||
|
write_array_body(
|
||||||
handle,
|
handle,
|
||||||
kind,
|
kind,
|
||||||
&value_bytes,
|
&value_bytes,
|
||||||
@@ -372,13 +499,16 @@ fn encode_inner(
|
|||||||
write_index,
|
write_index,
|
||||||
client_token,
|
client_token,
|
||||||
timestamp,
|
timestamp,
|
||||||
))
|
dst,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
WriteValue::Float64Array(arr) => {
|
WriteValue::Float64Array(arr) => {
|
||||||
let count = value.array_count().ok_or_else(array_too_large)?;
|
let count = value.array_count().ok_or_else(array_too_large)?;
|
||||||
let element_width = kind.array_element_width().unwrap_or(8);
|
let element_width = kind.array_element_width().unwrap_or(8);
|
||||||
let value_bytes = encode_f64_array(arr);
|
let value_bytes = encode_f64_array(arr);
|
||||||
Ok(encode_array(
|
let size = array_body_size(value_bytes.len());
|
||||||
|
resize_vec(dst, size);
|
||||||
|
write_array_body(
|
||||||
handle,
|
handle,
|
||||||
kind,
|
kind,
|
||||||
&value_bytes,
|
&value_bytes,
|
||||||
@@ -387,13 +517,16 @@ fn encode_inner(
|
|||||||
write_index,
|
write_index,
|
||||||
client_token,
|
client_token,
|
||||||
timestamp,
|
timestamp,
|
||||||
))
|
dst,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
WriteValue::StringArray(arr) => {
|
WriteValue::StringArray(arr) | WriteValue::DateTimeArray(arr) => {
|
||||||
let count = value.array_count().ok_or_else(array_too_large)?;
|
let count = value.array_count().ok_or_else(array_too_large)?;
|
||||||
// Variable arrays hard-code element_width = 4 (`NmxWriteMessage.cs:30, 52`).
|
// Variable arrays hard-code element_width = 4 (`NmxWriteMessage.cs:30, 52`).
|
||||||
let value_bytes = encode_variable_array(arr.iter().map(String::as_str));
|
let value_bytes = encode_variable_array(arr.iter().map(String::as_str));
|
||||||
Ok(encode_array(
|
let size = array_body_size(value_bytes.len());
|
||||||
|
resize_vec(dst, size);
|
||||||
|
write_array_body(
|
||||||
handle,
|
handle,
|
||||||
kind,
|
kind,
|
||||||
&value_bytes,
|
&value_bytes,
|
||||||
@@ -402,23 +535,162 @@ fn encode_inner(
|
|||||||
write_index,
|
write_index,
|
||||||
client_token,
|
client_token,
|
||||||
timestamp,
|
timestamp,
|
||||||
))
|
dst,
|
||||||
}
|
);
|
||||||
WriteValue::DateTimeArray(arr) => {
|
|
||||||
let count = value.array_count().ok_or_else(array_too_large)?;
|
|
||||||
let value_bytes = encode_variable_array(arr.iter().map(String::as_str));
|
|
||||||
Ok(encode_array(
|
|
||||||
handle,
|
|
||||||
kind,
|
|
||||||
&value_bytes,
|
|
||||||
count,
|
|
||||||
4,
|
|
||||||
write_index,
|
|
||||||
client_token,
|
|
||||||
timestamp,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `BytesMut` mirror of [`write_body_into_vec`]. Same body content; the only
|
||||||
|
/// difference is the buffer type. Kept as a parallel function rather than
|
||||||
|
/// generic over a trait to avoid pulling a trait abstraction into the public
|
||||||
|
/// API surface (`cargo public-api` baseline must stay unchanged for F52
|
||||||
|
/// per the followup DoD).
|
||||||
|
fn write_body_into_bytes_mut(
|
||||||
|
handle: &MxReferenceHandle,
|
||||||
|
value: &WriteValue,
|
||||||
|
write_index: i32,
|
||||||
|
client_token: u32,
|
||||||
|
timestamp: Option<i64>,
|
||||||
|
dst: &mut BytesMut,
|
||||||
|
) -> Result<(), CodecError> {
|
||||||
|
let kind = value.kind();
|
||||||
|
match value {
|
||||||
|
WriteValue::Boolean(b) => {
|
||||||
|
let size = boolean_body_size(timestamp);
|
||||||
|
resize_bytes_mut(dst, size);
|
||||||
|
write_boolean_body(handle, *b, write_index, client_token, timestamp, dst);
|
||||||
|
}
|
||||||
|
WriteValue::Int32(_) | WriteValue::Float32(_) | WriteValue::Float64(_) => {
|
||||||
|
let value_bytes = encode_scalar_value(value);
|
||||||
|
let size = fixed_body_size(value_bytes.len());
|
||||||
|
resize_bytes_mut(dst, size);
|
||||||
|
write_fixed_body(
|
||||||
|
handle,
|
||||||
|
kind,
|
||||||
|
&value_bytes,
|
||||||
|
write_index,
|
||||||
|
client_token,
|
||||||
|
timestamp,
|
||||||
|
dst,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WriteValue::String(s) | WriteValue::DateTime(s) => {
|
||||||
|
let value_bytes = encode_utf16_string(s);
|
||||||
|
let size = variable_body_size(value_bytes.len());
|
||||||
|
resize_bytes_mut(dst, size);
|
||||||
|
write_variable_body(
|
||||||
|
handle,
|
||||||
|
kind,
|
||||||
|
&value_bytes,
|
||||||
|
write_index,
|
||||||
|
client_token,
|
||||||
|
timestamp,
|
||||||
|
dst,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WriteValue::BooleanArray(arr) => {
|
||||||
|
let count = value.array_count().ok_or_else(array_too_large)?;
|
||||||
|
let element_width = kind.array_element_width().unwrap_or(2);
|
||||||
|
let value_bytes = encode_boolean_array(arr);
|
||||||
|
let size = array_body_size(value_bytes.len());
|
||||||
|
resize_bytes_mut(dst, size);
|
||||||
|
write_array_body(
|
||||||
|
handle,
|
||||||
|
kind,
|
||||||
|
&value_bytes,
|
||||||
|
count,
|
||||||
|
element_width,
|
||||||
|
write_index,
|
||||||
|
client_token,
|
||||||
|
timestamp,
|
||||||
|
dst,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WriteValue::Int32Array(arr) => {
|
||||||
|
let count = value.array_count().ok_or_else(array_too_large)?;
|
||||||
|
let element_width = kind.array_element_width().unwrap_or(4);
|
||||||
|
let value_bytes = encode_i32_array(arr);
|
||||||
|
let size = array_body_size(value_bytes.len());
|
||||||
|
resize_bytes_mut(dst, size);
|
||||||
|
write_array_body(
|
||||||
|
handle,
|
||||||
|
kind,
|
||||||
|
&value_bytes,
|
||||||
|
count,
|
||||||
|
element_width,
|
||||||
|
write_index,
|
||||||
|
client_token,
|
||||||
|
timestamp,
|
||||||
|
dst,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WriteValue::Float32Array(arr) => {
|
||||||
|
let count = value.array_count().ok_or_else(array_too_large)?;
|
||||||
|
let element_width = kind.array_element_width().unwrap_or(4);
|
||||||
|
let value_bytes = encode_f32_array(arr);
|
||||||
|
let size = array_body_size(value_bytes.len());
|
||||||
|
resize_bytes_mut(dst, size);
|
||||||
|
write_array_body(
|
||||||
|
handle,
|
||||||
|
kind,
|
||||||
|
&value_bytes,
|
||||||
|
count,
|
||||||
|
element_width,
|
||||||
|
write_index,
|
||||||
|
client_token,
|
||||||
|
timestamp,
|
||||||
|
dst,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WriteValue::Float64Array(arr) => {
|
||||||
|
let count = value.array_count().ok_or_else(array_too_large)?;
|
||||||
|
let element_width = kind.array_element_width().unwrap_or(8);
|
||||||
|
let value_bytes = encode_f64_array(arr);
|
||||||
|
let size = array_body_size(value_bytes.len());
|
||||||
|
resize_bytes_mut(dst, size);
|
||||||
|
write_array_body(
|
||||||
|
handle,
|
||||||
|
kind,
|
||||||
|
&value_bytes,
|
||||||
|
count,
|
||||||
|
element_width,
|
||||||
|
write_index,
|
||||||
|
client_token,
|
||||||
|
timestamp,
|
||||||
|
dst,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WriteValue::StringArray(arr) | WriteValue::DateTimeArray(arr) => {
|
||||||
|
let count = value.array_count().ok_or_else(array_too_large)?;
|
||||||
|
let value_bytes = encode_variable_array(arr.iter().map(String::as_str));
|
||||||
|
let size = array_body_size(value_bytes.len());
|
||||||
|
resize_bytes_mut(dst, size);
|
||||||
|
write_array_body(
|
||||||
|
handle,
|
||||||
|
kind,
|
||||||
|
&value_bytes,
|
||||||
|
count,
|
||||||
|
4,
|
||||||
|
write_index,
|
||||||
|
client_token,
|
||||||
|
timestamp,
|
||||||
|
dst,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize_vec(dst: &mut Vec<u8>, size: usize) {
|
||||||
|
dst.clear();
|
||||||
|
dst.resize(size, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize_bytes_mut(dst: &mut BytesMut, size: usize) {
|
||||||
|
dst.clear();
|
||||||
|
dst.resize(size, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn array_too_large() -> CodecError {
|
fn array_too_large() -> CodecError {
|
||||||
@@ -431,21 +703,53 @@ fn array_too_large() -> CodecError {
|
|||||||
|
|
||||||
// ---- Body builders --------------------------------------------------------
|
// ---- Body builders --------------------------------------------------------
|
||||||
|
|
||||||
|
// All builders below assume `body` is a pre-sized, zero-initialised slice
|
||||||
|
// (the dispatcher resizes the destination buffer up front). They are
|
||||||
|
// allocation-free; the only allocations on the encode path are (a) the
|
||||||
|
// destination buffer itself and (b) the per-value scratch buffer (e.g.
|
||||||
|
// `encode_scalar_value`). Pulling the size compute out of the builders
|
||||||
|
// is what lets F52.3 reuse the destination buffer across writes.
|
||||||
|
|
||||||
|
const fn boolean_body_size(timestamp: Option<i64>) -> usize {
|
||||||
|
if timestamp.is_some() {
|
||||||
|
// Timestamped: 1-byte payload + 14-byte timestamped suffix + 4-byte index.
|
||||||
|
KIND_OFFSET + 1 + 1 + 14 + 4
|
||||||
|
} else {
|
||||||
|
// Normal: 4-byte literal payload + 11-byte Boolean suffix + 4-byte index.
|
||||||
|
// Total = 18 + 4 + 11 + 4 = 37 bytes (`NmxWriteMessage.cs:123`).
|
||||||
|
KIND_OFFSET + 1 + 4 + 11 + 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn fixed_body_size(value_bytes_len: usize) -> usize {
|
||||||
|
KIND_OFFSET + 1 + value_bytes_len + 14 + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn variable_body_size(value_bytes_len: usize) -> usize {
|
||||||
|
// body alloc = 18 + 4 + 4 + N + 14 + 4 = 44 + N.
|
||||||
|
KIND_OFFSET + 1 + 4 + 4 + value_bytes_len + 14 + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn array_body_size(value_bytes_len: usize) -> usize {
|
||||||
|
// body alloc = 18 + 10 + N + 14 + 4 (`NmxWriteMessage.cs:179, 198`).
|
||||||
|
KIND_OFFSET + 1 + 10 + value_bytes_len + 14 + 4
|
||||||
|
}
|
||||||
|
|
||||||
/// Boolean write body. The normal form uses the 11-byte Boolean suffix
|
/// Boolean write body. The normal form uses the 11-byte Boolean suffix
|
||||||
/// (`NmxWriteMessage.cs:121-128`); the timestamped form uses a single-byte
|
/// (`NmxWriteMessage.cs:121-128`); the timestamped form uses a single-byte
|
||||||
/// payload with the 14-byte timestamped suffix (`NmxWriteMessage.cs:130-137`).
|
/// payload with the 14-byte timestamped suffix (`NmxWriteMessage.cs:130-137`).
|
||||||
fn encode_boolean(
|
fn write_boolean_body(
|
||||||
handle: &MxReferenceHandle,
|
handle: &MxReferenceHandle,
|
||||||
value: bool,
|
value: bool,
|
||||||
write_index: i32,
|
write_index: i32,
|
||||||
client_token: u32,
|
client_token: u32,
|
||||||
timestamp: Option<i64>,
|
timestamp: Option<i64>,
|
||||||
) -> Vec<u8> {
|
body: &mut [u8],
|
||||||
|
) {
|
||||||
if let Some(filetime) = timestamp {
|
if let Some(filetime) = timestamp {
|
||||||
// Timestamped: 1-byte payload + 14-byte timestamped suffix + 4-byte index.
|
// Timestamped: 1-byte payload + 14-byte timestamped suffix + 4-byte index.
|
||||||
// Total = 18 + 1 + 14 + 4 = 37. Same total as normal Boolean.
|
// Total = 18 + 1 + 14 + 4 = 37. Same total as normal Boolean.
|
||||||
let mut body = vec![0u8; KIND_OFFSET + 1 + 1 + 14 + 4];
|
write_common_prefix(body, handle, WriteValueKind::Boolean);
|
||||||
write_common_prefix(&mut body, handle, WriteValueKind::Boolean);
|
|
||||||
body[KIND_OFFSET + 1] = if value { 0xff } else { 0x00 };
|
body[KIND_OFFSET + 1] = if value { 0xff } else { 0x00 };
|
||||||
write_timestamped_suffix(
|
write_timestamped_suffix(
|
||||||
&mut body[KIND_OFFSET + 2..],
|
&mut body[KIND_OFFSET + 2..],
|
||||||
@@ -453,35 +757,31 @@ fn encode_boolean(
|
|||||||
write_index,
|
write_index,
|
||||||
client_token,
|
client_token,
|
||||||
);
|
);
|
||||||
body
|
|
||||||
} else {
|
} else {
|
||||||
// Normal: 4-byte literal payload + 11-byte Boolean suffix + 4-byte index.
|
// Normal: 4-byte literal payload + 11-byte Boolean suffix + 4-byte index.
|
||||||
// Total = 18 + 4 + 11 + 4 = 37 bytes (`NmxWriteMessage.cs:123`).
|
|
||||||
let value_bytes = encode_boolean_value(value);
|
let value_bytes = encode_boolean_value(value);
|
||||||
let mut body = vec![0u8; KIND_OFFSET + 1 + value_bytes.len() + 11 + 4];
|
write_common_prefix(body, handle, WriteValueKind::Boolean);
|
||||||
write_common_prefix(&mut body, handle, WriteValueKind::Boolean);
|
|
||||||
body[KIND_OFFSET + 1..KIND_OFFSET + 1 + value_bytes.len()].copy_from_slice(&value_bytes);
|
body[KIND_OFFSET + 1..KIND_OFFSET + 1 + value_bytes.len()].copy_from_slice(&value_bytes);
|
||||||
write_boolean_suffix(
|
write_boolean_suffix(
|
||||||
&mut body[KIND_OFFSET + 1 + value_bytes.len()..],
|
&mut body[KIND_OFFSET + 1 + value_bytes.len()..],
|
||||||
write_index,
|
write_index,
|
||||||
client_token,
|
client_token,
|
||||||
);
|
);
|
||||||
body
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fixed-size scalar (Int32, Float32, Float64). Mirrors `CreateFixed` /
|
/// Fixed-size scalar (Int32, Float32, Float64). Mirrors `CreateFixed` /
|
||||||
/// `CreateFixedTimestamped` (`NmxWriteMessage.cs:112-119, 139-146`).
|
/// `CreateFixedTimestamped` (`NmxWriteMessage.cs:112-119, 139-146`).
|
||||||
fn encode_fixed(
|
fn write_fixed_body(
|
||||||
handle: &MxReferenceHandle,
|
handle: &MxReferenceHandle,
|
||||||
kind: WriteValueKind,
|
kind: WriteValueKind,
|
||||||
value_bytes: &[u8],
|
value_bytes: &[u8],
|
||||||
write_index: i32,
|
write_index: i32,
|
||||||
client_token: u32,
|
client_token: u32,
|
||||||
timestamp: Option<i64>,
|
timestamp: Option<i64>,
|
||||||
) -> Vec<u8> {
|
body: &mut [u8],
|
||||||
let mut body = vec![0u8; KIND_OFFSET + 1 + value_bytes.len() + 14 + 4];
|
) {
|
||||||
write_common_prefix(&mut body, handle, kind);
|
write_common_prefix(body, handle, kind);
|
||||||
body[KIND_OFFSET + 1..KIND_OFFSET + 1 + value_bytes.len()].copy_from_slice(value_bytes);
|
body[KIND_OFFSET + 1..KIND_OFFSET + 1 + value_bytes.len()].copy_from_slice(value_bytes);
|
||||||
let suffix_start = KIND_OFFSET + 1 + value_bytes.len();
|
let suffix_start = KIND_OFFSET + 1 + value_bytes.len();
|
||||||
match timestamp {
|
match timestamp {
|
||||||
@@ -490,28 +790,26 @@ fn encode_fixed(
|
|||||||
}
|
}
|
||||||
None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token),
|
None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token),
|
||||||
}
|
}
|
||||||
body
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Variable-length payload (String, DateTime). Mirrors `CreateVariable` /
|
/// Variable-length payload (String, DateTime). Mirrors `CreateVariable` /
|
||||||
/// `CreateVariableTimestamped` (`NmxWriteMessage.cs:148-168`). Total length
|
/// `CreateVariableTimestamped` (`NmxWriteMessage.cs:148-168`). Total length
|
||||||
/// is `44 + utf16_bytes_len`.
|
/// is `44 + utf16_bytes_len`.
|
||||||
fn encode_variable(
|
fn write_variable_body(
|
||||||
handle: &MxReferenceHandle,
|
handle: &MxReferenceHandle,
|
||||||
kind: WriteValueKind,
|
kind: WriteValueKind,
|
||||||
value_bytes: &[u8],
|
value_bytes: &[u8],
|
||||||
write_index: i32,
|
write_index: i32,
|
||||||
client_token: u32,
|
client_token: u32,
|
||||||
timestamp: Option<i64>,
|
timestamp: Option<i64>,
|
||||||
) -> Vec<u8> {
|
body: &mut [u8],
|
||||||
// body alloc = 18 + 4 + 4 + N + 14 + 4 = 44 + N.
|
) {
|
||||||
let mut body = vec![0u8; KIND_OFFSET + 1 + 4 + 4 + value_bytes.len() + 14 + 4];
|
write_common_prefix(body, handle, kind);
|
||||||
write_common_prefix(&mut body, handle, kind);
|
|
||||||
// body[18..22] = outer_length = N + 4 (`NmxWriteMessage.cs:152, 163`)
|
// body[18..22] = outer_length = N + 4 (`NmxWriteMessage.cs:152, 163`)
|
||||||
let outer_len = (value_bytes.len() as i32).wrapping_add(4);
|
let outer_len = (value_bytes.len() as i32).wrapping_add(4);
|
||||||
write_i32_le(&mut body, 18, outer_len);
|
write_i32_le(body, 18, outer_len);
|
||||||
// body[22..26] = inner_length = N (`NmxWriteMessage.cs:153, 164`)
|
// body[22..26] = inner_length = N (`NmxWriteMessage.cs:153, 164`)
|
||||||
write_i32_le(&mut body, 22, value_bytes.len() as i32);
|
write_i32_le(body, 22, value_bytes.len() as i32);
|
||||||
// body[26..26+N] = payload (`NmxWriteMessage.cs:154, 165`)
|
// body[26..26+N] = payload (`NmxWriteMessage.cs:154, 165`)
|
||||||
body[26..26 + value_bytes.len()].copy_from_slice(value_bytes);
|
body[26..26 + value_bytes.len()].copy_from_slice(value_bytes);
|
||||||
let suffix_start = 26 + value_bytes.len();
|
let suffix_start = 26 + value_bytes.len();
|
||||||
@@ -521,13 +819,12 @@ fn encode_variable(
|
|||||||
}
|
}
|
||||||
None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token),
|
None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token),
|
||||||
}
|
}
|
||||||
body
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Array body. Mirrors `CreateArray` / `CreateArrayTimestamped`
|
/// Array body. Mirrors `CreateArray` / `CreateArrayTimestamped`
|
||||||
/// (`NmxWriteMessage.cs:170-205`).
|
/// (`NmxWriteMessage.cs:170-205`).
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn encode_array(
|
fn write_array_body(
|
||||||
handle: &MxReferenceHandle,
|
handle: &MxReferenceHandle,
|
||||||
kind: WriteValueKind,
|
kind: WriteValueKind,
|
||||||
value_bytes: &[u8],
|
value_bytes: &[u8],
|
||||||
@@ -536,16 +833,15 @@ fn encode_array(
|
|||||||
write_index: i32,
|
write_index: i32,
|
||||||
client_token: u32,
|
client_token: u32,
|
||||||
timestamp: Option<i64>,
|
timestamp: Option<i64>,
|
||||||
) -> Vec<u8> {
|
body: &mut [u8],
|
||||||
// body alloc = 18 + 10 + N + 14 + 4 (`NmxWriteMessage.cs:179, 198`).
|
) {
|
||||||
let mut body = vec![0u8; KIND_OFFSET + 1 + 10 + value_bytes.len() + 14 + 4];
|
write_common_prefix(body, handle, kind);
|
||||||
write_common_prefix(&mut body, handle, kind);
|
|
||||||
// body[22..24] = count u16 LE (`NmxWriteMessage.cs:181, 200`).
|
// body[22..24] = count u16 LE (`NmxWriteMessage.cs:181, 200`).
|
||||||
write_u16_le(&mut body, 22, count);
|
write_u16_le(body, 22, count);
|
||||||
// body[24..26] = element_width u16 LE (`NmxWriteMessage.cs:182, 201`).
|
// body[24..26] = element_width u16 LE (`NmxWriteMessage.cs:182, 201`).
|
||||||
write_u16_le(&mut body, 24, element_width);
|
write_u16_le(body, 24, element_width);
|
||||||
// body[18..22] and body[26..28] are zero-initialised by vec! and not
|
// body[18..22] and body[26..28] are zero-initialised by the dispatcher's
|
||||||
// written by the .NET reference either — they remain zero.
|
// resize and not written by the .NET reference either — they remain zero.
|
||||||
body[28..28 + value_bytes.len()].copy_from_slice(value_bytes);
|
body[28..28 + value_bytes.len()].copy_from_slice(value_bytes);
|
||||||
let suffix_start = 28 + value_bytes.len();
|
let suffix_start = 28 + value_bytes.len();
|
||||||
match timestamp {
|
match timestamp {
|
||||||
@@ -554,7 +850,6 @@ fn encode_array(
|
|||||||
}
|
}
|
||||||
None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token),
|
None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token),
|
||||||
}
|
}
|
||||||
body
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Prefix and suffix writers --------------------------------------------
|
// ---- Prefix and suffix writers --------------------------------------------
|
||||||
@@ -1578,7 +1873,7 @@ mod tests {
|
|||||||
expected.extend_from_slice(&[0x01, 0x00]); // .cs:210 (version=1)
|
expected.extend_from_slice(&[0x01, 0x00]); // .cs:210 (version=1)
|
||||||
expected.extend_from_slice(&projection); // .cs:211
|
expected.extend_from_slice(&projection); // .cs:211
|
||||||
expected.push(0x01); // .cs:98 Boolean wire kind
|
expected.push(0x01); // .cs:98 Boolean wire kind
|
||||||
// Boolean payload literal (.cs:257)
|
// Boolean payload literal (.cs:257)
|
||||||
expected.extend_from_slice(&[0xff, 0xff, 0xff, 0x00]);
|
expected.extend_from_slice(&[0xff, 0xff, 0xff, 0x00]);
|
||||||
// 7-byte zero region of Boolean suffix (.cs:235)
|
// 7-byte zero region of Boolean suffix (.cs:235)
|
||||||
expected.extend_from_slice(&[0; 7]);
|
expected.extend_from_slice(&[0; 7]);
|
||||||
|
|||||||
Reference in New Issue
Block a user