use dbsp::circuit::Circuit;
use dbsp::circuit::metadata::{
CIRCUIT_CPU_TIME_SECONDS, CIRCUIT_NONBLOCKING_PERCENT, CIRCUIT_RUNTIME_SECONDS,
CIRCUIT_WAIT_TIME_SECONDS, MetaItem, MetricId,
};
use dbsp::typed_batch::OrdZSet;
use dbsp::utils::Tup2;
use dbsp::{DBSPHandle, Runtime, operator::Generator};
use std::thread::sleep;
use std::time::Duration;
fn duration_metric(handle: &mut DBSPHandle, metric: &MetricId) -> f64 {
let profile = handle.retrieve_profile().unwrap();
profile
.worker_profiles
.iter()
.flat_map(|w| w.attribute_profile(metric).into_values())
.map(|item| match item {
MetaItem::Duration(d) => d.as_secs_f64(),
other => panic!("expected a duration, got {other:?}"),
})
.sum()
}
fn percent_metric(handle: &mut DBSPHandle, metric: &MetricId) -> Vec<(u64, u64)> {
let profile = handle.retrieve_profile().unwrap();
profile
.worker_profiles
.iter()
.flat_map(|w| w.attribute_profile(metric).into_values())
.map(|item| match item {
MetaItem::Percent {
numerator,
denominator,
} => (numerator, denominator),
other => panic!("expected a percent, got {other:?}"),
})
.collect()
}
fn skewed_circuit(workers: usize, stall: Duration, steps: usize) -> DBSPHandle {
let (mut handle, _) = Runtime::init_circuit(workers, move |circuit| {
let source = circuit.add_source(Generator::new(move || {
if Runtime::worker_index() == 0 && !stall.is_zero() {
sleep(stall);
}
let keys: Vec<Tup2<u64, i64>> = (0..64)
.map(|k| Tup2(k * 7 + Runtime::worker_index() as u64, 1i64))
.collect();
OrdZSet::from_keys((), keys)
}));
source.shard().integrate().apply(|_| ());
Ok(())
})
.unwrap();
handle.enable_cpu_profiler().unwrap();
handle.start_transaction().unwrap();
for _ in 0..steps {
handle.step().unwrap();
}
handle.commit_transaction().unwrap();
handle
}
#[test]
fn stalled_worker_is_reported_as_circuit_wait_time() {
const WORKERS: usize = 4;
const STEPS: usize = 20;
const STALL: Duration = Duration::from_millis(5);
let mut handle = skewed_circuit(WORKERS, STALL, STEPS);
let wait = duration_metric(&mut handle, &CIRCUIT_WAIT_TIME_SECONDS);
let stalled = STALL.as_secs_f64() * STEPS as f64;
let floor = stalled * (WORKERS - 1) as f64 * 0.5;
assert!(
wait > floor,
"circuit wait time {wait:.3}s should exceed {floor:.3}s with a \
{STALL:?} stall on 1 of {WORKERS} workers over {STEPS} steps"
);
handle.kill().unwrap();
}
#[test]
fn a_circuit_with_nothing_to_wait_for_reports_no_wait_time() {
let mut handle = skewed_circuit(1, Duration::ZERO, 20);
let wait = duration_metric(&mut handle, &CIRCUIT_WAIT_TIME_SECONDS);
let runtime = duration_metric(&mut handle, &CIRCUIT_RUNTIME_SECONDS);
assert!(
wait < runtime * 0.5,
"a single worker with no exchange waited {wait:.3}s of {runtime:.3}s"
);
handle.kill().unwrap();
}
#[test]
fn step_time_decomposes_into_cpu_and_wait() {
let mut handle = skewed_circuit(4, Duration::from_millis(2), 20);
let runtime = duration_metric(&mut handle, &CIRCUIT_RUNTIME_SECONDS);
let cpu = duration_metric(&mut handle, &CIRCUIT_CPU_TIME_SECONDS);
let wait = duration_metric(&mut handle, &CIRCUIT_WAIT_TIME_SECONDS);
assert!(cpu > 0.0, "circuit cpu time should not be zero");
assert!(
cpu + wait <= runtime * 1.1,
"cpu {cpu:.3}s + wait {wait:.3}s should fit within runtime {runtime:.3}s"
);
for (numerator, denominator) in percent_metric(&mut handle, &CIRCUIT_NONBLOCKING_PERCENT) {
assert!(denominator > 0, "nonblocking percent has no denominator");
assert!(
numerator <= denominator,
"nonblocking percent {numerator}/{denominator} exceeds 100%"
);
}
handle.kill().unwrap();
}