feat(ecat): 启动后把 ecat_native 提到 RealTime;跳拍优化实验脚本与报告
- esp32_server: Popen 后外部 SetPriorityClass(RT/High),native 自设会被钳成 High - 入库 skip_* 标定/实验脚本与 REPORT_ecat_skip_opt_20260917 - 前端 WIP:layout/app/polling/boot_ecat/index - 不含 lab/skip_meter 原始采样与 preview 截图
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""模拟页面 pollStatus:5s 超时算失败,连续失败即离线。"""
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
URL = "http://192.168.31.127/system/status"
|
||||
TIMEOUT = 5.0
|
||||
POLL = 1.0
|
||||
WINDOW = 75
|
||||
|
||||
|
||||
def probe():
|
||||
t0 = time.time()
|
||||
try:
|
||||
with urllib.request.urlopen(URL, timeout=TIMEOUT) as r:
|
||||
r.read(64)
|
||||
return True, "HTTP %s" % r.status, time.time() - t0
|
||||
except urllib.error.HTTPError as e:
|
||||
return False, "HTTP %s" % e.code, time.time() - t0
|
||||
except Exception as e:
|
||||
return False, "%s: %s" % (type(e).__name__, str(e)[:80]), time.time() - t0
|
||||
|
||||
|
||||
def main():
|
||||
print("=== monitor start (mimic pollStatus) ===", flush=True)
|
||||
prev = None
|
||||
fails = 0
|
||||
t_end = time.time() + WINDOW
|
||||
while time.time() < t_end:
|
||||
ok, msg, dt = probe()
|
||||
now = time.strftime("%H:%M:%S")
|
||||
if ok:
|
||||
fails = 0
|
||||
state = "ONLINE"
|
||||
else:
|
||||
fails += 1
|
||||
state = "FAIL#%d" % fails + (" -> OFFLINE" if fails >= 1 else "")
|
||||
mark = " *" if state != prev else ""
|
||||
print("[%s] %s (%s, %.0fms)%s" % (now, state, msg, dt * 1000, mark), flush=True)
|
||||
if state != prev:
|
||||
prev = state
|
||||
time.sleep(POLL)
|
||||
print("=== monitor end ===", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""在「以太网 2」(I210) 上关闭非 EtherCAT 绑定,测跳拍率;可 --restore 还原。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
LAB = os.path.join(ROOT, "lab", "skip_meter")
|
||||
NIC = "以太网 2"
|
||||
|
||||
|
||||
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 snapshot():
|
||||
out, err = ps(
|
||||
f"Get-NetAdapterBinding -Name '{NIC}' | "
|
||||
"Select-Object DisplayName,ComponentID,Enabled | ConvertTo-Json -Compress"
|
||||
)
|
||||
data = json.loads(out)
|
||||
return data if isinstance(data, list) else [data]
|
||||
|
||||
|
||||
def disable(component_ids):
|
||||
logs = []
|
||||
for cid in component_ids:
|
||||
out, err = ps(
|
||||
f"Disable-NetAdapterBinding -Name '{NIC}' -ComponentID '{cid}' -Confirm:$false; "
|
||||
f"Get-NetAdapterBinding -Name '{NIC}' -ComponentID '{cid}' | "
|
||||
"Select-Object ComponentID,Enabled | ConvertTo-Json -Compress"
|
||||
)
|
||||
logs.append({"cid": cid, "out": out[:300], "err": err[:300]})
|
||||
return logs
|
||||
|
||||
|
||||
def enable(component_ids):
|
||||
logs = []
|
||||
for cid in component_ids:
|
||||
out, err = ps(
|
||||
f"Enable-NetAdapterBinding -Name '{NIC}' -ComponentID '{cid}' -Confirm:$false; "
|
||||
f"Get-NetAdapterBinding -Name '{NIC}' -ComponentID '{cid}' | "
|
||||
"Select-Object ComponentID,Enabled | ConvertTo-Json -Compress"
|
||||
)
|
||||
logs.append({"cid": cid, "out": out[:300], "err": err[:300]})
|
||||
return logs
|
||||
|
||||
|
||||
def set_prio(level="RealTime"):
|
||||
p = int(subprocess.check_output(
|
||||
["powershell", "-NoProfile", "-Command", "(Get-Process ecat_native).Id"],
|
||||
text=True).strip())
|
||||
subprocess.check_output(
|
||||
["powershell", "-NoProfile", "-Command",
|
||||
f"$p=Get-Process -Id {p}; $p.PriorityClass='{level}'; $p.PriorityClass"],
|
||||
text=True)
|
||||
return p
|
||||
|
||||
|
||||
def meter(tag, secs=18):
|
||||
r = subprocess.run(
|
||||
[sys.executable, os.path.join(ROOT, "scripts", "skip_rate_meter.py"),
|
||||
"--tag", tag, "--secs", str(secs)],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
files = sorted(f for f in os.listdir(LAB) if f.startswith(tag + "_") and f.endswith(".json"))
|
||||
return json.load(open(os.path.join(LAB, files[-1]), encoding="utf-8"))
|
||||
|
||||
|
||||
# Offloads that are useless/dangerous for raw EtherCAT on a dedicated NIC
|
||||
OFFLOADS = [
|
||||
"vmware_bridge", # VMware Bridge — intercepts/injects, classic ECAT killer
|
||||
"ms_pacer", # QoS packet scheduler
|
||||
"ms_lldp", # LLDP
|
||||
"ms_rspndr", # LLTD responder
|
||||
"ms_lltdio", # LLTD mapper
|
||||
"ms_server", # file/printer sharing
|
||||
"ms_msclient", # Microsoft network client
|
||||
# keep ms_tcpip / ms_tcpip6 for now in case anything uses IP on this NIC
|
||||
# Npcap must stay: INSECURE_NPCAP
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(LAB, exist_ok=True)
|
||||
snap = snapshot()
|
||||
snap_path = os.path.join(LAB, f"nic_bind_{int(time.time())}.json")
|
||||
json.dump(snap, open(snap_path, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
||||
print("bind snapshot", snap_path)
|
||||
|
||||
if "--restore" in sys.argv:
|
||||
print(enable(OFFLOADS))
|
||||
return 0
|
||||
|
||||
print("disable", OFFLOADS)
|
||||
print(json.dumps(disable(OFFLOADS), ensure_ascii=False, indent=2))
|
||||
print("set RealTime")
|
||||
print("pid", set_prio("RealTime"))
|
||||
# binding change usually doesn't reset link; brief settle
|
||||
time.sleep(3)
|
||||
data = meter("bind_off", 18)
|
||||
row = {
|
||||
"late_per_s": data["late_per_s"],
|
||||
"skip_per_s": data["skip_per_s"],
|
||||
"late_frac_pct": data["late_frac_pct"],
|
||||
"achieved_hz": data["achieved_hz"],
|
||||
"n_late": data["n_late"],
|
||||
"send_stall_frac": data["send_stall_frac"],
|
||||
"rx_stall_frac": data["rx_stall_frac"],
|
||||
"work_p50": data["window_cyc_us"]["work"]["p50"],
|
||||
"rx_p50": data["window_cyc_us"]["rx"]["p50"],
|
||||
"send_p50": data["window_cyc_us"]["send"]["p50"],
|
||||
"work_p90": data["window_cyc_us"]["work"]["p90"],
|
||||
}
|
||||
print("ROW", json.dumps(row, ensure_ascii=False, indent=2))
|
||||
out = os.path.join(LAB, f"summary_bind_off_{int(time.time())}.json")
|
||||
json.dump({"row": row, "disabled": OFFLOADS, "snap": snap_path},
|
||||
open(out, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
||||
print("WROTE", out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
import json, os, subprocess, sys, time
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
LAB = os.path.join(ROOT, "lab", "skip_meter")
|
||||
NIC = "以太网 2"
|
||||
|
||||
def ps(cmd):
|
||||
r = subprocess.check_output(["powershell","-NoProfile","-Command",cmd],
|
||||
text=True, encoding="utf-8", errors="replace")
|
||||
return r.strip()
|
||||
|
||||
def set_prio(level="RealTime"):
|
||||
p = int(ps("(Get-Process ecat_native).Id"))
|
||||
ps(f"$p=Get-Process -Id {p}; $p.PriorityClass='{level}'; $p.PriorityClass")
|
||||
return p
|
||||
|
||||
def meter(tag, secs=18):
|
||||
subprocess.run([sys.executable, os.path.join(ROOT,"scripts","skip_rate_meter.py"),
|
||||
"--tag", tag, "--secs", str(secs)],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
files = sorted(f for f in os.listdir(LAB) if f.startswith(tag+"_") and f.endswith(".json"))
|
||||
return json.load(open(os.path.join(LAB, files[-1]), encoding="utf-8"))
|
||||
|
||||
def main():
|
||||
restore = "--restore" in sys.argv
|
||||
ids = ["ms_tcpip", "ms_tcpip6"]
|
||||
for cid in ids:
|
||||
if restore:
|
||||
ps(f"Enable-NetAdapterBinding -Name '{NIC}' -ComponentID '{cid}' -Confirm:$false")
|
||||
else:
|
||||
ps(f"Disable-NetAdapterBinding -Name '{NIC}' -ComponentID '{cid}' -Confirm:$false")
|
||||
print(ps(f"Get-NetAdapterBinding -Name '{NIC}' -ComponentID '{cid}' | Select-Object ComponentID,Enabled | ConvertTo-Json -Compress"))
|
||||
if restore:
|
||||
return
|
||||
print("pid", set_prio("RealTime"))
|
||||
time.sleep(3)
|
||||
data = meter("bind_noip", 18)
|
||||
row = {
|
||||
"late_per_s": data["late_per_s"], "skip_per_s": data["skip_per_s"],
|
||||
"late_frac_pct": data["late_frac_pct"], "achieved_hz": data["achieved_hz"],
|
||||
"n_late": data["n_late"],
|
||||
"send_stall_frac": data["send_stall_frac"], "rx_stall_frac": data["rx_stall_frac"],
|
||||
"work_p50": data["window_cyc_us"]["work"]["p50"],
|
||||
"rx_p50": data["window_cyc_us"]["rx"]["p50"],
|
||||
"send_p50": data["window_cyc_us"]["send"]["p50"],
|
||||
}
|
||||
print(json.dumps(row, ensure_ascii=False, indent=2))
|
||||
out = os.path.join(LAB, f"summary_bind_noip_{int(time.time())}.json")
|
||||
json.dump(row, open(out,"w",encoding="utf-8"), ensure_ascii=False, indent=2)
|
||||
print("WROTE", out)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""0.25ms 下 A/B:静默 vs 模拟 spi_test 前端轮询。"""
|
||||
import json, time, urllib.request, os
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
LAB = os.path.join(ROOT, "lab", "skip_meter")
|
||||
|
||||
def post(p, b=None, timeout=5):
|
||||
d = json.dumps(b or {}).encode()
|
||||
r = urllib.request.Request("http://127.0.0.1:8090"+p, data=d, headers={"Content-Type":"application/json"})
|
||||
with urllib.request.urlopen(r, timeout=timeout) as resp:
|
||||
return json.loads(resp.read().decode())
|
||||
|
||||
def snap():
|
||||
st = post("/api/motor/state")
|
||||
dbg = post("/api/motor/dbg")
|
||||
return {
|
||||
"cycles": st.get("cycles") or 0,
|
||||
"skip_total": dbg.get("skip_total") or 0,
|
||||
"gseq": (st.get("head") or {}).get("global_seq") or 0,
|
||||
"period": st.get("period_ms"),
|
||||
"wkc": st.get("wkc"),
|
||||
"pcore": st.get("pcore_mask"),
|
||||
"cyc_us": st.get("cyc_us"),
|
||||
}
|
||||
|
||||
def rate(a, b, dt):
|
||||
dc = b["cycles"] - a["cycles"]
|
||||
ds = (b["skip_total"] - a["skip_total"]) # may wrap? long long, no
|
||||
return {
|
||||
"dt": round(dt, 3),
|
||||
"pdo_hz": round(dc / dt, 1) if dt else None,
|
||||
"late_per_s": round(ds / dt, 2) if dt else None,
|
||||
"skip_frac": round(100.0 * ds / dc, 2) if dc else None,
|
||||
"gseq_per_s": round(((b["gseq"] - a["gseq"]) & 0xFFFFFFFF) / dt, 1) if dt else None,
|
||||
"dcyc": dc, "dskip": ds,
|
||||
}
|
||||
|
||||
def arm_period(ms):
|
||||
try:
|
||||
post("/api/motor/stop")
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.15)
|
||||
post("/api/motor/period", {"period_ms": ms})
|
||||
time.sleep(0.15)
|
||||
post("/api/motor/start")
|
||||
time.sleep(0.5)
|
||||
|
||||
print("ensure 0.25ms")
|
||||
arm_period(0.25)
|
||||
time.sleep(3)
|
||||
|
||||
# --- A: fully quiet ---
|
||||
a0 = snap()
|
||||
t0 = time.time()
|
||||
# no HTTP
|
||||
time.sleep(10)
|
||||
a1 = snap()
|
||||
A = rate(a0, a1, time.time() - t0)
|
||||
print("QUIET", json.dumps(A, ensure_ascii=False))
|
||||
print(" cyc_us", json.dumps(a1.get("cyc_us"), ensure_ascii=False))
|
||||
|
||||
# --- B: hammer like spi_test (state+load+dbg+rxraw) ---
|
||||
time.sleep(1)
|
||||
b0 = snap()
|
||||
t0 = time.time()
|
||||
n = 0
|
||||
while time.time() - t0 < 10:
|
||||
try:
|
||||
post("/api/motor/state")
|
||||
post("/api/app/load")
|
||||
post("/api/motor/dbg")
|
||||
post("/api/motor/rxraw")
|
||||
n += 1
|
||||
except Exception as e:
|
||||
print("hammer err", e)
|
||||
break
|
||||
time.sleep(0.05) # ~20Hz full set, similar to busy panel
|
||||
b1 = snap()
|
||||
B = rate(b0, b1, time.time() - t0)
|
||||
print("HAMMER", json.dumps(B, ensure_ascii=False), "polls", n)
|
||||
print(" cyc_us", json.dumps(b1.get("cyc_us"), ensure_ascii=False))
|
||||
|
||||
# --- C: only motor/state at max (no dbg/rxraw) ---
|
||||
time.sleep(1)
|
||||
c0 = snap()
|
||||
t0 = time.time()
|
||||
n = 0
|
||||
while time.time() - t0 < 10:
|
||||
try:
|
||||
post("/api/motor/state")
|
||||
n += 1
|
||||
except Exception:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
c1 = snap()
|
||||
C = rate(c0, c1, time.time() - t0)
|
||||
print("STATE_ONLY", json.dumps(C, ensure_ascii=False), "polls", n)
|
||||
|
||||
out = {"quiet": A, "hammer": B, "state_only": C,
|
||||
"quiet_cyc": a1.get("cyc_us"), "hammer_cyc": b1.get("cyc_us")}
|
||||
path = os.path.join(LAB, f"summary_fe_ab_{int(time.time())}.json")
|
||||
json.dump(out, open(path, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
||||
print("WROTE", path)
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""快照当前主机侧可回滚现场:I210 高级属性、中断亲和、ecat 进程。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
LAB = os.path.join(ROOT, "lab", "skip_meter")
|
||||
|
||||
|
||||
def ps(cmd: str) -> str:
|
||||
r = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command", cmd],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
return (r.stdout or "") + (("ERR:" + r.stderr) if r.stderr else "")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
os.makedirs(LAB, exist_ok=True)
|
||||
snap = {"ts": time.time(), "host": os.environ.get("COMPUTERNAME")}
|
||||
|
||||
snap["adapters"] = ps(
|
||||
"Get-NetAdapter | Select-Object Name,InterfaceDescription,LinkSpeed,Status,"
|
||||
"ifIndex,MacAddress | ConvertTo-Json -Compress"
|
||||
)
|
||||
# I210 specifically
|
||||
snap["i210"] = ps(
|
||||
"Get-NetAdapter | Where-Object {$_.InterfaceDescription -match 'I210'} | "
|
||||
"Select-Object Name,ifIndex,Status,LinkSpeed | ConvertTo-Json -Compress"
|
||||
)
|
||||
|
||||
# advanced properties (restore-able)
|
||||
snap["i210_adv"] = ps(
|
||||
"Get-NetAdapterAdvancedProperty -Name '以太网 2' -ErrorAction SilentlyContinue | "
|
||||
"Select-Object Name,DisplayName,DisplayValue,RegistryKeyword,RegistryValue | "
|
||||
"ConvertTo-Json -Compress"
|
||||
)
|
||||
|
||||
# interrupt moderation / RSS / power
|
||||
for kw in (
|
||||
"*InterruptModeration",
|
||||
"*ReceiveBuffers",
|
||||
"*TransmitBuffers",
|
||||
"*EEE",
|
||||
"*EnergyEfficient",
|
||||
"*FlowControl",
|
||||
"*PriorityVLANTag",
|
||||
"*TCPChecksumOffload*",
|
||||
"*IPChecksumOffload*",
|
||||
"*VMQ",
|
||||
"*RSS",
|
||||
"*InterruptModerationRate",
|
||||
):
|
||||
snap[f"adv_{kw.strip('*').replace('*','_')}"] = ps(
|
||||
f"Get-NetAdapterAdvancedProperty -Name '以太网 2' -ErrorAction SilentlyContinue | "
|
||||
f"Where-Object {{$_.RegistryKeyword -like '{kw}'}} | "
|
||||
f"Select-Object RegistryKeyword,DisplayValue,RegistryValue | ConvertTo-Json -Compress"
|
||||
)
|
||||
|
||||
snap["power"] = ps(
|
||||
"Get-NetAdapterPowerManagement -Name '以太网 2' -ErrorAction SilentlyContinue | "
|
||||
"Select-Object Name,AllowComputerToTurnOffDevice,WakeOnMagicPacket,"
|
||||
"WakeOnPattern,DeviceSleepOnDisconnect | ConvertTo-Json -Compress"
|
||||
)
|
||||
|
||||
snap["ecat_proc"] = ps(
|
||||
"Get-Process ecat_native -ErrorAction SilentlyContinue | "
|
||||
"Select-Object Id,Path,StartTime,CPU,WorkingSet | ConvertTo-Json -Compress"
|
||||
)
|
||||
|
||||
snap["ecat_cmdline"] = ps(
|
||||
"Get-CimInstance Win32_Process -Filter \"Name='ecat_native.exe'\" | "
|
||||
"Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress"
|
||||
)
|
||||
|
||||
# interrupt affinity via Get-NetAdapterInterrupt? not always available
|
||||
snap["msi_affinity"] = ps(
|
||||
"Get-NetAdapterAdvancedProperty -Name '以太网 2' -ErrorAction SilentlyContinue | "
|
||||
"Where-Object {$_.DisplayName -match 'Interrupt|Affinity|RSS|Moderation|Buffer|Energy|Green|Flow|Offload'} | "
|
||||
"Select-Object DisplayName,DisplayValue,RegistryKeyword | ConvertTo-Json -Compress"
|
||||
)
|
||||
|
||||
# CPU topology quick
|
||||
snap["cpu"] = ps(
|
||||
"$env:NUMBER_OF_PROCESSORS; (Get-CimInstance Win32_Processor | "
|
||||
"Select-Object Name,NumberOfCores,NumberOfLogicalProcessors | ConvertTo-Json -Compress)"
|
||||
)
|
||||
|
||||
out = os.path.join(LAB, f"host_snap_{int(time.time())}.json")
|
||||
with open(out, "w", encoding="utf-8") as f:
|
||||
json.dump(snap, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# also human-readable restore snippet
|
||||
lines = []
|
||||
lines.append("# Host snapshot restore notes")
|
||||
lines.append(f"time={snap['ts']}")
|
||||
lines.append("## I210 adapter advanced (raw)")
|
||||
lines.append(snap.get("i210_adv", "")[:8000])
|
||||
lines.append("## power")
|
||||
lines.append(snap.get("power", "")[:4000])
|
||||
lines.append("## ecat cmdline")
|
||||
lines.append(snap.get("ecat_cmdline", "")[:2000])
|
||||
txt = os.path.join(LAB, f"host_snap_{int(snap['ts'])}.txt")
|
||||
with open(txt, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines))
|
||||
|
||||
print("I210:", snap.get("i210", "")[:500])
|
||||
print("CMDLINE:", snap.get("ecat_cmdline", "")[:800])
|
||||
print("ADV keys sample:")
|
||||
print(snap.get("msi_affinity", "")[:2500])
|
||||
print("WROTE", out)
|
||||
print("WROTE", txt)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
import json, os, subprocess, time, urllib.request
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
LAB = os.path.join(ROOT, "lab", "skip_meter")
|
||||
NIC = "以太网 2"
|
||||
|
||||
def ps(cmd):
|
||||
return subprocess.check_output(["powershell","-NoProfile","-Command",cmd],
|
||||
text=True, encoding="utf-8", errors="replace").strip()
|
||||
|
||||
def post(p, b=None):
|
||||
d = json.dumps(b or {}).encode()
|
||||
r = urllib.request.Request("http://127.0.0.1:8090"+p, data=d, headers={"Content-Type":"application/json"})
|
||||
return json.loads(urllib.request.urlopen(r, timeout=8).read().decode())
|
||||
|
||||
def wait_ok(t=30):
|
||||
t0=time.time()
|
||||
while time.time()-t0<t:
|
||||
try:
|
||||
h=post("/api/native/health")
|
||||
if h.get("loop")==1 and int(h.get("wkc") or 0)>=3: return h
|
||||
except Exception: pass
|
||||
time.sleep(0.4)
|
||||
return None
|
||||
|
||||
def set_rt():
|
||||
p=int(ps("(Get-Process ecat_native).Id"))
|
||||
ps(f"$p=Get-Process -Id {p}; $p.PriorityClass='RealTime'; $p.PriorityClass")
|
||||
return p
|
||||
|
||||
def period(ms):
|
||||
post("/api/motor/stop"); time.sleep(0.12)
|
||||
post("/api/motor/period", {"period_ms": ms}); time.sleep(0.12)
|
||||
post("/api/motor/start")
|
||||
|
||||
def quiet(sec=10):
|
||||
st=post("/api/motor/state"); dbg=post("/api/motor/dbg")
|
||||
c0,s0=st.get("cycles") or 0, dbg.get("skip_total") or 0
|
||||
t0=time.time(); time.sleep(sec)
|
||||
st=post("/api/motor/state"); dbg=post("/api/motor/dbg")
|
||||
dt=time.time()-t0
|
||||
c1,s1=st.get("cycles") or 0, dbg.get("skip_total") or 0
|
||||
cu=st.get("cyc_us") or {}
|
||||
# also sample dbg ring k distribution
|
||||
ring=dbg.get("cyc") or []
|
||||
ks=[r[5] for r in ring if len(r)>=6]
|
||||
works=[(r[4]-r[0])*1e6 for r in ring if len(r)>=5]
|
||||
sends=[(r[2]-r[1])*1e6 for r in ring if len(r)>=3]
|
||||
rxs=[(r[3]-r[2])*1e6 for r in ring if len(r)>=4]
|
||||
def pct(a,q):
|
||||
if not a: return None
|
||||
a=sorted(a); return round(a[int(q*(len(a)-1))],1)
|
||||
return {
|
||||
"pdo_hz": round((c1-c0)/dt,1),
|
||||
"late_per_s": round((s1-s0)/dt,2),
|
||||
"achieved_vs_4k": round(100*(c1-c0)/dt/4000,1),
|
||||
"cyc_us": cu,
|
||||
"ring_n": len(ring),
|
||||
"k_gt0": sum(1 for k in ks if k>0),
|
||||
"k_sum": sum(ks),
|
||||
"work_pct": {"p50":pct(works,.5),"p90":pct(works,.9),"p99":pct(works,.99),"max":pct(works,1.0) if works else None},
|
||||
"send_pct": {"p50":pct(sends,.5),"p90":pct(sends,.9),"max":pct(sends,1.0) if sends else None},
|
||||
"rx_pct": {"p50":pct(rxs,.5),"p90":pct(rxs,.9),"max":pct(rxs,1.0) if rxs else None},
|
||||
}
|
||||
|
||||
# set ITR 200
|
||||
ps("Set-NetAdapterAdvancedProperty -Name '以太网 2' -RegistryKeyword '*InterruptModeration' -RegistryValue '1' -NoRestart")
|
||||
ps("Set-NetAdapterAdvancedProperty -Name '以太网 2' -RegistryKeyword 'ITR' -RegistryValue '200' -NoRestart")
|
||||
ps("Restart-NetAdapter -Name '以太网 2' -Confirm:$false")
|
||||
time.sleep(2)
|
||||
print("health", wait_ok())
|
||||
set_rt()
|
||||
|
||||
rows=[]
|
||||
for ms in (0.25, 0.5, 1.0):
|
||||
print(f"\n=== {ms} ms ITR=200 ===")
|
||||
period(ms)
|
||||
time.sleep(4)
|
||||
set_rt()
|
||||
r=quiet(10)
|
||||
print(json.dumps(r, ensure_ascii=False, indent=2))
|
||||
rows.append(r)
|
||||
|
||||
path=os.path.join(LAB, f"summary_itr200_periods_{int(time.time())}.json")
|
||||
json.dump(rows, open(path,"w",encoding="utf-8"), ensure_ascii=False, indent=2)
|
||||
print("WROTE", path)
|
||||
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
"""查 pdo 线程亲和 + 尝试 ITR 200/400 在 0.25ms 静默下的表现。"""
|
||||
import json, os, subprocess, time, urllib.request
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
LAB = os.path.join(ROOT, "lab", "skip_meter")
|
||||
NIC = "以太网 2"
|
||||
|
||||
def ps(cmd, timeout=60):
|
||||
return subprocess.check_output(["powershell","-NoProfile","-Command",cmd],
|
||||
text=True, encoding="utf-8", errors="replace",
|
||||
timeout=timeout).strip()
|
||||
|
||||
def post(p, b=None):
|
||||
d = json.dumps(b or {}).encode()
|
||||
r = urllib.request.Request("http://127.0.0.1:8090"+p, data=d, headers={"Content-Type":"application/json"})
|
||||
return json.loads(urllib.request.urlopen(r, timeout=8).read().decode())
|
||||
|
||||
def wait_ok(timeout=35):
|
||||
t0 = time.time()
|
||||
while time.time()-t0 < timeout:
|
||||
try:
|
||||
h = post("/api/native/health")
|
||||
if h.get("loop")==1 and int(h.get("wkc") or 0)>=3:
|
||||
return h
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.4)
|
||||
return None
|
||||
|
||||
def set_rt():
|
||||
p = int(ps("(Get-Process ecat_native).Id"))
|
||||
ps(f"$p=Get-Process -Id {p}; $p.PriorityClass='RealTime'; $p.PriorityClass")
|
||||
return p
|
||||
|
||||
def quiet_rate(sec=8):
|
||||
st = post("/api/motor/state")
|
||||
dbg = post("/api/motor/dbg")
|
||||
c0, s0 = st.get("cycles") or 0, dbg.get("skip_total") or 0
|
||||
t0 = time.time()
|
||||
time.sleep(sec)
|
||||
st = post("/api/motor/state")
|
||||
dbg = post("/api/motor/dbg")
|
||||
dt = time.time()-t0
|
||||
c1, s1 = st.get("cycles") or 0, dbg.get("skip_total") or 0
|
||||
return {
|
||||
"pdo_hz": round((c1-c0)/dt, 1),
|
||||
"late_per_s": round((s1-s0)/dt, 2),
|
||||
"cyc_us": st.get("cyc_us"),
|
||||
"period": st.get("period_ms"),
|
||||
}
|
||||
|
||||
def bus_restart_period(ms):
|
||||
try:
|
||||
post("/api/motor/stop")
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.15)
|
||||
post("/api/motor/period", {"period_ms": ms})
|
||||
time.sleep(0.15)
|
||||
post("/api/motor/start")
|
||||
|
||||
def nic_set(kw, val):
|
||||
out, err = "", ""
|
||||
r = subprocess.run(["powershell","-NoProfile","-Command",
|
||||
f"Set-NetAdapterAdvancedProperty -Name '{NIC}' -RegistryKeyword '{kw}' -RegistryValue '{val}' -NoRestart"],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
return r.stdout.strip(), r.stderr.strip()
|
||||
|
||||
# affinity of ecat threads via .NET is limited; dump processor affinity mask
|
||||
print("=== process affinity ===")
|
||||
print(ps(r'''
|
||||
$p = Get-Process ecat_native
|
||||
"PID $($p.Id) Priority=$($p.PriorityClass)"
|
||||
# ProcessorAffinity is a bitmask
|
||||
"Affinity=$($p.ProcessorAffinity)"
|
||||
'''))
|
||||
|
||||
print("=== I210 current ITR/moderation ===")
|
||||
print(ps(r'''
|
||||
Get-NetAdapterAdvancedProperty -Name '以太网 2' |
|
||||
Where-Object { $_.RegistryKeyword -in 'ITR','*InterruptModeration','*TransmitBuffers' } |
|
||||
Select-Object RegistryKeyword,DisplayValue | ConvertTo-Json -Compress
|
||||
'''))
|
||||
|
||||
results = []
|
||||
# try ITR values at 0.25ms quiet
|
||||
for itr in ("200", "400", "950", "65535"):
|
||||
print(f"\n===== ITR={itr} @0.25ms =====")
|
||||
o, e = nic_set("*InterruptModeration", "1")
|
||||
o2, e2 = nic_set("ITR", itr)
|
||||
print("set", o2 or o, e2 or e)
|
||||
# restart nic only if needed - ITR may need restart
|
||||
ps(f"Restart-NetAdapter -Name '{NIC}' -Confirm:$false")
|
||||
time.sleep(2)
|
||||
h = wait_ok(30)
|
||||
print("health", bool(h))
|
||||
if not h:
|
||||
try:
|
||||
post("/api/op/start"); post("/api/motor/start")
|
||||
h = wait_ok(20)
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
set_rt()
|
||||
bus_restart_period(0.25)
|
||||
time.sleep(3)
|
||||
set_rt()
|
||||
r = quiet_rate(8)
|
||||
print(json.dumps({k:r[k] for k in ("pdo_hz","late_per_s","period")}, ensure_ascii=False))
|
||||
print("work/send/rx", r["cyc_us"]["work"]["p50"], r["cyc_us"]["send"]["p50"], r["cyc_us"]["rx"]["p50"])
|
||||
results.append({"itr": itr, **{k:r[k] for k in ("pdo_hz","late_per_s")},
|
||||
"work_p50": r["cyc_us"]["work"]["p50"], "send_p50": r["cyc_us"]["send"]["p50"],
|
||||
"rx_p50": r["cyc_us"]["rx"]["p50"]})
|
||||
|
||||
print("\nSUMMARY", json.dumps(results, ensure_ascii=False, indent=2))
|
||||
# restore adaptive
|
||||
nic_set("ITR", "65535")
|
||||
ps(f"Restart-NetAdapter -Name '{NIC}' -Confirm:$false")
|
||||
time.sleep(2)
|
||||
wait_ok(25)
|
||||
set_rt()
|
||||
# leave at 1ms for safety
|
||||
bus_restart_period(1.0)
|
||||
time.sleep(1)
|
||||
print("restored 1ms", post("/api/native/health"))
|
||||
path = os.path.join(LAB, f"summary_itr_4k_{int(time.time())}.json")
|
||||
json.dump(results, open(path,"w",encoding="utf-8"), ensure_ascii=False, indent=2)
|
||||
print("WROTE", path)
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
import json, os, subprocess, sys, time
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
LAB = os.path.join(ROOT, "lab", "skip_meter")
|
||||
|
||||
def set_prio(level="RealTime"):
|
||||
p = int(subprocess.check_output(["powershell","-NoProfile","-Command","(Get-Process ecat_native).Id"], text=True).strip())
|
||||
subprocess.check_output(["powershell","-NoProfile","-Command",
|
||||
f"$p=Get-Process -Id {p}; $p.PriorityClass='{level}'; $p.PriorityClass"], text=True)
|
||||
return p
|
||||
|
||||
def meter(tag, secs=15):
|
||||
subprocess.run([sys.executable, os.path.join(ROOT,"scripts","skip_rate_meter.py"),
|
||||
"--tag", tag, "--secs", str(secs)],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
files = sorted(f for f in os.listdir(LAB) if f.startswith(tag+"_") and f.endswith(".json"))
|
||||
d = json.load(open(os.path.join(LAB, files[-1]), encoding="utf-8"))
|
||||
return {
|
||||
"tag": tag,
|
||||
"late_per_s": d["late_per_s"], "skip_per_s": d["skip_per_s"],
|
||||
"achieved_hz": d["achieved_hz"], "n_late": d["n_late"],
|
||||
"work_p50": d["window_cyc_us"]["work"]["p50"],
|
||||
"rx_p50": d["window_cyc_us"]["rx"]["p50"],
|
||||
"send_p50": d["window_cyc_us"]["send"]["p50"],
|
||||
"work_p90": d["window_cyc_us"]["work"]["p90"],
|
||||
}
|
||||
|
||||
def main():
|
||||
set_prio("RealTime")
|
||||
rows = []
|
||||
for i in range(1, 4):
|
||||
print(f"round {i} settle...")
|
||||
time.sleep(4)
|
||||
r = meter(f"multirt_{i}", 15)
|
||||
print(r)
|
||||
rows.append(r)
|
||||
lates = [x["late_per_s"] for x in rows]
|
||||
print("mean late", sum(lates)/len(lates), "min", min(lates), "max", max(lates))
|
||||
path = os.path.join(LAB, f"summary_multirt_{int(time.time())}.json")
|
||||
json.dump(rows, open(path,"w",encoding="utf-8"), ensure_ascii=False, indent=2)
|
||||
print("WROTE", path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,312 @@
|
||||
#!/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())
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""High vs RealTime 多轮 A/B,输出对比。结束后设为 --final 指定档。"""
|
||||
import json, os, subprocess, sys, time
|
||||
|
||||
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
LAB = os.path.join(ROOT, "lab", "skip_meter")
|
||||
|
||||
def ecat_pid():
|
||||
out = subprocess.check_output(
|
||||
["powershell","-NoProfile","-Command","(Get-Process ecat_native).Id"],
|
||||
text=True).strip()
|
||||
return int(out.splitlines()[0])
|
||||
|
||||
def set_prio(level):
|
||||
p = ecat_pid()
|
||||
subprocess.check_output(
|
||||
["powershell","-NoProfile","-Command",
|
||||
f"$p=Get-Process -Id {p}; $p.PriorityClass='{level}'; $p.PriorityClass"],
|
||||
text=True)
|
||||
return level
|
||||
|
||||
def meter(tag, secs=12):
|
||||
r = subprocess.run([sys.executable, os.path.join(ROOT,"scripts","skip_rate_meter.py"),
|
||||
"--tag", tag, "--secs", str(secs)],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
files = sorted([f for f in os.listdir(LAB) if f.startswith(tag+"_") and f.endswith(".json")])
|
||||
return json.load(open(os.path.join(LAB, files[-1]), encoding="utf-8"))
|
||||
|
||||
def row(level, data, n):
|
||||
return {
|
||||
"round": n, "level": level,
|
||||
"late_per_s": data["late_per_s"], "skip_per_s": data["skip_per_s"],
|
||||
"send_stall_frac": data["send_stall_frac"], "rx_stall_frac": data["rx_stall_frac"],
|
||||
"achieved_hz": data["achieved_hz"], "n_late": data["n_late"],
|
||||
"send_p90": (data.get("window_cyc_us") or {}).get("send",{}).get("p90"),
|
||||
"work_p90": (data.get("window_cyc_us") or {}).get("work",{}).get("p90"),
|
||||
}
|
||||
|
||||
def main():
|
||||
final = "RealTime"
|
||||
if len(sys.argv) > 1:
|
||||
final = sys.argv[1]
|
||||
rows = []
|
||||
# A-B-A-B
|
||||
seq = ["High","RealTime","High","RealTime"]
|
||||
for i, lv in enumerate(seq, 1):
|
||||
set_prio(lv)
|
||||
time.sleep(1.0)
|
||||
data = meter(f"ab_{lv}_{i}", 12)
|
||||
r = row(lv, data, i)
|
||||
rows.append(r)
|
||||
print(r)
|
||||
set_prio(final)
|
||||
print("FINAL", final, "pid", ecat_pid())
|
||||
# summary means
|
||||
by = {}
|
||||
for r in rows:
|
||||
by.setdefault(r["level"], []).append(r)
|
||||
means = {}
|
||||
for lv, rs in by.items():
|
||||
means[lv] = {
|
||||
"n": len(rs),
|
||||
"late_per_s_mean": round(sum(x["late_per_s"] for x in rs)/len(rs), 3),
|
||||
"skip_per_s_mean": round(sum(x["skip_per_s"] for x in rs)/len(rs), 3),
|
||||
"send_p90_mean": round(sum(x["send_p90"] or 0 for x in rs)/len(rs), 1),
|
||||
"work_p90_mean": round(sum(x["work_p90"] or 0 for x in rs)/len(rs), 1),
|
||||
}
|
||||
print("MEANS", json.dumps(means, ensure_ascii=False, indent=2))
|
||||
path = os.path.join(LAB, f"summary_ab_prio_{int(time.time())}.json")
|
||||
json.dump({"rows": rows, "means": means, "final": final}, open(path,"w",encoding="utf-8"),
|
||||
ensure_ascii=False, indent=2)
|
||||
print("WROTE", path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""进程优先级对照:不高/RealTime,各采 skip rate。不改网卡、不碰固件。"""
|
||||
import json, os, subprocess, sys, time, urllib.request
|
||||
|
||||
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
LAB = os.path.join(ROOT, "lab", "skip_meter")
|
||||
BASE = "http://127.0.0.1:8090"
|
||||
|
||||
def post(path, body=None):
|
||||
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=5) as r:
|
||||
return json.loads(r.read().decode())
|
||||
|
||||
def pid():
|
||||
return post("/api/native/health") # health has no pid; use process name
|
||||
# fallback below
|
||||
|
||||
def ecat_pid():
|
||||
out = subprocess.check_output(
|
||||
["powershell","-NoProfile","-Command",
|
||||
"(Get-Process ecat_native).Id"],
|
||||
text=True).strip()
|
||||
return int(out.splitlines()[0])
|
||||
|
||||
def set_prio(level):
|
||||
p = ecat_pid()
|
||||
cmd = f"$p=Get-Process -Id {p}; $p.PriorityClass='{level}'; $p.PriorityClass"
|
||||
return subprocess.check_output(["powershell","-NoProfile","-Command",cmd], text=True).strip()
|
||||
|
||||
def meter(tag, secs=16):
|
||||
r = subprocess.run([sys.executable, os.path.join(ROOT,"scripts","skip_rate_meter.py"),
|
||||
"--tag", tag, "--secs", str(secs)],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
files = sorted([f for f in os.listdir(LAB) if f.startswith(tag+"_") and f.endswith(".json")])
|
||||
data = json.load(open(os.path.join(LAB, files[-1]), encoding="utf-8")) if files else None
|
||||
return data, r.stdout
|
||||
|
||||
def main():
|
||||
p = ecat_pid()
|
||||
print("ecat pid", p)
|
||||
cur = subprocess.check_output(
|
||||
["powershell","-NoProfile","-Command", f"(Get-Process -Id {p}).PriorityClass"],
|
||||
text=True).strip()
|
||||
print("current PriorityClass", cur)
|
||||
|
||||
results = []
|
||||
for level in ("High", "RealTime"):
|
||||
print(f"\n=== set {level} ===")
|
||||
try:
|
||||
print(set_prio(level))
|
||||
except Exception as e:
|
||||
print("set fail", e)
|
||||
continue
|
||||
time.sleep(1.5)
|
||||
data, out = meter(f"prio_{level}", 16)
|
||||
if data:
|
||||
row = {"level": level, "late_per_s": data["late_per_s"], "skip_per_s": data["skip_per_s"],
|
||||
"late_frac_pct": data["late_frac_pct"], "send_stall_frac": data["send_stall_frac"],
|
||||
"rx_stall_frac": data["rx_stall_frac"], "achieved_hz": data["achieved_hz"],
|
||||
"n_late": data["n_late"]}
|
||||
results.append(row)
|
||||
print("ROW", row)
|
||||
print(out[-800:] if out else "")
|
||||
|
||||
# restore High (safe default; pdo thread already TIME_CRITICAL)
|
||||
try:
|
||||
print("restore High")
|
||||
print(set_prio("High"))
|
||||
except Exception as e:
|
||||
print("restore fail", e)
|
||||
|
||||
path = os.path.join(LAB, f"summary_prio_{int(time.time())}.json")
|
||||
json.dump({"results": results, "original": cur}, open(path,"w",encoding="utf-8"), ensure_ascii=False, indent=2)
|
||||
print("WROTE", path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""静默多档标定:1ms / 2ms / 4ms,每档先停环再设周期再开环,避免混拍。"""
|
||||
import json, os, subprocess, sys, time, urllib.request
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
LAB = os.path.join(ROOT, "lab", "skip_meter")
|
||||
BASE = "http://127.0.0.1:8090"
|
||||
|
||||
def post(p, b=None):
|
||||
d = json.dumps(b or {}).encode()
|
||||
r = urllib.request.Request(BASE+p, data=d, headers={"Content-Type":"application/json"})
|
||||
return json.loads(urllib.request.urlopen(r, timeout=8).read().decode())
|
||||
|
||||
def set_prio(level="RealTime"):
|
||||
p = int(subprocess.check_output(["powershell","-NoProfile","-Command","(Get-Process ecat_native).Id"], text=True).strip())
|
||||
subprocess.check_output(["powershell","-NoProfile","-Command",
|
||||
f"$p=Get-Process -Id {p}; $p.PriorityClass='{level}'; $p.PriorityClass"], text=True)
|
||||
return p
|
||||
|
||||
def set_period(ms):
|
||||
post("/api/motor/stop")
|
||||
time.sleep(0.2)
|
||||
r = post("/api/motor/period", {"period_ms": ms})
|
||||
time.sleep(0.2)
|
||||
post("/api/motor/start")
|
||||
time.sleep(0.8)
|
||||
return r
|
||||
|
||||
def meter(tag, secs=18):
|
||||
r = subprocess.run([sys.executable, os.path.join(ROOT,"scripts","skip_rate_meter.py"),
|
||||
"--tag", tag, "--secs", str(secs)],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
files = sorted([f for f in os.listdir(LAB) if f.startswith(tag+"_") and f.endswith(".json")])
|
||||
return json.load(open(os.path.join(LAB, files[-1]), encoding="utf-8"))
|
||||
|
||||
def main():
|
||||
print("set RealTime")
|
||||
print("pid", set_prio("RealTime"))
|
||||
rows = []
|
||||
for ms in (1.0, 2.0, 4.0):
|
||||
print(f"\n=== period {ms} ms ===")
|
||||
print(set_period(ms))
|
||||
time.sleep(2.0) # settle
|
||||
data = meter(f"quiet_{ms:g}ms", 18)
|
||||
row = {
|
||||
"period_ms": ms,
|
||||
"late_per_s": data["late_per_s"],
|
||||
"skip_per_s": data["skip_per_s"],
|
||||
"late_frac_pct": data["late_frac_pct"],
|
||||
"achieved_hz": data["achieved_hz"],
|
||||
"n_late": data["n_late"],
|
||||
"send_stall_frac": data["send_stall_frac"],
|
||||
"rx_stall_frac": data["rx_stall_frac"],
|
||||
"work_p50": data["window_cyc_us"]["work"]["p50"],
|
||||
"rx_p50": data["window_cyc_us"]["rx"]["p50"],
|
||||
"send_p50": data["window_cyc_us"]["send"]["p50"],
|
||||
"work_p90": data["window_cyc_us"]["work"]["p90"],
|
||||
}
|
||||
rows.append(row)
|
||||
print(row)
|
||||
|
||||
# restore 1ms as user default
|
||||
print("\nrestore period 1.0")
|
||||
print(set_period(1.0))
|
||||
path = os.path.join(LAB, f"summary_quiet_periods_{int(time.time())}.json")
|
||||
json.dump(rows, open(path,"w",encoding="utf-8"), ensure_ascii=False, indent=2)
|
||||
print("WROTE", path)
|
||||
print(json.dumps(rows, ensure_ascii=False, indent=2))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""跳拍率标定:固定时长采 /api/motor/dbg,输出可对比 JSON。
|
||||
|
||||
主判据: late_per_s, skip_per_s, send_stall_frac, rx_stall_frac
|
||||
用法:
|
||||
python scripts/skip_rate_meter.py --tag base --secs 20
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
BASE = os.environ.get("ECAT_BASE", "http://127.0.0.1:8090")
|
||||
OUTDIR = os.path.join(os.path.dirname(__file__), "..", "lab", "skip_meter")
|
||||
|
||||
|
||||
def post(path: str, body=None, timeout=5.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 main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--tag", default="sample")
|
||||
ap.add_argument("--secs", type=float, default=20.0)
|
||||
ap.add_argument("--hz", type=float, default=5.0)
|
||||
ap.add_argument("--out", default=None)
|
||||
args = ap.parse_args()
|
||||
|
||||
st0 = post("/api/motor/state")
|
||||
period = float(st0.get("period_ms") or 1.0)
|
||||
t0 = time.time()
|
||||
pulses = []
|
||||
last_t = None
|
||||
n_poll = 0
|
||||
# dbg t0 uses now_s() (QPC-based), NOT unix time. Track by last-seen t0 only.
|
||||
while time.time() - t0 < args.secs:
|
||||
dbg = post("/api/motor/dbg")
|
||||
n_poll += 1
|
||||
cyc = dbg.get("cyc") or []
|
||||
if last_t is not None:
|
||||
new = [r for r in cyc if r[0] > last_t]
|
||||
else:
|
||||
# first poll: adopt the whole ring's last timestamp as cursor;
|
||||
# count only k>0 from now on via subsequent polls. Also count
|
||||
# k>0 already in ring as "pre-existing" (not in window rate).
|
||||
new = []
|
||||
if cyc:
|
||||
last_t = cyc[-1][0]
|
||||
elif last_t is None:
|
||||
last_t = None
|
||||
for r in new:
|
||||
if len(r) < 8:
|
||||
continue
|
||||
k = int(r[5])
|
||||
if k <= 0:
|
||||
continue
|
||||
send_us = (r[2] - r[1]) * 1e6
|
||||
rx_us = (r[3] - r[2]) * 1e6
|
||||
pulses.append(
|
||||
{
|
||||
"t0": r[0],
|
||||
"k": k,
|
||||
"send_us": send_us,
|
||||
"rx_us": rx_us,
|
||||
"work_us": (r[4] - r[0]) * 1e6,
|
||||
"sd": float(r[6]) if len(r) > 6 else None,
|
||||
"rxc": int(r[7]) if len(r) > 7 else None,
|
||||
"kind": (
|
||||
"send"
|
||||
if send_us >= 500
|
||||
else ("rx" if rx_us >= 500 else "other")
|
||||
),
|
||||
}
|
||||
)
|
||||
time.sleep(max(0.0, 1.0 / args.hz))
|
||||
|
||||
st1 = post("/api/motor/state")
|
||||
wall = time.time() - t0
|
||||
n_late = len(pulses)
|
||||
n_skip = sum(p["k"] for p in pulses)
|
||||
by = {"send": 0, "rx": 0, "other": 0}
|
||||
for p in pulses:
|
||||
by[p["kind"]] += 1
|
||||
|
||||
# approximate expected beats in window
|
||||
# use cycles delta when available
|
||||
cyc0 = st0.get("cycles") or 0
|
||||
cyc1 = st1.get("cycles") or 0
|
||||
dcyc = max(0, cyc1 - cyc0)
|
||||
|
||||
cu = st1.get("cyc_us") or {}
|
||||
result = {
|
||||
"tag": args.tag,
|
||||
"ts": time.time(),
|
||||
"wall_s": round(wall, 3),
|
||||
"period_ms": period,
|
||||
"pcore_mask": st1.get("pcore_mask"),
|
||||
"wkc": st1.get("wkc"),
|
||||
"spin": st1.get("spin"),
|
||||
"cycles_delta": dcyc,
|
||||
"achieved_hz": round(dcyc / wall, 2) if wall > 0 else None,
|
||||
"late_per_s": round(n_late / wall, 3) if wall else None,
|
||||
"skip_per_s": round(n_skip / wall, 3) if wall else None,
|
||||
"late_frac_pct": round(100.0 * n_late / dcyc, 4) if dcyc else None,
|
||||
"skip_frac_pct": round(100.0 * n_skip / dcyc, 4) if dcyc else None,
|
||||
"send_stall_frac": round(by["send"] / n_late, 4) if n_late else None,
|
||||
"rx_stall_frac": round(by["rx"] / n_late, 4) if n_late else None,
|
||||
"n_late": n_late,
|
||||
"n_skip": n_skip,
|
||||
"by_kind": by,
|
||||
"window_cyc_us": cu,
|
||||
"n_poll": n_poll,
|
||||
"pulses_sample": pulses[:40],
|
||||
}
|
||||
|
||||
os.makedirs(OUTDIR, exist_ok=True)
|
||||
out = args.out or os.path.join(OUTDIR, f"{args.tag}_{int(time.time())}.json")
|
||||
with open(out, "w", encoding="utf-8") as f:
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
print(json.dumps({k: result[k] for k in result if k != "pulses_sample"},
|
||||
ensure_ascii=False, indent=2))
|
||||
print(f"WROTE {out}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""重启 ecat_native 进程(让 esp32_server/父进程重拉),测 rx 是否回到 ~90µs。"""
|
||||
import json, os, subprocess, sys, time, urllib.request
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
LAB = os.path.join(ROOT, "lab", "skip_meter")
|
||||
BASE = "http://127.0.0.1:8090"
|
||||
|
||||
def post(p, b=None, timeout=5):
|
||||
d = json.dumps(b or {}).encode()
|
||||
r = urllib.request.Request(BASE+p, data=d, headers={"Content-Type":"application/json"})
|
||||
return json.loads(urllib.request.urlopen(r, timeout=timeout).read().decode())
|
||||
|
||||
def ps(cmd):
|
||||
return subprocess.check_output(["powershell","-NoProfile","-Command",cmd],
|
||||
text=True, encoding="utf-8", errors="replace").strip()
|
||||
|
||||
def wait_health(timeout=40):
|
||||
t0 = time.time()
|
||||
while time.time()-t0 < timeout:
|
||||
try:
|
||||
h = post("/api/native/health")
|
||||
if h.get("loop")==1 and int(h.get("wkc") or 0)>=3:
|
||||
return True, h
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
return False, None
|
||||
|
||||
def meter(tag, secs=16):
|
||||
subprocess.run([sys.executable, os.path.join(ROOT,"scripts","skip_rate_meter.py"),
|
||||
"--tag", tag, "--secs", str(secs)],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
files = sorted(f for f in os.listdir(LAB) if f.startswith(tag+"_") and f.endswith(".json"))
|
||||
return json.load(open(os.path.join(LAB, files[-1]), encoding="utf-8"))
|
||||
|
||||
def main():
|
||||
# re-enable IP (keep VMware off — it's harmful and was only “enabled” by coincidence in morning)
|
||||
for cid in ("ms_tcpip", "ms_tcpip6"):
|
||||
try:
|
||||
ps(f"Enable-NetAdapterBinding -Name '以太网 2' -ComponentID '{cid}' -Confirm:$false")
|
||||
print("enabled", cid)
|
||||
except Exception as e:
|
||||
print("enable fail", cid, e)
|
||||
|
||||
info = ps("Get-CimInstance Win32_Process -Filter \"Name='ecat_native.exe'\" | Select-Object ProcessId,ParentProcessId,CommandLine | ConvertTo-Json -Compress")
|
||||
print("before", info[:500])
|
||||
|
||||
print("kill ecat_native...")
|
||||
ps("Stop-Process -Name ecat_native -Force -ErrorAction SilentlyContinue")
|
||||
time.sleep(2)
|
||||
|
||||
# parent may be esp32_server — wait for auto-restart
|
||||
ok, h = wait_health(45)
|
||||
print("auto restart?", ok, h)
|
||||
if not ok:
|
||||
print("manual start")
|
||||
exe = r"G:\Robot_v2\DCU\tools\ecat\build_native\ecat_native.exe"
|
||||
cwd = r"G:\Robot_v2\DCU\tools\ecat"
|
||||
subprocess.Popen(
|
||||
[exe, "--substr", "I210", "--mask", "0x50", "--web", "src/web"],
|
||||
cwd=cwd,
|
||||
stdout=open(os.path.join(cwd,"build_native","native_stdout.log"),"a"),
|
||||
stderr=subprocess.STDOUT,
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
|
||||
)
|
||||
time.sleep(3)
|
||||
ok, h = wait_health(30)
|
||||
print("manual health", ok, h)
|
||||
if ok:
|
||||
try:
|
||||
post("/api/op/start")
|
||||
post("/api/motor/start")
|
||||
except Exception as e:
|
||||
print("start fail", e)
|
||||
ok, h = wait_health(20)
|
||||
print("after start", ok, h)
|
||||
|
||||
# set RealTime
|
||||
try:
|
||||
p = int(ps("(Get-Process ecat_native).Id"))
|
||||
ps(f"$p=Get-Process -Id {p}; $p.PriorityClass='RealTime'; $p.PriorityClass")
|
||||
print("RealTime", p)
|
||||
except Exception as e:
|
||||
print("prio fail", e)
|
||||
|
||||
print("settle 10s")
|
||||
time.sleep(10)
|
||||
st = post("/api/motor/state")
|
||||
print("cyc", st.get("cyc_us"))
|
||||
print("period", st.get("period_ms"), "wkc", st.get("wkc"), "pcore", st.get("pcore_mask"))
|
||||
|
||||
data = meter("restart_rt", 16)
|
||||
row = {
|
||||
"late_per_s": data["late_per_s"], "skip_per_s": data["skip_per_s"],
|
||||
"achieved_hz": data["achieved_hz"], "n_late": data["n_late"],
|
||||
"work_p50": data["window_cyc_us"]["work"]["p50"],
|
||||
"rx_p50": data["window_cyc_us"]["rx"]["p50"],
|
||||
"send_p50": data["window_cyc_us"]["send"]["p50"],
|
||||
"work_p90": data["window_cyc_us"]["work"]["p90"],
|
||||
"send_stall_frac": data["send_stall_frac"],
|
||||
"rx_stall_frac": data["rx_stall_frac"],
|
||||
}
|
||||
print(json.dumps(row, ensure_ascii=False, indent=2))
|
||||
out = os.path.join(LAB, f"summary_restart_{int(time.time())}.json")
|
||||
json.dump(row, open(out,"w",encoding="utf-8"), ensure_ascii=False, indent=2)
|
||||
print("WROTE", out)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user