Support lazy metric point allocations#7346
Conversation
|
|
|
The CLA needs to be signed. |
DONE |
|
This PR was marked stale due to lack of activity and will be closed in 7 days. Commenting or pushing will instruct the bot to automatically remove the label. This bot runs once per day. |
|
I haven't review this PR in detail but one comment to think though:
Would be good if this can be discussed in SIG too. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #7346 +/- ##
=======================================
Coverage 90.29% 90.30%
=======================================
Files 287 288 +1
Lines 15617 15704 +87
=======================================
+ Hits 14102 14182 +80
- Misses 1515 1522 +7
Flags with carried forward coverage won't be shown. Click here to find out more.
|
Per PR review feedback, drop the verbose `MetricPoint` segment from the new lazy-allocation API surface. Also renames the matching `SetDefaultMetricPointLazyAllocation` extension/builder method, `UsesMetricPointLazyAllocation` test helper, the internal `enableMetricPointLazyAllocation` field/parameter names, and the `LazyMetricPointAllocationExperimentalApi` diagnostics constant for consistency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Documents the new lazy metric point allocation APIs (`MetricStreamConfiguration.EnableLazyAllocation` and `MeterProviderBuilderExtensions.SetDefaultLazyAllocation`) under docs/diagnostics/experimental-apis/ and registers OTEL1006 in the Active section of the README. Feedback link points at open-telemetry#6620. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Group `MetricPointAllocationBenchmarks` benchmarks by scenario (NoMeasurements, OneTimeSeries, TenTimeSeries, ManyMetricsHighCardinality) and mark the eager variant in each pair as `[Benchmark(Baseline = true)]` so the BenchmarkDotNet output reports the lazy variant with a ratio relative to its matching eager baseline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per PR review feedback, do not extend the default `MetricsBenchmarks` matrix with an `EnableLazyAllocation` `[Params(false, true)]` (and its accompanying `AddView`) — doubling every existing combination is not worth the runtime cost when the dedicated `MetricPointAllocationBenchmarks` already covers the lazy-vs-eager comparison. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per PR review feedback, switch the `ObservableLongCounterInstrument` test fixture callback from `() => 0L` (the default value of `long`, which reads like a placeholder and would be indistinguishable from a missing observation if the SDK ever pumped the callback) to `() => 42L` so the value is visibly distinct from the manually-driven `aggregatorStore.Update(1, …)` in `LazyAsynchronousCumulativeSnapshotSkipsStalePoints`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per PR review feedback, the timing adjustments to `CompositeMetricReader_PreservesOriginalTimeoutBudget` (`MultipleReadersTests`) and `CompositeActivityProcessor_PreservesOriginalTimeoutBudget` (`CompositeActivityProcessorTests`) are unrelated to the lazy metric point allocation work and should not be part of this PR. Reverts both files to their pre-PR state; the original tests still pass on net10.0 (11/11 and 10/10 respectively). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds two tests targeting code paths flagged by codecov as uncovered in AggregatorStore after the lazy allocation changes: * LazyAllocationWorksWithMultiTagDeltaTemporality - exercises the multi-tag (length > 1) allocation path in LookupAggregatorStoreForDeltaWithReclaim with lazy + delta. * LazyAllocationOverflowsDoubleCounter - exercises the overflow path in UpdateDoubleMetricPoint (GetMetricPoint(1).Update) and the TraceBased exemplar branch on a Counter<double>. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@martincostello I resolved all comments |
cijothomas
left a comment
There was a problem hiding this comment.
Please use the SIG meeting to discuss this more.
I don't think we should expand the configuration to add this feature - its forcing end users to make yet another decision.
If the original pre-allocation is not preferred, then it is better to change that itself and make it the default behavior.
|
@saguiitay Would you be able to join the community call to discuss more on this? (I won't be able to join, unfortunately, but I'll share some thoughts in the issue itself) |
|
Sure, as long as it's in reasonable hours (I'm located in Israel time - GMT+3) |
|
That's probably not workable for you then, as I think that would make the SIG at 2200 for you (it's usually at 1900 for me and I'm in the UK). |
I can work with that... |
If it's up to me, we keep the option for a while, gather feedback from users, and only enable it by default if it's proven in production. I'd hate changing the default behavior, and getting a backlash from users that we broke them. |
|
I've added it to today's SIG agenda. |
@martincostello was this discussed? Can you update if any decision was made.
@saguiitay Thanks, this idea makes sense. (And OTEL_DOTNET_EXPERIMENTAL_METRICS_ENABLE_LAZY_ALLOCATION approach would mean only advanced users will need to make this decision, and after evaluation, we can in future make this the default). |
There was a problem hiding this comment.
Pull request overview
This PR introduces an opt-in “lazy” metric point allocation mode in the Metrics SDK to reduce memory usage for high-cardinality streams by allocating metric point storage in fixed-size chunks on demand, while keeping eager allocation as the default.
Changes:
- Added segmented metric point storage and wired
AggregatorStoreto use it when lazy allocation is enabled. - Plumbed a provider-level lazy-allocation flag from
MeterProviderSdk→MetricReader→Metric/AggregatorStore, plus added an experimental environment-variable switch. - Added unit tests and benchmarks to validate chunk allocation behavior and quantify memory/time improvements; updated docs and changelog.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/OpenTelemetry.Tests/Metrics/SegmentedMetricPointStorageTests.cs | New unit tests validating segmented storage allocation behavior and concurrency safety. |
| test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs | Minor formatting-only change. |
| test/OpenTelemetry.Tests/Metrics/AggregatorTests.cs | Added tests covering lazy allocation behavior across cumulative/delta, overflow, reclaim/reuse, and tag handling. |
| test/Benchmarks/Metrics/MetricPointAllocationBenchmarks.cs | New benchmarks comparing eager vs lazy allocation in common scenarios. |
| src/OpenTelemetry/Metrics/Reader/MetricReaderExt.cs | Passes provider-level lazy allocation setting into Metric creation. |
| src/OpenTelemetry/Metrics/MetricPoint/SegmentedMetricPointStorage.cs | New internal segmented/chunked storage implementation for metric points. |
| src/OpenTelemetry/Metrics/MetricPoint/MetricPointsAccessor.cs | Updated public enumerator plumbing to support segmented storage. |
| src/OpenTelemetry/Metrics/Metric.cs | Threads lazy-allocation flag through to AggregatorStore. |
| src/OpenTelemetry/Metrics/MeterProviderSdk.cs | Adds experimental env-var config key and propagates the setting to readers. |
| src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs | Preprocessor change related to CodeAnalysis import guarding. |
| src/OpenTelemetry/Metrics/AggregatorStore.cs | Core implementation changes: optional segmented storage, lazy index reservation, and batch resizing logic. |
| src/OpenTelemetry/CHANGELOG.md | Documents the new experimental environment variable. |
| docs/metrics/customizing-the-sdk/README.md | Documents how to enable lazy metric point allocation via environment variable. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public readonly struct MetricPointsAccessor | ||
| { | ||
| private readonly MetricPoint[] metricsPoints; | ||
| private readonly MetricPoint[]? metricsPoints; | ||
| private readonly SegmentedMetricPointStorage? segmentedMetricPoints; | ||
| private readonly int[] metricPointsToProcess; |
| Metric metric = new( | ||
| metricStreamIdentity, | ||
| this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), | ||
| metricStreamConfig?.CardinalityLimit ?? this.cardinalityLimit, | ||
| exemplarFilter, | ||
| metricStreamConfig?.ExemplarReservoirFactory); | ||
| metricStreamConfig?.ExemplarReservoirFactory, | ||
| this.enableLazyAllocation); |
Yes, Itay joined the SIG call and myself and Alan discussed it. TL;DR was Itay was going to speak to Raj about it internally and then look to make an experimental opt-in to allow for it to be tested in the wild. I forget exactly what else we discussed, but it'll be in the recording. |
Got it. I agree with this approach. The experimental opt-in is via ENV variables right? @saguiitay Can you make the opt-in via ENV variable, and fix CI. I'll help review. |
Yep. |
Fixes #6620
Design discussion issue #
Changes
This PR adds opt-in lazy metric point allocation for high-cardinality metric streams. Today, AggregatorStore eagerly allocates storage for CardinalityLimit + 2 metric points per stream, which can consume a lot of memory even when little or no telemetry is emitted.
Default behavior is unchanged. Existing users continue to get eager allocation unless they explicitly opt in.
Public API
Adds experimental opt-in controls:
View configuration takes precedence over the provider default. The API is marked experimental with OTEL1006.
Implementation
Lazy mode uses segmented metric point storage, allocating metric points in fixed-size chunks as time series are observed instead of allocating the full cardinality limit up front. Eager allocation remains the default path.
Performance impact
For users who do not opt in, there should be no intentional CPU or memory behavior change.
For opt-in users with high CardinalityLimit and low actual cardinality, memory usage drops significantly.
Measured dry BenchmarkDotNet result for 100 metrics, each with CardinalityLimit = 100_000, emitting one time series:
That is roughly ~99.6% less allocated memory and ~46% lower cold-start/setup time in this benchmark. The CPU improvement comes mostly from avoiding large eager initialization.
Test coverage
Added focused unit, integration, stress, and benchmark coverage for lazy allocation, while also verifying that default eager behavior remains unchanged.
Merge requirement checklist
CHANGELOG.mdfiles updated for non-trivial changes