Skip to main content

Producer

Struct Producer 

Source
pub struct Producer<T> { /* private fields */ }
Expand description

Producer handle. Move it to the producing thread; the type system enforces the SPSC invariant.

Implementations§

Source§

impl<T: Copy> Producer<T>

Source

pub fn try_enqueue_bulk(&mut self, values: &[T]) -> usize

Copy as many items from values into the ring as will fit right now. Returns the count transferred. Single Release on the tail at the end.

Examples found in repository?
examples/sample_app.rs (line 126)
116fn bulk_batch_ingest() {
117    println!("\n== bulk: batch a NIC receive into the ring ==");
118    let (mut tx, mut rx) = SpscRingBuffer::with_capacity::<Tick>(16);
119    let batch: Vec<Tick> = (0..10)
120        .map(|seq| Tick {
121            seq,
122            price_cents: 20_000 + seq as u32,
123        })
124        .collect();
125
126    let pushed = tx.try_enqueue_bulk(&batch);
127    println!(
128        "  offered {} ticks, took {pushed} in one fenced call",
129        batch.len()
130    );
131    assert_eq!(pushed, 10);
132
133    let mut out = [Tick {
134        seq: 0,
135        price_cents: 0,
136    }; 10];
137    let drained = rx.try_dequeue_bulk(&mut out);
138    println!("  drained {drained} in one fenced call");
139    assert_eq!(drained, 10);
140    assert_eq!(out.as_slice(), batch.as_slice(), "bulk preserves order");
141}
More examples
Hide additional examples
examples/perf_features.rs (line 230)
189fn main() -> io::Result<()> {
190    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
191        .join("..")
192        .join(".subms")
193        .join("features")
194        .join("rust.json");
195    let existing = std::fs::read_to_string(&path).unwrap_or_default();
196    let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
197    // Stamp the box these numbers came from. The bench runs wherever it is
198    // invoked, so an unstamped manifest is indistinguishable from a fleet
199    // capture; the renderer will not publish one it cannot attribute.
200    let (source, instance) = SubMsP99Source::from_env();
201    manifest.set_p99_source(source, instance.as_deref());
202
203    // The baseline: the base wait-free push + pop round trip. Swept as well as
204    // sampled, because whether ring capacity moves the BASE op is the context
205    // every feature curve is read against.
206    let base_sweep = sweep("base/push+pop", |cap| {
207        let (mut tx, mut rx) = pair(cap);
208        batched(ITEMS_PER_SAMPLE, |i| {
209            let _ = tx.try_push(i as u64);
210            rx.try_pop().unwrap_or(0)
211        })
212    });
213    let base_p50 = base_sweep
214        .iter()
215        .find(|(n, _)| *n == CANON)
216        .map_or(0, |(_, v)| *v);
217    eprintln!("base push+pop p50 per {ITEMS_PER_SAMPLE}-item sample: {base_p50}ns");
218
219    // ---------- bulk: one fence per BULK_BATCH items ----------
220    #[cfg(feature = "bulk")]
221    {
222        // A sample moves ITEMS_PER_SAMPLE items either way; only the call width
223        // differs. That is the comparison the feature exists to win, and it is
224        // why the reps count is divided rather than the batch grown.
225        let sw = sweep("bulk/enqueue+dequeue", |cap| {
226            let (mut tx, mut rx) = pair(cap);
227            let batch = [0u64; BULK_BATCH];
228            let mut out = [0u64; BULK_BATCH];
229            batched(ITEMS_PER_SAMPLE / BULK_BATCH, |_| {
230                let n = tx.try_enqueue_bulk(&batch);
231                (n + rx.try_dequeue_bulk(&mut out)) as u64
232            })
233        });
234        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
235
236        let batch = [0u64; BULK_BATCH];
237        let mut out = [0u64; BULK_BATCH];
238        let (mut tx, mut rx) = pair(CANON);
239        let enq = single(
240            |_| tx.try_enqueue_bulk(&batch) as u64,
241            |_| rx.try_dequeue_bulk(&mut out) as u64,
242        );
243        let (mut tx, mut rx) = pair(CANON);
244        let deq = single(
245            |_| rx.try_dequeue_bulk(&mut out) as u64,
246            |_| tx.try_enqueue_bulk(&batch) as u64,
247        );
248        let mut p99 = BTreeMap::new();
249        p99.insert("enqueue_bulk".to_string(), enq);
250        p99.insert("dequeue_bulk".to_string(), deq);
251        manifest.set_feature("bulk", cat, &p99, &reason);
252    }
253
254    // ---------- wait-strategies: blocking wrappers, fast path only ----------
255    #[cfg(feature = "wait-strategies")]
256    {
257        use subms_spsc_ring_buffer::{
258            BlockingSpscConsumer, BlockingSpscProducer, BusySpin, ParkStrategy,
259        };
260        // Swept on BusySpin, whose `signal()` is a no-op, so the curve is the
261        // wrapper's own overhead over the base ring and nothing else. The ring
262        // is never full or empty, so `wait()` is never called; the parked-wakeup
263        // path is a scheduler latency, not a per-op cost, and is not measured.
264        let sw = sweep("wait-strategies/push+pop", |cap| {
265            let (tx, rx) = pair(cap);
266            let mut p = BlockingSpscProducer::new(tx, BusySpin);
267            let mut c = BlockingSpscConsumer::new(rx, BusySpin);
268            batched(ITEMS_PER_SAMPLE, |i| {
269                p.push(i as u64);
270                c.pop()
271            })
272        });
273        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
274
275        // ParkStrategy on the SAME ready ring, printed rather than classified.
276        // Its `signal()` fires on every successful op even when nothing is
277        // parked, and it takes a lock to do it, so the strategy choice shows up
278        // here on the fast path - not in `wait()`, which this never enters.
279        let park_batched = best(|| {
280            let (ps, cs) = ParkStrategy::pair();
281            let (tx, rx) = pair(CANON);
282            let mut p = BlockingSpscProducer::new(tx, ps);
283            let mut c = BlockingSpscConsumer::new(rx, cs);
284            batched(ITEMS_PER_SAMPLE, |i| {
285                p.push(i as u64);
286                c.pop()
287            })
288        });
289        eprintln!(
290            "wait-strategies park fast path at {CANON}: {park_batched}ns per \
291             {ITEMS_PER_SAMPLE}-item sample (spin {}ns) - signal() takes a lock per op",
292            sw.iter().find(|(n, _)| *n == CANON).map_or(0, |(_, v)| *v)
293        );
294
295        let (tx, rx) = pair(CANON);
296        let mut p = BlockingSpscProducer::new(tx, BusySpin);
297        let mut c = BlockingSpscConsumer::new(rx, BusySpin);
298        let push_spin = single(
299            |i| {
300                p.push(i as u64);
301                0
302            },
303            |_| c.pop(),
304        );
305        let (tx, rx) = pair(CANON);
306        let mut p = BlockingSpscProducer::new(tx, BusySpin);
307        let mut c = BlockingSpscConsumer::new(rx, BusySpin);
308        let pop_spin = single(
309            |_| c.pop(),
310            |i| {
311                p.push(i as u64);
312                0
313            },
314        );
315
316        let (ps, cs) = ParkStrategy::pair();
317        let (tx, rx) = pair(CANON);
318        let mut p = BlockingSpscProducer::new(tx, ps);
319        let mut c = BlockingSpscConsumer::new(rx, cs);
320        let push_park = single(
321            |i| {
322                p.push(i as u64);
323                0
324            },
325            |_| c.pop(),
326        );
327        let (ps, cs) = ParkStrategy::pair();
328        let (tx, rx) = pair(CANON);
329        let mut p = BlockingSpscProducer::new(tx, ps);
330        let mut c = BlockingSpscConsumer::new(rx, cs);
331        let pop_park = single(
332            |_| c.pop(),
333            |i| {
334                p.push(i as u64);
335                0
336            },
337        );
338
339        let mut p99 = BTreeMap::new();
340        p99.insert("push_spin".to_string(), push_spin);
341        p99.insert("pop_spin".to_string(), pop_spin);
342        p99.insert("push_park".to_string(), push_park);
343        p99.insert("pop_park".to_string(), pop_park);
344        manifest.set_feature("wait-strategies", cat, &p99, &reason);
345    }
346
347    // ---------- mpsc-fan-in: N rings, one round-robin consumer ----------
348    #[cfg(feature = "mpsc-fan-in")]
349    {
350        use subms_spsc_ring_buffer::{MpscFanIn, MpscFanInConsumer, MpscFanInProducer};
351        /// Producer count is held FIXED across the sweep. Moving it would sweep
352        /// the fan-in width, which sets the consumer's probe loop, not the ring.
353        const PRODUCERS: usize = 4;
354
355        fn fanin(cap: usize) -> (Vec<MpscFanInProducer<u64>>, MpscFanInConsumer<u64>) {
356            let (mut ps, c) = MpscFanIn::with_capacity::<u64>(PRODUCERS, cap);
357            for p in ps.iter_mut() {
358                for i in 0..cap / 2 {
359                    let _ = p.try_push(i as u64);
360                }
361            }
362            (ps, c)
363        }
364
365        // Pushes round-robin and the consumer cursor advances one ring per pop,
366        // so the two stay in step and every ring holds a constant half load.
367        // With every ring non-empty the consumer's probe hits on its first try,
368        // which is the steady-state shape; a starved fan-in probes all N and that
369        // is a different measurement.
370        let sw = sweep("mpsc-fan-in/push+pop", |cap| {
371            let (mut ps, mut c) = fanin(cap);
372            batched(ITEMS_PER_SAMPLE, |i| {
373                let _ = ps[i % PRODUCERS].try_push(i as u64);
374                c.try_pop().unwrap_or(0)
375            })
376        });
377        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
378
379        let (mut ps, mut c) = fanin(CANON);
380        let enq = single(
381            |i| {
382                let _ = ps[i % PRODUCERS].try_push(i as u64);
383                0
384            },
385            |_| c.try_pop().unwrap_or(0),
386        );
387        let (mut ps, mut c) = fanin(CANON);
388        let deq = single(
389            |_| c.try_pop().unwrap_or(0),
390            |i| {
391                let _ = ps[i % PRODUCERS].try_push(i as u64);
392                0
393            },
394        );
395        let mut p99 = BTreeMap::new();
396        p99.insert("fanin_enqueue".to_string(), enq);
397        p99.insert("fanin_dequeue".to_string(), deq);
398        manifest.set_feature("mpsc-fan-in", cat, &p99, &reason);
399    }
400
401    // ---------- mpmc-disruptor: CAS claim + sequence barrier ----------
402    #[cfg(feature = "mpmc-disruptor")]
403    {
404        use subms_spsc_ring_buffer::{DisruptorConsumer, DisruptorProducer, MpmcDisruptor};
405        /// One consumer, held fixed. `try_publish` scans every consumer cursor
406        /// before claiming, so the consumer count - not the capacity - is what
407        /// that loop is O(). Sweeping it would answer a different question.
408        const CONSUMERS: usize = 1;
409
410        fn disruptor(cap: usize) -> (DisruptorProducer<u64>, DisruptorConsumer<u64>) {
411            let (p, mut cs) = MpmcDisruptor::with_consumers::<u64>(cap, CONSUMERS);
412            let c = cs.remove(0);
413            for i in 0..cap / 2 {
414                let _ = p.try_publish(i as u64);
415            }
416            (p, c)
417        }
418
419        // Half a ring of published-but-unconsumed items keeps the producer clear
420        // of the gating spin (it only fires within one capacity of the slowest
421        // consumer) and the consumer clear of the unpublished early return.
422        let sw = sweep("mpmc-disruptor/publish+consume", |cap| {
423            let (p, mut c) = disruptor(cap);
424            batched(ITEMS_PER_SAMPLE, |i| {
425                let _ = p.try_publish(i as u64);
426                c.try_consume().unwrap_or(0)
427            })
428        });
429        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
430
431        let (p, mut c) = disruptor(CANON);
432        let published = single(
433            |i| {
434                let _ = p.try_publish(i as u64);
435                0
436            },
437            |_| c.try_consume().unwrap_or(0),
438        );
439        let (p, mut c) = disruptor(CANON);
440        let consumed = single(
441            |_| c.try_consume().unwrap_or(0),
442            |i| {
443                let _ = p.try_publish(i as u64);
444                0
445            },
446        );
447        let mut p99 = BTreeMap::new();
448        p99.insert("publish".to_string(), published);
449        p99.insert("consume".to_string(), consumed);
450        manifest.set_feature("mpmc-disruptor", cat, &p99, &reason);
451    }
452
453    // ---------- metrics: counters on the push / pop path ----------
454    #[cfg(feature = "metrics")]
455    {
456        use subms_spsc_ring_buffer::InstrumentedSpsc;
457
458        // The wrapper adds a `fetch_add` per op plus, on the producer side, a
459        // high-water-mark update. `local_depth` only ever increments - a producer
460        // cannot observe pops - so that update takes its CAS on every push rather
461        // than settling once the mark stops moving. The counter cost on the push
462        // path is two read-modify-writes, not one.
463        let sw = sweep("metrics/push+pop", |cap| {
464            let (tx, rx) = pair(cap);
465            let (mut tx, mut rx, _m) = InstrumentedSpsc::wrap(tx, rx);
466            batched(ITEMS_PER_SAMPLE, |i| {
467                let _ = tx.try_push(i as u64);
468                rx.try_pop().unwrap_or(0)
469            })
470        });
471        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
472
473        let (tx, rx) = pair(CANON);
474        let (mut tx, mut rx, _m) = InstrumentedSpsc::wrap(tx, rx);
475        let enq = single(
476            |i| {
477                let _ = tx.try_push(i as u64);
478                0
479            },
480            |_| rx.try_pop().unwrap_or(0),
481        );
482        let (tx, rx) = pair(CANON);
483        let (mut tx, mut rx, _m) = InstrumentedSpsc::wrap(tx, rx);
484        let deq = single(
485            |_| rx.try_pop().unwrap_or(0),
486            |i| {
487                let _ = tx.try_push(i as u64);
488                0
489            },
490        );
491        let mut p99 = BTreeMap::new();
492        p99.insert("metrics_enqueue".to_string(), enq);
493        p99.insert("metrics_dequeue".to_string(), deq);
494        manifest.set_feature("metrics", cat, &p99, &reason);
495    }
496
497    std::fs::create_dir_all(path.parent().unwrap())?;
498    std::fs::write(&path, manifest.to_json())?;
499    io::stdout().write_all(manifest.to_json().as_bytes())?;
500    Ok(())
501}
Source§

