Skip to content

Commit cc863d1

Browse files
authored
Improve server polling performances (#255)
Do not sleep the process if about to send data to the device. Prevent sleeping uselessly between the reception of a response and the sending of the next one when doing active polling.
1 parent d279b69 commit cc863d1

14 files changed

+173
-50
lines changed

scrutiny/server/device/device_handler.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from scrutiny.server.protocol.comm_handler import CommHandler
3636
from scrutiny.server.protocol.commands import DummyCommand
3737
from scrutiny.server.device.request_dispatcher import RequestDispatcher, RequestRecord
38+
from scrutiny.server.device.submodules.base_device_handler_submodule import BaseDeviceHandlerSubmodule
3839
from scrutiny.server.device.submodules.device_searcher import DeviceSearcher
3940
from scrutiny.server.device.submodules.heartbeat_generator import HeartbeatGenerator
4041
from scrutiny.server.device.submodules.info_poller import InfoPoller
@@ -531,6 +532,26 @@ def get_link_type(self) -> str:
531532
"""Returns what type of link is used to communicate with the device (serial, UDP, CanBus, etc)"""
532533
return self.comm_handler.get_link_type()
533534

535+
def need_keep_alive(self) -> bool:
536+
"""Inform the server layer that a call to process() will make meaningful work, therefore, avoid sleeping"""
537+
if self.active_request_record is not None: # waiting on data
538+
return False
539+
540+
submodule: BaseDeviceHandlerSubmodule
541+
for submodule in [
542+
self.device_searcher,
543+
self.session_initializer,
544+
self.heartbeat_generator,
545+
self.memory_reader,
546+
self.memory_writer,
547+
self.info_poller,
548+
self.datalogging_poller,
549+
]:
550+
if submodule.would_send_data():
551+
return True
552+
553+
return False
554+
534555
# Set communication state to a fresh start.
535556
def reset_comm(self) -> None:
536557
"""Reset the communication with the device. Reset all state machines, clear pending requests, reset internal status"""
@@ -885,7 +906,7 @@ def do_state_machine(self) -> None:
885906
)
886907

887908
if self.disconnect_complete:
888-
next_state != self.FsmState.DISCONNECTING
909+
next_state = self.FsmState.INIT
889910

890911
else:
891912
raise Exception('Unknown FSM state : %s' % self.fsm_state)

scrutiny/server/device/links/dummy_link.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
#
77
# Copyright (c) 2022 Scrutiny Debugger
88

9-
__all__ = [
10-
'DummyLink'
11-
]
9+
__all__ = ['DummyLink']
1210

1311
from .abstract_link import AbstractLink, LinkConfig
1412
from scrutiny.tools.typing import *

scrutiny/server/device/links/rtt_link.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66
#
77
# Copyright (c) 2024 Scrutiny Debugger
88

9-
__all__ = [
10-
'RttConfig',
11-
'RttLink'
12-
]
9+
__all__ = ['RttLink']
1310

1411
import logging
1512
import threading

scrutiny/server/device/links/serial_link.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@
66
#
77
# Copyright (c) 2022 Scrutiny Debugger
88

9-
__all__ = [
10-
'SerialConfig',
11-
'SerialLink'
12-
]
9+
__all__ = ['SerialLink']
1310

1411
import logging
1512
import serial # type: ignore
1613
import time
14+
import sys
1715

1816
from scrutiny.server.device.links.abstract_link import AbstractLink, LinkConfig
1917
from scrutiny.server.device.links.typing import SerialConfigDict
@@ -76,7 +74,7 @@ def get_stop_bits(cls, s: Union[str, float, int]) -> float:
7674

7775
@classmethod
7876
def get_data_bits(cls, s: Union[str, int]) -> int:
79-
""" Converts a data bist input (str or number) to a pyserial constant"""
77+
""" Converts a data bits input (str or number) to a pyserial constant"""
8078
try:
8179
s = int(s)
8280
except Exception:
@@ -168,7 +166,8 @@ def write(self, data: bytes) -> None:
168166
assert self.port is not None # For mypy
169167
try:
170168
self.port.write(data)
171-
self.port.flush()
169+
if sys.platform != 'win32': # pyserial 3.5 : Win32 implementation does nothing except sleeping
170+
self.port.flush()
172171
except Exception as e:
173172
self.logger.debug(f"Cannot write data. {e}")
174173
self.port.close()

scrutiny/server/device/links/udp_link.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66
#
77
# Copyright (c) 2022 Scrutiny Debugger
88

9-
__all__ = [
10-
'UdpConfig',
11-
'UdpLink'
12-
]
9+
__all__ = ['UdpLink']
1310

1411
import logging
1512
import socket
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
import abc
3+
4+
5+
class BaseDeviceHandlerSubmodule(abc.ABC):
6+
@abc.abstractmethod
7+
def would_send_data(self) -> bool:
8+
"""Returns ``True`` if a call to ``process()`` would dispatch a request to the device"""
9+
pass
10+
11+
@abc.abstractmethod
12+
def start(self) -> None:
13+
pass
14+
15+
@abc.abstractmethod
16+
def stop(self) -> None:
17+
pass
18+
19+
@abc.abstractmethod
20+
def fully_stopped(self) -> bool:
21+
pass
22+
23+
@abc.abstractmethod
24+
def process(self) -> None:
25+
pass

