CSIT-992: Add libraries for optimized search 90/11390/115
authorVratko Polak <vrpolak@cisco.com>
Fri, 27 Apr 2018 13:11:12 +0000 (15:11 +0200)
committerPeter Mikus <pmikus@cisco.com>
Fri, 4 May 2018 11:46:26 +0000 (11:46 +0000)
+ Place the libraries into resources/libraries/python/search/.
 + Except OptimizedTrexSearch in TrafficGenerator.py
+ Change traffic generator to support floats for duration and warmup.
+ Remove explicit type conversions where not needed.
+ Add robot keywords to performance_utils.robot
 + for calling the optimized search.
 + for reporting the resulting values.
 + for checking the minimal performance has been reached.
 + for running five second "Traffic should pass with no loss" after the search.
- Add methodology documentation in subsequent Change.
- Add simulator for testing algorithm correctness in a subsequent Change.
- Add tests using the libraries in subsequent Change.

Change-Id: Ia041008382ee4c9a562172099aea794c854d5f2f
Signed-off-by: Vratko Polak <vrpolak@cisco.com>
resources/libraries/python/TrafficGenerator.py
resources/libraries/python/search/AbstractRateProvider.py [new file with mode: 0644]
resources/libraries/python/search/AbstractSearchAlgorithm.py [new file with mode: 0644]
resources/libraries/python/search/NdrPdrResult.py [new file with mode: 0644]
resources/libraries/python/search/OptimizedSearchAlgorithm.py [new file with mode: 0644]
resources/libraries/python/search/ReceiveRateInterval.py [new file with mode: 0644]
resources/libraries/python/search/ReceiveRateMeasurement.py [new file with mode: 0644]
resources/libraries/python/search/__init__.py [new file with mode: 0644]
resources/libraries/robot/performance/performance_utils.robot
resources/tools/trex/trex_stateless_profile.py

index 4da1c87..aa839dd 100644 (file)
 from robot.api import logger
 from robot.libraries.BuiltIn import BuiltIn
 
-from resources.libraries.python.constants import Constants
-from resources.libraries.python.ssh import SSH
-from resources.libraries.python.topology import NodeType
-from resources.libraries.python.topology import NodeSubTypeTG
-from resources.libraries.python.topology import Topology
-from resources.libraries.python.DropRateSearch import DropRateSearch
+from .DropRateSearch import DropRateSearch
+from .constants import Constants
+from .ssh import SSH
+from .topology import NodeType
+from .topology import NodeSubTypeTG
+from .topology import Topology
+from .search.AbstractRateProvider import AbstractRateProvider
+from .search.OptimizedSearchAlgorithm import OptimizedSearchAlgorithm
+from .search.ReceiveRateMeasurement import ReceiveRateMeasurement
 
-__all__ = ['TrafficGenerator', 'TGDropRateSearchImpl']
+__all__ = ['TGDropRateSearchImpl', 'TrafficGenerator', 'OptimizedSearch']
 
 
 class TGDropRateSearchImpl(DropRateSearch):
@@ -40,9 +43,10 @@ class TGDropRateSearchImpl(DropRateSearch):
         :param frame_size: Size of frame.
         :param loss_acceptance: Permitted drop ratio or frames count.
         :param loss_acceptance_type: Type of permitted loss.
-        :param traffic_type: Traffic profile ([2,3]-node-L[2,3], ...).
+        :param traffic_type: Module name as a traffic type identifier.
+            See resources/traffic_profiles/trex for implemented modules.
         :param skip_warmup: Start TRex without warmup traffic if true.
-        :type rate: int
+        :type rate: float
         :type frame_size: str
         :type loss_acceptance: float
         :type loss_acceptance_type: LossAcceptanceType
@@ -66,7 +70,7 @@ class TGDropRateSearchImpl(DropRateSearch):
                 tg_instance.trex_stl_start_remote_exec(self.get_duration(),
                                                        unit_rate, frame_size,
                                                        traffic_type,
-                                                       warmup_time=0)
+                                                       warmup_time=0.0)
             else:
                 tg_instance.trex_stl_start_remote_exec(self.get_duration(),
                                                        unit_rate, frame_size,
@@ -97,8 +101,13 @@ class TGDropRateSearchImpl(DropRateSearch):
         return tg_instance.get_latency_int()
 
 
-class TrafficGenerator(object):
-    """Traffic Generator."""
+class TrafficGenerator(AbstractRateProvider):
+    """Traffic Generator.
+
+    FIXME: Describe API."""
+
+    # TODO: Decrease friction between various search and rate provider APIs.
+    # FIXME: Remove "trex" from lines which could work with other TGs.
 
     # use one instance of TrafficGenerator for all tests in test suite
     ROBOT_LIBRARY_SCOPE = 'TEST SUITE'
@@ -112,6 +121,10 @@ class TrafficGenerator(object):
         self._node = None
         # T-REX interface order mapping
         self._ifaces_reordered = False
+        # Parameters not given by measure().
+        self.frame_size = None
+        self.traffic_type = None
+        self.warmup_time = None
 
     @property
     def node(self):
@@ -374,23 +387,24 @@ class TrafficGenerator(object):
 
     def trex_stl_start_remote_exec(self, duration, rate, framesize,
                                    traffic_type, async_call=False,
-                                   latency=True, warmup_time=5):
+                                   latency=True, warmup_time=5.0):
         """Execute script on remote node over ssh to start traffic.
 
         :param duration: Time expresed in seconds for how long to send traffic.
         :param rate: Traffic rate expressed with units (pps, %)
         :param framesize: L2 frame size to send (without padding and IPG).
-        :param traffic_type: Traffic profile.
+        :param traffic_type: Module name as a traffic type identifier.
+            See resources/traffic_profiles/trex for implemented modules.
         :param async_call: If enabled then don't wait for all incomming trafic.
         :param latency: With latency measurement.
         :param warmup_time: Warmup time period.
-        :type duration: int
+        :type duration: float
         :type rate: str
         :type framesize: str
         :type traffic_type: str
         :type async_call: bool
         :type latency: bool
-        :type warmup_time: int
+        :type warmup_time: float
         :returns: Nothing
         :raises RuntimeError: In case of TG driver issue.
         """
@@ -418,7 +432,7 @@ class TrafficGenerator(object):
             "{9}'".  # --latency
             format(Constants.REMOTE_FW_DIR, profile_path, duration, framesize,
                    rate, warmup_time, _p0 - 1, _p1 - 1, _async, _latency),
-            timeout=int(duration) + 60)
+            timeout=float(duration) + 60)
 
         if int(ret) != 0:
             raise RuntimeError('TRex stateless runtime error')
@@ -462,7 +476,8 @@ class TrafficGenerator(object):
         :param duration: Duration of test traffic generation in seconds.
         :param rate: Offered load per interface (e.g. 1%, 3gbps, 4mpps, ...).
         :param framesize: Frame size (L2) in Bytes.
