use crate::circuit::{GlobalNodeId, RootCircuit, ThreadCpuTime, trace::SchedulerEvent};
use hashbrown::HashMap;
use std::{
cell::{Cell, RefCell},
rc::Rc,
sync::{
Arc,
atomic::{AtomicI32, AtomicU64, AtomicUsize, Ordering},
},
time::{Duration, Instant},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(usize)]
pub enum ParkReason {
Unattributed = 0,
MergeBackpressure = 1,
Peers = 2,
OperatorPending = 3,
Scheduler = 4,
StorageRead = 5,
StorageWrite = 6,
StorageSync = 7,
StorageMetadata = 8,
Network = 9,
}
impl ParkReason {
pub const COUNT: usize = 10;
pub const ALL: [Self; Self::COUNT] = [
Self::Unattributed,
Self::MergeBackpressure,
Self::Peers,
Self::OperatorPending,
Self::Scheduler,
Self::StorageRead,
Self::StorageWrite,
Self::StorageSync,
Self::StorageMetadata,
Self::Network,
];
const PRIORITY: [Self; Self::COUNT - 1] = [
Self::StorageRead,
Self::StorageWrite,
Self::StorageSync,
Self::StorageMetadata,
Self::MergeBackpressure,
Self::Network,
Self::Peers,
Self::OperatorPending,
Self::Scheduler,
];
pub fn name(self) -> &'static str {
match self {
Self::Unattributed => "unattributed",
Self::MergeBackpressure => "merge_backpressure",
Self::Peers => "peers",
Self::OperatorPending => "operator_pending",
Self::Scheduler => "scheduler",
Self::StorageRead => "storage_read",
Self::StorageWrite => "storage_write",
Self::StorageSync => "storage_sync",
Self::StorageMetadata => "storage_metadata",
Self::Network => "network",
}
}
}
thread_local! {
static PARKING_FOR: Arc<ParkDeclarations> = Arc::new(ParkDeclarations::new());
static BLOCKING_DEPTH: Cell<u32> = const { Cell::new(0) };
static RUNTIME_IDLE: RefCell<Option<RuntimeIdle>> = const { RefCell::new(None) };
}
#[cfg(test)]
pub(crate) fn current_runtime_idle() -> Option<RuntimeIdle> {
RUNTIME_IDLE.with(|idle| idle.borrow().clone())
}
struct ParkDeclarations([AtomicI32; ParkReason::COUNT]);
impl ParkDeclarations {
fn new() -> Self {
Self(std::array::from_fn(|_| AtomicI32::new(0)))
}
fn count(&self, reason: ParkReason) -> i32 {
self.0[reason as usize].load(Ordering::Relaxed)
}
}
pub(crate) fn current_park_reason() -> ParkReason {
PARKING_FOR.with(|declared| {
ParkReason::PRIORITY
.into_iter()
.find(|reason| declared.count(*reason) > 0)
.unwrap_or(ParkReason::Unattributed)
})
}
#[must_use = "the declaration lasts only as long as the guard"]
pub struct ParkingFor {
reason: ParkReason,
declared: Arc<ParkDeclarations>,
}
impl ParkingFor {
pub fn new(reason: ParkReason) -> Self {
let declared = PARKING_FOR.with(Arc::clone);
declared.0[reason as usize].fetch_add(1, Ordering::Relaxed);
Self { reason, declared }
}
}
impl Drop for ParkingFor {
fn drop(&mut self) {
let was = self.declared.0[self.reason as usize].fetch_sub(1, Ordering::Relaxed);
debug_assert!(was > 0, "{} was given back twice", self.reason.name());
}
}
#[must_use = "the time is charged when the guard drops"]
pub struct BlockingFor {
reason: ParkReason,
start: Option<(Instant, ThreadCpuTime)>,
}
impl BlockingFor {
pub fn new(reason: ParkReason) -> Self {
let outermost = BLOCKING_DEPTH.with(|depth| {
let depth_before = depth.get();
depth.set(depth_before + 1);
depth_before == 0
});
let start = (outermost && RUNTIME_IDLE.with(|idle| idle.borrow().is_some()))
.then(|| (Instant::now(), ThreadCpuTime::now()));
Self { reason, start }
}
}
impl Drop for BlockingFor {
fn drop(&mut self) {
BLOCKING_DEPTH.with(|depth| {
debug_assert!(depth.get() > 0, "a blocking guard was dropped twice");
depth.set(depth.get().saturating_sub(1));
});
if let Some((start, cpu)) = &self.start {
let blocked = start.elapsed().saturating_sub(cpu.elapsed());
RUNTIME_IDLE.with(|idle| {
if let Some(idle) = idle.borrow().as_ref() {
idle.blocked(self.reason, blocked);
}
});
}
}
}
#[derive(Clone, Debug)]
pub struct RuntimeIdle {
base: Instant,
park_start: Arc<AtomicU64>,
total: Arc<AtomicU64>,
by_reason: Arc<[AtomicU64; ParkReason::COUNT]>,
park_reason: Arc<AtomicUsize>,
}
impl Default for RuntimeIdle {
fn default() -> Self {
Self::new()
}
}
impl RuntimeIdle {
pub fn new() -> Self {
Self {
base: Instant::now(),
park_start: Arc::new(AtomicU64::new(0)),
total: Arc::new(AtomicU64::new(0)),
by_reason: Arc::new([const { AtomicU64::new(0) }; ParkReason::COUNT]),
park_reason: Arc::new(AtomicUsize::new(0)),
}
}
fn now(&self) -> u64 {
self.base.elapsed().as_nanos() as u64
}
pub fn park(&self) {
self.park_reason
.store(current_park_reason() as usize, Ordering::Release);
self.park_start.store(self.now(), Ordering::Release);
}
pub fn unpark(&self) {
let start = self.park_start.swap(0, Ordering::AcqRel);
if start != 0 {
let parked = self.now().saturating_sub(start);
self.total.fetch_add(parked, Ordering::Release);
self.by_reason[self.park_reason.load(Ordering::Acquire)]
.fetch_add(parked, Ordering::Release);
}
}
pub fn blocked(&self, reason: ParkReason, blocked: Duration) {
let nanos = blocked.as_nanos() as u64;
self.total.fetch_add(nanos, Ordering::Release);
self.by_reason[reason as usize].fetch_add(nanos, Ordering::Release);
}
pub fn install_on_this_thread(&self) {
RUNTIME_IDLE.with(|idle| *idle.borrow_mut() = Some(self.clone()));
}
pub fn total(&self) -> Duration {
Duration::from_nanos(self.total.load(Ordering::Acquire))
}
pub fn by_reason(&self) -> [Duration; ParkReason::COUNT] {
std::array::from_fn(|index| {
Duration::from_nanos(self.by_reason[index].load(Ordering::Acquire))
})
}
}
#[derive(Clone, Default, Debug)]
pub struct OperatorCPUProfile {
invocations: usize,
real_time: Duration,
cpu_time: Duration,
}
impl OperatorCPUProfile {
pub fn add_event(&mut self, real_time: Duration, cpu_time: Duration) {
self.invocations += 1;
self.real_time += real_time;
self.cpu_time += cpu_time;
}
pub fn invocations(&self) -> usize {
self.invocations
}
pub fn real_time(&self) -> Duration {
self.real_time
}
pub fn cpu_time(&self) -> Duration {
self.cpu_time
}
}
#[derive(Clone, Default, Debug)]
pub struct CircuitCPUProfile {
pub wait_profile: OperatorCPUProfile,
pub step_profile: OperatorCPUProfile,
pub wait_by_reason: [Duration; ParkReason::COUNT],
pub idle_profile: OperatorCPUProfile,
}
#[derive(Default, Debug)]
struct CPUProfilerInner {
operators: HashMap<GlobalNodeId, OperatorCPUProfile>,
step_start_times: HashMap<GlobalNodeId, Instant>,
step_end_times: HashMap<GlobalNodeId, Instant>,
step_start_cpu: HashMap<GlobalNodeId, Duration>,
step_start_idle: HashMap<GlobalNodeId, Duration>,
step_start_idle_by_reason: HashMap<GlobalNodeId, [Duration; ParkReason::COUNT]>,
circuit_profiles: HashMap<GlobalNodeId, CircuitCPUProfile>,
runtime_idle: Option<RuntimeIdle>,
}
impl CPUProfilerInner {
fn scheduler_event(&mut self, event: &SchedulerEvent) {
match event {
SchedulerEvent::StepStart { circuit_id } => {
if let Some(end_time) = self.step_end_times.remove(*circuit_id) {
let duration = Instant::now().duration_since(end_time);
let circuit_profile = self
.circuit_profiles
.entry((*circuit_id).clone())
.or_insert_with(Default::default);
circuit_profile
.idle_profile
.add_event(duration, Duration::ZERO);
};
self.step_start_times
.insert((*circuit_id).clone(), Instant::now());
self.step_start_cpu
.insert((*circuit_id).clone(), ThreadCpuTime::now().0);
if let Some(idle) = &self.runtime_idle {
self.step_start_idle
.insert((*circuit_id).clone(), idle.total());
self.step_start_idle_by_reason
.insert((*circuit_id).clone(), idle.by_reason());
}
}
SchedulerEvent::StepEnd { circuit_id } => {
if let Some(start_time) = self.step_start_times.remove(*circuit_id) {
let duration = Instant::now().duration_since(start_time);
let cpu = self
.step_start_cpu
.remove(*circuit_id)
.map(|start| ThreadCpuTime::now().0.saturating_sub(start))
.unwrap_or_default();
let circuit_profile = self
.circuit_profiles
.entry((*circuit_id).clone())
.or_insert_with(Default::default);
circuit_profile.step_profile.add_event(duration, cpu);
if let (Some(idle), Some(before)) = (
self.runtime_idle.as_ref(),
self.step_start_idle.remove(*circuit_id),
) {
circuit_profile
.wait_profile
.add_event(idle.total().saturating_sub(before), Duration::ZERO);
if let Some(before) = self.step_start_idle_by_reason.remove(*circuit_id) {
let now = idle.by_reason();
for (total, (now, before)) in circuit_profile
.wait_by_reason
.iter_mut()
.zip(now.iter().zip(before.iter()))
{
*total += now.saturating_sub(*before);
}
}
}
};
self.step_end_times
.insert((*circuit_id).clone(), Instant::now());
}
SchedulerEvent::EvalStart { .. } => {}
SchedulerEvent::EvalEnd { node, elapsed_time } => {
let op_profile = self
.operators
.entry(node.global_id().clone())
.or_insert_with(Default::default);
op_profile.add_event(elapsed_time.real, elapsed_time.cpu);
}
_ => (),
}
}
}
#[repr(transparent)]
#[derive(Clone, Default, Debug)]
pub struct CPUProfiler(Rc<RefCell<CPUProfilerInner>>);
impl CPUProfiler {
pub fn new() -> Self {
Self::default()
}
pub fn attach(&self, circuit: &RootCircuit, handler_name: &str, runtime_idle: RuntimeIdle) {
if let Ok(mut this) = self.0.try_borrow_mut() {
this.runtime_idle = Some(runtime_idle);
}
let self_clone = self.clone();
circuit.register_scheduler_event_handler(handler_name, move |event| {
if let Ok(mut this) = self_clone.0.try_borrow_mut() {
this.scheduler_event(event);
};
});
}
pub fn operator_profile(&self, node: &GlobalNodeId) -> Option<OperatorCPUProfile> {
if let Ok(this) = self.0.try_borrow() {
this.operators.get(node).cloned()
} else {
None
}
}
pub fn circuit_profile(&self, node: &GlobalNodeId) -> Option<CircuitCPUProfile> {
if let Ok(this) = self.0.try_borrow() {
this.circuit_profiles.get(node).cloned()
} else {
None
}
}
}
#[cfg(test)]
mod test {
use super::{
BlockingFor, ParkReason, ParkingFor, RuntimeIdle, ThreadCpuTime, current_park_reason,
current_runtime_idle,
};
use std::time::{Duration, Instant};
fn parked_under(idle: &RuntimeIdle, reason: ParkReason) -> Duration {
idle.by_reason()[reason as usize]
}
fn nap() {
std::thread::sleep(Duration::from_millis(20));
}
fn spin() {
let until = Instant::now() + Duration::from_micros(50);
while Instant::now() < until {
std::hint::spin_loop();
}
}
#[test]
fn every_reason_sits_at_its_own_index() {
for (index, reason) in ParkReason::ALL.into_iter().enumerate() {
assert_eq!(reason as usize, index, "{}", reason.name());
}
}
#[test]
fn every_reason_but_the_default_can_win() {
let mut ranked = ParkReason::PRIORITY.to_vec();
ranked.sort_by_key(|reason| *reason as usize);
let declarable = ParkReason::ALL
.into_iter()
.filter(|reason| *reason != ParkReason::Unattributed)
.collect::<Vec<_>>();
assert_eq!(ranked, declarable);
}
#[test]
fn a_guard_dropped_on_another_thread_is_given_back_to_its_own() {
let parked = ParkingFor::new(ParkReason::Peers);
assert_eq!(current_park_reason(), ParkReason::Peers);
std::thread::spawn(move || {
assert_eq!(current_park_reason(), ParkReason::Unattributed);
drop(parked);
assert_eq!(current_park_reason(), ParkReason::Unattributed);
})
.join()
.unwrap();
assert_eq!(current_park_reason(), ParkReason::Unattributed);
}
#[test]
fn no_declaration_leaves_a_park_unattributed() {
assert_eq!(current_park_reason(), ParkReason::Unattributed);
}
#[test]
fn a_declaration_lasts_only_as_long_as_its_guard() {
{
let _parked = ParkingFor::new(ParkReason::Peers);
assert_eq!(current_park_reason(), ParkReason::Peers);
}
assert_eq!(current_park_reason(), ParkReason::Unattributed);
}
#[test]
fn leaving_an_inner_declaration_uncovers_the_outer_one() {
let _outer = ParkingFor::new(ParkReason::Scheduler);
{
let _inner = ParkingFor::new(ParkReason::MergeBackpressure);
assert_eq!(current_park_reason(), ParkReason::MergeBackpressure);
}
assert_eq!(current_park_reason(), ParkReason::Scheduler);
}
#[test]
fn priority_decides_between_concurrent_declarations() {
for reversed in [false, true] {
let (first, second) = if reversed {
(ParkReason::Peers, ParkReason::OperatorPending)
} else {
(ParkReason::OperatorPending, ParkReason::Peers)
};
let _first = ParkingFor::new(first);
let _second = ParkingFor::new(second);
assert_eq!(current_park_reason(), ParkReason::Peers);
}
}
#[test]
fn a_park_is_charged_to_the_reason_it_began_under() {
let idle = RuntimeIdle::new();
{
let _parked = ParkingFor::new(ParkReason::MergeBackpressure);
idle.park();
spin();
}
idle.unpark();
assert!(parked_under(&idle, ParkReason::MergeBackpressure) > Duration::ZERO);
assert_eq!(
parked_under(&idle, ParkReason::Unattributed),
Duration::ZERO
);
assert_eq!(
idle.total(),
parked_under(&idle, ParkReason::MergeBackpressure)
);
}
#[test]
fn the_breakdown_accounts_for_the_whole_total() {
let idle = RuntimeIdle::new();
for reason in ParkReason::ALL {
let _parked = (reason != ParkReason::Unattributed).then(|| ParkingFor::new(reason));
idle.park();
spin();
idle.unpark();
}
let by_reason = idle.by_reason();
assert!(by_reason.iter().all(|parked| *parked > Duration::ZERO));
assert_eq!(by_reason.iter().sum::<Duration>(), idle.total());
}
#[test]
fn a_blocking_guard_charges_its_reason() {
let idle = RuntimeIdle::new();
idle.install_on_this_thread();
{
let _blocked = BlockingFor::new(ParkReason::StorageRead);
nap();
}
{
let _parked = ParkingFor::new(ParkReason::Peers);
idle.park();
spin();
}
idle.unpark();
assert!(parked_under(&idle, ParkReason::StorageRead) > Duration::ZERO);
assert!(parked_under(&idle, ParkReason::Peers) > Duration::ZERO);
assert_eq!(idle.by_reason().iter().sum::<Duration>(), idle.total());
}
#[test]
fn a_nested_blocking_guard_charges_nothing() {
let idle = RuntimeIdle::new();
idle.install_on_this_thread();
{
let _outer = BlockingFor::new(ParkReason::StorageWrite);
nap();
{
let _inner = BlockingFor::new(ParkReason::StorageSync);
nap();
}
}
assert!(parked_under(&idle, ParkReason::StorageWrite) > Duration::ZERO);
assert_eq!(parked_under(&idle, ParkReason::StorageSync), Duration::ZERO);
assert_eq!(idle.total(), parked_under(&idle, ParkReason::StorageWrite));
}
#[test]
fn a_blocking_guard_does_not_charge_time_on_the_cpu() {
let idle = RuntimeIdle::new();
idle.install_on_this_thread();
let wall = Instant::now();
let cpu = ThreadCpuTime::now();
{
let _blocked = BlockingFor::new(ParkReason::StorageWrite);
for _ in 0..200 {
spin();
}
}
let cpu = cpu.elapsed();
let off_cpu = wall.elapsed().saturating_sub(cpu);
assert!(
parked_under(&idle, ParkReason::StorageWrite) <= off_cpu + Duration::from_millis(1),
"charged {:?} of {off_cpu:?} spent off the CPU",
parked_under(&idle, ParkReason::StorageWrite)
);
}
#[test]
fn a_blocking_guard_off_a_worker_charges_nothing() {
assert!(current_runtime_idle().is_none());
let blocked = BlockingFor::new(ParkReason::StorageSync);
assert!(
blocked.start.is_none(),
"no clock is read where nothing can be charged"
);
spin();
drop(blocked);
}
#[test]
fn an_unmatched_unpark_adds_nothing() {
let idle = RuntimeIdle::new();
idle.unpark();
assert_eq!(idle.total(), Duration::ZERO);
assert_eq!(idle.by_reason(), [Duration::ZERO; ParkReason::COUNT]);
}
}