Repository: App-vNext/Polly

Issue:
Reduce allocations in telemetry
Revealed in #1311 

- Use pooling for `TelemetryEventArguments`



Hints:

Patch:
diff --git a/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.MultipleStrategiesBenchmark-report-github.md b/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.MultipleStrategiesBenchmark-report-github.md
index 761ed6b8320..56839c5e0a1 100644
--- a/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.MultipleStrategiesBenchmark-report-github.md
+++ b/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.MultipleStrategiesBenchmark-report-github.md
@@ -9,7 +9,8 @@ Job=MediumRun  Toolchain=InProcessEmitToolchain  IterationCount=15
 LaunchCount=2  WarmupCount=10  
 
 ```
-|                     Method |     Mean |     Error |    StdDev | Ratio |   Gen0 | Allocated | Alloc Ratio |
-|--------------------------- |---------:|----------:|----------:|------:|-------:|----------:|------------:|
-| ExecuteStrategyPipeline_V7 | 2.227 μs | 0.0077 μs | 0.0116 μs |  1.00 | 0.1106 |    2824 B |        1.00 |
-| ExecuteStrategyPipeline_V8 | 1.750 μs | 0.0060 μs | 0.0084 μs |  0.79 |      - |      40 B |        0.01 |
+|                               Method |     Mean |     Error |    StdDev | Ratio |   Gen0 | Allocated | Alloc Ratio |
+|------------------------------------- |---------:|----------:|----------:|------:|-------:|----------:|------------:|
+|           ExecuteStrategyPipeline_V7 | 2.269 μs | 0.0136 μs | 0.0204 μs |  1.00 | 0.1106 |    2824 B |        1.00 |
+|           ExecuteStrategyPipeline_V8 | 1.861 μs | 0.0111 μs | 0.0155 μs |  0.82 |      - |      40 B |        0.01 |
+| ExecuteStrategyPipeline_Telemetry_V8 | 2.402 μs | 0.0104 μs | 0.0156 μs |  1.06 |      - |      40 B |        0.01 |
diff --git a/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.TelemetryBenchmark-report-github.md b/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.TelemetryBenchmark-report-github.md
index f16cd939b84..c3ab5b54ae1 100644
--- a/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.TelemetryBenchmark-report-github.md
+++ b/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.TelemetryBenchmark-report-github.md
@@ -1,6 +1,6 @@
 ``` ini
 
-BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1702/22H2/2022Update/SunValley2), VM=Hyper-V
+BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1848/22H2/2022Update/SunValley2), VM=Hyper-V
 Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores
 .NET SDK=7.0.304
   [Host] : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2
