Skip to main content

ParkStrategy

Struct ParkStrategy 

Source
pub struct ParkStrategy { /* private fields */ }
Expand description

Park the calling thread; the opposite-end wrapper unparks it on progress. Lowest CPU; adds wakeup latency.

Both ends share a Parker instance via Arc so the producer can wake the consumer and vice versa.

Implementations§

Source§

impl ParkStrategy

Source

pub fn pair() -> (Self, Self)

Build a matched (producer_strategy, consumer_strategy) pair that share a parking state.

Examples found in repository?
examples/perf_features.rs (line 280)
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}

Trait Implementations§

Source§

impl WaitStrategy for ParkStrategy

Source§

fn wait(&mut self)

Wait briefly. Called between retries when the ring isn’t ready.
Source§

fn signal(&self)

Signal the waiter. Default is a no-op; only ParkStrategy overrides.

Auto Trait Implementations§

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.