diff --git a/scripts/queuestat b/scripts/queuestat index 3516386b11..ee3b6278d8 100755 --- a/scripts/queuestat +++ b/scripts/queuestat @@ -57,23 +57,31 @@ VoqStats = namedtuple( std_header = [ 'Port', 'TxQ', - 'Counter/pkts', 'Counter/bytes', 'Drop/pkts', 'Drop/bytes' + 'Counter/pkts', 'Counter/bytes', 'Drop/pkts', 'Drop/bytes', + 'Pkts/s', 'Bytes/s', 'Bits/s' ] all_header = [ 'Port', 'TxQ', 'Counter/pkts', 'Counter/bytes', 'Drop/pkts', 'Drop/bytes', - 'Trim/pkts', 'TrimSent/pkts', 'TrimDrop/pkts' + 'Trim/pkts', 'TrimSent/pkts', 'TrimDrop/pkts', + 'Pkts/s', 'Bytes/s', 'Bits/s' ] trim_header = [ 'Port', 'TxQ', - 'Trim/pkts', 'TrimSent/pkts', 'TrimDrop/pkts' + 'Trim/pkts', 'TrimSent/pkts', 'TrimDrop/pkts', + 'Pkts/s', 'Bytes/s', 'Bits/s' ] voq_header = [ 'Port', 'Voq', 'Counter/pkts', 'Counter/bytes', 'Drop/pkts', 'Drop/bytes', - 'Credit-WD-Del/pkts' + 'Credit-WD-Del/pkts', + 'Pkts/s', 'Bytes/s', 'Bits/s' ] +rates_key_list = [ 'Q_PPS', 'Q_BPS', 'Q_bPS'] +ratestat = [ 'qpktsps', 'qbytesps', 'qbitsps'] +RateStats = namedtuple("RateStats", ratestat) + counter_bucket_dict = { 'SAI_QUEUE_STAT_PACKETS': 2, 'SAI_QUEUE_STAT_BYTES': 3, @@ -101,6 +109,7 @@ SAI_QUEUE_TYPE_UNICAST = "SAI_QUEUE_TYPE_UNICAST" SAI_QUEUE_TYPE_UNICAST_VOQ = "SAI_QUEUE_TYPE_UNICAST_VOQ" SAI_QUEUE_TYPE_ALL = "SAI_QUEUE_TYPE_ALL" +RATES_TABLE_PREFIX = "RATES:" COUNTER_TABLE_PREFIX = "COUNTERS:" COUNTERS_PORT_NAME_MAP = "COUNTERS_PORT_NAME_MAP" COUNTERS_SYSTEM_PORT_NAME_MAP = "COUNTERS_SYSTEM_PORT_NAME_MAP" @@ -374,16 +383,33 @@ class Queuestat(object): cntr = QueueStats._make(fields)._asdict() return cntr + def get_rates(table_id): + """ + Get the rates from specific table. + """ + fields = ["0", "0", "0"] + for pos, name in enumerate(rates_key_list): + full_table_id = RATES_TABLE_PREFIX + table_id + counter_data = self.db.get(self.db.COUNTERS_DB, full_table_id, name) + if counter_data is None: + fields[pos] = STATUS_NA + elif fields[pos] != STATUS_NA: + fields[pos] = float(counter_data) + cntr = RateStats._make(fields) + return cntr + # Build a dictionary of the stats cnstat_dict = OrderedDict() cnstat_dict['time'] = datetime.datetime.now() + ratestat_dict = OrderedDict() if queue_map is None: return cnstat_dict for queue in natsorted(queue_map): cnstat_dict[queue] = get_counters(queue_map[queue]) - return cnstat_dict + ratestat_dict[queue] = get_rates(queue_map[queue]) + return cnstat_dict,ratestat_dict - def cnstat_print(self, port, cnstat_dict, json_opt, non_zero): + def cnstat_print(self, port, cnstat_dict, json_opt, non_zero,ratestat_dict): """ Print the cnstat. If JSON option is True, return data in JSON format. @@ -396,12 +422,23 @@ class Queuestat(object): if json_opt: json_output[port][key] = data continue + + qpktsps = qbytesps = qbitsps = STATUS_NA + rates = ratestat_dict.get(key, RateStats._make([STATUS_NA] * len(ratestat))) + if rates.qpktsps != STATUS_NA: + qpktsps = "{:,}".format(int(float(rates.qpktsps))) + if rates.qbytesps != STATUS_NA: + qbytesps = "{:,}".format(int(float(rates.qbytesps))) + if rates.qbitsps != STATUS_NA: + qbitsps = "{:,}".format(int(float(rates.qbitsps))) + if self.voq: if not non_zero or data['totalpacket'] != '0' or data['totalbytes'] != '0' or \ data['droppacket'] != '0' or data['dropbytes'] != '0' or data['creditWDpkts'] != '0': table.append((port, data['queuetype'] + str(data['queueindex']), data['totalpacket'], data['totalbytes'], - data['droppacket'], data['dropbytes'], data['creditWDpkts'])) + data['droppacket'], data['dropbytes'], data['creditWDpkts'], + qpktsps, qbytesps, qbitsps)) else: queuetag = data['queuetype'] + str(data['queueindex']) @@ -414,14 +451,16 @@ class Queuestat(object): port, queuetag, data['totalpacket'], data['totalbytes'], data['droppacket'], data['dropbytes'], - data['trimpkt'], data['trimsentpkt'], data['trimdroppkt'] + data['trimpkt'], data['trimsentpkt'], data['trimdroppkt'], + qpktsps, qbytesps, qbitsps )) elif self.trim: # Packet Trimming related statistics if not non_zero or \ data['trimpkt'] != '0' or data['trimsentpkt'] != '0' or data['trimdroppkt'] != '0': table.append(( port, queuetag, - data['trimpkt'], data['trimsentpkt'], data['trimdroppkt'] + data['trimpkt'], data['trimsentpkt'], data['trimdroppkt'], + qpktsps, qbytesps, qbitsps )) else: # Generic statistics if not non_zero or \ @@ -430,7 +469,8 @@ class Queuestat(object): table.append(( port, queuetag, data['totalpacket'], data['totalbytes'], - data['droppacket'], data['dropbytes'] + data['droppacket'], data['dropbytes'], + qpktsps, qbytesps, qbitsps )) if json_opt: @@ -452,7 +492,7 @@ class Queuestat(object): print(tabulate(table, hdr, tablefmt='simple', stralign='right')) print() - def cnstat_diff_print(self, port, cnstat_new_dict, cnstat_old_dict, json_opt, non_zero): + def cnstat_diff_print(self, port, cnstat_new_dict, cnstat_old_dict, json_opt, non_zero,ratestat_dict): """ Print the difference between two cnstat results. If JSON option is True, return data in JSON format. @@ -468,6 +508,16 @@ class Queuestat(object): old_cntr = None if key in cnstat_old_dict: old_cntr = cnstat_old_dict.get(key) + + qpktsps = qbytesps = qbitsps = STATUS_NA + rates = ratestat_dict.get(key, RateStats._make([STATUS_NA] * len(ratestat))) + if rates.qpktsps != STATUS_NA: + qpktsps = "{:,}".format(int(float(rates.qpktsps))) + if rates.qbytesps != STATUS_NA: + qbytesps = "{:,}".format(int(float(rates.qbytesps))) + if rates.qbitsps != STATUS_NA: + qbitsps = "{:,}".format(int(float(rates.qbitsps))) + if old_cntr is not None: if self.voq: if not non_zero or ns_diff(cntr['totalpacket'], old_cntr['totalpacket']) != '0' or \ @@ -480,7 +530,8 @@ class Queuestat(object): ns_diff(cntr['totalbytes'], old_cntr['totalbytes']), ns_diff(cntr['droppacket'], old_cntr['droppacket']), ns_diff(cntr['dropbytes'], old_cntr['dropbytes']), - ns_diff(cntr['creditWDpkts'], old_cntr['creditWDpkts']))) + ns_diff(cntr['creditWDpkts'], old_cntr['creditWDpkts']), + qpktsps, qbytesps, qbitsps)) else: queuetag = cntr['queuetype'] + str(cntr['queueindex']) @@ -501,7 +552,8 @@ class Queuestat(object): port, queuetag, totalpacket, totalbytes, droppacket, dropbytes, - trimpkt, trimsentpkt, trimdroppkt + trimpkt, trimsentpkt, trimdroppkt, + qpktsps, qbytesps, qbitsps )) elif self.trim: # Packet Trimming related statistics trimpkt = ns_diff(cntr['trimpkt'], old_cntr['trimpkt']) @@ -512,7 +564,8 @@ class Queuestat(object): trimpkt != '0' or trimsentpkt != '0' or trimdroppkt != '0': table.append(( port, queuetag, - trimpkt, trimsentpkt, trimdroppkt + trimpkt, trimsentpkt, trimdroppkt, + qpktsps, qbytesps, qbitsps )) else: # Generic statistics totalpacket = ns_diff(cntr['totalpacket'], old_cntr['totalpacket']) @@ -526,7 +579,8 @@ class Queuestat(object): table.append(( port, queuetag, totalpacket, totalbytes, - droppacket, dropbytes + droppacket, dropbytes, + qpktsps, qbytesps, qbitsps )) if json_opt: json_output[port].update(build_json(port, table, self.all, self.trim, self.voq)) @@ -569,16 +623,16 @@ class Queuestat(object): cnstat_cached_dict = json.load(open(cnstat_fqn_file_name, 'r')) if json_opt: json_output[port].update({"cached_time":cnstat_cached_dict.get('time')}) - json_output.update(self.cnstat_diff_print(port, cnstat_dict, cnstat_cached_dict, json_opt, non_zero)) + json_output.update(self.cnstat_diff_print(port, cnstat_dict, cnstat_cached_dict, json_opt, non_zero,ratestat_dict)) else: - self.cnstat_diff_print(port, cnstat_dict, cnstat_cached_dict, json_opt, non_zero) + self.cnstat_diff_print(port, cnstat_dict, cnstat_cached_dict, json_opt, non_zero,ratestat_dict) except IOError as e: print(e.errno, e) else: if json_opt: - json_output.update(self.cnstat_print(port, cnstat_dict, json_opt, non_zero)) + json_output.update(self.cnstat_print(port, cnstat_dict, json_opt, non_zero,ratestat_dict)) else: - self.cnstat_print(port, cnstat_dict, json_opt, non_zero) + self.cnstat_print(port, cnstat_dict, json_opt, non_zero,ratestat_dict) if json_opt: print(json_dump(json_output)) @@ -597,7 +651,7 @@ class Queuestat(object): if self.voq and device_info.is_supervisor(): cnstat_dict = self.get_aggregate_port_stats(port) else: - cnstat_dict = self.get_cnstat(self.port_queues_map[port]) + cnstat_dict,ratestat_dict = self.get_cnstat(self.port_queues_map[port]) cache_ns = '' if self.voq and self.namespace is not None: cache_ns = '-' + self.namespace + '-' @@ -609,17 +663,17 @@ class Queuestat(object): cnstat_cached_dict = json.load(open(cnstat_fqn_file_name, 'r')) if json_opt: json_output[port].update({"cached_time":cnstat_cached_dict.get('time')}) - json_output.update(self.cnstat_diff_print(port, cnstat_dict, cnstat_cached_dict, json_opt, non_zero)) + json_output.update(self.cnstat_diff_print(port, cnstat_dict, cnstat_cached_dict, json_opt, non_zero,ratestat_dict)) else: print(f"Last cached time{self.namespace_str} was " + str(cnstat_cached_dict.get('time'))) - self.cnstat_diff_print(port, cnstat_dict, cnstat_cached_dict, json_opt, non_zero) + self.cnstat_diff_print(port, cnstat_dict, cnstat_cached_dict, json_opt, non_zero,ratestat_dict) except IOError as e: print(e.errno, e) else: if json_opt: - json_output.update(self.cnstat_print(port, cnstat_dict, json_opt, non_zero)) + json_output.update(self.cnstat_print(port, cnstat_dict, json_opt, non_zero,ratestat_dict)) else: - self.cnstat_print(port, cnstat_dict, json_opt, non_zero) + self.cnstat_print(port, cnstat_dict, json_opt, non_zero,ratestat_dict) if json_opt: print(json_dump(json_output)) @@ -630,7 +684,7 @@ class Queuestat(object): if self.voq and self.namespace is not None: cache_ns = '-' + self.namespace + '-' for port in natsorted(self.counter_port_name_map): - cnstat_dict = self.get_cnstat(self.port_queues_map[port]) + cnstat_dict,ratestat_dict = self.get_cnstat(self.port_queues_map[port]) try: json.dump(cnstat_dict, open(cnstat_fqn_file + cache_ns + port, 'w'), default=json_serial) except IOError as e: diff --git a/tests/netstat_test.py b/tests/netstat_test.py new file mode 100644 index 0000000000..97ad75991d --- /dev/null +++ b/tests/netstat_test.py @@ -0,0 +1,48 @@ +"""Tests for utilities_common.netstat formatting using 1024-based units.""" + +import sys +from pathlib import Path +import importlib + + +# Try normal import first +try: + from utilities_common.netstat import ( + format_brate, + format_util, + STATUS_NA, + ) +except ModuleNotFoundError: + # Fallback: allow running this file directly by adding repo root to sys.path + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + netstat = importlib.import_module("utilities_common.netstat") + format_brate = netstat.format_brate + format_util = netstat.format_util + STATUS_NA = netstat.STATUS_NA + + +def test_format_brate_uses_1024_units(): + # > 10,000,000 bytes/s → MB path (now divides by 1024*1024) + assert format_brate(11 * 1024 * 1024) == "11.00 MB/s" + + # > 10,000 bytes/s → KB path (now divides by 1024) + assert format_brate(20 * 1024) == "20.00 KB/s" + + # <= 10,000 bytes/s → B path + assert format_brate(9_999) == "9999.00 B/s" + + +def test_format_util_uses_1024_conversion(): + """ + util = brate / (port_rate * 1024 * 1024 / 8) * 100 + Choose brate so utilization is exactly 50.00% at port_rate=1000 (Mb/s). + """ + port_rate_mbps = 1000 + bytes_per_sec_at_line_rate = (port_rate_mbps * 1024 * 1024) / 8.0 + brate = 0.5 * bytes_per_sec_at_line_rate + assert format_util(brate, port_rate_mbps) == "50.00%" + + +def test_format_util_status_na_passthrough(): + assert format_util(STATUS_NA, 1000) == STATUS_NA + assert format_util(12345, STATUS_NA) == STATUS_NA diff --git a/tests/queue_counter_rate_test.py b/tests/queue_counter_rate_test.py new file mode 100644 index 0000000000..ea35905261 --- /dev/null +++ b/tests/queue_counter_rate_test.py @@ -0,0 +1,263 @@ +"""Tests for queuestat queue rate columns and JSON output. + +This file: +- Loads `scripts/queuestat` even if it’s extensionless. +- Stubs platform-only deps (swsscommon, sonic_py_common, redis, utilities_common.cli). +- Verifies headers end with Pkts/s, Bytes/s, Bits/s and values appear in table/JSON. +""" + +from __future__ import annotations + +import importlib.machinery +import importlib.util +import json +import sys +import types +from collections import OrderedDict +from datetime import date, datetime +from pathlib import Path +from types import SimpleNamespace + +import pytest +from click.testing import CliRunner + +# ------------------------------------------------------------------- +# Locate repo root and put it on sys.path so utilities_common/* works +# ------------------------------------------------------------------- +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +# ------------------------------------------------------- +# Minimal stubs for platform-only deps used during import +# ------------------------------------------------------- + +# swsscommon (including ConfigDBConnector that device_info may import) +sc = types.ModuleType("swsscommon") +sc.swsscommon = sc + + +class SonicV2Connector: # pylint: disable=too-few-public-methods + """Stub for swsscommon.swsscommon.SonicV2Connector.""" + + def __init__(self, *args, **kwargs) -> None: + """No-op init for stub.""" + _ = (args, kwargs) # mark used + + def connect(self, *args, **kwargs) -> None: + """No-op connect for stub.""" + _ = (args, kwargs) + + +class DBConnector: # pylint: disable=too-few-public-methods + """Stub for swsscommon.swsscommon.DBConnector.""" + + def __init__(self, *args, **kwargs) -> None: + """No-op init for stub.""" + _ = (args, kwargs) + + +class ConfigDBConnector: # pylint: disable=too-few-public-methods + """Stub for swsscommon.swsscommon.ConfigDBConnector.""" + + def __init__(self, *args, **kwargs) -> None: + """No-op init for stub.""" + _ = (args, kwargs) + + def connect(self, *args, **kwargs) -> None: + """No-op connect for stub.""" + _ = (args, kwargs) + + +sc.SonicV2Connector = SonicV2Connector +sc.DBConnector = DBConnector +sc.ConfigDBConnector = ConfigDBConnector +sys.modules["swsscommon"] = sc +sys.modules["swsscommon.swsscommon"] = sc + +# sonic_py_common package + submodules +sp = types.ModuleType("sonic_py_common") +ma = types.ModuleType("multi_asic") +ma.is_multi_asic = lambda: False +ma.get_namespace_list = lambda: [] # required by click.Choice(...) + +di = types.ModuleType("device_info") +di.is_supervisor = lambda: False +di.is_chassis = lambda: False + +sp.multi_asic = ma +sp.device_info = di +sys.modules["sonic_py_common"] = sp +sys.modules["sonic_py_common.multi_asic"] = ma +sys.modules["sonic_py_common.device_info"] = di + +# optional: stub redis if not installed locally (avoid unused-import warning) +if importlib.util.find_spec("redis") is None: + rmod = types.ModuleType("redis") + + class Redis: # pylint: disable=too-few-public-methods + """Stub for redis.Redis.""" + + class Exceptions: # pylint: disable=too-few-public-methods + """Stub for redis.exceptions.""" + + rmod.Redis = Redis + rmod.exceptions = Exceptions + sys.modules["redis"] = rmod + +# optional: stub utilities_common.cli to avoid lazy_object_proxy dep +if "utilities_common.cli" not in sys.modules: + cli_mod = types.ModuleType("utilities_common.cli") + + def json_serial(obj): + """JSON default serializer that handles datetime/date -> ISO string.""" + if isinstance(obj, (datetime, date)): + return obj.isoformat() + return str(obj) + + def json_dump(obj): + """Dump JSON using json_serial for unsupported types.""" + return json.dumps(obj, default=json_serial) + + class UserCache: # pylint: disable=too-few-public-methods + """Tiny stub for utilities_common.cli.UserCache.""" + + def __init__(self, *args, **kwargs) -> None: + """No-op init for stub.""" + _ = (args, kwargs) + + # Optional get/set can be added here if tests ever use them. + + cli_mod.json_serial = json_serial + cli_mod.json_dump = json_dump + cli_mod.UserCache = UserCache + sys.modules["utilities_common.cli"] = cli_mod + +# --------------------------------------------------------- +# Try to import scripts/queuestat (extensionless or .py path) +# If it fails due to decorator eval, we’ll skip per-test. +# --------------------------------------------------------- +QUEUESTAT_MOD = None # will hold the loaded module or stay None +QUEUESTAT_AVAILABLE = False + +for _p in (ROOT / "scripts" / "queuestat", ROOT / "scripts" / "queuestat.py"): + if _p.is_file(): + _ldr = importlib.machinery.SourceFileLoader("queuestat", str(_p)) + _spec = importlib.util.spec_from_loader("queuestat", _ldr) + _mod = importlib.util.module_from_spec(_spec) + try: + _ldr.exec_module(_mod) + QUEUESTAT_MOD = _mod + QUEUESTAT_AVAILABLE = True + break + except AttributeError as err: + # Common failure: click.Choice(multi_asic.get_namespace_list()) if stub not accepted + if "get_namespace_list" in str(err): + QUEUESTAT_AVAILABLE = False + QUEUESTAT_MOD = None + break + QUEUESTAT_AVAILABLE = False + QUEUESTAT_MOD = None + break + except (ImportError, OSError, RuntimeError): + # Treat import-time dependency issues as "unavailable" and skip tests. + QUEUESTAT_AVAILABLE = False + QUEUESTAT_MOD = None + break + + +def require_queuestat() -> None: + """Skip the current test if queuestat couldn't be imported in this environment.""" + if not QUEUESTAT_AVAILABLE: + pytest.skip("queuestat unavailable (import-time dependency/decorator evaluation).") + + +# ========================= +# TESTS +# ========================= +def test_headers_have_rate_columns() -> None: + """Verify all header variants end with the three rate columns.""" + require_queuestat() + expected_tail = ["Pkts/s", "Bytes/s", "Bits/s"] + assert QUEUESTAT_MOD.std_header[-3:] == expected_tail + assert QUEUESTAT_MOD.all_header[-3:] == expected_tail + assert QUEUESTAT_MOD.trim_header[-3:] == expected_tail + assert QUEUESTAT_MOD.voq_header[-3:] == expected_tail + + +def test_cnstat_print_includes_rate_columns_and_values(capsys: pytest.CaptureFixture[str]) -> None: + """Feed cnstat_print() one queue with known rate values and verify table output.""" + require_queuestat() + # Minimal counter dict as produced by get_counters() + cnstat = OrderedDict() + cnstat["time"] = datetime.now() + cnstat["Ethernet0"] = { + "queuetype": "UC", + "queueindex": "0", + "totalpacket": "10", + "totalbytes": "100", + "droppacket": "0", + "dropbytes": "0", + "trimpkt": "0", + "trimsentpkt": "0", + "trimdroppkt": "0", + } + + # Minimal rate dict: RateStats(qpktsps, qbytesps, qbitsps) + ratestat = {"Ethernet0": QUEUESTAT_MOD.RateStats("111", "222", "333")} + + fake_self = SimpleNamespace(all=False, trim=False, voq=False) + + QUEUESTAT_MOD.Queuestat.cnstat_print( + fake_self, "Ethernet0", cnstat, json_opt=False, non_zero=False, ratestat_dict=ratestat + ) + out = capsys.readouterr().out + + for col in ("Pkts/s", "Bytes/s", "Bits/s"): + assert col in out + for val in ("111", "222", "333"): + assert val in out + + +def test_json_path_includes_rate_fields(monkeypatch: pytest.MonkeyPatch) -> None: + """Stub get_print_all_stat to emit JSON and ensure rate fields exist & are numeric strings.""" + require_queuestat() + # Fake JSON that queuestat.main --json is expected to print + fake_json = { + "Ethernet0": { + "UC0": { + "totalpacket": "10", + "totalbytes": "100", + "droppacket": "0", + "dropbytes": "0", + "qpktsps": "111", + "qbytesps": "222", + "qbitsps": "333", + } + } + } + + def fake_get_print_all_stat(_self, json_opt, _non_zero) -> None: + """Fake printer used by --json path in tests.""" + assert json_opt is True + print(json.dumps(fake_json)) + + # Avoid touching Redis/flex-counters during test + monkeypatch.setattr(QUEUESTAT_MOD.Queuestat, "get_print_all_stat", fake_get_print_all_stat) + monkeypatch.setattr(QUEUESTAT_MOD.Queuestat, "save_fresh_stats", lambda _self: None) + + runner = CliRunner() + result = runner.invoke(QUEUESTAT_MOD.main, ["--json"]) + assert result.exit_code == 0, result.output + + data = json.loads(result.output) + for _, port_dict in data.items(): + if isinstance(port_dict, dict): + port_dict.pop("time", None) + for _q, qdata in port_dict.items(): + if not isinstance(qdata, dict): + continue + for key in ("qpktsps", "qbytesps", "qbitsps"): + assert key in qdata + assert isinstance(qdata[key], str) + assert qdata[key].replace(".", "", 1).isdigit() diff --git a/utilities_common/netstat.py b/utilities_common/netstat.py index 83b2f4c18e..029637e815 100755 --- a/utilities_common/netstat.py +++ b/utilities_common/netstat.py @@ -113,9 +113,9 @@ def format_brate(rate): else: rate = float(rate) if rate > 1000*1000*10: - rate = "{:.2f}".format(rate/1000/1000.0)+' MB' + rate = "{:.2f}".format(rate/1024/1024.0)+' MB' elif rate > 1000*10: - rate = "{:.2f}".format(rate/1000.0)+' KB' + rate = "{:.2f}".format(rate/1024.0)+' KB' else: rate = "{:.2f}".format(rate)+' B' return rate+'/s' @@ -130,7 +130,6 @@ def format_prate(rate): else: return "{:.2f}".format(float(rate))+'/s' - def format_fec_ber(rate): """ Show the ber rate. @@ -148,7 +147,7 @@ def format_util(brate, port_rate): if brate == STATUS_NA or port_rate == STATUS_NA: return STATUS_NA else: - util = brate/(float(port_rate)*1000*1000/8.0)*100 + util = brate/(float(port_rate)*1024*1024/8.0)*100 return "{:.2f}%".format(util)