Skip to content

Commit

Permalink
feat(console): use tokio task ids in task views (#403)
Browse files Browse the repository at this point in the history
Tokio Console generates its own sequential Id for internal tracking and
indexing of objects (tasks, resources, etc.). However, this Id will be
recreated if Console is restarted.

In order to provide more useful information to the user, the task Id
generated by Tokio can be used in the task list and task details screens
instead. If used in this way, the ID field in the task list and task
detail views will be stable across restarts of Console (assuming the
monitored application is not restarted).

This also frees up horizontal space, as the `task.id` attribute
doesn't need to be displayed separately.

The disadvantage of using Tokio's task Id is that it is not guaranteed
to be present by the type system.

To avoid problems with missing task Ids, the Tokio task Id is store in
addition to the `span::Id` (used to communicate with the
`console-subscriber`) and the sequential console `Id` (used in the
`store`). If a task is missing the `task.id` field for whatever reason
it will still appear, but with an empty ID. If multiple runtimes are
active, then duplicate ID values will appear.

Fixes: #385

Co-authored-by: Eliza Weisman <eliza@buoyant.io>
  • Loading branch information
hds and hawkw committed Sep 29, 2023
1 parent 768534a commit f5b06d2
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 7 deletions.
2 changes: 1 addition & 1 deletion console-api/src/common.rs
Expand Up @@ -210,7 +210,7 @@ impl From<&dyn std::fmt::Debug> for field::Value {
// or vice versa. However, this is unavoidable here, because `prost` generates
// a struct with `#[derive(PartialEq)]`, but we cannot add`#[derive(Hash)]` to the
// generated code.
#[allow(clippy::derive_hash_xor_eq)]
#[allow(clippy::derived_hash_with_manual_eq)]
impl Hash for field::Name {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
Expand Down
1 change: 1 addition & 0 deletions tokio-console/src/state/mod.rs
Expand Up @@ -271,6 +271,7 @@ impl Metadata {
impl Field {
const SPAWN_LOCATION: &'static str = "spawn.location";
const NAME: &'static str = "task.name";
const TASK_ID: &'static str = "task.id";

/// Converts a wire-format `Field` into an internal `Field` representation,
/// using the provided `Metadata` for the task span that the field came
Expand Down
43 changes: 38 additions & 5 deletions tokio-console/src/state/tasks.rs
Expand Up @@ -5,7 +5,7 @@ use crate::{
histogram::DurationHistogram,
pb_duration,
store::{self, Id, SpanId, Store},
Field, Metadata, Visibility,
Field, FieldValue, Metadata, Visibility,
},
util::Percentage,
view,
Expand Down Expand Up @@ -58,17 +58,32 @@ pub(crate) enum TaskState {

pub(crate) type TaskRef = store::Ref<Task>;

/// The Id for a Tokio task.
///
/// This should be equivalent to [`tokio::task::Id`], which can't be
/// used because it's not possible to construct outside the `tokio`
/// crate.
///
/// Within the context of `tokio-console`, we don't depend on it
/// being the same as Tokio's own type, as the task id is recorded
/// as a `u64` in tracing and then sent via the wire protocol as such.
pub(crate) type TaskId = u64;

#[derive(Debug)]
pub(crate) struct Task {
/// The task's pretty (console-generated, sequential) task ID.
///
/// This is NOT the `tracing::span::Id` for the task's tracing span on the
/// remote.
id: Id<Task>,
/// The `tokio::task::Id` in the remote tokio runtime.
task_id: Option<TaskId>,
/// The `tracing::span::Id` on the remote process for this task's span.
///
/// This is used when requesting a task details stream.
span_id: SpanId,
/// A cached string representation of the Id for display purposes.
id_str: String,
short_desc: InternedStr,
formatted_fields: Vec<Vec<Span<'static>>>,
stats: TaskStats,
Expand Down Expand Up @@ -147,6 +162,7 @@ impl TasksState {
}
};
let mut name = None;
let mut task_id = None;
let mut fields = task
.fields
.drain(..)
Expand All @@ -157,6 +173,13 @@ impl TasksState {
name = Some(strings.string(field.value.to_string()));
return None;
}
if &*field.name == Field::TASK_ID {
task_id = match field.value {
FieldValue::U64(id) => Some(id as TaskId),
_ => None,
};
return None;
}
Some(field)
})
.collect::<Vec<_>>();
Expand All @@ -170,15 +193,19 @@ impl TasksState {
// remap the server's ID to a pretty, sequential task ID
let id = ids.id_for(span_id);

let short_desc = strings.string(match name.as_ref() {
Some(name) => format!("{} ({})", id, name),
None => format!("{}", id),
let short_desc = strings.string(match (task_id, name.as_ref()) {
(Some(task_id), Some(name)) => format!("{task_id} ({name})"),
(Some(task_id), None) => task_id.to_string(),
(None, Some(name)) => name.as_ref().to_owned(),
(None, None) => "".to_owned(),
});

let mut task = Task {
name,
id,
task_id,
span_id,
id_str: task_id.map(|id| id.to_string()).unwrap_or_default(),
short_desc,
formatted_fields,
stats,
Expand Down Expand Up @@ -245,6 +272,10 @@ impl Task {
self.span_id
}

pub(crate) fn id_str(&self) -> &str {
&self.id_str
}

pub(crate) fn target(&self) -> &str {
&self.target
}
Expand Down Expand Up @@ -426,7 +457,9 @@ impl Default for SortBy {
impl SortBy {
pub fn sort(&self, now: SystemTime, tasks: &mut [Weak<RefCell<Task>>]) {
match self {
Self::Tid => tasks.sort_unstable_by_key(|task| task.upgrade().map(|t| t.borrow().id)),
Self::Tid => {
tasks.sort_unstable_by_key(|task| task.upgrade().map(|t| t.borrow().task_id))
}
Self::Name => {
tasks.sort_unstable_by_key(|task| task.upgrade().map(|t| t.borrow().name.clone()))
}
Expand Down
2 changes: 1 addition & 1 deletion tokio-console/src/view/tasks.rs
Expand Up @@ -122,7 +122,7 @@ impl TableList<11> for TasksTable {
warnings,
Cell::from(id_width.update_str(format!(
"{:>width$}",
task.id(),
task.id_str(),
width = id_width.chars() as usize
))),
Cell::from(task.state().render(styles)),
Expand Down

0 comments on commit f5b06d2

Please sign in to comment.