1111# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212# See the License for the specific language governing permissions and
1313# limitations under the License.
14- from typing import Any , List
14+ from typing import Any , List , Optional
1515import statistics
1616import io
1717import os
18+ import socket
19+ import psutil
1820
21+ _C4_STANDARD_192_NIC = "ens3" # can be fetched via ip link show
1922
2023def publish_benchmark_extra_info (
2124 benchmark : Any ,
2225 params : Any ,
2326 benchmark_group : str = "read" ,
2427 true_times : List [float ] = [],
28+ download_bytes_list : Optional [List [int ]] = None ,
29+ duration : Optional [int ] = None ,
2530) -> None :
31+
2632 """
2733 Helper function to publish benchmark parameters to the extra_info property.
2834 """
@@ -41,13 +47,23 @@ def publish_benchmark_extra_info(
4147 benchmark .extra_info ["processes" ] = params .num_processes
4248 benchmark .group = benchmark_group
4349
44- object_size = params .file_size_bytes
45- num_files = params .num_files
46- total_uploaded_mib = object_size / (1024 * 1024 ) * num_files
47- min_throughput = total_uploaded_mib / benchmark .stats ["max" ]
48- max_throughput = total_uploaded_mib / benchmark .stats ["min" ]
49- mean_throughput = total_uploaded_mib / benchmark .stats ["mean" ]
50- median_throughput = total_uploaded_mib / benchmark .stats ["median" ]
50+ if download_bytes_list is not None :
51+ assert duration is not None , "Duration must be provided if total_bytes_transferred is provided."
52+ throughputs_list = [x / duration / (1024 * 1024 ) for x in download_bytes_list ]
53+ min_throughput = min (throughputs_list )
54+ max_throughput = max (throughputs_list )
55+ mean_throughput = statistics .mean (throughputs_list )
56+ median_throughput = statistics .median (throughputs_list )
57+
58+
59+ else :
60+ object_size = params .file_size_bytes
61+ num_files = params .num_files
62+ total_uploaded_mib = object_size / (1024 * 1024 ) * num_files
63+ min_throughput = total_uploaded_mib / benchmark .stats ["max" ]
64+ max_throughput = total_uploaded_mib / benchmark .stats ["min" ]
65+ mean_throughput = total_uploaded_mib / benchmark .stats ["mean" ]
66+ median_throughput = total_uploaded_mib / benchmark .stats ["median" ]
5167
5268 benchmark .extra_info ["throughput_MiB_s_min" ] = min_throughput
5369 benchmark .extra_info ["throughput_MiB_s_max" ] = max_throughput
@@ -165,3 +181,74 @@ def seek(self, offset, whence=io.SEEK_SET):
165181 # Clamp position to valid range [0, size]
166182 self ._pos = max (0 , min (new_pos , self ._size ))
167183 return self ._pos
184+
185+
186+ def get_nic_pci (nic ):
187+ """Gets the PCI address of a network interface."""
188+ return os .path .basename (os .readlink (f"/sys/class/net/{ nic } /device" ))
189+
190+
191+ def get_irqs_for_pci (pci ):
192+ """Gets the IRQs associated with a PCI address."""
193+ irqs = []
194+ with open ("/proc/interrupts" ) as f :
195+ for line in f :
196+ if pci in line :
197+ irq = line .split (":" )[0 ].strip ()
198+ irqs .append (irq )
199+ return irqs
200+
201+
202+ def get_affinity (irq ):
203+ """Gets the CPU affinity of an IRQ."""
204+ path = f"/proc/irq/{ irq } /smp_affinity_list"
205+ try :
206+ with open (path ) as f :
207+ return f .read ().strip ()
208+ except FileNotFoundError :
209+ return "N/A"
210+
211+
212+ def get_primary_interface_name ():
213+ primary_ip = None
214+
215+ # 1. Determine the Local IP used for internet access
216+ # We use UDP (SOCK_DGRAM) so we don't actually send a handshake/packet
217+ s = socket .socket (socket .AF_INET , socket .SOCK_DGRAM )
218+ try :
219+ # connect() to a public IP (Google DNS) to force route resolution
220+ s .connect (('8.8.8.8' , 80 ))
221+ primary_ip = s .getsockname ()[0 ]
222+ except Exception :
223+ # Fallback if no internet
224+ return None
225+ finally :
226+ s .close ()
227+
228+ # 2. Match that IP to an interface name using psutil
229+ if primary_ip :
230+ interfaces = psutil .net_if_addrs ()
231+ for name , addresses in interfaces .items ():
232+ for addr in addresses :
233+ # check if this interface has the IP we found
234+ if addr .address == primary_ip :
235+ return name
236+ return None
237+
238+
239+ def get_irq_affinity ():
240+ """Gets the set of CPUs for a given network interface."""
241+ nic = get_primary_interface_name ()
242+ if not nic :
243+ nic = _C4_STANDARD_192_NIC
244+
245+ pci = get_nic_pci (nic )
246+ irqs = get_irqs_for_pci (pci )
247+ cpus = set ()
248+ for irq in irqs :
249+ affinity_str = get_affinity (irq )
250+ if affinity_str != "N/A" :
251+ for part in affinity_str .split (',' ):
252+ if '-' not in part :
253+ cpus .add (int (part ))
254+ return cpus
0 commit comments