diff options
Diffstat (limited to 'yardstick/network_services/helpers')
8 files changed, 1065 insertions, 12 deletions
diff --git a/yardstick/network_services/helpers/cpu.py b/yardstick/network_services/helpers/cpu.py index 8c21754ff..279af204a 100644 --- a/yardstick/network_services/helpers/cpu.py +++ b/yardstick/network_services/helpers/cpu.py @@ -15,11 +15,15 @@ import io +# Number of threads per core. +NR_OF_THREADS = 2 + class CpuSysCores(object): def __init__(self, connection=""): self.core_map = {} + self.cpuinfo = {} self.connection = connection def _open_cpuinfo(self): @@ -33,11 +37,11 @@ class CpuSysCores(object): core_lines = {} for line in lines: if line.strip(): - name, value = line.split(":", 1) - core_lines[name.strip()] = value.strip() + name, value = line.split(":", 1) + core_lines[name.strip()] = value.strip() else: - core_details.append(core_lines) - core_lines = {} + core_details.append(core_lines) + core_lines = {} return core_details @@ -51,7 +55,7 @@ class CpuSysCores(object): lines = self._open_cpuinfo() core_details = self._get_core_details(lines) for core in core_details: - for k, v in core.items(): + for k, _ in core.items(): if k == "physical id": if core["physical id"] not in self.core_map: self.core_map[core['physical id']] = [] @@ -60,6 +64,17 @@ class CpuSysCores(object): return self.core_map + def get_cpu_layout(self): + _, stdout, _ = self.connection.execute("lscpu -p") + self.cpuinfo = {} + self.cpuinfo['cpuinfo'] = list() + for line in stdout.split("\n"): + if line and line[0] != "#": + self.cpuinfo['cpuinfo'].append( + [CpuSysCores._str2int(x) for x in + line.split(",")]) + return self.cpuinfo + def validate_cpu_cfg(self, vnf_cfg=None): if vnf_cfg is None: vnf_cfg = { @@ -78,3 +93,81 @@ class CpuSysCores(object): return -1 return 0 + + def is_smt_enabled(self): + return CpuSysCores.smt_enabled(self.cpuinfo) + + def cpu_list_per_node(self, cpu_node, smt_used=False): + cpu_node = int(cpu_node) + cpu_info = self.cpuinfo.get("cpuinfo") + if cpu_info is None: + raise RuntimeError("Node cpuinfo not available.") + + smt_enabled = self.is_smt_enabled() + if not smt_enabled and smt_used: + raise RuntimeError("SMT is not enabled.") + + cpu_list = [] + for cpu in cpu_info: + if cpu[3] == cpu_node: + cpu_list.append(cpu[0]) + + if not smt_enabled or smt_enabled and smt_used: + pass + + if smt_enabled and not smt_used: + cpu_list_len = len(cpu_list) + cpu_list = cpu_list[:int(cpu_list_len / NR_OF_THREADS)] + + return cpu_list + + def cpu_slice_of_list_per_node(self, cpu_node, skip_cnt=0, cpu_cnt=0, + smt_used=False): + cpu_list = self.cpu_list_per_node(cpu_node, smt_used) + + cpu_list_len = len(cpu_list) + if cpu_cnt + skip_cnt > cpu_list_len: + raise RuntimeError("cpu_cnt + skip_cnt > length(cpu list).") + + if cpu_cnt == 0: + cpu_cnt = cpu_list_len - skip_cnt + + if smt_used: + cpu_list_0 = cpu_list[:int(cpu_list_len / NR_OF_THREADS)] + cpu_list_1 = cpu_list[int(cpu_list_len / NR_OF_THREADS):] + cpu_list = [cpu for cpu in cpu_list_0[skip_cnt:skip_cnt + cpu_cnt]] + cpu_list_ex = [cpu for cpu in + cpu_list_1[skip_cnt:skip_cnt + cpu_cnt]] + cpu_list.extend(cpu_list_ex) + else: + cpu_list = [cpu for cpu in cpu_list[skip_cnt:skip_cnt + cpu_cnt]] + + return cpu_list + + def cpu_list_per_node_str(self, cpu_node, skip_cnt=0, cpu_cnt=0, sep=",", + smt_used=False): + cpu_list = self.cpu_slice_of_list_per_node(cpu_node, + skip_cnt=skip_cnt, + cpu_cnt=cpu_cnt, + smt_used=smt_used) + return sep.join(str(cpu) for cpu in cpu_list) + + @staticmethod + def _str2int(string): + try: + return int(string) + except ValueError: + return 0 + + @staticmethod + def smt_enabled(cpuinfo): + cpu_info = cpuinfo.get("cpuinfo") + if cpu_info is None: + raise RuntimeError("Node cpuinfo not available.") + cpu_mems = [item[-4:] for item in cpu_info] + cpu_mems_len = int(len(cpu_mems) / NR_OF_THREADS) + count = 0 + for cpu_mem in cpu_mems[:cpu_mems_len]: + if cpu_mem in cpu_mems[cpu_mems_len:]: + count += 1 + return count == cpu_mems_len diff --git a/yardstick/network_services/helpers/dpdkbindnic_helper.py b/yardstick/network_services/helpers/dpdkbindnic_helper.py index 1c74355ef..33a5e8c1d 100644 --- a/yardstick/network_services/helpers/dpdkbindnic_helper.py +++ b/yardstick/network_services/helpers/dpdkbindnic_helper.py @@ -13,7 +13,6 @@ # limitations under the License. import logging import os - import re from collections import defaultdict from itertools import chain @@ -21,7 +20,6 @@ from itertools import chain from yardstick.common import exceptions from yardstick.common.utils import validate_non_string_sequence - NETWORK_KERNEL = 'network_kernel' NETWORK_DPDK = 'network_dpdk' NETWORK_OTHER = 'network_other' @@ -284,15 +282,22 @@ printf "%s/driver:" $1 ; basename $(readlink -s $1/device/driver); } \ res = self.ssh_helper.execute(*args, **kwargs) if res[0] != 0: template = '{} command failed with rc={}' + LOG.critical("DPDK_DEVBIND Failure %s", res[1]) raise DpdkBindHelperException(template.format(self.dpdk_devbind, res[0])) return res - def load_dpdk_driver(self): + def load_dpdk_driver(self, dpdk_driver=None): + if dpdk_driver is None: + dpdk_driver = self.dpdk_driver cmd_template = "sudo modprobe {} && sudo modprobe {}" - self.ssh_helper.execute(cmd_template.format(self.UIO_DRIVER, self.dpdk_driver)) - - def check_dpdk_driver(self): - return self.ssh_helper.execute("lsmod | grep -i {}".format(self.dpdk_driver))[0] + self.ssh_helper.execute( + cmd_template.format(self.UIO_DRIVER, dpdk_driver)) + + def check_dpdk_driver(self, dpdk_driver=None): + if dpdk_driver is None: + dpdk_driver = self.dpdk_driver + return \ + self.ssh_helper.execute("lsmod | grep -i {}".format(dpdk_driver))[0] @property def _status_cmd(self): diff --git a/yardstick/network_services/helpers/vpp_helpers/__init__.py b/yardstick/network_services/helpers/vpp_helpers/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/yardstick/network_services/helpers/vpp_helpers/__init__.py diff --git a/yardstick/network_services/helpers/vpp_helpers/abstract_search_algorithm.py b/yardstick/network_services/helpers/vpp_helpers/abstract_search_algorithm.py new file mode 100644 index 000000000..fced05833 --- /dev/null +++ b/yardstick/network_services/helpers/vpp_helpers/abstract_search_algorithm.py @@ -0,0 +1,53 @@ +# Copyright (c) 2019 Viosoft Corporation +# +# 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. +# +# This is a modified copy of +# https://gerrit.fd.io/r/gitweb?p=csit.git;a=blob_plain;f=resources/libraries/python/MLRsearch/AbstractSearchAlgorithm.py;hb=HEAD + + +from abc import ABCMeta, abstractmethod + + +class AbstractSearchAlgorithm(object): + """Abstract class defining common API for search algorithms.""" + + __metaclass__ = ABCMeta + + def __init__(self, measurer): + """Store the rate provider. + + :param measurer: Object able to perform trial or composite measurements. + :type measurer: AbstractMeasurer.AbstractMeasurer + """ + # TODO: Type check for AbstractMeasurer? + self.measurer = measurer + + @abstractmethod + def narrow_down_ndr_and_pdr( + self, fail_rate, line_rate, packet_loss_ratio): + """Perform measurements to narrow down intervals, return them. + + This will be renamed when custom loss ratio lists are supported. + + :param fail_rate: Minimal target transmit rate [pps]. + :param line_rate: Maximal target transmit rate [pps]. + :param packet_loss_ratio: Fraction of packets lost, for PDR [1]. + :type fail_rate: float + :type line_rate: float + :type packet_loss_ratio: float + :returns: Structure containing narrowed down intervals + and their measurements. + :rtype: NdrPdrResult.NdrPdrResult + """ + # TODO: Do we agree on arguments related to precision or trial duration? diff --git a/yardstick/network_services/helpers/vpp_helpers/multiple_loss_ratio_search.py b/yardstick/network_services/helpers/vpp_helpers/multiple_loss_ratio_search.py new file mode 100644 index 000000000..582e3dc27 --- /dev/null +++ b/yardstick/network_services/helpers/vpp_helpers/multiple_loss_ratio_search.py @@ -0,0 +1,688 @@ +# Copyright (c) 2019 Viosoft Corporation +# +# 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. +# +# This is a modified copy of +# https://gerrit.fd.io/r/gitweb?p=csit.git;a=blob_plain;f=resources/libraries/python/MLRsearch/MultipleLossRatioSearch.py;hb=HEAD + +import datetime +import logging +import math +import time + +from yardstick.network_services.helpers.vpp_helpers.abstract_search_algorithm import \ + AbstractSearchAlgorithm +from yardstick.network_services.helpers.vpp_helpers.ndr_pdr_result import \ + NdrPdrResult +from yardstick.network_services.helpers.vpp_helpers.receive_rate_interval import \ + ReceiveRateInterval +from yardstick.network_services.helpers.vpp_helpers.receive_rate_measurement import \ + ReceiveRateMeasurement + +LOGGING = logging.getLogger(__name__) + + +class MultipleLossRatioSearch(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 not 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. + + TODO: Review and update this docstring according to rst docs. + TODO: Support configurable number of Packet Loss Ratios. + """ + + class ProgressState(object): + """Structure containing data to be passed around in recursion.""" + + def __init__( + self, result, phases, duration, width_goal, packet_loss_ratio, + minimum_transmit_rate, maximum_transmit_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 packet_loss_ratio: PDR fraction for the current search. + :param minimum_transmit_rate: Minimum target transmit rate + for the current search [pps]. + :param maximum_transmit_rate: Maximum target transmit rate + for the current search [pps]. + :type result: NdrPdrResult.NdrPdrResult + :type phases: int + :type duration: float + :type width_goal: float + :type packet_loss_ratio: float + :type minimum_transmit_rate: float + :type maximum_transmit_rate: float + """ + self.result = result + self.phases = int(phases) + self.duration = float(duration) + self.width_goal = float(width_goal) + self.packet_loss_ratio = float(packet_loss_ratio) + self.minimum_transmit_rate = float(minimum_transmit_rate) + self.maximum_transmit_rate = float(maximum_transmit_rate) + + def __init__(self, measurer, latency=False, pkt_size=64, + final_relative_width=0.005, + final_trial_duration=30.0, initial_trial_duration=1.0, + number_of_intermediate_phases=2, timeout=600.0, doublings=1): + """Store the measurer object and additional arguments. + + :param measurer: 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 number_of_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]. + :param doublings: How many doublings to do in external search step. + Default 1 is suitable for fairly stable tests, + less stable tests might get better overal duration with 2 or more. + :type measurer: AbstractMeasurer.AbstractMeasurer + :type final_relative_width: float + :type final_trial_duration: float + :type initial_trial_duration: int + :type number_of_intermediate_phases: int + :type timeout: float + :type doublings: int + """ + super(MultipleLossRatioSearch, self).__init__(measurer) + self.latency = latency + self.pkt_size = int(pkt_size) + self.final_trial_duration = float(final_trial_duration) + self.final_relative_width = float(final_relative_width) + self.number_of_intermediate_phases = int(number_of_intermediate_phases) + self.initial_trial_duration = float(initial_trial_duration) + self.timeout = float(timeout) + self.doublings = int(doublings) + + self.queue = None + self.port_pg_id = None + self.ports = [] + self.test_data = {} + self.profiles = {} + + @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 - MultipleLossRatioSearch.double_relative_width( + relative_width)) + + @staticmethod + def expand_down(relative_width, doublings, current_bound): + """Return rate of expanded logarithmic width below. + + :param relative_width: The base relative width to double. + :param doublings: How many doublings to do for expansion. + :param current_bound: The current target transmit rate to move [pps]. + :type relative_width: float + :type doublings: int + :type current_bound: float + :returns: Transmit rate smaller by logarithmically double width [pps]. + :rtype: float + """ + for _ in range(doublings): + relative_width = MultipleLossRatioSearch.double_relative_width( + relative_width) + return current_bound * (1.0 - 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 - MultipleLossRatioSearch.double_relative_width( + relative_width)) + + @staticmethod + def expand_up(relative_width, doublings, current_bound): + """Return rate of expanded logarithmic width above. + + :param relative_width: The base relative width to double. + :param doublings: How many doublings to do for expansion. + :param current_bound: The current target transmit rate to move [pps]. + :type relative_width: float + :type doublings: int + :type current_bound: float + :returns: Transmit rate smaller by logarithmically double width [pps]. + :rtype: float + """ + for _ in range(doublings): + relative_width = MultipleLossRatioSearch.double_relative_width( + relative_width) + return current_bound / (1.0 - 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 - MultipleLossRatioSearch.half_relative_width( + relative_width)) + + def init_generator(self, ports, port_pg_id, profiles, test_data, queue): + self.ports = ports + self.port_pg_id = port_pg_id + self.profiles = profiles + self.test_data = test_data + self.queue = queue + self.queue.cancel_join_thread() + + def collect_kpi(self, stats, test_value): + samples = self.measurer.generate_samples(stats, self.ports, + self.port_pg_id, self.latency) + samples.update(self.test_data) + LOGGING.info("Collect TG KPIs %s %s %s", datetime.datetime.now(), + test_value, samples) + self.queue.put(samples) + + def narrow_down_ndr_and_pdr( + self, minimum_transmit_rate, maximum_transmit_rate, + packet_loss_ratio): + """Perform initial phase, create state object, proceed with next phases. + + :param minimum_transmit_rate: Minimal target transmit rate [pps]. + :param maximum_transmit_rate: Maximal target transmit rate [pps]. + :param packet_loss_ratio: Fraction of packets lost, for PDR [1]. + :type minimum_transmit_rate: float + :type maximum_transmit_rate: float + :type packet_loss_ratio: float + :returns: Structure containing narrowed down intervals + and their measurements. + :rtype: NdrPdrResult.NdrPdrResult + :raises RuntimeError: If total duration is larger than timeout. + """ + minimum_transmit_rate = float(minimum_transmit_rate) + maximum_transmit_rate = float(maximum_transmit_rate) + packet_loss_ratio = float(packet_loss_ratio) + line_measurement = self.measure( + self.initial_trial_duration, maximum_transmit_rate, self.latency) + initial_width_goal = self.final_relative_width + for _ in range(self.number_of_intermediate_phases): + initial_width_goal = self.double_relative_width(initial_width_goal) + max_lo = maximum_transmit_rate * (1.0 - initial_width_goal) + mrr = max( + minimum_transmit_rate, + min(max_lo, line_measurement.receive_rate)) + mrr_measurement = self.measure( + self.initial_trial_duration, mrr, self.latency) + # Attempt to get narrower width. + if mrr_measurement.loss_fraction > 0.0: + max2_lo = mrr * (1.0 - initial_width_goal) + mrr2 = min(max2_lo, mrr_measurement.receive_rate) + else: + mrr2 = mrr / (1.0 - initial_width_goal) + if mrr2 > minimum_transmit_rate and mrr2 < maximum_transmit_rate: + line_measurement = mrr_measurement + mrr_measurement = self.measure( + self.initial_trial_duration, mrr2, self.latency) + if mrr2 > mrr: + buf = line_measurement + line_measurement = mrr_measurement + mrr_measurement = buf + starting_interval = ReceiveRateInterval( + mrr_measurement, line_measurement) + starting_result = NdrPdrResult(starting_interval, starting_interval) + state = self.ProgressState( + starting_result, self.number_of_intermediate_phases, + self.final_trial_duration, self.final_relative_width, + packet_loss_ratio, minimum_transmit_rate, maximum_transmit_rate) + state = self.ndrpdr(state) + result = state.result + # theor_max_thruput = 0 + result_samples = {} + + MultipleLossRatioSearch.display_single_bound(result_samples, + 'NDR_LOWER', result.ndr_interval.measured_low.transmit_rate, + self.pkt_size, result.ndr_interval.measured_low.latency) + MultipleLossRatioSearch.display_single_bound(result_samples, + 'NDR_UPPER', result.ndr_interval.measured_high.transmit_rate, + self.pkt_size) + MultipleLossRatioSearch.display_single_bound(result_samples, + 'PDR_LOWER', result.pdr_interval.measured_low.transmit_rate, + self.pkt_size, result.pdr_interval.measured_low.latency) + MultipleLossRatioSearch.display_single_bound(result_samples, + 'PDR_UPPER', result.pdr_interval.measured_high.transmit_rate, + self.pkt_size) + pdr_msg = self.check_ndrpdr_interval_validity(result_samples, "PDR", + result.pdr_interval, + packet_loss_ratio) + ndr_msg = self.check_ndrpdr_interval_validity(result_samples, "NDR", + result.ndr_interval) + self.queue.put(result_samples) + + LOGGING.debug("result_samples: %s", result_samples) + LOGGING.info(pdr_msg) + LOGGING.info(ndr_msg) + + self.perform_additional_measurements_based_on_ndrpdr_result(result) + + return result_samples + + 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.measure(state.duration, transmit_rate, self.latency) + ndr_interval = self._new_interval( + state.result.ndr_interval, measurement, 0.0) + pdr_interval = self._new_interval( + state.result.pdr_interval, measurement, state.packet_loss_ratio) + state.result = NdrPdrResult(ndr_interval, pdr_interval) + return state + + @staticmethod + def _new_interval(old_interval, measurement, packet_loss_ratio): + """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 packet_loss_ratio: Fraction for PDR (or zero for NDR). + :type old_interval: ReceiveRateInterval.ReceiveRateInterval + :type measurement: ReceiveRateMeasurement.ReceiveRateMeasurement + :type packet_loss_ratio: float + :returns: The updated interval. + :rtype: ReceiveRateInterval.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.loss_fraction > packet_loss_ratio: + # 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 loss rate. + if measurement.loss_fraction > packet_loss_ratio: + # 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.loss_fraction <= packet_loss_ratio: + # Old upper bound becomes valid new lower bound. + return ReceiveRateInterval(old_hi, measurement) + else: + # Internal measurement, replaced boundary + # depends on measured loss fraction. + if measurement.loss_fraction > packet_loss_ratio: + # 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 maximal rate" case. + return ReceiveRateInterval(measurement, old_hi) + # Fallback, the interval is unchanged by the measurement. + return old_interval + + 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 updated state. + :rtype: ProgressState + :raises RuntimeError: If total duration is larger than timeout. + """ + start_time = time.time() + 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) + while 1: + if time.time() > start_time + self.timeout: + raise RuntimeError("Optimized search takes too long.") + # Order of priorities: invalid 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 maximal or minimal rate, we cannot shift, + # but we can re-measure. + if ndr_lo.loss_fraction > 0.0: + if ndr_lo.target_tr > state.minimum_transmit_rate: + new_tr = max( + state.minimum_transmit_rate, + self.expand_down( + ndr_rel_width, self.doublings, 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 minimal re-measure") + state = self._measure_and_update_state( + state, state.minimum_transmit_rate) + continue + if pdr_lo.loss_fraction > state.packet_loss_ratio: + if pdr_lo.target_tr > state.minimum_transmit_rate: + new_tr = max( + state.minimum_transmit_rate, + self.expand_down( + pdr_rel_width, self.doublings, 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 minimal re-measure") + state = self._measure_and_update_state( + state, state.minimum_transmit_rate) + continue + if ndr_hi.loss_fraction <= 0.0: + if ndr_hi.target_tr < state.maximum_transmit_rate: + new_tr = min( + state.maximum_transmit_rate, + self.expand_up( + ndr_rel_width, self.doublings, 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 maximal re-measure") + state = self._measure_and_update_state( + state, state.maximum_transmit_rate) + continue + if pdr_hi.loss_fraction <= state.packet_loss_ratio: + if pdr_hi.target_tr < state.maximum_transmit_rate: + new_tr = min( + state.maximum_transmit_rate, + self.expand_up( + pdr_rel_width, self.doublings, 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 maximal re-measure") + state = self._measure_and_update_state( + state, state.maximum_transmit_rate) + continue + # If we are hitting maximum_transmit_rate, + # it is still worth narrowing width, + # hoping large enough loss fraction will happen. + # But if we are hitting the minimal 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.minimum_transmit_rate + and ndr_lo.loss_fraction > 0.0): + ndr_rel_width = 0.0 + if (pdr_lo.target_tr <= state.minimum_transmit_rate + and pdr_lo.loss_fraction > state.packet_loss_ratio): + pdr_rel_width = 0.0 + if ndr_rel_width > state.width_goal: + # We have to narrow NDR width first, as NDR internal search + # can invalidate PDR (but not vice versa). + 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 + if pdr_rel_width > state.width_goal: + # PDR iternal search. + 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") + state = self._measure_and_update_state(state, ndr_lo.target_tr) + continue + if pdr_lo.duration < state.duration: + LOGGING.info("re-measuring PDR lower bound") + state = self._measure_and_update_state(state, pdr_lo.target_tr) + continue + # Except when lower bounds have high loss fraction, 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") + state = 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") + state = self._measure_and_update_state(state, pdr_hi.target_tr) + continue + # Widths are narrow (or lower bound minimal), bound measurements + # are long enough, we can return. + LOGGING.info("phase done") + break + return state + + def measure(self, duration, transmit_rate, latency): + 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" + stats = self.measurer.send_traffic_on_tg(self.ports, self.port_pg_id, + duration, unit_rate, + latency=latency) + self.measurer.client.reset(ports=self.ports) + self.measurer.client.clear_stats(ports=self.ports) + self.measurer.client.remove_all_streams(ports=self.ports) + for port, profile in self.profiles.items(): + self.measurer.client.add_streams(profile, ports=[port]) + self.collect_kpi(stats, unit_rate) + transmit_count = int(self.measurer.sent) + loss_count = int(self.measurer.loss) + measurement = ReceiveRateMeasurement( + duration, transmit_rate, transmit_count, loss_count) + measurement.latency = self.measurer.latency + return measurement + + def perform_additional_measurements_based_on_ndrpdr_result(self, result): + duration = 5.0 + rate = "{}{}".format(result.ndr_interval.measured_low.target_tr / 2.0, + 'pps') + for _ in range(0, 1): + stats = self.measurer.send_traffic_on_tg(self.ports, + self.port_pg_id, duration, + rate) + self.collect_kpi(stats, rate) + LOGGING.info('Traffic loss occurred: %s', self.measurer.loss) + + @staticmethod + def display_single_bound(result_samples, result_type, rate_total, pkt_size, + latency=None): + bandwidth_total = float(rate_total) * (pkt_size + 20) * 8 / (10 ** 9) + + result_samples["Result_{}".format(result_type)] = { + "rate_total_pps": float(rate_total), + "bandwidth_total_Gbps": float(bandwidth_total), + } + + if latency: + for item in latency: + if latency.index(item) == 0: + name = "Result_{}_{}".format("stream0", result_type) + else: + name = "Result_{}_{}".format("stream1", result_type) + lat_min, lat_avg, lat_max = item.split('/') + result_samples[name] = { + "min_latency": float(lat_min), + "avg_latency": float(lat_avg), + "max_latency": float(lat_max), + } + + @staticmethod + def check_ndrpdr_interval_validity(result_samples, result_type, interval, + packet_loss_ratio=0.0): + lower_bound = interval.measured_low + lower_bound_lf = lower_bound.loss_fraction + + result_samples["Result_{}_packets_lost".format(result_type)] = { + "packet_loss_ratio": float(lower_bound_lf), + "packets_lost": float(lower_bound.loss_count), + } + + if lower_bound_lf <= packet_loss_ratio: + return "Minimal rate loss fraction {} reach target {}".format( + lower_bound_lf, packet_loss_ratio) + else: + message = "Minimal rate loss fraction {} does not reach target {}".format( + lower_bound_lf, packet_loss_ratio) + if lower_bound_lf >= 1.0: + return '{}\nZero packets forwarded!'.format(message) + else: + return '{}\n{} packets lost.'.format(message, + lower_bound.loss_count) diff --git a/yardstick/network_services/helpers/vpp_helpers/ndr_pdr_result.py b/yardstick/network_services/helpers/vpp_helpers/ndr_pdr_result.py new file mode 100644 index 000000000..34a97f9fb --- /dev/null +++ b/yardstick/network_services/helpers/vpp_helpers/ndr_pdr_result.py @@ -0,0 +1,68 @@ +# Copyright (c) 2019 Viosoft Corporation +# +# 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. +# +# This is a modified copy of +# https://gerrit.fd.io/r/gitweb?p=csit.git;a=blob_plain;f=resources/libraries/python/MLRsearch/NdrPdrResult.py;hb=HEAD + +from yardstick.network_services.helpers.vpp_helpers.receive_rate_interval 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.ReceiveRateInterval + :type pdr_interval: ReceiveRateInterval.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/yardstick/network_services/helpers/vpp_helpers/receive_rate_interval.py b/yardstick/network_services/helpers/vpp_helpers/receive_rate_interval.py new file mode 100644 index 000000000..517a99c1f --- /dev/null +++ b/yardstick/network_services/helpers/vpp_helpers/receive_rate_interval.py @@ -0,0 +1,88 @@ +# Copyright (c) 2019 Viosoft Corporation +# +# 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. +# +# This is a modified copy of +# https://gerrit.fd.io/r/gitweb?p=csit.git;a=blob_plain;f=resources/libraries/python/MLRsearch/ReceiveRateInterval.py;hb=HEAD + +import math + +from yardstick.network_services.helpers.vpp_helpers.receive_rate_measurement 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.ReceiveRateMeasurement + :type measured_high: ReceiveRateMeasurement.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. Absolute 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 = round( + self.abs_tr_width / self.measured_high.target_tr, 5) + + 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 round(math.log(1.0 - self.rel_tr_width) / math.log( + 1.0 - relative_width_goal), 5) + + 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/yardstick/network_services/helpers/vpp_helpers/receive_rate_measurement.py b/yardstick/network_services/helpers/vpp_helpers/receive_rate_measurement.py new file mode 100644 index 000000000..2c59ea104 --- /dev/null +++ b/yardstick/network_services/helpers/vpp_helpers/receive_rate_measurement.py @@ -0,0 +1,58 @@ +# Copyright (c) 2019 Viosoft Corporation +# +# 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. +# +# This is a modified copy of +# https://gerrit.fd.io/r/gitweb?p=csit.git;a=blob_plain;f=resources/libraries/python/MLRsearch/ReceiveRateMeasurement.py;hb=HEAD + + +class ReceiveRateMeasurement(object): + """Structure defining the result of single Rr measurement.""" + + def __init__(self, duration, target_tr, transmit_count, loss_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 loss_count: Number of packets transmitted but not received [1]. + :type duration: float + :type target_tr: float + :type transmit_count: int + :type loss_count: int + """ + self.duration = float(duration) + self.target_tr = float(target_tr) + self.transmit_count = int(transmit_count) + self.loss_count = int(loss_count) + self.receive_count = round(transmit_count - loss_count, 5) + self.transmit_rate = round(transmit_count / self.duration, 5) + self.loss_rate = round(loss_count / self.duration, 5) + self.receive_rate = round(self.receive_count / self.duration, 5) + self.loss_fraction = round( + float(self.loss_count) / self.transmit_count, 5) + # TODO: Do we want to store also the real time (duration + overhead)? + + def __str__(self): + """Return string reporting input and loss fraction.""" + return "d={dur!s},Tr={rate!s},Df={frac!s}".format( + dur=self.duration, rate=self.target_tr, frac=self.loss_fraction) + + def __repr__(self): + """Return string evaluable as a constructor call.""" + return ("ReceiveRateMeasurement(duration={dur!r},target_tr={rate!r}" + ",transmit_count={trans!r},loss_count={loss!r})".format( + dur=self.duration, rate=self.target_tr, + trans=self.transmit_count, + loss=self.loss_count)) |