rustbinary 0.1.6

A bounded nextjson binary codec with adaptive frames, zero-allocation paths, schema evolution, and authenticated pipelines
Documentation
use std::{
    env,
    hint::black_box,
    time::{Duration, Instant},
};

use nextjson::{NsonDeserialize, NsonSerialize};

const DEFAULT_SAMPLE_TIME: Duration = Duration::from_millis(200);
const SAMPLES: usize = 9;

#[derive(Clone, Debug, NsonDeserialize, PartialEq, NsonSerialize)]
struct Telemetry {
    sequence: u64,
    timestamp_delta: i64,
    status: u16,
    topic: String,
    readings: Vec<i32>,
    payload: Vec<u8>,
}

struct CodecResult {
    codec: &'static str,
    encoded_size: usize,
    encode_ns: f64,
    decode_ns: f64,
}

fn main() {
    let sample_time = env::var("RUSTBINARY_BENCH_MS")
        .ok()
        .and_then(|value| value.parse::<u64>().ok())
        .map(Duration::from_millis)
        .unwrap_or(DEFAULT_SAMPLE_TIME);

    println!("| shape | codec | bytes | encode ns/op | decode ns/op |");
    println!("| --- | --- | ---: | ---: | ---: |");
    run_shape("small", &small_record(), sample_time);
    run_shape("telemetry", &telemetry_record(32, 128), sample_time);
    run_shape("bulk", &telemetry_record(4_096, 16_384), sample_time);
}

fn small_record() -> Telemetry {
    Telemetry {
        sequence: 42,
        timestamp_delta: -7,
        status: 3,
        topic: "edge/a".to_owned(),
        readings: vec![17, 18, 19],
        payload: vec![0, 1, 2, 3],
    }
}

fn telemetry_record(readings: usize, payload: usize) -> Telemetry {
    Telemetry {
        sequence: 4_294_967_296,
        timestamp_delta: -86_400_000,
        status: 0x20,
        topic: "factory/line-07/temperature".to_owned(),
        readings: (0..readings)
            .map(|index| 20_000 + (index % 97) as i32)
            .collect(),
        payload: (0..payload)
            .map(|index| ((index.wrapping_mul(31)) % 251) as u8)
            .collect(),
    }
}

fn run_shape<T>(shape: &str, value: &T, sample_time: Duration)
where
    T: for<'de> nextjson::NsonDeserialize<'de> + PartialEq + NsonSerialize,
{
    let owned = benchmark_owned(value, sample_time);
    let caller_buffer = benchmark_caller_buffer(value, sample_time);

    for result in [owned, caller_buffer] {
        println!(
            "| {shape} | {} | {} | {:.1} | {:.1} |",
            result.codec, result.encoded_size, result.encode_ns, result.decode_ns
        );
    }
}

fn benchmark_owned<T>(value: &T, sample_time: Duration) -> CodecResult
where
    T: for<'de> nextjson::NsonDeserialize<'de> + PartialEq + NsonSerialize,
{
    let config = rustbinary::options();
    let encoded = config.serialize(value).expect("RustBinary encode failed");
    assert!(
        config
            .deserialize::<T>(&encoded)
            .expect("RustBinary decode failed")
            == *value
    );
    CodecResult {
        codec: "Compact V1 owned",
        encoded_size: encoded.len(),
        encode_ns: median_ns(sample_time, || {
            black_box(config.serialize(black_box(value)).expect("encode failed"));
        }),
        decode_ns: median_ns(sample_time, || {
            black_box(
                config
                    .deserialize::<T>(black_box(&encoded))
                    .expect("decode failed"),
            );
        }),
    }
}

fn benchmark_caller_buffer<T>(value: &T, sample_time: Duration) -> CodecResult
where
    T: for<'de> nextjson::NsonDeserialize<'de> + PartialEq + NsonSerialize,
{
    let config = rustbinary::options();
    let required = usize::try_from(
        config
            .serialized_size(value)
            .expect("size calculation failed"),
    )
    .expect("encoded size does not fit usize");
    let mut encoded = vec![0_u8; required];
    let written = config
        .serialize_into_slice(&mut encoded, value)
        .expect("caller-buffer encode failed");
    assert_eq!(written, required);
    assert!(
        config
            .deserialize::<T>(&encoded)
            .expect("caller-buffer decode failed")
            == *value
    );

    CodecResult {
        codec: "Compact V1 caller buffer",
        encoded_size: encoded.len(),
        encode_ns: median_ns(sample_time, || {
            let written = config
                .serialize_into_slice(black_box(&mut encoded), black_box(value))
                .expect("encode failed");
            black_box(written);
        }),
        decode_ns: median_ns(sample_time, || {
            black_box(
                config
                    .deserialize::<T>(black_box(&encoded))
                    .expect("decode failed"),
            );
        }),
    }
}

fn median_ns(mut minimum_time: Duration, mut operation: impl FnMut()) -> f64 {
    if minimum_time.is_zero() {
        minimum_time = Duration::from_millis(1);
    }
    let iterations = calibrate_iterations(minimum_time, &mut operation);
    let mut samples = [0.0; SAMPLES];
    for sample in &mut samples {
        let started = Instant::now();
        for _ in 0..iterations {
            operation();
        }
        *sample = started.elapsed().as_nanos() as f64 / iterations as f64;
    }
    samples.sort_by(f64::total_cmp);
    samples[SAMPLES / 2]
}

fn calibrate_iterations(minimum_time: Duration, operation: &mut impl FnMut()) -> u64 {
    let mut iterations = 1_u64;
    loop {
        let started = Instant::now();
        for _ in 0..iterations {
            operation();
        }
        let elapsed = started.elapsed();
        if elapsed >= minimum_time || iterations >= 1 << 30 {
            return iterations;
        }
        let elapsed_ns = elapsed.as_nanos().max(1);
        let target_ns = minimum_time.as_nanos();
        let factor = (target_ns / elapsed_ns).clamp(2, 10) as u64;
        iterations = iterations.saturating_mul(factor);
    }
}