pub struct DisruptorConsumer<T> { /* private fields */ }Expand description
One consumer side. Each consumer sees every published item.
Implementations§
Source§impl<T: Clone> DisruptorConsumer<T>
impl<T: Clone> DisruptorConsumer<T>
Sourcepub fn try_consume(&mut self) -> Option<T>
pub fn try_consume(&mut self) -> Option<T>
Returns a clone of the next published item, or None if not ready.
Clone because each consumer of a broadcast disruptor sees the
same item; we cannot move out of the slot.
Examples found in repository?
examples/sample_app.rs (line 252)
226fn broadcast_to_strategy_and_risk() {
227 use subms_spsc_ring_buffer::MpmcDisruptor;
228
229 println!("\n== mpmc-disruptor: broadcast to strategy + risk ==");
230 let n = 8u64;
231 let (producer, mut consumers) = MpmcDisruptor::with_consumers::<Tick>(16, 2);
232 let (strategy, rest) = consumers.split_at_mut(1);
233 let strategy = &mut strategy[0];
234 let risk = &mut rest[0];
235
236 // Small and single-threaded so the tour self-verifies; the threaded
237 // broadcast path is pinned in the tests.
238 let mut published = 0u64;
239 let mut strat_seen = Vec::new();
240 let mut risk_seen = Vec::new();
241 while published < n {
242 while published < n
243 && producer
244 .try_publish(Tick {
245 seq: published,
246 price_cents: 50_000,
247 })
248 .is_ok()
249 {
250 published += 1;
251 }
252 while let Some(t) = strategy.try_consume() {
253 strat_seen.push(t.seq);
254 }
255 while let Some(t) = risk.try_consume() {
256 risk_seen.push(t.seq);
257 }
258 }
259 println!(
260 " published {published}; strategy saw {}, risk saw {}",
261 strat_seen.len(),
262 risk_seen.len()
263 );
264 let expected: Vec<u64> = (0..n).collect();
265 assert_eq!(strat_seen, expected, "strategy sees every tick");
266 assert_eq!(risk_seen, expected, "risk monitor sees every tick too");
267}More examples
examples/perf_features.rs (line 426)
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}pub fn capacity(&self) -> usize
Trait Implementations§
impl<T: Send> Send for DisruptorConsumer<T>
Auto Trait Implementations§
impl<T> !RefUnwindSafe for DisruptorConsumer<T>
impl<T> !UnwindSafe for DisruptorConsumer<T>
impl<T> Freeze for DisruptorConsumer<T>
impl<T> Sync for DisruptorConsumer<T>where
T: Send,
impl<T> Unpin for DisruptorConsumer<T>
impl<T> UnsafeUnpin for DisruptorConsumer<T>
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more