[Exporter.Prometheus] Avoid allocations for tags#7488
Conversation
Add support for emitting resource attributes as metric labels based on a user-supplied predicate. Resolves open-telemetry#7437.
Add PR number.
Add missing comment.
- Allow the maximum buffer size to be configured to allow for scrape responses beyond 100,000 metrics. - Return an HTTP 500 if the buffer is not large enough to contain the scrape response.
Add PR number.
Use pooled buffers to write tags to avoid allocations when serializing metrics.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #7488 +/- ##
==========================================
- Coverage 90.29% 90.29% -0.01%
==========================================
Files 287 287
Lines 15617 15647 +30
==========================================
+ Hits 14102 14128 +26
- Misses 1515 1519 +4
Flags with carried forward coverage won't be shown. Click here to find out more.
|
Increase the initial size of the buffer and remember the high-water mark for the allocation size to avoid try-catch to resize large tag buffers.
Prometheus AspNetCore exporter — scrape performance on .NET 10: 1.15 vs 1.16 vs latestEnd-to-end scrape benchmark on .NET 10, comparing three points in history:
Method
Mean time per scrape (ms)
Allocated per scrape (MB)
Summary of the differences (100k series)
Caveat
|
Fix accidentally removed CHANGELOG entry.
There was a problem hiding this comment.
Pull request overview
This PR improves Prometheus HttpListener exporter scrape-time performance by reducing allocations during metric serialization, specifically by reusing pooled buffers when serializing tag sets (to avoid repeated per-bucket allocations for histograms).
Changes:
- Replaces per-call tag serialization allocations with
ArrayPool<byte>-backed buffers for histogram tag serialization. - Optimizes duplicate-label detection in the tag-writing fast path by replacing a
List<(int,int)>with a pooled(int,int)[]. - Introduces a
serializedTagsBufferHintto reduce re-rent/grow churn when serializing tags repeatedly.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Cap the maximum size of the scratch buffer.
Changes
Use pooled buffers to write tags to avoid allocations when serializing metrics.
Builds on top of #7471 and #7487 - 1379395 are the interesting changes until this is rebased once changes those are merged.Benchmarks comparing 1.15, 1.16, #7487 and then this PR are shown below:
Expand to view
Prometheus AspNetCore exporter — scrape performance across versions
End-to-end scrape benchmark across four points in history:
022ae547028158df5718ba4b99ab809710831f6dc39666185fc7d8c7d5052eaec172b143af8041dc81a9cf49c5d319bca4fb91d9cf14c3d261ca69931379395b15387c1f0097849ad9317d77fdb61341("Avoid allocations for tags")Method
MeterProviderbuilt withSdk.CreateMeterProviderBuilder(), exposed via the AspNetCore scrape middleware (UseOpenTelemetryPrometheusScrapingEndpoint), scraped over an in-memoryTestServer. Response cache disabled, so every scrape does a full collect + serialize.HttpCompletionOption.ResponseHeadersRead) and does not materialize the body. The exporter serializes the entire payload into its buffer before the response headers are produced, so this measures all the server-side generation effort while excluding body transfer and client-side materialization. A full-body read is done once in setup to warm the buffer.TimeSeriesunique data points, ~70% counters / ~10% histograms / ~20% observable-gauge; cardinality limit raised to match.MemoryDiagnoser. net9.0, Windows, 13th Gen Intel i7-13700H. 1,000,000 series is only run for the two latest versions (1.15/1.16 cannot serve a scrape that large — ~83 MiB response-buffer ceiling).Mean time per scrape (ms)
Allocated per scrape (MB)
Findings
1.16 was a large allocation regression — 100k allocations rose 3–6× over 1.15 (
otel_scope_*labels,_createdseries, and exemplars added in 1.16).orig latest(81a9cf4) recovered most of that regression relative to 1.16.updated latest(1379395, "Avoid allocations for tags") cuts a further ~21–23% off allocations across the board and improves time, with no regressions:At 1,000,000 series the reduction holds: allocations −21% (text/plain), −23% (OpenMetrics). text/plain allocations are now within ~5% of 1.15 while emitting the richer modern output.
Caveats / notes
application/openmetrics-text; version=1.0.0(content negotiation landed in 1.16), so its two format columns are the same text-style output — the 1.15 "OpenMetrics" rows are not a true OpenMetrics comparison.Merge requirement checklist
Unit tests added/updatedAppropriateCHANGELOG.mdfiles updated for non-trivial changesChanges in public API reviewed (if applicable)