Skip to content

Commit 72fad82

Browse files
authored
opentelemetry-sdk: speed up exemplars a bit (open-telemetry#4260)
* opentelemetry-sdk: speed up exemplars a bit Use a sparse dict to allocate ExemplarsBucket on demand instead of preallocating all of them in FixedSizeExemplarReservoirABC. Make the following return around 2X more loops for both trace_based and always_off exemplars filter: .tox/benchmark-opentelemetry-sdk/bin/pytest opentelemetry-sdk/benchmarks/metrics/ -k 'test_histogram_record_1000[7]' * Add benchmarks for steady state * Ignore benchmarks dirs for exported public symbols * Add changelog
1 parent db4ef06 commit 72fad82

File tree

4 files changed

+138
-12
lines changed

4 files changed

+138
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
([#4324](https://github.com/open-telemetry/opentelemetry-python/pull/4324))
2424
- Remove `TestBase.assertEqualSpanInstrumentationInfo` method, use `assertEqualSpanInstrumentationScope` instead
2525
([#4310](https://github.com/open-telemetry/opentelemetry-python/pull/4310))
26+
- sdk: instantiate lazily `ExemplarBucket`s in `ExemplarReservoir`s
27+
([#4260](https://github.com/open-telemetry/opentelemetry-python/pull/4260))
2628

2729
## Version 1.28.0/0.49b0 (2024-11-05)
2830

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# pylint: disable=invalid-name
16+
import itertools
17+
18+
from opentelemetry.sdk.metrics import MeterProvider
19+
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
20+
from opentelemetry.sdk.metrics.view import (
21+
ExplicitBucketHistogramAggregation,
22+
View,
23+
)
24+
25+
MAX_BOUND_VALUE = 10000
26+
27+
28+
def _generate_bounds(bound_count):
29+
bounds = []
30+
for i in range(bound_count):
31+
bounds.append(i * MAX_BOUND_VALUE / bound_count)
32+
return bounds
33+
34+
35+
hist_view_10 = View(
36+
instrument_name="test_histogram_10_bound",
37+
aggregation=ExplicitBucketHistogramAggregation(_generate_bounds(10)),
38+
)
39+
hist_view_49 = View(
40+
instrument_name="test_histogram_49_bound",
41+
aggregation=ExplicitBucketHistogramAggregation(_generate_bounds(49)),
42+
)
43+
hist_view_50 = View(
44+
instrument_name="test_histogram_50_bound",
45+
aggregation=ExplicitBucketHistogramAggregation(_generate_bounds(50)),
46+
)
47+
hist_view_1000 = View(
48+
instrument_name="test_histogram_1000_bound",
49+
aggregation=ExplicitBucketHistogramAggregation(_generate_bounds(1000)),
50+
)
51+
reader = InMemoryMetricReader()
52+
provider = MeterProvider(
53+
metric_readers=[reader],
54+
views=[
55+
hist_view_10,
56+
hist_view_49,
57+
hist_view_50,
58+
hist_view_1000,
59+
],
60+
)
61+
meter = provider.get_meter("sdk_meter_provider")
62+
hist = meter.create_histogram("test_histogram_default")
63+
hist10 = meter.create_histogram("test_histogram_10_bound")
64+
hist49 = meter.create_histogram("test_histogram_49_bound")
65+
hist50 = meter.create_histogram("test_histogram_50_bound")
66+
hist1000 = meter.create_histogram("test_histogram_1000_bound")
67+
68+
69+
def test_histogram_record(benchmark):
70+
values = itertools.cycle(_generate_bounds(10))
71+
72+
def benchmark_histogram_record():
73+
hist.record(next(values))
74+
75+
benchmark(benchmark_histogram_record)
76+
77+
78+
def test_histogram_record_10(benchmark):
79+
values = itertools.cycle(_generate_bounds(10))
80+
81+
def benchmark_histogram_record_10():
82+
hist10.record(next(values))
83+
84+
benchmark(benchmark_histogram_record_10)
85+
86+
87+
def test_histogram_record_49(benchmark):
88+
values = itertools.cycle(_generate_bounds(49))
89+
90+
def benchmark_histogram_record_49():
91+
hist49.record(next(values))
92+
93+
benchmark(benchmark_histogram_record_49)
94+
95+
96+
def test_histogram_record_50(benchmark):
97+
values = itertools.cycle(_generate_bounds(50))
98+
99+
def benchmark_histogram_record_50():
100+
hist50.record(next(values))
101+
102+
benchmark(benchmark_histogram_record_50)
103+
104+
105+
def test_histogram_record_1000(benchmark):
106+
values = itertools.cycle(_generate_bounds(1000))
107+
108+
def benchmark_histogram_record_1000():
109+
hist1000.record(next(values))
110+
111+
benchmark(benchmark_histogram_record_1000)

opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exemplar/exemplar_reservoir.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,18 @@
1313
# limitations under the License.
1414

1515
from abc import ABC, abstractmethod
16+
from collections import defaultdict
1617
from random import randrange
17-
from typing import Any, Callable, Dict, List, Optional, Sequence, Union
18+
from typing import (
19+
Any,
20+
Callable,
21+
Dict,
22+
List,
23+
Mapping,
24+
Optional,
25+
Sequence,
26+
Union,
27+
)
1828

1929
from opentelemetry import trace
2030
from opentelemetry.context import Context
@@ -155,9 +165,9 @@ class FixedSizeExemplarReservoirABC(ExemplarReservoir):
155165
def __init__(self, size: int, **kwargs) -> None:
156166
super().__init__(**kwargs)
157167
self._size: int = size
158-
self._reservoir_storage: List[ExemplarBucket] = [
159-
ExemplarBucket() for _ in range(self._size)
160-
]
168+
self._reservoir_storage: Mapping[int, ExemplarBucket] = defaultdict(
169+
ExemplarBucket
170+
)
161171

162172
def collect(self, point_attributes: Attributes) -> List[Exemplar]:
163173
"""Returns accumulated Exemplars and also resets the reservoir for the next
@@ -171,15 +181,16 @@ def collect(self, point_attributes: Attributes) -> List[Exemplar]:
171181
exemplars contain the attributes that were filtered out by the aggregator,
172182
but recorded alongside the original measurement.
173183
"""
174-
exemplars = filter(
175-
lambda e: e is not None,
176-
map(
177-
lambda bucket: bucket.collect(point_attributes),
178-
self._reservoir_storage,
179-
),
180-
)
184+
exemplars = [
185+
e
186+
for e in (
187+
bucket.collect(point_attributes)
188+
for _, bucket in sorted(self._reservoir_storage.items())
189+
)
190+
if e is not None
191+
]
181192
self._reset()
182-
return [*exemplars]
193+
return exemplars
183194

184195
def offer(
185196
self,

scripts/public_symbols_checker.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ def get_symbols(change_type, diff_lines_getter, prefix):
5555
and part[1] != "_"
5656
# tests directories
5757
or part == "tests"
58+
# benchmarks directories
59+
or part == "benchmarks"
5860
for part in b_file_path_obj.parts
5961
)
6062
):

0 commit comments

Comments
 (0)