From e12fb49d78e26a59525f28f8ec5c7b4111166fea Mon Sep 17 00:00:00 2001 From: Arpad Borsos Date: Thu, 7 Jan 2021 10:24:54 +0100 Subject: [PATCH 1/4] fix: Remove abandoned `im` crate in favor of Arc::make_mut --- sentry-core/Cargo.toml | 3 +-- sentry-core/src/hub.rs | 7 +++--- sentry-core/src/scope/real.rs | 45 ++++++++++++++++++++--------------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/sentry-core/Cargo.toml b/sentry-core/Cargo.toml index 18b1fb345..09c23f3c1 100644 --- a/sentry-core/Cargo.toml +++ b/sentry-core/Cargo.toml @@ -16,7 +16,7 @@ all-features = true [features] default = [] -client = ["im", "rand"] +client = ["rand"] # I would love to just have a `log` feature, but this is used inside a macro, # and macros actually expand features (and extern crate) where they are used! debug-logs = ["log_"] @@ -26,7 +26,6 @@ test = ["client"] sentry-types = { version = "0.21.0", path = "../sentry-types" } serde = { version = "1.0.104", features = ["derive"] } lazy_static = "1.4.0" -im = { version = "15.0.0", optional = true } rand = { version = "0.7.3", optional = true } serde_json = "1.0.46" log_ = { package = "log", version = "0.4.8", optional = true, features = ["std"] } diff --git a/sentry-core/src/hub.rs b/sentry-core/src/hub.rs index f3df858c0..bcd3ba200 100644 --- a/sentry-core/src/hub.rs +++ b/sentry-core/src/hub.rs @@ -427,16 +427,17 @@ impl Hub { if let Some(ref client) = top.client { let scope = Arc::make_mut(&mut top.scope); let options = client.options(); + let breadcrumbs = Arc::make_mut(&mut scope.breadcrumbs); for breadcrumb in breadcrumb.into_breadcrumbs() { let breadcrumb_opt = match options.before_breadcrumb { Some(ref callback) => callback(breadcrumb), None => Some(breadcrumb) }; if let Some(breadcrumb) = breadcrumb_opt { - scope.breadcrumbs.push_back(breadcrumb); + breadcrumbs.push_back(breadcrumb); } - while scope.breadcrumbs.len() > options.max_breadcrumbs { - scope.breadcrumbs.pop_front(); + while breadcrumbs.len() > options.max_breadcrumbs { + breadcrumbs.pop_front(); } } } diff --git a/sentry-core/src/scope/real.rs b/sentry-core/src/scope/real.rs index c900599b1..cb4b7627c 100644 --- a/sentry-core/src/scope/real.rs +++ b/sentry-core/src/scope/real.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::collections::{HashMap, VecDeque}; use std::fmt; use std::sync::{Arc, Mutex, PoisonError, RwLock}; @@ -36,12 +37,12 @@ pub struct Scope { pub(crate) level: Option, pub(crate) fingerprint: Option]>>, pub(crate) transaction: Option>, - pub(crate) breadcrumbs: im::Vector, + pub(crate) breadcrumbs: Arc>, pub(crate) user: Option>, - pub(crate) extra: im::HashMap, - pub(crate) tags: im::HashMap, - pub(crate) contexts: im::HashMap, - pub(crate) event_processors: im::Vector>, + pub(crate) extra: Arc>, + pub(crate) tags: Arc>, + pub(crate) contexts: Arc>, + pub(crate) event_processors: Arc>>, pub(crate) session: Arc>>, } @@ -157,7 +158,7 @@ impl Scope { /// Deletes current breadcrumbs from the scope. pub fn clear_breadcrumbs(&mut self) { - self.breadcrumbs.clear(); + self.breadcrumbs = Default::default(); } /// Sets a level override. @@ -183,34 +184,34 @@ impl Scope { /// Sets a tag to a specific value. pub fn set_tag(&mut self, key: &str, value: V) { - self.tags.insert(key.to_string(), value.to_string()); + Arc::make_mut(&mut self.tags).insert(key.to_string(), value.to_string()); } /// Removes a tag. /// /// If the tag is not set, does nothing. pub fn remove_tag(&mut self, key: &str) { - self.tags.remove(key); + Arc::make_mut(&mut self.tags).remove(key); } /// Sets a context for a key. pub fn set_context>(&mut self, key: &str, value: C) { - self.contexts.insert(key.to_string(), value.into()); + Arc::make_mut(&mut self.contexts).insert(key.to_string(), value.into()); } /// Removes a context for a key. pub fn remove_context(&mut self, key: &str) { - self.contexts.remove(key); + Arc::make_mut(&mut self.contexts).remove(key); } /// Sets a extra to a specific value. pub fn set_extra(&mut self, key: &str, value: Value) { - self.extra.insert(key.to_string(), value); + Arc::make_mut(&mut self.extra).insert(key.to_string(), value); } /// Removes a extra. pub fn remove_extra(&mut self, key: &str) { - self.extra.remove(key); + Arc::make_mut(&mut self.extra).remove(key); } /// Add an event processor to the scope. @@ -218,7 +219,7 @@ impl Scope { &mut self, f: Box) -> Option> + Send + Sync>, ) { - self.event_processors.push_back(Arc::new(f)); + Arc::make_mut(&mut self.event_processors).push(Arc::new(f)); } /// Applies the contained scoped data to fill an event. @@ -234,12 +235,18 @@ impl Scope { } } + event.breadcrumbs.extend(self.breadcrumbs.iter().cloned()); event - .breadcrumbs - .extend(self.breadcrumbs.clone().into_iter()); - event.extra.extend(self.extra.clone().into_iter()); - event.tags.extend(self.tags.clone().into_iter()); - event.contexts.extend(self.contexts.clone().into_iter()); + .extra + .extend(self.extra.iter().map(|(k, v)| (k.to_owned(), v.to_owned()))); + event + .tags + .extend(self.tags.iter().map(|(k, v)| (k.to_owned(), v.to_owned()))); + event.contexts.extend( + self.contexts + .iter() + .map(|(k, v)| (k.to_owned(), v.to_owned())), + ); if event.transaction.is_none() { if let Some(txn) = self.transaction.as_deref() { @@ -255,7 +262,7 @@ impl Scope { } } - for processor in &self.event_processors { + for processor in self.event_processors.as_ref() { let id = event.event_id; event = match processor(event) { Some(event) => event, From f30a7d2d9c799198d97b45f5b0396790839eccf8 Mon Sep 17 00:00:00 2001 From: Arpad Borsos Date: Fri, 8 Jan 2021 10:30:49 +0100 Subject: [PATCH 2/4] meta: Add a small benchmark suite to sentry-core (#306) The benchmarks so far test the scope with tags and breadcrumbs in a few situations: * inactive client, * active client, only scope manipulation * active client, scope and capturing messages The Benchmarks also allow measuring the "minimal API" mode, which basically removes the complete implementation at compile time. Measurements on my system are: Minimal API (compile-time noop): ~2.6ns Tags: * inactive: 66ns * active: 26us * capture: 53us Breadcrumbs: * inactive: 2us * active: 52us * capture: 88us --- sentry-core/Cargo.toml | 7 +- sentry-core/benches/scope_benchmark.rs | 159 +++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 sentry-core/benches/scope_benchmark.rs diff --git a/sentry-core/Cargo.toml b/sentry-core/Cargo.toml index 18b1fb345..db117b3e4 100644 --- a/sentry-core/Cargo.toml +++ b/sentry-core/Cargo.toml @@ -14,6 +14,10 @@ edition = "2018" [package.metadata.docs.rs] all-features = true +[[bench]] +name = "scope_benchmark" +harness = false + [features] default = [] client = ["im", "rand"] @@ -38,4 +42,5 @@ log_ = { package = "log", version = "0.4.8", optional = true, features = ["std"] sentry = { version = "0.21.0", path = "../sentry", default-features = false, features = ["test"] } thiserror = "1.0.15" anyhow = "1.0.30" -tokio = { version = "1.0", features = ["rt", "macros"] } +tokio = { version = "1.0", features = ["rt", "rt-multi-thread", "macros"] } +criterion = "0.3" diff --git a/sentry-core/benches/scope_benchmark.rs b/sentry-core/benches/scope_benchmark.rs new file mode 100644 index 000000000..53c74dd91 --- /dev/null +++ b/sentry-core/benches/scope_benchmark.rs @@ -0,0 +1,159 @@ +//! Sentry Scope Benchmarks +//! +//! Run the benchmarks with: +//! +//! ```text +//! $ cargo bench -p sentry-core +//! ``` +//! +//! We have the following tests: +//! * [`scope_with_breadcrumbs`] +//! * [`scope_with_tags`] +//! +//! We do test the following permutations: +//! * No active Hub is bound, meaning most API functions will just noop +//! * Doing scope manipulation with an active Hub, but *not* capturing any messages +//! * Doing scope manipulation with an active Hub, and capturing messages, discarding +//! them in the transport layer. +//! +//! # Testing the minimal API +//! Due to our circular dev-dependency on `sentry`, we will *always* run with the +//! `client` feature. To test without it, one needs to comment the circular dependency +//! before running the benchmark. + +#[cfg(feature = "client")] +use std::sync::Arc; + +use criterion::{criterion_group, criterion_main, Criterion}; +use sentry::protocol::Breadcrumb; +#[cfg(not(feature = "client"))] +use sentry_core as sentry; + +/// Tests Scopes with Breadcrumbs +/// +/// This uses the [`sentry::add_breadcrumb`] API in *callback mode*, which means +/// it is essentially a noop when the current Hub is inactive. +fn scope_with_breadcrumbs(capture: bool) { + for i in 0..50 { + sentry::add_breadcrumb(|| Breadcrumb { + message: Some(format!("Breadcrumb {}", i)), + ..Default::default() + }); + } + + if capture { + sentry::capture_message("capturing on outer scope", sentry::Level::Info); + } + + sentry::with_scope( + |_| (), + || { + // 50 + 70 exceeds the default max_breadcrumbs of 100 + for i in 0..70 { + sentry::add_breadcrumb(|| Breadcrumb { + message: Some(format!("Breadcrumb {}", i)), + ..Default::default() + }); + } + + if capture { + sentry::capture_message("capturing within a nested scope", sentry::Level::Info); + } + }, + ); + + sentry::configure_scope(|scope| scope.clear()); +} + +/// Tests Scopes with Tags +/// +/// This uses the [`sentry::Scope::set_tag`] function to define, and then overwrite/extend +/// the set of tags. +fn scope_with_tags(capture: bool) { + sentry::configure_scope(|scope| { + for i in 0..20 { + scope.set_tag(&format!("tag {}", i), format!("tag value {}", i)); + } + }); + + if capture { + sentry::capture_message("capturing on outer scope", sentry::Level::Info); + } + + sentry::with_scope( + |scope| { + for i in 10..30 { + // since this is a hashmap, we basically overwrite 10, and add 10 new tags + scope.set_tag(&format!("tag {}", i), format!("tag value {}", i)); + } + }, + || { + if capture { + sentry::capture_message("capturing within a nested scope", sentry::Level::Info); + } + }, + ); + + sentry::configure_scope(|scope| scope.clear()); +} + +/// Returns a new *active* [`sentry::Hub`] which discards Events in the Transport. +#[cfg(feature = "client")] +fn discarding_hub() -> sentry::Hub { + struct NoopTransport; + + impl sentry::Transport for NoopTransport { + fn send_envelope(&self, envelope: sentry::Envelope) { + drop(envelope) + } + } + + let client = Arc::new(sentry::Client::from(sentry::ClientOptions { + dsn: Some("https://public@sentry.invalid/1".parse().unwrap()), + // lol, this double arcing -_- + transport: Some(Arc::new(Arc::new(NoopTransport))), + // before_send: Some(Arc::new(|_| None)), + ..Default::default() + })); + let scope = Arc::new(sentry::Scope::default()); + sentry::Hub::new(Some(client), scope) +} + +fn scope_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("scoped-tags"); + + group.bench_function("no-client", |b| b.iter(|| scope_with_tags(true))); + #[cfg(feature = "client")] + { + group.bench_function("with-client", |b| { + let hub = Arc::new(discarding_hub()); + sentry::Hub::run(hub, || b.iter(|| scope_with_tags(false))) + }); + group.bench_function("dropping-client", |b| { + let hub = Arc::new(discarding_hub()); + sentry::Hub::run(hub, || b.iter(|| scope_with_tags(true))) + }); + } + + group.finish(); + + let mut group = c.benchmark_group("scoped-breadcrumbs"); + + group.bench_function("no-client", |b| b.iter(|| scope_with_breadcrumbs(true))); + #[cfg(feature = "client")] + { + group.bench_function("with-client", |b| { + let hub = Arc::new(discarding_hub()); + sentry::Hub::run(hub, || b.iter(|| scope_with_breadcrumbs(false))) + }); + group.bench_function("dropping-client", |b| { + let hub = Arc::new(discarding_hub()); + sentry::Hub::run(hub, || b.iter(|| scope_with_breadcrumbs(true))) + }); + } + + group.finish(); +} + +criterion_group!(benches, scope_benchmark); +criterion_main!(benches); From 16c9cedd04a333c061f7e1e56e2ac4a459d23205 Mon Sep 17 00:00:00 2001 From: Arpad Borsos Date: Thu, 7 Jan 2021 10:24:54 +0100 Subject: [PATCH 3/4] fix: Remove abandoned `im` crate in favor of Arc::make_mut --- sentry-core/Cargo.toml | 3 +-- sentry-core/src/hub.rs | 7 +++--- sentry-core/src/scope/real.rs | 45 ++++++++++++++++++++--------------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/sentry-core/Cargo.toml b/sentry-core/Cargo.toml index db117b3e4..d2fed7365 100644 --- a/sentry-core/Cargo.toml +++ b/sentry-core/Cargo.toml @@ -20,7 +20,7 @@ harness = false [features] default = [] -client = ["im", "rand"] +client = ["rand"] # I would love to just have a `log` feature, but this is used inside a macro, # and macros actually expand features (and extern crate) where they are used! debug-logs = ["log_"] @@ -30,7 +30,6 @@ test = ["client"] sentry-types = { version = "0.21.0", path = "../sentry-types" } serde = { version = "1.0.104", features = ["derive"] } lazy_static = "1.4.0" -im = { version = "15.0.0", optional = true } rand = { version = "0.7.3", optional = true } serde_json = "1.0.46" log_ = { package = "log", version = "0.4.8", optional = true, features = ["std"] } diff --git a/sentry-core/src/hub.rs b/sentry-core/src/hub.rs index f3df858c0..bcd3ba200 100644 --- a/sentry-core/src/hub.rs +++ b/sentry-core/src/hub.rs @@ -427,16 +427,17 @@ impl Hub { if let Some(ref client) = top.client { let scope = Arc::make_mut(&mut top.scope); let options = client.options(); + let breadcrumbs = Arc::make_mut(&mut scope.breadcrumbs); for breadcrumb in breadcrumb.into_breadcrumbs() { let breadcrumb_opt = match options.before_breadcrumb { Some(ref callback) => callback(breadcrumb), None => Some(breadcrumb) }; if let Some(breadcrumb) = breadcrumb_opt { - scope.breadcrumbs.push_back(breadcrumb); + breadcrumbs.push_back(breadcrumb); } - while scope.breadcrumbs.len() > options.max_breadcrumbs { - scope.breadcrumbs.pop_front(); + while breadcrumbs.len() > options.max_breadcrumbs { + breadcrumbs.pop_front(); } } } diff --git a/sentry-core/src/scope/real.rs b/sentry-core/src/scope/real.rs index c900599b1..cb4b7627c 100644 --- a/sentry-core/src/scope/real.rs +++ b/sentry-core/src/scope/real.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::collections::{HashMap, VecDeque}; use std::fmt; use std::sync::{Arc, Mutex, PoisonError, RwLock}; @@ -36,12 +37,12 @@ pub struct Scope { pub(crate) level: Option, pub(crate) fingerprint: Option]>>, pub(crate) transaction: Option>, - pub(crate) breadcrumbs: im::Vector, + pub(crate) breadcrumbs: Arc>, pub(crate) user: Option>, - pub(crate) extra: im::HashMap, - pub(crate) tags: im::HashMap, - pub(crate) contexts: im::HashMap, - pub(crate) event_processors: im::Vector>, + pub(crate) extra: Arc>, + pub(crate) tags: Arc>, + pub(crate) contexts: Arc>, + pub(crate) event_processors: Arc>>, pub(crate) session: Arc>>, } @@ -157,7 +158,7 @@ impl Scope { /// Deletes current breadcrumbs from the scope. pub fn clear_breadcrumbs(&mut self) { - self.breadcrumbs.clear(); + self.breadcrumbs = Default::default(); } /// Sets a level override. @@ -183,34 +184,34 @@ impl Scope { /// Sets a tag to a specific value. pub fn set_tag(&mut self, key: &str, value: V) { - self.tags.insert(key.to_string(), value.to_string()); + Arc::make_mut(&mut self.tags).insert(key.to_string(), value.to_string()); } /// Removes a tag. /// /// If the tag is not set, does nothing. pub fn remove_tag(&mut self, key: &str) { - self.tags.remove(key); + Arc::make_mut(&mut self.tags).remove(key); } /// Sets a context for a key. pub fn set_context>(&mut self, key: &str, value: C) { - self.contexts.insert(key.to_string(), value.into()); + Arc::make_mut(&mut self.contexts).insert(key.to_string(), value.into()); } /// Removes a context for a key. pub fn remove_context(&mut self, key: &str) { - self.contexts.remove(key); + Arc::make_mut(&mut self.contexts).remove(key); } /// Sets a extra to a specific value. pub fn set_extra(&mut self, key: &str, value: Value) { - self.extra.insert(key.to_string(), value); + Arc::make_mut(&mut self.extra).insert(key.to_string(), value); } /// Removes a extra. pub fn remove_extra(&mut self, key: &str) { - self.extra.remove(key); + Arc::make_mut(&mut self.extra).remove(key); } /// Add an event processor to the scope. @@ -218,7 +219,7 @@ impl Scope { &mut self, f: Box) -> Option> + Send + Sync>, ) { - self.event_processors.push_back(Arc::new(f)); + Arc::make_mut(&mut self.event_processors).push(Arc::new(f)); } /// Applies the contained scoped data to fill an event. @@ -234,12 +235,18 @@ impl Scope { } } + event.breadcrumbs.extend(self.breadcrumbs.iter().cloned()); event - .breadcrumbs - .extend(self.breadcrumbs.clone().into_iter()); - event.extra.extend(self.extra.clone().into_iter()); - event.tags.extend(self.tags.clone().into_iter()); - event.contexts.extend(self.contexts.clone().into_iter()); + .extra + .extend(self.extra.iter().map(|(k, v)| (k.to_owned(), v.to_owned()))); + event + .tags + .extend(self.tags.iter().map(|(k, v)| (k.to_owned(), v.to_owned()))); + event.contexts.extend( + self.contexts + .iter() + .map(|(k, v)| (k.to_owned(), v.to_owned())), + ); if event.transaction.is_none() { if let Some(txn) = self.transaction.as_deref() { @@ -255,7 +262,7 @@ impl Scope { } } - for processor in &self.event_processors { + for processor in self.event_processors.as_ref() { let id = event.event_id; event = match processor(event) { Some(event) => event, From 3ee9b8c4780616f705cc471df7379a01b2511c19 Mon Sep 17 00:00:00 2001 From: Arpad Borsos Date: Fri, 8 Jan 2021 11:16:46 +0100 Subject: [PATCH 4/4] Add more benchmarks With a populated outer scope, do a few nested `with_scope` calls and add tags / breadcrumbs in those. Perf numbers are similar to before: -8% for tags (speedup) and +2% for breadcrumbs, so quite negligible. --- sentry-core/benches/scope_benchmark.rs | 78 +++++++++++++++++++------- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/sentry-core/benches/scope_benchmark.rs b/sentry-core/benches/scope_benchmark.rs index 53c74dd91..6f0027695 100644 --- a/sentry-core/benches/scope_benchmark.rs +++ b/sentry-core/benches/scope_benchmark.rs @@ -21,6 +21,7 @@ //! `client` feature. To test without it, one needs to comment the circular dependency //! before running the benchmark. +use std::ops::Range; #[cfg(feature = "client")] use std::sync::Arc; @@ -34,12 +35,7 @@ use sentry_core as sentry; /// This uses the [`sentry::add_breadcrumb`] API in *callback mode*, which means /// it is essentially a noop when the current Hub is inactive. fn scope_with_breadcrumbs(capture: bool) { - for i in 0..50 { - sentry::add_breadcrumb(|| Breadcrumb { - message: Some(format!("Breadcrumb {}", i)), - ..Default::default() - }); - } + add_breadcrumbs(0..50); if capture { sentry::capture_message("capturing on outer scope", sentry::Level::Info); @@ -49,12 +45,7 @@ fn scope_with_breadcrumbs(capture: bool) { |_| (), || { // 50 + 70 exceeds the default max_breadcrumbs of 100 - for i in 0..70 { - sentry::add_breadcrumb(|| Breadcrumb { - message: Some(format!("Breadcrumb {}", i)), - ..Default::default() - }); - } + add_breadcrumbs(50..120); if capture { sentry::capture_message("capturing within a nested scope", sentry::Level::Info); @@ -65,16 +56,28 @@ fn scope_with_breadcrumbs(capture: bool) { sentry::configure_scope(|scope| scope.clear()); } +/// This does multiple nested [`sentry::with_scope`] calls that manipulates breadcrumbs +fn outer_scope_with_breadcrumbs() { + sentry::with_scope( + |_| (), + || { + add_breadcrumbs(20..50); + sentry::with_scope( + |_| (), + || { + add_breadcrumbs(50..80); + }, + ) + }, + ) +} + /// Tests Scopes with Tags /// /// This uses the [`sentry::Scope::set_tag`] function to define, and then overwrite/extend /// the set of tags. fn scope_with_tags(capture: bool) { - sentry::configure_scope(|scope| { - for i in 0..20 { - scope.set_tag(&format!("tag {}", i), format!("tag value {}", i)); - } - }); + sentry::configure_scope(|scope| add_tags(scope, 0..20)); if capture { sentry::capture_message("capturing on outer scope", sentry::Level::Info); @@ -82,10 +85,8 @@ fn scope_with_tags(capture: bool) { sentry::with_scope( |scope| { - for i in 10..30 { - // since this is a hashmap, we basically overwrite 10, and add 10 new tags - scope.set_tag(&format!("tag {}", i), format!("tag value {}", i)); - } + // since this is a hashmap, we basically overwrite 10, and add 10 new tags + add_tags(scope, 10..30) }, || { if capture { @@ -97,6 +98,31 @@ fn scope_with_tags(capture: bool) { sentry::configure_scope(|scope| scope.clear()); } +/// This does multiple nested [`sentry::with_scope`] calls that manipulates tags +fn outer_scope_with_tags() { + sentry::with_scope( + |scope| add_tags(scope, 20..22), + || sentry::with_scope(|scope| add_tags(scope, 22..24), || {}), + ) +} + +/// Adds a bunch of breadcrumbs +fn add_breadcrumbs(range: Range) { + for i in range { + sentry::add_breadcrumb(|| Breadcrumb { + message: Some(format!("Breadcrumb {}", i)), + ..Default::default() + }); + } +} + +/// Adds a bunch of tags +fn add_tags(scope: &mut sentry::Scope, range: Range) { + for i in range { + scope.set_tag(&format!("tag {}", i), format!("tag value {}", i)); + } +} + /// Returns a new *active* [`sentry::Hub`] which discards Events in the Transport. #[cfg(feature = "client")] fn discarding_hub() -> sentry::Hub { @@ -133,6 +159,11 @@ fn scope_benchmark(c: &mut Criterion) { let hub = Arc::new(discarding_hub()); sentry::Hub::run(hub, || b.iter(|| scope_with_tags(true))) }); + group.bench_function("outer-scope", |b| { + let hub = Arc::new(discarding_hub()); + sentry::configure_scope(|scope| add_tags(scope, 0..20)); + sentry::Hub::run(hub, || b.iter(outer_scope_with_tags)) + }); } group.finish(); @@ -150,6 +181,11 @@ fn scope_benchmark(c: &mut Criterion) { let hub = Arc::new(discarding_hub()); sentry::Hub::run(hub, || b.iter(|| scope_with_breadcrumbs(true))) }); + group.bench_function("outer-scope", |b| { + let hub = Arc::new(discarding_hub()); + add_breadcrumbs(0..20); + sentry::Hub::run(hub, || b.iter(outer_scope_with_breadcrumbs)) + }); } group.finish();