aboutsummaryrefslogtreecommitdiffstats
path: root/resources/libraries/python
diff options
context:
space:
mode:
authorVratko Polak <vrpolak@cisco.com>2018-04-27 15:11:12 +0200
committerPeter Mikus <pmikus@cisco.com>2018-05-04 11:46:26 +0000
commit35175924550d67f98fb1c2c50b0634656d29169e (patch)
treefe1008e80c9c6f09227dff84672bd0eaa831ff23 /resources/libraries/python
parent4bfd1dcd604dac12d1dd00ba63c0d5e4170a1f2b (diff)
CSIT-992: Add libraries for optimized search
+ 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>
Diffstat (limited to 'resources/libraries/python')
-rw-r--r--resources/libraries/python/TrafficGenerator.py154
-rw-r--r--resources/libraries/python/search/AbstractRateProvider.py35
-rw-r--r--resources/libraries/python/search/AbstractSearchAlgorithm.py49
-rw-r--r--resources/libraries/python/search/NdrPdrResult.py65
-rw-r--r--resources/libraries/python/search/OptimizedSearchAlgorithm.py480
-rw-r--r--resources/libraries/python/search/ReceiveRateInterval.py84
-rw-r--r--resources/libraries/python/search/ReceiveRateMeasurement.py54
-rw-r--r--resources/libraries/python/search/__init__.py16
8 files changed, 917 insertions, 20 deletions
diff --git a/resources/libraries/python/TrafficGenerator.py b/resources/libraries/python/TrafficGenerator.py
index 4da1c8766f..aa839dd80e 100644
--- a/resources/libraries/python/TrafficGenerator.py
+++ b/resources/libraries/python/TrafficGenerator.py
@@ -16,14 +16,17 @@
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
index 0000000000..125c2af8cc
--- /dev/null
+++ b/resources/libraries/python/search/AbstractRateProvider.py
@@ -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
index 0000000000..ae326bd4ca
--- /dev/null
+++ b/resources/libraries/python/search/AbstractSearchAlgorithm.py
@@ -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
index 0000000000..47fb757c07
--- /dev/null
+++ b/resources/libraries/python/search/NdrPdrResult.py
@@ -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
index 0000000000..43559e049b
--- /dev/null
+++ b/resources/libraries/python/search/OptimizedSearchAlgorithm.py
@@ -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
index 0000000000..11f812d886
--- /dev/null
+++ b/resources/libraries/python/search/ReceiveRateInterval.py
@@ -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
index 0000000000..58aa819017
--- /dev/null
+++ b/resources/libraries/python/search/ReceiveRateMeasurement.py
@@ -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
index 0000000000..619c4c09c5
--- /dev/null
+++ b/resources/libraries/python/search/__init__.py
@@ -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
+"""