new poll mechanism

- remove secop.poller and basic poller
- regular polls for 'important' parameters done by method doPoll
- all other parameters are polled slower (slowInterval) and
  with lower priority (only one at a time when main poll is due)
- nopoll decorator for read_* to disable poll
- enablePoll attribute (default True) for disabling polling a module
- fast polls may be implemented by means of a statemachine
- configurable slow poll interval
+ allow a Parameter to override a Property (parameter
  Readable.pollinterval overrides Module.pollinterval)

Change-Id: Ib1b3453041a233678b7c4b4add22ac399670e447
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/27832
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
This commit is contained in:
2022-02-23 16:42:28 +01:00
parent aa82bc580d
commit b423235c5d
23 changed files with 343 additions and 683 deletions

View File

@ -69,8 +69,8 @@ def test_handler():
data = []
class Mod(ModuleTest):
a = Parameter('', FloatRange(), readonly=False, poll=True)
b = Parameter('', FloatRange(), readonly=False, poll=True)
a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False)
@ReadHandler(['a', 'b'])
def read_hdl(self, pname):
@ -115,8 +115,8 @@ def test_common_handler():
data = []
class Mod(ModuleTest):
a = Parameter('', FloatRange(), readonly=False, poll=True)
b = Parameter('', FloatRange(), readonly=False, poll=True)
a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False)
@CommonReadHandler(['a', 'b'])
def read_hdl(self):
@ -164,8 +164,8 @@ def test_common_handler():
def test_nopoll():
class Mod1(ModuleTest):
a = Parameter('', FloatRange(), readonly=False, poll=True)
b = Parameter('', FloatRange(), readonly=False, poll=True)
a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False)
@ReadHandler(['a', 'b'])
def read_hdl(self):
@ -175,8 +175,8 @@ def test_nopoll():
assert Mod1.read_b.poll is True
class Mod2(ModuleTest):
a = Parameter('', FloatRange(), readonly=False, poll=True)
b = Parameter('', FloatRange(), readonly=False, poll=True)
a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False)
@CommonReadHandler(['a', 'b'])
def read_hdl(self):
@ -186,8 +186,8 @@ def test_nopoll():
assert Mod2.read_b.poll is False
class Mod3(ModuleTest):
a = Parameter('', FloatRange(), readonly=False, poll=True)
b = Parameter('', FloatRange(), readonly=False, poll=True)
a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False)
@ReadHandler(['a', 'b'])
@nopoll
@ -198,8 +198,8 @@ def test_nopoll():
assert Mod3.read_b.poll is False
class Mod4(ModuleTest):
a = Parameter('', FloatRange(), readonly=False, poll=True)
b = Parameter('', FloatRange(), readonly=False, poll=True)
a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False)
@nopoll
@ReadHandler(['a', 'b'])

View File

