11use anyhow:: { Context , Result } ;
22use libbpf_rs:: { MapCore , RingBufferBuilder } ;
3+ use parking_lot:: Mutex ;
4+ use std:: sync:: Arc ;
35use std:: sync:: mpsc:: { self , RecvTimeoutError , Sender } ;
46use std:: thread:: JoinHandle ;
57use std:: time:: Duration ;
68
9+ /// Items buffered before a channel send. `std::sync::mpsc` allocates a block
10+ /// every 31 messages, so sending one item at a time makes that allocation
11+ /// dominate the pipeline; batching amortizes it over a whole batch.
12+ const BATCH_ITEMS : usize = 1024 ;
13+
14+ /// The lock is released before the send so a slow consumer never blocks the
15+ /// ring-buffer callback.
16+ fn flush_batch < T > ( batch : & Mutex < Vec < T > > , tx : & Sender < Vec < T > > ) {
17+ let mut buf = batch. lock ( ) ;
18+ if buf. is_empty ( ) {
19+ return ;
20+ }
21+ let items = std:: mem:: replace ( & mut * buf, Vec :: with_capacity ( BATCH_ITEMS ) ) ;
22+ drop ( buf) ;
23+ let _ = tx. send ( items) ;
24+ }
25+
26+ fn poll_iteration < T > (
27+ control : std:: result:: Result < Sender < ( ) > , RecvTimeoutError > ,
28+ consume : impl FnOnce ( ) ,
29+ poll : impl FnOnce ( ) ,
30+ batch : & Mutex < Vec < T > > ,
31+ tx : & Sender < Vec < T > > ,
32+ ) -> bool {
33+ match control {
34+ Ok ( ack) => {
35+ consume ( ) ;
36+ // `drain` promises pending entries are in the channel before returning.
37+ flush_batch ( batch, tx) ;
38+ let _ = ack. send ( ( ) ) ;
39+ true
40+ }
41+ Err ( RecvTimeoutError :: Timeout ) => {
42+ poll ( ) ;
43+ flush_batch ( batch, tx) ;
44+ true
45+ }
46+ Err ( RecvTimeoutError :: Disconnected ) => {
47+ consume ( ) ;
48+ flush_batch ( batch, tx) ;
49+ false
50+ }
51+ }
52+ }
53+
754/// Polls a BPF ring buffer in a background thread, parsing raw entries with a
8- /// user-supplied closure and forwarding them to an mpsc channel.
55+ /// user-supplied closure and forwarding them to an mpsc channel in batches .
956///
1057/// The poll thread runs until the poller is dropped, doing a final full
1158/// `consume()` on shutdown so no buffered entries are lost.
@@ -15,17 +62,36 @@ pub struct RingBufferPoller {
1562}
1663
1764impl RingBufferPoller {
18- pub fn new < M , T , F > ( rb_map : & M , parse : F , tx : Sender < T > , poll_interval_ms : u64 ) -> Result < Self >
65+ pub fn new < M , T , F > (
66+ rb_map : & M ,
67+ parse : F ,
68+ tx : Sender < Vec < T > > ,
69+ poll_interval_ms : u64 ,
70+ ) -> Result < Self >
1971 where
2072 M : MapCore ,
2173 T : Send + ' static ,
2274 F : Fn ( & [ u8 ] ) -> Option < T > + Send + ' static ,
2375 {
76+ // `Arc<Mutex<_>>` rather than `Rc<RefCell<_>>`: the built `RingBuffer` moves
77+ // into the poll thread, so the callback must be `Send`.
78+ let batch = Arc :: new ( Mutex :: new ( Vec :: with_capacity ( BATCH_ITEMS ) ) ) ;
79+ let cb_batch = Arc :: clone ( & batch) ;
80+ let cb_tx = tx. clone ( ) ;
81+
2482 let mut builder = RingBufferBuilder :: new ( ) ;
2583 builder. add ( rb_map, move |data| {
26- if let Some ( item) = parse ( data) {
27- let _ = tx. send ( item) ;
84+ let Some ( item) = parse ( data) else {
85+ return 0 ;
86+ } ;
87+ let mut buf = cb_batch. lock ( ) ;
88+ buf. push ( item) ;
89+ if buf. len ( ) < BATCH_ITEMS {
90+ return 0 ;
2891 }
92+ let items = std:: mem:: replace ( & mut * buf, Vec :: with_capacity ( BATCH_ITEMS ) ) ;
93+ drop ( buf) ;
94+ let _ = cb_tx. send ( items) ;
2995 0
3096 } ) ?;
3197 let ringbuf = builder. build ( ) ?;
@@ -35,21 +101,17 @@ impl RingBufferPoller {
35101 // poll tick, and disconnection is the shutdown signal.
36102 let ( ctl, ctl_rx) = mpsc:: channel :: < Sender < ( ) > > ( ) ;
37103 let poll_thread = std:: thread:: spawn ( move || {
38- loop {
39- match ctl_rx. recv_timeout ( Duration :: from_millis ( poll_interval_ms) ) {
40- Ok ( ack) => {
41- let _ = ringbuf. consume ( ) ;
42- let _ = ack. send ( ( ) ) ;
43- }
44- Err ( RecvTimeoutError :: Timeout ) => {
45- let _ = ringbuf. poll ( Duration :: ZERO ) ;
46- }
47- Err ( RecvTimeoutError :: Disconnected ) => {
48- let _ = ringbuf. consume ( ) ;
49- break ;
50- }
51- }
52- }
104+ while poll_iteration (
105+ ctl_rx. recv_timeout ( Duration :: from_millis ( poll_interval_ms) ) ,
106+ || {
107+ let _ = ringbuf. consume ( ) ;
108+ } ,
109+ || {
110+ let _ = ringbuf. poll ( Duration :: ZERO ) ;
111+ } ,
112+ & batch,
113+ & tx,
114+ ) { }
53115 } ) ;
54116
55117 Ok ( Self {
@@ -77,3 +139,72 @@ impl Drop for RingBufferPoller {
77139 }
78140 }
79141}
142+
143+ #[ cfg( test) ]
144+ mod tests {
145+ use super :: * ;
146+ use std:: cell:: Cell ;
147+
148+ fn partial_batch ( ) -> Vec < u8 > {
149+ vec ! [ 42 ; BATCH_ITEMS - 1 ]
150+ }
151+
152+ #[ test]
153+ fn timeout_flushes_partial_batch ( ) {
154+ let expected = partial_batch ( ) ;
155+ let batch = Mutex :: new ( expected. clone ( ) ) ;
156+ let ( tx, rx) = mpsc:: channel ( ) ;
157+ let polled = Cell :: new ( false ) ;
158+
159+ assert ! ( poll_iteration(
160+ Err ( RecvTimeoutError :: Timeout ) ,
161+ || unreachable!( ) ,
162+ || polled. set( true ) ,
163+ & batch,
164+ & tx,
165+ ) ) ;
166+
167+ assert ! ( polled. get( ) ) ;
168+ assert_eq ! ( rx. recv( ) . unwrap( ) , expected) ;
169+ }
170+
171+ #[ test]
172+ fn drain_flushes_partial_batch ( ) {
173+ let expected = partial_batch ( ) ;
174+ let batch = Mutex :: new ( expected. clone ( ) ) ;
175+ let ( tx, rx) = mpsc:: channel ( ) ;
176+ let ( ack_tx, ack_rx) = mpsc:: channel ( ) ;
177+ let consumed = Cell :: new ( false ) ;
178+
179+ assert ! ( poll_iteration(
180+ Ok ( ack_tx) ,
181+ || consumed. set( true ) ,
182+ || unreachable!( ) ,
183+ & batch,
184+ & tx,
185+ ) ) ;
186+
187+ assert ! ( consumed. get( ) ) ;
188+ assert_eq ! ( rx. recv( ) . unwrap( ) , expected) ;
189+ ack_rx. recv ( ) . unwrap ( ) ;
190+ }
191+
192+ #[ test]
193+ fn shutdown_flushes_partial_batch ( ) {
194+ let expected = partial_batch ( ) ;
195+ let batch = Mutex :: new ( expected. clone ( ) ) ;
196+ let ( tx, rx) = mpsc:: channel ( ) ;
197+ let consumed = Cell :: new ( false ) ;
198+
199+ assert ! ( !poll_iteration(
200+ Err ( RecvTimeoutError :: Disconnected ) ,
201+ || consumed. set( true ) ,
202+ || unreachable!( ) ,
203+ & batch,
204+ & tx,
205+ ) ) ;
206+
207+ assert ! ( consumed. get( ) ) ;
208+ assert_eq ! ( rx. recv( ) . unwrap( ) , expected) ;
209+ }
210+ }
0 commit comments