Repository: commonwarexyz/monorepo
Pull request #962: [runtime] Reduce Duplicate Metrics Usage (with `spawn_metrics!`)
Author: login: patrick-ogrady
Created: 2025-05-19T19:59:34Z | Merged: 2025-05-19T22:06:39Z

Replaces: #960 
Resolves: #958 

## Summary
- factor out metrics setup macro for spawners
- include Label creation inside macro
- use new macro in deterministic and tokio runtimes


Diff:
diff --git a/runtime/src/deterministic.rs b/runtime/src/deterministic.rs
index ed32f5ea69..1b348e5576 100644
--- a/runtime/src/deterministic.rs
+++ b/runtime/src/deterministic.rs
@@ -721,18 +721,7 @@ impl crate::Spawner for Context {
         assert!(!self.spawned, "already spawned");
 
         // Get metrics
-        let label = Label::future(self.name.clone());
-        self.executor
-            .metrics
-            .tasks_spawned
-            .get_or_create(&label)
-            .inc();
-        let gauge = self
-            .executor
-            .metrics
-            .tasks_running
-            .get_or_create(&label)
-            .clone();
+        let (label, gauge) = spawn_metrics!(self, future);
 
         // Set up the task
         let executor = self.executor.clone();
@@ -754,18 +743,7 @@ impl crate::Spawner for Context {
         self.spawned = true;
 
         // Get metrics
-        let label = Label::future(self.name.clone());
-        self.executor
-            .metrics
-            .tasks_spawned
-            .get_or_create(&label)
-            .inc();
-        let gauge = self
-            .executor
-            .metrics
-            .tasks_running
-            .get_or_create(&label)
-            .clone();
+        let (label, gauge) = spawn_metrics!(self, future);
 
         // Set up the task
         let executor = self.executor.clone();
@@ -787,22 +765,7 @@ impl crate::Spawner for Context {
         assert!(!self.spawned, "already spawned");
 
         // Get metrics
-        let label = if dedicated {
-            Label::blocking_dedicated(self.name.clone())
-        } else {
-            Label::blocking_shared(self.name.clone())
-        };
-        self.executor
-            .metrics
-            .tasks_spawned
-            .get_or_create(&label)
-            .inc();
-        let gauge = self
-            .executor
-            .metrics
-            .tasks_running
-            .get_or_create(&label)
-            .clone();
+        let (label, gauge) = spawn_metrics!(self, blocking, dedicated);
 
         // Initialize the blocking task
         let executor = self.executor.clone();
@@ -824,22 +787,7 @@ impl crate::Spawner for Context {
         self.spawned = true;
 
         // Get metrics
-        let label = if dedicated {
-            Label::blocking_dedicated(self.name.clone())
-        } else {
-            Label::blocking_shared(self.name.clone())
-        };
-        self.executor
-            .metrics
-            .tasks_spawned
-            .get_or_create(&label)
-            .inc();
-        let gauge = self
-            .executor
-            .metrics
-            .tasks_running
-            .get_or_create(&label)
-            .clone();
+        let (label, gauge) = spawn_metrics!(self, blocking, dedicated);
 
         // Set up the task
         let executor = self.executor.clone();
diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs
index c06e6363bf..d5756dfa73 100644
--- a/runtime/src/lib.rs
+++ b/runtime/src/lib.rs
@@ -27,6 +27,9 @@ use std::{
 };
 use thiserror::Error;
 
+#[macro_use]
+mod macros;
+
 pub mod deterministic;
 pub mod mocks;
 cfg_if::cfg_if! {
diff --git a/runtime/src/macros.rs b/runtime/src/macros.rs
new file mode 100644
index 0000000000..d80fe6ad53
--- /dev/null
+++ b/runtime/src/macros.rs
@@ -0,0 +1,38 @@
+//! Macros shared across runtime implementations.
+
+/// Prepare metrics for a spawned task.
+///
+/// This macro returns a tuple `(Label, Gauge)`:
+/// - `Label`: A label representing the task's metrics.
+/// - `Gauge`: A gauge tracking the number of running tasks with the given label.
+#[macro_export]
+macro_rules! spawn_metrics {
+    // Handle future tasks
+    ($ctx:ident, future) => {
+        $crate::spawn_metrics!(
+            $crate::telemetry::metrics::task::Label::future($ctx.name.clone()),
+            @make $ctx
+        )
+    };
+
+    // Handle blocking tasks
+    ($ctx:ident, blocking, $dedicated:expr) => {
+        $crate::spawn_metrics!(
+            if $dedicated {
+                $crate::telemetry::metrics::task::Label::blocking_dedicated($ctx.name.clone())
+            } else {
+                $crate::telemetry::metrics::task::Label::blocking_shared($ctx.name.clone())
+            },
+            @make $ctx
+        )
+    };
+
+    // Increment the number of spawned tasks and return a gauge for the number of running tasks
+    ($label:expr, @make $ctx:ident) => {{
+        let label = $label;
+        let metrics = &$ctx.executor.metrics;
+        metrics.tasks_spawned.get_or_create(&label).inc();
+        let gauge = metrics.tasks_running.get_or_create(&label).clone();
+        (label, gauge)
+    }};
+}
diff --git a/runtime/src/tokio/runtime.rs b/runtime/src/tokio/runtime.rs
index 71d0f02355..a2b978fff4 100644
--- a/runtime/src/tokio/runtime.rs
+++ b/runtime/src/tokio/runtime.rs
@@ -345,18 +345,7 @@ impl crate::Spawner for Context {
         assert!(!self.spawned, "already spawned");
 
         // Get metrics
-        let label = Label::future(self.name.clone());
-        self.executor
-            .metrics
-            .tasks_spawned
-            .get_or_create(&label)
-            .inc();
-        let gauge = self
-            .executor
-            .metrics
-            .tasks_running
-            .get_or_create(&label)
-            .clone();
+        let (_, gauge) = spawn_metrics!(self, future);
 
         // Set up the task
         let catch_panics = self.executor.cfg.catch_panics;
@@ -379,18 +368,7 @@ impl crate::Spawner for Context {
         self.spawned = true;
 
         // Get metrics
-        let label = Label::future(self.name.clone());
-        self.executor
-            .metrics
-            .tasks_spawned
-            .get_or_create(&label)
-            .inc();
-        let gauge = self
-            .executor
-            .metrics
-            .tasks_running
-            .get_or_create(&label)
-            .clone();
+        let (_, gauge) = spawn_metrics!(self, future);
 
         // Set up the task
         let executor = self.executor.clone();
@@ -412,22 +390,7 @@ impl crate::Spawner for Context {
         assert!(!self.spawned, "already spawned");
 
         // Get metrics
-        let label = if dedicated {
-            Label::blocking_dedicated(self.name.clone())
-        } else {
-            Label::blocking_shared(self.name.clone())
-        };
-        self.executor
-            .metrics
-            .tasks_spawned
-            .get_or_create(&label)
-            .inc();
-        let gauge = self
-            .executor
-            .metrics
-            .tasks_running
-            .get_or_create(&label)
-            .clone();
+        let (_, gauge) = spawn_metrics!(self, blocking, dedicated);
 
         // Set up the task
         let executor = self.executor.clone();
@@ -452,22 +415,7 @@ impl crate::Spawner for Context {
         self.spawned = true;
 
         // Get metrics
-        let label = if dedicated {
-            Label::blocking_dedicated(self.name.clone())
-        } else {
-            Label::blocking_shared(self.name.clone())
-        };
-        self.executor
-            .metrics
-            .tasks_spawned
-            .get_or_create(&label)
-            .inc();
-        let gauge = self
-            .executor
-            .metrics
-            .tasks_running
-            .get_or_create(&label)
-            .clone();
+        let (_, gauge) = spawn_metrics!(self, blocking, dedicated);
 
         // Set up the task
         let executor = self.executor.clone();


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR factors out duplicated metrics setup into a new `spawn_metrics!` macro and applies it across both deterministic and Tokio runtimes.

- Introduces `spawn_metrics!` in `macros.rs` to centralize label creation, spawn counting, and gauge cloning.
- Replaces inline metrics setup in `runtime/src/tokio/runtime.rs` and `runtime/src/deterministic.rs` with `spawn_metrics!` calls.
- Adds and imports the `macros` module in `runtime/src/lib.rs` for macro availability.

### Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

| File                                | Description                                          |
| ----------------------------------- | ---------------------------------------------------- |
| runtime/src/macros.rs               | New `spawn_metrics!` macro encapsulating metrics setup |
| runtime/src/tokio/runtime.rs        | Replaced duplicated inline metrics with macro calls  |
| runtime/src/deterministic.rs        | Replaced duplicated inline metrics with macro calls  |
| runtime/src/lib.rs                  | Imported `macros` module via `#[macro_use]`          |


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**runtime/src/tokio/runtime.rs:348**
* [nitpick] The new `spawn_metrics!` invocation in the Tokio runtime should be covered by a unit or integration test to verify that metrics are being recorded correctly.
```
let (_, gauge) = spawn_metrics!(self, future);
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T20:09:02Z
user: login: BrendanChou
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-05-19T20:14:23Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T21:43:51Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR factors out duplicated metrics collection logic into a new macro, spawn_metrics!, reducing redundancy in both the Tokio and Deterministic spawner implementations.  
- Factored out metrics setup into a reusable macro.  
- Updated Tokio and Deterministic runtime implementations to use the new macro.  
- Imported the new macros module in the runtime library.

### Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

| File                          | Description                                                        |
| ----------------------------- | ------------------------------------------------------------------ |
| runtime/src/tokio/runtime.rs   | Replaced duplicate metrics updates with spawn_metrics! macro call  |
| runtime/src/macros.rs          | Introduced the spawn_metrics! macro to encapsulate metrics logic    |
| runtime/src/lib.rs             | Added module import for macros                                     |
| runtime/src/deterministic.rs   | Updated metrics handling to use the new spawn_metrics! macro call    |




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T21:56:20Z

Review comments:
diff_hunk: @@ -721,18 +721,7 @@ impl crate::Spawner for Context {
         assert!(!self.spawned, "already spawned");
 
         // Get metrics
-        let label = Label::future(self.name.clone());
-        self.executor
-            .metrics
-            .tasks_spawned
-            .get_or_create(&label)
-            .inc();
-        let gauge = self
-            .executor
-            .metrics
-            .tasks_running
-            .get_or_create(&label)
-            .clone();
+        let (label, gauge) = spawn_metrics!(self, future);
path: runtime/src/deterministic.rs
user: login: Copilot
body: The `label` variable from `spawn_metrics!` is not used afterwards and will trigger an unused variable warning; consider using `let (_, gauge) = spawn_metrics!(self, future);` to ignore it.
```suggestion
        let (_, gauge) = spawn_metrics!(self, future);
```
created_at: 2025-05-19T20:09:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -787,22 +765,7 @@ impl crate::Spawner for Context {
         assert!(!self.spawned, "already spawned");
 
         // Get metrics
-        let label = if dedicated {
-            Label::blocking_dedicated(self.name.clone())
-        } else {
-            Label::blocking_shared(self.name.clone())
-        };
-        self.executor
-            .metrics
-            .tasks_spawned
-            .get_or_create(&label)
-            .inc();
-        let gauge = self
-            .executor
-            .metrics
-            .tasks_running
-            .get_or_create(&label)
-            .clone();
+        let (label, gauge) = spawn_metrics!(self, blocking, dedicated);
path: runtime/src/deterministic.rs
user: login: Copilot
body: The `label` variable from `spawn_metrics!` is not used afterwards and will trigger an unused variable warning; consider using `let (_, gauge) = spawn_metrics!(self, blocking, dedicated);` to ignore it.
```suggestion
        let (_, gauge) = spawn_metrics!(self, blocking, dedicated);
```
created_at: 2025-05-19T20:09:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,40 @@
+//! Macros shared across runtime implementations.
+
+/// Prepare metrics for a spawned task.
path: runtime/src/macros.rs
user: login: Copilot
body: [nitpick] The doc comment could be expanded to note that this macro returns a `(Label, Gauge)` tuple to clarify its outputs for consumers.
```suggestion
/// Prepare metrics for a spawned task.
/// 
/// This macro returns a tuple `(Label, Gauge)`:
/// - `Label`: A label representing the task's metrics.
/// - `Gauge`: A gauge tracking the number of running tasks with the given label.
```
created_at: 2025-05-19T20:09:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,6 +27,9 @@ use std::{
 };
 use thiserror::Error;
 
+#[macro_use]
+mod macros;
path: runtime/src/lib.rs
user: login: Copilot
body: [nitpick] Using `#[macro_use]` is less idiomatic in Rust 2018; consider replacing this with `pub use crate::macros::spawn_metrics;` for explicit macro import.
```suggestion
mod macros;
pub use crate::macros::spawn_metrics;
```
created_at: 2025-05-19T20:09:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,40 @@
+//! Macros shared across runtime implementations.
+
+/// Prepare metrics for a spawned task.
+#[macro_export]
+macro_rules! spawn_metrics {
+    ($ctx:ident, future) => {{
+        let label = $crate::telemetry::metrics::task::Label::future($ctx.name.clone());
+        $ctx.executor
+            .metrics
+            .tasks_spawned
+            .get_or_create(&label)
+            .inc();
+        let gauge = $ctx
+            .executor
+            .metrics
+            .tasks_running
+            .get_or_create(&label)
+            .clone();
+        (label, gauge)
+    }};
+    ($ctx:ident, blocking, $dedicated:expr) => {{
+        let label = if $dedicated {
+            $crate::telemetry::metrics::task::Label::blocking_dedicated($ctx.name.clone())
+        } else {
+            $crate::telemetry::metrics::task::Label::blocking_shared($ctx.name.clone())
+        };
+        $ctx.executor
+            .metrics
+            .tasks_spawned
+            .get_or_create(&label)
+            .inc();
+        let gauge = $ctx
+            .executor
+            .metrics
+            .tasks_running
+            .get_or_create(&label)
+            .clone();
+        (label, gauge)
+    }};
+}
path: runtime/src/macros.rs
user: login: BrendanChou
body: Not super familiar with macros but is it standard to have this `future` vs `blocking` in the arguments, versus just having two differently-named macros? I feel like this way is just more confusing
created_at: 2025-05-19T20:14:21Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+//! Macros shared across runtime implementations.
+
+/// Prepare metrics for a spawned task.
+#[macro_export]
+macro_rules! spawn_metrics {
+    ($ctx:ident, future) => {{
+        let label = $crate::telemetry::metrics::task::Label::future($ctx.name.clone());
+        $ctx.executor
+            .metrics
+            .tasks_spawned
+            .get_or_create(&label)
+            .inc();
+        let gauge = $ctx
+            .executor
+            .metrics
+            .tasks_running
+            .get_or_create(&label)
+            .clone();
+        (label, gauge)
+    }};
+    ($ctx:ident, blocking, $dedicated:expr) => {{
+        let label = if $dedicated {
+            $crate::telemetry::metrics::task::Label::blocking_dedicated($ctx.name.clone())
+        } else {
+            $crate::telemetry::metrics::task::Label::blocking_shared($ctx.name.clone())
+        };
+        $ctx.executor
+            .metrics
+            .tasks_spawned
+            .get_or_create(&label)
+            .inc();
+        let gauge = $ctx
+            .executor
+            .metrics
+            .tasks_running
+            .get_or_create(&label)
+            .clone();
+        (label, gauge)
+    }};
+}
path: runtime/src/macros.rs
user: login: patrick-ogrady
body: IMHO macro use is a free-for-all of style (which makes them both very powerful but also very dangerous).

I cleaned up the one here to share the metrics logic (and now think it meets the hurdle for doing it this way).
created_at: 2025-05-19T21:43:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -345,18 +345,7 @@ impl crate::Spawner for Context {
         assert!(!self.spawned, "already spawned");
 
         // Get metrics
-        let label = Label::future(self.name.clone());
-        self.executor
-            .metrics
-            .tasks_spawned
-            .get_or_create(&label)
-            .inc();
-        let gauge = self
-            .executor
-            .metrics
-            .tasks_running
-            .get_or_create(&label)
-            .clone();
+        let (_, gauge) = spawn_metrics!(self, future);
path: runtime/src/tokio/runtime.rs
user: login: Copilot
body: Consider using a consistent destructuring approach for the macro return values across similar contexts. If the label is not required later, using an underscore consistently can improve clarity.
created_at: 2025-05-19T21:56:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -721,18 +721,7 @@ impl crate::Spawner for Context {
         assert!(!self.spawned, "already spawned");
 
         // Get metrics
-        let label = Label::future(self.name.clone());
-        self.executor
-            .metrics
-            .tasks_spawned
-            .get_or_create(&label)
-            .inc();
-        let gauge = self
-            .executor
-            .metrics
-            .tasks_running
-            .get_or_create(&label)
-            .clone();
+        let (label, gauge) = spawn_metrics!(self, future);
path: runtime/src/deterministic.rs
user: login: Copilot
body: The destructuring of the spawn_metrics! return tuple here differs from the Tokio runtime usage. If the label is unused afterward, consider discarding it with an underscore to maintain consistency.
created_at: 2025-05-19T21:56:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-05-19T22:59:40Z
body: ## [Codecov](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/962?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 90.42%. Comparing base [(`338aeab`)](https://app.codecov.io/gh/commonwarexyz/monorepo/commit/338aeab74889badadd033d8f6244e2baccd5326d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz) to head [(`bbf1a87`)](https://app.codecov.io/gh/commonwarexyz/monorepo/commit/bbf1a878a49d409616b7e90c57aca2f8816c0c6a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz).
> Report is 3 commits behind head on main.

```diff
@@           Coverage Diff            @@
##             main     #962    +/-   ##
========================================
  Coverage   90.42%   90.42%            
========================================
  Files         186      186            
  Lines       48266    48162   -104     
========================================
- Hits        43645    43552    -93     
+ Misses       4621     4610    -11     
```

| [Files with missing lines](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/962?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz) | Coverage Δ | |
|---|---|---|
| [runtime/src/deterministic.rs](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/962?src=pr&el=tree&filepath=runtime%2Fsrc%2Fdeterministic.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz#diff-cnVudGltZS9zcmMvZGV0ZXJtaW5pc3RpYy5ycw==) | `97.33% <100.00%> (-0.17%)` | :arrow_down: |
| [runtime/src/lib.rs](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/962?src=pr&el=tree&filepath=runtime%2Fsrc%2Flib.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz#diff-cnVudGltZS9zcmMvbGliLnJz) | `96.01% <ø> (ø)` | |
| [runtime/src/tokio/runtime.rs](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/962?src=pr&el=tree&filepath=runtime%2Fsrc%2Ftokio%2Fruntime.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz#diff-cnVudGltZS9zcmMvdG9raW8vcnVudGltZS5ycw==) | `83.23% <100.00%> (-2.16%)` | :arrow_down: |

... and [4 files with indirect coverage changes](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/962/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz)

------

[Continue to review full report in Codecov by Sentry](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/962?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/962?dropdown=coverage&src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz). Last update [338aeab...bbf1a87](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/962?dropdown=coverage&src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE