Skip to content

Commit c896fb6

Browse files
committed
feat: add LAN bufferbloat testing with iperf3 and RTT measurements
1 parent 0d07454 commit c896fb6

File tree

2 files changed

+131
-9
lines changed

2 files changed

+131
-9
lines changed

config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,14 @@
5858
LOCAL_UPSTREAM_SPEED_THRESHOLD: float = 225.0
5959
GATEWAY_DOWNSTREAM_SPEED_THRESHOLD: float = 300.0
6060
GATEWAY_UPSTREAM_SPEED_THRESHOLD: float = 300.0
61+
62+
# --- LAN Bufferbloat Test Configuration ---
63+
# Set to True to run the LAN-specific bufferbloat test against another
64+
# machine on your local network. Requires `iperf3` on both machines.
65+
RUN_LAN_BUFFERBLOAT_TEST: bool = True
66+
# The IP address of the second machine on your LAN running `iperf3 -s`.
67+
LAN_TEST_TARGET_IP: str = "192.168.4.135" # <--- CHANGE THIS TO YOUR SERVER's IP
68+
# How long (in seconds) the LAN load test should run.
69+
LAN_BUFFERBLOAT_TEST_DURATION: int = 10
70+
# Threshold for LAN bufferbloat delta (in ms).
71+
LAN_BUFFERBLOAT_DELTA_THRESHOLD: float = 50.0

main.py

Lines changed: 120 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,10 @@ def log_results(all_data: Mapping[str, str | float | int | None]) -> None:
284284
"WiFi_RSSI": all_data.get("wifi_rssi", "N/A"),
285285
"WiFi_Noise": all_data.get("wifi_noise", "N/A"),
286286
"WiFi_TxRate_Mbps": all_data.get("wifi_tx_rate", "N/A"),
287+
# LAN bufferbloat metrics
288+
"LAN_Idle_RTT_ms": all_data.get("lan_idle_rtt_ms"),
289+
"LAN_Under_Load_RTT_ms": all_data.get("lan_under_load_rtt_ms"),
290+
"LAN_Bufferbloat_ms": all_data.get("lan_bufferbloat_ms"),
287291
}
288292

