Skip to content

Commit 1dcc75d

Browse files
Make interface for serializing a ringbuffer more flexible. (#326)
Signed-off-by: Mathias L. Baumann <[email protected]>
2 parents f2eed52 + 355e4a8 commit 1dcc75d

File tree

5 files changed

+90
-109
lines changed

5 files changed

+90
-109
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* Add a resampler in the MovingWindow to control the granularity of the samples to be stored in the underlying buffer (#269).
1818
Notice that the parameter `sampling_period` has been renamed to `input_sampling_period`
1919
to better distinguish it from the sampling period parameter in the resampler.
20+
* The serialization feature for the ringbuffer was made more flexible. The `dump` and `load` methods can now work directly with a ringbuffer instance.
2021

2122
## New Features
2223

benchmarks/timeseries/serializable_ringbuffer.py renamed to benchmarks/timeseries/ringbuffer_serialization.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# License: MIT
22
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
33

4-
"""Benchmarks the `SerializableRingbuffer` class."""
4+
"""Benchmarks the serialization of the `OrderedRingBuffer` class."""
55

66
from __future__ import annotations
77

@@ -13,8 +13,9 @@
1313

1414
import numpy as np
1515

16+
import frequenz.sdk.timeseries._ringbuffer_serialization as io
1617
from frequenz.sdk.timeseries import Sample
17-
from frequenz.sdk.timeseries._serializable_ringbuffer import SerializableRingBuffer
18+
from frequenz.sdk.timeseries._ringbuffer import OrderedRingBuffer
1819

1920
FILE_NAME = "ringbuffer.pkl"
2021
FIVE_MINUTES = timedelta(minutes=5)
@@ -37,7 +38,7 @@ def delete_files_with_prefix(prefix: str) -> None:
3738

3839

3940
def benchmark_serialization(
40-
ringbuffer: SerializableRingBuffer[Any], iterations: int
41+
ringbuffer: OrderedRingBuffer[Any], iterations: int
4142
) -> float:
4243
"""Benchmark the given buffer `iteration` times.
4344
@@ -48,8 +49,8 @@ def benchmark_serialization(
4849
total = 0.0
4950
for _ in range(iterations):
5051
start = time.time()
51-
ringbuffer.dump()
52-
SerializableRingBuffer.load(FILE_NAME)
52+
io.dump(ringbuffer, FILE_NAME)
53+
io.load(FILE_NAME)
5354
end = time.time()
5455
total += end - start
5556
delete_files_with_prefix(FILE_NAME)
@@ -59,8 +60,8 @@ def benchmark_serialization(
5960

6061
def main() -> None:
6162
"""Run Benchmark."""
62-
ringbuffer = SerializableRingBuffer(
63-
np.arange(0, SIZE, dtype=np.float64), timedelta(minutes=5), FILE_NAME
63+
ringbuffer = OrderedRingBuffer(
64+
np.arange(0, SIZE, dtype=np.float64), timedelta(minutes=5)
6465
)
6566

6667
print("size:", SIZE)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# License: MIT
2+
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Ringbuffer dumping & loading functions."""
5+
6+
# For use of the class type hint inside the class itself.
7+
from __future__ import annotations
8+
9+
import pickle
10+
from os.path import exists
11+
12+
from ._ringbuffer import FloatArray, OrderedRingBuffer
13+
14+
# Version of the latest file dumping/loading format
15+
FILE_FORMAT_VERSION: int = 1
16+
17+
18+
def load(path: str) -> OrderedRingBuffer[FloatArray] | None:
19+
"""Load a ringbuffer from disk.
20+
21+
Args:
22+
path: Path to the file where the data is stored.
23+
24+
Raises:
25+
RuntimeError: when the file format version is unknown.
26+
27+
Returns:
28+
`None` when the file doesn't exist, otherwise an instance of the
29+
`OrderedRingBuffer` class, loaded from disk.
30+
"""
31+
if not exists(path):
32+
return None
33+
34+
with open(path, mode="rb") as fileobj:
35+
instance: OrderedRingBuffer[FloatArray]
36+
file_format_version: int
37+
38+
file_format_version, instance = pickle.load(fileobj)
39+
40+
if file_format_version != FILE_FORMAT_VERSION:
41+
raise RuntimeError(
42+
f"Unknown file format version: {file_format_version}. Can load: {FILE_FORMAT_VERSION}"
43+
)
44+
45+
return instance
46+
47+
48+
def dump(
49+
ringbuffer: OrderedRingBuffer[FloatArray],
50+
path: str,
51+
file_format_version: int = FILE_FORMAT_VERSION,
52+
) -> None:
53+
"""Dump a ringbuffer to disk.
54+
55+
Args:
56+
ringbuffer: Instance of the ringbuffer to dump.
57+
path: Path to where the data should be saved to.
58+
file_format_version: Version of the file format, optional.
59+
60+
Raises:
61+
I/O related exceptions when the file cannot be written.
62+
"""
63+
with open(path, mode="wb+") as fileobj:
64+
pickle.dump((file_format_version, ringbuffer), fileobj)

src/frequenz/sdk/timeseries/_serializable_ringbuffer.py

Lines changed: 0 additions & 86 deletions
This file was deleted.

tests/timeseries/test_serializable_ringbuffer.py renamed to tests/timeseries/test_ringbuffer_serialization.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@
1212
import numpy as np
1313
import pytest
1414

15+
import frequenz.sdk.timeseries._ringbuffer_serialization as io
1516
from frequenz.sdk.timeseries import Sample
16-
from frequenz.sdk.timeseries._serializable_ringbuffer import SerializableRingBuffer
17+
from frequenz.sdk.timeseries._ringbuffer import OrderedRingBuffer
1718

1819
FIVE_MINUTES = timedelta(minutes=5)
1920
_29_DAYS = 60 * 24 * 29
2021
ONE_MINUTE = timedelta(minutes=1)
2122

2223

23-
def load_dump_test(dumped: SerializableRingBuffer[Any]) -> None:
24+
def load_dump_test(dumped: OrderedRingBuffer[Any], path: str) -> None:
2425
"""Test ordered ring buffer."""
2526
size = dumped.maxlen
2627

@@ -42,11 +43,11 @@ def load_dump_test(dumped: SerializableRingBuffer[Any]) -> None:
4243
)
4344
)
4445

