fxcp_core/operations/napi.rs
1// SPDX-License-Identifier: GPL-2.0-or-later
2// Copyright (C) 2025 Joel Wirāmu Pauling <aenertia@aenertia.net>
3//
4//! io_uring NAPI busy-poll registration for NFS-target rings.
5//!
6//! NAPI busy-polling reduces network completion latency by having the kernel
7//! poll NIC receive queues directly from `io_uring_enter()`, avoiding the
8//! interrupt->softirq->wakeup path. This is particularly beneficial for NFS
9//! targets where each copy operation involves multiple RPC round-trips.
10//!
11//! **Kernel requirements:** Linux 6.9+, `CONFIG_NET_RX_BUSY_POLL=y`.
12//! The wrapper degrades gracefully on older kernels (returns `Err(ENOSYS)`).
13//!
14//! Uses the `io-uring` 0.7.13 crate's native `Submitter::register_napi` API
15//! (`io_uring::types::Napi`), which wraps `IORING_REGISTER_NAPI` (opcode 27).
16
17use std::io;
18
19use io_uring::IoUring;
20use io_uring::types::Napi;
21use tracing::{debug, warn};
22
23/// Default busy-poll timeout in microseconds.
24///
25/// 50us is long enough to absorb NFS completion jitter without burning
26/// excessive CPU on idle rings. Empirically a good balance for 1GbE-25GbE
27/// NFS targets.
28pub const DEFAULT_NAPI_BUSY_POLL_TO_US: u32 = 50;
29
30/// Register NAPI busy-poll parameters with an io_uring ring.
31///
32/// Configures the kernel to busy-poll NIC NAPI queues when waiting for
33/// completions on this ring, reducing tail latency for network I/O (NFS).
34///
35/// # Arguments
36///
37/// * `ring` -- The io_uring instance to configure.
38/// * `busy_poll_to_us` -- Busy-poll timeout in microseconds. Use
39/// [`DEFAULT_NAPI_BUSY_POLL_TO_US`] (50us) for NFS targets.
40/// * `prefer_busy_poll` -- If `true`, the kernel prefers busy-polling over
41/// interrupt-driven completion. Set `true` for dedicated NFS copy rings.
42///
43/// # Errors
44///
45/// * `ENOSYS` -- Kernel does not support io_uring NAPI (< 6.9 or
46/// `CONFIG_NET_RX_BUSY_POLL` disabled). Callers should treat this as a
47/// non-fatal degradation.
48/// * `EINVAL` -- Invalid parameters (should not occur with valid inputs).
49pub fn register_napi_with_ring(
50 ring: &IoUring,
51 busy_poll_to_us: u32,
52 prefer_busy_poll: bool,
53) -> io::Result<()> {
54 let mut napi = Napi::new()
55 .set_busy_poll_timeout(busy_poll_to_us)
56 .set_prefer_busy_poll(prefer_busy_poll);
57
58 let result = ring.submitter().register_napi(&mut napi);
59
60 match &result {
61 Ok(()) => {
62 debug!(
63 busy_poll_to_us,
64 prefer_busy_poll,
65 "io_uring NAPI busy-poll registered"
66 );
67 }
68 Err(e) if e.raw_os_error() == Some(libc::ENOSYS) => {
69 debug!("io_uring NAPI not supported by kernel (ENOSYS), continuing without busy-poll");
70 }
71 Err(e) => {
72 warn!(error = %e, "io_uring NAPI registration failed");
73 }
74 }
75
76 result
77}
78
79/// Unregister NAPI busy-poll from an io_uring ring.
80///
81/// Disables busy-polling previously configured via [`register_napi_with_ring`].
82/// The kernel writes the previous settings back into the `Napi` struct before
83/// clearing them.
84///
85/// # Errors
86///
87/// * `ENOSYS` -- Kernel does not support NAPI (non-fatal).
88/// * `EINVAL` -- NAPI was not registered on this ring.
89pub fn unregister_napi_from_ring(ring: &IoUring) -> io::Result<()> {
90 let mut napi = Napi::new();
91 let result = ring.submitter().unregister_napi(&mut napi);
92
93 match &result {
94 Ok(()) => {
95 debug!(
96 prev_timeout_us = napi.busy_poll_timeout(),
97 prev_prefer = napi.prefer_busy_poll(),
98 "io_uring NAPI busy-poll unregistered"
99 );
100 }
101 Err(e) if e.raw_os_error() == Some(libc::ENOSYS) => {
102 debug!("io_uring NAPI unregister: not supported (ENOSYS)");
103 }
104 Err(e) => {
105 warn!(error = %e, "io_uring NAPI unregister failed");
106 }
107 }
108
109 result
110}
111
112/// Attempt to register NAPI busy-poll, treating `ENOSYS` as success.
113///
114/// Convenience wrapper for callers that want fire-and-forget semantics:
115/// returns `Ok(true)` if NAPI was registered, `Ok(false)` if the kernel
116/// doesn't support it, and `Err` only on unexpected failures.
117pub fn try_register_napi(
118 ring: &IoUring,
119 busy_poll_to_us: u32,
120 prefer_busy_poll: bool,
121) -> io::Result<bool> {
122 match register_napi_with_ring(ring, busy_poll_to_us, prefer_busy_poll) {
123 Ok(()) => Ok(true),
124 Err(e) if e.raw_os_error() == Some(libc::ENOSYS) => Ok(false),
125 Err(e) => Err(e),
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
132 use super::*;
133
134 #[test]
135 fn napi_default_timeout_is_50us() {
136 assert_eq!(DEFAULT_NAPI_BUSY_POLL_TO_US, 50);
137 }
138
139 #[test]
140 fn napi_builder_roundtrip() {
141 // Verify the Napi builder from io-uring 0.7.13 works as expected.
142 let napi = Napi::new()
143 .set_busy_poll_timeout(100)
144 .set_prefer_busy_poll(true);
145 assert_eq!(napi.busy_poll_timeout(), 100);
146 assert!(napi.prefer_busy_poll());
147 }
148
149 #[test]
150 fn napi_builder_defaults() {
151 let napi = Napi::new();
152 assert_eq!(napi.busy_poll_timeout(), 0);
153 assert!(!napi.prefer_busy_poll());
154 }
155
156 #[test]
157 fn try_register_handles_enosys_gracefully() {
158 // On kernels without NAPI support, try_register_napi should return Ok(false).
159 // On kernels with NAPI, it returns Ok(true).
160 // Either way, it should not panic.
161 let ring = IoUring::new(8);
162 if let Ok(ring) = ring {
163 match try_register_napi(&ring, DEFAULT_NAPI_BUSY_POLL_TO_US, true) {
164 Ok(registered) => {
165 if registered {
166 // Clean up: unregister.
167 let _ = unregister_napi_from_ring(&ring);
168 }
169 }
170 Err(e) => {
171 // EPERM is expected in unprivileged test environments
172 // where io_uring_register is restricted.
173 assert!(
174 e.raw_os_error() == Some(libc::EPERM)
175 || e.raw_os_error() == Some(libc::ENOMEM),
176 "unexpected error: {e}"
177 );
178 }
179 }
180 }
181 // If IoUring::new fails (e.g., no io_uring support), that's fine -- skip.
182 }
183}