#!/usr/bin/env python3 """Проверить CSV UTUN_MULTIPATH_DIAG и построить автономный HTML с графиками и исходными срезами.""" import argparse import csv import json from pathlib import Path def read_samples(path): """Проверить схему, учёт очередей и интервалы; неправильный CSV не превращать в правдоподобный график.""" with path.open(newline="", encoding="utf-8") as source: reader = csv.DictReader(source) if not reader.fieldnames or len(set(reader.fieldnames)) != len(reader.fieldnames): raise ValueError("отсутствуют или повторяются заголовки CSV") rows = [] previous = {} for line, raw in enumerate(reader, 2): if None in raw or any(value is None for value in raw.values()): raise ValueError(f"строка {line}: число значений не соответствует заголовку") row = {key: value if key in ("case", "mask", "phase") else int(value) for key, value in raw.items()} key = (row["case"], row["mask"], row["side"], row["path"]) valid = row["dt_us"] > 0 and row["unacked_bytes"] == row["send_bytes"] + row["wait_ack_bytes"] valid &= row["send_count"] == row["send_fresh"] + row["send_retries"] valid &= row["flight_min"] <= row["flight_bytes"] <= row["flight_max"] valid &= row["cwnd_min"] <= row["cwnd_bytes"] <= row["cwnd_max"] valid &= row["early_ordinary_ack_entries"] <= row["ack_only_entries"] <= row["ack_entries"] valid &= row["early_ordinary_ack_packets"] <= row["ordinary_ack_only_packets"] <= row["ack_only_packets"] valid &= row["retries"] <= row["data_packets"] and row["rtt_samples"] <= row["bbr_samples"] if key in previous: valid &= row["time_us"] - previous[key] == row["dt_us"] if not valid: raise ValueError(f"строка {line}: нарушен учёт очередей, счётчиков или времени") previous[key] = row["time_us"] rows.append(row) if not rows: raise ValueError("CSV не содержит срезов") return rows PAGE = r''' ETCP: очереди и pacing

ETCP: динамика очередей, DATA и ACK

Источник: . Данные проверены. Наведите курсор на график для просмотра всех полей среза.

Очереди приложения/normalizer/send/wait_ack общие для соединения. Flight, pacer, ACK и канал — для выбранного пути. Блокировки — число проверок выбора, а не время ожидания. Оранжевый фон: BBR PROBE_RTT. Пики очереди и задержки эмулятора накоплены с последнего сброса его статистики.

Исходный срез

''' def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("csv", type=Path) parser.add_argument("html", type=Path) args = parser.parse_args() try: rows = read_samples(args.csv) page = PAGE.replace("__ROWS__", json.dumps(rows, ensure_ascii=False).replace("<", "\\u003c")) page = page.replace("__SOURCE__", json.dumps(args.csv.name, ensure_ascii=False).replace("<", "\\u003c")) args.html.write_text(page, encoding="utf-8") except (OSError, ValueError, KeyError) as error: parser.exit(1, f"Ошибка отчёта ETCP: {error}\n") print(f"Проверено {len(rows)} срезов; отчёт: {args.html}") if __name__ == "__main__": main()