289293
# --- CSV Logging ---
@@ -454,6 +458,25 @@ def format_value(
454458
print(f" Noise Level: {data_points['WiFi_Noise']}")
455459
print(f" Channel/Band: {data_points['WiFi_Channel']}")
456460
print(f" Transmit Rate: {data_points['WiFi_TxRate_Mbps']} Mbps")
461+
462+
# --- LAN Bufferbloat Test ---
463+
print("\n--- LAN Bufferbloat Test ---")
464+
lan_idle = format_value(
465+
data_points["LAN_Idle_RTT_ms"],
466+
"ms",
467+
None,
468+
default_color=Colors.CYAN,
469+
precision=3,
470+
)
471+
print(f" Idle LAN RTT: {lan_idle}")
472+
473+
lan_bloat = format_value(
474+
data_points["LAN_Bufferbloat_ms"],
475+
"ms",
476+
config.LAN_BUFFERBLOAT_DELTA_THRESHOLD,
477+
precision=2,
478+
)
479+
print(f" LAN Bufferbloat Delta: {lan_bloat}")
457480
print("------------------------------------")
458481
full_path = os.path.abspath(config.LOG_FILE)
459482
print(f"Results appended to: {full_path}")
@@ -675,6 +698,78 @@ def run_local_speed_test_task() -> Optional[SpeedResults]:
675698
return None
676699

677700

701+
# --- LAN Bufferbloat Test ---
702+
def run_lan_bufferbloat_task() -> dict[str, float | None]:
703+
"""
704+
Measures LAN-specific bufferbloat by pinging a local server
705+
with and without a concurrent iperf3 load test.
706+
"""
707+
if not config.LAN_TEST_TARGET_IP:
708+
print("Warning: LAN_TEST_TARGET_IP not set. Skipping LAN bufferbloat test.")
709+
return {}
710+
711+
target_ip = config.LAN_TEST_TARGET_IP
712+
duration = config.LAN_BUFFERBLOAT_TEST_DURATION
713+
results: dict[str, float | None] = {
714+
"lan_idle_rtt_ms": None,
715+
"lan_under_load_rtt_ms": None,
716+
"lan_bufferbloat_ms": None,
717+
}
718+
719+
print(f"--- Starting LAN Bufferbloat Test against {target_ip} ---")
720+
721+
try:
722+
# 1. Measure Idle Latency
723+
print("Measuring idle LAN latency...")
724+
idle_ping_results = run_local_ping_task(target_ip)
725+
results["lan_idle_rtt_ms"] = idle_ping_results.get("rtt_avg_ms")
726+
if results["lan_idle_rtt_ms"] is None:
727+
print("Error: Could not measure idle LAN latency. Aborting test.")
728+
return {}
729+
730+
# 2. Start iperf3 load in the background
731+
print(f"Starting iperf3 load test for {duration} seconds...")
732+
iperf_command = ["iperf3", "-c", target_ip, "-t", str(duration)]
733+
iperf_process = subprocess.Popen(
734+
iperf_command,
735+
stdout=subprocess.DEVNULL,
736+
stderr=subprocess.DEVNULL,
737+
)
738+
739+
# Give iperf a moment to start before pinging
740+
time.sleep(1)
741+
742+
# 3. Measure Latency Under Load (for the remaining duration)
743+
print("Measuring LAN latency under load...")
744+
ping_duration = max(1, duration - 1)
745+
# We use a different ping command here to control duration
746+
ping_command = ["ping", "-c", str(ping_duration), "-i", "1", target_ip]
747+
under_load_ping_process = subprocess.run(
748+
ping_command, capture_output=True, text=True, timeout=duration + 5
749+
)
750+
under_load_ping_results = parse_local_ping_results(under_load_ping_process.stdout)
751+
results["lan_under_load_rtt_ms"] = under_load_ping_results.get("rtt_avg_ms")
752+
753+
# 4. Wait for iperf3 to finish
754+
iperf_process.wait(timeout=5)
755+
print("LAN load test finished.")
756+
757+
# 5. Calculate LAN Bufferbloat
758+
if results["lan_under_load_rtt_ms"] is not None:
759+
results["lan_bufferbloat_ms"] = (
760+
results["lan_under_load_rtt_ms"] - results["lan_idle_rtt_ms"]
761+
)
762+
763+
return results
764+
765+
except FileNotFoundError:
766+
print("Error: 'iperf3' command not found. Please run 'brew install iperf3'.")
767+
return {}
768+
except Exception as e:
769+
print(f"An error occurred during the LAN bufferbloat test: {e}")
770+
return {}
771+
772+
678773
def run_wifi_diagnostics_task() -> WifiDiagnostics:
679774
"""
680775
Uses a hybrid approach: wdutil for live Wi-Fi stats (signal, etc.) and
@@ -802,6 +897,12 @@ def perform_checks() -> None:
802897
debug_log.log("run_local_speed_test_task: END")
803898
if local_speed_results:
804899
master_results.update(local_speed_results)
900+
if getattr(config, "RUN_LAN_BUFFERBLOAT_TEST", False):
901+
debug_log.log("run_lan_bufferbloat_task: START")
902+
lan_bloat_results = run_lan_bufferbloat_task()
903+
debug_log.log("run_lan_bufferbloat_task: END")
904+
if lan_bloat_results:
905+
master_results.update(lan_bloat_results)
805906

806907
# --- Run Gateway Tests (Selenium Required) in a single session ---
807908
should_run_gateway_speed_test = (
@@ -872,17 +973,27 @@ def perform_checks() -> None:
872973

873974

874975
# --- Scheduler ---
976+
def main() -> None:
977+
"""Sets up the schedule and runs the main application loop."""
978+
print("--- Simple Gateway Logger Starting ---")
979+
980+
# 1. Schedule the job to run every X minutes. This sets the timeline.
981+
schedule.every(config.RUN_INTERVAL_MINUTES).minutes.do(perform_checks)
982+
983+
# 2. Manually run the job once immediately.
984+
# The next scheduled run will still be based on the timeline set above.
985+
perform_checks()
986+
987+
# 3. Start the main loop to handle all subsequent scheduled runs.
988+
while True:
989+
schedule.run_pending()
990+
time.sleep(1)
991+
992+
875993
if __name__ == "__main__":
876994
if getattr(config, "ENABLE_DEBUG_LOGGING", False):
877995
logging.basicConfig()
878996
schedule_logger = logging.getLogger("schedule")
879997
schedule_logger.setLevel(level=logging.DEBUG)
880-
perform_checks()
881-
schedule.every(config.RUN_INTERVAL_MINUTES).minutes.do(perform_checks)
882-
if schedule.jobs:
883-
next_run_datetime = schedule.jobs[0].next_run
884-
print(f"Next test is scheduled for: {next_run_datetime.strftime('%Y-%m-%d %H:%M:%S')}")
885-
print("Press Ctrl+C to exit.")
886-
while True:
887-
schedule.run_pending()
888-
time.sleep(1)
998+
999+
main()

0 commit comments

Comments
 (0)