-        :param traffic_type: Traffic profile.
+        :param traffic_type: Module name as a traffic type identifier.
+            See resources/traffic_profiles/trex for implemented modules.
         :param warmup_time: Warmup phase in seconds.
         :param async_call: Async mode.
         :param latency: With latency measurement.
@@ -470,7 +485,7 @@ class TrafficGenerator(object):
         :type rate: str
         :type framesize: str
         :type traffic_type: str
-        :type warmup_time: int
+        :type warmup_time: float
         :type async_call: bool
         :type latency: bool
         :returns: TG output.
@@ -490,7 +505,7 @@ class TrafficGenerator(object):
         if node['subtype'] is None:
             raise RuntimeError('TG subtype not defined')
         elif node['subtype'] == NodeSubTypeTG.TREX:
-            self.trex_stl_start_remote_exec(int(duration), rate, framesize,
+            self.trex_stl_start_remote_exec(duration, rate, framesize,
                                             traffic_type, async_call, latency,
                                             warmup_time=warmup_time)
         else:
@@ -533,3 +548,102 @@ class TrafficGenerator(object):
         if loss > float(loss_acceptance):
             raise Exception("Traffic loss {} above loss acceptance: {}".format(
                 loss, loss_acceptance))
+
+    def set_rate_provider_defaults(self, frame_size, traffic_type,
+                                   warmup_time=0.0):
+        """Store values accessed by measure().
+
+        :param frame_size: Frame size identifier or value [B].
+        :param traffic_type: Module name as a traffic type identifier.
+            See resources/traffic_profiles/trex for implemented modules.
+        :param warmup_time: Traffic duration before measurement starts [s].
+        :type frame_size: str or int
+        :type traffic_type: str
+        :type warmup_time: float
+        """
+        self.frame_size = frame_size
+        self.traffic_type = str(traffic_type)
+        self.warmup_time = float(warmup_time)
+
+    def measure(self, duration, transmit_rate):
+        """Run bi-directional measurement, parse and return results.
+
+        :param duration: Trial duration [s].
+        :param transmit_rate: Target bidirectional transmit rate [pps].
+        :type duration: float
+        :type transmit_rate: float
+        :returns: Structure containing the result of the measurement.
+        :rtype: ReceiveRateMeasurement
+        :raises RuntimeError: If TG is not set, or if node is not TG,
+            or if subtype is not specified.
+        :raises NotImplementedError: If TG is not supported.
+        """
+        duration = float(duration)
+        transmit_rate = float(transmit_rate)
+        # Trex needs target Tr per stream, but reports aggregate Tx and Dx.
+        unit_rate = str(transmit_rate / 2.0) + "pps"
+        self.send_traffic_on_tg(
+            duration, unit_rate, self.frame_size, self.traffic_type,
+            self.warmup_time, latency=True)
+        transmit_count = int(self.get_sent())
+        drop_count = int(self.get_loss())
+        measurement = ReceiveRateMeasurement(
+            duration, transmit_rate, transmit_count, drop_count)
+        measurement.latency = self.get_latency_int()
+        return measurement
+
+
+class OptimizedSearch(object):
+    """Class to be imported as Robot Library, containing a single keyword."""
+
+    @staticmethod
+    def perform_optimized_ndrpdr_search(
+            frame_size, traffic_type, fail_rate, line_rate,
+            allowed_drop_fraction=0.005, final_relative_width=0.005,
+            final_trial_duration=30.0, initial_trial_duration=1.0,
+            intermediate_phases=2, timeout=600.0):
+        """Setup initialized TG, perform optimized search, return intervals.
+
+        :param frame_size: Frame size identifier or value [B].
+        :param traffic_type: Module name as a traffic type identifier.
+            See resources/traffic_profiles/trex for implemented modules.
+        :param fail_rate: Minimal target transmit rate [pps].
+        :param line_rate: Maximal target transmit rate [pps].
+        :param allowed_drop_fraction: Fraction of dropped packets for PDR [1].
+        :param final_relative_width: Final lower bound transmit rate
+            cannot be more distant that this multiple of upper bound [1].
+        :param final_trial_duration: Trial duration for the final phase [s].
+        :param initial_trial_duration: Trial duration for the initial phase
+            and also for the first intermediate phase [s].
+        :param intermediate_phases: Number of intermediate phases to perform
+            before the final phase [1].
+        :param timeout: The search will fail itself when not finished
+            before this overall time [s].
+        :type frame_size: str or int
+        :type traffic_type: str
+        :type fail_rate: float
+        :type line_rate: float
+        :type allowed_drop_fraction: float
+        :type final_relative_width: float
+        :type final_trial_duration: float
+        :type initial_trial_duration: float
+        :type intermediate_phases: int
+        :type timeout: float
+        :returns: Structure containing narrowed down intervals
+            and their measurements.
+        :rtype: NdrPdrResult
+        :raises RuntimeError: If total duration is larger than timeout.
+        """
+        # we need instance of TrafficGenerator instantiated by Robot Framework
+        # to be able to use trex_stl-*()
+        tg_instance = BuiltIn().get_library_instance(
+            'resources.libraries.python.TrafficGenerator')
+        tg_instance.set_rate_provider_defaults(frame_size, traffic_type)
+        algorithm = OptimizedSearchAlgorithm(
+            tg_instance, final_trial_duration=final_trial_duration,
+            final_relative_width=final_relative_width,
+            intermediate_phases=intermediate_phases,
+            initial_trial_duration=initial_trial_duration, timeout=timeout)
+        result = algorithm.narrow_down_ndr_and_pdr(
+            fail_rate, line_rate, allowed_drop_fraction)
+        return result
diff --git a/resources/libraries/python/search/AbstractRateProvider.py b/resources/libraries/python/search/AbstractRateProvider.py
new file mode 100644 (file)
index 0000000..125c2af
--- /dev/null
@@ -0,0 +1,35 @@
+# Copyright (c) 2018 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 AbstractRateProvider class."""
+
+from abc import ABCMeta, abstractmethod
+
+
+class AbstractRateProvider(object):
+    """Abstract class defining common API for rate providers."""
+
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def measure(self, duration, transmit_rate):
+        """Perform trial measurement and return the result.
+
+        :param duration: Trial duration [s].
+        :param transmit_rate: Target transmit rate [pps].
+        :type duration: float
+        :type transmit_rate: float
+        :returns: Structure containing the result of the measurement.
+        :rtype: ReceiveRateMeasurement
+        """
+        pass
diff --git a/resources/libraries/python/search/AbstractSearchAlgorithm.py b/resources/libraries/python/search/AbstractSearchAlgorithm.py
new file mode 100644 (file)
index 0000000..ae326bd
--- /dev/null
@@ -0,0 +1,49 @@
+# Copyright (c) 2018 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 AbstractSearchAlgorithm class."""
+
+from abc import ABCMeta, abstractmethod
+
+
+class AbstractSearchAlgorithm(object):
+    """Abstract class defining common API for search algorithms."""
+
+    __metaclass__ = ABCMeta
+
+    def __init__(self, rate_provider):
+        """Store the rate provider.
+
+        :param rate_provider: Object able to perform trial measurements.
+        :type rate_provider: AbstractRateProvider
+        """
+        # TODO: Type check for AbstractRateProvider?
+        self.rate_provider = rate_provider
+
+    @abstractmethod
+    def narrow_down_ndr_and_pdr(
+            self, fail_rate, line_rate, allowed_drop_fraction):
+        """Perform measurements to narrow down intervals, return them.
+
+        :param fail_rate: Minimal target transmit rate [pps].
+        :param line_rate: Maximal target transmit rate [pps].
+        :param allowed_drop_fraction: Fraction of dropped packets for PDR [1].
+        :type fail_rate: float
+        :type line_rate: float
+        :type allowed_drop_fraction: float
+        :returns: Structure containing narrowed down intervals
+            and their measurements.
+        :rtype: NdrPdrResult
+        """
+        # TODO: Do we agree on arguments related to precision or trial duration?
+        pass
diff --git a/resources/libraries/python/search/NdrPdrResult.py b/resources/libraries/python/search/NdrPdrResult.py
new file mode 100644 (file)
index 0000000..47fb757
--- /dev/null
@@ -0,0 +1,65 @@
+# Copyright (c) 2018 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 NdrPdrResult class."""
+
+from .ReceiveRateInterval import ReceiveRateInterval
+
+
+class NdrPdrResult(object):
+    """Two measurement intervals, return value of search algorithms.
+
+    Partial fraction is NOT part of the result. Pdr interval should be valid
+    for all partial fractions implied by the interval."""
+
+    def __init__(self, ndr_interval, pdr_interval):
+        """Store the measured intervals after checking argument types.
+
+        :param ndr_interval: Object containing data for NDR part of the result.
+        :param pdr_interval: Object containing data for PDR part of the result.
+        :type ndr_interval: ReceiveRateInterval
+        :type pdr_interval: ReceiveRateInterval
+        """
+        # TODO: Type checking is not very pythonic,
+        # perhaps users can fix wrong usage without it?
+        if not isinstance(ndr_interval, ReceiveRateInterval):
+            raise TypeError("ndr_interval, is not a ReceiveRateInterval: "
+                            "{ndr!r}".format(ndr=ndr_interval))
+        if not isinstance(pdr_interval, ReceiveRateInterval):
+            raise TypeError("pdr_interval, is not a ReceiveRateInterval: "
+                            "{pdr!r}".format(pdr=pdr_interval))
+        self.ndr_interval = ndr_interval
+        self.pdr_interval = pdr_interval
+
+    def width_in_goals(self, relative_width_goal):
+        """Return a debug string related to current widths in logarithmic scale.
+
+        :param relative_width_goal: Upper bound times this is the goal
+            difference between upper bound and lower bound.
+        :type relative_width_goal: float
+        :returns: Message containing NDR and PDR widths in goals.
+        :rtype: str
+        """
+        return "ndr {ndr_in_goals}; pdr {pdr_in_goals}".format(
+            ndr_in_goals=self.ndr_interval.width_in_goals(relative_width_goal),
+            pdr_in_goals=self.pdr_interval.width_in_goals(relative_width_goal))
+
+    def __str__(self):
+        """Return string as tuple of named values."""
+        return "NDR={ndr!s};PDR={pdr!s}".format(
+            ndr=self.ndr_interval, pdr=self.pdr_interval)
+
+    def __repr__(self):
+        """Return string evaluable as a constructor call."""
+        return "NdrPdrResult(ndr_interval={ndr!r},pdr_interval={pdr!r})".format(
+            ndr=self.ndr_interval, pdr=self.pdr_interval)
diff --git a/resources/libraries/python/search/OptimizedSearchAlgorithm.py b/resources/libraries/python/search/OptimizedSearchAlgorithm.py
new file mode 100644 (file)
index 0000000..43559e0
--- /dev/null
@@ -0,0 +1,480 @@
+# Copyright (c) 2018 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 OptimizedSearchAlgorithm class."""
+
+import logging
+import math
+import time
+
+from .AbstractSearchAlgorithm import AbstractSearchAlgorithm
+from .NdrPdrResult import NdrPdrResult
+from .ReceiveRateInterval import ReceiveRateInterval
+
+
+class OptimizedSearchAlgorithm(AbstractSearchAlgorithm):
+    """Optimized binary search algorithm for finding NDR and PDR bounds.
+
+    Traditional binary search algorithm needs initial interval
+    (lower and upper bound), and returns final interval after bisecting
+    (until some exit condition is met).
+    The exit condition is usually related to the interval width,
+    (upper bound value minus lower bound value).
+
+    The optimized algorithm contains several improvements
+    aimed to reduce overall search time.
+
+    One improvement is searching for two intervals at once.
+    The intervals are for NDR (No Drop Rate) and PDR (Partial Drop Rate).
+
+    Next improvement is that the initial interval does need to be valid.
+    Imagine initial interval (10, 11) where 11 is smaller
+    than the searched value.
+    The algorithm will try (11, 13) interval next, and if 13 is still smaller,
+    (13, 17) and so on, doubling width until the upper bound is valid.
+    The part when interval expands is called external search,
+    the part when interval is bisected is called internal search.
+
+    Next improvement is that trial measurements at small trial duration
+    can be used to find a reasonable interval for full trial duration search.
+    This results in more trials performed, but smaller overall duration
+    in general.
+
+    Next improvement is bisecting in logarithmic quantities,
+    so that exit criteria can be independent of measurement units.
+
+    Next improvement is basing the initial interval on receive rates.
+
+    Final improvement is exiting early if the minimal value
+    is not a valid lower bound.
+
+    The complete search consist of several phases,
+    each phase performing several trial measurements.
+    Initial phase creates initial interval based on receive rates
+    at maximum rate and at maximum receive rate (MRR).
+    Final phase and preceding intermediate phases are performing
+    external and internal search steps,
+    each resulting interval is the starting point for the next phase.
+    The resulting interval of final phase is the result of the whole algorithm.
+
+    Each non-initial phase uses its own trial duration and width goal.
+    Any non-initial phase stops searching (for NDR or PDR independently)
+    when minimum is not a valid lower bound (at current duration),
+    or all of the following is true:
+    Both bounds are valid, bound bounds are measured at the current phase
+    trial duration, interval width is less than the width goal
+    for current phase."""
+
+    class ProgressState(object):
+        """Structure containing data to be passed around in recursion."""
+
+        def __init__(self, result, phases, duration, width_goal,
+                     allowed_drop_fraction, fail_rate, line_rate):
+            """Convert and store the argument values.
+
+            :param result: Current measured NDR and PDR intervals.
+            :param phases: How many intermediate phases to perform
+                before the current one.
+            :param duration: Trial duration to use in the current phase [s].
+            :param width_goal: The goal relative width for the curreent phase.
+            :param allowed_drop_fraction: PDR fraction for the current search.
+            :param fail_rate: Minimum target transmit rate
+                for the current search [pps].
+            :param line_rate: Maximum target transmit rate
+                for the current search [pps].
+            :type result: NdrPdrResult
+            :type phases: int
+            :type duration: float
+            :type width_goal: float
+            :type allowed_drop_fraction: float
+            :type fail_rate: float
+            :type line_rate: float
+            """
+            self.result = result
+            self.phases = int(phases)
+            self.duration = float(duration)
+            self.width_goal = float(width_goal)
+            self.allowed_drop_fraction = float(allowed_drop_fraction)
+            self.fail_rate = float(fail_rate)
+            self.line_rate = float(line_rate)
+
+    def __init__(self, rate_provider, final_relative_width=0.005,
+                 final_trial_duration=30.0, initial_trial_duration=1.0,
+                 intermediate_phases=2, timeout=600.0):
+        """Store rate provider and additional arguments.
+
+        :param rate_provider: Rate provider to use by this search object.
+        :param final_relative_width: Final lower bound transmit rate
+            cannot be more distant that this multiple of upper bound [1].
+        :param final_trial_duration: Trial duration for the final phase [s].
+        :param initial_trial_duration: Trial duration for the initial phase
+            and also for the first intermediate phase [s].
+        :param intermediate_phases: Number of intermediate phases to perform
+            before the final phase [1].
+        :param timeout: The search will fail itself when not finished
+            before this overall time [s].
+        :type rate_provider: AbstractRateProvider
+        :type final_relative_width: float
+        :type final_trial_duration: float
+        :type initial_trial_duration: int
+        :type intermediate_phases: int
+        :type timeout: float
+        """
+        super(OptimizedSearchAlgorithm, self).__init__(rate_provider)
+        self.final_trial_duration = float(final_trial_duration)
+        self.final_relative_width = float(final_relative_width)
+        self.intermediate_phases = int(intermediate_phases)
+        self.initial_trial_duration = float(initial_trial_duration)
+        self.timeout = float(timeout)
+
+    def narrow_down_ndr_and_pdr(
+            self, fail_rate, line_rate, allowed_drop_fraction):
+        """Perform initial phase, create state object, proceed with next phases.
+
+        :param fail_rate: Minimal target transmit rate [pps].
+        :param line_rate: Maximal target transmit rate [pps].
+        :param allowed_drop_fraction: Fraction of dropped packets for PDR [1].
+        :type fail_rate: float
+        :type line_rate: float
+        :type allowed_drop_fraction: float
+        :returns: Structure containing narrowed down intervals
+            and their measurements.
+        :rtype: NdrPdrResult
+        :raises RuntimeError: If total duration is larger than timeout.
+        """
+        fail_rate = float(fail_rate)
+        line_rate = float(line_rate)
+        allowed_drop_fraction = float(allowed_drop_fraction)
+        line_measurement = self.rate_provider.measure(
+            self.initial_trial_duration, line_rate)
+        # 0.999 is to avoid rounding errors which make
+        # the subsequent logic think the width is too broad.
+        max_lo = max(
+            fail_rate, line_rate * (1.0 - 0.999 * self.final_relative_width))
+        mrr = min(max_lo, max(fail_rate, line_measurement.receive_rate))
+        mrr_measurement = self.rate_provider.measure(
+            self.initial_trial_duration, mrr)
+        # Attempt to get narrower width.
+        max2_lo = max(
+            fail_rate, mrr * (1.0 - 0.999 * self.final_relative_width))
+        mrr2 = min(max2_lo, mrr_measurement.receive_rate)
+        if mrr2 > fail_rate:
+            line_measurement = mrr_measurement
+            mrr_measurement = self.rate_provider.measure(
+                self.initial_trial_duration, mrr2)
+        starting_interval = ReceiveRateInterval(
+            mrr_measurement, line_measurement)
+        starting_result = NdrPdrResult(starting_interval, starting_interval)
+        state = self.ProgressState(
+            starting_result, self.intermediate_phases,
+            self.final_trial_duration, self.final_relative_width,
+            allowed_drop_fraction, fail_rate, line_rate)
+        state = self.ndrpdr(state)
+        return state.result
+
+    def _measure_and_update_state(self, state, transmit_rate):
+        """Perform trial measurement, update bounds, return new state.
+
+        :param state: State before this measurement.
+        :param transmit_rate: Target transmit rate for this measurement [pps].
+        :type state: ProgressState
+        :type transmit_rate: float
+        :returns: State after the measurement.
+        :rtype: ProgressState
+        """
+        # TODO: Implement https://stackoverflow.com/a/24683360
+        # to avoid the string manipulation if log verbosity is too low.
+        logging.info("result before update: %s", state.result)
+        logging.debug(
+            "relative widths in goals: %s", state.result.width_in_goals(
+                self.final_relative_width))
+        measurement = self.rate_provider.measure(state.duration, transmit_rate)
+        ndr_interval = self._new_interval(
+            state.result.ndr_interval, measurement, 0.0)
+        pdr_interval = self._new_interval(
+            state.result.pdr_interval, measurement, state.allowed_drop_fraction)
+        state.result = NdrPdrResult(ndr_interval, pdr_interval)
+        return state
+
+    @staticmethod
+    def _new_interval(old_interval, measurement, allowed_drop_fraction):
+        """Return new interval with bounds updated according to the measurement.
+
+        :param old_interval: The current interval before the measurement.
+        :param measurement: The new meaqsurement to take into account.
+        :param allowed_drop_fraction: Fraction for PDR (or zero for NDR).
+        :type old_interval: ReceiveRateInterval
+        :type measurement: ReceiveRateMeasurement
+        :type allowed_drop_fraction: float
+        :returns: The updated interval.
+        :rtype: ReceiveRateInterval
+        """
+        old_lo, old_hi = old_interval.measured_low, old_interval.measured_high
+        # Priority zero: direct replace if the target Tr is the same.
+        if measurement.target_tr in (old_lo.target_tr, old_hi.target_tr):
+            if measurement.target_tr == old_lo.target_tr:
+                return ReceiveRateInterval(measurement, old_hi)
+            else:
+                return ReceiveRateInterval(old_lo, measurement)
+        # Priority one: invalid lower bound allows only one type of update.
+        if old_lo.drop_fraction > allowed_drop_fraction:
+            # We can only expand down, old bound becomes valid upper one.
+            if measurement.target_tr < old_lo.target_tr:
+                return ReceiveRateInterval(measurement, old_lo)
+            else:
+                return old_interval
+        # Lower bound is now valid.
+        # Next priorities depend on target Tr.
+        if measurement.target_tr < old_lo.target_tr:
+            # Lower external measurement, relevant only
+            # if the new measurement has high drop rate.
+            if measurement.drop_fraction > allowed_drop_fraction:
+                # Returning the broader interval as old_lo
+                # would be invalid upper bound.
+                return ReceiveRateInterval(measurement, old_hi)
+        elif measurement.target_tr > old_hi.target_tr:
+            # Upper external measurement, only relevant for invalid upper bound.
+            if old_hi.drop_fraction <= allowed_drop_fraction:
+                # Old upper bound becomes valid new lower bound.
+                return ReceiveRateInterval(old_hi, measurement)
+        else:
+            # Internal measurement, replaced boundary
+            # depends on measured drop fraction.
+            if measurement.drop_fraction > allowed_drop_fraction:
+                # We have found a narrow valid interval,
+                # regardless of whether old upper bound was valid.
+                return ReceiveRateInterval(old_lo, measurement)
+            else:
+                # In ideal world, we would not want to shrink interval
+                # if upper bound is not valid.
+                # In the real world, we want to shrink it for
+                # "invalid upper bound at line rate" case.
+                return ReceiveRateInterval(measurement, old_hi)
+        # Fallback, the interval is unchanged by the measurement.
+        return old_interval
+
+    @staticmethod
+    def double_relative_width(relative_width):
+        """Return relative width corresponding to double logarithmic width.
+
+        :param relative_width: The base relative width to double.
+        :type relative_width: float
+        :returns: The relative width of double logarithmic size.
+        :rtype: float
+        """
+        return 1.999 * relative_width - relative_width * relative_width
+        # The number should be 2.0, but we want to avoid rounding errors,
+        # and ensure half of double is not larger than the original value.
+
+    @staticmethod
+    def double_step_down(relative_width, current_bound):
+        """Return rate of double logarithmic width below.
+
+        :param relative_width: The base relative width to double.
+        :param current_bound: The current target transmit rate to move [pps].
+        :type relative_width: float
+        :type current_bound: float
+        :returns: Transmit rate smaller by logarithmically double width [pps].
+        :rtype: float
+        """
+        return current_bound * (
+            1.0 - OptimizedSearchAlgorithm.double_relative_width(
+                relative_width))
+
+    @staticmethod
+    def double_step_up(relative_width, current_bound):
+        """Return rate of double logarithmic width above.
+
+        :param relative_width: The base relative width to double.
+        :param current_bound: The current target transmit rate to move [pps].
+        :type relative_width: float
+        :type current_bound: float
+        :returns: Transmit rate larger by logarithmically double width [pps].
+        :rtype: float
+        """
+        return current_bound / (
+            1.0 - OptimizedSearchAlgorithm.double_relative_width(
+                relative_width))
+
+    @staticmethod
+    def half_relative_width(relative_width):
+        """Return relative width corresponding to half logarithmic width.
+
+        :param relative_width: The base relative width to halve.
+        :type relative_width: float
+        :returns: The relative width of half logarithmic size.
+        :rtype: float
+        """
+        return 1.0 - math.sqrt(1.0 - relative_width)
+
+    @staticmethod
+    def half_step_up(relative_width, current_bound):
+        """Return rate of half logarithmic width above.
+
+        :param relative_width: The base relative width to halve.
+        :param current_bound: The current target transmit rate to move [pps].
+        :type relative_width: float
+        :type current_bound: float
+        :returns: Transmit rate larger by logarithmically half width [pps].
+        :rtype: float
+        """
+        return current_bound / (
+            1.0 - OptimizedSearchAlgorithm.half_relative_width(relative_width))
+
+    def ndrpdr(self, state):
+        """Pefrom trials for this phase. Return the new state when done.
+
+        :param state: State before this phase.
+        :type state: ProgressState
+        :returns: The updates state.
+        :rtype: ProgressState
+        :raises RuntimeError: If total duration is larger than timeout.
+        """
+        if state.phases > 0:
+            # We need to finish preceding intermediate phases first.
+            saved_phases = state.phases
+            state.phases -= 1
+            # Preceding phases have shorter duration.
+            saved_duration = state.duration
+            duration_multiplier = state.duration / self.initial_trial_duration
+            phase_exponent = float(state.phases) / saved_phases
+            state.duration = self.initial_trial_duration * math.pow(
+                duration_multiplier, phase_exponent)
+            # Shorter durations do not need that narrow widths.
+            saved_width = state.width_goal
+            state.width_goal = self.double_relative_width(state.width_goal)
+            # Recurse.
+            state = self.ndrpdr(state)
+            # Restore the state for current phase.
+            state.duration = saved_duration
+            state.width_goal = saved_width
+            state.phases = saved_phases  # Not needed, but just in case.
+        logging.info(
+            "starting iterations with duration %s and relative width goal %s",
+            state.duration, state.width_goal)
+        start_time = time.time()
+        while 1:
+            if time.time() > start_time + self.timeout:
+                raise RuntimeError("Optimized search takes too long.")
+            # Order of priorities: improper bounds (nl, pl, nh, ph),
+            # then narrowing relative Tr widths.
+            # Durations are not priorities yet,
+            # they will settle on their own hopefully.
+            ndr_lo = state.result.ndr_interval.measured_low
+            ndr_hi = state.result.ndr_interval.measured_high
+            pdr_lo = state.result.pdr_interval.measured_low
+            pdr_hi = state.result.pdr_interval.measured_high
+            ndr_rel_width = max(
+                state.width_goal, state.result.ndr_interval.rel_tr_width)
+            pdr_rel_width = max(
+                state.width_goal, state.result.pdr_interval.rel_tr_width)
+            # If we are hitting line or fail rate, we cannot shift,
+            # but we can re-measure.
+            if ndr_lo.drop_fraction > 0.0:
+                if ndr_lo.target_tr > state.fail_rate:
+                    new_tr = max(state.fail_rate, self.double_step_down(
+                        ndr_rel_width, ndr_lo.target_tr))
+                    logging.info("ndr lo external %s", new_tr)
+                    state = self._measure_and_update_state(state, new_tr)
+                    continue
+                elif ndr_lo.duration < state.duration:
+                    logging.info("ndr lo fail re-measure")
+                    state = self._measure_and_update_state(
+                        state, state.fail_rate)
+                    continue
+            if pdr_lo.drop_fraction > state.allowed_drop_fraction:
+                if pdr_lo.target_tr > state.fail_rate:
+                    new_tr = max(state.fail_rate, self.double_step_down(
+                        pdr_rel_width, pdr_lo.target_tr))
+                    logging.info("pdr lo external %s", new_tr)
+                    state = self._measure_and_update_state(state, new_tr)
+                    continue
+                elif pdr_lo.duration < state.duration:
+                    logging.info("pdr lo fail re-measure")
+                    state = self._measure_and_update_state(
+                        state, state.fail_rate)
+                    continue
+            if ndr_hi.drop_fraction <= 0.0:
+                if ndr_hi.target_tr < state.line_rate:
+                    new_tr = min(state.line_rate, self.double_step_up(
+                        ndr_rel_width, ndr_hi.target_tr))
+                    logging.info("ndr hi external %s", new_tr)
+                    state = self._measure_and_update_state(state, new_tr)
+                    continue
+                elif ndr_hi.duration < state.duration:
+                    logging.info("ndr hi line re-measure")
+                    state = self._measure_and_update_state(
+                        state, state.line_rate)
+                    continue
+            if pdr_hi.drop_fraction <= state.allowed_drop_fraction:
+                if pdr_hi.target_tr < state.line_rate:
+                    new_tr = min(state.line_rate, self.double_step_up(
+                        pdr_rel_width, pdr_hi.target_tr))
+                    logging.info("pdr hi external %s", new_tr)
+                    state = self._measure_and_update_state(state, new_tr)
+                    continue
+                elif pdr_hi.duration < state.duration:
+                    logging.info("ndr hi line re-measure")
+                    state = self._measure_and_update_state(
+                        state, state.line_rate)
+                    continue
+            # If we are hitting line_rate, it is still worth narrowing width,
+            # hoping large enough Df will happen.
+            # But if we are hitting fail rate (at current duration),
+            # no additional measurement will help with that,
+            # so we can stop narrowing in this phase.
+            if (ndr_lo.target_tr <= state.fail_rate
+                    and ndr_lo.drop_fraction > 0.0):
+                ndr_rel_width = 0.0
+            if (pdr_lo.target_tr <= state.fail_rate
+                    and pdr_lo.drop_fraction > state.allowed_drop_fraction):
+                pdr_rel_width = 0.0
+            if max(ndr_rel_width, pdr_rel_width) > state.width_goal:
+                # We have to narrow some width.
+                if ndr_rel_width >= pdr_rel_width:
+                    new_tr = self.half_step_up(ndr_rel_width, ndr_lo.target_tr)
+                    logging.info("Bisecting for NDR at %s", new_tr)
+                    state = self._measure_and_update_state(state, new_tr)
+                    continue
+                else:
+                    new_tr = self.half_step_up(pdr_rel_width, pdr_lo.target_tr)
+                    logging.info("Bisecting for PDR at %s", new_tr)
+                    state = self._measure_and_update_state(state, new_tr)
+                    continue
+            # We do not need to improve width, but there still might be
+            # some measurements with smaller duration.
+            # We need to re-measure with full duration, possibly
+            # creating invalid bounds to resolve (thus broadening width).
+            if ndr_lo.duration < state.duration:
+                logging.info("re-measuring NDR lower bound")
+                self._measure_and_update_state(state, ndr_lo.target_tr)
+                continue
+            if pdr_lo.duration < state.duration:
+                logging.info("re-measuring PDR lower bound")
+                self._measure_and_update_state(state, pdr_lo.target_tr)
+                continue
+            # Except when lower bounds have high Df, in that case
+            # we do not need to re-measure _upper_ bounds.
+            if ndr_hi.duration < state.duration and ndr_rel_width > 0.0:
+                logging.info("re-measuring NDR upper bound")
+                self._measure_and_update_state(state, ndr_hi.target_tr)
+                continue
+            if pdr_hi.duration < state.duration and pdr_rel_width > 0.0:
+                logging.info("re-measuring PDR upper bound")
+                self._measure_and_update_state(state, pdr_hi.target_tr)
+                continue
+            # Widths are narrow (or failing), bound measurements
+            # are long enough, we can return.
+            logging.info("phase done")
+            break
+        return state
diff --git a/resources/libraries/python/search/ReceiveRateInterval.py b/resources/libraries/python/search/ReceiveRateInterval.py
new file mode 100644 (file)
index 0000000..11f812d
--- /dev/null
@@ -0,0 +1,84 @@
+# Copyright (c) 2018 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 ReceiveRateInterval class."""
+
+import math
+
+from .ReceiveRateMeasurement import ReceiveRateMeasurement
+
+
+class ReceiveRateInterval(object):
+    """Structure defining two Rr measurements, and their relation."""
+
+    def __init__(self, measured_low, measured_high):
+        """Store the bound measurements after checking argument types.
+
+        :param measured_low: Measurement for the lower bound.
+        :param measured_high: Measurement for the upper bound.
+        :type measured_low: ReceiveRateMeasurement
+        :type measured_high: ReceiveRateMeasurement
+        """
+        # TODO: Type checking is not very pythonic,
+        # perhaps users can fix wrong usage without it?
+        if not isinstance(measured_low, ReceiveRateMeasurement):
+            raise TypeError("measured_low is not a ReceiveRateMeasurement: "
+                            "{low!r}".format(low=measured_low))
+        if not isinstance(measured_high, ReceiveRateMeasurement):
+            raise TypeError("measured_high is not a ReceiveRateMeasurement: "
+                            "{high!r}".format(high=measured_high))
+        self.measured_low = measured_low
+        self.measured_high = measured_high
+        # Declare secondary quantities to appease pylint.
+        self.abs_tr_width = None
+        """Absolute width of target transmit rate. Upper minus lower."""
+        self.rel_tr_width = None
+        """Relative width of target transmit rate. Ansolute divided by upper."""
+        self.sort()
+
+    def sort(self):
+        """Sort bounds by target Tr, compute secondary quantities."""
+        if self.measured_low.target_tr > self.measured_high.target_tr:
+            self.measured_low, self.measured_high = (
+                self.measured_high, self.measured_low)
+        self.abs_tr_width = (
+            self.measured_high.target_tr - self.measured_low.target_tr)
+        self.rel_tr_width = self.abs_tr_width / self.measured_high.target_tr
+
+    def width_in_goals(self, relative_width_goal):
+        """Return float value.
+
+        Relative width goal is some (negative) value on logarithmic scale.
+        Current relative width is another logarithmic value.
+        Return the latter divided by the former.
+        This is useful when investigating how did surprising widths come to be.
+
+        :param relative_width_goal: Upper bound times this is the goal
+            difference between upper bound and lower bound.
+        :type relative_width_goal: float
+        :returns: Current width as logarithmic multiple of goal width [1].
+        :rtype: float
+        """
+        return math.log(1.0 - self.rel_tr_width) / math.log(
+            1.0 - relative_width_goal)
+
+    def __str__(self):
+        """Return string as half-open interval."""
+        return "[{low!s};{high!s})".format(
+            low=self.measured_low, high=self.measured_high)
+
+    def __repr__(self):
+        """Return string evaluable as a constructor call."""
+        return ("ReceiveRateInterval(measured_low={low!r}"
+                ",measured_high={high!r})".format(
+                    low=self.measured_low, high=self.measured_high))
diff --git a/resources/libraries/python/search/ReceiveRateMeasurement.py b/resources/libraries/python/search/ReceiveRateMeasurement.py
new file mode 100644 (file)
index 0000000..58aa819
--- /dev/null
@@ -0,0 +1,54 @@
+# Copyright (c) 2018 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 ReceiveRateMeasurement class."""
+
+
+class ReceiveRateMeasurement(object):
+    """Structure defining the result of single Rr measurement."""
+
+    def __init__(self, duration, target_tr, transmit_count, drop_count):
+        """Constructor, normalize primary and compute secondary quantities.
+
+        :param duration: Measurement duration [s].
+        :param target_tr: Target transmit rate [pps].
+            If bidirectional traffic is measured, this is bidirectional rate.
+        :param transmit_count: Number of packets transmitted [1].
+        :param drop_count: Number of packets transmitted but not received [1].
+        :type duration: float
+        :type target_tr: float
+        :type transmit_count: int
+        :type drop_count: int
+        """
+        self.duration = float(duration)
+        self.target_tr = float(target_tr)
+        self.transmit_count = int(transmit_count)
+        self.drop_count = int(drop_count)
+        self.receive_count = transmit_count - drop_count
+        self.transmit_rate = transmit_count / self.duration
+        self.drop_rate = drop_count / self.duration
+        self.receive_rate = self.receive_count / self.duration
+        self.drop_fraction = float(self.drop_count) / self.transmit_count
+        # TODO: Do we want to store also the real time (duration + overhead)?
+
+    def __str__(self):
+        """Return string reporting input and drop fraction."""
+        return "d={dur!s},Tr={rate!s},Df={frac!s}".format(
+            dur=self.duration, rate=self.target_tr, frac=self.drop_fraction)
+
+    def __repr__(self):
+        """Return string evaluable as a constructor call."""
+        return ("ReceiveRateMeasurement(duration={dur!r},target_tr={rate!r}"
+                ",transmit_count={trans!r},drop_count={drop!r})".format(
+                    dur=self.duration, rate=self.target_tr,
+                    trans=self.transmit_count, drop=self.drop_count))
diff --git a/resources/libraries/python/search/__init__.py b/resources/libraries/python/search/__init__.py
new file mode 100644 (file)
index 0000000..619c4c0
--- /dev/null
@@ -0,0 +1,16 @@
+# Copyright (c) 2018 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 directory resources/libraries/python/search
+"""
index 4acaec9..38151cf 100644 (file)
@@ -20,6 +20,7 @@
 | Library | resources.libraries.python.KubernetesUtils
 | Library | resources.libraries.python.VhostUser
 | Library | resources.libraries.python.TrafficGenerator
+| Library | resources.libraries.python.TrafficGenerator.OptimizedSearch
 | Library | resources.libraries.python.TrafficGenerator.TGDropRateSearchImpl
 | Library | resources.libraries.python.Trace
 | Resource | resources/libraries/robot/shared/default.robot
 | | ... | ${framesize} | ${topology_type} | ${loss_acceptance}
 | | ... | ${loss_acceptance_type} | fail_on_loss=${False}
 
+| Find NDR and PDR intervals using optimized search
+| | [Documentation]
+| | ... | Find boundaries for RFC2544 compatible NDR and PDR values
+| | ... | using an optimized search algorithm.
+| | ... | Display results as formatted test message.
+| | ... | Fail if a resulting lower bound has too high drop fraction.
+| | ... | Proceed with Perform additional measurements based on NDRPDR result.
+| | ... | TODO: Should the trial duration of the additional
+| | ... | measurements be configurable?
+| | ...
+| | ... | *Arguments:*
+| | ... | - frame_size - L2 Frame Size [B] or IMIX string. Type: int or str
+| | ... | - topology_type - Topology type. Type: string
+| | ... | - fail_rate - Lower limit of search [pps]. Type: float
+| | ... | - line_rate - Upper limit of search [pps]. Type: float
+| | ... | - allowed_drop_fraction - Accepted loss during search. Type: float
+| | ... | - final_relative_width - Maximal width multiple of upper. Type: float
+| | ... | - final_trial_duration - Duration of final trials [s]. Type: float
+| | ... | - initial_trial_duration - Duration of initial trials [s]. Type: float
+| | ... | - intermediate phases - Number of intermediate phases [1]. Type: int
+| | ... | - timeout - Fail if search duration is longer [s]. Type: float
+| | ...
+| | ... | *Example:*
+| | ...
+| | ... | \| Find NDR and PDR intervals using optimized search \| \${64} \| \
+| | ... | 3-node-IPv4 \| \${100000} \| \${14880952} \| \${0.005} \| \${0.005} \
+| | ... | \| \${30.0} \| \${1.0} \| \${2} \| ${600.0}
+| | ...
+| | [Arguments] | ${frame_size} | ${topology_type} | ${fail_rate} | ${line_rate}
+| | ... | ${allowed_drop_fraction}=${0.005} | ${final_relative_width}=${0.005}
+| | ... | ${final_trial_duration}=${30.0} | ${initial_trial_duration}=${1.0}
+| | ... | ${intermediate_phases}=${2} | ${timeout}=${600.0}
+| | ...
+| | ${result}= | Perform optimized ndrpdr search | ${frame_size}
+| | ... | ${topology_type} | ${fail_rate} | ${line_rate}
+| | ... | ${allowed_drop_fraction} | ${final_relative_width}
+| | ... | ${final_trial_duration} | ${initial_trial_duration}
+| | ... | ${intermediate_phases} | timeout=${timeout}
+| | Display result of NDRPDR search | ${result} | ${frame_size}
+| | Check NDRPDR interval validity | ${result.pdr_interval}
+| | ... | ${allowed_drop_fraction}
+| | Check NDRPDR interval validity | ${result.ndr_interval}
+| | Perform additional measurements based on NDRPDR result
+| | ... | ${result} | ${frame_size} | ${topology_type}
+
+| Display single bound
+| | [Documentation]
+| | ... | Display one bound of NDR+PDR search,
+| | ... | in packet per seconds (total and per stream)
+| | ... | and Gbps total bandwidth with untagged packet.
+| | ... | Througput is calculated as:
+| | ... | Measured rate per stream * Total number of streams
+| | ... | Bandwidth is calculated as:
+| | ... | (Throughput * (L2 Frame Size + IPG) * 8) / Max bitrate of NIC
+| | ... | The given result should contain latency data as well.
+| | ...
+| | ... | *Arguments:*
+| | ... | - text - Flavor text describing which bound is this. Type: string
+| | ... | - rate_total - Total (not per stream) measured Tr [pps]. Type: float
+| | ... | - framesize - L2 Frame Size [B]. Type: integer
+| | ... | - latency - Latency data to display if non-empty. Type: string
+| | ...
+| | ... | *Example:*
+| | ...
+| | ... | \| Display single bound \| NDR lower bound \| \${12345.67} \
+| | ... | \| \${64} \| show_latency=\${EMPTY}
+| | ...
+| | [Arguments] | ${text} | ${rate_total} | ${framesize} | ${latency}=${EMPTY}
+| | ...
+| | ${bandwidth_total}= | Evaluate | ${rate_total}*(${framesize}+20)*8/(10**9)
+| | Set Test Message | ${\n}${text} | append=yes
+| | Set Test Message | ${\n}RATE: ${rate_total} pps | append=yes
+| | Set Test Message | ${\n}BANDWIDTH: ${bandwidth_total} Gbps (untagged)
+| | ... | append=yes
+| | Return From Keyword If | not """${latency}"""
+| | Set Test Message | ${\n}LATENCY usec [min/avg/max] per stream: ${latency}
+| | ... | append=yes
+
+| Display result of NDRPDR search
+| | [Documentation]
+| | ... | Display result of NDR+PDR search, both quantities, both bounds,
+| | ... | in packet per seconds (total and per stream)
+| | ... | and Gbps total bandwidth with untagged packet.
+| | ... | Througput is calculated as:
+| | ... | Measured rate per stream * Total number of streams
+| | ... | Bandwidth is calculated as:
+| | ... | (Throughput * (L2 Frame Size + IPG) * 8) / Max bitrate of NIC
+| | ... | The given result should contain latency data as well.
+| | ...
+| | ... | *Arguments:*
+| | ... | - result - Measured result data per stream [pps]. Type: NdrPdrResult
+| | ... | - framesize - L2 Frame Size [B]. Type: integer
+| | ...
+| | ... | *Example:*
+| | ...
+| | ... | \| Display result of NDRPDR search \| \${result} \| \${64}
+| | ...
+| | [Arguments] | ${result} | ${framesize}
+| | ...
+| | ${framesize}= | Get Frame Size | ${framesize}
+| | Set Test Message | NDR+PDR search results:
+| | Display single bound | NDR lower bound
+| | ... | ${result.ndr_interval.measured_low.transmit_rate} | ${framesize}
+| | ... | ${result.ndr_interval.measured_low.latency}
+| | Display single bound | NDR upper bound
+| | ... | ${result.ndr_interval.measured_high.transmit_rate} | ${framesize}
+| | Display single bound | PDR lower bound
+| | ... | ${result.pdr_interval.measured_low.transmit_rate} | ${framesize}
+| | ... | ${result.pdr_interval.measured_low.latency}
+| | Display single bound | PDR upper bound
+| | ... | ${result.pdr_interval.measured_high.transmit_rate} | ${framesize}
+
+| Check NDRPDR interval validity
+| | [Documentation]
+| | ... | Extract drop fraction of lower bound of the interval.
+| | ... | Fail if it does not reach the allowed value.
+| | ...
+| | ... | *Arguments:*
+| | ... | - interval - Measured interval. Type: ReceiveRateInterval
+| | ... | - allowed_drop_fraction - Accepted loss (0.0 for NDR). Type: float
+| | ...
+| | ... | *Example:*
+| | ...
+| | ... | \| Check NDRPDR interval validity \| \${result.pdr_interval} \
+| | ... | \| \${0.005}
+| | ...
+| | [Arguments] | ${interval} | ${allowed_drop_fraction}=${0.0}
+| | ...
+| | ${lower_bound_df}= | Set Variable | ${interval.measured_low.drop_fraction}
+| | Return From Keyword If | ${lower_bound_df} <= ${allowed_drop_fraction}
+| | ${messagge}= | Catenate | SEPARATOR=${SPACE}
+| | ... | Lower bound fraction ${lower_bound_df}
+| | ... | does not reach ${allowed_drop_fraction}.
+| | Fail | ${message}
+
+| Perform additional measurements based on NDRPDR result
+| | [Documentation]
+| | ... | Perform any additional measurements which are not directly needed
+| | ... | for determining NDR nor PDR, but which are needed for gathering
+| | ... | additional data for debug purposes.
+| | ... | Currently, just "Traffic should pass with no loss" is called.
+| | ... | TODO: Move latency measurements from optimized search here.
+| | ...
+| | ... | *Arguments:*
+| | ... | - result - Measured result data per stream [pps]. Type: NdrPdrResult
+| | ... | - frame_size - L2 Frame Size [B] or IMIX string. Type: int or str
+| | ... | - topology_type - Topology type. Type: string
+| | ...
+| | ... | *Example:*
+| | ... | \| Perform additional measurements based on NDRPDR result \
+| | ... | \| \${result} \| ${64} \| 3-node-IPv4
+| | ...
+| | [Arguments] | ${result} | ${framesize} | ${topology_type}
+| | ...
+| | ${duration}= | Set Variable | 5.0
+| | ${rate_per_stream}= | Evaluate
+| | ... | ${result.ndr_interval.measured_low.target_tr} / 2.0
+| | Traffic should pass with no loss | ${duration} | ${rate_per_stream}pps
+| | ... | ${framesize} | ${topology_type} | fail_on_loss=${False}
+
 | Display result of NDR search
 | | [Documentation]
 | | ... | Display result of NDR search in packet per seconds (total and per
index c792f3e..e1e56d9 100755 (executable)
@@ -87,9 +87,9 @@ def simple_burst(profile_file, duration, framesize, rate, warmup_time, port_0,
     :param async_start: Start the traffic and exit.
     :type profile_file: str
     :type framesize: int or str
-    :type duration: int
+    :type duration: float
     :type rate: str
-    :type warmup_time: int
+    :type warmup_time: float
     :type port_0: int
     :type port_1: int
     :type latency: boo;
@@ -247,7 +247,7 @@ def main():
                         help="Python traffic profile.")
     parser.add_argument("-d", "--duration",
                         required=True,
-                        type=int,
+                        type=float,
                         help="Duration of traffic run.")
     parser.add_argument("-s", "--frame_size",
                         required=True,
@@ -256,8 +256,8 @@ def main():
                         required=True,
                         help="Traffic rate with included units (%, pps).")
     parser.add_argument("-w", "--warmup_time",
-                        type=int,
-                        default=5,
+                        type=float,
+                        default=5.0,
                         help="Traffic warm-up time in seconds, 0 = disable.")
     parser.add_argument("--port_0",
                         required=True,
@@ -283,12 +283,12 @@ def main():
         framesize = args.frame_size
 
     simple_burst(profile_file=args.profile,
-                 duration=int(args.duration),
+                 duration=args.duration,
                  framesize=framesize,
                  rate=args.rate,
-                 warmup_time=int(args.warmup_time),
-                 port_0=int(args.port_0),
-                 port_1=int(args.port_1),
+                 warmup_time=args.warmup_time,
+                 port_0=args.port_0,
+                 port_1=args.port_1,
                  latency=args.latency,
                  async_start=args.async)