45-
dumped.dump()
46+
io.dump(dumped, path)
4647

4748
# Load old data
4849
# pylint: disable=protected-access
49-
loaded = SerializableRingBuffer.load(dumped._path)
50+
loaded = io.load(path)
5051
assert loaded is not None
5152

5253
np.testing.assert_equal(dumped[:], loaded[:])
@@ -72,21 +73,21 @@ def test_load_dump_short(tmp_path_factory: pytest.TempPathFactory) -> None:
7273
tmpdir = tmp_path_factory.mktemp("load_dump")
7374

7475
load_dump_test(
75-
SerializableRingBuffer(
76+
OrderedRingBuffer(
7677
[0.0] * int(24 * FIVE_MINUTES.total_seconds()),
7778
FIVE_MINUTES,
78-
f"{tmpdir}/test_list.bin",
7979
datetime(2, 2, 2, tzinfo=timezone.utc),
80-
)
80+
),
81+
f"{tmpdir}/test_list.bin",
8182
)
8283

8384
load_dump_test(
84-
SerializableRingBuffer(
85+
OrderedRingBuffer(
8586
np.empty(shape=(24 * int(FIVE_MINUTES.total_seconds()),), dtype=np.float64),
8687
FIVE_MINUTES,
87-
f"{tmpdir}/test_array.bin",
8888
datetime(2, 2, 2, tzinfo=timezone.utc),
89-
)
89+
),
90+
f"{tmpdir}/test_array.bin",
9091
)
9192

9293

@@ -95,19 +96,19 @@ def test_load_dump(tmp_path_factory: pytest.TempPathFactory) -> None:
9596
tmpdir = tmp_path_factory.mktemp("load_dump")
9697

9798
load_dump_test(
98-
SerializableRingBuffer(
99+
OrderedRingBuffer(
99100
[0.0] * _29_DAYS,
100101
ONE_MINUTE,
101-
f"{tmpdir}/test_list_29.bin",
102102
datetime(2, 2, 2, tzinfo=timezone.utc),
103-
)
103+
),
104+
f"{tmpdir}/test_list_29.bin",
104105
)
105106

106107
load_dump_test(
107-
SerializableRingBuffer(
108+
OrderedRingBuffer(
108109
np.empty(shape=(_29_DAYS,), dtype=np.float64),
109110
ONE_MINUTE,
110-
f"{tmpdir}/test_array_29.bin",
111111
datetime(2, 2, 2, tzinfo=timezone.utc),
112-
)
112+
),
113+
f"{tmpdir}/test_array_29.bin",
113114
)

0 commit comments

Comments
 (0)