From e5dbe10d9599b9a53fa07e6fadfaf427ba6d69e3 Mon Sep 17 00:00:00 2001 From: Vratko Polak Date: Tue, 17 Oct 2023 16:31:35 +0200 Subject: feat(MLRsearch): MLRsearch v7 Replaces MLRv2, suitable for "big bang" upgrade across CSIT. PyPI metadata updated only partially (full edits will come separately). Pylint wants less complexity, but the differences are only minor. + Use the same (new CSIT) defaults everywhere, also in Python library. + Update also PLRsearch to use the new result class. + Make upper bound optional in UTI. + Fix ASTF approximate duration detection. + Do not keep approximated_receive_rate (for MRR) in result structure. Change-Id: I03406f32d5c93f56b527cb3f93791b61955dfd74 Signed-off-by: Vratko Polak --- .../python/MLRsearch/trial_measurement/__init__.py | 19 +++ .../trial_measurement/abstract_measurer.py | 55 +++++++ .../trial_measurement/measurement_result.py | 161 +++++++++++++++++++++ 3 files changed, 235 insertions(+) create mode 100644 resources/libraries/python/MLRsearch/trial_measurement/__init__.py create mode 100644 resources/libraries/python/MLRsearch/trial_measurement/abstract_measurer.py create mode 100644 resources/libraries/python/MLRsearch/trial_measurement/measurement_result.py (limited to 'resources/libraries/python/MLRsearch/trial_measurement') diff --git a/resources/libraries/python/MLRsearch/trial_measurement/__init__.py b/resources/libraries/python/MLRsearch/trial_measurement/__init__.py new file mode 100644 index 0000000000..034ae41819 --- /dev/null +++ b/resources/libraries/python/MLRsearch/trial_measurement/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2023 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +__init__ file for Python package "trial_measurement". +""" + +from .abstract_measurer import AbstractMeasurer +from .measurement_result import MeasurementResult diff --git a/resources/libraries/python/MLRsearch/trial_measurement/abstract_measurer.py b/resources/libraries/python/MLRsearch/trial_measurement/abstract_measurer.py new file mode 100644 index 0000000000..6fab79c8dc --- /dev/null +++ b/resources/libraries/python/MLRsearch/trial_measurement/abstract_measurer.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module defining AbstractMeasurer class.""" + +from abc import ABCMeta, abstractmethod + +from .measurement_result import MeasurementResult as Result + + +class AbstractMeasurer(metaclass=ABCMeta): + """Abstract class defining common API for trial measurement providers. + + The original use of this class was in the realm of + RFC 2544 Throughput search, which explains the teminology + related to networks, frames, packets, offered load, forwarding rate + and similar. + + But the same logic can be used in higher level networking scenarios + (e.g. https requests) or even outside networking (database transactions). + + The current code uses language from packet forwarding, + docstring sometimes mention transactions as an alternative view. + """ + + @abstractmethod + def measure(self, intended_duration: float, intended_load: float) -> Result: + """Perform trial measurement and return the result. + + It is assumed the measurer got already configured with anything else + needed to perform the measurement (e.g. traffic profile + or transaction limit). + + Duration and load are the only values expected to vary + during the search. + + :param intended_duration: Intended trial duration [s]. + :param intended_load: Intended rate of transactions (packets) [tps]. + It is a per-port rate, e.g. uni-directional for SUTs + with two ports. + :type intended_duration: float + :type intended_load: float + :returns: Structure detailing the result of the measurement. + :rtype: measurement_result.MeasurementResult + """ diff --git a/resources/libraries/python/MLRsearch/trial_measurement/measurement_result.py b/resources/libraries/python/MLRsearch/trial_measurement/measurement_result.py new file mode 100644 index 0000000000..9dc1ccf5f1 --- /dev/null +++ b/resources/libraries/python/MLRsearch/trial_measurement/measurement_result.py @@ -0,0 +1,161 @@ +# Copyright (c) 2023 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module defining MeasurementResult class.""" + +from dataclasses import dataclass + + +@dataclass +class MeasurementResult: + """Structure defining the result of a single trial measurement. + + There are few primary (required) quantities. Various secondary (derived) + quantities are calculated and can be queried. + + The constructor allows broader argument types, + the post init function converts to the stricter types. + + Integer quantities (counts) are preferred, as float values + can suffer from rounding errors, and sometimes they are measured + at unknown (possibly very limited) precision and accuracy. + + There are relations between the counts (e.g. offered count + should be equal to a sum of forwarding count and loss count). + This implementation does not perform consistency checks, but uses them + for computing quantities the caller left unspecified. + + In some cases, the units of intended load are different from units + of loss count (e.g. load in transactions but loss in packets). + Quantities with relative_ prefix can be used to get load candidates + from forwarding results. + + Sometimes, the measurement provider is unable to reach the intended load, + and it can react by spending longer than intended duration + to reach its intended count. To signal irregular situations like this, + several optional fields can be given, and various secondary quantities + are populated, so the measurement consumer can query the quantity + it wants to rely on in these irregular situations. + + The current implementation intentionally limits the secondary quantities + to the few that proved useful in practice. + """ + + # Required primary quantities. + intended_duration: float + """Intended trial measurement duration [s].""" + intended_load: float + """Intended load [tps]. If bidirectional (or multi-port) traffic is used, + most users will put unidirectional (single-port) value here, + as bandwidth and pps limits are usually per-port.""" + # Two of the next three primary quantities are required. + offered_count: int = None + """Number of packets actually transmitted (transactions attempted). + This should be the aggregate (bidirectional, multi-port) value, + so that asymmetric trafic profiles are supported.""" + loss_count: int = None + """Number of packets transmitted but not received (transactions failed).""" + forwarding_count: int = None + """Number of packets successfully forwarded (transactions succeeded).""" + # Optional primary quantities. + offered_duration: float = None + """Estimate of the time [s] the trial was actually transmitting traffic.""" + duration_with_overheads: float = None + """Estimate of the time [s] it took to get the trial result + since the measurement started.""" + intended_count: int = None + """Expected number of packets to transmit. If not known, + the value of offered_count is used.""" + + def __post_init__(self) -> None: + """Convert types, compute missing values. + + Current caveats: + A failing assumption looks like a conversion error. + Negative counts are allowed, which can lead to errors later. + """ + self.intended_duration = float(self.intended_duration) + if self.offered_duration is None: + self.offered_duration = self.intended_duration + else: + self.offered_duration = float(self.offered_duration) + if self.duration_with_overheads is None: + self.duration_with_overheads = self.offered_duration + else: + self.duration_with_overheads = float(self.duration_with_overheads) + self.intended_load = float(self.intended_load) + if self.forwarding_count is None: + self.forwarding_count = int(self.offered_count) - int( + self.loss_count + ) + else: + self.forwarding_count = int(self.forwarding_count) + if self.offered_count is None: + self.offered_count = self.forwarding_count + int(self.loss_count) + else: + self.offered_count = int(self.offered_count) + if self.loss_count is None: + self.loss_count = self.offered_count - self.forwarding_count + else: + self.loss_count = int(self.loss_count) + if self.intended_count is None: + self.intended_count = self.offered_count + else: + self.intended_count = int(self.intended_count) + # TODO: Handle (somehow) situations where offered > intended? + + @property + def unsent_count(self) -> int: + """How many packets were not transmitted (transactions not started). + + :return: Intended count minus offered count. + :rtype: int + """ + return self.intended_count - self.offered_count + + @property + def loss_ratio(self) -> float: + """Bad count divided by overall count, zero if the latter is zero. + + The bad count includes not only loss count, but also unsent count. + If unsent count is negative, its absolute value is used. + The overall count is intended count or offered count, + whichever is bigger. + + Together, the resulting formula tends to increase loss ratio + (but not above 100%) in irregular situations, + thus guiding search algorithms towards lower loads + where there should be less irregularities. + The zero default is there to prevent search algorithms from + getting stuck on a too low intended load. + + :returns: Bad count divided by overall count. + :rtype: float + """ + overall = max(self.offered_count, self.intended_count) + bad = abs(self.loss_count) + abs(self.unsent_count) + return bad / overall if overall else 0.0 + + @property + def relative_forwarding_rate(self) -> float: + """Forwarding rate in load units as if duration and load was intended. + + The result is based purely on intended load and loss ratio. + While the resulting value may be far from what really happened, + it has nice behavior with respect to common assumptions + of search algorithms. + + :returns: Forwarding rate in load units estimated from loss ratio. + :rtype: float + """ + return self.intended_load * (1.0 - self.loss_ratio) -- cgit 1.2.3-korg