Skip to main content

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}