From 02be567b48cf39aadbdf314ba5f39ebc0deaed10 Mon Sep 17 00:00:00 2001
From: Mindaugas Vinkelis <mv@nikulipe.com>
Date: Thu, 24 Oct 2024 11:40:00 +0300
Subject: [PATCH] Metrics collect stress test

---
 opentelemetry-sdk/benches/metric.rs           |  39 +---
 .../src/testing/metrics/metric_reader.rs      |  35 ++++
 opentelemetry-sdk/src/testing/metrics/mod.rs  |   1 -
 stress/Cargo.toml                             |  40 +---
 stress/src/attributes.rs                      |  13 ++
 stress/src/{ => bin}/logs.rs                  |   2 +-
 stress/src/bin/metrics_collect.rs             | 176 ++++++++++++++++++
 stress/src/bin/metrics_counter.rs             |  36 ++++
 stress/src/bin/metrics_gauge.rs               |  33 ++++
 stress/src/bin/metrics_histogram.rs           |  39 ++++
 stress/src/bin/metrics_overflow.rs            |  28 +++
 stress/src/{ => bin}/random.rs                |  15 +-
 stress/src/{ => bin}/traces.rs                |  40 ++--
 stress/src/globals.rs                         |  17 ++
 stress/src/lib.rs                             |   3 +
 stress/src/metrics_counter.rs                 |  69 -------
 stress/src/metrics_gauge.rs                   |  66 -------
 stress/src/metrics_histogram.rs               |  69 -------
 stress/src/metrics_overflow.rs                |  46 -----
 19 files changed, 410 insertions(+), 357 deletions(-)
 create mode 100644 stress/src/attributes.rs
 rename stress/src/{ => bin}/logs.rs (98%)
 create mode 100644 stress/src/bin/metrics_collect.rs
 create mode 100644 stress/src/bin/metrics_counter.rs
 create mode 100644 stress/src/bin/metrics_gauge.rs
 create mode 100644 stress/src/bin/metrics_histogram.rs
 create mode 100644 stress/src/bin/metrics_overflow.rs
 rename stress/src/{ => bin}/random.rs (65%)
 rename stress/src/{ => bin}/traces.rs (61%)
 create mode 100644 stress/src/globals.rs
 create mode 100644 stress/src/lib.rs
 delete mode 100644 stress/src/metrics_counter.rs
 delete mode 100644 stress/src/metrics_gauge.rs
 delete mode 100644 stress/src/metrics_histogram.rs
 delete mode 100644 stress/src/metrics_overflow.rs

diff --git a/opentelemetry-sdk/benches/metric.rs b/opentelemetry-sdk/benches/metric.rs
index aaf3e6c0175..3937d70411b 100644
--- a/opentelemetry-sdk/benches/metric.rs
+++ b/opentelemetry-sdk/benches/metric.rs
@@ -1,9 +1,8 @@
 use rand::Rng;
-use std::sync::{Arc, Weak};
 
 use criterion::{criterion_group, criterion_main, Bencher, Criterion};
 use opentelemetry::{
-    metrics::{Counter, Histogram, MeterProvider as _, Result},
+    metrics::{Counter, Histogram, MeterProvider},
     Key, KeyValue,
 };
 use opentelemetry_sdk::{
@@ -14,34 +13,10 @@ use opentelemetry_sdk::{
         Aggregation, Instrument, InstrumentKind, ManualReader, Pipeline, SdkMeterProvider, Stream,
         View,
     },
+    testing::metrics::metric_reader::SharedReader,
     Resource,
 };
 
-#[derive(Clone, Debug)]
-struct SharedReader(Arc<dyn MetricReader>);
-
-impl MetricReader for SharedReader {
-    fn register_pipeline(&self, pipeline: Weak<Pipeline>) {
-        self.0.register_pipeline(pipeline)
-    }
-
-    fn collect(&self, rm: &mut ResourceMetrics) -> Result<()> {
-        self.0.collect(rm)
-    }
-
-    fn force_flush(&self) -> Result<()> {
-        self.0.force_flush()
-    }
-
-    fn shutdown(&self) -> Result<()> {
-        self.0.shutdown()
-    }
-
-    fn temporality(&self, kind: InstrumentKind) -> Temporality {
-        self.0.temporality(kind)
-    }
-}
-
 // * Summary *
 
 // rustc 1.68.0 (2c8cc3432 2023-03-06)
