Skip to content

Commit af32c8e

Browse files
committed
improve MessageSync timings
1 parent ae41b3e commit af32c8e

File tree

3 files changed

+35
-39
lines changed

3 files changed

+35
-39
lines changed

can/io/player.py

+15-10
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""
66
import gzip
77
import pathlib
8-
from time import time, sleep
8+
import time
99
import typing
1010

1111
from pkg_resources import iter_entry_points
@@ -132,8 +132,9 @@ def __init__(
132132
self.skip = skip
133133

134134
def __iter__(self) -> typing.Generator[Message, None, None]:
135-
playback_start_time = time()
135+
t_wakeup = playback_start_time = time.perf_counter()
136136
recorded_start_time = None
137+
t_skipped = 0.0
137138

138139
for message in self.raw_messages:
139140

@@ -142,15 +143,19 @@ def __iter__(self) -> typing.Generator[Message, None, None]:
142143
if recorded_start_time is None:
143144
recorded_start_time = message.timestamp
144145

145-
now = time()
146-
current_offset = now - playback_start_time
147-
recorded_offset_from_start = message.timestamp - recorded_start_time
148-
remaining_gap = max(0.0, recorded_offset_from_start - current_offset)
149-
150-
sleep_period = max(self.gap, min(self.skip, remaining_gap))
146+
t_wakeup = playback_start_time + (
147+
message.timestamp - t_skipped - recorded_start_time
148+
)
151149
else:
152-
sleep_period = self.gap
150+
t_wakeup += self.gap
151+
152+
sleep_period = t_wakeup - time.perf_counter()
153+
154+
if self.skip and sleep_period > self.skip:
155+
t_skipped += sleep_period - self.skip
156+
sleep_period = self.skip
153157

154-
sleep(sleep_period)
158+
if sleep_period > 1e-4:
159+
time.sleep(sleep_period)
155160

156161
yield message

test/test_message_sync.py

+18-22
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""
66

77
from copy import copy
8-
from time import time
8+
import time
99
import gc
1010

1111
import unittest
@@ -49,43 +49,40 @@ def teardown_method(self, _):
4949
# we need to reenable the garbage collector again
5050
gc.enable()
5151

52-
@pytest.mark.timeout(inc(0.2))
5352
def test_general(self):
5453
messages = [
5554
Message(timestamp=50.0),
5655
Message(timestamp=50.0),
5756
Message(timestamp=50.0 + 0.05),
58-
Message(timestamp=50.0 + 0.05 + 0.08),
57+
Message(timestamp=50.0 + 0.13),
5958
Message(timestamp=50.0), # back in time
6059
]
61-
sync = MessageSync(messages, gap=0.0)
60+
sync = MessageSync(messages, gap=0.0, skip=0.0)
6261

63-
start = time()
62+
t_start = time.perf_counter()
6463
collected = []
6564
timings = []
6665
for message in sync:
66+
t_now = time.perf_counter()
6767
collected.append(message)
68-
now = time()
69-
timings.append(now - start)
70-
start = now
68+
timings.append(t_now - t_start)
7169

7270
self.assertMessagesEqual(messages, collected)
7371
self.assertEqual(len(timings), len(messages), "programming error in test code")
7472

75-
self.assertTrue(0.0 <= timings[0] < inc(0.005), str(timings[0]))
76-
self.assertTrue(0.0 <= timings[1] < inc(0.005), str(timings[1]))
77-
self.assertTrue(0.045 <= timings[2] < inc(0.055), str(timings[2]))
78-
self.assertTrue(0.075 <= timings[3] < inc(0.085), str(timings[3]))
79-
self.assertTrue(0.0 <= timings[4] < inc(0.005), str(timings[4]))
73+
self.assertTrue(0.0 <= timings[0] < 0.0 + inc(0.02), str(timings[0]))
74+
self.assertTrue(0.0 <= timings[1] < 0.0 + inc(0.02), str(timings[1]))
75+
self.assertTrue(0.045 <= timings[2] < 0.05 + inc(0.02), str(timings[2]))
76+
self.assertTrue(0.125 <= timings[3] < 0.13 + inc(0.02), str(timings[3]))
77+
self.assertTrue(0.125 <= timings[4] < 0.13 + inc(0.02), str(timings[4]))
8078

81-
@pytest.mark.timeout(inc(0.1) * len(TEST_FEWER_MESSAGES)) # very conservative
8279
def test_skip(self):
8380
messages = copy(TEST_FEWER_MESSAGES)
8481
sync = MessageSync(messages, skip=0.005, gap=0.0)
8582

86-
before = time()
83+
before = time.perf_counter()
8784
collected = list(sync)
88-
after = time()
85+
after = time.perf_counter()
8986
took = after - before
9087

9188
# the handling of the messages itself also takes some time:
@@ -96,26 +93,25 @@ def test_skip(self):
9693

9794

9895
@skip_on_unreliable_platforms
99-
@pytest.mark.timeout(inc(0.3))
10096
@pytest.mark.parametrize(
101-
"timestamp_1,timestamp_2", [(0.0, 0.0), (0.0, 0.01), (0.01, 0.0)]
97+
"timestamp_1,timestamp_2", [(0.0, 0.0), (0.0, 0.01), (0.01, 1.5)]
10298
)
10399
def test_gap(timestamp_1, timestamp_2):
104100
"""This method is alone so it can be parameterized."""
105101
messages = [
106102
Message(arbitration_id=0x1, timestamp=timestamp_1),
107103
Message(arbitration_id=0x2, timestamp=timestamp_2),
108104
]
109-
sync = MessageSync(messages, gap=0.1)
105+
sync = MessageSync(messages, timestamps=False, gap=0.1)
110106

111107
gc.disable()
112-
before = time()
108+
before = time.perf_counter()
113109
collected = list(sync)
114-
after = time()
110+
after = time.perf_counter()
115111
gc.enable()
116112
took = after - before
117113

118-
assert 0.1 <= took < inc(0.3)
114+
assert 0.195 <= took < 0.2 + inc(0.02)
119115
assert messages == collected
120116

121117

test/test_player.py

+2-7
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def setUp(self) -> None:
2727
self.mock_virtual_bus.__enter__ = Mock(return_value=self.mock_virtual_bus)
2828

2929
# Patch time sleep object
30-
patcher_sleep = mock.patch("can.io.player.sleep", spec=True)
30+
patcher_sleep = mock.patch("can.io.player.time.sleep", spec=True)
3131
self.MockSleep = patcher_sleep.start()
3232
self.addCleanup(patcher_sleep.stop)
3333

@@ -60,7 +60,6 @@ def test_play_virtual(self):
6060
dlc=8,
6161
data=[0x5, 0xC, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0],
6262
)
63-
self.assertEqual(self.MockSleep.call_count, 2)
6463
if sys.version_info >= (3, 8):
6564
# The args argument was introduced with python 3.8
6665
self.assertTrue(
@@ -78,16 +77,14 @@ def test_play_virtual_verbose(self):
7877
self.assertIn("09 08 07 06 05 04 03 02", mock_stdout.getvalue())
7978
self.assertIn("05 0c 00 00 00 00 00 00", mock_stdout.getvalue())
8079
self.assertEqual(self.mock_virtual_bus.send.call_count, 2)
81-
self.assertEqual(self.MockSleep.call_count, 2)
8280
self.assertSuccessfulCleanup()
8381

8482
def test_play_virtual_exit(self):
8583
self.MockSleep.side_effect = [None, KeyboardInterrupt]
8684

8785
sys.argv = self.baseargs + [self.logfile]
8886
can.player.main()
89-
self.assertEqual(self.mock_virtual_bus.send.call_count, 1)
90-
self.assertEqual(self.MockSleep.call_count, 2)
87+
assert self.mock_virtual_bus.send.call_count <= 2
9188
self.assertSuccessfulCleanup()
9289

9390
def test_play_skip_error_frame(self):
@@ -97,7 +94,6 @@ def test_play_skip_error_frame(self):
9794
sys.argv = self.baseargs + ["-v", logfile]
9895
can.player.main()
9996
self.assertEqual(self.mock_virtual_bus.send.call_count, 9)
100-
self.assertEqual(self.MockSleep.call_count, 12)
10197
self.assertSuccessfulCleanup()
10298

10399
def test_play_error_frame(self):
@@ -107,7 +103,6 @@ def test_play_error_frame(self):
107103
sys.argv = self.baseargs + ["-v", "--error-frames", logfile]
108104
can.player.main()
109105
self.assertEqual(self.mock_virtual_bus.send.call_count, 12)
110-
self.assertEqual(self.MockSleep.call_count, 12)
111106
self.assertSuccessfulCleanup()
112107

113108

0 commit comments

Comments
 (0)