Skip to main content

io_engine/
merge.rs

1//! # IO Merging
2//!
3//! This module provides functionality to merge multiple sequential IO requests into a single larger request.
4//!
5//! ## Overview
6//!
7//! Merging IO requests can significantly improve performance by:
8//! - Reducing the number of system calls (`io_uring_enter` or `io_submit`).
9//! - Allowing larger sequential transfers which are often more efficient for storage devices.
10//! - Reducing per-request overhead in the driver and completion handling.
11//!
12//! ## Mechanism
13//!
14//! The core component is [`MergeSubmitter`], which buffers incoming [`IOEvent`]s.
15//!
16//! - **Buffering**: Events are added to [`MergeBuffer`]. They are merged if they are:
17//!   - Sequential (contiguous offsets).
18//!   - Same IO action (Read/Write).
19//!   - Same file descriptor.
20//!   - Total size does not exceed `merge_size_limit`.
21//!
22//! - **Flushing**: When the buffer is full, the limit is reached, or `flush()` is called, the merged request is submitted.
23//!
24//! - **Sub-tasks**:
25//!   - If events are merged, a new "master" [`IOEvent`] is created covering the entire range.
26//!   - The original events are attached as `sub_tasks` (a linked list) to this master event.
27//!   - **Write**: The data from individual buffers is copied into a single large aligned buffer.
28//!   - **Read**: A large buffer is allocated for the master event. Upon completion, data is copied back to the individual event buffers.
29//!   - **Completion**: When the master event completes, it iterates over sub-tasks, sets their results (copying data for reads), and triggers their individual callbacks.
30//!
31//! ## Components
32//! - [`MergeBuffer`]: Internal buffer logic.
33//! - [`MergeSubmitter`]: Wraps a sender channel and manages the merge logic before sending.
34
35use crate::tasks::{CbArgs, IOAction, IOEvent, IOEventMerged, TaskArgs};
36use crossfire::{BlockingTxTrait, SendError};
37use embed_seglist::SegList;
38use io_buffer::Buffer;
39use rustix::io::Errno;
40use std::borrow::{Borrow, BorrowMut};
41use std::marker::PhantomData;
42use std::os::fd::RawFd;
43
44/// Info about the first event and merged state.
45struct MergedInfo<C: CbArgs> {
46    /// First event stored as Box<IOEvent> to allow reuse when merging.
47    first_event: Box<IOEvent<C>>,
48    /// Tail offset: next contiguous address that can be merged.
49    tail_offset: i64,
50    /// Total size of all events including the first.
51    total_size: usize,
52}
53
54/// Buffers sequential IO events for merging.
55///
56/// This internal component collects [`IOEvent`]s,
57/// presuming the same IO action and file descriptor (it does not check),
58/// the merge upper bound is specified in `merge_size_limit`.
59pub struct MergeBuffer<C: CbArgs> {
60    pub merge_size_limit: usize,
61    merged_info: Option<MergedInfo<C>>,
62    /// Subsequent events stored as IOEventMerged for cache-friendly storage.
63    merged_events: SegList<IOEventMerged<C>>,
64}
65
66impl<C: CbArgs> MergeBuffer<C> {
67    /// Creates a new `MergeBuffer` with the specified merge size limit.
68    ///
69    /// # Arguments
70    /// * `merge_size_limit` - The maximum total data size to produce a merged event.
71    #[inline(always)]
72    pub fn new(merge_size_limit: usize) -> Self {
73        Self { merge_size_limit, merged_info: None, merged_events: SegList::new() }
74    }
75
76    /// Checks if a new event can be added to the current buffer for merging.
77    ///
78    /// An event can be added if:
79    /// - The buffer is empty.
80    /// - The event is contiguous with the last event in the buffer.
81    /// - Adding the event does not exceed the `merge_size_limit`.
82    ///
83    /// # Arguments
84    /// * `event` - The [`IOEvent`] to check.
85    ///
86    /// # Returns
87    /// `true` if the event can be added, `false` otherwise.
88    #[inline(always)]
89    pub fn may_add_event(&mut self, event: &IOEvent<C>) -> bool {
90        if let Some(ref info) = self.merged_info {
91            if event.get_size() as usize > self.merge_size_limit {
92                return false;
93            }
94            return info.tail_offset == event.offset;
95        } else {
96            return true;
97        }
98    }
99
100    /// Pushes an event into the buffer.
101    ///
102    /// This method assumes that `may_add_event` has already been called and returned `true`.
103    /// It updates the merged data size and tracks the merged offset.
104    ///
105    /// # Arguments
106    /// * `event` - The [`IOEvent`] to push.
107    ///
108    /// # Safety
109    ///
110    /// You should always check whether event is contiguous with [Self::may_add_event] before calling `push_event()`
111    ///
112    /// # Returns
113    /// `true` if the buffer size has reached or exceeded `merge_size_limit` after adding the event, `false` otherwise.
114    #[inline(always)]
115    pub fn push_event(&mut self, event: IOEvent<C>) -> bool {
116        if let Some(ref mut info) = self.merged_info {
117            // Safety check: ensure may_add_event was called
118            debug_assert_eq!(info.tail_offset, event.offset, "push_event: event not contiguous");
119            debug_assert!(
120                info.total_size + event.get_size() as usize <= self.merge_size_limit,
121                "push_event: exceeds merge_size_limit"
122            );
123            // If this is the second event, move first event's buffer to merged_events
124            if self.merged_events.is_empty() {
125                let first_merged = info.first_event.extract_merged();
126                self.merged_events.push(first_merged);
127            }
128            // Subsequent events: convert to IOEventMerged and store in SegList
129            info.total_size += event.get_size() as usize;
130            info.tail_offset += event.get_size() as i64;
131            self.merged_events.push(event.into_merged());
132            return info.total_size >= self.merge_size_limit;
133        } else {
134            // First event: store as Box<IOEvent> for potential reuse
135            let size = event.get_size() as usize;
136            let offset = event.offset;
137            self.merged_info = Some(MergedInfo {
138                first_event: Box::new(event),
139                tail_offset: offset + size as i64,
140                total_size: size,
141            });
142            return size >= self.merge_size_limit;
143        }
144    }
145
146    /// Returns the number of events currently in the buffer.
147    #[inline(always)]
148    pub fn len(&self) -> usize {
149        if !self.merged_events.is_empty() {
150            // First event buffer moved to merged_events, count is merged_events.len()
151            self.merged_events.len()
152        } else {
153            // Single event or empty
154            self.merged_info.as_ref().map(|_| 1).unwrap_or(0)
155        }
156    }
157
158    /// Takes all buffered events, building merged buffer if needed.
159    ///
160    /// - On success, Returns the master event (Box<IOEvent>) or None if empty;
161    /// - On failure, return merged event SegList
162    #[inline(always)]
163    fn take(
164        &mut self, action: IOAction,
165    ) -> Result<Option<Box<IOEvent<C>>>, SegList<IOEventMerged<C>>> {
166        if let Some(info) = self.merged_info.take() {
167            // Single event: return directly without mem::replace
168            if self.merged_events.is_empty() {
169                return Ok(Some(info.first_event));
170            }
171
172            // Multiple events: take merged_events and build merged buffer
173            let sub_tasks = std::mem::replace(&mut self.merged_events, SegList::new());
174            debug_assert!(sub_tasks.len() > 1);
175            let size = info.total_size;
176            match Buffer::aligned(size as i32) {
177                Ok(mut buffer) => {
178                    if action == IOAction::Write {
179                        let mut write_offset = 0;
180                        for merged in sub_tasks.iter() {
181                            buffer.copy_from(write_offset, merged.buf.as_ref());
182                            write_offset += merged.buf.len();
183                        }
184                    }
185
186                    // Reuse first_event as master, set merged buffer and subtasks
187                    let mut master = info.first_event;
188                    master.set_merged_tasks(buffer, sub_tasks);
189                    Ok(Some(master))
190                }
191                Err(_) => Err(sub_tasks),
192            }
193        } else {
194            Ok(None)
195        }
196    }
197
198    /// Flushes the buffered events, potentially merging them into a single [`IOEvent`].
199    ///
200    /// This method handles different scenarios based on the number of events in the buffer:
201    /// - If the buffer is empty, it returns `None`.
202    /// - If there is a single event, it returns `Some(event)` with the original event.
203    /// - If there are multiple events, it attempts to merge them:
204    ///   - If successful, reuses the first `Box<IOEvent>` as the master event, replacing its buffer.
205    ///   - If buffer allocation for the merged event fails, all original events are marked with an `NOMEM` error and their callbacks are triggered, then `None` is returned.
206    /// - This function will always override fd in IOEvent with argument
207    ///
208    /// After flushing, the buffer is reset.
209    ///
210    /// # Arguments
211    /// * `fd` - The raw file descriptor associated with the IO operations.
212    /// * `action` - The IO action (Read/Write) for the events.
213    ///
214    /// # Returns
215    /// - On success, an `Option<IOEvent<C>>` representing the merged event, a single original event, or `None` if the buffer was empty or merging failed.
216    /// - On failure, (no memory), will process the merged event CbArgs with on_failure, and return
217    ///   `None`
218    #[inline]
219    pub fn flush<F, B>(
220        &mut self, fd: RawFd, action: IOAction, on_failure: B,
221    ) -> Option<Box<IOEvent<C>>>
222    where
223        B: Borrow<F>,
224        F: Fn(C, Errno),
225    {
226        match self.take(action) {
227            Ok(Some(mut event)) => {
228                event.set_fd(fd);
229                Some(event)
230            }
231            Ok(None) => None,
232            Err(sub_tasks) => {
233                // Allocation failed: error out all events
234                for merged in sub_tasks {
235                    if let Some(args) = merged.args {
236                        (on_failure.borrow())(args, Errno::NOMEM);
237                    }
238                }
239                None
240            }
241        }
242    }
243}
244
245/// Manages the submission of IO events, attempting to merge sequential events
246/// before sending them to the IO driver.
247///
248/// This component buffers incoming [`IOEvent`]s into a [`MergeBuffer`].
249/// It ensures that events for the same file descriptor and IO action are
250/// considered for merging to optimize system calls.
251///
252/// `C`: The custom callback args for merged IOEvent
253/// `S`: The channel sender to submit IOEvent
254/// `B`: owned MergeBuffer, or reference to MergeBuffer
255/// `F`: the callback closure to process the merged event `C` on failure, allow them to capture
256///    customized parameters
257pub struct MergeSubmitter<
258    C: CbArgs,
259    S: BlockingTxTrait<Box<IOEvent<C>>>,
260    B: BorrowMut<MergeBuffer<C>>,
261    F: Fn(C, Errno),
262> {
263    fd: RawFd,
264    buffer: B,
265    sender: S,
266    action: IOAction,
267    on_failure: F,
268    _phan: PhantomData<fn(&C)>,
269}
270
271impl<C, S, F> MergeSubmitter<C, S, MergeBuffer<C>, F>
272where
273    C: CbArgs,
274    S: BlockingTxTrait<Box<IOEvent<C>>>,
275    F: Fn(C, Errno),
276{
277    /// Creates a new `MergeSubmitter`.
278    ///
279    /// # Arguments
280    /// * `fd` - The raw file descriptor for IO operations.
281    /// * `sender` - A channel sender to send prepared [`IOEvent`]s to the IO driver.
282    /// * `merge_size_limit` - The maximum data size for a merged event buffer.
283    /// * `action` - The primary IO action (Read/Write) for this submitter.
284    #[inline]
285    pub fn new(
286        fd: RawFd, sender: S, merge_size_limit: usize, action: IOAction, on_failure: F,
287    ) -> Self {
288        log_assert!(merge_size_limit > 0);
289        Self {
290            fd,
291            sender,
292            action,
293            buffer: MergeBuffer::<C>::new(merge_size_limit),
294            on_failure,
295            _phan: Default::default(),
296        }
297    }
298}
299
300impl<C, S, B, F> MergeSubmitter<C, S, B, F>
301where
302    C: CbArgs,
303    S: BlockingTxTrait<Box<IOEvent<C>>>,
304    B: BorrowMut<MergeBuffer<C>>,
305    F: Fn(C, Errno),
306{
307    #[inline]
308    pub fn with_buffer(fd: RawFd, sender: S, action: IOAction, buffer: B, on_failure: F) -> Self {
309        Self { fd, sender, action, buffer, _phan: Default::default(), on_failure }
310    }
311
312    /// Adds an [`IOEvent`] to the internal buffer, potentially triggering a flush.
313    ///
314    /// If the event cannot be merged with current buffered events (e.g., non-contiguous,
315    /// exceeding merge limit), the existing buffered events are flushed first.
316    /// If adding the new event fills the buffer to its `merge_size_limit`, a flush is also triggered.
317    ///
318    /// # Arguments
319    /// * `event` - The [`IOEvent`] to add.
320    ///
321    /// # Returns
322    /// An `Ok(())` on success.
323    /// When submit sender closed, set Errno::SHUTDOWN on `event` and return the same Errno
324    /// On debug mode, will validate event.fd and event.action.
325    #[inline]
326    pub fn add_event(&mut self, event: IOEvent<C>) -> Result<(), Errno> {
327        log_debug_assert_eq!(self.fd, event.fd);
328        log_debug_assert_eq!(event.action, self.action);
329        let event_size = event.get_size();
330        let buffer = self.buffer.borrow_mut();
331
332        if event_size >= buffer.merge_size_limit as u64 || !buffer.may_add_event(&event) {
333            if let Err(e) = self._flush() {
334                if let Some(TaskArgs::Callback(args)) = event.args {
335                    (self.on_failure)(args, e);
336                }
337                return Err(e);
338            }
339        }
340        if self.buffer.borrow_mut().push_event(event) {
341            self._flush()?;
342        }
343        return Ok(());
344    }
345
346    /// Explicitly flushes any pending buffered events to the IO driver.
347    ///
348    /// # Returns
349    /// An `Ok(())` on success, or an `rustix::Errno` when sending the submit tx closed.
350    #[inline]
351    pub fn flush(&mut self) -> Result<(), Errno> {
352        self._flush()
353    }
354
355    #[inline(always)]
356    fn _flush(&mut self) -> Result<(), Errno> {
357        if let Some(event) =
358            self.buffer.borrow_mut().flush::<F, &F>(self.fd, self.action, &self.on_failure)
359        {
360            trace!("mio: submit event from flush {:?}", event);
361            if let Err(SendError(fail_event)) = self.sender.send(event) {
362                let e = Errno::SHUTDOWN;
363                if let Some(TaskArgs::Callback(args)) = fail_event.args {
364                    (self.on_failure)(args, e);
365                }
366                return Err(e);
367            }
368        }
369        Ok(())
370    }
371}