Skip to content

Commit a7a584c

Browse files
authored
Merge pull request #21 from doublegate/copilot/improve-deserialization-performance
Migrate mmap serialization from bincode to rkyv with zero-copy deserialization
2 parents 273a763 + 34ed09a commit a7a584c

File tree

8 files changed

+377
-28
lines changed

8 files changed

+377
-28
lines changed

Cargo.lock

Lines changed: 119 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ rusqlite = { version = "0.31", features = ["bundled"] }
4343

4444
# Memory-mapped I/O (Sprint 6.6)
4545
memmap2 = "0.9"
46-
bincode = "1.3"
46+
47+
# Zero-copy serialization (rkyv migration)
48+
rkyv = { version = "0.8.14", features = ["std", "alloc"] }
4749

4850
# CSV export (promote to workspace)
4951
csv = "1.3"

crates/prtip-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ rand = { workspace = true }
3030
sysinfo = { workspace = true }
3131
flate2 = "1.0"
3232
dirs = "5.0"
33+
rkyv = { workspace = true }
3334

3435
[dev-dependencies]
3536
tokio = { workspace = true }

crates/prtip-core/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,7 @@ pub use resource_monitor::{
136136
pub use retry::{retry_with_backoff, RetryConfig};
137137
pub use service_db::{ServiceMatch, ServiceProbe, ServiceProbeDb};
138138
pub use templates::{ScanTemplate, TemplateManager};
139-
pub use types::{PortRange, PortState, Protocol, ScanResult, ScanTarget, ScanType, TimingTemplate};
139+
pub use types::{
140+
PortRange, PortState, Protocol, ScanResult, ScanResultRkyv, ScanTarget, ScanType,
141+
TimingTemplate,
142+
};

crates/prtip-core/src/types.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,128 @@ impl fmt::Display for ScanResult {
592592
}
593593
}
594594

595+
/// rkyv-compatible serialization format for ScanResult
596+
///
597+
/// This type is optimized for zero-copy deserialization using rkyv.
598+
/// It stores all data in a format that can be directly interpreted from
599+
/// memory-mapped files without allocation.
600+
///
601+
/// # Alignment Requirements
602+
///
603+
/// This structure must maintain proper alignment for rkyv's zero-copy
604+
/// deserialization. The fixed-size entry buffer (512 bytes) provides
605+
/// adequate alignment for typical rkyv requirements.
606+
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
607+
#[rkyv(derive(Debug))]
608+
pub struct ScanResultRkyv {
609+
/// Target IP address (16 bytes for IPv6 compatibility)
610+
pub target_ip_bytes: [u8; 16],
611+
/// Whether the IP is IPv4 (true) or IPv6 (false)
612+
pub is_ipv4: bool,
613+
/// Port number
614+
pub port: u16,
615+
/// Port state as u8 (Open=0, Closed=1, Filtered=2, Unknown=3)
616+
pub state: u8,
617+
/// Response time in nanoseconds (u64 to avoid truncation)
618+
pub response_time_nanos: u64,
619+
/// Timestamp in nanoseconds since Unix epoch
620+
pub timestamp_nanos: i64,
621+
/// Optional banner (max 128 bytes)
622+
pub banner: Option<String>,
623+
/// Optional service name (max 32 bytes)
624+
pub service: Option<String>,
625+
/// Optional service version (max 64 bytes)
626+
pub version: Option<String>,
627+
/// Optional raw response (limited to 256 bytes to fit in entry)
628+
pub raw_response: Option<Vec<u8>>,
629+
}
630+
631+
impl From<&ScanResult> for ScanResultRkyv {
632+
fn from(result: &ScanResult) -> Self {
633+
// Convert IpAddr to bytes
634+
let (target_ip_bytes, is_ipv4) = match result.target_ip {
635+
IpAddr::V4(ipv4) => {
636+
let mut bytes = [0u8; 16];
637+
bytes[..4].copy_from_slice(&ipv4.octets());
638+
(bytes, true)
639+
}
640+
IpAddr::V6(ipv6) => (ipv6.octets(), false),
641+
};
642+
643+
// Convert PortState to u8
644+
let state = match result.state {
645+
PortState::Open => 0,
646+
PortState::Closed => 1,
647+
PortState::Filtered => 2,
648+
PortState::Unknown => 3,
649+
};
650+
651+
// Convert response time to u64 nanoseconds (avoid truncation issues)
652+
// Note: u64 can represent up to ~584 years, which is more than sufficient
653+
// for network response times. We clamp to u64::MAX to avoid overflow.
654+
let response_time_nanos = result.response_time.as_nanos().min(u64::MAX as u128) as u64;
655+
656+
// Convert timestamp with proper error handling
657+
let timestamp_nanos = result
658+
.timestamp
659+
.timestamp_nanos_opt()
660+
.expect("timestamp out of range for nanosecond representation");
661+
662+
Self {
663+
target_ip_bytes,
664+
is_ipv4,
665+
port: result.port,
666+
state,
667+
response_time_nanos,
668+
timestamp_nanos,
669+
banner: result.banner.clone(),
670+
service: result.service.clone(),
671+
version: result.version.clone(),
672+
raw_response: result.raw_response.clone(),
673+
}
674+
}
675+
}
676+
677+
impl From<ScanResultRkyv> for ScanResult {
678+
fn from(rkyv: ScanResultRkyv) -> Self {
679+
// Convert bytes back to IpAddr
680+
let target_ip = if rkyv.is_ipv4 {
681+
let mut octets = [0u8; 4];
682+
octets.copy_from_slice(&rkyv.target_ip_bytes[..4]);
683+
IpAddr::V4(std::net::Ipv4Addr::from(octets))
684+
} else {
685+
IpAddr::V6(std::net::Ipv6Addr::from(rkyv.target_ip_bytes))
686+
};
687+
688+
// Convert u8 back to PortState
689+
let state = match rkyv.state {
690+
0 => PortState::Open,
691+
1 => PortState::Closed,
692+
2 => PortState::Filtered,
693+
_ => PortState::Unknown,
694+
};
695+
696+
// Convert u64 nanoseconds back to Duration
697+
// Safe: u64::MAX nanoseconds fits within Duration's range
698+
let response_time = Duration::from_nanos(rkyv.response_time_nanos);
699+
700+
// Convert i64 nanoseconds back to DateTime
701+
let timestamp = DateTime::from_timestamp_nanos(rkyv.timestamp_nanos);
702+
703+
Self {
704+
target_ip,
705+
port: rkyv.port,
706+
state,
707+
response_time,
708+
timestamp,
709+
banner: rkyv.banner,
710+
service: rkyv.service,
711+
version: rkyv.version,
712+
raw_response: rkyv.raw_response,
713+
}
714+
}
715+
}
716+
595717
/// Port filtering for exclusion/inclusion lists
596718
///
597719
/// Provides efficient port filtering using hash sets for O(1) lookups.

crates/prtip-scanner/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ pcap-file = "2.0"
8787

8888
# Memory-mapped I/O
8989
memmap2 = { workspace = true }
90-
bincode = { workspace = true }
90+
rkyv = { workspace = true }
9191

9292
[dev-dependencies]
9393
tokio = { workspace = true }

0 commit comments

Comments
 (0)