2.0 beta NFVBENCH-91 Allow multi-chaining with separate edge networks
[nfvbench.git] / nfvbench / packet_stats.py
diff --git a/nfvbench/packet_stats.py b/nfvbench/packet_stats.py
new file mode 100644 (file)
index 0000000..16dc965
--- /dev/null
@@ -0,0 +1,309 @@
+# Copyright 2018 Cisco Systems, Inc.  All rights reserved.
+#
+#    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.
+#
+"""Manage all classes related to counting packet stats.
+
+InterfaceStats counts RX/TX packet counters for one interface.
+PacketPathStats manages all InterfaceStats instances for a given chain.
+PacketPathStatsManager manages all packet path stats for all chains.
+"""
+
+import copy
+
+from traffic_gen.traffic_base import Latency
+
+class InterfaceStats(object):
+    """A class to hold the RX and TX counters for a virtual or physical interface.
+
+    An interface stats instance can represent a real interface (e.g. traffic gen port or
+    vhost interface) or can represent an aggegation of multiple interfaces when packets
+    are faned out (e.g. one vlan subinterface can fan out to multiple vhost interfaces
+    in the case of multi-chaining and when the network is shared across chains).
+    """
+
+    TX = 0
+    RX = 1
+
+    def __init__(self, name, device, shared=False):
+        """Create a new interface instance.
+
+        name: interface name specific to each chain (e.g. "trex port 0 chain 0")
+        device: on which device this interface resides (e.g. "trex server")
+        fetch_tx_rx: a fetch method that takes name, chain_index and returns a (tx, rx) tuple
+        shared: if true this interface stats is shared across all chains
+        """
+        self.name = name
+        self.device = device
+        self.shared = shared
+        # RX and TX counters for this interface
+        self.tx = 0
+        self.rx = 0
+
+    def get_packet_count(self, direction):
+        """Get packet count for given direction.
+
+        direction: InterfaceStats.TX or InterfaceStats.RX
+        """
+        return self.tx if direction == InterfaceStats.TX else self.rx
+
+    @staticmethod
+    def get_reverse_direction(direction):
+        """Get the reverse direction of a given direction.
+
+        direction: InterfaceStats.TX or InterfaceStats.RX
+        return: RX if TX given, or TX is RX given
+        """
+        return 1 - direction
+
+    @staticmethod
+    def get_direction_name(direction):
+        """Get the rdisplay name of a given direction.
+
+        direction: InterfaceStats.TX or InterfaceStats.RX
+        return: "TX" or "RX"
+        """
+        if direction == InterfaceStats.TX:
+            return 'TX'
+        return 'RX'
+
+    def add_if_stats(self, if_stats):
+        """Add another ifstats to this instance."""
+        self.tx += if_stats.tx
+        self.rx += if_stats.rx
+
+    def update_stats(self, tx, rx, diff):
+        """Update stats for this interface.
+
+        tx: new TX packet count
+        rx: new RX packet count
+        diff: if True, perform a diff of new value with previous baselined value,
+              otherwise store the new value
+        """
+        if diff:
+            self.tx = tx - self.tx
+            self.rx = rx - self.rx
+        else:
+            self.tx = tx
+            self.rx = rx
+
+    def get_display_name(self, dir, name=None, aggregate=False):
+        """Get the name to use to display stats for this interface stats.
+
+        dir: direction InterfaceStats.TX or InterfaceStats.RX
+        name: override self.name
+        aggregate: true if this is for an aggregate of multiple chains
+        """
+        if name is None:
+            name = self.name
+        return self.device + '.' + InterfaceStats.get_direction_name(dir) + '.' + name
+
+
+class PacketPathStats(object):
+    """Manage the packet path stats for 1 chain in both directions.
+
+    A packet path stats instance manages an ordered list of InterfaceStats objects
+    that can be traversed in the forward and reverse direction to display packet
+    counters in each direction.
+    The requirement is that RX and TX counters must always alternate as we travel
+    along one direction. For example with 4 interfaces per chain:
+    [ifstat0, ifstat1, ifstat2, ifstat3]
+    Packet counters in the forward direction are:
+    [ifstat0.TX, ifstat1.RX, ifstat2.TX, ifstat3.RX]
+    Packet counters in the reverse direction are:
+    [ifstat3.TX, ifstat2.RX, ifstat1.TX, ifstat0.RX]
+
+    A packet path stats also carries the latency data for each direction of the
+    chain.
+    """
+
+    def __init__(self, if_stats, aggregate=False):
+        """Create a packet path stats intance with the list of associated if stats.
+
+        if_stats: a list of interface stats that compose this packet path stats
+        aggregate: True if this is an aggregate packet path stats
+
+        Aggregate packet path stats are the only one that should show counters for shared
+        interface stats
+        """
+        self.if_stats = if_stats
+        # latency for packets sent from port 0 and 1
+        self.latencies = [Latency(), Latency()]
+        self.aggregate = aggregate
+
+
+    def add_packet_path_stats(self, pps):
+        """Add another packet path stat to this instance.
+
+        pps: the other packet path stats to add to this instance
+
+        This is used only for aggregating/collapsing multiple pps into 1
+        to form a "total" pps
+        """
+        for index, ifstats in enumerate(self.if_stats):
+            # shared interface stats must not be self added
+            if not ifstats.shared:
+                ifstats.add_if_stats(pps.if_stats[index])
+
+    @staticmethod
+    def get_agg_packet_path_stats(pps_list):
+        """Get the aggregated packet path stats from a list of packet path stats.
+
+        Interface counters are added, latency stats are updated.
+        """
+        agg_pps = None
+        for pps in pps_list:
+            if agg_pps is None:
+                # Get a clone of the first in the list
+                agg_pps = PacketPathStats(pps.get_cloned_if_stats(), aggregate=True)
+            else:
+                agg_pps.add_packet_path_stats(pps)
+        # aggregate all latencies
+        agg_pps.latencies = [Latency([pps.latencies[port] for pps in pps_list])
+                             for port in [0, 1]]
+        return agg_pps
+
+    def get_if_stats(self, reverse=False):
+        """Get interface stats for given direction.
+
+        reverse: if True, get the list of interface stats in the reverse direction
+                 else (default) gets the ist in the forward direction.
+        return: the list of interface stats indexed by the chain index
+        """
+        return self.if_stats[::-1] if reverse else self.if_stats
+
+    def get_cloned_if_stats(self):
+        """Get a clone copy of the interface stats list."""
+        return [copy.copy(ifstat) for ifstat in self.if_stats]
+
+
+    def get_header_labels(self, reverse=False, aggregate=False):
+        """Get the list of header labels for this packet path stats."""
+        labels = []
+        dir = InterfaceStats.TX
+        for ifstat in self.get_if_stats(reverse):
+            # starts at TX then RX then TX again etc...
+            labels.append(ifstat.get_display_name(dir, aggregate=aggregate))
+            dir = InterfaceStats.get_reverse_direction(dir)
+        return labels
+
+    def get_stats(self, reverse=False):
+        """Get the list of packet counters and latency data for this packet path stats.
+
+        return: a dict of packet counters and latency stats
+
+        {'packets': [2000054, 1999996, 1999996],
+         'min_usec': 10, 'max_usec': 187, 'avg_usec': 45},
+        """
+        counters = []
+        dir = InterfaceStats.TX
+        for ifstat in self.get_if_stats(reverse):
+            # starts at TX then RX then TX again etc...
+            if ifstat.shared and not self.aggregate:
+                # shared if stats countesr are only shown in aggregate pps
+                counters.append('')
+            else:
+                counters.append(ifstat.get_packet_count(dir))
+            dir = InterfaceStats.get_reverse_direction(dir)
+
+        # latency: use port 0 latency for forward, port 1 latency for reverse
+        latency = self.latencies[1] if reverse else self.latencies[0]
+
+        if latency.available():
+            results = {'lat_min_usec': latency.min_usec,
+                       'lat_max_usec': latency.max_usec,
+                       'lat_avg_usec': latency.avg_usec}
+        else:
+            results = {}
+        results['packets'] = counters
+        return results
+
+
+class PacketPathStatsManager(object):
+    """Manages all the packet path stats for all chains.
+
+    Each run will generate packet path stats for 1 or more chains.
+    """
+
+    def __init__(self, pps_list):
+        """Create a packet path stats intance with the list of associated if stats.
+
+        pps_list: a list of packet path stats indexed by the chain id.
+        All packet path stats must have the same length.
+        """
+        self.pps_list = pps_list
+
+    def insert_pps_list(self, chain_index, if_stats):
+        """Insert a list of interface stats for given chain right after the first in the list.
+
+        chain_index: index of chain where to insert
+        if_stats: list of interface stats to insert
+        """
+        # use slicing to insert the list
+        self.pps_list[chain_index].if_stats[1:1] = if_stats
+
+    def _get_if_agg_name(self, reverse):
+        """Get the aggegated name for all interface stats across all pps.
+
+        return: a list of aggregated names for each position of the chain for all chains
+
+        The agregated name is the interface stats name if there is only 1 chain.
+        Otherwise it is the common prefix for all interface stats names at same position in the
+        chain.
+        """
+        # if there is only one chain, use the if_stats names directly
+        return self.pps_list[0].get_header_labels(reverse, aggregate=(len(self.pps_list) > 1))
+
+    def _get_results(self, reverse=False):
+        """Get the digested stats for the forward or reverse directions.
+
+        return: a dict with all the labels, total and per chain counters
+        """
+        chains = {}
+        # insert the aggregated row if applicable
+        if len(self.pps_list) > 1:
+            agg_pps = PacketPathStats.get_agg_packet_path_stats(self.pps_list)
+            chains['total'] = agg_pps.get_stats(reverse)
+
+        for index, pps in enumerate(self.pps_list):
+            chains[index] = pps.get_stats(reverse)
+        return {'interfaces': self._get_if_agg_name(reverse),
+                'chains': chains}
+
+    def get_results(self):
+        """Get the digested stats for the forward and reverse directions.
+
+        return: a dictionary of results for each direction and each chain
+
+        Example:
+
+        {
+            'Forward': {
+                'interfaces': ['Port0', 'vhost0', 'Port1'],
+                'chains': {
+                    0: {'packets': [2000054, 1999996, 1999996],
+                        'min_usec': 10,
+                        'max_usec': 187,
+                        'avg_usec': 45},
+                    1: {...},
+                    'total': {...}
+                }
+            },
+            'Reverse': {...
+            }
+        }
+
+        """
+        results = {'Forward': self._get_results(),
+                   'Reverse': self._get_results(reverse=True)}
+        return results