d6b9a68179818c922500894e9361f09ab53e619d
[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 hdrh.histogram import HdrHistogram
25 from .traffic_gen.traffic_base import Latency
26
27 class InterfaceStats(object):
28     """A class to hold the RX and TX counters for a virtual or physical interface.
29
30     An interface stats instance can represent a real interface (e.g. traffic gen port or
31     vhost interface) or can represent an aggegation of multiple interfaces when packets
32     are faned out (e.g. one vlan subinterface can fan out to multiple vhost interfaces
33     in the case of multi-chaining and when the network is shared across chains).
34     """
35
36     TX = 0
37     RX = 1
38
39     def __init__(self, name, device, shared=False):
40         """Create a new interface instance.
41
42         name: interface name specific to each chain (e.g. "trex port 0 chain 0")
43         device: on which device this interface resides (e.g. "trex server")
44         fetch_tx_rx: a fetch method that takes name, chain_index and returns a (tx, rx) tuple
45         shared: if true this interface stats is shared across all chains
46         """
47         self.name = name
48         self.device = device
49         self.shared = shared
50         # RX and TX counters for this interface
51         # A None value can be set to mean that the data is not available
52         self.tx = 0
53         self.rx = 0
54         # This is a special field to hold an optional total rx count that is only
55         # used for column aggregation to compute a total intertface stats
56         # Set to non zero to be picked by the add interface stats method for rx total
57         self.rx_total = None
58
59     def get_packet_count(self, direction):
60         """Get packet count for given direction.
61
62         direction: InterfaceStats.TX or InterfaceStats.RX
63         """
64         return self.tx if direction == InterfaceStats.TX else self.rx
65
66     @staticmethod
67     def get_reverse_direction(direction):
68         """Get the reverse direction of a given direction.
69
70         direction: InterfaceStats.TX or InterfaceStats.RX
71         return: RX if TX given, or TX is RX given
72         """
73         return 1 - direction
74
75     @staticmethod
76     def get_direction_name(direction):
77         """Get the rdisplay name of a given direction.
78
79         direction: InterfaceStats.TX or InterfaceStats.RX
80         return: "TX" or "RX"
81         """
82         if direction == InterfaceStats.TX:
83             return 'TX'
84         return 'RX'
85
86     def add_if_stats(self, if_stats):
87         """Add another ifstats to this instance."""
88         def added_counter(old_value, new_value_to_add):
89             if new_value_to_add:
90                 if old_value is None:
91                     return new_value_to_add
92                 return old_value + new_value_to_add
93             return old_value
94
95         self.tx = added_counter(self.tx, if_stats.tx)
96         self.rx = added_counter(self.rx, if_stats.rx)
97         # Add special rx total value if set
98         self.rx = added_counter(self.rx, if_stats.rx_total)
99
100     def update_stats(self, tx, rx, diff):
101         """Update stats for this interface.
102
103         tx: new TX packet count
104         rx: new RX packet count
105         diff: if True, perform a diff of new value with previous baselined value,
106               otherwise store the new value
107         """
108         if diff:
109             self.tx = tx - self.tx
110             self.rx = rx - self.rx
111         else:
112             self.tx = tx
113             self.rx = rx
114
115     def get_display_name(self, dir, name=None, aggregate=False):
116         """Get the name to use to display stats for this interface stats.
117
118         dir: direction InterfaceStats.TX or InterfaceStats.RX
119         name: override self.name
120         aggregate: true if this is for an aggregate of multiple chains
121         """
122         if name is None:
123             name = self.name
124         return self.device + '.' + InterfaceStats.get_direction_name(dir) + '.' + name
125
126
127 class PacketPathStats(object):
128     """Manage the packet path stats for 1 chain in both directions.
129
130     A packet path stats instance manages an ordered list of InterfaceStats objects
131     that can be traversed in the forward and reverse direction to display packet
132     counters in each direction.
133     The requirement is that RX and TX counters must always alternate as we travel
134     along one direction. For example with 4 interfaces per chain:
135     [ifstat0, ifstat1, ifstat2, ifstat3]
136     Packet counters in the forward direction are:
137     [ifstat0.TX, ifstat1.RX, ifstat2.TX, ifstat3.RX]
138     Packet counters in the reverse direction are:
139     [ifstat3.TX, ifstat2.RX, ifstat1.TX, ifstat0.RX]
140
141     A packet path stats also carries the latency data for each direction of the
142     chain.
143     """
144
145     def __init__(self, config, if_stats, aggregate=False):
146         """Create a packet path stats intance with the list of associated if stats.
147
148         if_stats: a list of interface stats that compose this packet path stats
149         aggregate: True if this is an aggregate packet path stats
150
151         Aggregate packet path stats are the only one that should show counters for shared
152         interface stats
153         """
154         self.config = config
155         self.if_stats = if_stats
156         # latency for packets sent from port 0 and 1
157         self.latencies = [Latency(), Latency()]
158         self.aggregate = aggregate
159
160
161     def add_packet_path_stats(self, pps):
162         """Add another packet path stat to this instance.
163
164         pps: the other packet path stats to add to this instance
165
166         This is used only for aggregating/collapsing multiple pps into 1
167         to form a "total" pps
168         """
169         for index, ifstats in enumerate(self.if_stats):
170             # shared interface stats must not be self added
171             if not ifstats.shared:
172                 ifstats.add_if_stats(pps.if_stats[index])
173
174     @staticmethod
175     def get_agg_packet_path_stats(config, pps_list):
176         """Get the aggregated packet path stats from a list of packet path stats.
177
178         Interface counters are added, latency stats are updated.
179         """
180         agg_pps = None
181         for pps in pps_list:
182             if agg_pps is None:
183                 # Get a clone of the first in the list
184                 agg_pps = PacketPathStats(config, pps.get_cloned_if_stats(), aggregate=True)
185             else:
186                 agg_pps.add_packet_path_stats(pps)
187         # aggregate all latencies
188         agg_pps.latencies = [Latency([pps.latencies[port] for pps in pps_list])
189                              for port in [0, 1]]
190         return agg_pps
191
192     def get_if_stats(self, reverse=False):
193         """Get interface stats for given direction.
194
195         reverse: if True, get the list of interface stats in the reverse direction
196                  else (default) gets the ist in the forward direction.
197         return: the list of interface stats indexed by the chain index
198         """
199         return self.if_stats[::-1] if reverse else self.if_stats
200
201     def get_cloned_if_stats(self):
202         """Get a clone copy of the interface stats list."""
203         return [copy.copy(ifstat) for ifstat in self.if_stats]
204
205
206     def get_header_labels(self, reverse=False, aggregate=False):
207         """Get the list of header labels for this packet path stats."""
208         labels = []
209         dir = InterfaceStats.TX
210         for ifstat in self.get_if_stats(reverse):
211             # starts at TX then RX then TX again etc...
212             labels.append(ifstat.get_display_name(dir, aggregate=aggregate))
213             dir = InterfaceStats.get_reverse_direction(dir)
214         return labels
215
216     def get_stats(self, reverse=False):
217         """Get the list of packet counters and latency data for this packet path stats.
218
219         return: a dict of packet counters and latency stats
220
221         {'packets': [2000054, 1999996, 1999996],
222          'min_usec': 10, 'max_usec': 187, 'avg_usec': 45},
223         """
224         counters = []
225         dir = InterfaceStats.TX
226         for ifstat in self.get_if_stats(reverse):
227             # starts at TX then RX then TX again etc...
228             if ifstat.shared and not self.aggregate:
229                 # shared if stats countesr are only shown in aggregate pps
230                 counters.append('')
231             else:
232                 counters.append(ifstat.get_packet_count(dir))
233             dir = InterfaceStats.get_reverse_direction(dir)
234
235         # latency: use port 0 latency for forward, port 1 latency for reverse
236         latency = self.latencies[1] if reverse else self.latencies[0]
237
238         if latency.available():
239             results = {'lat_min_usec': latency.min_usec,
240                        'lat_max_usec': latency.max_usec,
241                        'lat_avg_usec': latency.avg_usec}
242             if latency.hdrh:
243                 results['hdrh'] = latency.hdrh
244                 decoded_histogram = HdrHistogram.decode(latency.hdrh)
245                 # override min max and avg from hdrh
246                 results['lat_min_usec'] = decoded_histogram.get_min_value()
247                 results['lat_max_usec'] = decoded_histogram.get_max_value()
248                 results['lat_avg_usec'] = decoded_histogram.get_mean_value()
249                 results['lat_percentile'] = {}
250                 for percentile in self.config.lat_percentiles:
251                     results['lat_percentile'][percentile] = decoded_histogram.\
252                         get_value_at_percentile(percentile)
253
254         else:
255             results = {}
256         results['packets'] = counters
257         return results
258
259
260 class PacketPathStatsManager(object):
261     """Manages all the packet path stats for all chains.
262
263     Each run will generate packet path stats for 1 or more chains.
264     """
265
266     def __init__(self, config, pps_list):
267         """Create a packet path stats intance with the list of associated if stats.
268
269         pps_list: a list of packet path stats indexed by the chain id.
270         All packet path stats must have the same length.
271         """
272         self.config = config
273         self.pps_list = pps_list
274
275     def insert_pps_list(self, chain_index, if_stats):
276         """Insert a list of interface stats for given chain right after the first in the list.
277
278         chain_index: index of chain where to insert
279         if_stats: list of interface stats to insert
280         """
281         # use slicing to insert the list
282         self.pps_list[chain_index].if_stats[1:1] = if_stats
283
284     def _get_if_agg_name(self, reverse):
285         """Get the aggegated name for all interface stats across all pps.
286
287         return: a list of aggregated names for each position of the chain for all chains
288
289         The agregated name is the interface stats name if there is only 1 chain.
290         Otherwise it is the common prefix for all interface stats names at same position in the
291         chain.
292         """
293         # if there is only one chain, use the if_stats names directly
294         return self.pps_list[0].get_header_labels(reverse, aggregate=(len(self.pps_list) > 1))
295
296     def _get_results(self, reverse=False):
297         """Get the digested stats for the forward or reverse directions.
298
299         return: a dict with all the labels, total and per chain counters
300         """
301         chains = {}
302         # insert the aggregated row if applicable
303         if len(self.pps_list) > 1:
304             agg_pps = PacketPathStats.get_agg_packet_path_stats(self.config, self.pps_list)
305             chains['total'] = agg_pps.get_stats(reverse)
306
307         for index, pps in enumerate(self.pps_list):
308             chains[str(index)] = pps.get_stats(reverse)
309         return {'interfaces': self._get_if_agg_name(reverse),
310                 'chains': chains}
311
312     def get_results(self):
313         """Get the digested stats for the forward and reverse directions.
314
315         return: a dictionary of results for each direction and each chain
316
317         Example:
318
319         {
320             'Forward': {
321                 'interfaces': ['Port0', 'vhost0', 'Port1'],
322                 'chains': {
323                     '0': {'packets': [2000054, 1999996, 1999996],
324                         'min_usec': 10,
325                         'max_usec': 187,
326                         'avg_usec': 45},
327                     '1': {...},
328                     'total': {...}
329                 }
330             },
331             'Reverse': {...
332             }
333         }
334
335         """
336         results = {'Forward': self._get_results(),
337                    'Reverse': self._get_results(reverse=True)}
338         return results