Files
DCU_DesktopController/scripts/skip_nic_experiment.py
T

313 lines
9.2 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""可回滚的 I210 网卡参数实验:停环 → 改高级属性 → 恢复 OP → 标定跳拍率。
默认只测、不永久破坏:原值会写入 restore json;`--restore` 按快照还原。
改网卡会触发短暂链路重协商,期间从站可能掉 OP,脚本会等 link+OP 恢复。
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
import time
import urllib.request
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
LAB = os.path.join(ROOT, "lab", "skip_meter")
NIC = "以太网 2"
BASE = os.environ.get("ECAT_BASE", "http://127.0.0.1:8090")
def post(path, body=None, timeout=8.0):
data = json.dumps(body or {}).encode()
req = urllib.request.Request(
BASE + path, data=data, headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read().decode())
def ps(cmd, timeout=60):
r = subprocess.run(
["powershell", "-NoProfile", "-Command", cmd],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=timeout,
)
return (r.stdout or "").strip(), (r.stderr or "").strip()
def nic_snapshot():
out, err = ps(
f"Get-NetAdapterAdvancedProperty -Name '{NIC}' | "
"Select-Object RegistryKeyword,DisplayValue,RegistryValue | "
"ConvertTo-Json -Compress"
)
if not out:
return []
data = json.loads(out)
if isinstance(data, dict):
return [data]
return data
def nic_set(keyword: str, value: str):
# value as string; Intel accepts "0"/"1"/numbers
cmd = (
f"Set-NetAdapterAdvancedProperty -Name '{NIC}' "
f"-RegistryKeyword '{keyword}' -RegistryValue '{value}' -NoRestart"
)
out, err = ps(cmd)
return out, err
def nic_restart():
out, err = ps(
f"Restart-NetAdapter -Name '{NIC}' -Confirm:$false; Start-Sleep -Seconds 2; "
f"Get-NetAdapter -Name '{NIC}' | Select-Object Status,LinkSpeed | ConvertTo-Json -Compress"
)
return out, err
def wait_link(timeout=20.0):
t0 = time.time()
while time.time() - t0 < timeout:
out, _ = ps(
f"Get-NetAdapter -Name '{NIC}' | Select-Object Status,LinkSpeed | ConvertTo-Json -Compress"
)
try:
j = json.loads(out)
if isinstance(j, list):
j = j[0]
if str(j.get("Status", "")).lower() == "up":
return True, j
except Exception:
pass
time.sleep(0.5)
return False, None
def wait_ecat_healthy(timeout=25.0):
t0 = time.time()
last = None
while time.time() - t0 < timeout:
try:
h = post("/api/native/health")
last = h
if h.get("loop") == 1 and int(h.get("wkc") or 0) >= 3 and h.get("op") == 1:
# also require cycles advancing
c0 = h.get("cycles") or 0
time.sleep(0.4)
h2 = post("/api/native/health")
c1 = h2.get("cycles") or 0
if c1 > c0:
return True, h2
except Exception as e:
last = {"err": str(e)}
time.sleep(0.5)
return False, last
def stop_bus():
logs = []
for path in ("/api/motor/stop", "/api/op/stop"):
try:
r = post(path)
logs.append((path, r.get("ok"), (r.get("log") or r)[:2]))
except Exception as e:
logs.append((path, False, str(e)))
return logs
def start_bus():
logs = []
try:
r = post("/api/op/start")
logs.append(("/api/op/start", r.get("ok"), (r.get("log") or [""])[:2]))
except Exception as e:
logs.append(("/api/op/start", False, str(e)))
try:
r = post("/api/motor/start")
logs.append(("/api/motor/start", r.get("ok"), (r.get("log") or [""])[:2]))
except Exception as e:
logs.append(("/api/motor/start", False, str(e)))
return logs
def meter(tag: str, secs: float):
# reuse skip_rate_meter as subprocess for isolation
cmd = [
sys.executable,
os.path.join(ROOT, "scripts", "skip_rate_meter.py"),
"--tag",
tag,
"--secs",
str(secs),
]
r = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace")
# last json file for tag
files = sorted(
[f for f in os.listdir(LAB) if f.startswith(tag + "_") and f.endswith(".json")]
)
data = None
if files:
with open(os.path.join(LAB, files[-1]), encoding="utf-8") as f:
data = json.load(f)
return data, r.stdout, r.stderr
def apply_batch(changes: list[tuple[str, str]]):
results = []
for k, v in changes:
out, err = nic_set(k, v)
results.append({"key": k, "value": v, "out": out[:300], "err": err[:300]})
return results
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--restore", action="store_true", help="restore from snap json")
ap.add_argument("--snap", default=None, help="snapshot json for restore")
ap.add_argument("--exp", default=None, help="named experiment")
ap.add_argument("--secs", type=float, default=16.0)
ap.add_argument("--list-exp", action="store_true")
args = ap.parse_args()
os.makedirs(LAB, exist_ok=True)
# experiment table: list of (keyword, value)
EXPS = {
"itr_off": [
("*InterruptModeration", "0"),
("ITR", "0"),
],
"itr_low": [
("*InterruptModeration", "1"),
("ITR", "400"), # valid: 65535,3600,2000,950,400,200,0
],
"itr_200": [
("*InterruptModeration", "1"),
("ITR", "200"),
],
"itr_950": [
("*InterruptModeration", "1"),
("ITR", "950"),
],
"eee_off": [
("EEELinkAdvertisement", "0"),
],
"flow_off": [
("*FlowControl", "0"),
],
"txbuf_max": [
("*TransmitBuffers", "4096"),
],
"rxbuf_max": [
("*ReceiveBuffers", "1024"),
],
"combo_lowlat": [
("*InterruptModeration", "0"),
("ITR", "0"),
("EEELinkAdvertisement", "0"),
("*FlowControl", "0"),
("*TransmitBuffers", "2048"),
("*ReceiveBuffers", "512"),
],
}
if args.list_exp:
print(json.dumps({k: v for k, v in EXPS.items()}, indent=2))
return 0
# always keep a fresh snapshot before mutating
snap = nic_snapshot()
snap_path = os.path.join(LAB, f"nic_before_{int(time.time())}.json")
with open(snap_path, "w", encoding="utf-8") as f:
json.dump(snap, f, ensure_ascii=False, indent=2)
print(f"NIC snapshot → {snap_path}")
if args.restore:
path = args.snap or snap_path
with open(path, encoding="utf-8") as f:
data = json.load(f)
# stop, apply all, restart
print("restore: stop bus")
print(stop_bus())
n = 0
for row in data:
kw = row.get("RegistryKeyword")
val = row.get("RegistryValue")
if not kw or val is None:
continue
if isinstance(val, list):
val = val[0] if val else ""
val = str(val)
apply_batch([(kw, val)])
n += 1
print("restore applied", n, "keys; restarting NIC")
print(nic_restart())
print(wait_link())
print("start bus")
print(start_bus())
ok, h = wait_ecat_healthy()
print("healthy", ok, h)
return 0
exp = args.exp or "itr_off"
if exp not in EXPS:
print("unknown exp", exp, "use --list-exp")
return 2
changes = EXPS[exp]
print(f"\n=== EXP {exp} {changes} ===")
print("stop bus...")
print(stop_bus())
time.sleep(0.3)
print("apply...")
print(apply_batch(changes))
print("restart nic...")
print(nic_restart())
ok, info = wait_link()
print("link", ok, info)
print("start bus...")
print(start_bus())
ok, h = wait_ecat_healthy(timeout=30)
print("healthy", ok, h)
if not ok:
print("ABORT: bus not healthy after change")
return 1
# settle longer: NIC restart + re-enumeration leaves ~2–5s of elevated skip
time.sleep(8.0)
data, out, err = meter(f"exp_{exp}", args.secs)
print(out)
if err:
print("meter stderr", err[:500])
if data:
summary = {
"exp": exp,
"changes": changes,
"late_per_s": data.get("late_per_s"),
"skip_per_s": data.get("skip_per_s"),
"late_frac_pct": data.get("late_frac_pct"),
"send_stall_frac": data.get("send_stall_frac"),
"rx_stall_frac": data.get("rx_stall_frac"),
"achieved_hz": data.get("achieved_hz"),
"n_late": data.get("n_late"),
}
sp = os.path.join(LAB, f"summary_{exp}_{int(time.time())}.json")
with open(sp, "w", encoding="utf-8") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print("SUMMARY", json.dumps(summary, ensure_ascii=False))
print("WROTE", sp)
return 0
if __name__ == "__main__":
raise SystemExit(main())