16dc9655926c421c3bba7a0e0b8af1927617b34e
[nfvbench.git] / nfvbench / packet_stats.py
1 # Copyright 2018 Cisco Systems, Inc.  All rights reserved.
2 #
3 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
4 #    not use this file except in compliance with the License. You may obtain
5 #    a copy of the License at
6 #
7 #         http://www.apache.org/licenses/LICENSE-2.0
8 #
9 #    Unless required by applicable law or agreed to in writing, software
10 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 #    License for the specific language governing permissions and limitations
13 #    under the License.
14 #
15 """Manage all classes related to counting packet stats.
16
17 InterfaceStats counts RX/TX packet counters for one interface.
18 PacketPathStats manages all InterfaceStats instances for a given chain.
19 PacketPathStatsManager manages all packet path stats for all chains.
20 """
21
22 import copy
23
24 from traffic_gen.traffic_base import Latency
25
26 class InterfaceStats(object):
27     """A class to hold the RX and TX counters for a virtual or physical interface.
28
29     An interface stats instance can represent a real interface (e.g. traffic gen port or
30     vhost interface) or can represent an aggegation of multiple interfaces when packets
31     are faned out (e.g. one vlan subinterface can fan out to multiple vhost interfaces
32     in the case of multi-chaining and when the network is shared across chains).
33     """
34
35     TX = 0
36     RX = 1
37
38     def __init__(self, name, device, shared=False):
39         """Create a new interface instance.
40
41         name: interface name specific to each chain (e.g. "trex port 0 chain 0")
42         device: on which device this interface resides (e.g. "trex server")
43         fetch_tx_rx: a fetch method that takes name, chain_index and returns a (tx, rx) tuple
44         shared: if true this interface stats is shared across all chains
45         """
46         self.name = name
47         self.device = device
48         self.shared = shared
49         # RX and TX counters for this interface
50         self.tx = 0
51         self.rx = 0
52
53     def get_packet_count(self, direction):
54         """Get packet count for given direction.
55
56         direction: InterfaceStats.TX or InterfaceStats.RX
57         """
58         return self.tx if direction == InterfaceStats.TX else self.rx
59
60     @staticmethod
61     def get_reverse_direction(direction):
62         """Get the reverse direction of a given direction.
63
64         direction: InterfaceStats.TX or InterfaceStats.RX
65         return: RX if TX given, or TX is RX given
66         """
67         return 1 - direction
68
69     @staticmethod
70     def get_direction_name(direction):
71         """Get the rdisplay name of a given direction.
72
73         direction: InterfaceStats.TX or InterfaceStats.RX
74         return: "TX" or "RX"
75         """
76         if direction == InterfaceStats.TX:
77             return 'TX'
78         return 'RX'
79
80     def add_if_stats(self, if_stats):
81         """Add another ifstats to this instance."""
82         self.tx += if_stats.tx
83         self.rx += if_stats.rx
84
85     def update_stats(self, tx, rx, diff):
86         """Update stats for this interface.
87
88         tx: new TX packet count
89         rx: new RX packet count
90         diff: if True, perform a diff of new value with previous baselined value,
91               otherwise store the new value
92         """
93         if diff:
94             self.tx = tx - self.tx
95             self.rx = rx - self.rx
96         else:
97             self.tx = tx
98             self.rx = rx
99
100     def get_display_name(self, dir, name=None, aggregate=False):
101         """Get the name to use to display stats for this interface stats.
102
103         dir: direction InterfaceStats.TX or InterfaceStats.RX
104         name: override self.name
105         aggregate: true if this is for an aggregate of multiple chains
106         """
107         if name is None:
108             name = self.name
109         return self.device + '.' + InterfaceStats.get_direction_name(dir) + '.' + name
110
111
112 class PacketPathStats(object):
113     """Manage the packet path stats for 1 chain in both directions.
114
115     A packet path stats instance manages an ordered list of InterfaceStats objects
116     that can be traversed in the forward and reverse direction to display packet
117     counters in each direction.
118     The requirement is that RX and TX counters must always alternate as we travel
119     along one direction. For example with 4 interfaces per chain:
120     [ifstat0, ifstat1, ifstat2, ifstat3]
121     Packet counters in the forward direction are:
122     [ifstat0.TX, ifstat1.RX, ifstat2.TX, ifstat3.RX]
123     Packet counters in the reverse direction are:
124     [ifstat3.TX, ifstat2.RX, ifstat1.TX, ifstat0.RX]
125
126     A packet path stats also carries the latency data for each direction of the
127     chain.
128     """
129
130     def __init__(self, if_stats, aggregate=False):
131         """Create a packet path stats intance with the list of associated if stats.
132
133         if_stats: a list of interface stats that compose this packet path stats
134         aggregate: True if this is an aggregate packet path stats
135
136         Aggregate packet path stats are the only one that should show counters for shared
137         interface stats
138         """
139         self.if_stats = if_stats
140         # latency for packets sent from port 0 and 1
141         self.latencies = [Latency(), Latency()]
142         self.aggregate = aggregate
143
144
145     def add_packet_path_stats(self, pps):
146         """Add another packet path stat to this instance.
147
148         pps: the other packet path stats to add to this instance
149
150         This is used only for aggregating/collapsing multiple pps into 1
151         to form a "total" pps
152         """
153         for index, ifstats in enumerate(self.if_stats):
154             # shared interface stats must not be self added
155             if not ifstats.shared:
156                 ifstats.add_if_stats(pps.if_stats[index])
157
158     @staticmethod
159     def get_agg_packet_path_stats(pps_list):
160         """Get the aggregated packet path stats from a list of packet path stats.
161
162         Interface counters are added, latency stats are updated.
163         """
164         agg_pps = None
165         for pps in pps_list:
166             if agg_pps is None:
167                 # Get a clone of the first in the list
168                 agg_pps = PacketPathStats(pps.get_cloned_if_stats(), aggregate=True)
169             else:
170                 agg_pps.add_packet_path_stats(pps)
171         # aggregate all latencies
172         agg_pps.latencies = [Latency([pps.latencies[port] for pps in pps_list])
173                              for port in [0, 1]]
174         return agg_pps
175
176     def get_if_stats(self, reverse=False):
177         """Get interface stats for given direction.
178
179         reverse: if True, get the list of interface stats in the reverse direction
180                  else (default) gets the ist in the forward direction.
181         return: the list of interface stats indexed by the chain index
182         """
183         return self.if_stats[::-1] if reverse else self.if_stats
184
185     def get_cloned_if_stats(self):
186         """Get a clone copy of the interface stats list."""
187         return [copy.copy(ifstat) for ifstat in self.if_stats]
188
189
190     def get_header_labels(self, reverse=False, aggregate=False):
191         """Get the list of header labels for this packet path stats."""
192         labels = []
193         dir = InterfaceStats.TX
194         for ifstat in self.get_if_stats(reverse):
195             # starts at TX then RX then TX again etc...
196             labels.append(ifstat.get_display_name(dir, aggregate=aggregate))
197             dir = InterfaceStats.get_reverse_direction(dir)
198         return labels
199
200     def get_stats(self, reverse=False):
201         """Get the list of packet counters and latency data for this packet path stats.
202
203         return: a dict of packet counters and latency stats
204
205         {'packets': [2000054, 1999996, 1999996],
206          'min_usec': 10, 'max_usec': 187, 'avg_usec': 45},
207         """
208         counters = []
209         dir = InterfaceStats.TX
210         for ifstat in self.get_if_stats(reverse):
211             # starts at TX then RX then TX again etc...
212             if ifstat.shared and not self.aggregate:
213                 # shared if stats countesr are only shown in aggregate pps
214                 counters.append('')
215             else:
216                 counters.append(ifstat.get_packet_count(dir))
217             dir = InterfaceStats.get_reverse_direction(dir)
218
219         # latency: use port 0 latency for forward, port 1 latency for reverse
220         latency = self.latencies[1] if reverse else self.latencies[0]
221
222         if latency.available():
223             results = {'lat_min_usec': latency.min_usec,
224                        'lat_max_usec': latency.max_usec,
225                        'lat_avg_usec': latency.avg_usec}
226         else:
227             results = {}
228         results['packets'] = counters
229         return results
230
231
232 class PacketPathStatsManager(object):
233     """Manages all the packet path stats for all chains.
234
235     Each run will generate packet path stats for 1 or more chains.
236     """
237
238     def __init__(self, pps_list):
239         """Create a packet path stats intance with the list of associated if stats.
240
241         pps_list: a list of packet path stats indexed by the chain id.
242         All packet path stats must have the same length.
243         """
244         self.pps_list = pps_list
245
246     def insert_pps_list(self, chain_index, if_stats):
247         """Insert a list of interface stats for given chain right after the first in the list.
248
249         chain_index: index of chain where to insert
250         if_stats: list of interface stats to insert
251         """
252         # use slicing to insert the list
253         self.pps_list[chain_index].if_stats[1:1] = if_stats
254
255     def _get_if_agg_name(self, reverse):
256         """Get the aggegated name for all interface stats across all pps.
257
258         return: a list of aggregated names for each position of the chain for all chains
259
260         The agregated name is the interface stats name if there is only 1 chain.
261         Otherwise it is the common prefix for all interface stats names at same position in the
262         chain.
263         """
264         # if there is only one chain, use the if_stats names directly
265         return self.pps_list[0].get_header_labels(reverse, aggregate=(len(self.pps_list) > 1))
266
267     def _get_results(self, reverse=False):
268         """Get the digested stats for the forward or reverse directions.
269
270         return: a dict with all the labels, total and per chain counters
271         """
272         chains = {}
273         # insert the aggregated row if applicable
274         if len(self.pps_list) > 1:
275             agg_pps = PacketPathStats.get_agg_packet_path_stats(self.pps_list)
276             chains['total'] = agg_pps.get_stats(reverse)
277
278         for index, pps in enumerate(self.pps_list):
279             chains[index] = pps.get_stats(reverse)
280         return {'interfaces': self._get_if_agg_name(reverse),
281                 'chains': chains}
282
283     def get_results(self):
284         """Get the digested stats for the forward and reverse directions.
285
286         return: a dictionary of results for each direction and each chain
287
288         Example:
289
290         {
291             'Forward': {
292                 'interfaces': ['Port0', 'vhost0', 'Port1'],
293                 'chains': {
294                     0: {'packets': [2000054, 1999996, 1999996],
295                         'min_usec': 10,
296                         'max_usec': 187,
297                         'avg_usec': 45},
298                     1: {...},
299                     'total': {...}
300                 }
301             },
302             'Reverse': {...
303             }
304         }
305
306         """
307         results = {'Forward': self._get_results(),
308                    'Reverse': self._get_results(reverse=True)}
309         return results