88import time
99import serial
1010import atexit
11+ import re
12+ import subprocess
1113
1214import datetime
1315import json
@@ -83,6 +85,18 @@ def _open_ethernet_socket(self) -> socket.socket:
8385 s .setsockopt (socket .SOL_SOCKET , socket .SO_REUSEADDR , 1 )
8486 # Low latency for small request/response commands.
8587 s .setsockopt (socket .IPPROTO_TCP , socket .TCP_NODELAY , 1 )
88+ # On Linux, TCP_QUICKACK asks the kernel to ACK promptly, which can
89+ # reduce delayed-ACK induced jitter for tiny request/response loops.
90+ if hasattr (socket , "TCP_QUICKACK" ):
91+ try :
92+ s .setsockopt (socket .IPPROTO_TCP , socket .TCP_QUICKACK , 1 )
93+ except OSError :
94+ pass
95+ # Keepalive can help detect dead peers on long runs.
96+ try :
97+ s .setsockopt (socket .SOL_SOCKET , socket .SO_KEEPALIVE , 1 )
98+ except OSError :
99+ pass
86100 s .settimeout (SOCKET_TIMEOUT )
87101 s .connect ((self ._ethernet_ip_address , ETHERNET_SERVER_PORT ))
88102 return s
@@ -535,7 +549,7 @@ def stream_frames(
535549 frames = [blob [i * frame_size :(i + 1 ) * frame_size ] for i in range (num_frames )]
536550
537551 runtime_duration_s = float (runtime_duration ) / float (RUNTIME_DURATION_PER_SECOND )
538- frames_total = int (runtime_duration_s * float (frame_rate ))
552+ frames_target = int (runtime_duration_s * float (frame_rate )) if frame_rate else 0
539553 frame_period_ns = int ((1.0 / float (frame_rate )) * 1e9 ) if frame_rate else 0
540554
541555 analog_update_period_ns = int ((1.0 / float (analog_update_rate )) * 1e9 ) if analog_update_rate else 0
@@ -567,11 +581,39 @@ def analog_waveform_for(name: str):
567581 analog_value_cached = int (round (analog_offset ))
568582
569583 start_time_ns = time .perf_counter_ns ()
584+ end_time_ns = start_time_ns + int (runtime_duration_s * 1e9 )
585+
586+ # Schedule the first frame "immediately" and then advance by period.
587+ next_frame_deadline_ns = start_time_ns
588+
570589 next_progress_ns = None
571590 if progress_interval_s and (progress_interval_s > 0 ):
572591 next_progress_ns = start_time_ns + int (progress_interval_s * 1e9 )
573592
574- for i in range (frames_total ):
593+ i = 0
594+ while True :
595+ now_ns = time .perf_counter_ns ()
596+ if now_ns >= end_time_ns :
597+ break
598+
599+ # Rate limiting: hybrid sleep + spin. This keeps CPU load reasonable,
600+ # but still gives predictable timing at ~200Hz+.
601+ if frame_period_ns :
602+ while True :
603+ now_ns = time .perf_counter_ns ()
604+ remaining_ns = next_frame_deadline_ns - now_ns
605+ if remaining_ns <= 0 :
606+ break
607+ if remaining_ns > 2_000_000 : # > 2ms
608+ time .sleep ((remaining_ns - 1_000_000 ) / 1e9 ) # leave ~1ms for spin
609+ else :
610+ pass
611+
612+ # If we crossed the end-of-window while waiting, stop without
613+ # starting another frame.
614+ if time .perf_counter_ns () >= end_time_ns :
615+ break
616+
575617 frame_index = i % num_frames
576618 frame = frames [frame_index ]
577619
@@ -611,15 +653,17 @@ def analog_waveform_for(name: str):
611653 if now_ns >= next_progress_ns :
612654 elapsed_s = (now_ns - start_time_ns ) / 1e9
613655 rate_hz = frames_streamed / elapsed_s if elapsed_s > 0 else 0.0
614- print (f'progress: { frames_streamed } /{ frames_total } frames ({ rate_hz :.1f} Hz)' )
656+ if frames_target :
657+ print (f'progress: { frames_streamed } /{ frames_target } frames ({ rate_hz :.1f} Hz)' )
658+ else :
659+ print (f'progress: { frames_streamed } frames ({ rate_hz :.1f} Hz)' )
615660 next_progress_ns += int (progress_interval_s * 1e9 )
616661
617- # Rate limiting (busy-wait)
618662 if frame_period_ns :
619- while ( time . perf_counter_ns () - start_time_ns ) < (( i + 1 ) * frame_period_ns ):
620- pass
663+ next_frame_deadline_ns += frame_period_ns
664+ i += 1
621665
622- # End the mode
666+ # End the mode
623667 self ._send_and_receive (bytes ([1 , 0 ]), ethernet_socket )
624668
625669 elapsed_s = (time .perf_counter_ns () - start_time_ns ) / 1e9
@@ -633,6 +677,8 @@ def analog_waveform_for(name: str):
633677 "rate_hz" : rate_hz ,
634678 "bytes_sent" : bytes_sent ,
635679 "tx_mbps" : mbps ,
680+ "duration_requested_s" : runtime_duration_s ,
681+ "frames_target" : frames_target ,
636682 }
637683
638684 def all_off_str (self ):
@@ -726,13 +772,67 @@ def bench_metadata(self, label: str | None = None) -> dict:
726772 except OSError :
727773 pass
728774
775+ # Best-effort interface / route metadata (useful when comparing switches/NICs).
776+ peer_ip = meta .get ("ethernet_ip" )
777+ if peer_ip :
778+ try :
779+ route_out = subprocess .check_output (["ip" , "route" , "get" , str (peer_ip )], text = True ).strip ()
780+ meta ["net_route_get" ] = route_out
781+ m = re .search (r"\bdev\s+(\S+)" , route_out )
782+ iface = m .group (1 ) if m else None
783+ if iface :
784+ info : dict [str , object ] = {"interface" : iface }
785+ sysfs_base = f"/sys/class/net/{ iface } "
786+
787+ def _read_sysfs (name : str ) -> str | None :
788+ try :
789+ with open (f"{ sysfs_base } /{ name } " , "r" , encoding = "utf-8" ) as f :
790+ return f .read ().strip ()
791+ except Exception :
792+ return None
793+
794+ mtu = _read_sysfs ("mtu" )
795+ if mtu is not None :
796+ try :
797+ info ["mtu" ] = int (mtu )
798+ except ValueError :
799+ info ["mtu" ] = mtu
800+
801+ speed = _read_sysfs ("speed" )
802+ if speed is not None :
803+ try :
804+ info ["speed_mbps" ] = int (speed )
805+ except ValueError :
806+ info ["speed_mbps" ] = speed
807+
808+ duplex = _read_sysfs ("duplex" )
809+ if duplex is not None :
810+ info ["duplex" ] = duplex
811+
812+ operstate = _read_sysfs ("operstate" )
813+ if operstate is not None :
814+ info ["operstate" ] = operstate
815+
816+ mac = _read_sysfs ("address" )
817+ if mac is not None :
818+ info ["mac" ] = mac
819+
820+ meta ["net_interface" ] = info
821+ except Exception :
822+ # Non-Linux hosts (or minimal containers) may not have `ip` or sysfs.
823+ pass
824+
825+
729826 return meta
730827
731828 def bench_connect_time (self , iters : int = 200 ) -> dict :
732829 """Measure TCP connect() time to the controller (Ethernet only)."""
733830 if not self ._ethernet_ip_address :
734831 raise RuntimeError ("bench_connect_time requires Ethernet mode (set_ethernet_mode)." )
735832
833+ # Clear any prior socket error so results are per-run.
834+ self ._socket_last_error = None
835+
736836 samples_ms : list [float ] = []
737837 errors = 0
738838
@@ -789,6 +889,10 @@ def bench_command_rtt(
789889 if connect_mode not in {"persistent" , "new_connection" }:
790890 raise ValueError ("connect_mode must be 'persistent' or 'new_connection'" )
791891
892+ # Clear any prior socket error so results are per-run.
893+ self ._socket_last_error = None
894+
895+
792896 reconnects_before = self .get_socket_reconnects (reset = False )
793897
794898 if wrap_mode :
@@ -887,6 +991,9 @@ def bench_spf_updates(
887991 if pacing not in {"target" , "max" }:
888992 raise ValueError ("pacing must be 'target' or 'max'" )
889993
994+ # Clear any prior socket error so results are per-run.
995+ self ._socket_last_error = None
996+
890997 reconnects_before = self .get_socket_reconnects (reset = False )
891998
892999 self .reset_perf_stats ()
@@ -1004,6 +1111,9 @@ def bench_stream_frames(
10041111 - `pattern_path` can be either a `.pattern` file (frame_size header) or
10051112 a `.pat` file from the `patterns/` directory.
10061113 """
1114+ # Clear any prior socket error so results are per-run.
1115+ self ._socket_last_error = None
1116+
10071117 reconnects_before = self .get_socket_reconnects (reset = False )
10081118
10091119 self .reset_perf_stats ()
0 commit comments