impl<T> Producer<T>

Source

pub fn try_push(&mut self, value: T) -> Result<(), T>

Push a value. Returns the input back as Err(value) if the buffer is full. Wait-free: at most one atomic load + one atomic store.

Examples found in repository?
examples/perf_features.rs (line 184)
181fn pair(cap: usize) -> (Producer<u64>, Consumer<u64>) {
182    let (mut tx, rx) = SpscRingBuffer::with_capacity::<u64>(cap);
183    for i in 0..cap / 2 {
184        let _ = tx.try_push(i as u64);
185    }
186    (tx, rx)
187}
188
189fn main() -> io::Result<()> {
190    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
191        .join("..")
192        .join(".subms")
193        .join("features")
194        .join("rust.json");
195    let existing = std::fs::read_to_string(&path).unwrap_or_default();
196    let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
197    // Stamp the box these numbers came from. The bench runs wherever it is
198    // invoked, so an unstamped manifest is indistinguishable from a fleet
199    // capture; the renderer will not publish one it cannot attribute.
200    let (source, instance) = SubMsP99Source::from_env();
201    manifest.set_p99_source(source, instance.as_deref());
202
203    // The baseline: the base wait-free push + pop round trip. Swept as well as
204    // sampled, because whether ring capacity moves the BASE op is the context
205    // every feature curve is read against.
206    let base_sweep = sweep("base/push+pop", |cap| {
207        let (mut tx, mut rx) = pair(cap);
208        batched(ITEMS_PER_SAMPLE, |i| {
209            let _ = tx.try_push(i as u64);
210            rx.try_pop().unwrap_or(0)
211        })
212    });
213    let base_p50 = base_sweep
214        .iter()
215        .find(|(n, _)| *n == CANON)
216        .map_or(0, |(_, v)| *v);
217    eprintln!("base push+pop p50 per {ITEMS_PER_SAMPLE}-item sample: {base_p50}ns");
218
219    // ---------- bulk: one fence per BULK_BATCH items ----------
220    #[cfg(feature = "bulk")]
221    {
222        // A sample moves ITEMS_PER_SAMPLE items either way; only the call width
223        // differs. That is the comparison the feature exists to win, and it is
224        // why the reps count is divided rather than the batch grown.
225        let sw = sweep("bulk/enqueue+dequeue", |cap| {
226            let (mut tx, mut rx) = pair(cap);
227            let batch = [0u64; BULK_BATCH];
228            let mut out = [0u64; BULK_BATCH];
229            batched(ITEMS_PER_SAMPLE / BULK_BATCH, |_| {
230                let n = tx.try_enqueue_bulk(&batch);
231                (n + rx.try_dequeue_bulk(&mut out)) as u64
232            })
233        });
234        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
235
236        let batch = [0u64; BULK_BATCH];
237        let mut out = [0u64; BULK_BATCH];
238        let (mut tx, mut rx) = pair(CANON);
239        let enq = single(
240            |_| tx.try_enqueue_bulk(&batch) as u64,
241            |_| rx.try_dequeue_bulk(&mut out) as u64,
242        );
243        let (mut tx, mut rx) = pair(CANON);
244        let deq = single(
245            |_| rx.try_dequeue_bulk(&mut out) as u64,
246            |_| tx.try_enqueue_bulk(&batch) as u64,
247        );
248        let mut p99 = BTreeMap::new();
249        p99.insert("enqueue_bulk".to_string(), enq);
250        p99.insert("dequeue_bulk".to_string(), deq);
251        manifest.set_feature("bulk", cat, &p99, &reason);
252    }
253
254    // ---------- wait-strategies: blocking wrappers, fast path only ----------
255    #[cfg(feature = "wait-strategies")]
256    {
257        use subms_spsc_ring_buffer::{
258            BlockingSpscConsumer, BlockingSpscProducer, BusySpin, ParkStrategy,
259        };
260        // Swept on BusySpin, whose `signal()` is a no-op, so the curve is the
261        // wrapper's own overhead over the base ring and nothing else. The ring
262        // is never full or empty, so `wait()` is never called; the parked-wakeup
263        // path is a scheduler latency, not a per-op cost, and is not measured.
264        let sw = sweep("wait-strategies/push+pop", |cap| {
265            let (tx, rx) = pair(cap);
266            let mut p = BlockingSpscProducer::new(tx, BusySpin);
267            let mut c = BlockingSpscConsumer::new(rx, BusySpin);
268            batched(ITEMS_PER_SAMPLE, |i| {
269                p.push(i as u64);
270                c.pop()
271            })
272        });
273        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
274
275        // ParkStrategy on the SAME ready ring, printed rather than classified.
276        // Its `signal()` fires on every successful op even when nothing is
277        // parked, and it takes a lock to do it, so the strategy choice shows up
278        // here on the fast path - not in `wait()`, which this never enters.
279        let park_batched = best(|| {
280            let (ps, cs) = ParkStrategy::pair();
281            let (tx, rx) = pair(CANON);
282            let mut p = BlockingSpscProducer::new(tx, ps);
283            let mut c = BlockingSpscConsumer::new(rx, cs);
284            batched(ITEMS_PER_SAMPLE, |i| {
285                p.push(i as u64);
286                c.pop()
287            })
288        });
289        eprintln!(
290            "wait-strategies park fast path at {CANON}: {park_batched}ns per \
291             {ITEMS_PER_SAMPLE}-item sample (spin {}ns) - signal() takes a lock per op",
292            sw.iter().find(|(n, _)| *n == CANON).map_or(0, |(_, v)| *v)
293        );
294
295        let (tx, rx) = pair(CANON);
296        let mut p = BlockingSpscProducer::new(tx, BusySpin);
297        let mut c = BlockingSpscConsumer::new(rx, BusySpin);
298        let push_spin = single(
299            |i| {
300                p.push(i as u64);
301                0
302            },
303            |_| c.pop(),
304        );
305        let (tx, rx) = pair(CANON);
306        let mut p = BlockingSpscProducer::new(tx, BusySpin);
307        let mut c = BlockingSpscConsumer::new(rx, BusySpin);
308        let pop_spin = single(
309            |_| c.pop(),
310            |i| {
311                p.push(i as u64);
312                0
313            },
314        );
315
316        let (ps, cs) = ParkStrategy::pair();
317        let (tx, rx) = pair(CANON);
318        let mut p = BlockingSpscProducer::new(tx, ps);
319        let mut c = BlockingSpscConsumer::new(rx, cs);
320        let push_park = single(
321            |i| {
322                p.push(i as u64);
323                0
324            },
325            |_| c.pop(),
326        );
327        let (ps, cs) = ParkStrategy::pair();
328        let (tx, rx) = pair(CANON);
329        let mut p = BlockingSpscProducer::new(tx, ps);
330        let mut c = BlockingSpscConsumer::new(rx, cs);
331        let pop_park = single(
332            |_| c.pop(),
333            |i| {
334                p.push(i as u64);
335                0
336            },
337        );
338
339        let mut p99 = BTreeMap::new();
340        p99.insert("push_spin".to_string(), push_spin);
341        p99.insert("pop_spin".to_string(), pop_spin);
342        p99.insert("push_park".to_string(), push_park);
343        p99.insert("pop_park".to_string(), pop_park);
344        manifest.set_feature("wait-strategies", cat, &p99, &reason);
345    }
346
347    // ---------- mpsc-fan-in: N rings, one round-robin consumer ----------
348    #[cfg(feature = "mpsc-fan-in")]
349    {
350        use subms_spsc_ring_buffer::{MpscFanIn, MpscFanInConsumer, MpscFanInProducer};
351        /// Producer count is held FIXED across the sweep. Moving it would sweep
352        /// the fan-in width, which sets the consumer's probe loop, not the ring.
353        const PRODUCERS: usize = 4;
354
355        fn fanin(cap: usize) -> (Vec<MpscFanInProducer<u64>>, MpscFanInConsumer<u64>) {
356            let (mut ps, c) = MpscFanIn::with_capacity::<u64>(PRODUCERS, cap);
357            for p in ps.iter_mut() {
358                for i in 0..cap / 2 {
359                    let _ = p.try_push(i as u64);
360                }
361            }
362            (ps, c)
363        }
364
365        // Pushes round-robin and the consumer cursor advances one ring per pop,
366        // so the two stay in step and every ring holds a constant half load.
367        // With every ring non-empty the consumer's probe hits on its first try,
368        // which is the steady-state shape; a starved fan-in probes all N and that
369        // is a different measurement.
370        let sw = sweep("mpsc-fan-in/push+pop", |cap| {
371            let (mut ps, mut c) = fanin(cap);
372            batched(ITEMS_PER_SAMPLE, |i| {
373                let _ = ps[i % PRODUCERS].try_push(i as u64);
374                c.try_pop().unwrap_or(0)
375            })
376        });
377        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
378
379        let (mut ps, mut c) = fanin(CANON);
380        let enq = single(
381            |i| {
382                let _ = ps[i % PRODUCERS].try_push(i as u64);
383                0
384            },
385            |_| c.try_pop().unwrap_or(0),
386        );
387        let (mut ps, mut c) = fanin(CANON);
388        let deq = single(
389            |_| c.try_pop().unwrap_or(0),
390            |i| {
391                let _ = ps[i % PRODUCERS].try_push(i as u64);
392                0
393            },
394        );
395        let mut p99 = BTreeMap::new();
396        p99.insert("fanin_enqueue".to_string(), enq);
397        p99.insert("fanin_dequeue".to_string(), deq);
398        manifest.set_feature("mpsc-fan-in", cat, &p99, &reason);
399    }
400
401    // ---------- mpmc-disruptor: CAS claim + sequence barrier ----------
402    #[cfg(feature = "mpmc-disruptor")]
403    {
404        use subms_spsc_ring_buffer::{DisruptorConsumer, DisruptorProducer, MpmcDisruptor};
405        /// One consumer, held fixed. `try_publish` scans every consumer cursor
406        /// before claiming, so the consumer count - not the capacity - is what
407        /// that loop is O(). Sweeping it would answer a different question.
408        const CONSUMERS: usize = 1;
409
410        fn disruptor(cap: usize) -> (DisruptorProducer<u64>, DisruptorConsumer<u64>) {
411            let (p, mut cs) = MpmcDisruptor::with_consumers::<u64>(cap, CONSUMERS);
412            let c = cs.remove(0);
413            for i in 0..cap / 2 {
414                let _ = p.try_publish(i as u64);
415            }
416            (p, c)
417        }
418
419        // Half a ring of published-but-unconsumed items keeps the producer clear
420        // of the gating spin (it only fires within one capacity of the slowest
421        // consumer) and the consumer clear of the unpublished early return.
422        let sw = sweep("mpmc-disruptor/publish+consume", |cap| {
423            let (p, mut c) = disruptor(cap);
424            batched(ITEMS_PER_SAMPLE, |i| {
425                let _ = p.try_publish(i as u64);
426                c.try_consume().unwrap_or(0)
427            })
428        });
429        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
430
431        let (p, mut c) = disruptor(CANON);
432        let published = single(
433            |i| {
434                let _ = p.try_publish(i as u64);
435                0
436            },
437            |_| c.try_consume().unwrap_or(0),
438        );
439        let (p, mut c) = disruptor(CANON);
440        let consumed = single(
441            |_| c.try_consume().unwrap_or(0),
442            |i| {
443                let _ = p.try_publish(i as u64);
444                0
445            },
446        );
447        let mut p99 = BTreeMap::new();
448        p99.insert("publish".to_string(), published);
449        p99.insert("consume".to_string(), consumed);
450        manifest.set_feature("mpmc-disruptor", cat, &p99, &reason);
451    }
452
453    // ---------- metrics: counters on the push / pop path ----------
454    #[cfg(feature = "metrics")]
455    {
456        use subms_spsc_ring_buffer::InstrumentedSpsc;
457
458        // The wrapper adds a `fetch_add` per op plus, on the producer side, a
459        // high-water-mark update. `local_depth` only ever increments - a producer
460        // cannot observe pops - so that update takes its CAS on every push rather
461        // than settling once the mark stops moving. The counter cost on the push
462        // path is two read-modify-writes, not one.
463        let sw = sweep("metrics/push+pop", |cap| {
464            let (tx, rx) = pair(cap);
465            let (mut tx, mut rx, _m) = InstrumentedSpsc::wrap(tx, rx);
466            batched(ITEMS_PER_SAMPLE, |i| {
467                let _ = tx.try_push(i as u64);
468                rx.try_pop().unwrap_or(0)
469            })
470        });
471        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
472
473        let (tx, rx) = pair(CANON);
474        let (mut tx, mut rx, _m) = InstrumentedSpsc::wrap(tx, rx);
475        let enq = single(
476            |i| {
477                let _ = tx.try_push(i as u64);
478                0
479            },
480            |_| rx.try_pop().unwrap_or(0),
481        );
482        let (tx, rx) = pair(CANON);
483        let (mut tx, mut rx, _m) = InstrumentedSpsc::wrap(tx, rx);
484        let deq = single(
485            |_| rx.try_pop().unwrap_or(0),
486            |i| {
487                let _ = tx.try_push(i as u64);
488                0
489            },
490        );
491        let mut p99 = BTreeMap::new();
492        p99.insert("metrics_enqueue".to_string(), enq);
493        p99.insert("metrics_dequeue".to_string(), deq);
494        manifest.set_feature("metrics", cat, &p99, &reason);
495    }
496
497    std::fs::create_dir_all(path.parent().unwrap())?;
498    std::fs::write(&path, manifest.to_json())?;
499    io::stdout().write_all(manifest.to_json().as_bytes())?;
500    Ok(())
501}
More examples
Hide additional examples
examples/demo.rs (line 9)
4fn main() {
5    let (mut tx, mut rx) = SpscRingBuffer::with_capacity::<u32>(16);
6
7    let producer = thread::spawn(move || {
8        for i in 0..10u32 {
9            while tx.try_push(i).is_err() {}
10        }
11    });
12
13    let consumer = thread::spawn(move || {
14        let mut seen = Vec::new();
15        while seen.len() < 10 {
16            if let Some(v) = rx.try_pop() {
17                seen.push(v);
18            }
19        }
20        seen
21    });
22
23    producer.join().unwrap();
24    let seen = consumer.join().unwrap();
25    println!("consumed: {seen:?}");
26}
examples/sample_app.rs (line 61)
49fn base_feed_to_strategy() {
50    println!("== base: feed-handler -> strategy handoff ==");
51
52    // Drop-on-full is the caller's decision. Shown deterministically on a
53    // small ring: capacity 4, six ticks offered, the last two are dropped.
54    let (mut tx, mut rx) = SpscRingBuffer::with_capacity::<Tick>(4);
55    let mut dropped = 0usize;
56    for seq in 0..6u64 {
57        let tick = Tick {
58            seq,
59            price_cents: 10_000 + seq as u32,
60        };
61        if tx.try_push(tick).is_err() {
62            dropped += 1;
63        }
64    }
65    println!("  cap-4 ring, 6 offered -> {dropped} dropped under backpressure");
66    assert_eq!(
67        dropped, 2,
68        "two ticks past capacity are dropped, not blocked"
69    );
70
71    // Occupancy is what a queue-depth alarm reads, and peek lets the strategy
72    // inspect the oldest tick before deciding to consume it.
73    let oldest = rx.peek().expect("ring is full").seq;
74    println!(
75        "  depth {}/{} full={}, oldest queued seq {oldest}",
76        rx.len(),
77        rx.capacity(),
78        tx.is_full()
79    );
80    println!("  dropped {} stale ticks on resync", rx.clear());
81
82    // Steady state: a drained ring loses nothing and preserves feed order.
83    let n = 50_000u64;
84    let (mut tx, mut rx) = SpscRingBuffer::with_capacity::<Tick>(1024);
85    let feed = thread::spawn(move || {
86        for seq in 0..n {
87            let tick = Tick {
88                seq,
89                price_cents: 10_000 + (seq % 500) as u32,
90            };
91            while tx.try_push(tick).is_err() {
92                std::hint::spin_loop();
93            }
94        }
95    });
96    let strategy = thread::spawn(move || {
97        let mut expected = 0u64;
98        while expected < n {
99            if let Some(tick) = rx.try_pop() {
100                assert_eq!(tick.seq, expected, "ticks arrive in feed order");
101                expected += 1;
102            }
103        }
104        expected
105    });
106    feed.join().unwrap();
107    let received = strategy.join().unwrap();
108    println!("  streamed {received} ticks in order, zero loss when drained");
109    assert_eq!(received, n);
110}
Source

