Skip to main content

laminar_core/io_uring/
config.rs

1//! Configuration types for `io_uring`.
2
3/// Ring operation mode.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum RingMode {
6    /// Standard mode with interrupt-based completions.
7    #[default]
8    Standard,
9    /// SQPOLL mode with kernel polling thread (no syscalls under load).
10    SqPoll,
11    /// IOPOLL mode for `NVMe` devices (polls completions from device).
12    /// Cannot be used with socket operations.
13    IoPoll,
14    /// Combined SQPOLL + IOPOLL for maximum performance on `NVMe`.
15    SqPollIoPoll,
16}
17
18impl RingMode {
19    /// Returns true if SQPOLL is enabled.
20    #[must_use]
21    pub const fn uses_sqpoll(&self) -> bool {
22        matches!(self, Self::SqPoll | Self::SqPollIoPoll)
23    }
24
25    /// Returns true if IOPOLL is enabled.
26    #[must_use]
27    pub const fn uses_iopoll(&self) -> bool {
28        matches!(self, Self::IoPoll | Self::SqPollIoPoll)
29    }
30}
31
32/// Configuration for `io_uring` operations.
33#[derive(Debug, Clone)]
34pub struct IoUringConfig {
35    /// Number of submission queue entries (power of 2, typically 256-4096).
36    pub ring_entries: u32,
37    /// Ring operation mode.
38    pub mode: RingMode,
39    /// SQPOLL idle timeout in milliseconds before kernel thread sleeps.
40    /// Only used when mode uses SQPOLL.
41    pub sqpoll_idle_ms: u32,
42    /// CPU to pin the SQPOLL kernel thread to (usually same NUMA node as core).
43    /// Only used when mode uses SQPOLL.
44    pub sqpoll_cpu: Option<u32>,
45    /// Size of each registered buffer in bytes (typically 64KB).
46    pub buffer_size: usize,
47    /// Number of buffers in the registered pool.
48    pub buffer_count: usize,
49    /// Enable cooperative task running to reduce kernel-userspace transitions.
50    pub coop_taskrun: bool,
51    /// Optimize for single-threaded submission (thread-per-core model).
52    pub single_issuer: bool,
53    /// Enable direct file descriptor table for faster operations.
54    pub direct_table: bool,
55    /// Number of direct file descriptor slots.
56    pub direct_table_size: u32,
57}
58
59impl Default for IoUringConfig {
60    fn default() -> Self {
61        Self {
62            ring_entries: 256,
63            mode: RingMode::Standard,
64            sqpoll_idle_ms: 1000,
65            sqpoll_cpu: None,
66            buffer_size: 64 * 1024, // 64KB per buffer
67            buffer_count: 256,      // 256 buffers = 16MB total
68            coop_taskrun: true,
69            single_issuer: true,
70            direct_table: false,
71            direct_table_size: 256,
72        }
73    }
74}
75
76impl IoUringConfig {
77    /// Create a new builder for `IoUringConfig`.
78    #[must_use]
79    pub fn builder() -> IoUringConfigBuilder {
80        IoUringConfigBuilder::default()
81    }
82
83    /// Create configuration with automatic detection.
84    ///
85    /// Detects system capabilities and generates an optimal configuration:
86    /// - Enables SQPOLL mode on Linux 5.11+ with the io-uring feature
87    /// - Enables IOPOLL mode on Linux 5.19+ with `NVMe` storage
88    /// - Uses optimal buffer sizes based on available memory
89    ///
90    /// # Example
91    ///
92    /// ```rust,ignore
93    /// use laminar_core::io_uring::IoUringConfig;
94    ///
95    /// let config = IoUringConfig::auto();
96    /// println!("SQPOLL: {}", config.mode.uses_sqpoll());
97    /// ```
98    #[must_use]
99    pub fn auto() -> Self {
100        let caps = crate::detect::SystemCapabilities::detect();
101
102        let mode = if caps.io_uring.iopoll_supported && caps.storage.device_type.supports_iopoll() {
103            if caps.io_uring.sqpoll_supported {
104                RingMode::SqPollIoPoll
105            } else {
106                RingMode::IoPoll
107            }
108        } else if caps.io_uring.sqpoll_supported {
109            RingMode::SqPoll
110        } else {
111            RingMode::Standard
112        };
113
114        Self {
115            ring_entries: 256,
116            mode,
117            sqpoll_idle_ms: 1000,
118            sqpoll_cpu: None,
119            buffer_size: 64 * 1024,
120            buffer_count: 256,
121            coop_taskrun: caps.io_uring.coop_taskrun,
122            single_issuer: caps.io_uring.single_issuer,
123            direct_table: false,
124            direct_table_size: 256,
125        }
126    }
127
128    /// Total buffer pool size in bytes.
129    #[must_use]
130    pub const fn total_buffer_size(&self) -> usize {
131        self.buffer_size * self.buffer_count
132    }
133
134    /// Validate the configuration.
135    ///
136    /// # Errors
137    ///
138    /// Returns an error if the configuration is invalid.
139    pub fn validate(&self) -> Result<(), super::IoUringError> {
140        // Ring entries must be power of 2
141        if !self.ring_entries.is_power_of_two() {
142            return Err(super::IoUringError::InvalidConfig(format!(
143                "ring_entries must be power of 2, got {}",
144                self.ring_entries
145            )));
146        }
147
148        // Must have at least 2 entries
149        if self.ring_entries < 2 {
150            return Err(super::IoUringError::InvalidConfig(
151                "ring_entries must be at least 2".to_string(),
152            ));
153        }
154
155        // Buffer size must be positive and reasonable
156        if self.buffer_size == 0 {
157            return Err(super::IoUringError::InvalidConfig(
158                "buffer_size must be positive".to_string(),
159            ));
160        }
161
162        if self.buffer_size > 16 * 1024 * 1024 {
163            return Err(super::IoUringError::InvalidConfig(
164                "buffer_size cannot exceed 16MB".to_string(),
165            ));
166        }
167
168        // Buffer count must be reasonable
169        if self.buffer_count == 0 {
170            return Err(super::IoUringError::InvalidConfig(
171                "buffer_count must be positive".to_string(),
172            ));
173        }
174
175        if self.buffer_count > 65536 {
176            return Err(super::IoUringError::InvalidConfig(
177                "buffer_count cannot exceed 65536".to_string(),
178            ));
179        }
180
181        Ok(())
182    }
183}
184
185/// Builder for `IoUringConfig`.
186#[derive(Debug, Default)]
187pub struct IoUringConfigBuilder {
188    config: IoUringConfig,
189}
190
191impl IoUringConfigBuilder {
192    /// Set the number of ring entries.
193    #[must_use]
194    pub const fn ring_entries(mut self, entries: u32) -> Self {
195        self.config.ring_entries = entries;
196        self
197    }
198
199    /// Set the ring mode.
200    #[must_use]
201    pub const fn mode(mut self, mode: RingMode) -> Self {
202        self.config.mode = mode;
203        self
204    }
205
206    /// Enable SQPOLL mode with the specified idle timeout.
207    #[must_use]
208    pub const fn enable_sqpoll(mut self, idle_ms: u32) -> Self {
209        self.config.mode = RingMode::SqPoll;
210        self.config.sqpoll_idle_ms = idle_ms;
211        self
212    }
213
214    /// Set the CPU for the SQPOLL kernel thread.
215    #[must_use]
216    pub const fn sqpoll_cpu(mut self, cpu: u32) -> Self {
217        self.config.sqpoll_cpu = Some(cpu);
218        self
219    }
220
221    /// Enable IOPOLL mode (for `NVMe` devices).
222    #[must_use]
223    pub const fn enable_iopoll(mut self) -> Self {
224        self.config.mode = match self.config.mode {
225            RingMode::SqPoll | RingMode::SqPollIoPoll => RingMode::SqPollIoPoll,
226            _ => RingMode::IoPoll,
227        };
228        self
229    }
230
231    /// Set the size of each buffer in the pool.
232    #[must_use]
233    pub const fn buffer_size(mut self, size: usize) -> Self {
234        self.config.buffer_size = size;
235        self
236    }
237
238    /// Set the number of buffers in the pool.
239    #[must_use]
240    pub const fn buffer_count(mut self, count: usize) -> Self {
241        self.config.buffer_count = count;
242        self
243    }
244
245    /// Enable cooperative task running.
246    #[must_use]
247    pub const fn coop_taskrun(mut self, enable: bool) -> Self {
248        self.config.coop_taskrun = enable;
249        self
250    }
251
252    /// Enable single-issuer optimization.
253    #[must_use]
254    pub const fn single_issuer(mut self, enable: bool) -> Self {
255        self.config.single_issuer = enable;
256        self
257    }
258
259    /// Enable direct file descriptor table.
260    #[must_use]
261    pub const fn direct_table(mut self, size: u32) -> Self {
262        self.config.direct_table = true;
263        self.config.direct_table_size = size;
264        self
265    }
266
267    /// Build the configuration.
268    ///
269    /// # Errors
270    ///
271    /// Returns an error if the configuration is invalid.
272    pub fn build(self) -> Result<IoUringConfig, super::IoUringError> {
273        self.config.validate()?;
274        Ok(self.config)
275    }
276
277    /// Build the configuration without validation (for testing).
278    #[must_use]
279    pub fn build_unchecked(self) -> IoUringConfig {
280        self.config
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn test_default_config() {
290        let config = IoUringConfig::default();
291        assert_eq!(config.ring_entries, 256);
292        assert_eq!(config.mode, RingMode::Standard);
293        assert_eq!(config.buffer_size, 64 * 1024);
294        assert_eq!(config.buffer_count, 256);
295        assert!(config.coop_taskrun);
296        assert!(config.single_issuer);
297    }
298
299    #[test]
300    fn test_builder() {
301        let config = IoUringConfig::builder()
302            .ring_entries(512)
303            .enable_sqpoll(2000)
304            .sqpoll_cpu(4)
305            .buffer_size(128 * 1024)
306            .buffer_count(512)
307            .build()
308            .unwrap();
309
310        assert_eq!(config.ring_entries, 512);
311        assert!(config.mode.uses_sqpoll());
312        assert_eq!(config.sqpoll_idle_ms, 2000);
313        assert_eq!(config.sqpoll_cpu, Some(4));
314        assert_eq!(config.buffer_size, 128 * 1024);
315        assert_eq!(config.buffer_count, 512);
316    }
317
318    #[test]
319    fn test_ring_mode() {
320        assert!(!RingMode::Standard.uses_sqpoll());
321        assert!(!RingMode::Standard.uses_iopoll());
322
323        assert!(RingMode::SqPoll.uses_sqpoll());
324        assert!(!RingMode::SqPoll.uses_iopoll());
325
326        assert!(!RingMode::IoPoll.uses_sqpoll());
327        assert!(RingMode::IoPoll.uses_iopoll());
328
329        assert!(RingMode::SqPollIoPoll.uses_sqpoll());
330        assert!(RingMode::SqPollIoPoll.uses_iopoll());
331    }
332
333    #[test]
334    fn test_total_buffer_size() {
335        let config = IoUringConfig {
336            buffer_size: 64 * 1024,
337            buffer_count: 256,
338            ..Default::default()
339        };
340        assert_eq!(config.total_buffer_size(), 16 * 1024 * 1024); // 16MB
341    }
342
343    #[test]
344    fn test_validation_ring_entries_power_of_two() {
345        let config = IoUringConfig {
346            ring_entries: 100, // Not power of 2
347            ..Default::default()
348        };
349        assert!(config.validate().is_err());
350    }
351
352    #[test]
353    fn test_validation_buffer_size_zero() {
354        let config = IoUringConfig {
355            buffer_size: 0,
356            ..Default::default()
357        };
358        assert!(config.validate().is_err());
359    }
360
361    #[test]
362    fn test_validation_buffer_count_zero() {
363        let config = IoUringConfig {
364            buffer_count: 0,
365            ..Default::default()
366        };
367        assert!(config.validate().is_err());
368    }
369
370    #[test]
371    fn test_enable_iopoll_combines_with_sqpoll() {
372        let config = IoUringConfig::builder()
373            .enable_sqpoll(1000)
374            .enable_iopoll()
375            .build_unchecked();
376
377        assert_eq!(config.mode, RingMode::SqPollIoPoll);
378    }
379
380    #[test]
381    fn test_io_uring_config_auto() {
382        let config = IoUringConfig::auto();
383
384        // Auto config should have valid default values
385        assert_eq!(config.ring_entries, 256);
386        assert_eq!(config.buffer_size, 64 * 1024);
387        assert_eq!(config.buffer_count, 256);
388
389        // Validation should pass
390        assert!(config.validate().is_ok());
391
392        // Mode depends on platform capabilities
393        // On non-Linux or without io-uring feature, should be Standard
394        #[cfg(not(all(target_os = "linux", feature = "io-uring")))]
395        {
396            assert_eq!(config.mode, RingMode::Standard);
397        }
398    }
399}