trex_stl_stats.py revision 2dab6b6d
1#!/router/bin/python
2
3from .utils import text_tables
4from .utils.text_opts import format_text, format_threshold, format_num
5from .trex_stl_types import StatNotAvailable, is_integer
6from .trex_stl_exceptions import STLError
7
8from collections import namedtuple, OrderedDict, deque
9import sys
10import copy
11import datetime
12import time
13import re
14import math
15import threading
16import pprint
17
18GLOBAL_STATS = 'g'
19PORT_STATS = 'p'
20PORT_GRAPH = 'pg'
21PORT_STATUS = 'ps'
22STREAMS_STATS = 's'
23LATENCY_STATS = 'ls'
24LATENCY_HISTOGRAM = 'lh'
25CPU_STATS = 'c'
26MBUF_STATS = 'm'
27EXTENDED_STATS = 'x'
28EXTENDED_INC_ZERO_STATS = 'xz'
29
30ALL_STATS_OPTS = [GLOBAL_STATS, PORT_STATS, PORT_STATUS, STREAMS_STATS, LATENCY_STATS, PORT_GRAPH, LATENCY_HISTOGRAM, CPU_STATS, MBUF_STATS, EXTENDED_STATS, EXTENDED_INC_ZERO_STATS]
31COMPACT = [GLOBAL_STATS, PORT_STATS]
32GRAPH_PORT_COMPACT = [GLOBAL_STATS, PORT_GRAPH]
33SS_COMPAT = [GLOBAL_STATS, STREAMS_STATS] # stream stats
34LS_COMPAT = [GLOBAL_STATS, LATENCY_STATS] # latency stats
35LH_COMPAT = [GLOBAL_STATS, LATENCY_HISTOGRAM] # latency histogram
36UT_COMPAT = [GLOBAL_STATS, CPU_STATS, MBUF_STATS] # utilization
37
38ExportableStats = namedtuple('ExportableStats', ['raw_data', 'text_table'])
39
40def round_float (f):
41    return float("%.2f" % f) if type(f) is float else f
42
43def try_int(i):
44    try:
45        return int(i)
46    except:
47        return i
48
49# deep mrege of dicts dst = src + dst
50def deep_merge_dicts (dst, src):
51    for k, v in src.items():
52        # if not exists - deep copy it
53        if not k in dst:
54            dst[k] = copy.deepcopy(v)
55        else:
56            if isinstance(v, dict):
57                deep_merge_dicts(dst[k], v)
58
59# BPS L1 from pps and BPS L2
60def calc_bps_L1 (bps, pps):
61    if (pps == 0) or (bps == 0):
62        return 0
63
64    factor = bps / (pps * 8.0)
65    return bps * ( 1 + (20 / factor) )
66#
67
68def is_intable (value):
69    try:
70        int(value)
71        return True
72    except ValueError:
73        return False
74
75# use to calculate diffs relative to the previous values
76# for example, BW
77def calculate_diff (samples):
78    total = 0.0
79
80    weight_step = 1.0 / sum(range(0, len(samples)))
81    weight = weight_step
82
83    for i in range(0, len(samples) - 1):
84        current = samples[i] if samples[i] > 0 else 1
85        next = samples[i + 1] if samples[i + 1] > 0 else 1
86
87        s = 100 * ((float(next) / current) - 1.0)
88
89        # block change by 100%
90        total  += (min(s, 100) * weight)
91        weight += weight_step
92
93    return total
94
95
96# calculate by absolute values and not relatives (useful for CPU usage in % and etc.)
97def calculate_diff_raw (samples):
98    total = 0.0
99
100    weight_step = 1.0 / sum(range(0, len(samples)))
101    weight = weight_step
102
103    for i in range(0, len(samples) - 1):
104        current = samples[i]
105        next = samples[i + 1]
106
107        total  += ( (next - current) * weight )
108        weight += weight_step
109
110    return total
111
112get_number_of_bytes_cache = {}
113# get number of bytes: '64b'->64, '9kb'->9000 etc.
114def get_number_of_bytes(val):
115    if val not in get_number_of_bytes_cache:
116        get_number_of_bytes_cache[val] = int(val[:-1].replace('k', '000'))
117    return get_number_of_bytes_cache[val]
118
119# a simple object to keep a watch over a field
120class WatchedField(object):
121
122    def __init__ (self, name, suffix, high_th, low_th, events_handler):
123        self.name           = name
124        self.suffix         = suffix
125        self.high_th        = high_th
126        self.low_th         = low_th
127        self.events_handler = events_handler
128
129        self.hot     = False
130        self.current = None
131
132    def update (self, value):
133        if value is None:
134            return
135
136        if value > self.high_th and not self.hot:
137            self.events_handler.log_warning("{0} is high: {1}{2}".format(self.name, value, self.suffix))
138            self.hot = True
139
140        if value < self.low_th and self.hot:
141            self.hot = False
142
143        self.current = value
144
145
146
147class CTRexInfoGenerator(object):
148    """
149    This object is responsible of generating stats and information from objects maintained at
150    STLClient and the ports.
151    """
152
153    def __init__(self, global_stats_ref, ports_dict_ref, rx_stats_ref, latency_stats_ref, util_stats_ref, xstats_ref, async_monitor):
154        self._global_stats = global_stats_ref
155        self._ports_dict = ports_dict_ref
156        self._rx_stats_ref = rx_stats_ref
157        self._latency_stats_ref = latency_stats_ref
158        self._util_stats_ref = util_stats_ref
159        self._xstats_ref = xstats_ref
160        self._async_monitor = async_monitor
161
162    def generate_single_statistic(self, port_id_list, statistic_type):
163        if statistic_type == GLOBAL_STATS:
164            return self._generate_global_stats()
165
166        elif statistic_type == PORT_STATS:
167            return self._generate_port_stats(port_id_list)
168
169        elif statistic_type == PORT_GRAPH:
170            return self._generate_port_graph(port_id_list)
171
172        elif statistic_type == PORT_STATUS:
173            return self._generate_port_status(port_id_list)
174
175        elif statistic_type == STREAMS_STATS:
176            return self._generate_streams_stats()
177
178        elif statistic_type == LATENCY_STATS:
179            return self._generate_latency_stats()
180
181        elif statistic_type == LATENCY_HISTOGRAM:
182            return self._generate_latency_histogram()
183
184        elif statistic_type == CPU_STATS:
185            return self._generate_cpu_util_stats()
186
187        elif statistic_type == MBUF_STATS:
188            return self._generate_mbuf_util_stats()
189
190        elif statistic_type == EXTENDED_STATS:
191            return self._generate_xstats(port_id_list, include_zero_lines = False)
192
193        elif statistic_type == EXTENDED_INC_ZERO_STATS:
194            return self._generate_xstats(port_id_list, include_zero_lines = True)
195
196        else:
197            # ignore by returning empty object
198            return {}
199
200    def generate_streams_info(self, port_id_list, stream_id_list):
201        relevant_ports = self.__get_relevant_ports(port_id_list)
202        return_data = OrderedDict()
203
204        for port_obj in relevant_ports:
205            streams_data = self._generate_single_port_streams_info(port_obj, stream_id_list)
206            if not streams_data:
207                continue
208            hdr_key = "Port {port}:".format(port= port_obj.port_id)
209
210            # TODO: test for other ports with same stream structure, and join them
211            return_data[hdr_key] = streams_data
212
213        return return_data
214
215    def _generate_global_stats(self):
216        global_stats = self._global_stats
217
218        stats_data_left = OrderedDict([("connection", "{host}, Port {port}".format(host=global_stats.connection_info.get("server"),
219                                                                     port=global_stats.connection_info.get("sync_port"))),
220                             ("version", "{ver}, UUID: {uuid}".format(ver=global_stats.server_version.get("version", "N/A"),
221                                                                      uuid="N/A")),
222
223                             ("cpu_util.", "{0}% @ {2} cores ({3} per port) {1}".format( format_threshold(round_float(global_stats.get("m_cpu_util")), [85, 100], [0, 85]),
224                                                                                         global_stats.get_trend_gui("m_cpu_util", use_raw = True),
225                                                                                         global_stats.system_info.get('dp_core_count'),
226                                                                                         global_stats.system_info.get('dp_core_count_per_port'),
227                                                                                         )),
228
229                             ("rx_cpu_util.", "{0}% {1}".format( format_threshold(round_float(global_stats.get("m_rx_cpu_util")), [85, 100], [0, 85]),
230                                                                global_stats.get_trend_gui("m_rx_cpu_util", use_raw = True))),
231
232                             ("async_util.", "{0}% / {1}".format( format_threshold(round_float(self._async_monitor.get_cpu_util()), [85, 100], [0, 85]),
233                                                                 format_num(self._async_monitor.get_bps() / 8.0, suffix = "B/sec"))),
234                            ])
235
236        stats_data_right = OrderedDict([
237                             ("total_tx_L2", "{0} {1}".format( global_stats.get("m_tx_bps", format=True, suffix="b/sec"),
238                                                                global_stats.get_trend_gui("m_tx_bps"))),
239
240                             ("total_tx_L1", "{0} {1}".format( global_stats.get("m_tx_bps_L1", format=True, suffix="b/sec"),
241                                                                global_stats.get_trend_gui("m_tx_bps_L1"))),
242
243                             ("total_rx", "{0} {1}".format( global_stats.get("m_rx_bps", format=True, suffix="b/sec"),
244                                                              global_stats.get_trend_gui("m_rx_bps"))),
245
246                             ("total_pps", "{0} {1}".format( global_stats.get("m_tx_pps", format=True, suffix="pkt/sec"),
247                                                              global_stats.get_trend_gui("m_tx_pps"))),
248
249                             ("drop_rate", "{0}".format( format_num(global_stats.get("m_rx_drop_bps"),
250                                                                    suffix = 'b/sec',
251                                                                    opts = 'green' if (global_stats.get("m_rx_drop_bps")== 0) else 'red'),
252                                                            )),
253
254                             ("queue_full", "{0}".format( format_num(global_stats.get_rel("m_total_queue_full"),
255                                                                     suffix = 'pkts',
256                                                                     compact = False,
257                                                                     opts = 'green' if (global_stats.get_rel("m_total_queue_full")== 0) else 'red'))),
258                             ])
259
260        # build table representation
261        stats_table = text_tables.TRexTextInfo()
262        stats_table.set_cols_align(["l", "l"])
263        stats_table.set_deco(0)
264        stats_table.set_cols_width([50, 45])
265        max_lines = max(len(stats_data_left), len(stats_data_right))
266        for line_num in range(max_lines):
267            row = []
268            if line_num < len(stats_data_left):
269                key = list(stats_data_left.keys())[line_num]
270                row.append('{:<12} : {}'.format(key, stats_data_left[key]))
271            else:
272                row.append('')
273            if line_num < len(stats_data_right):
274                key = list(stats_data_right.keys())[line_num]
275                row.append('{:<12} : {}'.format(key, stats_data_right[key]))
276            else:
277                row.append('')
278            stats_table.add_row(row)
279
280        return {"global_statistics": ExportableStats(None, stats_table)}
281
282    def _generate_streams_stats (self):
283        flow_stats = self._rx_stats_ref
284        # for TUI - maximum 4
285        pg_ids = list(filter(is_intable, flow_stats.latest_stats.keys()))[:4]
286        stream_count = len(pg_ids)
287
288        sstats_data = OrderedDict([ ('Tx pps',  []),
289                                        ('Tx bps L2',      []),
290                                        ('Tx bps L1',      []),
291                                        ('---', [''] * stream_count),
292                                        ('Rx pps',      []),
293                                        ('Rx bps',      []),
294                                        ('----', [''] * stream_count),
295                                        ('opackets',    []),
296                                        ('ipackets',    []),
297                                        ('obytes',      []),
298                                        ('ibytes',      []),
299                                        ('-----', [''] * stream_count),
300                                        ('tx_pkts',     []),
301                                        ('rx_pkts',     []),
302                                        ('tx_bytes',    []),
303                                        ('rx_bytes',    [])
304                                      ])
305
306
307
308        # maximum 4
309        for pg_id in pg_ids:
310
311            sstats_data['Tx pps'].append(flow_stats.get([pg_id, 'tx_pps_lpf', 'total'], format = True, suffix = "pps"))
312            sstats_data['Tx bps L2'].append(flow_stats.get([pg_id, 'tx_bps_lpf', 'total'], format = True, suffix = "bps"))
313
314            sstats_data['Tx bps L1'].append(flow_stats.get([pg_id, 'tx_bps_L1_lpf', 'total'], format = True, suffix = "bps"))
315
316            sstats_data['Rx pps'].append(flow_stats.get([pg_id, 'rx_pps_lpf', 'total'], format = True, suffix = "pps"))
317            sstats_data['Rx bps'].append(flow_stats.get([pg_id, 'rx_bps_lpf', 'total'], format = True, suffix = "bps"))
318
319            sstats_data['opackets'].append(flow_stats.get_rel([pg_id, 'tx_pkts', 'total']))
320            sstats_data['ipackets'].append(flow_stats.get_rel([pg_id, 'rx_pkts', 'total']))
321            sstats_data['obytes'].append(flow_stats.get_rel([pg_id, 'tx_bytes', 'total']))
322            sstats_data['ibytes'].append(flow_stats.get_rel([pg_id, 'rx_bytes', 'total']))
323            sstats_data['tx_bytes'].append(flow_stats.get_rel([pg_id, 'tx_bytes', 'total'], format = True, suffix = "B"))
324            sstats_data['rx_bytes'].append(flow_stats.get_rel([pg_id, 'rx_bytes', 'total'], format = True, suffix = "B"))
325            sstats_data['tx_pkts'].append(flow_stats.get_rel([pg_id, 'tx_pkts', 'total'], format = True, suffix = "pkts"))
326            sstats_data['rx_pkts'].append(flow_stats.get_rel([pg_id, 'rx_pkts', 'total'], format = True, suffix = "pkts"))
327
328
329        stats_table = text_tables.TRexTextTable()
330        stats_table.set_cols_align(["l"] + ["r"] * stream_count)
331        stats_table.set_cols_width([10] + [17]   * stream_count)
332        stats_table.set_cols_dtype(['t'] + ['t'] * stream_count)
333
334        stats_table.add_rows([[k] + v
335                              for k, v in sstats_data.items()],
336                              header=False)
337
338        header = ["PG ID"] + [key for key in pg_ids]
339        stats_table.header(header)
340
341        return {"streams_statistics": ExportableStats(sstats_data, stats_table)}
342
343    def _generate_latency_stats(self):
344        lat_stats = self._latency_stats_ref
345        latency_window_size = 10
346
347        # for TUI - maximum 5
348        pg_ids = list(filter(is_intable, lat_stats.latest_stats.keys()))[:5]
349        stream_count = len(pg_ids)
350        lstats_data = OrderedDict([('TX pkts',       []),
351                                   ('RX pkts',       []),
352                                   ('Max latency',   []),
353                                   ('Avg latency',   []),
354                                   ('-- Window --', [''] * stream_count),
355                                   ('Last (max)',     []),
356                                  ] + [('Last-%s' % i, []) for i in range(1, latency_window_size)] + [
357                                   ('---', [''] * stream_count),
358                                   ('Jitter',        []),
359                                   ('----', [''] * stream_count),
360                                   ('Errors',        []),
361                                  ])
362
363        with lat_stats.lock:
364            history = [x for x in lat_stats.history]
365        flow_stats = self._rx_stats_ref.get_stats()
366        for pg_id in pg_ids:
367            lstats_data['TX pkts'].append(flow_stats[pg_id]['tx_pkts']['total'] if pg_id in flow_stats else '')
368            lstats_data['RX pkts'].append(flow_stats[pg_id]['rx_pkts']['total'] if pg_id in flow_stats else '')
369            lstats_data['Avg latency'].append(try_int(lat_stats.get([pg_id, 'latency', 'average'])))
370            lstats_data['Max latency'].append(try_int(lat_stats.get([pg_id, 'latency', 'total_max'])))
371            lstats_data['Last (max)'].append(try_int(lat_stats.get([pg_id, 'latency', 'last_max'])))
372            for i in range(1, latency_window_size):
373                val = history[-i - 1].get(pg_id, {}).get('latency', {}).get('last_max', '') if len(history) > i else ''
374                lstats_data['Last-%s' % i].append(try_int(val))
375            lstats_data['Jitter'].append(try_int(lat_stats.get([pg_id, 'latency', 'jitter'])))
376            errors = 0
377            seq_too_low = lat_stats.get([pg_id, 'err_cntrs', 'seq_too_low'])
378            if is_integer(seq_too_low):
379                errors += seq_too_low
380            seq_too_high = lat_stats.get([pg_id, 'err_cntrs', 'seq_too_high'])
381            if is_integer(seq_too_high):
382                errors += seq_too_high
383            lstats_data['Errors'].append(format_num(errors,
384                                            opts = 'green' if errors == 0 else 'red'))
385
386
387        stats_table = text_tables.TRexTextTable()
388        stats_table.set_cols_align(["l"] + ["r"] * stream_count)
389        stats_table.set_cols_width([12] + [14]   * stream_count)
390        stats_table.set_cols_dtype(['t'] + ['t'] * stream_count)
391        stats_table.add_rows([[k] + v
392                              for k, v in lstats_data.items()],
393                              header=False)
394
395        header = ["PG ID"] + [key for key in pg_ids]
396        stats_table.header(header)
397
398        return {"latency_statistics": ExportableStats(lstats_data, stats_table)}
399
400    def _generate_latency_histogram(self):
401        lat_stats = self._latency_stats_ref.latest_stats
402        max_histogram_size = 17
403
404        # for TUI - maximum 5
405        pg_ids = list(filter(is_intable, lat_stats.keys()))[:5]
406
407        merged_histogram = {}
408        for pg_id in pg_ids:
409            merged_histogram.update(lat_stats[pg_id]['latency']['histogram'])
410        histogram_size = min(max_histogram_size, len(merged_histogram))
411
412        stream_count = len(pg_ids)
413        stats_table = text_tables.TRexTextTable()
414        stats_table.set_cols_align(["l"] + ["r"] * stream_count)
415        stats_table.set_cols_width([12] + [14]   * stream_count)
416        stats_table.set_cols_dtype(['t'] + ['t'] * stream_count)
417
418        for i in range(max_histogram_size - histogram_size):
419            if i == 0 and not merged_histogram:
420                stats_table.add_row(['  No Data'] + [' '] * stream_count)
421            else:
422                stats_table.add_row([' '] * (stream_count + 1))
423        for key in list(reversed(sorted(merged_histogram.keys())))[:histogram_size]:
424            hist_vals = []
425            for pg_id in pg_ids:
426                hist_vals.append(lat_stats[pg_id]['latency']['histogram'].get(key, ' '))
427            stats_table.add_row([key] + hist_vals)
428
429        stats_table.add_row(['- Counters -'] + [' '] * stream_count)
430        err_cntrs_dict = OrderedDict()
431        for pg_id in pg_ids:
432            for err_cntr in sorted(lat_stats[pg_id]['err_cntrs'].keys()):
433                if err_cntr not in err_cntrs_dict:
434                    err_cntrs_dict[err_cntr] = [lat_stats[pg_id]['err_cntrs'][err_cntr]]
435                else:
436                    err_cntrs_dict[err_cntr].append(lat_stats[pg_id]['err_cntrs'][err_cntr])
437        for err_cntr, val_list in err_cntrs_dict.items():
438            stats_table.add_row([err_cntr] + val_list)
439        header = ["PG ID"] + [key for key in pg_ids]
440        stats_table.header(header)
441        return {"latency_histogram": ExportableStats(None, stats_table)}
442
443    def _generate_cpu_util_stats(self):
444        util_stats = self._util_stats_ref.get_stats(use_1sec_cache = True)
445
446        stats_table = text_tables.TRexTextTable()
447        if util_stats:
448            if 'cpu' not in util_stats:
449                raise Exception("Excepting 'cpu' section in stats %s" % util_stats)
450            cpu_stats = util_stats['cpu']
451            hist_len = len(cpu_stats[0]["history"])
452            avg_len = min(5, hist_len)
453            show_len = min(15, hist_len)
454            stats_table.header(['Thread', 'Avg', 'Latest'] + list(range(-1, 0 - show_len, -1)))
455            stats_table.set_cols_align(['l'] + ['r'] * (show_len + 1))
456            stats_table.set_cols_width([10, 3, 6] + [3] * (show_len - 1))
457            stats_table.set_cols_dtype(['t'] * (show_len + 2))
458
459            for i in range(min(14, len(cpu_stats))):
460                history = cpu_stats[i]["history"]
461                ports = cpu_stats[i]["ports"]
462                avg = int(round(sum(history[:avg_len]) / avg_len))
463
464                # decode active ports for core
465                if ports == [-1, -1]:
466                    interfaces = "(IDLE)"
467                elif not -1 in ports:
468                    interfaces = "({:},{:})".format(ports[0], ports[1])
469                else:
470                    interfaces = "({:})".format(ports[0] if ports[0] != -1 else ports[1])
471
472                thread = "{:2} {:^7}".format(i, interfaces)
473                stats_table.add_row([thread, avg] + history[:show_len])
474        else:
475            stats_table.add_row(['No Data.'])
476        return {'cpu_util(%)': ExportableStats(None, stats_table)}
477
478    def _generate_mbuf_util_stats(self):
479        util_stats = self._util_stats_ref.get_stats(use_1sec_cache = True)
480        stats_table = text_tables.TRexTextTable()
481        if util_stats:
482            if 'mbuf_stats' not in util_stats:
483                raise Exception("Excepting 'mbuf_stats' section in stats %s" % util_stats)
484            mbuf_stats = util_stats['mbuf_stats']
485            for mbufs_per_socket in mbuf_stats.values():
486                first_socket_mbufs = mbufs_per_socket
487                break
488            if not self._util_stats_ref.mbuf_types_list:
489                mbuf_keys = list(first_socket_mbufs.keys())
490                mbuf_keys.sort(key = get_number_of_bytes)
491                self._util_stats_ref.mbuf_types_list = mbuf_keys
492            types_len = len(self._util_stats_ref.mbuf_types_list)
493            stats_table.set_cols_align(['l'] + ['r'] * (types_len + 1))
494            stats_table.set_cols_width([10] + [7] * (types_len + 1))
495            stats_table.set_cols_dtype(['t'] * (types_len + 2))
496            stats_table.header([''] + self._util_stats_ref.mbuf_types_list + ['RAM(MB)'])
497            total_list = []
498            sum_totals = 0
499            for mbuf_type in self._util_stats_ref.mbuf_types_list:
500                sum_totals += first_socket_mbufs[mbuf_type][1] * get_number_of_bytes(mbuf_type) + 64
501                total_list.append(first_socket_mbufs[mbuf_type][1])
502            sum_totals *= len(list(mbuf_stats.values()))
503            total_list.append(int(sum_totals/1e6))
504            stats_table.add_row(['Total:'] + total_list)
505            stats_table.add_row(['Used:'] + [''] * (types_len + 1))
506            for socket_name in sorted(list(mbuf_stats.keys())):
507                mbufs = mbuf_stats[socket_name]
508                socket_show_name = socket_name.replace('cpu-', '').replace('-', ' ').capitalize() + ':'
509                sum_used = 0
510                used_list = []
511                percentage_list = []
512                for mbuf_type in self._util_stats_ref.mbuf_types_list:
513                    used = mbufs[mbuf_type][1] - mbufs[mbuf_type][0]
514                    sum_used += used * get_number_of_bytes(mbuf_type) + 64
515                    used_list.append(used)
516                    percentage_list.append('%s%%' % int(100 * used / mbufs[mbuf_type][1]))
517                used_list.append(int(sum_used/1e6))
518                stats_table.add_row([socket_show_name] + used_list)
519                stats_table.add_row(['Percent:'] + percentage_list + [''])
520        else:
521            stats_table.add_row(['No Data.'])
522        return {'mbuf_util': ExportableStats(None, stats_table)}
523
524    def _generate_xstats(self, port_id_list, include_zero_lines = False):
525        relevant_ports = [port.port_id for port in self.__get_relevant_ports(port_id_list)]
526        # get the data on relevant ports
527        xstats_data = OrderedDict()
528        for port_id in relevant_ports:
529            for key, val in self._xstats_ref.get_stats(port_id).items():
530                if key not in xstats_data:
531                    xstats_data[key] = []
532                xstats_data[key].append(val)
533
534        # put into table
535        stats_table = text_tables.TRexTextTable()
536        stats_table.header(['Name:'] + ['Port %s:' % port_id for port_id in relevant_ports])
537        stats_table.set_cols_align(['l'] + ['r'] * len(relevant_ports))
538        stats_table.set_cols_width([30] + [15] * len(relevant_ports))
539        stats_table.set_cols_dtype(['t'] * (len(relevant_ports) + 1))
540        for key, arr in xstats_data.items():
541            if include_zero_lines or list(filter(None, arr)):
542                if len(key) > 28:
543                    key = key[:28]
544                stats_table.add_row([key] + arr)
545        return {'xstats:': ExportableStats(None, stats_table)}
546
547    @staticmethod
548    def _get_rational_block_char(value, range_start, interval):
549        # in Konsole, utf-8 is sometimes printed with artifacts, return ascii for now
550        #return 'X' if value >= range_start + float(interval) / 2 else ' '
551
552        if sys.__stdout__.encoding != 'UTF-8':
553            return 'X' if value >= range_start + float(interval) / 2 else ' '
554
555        value -= range_start
556        ratio = float(value) / interval
557        if ratio <= 0.0625:
558            return u' '         # empty block
559        if ratio <= 0.1875:
560            return u'\u2581'    # 1/8
561        if ratio <= 0.3125:
562            return u'\u2582'    # 2/8
563        if ratio <= 0.4375:
564            return u'\u2583'    # 3/8
565        if ratio <= 0.5625:
566            return u'\u2584'    # 4/8
567        if ratio <= 0.6875:
568            return u'\u2585'    # 5/8
569        if ratio <= 0.8125:
570            return u'\u2586'    # 6/8
571        if ratio <= 0.9375:
572            return u'\u2587'    # 7/8
573        return u'\u2588'        # full block
574
575    def _generate_port_graph(self, port_id_list):
576        relevant_port = self.__get_relevant_ports(port_id_list)[0]
577        hist_len = len(relevant_port.port_stats.history)
578        hist_maxlen = relevant_port.port_stats.history.maxlen
579        util_tx_hist = [0] * (hist_maxlen - hist_len) + [round(relevant_port.port_stats.history[i]['tx_percentage']) for i in range(hist_len)]
580        util_rx_hist = [0] * (hist_maxlen - hist_len) + [round(relevant_port.port_stats.history[i]['rx_percentage']) for i in range(hist_len)]
581
582
583        stats_table = text_tables.TRexTextTable()
584        stats_table.header([' Util(%)', 'TX', 'RX'])
585        stats_table.set_cols_align(['c', 'c', 'c'])
586        stats_table.set_cols_width([8, hist_maxlen, hist_maxlen])
587        stats_table.set_cols_dtype(['t', 't', 't'])
588
589        for y in range(95, -1, -5):
590            stats_table.add_row([y, ''.join([self._get_rational_block_char(util_tx, y, 5) for util_tx in util_tx_hist]),
591                                    ''.join([self._get_rational_block_char(util_rx, y, 5) for util_rx in util_rx_hist])])
592
593        return {"port_graph": ExportableStats({}, stats_table)}
594
595    def _generate_port_stats(self, port_id_list):
596        relevant_ports = self.__get_relevant_ports(port_id_list)
597
598        return_stats_data = {}
599        per_field_stats = OrderedDict([("owner", []),
600                                       ('link', []),
601                                       ("state", []),
602                                       ("speed", []),
603                                       ("CPU util.", []),
604                                       ("--", []),
605                                       ("Tx bps L2", []),
606                                       ("Tx bps L1", []),
607                                       ("Tx pps", []),
608                                       ("Line Util.", []),
609
610                                       ("---", []),
611                                       ("Rx bps", []),
612                                       ("Rx pps", []),
613
614                                       ("----", []),
615                                       ("opackets", []),
616                                       ("ipackets", []),
617                                       ("obytes", []),
618                                       ("ibytes", []),
619                                       ("tx-bytes", []),
620                                       ("rx-bytes", []),
621                                       ("tx-pkts", []),
622                                       ("rx-pkts", []),
623
624                                       ("-----", []),
625                                       ("oerrors", []),
626                                       ("ierrors", []),
627
628                                      ])
629
630        total_stats = CPortStats(None)
631
632        for port_obj in relevant_ports:
633            # fetch port data
634            port_stats = port_obj.generate_port_stats()
635
636            total_stats += port_obj.port_stats
637
638            # populate to data structures
639            return_stats_data[port_obj.port_id] = port_stats
640            self.__update_per_field_dict(port_stats, per_field_stats)
641
642        total_cols = len(relevant_ports)
643        header = ["port"] + [port.port_id for port in relevant_ports]
644
645        if (total_cols > 1):
646            self.__update_per_field_dict(total_stats.generate_stats(), per_field_stats)
647            header += ['total']
648            total_cols += 1
649
650        stats_table = text_tables.TRexTextTable()
651        stats_table.set_cols_align(["l"] + ["r"] * total_cols)
652        stats_table.set_cols_width([10] + [17]   * total_cols)
653        stats_table.set_cols_dtype(['t'] + ['t'] * total_cols)
654
655        stats_table.add_rows([[k] + v
656                              for k, v in per_field_stats.items()],
657                              header=False)
658
659        stats_table.header(header)
660
661        return {"port_statistics": ExportableStats(return_stats_data, stats_table)}
662
663    def _generate_port_status(self, port_id_list):
664        relevant_ports = self.__get_relevant_ports(port_id_list)
665
666        return_stats_data = {}
667        per_field_status = OrderedDict([("driver", []),
668                                        ("description", []),
669                                        ("link speed", []),
670                                        ("status", []),
671                                        ("promiscuous", []),
672                                        ("flow ctrl", []),
673                                        ("--", []),
674                                        ("HW src mac", []),
675                                        ("SW src mac", []),
676                                        ("SW dst mac", []),
677                                        ("---", []),
678                                        ("PCI Address", []),
679                                        ("NUMA Node", []),
680                                        ]
681                                       )
682
683        for port_obj in relevant_ports:
684            # fetch port data
685            # port_stats = self._async_stats.get_port_stats(port_obj.port_id)
686            port_status = port_obj.generate_port_status()
687
688            # populate to data structures
689            return_stats_data[port_obj.port_id] = port_status
690
691            self.__update_per_field_dict(port_status, per_field_status)
692
693        stats_table = text_tables.TRexTextTable()
694        stats_table.set_cols_align(["l"] + ["c"]*len(relevant_ports))
695        stats_table.set_cols_width([15] + [20] * len(relevant_ports))
696
697        stats_table.add_rows([[k] + v
698                              for k, v in per_field_status.items()],
699                             header=False)
700        stats_table.header(["port"] + [port.port_id
701                                       for port in relevant_ports])
702
703        return {"port_status": ExportableStats(return_stats_data, stats_table)}
704
705    def _generate_single_port_streams_info(self, port_obj, stream_id_list):
706
707        return_streams_data = port_obj.generate_loaded_streams_sum()
708
709        if not return_streams_data.get("streams"):
710            # we got no streams available
711            return None
712
713        # FORMAT VALUES ON DEMAND
714
715        # because we mutate this - deep copy before
716        return_streams_data = copy.deepcopy(return_streams_data)
717
718        p_type_field_len = 0
719
720        for stream_id, stream_id_sum in return_streams_data['streams'].items():
721            stream_id_sum['packet_type'] = self._trim_packet_headers(stream_id_sum['packet_type'], 30)
722            p_type_field_len = max(p_type_field_len, len(stream_id_sum['packet_type']))
723
724        info_table = text_tables.TRexTextTable()
725        info_table.set_cols_align(["c"] + ["l"] + ["r"] + ["c"] + ["r"] + ["c"])
726        info_table.set_cols_width([10]   + [p_type_field_len]  + [8]   + [16]  + [15]  + [12])
727        info_table.set_cols_dtype(["t"] + ["t"] + ["t"] + ["t"] + ["t"] + ["t"])
728
729        info_table.add_rows([v.values()
730                             for k, v in return_streams_data['streams'].items()],
731                             header=False)
732        info_table.header(["ID", "packet type", "length", "mode", "rate", "next stream"])
733
734        return ExportableStats(return_streams_data, info_table)
735
736
737    def __get_relevant_ports(self, port_id_list):
738        # fetch owned ports
739        ports = [port_obj
740                 for _, port_obj in self._ports_dict.items()
741                 if port_obj.port_id in port_id_list]
742
743        # display only the first FOUR options, by design
744        if len(ports) > 4:
745            #self.logger is not defined
746            #self.logger.log(format_text("[WARNING]: ", 'magenta', 'bold'), format_text("displaying up to 4 ports", 'magenta'))
747            ports = ports[:4]
748        return ports
749
750    def __update_per_field_dict(self, dict_src_data, dict_dest_ref):
751        for key, val in dict_src_data.items():
752            if key in dict_dest_ref:
753                dict_dest_ref[key].append(val)
754
755    @staticmethod
756    def _trim_packet_headers(headers_str, trim_limit):
757        if len(headers_str) < trim_limit:
758            # do nothing
759            return headers_str
760        else:
761            return (headers_str[:trim_limit-3] + "...")
762
763
764
765class CTRexStats(object):
766    """ This is an abstract class to represent a stats object """
767
768    def __init__(self):
769        self.reference_stats = {}
770        self.latest_stats = {}
771        self.last_update_ts = time.time()
772        self.history = deque(maxlen = 47)
773        self.lock = threading.Lock()
774        self.has_baseline = False
775
776    ######## abstract methods ##########
777
778    # get stats for user / API
779    def get_stats (self):
780        raise NotImplementedError()
781
782    # generate format stats (for TUI)
783    def generate_stats(self):
784        raise NotImplementedError()
785
786    # called when a snapshot arrives - add more fields
787    def _update (self, snapshot, baseline):
788        raise NotImplementedError()
789
790
791    ######## END abstract methods ##########
792
793    def update(self, snapshot, baseline):
794
795        # no update is valid before baseline
796        if not self.has_baseline and not baseline:
797            return
798
799        # call the underlying method
800        rc = self._update(snapshot)
801        if not rc:
802            return
803
804        # sync one time
805        if not self.has_baseline and baseline:
806            self.reference_stats = copy.deepcopy(self.latest_stats)
807            self.has_baseline = True
808
809        # save history
810        with self.lock:
811            self.history.append(self.latest_stats)
812
813
814    def clear_stats(self):
815        self.reference_stats = copy.deepcopy(self.latest_stats)
816        self.history.clear()
817
818
819    def invalidate (self):
820        self.latest_stats = {}
821
822
823    def _get (self, src, field, default = None):
824        if isinstance(field, list):
825            # deep
826            value = src
827            for level in field:
828                if not level in value:
829                    return default
830                value = value[level]
831        else:
832            # flat
833            if not field in src:
834                return default
835            value = src[field]
836
837        return value
838
839    def get(self, field, format=False, suffix="", opts = None):
840        value = self._get(self.latest_stats, field)
841        if value == None:
842            return 'N/A'
843
844        return value if not format else format_num(value, suffix = suffix, opts = opts)
845
846
847    def get_rel(self, field, format=False, suffix=""):
848        ref_value = self._get(self.reference_stats, field)
849        latest_value = self._get(self.latest_stats, field)
850
851        # latest value is an aggregation - must contain the value
852        if latest_value == None:
853            return 'N/A'
854
855        if ref_value == None:
856            ref_value = 0
857
858        value = latest_value - ref_value
859
860        return value if not format else format_num(value, suffix)
861
862
863    # get trend for a field
864    def get_trend (self, field, use_raw = False, percision = 10.0):
865        if field not in self.latest_stats:
866            return 0
867
868        # not enough history - no trend
869        if len(self.history) < 5:
870            return 0
871
872        # absolute value is too low 0 considered noise
873        if self.latest_stats[field] < percision:
874            return 0
875
876        # must lock, deque is not thread-safe for iteration
877        with self.lock:
878            field_samples = [sample[field] for sample in list(self.history)[-5:]]
879
880        if use_raw:
881            return calculate_diff_raw(field_samples)
882        else:
883            return calculate_diff(field_samples)
884
885
886    def get_trend_gui (self, field, show_value = False, use_raw = False, up_color = 'red', down_color = 'green'):
887        v = self.get_trend(field, use_raw)
888
889        value = abs(v)
890
891        # use arrows if utf-8 is supported
892        if sys.__stdout__.encoding == 'UTF-8':
893            arrow = u'\u25b2' if v > 0 else u'\u25bc'
894        else:
895            arrow = ''
896
897        if sys.version_info < (3,0):
898            arrow = arrow.encode('utf-8')
899
900        color = up_color if v > 0 else down_color
901
902        # change in 1% is not meaningful
903        if value < 1:
904            return ""
905
906        elif value > 5:
907
908            if show_value:
909                return format_text("{0}{0}{0} {1:.2f}%".format(arrow,v), color)
910            else:
911                return format_text("{0}{0}{0}".format(arrow), color)
912
913        elif value > 2:
914
915            if show_value:
916                return format_text("{0}{0} {1:.2f}%".format(arrow,v), color)
917            else:
918                return format_text("{0}{0}".format(arrow), color)
919
920        else:
921            if show_value:
922                return format_text("{0} {1:.2f}%".format(arrow,v), color)
923            else:
924                return format_text("{0}".format(arrow), color)
925
926
927
928class CGlobalStats(CTRexStats):
929
930    def __init__(self, connection_info, server_version, ports_dict_ref, events_handler):
931        super(CGlobalStats, self).__init__()
932
933        self.connection_info = connection_info
934        self.server_version  = server_version
935        self._ports_dict     = ports_dict_ref
936        self.events_handler  = events_handler
937
938        self.watched_cpu_util    = WatchedField('CPU util.', '%', 85, 60, events_handler)
939        self.watched_rx_cpu_util = WatchedField('RX core util.', '%', 85, 60, events_handler)
940
941    def get_stats (self):
942        stats = {}
943
944        # absolute
945        stats['cpu_util']    = self.get("m_cpu_util")
946        stats['rx_cpu_util'] = self.get("m_rx_cpu_util")
947        stats['bw_per_core'] = self.get("m_bw_per_core")
948
949        stats['tx_bps'] = self.get("m_tx_bps")
950        stats['tx_pps'] = self.get("m_tx_pps")
951
952        stats['rx_bps'] = self.get("m_rx_bps")
953        stats['rx_pps'] = self.get("m_rx_pps")
954        stats['rx_drop_bps'] = self.get("m_rx_drop_bps")
955
956        # relatives
957        stats['queue_full'] = self.get_rel("m_total_queue_full")
958
959        return stats
960
961
962
963    def _update(self, snapshot):
964        # L1 bps
965        bps = snapshot.get("m_tx_bps")
966        pps = snapshot.get("m_tx_pps")
967
968        snapshot['m_tx_bps_L1'] = calc_bps_L1(bps, pps)
969
970
971        # simple...
972        self.latest_stats = snapshot
973
974        self.watched_cpu_util.update(snapshot.get('m_cpu_util'))
975        self.watched_rx_cpu_util.update(snapshot.get('m_rx_cpu_util'))
976
977        return True
978
979
980class CPortStats(CTRexStats):
981
982    def __init__(self, port_obj):
983        super(CPortStats, self).__init__()
984        self._port_obj = port_obj
985
986    @staticmethod
987    def __merge_dicts (target, src):
988        for k, v in src.items():
989            if k in target:
990                target[k] += v
991            else:
992                target[k] = v
993
994
995    def __add__ (self, x):
996        if not isinstance(x, CPortStats):
997            raise TypeError("cannot add non stats object to stats")
998
999        # main stats
1000        if not self.latest_stats:
1001            self.latest_stats = {}
1002
1003        self.__merge_dicts(self.latest_stats, x.latest_stats)
1004
1005        # reference stats
1006        if x.reference_stats:
1007            if not self.reference_stats:
1008                self.reference_stats = x.reference_stats.copy()
1009            else:
1010                self.__merge_dicts(self.reference_stats, x.reference_stats)
1011
1012        # history - should be traverse with a lock
1013        with self.lock, x.lock:
1014            if not self.history:
1015                self.history = copy.deepcopy(x.history)
1016            else:
1017                for h1, h2 in zip(self.history, x.history):
1018                    self.__merge_dicts(h1, h2)
1019
1020        return self
1021
1022    # for port we need to do something smarter
1023    def get_stats (self):
1024        stats = {}
1025
1026        stats['opackets'] = self.get_rel("opackets")
1027        stats['ipackets'] = self.get_rel("ipackets")
1028        stats['obytes']   = self.get_rel("obytes")
1029        stats['ibytes']   = self.get_rel("ibytes")
1030        stats['oerrors']  = self.get_rel("oerrors")
1031        stats['ierrors']  = self.get_rel("ierrors")
1032
1033        stats['tx_bps']     = self.get("m_total_tx_bps")
1034        stats['tx_pps']     = self.get("m_total_tx_pps")
1035        stats['tx_bps_L1']  = self.get("m_total_tx_bps_L1")
1036        stats['tx_util']    = self.get("m_tx_util")
1037
1038        stats['rx_bps']     = self.get("m_total_rx_bps")
1039        stats['rx_pps']     = self.get("m_total_rx_pps")
1040        stats['rx_bps_L1']  = self.get("m_total_rx_bps_L1")
1041        stats['rx_util']    = self.get("m_rx_util")
1042
1043        return stats
1044
1045
1046
1047    def _update(self, snapshot):
1048        speed = self._port_obj.get_speed_bps()
1049
1050        # L1 bps
1051        tx_bps  = snapshot.get("m_total_tx_bps")
1052        tx_pps  = snapshot.get("m_total_tx_pps")
1053        rx_bps  = snapshot.get("m_total_rx_bps")
1054        rx_pps  = snapshot.get("m_total_rx_pps")
1055        ts_diff = 0.5 # TODO: change this to real ts diff from server
1056
1057        bps_tx_L1 = calc_bps_L1(tx_bps, tx_pps)
1058        bps_rx_L1 = calc_bps_L1(rx_bps, rx_pps)
1059
1060        snapshot['m_total_tx_bps_L1'] = bps_tx_L1
1061        if speed:
1062            snapshot['m_tx_util'] = (bps_tx_L1 / speed) * 100.0
1063        else:
1064            snapshot['m_tx_util'] = 0
1065
1066        snapshot['m_total_rx_bps_L1'] = bps_rx_L1
1067        if speed:
1068            snapshot['m_rx_util'] = (bps_rx_L1 / speed) * 100.0
1069        else:
1070            snapshot['m_rx_util'] = 0
1071
1072        # TX line util not smoothed
1073        diff_tx_pkts = snapshot.get('opackets', 0) - self.latest_stats.get('opackets', 0)
1074        diff_tx_bytes = snapshot.get('obytes', 0) - self.latest_stats.get('obytes', 0)
1075        tx_bps_L1 = calc_bps_L1(8.0 * diff_tx_bytes / ts_diff, float(diff_tx_pkts) / ts_diff)
1076        if speed:
1077            snapshot['tx_percentage'] = 100.0 * tx_bps_L1 / speed
1078        else:
1079            snapshot['tx_percentage'] = 0
1080
1081        # RX line util not smoothed
1082        diff_rx_pkts = snapshot.get('ipackets', 0) - self.latest_stats.get('ipackets', 0)
1083        diff_rx_bytes = snapshot.get('ibytes', 0) - self.latest_stats.get('ibytes', 0)
1084        rx_bps_L1 = calc_bps_L1(8.0 * diff_rx_bytes / ts_diff, float(diff_rx_pkts) / ts_diff)
1085        if speed:
1086            snapshot['rx_percentage'] = 100.0 * rx_bps_L1 / speed
1087        else:
1088            snapshot['rx_percentage'] = 0
1089
1090        # simple...
1091        self.latest_stats = snapshot
1092
1093        return True
1094
1095
1096    def generate_stats(self):
1097
1098        port_state = self._port_obj.get_port_state_name() if self._port_obj else ""
1099        if port_state == "ACTIVE":
1100            port_state = format_text('TRANSMITTING', 'green', 'bold')
1101        elif port_state == "PAUSE":
1102            port_state = format_text(port_state, 'magenta', 'bold')
1103        else:
1104            port_state = format_text(port_state, 'bold')
1105
1106        if self._port_obj:
1107            if 'link' in self._port_obj.attr:
1108                if self._port_obj.attr.get('link', {}).get('up') == False:
1109                    link_state = format_text('DOWN', 'red', 'bold')
1110                else:
1111                    link_state = 'UP'
1112            else:
1113                link_state = 'N/A'
1114        else:
1115            link_state = ''
1116
1117        # default rate format modifiers
1118        rate_format = {'bpsl1': None, 'bps': None, 'pps': None, 'percentage': 'bold'}
1119
1120        # mark owned ports by color
1121        if self._port_obj:
1122            owner = self._port_obj.get_owner()
1123            rate_format[self._port_obj.last_factor_type] = ('blue', 'bold')
1124            if self._port_obj.is_acquired():
1125                owner = format_text(owner, 'green')
1126
1127        else:
1128            owner = ''
1129
1130
1131        return {"owner": owner,
1132                "state": "{0}".format(port_state),
1133                'link': link_state,
1134                "speed": self._port_obj.get_formatted_speed() if self._port_obj else '',
1135                "CPU util.": "{0} {1}%".format(self.get_trend_gui("m_cpu_util", use_raw = True),
1136                                               format_threshold(round_float(self.get("m_cpu_util")), [85, 100], [0, 85])) if self._port_obj else '' ,
1137                "--": " ",
1138                "---": " ",
1139                "----": " ",
1140                "-----": " ",
1141
1142                "Tx bps L1": "{0} {1}".format(self.get_trend_gui("m_total_tx_bps_L1", show_value = False),
1143                                               self.get("m_total_tx_bps_L1", format = True, suffix = "bps", opts = rate_format['bpsl1'])),
1144
1145                "Tx bps L2": "{0} {1}".format(self.get_trend_gui("m_total_tx_bps", show_value = False),
1146                                                self.get("m_total_tx_bps", format = True, suffix = "bps", opts = rate_format['bps'])),
1147
1148                "Line Util.": "{0} {1}".format(self.get_trend_gui("m_tx_util", show_value = False) if self._port_obj else "",
1149                                               self.get("m_tx_util", format = True, suffix = "%", opts = rate_format['percentage']) if self._port_obj else ""),
1150
1151                "Rx bps": "{0} {1}".format(self.get_trend_gui("m_total_rx_bps", show_value = False),
1152                                            self.get("m_total_rx_bps", format = True, suffix = "bps")),
1153
1154                "Tx pps": "{0} {1}".format(self.get_trend_gui("m_total_tx_pps", show_value = False),
1155                                            self.get("m_total_tx_pps", format = True, suffix = "pps", opts = rate_format['pps'])),
1156
1157                "Rx pps": "{0} {1}".format(self.get_trend_gui("m_total_rx_pps", show_value = False),
1158                                            self.get("m_total_rx_pps", format = True, suffix = "pps")),
1159
1160                 "opackets" : self.get_rel("opackets"),
1161                 "ipackets" : self.get_rel("ipackets"),
1162                 "obytes"   : self.get_rel("obytes"),
1163                 "ibytes"   : self.get_rel("ibytes"),
1164
1165                 "tx-bytes": self.get_rel("obytes", format = True, suffix = "B"),
1166                 "rx-bytes": self.get_rel("ibytes", format = True, suffix = "B"),
1167                 "tx-pkts": self.get_rel("opackets", format = True, suffix = "pkts"),
1168                 "rx-pkts": self.get_rel("ipackets", format = True, suffix = "pkts"),
1169
1170                 "oerrors"  : format_num(self.get_rel("oerrors"),
1171                                         compact = False,
1172                                         opts = 'green' if (self.get_rel("oerrors")== 0) else 'red'),
1173
1174                 "ierrors"  : format_num(self.get_rel("ierrors"),
1175                                         compact = False,
1176                                         opts = 'green' if (self.get_rel("ierrors")== 0) else 'red'),
1177
1178                }
1179
1180
1181class CLatencyStats(CTRexStats):
1182    def __init__(self, ports):
1183        super(CLatencyStats, self).__init__()
1184
1185
1186    # for API
1187    def get_stats (self):
1188        return copy.deepcopy(self.latest_stats)
1189
1190
1191    def _update(self, snapshot):
1192        if snapshot is None:
1193            snapshot = {}
1194        output = {}
1195
1196        output['global'] = {}
1197        for field in ['bad_hdr', 'old_flow']:
1198            if 'global' in snapshot and field in snapshot['global']:
1199                output['global'][field] = snapshot['global'][field]
1200            else:
1201                output['global'][field] = 0
1202
1203        # we care only about the current active keys
1204        pg_ids = list(filter(is_intable, snapshot.keys()))
1205
1206        for pg_id in pg_ids:
1207            current_pg = snapshot.get(pg_id)
1208            int_pg_id = int(pg_id)
1209            output[int_pg_id] = {}
1210            output[int_pg_id]['err_cntrs'] = current_pg['err_cntrs']
1211            output[int_pg_id]['latency'] = {}
1212
1213            if 'latency' in current_pg:
1214                for field in ['jitter', 'average', 'total_max', 'last_max']:
1215                    if field in current_pg['latency']:
1216                        output[int_pg_id]['latency'][field] = current_pg['latency'][field]
1217                    else:
1218                        output[int_pg_id]['latency'][field] = StatNotAvailable(field)
1219
1220                if 'histogram' in current_pg['latency']:
1221                    output[int_pg_id]['latency']['histogram'] = {int(elem): current_pg['latency']['histogram'][elem]
1222                                                                 for elem in current_pg['latency']['histogram']}
1223                    min_val = min(output[int_pg_id]['latency']['histogram'].keys())
1224                    if min_val == 0:
1225                        min_val = 2
1226                    output[int_pg_id]['latency']['total_min'] = min_val
1227                else:
1228                    output[int_pg_id]['latency']['total_min'] = StatNotAvailable('total_min')
1229                    output[int_pg_id]['latency']['histogram'] = {}
1230
1231        self.latest_stats = output
1232        return True
1233
1234
1235# RX stats objects - COMPLEX :-(
1236class CRxStats(CTRexStats):
1237    def __init__(self, ports):
1238        super(CRxStats, self).__init__()
1239        self.ports = ports
1240
1241
1242    # calculates a diff between previous snapshot
1243    # and current one
1244    def calculate_diff_sec (self, current, prev):
1245        if not 'ts' in current:
1246            raise ValueError("INTERNAL ERROR: RX stats snapshot MUST contain 'ts' field")
1247
1248        if prev:
1249            prev_ts   = prev['ts']
1250            now_ts    = current['ts']
1251            diff_sec  = (now_ts['value'] - prev_ts['value']) / float(now_ts['freq'])
1252        else:
1253            diff_sec = 0.0
1254
1255        return diff_sec
1256
1257
1258    # this is the heart of the complex
1259    def process_single_pg (self, current_pg, prev_pg):
1260
1261        # start with the previous PG
1262        output = copy.deepcopy(prev_pg)
1263
1264        for field in ['tx_pkts', 'tx_bytes', 'rx_pkts', 'rx_bytes']:
1265            # is in the first time ? (nothing in prev)
1266            if field not in output:
1267                output[field] = {}
1268
1269            # does the current snapshot has this field ?
1270            if field in current_pg:
1271                for port, pv in current_pg[field].items():
1272                    if not is_intable(port):
1273                        continue
1274
1275                    output[field][port] = pv
1276
1277            # sum up
1278            total = None
1279            for port, pv in output[field].items():
1280                if not is_intable(port):
1281                    continue
1282                if total is None:
1283                    total = 0
1284                total += pv
1285
1286            output[field]['total'] = total
1287
1288
1289        return output
1290
1291
1292    def process_snapshot (self, current, prev):
1293
1294        # final output
1295        output = {}
1296
1297        # copy timestamp field
1298        output['ts'] = current['ts']
1299
1300        # global (not per pg_id) error counters
1301        output['global'] = {}
1302        for field in ['rx_err', 'tx_err']:
1303            output['global'][field] = {}
1304            if 'global' in current and field in current['global']:
1305                for port in current['global'][field]:
1306                    output['global'][field][int(port)] = current['global'][field][port]
1307
1308        # we care only about the current active keys
1309        pg_ids = list(filter(is_intable, current.keys()))
1310
1311        for pg_id in pg_ids:
1312
1313            current_pg = current.get(pg_id, {})
1314
1315            # first time - we do not care
1316            if current_pg.get('first_time'):
1317                # new value - ignore history
1318                output[pg_id] = self.process_single_pg(current_pg, {})
1319                self.reference_stats[pg_id] = {}
1320
1321                # 'dry' B/W
1322                self.calculate_bw_for_pg(output[pg_id])
1323
1324            else:
1325                # aggregate the two values
1326                prev_pg = prev.get(pg_id, {})
1327                output[pg_id] = self.process_single_pg(current_pg, prev_pg)
1328
1329                # calculate B/W
1330                diff_sec = self.calculate_diff_sec(current, prev)
1331                self.calculate_bw_for_pg(output[pg_id], prev_pg, diff_sec)
1332
1333
1334        # cleanp old reference values - they are dead
1335        ref_pg_ids = list(filter(is_intable, self.reference_stats.keys()))
1336
1337        deleted_pg_ids = set(ref_pg_ids).difference(pg_ids)
1338        for d_pg_id in deleted_pg_ids:
1339            del self.reference_stats[d_pg_id]
1340
1341        return output
1342
1343
1344
1345    def calculate_bw_for_pg (self, pg_current, pg_prev = None, diff_sec = 0.0):
1346        # no previous values
1347        if (not pg_prev) or not (diff_sec > 0):
1348            pg_current['tx_pps']        = {}
1349            pg_current['tx_bps']        = {}
1350            pg_current['tx_bps_L1']     = {}
1351            pg_current['tx_line_util']  = {}
1352            pg_current['rx_pps']        = {}
1353            pg_current['rx_bps']        = {}
1354            pg_current['rx_bps_L1']     = {}
1355            pg_current['rx_line_util']  = {}
1356
1357            pg_current['tx_pps_lpf']    = {}
1358            pg_current['tx_bps_lpf']    = {}
1359            pg_current['tx_bps_L1_lpf'] = {}
1360            pg_current['rx_pps_lpf']    = {}
1361            pg_current['rx_bps_lpf']    = {}
1362            pg_current['rx_bps_L1_lpf'] = {}
1363            return
1364
1365        # TX
1366        for port in pg_current['tx_pkts'].keys():
1367
1368            prev_tx_pps   = pg_prev['tx_pps'].get(port)
1369            now_tx_pkts   = pg_current['tx_pkts'].get(port)
1370            prev_tx_pkts  = pg_prev['tx_pkts'].get(port)
1371            pg_current['tx_pps'][port], pg_current['tx_pps_lpf'][port] = self.calc_pps(prev_tx_pps, now_tx_pkts, prev_tx_pkts, diff_sec)
1372
1373            prev_tx_bps   = pg_prev['tx_bps'].get(port)
1374            now_tx_bytes  = pg_current['tx_bytes'].get(port)
1375            prev_tx_bytes = pg_prev['tx_bytes'].get(port)
1376
1377            pg_current['tx_bps'][port], pg_current['tx_bps_lpf'][port] = self.calc_bps(prev_tx_bps, now_tx_bytes, prev_tx_bytes, diff_sec)
1378
1379            if pg_current['tx_bps'].get(port) != None and pg_current['tx_pps'].get(port) != None:
1380                pg_current['tx_bps_L1'][port] = calc_bps_L1(pg_current['tx_bps'][port], pg_current['tx_pps'][port])
1381                pg_current['tx_bps_L1_lpf'][port] = calc_bps_L1(pg_current['tx_bps_lpf'][port], pg_current['tx_pps_lpf'][port])
1382            else:
1383                pg_current['tx_bps_L1'][port] = None
1384                pg_current['tx_bps_L1_lpf'][port] = None
1385
1386
1387        # RX
1388        for port in pg_current['rx_pkts'].keys():
1389
1390            prev_rx_pps   = pg_prev['rx_pps'].get(port)
1391            now_rx_pkts   = pg_current['rx_pkts'].get(port)
1392            prev_rx_pkts  = pg_prev['rx_pkts'].get(port)
1393            pg_current['rx_pps'][port], pg_current['rx_pps_lpf'][port] = self.calc_pps(prev_rx_pps, now_rx_pkts, prev_rx_pkts, diff_sec)
1394
1395            prev_rx_bps   = pg_prev['rx_bps'].get(port)
1396            now_rx_bytes  = pg_current['rx_bytes'].get(port)
1397            prev_rx_bytes = pg_prev['rx_bytes'].get(port)
1398            pg_current['rx_bps'][port], pg_current['rx_bps_lpf'][port] = self.calc_bps(prev_rx_bps, now_rx_bytes, prev_rx_bytes, diff_sec)
1399            if pg_current['rx_bps'].get(port) != None and pg_current['rx_pps'].get(port) != None:
1400                pg_current['rx_bps_L1'][port] = calc_bps_L1(pg_current['rx_bps'][port], pg_current['rx_pps'][port])
1401                pg_current['rx_bps_L1_lpf'][port] = calc_bps_L1(pg_current['rx_bps_lpf'][port], pg_current['rx_pps_lpf'][port])
1402            else:
1403                pg_current['rx_bps_L1'][port] = None
1404                pg_current['rx_bps_L1_lpf'][port] = None
1405
1406
1407    def calc_pps (self, prev_bw, now, prev, diff_sec):
1408        return self.calc_bw(prev_bw, now, prev, diff_sec, False)
1409
1410
1411    def calc_bps (self, prev_bw, now, prev, diff_sec):
1412        return self.calc_bw(prev_bw, now, prev, diff_sec, True)
1413
1414    # returns tuple - first value is real, second is low pass filtered
1415    def calc_bw (self, prev_bw, now, prev, diff_sec, is_bps):
1416        # B/W is not valid when the values are None
1417        if (now is None) or (prev is None):
1418            return (None, None)
1419
1420        # calculate the B/W for current snapshot
1421        current_bw = (now - prev) / diff_sec
1422        if is_bps:
1423            current_bw *= 8
1424
1425        # previous B/W is None ? ignore it
1426        if prev_bw is None:
1427            prev_bw = 0
1428
1429        return (current_bw, 0.5 * prev_bw + 0.5 * current_bw)
1430
1431
1432
1433
1434    def _update (self, snapshot):
1435        #print(snapshot)
1436        # generate a new snapshot
1437        new_snapshot = self.process_snapshot(snapshot, self.latest_stats)
1438
1439        #print new_snapshot
1440        # advance
1441        self.latest_stats = new_snapshot
1442
1443
1444        return True
1445
1446
1447
1448    # for API
1449    def get_stats (self):
1450        stats = {}
1451
1452        for pg_id, value in self.latest_stats.items():
1453            # skip non ints
1454            if not is_intable(pg_id):
1455                # 'global' stats are in the same level of the pg_ids. We do want them to go to the user
1456                if pg_id == 'global':
1457                    stats[pg_id] = value
1458                continue
1459            # bare counters
1460            stats[int(pg_id)] = {}
1461            for field in ['tx_pkts', 'tx_bytes', 'rx_pkts', 'rx_bytes']:
1462                val = self.get_rel([pg_id, field, 'total'])
1463                stats[int(pg_id)][field] = {'total': val if val != 'N/A' else StatNotAvailable(field)}
1464                for port in value[field].keys():
1465                    if is_intable(port):
1466                        val = self.get_rel([pg_id, field, port])
1467                        stats[int(pg_id)][field][int(port)] = val if val != 'N/A' else StatNotAvailable(field)
1468
1469            # BW values
1470            for field in ['tx_pps', 'tx_bps', 'tx_bps_L1', 'rx_pps', 'rx_bps', 'rx_bps_L1']:
1471                val = self.get([pg_id, field, 'total'])
1472                stats[int(pg_id)][field] = {'total': val if val != 'N/A' else StatNotAvailable(field)}
1473                for port in value[field].keys():
1474                    if is_intable(port):
1475                        val = self.get([pg_id, field, port])
1476                        stats[int(pg_id)][field][int(port)] = val if val != 'N/A' else StatNotAvailable(field)
1477
1478        return stats
1479
1480class CUtilStats(CTRexStats):
1481
1482    def __init__(self, client):
1483        super(CUtilStats, self).__init__()
1484        self.client = client
1485        self.history = deque(maxlen = 1)
1486        self.mbuf_types_list = None
1487        self.last_update_ts = -999
1488
1489    def get_stats(self, use_1sec_cache = False):
1490        time_now = time.time()
1491        if self.last_update_ts + 1 < time_now or not self.history or not use_1sec_cache:
1492            if self.client.is_connected():
1493                rc = self.client._transmit('get_utilization')
1494                if not rc:
1495                    raise STLError(rc)
1496                self.last_update_ts = time_now
1497                self.history.append(rc.data())
1498            else:
1499                self.history.append({})
1500
1501        return self.history[-1]
1502
1503class CXStats(CTRexStats):
1504
1505    def __init__(self, client):
1506        super(CXStats, self).__init__()
1507        self.client = client
1508        self.names = []
1509        self.last_update_ts = -999
1510
1511    def clear_stats(self, port_id = None):
1512        if port_id == None:
1513            ports = self.client.get_all_ports()
1514        elif type(port_id) is list:
1515            ports = port_id
1516        else:
1517            ports = [port_id]
1518
1519        for port_id in ports:
1520            self.reference_stats[port_id] = self.get_stats(port_id, relative = False)
1521
1522    def get_stats(self, port_id, use_1sec_cache = False, relative = True):
1523        time_now = time.time()
1524        if self.last_update_ts + 1 < time_now or not self.latest_stats or not use_1sec_cache:
1525            if self.client.is_connected():
1526                rc = self.client._transmit('get_port_xstats_values', params = {'port_id': port_id})
1527                if not rc:
1528                    raise STLError(rc)
1529                self.last_update_ts = time_now
1530                values = rc.data().get('xstats_values', [])
1531                if len(values) != len(self.names): # need to update names ("keys")
1532                    rc = self.client._transmit('get_port_xstats_names', params = {'port_id': port_id})
1533                    if not rc:
1534                        raise STLError(rc)
1535                    self.names = rc.data().get('xstats_names', [])
1536                if len(values) != len(self.names):
1537                    raise STLError('Length of get_xstats_names: %s and get_port_xstats_values: %s' % (len(self.names), len(values)))
1538                self.latest_stats[port_id] = OrderedDict([(key, val) for key, val in zip(self.names, values)])
1539
1540        stats = OrderedDict()
1541        for key, val in self.latest_stats[port_id].items():
1542            if relative:
1543                stats[key] = self.get_rel([port_id, key])
1544            else:
1545                stats[key] = self.get([port_id, key])
1546        return stats
1547
1548if __name__ == "__main__":
1549    pass
1550
1551