diff --git a/Cargo.lock b/Cargo.lock index 8c513c5a9..db0812954 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1235,11 +1235,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee" +checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" dependencies = [ - "autocfg", "bytes", "libc", "memchr", @@ -1290,9 +1289,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.5.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "114383b041aa6212c579467afa0075fbbdd0718de036100bc0ba7961d8cb9095" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 1a6966772..6c7ec0843 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,4 @@ members = [ "console-subscriber", "console-api" ] -resolver = "2" \ No newline at end of file +resolver = "2" diff --git a/console-api/proto/async_ops.proto b/console-api/proto/async_ops.proto index 4c13cf898..254f09223 100644 --- a/console-api/proto/async_ops.proto +++ b/console-api/proto/async_ops.proto @@ -41,6 +41,17 @@ message AsyncOp { // The source of this async operation. Most commonly this should be the name // of the method where the instantiation of this op has happened. string source = 3; + // The ID of the parent async op. + // + // This field is only set if this async op was created while inside of another + // async op. For example, `tokio::sync`'s `Mutex::lock` internally calls + // `Semaphore::acquire`. + // + // This field can be empty; if it is empty, this async op is not a child of another + // async op. + common.Id parent_async_op_id = 4; + // The resources's ID. + common.Id resource_id = 5; } // Statistics associated with a given async operation. @@ -49,12 +60,11 @@ message Stats { google.protobuf.Timestamp created_at = 1; // Timestamp of when the async op was dropped. google.protobuf.Timestamp dropped_at = 2; - // The resource Id this `AsyncOp` is associated with. Note that both - // `resource_id` and `task_id` can be None if this async op has not been polled yet - common.Id resource_id = 3; // The Id of the task that is awaiting on this op. common.Id task_id = 4; // Contains the operation poll stats. common.PollStats poll_stats = 5; + // State attributes of the async op. + repeated common.Attribute attributes = 6; } diff --git a/console-api/proto/common.proto b/console-api/proto/common.proto index a7dd9d647..0164bd8c1 100644 --- a/console-api/proto/common.proto +++ b/console-api/proto/common.proto @@ -183,3 +183,18 @@ message PollStats { // has spent *waiting* to be polled. google.protobuf.Duration busy_time = 6; } + +// State attributes of an entity. These are dependent on the type of the entity. +// +// For example, a timer resource will have a duration, while a semaphore resource may +// have a permit count. Likewise, the async ops of a semaphore may have attributes +// indicating how many permits they are trying to acquire vs how many are acquired. +// These values may change over time. Therefore, they live in the runtime stats rather +// than the static data describing the entity. +message Attribute { + // The key-value pair for the attribute + common.Field field = 1; + // Some values carry a unit of measurement. For example, a duration + // carries an associated unit of time, such as "ms" for milliseconds. + optional string unit = 2; +} \ No newline at end of file diff --git a/console-api/proto/resources.proto b/console-api/proto/resources.proto index 5f0e39822..5f851d557 100644 --- a/console-api/proto/resources.proto +++ b/console-api/proto/resources.proto @@ -41,6 +41,13 @@ message Resource { Kind kind = 4; // The location in code where the resource was created. common.Location location = 5; + // The ID of the parent resource. + common.Id parent_resource_id = 6; + // Is the resource an internal component of another resource? + // + // For example, a `tokio::time::Interval` resource might contain a + // `tokio::time::Sleep` resource internally. + bool is_internal = 7; // The kind of resource (e.g. timer, mutex). message Kind { @@ -70,16 +77,7 @@ message Stats { // have permits as an attribute. These values may change over time as the state of // the resource changes. Therefore, they live in the runtime stats rather than the // static data describing the resource. - repeated Attribute attributes = 3; - - // A single key-value pair associated with a resource. - message Attribute { - // The key-value pair for the attribute - common.Field field = 1; - // Some values carry a unit of measurement. For example, a duration - // carries an associated unit of time, such as "ms" for milliseconds. - optional string unit = 2; - } + repeated common.Attribute attributes = 3; } // A `PollOp` describes each poll operation that completes within the async diff --git a/console-subscriber/Cargo.toml b/console-subscriber/Cargo.toml index b621f283e..619199623 100644 --- a/console-subscriber/Cargo.toml +++ b/console-subscriber/Cargo.toml @@ -12,7 +12,7 @@ parking_lot = ["parking_lot_crate", "tracing-subscriber/parking_lot"] [dependencies] -tokio = { version = "^1.13", features = ["sync", "time", "macros", "tracing"] } +tokio = { version = "^1.15", features = ["sync", "time", "macros", "tracing"] } tokio-stream = "0.1" thread_local = "1.1.3" console-api = { path = "../console-api", features = ["transport"] } diff --git a/console-subscriber/examples/barrier.rs b/console-subscriber/examples/barrier.rs new file mode 100644 index 000000000..c9f54c273 --- /dev/null +++ b/console-subscriber/examples/barrier.rs @@ -0,0 +1,40 @@ +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::Barrier; +use tokio::task; + +#[tokio::main] +async fn main() -> Result<(), Box> { + console_subscriber::init(); + task::Builder::default() + .name("main-task") + .spawn(async move { + let mut handles = Vec::with_capacity(30); + let barrier = Arc::new(Barrier::new(30)); + for i in 0..30 { + let c = barrier.clone(); + let task_name = format!("task-{}", i); + handles.push(task::Builder::default().name(&task_name).spawn(async move { + tokio::time::sleep(Duration::from_secs(i)).await; + let wait_result = c.wait().await; + wait_result + })); + } + + // Will not resolve until all "after wait" messages have been printed + let mut num_leaders = 0; + for handle in handles { + let wait_result = handle.await.unwrap(); + if wait_result.is_leader() { + num_leaders += 1; + } + } + + tokio::time::sleep(Duration::from_secs(10)).await; + // Exactly one barrier will resolve as the "leader" + assert_eq!(num_leaders, 1); + }) + .await?; + + Ok(()) +} diff --git a/console-subscriber/examples/mutex.rs b/console-subscriber/examples/mutex.rs new file mode 100644 index 000000000..483ff8ebc --- /dev/null +++ b/console-subscriber/examples/mutex.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; +use std::time::Duration; +use tokio::{sync::Mutex, task}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + console_subscriber::init(); + task::Builder::default() + .name("main-task") + .spawn(async move { + let count = Arc::new(Mutex::new(0)); + for i in 0..5 { + let my_count = Arc::clone(&count); + let task_name = format!("increment-{}", i); + tokio::task::Builder::default() + .name(&task_name) + .spawn(async move { + for _ in 0..10 { + let mut lock = my_count.lock().await; + *lock += 1; + tokio::time::sleep(Duration::from_secs(1)).await; + } + }); + } + + while *count.lock().await < 50 {} + }) + .await?; + + Ok(()) +} diff --git a/console-subscriber/examples/rwlock.rs b/console-subscriber/examples/rwlock.rs new file mode 100644 index 000000000..34e06dcc7 --- /dev/null +++ b/console-subscriber/examples/rwlock.rs @@ -0,0 +1,37 @@ +use std::sync::Arc; +use std::time::Duration; +use tokio::{sync::RwLock, task}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + console_subscriber::init(); + task::Builder::default() + .name("main-task") + .spawn(async move { + let count = Arc::new(RwLock::new(0)); + for i in 0..5 { + let my_count = Arc::clone(&count); + let task_name = format!("increment-{}", i); + tokio::task::Builder::default() + .name(&task_name) + .spawn(async move { + for _ in 0..10 { + let mut lock = my_count.write().await; + *lock += 1; + tokio::time::sleep(Duration::from_secs(1)).await; + } + }); + } + + loop { + let c = count.read().await; + tokio::time::sleep(Duration::from_secs(1)).await; + if *c >= 50 { + break; + } + } + }) + .await?; + + Ok(()) +} diff --git a/console-subscriber/examples/semaphore.rs b/console-subscriber/examples/semaphore.rs new file mode 100644 index 000000000..223d73400 --- /dev/null +++ b/console-subscriber/examples/semaphore.rs @@ -0,0 +1,40 @@ +use std::sync::Arc; +use std::time::Duration; +use tokio::task; + +#[tokio::main] +async fn main() -> Result<(), Box> { + console_subscriber::init(); + task::Builder::default() + .name("main-task") + .spawn(async move { + let sem = Arc::new(tokio::sync::Semaphore::new(0)); + let mut tasks = Vec::default(); + for i in 0..5 { + let acquire_sem = Arc::clone(&sem); + let add_sem = Arc::clone(&sem); + let acquire_task_name = format!("acquire-{}", i); + let add_task_name = format!("add-{}", i); + tasks.push( + tokio::task::Builder::default() + .name(&acquire_task_name) + .spawn(async move { + let _permit = acquire_sem.acquire_many(i).await.unwrap(); + tokio::time::sleep(Duration::from_secs(i as u64 * 2)).await; + }), + ); + tasks.push(tokio::task::Builder::default().name(&add_task_name).spawn( + async move { + tokio::time::sleep(Duration::from_secs(i as u64 * 5)).await; + add_sem.add_permits(i as usize); + }, + )); + } + + let all_tasks = futures::future::try_join_all(tasks); + all_tasks.await.unwrap(); + }) + .await?; + + Ok(()) +} diff --git a/console-subscriber/src/aggregator/mod.rs b/console-subscriber/src/aggregator/mod.rs index 798a538d2..e9876f284 100644 --- a/console-subscriber/src/aggregator/mod.rs +++ b/console-subscriber/src/aggregator/mod.rs @@ -1,8 +1,8 @@ -use super::{AttributeUpdate, AttributeUpdateOp, Command, Event, WakeOp, Watch}; +use super::{AttributeUpdate, AttributeUpdateOp, Command, Event, UpdateType, WakeOp, Watch}; use crate::{record::Recorder, WatchRequest}; use console_api as proto; use proto::resources::resource; -use proto::resources::stats::Attribute; +use proto::Attribute; use tokio::sync::{mpsc, Notify}; use futures::FutureExt; @@ -148,10 +148,13 @@ struct PollStats { // Represent static data for resources struct Resource { id: Id, + parent_id: Option, metadata: &'static Metadata<'static>, concrete_type: String, kind: resource::Kind, location: Option, + is_internal: bool, + inherit_child_attrs: bool, } /// Represents a key for a `proto::field::Name`. Because the @@ -159,7 +162,7 @@ struct Resource { /// resource id in this key #[derive(Hash, PartialEq, Eq)] struct FieldKey { - resource_id: u64, + update_id: u64, field_name: proto::field::Name, } @@ -196,17 +199,20 @@ struct TaskStats { struct AsyncOp { id: Id, + parent_id: Option, + resource_id: Id, metadata: &'static Metadata<'static>, source: String, + inherit_child_attrs: bool, } #[derive(Default)] struct AsyncOpStats { created_at: Option, dropped_at: Option, - resource_id: Option, task_id: Option, poll_stats: PollStats, + attributes: HashMap, } impl DroppedAt for ResourceStats { @@ -607,19 +613,24 @@ impl Aggregator { ); } - Event::Enter { id, at } => { + Event::Enter { id, parent_id, at } => { let id = self.ids.id_for(id); + let parent_id = parent_id.map(|id| self.ids.id_for(id)); if let Some(mut task_stats) = self.task_stats.update(&id) { task_stats.poll_stats.update_on_span_enter(at); + return; } - if let Some(mut async_op_stats) = self.async_op_stats.update(&id) { + if let Some(mut async_op_stats) = + parent_id.and_then(|parent_id| self.async_op_stats.update(&parent_id)) + { async_op_stats.poll_stats.update_on_span_enter(at); } } - Event::Exit { id, at } => { + Event::Exit { id, parent_id, at } => { let id = self.ids.id_for(id); + let parent_id = parent_id.map(|id| self.ids.id_for(id)); if let Some(mut task_stats) = self.task_stats.update(&id) { task_stats.poll_stats.update_on_span_exit(at); if let Some(since_last_poll) = task_stats.poll_stats.since_last_poll(at) { @@ -628,9 +639,12 @@ impl Aggregator { .record(since_last_poll.as_nanos().try_into().unwrap_or(u64::MAX)) .unwrap(); } + return; } - if let Some(mut async_op_stats) = self.async_op_stats.update(&id) { + if let Some(mut async_op_stats) = + parent_id.and_then(|parent_id| self.async_op_stats.update(&parent_id)) + { async_op_stats.poll_stats.update_on_span_exit(at); } } @@ -695,21 +709,28 @@ impl Aggregator { Event::Resource { at, id, + parent_id, metadata, kind, concrete_type, location, + is_internal, + inherit_child_attrs, .. } => { let id = self.ids.id_for(id); + let parent_id = parent_id.map(|id| self.ids.id_for(id)); self.resources.insert( id, Resource { id, + parent_id, kind, metadata, concrete_type, location, + is_internal, + inherit_child_attrs, }, ); @@ -724,7 +745,6 @@ impl Aggregator { Event::PollOp { metadata, - at, resource_id, op_name, async_op_id, @@ -736,13 +756,7 @@ impl Aggregator { let task_id = self.ids.id_for(task_id); let mut async_op_stats = self.async_op_stats.update_or_default(async_op_id); - async_op_stats.poll_stats.polls += 1; async_op_stats.task_id.get_or_insert(task_id); - async_op_stats.resource_id.get_or_insert(resource_id); - - if !is_ready && async_op_stats.poll_stats.first_poll.is_none() { - async_op_stats.poll_stats.first_poll = Some(at); - } let poll_op = proto::resources::PollOp { metadata: Some(metadata.into()), @@ -758,14 +772,45 @@ impl Aggregator { } Event::StateUpdate { - resource_id, + update_id, + update_type, update, .. } => { - let resource_id = self.ids.id_for(resource_id); - if let Some(mut stats) = self.resource_stats.update(&resource_id) { - let field_name = match update.field.name.clone() { - Some(name) => name, + let update_id = self.ids.id_for(update_id); + let mut to_update = vec![(update_id, update_type.clone())]; + + fn update_entry(e: Entry<'_, FieldKey, Attribute>, upd: &AttributeUpdate) { + e.and_modify(|attr| update_attribute(attr, upd)) + .or_insert_with(|| upd.clone().into()); + } + + match update_type { + UpdateType::Resource => { + if let Some(parent) = self + .resources + .get(&update_id) + .and_then(|r| self.resources.get(r.parent_id.as_ref()?)) + .filter(|parent| parent.inherit_child_attrs) + { + to_update.push((parent.id, UpdateType::Resource)); + } + } + UpdateType::AsyncOp => { + if let Some(parent) = self + .async_ops + .get(&update_id) + .and_then(|r| self.async_ops.get(r.parent_id.as_ref()?)) + .filter(|parent| parent.inherit_child_attrs) + { + to_update.push((parent.id, UpdateType::AsyncOp)); + } + } + } + + for (update_id, update_type) in to_update { + let field_name = match update.field.name.as_ref() { + Some(name) => name.clone(), None => { tracing::warn!(?update.field, "field missing name, skipping..."); return; @@ -773,17 +818,26 @@ impl Aggregator { }; let upd_key = FieldKey { - resource_id, + update_id, field_name, }; - match stats.attributes.entry(upd_key) { - Entry::Occupied(ref mut attr) => { - update_attribute(attr.get_mut(), update); + + match update_type { + UpdateType::Resource => { + let mut stats = self.resource_stats.update(&update_id); + let entry = stats.as_mut().map(|s| s.attributes.entry(upd_key)); + if let Some(entry) = entry { + update_entry(entry, &update); + } } - Entry::Vacant(attr) => { - attr.insert(update.into()); + UpdateType::AsyncOp => { + let mut stats = self.async_op_stats.update(&update_id); + let entry = stats.as_mut().map(|s| s.attributes.entry(upd_key)); + if let Some(entry) = entry { + update_entry(entry, &update); + } } - } + }; } } @@ -791,16 +845,25 @@ impl Aggregator { at, id, source, + resource_id, metadata, + parent_id, + inherit_child_attrs, .. } => { let id = self.ids.id_for(id); + let parent_id = parent_id.map(|id| self.ids.id_for(id)); + let resource_id = self.ids.id_for(resource_id); + self.async_ops.insert( id, AsyncOp { id, + resource_id, metadata, source, + parent_id, + inherit_child_attrs, }, ); @@ -903,10 +966,12 @@ impl ToProto for Resource { fn to_proto(&self) -> Self::Output { proto::resources::Resource { id: Some(self.id.into()), + parent_resource_id: self.parent_id.map(Into::into), kind: Some(self.kind.clone()), metadata: Some(self.metadata.into()), concrete_type: self.concrete_type.clone(), location: self.location.clone(), + is_internal: self.is_internal, } } } @@ -931,7 +996,9 @@ impl ToProto for AsyncOp { proto::async_ops::AsyncOp { id: Some(self.id.into()), metadata: Some(self.metadata.into()), + resource_id: Some(self.resource_id.into()), source: self.source.clone(), + parent_async_op_id: self.parent_id.map(Into::into), } } } @@ -940,12 +1007,13 @@ impl ToProto for AsyncOpStats { type Output = proto::async_ops::Stats; fn to_proto(&self) -> Self::Output { + let attributes = self.attributes.values().cloned().collect(); proto::async_ops::Stats { poll_stats: Some(self.poll_stats.to_proto()), created_at: self.created_at.map(Into::into), dropped_at: self.dropped_at.map(Into::into), - resource_id: self.resource_id.map(Into::into), task_id: self.task_id.map(Into::into), + attributes, } } } @@ -987,12 +1055,11 @@ fn serialize_histogram(histogram: &Histogram) -> Result, V2Serializ Ok(buf) } -fn update_attribute(attribute: &mut Attribute, update: AttributeUpdate) { +fn update_attribute(attribute: &mut Attribute, update: &AttributeUpdate) { use proto::field::Value::*; let attribute_val = attribute.field.as_mut().and_then(|a| a.value.as_mut()); - let update_val = update.field.value; - let update_name = update.field.name; - + let update_val = update.field.value.clone(); + let update_name = update.field.name.clone(); match (attribute_val, update_val) { (Some(BoolVal(v)), Some(BoolVal(upd))) => *v = upd, diff --git a/console-subscriber/src/lib.rs b/console-subscriber/src/lib.rs index 4fa9472d6..e34a928fa 100644 --- a/console-subscriber/src/lib.rs +++ b/console-subscriber/src/lib.rs @@ -31,7 +31,7 @@ use aggregator::Aggregator; pub use builder::Builder; use callsites::Callsites; use stack::SpanStack; -use visitors::{AsyncOpVisitor, ResourceVisitor, TaskVisitor, WakerVisitor}; +use visitors::{AsyncOpVisitor, ResourceVisitor, ResourceVisitorResult, TaskVisitor, WakerVisitor}; pub use builder::{init, spawn}; @@ -65,20 +65,30 @@ pub struct TasksLayer { /// TODO: Take some time to determine more reasonable numbers resource_callsites: Callsites<32>, - /// Set of callsites for spans reprensing async operations on resources + /// Set of callsites for spans representing async operations on resources /// /// TODO: Take some time to determine more reasonable numbers async_op_callsites: Callsites<32>, - /// Set of callsites for events reprensing poll operation invocations on resources + /// Set of callsites for spans representing async op poll operations + /// + /// TODO: Take some time to determine more reasonable numbers + async_op_poll_callsites: Callsites<32>, + + /// Set of callsites for events representing poll operation invocations on resources /// /// TODO: Take some time to determine more reasonable numbers poll_op_callsites: Callsites<32>, - /// Set of callsites for events reprensing state attribute state updates on resources + /// Set of callsites for events representing state attribute state updates on resources + /// + /// TODO: Take some time to determine more reasonable numbers + resource_state_update_callsites: Callsites<32>, + + /// Set of callsites for events representing state attribute state updates on async resource ops /// /// TODO: Take some time to determine more reasonable numbers - state_update_callsites: Callsites<32>, + async_op_state_update_callsites: Callsites<32>, /// Used for unsetting the default dispatcher inside of span callbacks. no_dispatch: Dispatch, @@ -118,10 +128,12 @@ enum Event { }, Enter { id: span::Id, + parent_id: Option, at: SystemTime, }, Exit { id: span::Id, + parent_id: Option, at: SystemTime, }, Close { @@ -135,15 +147,17 @@ enum Event { }, Resource { id: span::Id, + parent_id: Option, metadata: &'static Metadata<'static>, at: SystemTime, concrete_type: String, kind: resource::Kind, location: Option, + is_internal: bool, + inherit_child_attrs: bool, }, PollOp { metadata: &'static Metadata<'static>, - at: SystemTime, resource_id: span::Id, op_name: String, async_op_id: span::Id, @@ -151,21 +165,27 @@ enum Event { is_ready: bool, }, StateUpdate { - // these fields aren't currently used, but we will probably use them - // later. put them back if we need them. - // metadata: &'static Metadata<'static>, - // at: SystemTime, - resource_id: span::Id, + update_id: span::Id, + update_type: UpdateType, update: AttributeUpdate, }, AsyncResourceOp { id: span::Id, + parent_id: Option, + resource_id: span::Id, metadata: &'static Metadata<'static>, at: SystemTime, source: String, + inherit_child_attrs: bool, }, } +#[derive(Debug, Clone)] +enum UpdateType { + Resource, + AsyncOp, +} + #[derive(Debug, Clone)] struct AttributeUpdate { field: proto::Field, @@ -233,6 +253,7 @@ impl TasksLayer { client_buffer: config.client_buffer_capacity, }; let layer = Self { + current_spans: ThreadLocal::new(), tx, flush, flush_under_capacity, @@ -240,9 +261,10 @@ impl TasksLayer { waker_callsites: Callsites::default(), resource_callsites: Callsites::default(), async_op_callsites: Callsites::default(), + async_op_poll_callsites: Callsites::default(), poll_op_callsites: Callsites::default(), - state_update_callsites: Callsites::default(), - current_spans: ThreadLocal::new(), + resource_state_update_callsites: Callsites::default(), + async_op_state_update_callsites: Callsites::default(), no_dispatch: Dispatch::new(NoSubscriber::default()), }; (layer, server) @@ -269,6 +291,10 @@ impl TasksLayer { self.async_op_callsites.contains(meta) } + fn is_async_op_poll(&self, meta: &'static Metadata<'static>) -> bool { + self.async_op_poll_callsites.contains(meta) + } + fn is_id_spawned(&self, id: &span::Id, cx: &Context<'_, S>) -> bool where S: Subscriber + for<'a> LookupSpan<'a>, @@ -296,11 +322,23 @@ impl TasksLayer { .unwrap_or(false) } + fn is_id_async_op_poll(&self, id: &span::Id, cx: &Context<'_, S>) -> bool + where + S: Subscriber + for<'a> LookupSpan<'a>, + { + cx.span(id) + .map(|span| self.is_async_op_poll(span.metadata())) + .unwrap_or(false) + } + fn is_id_tracked(&self, id: &span::Id, cx: &Context<'_, S>) -> bool where S: Subscriber + for<'a> LookupSpan<'a>, { - self.is_id_async_op(id, cx) || self.is_id_resource(id, cx) || self.is_id_spawned(id, cx) + self.is_id_async_op(id, cx) + || self.is_id_resource(id, cx) + || self.is_id_spawned(id, cx) + || self.is_id_async_op_poll(id, cx) } fn first_entered

(&self, stack: &SpanStack, p: P) -> Option @@ -350,9 +388,13 @@ where (_, "runtime::waker") | (_, "tokio::task::waker") => self.waker_callsites.insert(meta), (ResourceVisitor::RES_SPAN_NAME, _) => self.resource_callsites.insert(meta), (AsyncOpVisitor::ASYNC_OP_SPAN_NAME, _) => self.async_op_callsites.insert(meta), + ("runtime.resource.async_op.poll", _) => self.async_op_poll_callsites.insert(meta), (_, PollOpVisitor::POLL_OP_EVENT_TARGET) => self.poll_op_callsites.insert(meta), - (_, StateUpdateVisitor::STATE_UPDATE_EVENT_TARGET) => { - self.state_update_callsites.insert(meta) + (_, StateUpdateVisitor::RE_STATE_UPDATE_EVENT_TARGET) => { + self.resource_state_update_callsites.insert(meta) + } + (_, StateUpdateVisitor::AO_STATE_UPDATE_EVENT_TARGET) => { + self.async_op_state_update_callsites.insert(meta) } (_, _) => {} } @@ -361,7 +403,7 @@ where subscriber::Interest::always() } - fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, _: Context<'_, S>) { + fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) { let metadata = attrs.metadata(); if self.is_spawn(metadata) { let at = SystemTime::now(); @@ -375,31 +417,63 @@ where fields, location, }); - } else if self.is_resource(metadata) { + return; + } + + if self.is_resource(metadata) { let mut resource_visitor = ResourceVisitor::default(); attrs.record(&mut resource_visitor); - if let Some((concrete_type, kind, location)) = resource_visitor.result() { + if let Some(result) = resource_visitor.result() { + let ResourceVisitorResult { + concrete_type, + kind, + location, + is_internal, + inherit_child_attrs, + } = result; let at = SystemTime::now(); + let parent_id = self.current_spans.get().and_then(|stack| { + self.first_entered(&stack.borrow(), |id| self.is_id_resource(id, &ctx)) + }); self.send(Event::Resource { id: id.clone(), + parent_id, metadata, at, concrete_type, kind, location, + is_internal, + inherit_child_attrs, }); } // else unknown resource span format - } else if self.is_async_op(metadata) { + return; + } + + if self.is_async_op(metadata) { let mut async_op_visitor = AsyncOpVisitor::default(); attrs.record(&mut async_op_visitor); - if let Some(source) = async_op_visitor.result() { + if let Some((source, inherit_child_attrs)) = async_op_visitor.result() { let at = SystemTime::now(); - self.send(Event::AsyncResourceOp { - id: id.clone(), - at, - metadata, - source, + let resource_id = self.current_spans.get().and_then(|stack| { + self.first_entered(&stack.borrow(), |id| self.is_id_resource(id, &ctx)) + }); + + let parent_id = self.current_spans.get().and_then(|stack| { + self.first_entered(&stack.borrow(), |id| self.is_id_async_op(id, &ctx)) }); + + if let Some(resource_id) = resource_id { + self.send(Event::AsyncResourceOp { + id: id.clone(), + parent_id, + resource_id, + at, + metadata, + source, + inherit_child_attrs, + }); + } } // else async op span needs to have a source field } @@ -407,7 +481,7 @@ where fn on_event(&self, event: &tracing::Event<'_>, ctx: Context<'_, S>) { let metadata = event.metadata(); - if self.waker_callsites.contains(event.metadata()) { + if self.waker_callsites.contains(metadata) { let at = SystemTime::now(); let mut visitor = WakerVisitor::default(); event.record(&mut visitor); @@ -425,57 +499,79 @@ where self.send(Event::Waker { id, op, at }); } // else unknown waker event... what to do? can't trace it from here... - } else if self.poll_op_callsites.contains(event.metadata()) { - match ctx.event_span(event) { - // poll op event should have a resource span parent - Some(resource_span) if self.is_resource(resource_span.metadata()) => { - let mut poll_op_visitor = PollOpVisitor::default(); - event.record(&mut poll_op_visitor); + return; + } + + if self.poll_op_callsites.contains(metadata) { + let resource_id = self.current_spans.get().and_then(|stack| { + self.first_entered(&stack.borrow(), |id| self.is_id_resource(id, &ctx)) + }); + // poll op event should have a resource span parent + if let Some(resource_id) = resource_id { + let mut poll_op_visitor = PollOpVisitor::default(); + event.record(&mut poll_op_visitor); + if let Some((op_name, is_ready)) = poll_op_visitor.result() { + let task_and_async_op_ids = self.current_spans.get().and_then(|stack| { + let stack = stack.borrow(); + let task_id = + self.first_entered(&stack, |id| self.is_id_spawned(id, &ctx))?; + let async_op_id = + self.first_entered(&stack, |id| self.is_id_async_op(id, &ctx))?; + Some((task_id, async_op_id)) + }); + // poll op event should be emitted in the context of an async op and task spans - if let Some((op_name, is_ready)) = poll_op_visitor.result() { - let task_and_async_op_ids = self.current_spans.get().and_then(|stack| { - let stack = stack.borrow(); - let task_id = - self.first_entered(&stack, |id| self.is_id_spawned(id, &ctx))?; - let async_op_id = - self.first_entered(&stack, |id| self.is_id_async_op(id, &ctx))?; - Some((task_id, async_op_id)) + if let Some((task_id, async_op_id)) = task_and_async_op_ids { + self.send(Event::PollOp { + metadata, + op_name, + resource_id, + async_op_id, + task_id, + is_ready, }); - - if let Some((task_id, async_op_id)) = task_and_async_op_ids { - let at = SystemTime::now(); - self.send(Event::PollOp { - metadata, - at, - resource_id: resource_span.id(), - op_name, - async_op_id, - task_id, - is_ready, - }); - } } } - _ => {} } - } else if self.state_update_callsites.contains(event.metadata()) { - match ctx.event_span(event) { - // state update event should have a resource span parent: - Some(resource_span) if self.is_resource(resource_span.metadata()) => { - let meta_id = event.metadata().into(); - let mut state_update_visitor = StateUpdateVisitor::new(meta_id); - event.record(&mut state_update_visitor); - if let Some(update) = state_update_visitor.result() { - // let at = SystemTime::now(); - self.send(Event::StateUpdate { - // metadata, - // at, - resource_id: resource_span.id(), - update, - }); - } + return; + } + + if self.resource_state_update_callsites.contains(metadata) { + // state update event should have a resource span parent + let resource_id = self.current_spans.get().and_then(|stack| { + self.first_entered(&stack.borrow(), |id| self.is_id_resource(id, &ctx)) + }); + + if let Some(resource_id) = resource_id { + let meta_id = event.metadata().into(); + let mut state_update_visitor = StateUpdateVisitor::new(meta_id); + event.record(&mut state_update_visitor); + if let Some(update) = state_update_visitor.result() { + self.send(Event::StateUpdate { + update_id: resource_id, + update_type: UpdateType::Resource, + update, + }) + } + } + return; + } + + if self.async_op_state_update_callsites.contains(metadata) { + let async_op_id = self.current_spans.get().and_then(|stack| { + self.first_entered(&stack.borrow(), |id| self.is_id_async_op(id, &ctx)) + }); + if let Some(async_op_id) = async_op_id { + let meta_id = event.metadata().into(); + let mut state_update_visitor = StateUpdateVisitor::new(meta_id); + event.record(&mut state_update_visitor); + if let Some(update) = state_update_visitor.result() { + self.send(Event::StateUpdate { + update_id: async_op_id, + update_type: UpdateType::AsyncOp, + update, + }); } - _ => {} } } } @@ -484,16 +580,17 @@ where if !self.is_id_tracked(id, &cx) { return; } - let _default = dispatcher::set_default(&self.no_dispatch); self.current_spans .get_or_default() .borrow_mut() .push(id.clone()); + let parent_id = cx.span(id).and_then(|s| s.parent().map(|p| p.id())); self.send(Event::Enter { at: SystemTime::now(), id: id.clone(), + parent_id, }); } @@ -507,9 +604,12 @@ where spans.borrow_mut().pop(id); } + let parent_id = cx.span(id).and_then(|s| s.parent().map(|p| p.id())); + self.send(Event::Exit { - at: SystemTime::now(), id: id.clone(), + parent_id, + at: SystemTime::now(), }); } diff --git a/console-subscriber/src/record.rs b/console-subscriber/src/record.rs index be8fe4a3e..dc7b5708f 100644 --- a/console-subscriber/src/record.rs +++ b/console-subscriber/src/record.rs @@ -103,11 +103,11 @@ impl Recorder { at: *at, fields: SerializeFields(fields), }, - crate::Event::Enter { id, at } => Event::Enter { + crate::Event::Enter { id, at, .. } => Event::Enter { id: id.into_u64(), at: *at, }, - crate::Event::Exit { id, at } => Event::Exit { + crate::Event::Exit { id, at, .. } => Event::Exit { id: id.into_u64(), at: *at, }, diff --git a/console-subscriber/src/visitors.rs b/console-subscriber/src/visitors.rs index dffb8e23c..f0027094b 100644 --- a/console-subscriber/src/visitors.rs +++ b/console-subscriber/src/visitors.rs @@ -13,6 +13,7 @@ use tracing_core::{ const LOCATION_FILE: &str = "loc.file"; const LOCATION_LINE: &str = "loc.line"; const LOCATION_COLUMN: &str = "loc.col"; +const INHERIT_FIELD_NAME: &str = "inherits_child_attrs"; /// Used to extract the fields needed to construct /// an Event::Resource from the metadata of a tracing span @@ -22,20 +23,34 @@ const LOCATION_COLUMN: &str = "loc.col"; /// "runtime.resource", /// concrete_type = "Sleep", /// kind = "timer", +/// is_internal = true, +/// inherits_child_attrs = true, /// ); /// /// Fields: /// concrete_type - indicates the concrete rust type for this resource /// kind - indicates the type of resource (i.e. timer, sync, io ) +/// is_internal - whether this is a resource type that is not exposed publicly (i.e. BatchSemaphore) +/// inherits_child_attrs - whether this resource should inherit the state attributes of its children #[derive(Default)] pub(crate) struct ResourceVisitor { concrete_type: Option, kind: Option, + is_internal: bool, + inherit_child_attrs: bool, line: Option, file: Option, column: Option, } +pub(crate) struct ResourceVisitorResult { + pub(crate) concrete_type: String, + pub(crate) kind: resource::Kind, + pub(crate) location: Option, + pub(crate) is_internal: bool, + pub(crate) inherit_child_attrs: bool, +} + /// Used to extract all fields from the metadata /// of a tracing span pub(crate) struct FieldVisitor { @@ -86,6 +101,7 @@ pub(crate) struct TaskVisitor { #[derive(Default)] pub(crate) struct AsyncOpVisitor { source: Option, + inherit_child_attrs: bool, } /// Used to extract the fields needed to construct @@ -152,10 +168,11 @@ pub(crate) struct StateUpdateVisitor { impl ResourceVisitor { pub(crate) const RES_SPAN_NAME: &'static str = "runtime.resource"; const RES_CONCRETE_TYPE_FIELD_NAME: &'static str = "concrete_type"; + const RES_VIZ_FIELD_NAME: &'static str = "is_internal"; const RES_KIND_FIELD_NAME: &'static str = "kind"; const RES_KIND_TIMER: &'static str = "timer"; - pub(crate) fn result(self) -> Option<(String, resource::Kind, Option)> { + pub(crate) fn result(self) -> Option { let concrete_type = self.concrete_type?; let kind = self.kind?; @@ -170,7 +187,13 @@ impl ResourceVisitor { None }; - Some((concrete_type, kind, location)) + Some(ResourceVisitorResult { + concrete_type, + kind, + location, + is_internal: self.is_internal, + inherit_child_attrs: self.inherit_child_attrs, + }) } } @@ -194,6 +217,14 @@ impl Visit for ResourceVisitor { } } + fn record_bool(&mut self, field: &tracing_core::Field, value: bool) { + match field.name() { + Self::RES_VIZ_FIELD_NAME => self.is_internal = value, + INHERIT_FIELD_NAME => self.inherit_child_attrs = value, + _ => {} + } + } + fn record_u64(&mut self, field: &tracing_core::Field, value: u64) { match field.name() { LOCATION_LINE => self.line = Some(value as u32), @@ -318,8 +349,9 @@ impl AsyncOpVisitor { pub(crate) const ASYNC_OP_SPAN_NAME: &'static str = "runtime.resource.async_op"; const ASYNC_OP_SRC_FIELD_NAME: &'static str = "source"; - pub(crate) fn result(self) -> Option { - self.source + pub(crate) fn result(self) -> Option<(String, bool)> { + let inherit = self.inherit_child_attrs; + self.source.map(|s| (s, inherit)) } } @@ -331,6 +363,12 @@ impl Visit for AsyncOpVisitor { self.source = Some(value.to_string()); } } + + fn record_bool(&mut self, field: &tracing_core::Field, value: bool) { + if field.name() == INHERIT_FIELD_NAME { + self.inherit_child_attrs = value; + } + } } impl WakerVisitor { @@ -398,7 +436,9 @@ impl Visit for PollOpVisitor { } impl StateUpdateVisitor { - pub(crate) const STATE_UPDATE_EVENT_TARGET: &'static str = "runtime::resource::state_update"; + pub(crate) const RE_STATE_UPDATE_EVENT_TARGET: &'static str = "runtime::resource::state_update"; + pub(crate) const AO_STATE_UPDATE_EVENT_TARGET: &'static str = + "runtime::resource::async_op::state_update"; const STATE_OP_SUFFIX: &'static str = ".op"; const STATE_UNIT_SUFFIX: &'static str = ".unit"; diff --git a/console/src/conn.rs b/console/src/conn.rs index c20d3327b..faf42e9e0 100644 --- a/console/src/conn.rs +++ b/console/src/conn.rs @@ -23,7 +23,7 @@ pub struct Connection { enum State { Connected { client: InstrumentClient, - stream: Streaming, + stream: Box>, }, Disconnected(Duration), } @@ -80,7 +80,7 @@ impl Connection { let try_connect = async { let mut client = InstrumentClient::connect(self.target.clone()).await?; let request = tonic::Request::new(InstrumentRequest {}); - let stream = client.watch_updates(request).await?.into_inner(); + let stream = Box::new(client.watch_updates(request).await?.into_inner()); Ok::>(State::Connected { client, stream }) }; self.state = match try_connect.await { diff --git a/console/src/state/async_ops.rs b/console/src/state/async_ops.rs new file mode 100644 index 000000000..10fd78bbf --- /dev/null +++ b/console/src/state/async_ops.rs @@ -0,0 +1,322 @@ +use crate::{ + intern::{self, InternedStr}, + state::{pb_duration, Attribute, Field, Metadata, Visibility}, + view, +}; +use console_api as proto; +use std::{ + cell::RefCell, + collections::HashMap, + convert::{TryFrom, TryInto}, + rc::{Rc, Weak}, + time::{Duration, SystemTime}, +}; +use tui::text::Span; + +#[derive(Default, Debug)] +pub(crate) struct AsyncOpsState { + async_ops: HashMap>>, + new_async_ops: Vec, +} + +#[derive(Debug, Copy, Clone)] +#[repr(usize)] +pub(crate) enum SortBy { + Aid = 0, + Task = 1, + Source = 2, + Total = 3, + Busy = 4, + Idle = 5, + Polls = 6, +} + +#[derive(Debug)] +pub(crate) struct AsyncOp { + id: u64, + parent_id: InternedStr, + resource_id: u64, + meta_id: u64, + source: InternedStr, + stats: AsyncOpStats, +} + +pub(crate) type AsyncOpRef = Weak>; + +#[derive(Debug)] +struct AsyncOpStats { + created_at: SystemTime, + dropped_at: Option, + + polls: u64, + busy: Duration, + last_poll_started: Option, + last_poll_ended: Option, + idle: Option, + total: Option, + task_id: Option, + task_id_str: InternedStr, + formatted_attributes: Vec>>, +} + +impl Default for SortBy { + fn default() -> Self { + Self::Aid + } +} + +impl SortBy { + pub fn sort(&self, now: SystemTime, ops: &mut Vec>>) { + match self { + Self::Aid => ops.sort_unstable_by_key(|ao| ao.upgrade().map(|a| a.borrow().id)), + Self::Task => ops.sort_unstable_by_key(|ao| ao.upgrade().map(|a| a.borrow().task_id())), + Self::Source => { + ops.sort_unstable_by_key(|ao| ao.upgrade().map(|a| a.borrow().source.clone())) + } + Self::Total => { + ops.sort_unstable_by_key(|ao| ao.upgrade().map(|a| a.borrow().total(now))) + } + Self::Busy => ops.sort_unstable_by_key(|ao| ao.upgrade().map(|a| a.borrow().busy(now))), + Self::Idle => ops.sort_unstable_by_key(|ao| ao.upgrade().map(|a| a.borrow().idle(now))), + Self::Polls => { + ops.sort_unstable_by_key(|ao| ao.upgrade().map(|a| a.borrow().stats.polls)) + } + } + } +} + +impl TryFrom for SortBy { + type Error = (); + fn try_from(idx: usize) -> Result { + match idx { + idx if idx == Self::Aid as usize => Ok(Self::Aid), + idx if idx == Self::Task as usize => Ok(Self::Task), + idx if idx == Self::Source as usize => Ok(Self::Source), + idx if idx == Self::Total as usize => Ok(Self::Total), + idx if idx == Self::Busy as usize => Ok(Self::Busy), + idx if idx == Self::Idle as usize => Ok(Self::Idle), + idx if idx == Self::Polls as usize => Ok(Self::Polls), + _ => Err(()), + } + } +} + +impl view::SortBy for SortBy { + fn as_column(&self) -> usize { + *self as usize + } +} + +impl AsyncOpsState { + /// Returns any new async ops for a resource that were added since the last async ops update. + pub(crate) fn take_new_async_ops(&mut self) -> impl Iterator + '_ { + self.new_async_ops.drain(..) + } + + /// Returns all async ops. + pub(crate) fn async_ops(&self) -> impl Iterator + '_ { + self.async_ops.values().map(Rc::downgrade) + } + + pub(crate) fn update_async_ops( + &mut self, + styles: &view::Styles, + strings: &mut intern::Strings, + metas: &HashMap, + update: proto::async_ops::AsyncOpUpdate, + visibility: Visibility, + ) { + let mut stats_update = update.stats_update; + let new_list = &mut self.new_async_ops; + if matches!(visibility, Visibility::Show) { + new_list.clear(); + } + + let new_async_ops = update.new_async_ops.into_iter().filter_map(|async_op| { + if async_op.id.is_none() { + tracing::warn!(?async_op, "skipping async op with no id"); + } + + let meta_id = match async_op.metadata.as_ref() { + Some(id) => id.id, + None => { + tracing::warn!(?async_op, "async op has no metadata ID, skipping"); + return None; + } + }; + let meta = match metas.get(&meta_id) { + Some(meta) => meta, + None => { + tracing::warn!(?async_op, meta_id, "no metadata for async op, skipping"); + return None; + } + }; + + let id = async_op.id?.id; + let resource_id = async_op.resource_id?.id; + let parent_id = match async_op.parent_async_op_id { + Some(id) => strings.string(format!("{}", id.id)), + None => strings.string("n/a".to_string()), + }; + + let source = strings.string(async_op.source); + let stats = AsyncOpStats::from_proto(stats_update.remove(&id)?, meta, styles, strings); + + let async_op = AsyncOp { + id, + parent_id, + resource_id, + meta_id, + source, + stats, + }; + let async_op = Rc::new(RefCell::new(async_op)); + new_list.push(Rc::downgrade(&async_op)); + Some((id, async_op)) + }); + + self.async_ops.extend(new_async_ops); + + for (id, stats) in stats_update { + if let Some(async_op) = self.async_ops.get_mut(&id) { + let mut async_op = async_op.borrow_mut(); + if let Some(meta) = metas.get(&async_op.meta_id) { + async_op.stats = AsyncOpStats::from_proto(stats, meta, styles, strings); + } + } + } + } + + pub(crate) fn retain_active(&mut self, now: SystemTime, retain_for: Duration) { + self.async_ops.retain(|_, async_op| { + let async_op = async_op.borrow(); + + async_op + .stats + .dropped_at + .map(|d| { + let dropped_for = now.duration_since(d).unwrap(); + retain_for > dropped_for + }) + .unwrap_or(true) + }) + } +} + +impl AsyncOp { + pub(crate) fn id(&self) -> u64 { + self.id + } + + pub(crate) fn parent_id(&self) -> &str { + &self.parent_id + } + + pub(crate) fn resource_id(&self) -> u64 { + self.resource_id + } + + pub(crate) fn task_id(&self) -> Option { + self.stats.task_id + } + + pub(crate) fn task_id_str(&self) -> &str { + &self.stats.task_id_str + } + + pub(crate) fn source(&self) -> &str { + &self.source + } + + pub(crate) fn total(&self, since: SystemTime) -> Duration { + self.stats + .total + .unwrap_or_else(|| since.duration_since(self.stats.created_at).unwrap()) + } + + pub(crate) fn busy(&self, since: SystemTime) -> Duration { + if let (Some(last_poll_started), None) = + (self.stats.last_poll_started, self.stats.last_poll_ended) + { + let current_time_in_poll = since.duration_since(last_poll_started).unwrap(); + return self.stats.busy + current_time_in_poll; + } + self.stats.busy + } + + pub(crate) fn idle(&self, since: SystemTime) -> Duration { + self.stats + .idle + .unwrap_or_else(|| self.total(since) - self.busy(since)) + } + + pub(crate) fn total_polls(&self) -> u64 { + self.stats.polls + } + + pub(crate) fn dropped(&self) -> bool { + self.stats.total.is_some() + } + + pub(crate) fn formatted_attributes(&self) -> &[Vec>] { + &self.stats.formatted_attributes + } +} + +impl AsyncOpStats { + fn from_proto( + pb: proto::async_ops::Stats, + meta: &Metadata, + styles: &view::Styles, + strings: &mut intern::Strings, + ) -> Self { + let mut pb = pb; + + let mut attributes = pb + .attributes + .drain(..) + .filter_map(|pb| { + let field = pb.field?; + let field = Field::from_proto(field, meta, strings)?; + Some(Attribute { + field, + unit: pb.unit, + }) + }) + .collect::>(); + + let created_at = pb + .created_at + .expect("async op span was never created") + .try_into() + .unwrap(); + + let dropped_at: Option = pb.dropped_at.map(|v| v.try_into().unwrap()); + let total = dropped_at.map(|d| d.duration_since(created_at).unwrap()); + + let poll_stats = pb.poll_stats.expect("task should have poll stats"); + let busy = poll_stats.busy_time.map(pb_duration).unwrap_or_default(); + let idle = total.map(|total| total - busy); + let formatted_attributes = Attribute::make_formatted(styles, &mut attributes); + let task_id = pb.task_id.map(|id| id.id); + let task_id_str = strings.string( + task_id + .as_ref() + .map(u64::to_string) + .unwrap_or_else(|| "n/a".to_string()), + ); + Self { + total, + idle, + task_id, + task_id_str, + busy, + last_poll_started: poll_stats.last_poll_started.map(|v| v.try_into().unwrap()), + last_poll_ended: poll_stats.last_poll_ended.map(|v| v.try_into().unwrap()), + polls: poll_stats.polls, + created_at, + dropped_at, + formatted_attributes, + } + } +} diff --git a/console/src/state/mod.rs b/console/src/state/mod.rs index e13ab0a72..db13360bc 100644 --- a/console/src/state/mod.rs +++ b/console/src/state/mod.rs @@ -1,4 +1,4 @@ -use self::resources::ResourcesState; +use self::{async_ops::AsyncOpsState, resources::ResourcesState}; use crate::{ intern::{self, InternedStr}, view, @@ -8,7 +8,7 @@ use console_api as proto; use std::{ cell::RefCell, collections::HashMap, - convert::TryInto, + convert::{TryFrom, TryInto}, fmt, io::Cursor, rc::Rc, @@ -20,6 +20,7 @@ use tui::{ text::Span, }; +pub mod async_ops; pub mod resources; pub mod tasks; @@ -32,6 +33,7 @@ pub(crate) struct State { temporality: Temporality, tasks_state: TasksState, resources_state: ResourcesState, + async_ops_state: AsyncOpsState, current_task_details: DetailsRef, retain_for: Option, strings: intern::Strings, @@ -70,6 +72,12 @@ enum Temporality { Paused, } +#[derive(Debug)] +pub(crate) struct Attribute { + field: Field, + unit: Option, +} + impl State { pub(crate) fn with_retain_for(mut self, retain_for: Option) -> Self { self.retain_for = retain_for; @@ -137,6 +145,21 @@ impl State { visibility, ) } + + if let Some(async_ops_update) = update.async_op_update { + let visibility = if matches!(current_view, view::ViewState::ResourceInstance(_)) { + Visibility::Show + } else { + Visibility::Hide + }; + self.async_ops_state.update_async_ops( + styles, + &mut self.strings, + &self.metas, + async_ops_update, + visibility, + ) + } } pub(crate) fn retain_active(&mut self) { @@ -147,6 +170,7 @@ impl State { if let (Some(now), Some(retain_for)) = (self.last_updated_at(), self.retain_for) { self.tasks_state.retain_active(now, retain_for); self.resources_state.retain_active(now, retain_for); + self.async_ops_state.retain_active(now, retain_for); } // After dropping idle tasks & resources, prune any interned strings @@ -170,6 +194,14 @@ impl State { &mut self.resources_state } + pub(crate) fn async_ops_state(&self) -> &AsyncOpsState { + &self.async_ops_state + } + + pub(crate) fn async_ops_state_mut(&mut self) -> &mut AsyncOpsState { + &mut self.async_ops_state + } + pub(crate) fn update_task_details(&mut self, update: proto::tasks::TaskDetails) { if let Some(id) = update.task_id { let details = Details { @@ -179,7 +211,6 @@ impl State { .deserialize(&mut Cursor::new(&data)) .ok() }), - // last_updated_at: update.now.map(|now| now.try_into().unwrap()), }; *self.current_task_details.borrow_mut() = Some(details); @@ -386,6 +417,35 @@ impl FieldValue { } } +impl Attribute { + fn make_formatted( + styles: &view::Styles, + attributes: &mut Vec, + ) -> Vec>> { + let key_style = styles.fg(Color::LightBlue).add_modifier(Modifier::BOLD); + let delim_style = styles.fg(Color::LightBlue).add_modifier(Modifier::DIM); + let val_style = styles.fg(Color::Yellow); + let unit_style = styles.fg(Color::LightBlue); + + let mut formatted = Vec::with_capacity(attributes.len()); + let attributes = attributes.iter(); + for attr in attributes { + let mut elems = vec![ + Span::styled(attr.field.name.to_string(), key_style), + Span::styled("=", delim_style), + Span::styled(format!("{}", attr.field.value), val_style), + ]; + + if let Some(unit) = &attr.unit { + elems.push(Span::styled(unit.clone(), unit_style)) + } + elems.push(Span::raw(" ")); + formatted.push(elems) + } + formatted + } +} + impl From for FieldValue { fn from(pb: proto::field::Value) -> Self { match pb { @@ -426,3 +486,9 @@ fn format_location(loc: Option) -> String { }) .unwrap_or_else(|| "".to_string()) } + +fn pb_duration(dur: prost_types::Duration) -> Duration { + let secs = u64::try_from(dur.seconds).expect("duration should not be negative!"); + let nanos = u64::try_from(dur.nanos).expect("duration should not be negative!"); + Duration::from_secs(secs) + Duration::from_nanos(nanos) +} diff --git a/console/src/state/resources.rs b/console/src/state/resources.rs index 399c11c06..023148955 100644 --- a/console/src/state/resources.rs +++ b/console/src/state/resources.rs @@ -1,5 +1,5 @@ use crate::intern::{self, InternedStr}; -use crate::state::{format_location, Field, Metadata, Visibility}; +use crate::state::{format_location, Attribute, Field, Metadata, Visibility}; use crate::view; use console_api as proto; use std::{ @@ -9,10 +9,7 @@ use std::{ rc::{Rc, Weak}, time::{Duration, SystemTime}, }; -use tui::{ - style::{Color, Modifier}, - text::Span, -}; +use tui::{style::Color, text::Span}; #[derive(Default, Debug)] pub(crate) struct ResourcesState { @@ -20,6 +17,12 @@ pub(crate) struct ResourcesState { new_resources: Vec, } +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub(crate) enum TypeVisibility { + Public, + Internal, +} + #[derive(Debug, Copy, Clone)] #[repr(usize)] pub(crate) enum SortBy { @@ -30,39 +33,28 @@ pub(crate) enum SortBy { Total = 4, } -#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub(crate) enum Kind { - Timer, - Other(InternedStr), -} - #[derive(Debug)] pub(crate) struct Resource { id: u64, + id_str: InternedStr, + parent: InternedStr, + parent_id: InternedStr, meta_id: u64, - kind: Kind, + kind: InternedStr, stats: ResourceStats, target: InternedStr, concrete_type: InternedStr, location: String, + visibility: TypeVisibility, } pub(crate) type ResourceRef = Weak>; -#[derive(Debug)] -pub(crate) struct Attribute { - field: Field, - unit: Option, -} - #[derive(Debug)] struct ResourceStats { created_at: SystemTime, dropped_at: Option, total: Option, - // this will be read in a subsequent PR - #[allow(dead_code)] - attributes: Vec, formatted_attributes: Vec>>, } @@ -118,6 +110,10 @@ impl ResourcesState { self.new_resources.drain(..) } + pub(crate) fn resource(&self, id: u64) -> Option { + self.resources.get(&id).map(Rc::downgrade) + } + pub(crate) fn update_resources( &mut self, styles: &view::Styles, @@ -126,6 +122,16 @@ impl ResourcesState { update: proto::resources::ResourceUpdate, visibility: Visibility, ) { + let parents: HashMap = update + .new_resources + .iter() + .filter_map(|resource| { + let parent_id = resource.parent_resource_id?.id; + let parent = self.resource(parent_id)?; + Some((parent_id, parent)) + }) + .collect(); + let mut stats_update = update.stats_update; let new_list = &mut self.new_resources; if matches!(visibility, Visibility::Show) { @@ -151,7 +157,7 @@ impl ResourcesState { return None; } }; - let kind = match Kind::from_proto(resource.kind?, strings) { + let kind = match kind_from_proto(resource.kind?, strings) { Ok(kind) => kind, Err(err) => { tracing::warn!(%err, "resource kind cannot be parsed"); @@ -160,22 +166,53 @@ impl ResourcesState { }; let id = resource.id?.id; + let parent_id = resource.parent_resource_id.map(|id| id.id); + + let parent = strings.string(match parent_id { + Some(id) => parents + .get(&id) + .and_then(|r| r.upgrade()) + .map(|r| { + let r = r.borrow(); + format!("{} ({}::{})", r.id(), r.target(), r.concrete_type()) + }) + .unwrap_or_else(|| id.to_string()), + None => "n/a".to_string(), + }); + + let parent_id = strings.string( + parent_id + .as_ref() + .map(u64::to_string) + .unwrap_or_else(|| "n/a".to_string()), + ); + let stats = ResourceStats::from_proto(stats_update.remove(&id)?, meta, styles, strings); let location = format_location(resource.location); + let visibility = if resource.is_internal { + TypeVisibility::Internal + } else { + TypeVisibility::Public + }; let resource = Resource { id, + id_str: strings.string(id.to_string()), + parent, + parent_id, kind, stats, target: meta.target.clone(), concrete_type: strings.string(resource.concrete_type), meta_id, location, + visibility, }; let resource = Rc::new(RefCell::new(resource)); new_list.push(Rc::downgrade(&resource)); Some((id, resource)) }); + self.resources.extend(new_resources); for (id, stats) in stats_update { @@ -209,6 +246,22 @@ impl Resource { self.id } + pub(crate) fn id_str(&self) -> &str { + &self.id_str + } + + pub(crate) fn parent(&self) -> &str { + &self.parent + } + + pub(crate) fn parent_id(&self) -> &str { + &self.parent_id + } + + pub(crate) fn type_visibility(&self) -> TypeVisibility { + self.visibility + } + pub(crate) fn target(&self) -> &str { &self.target } @@ -218,10 +271,7 @@ impl Resource { } pub(crate) fn kind(&self) -> &str { - match &self.kind { - Kind::Timer => "Timer", - Kind::Other(other) => other, - } + &self.kind } pub(crate) fn formatted_attributes(&self) -> &[Vec>] { @@ -277,53 +327,33 @@ impl ResourceStats { created_at, dropped_at, total, - attributes, formatted_attributes, } } } -impl Kind { - fn from_proto( - pb: proto::resources::resource::Kind, - strings: &mut intern::Strings, - ) -> Result { - use proto::resources::resource::kind::Kind::Known as PbKnown; - use proto::resources::resource::kind::Kind::Other as PBOther; - use proto::resources::resource::kind::Known::Timer as PbTimer; - - match pb.kind.expect("a resource should have a kind field") { - PbKnown(known) if known == (PbTimer as i32) => Ok(Kind::Timer), - PbKnown(known) => Err(format!("failed to parse known kind from {}", known)), - PBOther(other) => Ok(Kind::Other(strings.string(other))), - } +fn kind_from_proto( + pb: proto::resources::resource::Kind, + strings: &mut intern::Strings, +) -> Result { + use proto::resources::resource::kind::Kind::Known as PbKnown; + use proto::resources::resource::kind::Kind::Other as PBOther; + use proto::resources::resource::kind::Known::Timer as PbTimer; + + match pb.kind.expect("a resource should have a kind field") { + PbKnown(known) if known == (PbTimer as i32) => Ok(strings.string("Timer".to_string())), + PbKnown(known) => Err(format!("failed to parse known kind from {}", known)), + PBOther(other) => Ok(strings.string(other)), } } -impl Attribute { - fn make_formatted( - styles: &view::Styles, - attributes: &mut Vec, - ) -> Vec>> { - let key_style = styles.fg(Color::LightBlue).add_modifier(Modifier::BOLD); - let delim_style = styles.fg(Color::LightBlue).add_modifier(Modifier::DIM); - let val_style = styles.fg(Color::Yellow); - let unit_style = styles.fg(Color::LightBlue); - - let mut formatted = Vec::with_capacity(attributes.len()); - let attributes = attributes.iter(); - for attr in attributes { - let mut elems = vec![ - Span::styled(attr.field.name.to_string(), key_style), - Span::styled("=", delim_style), - Span::styled(format!("{}", attr.field.value), val_style), - ]; - - if let Some(unit) = &attr.unit { - elems.push(Span::styled(unit.clone(), unit_style)) - } - formatted.push(elems) +impl TypeVisibility { + pub(crate) fn render(self, styles: &crate::view::Styles) -> Span<'static> { + const INT_UTF8: &str = "\u{1F512}"; + const PUB_UTF8: &str = "\u{2705}"; + match self { + Self::Internal => Span::styled(styles.if_utf8(INT_UTF8, "INT"), styles.fg(Color::Red)), + Self::Public => Span::styled(styles.if_utf8(PUB_UTF8, "PUB"), styles.fg(Color::Green)), } - formatted } } diff --git a/console/src/state/tasks.rs b/console/src/state/tasks.rs index e3e35de47..befb17404 100644 --- a/console/src/state/tasks.rs +++ b/console/src/state/tasks.rs @@ -1,6 +1,6 @@ use crate::{ intern::{self, InternedStr}, - state::{format_location, Field, Metadata, Visibility}, + state::{format_location, pb_duration, Field, Metadata, Visibility}, util::Percentage, view, warnings::Linter, @@ -27,7 +27,6 @@ pub(crate) struct TasksState { pub(crate) struct Details { pub(crate) task_id: u64, pub(crate) poll_times_histogram: Option>, - // pub(crate) last_updated_at: Option, } #[derive(Debug, Copy, Clone)] @@ -57,7 +56,7 @@ pub(crate) type TaskRef = Weak>; #[derive(Debug)] pub(crate) struct Task { id: u64, - // fields: Vec, + short_desc: InternedStr, formatted_fields: Vec>>, stats: TaskStats, target: InternedStr, @@ -154,10 +153,15 @@ impl TasksState { let stats = stats_update.remove(&id)?.into(); let location = format_location(task.location); + let short_desc = strings.string(match name.as_ref() { + Some(name) => format!("{} ({})", id, name), + None => format!("{}", id), + }); + let mut task = Task { name, id, - // fields, + short_desc, formatted_fields, stats, target: meta.target.clone(), @@ -197,6 +201,10 @@ impl TasksState { pub(crate) fn warnings(&self) -> impl Iterator> { self.linters.iter().filter(|linter| linter.count() > 0) } + + pub(crate) fn task(&self, id: u64) -> Option { + self.tasks.get(&id).map(Rc::downgrade) + } } impl Details { @@ -218,6 +226,10 @@ impl Task { &self.target } + pub(crate) fn short_desc(&self) -> &str { + &self.short_desc + } + pub(crate) fn name(&self) -> Option<&str> { self.name.as_ref().map(AsRef::as_ref) } @@ -350,14 +362,6 @@ impl Task { impl From for TaskStats { fn from(pb: proto::tasks::Stats) -> Self { - fn pb_duration(dur: prost_types::Duration) -> Duration { - let secs = - u64::try_from(dur.seconds).expect("a task should not have a negative duration!"); - let nanos = - u64::try_from(dur.nanos).expect("a task should not have a negative duration!"); - Duration::from_secs(secs) + Duration::from_nanos(nanos) - } - let created_at = pb .created_at .expect("task span was never created") diff --git a/console/src/view/async_ops.rs b/console/src/view/async_ops.rs new file mode 100644 index 000000000..3dce3b53c --- /dev/null +++ b/console/src/view/async_ops.rs @@ -0,0 +1,232 @@ +use crate::{ + state::{ + async_ops::{AsyncOp, SortBy}, + State, + }, + view::{ + self, bold, + table::{self, TableList, TableListState}, + DUR_LEN, DUR_PRECISION, + }, +}; + +use tui::{ + layout, + style::{self, Color, Style}, + text::Spans, + widgets::{Cell, Paragraph, Row, Table}, +}; + +#[derive(Debug, Default)] +pub(crate) struct AsyncOpsTable {} + +pub(crate) struct AsyncOpsTableCtx { + pub(crate) initial_render: bool, + pub(crate) resource_id: u64, +} + +impl TableList for AsyncOpsTable { + type Row = AsyncOp; + type Sort = SortBy; + type Context = AsyncOpsTableCtx; + + const HEADER: &'static [&'static str] = &[ + "ID", + "Parent", + "Task", + "Source", + "Total", + "Busy", + "Idle", + "Polls", + "Attributes", + ]; + + fn render( + table_list_state: &mut TableListState, + styles: &view::Styles, + frame: &mut tui::terminal::Frame, + area: layout::Rect, + state: &mut State, + ctx: Self::Context, + ) { + let now = if let Some(now) = state.last_updated_at() { + now + } else { + // If we have never gotten an update yet, skip... + return; + }; + + let AsyncOpsTableCtx { + initial_render, + resource_id, + } = ctx; + + if initial_render { + table_list_state + .sorted_items + .extend(state.async_ops_state().async_ops().filter(|op| { + op.upgrade() + .map(|op| resource_id == op.borrow().resource_id()) + .unwrap_or(false) + })) + } else { + table_list_state.sorted_items.extend( + state + .async_ops_state_mut() + .take_new_async_ops() + .filter(|op| { + op.upgrade() + .map(|op| resource_id == op.borrow().resource_id()) + .unwrap_or(false) + }), + ) + }; + table_list_state + .sort_by + .sort(now, &mut table_list_state.sorted_items); + + let mut id_width = view::Width::new(Self::HEADER[0].len() as u16); + let mut parent_width = view::Width::new(Self::HEADER[1].len() as u16); + let mut task_width = view::Width::new(Self::HEADER[2].len() as u16); + let mut source_width = view::Width::new(Self::HEADER[3].len() as u16); + let mut polls_width = view::Width::new(Self::HEADER[7].len() as u16); + + let dur_cell = |dur: std::time::Duration| -> Cell<'static> { + Cell::from(styles.time_units(format!( + "{:>width$.prec$?}", + dur, + width = DUR_LEN, + prec = DUR_PRECISION, + ))) + }; + + let rows = { + let id_width = &mut id_width; + let parent_width = &mut parent_width; + let task_width = &mut task_width; + let source_width = &mut source_width; + let polls_width = &mut polls_width; + + table_list_state + .sorted_items + .iter() + .filter_map(move |async_op| { + let async_op = async_op.upgrade()?; + let async_op = async_op.borrow(); + let task_id = async_op.task_id()?; + let task = state + .tasks_state() + .task(task_id) + .and_then(|t| t.upgrade()) + .map(|t| t.borrow().short_desc().to_owned()); + let task_str = task.unwrap_or_else(|| async_op.task_id_str().to_owned()); + + let mut row = Row::new(vec![ + Cell::from(id_width.update_str(format!( + "{:>width$}", + async_op.id(), + width = id_width.chars() as usize + ))), + Cell::from(parent_width.update_str(async_op.parent_id()).to_owned()), + Cell::from(task_width.update_str(task_str)), + Cell::from(source_width.update_str(async_op.source()).to_owned()), + dur_cell(async_op.total(now)), + dur_cell(async_op.busy(now)), + dur_cell(async_op.idle(now)), + Cell::from(polls_width.update_str(async_op.total_polls().to_string())), + Cell::from(Spans::from( + async_op + .formatted_attributes() + .iter() + .flatten() + .cloned() + .collect::>(), + )), + ]); + + if async_op.dropped() { + row = row.style(styles.terminated()); + } + + Some(row) + }) + }; + + let (selected_style, header_style) = if let Some(cyan) = styles.color(Color::Cyan) { + (Style::default().fg(cyan), Style::default()) + } else { + ( + Style::default().remove_modifier(style::Modifier::REVERSED), + Style::default().add_modifier(style::Modifier::REVERSED), + ) + }; + let header_style = header_style.add_modifier(style::Modifier::BOLD); + + let header = Row::new(Self::HEADER.iter().enumerate().map(|(idx, &value)| { + let cell = Cell::from(value); + if idx == table_list_state.selected_column { + cell.style(selected_style) + } else { + cell + } + })) + .height(1) + .style(header_style); + + let table = if table_list_state.sort_descending { + Table::new(rows) + } else { + Table::new(rows.rev()) + }; + + let block = styles.border_block().title(vec![bold(format!( + "Async Ops ({}) ", + table_list_state.len() + ))]); + + let layout = layout::Layout::default() + .direction(layout::Direction::Vertical) + .margin(0); + + let chunks = layout + .constraints( + [ + layout::Constraint::Length(1), + layout::Constraint::Min(area.height - 1), + ] + .as_ref(), + ) + .split(area); + + let controls_area = chunks[0]; + let async_ops_area = chunks[1]; + + let attributes_width = layout::Constraint::Percentage(100); + let widths = &[ + id_width.constraint(), + parent_width.constraint(), + task_width.constraint(), + source_width.constraint(), + layout::Constraint::Length(DUR_LEN as u16), + layout::Constraint::Length(DUR_LEN as u16), + layout::Constraint::Length(DUR_LEN as u16), + polls_width.constraint(), + attributes_width, + ]; + + let table = table + .header(header) + .block(block) + .widths(widths) + .highlight_symbol(view::TABLE_HIGHLIGHT_SYMBOL) + .highlight_style(Style::default().add_modifier(style::Modifier::BOLD)); + + frame.render_stateful_widget(table, async_ops_area, &mut table_list_state.table_state); + frame.render_widget(Paragraph::new(table::controls(styles)), controls_area); + + table_list_state + .sorted_items + .retain(|t| t.upgrade().is_some()); + } +} diff --git a/console/src/view/mod.rs b/console/src/view/mod.rs index cf1ccd98d..7a0573874 100644 --- a/console/src/view/mod.rs +++ b/console/src/view/mod.rs @@ -7,7 +7,9 @@ use tui::{ text::Span, }; +mod async_ops; mod mini_histogram; +mod resource; mod resources; mod styles; mod table; @@ -44,6 +46,8 @@ pub(crate) enum ViewState { ResourcesList, /// Inspecting a single task instance. TaskInstance(self::task::TaskView), + /// Inspecting a single resource instance. + ResourceInstance(self::resource::ResourceView), } /// The outcome of the update_input method @@ -53,6 +57,8 @@ pub(crate) enum UpdateKind { SelectTask(u64), /// The TaskView is exited ExitTaskView, + /// A new resource is selected + SelectResource(u64), /// No significant change Other, } @@ -115,6 +121,12 @@ impl View { } ResourcesList => { match event { + key!(Enter) => { + if let Some(res) = self.resources_list.selected_item().upgrade() { + update_kind = UpdateKind::SelectResource(res.borrow().id()); + self.state = ResourceInstance(self::resource::ResourceView::new(res)); + } + } key!(Char('t')) => { self.state = TasksList; } @@ -124,6 +136,20 @@ impl View { } } } + ResourceInstance(ref mut view) => { + // The escape key changes views, so handle here since we can + // mutate the currently selected view. + match event { + key!(Esc) => { + self.state = ResourcesList; + update_kind = UpdateKind::Other; + } + _ => { + // otherwise pass on to view + view.update_input(event); + } + } + } TaskInstance(ref mut view) => { // The escape key changes views, so handle here since we can // mutate the currently selected view. @@ -150,10 +176,11 @@ impl View { ) { match self.state { ViewState::TasksList => { - self.tasks_list.render(&self.styles, frame, area, state); + self.tasks_list.render(&self.styles, frame, area, state, ()); } ViewState::ResourcesList => { - self.resources_list.render(&self.styles, frame, area, state); + self.resources_list + .render(&self.styles, frame, area, state, ()); } ViewState::TaskInstance(ref mut view) => { let now = state @@ -161,6 +188,9 @@ impl View { .expect("task view implies we've received an update"); view.render(&self.styles, frame, area, now); } + ViewState::ResourceInstance(ref mut view) => { + view.render(&self.styles, frame, area, state); + } } state.retain_active(); diff --git a/console/src/view/resource.rs b/console/src/view/resource.rs new file mode 100644 index 000000000..51c428c60 --- /dev/null +++ b/console/src/view/resource.rs @@ -0,0 +1,121 @@ +use crate::{ + input, + state::resources::Resource, + state::State, + view::{ + self, + async_ops::{AsyncOpsTable, AsyncOpsTableCtx}, + bold, TableListState, + }, +}; +use std::{cell::RefCell, rc::Rc}; +use tui::{ + layout::{self, Layout}, + text::{Span, Spans, Text}, + widgets::{Block, Paragraph}, +}; + +pub(crate) struct ResourceView { + resource: Rc>, + async_ops_table: TableListState, + initial_render: bool, +} + +impl ResourceView { + pub(super) fn new(resource: Rc>) -> Self { + ResourceView { + resource, + async_ops_table: TableListState::::default(), + initial_render: true, + } + } + + pub(crate) fn update_input(&mut self, event: input::Event) { + self.async_ops_table.update_input(event) + } + + pub(crate) fn render( + &mut self, + styles: &view::Styles, + frame: &mut tui::terminal::Frame, + area: layout::Rect, + state: &mut State, + ) { + let resource = &*self.resource.borrow(); + + let (controls_area, stats_area, async_ops_area) = { + let chunks = Layout::default() + .direction(layout::Direction::Vertical) + .constraints( + [ + // controls + layout::Constraint::Length(1), + // resource stats + layout::Constraint::Length(8), + // async ops + layout::Constraint::Percentage(60), + ] + .as_ref(), + ) + .split(area); + (chunks[0], chunks[1], chunks[2]) + }; + + let stats_area = Layout::default() + .direction(layout::Direction::Horizontal) + .constraints( + [ + layout::Constraint::Percentage(50), + layout::Constraint::Percentage(50), + ] + .as_ref(), + ) + .split(stats_area); + + let controls = Spans::from(vec![ + Span::raw("controls: "), + bold(styles.if_utf8("\u{238B} esc", "esc")), + Span::raw(" = return to task list, "), + bold("q"), + Span::raw(" = quit"), + ]); + + let overview = vec![ + Spans::from(vec![bold("ID: "), Span::raw(resource.id_str())]), + Spans::from(vec![bold("Parent ID: "), Span::raw(resource.parent())]), + Spans::from(vec![bold("Kind: "), Span::raw(resource.kind())]), + Spans::from(vec![bold("Target: "), Span::raw(resource.target())]), + Spans::from(vec![ + bold("Type: "), + Span::raw(resource.concrete_type()), + Span::raw(" "), + resource.type_visibility().render(styles), + ]), + Spans::from(vec![bold("Location: "), Span::raw(resource.location())]), + ]; + + let mut fields = Text::default(); + fields.extend( + resource + .formatted_attributes() + .iter() + .cloned() + .map(Spans::from), + ); + + let resource_widget = + Paragraph::new(overview).block(styles.border_block().title("Resource")); + let fields_widget = Paragraph::new(fields).block(styles.border_block().title("Attributes")); + + frame.render_widget(Block::default().title(controls), controls_area); + frame.render_widget(resource_widget, stats_area[0]); + frame.render_widget(fields_widget, stats_area[1]); + let ctx = AsyncOpsTableCtx { + initial_render: self.initial_render, + resource_id: resource.id(), + }; + self.async_ops_table + .render(styles, frame, async_ops_area, state, ctx); + self.initial_render = false; + } +} diff --git a/console/src/view/resources.rs b/console/src/view/resources.rs index b06c9622d..fde61cf25 100644 --- a/console/src/view/resources.rs +++ b/console/src/view/resources.rs @@ -23,13 +23,16 @@ pub(crate) struct ResourcesTable {} impl TableList for ResourcesTable { type Row = Resource; type Sort = SortBy; + type Context = (); const HEADER: &'static [&'static str] = &[ "ID", + "Parent", "Kind", "Total", "Target", "Type", + "Vis", "Location", "Attributes", ]; @@ -40,6 +43,7 @@ impl TableList for ResourcesTable { frame: &mut tui::terminal::Frame, area: layout::Rect, state: &mut State, + _: Self::Context, ) { let now = if let Some(now) = state.last_updated_at() { now @@ -55,14 +59,19 @@ impl TableList for ResourcesTable { .sort_by .sort(now, &mut table_list_state.sorted_items); + let viz_len: u16 = Self::HEADER[6].len() as u16; + let mut id_width = view::Width::new(Self::HEADER[0].len() as u16); - let mut kind_width = view::Width::new(Self::HEADER[1].len() as u16); - let mut target_width = view::Width::new(Self::HEADER[3].len() as u16); - let mut type_width = view::Width::new(Self::HEADER[4].len() as u16); - let mut location_width = view::Width::new(Self::HEADER[5].len() as u16); + let mut parent_width = view::Width::new(Self::HEADER[1].len() as u16); + + let mut kind_width = view::Width::new(Self::HEADER[2].len() as u16); + let mut target_width = view::Width::new(Self::HEADER[4].len() as u16); + let mut type_width = view::Width::new(Self::HEADER[5].len() as u16); + let mut location_width = view::Width::new(Self::HEADER[7].len() as u16); let rows = { let id_width = &mut id_width; + let parent_width = &mut parent_width; let kind_width = &mut kind_width; let target_width = &mut target_width; let type_width = &mut type_width; @@ -81,6 +90,7 @@ impl TableList for ResourcesTable { resource.id(), width = id_width.chars() as usize ))), + Cell::from(parent_width.update_str(resource.parent_id()).to_owned()), Cell::from(kind_width.update_str(resource.kind()).to_owned()), Cell::from(styles.time_units(format!( "{:>width$.prec$?}", @@ -90,6 +100,7 @@ impl TableList for ResourcesTable { ))), Cell::from(target_width.update_str(resource.target()).to_owned()), Cell::from(type_width.update_str(resource.concrete_type()).to_owned()), + Cell::from(resource.type_visibility().render(styles)), Cell::from(location_width.update_str(resource.location().to_owned())), Cell::from(Spans::from( resource @@ -160,10 +171,12 @@ impl TableList for ResourcesTable { let attributes_width = layout::Constraint::Percentage(100); let widths = &[ id_width.constraint(), + parent_width.constraint(), kind_width.constraint(), layout::Constraint::Length(DUR_LEN as u16), target_width.constraint(), type_width.constraint(), + layout::Constraint::Length(viz_len), location_width.constraint(), attributes_width, ]; diff --git a/console/src/view/table.rs b/console/src/view/table.rs index 9231d1e3d..157d05369 100644 --- a/console/src/view/table.rs +++ b/console/src/view/table.rs @@ -15,6 +15,8 @@ use std::rc::Weak; pub(crate) trait TableList { type Row; type Sort: SortBy + TryFrom; + type Context; + const HEADER: &'static [&'static str]; fn render( @@ -23,6 +25,7 @@ pub(crate) trait TableList { frame: &mut tui::terminal::Frame, area: layout::Rect, state: &mut state::State, + cx: Self::Context, ) where Self: Sized; } @@ -150,8 +153,9 @@ impl TableListState { frame: &mut tui::terminal::Frame, area: layout::Rect, state: &mut state::State, + ctx: T::Context, ) { - T::render(self, styles, frame, area, state) + T::render(self, styles, frame, area, state, ctx) } } diff --git a/console/src/view/tasks.rs b/console/src/view/tasks.rs index bb49c590a..efe21adcf 100644 --- a/console/src/view/tasks.rs +++ b/console/src/view/tasks.rs @@ -22,6 +22,7 @@ pub(crate) struct TasksTable {} impl TableList for TasksTable { type Row = Task; type Sort = SortBy; + type Context = (); const HEADER: &'static [&'static str] = &[ "Warn", "ID", "State", "Name", "Total", "Busy", "Idle", "Polls", "Target", "Location", @@ -34,6 +35,7 @@ impl TableList for TasksTable { frame: &mut tui::terminal::Frame, area: layout::Rect, state: &mut State, + _: Self::Context, ) { let state_len: u16 = Self::HEADER[2].len() as u16; let now = if let Some(now) = state.last_updated_at() {