X-Git-Url: https://gerrit.opnfv.org/gerrit/gitweb?a=blobdiff_plain;f=nfvbench%2Ftraffic_client.py;h=c711cfbe11e87c1e104fd15ff36b117c939c2687;hb=f2d211ad71a54286b1179434744abb7bbc2598b4;hp=f26a74777034af1ea30d1f88724c70e498086088;hpb=caac5c7f6e80d33d5031f26e5e0f0ea6f1d3789b;p=nfvbench.git diff --git a/nfvbench/traffic_client.py b/nfvbench/traffic_client.py index f26a747..c711cfb 100755 --- a/nfvbench/traffic_client.py +++ b/nfvbench/traffic_client.py @@ -13,14 +13,13 @@ # under the License. """Interface to the traffic generator clients including NDR/PDR binary search.""" - -from math import gcd import socket import struct import time from attrdict import AttrDict import bitmath +from hdrh.histogram import HdrHistogram from netaddr import IPNetwork # pylint: disable=import-error from trex.stl.api import Ether @@ -37,7 +36,7 @@ from .packet_stats import PacketPathStats from .stats_collector import IntervalCollector from .stats_collector import IterationCollector from .traffic_gen import traffic_utils as utils -from .utils import cast_integer +from .utils import cast_integer, find_max_size, find_tuples_equal_to_lcm_value, get_divisors, lcm class TrafficClientException(Exception): """Generic traffic client exception.""" @@ -116,6 +115,8 @@ class IpBlock(object): def __init__(self, base_ip, step_ip, count_ip): """Create an IP block.""" self.base_ip_int = Device.ip_to_int(base_ip) + if step_ip == 'random': + step_ip = '0.0.0.1' self.step = Device.ip_to_int(step_ip) self.max_available = count_ip self.next_free = 0 @@ -126,25 +127,23 @@ class IpBlock(object): raise IndexError('Index out of bounds: %d (max=%d)' % (index, self.max_available)) return Device.int_to_ip(self.base_ip_int + index * self.step) - def reserve_ip_range(self, count, force_ip_reservation=False): + def get_ip_from_chain_first_ip(self, first_ip, index=0): + """Return the IP address at given index starting from chain first ip.""" + if index < 0 or index >= self.max_available: + raise IndexError('Index out of bounds: %d (max=%d)' % (index, self.max_available)) + return Device.int_to_ip(first_ip + index * self.step) + + def reserve_ip_range(self, count): """Reserve a range of count consecutive IP addresses spaced by step. - force_ip_reservation parameter allows to continue the calculation of IPs when - the 2 sides (ports) have different size and the flow is greater than - the size as well. """ - if self.next_free + count > self.max_available and force_ip_reservation is False: + if self.next_free + count > self.max_available: raise IndexError('No more IP addresses next free=%d max_available=%d requested=%d' % (self.next_free, self.max_available, count)) - if self.next_free + count > self.max_available and force_ip_reservation is True: - first_ip = self.get_ip(self.next_free) - last_ip = self.get_ip(self.next_free + self.max_available - 1) - self.next_free += self.max_available - else: - first_ip = self.get_ip(self.next_free) - last_ip = self.get_ip(self.next_free + count - 1) - self.next_free += count + first_ip = self.get_ip(self.next_free) + last_ip = self.get_ip(self.next_free + count - 1) + self.next_free += count return (first_ip, last_ip) def reset_reservation(self): @@ -154,14 +153,24 @@ class IpBlock(object): class UdpPorts(object): - def __init__(self, src_min, src_max, dst_min, dst_max, step): + def __init__(self, src_min, src_max, dst_min, dst_max, udp_src_size, udp_dst_size, step): - self.src_min = src_min - self.src_max = src_max - self.dst_min = dst_min - self.dst_max = dst_max + self.src_min = int(src_min) + self.src_max = int(src_max) + self.dst_min = int(dst_min) + self.dst_max = int(dst_max) + self.udp_src_size = udp_src_size + self.udp_dst_size = udp_dst_size self.step = step + def get_src_max(self, index=0): + """Return the UDP src port at given index.""" + return int(self.src_min) + index * int(self.step) + + def get_dst_max(self, index=0): + """Return the UDP dst port at given index.""" + return int(self.dst_min) + index * int(self.step) + class Device(object): """Represent a port device and all information associated to it. @@ -202,17 +211,17 @@ class Device(object): self.ip_src_static = generator_config.ip_src_static self.ip_addrs_step = generator_config.ip_addrs_step if self.ip_addrs_step == 'random': - # Set step to 1 to calculate the IP range size (see check_ip_size below) + # Set step to 1 to calculate the IP range size (see check_range_size below) step = '0.0.0.1' else: step = self.ip_addrs_step - self.ip_size = self.check_ipsize(IPNetwork(self.ip_addrs).size, Device.ip_to_int(step)) + self.ip_size = self.check_range_size(IPNetwork(self.ip_addrs).size, Device.ip_to_int(step)) self.ip = str(IPNetwork(self.ip_addrs).network) ip_addrs_left = generator_config.ip_addrs[0] ip_addrs_right = generator_config.ip_addrs[1] self.ip_addrs_size = { - 'left': self.check_ipsize(IPNetwork(ip_addrs_left).size, Device.ip_to_int(step)), - 'right': self.check_ipsize(IPNetwork(ip_addrs_right).size, Device.ip_to_int(step))} + 'left': self.check_range_size(IPNetwork(ip_addrs_left).size, Device.ip_to_int(step)), + 'right': self.check_range_size(IPNetwork(ip_addrs_right).size, Device.ip_to_int(step))} udp_src_port = generator_config.gen_config.udp_src_port if udp_src_port is None: udp_src_port = 53 @@ -221,36 +230,28 @@ class Device(object): udp_dst_port = 53 src_max, src_min = self.define_udp_range(udp_src_port, 'udp_src_port') dst_max, dst_min = self.define_udp_range(udp_dst_port, 'udp_dst_port') - udp_src_range = int(src_max) - int(src_min) + 1 - udp_dst_range = int(dst_max) - int(dst_min) + 1 - lcm_port = self.lcm(udp_src_range, udp_dst_range) + if generator_config.gen_config.udp_port_step == 'random': + # Set step to 1 to calculate the UDP range size + udp_step = 1 + else: + udp_step = int(generator_config.gen_config.udp_port_step) + udp_src_size = self.check_range_size(int(src_max) - int(src_min) + 1, udp_step) + udp_dst_size = self.check_range_size(int(dst_max) - int(dst_min) + 1, udp_step) + lcm_port = lcm(udp_src_size, udp_dst_size) if self.ip_src_static is True: - lcm_ip = self.lcm(1, min(self.ip_addrs_size['left'], self.ip_addrs_size['right'])) + lcm_ip = lcm(1, min(self.ip_addrs_size['left'], self.ip_addrs_size['right'])) else: - lcm_ip = self.lcm(self.ip_addrs_size['left'], self.ip_addrs_size['right']) - flow_max = self.lcm(lcm_port, lcm_ip) + lcm_ip = lcm(self.ip_addrs_size['left'], self.ip_addrs_size['right']) + flow_max = lcm(lcm_port, lcm_ip) if self.flow_count > flow_max: raise TrafficClientException('Trying to set unachievable traffic (%d > %d)' % (self.flow_count, flow_max)) - # manage udp range regarding flow count value - # UDP dst range is greater than FC => range will be limited to min + FC - if self.flow_count <= udp_dst_range: - dst_max = int(dst_min) + self.flow_count - 1 - # UDP src range is greater than FC => range will be limited to min + FC - if self.flow_count <= udp_src_range: - src_max = int(src_min) + self.flow_count - 1 - # Define IP block limit regarding flow count - if self.flow_count <= self.ip_size: - self.ip_block = IpBlock(self.ip, step, self.flow_count) - else: - self.ip_block = IpBlock(self.ip, step, self.ip_size) + self.udp_ports = UdpPorts(src_min, src_max, dst_min, dst_max, udp_src_size, udp_dst_size, + generator_config.gen_config.udp_port_step) + + self.ip_block = IpBlock(self.ip, step, self.ip_size) - if generator_config.gen_config.udp_port_step == 'random': - step = 1 - else: - step = generator_config.gen_config.udp_port_step - self.udp_ports = UdpPorts(src_min, src_max, dst_min, dst_max, step) self.gw_ip_block = IpBlock(generator_config.gateway_ips[port], generator_config.gateway_ip_addrs_step, self.chain_count) @@ -259,6 +260,120 @@ class Device(object): generator_config.tg_gateway_ip_addrs_step, self.chain_count) + def limit_ip_udp_ranges(self, peer_ip_size, cur_chain_flow_count): + # init to min value in case of no matching values found with lcm calculation + new_src_ip_size = 1 + new_peer_ip_size = 1 + new_src_udp_size = 1 + new_dst_udp_size = 1 + + if self.ip_src_static is True: + src_ip_size = 1 + else: + src_ip_size = self.ip_size + ip_src_divisors = list(get_divisors(src_ip_size)) + ip_dst_divisors = list(get_divisors(peer_ip_size)) + udp_src_divisors = list(get_divisors(self.udp_ports.udp_src_size)) + udp_dst_divisors = list(get_divisors(self.udp_ports.udp_dst_size)) + fc = int(cur_chain_flow_count) + tuples_ip = list(find_tuples_equal_to_lcm_value(ip_src_divisors, ip_dst_divisors, fc)) + tuples_udp = list(find_tuples_equal_to_lcm_value(udp_src_divisors, udp_dst_divisors, fc)) + + if tuples_ip: + new_src_ip_size = tuples_ip[-1][0] + new_peer_ip_size = tuples_ip[-1][1] + + if tuples_udp: + new_src_udp_size = tuples_udp[-1][0] + new_dst_udp_size = tuples_udp[-1][1] + + tuples_src = [] + tuples_dst = [] + if not tuples_ip and not tuples_udp: + # in case of not divisors in common matching LCM value (i.e. requested flow count) + # try to find an accurate UDP range to fit requested flow count + udp_src_int = range(self.udp_ports.src_min, self.udp_ports.src_max) + udp_dst_int = range(self.udp_ports.dst_min, self.udp_ports.dst_max) + tuples_src = list(find_tuples_equal_to_lcm_value(ip_src_divisors, udp_src_int, fc)) + tuples_dst = list(find_tuples_equal_to_lcm_value(ip_dst_divisors, udp_dst_int, fc)) + + if not tuples_src and not tuples_dst: + # iterate IP and UDP ranges to find a tuple that match flow count values + src_ip_range = range(1,src_ip_size) + dst_ip_range = range(1, peer_ip_size) + tuples_src = list(find_tuples_equal_to_lcm_value(src_ip_range, udp_src_int, fc)) + tuples_dst = list(find_tuples_equal_to_lcm_value(dst_ip_range, udp_dst_int, fc)) + + if tuples_src or tuples_dst: + if tuples_src: + new_src_ip_size = tuples_src[-1][0] + new_src_udp_size = tuples_src[-1][1] + if tuples_dst: + new_peer_ip_size = tuples_dst[-1][0] + new_dst_udp_size = tuples_dst[-1][1] + else: + if not tuples_ip: + if src_ip_size != 1: + if src_ip_size > fc: + new_src_ip_size = fc + else: + new_src_ip_size = find_max_size(src_ip_size, tuples_udp, fc) + if peer_ip_size != 1: + if peer_ip_size > fc: + new_peer_ip_size = fc + else: + new_peer_ip_size = find_max_size(peer_ip_size, tuples_udp, fc) + + if not tuples_udp: + if self.udp_ports.udp_src_size != 1: + if self.udp_ports.udp_src_size > fc: + new_src_udp_size = fc + else: + new_src_udp_size = find_max_size(self.udp_ports.udp_src_size, + tuples_ip, fc) + if self.udp_ports.udp_dst_size != 1: + if self.udp_ports.udp_dst_size > fc: + new_dst_udp_size = fc + else: + new_dst_udp_size = find_max_size(self.udp_ports.udp_dst_size, + tuples_ip, fc) + max_possible_flows = lcm(lcm(new_src_ip_size, new_peer_ip_size), + lcm(new_src_udp_size, new_dst_udp_size)) + + LOG.debug("IP dst size: %d", new_peer_ip_size) + LOG.debug("LCM IP: %d", lcm(new_src_ip_size, new_peer_ip_size)) + LOG.debug("LCM UDP: %d", lcm(new_src_udp_size, new_dst_udp_size)) + LOG.debug("Global LCM: %d", max_possible_flows) + LOG.debug("IP src size: %d, IP dst size: %d, UDP src size: %d, UDP dst size: %d", + new_src_ip_size, new_peer_ip_size, self.udp_ports.udp_src_size, + self.udp_ports.udp_dst_size) + if not max_possible_flows == cur_chain_flow_count: + if (self.ip_addrs_step != '0.0.0.1' or self.udp_ports.step != '1') and not ( + self.ip_addrs_step == 'random' and self.udp_ports.step == 'random'): + LOG.warning("Current values of ip_addrs_step and/or udp_port_step properties " + "do not allow to control an accurate flow count. " + "Values will be overridden as follows:") + if self.ip_addrs_step != '0.0.0.1': + LOG.info("ip_addrs_step='0.0.0.1' (previous value: ip_addrs_step='%s')", + self.ip_addrs_step) + self.ip_addrs_step = '0.0.0.1' + + if self.udp_ports.step != '1': + LOG.info("udp_port_step='1' (previous value: udp_port_step='%s')", + self.udp_ports.step) + self.udp_ports.step = '1' + # override config for not logging random step warning message in trex_gen.py + self.generator_config.gen_config.udp_port_step = self.udp_ports.step + else: + LOG.error("Current values of ip_addrs_step and udp_port_step properties " + "do not allow to control an accurate flow count.") + else: + src_ip_size = new_src_ip_size + peer_ip_size = new_peer_ip_size + self.udp_ports.udp_src_size = new_src_udp_size + self.udp_ports.udp_dst_size = new_dst_udp_size + return src_ip_size, peer_ip_size + @staticmethod def define_udp_range(udp_port, property_name): if isinstance(udp_port, int): @@ -272,26 +387,18 @@ class Device(object): % property_name) return max, min - @staticmethod - def lcm(a, b): - """Calculate the maximum possible value for both IP and ports, - eventually for maximum possible flux.""" - if a != 0 and b != 0: - lcm_value = a * b // gcd(a, b) - return lcm_value - raise TypeError(" IP size or port range can't be zero !") @staticmethod - def check_ipsize(ip_size, step): - """Check and set the available IPs, considering the step.""" + def check_range_size(range_size, step): + """Check and set the available IPs or UDP ports, considering the step.""" try: - if ip_size % step == 0: - value = int(ip_size / step) + if range_size % step == 0: + value = range_size // step else: - value = int((ip_size / step)) + 1 + value = range_size // step + 1 return value except ZeroDivisionError: - raise ZeroDivisionError("step can't be zero !") + raise ZeroDivisionError("step can't be zero !") from ZeroDivisionError def set_mac(self, mac): """Set the local MAC for this port device.""" @@ -392,27 +499,42 @@ class Device(object): # example 11 flows and 3 chains => 3, 4, 4 flows_per_chain = int((self.flow_count + self.chain_count - 1) / self.chain_count) cur_chain_flow_count = int(self.flow_count - flows_per_chain * (self.chain_count - 1)) - force_ip_reservation = False - # use case example of this parameter: - # - static IP addresses (source & destination), netmask = /30 - # - 4 varying UDP source ports | 1 UDP destination port - # - Flow count = 8 - # --> parameter 'reserve_ip_range' should have flag 'force_ip_reservation' - # in order to assign the maximum available IP on each iteration - if self.ip_size < cur_chain_flow_count \ - or self.ip_addrs_size['left'] != self.ip_addrs_size['right']: - force_ip_reservation = True peer = self.get_peer_device() self.ip_block.reset_reservation() peer.ip_block.reset_reservation() dest_macs = self.get_dest_macs() + # limit ranges of UDP ports and IP to avoid overflow of the number of flows + peer_size = peer.ip_size // self.chain_count + for chain_idx in range(self.chain_count): - src_ip_first, src_ip_last = self.ip_block.reserve_ip_range\ - (cur_chain_flow_count, force_ip_reservation) - dst_ip_first, dst_ip_last = peer.ip_block.reserve_ip_range\ - (cur_chain_flow_count, force_ip_reservation) + src_ip_size, peer_ip_size = self.limit_ip_udp_ranges(peer_size, cur_chain_flow_count) + + src_ip_first, src_ip_last = self.ip_block.reserve_ip_range \ + (src_ip_size) + dst_ip_first, dst_ip_last = peer.ip_block.reserve_ip_range \ + (peer_ip_size) + + if self.ip_addrs_step != 'random': + src_ip_last = self.ip_block.get_ip_from_chain_first_ip( + Device.ip_to_int(src_ip_first), src_ip_size - 1) + dst_ip_last = peer.ip_block.get_ip_from_chain_first_ip( + Device.ip_to_int(dst_ip_first), peer_ip_size - 1) + if self.udp_ports.step != 'random': + self.udp_ports.src_max = self.udp_ports.get_src_max(self.udp_ports.udp_src_size - 1) + self.udp_ports.dst_max = self.udp_ports.get_dst_max(self.udp_ports.udp_dst_size - 1) + if self.ip_src_static: + src_ip_last = src_ip_first + + LOG.info("Port %d, chain %d: IP src range [%s,%s]", self.port, chain_idx, + src_ip_first, src_ip_last) + LOG.info("Port %d, chain %d: IP dst range [%s,%s]", self.port, chain_idx, + dst_ip_first, dst_ip_last) + LOG.info("Port %d, chain %d: UDP src range [%s,%s]", self.port, chain_idx, + self.udp_ports.src_min, self.udp_ports.src_max) + LOG.info("Port %d, chain %d: UDP dst range [%s,%s]", self.port, chain_idx, + self.udp_ports.dst_min, self.udp_ports.dst_max) configs.append({ 'count': cur_chain_flow_count, @@ -420,18 +542,18 @@ class Device(object): 'mac_dst': dest_macs[chain_idx], 'ip_src_addr': src_ip_first, 'ip_src_addr_max': src_ip_last, - 'ip_src_count': cur_chain_flow_count, + 'ip_src_count': src_ip_size, 'ip_dst_addr': dst_ip_first, 'ip_dst_addr_max': dst_ip_last, - 'ip_dst_count': cur_chain_flow_count, + 'ip_dst_count': peer_ip_size, 'ip_addrs_step': self.ip_addrs_step, 'ip_src_static': self.ip_src_static, 'udp_src_port': self.udp_ports.src_min, 'udp_src_port_max': self.udp_ports.src_max, - 'udp_src_count': int(self.udp_ports.src_max) - int(self.udp_ports.src_min) + 1, + 'udp_src_count': self.udp_ports.udp_src_size, 'udp_dst_port': self.udp_ports.dst_min, 'udp_dst_port_max': self.udp_ports.dst_max, - 'udp_dst_count': int(self.udp_ports.dst_max) - int(self.udp_ports.dst_min) + 1, + 'udp_dst_count': self.udp_ports.udp_dst_size, 'udp_port_step': self.udp_ports.step, 'mac_discovery_gw': self.get_gw_ip(chain_idx), 'ip_src_tg_gw': self.tg_gw_ip_block.get_ip(chain_idx), @@ -489,15 +611,24 @@ class GeneratorConfig(object): self.cores = config.cores else: self.cores = gen_config.get('cores', 1) + # let's report the value actually used in the end + config.cores_used = self.cores self.mbuf_factor = config.mbuf_factor self.mbuf_64 = config.mbuf_64 self.hdrh = not config.disable_hdrh - if gen_config.intf_speed: - # interface speed is overriden from config - self.intf_speed = bitmath.parse_string(gen_config.intf_speed.replace('ps', '')).bits + if config.intf_speed: + # interface speed is overriden from the command line + self.intf_speed = config.intf_speed + elif gen_config.intf_speed: + # interface speed is overriden from the generator config + self.intf_speed = gen_config.intf_speed else: + self.intf_speed = "auto" + if self.intf_speed == "auto" or self.intf_speed == "0": # interface speed is discovered/provided by the traffic generator self.intf_speed = 0 + else: + self.intf_speed = bitmath.parse_string(self.intf_speed.replace('ps', '')).bits self.name = gen_config.name self.zmq_pub_port = gen_config.get('zmq_pub_port', 4500) self.zmq_rpc_port = gen_config.get('zmq_rpc_port', 4501) @@ -716,13 +847,17 @@ class TrafficClient(object): # interface speed is overriden from config if self.intf_speed != tg_if_speed: # Warn the user if the speed in the config is different - LOG.warning('Interface speed provided is different from actual speed (%d Gbps)', - intf_speeds[0]) + LOG.warning( + 'Interface speed provided (%g Gbps) is different from actual speed (%d Gbps)', + self.intf_speed / 1000000000.0, intf_speeds[0]) else: # interface speed not provisioned by config self.intf_speed = tg_if_speed # also update the speed in the tg config self.generator_config.intf_speed = tg_if_speed + # let's report detected and actually used interface speed + self.config.intf_speed_detected = tg_if_speed + self.config.intf_speed_used = self.intf_speed # Save the traffic generator local MAC for mac, device in zip(self.gen.get_macs(), self.generator_config.devices): @@ -864,8 +999,10 @@ class TrafficClient(object): if self.config.no_latency_streams: LOG.info("Latency streams are disabled") + # in service mode, we must disable flow stats (e2e=True) self.gen.create_traffic(frame_size, self.run_config['rates'], bidirectional, - latency=not self.config.no_latency_streams) + latency=not self.config.no_latency_streams, + e2e=self.runner.service_mode) def _modify_load(self, load): self.current_total_rate = {'rate_percent': str(load)} @@ -922,8 +1059,11 @@ class TrafficClient(object): def get_stats(self): """Collect final stats for previous run.""" - stats = self.gen.get_stats() - retDict = {'total_tx_rate': stats['total_tx_rate']} + stats = self.gen.get_stats(self.ifstats) + retDict = {'total_tx_rate': stats['total_tx_rate'], + 'offered_tx_rate_bps': stats['offered_tx_rate_bps'], + 'theoretical_tx_rate_bps': stats['theoretical_tx_rate_bps'], + 'theoretical_tx_rate_pps': stats['theoretical_tx_rate_pps']} tx_keys = ['total_pkts', 'total_pkt_bytes', 'pkt_rate', 'pkt_bit_rate'] rx_keys = tx_keys + ['dropped_pkts'] @@ -969,6 +1109,18 @@ class TrafficClient(object): for key in ['pkt_bit_rate', 'pkt_rate']: for dirc in ['tx', 'rx']: retDict['overall'][dirc][key] /= 2.0 + retDict['overall']['hdrh'] = stats.get('hdrh', None) + if retDict['overall']['hdrh']: + decoded_histogram = HdrHistogram.decode(retDict['overall']['hdrh']) + # override min max and avg from hdrh + retDict['overall']['rx']['min_delay_usec'] = decoded_histogram.get_min_value() + retDict['overall']['rx']['max_delay_usec'] = decoded_histogram.get_max_value() + retDict['overall']['rx']['avg_delay_usec'] = decoded_histogram.get_mean_value() + retDict['overall']['rx']['lat_percentile'] = {} + for percentile in self.config.lat_percentiles: + retDict['overall']['rx']['lat_percentile'][percentile] = \ + decoded_histogram.get_value_at_percentile(percentile) + else: retDict['overall'] = retDict[ports[0]] retDict['overall']['drop_rate_percent'] = self.__get_dropped_rate(retDict['overall']) @@ -999,6 +1151,20 @@ class TrafficClient(object): 'min_delay_usec': interface['rx']['min_delay_usec'], } + if key == 'overall': + stats[key]['hdrh'] = interface.get('hdrh', None) + if stats[key]['hdrh']: + decoded_histogram = HdrHistogram.decode(stats[key]['hdrh']) + # override min max and avg from hdrh + stats[key]['min_delay_usec'] = decoded_histogram.get_min_value() + stats[key]['max_delay_usec'] = decoded_histogram.get_max_value() + stats[key]['avg_delay_usec'] = decoded_histogram.get_mean_value() + stats[key]['lat_percentile'] = {} + for percentile in self.config.lat_percentiles: + stats[key]['lat_percentile'][percentile] = decoded_histogram.\ + get_value_at_percentile(percentile) + + return stats def __targets_found(self, rate, targets, results): @@ -1216,7 +1382,7 @@ class TrafficClient(object): for chain_idx in range(self.config.service_chain_count)] # note that we need to make a copy of the ifs list so that any modification in the # list from pps will not change the list saved in self.ifstats - self.pps_list = [PacketPathStats(list(ifs)) for ifs in self.ifstats] + self.pps_list = [PacketPathStats(self.config, list(ifs)) for ifs in self.ifstats] # insert the corresponding pps in the passed list pps_list.extend(self.pps_list) @@ -1235,7 +1401,7 @@ class TrafficClient(object): ] """ if diff: - stats = self.gen.get_stats() + stats = self.gen.get_stats(self.ifstats) for chain_idx, ifs in enumerate(self.ifstats): # each ifs has exactly 2 InterfaceStats and 2 Latency instances # corresponding to the