pub struct BoundedMpscQueue<T> { /* private fields */ }Expand description
Bounded MPSC ring queue. Capacity is rounded up to the next power of two (minimum 2) so the modulo can be a bitmask.
Implementations§
Source§impl<T> BoundedMpscQueue<T>
impl<T> BoundedMpscQueue<T>
Sourcepub fn new(capacity: usize) -> Self
pub fn new(capacity: usize) -> Self
New empty queue. capacity is rounded up to a power of two,
minimum 2.
Examples found in repository?
195fn main() -> io::Result<()> {
196 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
197 .join("..")
198 .join(".subms")
199 .join("features")
200 .join("rust.json");
201 let existing = std::fs::read_to_string(&path).unwrap_or_default();
202 let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
203 // Stamp the box these numbers came from. The bench runs wherever it is
204 // invoked, so an unstamped manifest is indistinguishable from a fleet
205 // capture; the renderer will not publish one it cannot attribute.
206 let (source, instance) = SubMsP99Source::from_env();
207 manifest.set_p99_source(source, instance.as_deref());
208
209 // Optional, off by default: pin this thread to one core for the whole run.
210 // On a heterogeneous laptop the scheduler moves the bench between core
211 // clusters and every measurement lands in one of two clock states 1.31x
212 // apart - a spread three times wider than the deltas being classified, and
213 // large enough on its own to flip a feature between auxiliary and hot-path.
214 // Pinned, the same sweep repeats to within 1%. Left OFF by default because a
215 // fleet box isolates cores outside the process, and pinning from in here
216 // would override that placement with a core the orchestrator did not choose.
217 #[cfg(feature = "affinity")]
218 if let Some(core) = std::env::var("SUBMS_PIN").ok().and_then(|v| v.parse().ok()) {
219 let _ = subms_mpsc_queue::set_affinity(&[core]);
220 }
221
222 // Burn before the first measurement, not just before each one. Every
223 // `batched` call warms itself, but the FIRST measurement in the process pays
224 // a ramp the per-measurement warm sits inside rather than absorbs, and the
225 // sweep runs smallest-first: without this the base curve read 71800 / 45300 /
226 // 46500 ns, a 1.6x fall with size that is the process settling, not the
227 // queue.
228 {
229 let mut q = filled(CANON);
230 let start = std::time::Instant::now();
231 while (start.elapsed().as_nanos() as u64) < BURN_NANOS {
232 for i in 0..ITEMS_PER_SAMPLE {
233 q.push(i as u64);
234 black_box(q.try_pop());
235 }
236 }
237 }
238
239 // The baseline: the base queue's push + try_pop round trip. Swept as well as
240 // sampled, because whether queue depth moves the BASE op is the context
241 // every feature curve is read against.
242 let base_sweep = sweep("base/push+pop", |n| {
243 let mut q = filled(n);
244 batched(ITEMS_PER_SAMPLE, |i| {
245 q.push(i as u64);
246 black_box(q.try_pop());
247 })
248 });
249 let base_p50 = base_sweep
250 .iter()
251 .find(|(n, _)| *n == CANON)
252 .map_or(0, |(_, v)| *v);
253 eprintln!("base push+pop p50 per {ITEMS_PER_SAMPLE}-item sample: {base_p50}ns");
254
255 // ---------- bounded: fixed-capacity ring, backpressure on enqueue ----------
256 #[cfg(feature = "bounded")]
257 {
258 use subms_mpsc_queue::BoundedMpscQueue;
259 // Half full at every sweep point. Filled to a FIXED element count
260 // instead, the big rings would sit 98% empty and the enqueue would be
261 // measuring the fill fraction rather than the footprint.
262 fn ring(n: usize) -> BoundedMpscQueue<u64> {
263 let q = BoundedMpscQueue::new(n);
264 for i in 0..n / 2 {
265 let _ = q.try_enqueue(i as u64);
266 }
267 q
268 }
269 let sw = sweep("bounded/enqueue+dequeue", |n| {
270 let mut q = ring(n);
271 batched(ITEMS_PER_SAMPLE, |i| {
272 let _ = q.try_enqueue(i as u64);
273 black_box(q.try_dequeue());
274 })
275 });
276 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
277
278 let mut q = ring(CANON);
279 let mut p99 = BTreeMap::new();
280 p99.insert(
281 "enqueue".to_string(),
282 single(|st, i| {
283 st.time(|| {
284 let _ = q.try_enqueue(i as u64);
285 });
286 black_box(q.try_dequeue());
287 }),
288 );
289 p99.insert(
290 "dequeue".to_string(),
291 single(|st, i| {
292 let _ = q.try_enqueue(i as u64);
293 st.time(|| black_box(q.try_dequeue()));
294 }),
295 );
296 // The reject path, which is the reason the feature exists. The ring is
297 // filled to capacity once, OUTSIDE the timed region; every timed call
298 // then takes the full branch and hands the value back to the caller.
299 let full: BoundedMpscQueue<u64> = BoundedMpscQueue::new(CANON);
300 while full.try_enqueue(0).is_ok() {}
301 p99.insert(
302 "enqueue_full".to_string(),
303 single(|st, i| {
304 st.time(|| {
305 let _ = full.try_enqueue(i as u64);
306 });
307 }),
308 );
309 manifest.set_feature("bounded", cat, &p99, &reason);
310 }
311
312 // ---------- mpmc: bounded ring, sequence CAS on both ends ----------
313 #[cfg(feature = "mpmc")]
314 {
315 use subms_mpsc_queue::MpmcQueue;
316 fn ring(n: usize) -> MpmcQueue<u64> {
317 let q = MpmcQueue::new(n);
318 for i in 0..n / 2 {
319 let _ = q.try_enqueue(i as u64);
320 }
321 q
322 }
323 // Uncontended, so every CAS succeeds first try. That is the figure the
324 // category is about: what the multi-consumer claim costs a queue that is
325 // NOT contended, which is the state a well-sized pipeline runs in.
326 let sw = sweep("mpmc/enqueue+dequeue", |n| {
327 let q = ring(n);
328 batched(ITEMS_PER_SAMPLE, |i| {
329 let _ = q.try_enqueue(i as u64);
330 black_box(q.try_dequeue());
331 })
332 });
333 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
334
335 let q = ring(CANON);
336 let mut p99 = BTreeMap::new();
337 p99.insert(
338 "enqueue".to_string(),
339 single(|st, i| {
340 st.time(|| {
341 let _ = q.try_enqueue(i as u64);
342 });
343 black_box(q.try_dequeue());
344 }),
345 );
346 p99.insert(
347 "dequeue".to_string(),
348 single(|st, i| {
349 let _ = q.try_enqueue(i as u64);
350 st.time(|| black_box(q.try_dequeue()));
351 }),
352 );
353 manifest.set_feature("mpmc", cat, &p99, &reason);
354 }
355
356 // ---------- batch: drain up to BATCH items behind one acquire fence ----------
357 #[cfg(feature = "batch")]
358 {
359 use subms_mpsc_queue::BatchMpscQueue;
360 fn filled_batch(n: usize) -> BatchMpscQueue<u64> {
361 let q = BatchMpscQueue::new();
362 for i in 0..n {
363 q.push(i as u64);
364 }
365 q
366 }
367 // A sample moves ITEMS_PER_SAMPLE items either way; only the call width
368 // differs. That is why the reps count is divided rather than the batch
369 // grown - growing it would sweep the batch size, and the number would
370 // stop being comparable to the base round trip.
371 let sw = sweep("batch/push+dequeue_batch", |n| {
372 let mut q = filled_batch(n);
373 let mut buf: Vec<Option<u64>> = (0..BATCH).map(|_| None).collect();
374 batched(ITEMS_PER_SAMPLE / BATCH, |i| {
375 for j in 0..BATCH {
376 q.push((i + j) as u64);
377 }
378 black_box(q.try_dequeue_batch(&mut buf));
379 })
380 });
381 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
382
383 let mut q = filled_batch(CANON);
384 let mut buf: Vec<Option<u64>> = (0..BATCH).map(|_| None).collect();
385 let mut p99 = BTreeMap::new();
386 // The refill is outside the timed region: timing it would put a BATCH of
387 // pushes inside the drain's number and the stage would stop being a
388 // drain figure at all.
389 p99.insert(
390 "dequeue_batch".to_string(),
391 single(|st, i| {
392 st.time(|| black_box(q.try_dequeue_batch(&mut buf)));
393 for j in 0..BATCH {
394 q.push((i + j) as u64);
395 }
396 }),
397 );
398 p99.insert(
399 "enqueue".to_string(),
400 single(|st, i| {
401 st.time(|| q.push(i as u64));
402 let _ = q.try_dequeue_batch(&mut buf[..1]);
403 }),
404 );
405 // The producer mirror: BATCH items published behind one head swap. The
406 // drain that puts the queue back is outside the timed region for the
407 // same reason the refill is above.
408 p99.insert(
409 "enqueue_batch".to_string(),
410 single(|st, i| {
411 let base = i as u64;
412 st.time(|| black_box(q.push_batch(base..base + BATCH as u64)));
413 let _ = q.try_dequeue_batch(&mut buf);
414 }),
415 );
416 manifest.set_feature("batch", cat, &p99, &reason);
417 }
418
419 // ---------- metrics: relaxed atomic counters around each op ----------
420 #[cfg(feature = "metrics")]
421 {
422 use subms_mpsc_queue::MetricsMpscQueue;
423 fn filled_metrics(n: usize) -> MetricsMpscQueue<u64> {
424 let q = MetricsMpscQueue::new();
425 for i in 0..n {
426 q.push(i as u64);
427 }
428 q
429 }
430 let sw = sweep("metrics/push+pop", |n| {
431 let mut q = filled_metrics(n);
432 batched(ITEMS_PER_SAMPLE, |i| {
433 q.push(i as u64);
434 black_box(q.try_pop());
435 })
436 });
437 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
438
439 let mut q = filled_metrics(CANON);
440 let mut p99 = BTreeMap::new();
441 p99.insert(
442 "enqueue".to_string(),
443 single(|st, i| {
444 st.time(|| q.push(i as u64));
445 black_box(q.try_pop());
446 }),
447 );
448 p99.insert(
449 "dequeue".to_string(),
450 single(|st, i| {
451 q.push(i as u64);
452 st.time(|| black_box(q.try_pop()));
453 }),
454 );
455 p99.insert(
456 "snapshot".to_string(),
457 single(|st, _| {
458 st.time(|| black_box(q.snapshot()));
459 }),
460 );
461 manifest.set_feature("metrics", cat, &p99, &reason);
462 }
463
464 // ---------- affinity: pin the calling thread, once, at startup ----------
465 // Runs LAST because measuring it pins THIS process to core 0, and every
466 // number taken afterwards would be a number taken on one core.
467 #[cfg(feature = "affinity")]
468 {
469 use subms_mpsc_queue::set_affinity;
470 // Swept over the same axis to show what it is: a call that touches no
471 // queue state and cannot move with queue size. PINNED auxiliary rather
472 // than left to the base-delta test, which would see a syscall costing
473 // more than an enqueue and call it hot-path. It is not on the hot path at
474 // any price - `set_affinity` is called once per thread at startup and
475 // appears in neither `push` nor `try_pop`. The two ports are not even
476 // measuring the same thing: Rust issues a real `SetThreadAffinityMask` /
477 // `sched_setaffinity`, while the Java sibling validates its argument and
478 // returns UNSUPPORTED because the stock JDK has no pinning API. The
479 // per-call figure, not the sample, is the interpretable one and it is in
480 // `p99ByStage`.
481 let sw = sweep("affinity/set_affinity", |_| {
482 batched(ITEMS_PER_SAMPLE, |_| {
483 let _ = set_affinity(&[0]);
484 })
485 });
486 let (cat, reason) = classify_feature(
487 &sw,
488 Some(base_p50),
489 Some(subms::SubMsFeatureCategory::Auxiliary),
490 );
491
492 let mut p99 = BTreeMap::new();
493 p99.insert(
494 "set_affinity".to_string(),
495 single(|st, _| {
496 st.time(|| {
497 let _ = set_affinity(&[0]);
498 });
499 }),
500 );
501 manifest.set_feature("affinity", cat, &p99, &reason);
502
503 let cores: Vec<usize> = (0..std::thread::available_parallelism()
504 .map_or(1, std::num::NonZeroUsize::get))
505 .collect();
506 let _ = set_affinity(&cores);
507 }
508
509 std::fs::create_dir_all(path.parent().unwrap())?;
510 std::fs::write(&path, manifest.to_json())?;
511 io::stdout().write_all(manifest.to_json().as_bytes())?;
512 Ok(())
513}More examples
199fn bounded_inbox_backpressure() {
200 use subms_mpsc_queue::BoundedMpscQueue;
201
202 println!("\n== bounded: a fixed-capacity inbox that pushes back ==");
203 let mut inbox: BoundedMpscQueue<u64> = BoundedMpscQueue::new(4);
204 let cap = inbox.capacity();
205
206 let mut accepted = 0usize;
207 let mut rejected = 0usize;
208 for seq in 0..cap + 2 {
209 match inbox.try_enqueue(order_id(0, seq)) {
210 Ok(()) => accepted += 1,
211 Err(_order) => rejected += 1,
212 }
213 }
214 println!(" capacity {cap}: accepted {accepted}, shed {rejected} while full");
215 assert_eq!(accepted, cap, "accepts exactly one full ring");
216 assert_eq!(rejected, 2, "the overflow is handed back, not queued");
217 assert!(inbox.is_full());
218
219 // Drain one, and a previously-rejected order now fits.
220 assert!(
221 inbox.try_dequeue().is_some(),
222 "match loop consumes one order"
223 );
224 assert!(
225 inbox.try_enqueue(order_id(0, 99)).is_ok(),
226 "a freed slot reopens the inbox"
227 );
228
229 // The two monotonic cursors are what a health check scrapes: their
230 // difference is inbox lag, and each on its own gives a rate between polls.
231 println!(
232 " producer index {} - consumer index {} = lag {}",
233 inbox.producer_index(),
234 inbox.consumer_index(),
235 inbox.len()
236 );
237 assert_eq!(inbox.producer_index() - inbox.consumer_index(), inbox.len());
238}Sourcepub fn capacity(&self) -> usize
pub fn capacity(&self) -> usize
Capacity (power-of-two; possibly larger than requested).
Examples found in repository?
199fn bounded_inbox_backpressure() {
200 use subms_mpsc_queue::BoundedMpscQueue;
201
202 println!("\n== bounded: a fixed-capacity inbox that pushes back ==");
203 let mut inbox: BoundedMpscQueue<u64> = BoundedMpscQueue::new(4);
204 let cap = inbox.capacity();
205
206 let mut accepted = 0usize;
207 let mut rejected = 0usize;
208 for seq in 0..cap + 2 {
209 match inbox.try_enqueue(order_id(0, seq)) {
210 Ok(()) => accepted += 1,
211 Err(_order) => rejected += 1,
212 }
213 }
214 println!(" capacity {cap}: accepted {accepted}, shed {rejected} while full");
215 assert_eq!(accepted, cap, "accepts exactly one full ring");
216 assert_eq!(rejected, 2, "the overflow is handed back, not queued");
217 assert!(inbox.is_full());
218
219 // Drain one, and a previously-rejected order now fits.
220 assert!(
221 inbox.try_dequeue().is_some(),
222 "match loop consumes one order"
223 );
224 assert!(
225 inbox.try_enqueue(order_id(0, 99)).is_ok(),
226 "a freed slot reopens the inbox"
227 );
228
229 // The two monotonic cursors are what a health check scrapes: their
230 // difference is inbox lag, and each on its own gives a rate between polls.
231 println!(
232 " producer index {} - consumer index {} = lag {}",
233 inbox.producer_index(),
234 inbox.consumer_index(),
235 inbox.len()
236 );
237 assert_eq!(inbox.producer_index() - inbox.consumer_index(), inbox.len());
238}Sourcepub fn producer_index(&self) -> usize
pub fn producer_index(&self) -> usize
Monotonic count of slots ever claimed by producers. Safe to read from
any thread; pair it with Self::consumer_index to get lag, or sample
it twice to get throughput without disturbing either end.
Examples found in repository?
199fn bounded_inbox_backpressure() {
200 use subms_mpsc_queue::BoundedMpscQueue;
201
202 println!("\n== bounded: a fixed-capacity inbox that pushes back ==");
203 let mut inbox: BoundedMpscQueue<u64> = BoundedMpscQueue::new(4);
204 let cap = inbox.capacity();
205
206 let mut accepted = 0usize;
207 let mut rejected = 0usize;
208 for seq in 0..cap + 2 {
209 match inbox.try_enqueue(order_id(0, seq)) {
210 Ok(()) => accepted += 1,
211 Err(_order) => rejected += 1,
212 }
213 }
214 println!(" capacity {cap}: accepted {accepted}, shed {rejected} while full");
215 assert_eq!(accepted, cap, "accepts exactly one full ring");
216 assert_eq!(rejected, 2, "the overflow is handed back, not queued");
217 assert!(inbox.is_full());
218
219 // Drain one, and a previously-rejected order now fits.
220 assert!(
221 inbox.try_dequeue().is_some(),
222 "match loop consumes one order"
223 );
224 assert!(
225 inbox.try_enqueue(order_id(0, 99)).is_ok(),
226 "a freed slot reopens the inbox"
227 );
228
229 // The two monotonic cursors are what a health check scrapes: their
230 // difference is inbox lag, and each on its own gives a rate between polls.
231 println!(
232 " producer index {} - consumer index {} = lag {}",
233 inbox.producer_index(),
234 inbox.consumer_index(),
235 inbox.len()
236 );
237 assert_eq!(inbox.producer_index() - inbox.consumer_index(), inbox.len());
238}Sourcepub fn consumer_index(&self) -> usize
pub fn consumer_index(&self) -> usize
Monotonic count of slots ever consumed. Safe to read from any thread.
Examples found in repository?
199fn bounded_inbox_backpressure() {
200 use subms_mpsc_queue::BoundedMpscQueue;
201
202 println!("\n== bounded: a fixed-capacity inbox that pushes back ==");
203 let mut inbox: BoundedMpscQueue<u64> = BoundedMpscQueue::new(4);
204 let cap = inbox.capacity();
205
206 let mut accepted = 0usize;
207 let mut rejected = 0usize;
208 for seq in 0..cap + 2 {
209 match inbox.try_enqueue(order_id(0, seq)) {
210 Ok(()) => accepted += 1,
211 Err(_order) => rejected += 1,
212 }
213 }
214 println!(" capacity {cap}: accepted {accepted}, shed {rejected} while full");
215 assert_eq!(accepted, cap, "accepts exactly one full ring");
216 assert_eq!(rejected, 2, "the overflow is handed back, not queued");
217 assert!(inbox.is_full());
218
219 // Drain one, and a previously-rejected order now fits.
220 assert!(
221 inbox.try_dequeue().is_some(),
222 "match loop consumes one order"
223 );
224 assert!(
225 inbox.try_enqueue(order_id(0, 99)).is_ok(),
226 "a freed slot reopens the inbox"
227 );
228
229 // The two monotonic cursors are what a health check scrapes: their
230 // difference is inbox lag, and each on its own gives a rate between polls.
231 println!(
232 " producer index {} - consumer index {} = lag {}",
233 inbox.producer_index(),
234 inbox.consumer_index(),
235 inbox.len()
236 );
237 assert_eq!(inbox.producer_index() - inbox.consumer_index(), inbox.len());
238}Sourcepub fn try_enqueue(&self, value: T) -> Result<(), T>
pub fn try_enqueue(&self, value: T) -> Result<(), T>
Multi-producer push. Returns Err(value) when the ring is
full so the caller can retry, drop, or apply backpressure.
Examples found in repository?
195fn main() -> io::Result<()> {
196 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
197 .join("..")
198 .join(".subms")
199 .join("features")
200 .join("rust.json");
201 let existing = std::fs::read_to_string(&path).unwrap_or_default();
202 let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
203 // Stamp the box these numbers came from. The bench runs wherever it is
204 // invoked, so an unstamped manifest is indistinguishable from a fleet
205 // capture; the renderer will not publish one it cannot attribute.
206 let (source, instance) = SubMsP99Source::from_env();
207 manifest.set_p99_source(source, instance.as_deref());
208
209 // Optional, off by default: pin this thread to one core for the whole run.
210 // On a heterogeneous laptop the scheduler moves the bench between core
211 // clusters and every measurement lands in one of two clock states 1.31x
212 // apart - a spread three times wider than the deltas being classified, and
213 // large enough on its own to flip a feature between auxiliary and hot-path.
214 // Pinned, the same sweep repeats to within 1%. Left OFF by default because a
215 // fleet box isolates cores outside the process, and pinning from in here
216 // would override that placement with a core the orchestrator did not choose.
217 #[cfg(feature = "affinity")]
218 if let Some(core) = std::env::var("SUBMS_PIN").ok().and_then(|v| v.parse().ok()) {
219 let _ = subms_mpsc_queue::set_affinity(&[core]);
220 }
221
222 // Burn before the first measurement, not just before each one. Every
223 // `batched` call warms itself, but the FIRST measurement in the process pays
224 // a ramp the per-measurement warm sits inside rather than absorbs, and the
225 // sweep runs smallest-first: without this the base curve read 71800 / 45300 /
226 // 46500 ns, a 1.6x fall with size that is the process settling, not the
227 // queue.
228 {
229 let mut q = filled(CANON);
230 let start = std::time::Instant::now();
231 while (start.elapsed().as_nanos() as u64) < BURN_NANOS {
232 for i in 0..ITEMS_PER_SAMPLE {
233 q.push(i as u64);
234 black_box(q.try_pop());
235 }
236 }
237 }
238
239 // The baseline: the base queue's push + try_pop round trip. Swept as well as
240 // sampled, because whether queue depth moves the BASE op is the context
241 // every feature curve is read against.
242 let base_sweep = sweep("base/push+pop", |n| {
243 let mut q = filled(n);
244 batched(ITEMS_PER_SAMPLE, |i| {
245 q.push(i as u64);
246 black_box(q.try_pop());
247 })
248 });
249 let base_p50 = base_sweep
250 .iter()
251 .find(|(n, _)| *n == CANON)
252 .map_or(0, |(_, v)| *v);
253 eprintln!("base push+pop p50 per {ITEMS_PER_SAMPLE}-item sample: {base_p50}ns");
254
255 // ---------- bounded: fixed-capacity ring, backpressure on enqueue ----------
256 #[cfg(feature = "bounded")]
257 {
258 use subms_mpsc_queue::BoundedMpscQueue;
259 // Half full at every sweep point. Filled to a FIXED element count
260 // instead, the big rings would sit 98% empty and the enqueue would be
261 // measuring the fill fraction rather than the footprint.
262 fn ring(n: usize) -> BoundedMpscQueue<u64> {
263 let q = BoundedMpscQueue::new(n);
264 for i in 0..n / 2 {
265 let _ = q.try_enqueue(i as u64);
266 }
267 q
268 }
269 let sw = sweep("bounded/enqueue+dequeue", |n| {
270 let mut q = ring(n);
271 batched(ITEMS_PER_SAMPLE, |i| {
272 let _ = q.try_enqueue(i as u64);
273 black_box(q.try_dequeue());
274 })
275 });
276 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
277
278 let mut q = ring(CANON);
279 let mut p99 = BTreeMap::new();
280 p99.insert(
281 "enqueue".to_string(),
282 single(|st, i| {
283 st.time(|| {
284 let _ = q.try_enqueue(i as u64);
285 });
286 black_box(q.try_dequeue());
287 }),
288 );
289 p99.insert(
290 "dequeue".to_string(),
291 single(|st, i| {
292 let _ = q.try_enqueue(i as u64);
293 st.time(|| black_box(q.try_dequeue()));
294 }),
295 );
296 // The reject path, which is the reason the feature exists. The ring is
297 // filled to capacity once, OUTSIDE the timed region; every timed call
298 // then takes the full branch and hands the value back to the caller.
299 let full: BoundedMpscQueue<u64> = BoundedMpscQueue::new(CANON);
300 while full.try_enqueue(0).is_ok() {}
301 p99.insert(
302 "enqueue_full".to_string(),
303 single(|st, i| {
304 st.time(|| {
305 let _ = full.try_enqueue(i as u64);
306 });
307 }),
308 );
309 manifest.set_feature("bounded", cat, &p99, &reason);
310 }
311
312 // ---------- mpmc: bounded ring, sequence CAS on both ends ----------
313 #[cfg(feature = "mpmc")]
314 {
315 use subms_mpsc_queue::MpmcQueue;
316 fn ring(n: usize) -> MpmcQueue<u64> {
317 let q = MpmcQueue::new(n);
318 for i in 0..n / 2 {
319 let _ = q.try_enqueue(i as u64);
320 }
321 q
322 }
323 // Uncontended, so every CAS succeeds first try. That is the figure the
324 // category is about: what the multi-consumer claim costs a queue that is
325 // NOT contended, which is the state a well-sized pipeline runs in.
326 let sw = sweep("mpmc/enqueue+dequeue", |n| {
327 let q = ring(n);
328 batched(ITEMS_PER_SAMPLE, |i| {
329 let _ = q.try_enqueue(i as u64);
330 black_box(q.try_dequeue());
331 })
332 });
333 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
334
335 let q = ring(CANON);
336 let mut p99 = BTreeMap::new();
337 p99.insert(
338 "enqueue".to_string(),
339 single(|st, i| {
340 st.time(|| {
341 let _ = q.try_enqueue(i as u64);
342 });
343 black_box(q.try_dequeue());
344 }),
345 );
346 p99.insert(
347 "dequeue".to_string(),
348 single(|st, i| {
349 let _ = q.try_enqueue(i as u64);
350 st.time(|| black_box(q.try_dequeue()));
351 }),
352 );
353 manifest.set_feature("mpmc", cat, &p99, &reason);
354 }
355
356 // ---------- batch: drain up to BATCH items behind one acquire fence ----------
357 #[cfg(feature = "batch")]
358 {
359 use subms_mpsc_queue::BatchMpscQueue;
360 fn filled_batch(n: usize) -> BatchMpscQueue<u64> {
361 let q = BatchMpscQueue::new();
362 for i in 0..n {
363 q.push(i as u64);
364 }
365 q
366 }
367 // A sample moves ITEMS_PER_SAMPLE items either way; only the call width
368 // differs. That is why the reps count is divided rather than the batch
369 // grown - growing it would sweep the batch size, and the number would
370 // stop being comparable to the base round trip.
371 let sw = sweep("batch/push+dequeue_batch", |n| {
372 let mut q = filled_batch(n);
373 let mut buf: Vec<Option<u64>> = (0..BATCH).map(|_| None).collect();
374 batched(ITEMS_PER_SAMPLE / BATCH, |i| {
375 for j in 0..BATCH {
376 q.push((i + j) as u64);
377 }
378 black_box(q.try_dequeue_batch(&mut buf));
379 })
380 });
381 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
382
383 let mut q = filled_batch(CANON);
384 let mut buf: Vec<Option<u64>> = (0..BATCH).map(|_| None).collect();
385 let mut p99 = BTreeMap::new();
386 // The refill is outside the timed region: timing it would put a BATCH of
387 // pushes inside the drain's number and the stage would stop being a
388 // drain figure at all.
389 p99.insert(
390 "dequeue_batch".to_string(),
391 single(|st, i| {
392 st.time(|| black_box(q.try_dequeue_batch(&mut buf)));
393 for j in 0..BATCH {
394 q.push((i + j) as u64);
395 }
396 }),
397 );
398 p99.insert(
399 "enqueue".to_string(),
400 single(|st, i| {
401 st.time(|| q.push(i as u64));
402 let _ = q.try_dequeue_batch(&mut buf[..1]);
403 }),
404 );
405 // The producer mirror: BATCH items published behind one head swap. The
406 // drain that puts the queue back is outside the timed region for the
407 // same reason the refill is above.
408 p99.insert(
409 "enqueue_batch".to_string(),
410 single(|st, i| {
411 let base = i as u64;
412 st.time(|| black_box(q.push_batch(base..base + BATCH as u64)));
413 let _ = q.try_dequeue_batch(&mut buf);
414 }),
415 );
416 manifest.set_feature("batch", cat, &p99, &reason);
417 }
418
419 // ---------- metrics: relaxed atomic counters around each op ----------
420 #[cfg(feature = "metrics")]
421 {
422 use subms_mpsc_queue::MetricsMpscQueue;
423 fn filled_metrics(n: usize) -> MetricsMpscQueue<u64> {
424 let q = MetricsMpscQueue::new();
425 for i in 0..n {
426 q.push(i as u64);
427 }
428 q
429 }
430 let sw = sweep("metrics/push+pop", |n| {
431 let mut q = filled_metrics(n);
432 batched(ITEMS_PER_SAMPLE, |i| {
433 q.push(i as u64);
434 black_box(q.try_pop());
435 })
436 });
437 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
438
439 let mut q = filled_metrics(CANON);
440 let mut p99 = BTreeMap::new();
441 p99.insert(
442 "enqueue".to_string(),
443 single(|st, i| {
444 st.time(|| q.push(i as u64));
445 black_box(q.try_pop());
446 }),
447 );
448 p99.insert(
449 "dequeue".to_string(),
450 single(|st, i| {
451 q.push(i as u64);
452 st.time(|| black_box(q.try_pop()));
453 }),
454 );
455 p99.insert(
456 "snapshot".to_string(),
457 single(|st, _| {
458 st.time(|| black_box(q.snapshot()));
459 }),
460 );
461 manifest.set_feature("metrics", cat, &p99, &reason);
462 }
463
464 // ---------- affinity: pin the calling thread, once, at startup ----------
465 // Runs LAST because measuring it pins THIS process to core 0, and every
466 // number taken afterwards would be a number taken on one core.
467 #[cfg(feature = "affinity")]
468 {
469 use subms_mpsc_queue::set_affinity;
470 // Swept over the same axis to show what it is: a call that touches no
471 // queue state and cannot move with queue size. PINNED auxiliary rather
472 // than left to the base-delta test, which would see a syscall costing
473 // more than an enqueue and call it hot-path. It is not on the hot path at
474 // any price - `set_affinity` is called once per thread at startup and
475 // appears in neither `push` nor `try_pop`. The two ports are not even
476 // measuring the same thing: Rust issues a real `SetThreadAffinityMask` /
477 // `sched_setaffinity`, while the Java sibling validates its argument and
478 // returns UNSUPPORTED because the stock JDK has no pinning API. The
479 // per-call figure, not the sample, is the interpretable one and it is in
480 // `p99ByStage`.
481 let sw = sweep("affinity/set_affinity", |_| {
482 batched(ITEMS_PER_SAMPLE, |_| {
483 let _ = set_affinity(&[0]);
484 })
485 });
486 let (cat, reason) = classify_feature(
487 &sw,
488 Some(base_p50),
489 Some(subms::SubMsFeatureCategory::Auxiliary),
490 );
491
492 let mut p99 = BTreeMap::new();
493 p99.insert(
494 "set_affinity".to_string(),
495 single(|st, _| {
496 st.time(|| {
497 let _ = set_affinity(&[0]);
498 });
499 }),
500 );
501 manifest.set_feature("affinity", cat, &p99, &reason);
502
503 let cores: Vec<usize> = (0..std::thread::available_parallelism()
504 .map_or(1, std::num::NonZeroUsize::get))
505 .collect();
506 let _ = set_affinity(&cores);
507 }
508
509 std::fs::create_dir_all(path.parent().unwrap())?;
510 std::fs::write(&path, manifest.to_json())?;
511 io::stdout().write_all(manifest.to_json().as_bytes())?;
512 Ok(())
513}More examples
199fn bounded_inbox_backpressure() {
200 use subms_mpsc_queue::BoundedMpscQueue;
201
202 println!("\n== bounded: a fixed-capacity inbox that pushes back ==");
203 let mut inbox: BoundedMpscQueue<u64> = BoundedMpscQueue::new(4);
204 let cap = inbox.capacity();
205
206 let mut accepted = 0usize;
207 let mut rejected = 0usize;
208 for seq in 0..cap + 2 {
209 match inbox.try_enqueue(order_id(0, seq)) {
210 Ok(()) => accepted += 1,
211 Err(_order) => rejected += 1,
212 }
213 }
214 println!(" capacity {cap}: accepted {accepted}, shed {rejected} while full");
215 assert_eq!(accepted, cap, "accepts exactly one full ring");
216 assert_eq!(rejected, 2, "the overflow is handed back, not queued");
217 assert!(inbox.is_full());
218
219 // Drain one, and a previously-rejected order now fits.
220 assert!(
221 inbox.try_dequeue().is_some(),
222 "match loop consumes one order"
223 );
224 assert!(
225 inbox.try_enqueue(order_id(0, 99)).is_ok(),
226 "a freed slot reopens the inbox"
227 );
228
229 // The two monotonic cursors are what a health check scrapes: their
230 // difference is inbox lag, and each on its own gives a rate between polls.
231 println!(
232 " producer index {} - consumer index {} = lag {}",
233 inbox.producer_index(),
234 inbox.consumer_index(),
235 inbox.len()
236 );
237 assert_eq!(inbox.producer_index() - inbox.consumer_index(), inbox.len());
238}Sourcepub fn try_dequeue(&mut self) -> Option<T>
pub fn try_dequeue(&mut self) -> Option<T>
Single-consumer pop. Returns None when the ring is empty.
Examples found in repository?
199fn bounded_inbox_backpressure() {
200 use subms_mpsc_queue::BoundedMpscQueue;
201
202 println!("\n== bounded: a fixed-capacity inbox that pushes back ==");
203 let mut inbox: BoundedMpscQueue<u64> = BoundedMpscQueue::new(4);
204 let cap = inbox.capacity();
205
206 let mut accepted = 0usize;
207 let mut rejected = 0usize;
208 for seq in 0..cap + 2 {
209 match inbox.try_enqueue(order_id(0, seq)) {
210 Ok(()) => accepted += 1,
211 Err(_order) => rejected += 1,
212 }
213 }
214 println!(" capacity {cap}: accepted {accepted}, shed {rejected} while full");
215 assert_eq!(accepted, cap, "accepts exactly one full ring");
216 assert_eq!(rejected, 2, "the overflow is handed back, not queued");
217 assert!(inbox.is_full());
218
219 // Drain one, and a previously-rejected order now fits.
220 assert!(
221 inbox.try_dequeue().is_some(),
222 "match loop consumes one order"
223 );
224 assert!(
225 inbox.try_enqueue(order_id(0, 99)).is_ok(),
226 "a freed slot reopens the inbox"
227 );
228
229 // The two monotonic cursors are what a health check scrapes: their
230 // difference is inbox lag, and each on its own gives a rate between polls.
231 println!(
232 " producer index {} - consumer index {} = lag {}",
233 inbox.producer_index(),
234 inbox.consumer_index(),
235 inbox.len()
236 );
237 assert_eq!(inbox.producer_index() - inbox.consumer_index(), inbox.len());
238}More examples
195fn main() -> io::Result<()> {
196 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
197 .join("..")
198 .join(".subms")
199 .join("features")
200 .join("rust.json");
201 let existing = std::fs::read_to_string(&path).unwrap_or_default();
202 let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
203 // Stamp the box these numbers came from. The bench runs wherever it is
204 // invoked, so an unstamped manifest is indistinguishable from a fleet
205 // capture; the renderer will not publish one it cannot attribute.
206 let (source, instance) = SubMsP99Source::from_env();
207 manifest.set_p99_source(source, instance.as_deref());
208
209 // Optional, off by default: pin this thread to one core for the whole run.
210 // On a heterogeneous laptop the scheduler moves the bench between core
211 // clusters and every measurement lands in one of two clock states 1.31x
212 // apart - a spread three times wider than the deltas being classified, and
213 // large enough on its own to flip a feature between auxiliary and hot-path.
214 // Pinned, the same sweep repeats to within 1%. Left OFF by default because a
215 // fleet box isolates cores outside the process, and pinning from in here
216 // would override that placement with a core the orchestrator did not choose.
217 #[cfg(feature = "affinity")]
218 if let Some(core) = std::env::var("SUBMS_PIN").ok().and_then(|v| v.parse().ok()) {
219 let _ = subms_mpsc_queue::set_affinity(&[core]);
220 }
221
222 // Burn before the first measurement, not just before each one. Every
223 // `batched` call warms itself, but the FIRST measurement in the process pays
224 // a ramp the per-measurement warm sits inside rather than absorbs, and the
225 // sweep runs smallest-first: without this the base curve read 71800 / 45300 /
226 // 46500 ns, a 1.6x fall with size that is the process settling, not the
227 // queue.
228 {
229 let mut q = filled(CANON);
230 let start = std::time::Instant::now();
231 while (start.elapsed().as_nanos() as u64) < BURN_NANOS {
232 for i in 0..ITEMS_PER_SAMPLE {
233 q.push(i as u64);
234 black_box(q.try_pop());
235 }
236 }
237 }
238
239 // The baseline: the base queue's push + try_pop round trip. Swept as well as
240 // sampled, because whether queue depth moves the BASE op is the context
241 // every feature curve is read against.
242 let base_sweep = sweep("base/push+pop", |n| {
243 let mut q = filled(n);
244 batched(ITEMS_PER_SAMPLE, |i| {
245 q.push(i as u64);
246 black_box(q.try_pop());
247 })
248 });
249 let base_p50 = base_sweep
250 .iter()
251 .find(|(n, _)| *n == CANON)
252 .map_or(0, |(_, v)| *v);
253 eprintln!("base push+pop p50 per {ITEMS_PER_SAMPLE}-item sample: {base_p50}ns");
254
255 // ---------- bounded: fixed-capacity ring, backpressure on enqueue ----------
256 #[cfg(feature = "bounded")]
257 {
258 use subms_mpsc_queue::BoundedMpscQueue;
259 // Half full at every sweep point. Filled to a FIXED element count
260 // instead, the big rings would sit 98% empty and the enqueue would be
261 // measuring the fill fraction rather than the footprint.
262 fn ring(n: usize) -> BoundedMpscQueue<u64> {
263 let q = BoundedMpscQueue::new(n);
264 for i in 0..n / 2 {
265 let _ = q.try_enqueue(i as u64);
266 }
267 q
268 }
269 let sw = sweep("bounded/enqueue+dequeue", |n| {
270 let mut q = ring(n);
271 batched(ITEMS_PER_SAMPLE, |i| {
272 let _ = q.try_enqueue(i as u64);
273 black_box(q.try_dequeue());
274 })
275 });
276 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
277
278 let mut q = ring(CANON);
279 let mut p99 = BTreeMap::new();
280 p99.insert(
281 "enqueue".to_string(),
282 single(|st, i| {
283 st.time(|| {
284 let _ = q.try_enqueue(i as u64);
285 });
286 black_box(q.try_dequeue());
287 }),
288 );
289 p99.insert(
290 "dequeue".to_string(),
291 single(|st, i| {
292 let _ = q.try_enqueue(i as u64);
293 st.time(|| black_box(q.try_dequeue()));
294 }),
295 );
296 // The reject path, which is the reason the feature exists. The ring is
297 // filled to capacity once, OUTSIDE the timed region; every timed call
298 // then takes the full branch and hands the value back to the caller.
299 let full: BoundedMpscQueue<u64> = BoundedMpscQueue::new(CANON);
300 while full.try_enqueue(0).is_ok() {}
301 p99.insert(
302 "enqueue_full".to_string(),
303 single(|st, i| {
304 st.time(|| {
305 let _ = full.try_enqueue(i as u64);
306 });
307 }),
308 );
309 manifest.set_feature("bounded", cat, &p99, &reason);
310 }
311
312 // ---------- mpmc: bounded ring, sequence CAS on both ends ----------
313 #[cfg(feature = "mpmc")]
314 {
315 use subms_mpsc_queue::MpmcQueue;
316 fn ring(n: usize) -> MpmcQueue<u64> {
317 let q = MpmcQueue::new(n);
318 for i in 0..n / 2 {
319 let _ = q.try_enqueue(i as u64);
320 }
321 q
322 }
323 // Uncontended, so every CAS succeeds first try. That is the figure the
324 // category is about: what the multi-consumer claim costs a queue that is
325 // NOT contended, which is the state a well-sized pipeline runs in.
326 let sw = sweep("mpmc/enqueue+dequeue", |n| {
327 let q = ring(n);
328 batched(ITEMS_PER_SAMPLE, |i| {
329 let _ = q.try_enqueue(i as u64);
330 black_box(q.try_dequeue());
331 })
332 });
333 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
334
335 let q = ring(CANON);
336 let mut p99 = BTreeMap::new();
337 p99.insert(
338 "enqueue".to_string(),
339 single(|st, i| {
340 st.time(|| {
341 let _ = q.try_enqueue(i as u64);
342 });
343 black_box(q.try_dequeue());
344 }),
345 );
346 p99.insert(
347 "dequeue".to_string(),
348 single(|st, i| {
349 let _ = q.try_enqueue(i as u64);
350 st.time(|| black_box(q.try_dequeue()));
351 }),
352 );
353 manifest.set_feature("mpmc", cat, &p99, &reason);
354 }
355
356 // ---------- batch: drain up to BATCH items behind one acquire fence ----------
357 #[cfg(feature = "batch")]
358 {
359 use subms_mpsc_queue::BatchMpscQueue;
360 fn filled_batch(n: usize) -> BatchMpscQueue<u64> {
361 let q = BatchMpscQueue::new();
362 for i in 0..n {
363 q.push(i as u64);
364 }
365 q
366 }
367 // A sample moves ITEMS_PER_SAMPLE items either way; only the call width
368 // differs. That is why the reps count is divided rather than the batch
369 // grown - growing it would sweep the batch size, and the number would
370 // stop being comparable to the base round trip.
371 let sw = sweep("batch/push+dequeue_batch", |n| {
372 let mut q = filled_batch(n);
373 let mut buf: Vec<Option<u64>> = (0..BATCH).map(|_| None).collect();
374 batched(ITEMS_PER_SAMPLE / BATCH, |i| {
375 for j in 0..BATCH {
376 q.push((i + j) as u64);
377 }
378 black_box(q.try_dequeue_batch(&mut buf));
379 })
380 });
381 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
382
383 let mut q = filled_batch(CANON);
384 let mut buf: Vec<Option<u64>> = (0..BATCH).map(|_| None).collect();
385 let mut p99 = BTreeMap::new();
386 // The refill is outside the timed region: timing it would put a BATCH of
387 // pushes inside the drain's number and the stage would stop being a
388 // drain figure at all.
389 p99.insert(
390 "dequeue_batch".to_string(),
391 single(|st, i| {
392 st.time(|| black_box(q.try_dequeue_batch(&mut buf)));
393 for j in 0..BATCH {
394 q.push((i + j) as u64);
395 }
396 }),
397 );
398 p99.insert(
399 "enqueue".to_string(),
400 single(|st, i| {
401 st.time(|| q.push(i as u64));
402 let _ = q.try_dequeue_batch(&mut buf[..1]);
403 }),
404 );
405 // The producer mirror: BATCH items published behind one head swap. The
406 // drain that puts the queue back is outside the timed region for the
407 // same reason the refill is above.
408 p99.insert(
409 "enqueue_batch".to_string(),
410 single(|st, i| {
411 let base = i as u64;
412 st.time(|| black_box(q.push_batch(base..base + BATCH as u64)));
413 let _ = q.try_dequeue_batch(&mut buf);
414 }),
415 );
416 manifest.set_feature("batch", cat, &p99, &reason);
417 }
418
419 // ---------- metrics: relaxed atomic counters around each op ----------
420 #[cfg(feature = "metrics")]
421 {
422 use subms_mpsc_queue::MetricsMpscQueue;
423 fn filled_metrics(n: usize) -> MetricsMpscQueue<u64> {
424 let q = MetricsMpscQueue::new();
425 for i in 0..n {
426 q.push(i as u64);
427 }
428 q
429 }
430 let sw = sweep("metrics/push+pop", |n| {
431 let mut q = filled_metrics(n);
432 batched(ITEMS_PER_SAMPLE, |i| {
433 q.push(i as u64);
434 black_box(q.try_pop());
435 })
436 });
437 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
438
439 let mut q = filled_metrics(CANON);
440 let mut p99 = BTreeMap::new();
441 p99.insert(
442 "enqueue".to_string(),
443 single(|st, i| {
444 st.time(|| q.push(i as u64));
445 black_box(q.try_pop());
446 }),
447 );
448 p99.insert(
449 "dequeue".to_string(),
450 single(|st, i| {
451 q.push(i as u64);
452 st.time(|| black_box(q.try_pop()));
453 }),
454 );
455 p99.insert(
456 "snapshot".to_string(),
457 single(|st, _| {
458 st.time(|| black_box(q.snapshot()));
459 }),
460 );
461 manifest.set_feature("metrics", cat, &p99, &reason);
462 }
463
464 // ---------- affinity: pin the calling thread, once, at startup ----------
465 // Runs LAST because measuring it pins THIS process to core 0, and every
466 // number taken afterwards would be a number taken on one core.
467 #[cfg(feature = "affinity")]
468 {
469 use subms_mpsc_queue::set_affinity;
470 // Swept over the same axis to show what it is: a call that touches no
471 // queue state and cannot move with queue size. PINNED auxiliary rather
472 // than left to the base-delta test, which would see a syscall costing
473 // more than an enqueue and call it hot-path. It is not on the hot path at
474 // any price - `set_affinity` is called once per thread at startup and
475 // appears in neither `push` nor `try_pop`. The two ports are not even
476 // measuring the same thing: Rust issues a real `SetThreadAffinityMask` /
477 // `sched_setaffinity`, while the Java sibling validates its argument and
478 // returns UNSUPPORTED because the stock JDK has no pinning API. The
479 // per-call figure, not the sample, is the interpretable one and it is in
480 // `p99ByStage`.
481 let sw = sweep("affinity/set_affinity", |_| {
482 batched(ITEMS_PER_SAMPLE, |_| {
483 let _ = set_affinity(&[0]);
484 })
485 });
486 let (cat, reason) = classify_feature(
487 &sw,
488 Some(base_p50),
489 Some(subms::SubMsFeatureCategory::Auxiliary),
490 );
491
492 let mut p99 = BTreeMap::new();
493 p99.insert(
494 "set_affinity".to_string(),
495 single(|st, _| {
496 st.time(|| {
497 let _ = set_affinity(&[0]);
498 });
499 }),
500 );
501 manifest.set_feature("affinity", cat, &p99, &reason);
502
503 let cores: Vec<usize> = (0..std::thread::available_parallelism()
504 .map_or(1, std::num::NonZeroUsize::get))
505 .collect();
506 let _ = set_affinity(&cores);
507 }
508
509 std::fs::create_dir_all(path.parent().unwrap())?;
510 std::fs::write(&path, manifest.to_json())?;
511 io::stdout().write_all(manifest.to_json().as_bytes())?;
512 Ok(())
513}Sourcepub fn peek(&mut self) -> Option<&T>
pub fn peek(&mut self) -> Option<&T>
Borrow the next value without consuming it. None when the ring is
empty. Consumer-side only.
Sourcepub fn clear(&mut self) -> usize
pub fn clear(&mut self) -> usize
Drop everything currently readable and return the count. Producers keep publishing throughout, so the ring is not guaranteed empty on return. Consumer-side only.
Sourcepub fn len(&self) -> usize
pub fn len(&self) -> usize
Best-effort length. Approximate under producer contention.
Examples found in repository?
199fn bounded_inbox_backpressure() {
200 use subms_mpsc_queue::BoundedMpscQueue;
201
202 println!("\n== bounded: a fixed-capacity inbox that pushes back ==");
203 let mut inbox: BoundedMpscQueue<u64> = BoundedMpscQueue::new(4);
204 let cap = inbox.capacity();
205
206 let mut accepted = 0usize;
207 let mut rejected = 0usize;
208 for seq in 0..cap + 2 {
209 match inbox.try_enqueue(order_id(0, seq)) {
210 Ok(()) => accepted += 1,
211 Err(_order) => rejected += 1,
212 }
213 }
214 println!(" capacity {cap}: accepted {accepted}, shed {rejected} while full");
215 assert_eq!(accepted, cap, "accepts exactly one full ring");
216 assert_eq!(rejected, 2, "the overflow is handed back, not queued");
217 assert!(inbox.is_full());
218
219 // Drain one, and a previously-rejected order now fits.
220 assert!(
221 inbox.try_dequeue().is_some(),
222 "match loop consumes one order"
223 );
224 assert!(
225 inbox.try_enqueue(order_id(0, 99)).is_ok(),
226 "a freed slot reopens the inbox"
227 );
228
229 // The two monotonic cursors are what a health check scrapes: their
230 // difference is inbox lag, and each on its own gives a rate between polls.
231 println!(
232 " producer index {} - consumer index {} = lag {}",
233 inbox.producer_index(),
234 inbox.consumer_index(),
235 inbox.len()
236 );
237 assert_eq!(inbox.producer_index() - inbox.consumer_index(), inbox.len());
238}pub fn is_empty(&self) -> bool
Sourcepub fn is_full(&self) -> bool
pub fn is_full(&self) -> bool
Best-effort fullness. A true can go stale the instant the consumer
drains a slot, so branch on Self::try_enqueue instead of this when
the answer decides whether a push lands.
Examples found in repository?
199fn bounded_inbox_backpressure() {
200 use subms_mpsc_queue::BoundedMpscQueue;
201
202 println!("\n== bounded: a fixed-capacity inbox that pushes back ==");
203 let mut inbox: BoundedMpscQueue<u64> = BoundedMpscQueue::new(4);
204 let cap = inbox.capacity();
205
206 let mut accepted = 0usize;
207 let mut rejected = 0usize;
208 for seq in 0..cap + 2 {
209 match inbox.try_enqueue(order_id(0, seq)) {
210 Ok(()) => accepted += 1,
211 Err(_order) => rejected += 1,
212 }
213 }
214 println!(" capacity {cap}: accepted {accepted}, shed {rejected} while full");
215 assert_eq!(accepted, cap, "accepts exactly one full ring");
216 assert_eq!(rejected, 2, "the overflow is handed back, not queued");
217 assert!(inbox.is_full());
218
219 // Drain one, and a previously-rejected order now fits.
220 assert!(
221 inbox.try_dequeue().is_some(),
222 "match loop consumes one order"
223 );
224 assert!(
225 inbox.try_enqueue(order_id(0, 99)).is_ok(),
226 "a freed slot reopens the inbox"
227 );
228
229 // The two monotonic cursors are what a health check scrapes: their
230 // difference is inbox lag, and each on its own gives a rate between polls.
231 println!(
232 " producer index {} - consumer index {} = lag {}",
233 inbox.producer_index(),
234 inbox.consumer_index(),
235 inbox.len()
236 );
237 assert_eq!(inbox.producer_index() - inbox.consumer_index(), inbox.len());
238}