scrutiny/server/device/submodules/datalogging_poller.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from enum import Enum, auto
1818
from dataclasses import dataclass
1919

20+
from scrutiny.server.device.submodules.base_device_handler_submodule import BaseDeviceHandlerSubmodule
2021
from scrutiny.server.protocol import *
2122
from scrutiny.server.device.request_dispatcher import RequestDispatcher
2223
import scrutiny.server.datalogging.definitions.device as device_datalogging
@@ -69,7 +70,7 @@ class _ReceivedChunk:
6970
DatalogSubfn = cmd.DatalogControl.Subfunction
7071

7172

72-
class DataloggingPoller:
73+
class DataloggingPoller(BaseDeviceHandlerSubmodule):
7374

7475
UPDATE_STATUS_INTERVAL_IDLE = 0.5
7576
UPDATE_STATUS_INTERVAL_ACQUIRING = 0.2
@@ -327,6 +328,19 @@ def is_ready_to_receive_new_request(self) -> bool:
327328
"""Tells if request_acquisition() can be called. """
328329
return self.started and not self.error and not self.cancel_requested and not self.stop_requested and self.device_setup is not None
329330

331+
def would_send_data(self) -> bool:
332+
# We don't need to be accurate here.
333+
# It's only an optimization to prevent the server from sleeping.
334+
# Since we sent occasional message, optimizing few milliseconds is not worth it.
335+
336+
if not self.started or not self.enabled or self.stop_requested or self.error:
337+
return False
338+
339+
if self.require_status_update or self.update_status_timer.is_timed_out():
340+
return True
341+
# Don't bother about all the other possibilities, not worth it.
342+
return False
343+
330344
def process(self) -> None:
331345
"""To be called periodically to make the process move forward"""
332346
# Handle conditions that prevent the DataloggingPoller to function
@@ -504,7 +518,6 @@ def process(self) -> None:
504518

505519
if self.cancel_requested: # New request interrupts the previous one
506520
if not self.request_pending[DatalogSubfn.ReadAcquisition]:
507-
self.must_send_read_data_request = False
508521
next_state = _FSMState.REQUEST_RESET
509522

510523
elif self.device_datalogging_state == device_datalogging.DataloggerState.ERROR:

scrutiny/server/device/submodules/device_searcher.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414
import binascii
1515
import traceback
1616

17+
from scrutiny.server.device.submodules.base_device_handler_submodule import BaseDeviceHandlerSubmodule
1718
from scrutiny.server.protocol import *
1819
import scrutiny.server.protocol.typing as protocol_typing
1920
from scrutiny.server.device.request_dispatcher import RequestDispatcher
2021

2122
from scrutiny.tools.typing import *
2223

2324

24-
class DeviceSearcher:
25+
class DeviceSearcher(BaseDeviceHandlerSubmodule):
2526
"""
2627
Generates Discover request in loop and inform the upper layers if a device has been found
2728
"""
@@ -105,6 +106,14 @@ def get_device_protocol_version(self) -> Optional[Tuple[int, int]]:
105106
return (self.found_device['protocol_major'], self.found_device['protocol_minor'])
106107
return None
107108

109+
def _search_request_timedout(self) -> bool:
110+
return self.last_request_timestamp is None or (time.monotonic() - self.last_request_timestamp > self.DISCOVER_INTERVAL)
111+
112+
def would_send_data(self) -> bool:
113+
if not self.started or self.pending:
114+
return False
115+
return self._search_request_timedout()
116+
108117
def process(self) -> None:
109118
"""To be called periodically"""
110119
if not self.started:
@@ -116,7 +125,7 @@ def process(self) -> None:
116125
self.found_device = None
117126

