More console implementation stuff

This commit is contained in:
Nadja Reitzenstein 2022-06-21 16:20:44 +02:00
parent df7bd80d06
commit 8a35818b4f
8 changed files with 715 additions and 6 deletions

371
Cargo.lock generated
View File

@ -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"

View File

@ -7,4 +7,10 @@ edition = "2021"
[dependencies]
console-api = "0.3"
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"

View File

@ -0,0 +1,12 @@
use crate::Event;
use crossbeam_channel::Receiver;
pub(crate) struct Aggregator {
events: Receiver<Event>,
}
impl Aggregator {
pub fn new(events: Receiver<Event>) -> Self {
Self { events }
}
}

View File

@ -30,13 +30,13 @@ impl<const MAX_CALLSITES: usize> Callsites<MAX_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<const MAX_CALLSITES: usize> Callsites<MAX_CALLSITES> {
impl<const MAX_CALLSITES: usize> Default for Callsites<MAX_CALLSITES> {
fn default() -> Self {
const NULLPTR: AtomicPtr<_> = AtomicPtr::new(ptr::null_mut());
const NULLPTR: AtomicPtr<Metadata<'static>> = AtomicPtr::new(ptr::null_mut());
Self {
array: [NULLPTR; MAX_CALLSITES],
len: AtomicUsize::new(0),

View File

@ -0,0 +1,5 @@
use tracing_core::Metadata;
pub(crate) enum Event {
Metadata(&'static Metadata<'static>),
}

View File

@ -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<RefCell<SpanStack>>,
tx: Sender<Event>,
shared: Arc<Shared>,
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<S>(
&self,
dropped: &AtomicUsize,
mkEvent: impl FnOnce() -> (Event, S),
) -> Option<S> {
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<S> Layer<S> 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 {

View File

@ -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<dyn Error + Send + Sync + 'static>> {
// 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<InstrumentRequest>,
) -> Result<tonic::Response<Self::WatchUpdatesStream>, tonic::Status> {
/*
match request.remote_addr() {
Some(addr) => tracing::debug!(client.addr = %addr, "starting a new watch"),
None => tracing::debug!(client.addr = %"<unknown>", "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<TaskDetailsRequest>,
) -> Result<tonic::Response<Self::WatchTaskDetailsStream>, tonic::Status> {
todo!()
}
async fn pause(
&self,
request: tonic::Request<PauseRequest>,
) -> Result<tonic::Response<PauseResponse>, tonic::Status> {
todo!()
}
async fn resume(
&self,
request: tonic::Request<ResumeRequest>,
) -> Result<tonic::Response<ResumeResponse>, tonic::Status> {
todo!()
}
}

View File

@ -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<ContextId>,
}
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<Item = &Id> {
self.stack
.iter()
.filter_map(|ContextId { id, duplicate }| if *duplicate { None } else { Some(id) })
}
pub(crate) fn stack(&self) -> &Vec<ContextId> {
&self.stack
}
}