Skip to content

Commit 26fa527

Browse files
committed
Use plain floats in the resampler buffer
When resampling we know we are working always with the same units, so using a `Quantity` doesn't add any advantages to plain `float`s. Instead, it makes the resampler slower, as it needs to do an extra lookup each time a buffer value is needed and potentially avoids the need to create `Quantity`s from the inputs. The performance can be improved by about 25% (for resamples=1000 samples=1000, before 3.7s, now 2.8s) according to the resampling benchmark. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 5503c83 commit 26fa527

File tree

5 files changed

+73
-71
lines changed

5 files changed

+73
-71
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
## Upgrading
88

99
* The microgrid client dependency has been updated to version 0.9.0
10+
* The resampling function now takes plain `float`s as values instead of `Quantity` objects.
1011

1112
## New Features
1213

benchmarks/timeseries/resampling.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,13 @@
77
from datetime import datetime, timedelta, timezone
88
from timeit import timeit
99

10-
from frequenz.quantities import Quantity
11-
1210
from frequenz.sdk.timeseries import ResamplerConfig
1311
from frequenz.sdk.timeseries._resampling._base_types import SourceProperties
1412
from frequenz.sdk.timeseries._resampling._resampler import _ResamplingHelper
1513

1614

1715
def nop( # pylint: disable=unused-argument
18-
samples: Sequence[tuple[datetime, Quantity]],
16+
samples: Sequence[tuple[datetime, float]],
1917
resampler_config: ResamplerConfig,
2018
source_properties: SourceProperties,
2119
) -> float:
@@ -43,7 +41,7 @@ def _do_work() -> None:
4341
for _n_resample in range(resamples):
4442
for _n_sample in range(samples):
4543
now = now + timedelta(seconds=1 / samples)
46-
helper.add_sample((now, Quantity(0.0)))
44+
helper.add_sample((now, 0.0))
4745
helper.resample(now)
4846

4947
print(timeit(_do_work, number=5))

src/frequenz/sdk/timeseries/_resampling/_config.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@
1111
from datetime import datetime, timedelta
1212
from typing import Protocol
1313

14-
from frequenz.quantities import Quantity
15-
16-
from .._base_types import UNIX_EPOCH, QuantityT
14+
from .._base_types import UNIX_EPOCH
1715
from ._base_types import SourceProperties
1816

1917
_logger = logging.getLogger(__name__)
@@ -60,7 +58,7 @@ class ResamplingFunction(Protocol):
6058

