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
impl ParkStrategy
Sourcepub fn pair() -> (Self, Self)
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
impl WaitStrategy for ParkStrategy
Auto Trait Implementations§
impl !RefUnwindSafe for ParkStrategy
impl !UnwindSafe for ParkStrategy
impl Freeze for ParkStrategy
impl Send for ParkStrategy
impl Sync for ParkStrategy
impl Unpin for ParkStrategy
impl UnsafeUnpin for ParkStrategy
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