@@ -9,9 +9,9 @@ Job=MediumRun  Toolchain=InProcessEmitToolchain  IterationCount=15
 LaunchCount=2  WarmupCount=10  
 
 ```
-|  Method | Telemetry | Enrichment |        Mean |    Error |    StdDev |   Gen0 | Allocated |
-|-------- |---------- |----------- |------------:|---------:|----------:|-------:|----------:|
-| **Execute** |     **False** |      **False** |    **80.13 ns** | **0.324 ns** |  **0.486 ns** |      **-** |         **-** |
-| **Execute** |     **False** |       **True** |    **74.33 ns** | **0.286 ns** |  **0.392 ns** |      **-** |         **-** |
-| **Execute** |      **True** |      **False** |   **494.33 ns** | **3.973 ns** |  **5.698 ns** | **0.0029** |      **72 B** |
-| **Execute** |      **True** |       **True** | **1,157.27 ns** | **7.130 ns** | **10.450 ns** | **0.0286** |     **728 B** |
+|  Method | Telemetry | Enrichment |        Mean |    Error |   StdDev | Allocated |
+|-------- |---------- |----------- |------------:|---------:|---------:|----------:|
+| **Execute** |     **False** |      **False** |    **80.27 ns** | **1.992 ns** | **2.920 ns** |         **-** |
+| **Execute** |     **False** |       **True** |    **79.68 ns** | **1.324 ns** | **1.982 ns** |         **-** |
+| **Execute** |      **True** |      **False** |   **750.41 ns** | **4.875 ns** | **6.673 ns** |         **-** |
+| **Execute** |      **True** |       **True** | **1,034.73 ns** | **4.941 ns** | **7.242 ns** |         **-** |
diff --git a/bench/Polly.Core.Benchmarks/MultipleStrategiesBenchmark.cs b/bench/Polly.Core.Benchmarks/MultipleStrategiesBenchmark.cs
index b7d1bca8861..8fd1769a4f0 100644
--- a/bench/Polly.Core.Benchmarks/MultipleStrategiesBenchmark.cs
+++ b/bench/Polly.Core.Benchmarks/MultipleStrategiesBenchmark.cs
@@ -1,20 +1,32 @@
+using System.Diagnostics.Metrics;
+
 namespace Polly.Core.Benchmarks;
 
 public class MultipleStrategiesBenchmark
 {
+    private MeterListener? _meterListener;
     private object? _strategyV7;
     private object? _strategyV8;
+    private object? _strategyTelemetryV8;
 
     [GlobalSetup]
     public void Setup()
     {
-        _strategyV7 = Helper.CreateStrategyPipeline(PollyVersion.V7);
-        _strategyV8 = Helper.CreateStrategyPipeline(PollyVersion.V8);
+        _meterListener = MeteringUtil.ListenPollyMetrics();
+        _strategyV7 = Helper.CreateStrategyPipeline(PollyVersion.V7, false);
+        _strategyV8 = Helper.CreateStrategyPipeline(PollyVersion.V8, false);
+        _strategyTelemetryV8 = Helper.CreateStrategyPipeline(PollyVersion.V8, true);
     }
 
+    [GlobalCleanup]
+    public void Cleanup() => _meterListener?.Dispose();
+
     [Benchmark(Baseline = true)]
     public ValueTask ExecuteStrategyPipeline_V7() => _strategyV7!.ExecuteAsync(PollyVersion.V7);
 
     [Benchmark]
     public ValueTask ExecuteStrategyPipeline_V8() => _strategyV8!.ExecuteAsync(PollyVersion.V8);
+
+    [Benchmark]
+    public ValueTask ExecuteStrategyPipeline_Telemetry_V8() => _strategyTelemetryV8!.ExecuteAsync(PollyVersion.V8);
 }
diff --git a/bench/Polly.Core.Benchmarks/TelemetryBenchmark.cs b/bench/Polly.Core.Benchmarks/TelemetryBenchmark.cs
index 23143f6e3fe..32f1d1fd19c 100644
--- a/bench/Polly.Core.Benchmarks/TelemetryBenchmark.cs
+++ b/bench/Polly.Core.Benchmarks/TelemetryBenchmark.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.Metrics;
 using Microsoft.Extensions.Logging.Abstractions;
 using Polly.Extensions.Telemetry;
 using Polly.Telemetry;
@@ -7,13 +8,22 @@ namespace Polly.Core.Benchmarks;
 public class TelemetryBenchmark
 {
     private ResilienceStrategy? _strategy;
+    private MeterListener? _meterListener;
 
     [GlobalSetup]
     public void Prepare()
     {
         _strategy = Build(new ResilienceStrategyBuilder());
+
+        if (Telemetry)
+        {
+            _meterListener = MeteringUtil.ListenPollyMetrics();
+        }
     }
 
+    [GlobalCleanup]
+    public void Cleanup() => _meterListener?.Dispose();
+
     [Params(true, false)]
     public bool Telemetry { get; set; }
 
@@ -72,5 +82,4 @@ protected override ValueTask<Outcome<TResult>> ExecuteCoreAsync<TResult, TState>
             return callback(context, state);
         }
     }
-
 }
diff --git a/bench/Polly.Core.Benchmarks/Utils/Helper.MultipleStrategies.cs b/bench/Polly.Core.Benchmarks/Utils/Helper.MultipleStrategies.cs
index 548b4e1cb88..8a549667e75 100644
--- a/bench/Polly.Core.Benchmarks/Utils/Helper.MultipleStrategies.cs
+++ b/bench/Polly.Core.Benchmarks/Utils/Helper.MultipleStrategies.cs
@@ -1,10 +1,11 @@
 using System.Threading.RateLimiting;
+using Microsoft.Extensions.Logging.Abstractions;
 
 namespace Polly.Core.Benchmarks.Utils;
 
 internal static partial class Helper
 {
-    public static object CreateStrategyPipeline(PollyVersion technology) => technology switch
+    public static object CreateStrategyPipeline(PollyVersion technology, bool telemetry) => technology switch
     {
         PollyVersion.V7 => Policy.WrapAsync(
             Policy.HandleResult(Failure).Or<InvalidOperationException>().AdvancedCircuitBreakerAsync(0.5, TimeSpan.FromSeconds(30), 10, TimeSpan.FromSeconds(5)),
@@ -40,6 +41,11 @@ internal static partial class Helper
                         _ => PredicateResult.False
                     }
                 });
+
+            if (telemetry)
+            {
+                builder.EnableTelemetry(NullLoggerFactory.Instance);
+            }
         }),
         _ => throw new NotSupportedException()
     };
diff --git a/bench/Polly.Core.Benchmarks/Utils/MeteringUtil.cs b/bench/Polly.Core.Benchmarks/Utils/MeteringUtil.cs
new file mode 100644
index 00000000000..3a4245c6dbc
--- /dev/null
+++ b/bench/Polly.Core.Benchmarks/Utils/MeteringUtil.cs
@@ -0,0 +1,36 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Diagnostics.Metrics;
+
+namespace Polly.Core.Benchmarks.Utils;
+
+internal class MeteringUtil
+{
+    public static MeterListener ListenPollyMetrics()
+    {
+        var meterListener = new MeterListener
+        {
+            InstrumentPublished = (instrument, listener) =>
+            {
+                if (instrument.Meter.Name is "Polly")
+                {
+                    listener.EnableMeasurementEvents(instrument);
+                }
+            }
+        };
+
+        meterListener.SetMeasurementEventCallback<int>(OnMeasurementRecorded);
+        meterListener.Start();
+
+        static void OnMeasurementRecorded<T>(
+            Instrument instrument,
+            T measurement,
+            ReadOnlySpan<KeyValuePair<string, object?>> tags,
+            object? state)
+        {
+            // do nothing
+        }
+
+        return meterListener;
+    }
+}
diff --git a/src/Polly.Core/Telemetry/ResilienceStrategyTelemetry.cs b/src/Polly.Core/Telemetry/ResilienceStrategyTelemetry.cs
index b3ae0d1b5bc..ff3f7984ac0 100644
--- a/src/Polly.Core/Telemetry/ResilienceStrategyTelemetry.cs
+++ b/src/Polly.Core/Telemetry/ResilienceStrategyTelemetry.cs
@@ -38,7 +38,11 @@ public void Report<TArgs>(string eventName, ResilienceContext context, TArgs arg
             return;
         }
 
-        DiagnosticSource.Write(eventName, new TelemetryEventArguments(TelemetrySource, eventName, context, null, args!));
+        var telemetryArgs = TelemetryEventArguments.Get(TelemetrySource, eventName, context, null, args!);
+
+        DiagnosticSource.Write(eventName, telemetryArgs);
+
+        TelemetryEventArguments.Return(telemetryArgs);
     }
 
     /// <summary>
@@ -60,7 +64,11 @@ public void Report<TArgs, TResult>(string eventName, OutcomeArguments<TResult, T
             return;
         }
 
-        DiagnosticSource.Write(eventName, new TelemetryEventArguments(TelemetrySource, eventName, args.Context, args.Outcome.AsOutcome(), args.Arguments!));
+        var telemetryArgs = TelemetryEventArguments.Get(TelemetrySource, eventName, args.Context, args.Outcome.AsOutcome(), args.Arguments!);
+
+        DiagnosticSource.Write(eventName, telemetryArgs);
+
+        TelemetryEventArguments.Return(telemetryArgs);
     }
 }
 
diff --git a/src/Polly.Core/Telemetry/TelemetryEventArguments.Pool.cs b/src/Polly.Core/Telemetry/TelemetryEventArguments.Pool.cs
new file mode 100644
index 00000000000..9d701004ecb
--- /dev/null
+++ b/src/Polly.Core/Telemetry/TelemetryEventArguments.Pool.cs
@@ -0,0 +1,33 @@
+﻿namespace Polly.Telemetry;
+
+public sealed partial record class TelemetryEventArguments
+{
+    private static readonly ObjectPool<TelemetryEventArguments> Pool = new(() => new TelemetryEventArguments(), args =>
+    {
+        args.Source = null!;
+        args.EventName = null!;
+        args.Context = null!;
+        args.Outcome = default;
+        args.Arguments = null!;
+    });
+
+    internal static TelemetryEventArguments Get(
+        ResilienceTelemetrySource source,
+        string eventName,
+        ResilienceContext context,
+        Outcome<object>? outcome,
+        object arguments)
+    {
+        var args = Pool.Get();
+
+        args.Source = source;
+        args.EventName = eventName;
+        args.Context = context;
+        args.Outcome = outcome;
+        args.Arguments = arguments;
+
+        return args;
+    }
+
+    internal static void Return(TelemetryEventArguments args) => Pool.Return(args);
+}
diff --git a/src/Polly.Core/Telemetry/TelemetryEventArguments.cs b/src/Polly.Core/Telemetry/TelemetryEventArguments.cs
index 320c33aa6e8..1c98f3145f2 100644
--- a/src/Polly.Core/Telemetry/TelemetryEventArguments.cs
+++ b/src/Polly.Core/Telemetry/TelemetryEventArguments.cs
@@ -5,17 +5,35 @@ namespace Polly.Telemetry;
 /// <summary>
 /// The arguments of the telemetry event.
 /// </summary>
-/// <param name="Source">The source of the event.</param>
-/// <param name="EventName">The event name.</param>
-/// <param name="Context">The resilience context.</param>
-/// <param name="Outcome">The outcome of an execution.</param>
-/// <param name="Arguments">The arguments associated with the event.</param>
 [EditorBrowsable(EditorBrowsableState.Never)]
-public sealed record class TelemetryEventArguments(
-    ResilienceTelemetrySource Source,
-    string EventName,
-    ResilienceContext Context,
-    Outcome<object>? Outcome,
-    object Arguments)
+public sealed partial record class TelemetryEventArguments
 {
+    private TelemetryEventArguments()
+    {
+    }
+
+    /// <summary>
+    /// Gets the source of the event.
+    /// </summary>
+    public ResilienceTelemetrySource Source { get; private set; } = null!;
+
+    /// <summary>
+    /// Gets the event name.
+    /// </summary>
+    public string EventName { get; private set; } = null!;
+
+    /// <summary>
+    /// Gets the resilience context.
+    /// </summary>
+    public ResilienceContext Context { get; private set; } = null!;
+
+    /// <summary>
+    /// Gets the outcome of an execution.
+    /// </summary>
+    public Outcome<object>? Outcome { get; private set; }
+
+    /// <summary>
+    /// Gets the arguments associated with the event.
+    /// </summary>
+    public object Arguments { get; private set; } = null!;
 }
diff --git a/src/Polly.Core/Utils/ObjectPool.cs b/src/Polly.Core/Utils/ObjectPool.cs
index b34ca97fa38..886e38a1dc8 100644
--- a/src/Polly.Core/Utils/ObjectPool.cs
+++ b/src/Polly.Core/Utils/ObjectPool.cs
@@ -14,6 +14,11 @@ internal sealed class ObjectPool<T>
     private T? _fastItem;
     private int _numItems;
 
+    public ObjectPool(Func<T> createFunc, Action<T> reset)
+        : this(createFunc, o => { reset(o); return true; })
+    {
+    }
+
     public ObjectPool(Func<T> createFunc, Func<T, bool> returnFunc)
         : this(_ => createFunc(), returnFunc)
     {
diff --git a/src/Polly.Extensions/Telemetry/EnrichmentContext.Pool.cs b/src/Polly.Extensions/Telemetry/EnrichmentContext.Pool.cs
index 60224e48c8a..fc03506a254 100644
--- a/src/Polly.Extensions/Telemetry/EnrichmentContext.Pool.cs
+++ b/src/Polly.Extensions/Telemetry/EnrichmentContext.Pool.cs
@@ -26,6 +26,7 @@ internal static EnrichmentContext Get(ResilienceContext resilienceContext, objec
 
     internal static void Return(EnrichmentContext context)
     {
+        Array.Clear(context._tagsArray, 0, context.Tags.Count);
         context.Tags.Clear();
         ContextPool.Return(context);
     }
diff --git a/src/Polly.Extensions/Telemetry/EnrichmentContext.cs b/src/Polly.Extensions/Telemetry/EnrichmentContext.cs
index 51b54c2a111..6a0c8e5ba2a 100644
--- a/src/Polly.Extensions/Telemetry/EnrichmentContext.cs
+++ b/src/Polly.Extensions/Telemetry/EnrichmentContext.cs
@@ -5,6 +5,10 @@ namespace Polly.Extensions.Telemetry;
 /// </summary>
 public sealed partial class EnrichmentContext
 {
+    private const int InitialArraySize = 20;
+
+    private KeyValuePair<string, object?>[] _tagsArray = new KeyValuePair<string, object?>[InitialArraySize];
+
     private EnrichmentContext()
     {
     }
@@ -27,5 +31,24 @@ private EnrichmentContext()
     /// <summary>
     /// Gets the tags associated with the resilience event.
     /// </summary>
-    public ICollection<KeyValuePair<string, object?>> Tags { get; } = new List<KeyValuePair<string, object?>>();
+    public IList<KeyValuePair<string, object?>> Tags { get; } = new List<KeyValuePair<string, object?>>();
+
+    internal ReadOnlySpan<KeyValuePair<string, object?>> TagsSpan
+    {
+        get
+        {
+            // stryker disable once equality : no means to test this
+            if (Tags.Count > _tagsArray.Length)
+            {
+                Array.Resize(ref _tagsArray, Tags.Count);
+            }
+
+            for (int i = 0; i < Tags.Count; i++)
+            {
+                _tagsArray[i] = Tags[i];
+            }
+
+            return _tagsArray.AsSpan(0, Tags.Count);
+        }
+    }
 }
diff --git a/src/Polly.Extensions/Telemetry/EnrichmentUtil.cs b/src/Polly.Extensions/Telemetry/EnrichmentUtil.cs
index 5575337cf16..b63606d53a0 100644
--- a/src/Polly.Extensions/Telemetry/EnrichmentUtil.cs
+++ b/src/Polly.Extensions/Telemetry/EnrichmentUtil.cs
@@ -1,31 +1,19 @@
+using System.Collections.Generic;
+
 namespace Polly.Extensions.Telemetry;
 
 internal static class EnrichmentUtil
 {
-    public static void Enrich(
-        ref TagList tags,
-        List<Action<EnrichmentContext>> enrichers,
-        ResilienceContext resilienceContext,
-        Outcome<object>? outcome,
-        object? resilienceArguments)
+    public static void Enrich(EnrichmentContext context, List<Action<EnrichmentContext>> enrichers)
     {
         if (enrichers.Count == 0)
         {
             return;
         }
 
-        var context = EnrichmentContext.Get(resilienceContext, resilienceArguments, outcome);
-
         foreach (var enricher in enrichers)
         {
             enricher(context);
         }
-
-        foreach (var pair in context.Tags)
-        {
-            tags.Add(pair.Key, pair.Value);
-        }
-
-        EnrichmentContext.Return(context);
     }
 }
diff --git a/src/Polly.Extensions/Telemetry/ResilienceTelemetryDiagnosticSource.cs b/src/Polly.Extensions/Telemetry/ResilienceTelemetryDiagnosticSource.cs
index 55e637fa36a..0f4a58bb38a 100644
--- a/src/Polly.Extensions/Telemetry/ResilienceTelemetryDiagnosticSource.cs
+++ b/src/Polly.Extensions/Telemetry/ResilienceTelemetryDiagnosticSource.cs
@@ -41,20 +41,26 @@ public override void Write(string name, object? value)
 
     private void MeterEvent(TelemetryEventArguments args)
     {
-        var source = args.Source;
-        var tags = new TagList
+        if (!Counter.Enabled)
         {
-            { ResilienceTelemetryTags.EventName, args.EventName },
-            { ResilienceTelemetryTags.BuilderName, source.BuilderName },
-            { ResilienceTelemetryTags.StrategyName, source.StrategyName },
-            { ResilienceTelemetryTags.StrategyType, source.StrategyType },
-            { ResilienceTelemetryTags.StrategyKey, source.BuilderProperties.GetValue(TelemetryUtil.StrategyKey, null!) },
-            { ResilienceTelemetryTags.ResultType, args.Context.GetResultType() },
-            { ResilienceTelemetryTags.ExceptionName, args.Outcome?.Exception?.GetType().FullName }
-        };
-
-        EnrichmentUtil.Enrich(ref tags, _enrichers, args.Context, args.Outcome, args.Arguments);
-        Counter.Add(1, tags);
+            return;
+        }
+
+        var source = args.Source;
+
+        var enrichmentContext = EnrichmentContext.Get(args.Context, args.Arguments, args.Outcome);
+        enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.EventName, args.EventName));
+        enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.BuilderName, source.BuilderName));
+        enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.StrategyName, source.StrategyName));
+        enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.StrategyType, source.StrategyType));
+        enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.StrategyKey, source.BuilderProperties.GetValue(TelemetryUtil.StrategyKey, null!)));
+        enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.ResultType, args.Context.GetResultType()));
+        enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.ExceptionName, args.Outcome?.Exception?.GetType().FullName));
+        EnrichmentUtil.Enrich(enrichmentContext, _enrichers);
+
+        Counter.Add(1, enrichmentContext.TagsSpan);
+
+        EnrichmentContext.Return(enrichmentContext);
     }
 
     private void LogEvent(TelemetryEventArguments args)
diff --git a/src/Polly.Extensions/Telemetry/TelemetryResilienceStrategy.cs b/src/Polly.Extensions/Telemetry/TelemetryResilienceStrategy.cs
index 34b7b5a485d..b9c275ff246 100644
--- a/src/Polly.Extensions/Telemetry/TelemetryResilienceStrategy.cs
+++ b/src/Polly.Extensions/Telemetry/TelemetryResilienceStrategy.cs
@@ -1,5 +1,6 @@
 using System.Diagnostics.Metrics;
 using Microsoft.Extensions.Logging;
+using Polly.Extensions.Telemetry;
 using Polly.Extensions.Utils;
 using Polly.Utils;
 
@@ -71,26 +72,33 @@ protected override async ValueTask<Outcome<TResult>> ExecuteCoreAsync<TResult, T
             duration.TotalMilliseconds,
             outcome.Exception);
 
-        var tags = new TagList
-        {
-            { ResilienceTelemetryTags.BuilderName, _builderName },
-            { ResilienceTelemetryTags.StrategyKey, _strategyKey },
-            { ResilienceTelemetryTags.ResultType, context.GetResultType() },
-            { ResilienceTelemetryTags.ExceptionName, outcome.Exception?.GetType().FullName },
-            { ResilienceTelemetryTags.ExecutionHealth, context.GetExecutionHealth() }
-        };
-
-        EnrichmentUtil.Enrich(ref tags, _enrichers, context, CreateOutcome(outcome), resilienceArguments: null);
-
-        ExecutionDuration.Record(duration.TotalMilliseconds, tags);
+        RecordDuration(context, outcome, duration);
 
         return outcome;
     }
 
-    private static Outcome<object> CreateOutcome<TResult>(Outcome<TResult> outcome) =>
-        outcome.HasResult ?
-            new Outcome<object>(outcome.Result) :
-            new Outcome<object>(outcome.Exception!);
+    private static Outcome<object> CreateOutcome<TResult>(Outcome<TResult> outcome) => outcome.HasResult ?
+        new Outcome<object>(outcome.Result) :
+        new Outcome<object>(outcome.Exception!);
+
+    private void RecordDuration<TResult>(ResilienceContext context, Outcome<TResult> outcome, TimeSpan duration)
+    {
+        if (!ExecutionDuration.Enabled)
+        {
+            return;
+        }
+
+        var enrichmentContext = EnrichmentContext.Get(context, null, CreateOutcome(outcome));
+        enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.BuilderName, _builderName));
+        enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.StrategyKey, _strategyKey));
+        enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.ResultType, context.GetResultType()));
+        enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.ExceptionName, outcome.Exception?.GetType().FullName));
+        enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.ExecutionHealth, context.GetExecutionHealth()));
+        EnrichmentUtil.Enrich(enrichmentContext, _enrichers);
+
+        ExecutionDuration.Record(duration.TotalMilliseconds, enrichmentContext.TagsSpan);
+        EnrichmentContext.Return(enrichmentContext);
+    }
 
     private object? ExpandOutcome<TResult>(ResilienceContext context, Outcome<TResult> outcome)
     {


Test patch:
diff --git a/test/Polly.Core.Tests/Telemetry/TelemetryEventArgumentsTests.cs b/test/Polly.Core.Tests/Telemetry/TelemetryEventArgumentsTests.cs
new file mode 100644
index 00000000000..a75aa1cdb01
--- /dev/null
+++ b/test/Polly.Core.Tests/Telemetry/TelemetryEventArgumentsTests.cs
@@ -0,0 +1,39 @@
+﻿using System;
+using Polly.Telemetry;
+
+namespace Polly.Extensions.Tests.Telemetry;
+
+public class TelemetryEventArgumentsTests
+{
+    private readonly ResilienceTelemetrySource _source = new("builder", new ResilienceProperties(), "strategy", "type");
+
+    [Fact]
+    public void Get_Ok()
+    {
+        var context = ResilienceContext.Get();
+        var args = TelemetryEventArguments.Get(_source, "ev", context, new Outcome<object>("dummy"), "arg");
+
+        args.Outcome!.Value.Result.Should().Be("dummy");
+        args.Context.Should().Be(context);
+        args.EventName.Should().Be("ev");
+        args.Source.Should().Be(_source);
+        args.Arguments.Should().BeEquivalentTo("arg");
+        args.Context.Should().Be(context);
+    }
+
+    [Fact]
+    public void Return_EnsurePropertiesCleared()
+    {
+        var context = ResilienceContext.Get();
+        var args = TelemetryEventArguments.Get(_source, "ev", context, new Outcome<object>("dummy"), "arg");
+
+        TelemetryEventArguments.Return(args);
+
+        args.Outcome.Should().BeNull();
+        args.Context.Should().BeNull();
+        args.EventName.Should().BeNull();
+        args.Source.Should().BeNull();
+        args.Arguments.Should().BeNull();
+        args.Context.Should().BeNull();
+    }
+}
diff --git a/test/Polly.Core.Tests/Utils/ObjectPoolTests.cs b/test/Polly.Core.Tests/Utils/ObjectPoolTests.cs
index 738c7080aec..b9383df28c9 100644
--- a/test/Polly.Core.Tests/Utils/ObjectPoolTests.cs
+++ b/test/Polly.Core.Tests/Utils/ObjectPoolTests.cs
@@ -8,7 +8,7 @@ public class ObjectPoolTests
     public void GetAnd_ReturnObject_SameInstance()
     {
         // Arrange
-        var pool = new ObjectPool<object>(() => new object(), _ => true);
+        var pool = new ObjectPool<object>(() => new object(), _ => { });
 
         var obj1 = pool.Get();
         pool.Return(obj1);
diff --git a/test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs b/test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs
index ed5983c14c5..2ebce931327 100644
--- a/test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs
+++ b/test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs
@@ -151,50 +151,39 @@ public void WriteEvent_MeteringWithoutEnrichers_Ok(bool noOutcome, bool exceptio
         }
     }
 
-    [InlineData(true)]
-    [InlineData(false)]
+    [InlineData(1)]
+    [InlineData(100)]
     [Theory]
-    public void WriteEvent_MeteringWithEnrichers_Ok(bool noOutcome)
+    public void WriteEvent_MeteringWithEnrichers_Ok(int count)
     {
+        const int DefaultDimensions = 7;
         var telemetry = Create(enrichers =>
         {
             enrichers.Add(context =>
             {
-                if (noOutcome)
+                for (int i = 0; i < count; i++)
                 {
-                    context.Outcome.Should().BeNull();
+                    context.Tags.Add(new KeyValuePair<string, object?>($"custom-{i}", $"custom-{i}-value"));
                 }
-                else
-                {
-                    context.Outcome!.Value.Result.Should().Be(true);
-                }
-
-                context.Context.Should().NotBeNull();
-                context.Arguments.Should().BeOfType<TestArguments>();
-                context.Tags.Add(new KeyValuePair<string, object?>("custom-1", "custom-1-value"));
             });
 
             enrichers.Add(context =>
             {
-                context.Tags.Add(new KeyValuePair<string, object?>("custom-2", "custom-2-value"));
+                context.Tags.Add(new KeyValuePair<string, object?>("other", "other-value"));
             });
         });
 
-        ReportEvent(telemetry, noOutcome ? null : new Outcome<object>(true));
-        ReportEvent(telemetry, noOutcome ? null : new Outcome<object>(true));
+        ReportEvent(telemetry, new Outcome<object>(true));
 
         var events = GetEvents("resilience-events");
         var ev = events[0];
-        ev.Count.Should().Be(9);
+        ev.Count.Should().Be(DefaultDimensions + count + 1);
+        ev["other"].Should().Be("other-value");
 
-        ev["custom-1"].Should().Be("custom-1-value");
-        ev["custom-2"].Should().Be("custom-2-value");
-
-        ev = events[1];
-        ev.Count.Should().Be(9);
-
-        ev["custom-1"].Should().Be("custom-1-value");
-        ev["custom-2"].Should().Be("custom-2-value");
+        for (int i = 0; i < count; i++)
+        {
+            ev[$"custom-{i}"].Should().Be($"custom-{i}-value");
+        }
     }
 
     [Fact]
diff --git a/test/Polly.TestUtils/TestUtilities.cs b/test/Polly.TestUtils/TestUtilities.cs
index 08eb7b849f0..540e83d4374 100644
--- a/test/Polly.TestUtils/TestUtilities.cs
+++ b/test/Polly.TestUtils/TestUtilities.cs
@@ -88,7 +88,7 @@ public static void ReportEvent(
         object arguments)
 #pragma warning restore S107 // Methods should not have too many parameters
     {
-        source.Write(eventName, new TelemetryEventArguments(
+        source.Write(eventName, TelemetryEventArguments.Get(
             new ResilienceTelemetrySource(builderName, builderProperties, strategyName, strategyType),
             eventName,
             context,
@@ -110,6 +110,13 @@ private sealed class CallbackDiagnosticSource : DiagnosticSource
 
         public override bool IsEnabled(string name) => true;
 
-        public override void Write(string name, object? value) => _callback((TelemetryEventArguments)value!);
+        public override void Write(string name, object? value)
+        {
+            var args = (TelemetryEventArguments)value!;
+
+            // copy the args because these are pooled and in tests we want to preserve them
+            args = TelemetryEventArguments.Get(args.Source, args.EventName, args.Context, args.Outcome, args.Arguments);
+            _callback(args);
+        }
     }
 }