173 lines
6.5 KiB
Python
173 lines
6.5 KiB
Python
"""G0 probe — đo latency từng plugin + jitter round-trip ≥10k block.
|
|
|
|
Dùng ĐÚNG pipeline thật (FxRealtimeSession → SHM → fx_vst_bridge → SHM),
|
|
không sửa app code. WS browser → Python không đo được từ host; phần đó lấy
|
|
bằng chứng từ dbg.log (fx_play_first calib + fx_stats u/s) — probe này đo
|
|
Python pump (batching) + SHM + bridge + return.
|
|
|
|
Chạy: python native_bridge/debug/g0_latency_probe.py [--blocks 10000]
|
|
"""
|
|
import argparse
|
|
import atexit
|
|
import ctypes
|
|
import os
|
|
import statistics
|
|
import sys
|
|
import time
|
|
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
|
|
|
# Windows Sleep granularity mặc định 15.6ms → probe pacing ~22ms/batch ≠
|
|
# realtime 21.33ms → số liệu round-trip lệch. Bridge C++ đã gọi
|
|
# timeBeginPeriod(1) (RealtimeFxLoop.cpp:160); probe phải làm tương tự để
|
|
# pacing đúng realtime.
|
|
try:
|
|
_winmm = ctypes.windll.winmm
|
|
_winmm.timeBeginPeriod(1)
|
|
atexit.register(_winmm.timeEndPeriod, 1)
|
|
except Exception:
|
|
pass
|
|
|
|
import numpy as np # noqa: E402
|
|
|
|
from app.core.fx_realtime import FxRealtimeSession, FXRT_BLOCK # noqa: E402
|
|
|
|
SR = 48000
|
|
PERIOD = FXRT_BLOCK / SR # 5.333ms/block; batch 4 = 21.333ms
|
|
|
|
PURECOMP = r"C:\Program Files\Common Files\VST3\Sonible\purecomp.vst3"
|
|
OZONE = r"C:\Program Files\Common Files\VST3\iZotope\Ozone 11.vst3"
|
|
|
|
CONFIGS = [
|
|
("empty (passthrough)", []),
|
|
("pureComp", [{"type": "vst3", "path": PURECOMP, "bypass": False, "preset_b64": ""}]),
|
|
("Ozone 11", [{"type": "vst3", "path": OZONE, "bypass": False, "preset_b64": ""}]),
|
|
]
|
|
|
|
|
|
def make_block():
|
|
t = np.arange(FXRT_BLOCK, dtype=np.float32) / SR
|
|
s = (np.sin(2 * np.pi * 440 * t) * 0.1).astype(np.float32)
|
|
inter = np.empty(FXRT_BLOCK * 2, dtype=np.float32)
|
|
inter[0::2] = s
|
|
inter[1::2] = s
|
|
return inter.tobytes()
|
|
|
|
|
|
def run(name, chain, n_blocks):
|
|
log_path = os.path.join(os.environ.get("TEMP", "."), "g0_probe_%d.log" % os.getpid())
|
|
os.environ["FXRT_BRIDGE_LOG"] = log_path
|
|
sess = FxRealtimeSession(chain, SR)
|
|
stats = {}
|
|
try:
|
|
sess.wait_ready(30)
|
|
time.sleep(1.0) # chain ready + latency report
|
|
stats["plugin_latency"] = dict(sess.get_latencies())
|
|
blk = make_block()
|
|
send_t = [] # block idx -> send time (call time)
|
|
recv_t = [] # (block idx, recv time) — FIFO: out block j <=> in block j
|
|
next_idx = 0
|
|
batch_dur = PERIOD * 4
|
|
batches = n_blocks // 4
|
|
pump_t0 = time.perf_counter()
|
|
for b in range(batches):
|
|
t0 = time.perf_counter()
|
|
for _ in range(4):
|
|
send_t.append(time.perf_counter())
|
|
sess.write_input(blk)
|
|
next_idx += 1
|
|
sess.flush_input_stale(0.001)
|
|
outs = sess.read_output()
|
|
base = len(recv_t)
|
|
for k in range(len(outs)):
|
|
recv_t.append((base + k, time.perf_counter()))
|
|
dt = time.perf_counter() - t0
|
|
if dt < batch_dur:
|
|
time.sleep(batch_dur - dt)
|
|
# Drain phần còn lại (bridge xử lí nốt backlog) — nếu bridge theo kịp
|
|
# input, recv đủ = sent ngay sau pump; thiếu kéo dài = drift thật.
|
|
drain_t0 = time.perf_counter()
|
|
while len(recv_t) < len(send_t) and time.perf_counter() - drain_t0 < 2.0:
|
|
outs = sess.read_output()
|
|
base = len(recv_t)
|
|
for k in range(len(outs)):
|
|
recv_t.append((base + k, time.perf_counter()))
|
|
if not outs:
|
|
time.sleep(0.002)
|
|
pump_t1 = time.perf_counter()
|
|
stats["pump_rate"] = len(recv_t) / (pump_t1 - pump_t0)
|
|
stats["drained"] = len(recv_t) == len(send_t)
|
|
# round-trip per block
|
|
rt = []
|
|
for j, tr in recv_t:
|
|
if j < len(send_t):
|
|
rt.append((tr - send_t[j]) * 1000.0)
|
|
# inter-arrival deviation vs ideal period
|
|
recv_abs = [tr for _, tr in recv_t]
|
|
ia = []
|
|
for k in range(1, len(recv_abs)):
|
|
ia.append((recv_abs[k] - recv_abs[k - 1]) * 1000.0 - PERIOD * 1000.0)
|
|
stats["roundtrip_ms"] = {
|
|
"n": len(rt), "mean": statistics.mean(rt), "sd": statistics.pstdev(rt),
|
|
"min": min(rt), "max": max(rt),
|
|
"p50": sorted(rt)[len(rt) // 2],
|
|
"p99": sorted(rt)[int(len(rt) * 0.99) - 1],
|
|
}
|
|
stats["interarrival_dev_ms"] = {
|
|
"n": len(ia), "mean": statistics.mean(ia), "sd": statistics.pstdev(ia),
|
|
"min": min(ia), "max": max(ia),
|
|
}
|
|
stats["sent"] = len(send_t)
|
|
stats["recv"] = len(recv_t)
|
|
# bridge FxRTPerf
|
|
perf = []
|
|
if os.path.exists(log_path):
|
|
with open(log_path, "rb") as f:
|
|
data = f.read().decode("utf-8", "replace")
|
|
for line in data.splitlines():
|
|
if "[FxRTPerf]" in line:
|
|
perf.append(line)
|
|
if perf:
|
|
stats["bridge_perf_last"] = perf[-1].split("] ", 1)[-1]
|
|
finally:
|
|
sess.close()
|
|
try:
|
|
os.remove(log_path)
|
|
except OSError:
|
|
pass
|
|
return stats
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--blocks", type=int, default=10000)
|
|
args = ap.parse_args()
|
|
print("G0 probe — SR=%d block=%d n_blocks=%d (%.1fs/config)" % (SR, FXRT_BLOCK, args.blocks, args.blocks * PERIOD))
|
|
results = {}
|
|
for name, chain in CONFIGS:
|
|
print("\n=== %s ===" % name, flush=True)
|
|
st = run(name, chain, args.blocks)
|
|
results[name] = st
|
|
print("plugin_latency:", st.get("plugin_latency"))
|
|
print("pump_rate=%.3f blk/s drained=%s (bridge theo kip input: rate ~187.5 va drained=True)" %
|
|
(st.get("pump_rate", 0), st.get("drained")))
|
|
rt = st.get("roundtrip_ms", {})
|
|
print("roundtrip_ms n=%d mean=%.3f sd=%.3f min=%.3f p50=%.3f p99=%.3f max=%.3f" %
|
|
(rt.get("n", 0), rt.get("mean", 0), rt.get("sd", 0), rt.get("min", 0),
|
|
rt.get("p50", 0), rt.get("p99", 0), rt.get("max", 0)))
|
|
ia = st.get("interarrival_dev_ms", {})
|
|
print("interarrival_dev_ms n=%d mean=%.3f sd=%.3f min=%.3f max=%.3f" %
|
|
(ia.get("n", 0), ia.get("mean", 0), ia.get("sd", 0), ia.get("min", 0), ia.get("max", 0)))
|
|
print("bridge:", st.get("bridge_perf_last", "n/a"))
|
|
print("\n=== JSON ===")
|
|
print(json_dump(results))
|
|
|
|
|
|
def json_dump(results):
|
|
import json
|
|
return json.dumps(results, indent=1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|