use std::collections::BTreeMap;
use std::time::Duration;
use crate::profile::export::pprof::AsyncProfileBuilder;
use crate::profile::MAX_DURATION;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TaskSample {
pub spawn_stack: Vec<String>,
pub idle_nanos: i64,
pub busy_nanos: i64,
pub scheduled_nanos: i64,
pub polls: i64,
pub wakes: i64,
pub name: String,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum AsyncUnavailable {
#[error("no async profiling adapter named {requested:?}; available: {available}")]
UnknownAdapter {
requested: String,
available: String,
},
#[error("the {adapter} source is not reachable at {endpoint}: {detail}\n{remediation}")]
SourceUnreachable {
adapter: &'static str,
endpoint: String,
detail: String,
remediation: String,
},
#[error(
"the {adapter} source produced no task data over {seconds}s. Either nothing \
ran, or the application is not instrumented — an empty profile and an idle \
program look identical, so this is reported rather than drawn."
)]
NoData {
adapter: &'static str,
seconds: u64,
},
}
pub trait AsyncAdapter {
fn name(&self) -> &'static str;
fn collect(&mut self, window: Duration) -> Result<Vec<TaskSample>, AsyncUnavailable>;
}
pub fn clamp_window(requested: Duration) -> Duration {
requested.min(MAX_DURATION)
}
pub fn to_pprof(samples: &[TaskSample]) -> Vec<u8> {
let mut builder = AsyncProfileBuilder::new();
for sample in samples {
builder.add_sample(
&sample.spawn_stack,
[
sample.idle_nanos,
sample.busy_nanos,
sample.scheduled_nanos,
sample.polls,
sample.wakes,
],
&sample.name,
);
}
builder.finish()
}
pub fn to_collapsed(samples: &[TaskSample]) -> String {
let mut folded: BTreeMap<String, i64> = BTreeMap::new();
for sample in samples {
if sample.idle_nanos <= 0 || sample.spawn_stack.is_empty() {
continue;
}
let stack: Vec<String> = sample
.spawn_stack
.iter()
.map(|frame| frame.replace(';', ":"))
.collect();
*folded.entry(stack.join(";")).or_insert(0) += sample.idle_nanos;
}
let mut rows: Vec<(String, i64)> = folded.into_iter().collect();
rows.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
rows.into_iter()
.map(|(stack, nanos)| format!("{stack} {nanos}\n"))
.collect()
}
#[cfg(feature = "tokio-console")]
#[derive(Debug)]
pub struct AsyncEngineAdapter {
pub endpoint: String,
}
#[cfg(feature = "tokio-console")]
impl Default for AsyncEngineAdapter {
fn default() -> Self {
Self {
endpoint: "http://127.0.0.1:6669".to_string(),
}
}
}
#[cfg(feature = "tokio-console")]
impl AsyncAdapter for AsyncEngineAdapter {
fn name(&self) -> &'static str {
"kernal-api-async"
}
fn collect(&mut self, window: Duration) -> Result<Vec<TaskSample>, AsyncUnavailable> {
crate::profile::async_tokio::collect(&self.endpoint, window)
}
}
pub struct CustomAdapter<F> {
producer: F,
}
impl<F> std::fmt::Debug for CustomAdapter<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CustomAdapter").finish_non_exhaustive()
}
}
impl<F> CustomAdapter<F>
where
F: FnMut(Duration) -> Vec<TaskSample>,
{
pub fn new(producer: F) -> Self {
Self { producer }
}
}
impl<F> AsyncAdapter for CustomAdapter<F>
where
F: FnMut(Duration) -> Vec<TaskSample>,
{
fn name(&self) -> &'static str {
"custom"
}
fn collect(&mut self, window: Duration) -> Result<Vec<TaskSample>, AsyncUnavailable> {
let samples = (self.producer)(window);
if samples.is_empty() {
return Err(AsyncUnavailable::NoData {
adapter: "custom",
seconds: window.as_secs(),
});
}
Ok(samples)
}
}
pub fn profile(
adapter: &mut dyn AsyncAdapter,
requested: Duration,
) -> Result<Vec<TaskSample>, AsyncUnavailable> {
adapter.collect(clamp_window(requested))
}
#[cfg(test)]
mod tests;