Skip to content

Commit 0939a2b

Browse files
committed
Update tests
1 parent 9755d43 commit 0939a2b

File tree

3 files changed

+125
-7
lines changed

3 files changed

+125
-7
lines changed

.metadata/metadata.org

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,10 @@ arena-interface --ethernet 192.168.10.104 bench \
330330
--stream-path ./patterns/pat0004.pat
331331
#+END_SRC
332332

333+
Notes:
334+
335+
- =--stream-seconds= is wall-clock duration. If the firmware/host link can't sustain =--stream-rate-hz=, the run still ends after that duration and =achieved_hz= will be lower.
336+
333337
Tips for comparing Ethernet switches/hosts/LANs:
334338

335339
- Keep the firmware build and the pattern file constant across runs.

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ Options:
196196
--stream-path ./patterns/pat0004.pat
197197
```
198198

199+
Notes:
200+
201+
- `--stream-seconds` is wall-clock duration. If the firmware/host link can't sustain `--stream-rate-hz`, the run still ends after that duration and `achieved` will be lower.
202+
199203
Tips for comparing Ethernet switches/hosts/LANs:
200204
201205
- Keep the firmware build and the pattern file constant across runs.

arena_interface/arena_interface.py

Lines changed: 117 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import time
99
import serial
1010
import atexit
11+
import re
12+
import subprocess
1113

1214
import datetime
1315
import 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

Comments
 (0)