#![allow(dead_code)]
use std::fmt::Write;
use std::future::{Future, ready};
use std::hint::black_box;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::convert::Infallible;
use gungraun::{Callgrind, Dhat, DhatMetric, EntryPoint, EventKind, LibraryBenchmarkConfig};
use ruststream::memory::prelude::*;
use ruststream::memory::{ConnectedMemoryBroker, MemoryBroker, MemoryPublisher};
use ruststream::runtime::{BrokerScope, Identity, RunningApp};
use ruststream::{HeaderMap, Lend, OutgoingMessage, PairError, PublishPolicy, Publisher};
use serde::Deserialize;
use tokio::runtime::{Builder, Runtime};
use tokio::sync::Notify;
pub const INPUT: &str = "orders";
#[derive(Debug, Clone, Copy)]
pub struct SinkPublish;
impl PublishPolicy<ConnectedMemoryBroker> for SinkPublish {
type Live = Sink;
fn pair(
self,
_connected: &ConnectedMemoryBroker,
) -> impl Future<Output = Result<Sink, PairError>> {
ready(Ok(Sink))
}
}
#[derive(Debug, Clone, Copy)]
pub struct Sink;
impl Publisher for Sink {
type Payload = Lend;
type Error = Infallible;
type Options = ();
fn publish(
&self,
msg: OutgoingMessage<'_, &[u8]>,
_options: Option<&()>,
) -> impl Future<Output = Result<(), Infallible>> {
let (name, payload, headers) = msg.into_parts();
black_box((name.len(), payload, headers.len()));
ready(Ok(()))
}
}
const ID: u64 = 1_000_000;
const QUANTITY: u32 = 37;
#[derive(Debug, Deserialize)]
pub struct Order {
pub id: u64,
pub quantity: u32,
}
pub const MESSAGES: usize = messages(option_env!("RUSTSTREAM_BENCH_MESSAGES"));
const DEFAULT_MESSAGES: usize = 1_000;
const fn messages(configured: Option<&str>) -> usize {
let Some(text) = configured else {
return DEFAULT_MESSAGES;
};
let bytes = text.as_bytes();
let mut count = 0usize;
let mut index = 0;
while index < bytes.len() {
let digit = bytes[index];
assert!(
digit.is_ascii_digit(),
"RUSTSTREAM_BENCH_MESSAGES must be a positive number of deliveries"
);
count = count * 10 + (digit - b'0') as usize;
index += 1;
}
assert!(
count > 0,
"RUSTSTREAM_BENCH_MESSAGES must be a positive number of deliveries"
);
count
}
pub fn config(steady: u64, cold: u64) -> LibraryBenchmarkConfig {
config_every(steady, 1, cold)
}
pub fn config_every(steady: u64, per: u64, cold: u64) -> LibraryBenchmarkConfig {
let mut config = LibraryBenchmarkConfig::default();
config
.tool(callgrind().soft_limits([(EventKind::Ir, 2f64)]))
.tool(dhat().hard_limits([(DhatMetric::TotalBlocks, blocks(steady, per, cold))]));
config
}
const fn blocks(steady: u64, per: u64, cold: u64) -> u64 {
cold + (steady * 2 * MESSAGES as u64).div_ceil(per)
}
pub fn config_ungated() -> LibraryBenchmarkConfig {
let mut config = LibraryBenchmarkConfig::default();
config.tool(callgrind()).tool(dhat());
config
}
fn callgrind() -> Callgrind {
let mut callgrind = Callgrind::with_args([
"--collect-atstart=no",
&format!("--toggle-collect={REGION}"),
]);
callgrind.entry_point(EntryPoint::None);
callgrind
}
#[inline(never)]
pub fn measure<T>(body: impl FnOnce() -> T) -> T {
black_box(body())
}
fn dhat() -> Dhat {
let mut dhat = Dhat::with_args(["--num-callers=128"]);
dhat.entry_point(EntryPoint::Custom(REGION.to_owned()));
dhat
}
const REGION: &str = "*common::measure*";
pub fn runtime() -> Runtime {
Builder::new_current_thread()
.enable_all()
.build()
.expect("current-thread runtime")
}
pub fn worker_runtime() -> Runtime {
Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.expect("multi-thread runtime")
}
#[derive(Clone, Debug)]
pub struct Latch(Arc<Inner>);
#[derive(Debug)]
struct Inner {
remaining: AtomicUsize,
drained: Notify,
}
impl Default for Latch {
fn default() -> Self {
Self(Arc::new(Inner {
remaining: AtomicUsize::new(0),
drained: Notify::new(),
}))
}
}
impl Latch {
pub fn expect(&self, count: usize) {
self.0.remaining.store(count, Ordering::Release);
}
pub fn arrived(&self) {
if self.0.remaining.fetch_sub(1, Ordering::Relaxed) == 1 {
self.0.drained.notify_one();
}
}
pub fn remaining(&self) -> usize {
self.0.remaining.load(Ordering::Acquire)
}
pub async fn drained(&self) {
while self.0.remaining.load(Ordering::Acquire) > 0 {
self.0.drained.notified().await;
}
}
}
pub fn json_body(size: usize) -> Vec<u8> {
let mut body = format!("{{\"id\":{ID},\"quantity\":{QUANTITY}");
let mut field = 0u32;
while body.len() + 2 < size {
write!(body, ",\"f{field}\":\"{field:016}\"").expect("writing to a String");
field += 1;
}
body.push('}');
body.into_bytes()
}
pub fn fill_with_headers(
publisher: &MemoryPublisher,
runtime: &Runtime,
name: &str,
count: usize,
headers: &[(&'static str, &str)],
) {
let body = json_body(0);
runtime.block_on(async move {
for _ in 0..count {
let mut map = HeaderMap::new();
for (key, value) in headers {
map.insert(Str::from_static(key), (*value).to_owned());
}
publisher
.publish(OutgoingMessage::new(name, &body).with_headers(map), None)
.await
.expect("in-process publish");
}
});
}
pub fn fill(publisher: &MemoryPublisher, runtime: &Runtime, name: &str, count: usize, size: usize) {
let body = json_body(size);
runtime.block_on(async move {
for _ in 0..count {
publisher
.publish(OutgoingMessage::new(name, &body), None)
.await
.expect("in-process publish");
}
});
}
pub struct Pending {
pub runtime: Runtime,
pub latch: Latch,
pub broker: MemoryBroker,
start: Box<dyn FnOnce(&Runtime) -> RunningApp>,
pub messages: usize,
pub size: usize,
pub expected: usize,
pub headers: &'static [(&'static str, &'static str)],
}
pub type Mount<'a> = &'a mut BrokerScope<MemoryBroker, Identity, (), Latch>;
pub fn pending(messages: usize, size: usize, mount: impl FnOnce(Mount<'_>)) -> Pending {
pending_on(runtime(), messages, size, mount)
}
pub fn pending_on(
runtime: Runtime,
messages: usize,
size: usize,
mount: impl FnOnce(Mount<'_>),
) -> Pending {
let latch = Latch::default();
let broker = MemoryBroker::new();
let state = latch.clone();
let app = RustStream::new(AppInfo::new("bench", "0.0.0"))
.on_startup(async move |()| Ok::<_, Infallible>(state))
.with_broker(broker.clone(), mount);
built(runtime, latch, broker, app, messages, size)
}
pub fn pending_with_headers(
messages: usize,
headers: &'static [(&'static str, &'static str)],
mount: impl FnOnce(Mount<'_>),
) -> Pending {
Pending {
headers,
..pending(messages, 0, mount)
}
}
pub fn built<Layers, State, Pipeline, Phase>(
runtime: Runtime,
latch: Latch,
broker: MemoryBroker,
app: RustStream<Layers, State, Pipeline, Phase>,
messages: usize,
size: usize,
) -> Pending
where
Layers: Send + 'static,
State: Send + Sync + 'static,
Pipeline: 'static,
Phase: 'static,
{
Pending {
runtime,
latch,
broker,
start: Box::new(move |runtime| runtime.block_on(app.start()).expect("the service starts")),
messages,
size,
expected: messages,
headers: &[],
}
}
impl Pending {
#[must_use]
pub fn ready(self) -> Ready {
let running = (self.start)(&self.runtime);
self.latch.expect(self.expected);
if self.headers.is_empty() {
fill(
&self.broker.publisher(),
&self.runtime,
INPUT,
self.messages,
self.size,
);
} else {
fill_with_headers(
&self.broker.publisher(),
&self.runtime,
INPUT,
self.messages,
self.headers,
);
}
Ready {
runtime: self.runtime,
latch: self.latch,
_app: running,
}
}
#[must_use]
pub fn started(self) -> Started {
let running = (self.start)(&self.runtime);
self.latch.expect(self.expected);
Started {
publisher: self.broker.publisher(),
body: json_body(self.size),
messages: self.messages,
runtime: self.runtime,
latch: self.latch,
_app: running,
}
}
}
pub struct Started {
pub runtime: Runtime,
latch: Latch,
publisher: MemoryPublisher,
body: Vec<u8>,
messages: usize,
_app: RunningApp,
}
impl Started {
pub fn publish_and_drain(&self) {
self.runtime.block_on(async {
for _ in 0..self.messages {
self.publisher
.publish(OutgoingMessage::new(INPUT, &self.body), None)
.await
.expect("in-process publish");
}
self.latch.drained().await;
});
}
}
pub struct Ready {
pub runtime: Runtime,
pub latch: Latch,
_app: RunningApp,
}
pub fn start_and_drain(pending: Pending) {
let Pending {
runtime,
latch,
broker,
start,
messages,
size,
expected,
headers,
} = pending;
let running = measure(|| start(&runtime));
latch.expect(expected);
if headers.is_empty() {
fill(&broker.publisher(), &runtime, INPUT, messages, size);
} else {
fill_with_headers(&broker.publisher(), &runtime, INPUT, messages, headers);
}
assert_eq!(
latch.remaining(),
expected,
"the queue was consumed while it was being filled, so the measured region would be short"
);
measure(|| runtime.block_on(latch.drained()));
drop(running);
}
#[cfg(feature = "testing")]
compile_error!(
"benchmarks must be built without the `testing` feature; run them through `just bench`"
);