diff --git a/Cargo.lock b/Cargo.lock index 1a9d6f4..d815106 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,6 +88,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "anyhow" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" + [[package]] name = "api" version = "0.3.2" @@ -270,6 +276,27 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "async-stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-task" version = "4.2.0" @@ -310,6 +337,49 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4af7447fc1214c1f3a1ace861d0216a6c8bb13965b64bbad9650f375b67689a" +dependencies = [ + "async-trait", + "axum-core", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa 1.0.1", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bdc19781b16e32f8a7200368a336fa4509d4b72ef15dd4e41df5290855ee1e6" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", +] + [[package]] name = "backtrace" version = "0.3.65" @@ -596,6 +666,32 @@ dependencies = [ "cache-padded", ] +[[package]] +name = "console" +version = "0.1.0" +dependencies = [ + "console-api", + "crossbeam-channel", + "hyper", + "thread_local", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber 0.3.9", +] + +[[package]] +name = "console-api" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c5fd425783d81668ed68ec98408a80498fb4ae2fd607797539e1a9dfa3618f" +dependencies = [ + "prost", + "prost-types", + "tonic", + "tracing-core", +] + [[package]] name = "const_format" version = "0.2.23" @@ -1286,6 +1382,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "h2" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "1.8.2" @@ -1336,6 +1451,71 @@ dependencies = [ "itoa 1.0.1", ] +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" + +[[package]] +name = "httparse" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 1.0.1", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "idna" version = "0.2.3" @@ -1594,6 +1774,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "matchit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" + [[package]] name = "memchr" version = "2.4.1" @@ -1640,6 +1826,12 @@ dependencies = [ "syn", ] +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + [[package]] name = "miniz_oxide" version = "0.5.3" @@ -1985,6 +2177,39 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "prost" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71adf41db68aa0daaefc69bb30bcd68ded9b9abaad5d1fbb6304c4fb390e083e" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc" +dependencies = [ + "anyhow", + "itertools 0.10.3", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0a014229361011dc8e69c8a1ec6c2e8d0f2af7c91e3ea3f5b2170298461e68" +dependencies = [ + "bytes", + "prost", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -2647,6 +2872,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "sync_wrapper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" + [[package]] name = "tempfile" version = "3.3.0" @@ -2766,20 +2997,31 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.17.0" +version = "1.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" dependencies = [ "bytes", "libc", "memchr", "mio", + "once_cell", "pin-project-lite", "socket2", "tokio-macros", "winapi", ] +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-macros" version = "1.7.0" @@ -2802,6 +3044,31 @@ dependencies = [ "webpki", ] +[[package]] +name = "tokio-stream" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "toml" version = "0.5.8" @@ -2811,6 +3078,89 @@ dependencies = [ "serde", ] +[[package]] +name = "tonic" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9d60db39854b30b835107500cf0aca0b0d14d6e1c3de124217c23a29c2ddb" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "prost-derive", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", + "tracing-futures", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.32" @@ -2818,6 +3168,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f" dependencies = [ "cfg-if", + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2913,6 +3264,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + [[package]] name = "trybuild" version = "1.0.56" @@ -3054,6 +3411,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" diff --git a/runtime/console/Cargo.toml b/runtime/console/Cargo.toml index 45d09e9..592b083 100644 --- a/runtime/console/Cargo.toml +++ b/runtime/console/Cargo.toml @@ -7,4 +7,10 @@ edition = "2021" [dependencies] console-api = "0.3" -tracing = "0.1" \ No newline at end of file +tonic = { version = "0.7.2", default_features = false, features = [] } +hyper = { version = "0.14", default_features = false, features = ["http2", "server", "stream"] } +thread_local = "1.1" +tracing = "0.1" +tracing-core = "0.1" +tracing-subscriber = { version = "0.3", default_features = false, features = ["registry"] } +crossbeam-channel = "0.5" \ No newline at end of file diff --git a/runtime/console/src/aggregate.rs b/runtime/console/src/aggregate.rs new file mode 100644 index 0000000..1c0a5a1 --- /dev/null +++ b/runtime/console/src/aggregate.rs @@ -0,0 +1,12 @@ +use crate::Event; +use crossbeam_channel::Receiver; + +pub(crate) struct Aggregator { + events: Receiver, +} + +impl Aggregator { + pub fn new(events: Receiver) -> Self { + Self { events } + } +} diff --git a/runtime/console/src/callsites.rs b/runtime/console/src/callsites.rs index d10c395..35a0da4 100644 --- a/runtime/console/src/callsites.rs +++ b/runtime/console/src/callsites.rs @@ -30,13 +30,13 @@ impl Callsites { } } - pub(crate) fn contains(&self, callsite: &'static Metadata<'static>) -> bool { + pub(crate) fn contains(&self, callsite: &Metadata<'static>) -> bool { let mut idx = 0; let mut end = self.len.load(Ordering::Acquire); while { for cs in &self.array[idx..end] { let ptr = cs.load(Ordering::Acquire); - let meta = unsafe { ptr as *const _ as &'static Metadata<'static> }; + let meta = unsafe { ptr as *const _ as &Metadata<'static> }; if meta.callsite() == callsite.callsite() { return true; } @@ -55,7 +55,7 @@ impl Callsites { impl Default for Callsites { fn default() -> Self { - const NULLPTR: AtomicPtr<_> = AtomicPtr::new(ptr::null_mut()); + const NULLPTR: AtomicPtr> = AtomicPtr::new(ptr::null_mut()); Self { array: [NULLPTR; MAX_CALLSITES], len: AtomicUsize::new(0), diff --git a/runtime/console/src/event.rs b/runtime/console/src/event.rs new file mode 100644 index 0000000..65b316f --- /dev/null +++ b/runtime/console/src/event.rs @@ -0,0 +1,5 @@ +use tracing_core::Metadata; + +pub(crate) enum Event { + Metadata(&'static Metadata<'static>), +} diff --git a/runtime/console/src/lib.rs b/runtime/console/src/lib.rs index b02b962..b0b8688 100644 --- a/runtime/console/src/lib.rs +++ b/runtime/console/src/lib.rs @@ -1,4 +1,164 @@ +use crossbeam_channel::{Sender, TrySendError}; +use std::any::TypeId; +use std::cell::RefCell; +use std::net::IpAddr; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use thread_local::ThreadLocal; +use tracing_core::span::{Attributes, Id, Record}; +use tracing_core::{Interest, LevelFilter, Metadata, Subscriber}; +use tracing_subscriber::filter::Filtered; +use tracing_subscriber::layer::{Context, Filter, Layered}; +use tracing_subscriber::registry::LookupSpan; +use tracing_subscriber::Layer; + +mod aggregate; mod callsites; +mod event; +mod server; +mod stack; + +use crate::aggregate::Aggregator; +use crate::callsites::Callsites; +use event::Event; +pub use server::Server; +use stack::SpanStack; + +pub struct ConsoleLayer { + current_spans: ThreadLocal>, + + tx: Sender, + shared: Arc, + + spawn_callsites: Callsites<8>, + waker_callsites: Callsites<8>, +} + +#[derive(Debug)] +pub struct Builder { + /// Network Address the console server will listen on + server_addr: IpAddr, + /// Network Port the console server will listen on + server_port: u16, + + /// Number of events that can be buffered before events are dropped. + /// + /// A smaller number will reduce the memory footprint but may lead to more events being dropped + /// during activity bursts. + event_buffer_capacity: usize, +} +impl Builder { + pub fn build(self) -> (ConsoleLayer, Server) { + ConsoleLayer::build(self) + } +} +impl Default for Builder { + fn default() -> Self { + Self { + // Listen on `::1` (aka localhost) by default + server_addr: Server::DEFAULT_ADDR, + server_port: Server::DEFAULT_PORT, + event_buffer_capacity: ConsoleLayer::DEFAULT_EVENT_BUFFER_CAPACITY, + } + } +} + +#[derive(Debug, Default)] +struct Shared { + dropped_tasks: AtomicUsize, + dropped_resources: AtomicUsize, +} + +impl ConsoleLayer { + pub fn new() -> (Self, Server) { + Self::builder().build() + } + pub fn builder() -> Builder { + Builder::default() + } + fn build(config: Builder) -> (Self, Server) { + tracing::debug!( + ?config.server_addr, + config.event_buffer_capacity, + "configured console subscriber" + ); + + let (tx, events) = crossbeam_channel::bounded(config.event_buffer_capacity); + let shared = Arc::new(Shared::default()); + let aggregator = Aggregator::new(events); + let server = Server::new(aggregator); + let layer = Self { + current_spans: ThreadLocal::new(), + tx, + shared, + spawn_callsites: Callsites::default(), + waker_callsites: Callsites::default(), + }; + + (layer, server) + } +} + +impl ConsoleLayer { + const DEFAULT_EVENT_BUFFER_CAPACITY: usize = 1024; + const DEFAULT_CLIENT_BUFFER_CAPACITY: usize = 1024; + + fn is_spawn(&self, metadata: &Metadata<'static>) -> bool { + self.spawn_callsites.contains(metadata) + } + + fn is_waker(&self, metadata: &Metadata<'static>) -> bool { + self.waker_callsites.contains(metadata) + } + + fn send_stats( + &self, + dropped: &AtomicUsize, + mkEvent: impl FnOnce() -> (Event, S), + ) -> Option { + if self.tx.is_full() { + dropped.fetch_add(1, Ordering::Release); + return None; + } + + let (event, stats) = mkEvent(); + match self.tx.try_send(event) { + Ok(()) => Some(stats), + Err(TrySendError::Full(_)) => { + dropped.fetch_add(1, Ordering::Release); + None + } + Err(TrySendError::Disconnected(_)) => None, + } + } + + fn send_metadata(&self, dropped: &AtomicUsize, event: Event) -> bool { + self.send_stats(dropped, || (event, ())).is_some() + } +} + +impl Layer for ConsoleLayer +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest { + let dropped = match (metadata.name(), metadata.target()) { + (_, "executor::spawn") => { + self.spawn_callsites.insert(metadata); + &self.shared.dropped_tasks + } + (_, "executor::waker") => { + self.waker_callsites.insert(metadata); + &self.shared.dropped_tasks + } + (_, _) => &self.shared.dropped_tasks, + }; + + self.send_metadata(dropped, Event::Metadata(metadata)); + + Interest::always() + } +} #[cfg(test)] mod tests { diff --git a/runtime/console/src/server.rs b/runtime/console/src/server.rs new file mode 100644 index 0000000..2bf0ba1 --- /dev/null +++ b/runtime/console/src/server.rs @@ -0,0 +1,95 @@ +use crate::Aggregator; +use console_api::instrument::instrument_server::{Instrument, InstrumentServer}; +use console_api::instrument::{ + InstrumentRequest, PauseRequest, PauseResponse, ResumeRequest, ResumeResponse, + TaskDetailsRequest, +}; +use std::error::Error; +use std::net::{IpAddr, Ipv6Addr}; + +pub struct Server { + aggregator: Aggregator, + client_buffer_size: usize, +} + +impl Server { + pub(crate) const DEFAULT_ADDR: IpAddr = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)); + pub(crate) const DEFAULT_PORT: u16 = 49289; + + pub(crate) fn new(aggregator: Aggregator, client_buffer_size: usize) -> Self { + Self { + aggregator, + client_buffer_size, + } + } + + pub(crate) async fn serve( + mut self, /*, incoming: I */ + ) -> Result<(), Box> { + // TODO: Spawn two tasks; the aggregator that's collecting stats, aggregating and + // collating them and the server task doing the tonic gRPC stuff + + let svc = InstrumentServer::new(self); + + // The gRPC server task; requires a `Stream` of `tokio::AsyncRead + tokio::AsyncWrite`. + // TODO: Pass an async listening socket that implements the tokio versions of Read/Write + let incoming = todo!(); + tonic::transport::Server::builder() + .add_service(svc) + .serve_with_incoming(incoming) + .await?; + + // TODO: Kill the aggregator task if the serve task has ended. + + Ok(()) + } +} + +#[tonic::async_trait] +impl Instrument for Server { + type WatchUpdatesStream = (); + + async fn watch_updates( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + /* + match request.remote_addr() { + Some(addr) => tracing::debug!(client.addr = %addr, "starting a new watch"), + None => tracing::debug!(client.addr = %"", "starting a new watch"), + } + let permit = self.subscribe.reserve().await.map_err(|_| { + tonic::Status::internal("cannot start new watch, aggregation task is not running") + })?; + let (tx, rx) = mpsc::channel(self.client_buffer); + permit.send(Command::Instrument(Watch(tx))); + tracing::debug!("watch started"); + let stream = tokio_stream::wrappers::ReceiverStream::new(rx); + Ok(tonic::Response::new(stream)) + */ + todo!() + } + + type WatchTaskDetailsStream = (); + + async fn watch_task_details( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + todo!() + } + + async fn pause( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + todo!() + } + + async fn resume( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + todo!() + } +} diff --git a/runtime/console/src/stack.rs b/runtime/console/src/stack.rs new file mode 100644 index 0000000..2bed34d --- /dev/null +++ b/runtime/console/src/stack.rs @@ -0,0 +1,64 @@ +use tracing_core::span::Id; + +// This has been copied from tracing-subscriber. Once the library adds +// the ability to iterate over entered spans, this code will +// no longer be needed here +// +// https://github.com/tokio-rs/tracing/blob/master/tracing-subscriber/src/registry/stack.rs +#[derive(Debug, Clone)] +pub(crate) struct ContextId { + id: Id, + duplicate: bool, +} + +impl ContextId { + pub fn id(&self) -> &Id { + &self.id + } +} + +/// `SpanStack` tracks what spans are currently executing on a thread-local basis. +/// +/// A "separate current span" for each thread is a semantic choice, as each span +/// can be executing in a different thread. +#[derive(Debug, Default)] +pub(crate) struct SpanStack { + stack: Vec, +} + +impl SpanStack { + #[inline] + pub(crate) fn push(&mut self, id: Id) -> bool { + let duplicate = self.stack.iter().any(|i| i.id == id); + self.stack.push(ContextId { id, duplicate }); + !duplicate + } + + /// Pop a currently entered span. + /// + /// Returns `true` if the span was actually exited. + #[inline] + pub(crate) fn pop(&mut self, expected_id: &Id) -> bool { + if let Some((idx, _)) = self + .stack + .iter() + .enumerate() + .rev() + .find(|(_, ctx_id)| ctx_id.id == *expected_id) + { + let ContextId { id: _, duplicate } = self.stack.remove(idx); + return !duplicate; + } + false + } + + pub(crate) fn iter(&self) -> impl Iterator { + self.stack + .iter() + .filter_map(|ContextId { id, duplicate }| if *duplicate { None } else { Some(id) }) + } + + pub(crate) fn stack(&self) -> &Vec { + &self.stack + } +}