Skip to content

Commit 3987dcb

Browse files
unsafePtrtarekgh
andauthored
perf: avoid per-observation array allocation in ObservableInstrument (#128039)
## Description Eliminate the per-observation `Measurement<T>[1]` allocation in the built-in observable instruments `ObservableCounter<T>`, `ObservableGauge<T>`, `ObservableUpDownCounter<T>`). `ObservableInstrument<T>.Observe(MeterListener)` now pattern-matches on `this` to read the built-in's `_callback` directly and dispatches single-value callbacks straight to `MeterListener.NotifyMeasurement`, skipping the array wrapper and the foreach allocation. The `Func<IEnumerable<Measurement<T>>>` shape is unchanged in behavior — the user owns the enumerable. ## Benchmark BenchmarkDotNet on .NET 11 preview, one `RecordObservableInstruments()` call per benchmark: | Callback shape | Baseline | Patched | | --- | --- | --- | | `Func<T>` | 22.91 ns / 72 B | 5.71 ns / 0 B | | `Func<Measurement<T>>` | 24.52 ns / 72 B | 6.78 ns / 0 B | | `Func<IEnumerable<Measurement<T>>>` | 37.49 ns / 104 B | 28.75 ns / 72 B | The IEnumerable row also improves (−32 B) because the simpler call chain lets the JIT elide the enumerator the baseline kept. The user-allocated array itself (72 B) is unchanged. ## Compatibility No public API change. The three built-ins are `public sealed` with `internal` constructors, so the `this switch` is exhaustive for instances that flow through this method. --------- Co-authored-by: Tarek Mahmoud Sayed <10833894+tarekgh@users.noreply.github.com>
1 parent 6969309 commit 3987dcb

4 files changed

Lines changed: 30 additions & 25 deletions

File tree

src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableCounter.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public sealed class ObservableCounter<T> : ObservableInstrument<T> where T : str
1717
{
1818
private readonly object _callback;
1919

20+
internal override object? Callback => _callback;
21+
2022
internal ObservableCounter(Meter meter, string name, Func<T> observeValue, string? unit, string? description) : this(meter, name, observeValue, unit, description, tags: null)
2123
{
2224
}
@@ -50,6 +52,6 @@ internal ObservableCounter(Meter meter, string name, Func<IEnumerable<Measuremen
5052
/// <summary>
5153
/// Observe() fetches the current measurements being tracked by this observable counter.
5254
/// </summary>
53-
protected override IEnumerable<Measurement<T>> Observe() => Observe(_callback);
55+
protected override IEnumerable<Measurement<T>> Observe() => ((Func<IEnumerable<Measurement<T>>>)_callback)();
5456
}
5557
}

src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableGauge.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public sealed class ObservableGauge<T> : ObservableInstrument<T> where T : struc
1717
{
1818
private readonly object _callback;
1919

20+
internal override object? Callback => _callback;
21+
2022
internal ObservableGauge(Meter meter, string name, Func<T> observeValue, string? unit, string? description) : this(meter, name, observeValue, unit, description, tags: null)
2123
{
2224
}
@@ -50,6 +52,6 @@ internal ObservableGauge(Meter meter, string name, Func<IEnumerable<Measurement<
5052
/// <summary>
5153
/// Observe() fetches the current measurements being tracked by this observable counter.
5254
/// </summary>
53-
protected override IEnumerable<Measurement<T>> Observe() => Observe(_callback);
55+
protected override IEnumerable<Measurement<T>> Observe() => ((Func<IEnumerable<Measurement<T>>>)_callback)();
5456
}
5557
}

src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableInstrument.cs

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Generic;
5-
using System.Runtime.CompilerServices;
65

76
namespace System.Diagnostics.Metrics
87
{
@@ -50,45 +49,45 @@ protected ObservableInstrument(Meter meter, string name, string? unit, string? d
5049
/// </summary>
5150
public override bool IsObservable => true;
5251

52+
// Returns the underlying user callback for built-in observable instruments, or null for user-defined subclasses.
53+
// Subclasses provide their measurements via the abstract Observe() method instead.
54+
internal virtual object? Callback => null;
55+
5356
// Will be called from MeterListener.RecordObservableInstruments for each observable instrument.
5457
internal override void Observe(MeterListener listener)
5558
{
5659
object? state = GetSubscriptionState(listener);
5760

58-
IEnumerable<Measurement<T>> measurements = Observe();
59-
if (measurements is null)
61+
// Fast path for the built-in observable instruments: dispatch their callbacks
62+
// directly to the listener, avoiding the per-observation Measurement<T>[1] allocation that
63+
// the IEnumerable<Measurement<T>> path used to require.
64+
object? callback = Callback;
65+
66+
if (callback is Func<T> valueOnlyFunc)
6067
{
68+
listener.NotifyMeasurement(this, valueOnlyFunc(), Instrument.EmptyTags, state);
6169
return;
6270
}
6371

64-
foreach (Measurement<T> measurement in measurements)
72+
if (callback is Func<Measurement<T>> measurementOnlyFunc)
6573
{
74+
Measurement<T> measurement = measurementOnlyFunc();
6675
listener.NotifyMeasurement(this, measurement.Value, measurement.Tags, state);
67-
}
68-
}
69-
70-
// Will be called from the concrete classes which extends ObservabilityInstrument<T> when calling Observe() method.
71-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
72-
internal static IEnumerable<Measurement<T>> Observe(object callback)
73-
{
74-
if (callback is Func<T> valueOnlyFunc)
75-
{
76-
return new Measurement<T>[1] { new Measurement<T>(valueOnlyFunc()) };
76+
return;
7777
}
7878

79-
if (callback is Func<Measurement<T>> measurementOnlyFunc)
79+
// Func<IEnumerable<Measurement<T>>> built-ins and user-defined ObservableInstrument<T> subclasses
80+
// both fall through to the virtual Observe() override.
81+
IEnumerable<Measurement<T>> measurements = Observe();
82+
if (measurements is null)
8083
{
81-
return new Measurement<T>[1] { measurementOnlyFunc() };
84+
return;
8285
}
8386

84-
if (callback is Func<IEnumerable<Measurement<T>>> listOfMeasurementsFunc)
87+
foreach (Measurement<T> measurement in measurements)
8588
{
86-
return listOfMeasurementsFunc();
89+
listener.NotifyMeasurement(this, measurement.Value, measurement.Tags, state);
8790
}
88-
89-
Debug.Fail("Execution shouldn't reach this point");
90-
return null;
9191
}
92-
9392
}
9493
}

src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableUpDownCounter.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public sealed class ObservableUpDownCounter<T> : ObservableInstrument<T> where T
1717
{
1818
private readonly object _callback;
1919

20+
internal override object? Callback => _callback;
21+
2022
internal ObservableUpDownCounter(Meter meter, string name, Func<T> observeValue, string? unit, string? description) : this(meter, name, observeValue, unit, description, tags: null)
2123
{
2224
}
@@ -50,6 +52,6 @@ internal ObservableUpDownCounter(Meter meter, string name, Func<IEnumerable<Meas
5052
/// <summary>
5153
/// Observe() fetches the current measurements being tracked by this observable counter.
5254
/// </summary>
53-
protected override IEnumerable<Measurement<T>> Observe() => Observe(_callback);
55+
protected override IEnumerable<Measurement<T>> Observe() => ((Func<IEnumerable<Measurement<T>>>)_callback)();
5456
}
5557
}

0 commit comments

Comments
 (0)