use peat_btle::peat_mesh::{PeatMesh, PeatMeshConfig};
use peat_btle::NodeId;
use std::time::{Duration, Instant};
const DOCUMENT_SIZES: &[(usize, &str)] = &[
(0, "minimal (counter only)"),
(1, "small (1 chat message)"),
(5, "medium (5 chat messages)"),
(10, "large (10 chat messages)"),
(20, "very large (20 chat messages)"),
];
const ITERATIONS: usize = 100;
const TEST_SECRET: [u8; 32] = [
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
];
fn create_unencrypted_mesh(node_id: u32, callsign: &str) -> PeatMesh {
let config = PeatMeshConfig::new(NodeId::new(node_id), callsign, "BENCH");
PeatMesh::new(config)
}
fn create_encrypted_mesh(node_id: u32, callsign: &str) -> PeatMesh {
let config =
PeatMeshConfig::new(NodeId::new(node_id), callsign, "BENCH").with_encryption(TEST_SECRET);
PeatMesh::new(config)
}
#[cfg(feature = "legacy-chat")]
fn populate_mesh_with_chats(mesh: &PeatMesh, count: usize) {
for i in 0..count {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let message = format!("Test message {} with some padding content", i);
mesh.send_chat("BENCH", &message, now + i as u64);
}
}
fn benchmark_build_document(mesh: &PeatMesh, iterations: usize) -> Duration {
let start = Instant::now();
for _ in 0..iterations {
let _doc = mesh.build_document();
}
start.elapsed()
}
fn benchmark_sync_cycle(sender: &PeatMesh, receiver: &PeatMesh, iterations: usize) -> Duration {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let start = Instant::now();
for i in 0..iterations {
let doc_bytes = sender.build_document();
let received = doc_bytes.clone();
let _ = receiver.on_ble_data("bench-peer", &received, now_ms + i as u64);
}
start.elapsed()
}
fn print_results(
label: &str,
unencrypted_build: Duration,
encrypted_build: Duration,
unencrypted_sync: Duration,
encrypted_sync: Duration,
unencrypted_size: usize,
encrypted_size: usize,
) {
let build_overhead_us =
encrypted_build.as_micros() as i64 - unencrypted_build.as_micros() as i64;
let build_overhead_pct = if unencrypted_build.as_micros() > 0 {
(build_overhead_us as f64 / unencrypted_build.as_micros() as f64) * 100.0
} else {
0.0
};
let sync_overhead_us = encrypted_sync.as_micros() as i64 - unencrypted_sync.as_micros() as i64;
let sync_overhead_pct = if unencrypted_sync.as_micros() > 0 {
(sync_overhead_us as f64 / unencrypted_sync.as_micros() as f64) * 100.0
} else {
0.0
};
let size_overhead = encrypted_size as i64 - unencrypted_size as i64;
println!(
"| {:30} | {:>8} | {:>8} | {:>+8} ({:>+6.1}%) | {:>8} | {:>8} | {:>+8} ({:>+6.1}%) | {:>6} | {:>6} | {:>+5} |",
label,
format!("{}µs", unencrypted_build.as_micros() / ITERATIONS as u128),
format!("{}µs", encrypted_build.as_micros() / ITERATIONS as u128),
format!("{}µs", build_overhead_us / ITERATIONS as i64),
build_overhead_pct,
format!("{}µs", unencrypted_sync.as_micros() / ITERATIONS as u128),
format!("{}µs", encrypted_sync.as_micros() / ITERATIONS as u128),
format!("{}µs", sync_overhead_us / ITERATIONS as i64),
sync_overhead_pct,
unencrypted_size,
encrypted_size,
size_overhead,
);
}
#[test]
fn benchmark_encryption_latency() {
println!("\n");
println!("{}", "=".repeat(180));
println!("PEAT-BTLE Encryption Benchmark: Sync Latency Comparison");
println!("{}", "=".repeat(180));
println!("Iterations per test: {}", ITERATIONS);
println!("Encryption: ChaCha20-Poly1305 (30 byte overhead)");
println!();
println!(
"| {:30} | {:>8} | {:>8} | {:>20} | {:>8} | {:>8} | {:>20} | {:>6} | {:>6} | {:>5} |",
"Document Size",
"Build",
"Build",
"Build Overhead",
"Sync",
"Sync",
"Sync Overhead",
"Size",
"Size",
"Size"
);
println!(
"| {:30} | {:>8} | {:>8} | {:>20} | {:>8} | {:>8} | {:>20} | {:>6} | {:>6} | {:>5} |",
"", "Plain", "Enc", "(per op)", "Plain", "Enc", "(per op)", "Plain", "Enc", "Δ"
);
println!("{}", "-".repeat(180));
for (_chat_count, label) in DOCUMENT_SIZES {
let sender_plain = create_unencrypted_mesh(0x1111, "SEND");
let receiver_plain = create_unencrypted_mesh(0x2222, "RECV");
let sender_enc = create_encrypted_mesh(0x3333, "SEND");
let receiver_enc = create_encrypted_mesh(0x4444, "RECV");
#[cfg(feature = "legacy-chat")]
{
populate_mesh_with_chats(&sender_plain, *_chat_count);
populate_mesh_with_chats(&sender_enc, *_chat_count);
}
let plain_doc = sender_plain.build_document();
let enc_doc = sender_enc.build_document();
let plain_size = plain_doc.len();
let enc_size = enc_doc.len();
let plain_build = benchmark_build_document(&sender_plain, ITERATIONS);
let enc_build = benchmark_build_document(&sender_enc, ITERATIONS);
let plain_sync = benchmark_sync_cycle(&sender_plain, &receiver_plain, ITERATIONS);
let enc_sync = benchmark_sync_cycle(&sender_enc, &receiver_enc, ITERATIONS);
print_results(
label,
plain_build,
enc_build,
plain_sync,
enc_sync,
plain_size,
enc_size,
);
}
println!("{}", "-".repeat(180));
println!();
println!("Notes:");
println!(" - Build: Time to serialize document (includes encryption if enabled)");
println!(
" - Sync: Full cycle: build + transmit (memory copy) + process (includes decryption)"
);
println!(" - Size overhead is ~30 bytes (2 byte marker + 12 byte nonce + 16 byte auth tag)");
println!(" - Latency overhead is primarily from ChaCha20-Poly1305 AEAD operations");
println!();
}
#[test]
fn benchmark_encryption_throughput() {
println!("\n");
println!("{}", "=".repeat(100));
println!("PEAT-BTLE Encryption Throughput Benchmark");
println!("{}", "=".repeat(100));
let sender_plain = create_unencrypted_mesh(0x1111, "SEND");
let sender_enc = create_encrypted_mesh(0x2222, "SEND");
#[cfg(feature = "legacy-chat")]
{
populate_mesh_with_chats(&sender_plain, 10);
populate_mesh_with_chats(&sender_enc, 10);
}
let plain_size = sender_plain.build_document().len();
let enc_size = sender_enc.build_document().len();
let duration = Duration::from_secs(1);
let start = Instant::now();
let mut plain_count = 0u64;
while start.elapsed() < duration {
let _doc = sender_plain.build_document();
plain_count += 1;
}
let plain_elapsed = start.elapsed();
let start = Instant::now();
let mut enc_count = 0u64;
while start.elapsed() < duration {
let _doc = sender_enc.build_document();
enc_count += 1;
}
let enc_elapsed = start.elapsed();
let plain_rate = plain_count as f64 / plain_elapsed.as_secs_f64();
let enc_rate = enc_count as f64 / enc_elapsed.as_secs_f64();
let plain_throughput_kbps = (plain_rate * plain_size as f64 * 8.0) / 1000.0;
let enc_throughput_kbps = (enc_rate * enc_size as f64 * 8.0) / 1000.0;
println!();
println!(
"Document size: {} bytes (plain), {} bytes (encrypted)",
plain_size, enc_size
);
println!();
println!(
"| {:20} | {:>15} | {:>15} | {:>15} |",
"Mode", "Docs/sec", "KB/sec", "Throughput"
);
println!("{}", "-".repeat(75));
println!(
"| {:20} | {:>15.0} | {:>15.2} | {:>15.2} kbps |",
"Unencrypted",
plain_rate,
plain_rate * plain_size as f64 / 1024.0,
plain_throughput_kbps
);
println!(
"| {:20} | {:>15.0} | {:>15.2} | {:>15.2} kbps |",
"Encrypted",
enc_rate,
enc_rate * enc_size as f64 / 1024.0,
enc_throughput_kbps
);
println!("{}", "-".repeat(75));
println!();
println!(
"Encryption overhead: {:.1}% reduction in throughput",
(1.0 - enc_rate / plain_rate) * 100.0
);
println!();
}
#[cfg(feature = "legacy-chat")]
#[test]
fn test_encryption_correctness() {
let sender = create_encrypted_mesh(0x1111, "SEND");
let receiver = create_encrypted_mesh(0x2222, "RECV");
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
sender.send_chat("ALICE", "Hello encrypted world!", now);
let doc_bytes = sender.build_document();
assert_eq!(
doc_bytes[0], 0xAE,
"Document should be encrypted (marker 0xAE)"
);
let result = receiver.on_ble_data("sender", &doc_bytes, now);
assert!(result.is_some(), "Encrypted document should be processable");
assert!(receiver.chat_count() > 0, "Chat should have been synced");
println!("Encryption correctness test passed!");
}