Skip to content

Commit 502b5fd

Browse files
committed
Ordered ringbuffer with tests
Signed-off-by: Mathias L. Baumann <[email protected]>
1 parent eef25ec commit 502b5fd

File tree

4 files changed

+849
-1
lines changed

4 files changed

+849
-1
lines changed

RELEASE_NOTES.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66

77
## Upgrading
88

9-
<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
9+
<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
1010

1111
## New Features
1212

1313
<!-- Here goes the main new features and examples or instructions on how to use them -->
1414

15+
* A new class `OrderedRingBuffer` is now available, providing a sorted ring buffer of datetime-value pairs with tracking of any values that have not yet been written.
16+
17+
1518
## Bug Fixes
1619

1720
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# License: MIT
2+
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Performance test for the `Ringbuffer` class."""
5+
6+
import random
7+
import timeit
8+
from datetime import datetime, timedelta
9+
from typing import Any, TypeVar
10+
11+
import numpy as np
12+
13+
from frequenz.sdk.timeseries._ringbuffer import OrderedRingBuffer
14+
15+
MINUTES_IN_A_DAY = 24 * 60
16+
MINUTES_IN_29_DAYS = 29 * MINUTES_IN_A_DAY
17+
18+
19+
T = TypeVar("T")
20+
21+
22+
def fill_buffer(
23+
days: int, buffer: OrderedRingBuffer[T, Any], element_type: type
24+
) -> None:
25+
"""Fill the given buffer up to the given amount of days, one sample per minute."""
26+
random.seed(0)
27+
basetime = datetime(2022, 1, 1)
28+
29+
for day in range(days):
30+
# Push in random order
31+
for i in random.sample(range(MINUTES_IN_A_DAY), MINUTES_IN_A_DAY):
32+
buffer.update(
33+
basetime + timedelta(days=day, minutes=i, seconds=i % 3),
34+
element_type(i),
35+
)
36+
37+
38+
def test_days(days: int, buffer: OrderedRingBuffer[int, Any]) -> None:
39+
"""Fills a buffer completely up and then gets the data for each of the 29 days."""
40+
print(".", end="", flush=True)
41+
42+
fill_buffer(days, buffer, int)
43+
44+
basetime = datetime(2022, 1, 1)
45+
46+
for day in range(days):
47+
# pylint: disable=unused-variable
48+
minutes = buffer.window(
49+
basetime + timedelta(days=day), basetime + timedelta(days=day + 1)
50+
)
51+
52+
53+
def test_slices(days: int, buffer: OrderedRingBuffer[T, Any]) -> None:
54+
"""Benchmark slicing.
55+
56+
Takes a buffer, fills it up and then excessively gets
57+
the data for each day to calculate the average/median.
58+
"""
59+
print(".", end="", flush=True)
60+
fill_buffer(days, buffer, float)
61+
62+
# Chose uneven starting point so that for the first/last window data has to
63+
# be copied
64+
basetime = datetime(2022, 1, 1, 0, 5, 13, 88)
65+
66+
total_avg = 0.0
67+
total_median = 0.0
68+
69+
for _ in range(5):
70+
for day in range(days):
71+
minutes = buffer.window(
72+
basetime + timedelta(days=day), basetime + timedelta(days=day + 1)
73+
)
74+
75+
total_avg += float(np.average(minutes))
76+
total_median += float(np.median(minutes))
77+
78+
79+
def test_29_days_list() -> None:
80+
"""Run the 29 day test on the list backend."""
81+
test_days(29, OrderedRingBuffer([0] * MINUTES_IN_29_DAYS, timedelta(minutes=1)))
82+
83+
84+
def test_29_days_array() -> None:
85+
"""Run the 29 day test on the array backend."""
86+
test_days(
87+
29,
88+
OrderedRingBuffer(
89+
np.empty(
90+
shape=MINUTES_IN_29_DAYS,
91+
),
92+
timedelta(minutes=1),
93+
),
94+
)
95+
96+
97+
def test_29_days_slicing_list() -> None:
98+
"""Run slicing tests on list backend."""
99+
test_slices(29, OrderedRingBuffer([0] * MINUTES_IN_29_DAYS, timedelta(minutes=1)))
100+
101+
102+
def test_29_days_slicing_array() -> None:
103+
"""Run slicing tests on array backend."""
104+
test_slices(
105+
29,
106+
OrderedRingBuffer(
107+
np.empty(
108+
shape=MINUTES_IN_29_DAYS,
109+
),
110+
timedelta(minutes=1),
111+
),
112+
)
113+
114+
115+
def main() -> None:
116+
"""Run benchmark.
117+
118+
Result of previous run:
119+
120+
Date: Do 22. Dez 15:03:05 CET 2022
121+
Result:
122+
123+
=========================================
124+
Array: ........................................
125+
List: ........................................
126+
Time to fill 29 days with data:
127+
Array: 0.09411649959984061 seconds
128+
List: 0.0906366748000437 seconds
129+
Diff: 0.0034798247997969156
130+
=========================================
131+
Array: ........................................
132+
List: ........................................
133+
Filling 29 days and running average & mean on every day:
134+
Array: 0.09842290654996759 seconds
135+
List: 0.1316629376997298 seconds
136+
Diff: -0.03324003114976222
137+
"""
138+
num_runs = 40
139+
140+
print(f" {''.join(['='] * (num_runs + 1))}")
141+
print("Array: ", end="")
142+
duration_array = timeit.Timer(test_29_days_array).timeit(number=num_runs)
143+
print("\nList: ", end="")
144+
duration_list = timeit.Timer(test_29_days_list).timeit(number=num_runs)
145+
print("")
146+
147+
print(
148+
"Time to fill 29 days with data:\n\t"
149+
+ f"Array: {duration_array/num_runs} seconds\n\t"
150+
+ f"List: {duration_list/num_runs} seconds\n\t"
151+
+ f"Diff: {duration_array/num_runs - duration_list/num_runs}"
152+
)
153+
154+
print(f" {''.join(['='] * (num_runs + 1))}")
155+
print("Array: ", end="")
156+
duration_array = timeit.Timer(test_29_days_slicing_array).timeit(number=num_runs)
157+
print("\nList: ", end="")
158+
duration_list = timeit.Timer(test_29_days_slicing_list).timeit(number=num_runs)
159+
print("")
160+
161+
print(
162+
"Filling 29 days and running average & mean on every day:\n\t"
163+
+ f"Array: {duration_array/num_runs} seconds\n\t"
164+
+ f"List: {duration_list/num_runs} seconds\n\t"
165+
+ f"Diff: {duration_array/num_runs - duration_list/num_runs}"
166+
)
167+
168+
169+
if __name__ == "__main__":
170+
main()

0 commit comments

Comments
 (0)