[F52.3] mxaccess-codec: caller-supplied scratch buffer for write encoder
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:
- Int32 / Float32 / Float64: 2 → 1 allocs/op
(only the `encode_scalar_value` scratch `Vec<u8>` remains)
- Boolean: 1 → 0 allocs/op
(no per-value scratch — the literal payload is a stack `[u8; 4]`)
Bench delta in `design/M6-bench-baseline.md` § F52.3. The
`encode_scalar_value` Vec is the remaining 1 alloc/op for fixed-width
scalars; eliminating it would require inlining the LE-bytes write
into the body slice (left for a follow-up since the F52 spec only
asks for 2 → 1).
Resolves F52 (all three optimisations landed: 4e76b44 F52.1,
a0fa5be F52.2, this commit F52.3). Existing `encode` / `encode_to_bytes_mut`
public surface unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@
|
||||
use std::alloc::{GlobalAlloc, Layout, System};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use bytes::BytesMut;
|
||||
use mxaccess_codec::{
|
||||
write_message, write_message::WriteValue, MxReferenceHandle, NmxSubscriptionMessage,
|
||||
};
|
||||
@@ -214,6 +215,39 @@ fn bench_write_int32_bytes_mut() -> Row {
|
||||
})
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Build a single-record DataUpdate body once; decode N times.
|
||||
let body = build_data_update_int32_body(42);
|
||||
@@ -275,6 +309,8 @@ fn main() {
|
||||
bench_write_bool(),
|
||||
bench_write_string(),
|
||||
bench_write_int32_bytes_mut(),
|
||||
bench_write_int32_into_pooled(),
|
||||
bench_write_bool_into_pooled(),
|
||||
bench_handle_from_names(),
|
||||
bench_subscription_decode(),
|
||||
];
|
||||
|
||||
@@ -276,6 +276,29 @@ pub fn encode_to_bytes_mut(
|
||||
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`
|
||||
/// (`NmxWriteMessage.cs:36-56`).
|
||||
///
|
||||
@@ -326,6 +349,29 @@ pub fn encode_timestamped_to_bytes_mut(
|
||||
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(
|
||||
handle: &MxReferenceHandle,
|
||||
value: &WriteValue,
|
||||
|
||||
Reference in New Issue
Block a user