X-Git-Url: https://gerrit.fd.io/r/gitweb?a=blobdiff_plain;f=resources%2Flibraries%2Fpython%2FMLRsearch%2Ftrial_measurement%2Fmeasurement_result.py;fp=resources%2Flibraries%2Fpython%2FMLRsearch%2Ftrial_measurement%2Fmeasurement_result.py;h=9dc1ccf5f18132f6203d41166241d7b69405ce1a;hb=e5dbe10d9599b9a53fa07e6fadfaf427ba6d69e3;hp=0000000000000000000000000000000000000000;hpb=c6dfb6c09c5dafd1d522f96b4b86c5ec5efc1c83;p=csit.git 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)