118127
if self.pending == False:
119-
if self.last_request_timestamp is None or (time.monotonic() - self.last_request_timestamp > self.DISCOVER_INTERVAL):
128+
if self._search_request_timedout():
120129
if self.logger.isEnabledFor(logging.DEBUG): # pragma: no cover
121130
self.logger.debug('Registering a Discover request')
122131
self.dispatcher.register_request(

scrutiny/server/device/submodules/heartbeat_generator.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@
1212
import time
1313
import logging
1414

15+
from scrutiny.server.device.submodules.base_device_handler_submodule import BaseDeviceHandlerSubmodule
1516
from scrutiny.server.protocol import *
1617
import scrutiny.server.protocol.typing as protocol_typing
1718
from scrutiny.server.device.request_dispatcher import RequestDispatcher
1819
from scrutiny import tools
1920
from scrutiny.tools.typing import *
2021

2122

22-
class HeartbeatGenerator:
23+
class HeartbeatGenerator(BaseDeviceHandlerSubmodule):
2324
"""
2425
Poll the device with periodic heartbeat message to know if it is still there and alive.
2526
"""
@@ -32,15 +33,15 @@ class HeartbeatGenerator:
3233
"""Our dispatcher priority"""
3334
session_id: Optional[int]
3435
"""The session ID to include in the heartbeat request"""
35-
last_heartbeat_request: Optional[float]
36+
last_heartbeat_request_timestamp: Optional[float]
3637
"""Time at which that last heartbeat request has been sent."""
3738
last_heartbeat_timestamp: Optional[float]
3839
"""Time at which the last successful heartbeat response has been received"""
3940
challenge: int
40-
"""The computation challenge included in the pending request."""
41+
"""The computation challenge included in the request_pending request."""
4142
interval: float
4243
"""Heartbeat interval in seconds"""
43-
pending: bool
44+
request_pending: bool
4445
"""True when a request is sent and we are waiting for a response"""
4546
started: bool
4647
"""True when started. Sends heartbeat only when started, otherwise keep silent"""
@@ -50,7 +51,7 @@ def __init__(self, protocol: Protocol, dispatcher: RequestDispatcher, priority:
5051
self.dispatcher = dispatcher
5152
self.protocol = protocol
5253
self.session_id = None
53-
self.last_heartbeat_request = None
54+
self.last_heartbeat_request_timestamp = None
5455
self.last_heartbeat_timestamp = time.monotonic()
5556
self.challenge = 0
5657
self.interval = 3
@@ -76,24 +77,36 @@ def stop(self) -> None:
7677
self.started = False
7778

7879
def fully_stopped(self) -> bool:
79-
"""Indicates that this submodule is stopped and has no pending state"""
80+
"""Indicates that this submodule is stopped and has no request_pending state"""
8081
return self.started == False
8182

8283
def reset(self) -> None:
8384
"""Put the heartbeat generator in its startup state"""
84-
self.pending = False
85+
self.request_pending = False
8586
self.started = False
8687
self.session_id = None
8788

89+
def _heartbeat_timedout(self) -> bool:
90+
return self.last_heartbeat_request_timestamp is None or (time.monotonic() - self.last_heartbeat_request_timestamp > self.interval)
91+
92+
def would_send_data(self) -> bool:
93+
if not self.started:
94+
return False
95+
96+
if self.request_pending or self.session_id is None:
97+
return False
98+
99+
return self._heartbeat_timedout()
100+
88101
def process(self) -> None:
89102
"""To be called periodically"""
90103
if not self.started:
91104
self.reset()
92105
return
93106

94107
# If no request is being waited and we have a session ID assigned
95-
if self.pending == False and self.session_id is not None:
96-
if self.last_heartbeat_request is None or (time.monotonic() - self.last_heartbeat_request > self.interval):
108+
if self.request_pending == False and self.session_id is not None:
109+
if self._heartbeat_timedout():
97110
if self.logger.isEnabledFor(logging.DEBUG): # pragma: no cover
98111
self.logger.debug('Registering a Heartbeat request')
99112
self.dispatcher.register_request(
@@ -102,8 +115,8 @@ def process(self) -> None:
102115
failure_callback=self._failure_callback,
103116
priority=self.priority
104117
)
105-
self.pending = True
106-
self.last_heartbeat_request = time.monotonic()
118+
self.request_pending = True
119+
self.last_heartbeat_request_timestamp = time.monotonic()
107120

108121
def _success_callback(self, request: Request, response: Response, params: Any = None) -> None:
109122
""" Called by the dispatcher when a request is completed and succeeded"""
@@ -140,4 +153,4 @@ def _failure_callback(self, request: Request, params: Any = None) -> None:
140153
def _completed(self) -> None:
141154
""" Common code between success and failure"""
142155
self.challenge = (self.challenge + 1) & 0xFFFF # Next challenge
143-
self.pending = False
156+
self.request_pending = False

scrutiny/server/device/submodules/info_poller.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import enum
1919
import copy
2020

21+
from scrutiny.server.device.submodules.base_device_handler_submodule import BaseDeviceHandlerSubmodule
2122
from scrutiny.core.basic_types import MemoryRegion
2223
from scrutiny.server.protocol import ResponseCode
2324
from scrutiny.server.device.device_info import *
@@ -35,7 +36,7 @@
3536
CommParamCallback = Callable[[DeviceInfo], Any]
3637

3738

38-
class InfoPoller:
39+
class InfoPoller(BaseDeviceHandlerSubmodule):
3940
"""Class that will successfully sends polling request to the device
4041
to gather all of its internal parameters. Will fill a DeviceInformation structure"""
4142

@@ -155,6 +156,17 @@ def reset(self) -> None:
155156
self.error_message = ""
156157
self.info.clear()
157158

159+
def would_send_data(self) -> bool:
160+
if not self.started:
161+
return False
162+
if self.stop_requested:
163+
return False
164+
if self.request_pending:
165+
return False
166+
if self.fsm_state in (self.FsmState.Done, self.FsmState.Error):
167+
return False
168+
return True
169+
158170
def process(self) -> None:
159171
"""To be called periodically to make the process move forward"""
160172
if not self.started:

0 commit comments

Comments
 (0)