[F52.3] mxaccess-codec: caller-supplied scratch buffer for write encoder
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled

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:
Joseph Doherty
2026-05-06 22:53:07 -04:00
parent a0fa5bedfd
commit ceeaeefa71
4 changed files with 114 additions and 5 deletions
@@ -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,