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