pub fn capacity(&self) -> usize

Total slot count (power of two; not the requested capacity).

Source

pub fn len(&self) -> usize

Items currently buffered. A snapshot: the consumer runs concurrently, so the true count can only be lower by the time the caller acts on it. Use it for occupancy alarms and sizing, never to decide whether a push will succeed - try_push already answers that without a race.

Source

pub fn is_empty(&self) -> bool

True when no items are buffered. Snapshot semantics, as Producer::len.

Source

pub fn is_full(&self) -> bool

True when the ring holds capacity items. Snapshot semantics.

Examples found in repository?
examples/sample_app.rs (line 78)
49fn base_feed_to_strategy() {
50    println!("== base: feed-handler -> strategy handoff ==");
51
52    // Drop-on-full is the caller's decision. Shown deterministically on a
53    // small ring: capacity 4, six ticks offered, the last two are dropped.
54    let (mut tx, mut rx) = SpscRingBuffer::with_capacity::<Tick>(4);
55    let mut dropped = 0usize;
56    for seq in 0..6u64 {
57        let tick = Tick {
58            seq,
59            price_cents: 10_000 + seq as u32,
60        };
61        if tx.try_push(tick).is_err() {
62            dropped += 1;
63        }
64    }
65    println!("  cap-4 ring, 6 offered -> {dropped} dropped under backpressure");
66    assert_eq!(
67        dropped, 2,
68        "two ticks past capacity are dropped, not blocked"
69    );
70
71    // Occupancy is what a queue-depth alarm reads, and peek lets the strategy
72    // inspect the oldest tick before deciding to consume it.
73    let oldest = rx.peek().expect("ring is full").seq;
74    println!(
75        "  depth {}/{} full={}, oldest queued seq {oldest}",
76        rx.len(),
77        rx.capacity(),
78        tx.is_full()
79    );
80    println!("  dropped {} stale ticks on resync", rx.clear());
81
82    // Steady state: a drained ring loses nothing and preserves feed order.
83    let n = 50_000u64;
84    let (mut tx, mut rx) = SpscRingBuffer::with_capacity::<Tick>(1024);
85    let feed = thread::spawn(move || {
86        for seq in 0..n {
87            let tick = Tick {
88                seq,
89                price_cents: 10_000 + (seq % 500) as u32,
90            };
91            while tx.try_push(tick).is_err() {
92                std::hint::spin_loop();
93            }
94        }
95    });
96    let strategy = thread::spawn(move || {
97        let mut expected = 0u64;
98        while expected < n {
99            if let Some(tick) = rx.try_pop() {
100                assert_eq!(tick.seq, expected, "ticks arrive in feed order");
101                expected += 1;
102            }
103        }
104        expected
105    });
106    feed.join().unwrap();
107    let received = strategy.join().unwrap();
108    println!("  streamed {received} ticks in order, zero loss when drained");
109    assert_eq!(received, n);
110}

Trait Implementations§

Source§

impl<T: Send> Send for Producer<T>

Auto Trait Implementations§

§

impl<T> !RefUnwindSafe for Producer<T>

§

impl<T> !UnwindSafe for Producer<T>

§

impl<T> Freeze for Producer<T>

§

impl<T> Sync for Producer<T>
where T: Send,

§

impl<T> Unpin for Producer<T>

§

impl<T> UnsafeUnpin for Producer<T>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.