diff --git a/doc/etcp_multipath_plan.txt b/doc/etcp_multipath_plan.txt index df5459fe..f366ff77 100644 --- a/doc/etcp_multipath_plan.txt +++ b/doc/etcp_multipath_plan.txt @@ -3,6 +3,62 @@ ETCP: независимая обратная связь каналов и до Статус +24.10.2026, продолжение после push: отдельно исследован одиночный дуплекс2Мбит/с. +Все прежние15 локальных коммитов вошли в origin/master после rebase; патчи сохранены. +Из-за параллельных изменений основного каталога продолжение изолировано в ветке +etcp/solo-duplex-20261024, база a198cfb9. Протокол ETCP/BBR на этом этапе не изменён. + +Добавлен нативный сценарий duplex-slow: один20мс/2Мбит/с путь, два насыщенных отправителя. +Он использует прежние проверки полезных/свежих байтов, очереди, retries и полного drain. +UTUN_MULTIPATH_SAMPLE_MS=1000..60000 задаёт только длительность steady скоростных замеров; +по умолчанию прежние4с, warmup3с и отказные интервалы сохранены. Пороги не снижены. +Таблица скоростных профилей теперь содержит count/duplex вместе с настройками; +выбор режима/числа путей больше не зависит от порядкового номера строки. +Для единственного пути замер не повторяется как фиктивный агрегат. +Длинный замер дополняет короткие повторы: разовый PASS одного из них задачу не закрывает. + +Точные ACK/TX-события буферизованы отдельным диагностическим observer в /tmp, +без синхронного I/O внутри замера и без изменения BBR. Контрольный20-секундный steady: +полезная скорость1.726/1.658Мбит/с, retries=0; пик очереди23.7/23.0мс. +Сторона0 провела около4.49с в PROBE_DOWN, сторона1 —6.43с из20с. +В этой фазе pacing около1.39Мбит/с при доступной полосе2Мбит/с. + +Разложены7763 действующих DATA ACK без повторов: прямой путь DATA, +ожидание ACK у получателя, обратный путь ACK. Пример side=1, seq1084, t=5.830182с: +DATA15.909мс + ожидание ACK0.029мс + обратный путь32.219мс = raw RTT48.157мс. +В BBR переданы48.128мс после вычитания ожидания, minRTT25.451мс. +PROBE_DOWN длится142.4мс; DATA-flight после ACK7644байта, цель осушения5915байт. +При почти свободном прямом пути выход из DOWN продолжает ждать уменьшения +неподтверждённого flight, который также зависит от встречной очереди ACK. +По7763 точкам проверена сумма трёх частей и соответствие RTT после вычитания hold. +Это подтверждает вклад обратной очереди; не доказывает, что он единственный. + +Проверенные прототипы остаются только в /tmp, в рабочий BBR не переносились: +- округление цели DOWN до MSS, округление Reno-раундов, их комбинация: + есть FAIL повторных коротких прогонов; один успешный прогон не воспроизводится устойчиво; +- добавление2мс ACK-резерва: короткий прогон1.754/1.674 FAIL, длинный1.732/1.757 PASS; +- уменьшение очереди до min(20мс,minRTT/4): длинный1.257/1.036 FAIL; + задержка ниже, но утилизация ухудшилась, также пострадал дуплексный агрегат; +- временный предел окна DOWN существующей целью осушения: DOWN сократился до6..22мс, + длинный1.754/1.764 PASS, короткий1.752/1.656 FAIL; +- округление верхнего предела очереди до MSS, в том числе вместе с пределом DOWN: + короткие1.734/1.681 и1.721/1.664 FAIL; длинный1.752/1.752 PASS. +Следовательно, ни фиксированный ACK-резерв, ни округление, ни одно сокращение DOWN +не подтверждены как самостоятельное полное исправление. Повышать minRTT/порог задержки, +отключать PROBE_RTT или понижать порог скорости для получения PASS оснований нет. +Далее нужны сопоставленные причины остановки DATA в CRUISE/REFILL после короткого DOWN: +окно физического flight, общий admission budget и pacer; затем один причинный фикс +с отдельными модельными регрессиями, короткими повторами и длинным сетевым замером. + +Повтор текущего кода из корня: + UTUN_MULTIPATH_DIAG=/tmp/solo.csv ./tests/test_etcp_multipath duplex-slow + UTUN_MULTIPATH_SAMPLE_MS=20000 UTUN_MULTIPATH_DIAG=/tmp/solo-long.csv ./tests/test_etcp_multipath duplex-slow + python3 tools/etcp_diag_report.py /tmp/solo-long.csv /tmp/solo-long.html +Для длинного замера использовать обычные100мс срезы:10мс/60с может исчерпать буфер. +Артефакты контрольного RTT-разложения: /tmp/etcp-solo-feedback-{model,tx,diag}.csv +и /tmp/etcp-solo-feedback.log; observer/build: /tmp/etcp-solo-bbr-probe.c/.mk. +Прототипы: /tmp/etcp-solo-bbr-control.c, /tmp/etcp-solo-*-model.csv и соответствующие .log. + 24.10.2026: исправлены возраст RTT-фильтра, сроки PROBE_RTT и замораживание DATA-измерений при росте RTT. min_rtt_us теперь копируется вместе с probe_rtt_min_stamp_tb. Повторное использование сохранённого короткого минимума не ставит текущее время на старое измерение. diff --git a/tests/test_etcp_multipath.c b/tests/test_etcp_multipath.c index d4dafa57..4c59e718 100644 --- a/tests/test_etcp_multipath.c +++ b/tests/test_etcp_multipath.c @@ -64,6 +64,7 @@ static struct profile profiles[PATHS]; static struct trace traces[2][TRACE_COUNT]; static struct path_stats traffic[2][PATHS]; static unsigned path_count, failures, enabled_mask, bidirectional, low_load, measuring; +static unsigned sample_ms = SAMPLE_MS; // UTUN_MULTIPATH_SAMPLE_MS=1000..60000; только скоростные замеры static uint64_t started, next_message[2], measure_start, fault_start; static uint32_t fault_seq; static unsigned fault_trace_count; @@ -666,7 +667,7 @@ static uint64_t measure_rate(const struct profile* settings, unsigned count, uns setup(settings, count, mask); low_load = 0; run_for(WARMUP_MS, 1, "warmup"); begin_measure(); uint64_t before[2] = {ep[0].bytes, ep[1].bytes}; - run_for(SAMPLE_MS, 1, "steady"); + run_for(sample_ms, 1, "steady"); uint64_t elapsed = get_time_us() - measure_start, rate = 0; for (unsigned side = 0; side <= bidirectional; side++) { uint64_t useful = (ep[1-side].bytes-before[1-side]) * 8000 / elapsed, capacity = 0, unique_total = 0; @@ -862,6 +863,15 @@ int main(int argc, char** argv) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); test_clock_init(10); log_bbr_samples = getenv("UTUN_BBR_SAMPLES") != NULL; if (getenv("UTUN_TEST_DEBUG")) debug_set_category_level(DEBUG_CATEGORY_ETCP, DEBUG_LEVEL_DEBUG); + const char* duration = getenv("UTUN_MULTIPATH_SAMPLE_MS"); + if (duration) { + char* end = NULL; errno = 0; + unsigned long ms = strtoul(duration, &end, 10); + if (errno || end == duration || *end || ms < 1000 || ms > 60000) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "Invalid UTUN_MULTIPATH_SAMPLE_MS='%s': expected 1000..60000", duration); return 1; + } + sample_ms = ms; + } CHECK(socket_platform_init() == 0); const char* output = getenv("UTUN_MULTIPATH_CSV"); if (output) { csv = fopen(output, "w"); CHECK(csv); fprintf(csv, @@ -902,22 +912,27 @@ int main(int argc, char** argv) { DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Multipath diagnostics: period_us=%u file=%s; snapshots buffered until drain", diagnostic_period_us, detail); } - struct profile matrix[][3] = { - {{20,8000},{20,8000}}, {{20,2000},{20,8000}}, {{20,8000},{200,8000}}, - {{20,2000},{200,8000}}, {{20,8000},{200,2000}}, {{20,2000},{80,8000},{200,2000}}, {{20,2000},{80,8000}} + const struct { const char* name; unsigned count, duplex; struct profile paths[PATHS]; } matrix[] = { + {"equal",2,0,{{20,8000},{20,8000}}}, {"bandwidth",2,0,{{20,2000},{20,8000}}}, + {"rtt",2,0,{{20,8000},{200,8000}}}, {"fast-distant",2,0,{{20,2000},{200,8000}}}, + {"slow-distant",2,0,{{20,8000},{200,2000}}}, {"three",3,0,{{20,2000},{80,8000},{200,2000}}}, + {"bidirectional",2,1,{{20,2000},{80,8000}}}, {"duplex-slow",1,1,{{20,2000}}} }; - const char* names[] = {"equal", "bandwidth", "rtt", "fast-distant", "slow-distant", "three", "bidirectional"}; + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Multipath rate measurement: warmup_ms=%u sample_ms=%u; goodput threshold=85%%", + WARMUP_MS, sample_ms); size_t baseline = u_get_allocated_count(); unsigned selected = 0; - for (unsigned n = 0; n < sizeof(names)/sizeof(names[0]); n++) { - if (argc > 1 && strcmp(argv[1], names[n])) continue; + for (unsigned n = 0; n < sizeof(matrix)/sizeof(matrix[0]); n++) { + if (argc > 1 && strcmp(argv[1], matrix[n].name)) continue; selected++; - bidirectional = n == 6; - case_name = names[n]; unsigned count = n == 5 ? 3 : 2; + bidirectional = matrix[n].duplex; + case_name = matrix[n].name; unsigned count = matrix[n].count; uint64_t solo_sum = 0; - for (unsigned p = 0; p < count; p++) solo_sum += measure_rate(matrix[n], count, 1u << p); - uint64_t aggregate = measure_rate(matrix[n], count, (1u << count)-1); - expect(aggregate >= solo_sum * 90 / 100, "aggregation >=90% sum of solo rates", aggregate, solo_sum * 90 / 100); + for (unsigned p = 0; p < count; p++) solo_sum += measure_rate(matrix[n].paths, count, 1u << p); + if (count > 1) { + uint64_t aggregate = measure_rate(matrix[n].paths, count, (1u << count)-1); + expect(aggregate >= solo_sum * 90 / 100, "aggregation >=90% sum of solo rates", aggregate, solo_sum * 90 / 100); + } CHECK(u_get_allocated_count() == baseline); } struct { const char* name; unsigned count, broken, directions, sparse, ms; enum dummynet_mode mode; } faults[] = {