# Copyright (C) 2020 Michel Gokan Khan
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# This file is a part of the PerfSim project, which is now open source and available under the GPLv2.
# Written by Michel Gokan Khan, February 2020
from __future__ import annotations
from typing import TYPE_CHECKING, Union, Dict, Tuple
import networkx as nx
import numpy as np
from perfsim import Transmission
# from overloading import overload
if TYPE_CHECKING:
from perfsim import Host, Request, MicroserviceEndpointFunction, MicroserviceReplica, Router
[docs]
class Nic:
"""
This class represents a Network Interface Card (NIC) in a host or a router. A NIC is a hardware component that
connects the host or router to the network. It has a bandwidth that determines the maximum amount of data that can
be transmitted over the network. The NIC can reserve and release bandwidth for transmissions.
"""
#: A name for the NIC
name: str
#: The parent equipment object. E.g., the host or router object that this NIC belongs to it
equipment: Union[Host, Router]
#: Nic's bandwidth (Bps)
bandwidth: int
#: A dictionary consisting of all active transmissions, that can be accessed by tuple (subchain_id, request) as keys
transmissions: Dict[(int, Request)]
#: The total bandwidth requests on this NIC. Useful for scoring hosts.
bandwidth_requests_total: int
def __init__(self,
name: str,
bandwidth: int,
equipment: Union[Host, Router]):
# super().__init__.py("nic", "nic_" + name, True, "bytes", bandwidth)
self.name = name
self.equipment = equipment
self.bandwidth = bandwidth # bytes per second
self.transmissions = {}
self.bandwidth_requests_total = 0
# type hinting in overloading module is not compatible with python 3.7 annotations
# @overload
# def reserve_transmission(self, request):
[docs]
def reserve_transmission_for_request(self,
request: Request,
subchain_id: int,
src_replica: MicroserviceReplica,
source_node: Tuple[int, MicroserviceEndpointFunction],
destination_replica: MicroserviceReplica,
destination_node: Tuple[int, MicroserviceEndpointFunction]):
if (subchain_id, request) not in self.transmissions:
_link_data = request.scm.service_chain.get_edge_data(source_node[1], destination_node[1])
self.transmissions[(subchain_id, request)] = \
Transmission(id=self.equipment.cluster.sim.load_generator.last_transmission_id,
payload_size=_link_data[next(iter(_link_data))]["payload"],
src_replica=src_replica,
dst_replica=destination_replica,
subchain_id_request_pair=(request, subchain_id),
recalculate_bandwidths_in_links=False)
self.transmissions[(subchain_id, request)].topology.active_transmissions.add(
self.transmissions[(subchain_id, request)])
self.equipment.cluster.sim.load_generator.last_transmission_id += 1
return self.transmissions[(subchain_id, request)]
# return self.requests[(subchain_id, request)].calculate_transmission_time()
else:
raise Exception("A request already exists in the NIC for (subchain_id, request) pair (" +
str(subchain_id) + ", " + str(request) + ")!")
# type hinting in overloading module is not compatible with python 3.7 annotations
# @overload
# def release_transmission(self, request):
[docs]
def release_transmission_for_request(self, request: Request, subchain_id: int):
if (subchain_id, request) in self.transmissions:
result = self.release_transmission_in_nic(
payload_size=self.transmissions[(subchain_id, request)],
destination_nic=request._next_replicas_in_nodes[subchain_id][1].host.nic["ingress"])
self.transmissions[(subchain_id, request)].finish()
del self.transmissions[(subchain_id, request)]
return result
else:
raise Exception(
"NIC is trying to release a transmission for a request which is not exists. What the hell?!")
# return True
# type hinting in overloading module is not compatible with python 3.7 annotations
# @overload
# def reserve_transmission(self, payload_size, destination_nic):
[docs]
def reserve_transmission_in_nic(self,
payload_size: float,
src_replica: MicroserviceReplica,
destination_replica: MicroserviceReplica):
_transmission_time = self.calculate_transmission_time(payload_size=payload_size,
src_replica=src_replica,
destination_replica=destination_replica)
if _transmission_time != 0:
_destination_nic = destination_replica.host.nic["ingress"]
_destination_nic.reserve(payload_size)
return _transmission_time
else:
return False
# type hinting in overloading module is not compatible with python 3.7 annotations
# @overload
# def release_transmission(self, payload_size, destination_nic):
[docs]
def release_transmission_in_nic(self, payload_size, destination_nic):
return True
[docs]
def calculate_transmission_time(self,
payload_size: float,
src_replica: MicroserviceReplica,
destination_replica: MicroserviceReplica) -> Union[int, float]:
"""
This method calculates the transmission time between two replicas. It calculates the transmission time based on
the minimum bandwidth between the source and destination replicas, the minimum bandwidth between the source and
destination hosts, and the minimum bandwidth between the source and destination NICs. The transmission time is
calculated as the time it takes to transmit the payload over the minimum bandwidth.
:param payload_size:
:param src_replica:
:param destination_replica:
:return:
"""
if self.host.cluster.sim.debug:
self.host.cluster.sim.logger.log(
" ***** [NIC] Calculating transmission time between " +
str(self.host.name) + " -> " + str(destination_replica.host.name), 3)
_destination_nic = destination_replica.host.nic["ingress"]
if self == _destination_nic:
if self.host.cluster.sim.debug:
self.host.cluster.sim.logger.log(" ****** Transmission time = 0", 3)
return 0
else:
min_host_bw = self.bandwidth if self.bandwidth < _destination_nic.bandwidth else _destination_nic.bandwidth
min_replica_bw = src_replica.process.egress_bw \
if src_replica.process.egress_bw < destination_replica.process.ingress_bw \
else destination_replica.process.ingress_bw
min_bw = min_host_bw if min_host_bw < min_replica_bw else min_replica_bw
transmission_time = (payload_size / min_bw) * 1000000000
path = nx.shortest_path(self.host.cluster.topology, self.host, _destination_nic.host)
edges = list(zip(path, path[1:]))
path_len = len(path)
latency = 0
transmission_str = ""
for node_id in np.arange(path_len - 1):
if isinstance(path[node_id], Router):
latency += path[node_id].latency
transmission_str += str(path[node_id].latency) + " + "
link_data = self.host.cluster.topology.get_edge_data(path[node_id], path[node_id + 1])[0]
latency += link_data['latency']
transmission_str += str(link_data['latency']) + " + "
transmission_str += str(transmission_time)
transmission_time += latency
if self.host.cluster.sim.debug:
self.host.cluster.sim.logger.log(" ****** Transmission time = " + str(
transmission_str) + " = " + str(transmission_time), 3)
return transmission_time # if not 0 <= transmission_time < 1 else 1 # in nanoseconds
[docs]
def dismiss_bw(self, bandwidth_request):
"""
This method is used to dismiss the bandwidth request from the NIC. It is used when the transmission is finished.
:param bandwidth_request:
:return:
"""
self.bandwidth_requests_total -= bandwidth_request
if self.bandwidth_requests_total < 0:
raise Exception("The NIC " + str(self) +
" is trying to release more bandwidth requestes than overally requested from this NIC")
[docs]
def request_bw(self, bandwidth_request):
"""
This method is used to request bandwidth from the NIC. It is used when a transmission is started.
:param bandwidth_request:
:return:
"""
self.bandwidth_requests_total += bandwidth_request
[docs]
def get_available(self):
"""
This simply returns the difference between the NIC's total bandwidth and requested bandwidth. If available bandwidth
is less than 0, it returns 0.
:return:
"""
available = self.bandwidth - self.bandwidth_requests_total
return self.bandwidth if self.bandwidth_requests_total > self.bandwidth else available