use std::collections::HashMap;
use std::time::Duration;
use console_api::instrument::instrument_client::InstrumentClient;
use console_api::instrument::InstrumentRequest;
use super::async_profile::{AsyncUnavailable, TaskSample};
pub fn collect(endpoint: &str, window: Duration) -> Result<Vec<TaskSample>, AsyncUnavailable> {
let owned_endpoint = endpoint.to_owned();
let error_endpoint = owned_endpoint.clone();
std::thread::Builder::new()
.name("kernal-api-tokio-profile".to_owned())
.spawn(move || collect_on_dedicated_runtime(&owned_endpoint, window))
.map_err(|error| {
unreachable(
&error_endpoint,
format!("could not start the collector thread: {error}"),
)
})?
.join()
.map_err(|_| unreachable(&error_endpoint, "collector thread panicked".to_owned()))?
}
fn collect_on_dedicated_runtime(
endpoint: &str,
window: Duration,
) -> Result<Vec<TaskSample>, AsyncUnavailable> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| unreachable(endpoint, format!("could not start a runtime: {e}")))?;
runtime.block_on(collect_async(endpoint, window))
}
pub async fn collect_async(
endpoint: &str,
window: Duration,
) -> Result<Vec<TaskSample>, AsyncUnavailable> {
subscribe(endpoint, window).await
}
fn unreachable(endpoint: &str, detail: String) -> AsyncUnavailable {
AsyncUnavailable::SourceUnreachable {
adapter: "tokio",
endpoint: endpoint.to_string(),
detail,
remediation: "Add `console-subscriber = \"0.5\"` to the application, call \
`console_subscriber::init()` at startup, and build it with \
`RUSTFLAGS=\"--cfg tokio_unstable\"`."
.to_string(),
}
}
async fn subscribe(endpoint: &str, window: Duration) -> Result<Vec<TaskSample>, AsyncUnavailable> {
let mut client = InstrumentClient::connect(endpoint.to_string())
.await
.map_err(|e| unreachable(endpoint, e.to_string()))?;
let mut stream = client
.watch_updates(InstrumentRequest {})
.await
.map_err(|e| unreachable(endpoint, e.to_string()))?
.into_inner();
let mut spawned: HashMap<u64, String> = HashMap::new();
let mut stats: HashMap<u64, console_api::tasks::Stats> = HashMap::new();
let deadline = tokio::time::Instant::now() + window;
loop {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
break;
}
match tokio::time::timeout(remaining, stream.message()).await {
Ok(Ok(Some(update))) => {
let Some(task_update) = update.task_update else {
continue;
};
for task in task_update.new_tasks {
if let Some(id) = task.id.as_ref().map(|id| id.id) {
spawned.insert(id, describe(&task));
}
}
for (id, stat) in task_update.stats_update {
stats.insert(id, stat);
}
}
Ok(Ok(None)) => break,
Ok(Err(e)) => return Err(unreachable(endpoint, e.to_string())),
Err(_) => break,
}
}
if spawned.is_empty() && stats.is_empty() {
return Err(AsyncUnavailable::NoData {
adapter: "tokio",
seconds: window.as_secs(),
});
}
Ok(join(spawned, stats))
}
fn describe(task: &console_api::tasks::Task) -> String {
if let Some(location) = task.location.as_ref() {
let file = location.file.as_deref().or(location.module_path.as_deref());
if let Some(file) = file {
return match location.line {
Some(line) => format!("{file}:{line}"),
None => file.to_string(),
};
}
}
match task.id.as_ref() {
Some(id) => format!("task#{}", id.id),
None => "task".to_string(),
}
}
fn is_runtime_internal(location: &str) -> bool {
let normalized = location.replace('\\', "/");
normalized.contains("/tokio-") && normalized.contains("/src/runtime/")
}
fn join(
spawned: HashMap<u64, String>,
stats: HashMap<u64, console_api::tasks::Stats>,
) -> Vec<TaskSample> {
let mut samples: Vec<TaskSample> = stats
.into_iter()
.filter(|(id, _)| {
spawned
.get(id)
.is_none_or(|location| !is_runtime_internal(location))
})
.map(|(id, stat)| {
let name = spawned
.get(&id)
.cloned()
.unwrap_or_else(|| format!("task#{id}"));
let poll = stat.poll_stats.unwrap_or_default();
let busy = nanos(poll.busy_time.as_ref());
let scheduled = nanos(stat.scheduled_time.as_ref());
let lifetime = lifetime_nanos(stat.created_at.as_ref(), stat.dropped_at.as_ref());
TaskSample {
spawn_stack: vec![name.clone()],
idle_nanos: (lifetime - busy - scheduled).max(0),
busy_nanos: busy,
scheduled_nanos: scheduled,
polls: poll.polls as i64,
wakes: stat.wakes as i64,
name,
}
})
.collect();
samples.sort_by(|a, b| a.name.cmp(&b.name));
samples
}
fn nanos(duration: Option<&prost_types::Duration>) -> i64 {
duration.map_or(0, |d| d.seconds * 1_000_000_000 + i64::from(d.nanos))
}
fn lifetime_nanos(
created_at: Option<&prost_types::Timestamp>,
dropped_at: Option<&prost_types::Timestamp>,
) -> i64 {
let Some(created) = created_at else {
return 0;
};
let end = match dropped_at {
Some(dropped) => stamp_nanos(dropped),
None => match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
Ok(now) => now.as_nanos() as i64,
Err(_) => return 0,
},
};
(end - stamp_nanos(created)).max(0)
}
fn stamp_nanos(stamp: &prost_types::Timestamp) -> i64 {
stamp.seconds * 1_000_000_000 + i64::from(stamp.nanos)
}
#[cfg(test)]
mod tests;