@ -22,15 +22,15 @@
# *****************************************************************************
"""test data types."""
import sys
import threading
import pytest
from secop.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger
from secop.errors import ProgrammingError, ConfigError
from secop.modules import Communicator, Drivable, Readable, Module
from secop.params import Command, Parameter
from secop.poller import BasicPoller
from secop.lib.multievent import MultiEvent
from secop.rwhandler import ReadHandler, WriteHandler
from secop.rwhandler import ReadHandler, WriteHandler, nopoll
from secop.lib import generalConfig
@ -67,11 +67,19 @@ class ServerStub:
self.dispatcher = DispatcherStub(updates)
class DummyMultiEvent(threading.Event):
def get_trigger(self):
def trigger(event=self):
event.set()
sys.exit()
return trigger
def test_Communicator():
o = Communicator('communicator', LoggerStub(), {'.description': ''}, ServerStub({}))
o.earlyInit()
o.initModule()
event = MultiEvent()
event = DummyMultiEvent()
o.startModule(event)
assert event.is_set() # event should be set immediately
@ -96,8 +104,6 @@ def test_ModuleMagic():
"""another stuff"""
return not arg
pollerClass = BasicPoller
def read_param1(self):
return True
@ -107,6 +113,7 @@ def test_ModuleMagic():
def read_a1(self):
return True
@nopoll
def read_a2(self):
return True
@ -140,8 +147,10 @@ def test_ModuleMagic():
value = Parameter(datatype=FloatRange(unit='deg'))
target = Parameter(datatype=FloatRange(), default=0)
a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False)
# remark: it might be a programming error to override the datatype
# and not overriding the read_* method. This is not checked!
b2 = Parameter('<b2>', datatype=BoolType(), default=True,
poll=True, readonly=False, initwrite=True)
readonly=False, initwrite=True)
def write_a1(self, value):
self._a1_written = value
@ -182,12 +191,13 @@ def test_ModuleMagic():
'value': 'first'}
assert updates.pop('o1') == expectedBeforeStart
o1.earlyInit()
event = MultiEvent()
event = DummyMultiEvent()
o1.startModule(event)
event.wait()
# should contain polled values
expectedAfterStart = {'status': (Drivable.Status.IDLE, 'ok'),
'value': 'second'}
expectedAfterStart = {
'status': (Drivable.Status.IDLE, 'ok'), 'value': 'second',
'param1': True, 'param2': 0.0, 'a1': True}
assert updates.pop('o1') == expectedAfterStart
# check in addition if parameters are written
@ -197,11 +207,12 @@ def test_ModuleMagic():
expectedBeforeStart['target'] = 0.0
assert updates.pop('o2') == expectedBeforeStart
o2.earlyInit()
event = MultiEvent()
event = DummyMultiEvent()
o2.startModule(event)
event.wait()
# value has changed type, b2 and a1 are written
expectedAfterStart.update(value=0, b2=True, a1=2.7)
expectedAfterStart.update(value=0, b2=True, a1=True)
# ramerk: a1=True: this behaviour is a Porgamming error
assert updates.pop('o2') == expectedAfterStart
assert o2._a1_written == 2.7
assert o2._b2_written is True
@ -218,13 +229,15 @@ def test_ModuleMagic():
# check '$' in unit works properly
assert o2.parameters['a1'].datatype.unit == 'mm/s'
cfg = Newclass2.configurables
assert set(cfg.keys()) == {'export', 'group', 'description', 'disable_value_range_check',
assert set(cfg.keys()) == {
'export', 'group', 'description', 'disable_value_range_check',
'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop',
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'b2', 'cmd2', 'value',
'a1'}
assert set(cfg['value'].keys()) == {'group', 'export', 'relative_resolution',
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'slowinterval', 'b2',
'cmd2', 'value', 'a1'}
assert set(cfg['value'].keys()) == {
'group', 'export', 'relative_resolution',
'visibility', 'unit', 'default', 'datatype', 'fmtstr',
'absolute_resolution', 'poll', 'max', 'min', 'readonly', 'constant',
'absolute_resolution', 'max', 'min', 'readonly', 'constant',
'description', 'needscfg'}
# check on the level of classes

View File

@ -21,18 +21,20 @@
# *****************************************************************************
"""test poller."""
import sys
import threading
import time
from collections import OrderedDict
import logging
import pytest
from secop.modules import Drivable
from secop.poller import DYNAMIC, REGULAR, SLOW, Poller
from secop.core import Module, Parameter, FloatRange, Readable, ReadHandler, nopoll
from secop.lib.multievent import MultiEvent
Status = Drivable.Status
class Time:
STARTTIME = 1000 # artificial time zero
STARTTIME = 1000 # artificial time zero
def __init__(self):
self.reset()
self.finish = float('inf')
@ -61,190 +63,103 @@ class Time:
self.seconds += seconds
self.busytime += seconds
artime = Time() # artificial test time
@pytest.fixture(autouse=True)
def patch_time(monkeypatch):
monkeypatch.setattr(time, 'time', artime.time)
class Event(threading.Event):
def wait(self, timeout=None):
artime.sleep(max(0, timeout))
class Event:
def __init__(self):
self.flag = False
class DispatcherStub:
maxcycles = 10
def wait(self, timeout):
artime.sleep(max(0,timeout))
def set(self):
self.flag = True
def clear(self):
self.flag = False
def is_set(self):
return self.flag
class Parameter:
def __init__(self, name, readonly, poll, polltype, interval):
self.poll = poll
self.polltype = polltype # used for check only
self.export = name
self.readonly = readonly
self.interval = interval
self.timestamp = 0
self.handler = None
self.reset()
def reset(self):
self.cnt = 0
self.span = 0
self.maxspan = 0
def rfunc(self):
artime.busy(artime.commtime)
def announce_update(self, modulename, pname, pobj):
now = artime.time()
self.span = now - self.timestamp
self.maxspan = max(self.maxspan, self.span)
self.timestamp = now
self.cnt += 1
return True
def __repr__(self):
return 'Parameter(%s)' % ", ".join("%s=%r" % item for item in self.__dict__.items())
class Module:
properties = {}
pollerClass = Poller
class io:
name = 'common_io'
def __init__(self, name, pollinterval=5, fastfactor=0.25, slowfactor=4, busy=False,
counts=(), auto=None):
'''create a dummy module
nauto, ndynamic, nregular, nslow are the number of parameters of each polltype
'''
self.pollinterval = pollinterval
self.fast_pollfactor = fastfactor
self.slow_pollfactor = slowfactor
self.parameters = OrderedDict()
self.name = name
self.is_busy = busy
if auto is not None:
self.pvalue = self.addPar('value', True, auto or DYNAMIC, DYNAMIC)
# readonly = False should not matter:
self.pstatus = self.addPar('status', False, auto or DYNAMIC, DYNAMIC)
self.pregular = self.addPar('regular', True, auto or REGULAR, REGULAR)
self.pslow = self.addPar('slow', False, auto or SLOW, SLOW)
self.addPar('notpolled', True, False, 0)
self.counts = 'auto'
if hasattr(pobj, 'stat'):
pobj.stat.append(now)
else:
ndynamic, nregular, nslow = counts
for i in range(ndynamic):
self.addPar('%s:d%d' % (name, i), True, DYNAMIC, DYNAMIC)
for i in range(nregular):
self.addPar('%s:r%d' % (name, i), True, REGULAR, REGULAR)
for i in range(nslow):
self.addPar('%s:s%d' % (name, i), False, SLOW, SLOW)
self.counts = counts
pobj.stat = [now]
self.maxcycles -= 1
if self.maxcycles <= 0:
self.finish_event.set()
sys.exit() # stop thread
def addPar(self, name, readonly, poll, expected_polltype):
# self.count[polltype] += 1
expected_interval = self.pollinterval
if expected_polltype == SLOW:
expected_interval *= self.slow_pollfactor
elif expected_polltype == DYNAMIC and self.is_busy:
expected_interval *= self.fast_pollfactor
pobj = Parameter(name, readonly, poll, expected_polltype, expected_interval)
setattr(self, 'read_' + pobj.export, pobj.rfunc)
self.parameters[pobj.export] = pobj
return pobj
def isBusy(self):
return self.is_busy
class ServerStub:
def __init__(self):
self.dispatcher = DispatcherStub()
def pollOneParam(self, pname):
getattr(self, 'read_' + pname)()
def writeInitParams(self):
pass
class Base(Module):
def __init__(self):
srv = ServerStub()
super().__init__('mod', logging.getLogger('dummy'), dict(description=''), srv)
self.dispatcher = srv.dispatcher
self.nextPollEvent = Event()
def __repr__(self):
rdict = self.__dict__.copy()
rdict.pop('parameters')
return 'Module(%r, counts=%r, f=%r, pollinterval=%g, is_busy=%r)' % (self.name,
self.counts, (self.fast_pollfactor, self.slow_pollfactor, 1),
self.pollinterval, self.is_busy)
def run(self, maxcycles):
self.dispatcher.maxcycles = maxcycles
self.dispatcher.finish_event = threading.Event()
self.startModule(MultiEvent())
self.dispatcher.finish_event.wait(1)
module_list = [
[Module('x', 3.0, 0.125, 10, False, auto=True),
Module('y', 3.0, 0.125, 10, False, auto=False)],
[Module('a', 1.0, 0.25, 4, True, (5, 5, 10)),
Module('b', 2.0, 0.25, 4, True, (5, 5, 50))],
[Module('c', 1.0, 0.25, 4, False, (5, 0, 0))],
[Module('d', 1.0, 0.25, 4, True, (0, 9, 0))],
[Module('e', 1.0, 0.25, 4, True, (0, 0, 9))],
[Module('f', 1.0, 0.25, 4, True, (0, 0, 0))],
]
@pytest.mark.parametrize('modules', module_list)
def test_Poller(modules):
# check for proper timing
for overloaded in False, True:
artime.reset()
count = {DYNAMIC: 0, REGULAR: 0, SLOW: 0}
maxspan = {DYNAMIC: 0, REGULAR: 0, SLOW: 0}
pollTable = dict()
for module in modules:
Poller.add_to_table(pollTable, module)
for pobj in module.parameters.values():
if pobj.poll:
maxspan[pobj.polltype] = max(maxspan[pobj.polltype], pobj.interval)
count[pobj.polltype] += 1
pobj.reset()
assert len(pollTable) == 1
poller = pollTable[(Poller, 'common_io')]
artime.stop = poller.stop
poller._event = Event() # patch Event.wait
class Mod1(Base, Readable):
param1 = Parameter('', FloatRange())
param2 = Parameter('', FloatRange())
param3 = Parameter('', FloatRange())
param4 = Parameter('', FloatRange())
assert (sum(count.values()) > 0) == bool(poller)
@ReadHandler(('param1', 'param2', 'param3'))
def read_param(self, name):
artime.sleep(1.0)
return 0
def started_callback(modules=modules):
for module in modules:
for pobj in module.parameters.values():
assert pobj.cnt == bool(pobj.poll) # all parameters have to be polled once
pobj.reset() # set maxspan and cnt to 0
@nopoll
def read_param4(self):
return 0
if overloaded:
# overloaded scenario
artime.commtime = 1.0
ncycles = 10
if count[SLOW] > 0:
cycletime = (count[REGULAR] + 1) * count[SLOW] * 2
else:
cycletime = max(count[REGULAR], count[DYNAMIC]) * 2
artime.reset(cycletime * ncycles * 1.01) # poller will quit given time
poller.run(started_callback)
total = artime.time() - artime.STARTTIME
for module in modules:
for pobj in module.parameters.values():
if pobj.poll:
# average_span = total / (pobj.cnt + 1)
assert total / (pobj.cnt + 1) <= max(cycletime, pobj.interval * 1.1)
else:
# normal scenario
artime.commtime = 0.001
artime.reset(max(maxspan.values()) * 5) # poller will quit given time
poller.run(started_callback)
total = artime.time() - artime.STARTTIME
for module in modules:
for pobj in module.parameters.values():
if pobj.poll:
assert pobj.cnt > 0
assert pobj.maxspan <= maxspan[pobj.polltype] * 1.1
assert (pobj.cnt + 1) * pobj.interval >= total * 0.99
assert abs(pobj.span - pobj.interval) < 0.01
pobj.reset()
def read_status(self):
artime.sleep(1.0)
return 0
def read_value(self):
artime.sleep(1.0)
return 0
@pytest.mark.parametrize(
'ncycles, pollinterval, slowinterval, mspan, pspan',
[ # normal case: 5+-1 15+-1
( 60, 5, 15, (4, 6), (14, 16)),
# pollinterval faster then reading: mspan max 3 s (polls of value, status and ONE other parameter)
( 60, 1, 5, (1, 3), (5, 16)),
])
def test_poll(ncycles, pollinterval, slowinterval, mspan, pspan, monkeypatch):
monkeypatch.setattr(time, 'time', artime.time)
artime.reset()
m = Mod1()
m.pollinterval = pollinterval
m.slowInterval = slowinterval
m.run(ncycles)
assert not hasattr(m.parameters['param4'], 'stat')
for pname in ['value', 'status']:
pobj = m.parameters[pname]
lowcnt = 0
for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1]):
if t2 - t1 < mspan[0]:
print(t2 - t1)
lowcnt += 1
assert t2 - t1 <= mspan[1]
assert lowcnt <= 1
for pname in ['param1', 'param2', 'param3']:
pobj = m.parameters[pname]
lowcnt = 0
for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1]):
if t2 - t1 < pspan[0]:
print(pname, t2 - t1)
lowcnt += 1
assert t2 - t1 <= pspan[1]
assert lowcnt <= 1

View File

@ -26,6 +26,7 @@ import pytest
from secop.datatypes import FloatRange, IntRange, StringType, ValueType
from secop.errors import BadValueError, ConfigError, ProgrammingError
from secop.properties import HasProperties, Property
from secop.core import Parameter
def Prop(*args, name=None, **kwds):
@ -149,17 +150,25 @@ def test_Property_override():
assert o2.a == 3
with pytest.raises(ProgrammingError) as e:
class cx(c): # pylint: disable=unused-variable
class cx(c): # pylint: disable=unused-variable
def a(self):
pass
assert 'collides with' in str(e.value)
with pytest.raises(ProgrammingError) as e:
class cz(c): # pylint: disable=unused-variable
class cy(c): # pylint: disable=unused-variable
a = 's'
assert 'can not set' in str(e.value)
with pytest.raises(ProgrammingError) as e:
class cz(c): # pylint: disable=unused-variable
a = 's'
class cp(c): # pylint: disable=unused-variable
# overriding a Property with a Parameter is allowed
a = Parameter('x', IntRange())
def test_Properties_mro():
class Base(HasProperties):