@@ -112,13 +87,13 @@ impl MetricReader for SharedReader {
 //                         time:   [726.87 ns 736.52 ns 747.09 ns]
 fn bench_counter(view: Option<Box<dyn View>>, temporality: &str) -> (SharedReader, Counter<u64>) {
     let rdr = if temporality == "cumulative" {
-        SharedReader(Arc::new(ManualReader::builder().build()))
+        SharedReader::new(ManualReader::builder().build())
     } else {
-        SharedReader(Arc::new(
+        SharedReader::new(
             ManualReader::builder()
                 .with_temporality(Temporality::Delta)
                 .build(),
-        ))
+        )
     };
     let mut builder = SdkMeterProvider::builder().with_reader(rdr.clone());
     if let Some(view) = view {
@@ -336,7 +311,7 @@ fn bench_histogram(bound_count: usize) -> (SharedReader, Histogram<u64>) {
         .unwrap(),
     );
 
-    let r = SharedReader(Arc::new(ManualReader::default()));
+    let r = SharedReader::new(ManualReader::default());
     let mut builder = SdkMeterProvider::builder().with_reader(r.clone());
     if let Some(view) = view {
         builder = builder.with_view(view);
@@ -377,7 +352,7 @@ fn histograms(c: &mut Criterion) {
 }
 
 fn benchmark_collect_histogram(b: &mut Bencher, n: usize) {
-    let r = SharedReader(Arc::new(ManualReader::default()));
+    let r = SharedReader::new(ManualReader::default());
     let mtr = SdkMeterProvider::builder()
         .with_reader(r.clone())
         .build()
diff --git a/opentelemetry-sdk/src/testing/metrics/metric_reader.rs b/opentelemetry-sdk/src/testing/metrics/metric_reader.rs
index 0b67b155a33..87182c43ebc 100644
--- a/opentelemetry-sdk/src/testing/metrics/metric_reader.rs
+++ b/opentelemetry-sdk/src/testing/metrics/metric_reader.rs
@@ -57,3 +57,38 @@ impl MetricReader for TestMetricReader {
         Temporality::default()
     }
 }
+
+/// Allow to clone [`MetricReader`], so it could be accessed in tests
+#[derive(Clone, Debug)]
+pub struct SharedReader(Arc<dyn MetricReader>);
+
+impl SharedReader {
+    pub fn new<R>(reader: R) -> Self
+    where
+        R: MetricReader,
+    {
+        Self(Arc::new(reader))
+    }
+}
+
+impl MetricReader for SharedReader {
+    fn register_pipeline(&self, pipeline: Weak<Pipeline>) {
+        self.0.register_pipeline(pipeline)
+    }
+
+    fn collect(&self, rm: &mut ResourceMetrics) -> Result<()> {
+        self.0.collect(rm)
+    }
+
+    fn force_flush(&self) -> Result<()> {
+        self.0.force_flush()
+    }
+
+    fn shutdown(&self) -> Result<()> {
+        self.0.shutdown()
+    }
+
+    fn temporality(&self, kind: InstrumentKind) -> Temporality {
+        self.0.temporality(kind)
+    }
+}
diff --git a/opentelemetry-sdk/src/testing/metrics/mod.rs b/opentelemetry-sdk/src/testing/metrics/mod.rs
index cac9f58ce4d..96d1ed84224 100644
--- a/opentelemetry-sdk/src/testing/metrics/mod.rs
+++ b/opentelemetry-sdk/src/testing/metrics/mod.rs
@@ -7,4 +7,3 @@ pub use in_memory_exporter::{InMemoryMetricsExporter, InMemoryMetricsExporterBui
 
 #[doc(hidden)]
 pub mod metric_reader;
-pub use metric_reader::TestMetricReader;
diff --git a/stress/Cargo.toml b/stress/Cargo.toml
index 0591cde7ebf..5b6790ca0ad 100644
--- a/stress/Cargo.toml
+++ b/stress/Cargo.toml
@@ -4,53 +4,19 @@ version = "0.1.0"
 edition = "2021"
 publish = false
 
-[[bin]] # Bin to run the metrics stress tests for Counter
-name = "metrics"
-path = "src/metrics_counter.rs"
-doc = false
-
-[[bin]] # Bin to run the metrics stress tests for Gauge
-name = "metrics_gauge"
-path = "src/metrics_gauge.rs"
-doc = false
-
-[[bin]] # Bin to run the metrics stress tests for Histogram
-name = "metrics_histogram"
-path = "src/metrics_histogram.rs"
-doc = false
-
-[[bin]] # Bin to run the metrics overflow stress tests
-name = "metrics_overflow"
-path = "src/metrics_overflow.rs"
-doc = false
-
-[[bin]] # Bin to run the logs stress tests
-name = "logs"
-path = "src/logs.rs"
-doc = false
-
-[[bin]] # Bin to run the traces stress tests
-name = "traces"
-path = "src/traces.rs"
-doc = false
-
-[[bin]] # Bin to run the stress tests to show the cost of random number generation
-name = "random"
-path = "src/random.rs"
-doc = false
-
 [dependencies]
 ctrlc = "3.2.5"
 lazy_static = "1.4.0"
 num_cpus = "1.15.0"
 opentelemetry = { path = "../opentelemetry", features = ["metrics", "logs", "trace", "logs_level_enabled"] }
-opentelemetry_sdk = { path = "../opentelemetry-sdk", features = ["metrics", "logs", "trace", "logs_level_enabled"] }
+opentelemetry_sdk = { path = "../opentelemetry-sdk", features = ["metrics", "logs", "trace", "logs_level_enabled", "testing"] }
 opentelemetry-appender-tracing = { path = "../opentelemetry-appender-tracing"}
 rand = { version = "0.8.4", features = ["small_rng"] }
 tracing = { workspace = true, features = ["std"]}
 tracing-subscriber = { workspace = true, features = ["registry", "std"] }
 num-format = "0.4.4"
 sysinfo = { version = "0.30.12", optional = true }
+clap = { version = "4.5.20", features = ["derive"] }
 
 [features]
-stats = ["sysinfo"]
\ No newline at end of file
+stats = ["sysinfo"]
diff --git a/stress/src/attributes.rs b/stress/src/attributes.rs
new file mode 100644
index 00000000000..3c855bf2843
--- /dev/null
+++ b/stress/src/attributes.rs
@@ -0,0 +1,13 @@
+use opentelemetry::KeyValue;
+use rand::{rngs::SmallRng, Rng};
+
+pub fn random_attribute_set3(rng: &mut SmallRng, values: &[&'static str]) -> [KeyValue; 3] {
+    let len = values.len();
+    unsafe {
+        [
+            KeyValue::new("attribute1", *values.get_unchecked(rng.gen_range(0..len))),
+            KeyValue::new("attribute2", *values.get_unchecked(rng.gen_range(0..len))),
+            KeyValue::new("attribute3", *values.get_unchecked(rng.gen_range(0..len))),
+        ]
+    }
+}
diff --git a/stress/src/logs.rs b/stress/src/bin/logs.rs
similarity index 98%
rename from stress/src/logs.rs
rename to stress/src/bin/logs.rs
index 7744708db9d..38fae423741 100644
--- a/stress/src/logs.rs
+++ b/stress/src/bin/logs.rs
@@ -15,7 +15,7 @@ use opentelemetry_sdk::logs::{LogProcessor, LoggerProvider};
 use tracing::error;
 use tracing_subscriber::prelude::*;
 
-mod throughput;
+use stress::throughput;
 
 #[derive(Debug)]
 pub struct NoOpLogProcessor;
diff --git a/stress/src/bin/metrics_collect.rs b/stress/src/bin/metrics_collect.rs
new file mode 100644
index 00000000000..615a7ecc2cc
--- /dev/null
+++ b/stress/src/bin/metrics_collect.rs
@@ -0,0 +1,176 @@
+use std::{
+    ops::DerefMut,
+    sync::{
+        atomic::{AtomicBool, AtomicUsize, Ordering},
+        Barrier,
+    },
+    time::{Duration, Instant},
+};
+
+use opentelemetry::metrics::{Histogram, MeterProvider};
+use opentelemetry_sdk::{
+    metrics::{
+        data::{ResourceMetrics, Temporality},
+        reader::MetricReader,
+        ManualReader, SdkMeterProvider,
+    },
+    testing::metrics::metric_reader::SharedReader,
+    Resource,
+};
+
+use stress::{
+    attributes::random_attribute_set3,
+    globals::{ATTRIBUTE_VALUES, CURRENT_RNG},
+};
+
+use clap::{Parser, ValueEnum};
+
+#[derive(Debug, Clone, Copy, ValueEnum)]
+enum CliTemporality {
+    Cumulative,
+    Delta,
+}
+
+/// Simple program to greet a person
+#[derive(Parser, Debug)]
+#[command(
+    version,
+    about = "Measure metrics performance while collecting",
+    long_about = "The purpose of this test is to see how collecing interferre with measurements.\n\
+    Most of the test measure how fast is collecting phase, but more important is\n\
+    that it doesn't \"stop-the-world\" while collection phase is running."
+)]
+struct Cli {
+    /// Select collection phase temporality
+    temporality: CliTemporality,
+}
+
+fn main() {
+    let cli = Cli::parse();
+    let temporality = match cli.temporality {
+        CliTemporality::Cumulative => Temporality::Cumulative,
+        CliTemporality::Delta => Temporality::Delta,
+    };
+    let reader = SharedReader::new(
+        ManualReader::builder()
+            .with_temporality(temporality)
+            .build(),
+    );
+    let provider = SdkMeterProvider::builder()
+        .with_reader(reader.clone())
+        .build();
+    // use histogram, as it is a bit more complicated during
+    let histogram = provider.meter("test").u64_histogram("hello").build();
+
+    calculate_measurements_during_collection(histogram, reader).print_results();
+}
+
+fn calculate_measurements_during_collection(
+    histogram: Histogram<u64>,
+    reader: SharedReader,
+) -> MeasurementResults {
+    // we don't need to use every single CPU, better leave other CPU for operating system work,
+    // so our running threads could be much more stable in performance.
+    // just for the record, this is has HUGE effect on my machine (laptop intel i7-1355u)
+    let num_threads = num_cpus::get() / 2;
+
+    let mut res = MeasurementResults {
+        total_measurements_count: 0,
+        total_time_collecting: 0,
+        num_iterations: 0,
+    };
+    let start = Instant::now();
+    while start.elapsed() < Duration::from_secs(3) {
+        res.num_iterations += 1;
+        let is_collecting = AtomicBool::new(false);
+        let measurements_while_collecting = AtomicUsize::new(0);
+        let time_while_collecting = AtomicUsize::new(0);
+        let barrier = Barrier::new(num_threads + 1);
+        std::thread::scope(|s| {
+            // first create bunch of measurements,
+            // so that collection phase wouldn't be "empty"
+            let mut handles = Vec::new();
+            for _t in 0..num_threads {
+                handles.push(s.spawn(|| {
+                    for _i in 0..1000 {
+                        CURRENT_RNG.with(|rng| {
+                            histogram.record(
+                                1,
+                                &random_attribute_set3(
+                                    rng.borrow_mut().deref_mut(),
+                                    ATTRIBUTE_VALUES.as_ref(),
+                                ),
+                            );
+                        });
+                    }
+                }));
+            }
+            for handle in handles {
+                handle.join().unwrap();
+            }
+
+            // simultaneously start collecting and creating more measurements
+            for _ in 0..num_threads - 1 {
+                s.spawn(|| {
+                    barrier.wait();
+                    let now = Instant::now();
+                    let mut count = 0;
+                    while is_collecting.load(Ordering::Acquire) {
+                        CURRENT_RNG.with(|rng| {
+                            histogram.record(
+                                1,
+                                &random_attribute_set3(
+                                    rng.borrow_mut().deref_mut(),
+                                    ATTRIBUTE_VALUES.as_ref(),
+                                ),
+                            );
+                        });
+                        count += 1;
+                    }
+                    measurements_while_collecting.fetch_add(count, Ordering::AcqRel);
+                    time_while_collecting
+                        .fetch_add(now.elapsed().as_micros() as usize, Ordering::AcqRel);
+                });
+            }
+
+            let collect_handle = s.spawn(|| {
+                let mut rm = ResourceMetrics {
+                    resource: Resource::empty(),
+                    scope_metrics: Vec::new(),
+                };
+                is_collecting.store(true, Ordering::Release);
+                barrier.wait();
+                reader.collect(&mut rm).unwrap();
+                is_collecting.store(false, Ordering::Release);
+            });
+            barrier.wait();
+            collect_handle.join().unwrap();
+        });
+        res.total_measurements_count += measurements_while_collecting.load(Ordering::Acquire);
+        res.total_time_collecting += time_while_collecting.load(Ordering::Acquire);
+    }
+    res
+}
+
+struct MeasurementResults {
+    total_measurements_count: usize,
+    total_time_collecting: usize,
+    num_iterations: usize,
+}
+
+impl MeasurementResults {
+    fn print_results(&self) {
+        println!(
+            "{:>10.2} measurements/ms",
+            self.total_measurements_count as f32 / (self.total_time_collecting as f32 / 1000.0f32)
+        );
+        println!(
+            "{:>10.2} measurements/it",
+            self.total_measurements_count as f32 / self.num_iterations as f32,
+        );
+        println!(
+            "{:>10.2} μs/it",
+            self.total_time_collecting as f32 / self.num_iterations as f32,
+        );
+    }
+}
diff --git a/stress/src/bin/metrics_counter.rs b/stress/src/bin/metrics_counter.rs
new file mode 100644
index 00000000000..0b53565beba
--- /dev/null
+++ b/stress/src/bin/metrics_counter.rs
@@ -0,0 +1,36 @@
+/*
+    Stress test results:
+    OS: Ubuntu 22.04.4 LTS (5.15.153.1-microsoft-standard-WSL2)
+    Hardware: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz, 16vCPUs,
+    RAM: 64.0 GB
+    ~9.5 M /sec
+
+    Hardware: AMD EPYC 7763 64-Core Processor - 2.44 GHz, 16vCPUs,
+    ~20 M /sec
+*/
+
+use std::ops::DerefMut;
+
+use opentelemetry::metrics::MeterProvider;
+use opentelemetry_sdk::metrics::{ManualReader, SdkMeterProvider};
+
+use stress::{
+    attributes::random_attribute_set3,
+    globals::{ATTRIBUTE_VALUES, CURRENT_RNG},
+    throughput,
+};
+
+fn main() {
+    let provider = SdkMeterProvider::builder()
+        .with_reader(ManualReader::builder().build())
+        .build();
+    let counter = provider.meter("test").u64_counter("hello").build();
+    throughput::test_throughput(move || {
+        CURRENT_RNG.with(|rng| {
+            counter.add(
+                1,
+                &random_attribute_set3(rng.borrow_mut().deref_mut(), ATTRIBUTE_VALUES.as_ref()),
+            );
+        });
+    });
+}
diff --git a/stress/src/bin/metrics_gauge.rs b/stress/src/bin/metrics_gauge.rs
new file mode 100644
index 00000000000..5b8fbb0326f
--- /dev/null
+++ b/stress/src/bin/metrics_gauge.rs
@@ -0,0 +1,33 @@
+/*
+    Stress test results:
+    OS: Ubuntu 22.04.4 LTS (5.15.153.1-microsoft-standard-WSL2)
+    Hardware: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz, 16vCPUs,
+    RAM: 64.0 GB
+    ~11.5 M/sec
+*/
+
+use std::ops::DerefMut;
+
+use opentelemetry::metrics::MeterProvider;
+use opentelemetry_sdk::metrics::{ManualReader, SdkMeterProvider};
+
+use stress::{
+    attributes::random_attribute_set3,
+    globals::{ATTRIBUTE_VALUES, CURRENT_RNG},
+    throughput,
+};
+
+fn main() {
+    let provider = SdkMeterProvider::builder()
+        .with_reader(ManualReader::builder().build())
+        .build();
+    let gauge = provider.meter("test").u64_gauge("test_gauge").build();
+    throughput::test_throughput(move || {
+        CURRENT_RNG.with(|rng| {
+            gauge.record(
+                1,
+                &random_attribute_set3(rng.borrow_mut().deref_mut(), ATTRIBUTE_VALUES.as_ref()),
+            );
+        });
+    });
+}
diff --git a/stress/src/bin/metrics_histogram.rs b/stress/src/bin/metrics_histogram.rs
new file mode 100644
index 00000000000..a04809dba7e
--- /dev/null
+++ b/stress/src/bin/metrics_histogram.rs
@@ -0,0 +1,39 @@
+/*
+    Stress test results:
+    OS: Ubuntu 22.04.4 LTS (5.15.153.1-microsoft-standard-WSL2)
+    Hardware: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz, 16vCPUs,
+    RAM: 64.0 GB
+    ~9.0 M/sec
+
+    Hardware: AMD EPYC 7763 64-Core Processor - 2.44 GHz, 16vCPUs,
+    ~12.0 M /sec
+*/
+
+use std::ops::DerefMut;
+
+use opentelemetry::metrics::MeterProvider;
+use opentelemetry_sdk::metrics::{ManualReader, SdkMeterProvider};
+
+use stress::{
+    attributes::random_attribute_set3,
+    globals::{ATTRIBUTE_VALUES, CURRENT_RNG},
+    throughput,
+};
+
+fn main() {
+    let provider = SdkMeterProvider::builder()
+        .with_reader(ManualReader::builder().build())
+        .build();
+    let histogram = provider
+        .meter("test")
+        .u64_histogram("test_histogram")
+        .build();
+    throughput::test_throughput(move || {
+        CURRENT_RNG.with(|rng| {
+            histogram.record(
+                1,
+                &random_attribute_set3(rng.borrow_mut().deref_mut(), ATTRIBUTE_VALUES.as_ref()),
+            );
+        });
+    });
+}
diff --git a/stress/src/bin/metrics_overflow.rs b/stress/src/bin/metrics_overflow.rs
new file mode 100644
index 00000000000..aa8c7fa8947
--- /dev/null
+++ b/stress/src/bin/metrics_overflow.rs
@@ -0,0 +1,28 @@
+/*
+    Stress test results:
+    OS: Ubuntu 22.04.4 LTS (5.15.153.1-microsoft-standard-WSL2)
+    Hardware: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz, 16vCPUs,
+    RAM: 64.0 GB
+    ~1.9 M/sec
+*/
+
+use opentelemetry::{metrics::MeterProvider, KeyValue};
+use opentelemetry_sdk::metrics::{ManualReader, SdkMeterProvider};
+
+use rand::Rng;
+use stress::{globals::CURRENT_RNG, throughput};
+
+fn main() {
+    // The main goal of this test is to ensure that OTel SDK is not growing its
+    // memory usage indefinitely even when user code misbehaves by producing
+    // unbounded metric points (unique time series).
+    // It also checks that SDK's internal logging is also done in a bounded way.
+    let provider = SdkMeterProvider::builder()
+        .with_reader(ManualReader::builder().build())
+        .build();
+    let counter = provider.meter("test").u64_counter("hello").build();
+    throughput::test_throughput(move || {
+        let rand = CURRENT_RNG.with(|rng| rng.borrow_mut().gen_range(0..100000000));
+        counter.add(1, &[KeyValue::new("A", rand)]);
+    });
+}
diff --git a/stress/src/random.rs b/stress/src/bin/random.rs
similarity index 65%
rename from stress/src/random.rs
rename to stress/src/bin/random.rs
index 9d4b7f997e4..b6b8a592d81 100644
--- a/stress/src/random.rs
+++ b/stress/src/bin/random.rs
@@ -6,19 +6,8 @@
     ~540 M/sec
 */
 
-use rand::{
-    rngs::{self},
-    Rng, SeedableRng,
-};
-
-mod throughput;
-
-use std::cell::RefCell;
-
-thread_local! {
-    /// Store random number generator for each thread
-    static CURRENT_RNG: RefCell<rngs::SmallRng> = RefCell::new(rngs::SmallRng::from_entropy());
-}
+use rand::Rng;
+use stress::{globals::CURRENT_RNG, throughput};
 
 fn main() {
     throughput::test_throughput(test_random_generation);
diff --git a/stress/src/traces.rs b/stress/src/bin/traces.rs
similarity index 61%
rename from stress/src/traces.rs
rename to stress/src/bin/traces.rs
index 62598b10ada..30a7fba6cee 100644
--- a/stress/src/traces.rs
+++ b/stress/src/bin/traces.rs
@@ -9,7 +9,6 @@
     ~10.6 M /sec
 */
 
-use lazy_static::lazy_static;
 use opentelemetry::{
     trace::{Span, SpanBuilder, TraceResult, Tracer, TracerProvider as _},
     Context, KeyValue,
@@ -19,15 +18,7 @@ use opentelemetry_sdk::{
     trace::{self as sdktrace, SpanProcessor},
 };
 
-mod throughput;
-
-lazy_static! {
-    static ref PROVIDER: sdktrace::TracerProvider = sdktrace::TracerProvider::builder()
-        .with_config(sdktrace::Config::default().with_sampler(sdktrace::Sampler::AlwaysOn))
-        .with_span_processor(NoOpSpanProcessor {})
-        .build();
-    static ref TRACER: sdktrace::Tracer = PROVIDER.tracer("stress");
-}
+use stress::throughput;
 
 #[derive(Debug)]
 pub struct NoOpSpanProcessor;
@@ -51,17 +42,20 @@ impl SpanProcessor for NoOpSpanProcessor {
 }
 
 fn main() {
-    throughput::test_throughput(test_span);
-}
-
-fn test_span() {
-    let span_builder = SpanBuilder::from_name("test_span").with_attributes(vec![
-        KeyValue::new("attribute_at_span_start1", "value1"),
-        KeyValue::new("attribute_at_span_start2", "value2"),
-    ]);
-
-    let mut span = TRACER.build(span_builder);
-    span.set_attribute(KeyValue::new("key3", "value3"));
-    span.set_attribute(KeyValue::new("key4", "value4"));
-    span.end();
+    let provider = sdktrace::TracerProvider::builder()
+        .with_config(sdktrace::Config::default().with_sampler(sdktrace::Sampler::AlwaysOn))
+        .with_span_processor(NoOpSpanProcessor {})
+        .build();
+    let tracer = provider.tracer("stress");
+    throughput::test_throughput(move || {
+        let span_builder = SpanBuilder::from_name("test_span").with_attributes(vec![
+            KeyValue::new("attribute_at_span_start1", "value1"),
+            KeyValue::new("attribute_at_span_start2", "value2"),
+        ]);
+
+        let mut span = tracer.build(span_builder);
+        span.set_attribute(KeyValue::new("key3", "value3"));
+        span.set_attribute(KeyValue::new("key4", "value4"));
+        span.end();
+    });
 }
diff --git a/stress/src/globals.rs b/stress/src/globals.rs
new file mode 100644
index 00000000000..fefead86a04
--- /dev/null
+++ b/stress/src/globals.rs
@@ -0,0 +1,17 @@
+use std::cell::RefCell;
+
+use lazy_static::lazy_static;
+use rand::{rngs, SeedableRng};
+
+lazy_static! {
+    pub static ref ATTRIBUTE_VALUES: [&'static str; 10] = [
+        "value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8", "value9",
+        "value10"
+    ];
+}
+
+thread_local! {
+
+    /// Store random number generator for each thread
+    pub static CURRENT_RNG: RefCell<rngs::SmallRng> = RefCell::new(rngs::SmallRng::from_entropy());
+}
diff --git a/stress/src/lib.rs b/stress/src/lib.rs
new file mode 100644
index 00000000000..8ea89201cec
--- /dev/null
+++ b/stress/src/lib.rs
@@ -0,0 +1,3 @@
+pub mod attributes;
+pub mod globals;
+pub mod throughput;
diff --git a/stress/src/metrics_counter.rs b/stress/src/metrics_counter.rs
deleted file mode 100644
index d64f2d11f88..00000000000
--- a/stress/src/metrics_counter.rs
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
-    Stress test results:
-    OS: Ubuntu 22.04.4 LTS (5.15.153.1-microsoft-standard-WSL2)
-    Hardware: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz, 16vCPUs,
-    RAM: 64.0 GB
-    ~9.5 M /sec
-
-    Hardware: AMD EPYC 7763 64-Core Processor - 2.44 GHz, 16vCPUs,
-    ~20 M /sec
-*/
-
-use lazy_static::lazy_static;
-use opentelemetry::{
-    metrics::{Counter, MeterProvider as _},
-    KeyValue,
-};
-use opentelemetry_sdk::metrics::{ManualReader, SdkMeterProvider};
-use rand::{
-    rngs::{self},
-    Rng, SeedableRng,
-};
-use std::cell::RefCell;
-
-mod throughput;
-
-lazy_static! {
-    static ref PROVIDER: SdkMeterProvider = SdkMeterProvider::builder()
-        .with_reader(ManualReader::builder().build())
-        .build();
-    static ref ATTRIBUTE_VALUES: [&'static str; 10] = [
-        "value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8", "value9",
-        "value10"
-    ];
-    static ref COUNTER: Counter<u64> = PROVIDER.meter("test").u64_counter("hello").build();
-}
-
-thread_local! {
-    /// Store random number generator for each thread
-    static CURRENT_RNG: RefCell<rngs::SmallRng> = RefCell::new(rngs::SmallRng::from_entropy());
-}
-
-fn main() {
-    throughput::test_throughput(test_counter);
-}
-
-fn test_counter() {
-    let len = ATTRIBUTE_VALUES.len();
-    let rands = CURRENT_RNG.with(|rng| {
-        let mut rng = rng.borrow_mut();
-        [
-            rng.gen_range(0..len),
-            rng.gen_range(0..len),
-            rng.gen_range(0..len),
-        ]
-    });
-    let index_first_attribute = rands[0];
-    let index_second_attribute = rands[1];
-    let index_third_attribute = rands[2];
-
-    // each attribute has 10 possible values, so there are 1000 possible combinations (time-series)
-    COUNTER.add(
-        1,
-        &[
-            KeyValue::new("attribute1", ATTRIBUTE_VALUES[index_first_attribute]),
-            KeyValue::new("attribute2", ATTRIBUTE_VALUES[index_second_attribute]),
-            KeyValue::new("attribute3", ATTRIBUTE_VALUES[index_third_attribute]),
-        ],
-    );
-}
diff --git a/stress/src/metrics_gauge.rs b/stress/src/metrics_gauge.rs
deleted file mode 100644
index d69efb3c4f5..00000000000
--- a/stress/src/metrics_gauge.rs
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
-    Stress test results:
-    OS: Ubuntu 22.04.4 LTS (5.15.153.1-microsoft-standard-WSL2)
-    Hardware: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz, 16vCPUs,
-    RAM: 64.0 GB
-    ~11.5 M/sec
-*/
-
-use lazy_static::lazy_static;
-use opentelemetry::{
-    metrics::{Gauge, MeterProvider as _},
-    KeyValue,
-};
-use opentelemetry_sdk::metrics::{ManualReader, SdkMeterProvider};
-use rand::{
-    rngs::{self},
-    Rng, SeedableRng,
-};
-use std::cell::RefCell;
-
-mod throughput;
-
-lazy_static! {
-    static ref PROVIDER: SdkMeterProvider = SdkMeterProvider::builder()
-        .with_reader(ManualReader::builder().build())
-        .build();
-    static ref ATTRIBUTE_VALUES: [&'static str; 10] = [
-        "value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8", "value9",
-        "value10"
-    ];
-    static ref GAUGE: Gauge<u64> = PROVIDER.meter("test").u64_gauge("test_gauge").build();
-}
-
-thread_local! {
-    /// Store random number generator for each thread
-    static CURRENT_RNG: RefCell<rngs::SmallRng> = RefCell::new(rngs::SmallRng::from_entropy());
-}
-
-fn main() {
-    throughput::test_throughput(test_gauge);
-}
-
-fn test_gauge() {
-    let len = ATTRIBUTE_VALUES.len();
-    let rands = CURRENT_RNG.with(|rng| {
-        let mut rng = rng.borrow_mut();
-        [
-            rng.gen_range(0..len),
-            rng.gen_range(0..len),
-            rng.gen_range(0..len),
-        ]
-    });
-    let index_first_attribute = rands[0];
-    let index_second_attribute = rands[1];
-    let index_third_attribute = rands[2];
-
-    // each attribute has 10 possible values, so there are 1000 possible combinations (time-series)
-    GAUGE.record(
-        1,
-        &[
-            KeyValue::new("attribute1", ATTRIBUTE_VALUES[index_first_attribute]),
-            KeyValue::new("attribute2", ATTRIBUTE_VALUES[index_second_attribute]),
-            KeyValue::new("attribute3", ATTRIBUTE_VALUES[index_third_attribute]),
-        ],
-    );
-}
diff --git a/stress/src/metrics_histogram.rs b/stress/src/metrics_histogram.rs
deleted file mode 100644
index 860d2bdd20a..00000000000
--- a/stress/src/metrics_histogram.rs
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
-    Stress test results:
-    OS: Ubuntu 22.04.4 LTS (5.15.153.1-microsoft-standard-WSL2)
-    Hardware: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz, 16vCPUs,
-    RAM: 64.0 GB
-    ~9.0 M/sec
-
-    Hardware: AMD EPYC 7763 64-Core Processor - 2.44 GHz, 16vCPUs,
-    ~12.0 M /sec
-*/
-
-use lazy_static::lazy_static;
-use opentelemetry::{
-    metrics::{Histogram, MeterProvider as _},
-    KeyValue,
-};
-use opentelemetry_sdk::metrics::{ManualReader, SdkMeterProvider};
-use rand::{
-    rngs::{self},
-    Rng, SeedableRng,
-};
-use std::cell::RefCell;
-
-mod throughput;
-
-lazy_static! {
-    static ref PROVIDER: SdkMeterProvider = SdkMeterProvider::builder()
-        .with_reader(ManualReader::builder().build())
-        .build();
-    static ref ATTRIBUTE_VALUES: [&'static str; 10] = [
-        "value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8", "value9",
-        "value10"
-    ];
-    static ref HISTOGRAM: Histogram<u64> = PROVIDER.meter("test").u64_histogram("hello").build();
-}
-
-thread_local! {
-    /// Store random number generator for each thread
-    static CURRENT_RNG: RefCell<rngs::SmallRng> = RefCell::new(rngs::SmallRng::from_entropy());
-}
-
-fn main() {
-    throughput::test_throughput(test_histogram);
-}
-
-fn test_histogram() {
-    let len = ATTRIBUTE_VALUES.len();
-    let rands = CURRENT_RNG.with(|rng| {
-        let mut rng = rng.borrow_mut();
-        [
-            rng.gen_range(0..len),
-            rng.gen_range(0..len),
-            rng.gen_range(0..len),
-        ]
-    });
-    let index_first_attribute = rands[0];
-    let index_second_attribute = rands[1];
-    let index_third_attribute = rands[2];
-
-    // each attribute has 10 possible values, so there are 1000 possible combinations (time-series)
-    HISTOGRAM.record(
-        1,
-        &[
-            KeyValue::new("attribute1", ATTRIBUTE_VALUES[index_first_attribute]),
-            KeyValue::new("attribute2", ATTRIBUTE_VALUES[index_second_attribute]),
-            KeyValue::new("attribute3", ATTRIBUTE_VALUES[index_third_attribute]),
-        ],
-    );
-}
diff --git a/stress/src/metrics_overflow.rs b/stress/src/metrics_overflow.rs
deleted file mode 100644
index bbd79db780a..00000000000
--- a/stress/src/metrics_overflow.rs
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
-    Stress test results:
-    OS: Ubuntu 22.04.4 LTS (5.15.153.1-microsoft-standard-WSL2)
-    Hardware: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz, 16vCPUs,
-    RAM: 64.0 GB
-    ~1.9 M/sec
-*/
-
-use lazy_static::lazy_static;
-use opentelemetry::{
-    metrics::{Counter, MeterProvider as _},
-    KeyValue,
-};
-use opentelemetry_sdk::metrics::{ManualReader, SdkMeterProvider};
-use rand::{
-    rngs::{self},
-    Rng, SeedableRng,
-};
-use std::cell::RefCell;
-
-mod throughput;
-
-lazy_static! {
-    static ref PROVIDER: SdkMeterProvider = SdkMeterProvider::builder()
-        .with_reader(ManualReader::builder().build())
-        .build();
-    static ref COUNTER: Counter<u64> = PROVIDER.meter("test").u64_counter("hello").build();
-}
-
-thread_local! {
-    /// Store random number generator for each thread
-    static CURRENT_RNG: RefCell<rngs::SmallRng> = RefCell::new(rngs::SmallRng::from_entropy());
-}
-
-fn main() {
-    throughput::test_throughput(test_counter);
-}
-
-fn test_counter() {
-    // The main goal of this test is to ensure that OTel SDK is not growing its
-    // memory usage indefinitely even when user code misbehaves by producing
-    // unbounded metric points (unique time series).
-    // It also checks that SDK's internal logging is also done in a bounded way.
-    let rand = CURRENT_RNG.with(|rng| rng.borrow_mut().gen_range(0..100000000));
-    COUNTER.add(1, &[KeyValue::new("A", rand)]);
-}