313 lines
9.2 KiB
Python
313 lines
9.2 KiB
Python
#!/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())
|