6159
def __call__(
6260
self,
63-
input_samples: Sequence[tuple[datetime, Quantity]],
61+
input_samples: Sequence[tuple[datetime, float]],
6462
resampler_config: ResamplerConfig,
6563
source_properties: SourceProperties,
6664
/,
@@ -82,7 +80,7 @@ def __call__(
8280

8381

8482
def average(
85-
samples: Sequence[tuple[datetime, QuantityT]],
83+
samples: Sequence[tuple[datetime, float]],
8684
resampler_config: ResamplerConfig, # pylint: disable=unused-argument
8785
source_properties: SourceProperties, # pylint: disable=unused-argument
8886
) -> float:
@@ -98,7 +96,7 @@ def average(
9896
The average of all `samples` values.
9997
"""
10098
assert len(samples) > 0, "Average cannot be given an empty list of samples"
101-
values = list(sample[1].base_value for sample in samples)
99+
values = list(sample[1] for sample in samples)
102100
return sum(values) / len(values)
103101

104102

src/frequenz/sdk/timeseries/_resampling/_resampler.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ def __init__(self, name: str, config: ResamplerConfig) -> None:
258258
"""
259259
self._name = name
260260
self._config = config
261-
self._buffer: deque[tuple[datetime, Quantity]] = deque(
261+
self._buffer: deque[tuple[datetime, float]] = deque(
262262
maxlen=config.initial_buffer_len
263263
)
264264
self._source_properties: SourceProperties = SourceProperties()
@@ -272,7 +272,7 @@ def source_properties(self) -> SourceProperties:
272272
"""
273273
return self._source_properties
274274

275-
def add_sample(self, sample: tuple[datetime, Quantity]) -> None:
275+
def add_sample(self, sample: tuple[datetime, float]) -> None:
276276
"""Add a new sample to the internal buffer.
277277
278278
Args:
@@ -511,7 +511,7 @@ async def _receive_samples(self) -> None:
511511
"""
512512
async for sample in self._source:
513513
if sample.value is not None and not sample.value.isnan():
514-
self._helper.add_sample((sample.timestamp, sample.value))
514+
self._helper.add_sample((sample.timestamp, sample.value.base_value))
515515

516516
# We need the noqa because pydoclint can't figure out that `recv_exception` is an
517517
# `Exception` instance.

tests/timeseries/test_resampling.py

Lines changed: 63 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import asyncio
88
import logging
99
from collections.abc import AsyncIterator
10-
from dataclasses import astuple
1110
from datetime import datetime, timedelta, timezone
1211
from unittest.mock import AsyncMock, MagicMock
1312

@@ -55,6 +54,12 @@ async def source_chan() -> AsyncIterator[Broadcast[Sample[Quantity]]]:
5554
await chan.close()
5655

5756

57+
def as_float_tuple(sample: Sample[Quantity]) -> tuple[datetime, float]:
58+
"""Convert a sample to a tuple of datetime and float value."""
59+
assert sample.value is not None, "Sample value should not be None"
60+
return (sample.timestamp, sample.value.base_value)
61+
62+
5863
async def _advance_time(fake_time: time_machine.Coordinates, seconds: float) -> None:
5964
"""Advance the time by the given number of seconds.
6065
@@ -160,7 +165,7 @@ async def test_helper_buffer_too_big(
160165
helper = _ResamplingHelper("test", config)
161166

162167
for i in range(DEFAULT_BUFFER_LEN_MAX + 1):
163-
sample = (datetime.now(timezone.utc), Quantity(i))
168+
sample = (datetime.now(timezone.utc), i)
164169
helper.add_sample(sample)
165170
await _advance_time(fake_time, 1)
166171

@@ -315,7 +320,7 @@ async def test_resampling_window_size_is_constant(
315320
)
316321
)
317322
resampling_fun_mock.assert_called_once_with(
318-
a_sequence(astuple(sample1s)), config, source_props
323+
a_sequence(as_float_tuple(sample1s)), config, source_props
319324
)
320325
sink_mock.reset_mock()
321326
resampling_fun_mock.reset_mock()
@@ -343,9 +348,9 @@ async def test_resampling_window_size_is_constant(
343348
)
344349
resampling_fun_mock.assert_called_once_with(
345350
a_sequence(
346-
astuple(sample2_5s),
347-
astuple(sample3s),
348-
astuple(sample4s),
351+
as_float_tuple(sample2_5s),
352+
as_float_tuple(sample3s),
353+
as_float_tuple(sample4s),
349354
),
350355
config,
351356
source_props,
@@ -415,8 +420,8 @@ async def test_timer_errors_are_logged( # pylint: disable=too-many-statements
415420
)
416421
resampling_fun_mock.assert_called_once_with(
417422
a_sequence(
418-
astuple(sample0s),
419-
astuple(sample1s),
423+
as_float_tuple(sample0s),
424+
as_float_tuple(sample1s),
420425
),
421426
config,
422427
source_props,
@@ -454,10 +459,10 @@ async def test_timer_errors_are_logged( # pylint: disable=too-many-statements
454459
)
455460
resampling_fun_mock.assert_called_once_with(
456461
a_sequence(
457-
astuple(sample1s),
458-
astuple(sample2_5s),
459-
astuple(sample3s),
460-
astuple(sample4s),
462+
as_float_tuple(sample1s),
463+
as_float_tuple(sample2_5s),
464+
as_float_tuple(sample3s),
465+
as_float_tuple(sample4s),
461466
),
462467
config,
463468
source_props,
@@ -493,11 +498,11 @@ async def test_timer_errors_are_logged( # pylint: disable=too-many-statements
493498
)
494499
resampling_fun_mock.assert_called_once_with(
495500
a_sequence(
496-
astuple(sample3s),
497-
astuple(sample4s),
498-
astuple(sample4_5s),
499-
astuple(sample5s),
500-
astuple(sample6s),
501+
as_float_tuple(sample3s),
502+
as_float_tuple(sample4s),
503+
as_float_tuple(sample4_5s),
504+
as_float_tuple(sample5s),
505+
as_float_tuple(sample6s),
501506
),
502507
config,
503508
source_props,
@@ -570,8 +575,8 @@ async def test_future_samples_not_included(
570575
)
571576
resampling_fun_mock.assert_called_once_with(
572577
a_sequence(
573-
astuple(sample0s),
574-
astuple(sample1s),
578+
as_float_tuple(sample0s),
579+
as_float_tuple(sample1s),
575580
),
576581
config,
577582
source_props, # sample2_1s is not here
@@ -600,9 +605,9 @@ async def test_future_samples_not_included(
600605
)
601606
resampling_fun_mock.assert_called_once_with(
602607
a_sequence(
603-
astuple(sample1s),
604-
astuple(sample2_1s),
605-
astuple(sample3s),
608+
as_float_tuple(sample1s),
609+
as_float_tuple(sample2_1s),
610+
as_float_tuple(sample3s),
606611
),
607612
config,
608613
source_props, # sample4_1s is not here
@@ -662,7 +667,7 @@ async def test_resampling_with_one_window(
662667
)
663668
resampling_fun_mock.assert_called_once_with(
664669
a_sequence(
665-
astuple(sample1s),
670+
as_float_tuple(sample1s),
666671
),
667672
config,
668673
source_props,
@@ -693,9 +698,9 @@ async def test_resampling_with_one_window(
693698
)
694699
resampling_fun_mock.assert_called_once_with(
695700
a_sequence(
696-
astuple(sample2_5s),
697-
astuple(sample3s),
698-
astuple(sample4s),
701+
as_float_tuple(sample2_5s),
702+
as_float_tuple(sample3s),
703+
as_float_tuple(sample4s),
699704
),
700705
config,
701706
source_props,
@@ -786,8 +791,8 @@ async def test_resampling_with_one_and_a_half_windows( # pylint: disable=too-ma
786791
)
787792
resampling_fun_mock.assert_called_once_with(
788793
a_sequence(
789-
astuple(sample0s),
790-
astuple(sample1s),
794+
as_float_tuple(sample0s),
795+
as_float_tuple(sample1s),
791796
),
792797
config,
793798
source_props,
@@ -819,9 +824,9 @@ async def test_resampling_with_one_and_a_half_windows( # pylint: disable=too-ma
819824
# It should include samples in the interval (1, 4] seconds
820825
resampling_fun_mock.assert_called_once_with(
821826
a_sequence(
822-
astuple(sample2_5s),
823-
astuple(sample3s),
824-
astuple(sample4s),
827+
as_float_tuple(sample2_5s),
828+
as_float_tuple(sample3s),
829+
as_float_tuple(sample4s),
825830
),
826831
config,
827832
source_props,
@@ -851,9 +856,9 @@ async def test_resampling_with_one_and_a_half_windows( # pylint: disable=too-ma
851856
# It should include samples in the interval (3, 6] seconds
852857
resampling_fun_mock.assert_called_once_with(
853858
a_sequence(
854-
astuple(sample4s),
855-
astuple(sample5s),
856-
astuple(sample6s),
859+
as_float_tuple(sample4s),
860+
as_float_tuple(sample5s),
861+
as_float_tuple(sample6s),
857862
),
858863
config,
859864
source_props,
@@ -885,7 +890,7 @@ async def test_resampling_with_one_and_a_half_windows( # pylint: disable=too-ma
885890
)
886891
# It should include samples in the interval (5, 8] seconds
887892
resampling_fun_mock.assert_called_once_with(
888-
a_sequence(astuple(sample6s)),
893+
a_sequence(as_float_tuple(sample6s)),
889894
config,
890895
source_props,
891896
)
@@ -965,8 +970,8 @@ async def test_resampling_with_two_windows( # pylint: disable=too-many-statemen
965970
)
966971
resampling_fun_mock.assert_called_once_with(
967972
a_sequence(
968-
astuple(sample0s),
969-
astuple(sample1s),
973+
as_float_tuple(sample0s),
974+
as_float_tuple(sample1s),
970975
),
971976
config,
972977
source_props,
@@ -998,10 +1003,10 @@ async def test_resampling_with_two_windows( # pylint: disable=too-many-statemen
9981003
# It should include samples in the interval (0, 4] seconds
9991004
resampling_fun_mock.assert_called_once_with(
10001005
a_sequence(
1001-
astuple(sample1s),
1002-
astuple(sample2_5s),
1003-
astuple(sample3s),
1004-
astuple(sample4s),
1006+
as_float_tuple(sample1s),
1007+
as_float_tuple(sample2_5s),
1008+
as_float_tuple(sample3s),
1009+
as_float_tuple(sample4s),
10051010
),
10061011
config,
10071012
source_props,
@@ -1031,11 +1036,11 @@ async def test_resampling_with_two_windows( # pylint: disable=too-many-statemen
10311036
# It should include samples in the interval (2, 6] seconds
10321037
resampling_fun_mock.assert_called_once_with(
10331038
a_sequence(
1034-
astuple(sample2_5s),
1035-
astuple(sample3s),
1036-
astuple(sample4s),
1037-
astuple(sample5s),
1038-
astuple(sample6s),
1039+
as_float_tuple(sample2_5s),
1040+
as_float_tuple(sample3s),
1041+
as_float_tuple(sample4s),
1042+
as_float_tuple(sample5s),
1043+
as_float_tuple(sample6s),
10391044
),
10401045
config,
10411046
source_props,
@@ -1061,8 +1066,8 @@ async def test_resampling_with_two_windows( # pylint: disable=too-many-statemen
10611066
# It should include samples in the interval (4, 8] seconds
10621067
resampling_fun_mock.assert_called_once_with(
10631068
a_sequence(
1064-
astuple(sample5s),
1065-
astuple(sample6s),
1069+
as_float_tuple(sample5s),
1070+
as_float_tuple(sample6s),
10661071
),
10671072
config,
10681073
source_props,
@@ -1130,7 +1135,7 @@ async def test_receiving_stopped_resampling_error(
11301135
)
11311136
)
11321137
resampling_fun_mock.assert_called_once_with(
1133-
a_sequence(astuple(sample0s)), config, source_props
1138+
a_sequence(as_float_tuple(sample0s)), config, source_props
11341139
)
11351140
sink_mock.reset_mock()
11361141
resampling_fun_mock.reset_mock()
@@ -1266,11 +1271,11 @@ async def test_timer_is_aligned(
12661271
)
12671272
resampling_fun_mock.assert_called_once_with(
12681273
a_sequence(
1269-
astuple(sample1s),
1270-
astuple(sample1_5s),
1271-
astuple(sample2_5s),
1272-
astuple(sample3s),
1273-
astuple(sample4s),
1274+
as_float_tuple(sample1s),
1275+
as_float_tuple(sample1_5s),
1276+
as_float_tuple(sample2_5s),
1277+
as_float_tuple(sample3s),
1278+
as_float_tuple(sample4s),
12741279
),
12751280
config,
12761281
source_props,
@@ -1337,7 +1342,7 @@ async def test_resampling_all_zeros(
13371342
)
13381343
)
13391344
resampling_fun_mock.assert_called_once_with(
1340-
a_sequence(astuple(sample1s)), config, source_props
1345+
a_sequence(as_float_tuple(sample1s)), config, source_props
13411346
)
13421347
assert source_props == SourceProperties(
13431348
sampling_start=timestamp, received_samples=2, sampling_period=None
@@ -1365,9 +1370,9 @@ async def test_resampling_all_zeros(
13651370
)
13661371
resampling_fun_mock.assert_called_once_with(
13671372
a_sequence(
1368-
astuple(sample2_5s),
1369-
astuple(sample3s),
1370-
astuple(sample4s),
1373+
as_float_tuple(sample2_5s),
1374+
as_float_tuple(sample3s),
1375+
as_float_tuple(sample4s),
13711376
),
13721377
config,
13731378
source_props,

0 commit comments

Comments
 (0)