From 2f36ce0ab13e8c7175d01beef650fbc4256df746 Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Wed, 24 Jun 2026 13:43:24 +0200 Subject: [PATCH 1/5] feat(instrumentation-runtime)!: rename runtime crate and add handle primitives Rename `quent-instrumentation` to `quent-instrumentation-runtime` so the schema-driven generator's `::quent_instrumentation_runtime::*` references resolve, and add the primitives generated entity handles need: - `Handle`: per-instance handle holding the entity id, a `u128` once-emit bitset, and a shared `Arc>`. `emit` sends a multi-cardinality event; `emit_once` guards a once-cardinality event by its bit index and fails with `ObserverError::OnceAlreadyEmitted` on a repeat. - `EntityRef` backing `DataType::EntityRef` event fields, which the generator has referenced since #218 with no type behind it. - Re-export `build_info`, `EntityEvent`, `Event`, and `ExporterOptions` so generated code references one crate path. The existing `Context`/`Observer` API is unchanged, so the macro path (`quent-model`) is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 51 +++---- Cargo.toml | 4 +- .../Cargo.toml | 5 +- .../benches/README.md | 0 .../benches/event_emit.rs | 2 +- .../src/lib.rs | 124 ++++++++++++++++-- .../tests/collector_roundtrip.rs | 2 +- .../tests/common/mod.rs | 0 .../tests/runtime_flavors.rs | 2 +- crates/model/Cargo.toml | 2 +- crates/model/src/lib.rs | 2 +- 11 files changed, 151 insertions(+), 43 deletions(-) rename crates/{instrumentation => instrumentation-runtime}/Cargo.toml (91%) rename crates/{instrumentation => instrumentation-runtime}/benches/README.md (100%) rename crates/{instrumentation => instrumentation-runtime}/benches/event_emit.rs (99%) rename crates/{instrumentation => instrumentation-runtime}/src/lib.rs (80%) rename crates/{instrumentation => instrumentation-runtime}/tests/collector_roundtrip.rs (98%) rename crates/{instrumentation => instrumentation-runtime}/tests/common/mod.rs (100%) rename crates/{instrumentation => instrumentation-runtime}/tests/runtime_flavors.rs (98%) diff --git a/Cargo.lock b/Cargo.lock index d9c88531..fc5fe1be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1992,30 +1992,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "quent-instrumentation" -version = "0.1.0" -dependencies = [ - "ciborium", - "criterion", - "pprof", - "quent-attributes", - "quent-build-info", - "quent-collector", - "quent-collector-proto", - "quent-events", - "quent-exporter", - "quent-exporter-types", - "serde", - "tempfile", - "tokio", - "tokio-stream", - "tokio-util", - "tonic", - "tracing", - "uuid", -] - [[package]] name = "quent-instrumentation-build" version = "0.1.0" @@ -2043,6 +2019,31 @@ dependencies = [ "uuid", ] +[[package]] +name = "quent-instrumentation-runtime" +version = "0.1.0" +dependencies = [ + "ciborium", + "criterion", + "pprof", + "quent-attributes", + "quent-build-info", + "quent-collector", + "quent-collector-proto", + "quent-events", + "quent-exporter", + "quent-exporter-types", + "serde", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-util", + "tonic", + "tracing", + "uuid", +] + [[package]] name = "quent-model" version = "0.1.0" @@ -2053,7 +2054,7 @@ dependencies = [ "quent-collector-client", "quent-events", "quent-exporter", - "quent-instrumentation", + "quent-instrumentation-runtime", "quent-model-macros", "quent-stdlib", "quent-time", diff --git a/Cargo.toml b/Cargo.toml index 9b51a30f..70fa0e98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ members = [ "crates/exporter/ndjson", "crates/exporter/postcard", "crates/exporter/types", - "crates/instrumentation", + "crates/instrumentation-runtime", "crates/model", "crates/model-macros", "crates/stdlib", @@ -77,7 +77,7 @@ default-members = [ "crates/exporter/ndjson", "crates/exporter/postcard", "crates/exporter/types", - "crates/instrumentation", + "crates/instrumentation-runtime", "crates/model", "crates/model-macros", "crates/stdlib", diff --git a/crates/instrumentation/Cargo.toml b/crates/instrumentation-runtime/Cargo.toml similarity index 91% rename from crates/instrumentation/Cargo.toml rename to crates/instrumentation-runtime/Cargo.toml index 5f4bf0ff..b8a344ec 100644 --- a/crates/instrumentation/Cargo.toml +++ b/crates/instrumentation-runtime/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "quent-instrumentation" +name = "quent-instrumentation-runtime" version.workspace = true edition.workspace = true publish.workspace = true @@ -14,7 +14,8 @@ quent-build-info = { path = "../build-info" } quent-events = { path = "../events" } quent-exporter = { path = "../exporter" } quent-exporter-types = { path = "../exporter/types" } -serde.workspace = true +serde = { workspace = true, features = ["derive"] } +thiserror.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "sync"]} tokio-util.workspace = true tracing.workspace = true diff --git a/crates/instrumentation/benches/README.md b/crates/instrumentation-runtime/benches/README.md similarity index 100% rename from crates/instrumentation/benches/README.md rename to crates/instrumentation-runtime/benches/README.md diff --git a/crates/instrumentation/benches/event_emit.rs b/crates/instrumentation-runtime/benches/event_emit.rs similarity index 99% rename from crates/instrumentation/benches/event_emit.rs rename to crates/instrumentation-runtime/benches/event_emit.rs index 2fe183a4..9485555f 100644 --- a/crates/instrumentation/benches/event_emit.rs +++ b/crates/instrumentation-runtime/benches/event_emit.rs @@ -28,7 +28,7 @@ use quent_events::{EntityEvent, Event}; use quent_exporter::{ CollectorExporterOptions, ExporterOptions, FileSystemExporterOptions, FileSystemFormat, }; -use quent_instrumentation::{Context, Observer}; +use quent_instrumentation_runtime::{Context, Observer}; use serde::{Deserialize, Serialize}; use tempfile::TempDir; use tokio_stream::wrappers::TcpListenerStream; diff --git a/crates/instrumentation/src/lib.rs b/crates/instrumentation-runtime/src/lib.rs similarity index 80% rename from crates/instrumentation/src/lib.rs rename to crates/instrumentation-runtime/src/lib.rs index 23a73a63..19230afc 100644 --- a/crates/instrumentation/src/lib.rs +++ b/crates/instrumentation-runtime/src/lib.rs @@ -8,16 +8,19 @@ //! generated instrumentation library only. use quent_build_info::{ArtifactInfo, ModelInfo}; -use quent_events::{EntityEvent, Event}; -use quent_exporter::{ExporterOptions, create_exporter}; +use quent_exporter::create_exporter; use serde::Serialize; + +pub use quent_build_info as build_info; +pub use quent_events::{EntityEvent, Event}; +pub use quent_exporter::ExporterOptions; use std::future::Future; use std::sync::{ Arc, atomic::{AtomicBool, Ordering}, }; use tokio::{ - runtime::{Handle, Runtime}, + runtime::{Handle as RuntimeHandle, Runtime}, sync::mpsc::{UnboundedSender, unbounded_channel}, task::JoinHandle, }; @@ -25,6 +28,19 @@ use tokio_util::sync::CancellationToken; use tracing::{debug, warn}; use uuid::Uuid; +/// Reference from one entity instance to another by id, optionally carrying +/// payload data `T`. +/// +/// Placeholder backing the schema generator's `DataType::EntityRef` fields. +// TODO(johanpel): flesh out ref-target semantics (see `quent.ref-target.v1`). +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct EntityRef { + /// Id of the referenced entity instance. + pub target: Uuid, + /// Payload carried alongside the reference. + pub data: T, +} + /// Wrapper around an optional channel sender. /// /// When the inner sender is `None` (i.e. the noop exporter is selected), `send` @@ -83,11 +99,11 @@ impl EventSender { enum BackendRuntime { /// A handle to a runtime owned elsewhere (`#[tokio::main]`, a caller-managed /// one) and kept alive by that owner. - Borrowed(Handle), + Borrowed(RuntimeHandle), /// The runtime this context spawned, shared by the context and every observer /// (hence `Arc`) and shut down by the last holder's `Drop`. Owned { - handle: Handle, + handle: RuntimeHandle, /// `Option` only so `Drop` can move the `Arc` out of `&mut self`; `Some` /// for the value's whole life until then. runtime: Option>, @@ -96,7 +112,7 @@ enum BackendRuntime { impl BackendRuntime { /// The handle observers spawn and block on. - fn handle(&self) -> Handle { + fn handle(&self) -> RuntimeHandle { match self { Self::Borrowed(handle) | Self::Owned { handle, .. } => handle.clone(), } @@ -177,7 +193,7 @@ impl Context { }); }; - let runtime = if let Ok(handle) = Handle::try_current() { + let runtime = if let Ok(handle) = RuntimeHandle::try_current() { debug!("using existing async runtime"); BackendRuntime::Borrowed(handle) } else { @@ -382,6 +398,79 @@ where } } +/// An error from emitting through a [`Handle`]. +#[derive(Debug, thiserror::Error)] +pub enum ObserverError { + /// A once-cardinality event was emitted more than once for one entity + /// instance. + #[error("once-event `{event}` already emitted for this entity instance")] + OnceAlreadyEmitted { + /// Name of the event that was re-emitted. + event: &'static str, + }, +} + +/// A handle to one entity instance: emits that instance's events through a +/// shared [`Observer`], enforcing once-cardinality events at most once. +/// +/// Holds a shared reference to the observer's export pipeline, keeping it alive +/// while any handle does. Not `Clone`: the once-emit state is unique to one +/// instance, so a clone could re-emit a once-event. +#[doc(hidden)] +pub struct Handle +where + E: Serialize + Send + EntityEvent + 'static, +{ + id: Uuid, + /// One bit per once-cardinality event, set once that event is emitted. + once_flags: u128, + observer: Arc>, +} + +impl Handle +where + E: Serialize + Send + EntityEvent + 'static, +{ + /// Create a handle emitting for entity instance `id` through `observer`. + pub fn new(id: Uuid, observer: Arc>) -> Self { + Self { + id, + once_flags: 0, + observer, + } + } + + /// The entity instance id this handle emits for. + pub fn id(&self) -> Uuid { + self.id + } + + /// Emit a multi-cardinality event for this instance. + pub fn emit(&self, event: E) { + self.observer.emit(self.id, event); + } + + /// Emit a once-cardinality event, tracked by its `bit` index. + /// + /// Returns [`ObserverError::OnceAlreadyEmitted`] if this handle already + /// emitted the event; otherwise records and emits it. `bit` must be below + /// 128. + pub fn emit_once( + &mut self, + bit: u32, + event_name: &'static str, + event: E, + ) -> Result<(), ObserverError> { + let mask = 1u128 << bit; + if self.once_flags & mask != 0 { + return Err(ObserverError::OnceAlreadyEmitted { event: event_name }); + } + self.once_flags |= mask; + self.observer.emit(self.id, event); + Ok(()) + } +} + /// Drive `fut` to completion on `handle`'s runtime, blocking the current thread. /// /// Off a runtime, it blocks directly. On a multi-threaded runtime worker it @@ -389,8 +478,8 @@ where /// /// # Panics /// On a current-thread runtime, this panics. -fn drive(handle: &Handle, fut: F) -> F::Output { - if Handle::try_current().is_ok() { +fn drive(handle: &RuntimeHandle, fut: F) -> F::Output { + if RuntimeHandle::try_current().is_ok() { tokio::task::block_in_place(|| handle.block_on(fut)) } else { handle.block_on(fut) @@ -478,4 +567,21 @@ mod tests { "one UUID-named ndjson batch file in the entity subdirectory" ); } + + #[test] + fn handle_emit_once_rejects_repeat_per_bit() { + let ctx = Context::try_new(TestModel::model_info(), None).unwrap(); + let observer = Arc::new(ctx.block_on(ctx.observer::()).unwrap()); + let mut handle = Handle::new(Uuid::now_v7(), observer); + + assert!(handle.emit_once(0, "ev", TestEvent).is_ok()); + assert!(matches!( + handle.emit_once(0, "ev", TestEvent), + Err(ObserverError::OnceAlreadyEmitted { event: "ev" }) + )); + // A different bit tracks independently. + assert!(handle.emit_once(1, "other", TestEvent).is_ok()); + // Multi emit is unconditional. + handle.emit(TestEvent); + } } diff --git a/crates/instrumentation/tests/collector_roundtrip.rs b/crates/instrumentation-runtime/tests/collector_roundtrip.rs similarity index 98% rename from crates/instrumentation/tests/collector_roundtrip.rs rename to crates/instrumentation-runtime/tests/collector_roundtrip.rs index 5a73e551..9b21adb0 100644 --- a/crates/instrumentation/tests/collector_roundtrip.rs +++ b/crates/instrumentation-runtime/tests/collector_roundtrip.rs @@ -20,7 +20,7 @@ use quent_collector::{CollectorSink, server::CollectorService}; use quent_collector_proto::collector_server::CollectorServer; use quent_events::{EntityEvent, Event}; use quent_exporter::{CollectorExporterOptions, ExporterOptions}; -use quent_instrumentation::Context; +use quent_instrumentation_runtime::Context; use tokio_stream::wrappers::TcpListenerStream; use tonic::transport::Server as GrpcServer; use uuid::Uuid; diff --git a/crates/instrumentation/tests/common/mod.rs b/crates/instrumentation-runtime/tests/common/mod.rs similarity index 100% rename from crates/instrumentation/tests/common/mod.rs rename to crates/instrumentation-runtime/tests/common/mod.rs diff --git a/crates/instrumentation/tests/runtime_flavors.rs b/crates/instrumentation-runtime/tests/runtime_flavors.rs similarity index 98% rename from crates/instrumentation/tests/runtime_flavors.rs rename to crates/instrumentation-runtime/tests/runtime_flavors.rs index 16f7dac6..11fce908 100644 --- a/crates/instrumentation/tests/runtime_flavors.rs +++ b/crates/instrumentation-runtime/tests/runtime_flavors.rs @@ -12,7 +12,7 @@ use std::path::Path; use common::{TestEvent, TestModel}; use quent_build_info::ModelSource; use quent_exporter::{ExporterOptions, FileSystemExporterOptions, FileSystemFormat}; -use quent_instrumentation::{Context, Observer}; +use quent_instrumentation_runtime::{Context, Observer}; use uuid::Uuid; fn fs_opts(root: &Path) -> ExporterOptions { diff --git a/crates/model/Cargo.toml b/crates/model/Cargo.toml index 6f764e1f..c074a6df 100644 --- a/crates/model/Cargo.toml +++ b/crates/model/Cargo.toml @@ -19,7 +19,7 @@ quent-build-info = { path = "../build-info" } quent-model-macros = { path = "../model-macros" } quent-events = { path = "../events" } quent-exporter = { path = "../exporter" } -quent-instrumentation = { path = "../instrumentation" } +quent-instrumentation-runtime = { path = "../instrumentation-runtime" } quent-time = { path = "../time" } tokio = { workspace = true, features = ["macros"] } diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index d8d3bf99..975e2516 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -90,7 +90,7 @@ pub use quent_build_info as build_info; pub use quent_collector_client::CollectorSink; pub use quent_events::{EntityEvent, Event}; pub use quent_exporter as exporter; -pub use quent_instrumentation::{Context, Observer}; +pub use quent_instrumentation_runtime::{Context, Observer}; pub use quent_time::timestamp; #[cfg(feature = "serde")] pub use serde; From 86e199f34077ea3c5f1c605d9800dac630e2efa3 Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Wed, 24 Jun 2026 14:46:30 +0200 Subject: [PATCH 2/5] feat(instrumentation-build): generate observers, handles, and a context from the schema Emit the live instrumentation surface alongside the record/event types. Per entity: an `EntityEvent` impl (stream name = the entity's snake-case name), a cloneable `{Entity}Observer` factory, and a `{Entity}Handle` with one emit method per event. Once-cardinality events take `&mut self` and are guarded by a per-handle `u64` flag word (at most 64 per entity, else `GenerateError::TooManyOnceEvents`); multi-events take `&self`. A `{Schema}Context` builds every observer on construction and hands out cheap `Arc` clones via `{entity}_observer()`. The generator lives under `src/runtime/`, split into context/observer/handle. Refine the runtime `Handle` to match the generated calls: `new()` mints a fresh id, `with_id()` adopts a caller-supplied one, and the once-flag word is a `u64`. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 1 + .../instrumentation-build/example/Cargo.toml | 1 + crates/instrumentation-build/example/build.rs | 6 +- .../instrumentation-build/example/src/main.rs | 26 +- crates/instrumentation-build/src/lib.rs | 27 ++- .../src/runtime/context.rs | 156 ++++++++++++ .../src/runtime/handle.rs | 228 ++++++++++++++++++ .../instrumentation-build/src/runtime/mod.rs | 108 +++++++++ .../src/runtime/observer.rs | 98 ++++++++ crates/instrumentation-runtime/src/lib.rs | 17 +- 10 files changed, 651 insertions(+), 17 deletions(-) create mode 100644 crates/instrumentation-build/src/runtime/context.rs create mode 100644 crates/instrumentation-build/src/runtime/handle.rs create mode 100644 crates/instrumentation-build/src/runtime/mod.rs create mode 100644 crates/instrumentation-build/src/runtime/observer.rs diff --git a/Cargo.lock b/Cargo.lock index fc5fe1be..8b358cb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2013,6 +2013,7 @@ dependencies = [ "quent-attributes", "quent-constraints", "quent-instrumentation-build", + "quent-instrumentation-runtime", "quent-schema", "serde", "serde_json", diff --git a/crates/instrumentation-build/example/Cargo.toml b/crates/instrumentation-build/example/Cargo.toml index ab8147ef..26c38a7b 100644 --- a/crates/instrumentation-build/example/Cargo.toml +++ b/crates/instrumentation-build/example/Cargo.toml @@ -6,6 +6,7 @@ publish.workspace = true [dependencies] quent-attributes = { path = "../../attributes" } +quent-instrumentation-runtime = { path = "../../instrumentation-runtime" } serde = { workspace = true } serde_json = { workspace = true } uuid = { workspace = true, features = ["serde"] } diff --git a/crates/instrumentation-build/example/build.rs b/crates/instrumentation-build/example/build.rs index 91b47a64..a372ac4f 100644 --- a/crates/instrumentation-build/example/build.rs +++ b/crates/instrumentation-build/example/build.rs @@ -13,9 +13,11 @@ fn main() -> std::result::Result<(), Box> { let schema = demo_schema()?; + // Observers require the event type to be `Serialize`; records embedded in + // events must be too. let opts = Options { - event_derives: &["Debug"], - record_derives: &["Debug"], + event_derives: &["Debug", "Clone", "::serde::Serialize"], + record_derives: &["Debug", "Clone", "::serde::Serialize"], ..Default::default() }; diff --git a/crates/instrumentation-build/example/src/main.rs b/crates/instrumentation-build/example/src/main.rs index 2827e7cf..66fb5cca 100644 --- a/crates/instrumentation-build/example/src/main.rs +++ b/crates/instrumentation-build/example/src/main.rs @@ -5,13 +5,27 @@ pub mod demo { include!(concat!(env!("OUT_DIR"), "/demo.rs")); } -fn main() { - let opened = demo::ConnectionEvent::Opened { - peer: demo::Endpoint { +fn main() -> Result<(), Box> { + // The context builds one observer per entity. No exporter configured here, + // so events go to a noop sink. + let context = demo::DemoContext::try_new(None)?; + let observer = context.connection_observer(); + let mut conn = observer.handle(); + + // `opened` and `closed` are once-events (take `&mut`); `data` is multi. + conn.opened( + demo::Endpoint { host: "localhost".to_owned(), port: 8080, }, - session: uuid::Uuid::nil(), - }; - dbg!(opened); + uuid::Uuid::nil(), + )?; + conn.data(1234, None)?; + conn.data(5678, None)?; + conn.closed()?; + + // Emitting a once-event a second time fails. + assert!(conn.closed().is_err()); + + Ok(()) } diff --git a/crates/instrumentation-build/src/lib.rs b/crates/instrumentation-build/src/lib.rs index 88d89501..7957b97a 100644 --- a/crates/instrumentation-build/src/lib.rs +++ b/crates/instrumentation-build/src/lib.rs @@ -36,20 +36,30 @@ //! include!(concat!(env!("OUT_DIR"), "/demo.rs")); //! } //! ``` +//! +//! # Restrictions +//! +//! The schema does not limit how many events an entity declares, but this +//! generator caps once-cardinality +//! ([`Cardinality::Once`](quent_schema::Cardinality::Once)) events at 64 per +//! entity; beyond that, generation fails with +//! [`GenerateError::TooManyOnceEvents`]. mod common; mod data_type; mod events; mod records; +mod runtime; use std::path::PathBuf; use quent_constraints::{BaseConstraintsError, Report, validate}; -use quent_schema::Schema; +use quent_schema::{Identifier, Schema}; use quote::quote; use events::generate_event_types; use records::generate_record_types; +use runtime::generate_runtime_types; /// Options controlling instrumentation library generation. pub struct Options { @@ -99,6 +109,16 @@ pub enum GenerateError { }, #[error("generated code did not form a valid Rust file")] InvalidGeneratedCode(#[source] syn::Error), + #[error( + "entity `{entity}` declares {count} once-events, exceeding the maximum of {max}", + max = crate::runtime::MAX_ONCE_EVENTS + )] + TooManyOnceEvents { + /// The offending entity. + entity: Identifier, + /// The number of once-cardinality events the entity declares. + count: usize, + }, #[error("failed to write generated file")] Io(#[from] std::io::Error), } @@ -138,10 +158,11 @@ pub fn generate(schema: &Schema, opts: &Options) -> Result Result { - // record structs first, then event enums + // record structs, event enums, then the live instrumentation surface let records = generate_record_types(schema, opts)?; let events = generate_event_types(schema, opts)?; - let file = syn::parse2::(quote! { #records #events }) + let runtime = generate_runtime_types(schema)?; + let file = syn::parse2::(quote! { #records #events #runtime }) .map_err(GenerateError::InvalidGeneratedCode)?; Ok(prettyplease::unparse(&file)) } diff --git a/crates/instrumentation-build/src/runtime/context.rs b/crates/instrumentation-build/src/runtime/context.rs new file mode 100644 index 00000000..68cb2b84 --- /dev/null +++ b/crates/instrumentation-build/src/runtime/context.rs @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Generation of the schema context — builds every entity's observer on +//! construction and hands out cheap clones. + +use convert_case::Case; +use proc_macro2::TokenStream; +use quent_schema::Schema; +use quote::quote; + +use super::{event_ident, observer_ident}; +use crate::common::{raw_ident, to_case}; + +/// The `{Schema}Context`: builds one observer per entity on construction and +/// hands out cheap clones via `{entity}_observer()`. +pub(super) fn schema_context(schema: &Schema) -> TokenStream { + let schema_pascal = to_case(schema.name(), Case::Pascal); + let context_ty = raw_ident(format!("{schema_pascal}Context")); + let model_name = schema.name().to_string(); + + let fields: Vec<_> = schema + .entities() + .map(|e| raw_ident(to_case(e.name(), Case::Snake))) + .collect(); + let observer_tys: Vec<_> = schema.entities().map(observer_ident).collect(); + let event_tys: Vec<_> = schema.entities().map(event_ident).collect(); + let accessors: Vec<_> = schema + .entities() + .map(|e| raw_ident(format!("{}_observer", to_case(e.name(), Case::Snake)))) + .collect(); + let accessor_docs: Vec = schema + .entities() + .map(|e| { + format!( + "Observer for `{}` entities.", + to_case(e.name(), Case::Pascal) + ) + }) + .collect(); + + let context_doc = format!( + "Instrumentation context for the `{model_name}` model. Construct it with \ + [`Self::try_new`], then call the `*_observer()` accessors to emit events." + ); + + quote! { + #[doc = #context_doc] + pub struct #context_ty { + #(#fields: #observer_tys,)* + _inner: ::quent_instrumentation_runtime::Context, + } + + impl #context_ty { + /// Create a context, building every entity's exporter pipeline. + /// Pass `None` for a no-op context that discards events. + pub fn try_new( + exporter: ::core::option::Option<::quent_instrumentation_runtime::ExporterOptions>, + ) -> ::core::result::Result> { + Self::assemble(::quent_instrumentation_runtime::Context::try_new( + Self::model_info(), + exporter, + )?) + } + + /// Create a context that adopts an existing `id` rather than + /// generating one. + pub fn try_with_id( + id: ::uuid::Uuid, + exporter: ::core::option::Option<::quent_instrumentation_runtime::ExporterOptions>, + ) -> ::core::result::Result> { + Self::assemble(::quent_instrumentation_runtime::Context::try_with_id( + id, + Self::model_info(), + exporter, + )?) + } + + fn model_info() -> ::quent_instrumentation_runtime::build_info::ModelInfo { + ::quent_instrumentation_runtime::build_info::ModelInfo { + name: #model_name.to_string(), + package: env!("CARGO_PKG_NAME").to_string(), + // No umbrella event enum on the schema-driven path; record + // the module the generated library is included into. + type_path: module_path!().to_string(), + source: ::quent_instrumentation_runtime::build_info::source_or_quent( + env!("CARGO_PKG_VERSION"), + option_env!("QUENT_SOURCE_REMOTE"), + option_env!("QUENT_SOURCE_COMMIT"), + option_env!("QUENT_SOURCE_BRANCH"), + option_env!("QUENT_SOURCE_DIRTY"), + option_env!("QUENT_SOURCE_BUILT_AT"), + ), + } + } + + fn assemble( + inner: ::quent_instrumentation_runtime::Context, + ) -> ::core::result::Result> { + let ( #(#fields,)* ) = inner.block_on(async { + ::core::result::Result::<_, ::std::boxed::Box>::Ok(( + #( inner.observer::<#event_tys>().await?, )* + )) + })?; + ::core::result::Result::Ok(Self { + #( #fields: #observer_tys { inner: ::std::sync::Arc::new(#fields) }, )* + _inner: inner, + }) + } + + /// Identity of this context. + pub fn id(&self) -> ::uuid::Uuid { + self._inner.id() + } + + #( + #[doc = #accessor_docs] + pub fn #accessors(&self) -> #observer_tys { + ::core::clone::Clone::clone(&self.#fields) + } + )* + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::common::pretty; + use quent_schema::DataType; + use quent_schema::builder::SchemaBuilder; + use quent_schema::test_utils::{entity, event, field, ident}; + + #[test] + fn context_builds_and_exposes_one_observer_per_entity() { + let s = SchemaBuilder::new(ident("Demo")) + .entities([ + entity( + "Connection", + [event("data", [field("bytes", DataType::U64)])], + ), + entity("Sensor", [event("reading", [field("v", DataType::F64)])]), + ]) + .unwrap() + .build(); + let src = pretty(schema_context(&s)); + assert!(src.contains("pub struct DemoContext")); + assert!(src.contains("connection: ConnectionObserver")); + assert!(src.contains("sensor: SensorObserver")); + assert!(src.contains("inner.observer::().await?")); + assert!(src.contains("inner.observer::().await?")); + assert!(src.contains("pub fn connection_observer(&self) -> ConnectionObserver")); + assert!(src.contains("pub fn sensor_observer(&self) -> SensorObserver")); + assert!(src.contains(r#"name: "Demo".to_string()"#)); + } +} diff --git a/crates/instrumentation-build/src/runtime/handle.rs b/crates/instrumentation-build/src/runtime/handle.rs new file mode 100644 index 00000000..9b8586e7 --- /dev/null +++ b/crates/instrumentation-build/src/runtime/handle.rs @@ -0,0 +1,228 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Generation of per-entity handles — the per-instance emit surface. + +use convert_case::Case; +use proc_macro2::{Literal, TokenStream}; +use quent_schema::{Cardinality, Entity}; +use quote::quote; + +use super::{event_ident, handle_ident}; +use crate::GenerateError; +use crate::common::{doc_attr, raw_ident, to_case}; +use crate::data_type::map_data_type; + +/// The maximum once-events an entity may declare: one bit per event in the +/// handle's `u64` once-flag word. +pub(crate) const MAX_ONCE_EVENTS: usize = u64::BITS as usize; + +/// The `{Entity}Handle`: one emit method per event over a runtime `Handle`. +/// Once-events take `&mut self` and are guarded by a flag bit; multi-events +/// take `&self`. +/// +/// # Errors +/// +/// Returns [`GenerateError::TooManyOnceEvents`] if the entity declares more +/// once-cardinality events than fit the once-flag word. +pub(super) fn entity_handle(entity: &Entity) -> Result { + let entity_pascal = to_case(entity.name(), Case::Pascal); + let event_ty = event_ident(entity); + let handle_ty = handle_ident(entity); + + let once_count = entity + .events() + .filter(|e| e.cardinality() == Cardinality::Once) + .count(); + if once_count > MAX_ONCE_EVENTS { + return Err(GenerateError::TooManyOnceEvents { + entity: entity.name().clone(), + count: once_count, + }); + } + + // Once-events claim successive bits of the handle's flag word, in + // declaration order; multi-events route straight through `emit`. + let mut once_bit = 0u32; + let methods: Vec = entity + .events() + .map(|event| { + let method = raw_ident(to_case(event.name(), Case::Snake)); + let variant = raw_ident(to_case(event.name(), Case::Pascal)); + let docs = doc_attr(event.annotations().docs()); + + let params: Vec = event + .fields() + .map(|f| { + let name = raw_ident(to_case(f.name(), Case::Snake)); + let ty = map_data_type(f.ty(), 0); + quote! { #name: #ty } + }) + .collect(); + let field_names: Vec = event + .fields() + .map(|f| { + let name = raw_ident(to_case(f.name(), Case::Snake)); + quote! { #name } + }) + .collect(); + let construct = if field_names.is_empty() { + quote! { #event_ty::#variant } + } else { + quote! { #event_ty::#variant { #(#field_names),* } } + }; + + match event.cardinality() { + Cardinality::Once => { + let bit = Literal::u32_unsuffixed(once_bit); + once_bit += 1; + let event_name = event.name().to_string(); + quote! { + #docs + pub fn #method( + &mut self, + #(#params),* + ) -> ::core::result::Result<(), ::quent_instrumentation_runtime::ObserverError> { + self.inner.emit_once(#bit, #event_name, #construct) + } + } + } + Cardinality::Multi => quote! { + #docs + pub fn #method( + &self, + #(#params),* + ) -> ::core::result::Result<(), ::quent_instrumentation_runtime::ObserverError> { + self.inner.emit(#construct); + ::core::result::Result::Ok(()) + } + }, + } + }) + .collect(); + + let handle_doc = format!("Handle to one `{entity_pascal}` entity instance."); + Ok(quote! { + #[doc = #handle_doc] + pub struct #handle_ty { + inner: ::quent_instrumentation_runtime::Handle<#event_ty>, + } + + impl #handle_ty { + /// Id of the entity instance this handle emits for. + pub fn uuid(&self) -> ::uuid::Uuid { + self.inner.id() + } + + #(#methods)* + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::common::pretty; + use quent_schema::DataType; + use quent_schema::builder::{EntityBuilder, EventBuilder}; + use quent_schema::test_utils::{field, ident}; + + fn once( + name: &str, + fields: impl IntoIterator, + ) -> quent_schema::Event { + EventBuilder::new(ident(name), Cardinality::Once) + .fields(fields) + .unwrap() + .build() + } + + fn multi( + name: &str, + fields: impl IntoIterator, + ) -> quent_schema::Event { + EventBuilder::new(ident(name), Cardinality::Multi) + .fields(fields) + .unwrap() + .build() + } + + fn entity(name: &str, events: impl IntoIterator) -> Entity { + EntityBuilder::new(ident(name)) + .events(events) + .unwrap() + .build() + } + + #[test] + fn once_takes_mut_self_and_multi_takes_ref() { + let e = entity( + "Connection", + [ + once( + "opened", + [ + field("peer", DataType::String), + field("port", DataType::U16), + ], + ), + multi("data", [field("bytes", DataType::U64)]), + once("closed", []), + ], + ); + let expected = quote! { + #[doc = "Handle to one `Connection` entity instance."] + pub struct ConnectionHandle { + inner: ::quent_instrumentation_runtime::Handle, + } + impl ConnectionHandle { + /// Id of the entity instance this handle emits for. + pub fn uuid(&self) -> ::uuid::Uuid { + self.inner.id() + } + pub fn opened( + &mut self, + peer: String, + port: u16, + ) -> ::core::result::Result<(), ::quent_instrumentation_runtime::ObserverError> { + self.inner.emit_once(0, "opened", ConnectionEvent::Opened { peer, port }) + } + pub fn data( + &self, + bytes: u64, + ) -> ::core::result::Result<(), ::quent_instrumentation_runtime::ObserverError> { + self.inner.emit(ConnectionEvent::Data { bytes }); + ::core::result::Result::Ok(()) + } + pub fn closed( + &mut self, + ) -> ::core::result::Result<(), ::quent_instrumentation_runtime::ObserverError> { + self.inner.emit_once(1, "closed", ConnectionEvent::Closed) + } + } + }; + assert_eq!(pretty(entity_handle(&e).unwrap()), pretty(expected)); + } + + #[test] + fn once_events_claim_successive_bits() { + let e = entity( + "Job", + [once("started", []), multi("tick", []), once("finished", [])], + ); + let src = pretty(entity_handle(&e).unwrap()); + assert!(src.contains(r#"emit_once(0, "started", JobEvent::Started)"#)); + assert!(src.contains(r#"emit_once(1, "finished", JobEvent::Finished)"#)); + } + + #[test] + fn too_many_once_events_is_an_error() { + let events = (0..=MAX_ONCE_EVENTS).map(|i| once(&format!("e{i}"), [])); + let e = entity("Big", events); + let err = entity_handle(&e).unwrap_err(); + assert!(matches!( + err, + GenerateError::TooManyOnceEvents { count, .. } if count == MAX_ONCE_EVENTS + 1 + )); + } +} diff --git a/crates/instrumentation-build/src/runtime/mod.rs b/crates/instrumentation-build/src/runtime/mod.rs new file mode 100644 index 00000000..2fa00f95 --- /dev/null +++ b/crates/instrumentation-build/src/runtime/mod.rs @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Generation of the live instrumentation surface: per-entity observers and +//! handles, plus the schema's context that deals them out. + +use convert_case::Case; +use proc_macro2::TokenStream; +use quent_schema::{Entity, Schema}; +use quote::quote; +use syn::Ident; + +use crate::GenerateError; +use crate::common::{raw_ident, to_case}; + +mod context; +mod handle; +mod observer; + +pub(crate) use handle::MAX_ONCE_EVENTS; + +/// The full instrumentation surface for `schema`: per entity, an `EntityEvent` +/// impl, an observer, and a handle; then the `{Schema}Context` that builds and +/// hands out the observers. +/// +/// # Errors +/// +/// Returns [`GenerateError::TooManyOnceEvents`] if an entity declares more +/// once-cardinality events than the per-handle flag word holds. +pub(crate) fn generate_runtime_types(schema: &Schema) -> Result { + let entities: Vec = schema + .entities() + .map(|entity| { + let event_impl = entity_event_impl(entity); + let observer = observer::entity_observer(entity); + let handle = handle::entity_handle(entity)?; + Ok::<_, GenerateError>(quote! { + #event_impl + #observer + #handle + }) + }) + .collect::>()?; + let context = context::schema_context(schema); + Ok(quote! { + #(#entities)* + #context + }) +} + +/// Tie an entity's event enum to its stream name (the entity's snake-case name). +fn entity_event_impl(entity: &Entity) -> TokenStream { + let event_ty = event_ident(entity); + let stream_name = to_case(entity.name(), Case::Snake); + quote! { + impl ::quent_instrumentation_runtime::EntityEvent for #event_ty { + const NAME: &'static str = #stream_name; + } + } +} + +/// `{Entity}Event` — the entity's event enum. +fn event_ident(entity: &Entity) -> Ident { + raw_ident(format!("{}Event", to_case(entity.name(), Case::Pascal))) +} + +/// `{Entity}Observer`. +fn observer_ident(entity: &Entity) -> Ident { + raw_ident(format!("{}Observer", to_case(entity.name(), Case::Pascal))) +} + +/// `{Entity}Handle`. +fn handle_ident(entity: &Entity) -> Ident { + raw_ident(format!("{}Handle", to_case(entity.name(), Case::Pascal))) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::common::pretty; + use quent_schema::Cardinality; + use quent_schema::DataType; + use quent_schema::builder::{EntityBuilder, EventBuilder, SchemaBuilder}; + use quent_schema::test_utils::{field, ident}; + + #[test] + fn generate_assembles_event_impl_observer_handle_and_context() { + let connection = EntityBuilder::new(ident("Connection")) + .events([EventBuilder::new(ident("data"), Cardinality::Multi) + .fields([field("bytes", DataType::U64)]) + .unwrap() + .build()]) + .unwrap() + .build(); + let s = SchemaBuilder::new(ident("Demo")) + .entity(connection) + .unwrap() + .build(); + let src = pretty(generate_runtime_types(&s).unwrap()); + assert!( + src.contains("impl ::quent_instrumentation_runtime::EntityEvent for ConnectionEvent") + ); + assert!(src.contains(r#"const NAME: &'static str = "connection""#)); + assert!(src.contains("pub struct ConnectionObserver")); + assert!(src.contains("pub struct ConnectionHandle")); + assert!(src.contains("pub struct DemoContext")); + } +} diff --git a/crates/instrumentation-build/src/runtime/observer.rs b/crates/instrumentation-build/src/runtime/observer.rs new file mode 100644 index 00000000..8b57e805 --- /dev/null +++ b/crates/instrumentation-build/src/runtime/observer.rs @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Generation of per-entity observers — the cheap-clone factories for handles. + +use convert_case::Case; +use proc_macro2::TokenStream; +use quent_schema::Entity; +use quote::quote; + +use super::{event_ident, handle_ident, observer_ident}; +use crate::common::to_case; + +/// The `{Entity}Observer`: an `Arc`-shared, cloneable factory that mints +/// per-instance handles. +pub(super) fn entity_observer(entity: &Entity) -> TokenStream { + let entity_pascal = to_case(entity.name(), Case::Pascal); + let event_ty = event_ident(entity); + let observer_ty = observer_ident(entity); + let handle_ty = handle_ident(entity); + + let observer_doc = format!( + "Observer for `{entity_pascal}` entities. Obtain a per-instance handle \ + with [`Self::handle`]." + ); + let handle_fn_doc = format!("Create a handle for a fresh `{entity_pascal}` instance."); + let handle_with_id_doc = + format!("Create a handle for the `{entity_pascal}` instance identified by `id`."); + + quote! { + #[doc = #observer_doc] + #[derive(Clone)] + pub struct #observer_ty { + inner: ::std::sync::Arc<::quent_instrumentation_runtime::Observer<#event_ty>>, + } + + impl #observer_ty { + #[doc = #handle_fn_doc] + pub fn handle(&self) -> #handle_ty { + #handle_ty { + inner: ::quent_instrumentation_runtime::Handle::new( + ::core::clone::Clone::clone(&self.inner), + ), + } + } + + #[doc = #handle_with_id_doc] + pub fn handle_with_id(&self, id: ::uuid::Uuid) -> #handle_ty { + #handle_ty { + inner: ::quent_instrumentation_runtime::Handle::with_id( + id, + ::core::clone::Clone::clone(&self.inner), + ), + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::common::pretty; + use quent_schema::test_utils::entity; + + #[test] + fn observer_is_a_cloneable_handle_factory() { + // The observer is independent of the entity's events. + let e = entity("Connection", []); + let expected = quote! { + #[doc = "Observer for `Connection` entities. Obtain a per-instance handle with [`Self::handle`]."] + #[derive(Clone)] + pub struct ConnectionObserver { + inner: ::std::sync::Arc<::quent_instrumentation_runtime::Observer>, + } + impl ConnectionObserver { + #[doc = "Create a handle for a fresh `Connection` instance."] + pub fn handle(&self) -> ConnectionHandle { + ConnectionHandle { + inner: ::quent_instrumentation_runtime::Handle::new( + ::core::clone::Clone::clone(&self.inner), + ), + } + } + #[doc = "Create a handle for the `Connection` instance identified by `id`."] + pub fn handle_with_id(&self, id: ::uuid::Uuid) -> ConnectionHandle { + ConnectionHandle { + inner: ::quent_instrumentation_runtime::Handle::with_id( + id, + ::core::clone::Clone::clone(&self.inner), + ), + } + } + } + }; + assert_eq!(pretty(entity_observer(&e)), pretty(expected)); + } +} diff --git a/crates/instrumentation-runtime/src/lib.rs b/crates/instrumentation-runtime/src/lib.rs index 19230afc..fa045eab 100644 --- a/crates/instrumentation-runtime/src/lib.rs +++ b/crates/instrumentation-runtime/src/lib.rs @@ -423,7 +423,7 @@ where { id: Uuid, /// One bit per once-cardinality event, set once that event is emitted. - once_flags: u128, + once_flags: u64, observer: Arc>, } @@ -431,8 +431,13 @@ impl Handle where E: Serialize + Send + EntityEvent + 'static, { - /// Create a handle emitting for entity instance `id` through `observer`. - pub fn new(id: Uuid, observer: Arc>) -> Self { + /// Create a handle for a fresh entity instance, with a generated id. + pub fn new(observer: Arc>) -> Self { + Self::with_id(Uuid::now_v7(), observer) + } + + /// Create a handle for the entity instance identified by `id`. + pub fn with_id(id: Uuid, observer: Arc>) -> Self { Self { id, once_flags: 0, @@ -454,14 +459,14 @@ where /// /// Returns [`ObserverError::OnceAlreadyEmitted`] if this handle already /// emitted the event; otherwise records and emits it. `bit` must be below - /// 128. + /// 64. pub fn emit_once( &mut self, bit: u32, event_name: &'static str, event: E, ) -> Result<(), ObserverError> { - let mask = 1u128 << bit; + let mask = 1u64 << bit; if self.once_flags & mask != 0 { return Err(ObserverError::OnceAlreadyEmitted { event: event_name }); } @@ -572,7 +577,7 @@ mod tests { fn handle_emit_once_rejects_repeat_per_bit() { let ctx = Context::try_new(TestModel::model_info(), None).unwrap(); let observer = Arc::new(ctx.block_on(ctx.observer::()).unwrap()); - let mut handle = Handle::new(Uuid::now_v7(), observer); + let mut handle = Handle::new(observer); assert!(handle.emit_once(0, "ev", TestEvent).is_ok()); assert!(matches!( From cc0ca7104853878953e9acb460a0a4ec7a95329f Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Wed, 24 Jun 2026 14:51:40 +0200 Subject: [PATCH 3/5] Update a word --- crates/instrumentation-runtime/benches/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/instrumentation-runtime/benches/README.md b/crates/instrumentation-runtime/benches/README.md index d48b48de..af989dbf 100644 --- a/crates/instrumentation-runtime/benches/README.md +++ b/crates/instrumentation-runtime/benches/README.md @@ -1,4 +1,4 @@ -# Instrumentation API microbenchmarks +# Instrumentation runtime microbenchmarks Measures the caller-side cost of `EventSender::emit` with each provided exporter, plus a `noop` baseline. From 875227276c21d03ecd8b412195e65bb217b63a31 Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Wed, 24 Jun 2026 15:22:24 +0200 Subject: [PATCH 4/5] docs: repoint faq microbenchmark link after the runtime rename, tidy example Fix the stale `crates/instrumentation/benches/README.md` link in docs/faq.md (rumdl MD057) to the renamed `crates/instrumentation-runtime/...` path and reflow it under 80 columns. Trim the instrumentation-build example's comments: drop the noop-sink aside and explain why the handle is `&mut` for once-events. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/instrumentation-build/example/src/main.rs | 5 ++--- docs/faq.md | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/instrumentation-build/example/src/main.rs b/crates/instrumentation-build/example/src/main.rs index 66fb5cca..8326e7f4 100644 --- a/crates/instrumentation-build/example/src/main.rs +++ b/crates/instrumentation-build/example/src/main.rs @@ -6,13 +6,12 @@ pub mod demo { } fn main() -> Result<(), Box> { - // The context builds one observer per entity. No exporter configured here, - // so events go to a noop sink. let context = demo::DemoContext::try_new(None)?; let observer = context.connection_observer(); let mut conn = observer.handle(); - // `opened` and `closed` are once-events (take `&mut`); `data` is multi. + // The handle (may) hold per-instance state that enforces once-cardinality, + // hence it is mut so it can update it state after producing a once-event. conn.opened( demo::Endpoint { host: "localhost".to_owned(), diff --git a/docs/faq.md b/docs/faq.md index c6a3966e..33ce88ec 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -95,7 +95,7 @@ memory is saturated, or re-scheduling work across nodes). ## What is the overhead of emitting events? -See the [microbenchmark](../crates/instrumentation/benches/README.md) for -measurements and how to run them. The caller-side hot path is a timestamp +See the [microbenchmark](../crates/instrumentation-runtime/benches/README.md) +for measurements and how to run them. The caller-side hot path is a timestamp acquisition plus an `mpsc` push; serialization and I/O happen asynchronously in a `forwarder` task. From e03a8cbd22ed9f5cb87021991cf9ab3f8c389210 Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Wed, 24 Jun 2026 15:33:55 +0200 Subject: [PATCH 5/5] docs(instrumentation-runtime): point bench README commands at the renamed crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cargo bench -p quent-instrumentation` no longer resolves after the `quent-instrumentation` → `quent-instrumentation-runtime` rename; update the three invocations to the new package name. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/instrumentation-runtime/benches/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/instrumentation-runtime/benches/README.md b/crates/instrumentation-runtime/benches/README.md index af989dbf..820b4943 100644 --- a/crates/instrumentation-runtime/benches/README.md +++ b/crates/instrumentation-runtime/benches/README.md @@ -6,7 +6,7 @@ exporter, plus a `noop` baseline. ## Run ```sh -cargo bench -p quent-instrumentation --bench event_emit +cargo bench -p quent-instrumentation-runtime --bench event_emit ``` Report: `target/criterion/report/index.html`. @@ -14,7 +14,7 @@ Report: `target/criterion/report/index.html`. ## Run with profiling ```sh -QUENT_BENCH_PROFILE_TIME=10 cargo bench -p quent-instrumentation --bench event_emit +QUENT_BENCH_PROFILE_TIME=10 cargo bench -p quent-instrumentation-runtime --bench event_emit ``` Flamegraphs: `target/criterion/emit//profile/flamegraph.svg`. @@ -24,7 +24,7 @@ from measurement to profile mode and writes one flamegraph SVG per variant. `QUENT_BENCH_PROFILE_HZ` overrides the SIGPROF sampling rate (default 4999 Hz): ```sh -QUENT_BENCH_PROFILE_TIME=10 QUENT_BENCH_PROFILE_HZ=9999 cargo bench -p quent-instrumentation --bench event_emit +QUENT_BENCH_PROFILE_TIME=10 QUENT_BENCH_PROFILE_HZ=9999 cargo bench -p quent-instrumentation-runtime --bench event_emit ``` ## Clear results