diff options
Diffstat (limited to 'resources/libraries')
9 files changed, 1078 insertions, 20 deletions
diff --git a/resources/libraries/python/TrafficGenerator.py b/resources/libraries/python/TrafficGenerator.py index b7c8d6ef4e..9114f84ce1 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 +""" diff --git a/resources/libraries/robot/performance/performance_utils.robot b/resources/libraries/robot/performance/performance_utils.robot index 4acaec9c33..38151cf003 100644 --- a/resources/libraries/robot/performance/performance_utils.robot +++ b/resources/libraries/robot/performance/performance_utils.robot @@ -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 @@ -354,6 +355,166 @@ | | ... | ${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 |