diff --git a/src/Makefile.am b/src/Makefile.am index e2f14968..ecacd801 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -183,6 +183,8 @@ utun_CORE_SOURCES = \ dm/dm_mailbox_media.c \ dm/dm_mailbox_media.h \ call/call.c \ + call/call_video_rate.c \ + call/call_video_rate.h \ call/call_headless.c \ radio/radio.c \ radio/radio_headless.c \ @@ -334,6 +336,8 @@ libutun_a_SOURCES = \ dm/dm_mailbox_media.c \ dm/dm_mailbox_media.h \ call/call.c \ + call/call_video_rate.c \ + call/call_video_rate.h \ call/call_headless.c \ radio/radio.c \ radio/radio_headless.c \ diff --git a/tests/Makefile.am b/tests/Makefile.am index 5e96ee75..40906eee 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -10,6 +10,7 @@ EXTRA_DIST = fixtures/reality_openssl_hello.hex data/silero_speech.pcm data/sile # Быстрые проверки (до 100мс). check_PROGRAMS = \ test_route_tx_stats \ + test_call_video_rate \ test_memory_pool_and_config \ test_serialize \ test_reed_solomon \ @@ -676,6 +677,9 @@ test_dm_worker_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_call_SOURCES = test_call.c test_route_tx_stats_SOURCES = test_route_tx_stats.c test_route_tx_stats_LDADD = $(LIBUTUN) $(COMMON_LIBS) + +test_call_video_rate_SOURCES = test_call_video_rate.c +test_call_video_rate_LDADD = $(LIBUTUN) $(COMMON_LIBS) test_call_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/chat -I$(top_srcdir)/src/dm -I$(top_srcdir)/src/call -I$(top_srcdir)/src/routing_layer -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/lib test_call_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) diff --git a/tests/test_call_video_rate.c b/tests/test_call_video_rate.c new file mode 100644 index 00000000..96c0f295 --- /dev/null +++ b/tests/test_call_video_rate.c @@ -0,0 +1,102 @@ +/* Замкнутая модель источника/узкого канала/ACK с управляемым временем. */ +#include +#include +#include "../src/call/call_video_rate.h" +#include "../lib/debug_config.h" + +#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "rate test line=%d: %s", __LINE__, #x); exit(1); } } while (0) + +static struct call_video_rate_sample sample(uint64_t now) { + struct call_video_rate_sample s = {.now_us = now, .window_end_us = now / 50000 * 50000, + .applied_bps = 2000000, .minimum_bps = 100000, .maximum_bps = 4000000, + .baseline_rtt_us = 100000, .rtt_us = 100000, .frame_interval_us = 33333, + .debt_limit_bytes = 500000, .valid = 1, .ready = 1}; + return s; +} + +static void test_gates(void) { + struct call_video_rate rate; + struct call_video_rate_result out; + struct call_video_rate_sample s = sample(1000000); + call_video_rate_init(&rate, s.now_us); + CHECK(call_video_rate_step(&rate, &s, &out) == 0 && out.target_bps == s.applied_bps); + s.now_us = s.window_end_us = 2000000; + s.maximum_bps = 1000000; s.command_pending = 1; + CHECK(call_video_rate_step(&rate, &s, &out) == 0 && out.target_bps == s.applied_bps); + s.command_pending = 0; + CHECK(call_video_rate_step(&rate, &s, &out) == 0 && out.target_bps == 1000000 && out.reason == CALL_VIDEO_RATE_LIMIT); + s.maximum_bps = 4000000; s.debt_bytes = s.debt_limit_bytes; + s.no_route = 1; + CHECK(call_video_rate_step(&rate, &s, &out) == 0 && !out.admit && out.reason == CALL_VIDEO_RATE_BLOCKED); + s.no_route = 0; s.valid = 0; + CHECK(call_video_rate_step(&rate, &s, &out) == 0 && !out.admit && out.target_bps == s.applied_bps); + s.valid = 1; s.probing = 1; s.now_us = s.window_end_us = 3000000; + CHECK(call_video_rate_step(&rate, &s, &out) == 0 && out.target_bps == s.applied_bps); + s.probing = 0; s.locally_held = 1; + CHECK(call_video_rate_step(&rate, &s, &out) == 0 && out.target_bps == s.applied_bps); + struct call_video_rate saved = rate; + s.now_us--; + CHECK(call_video_rate_step(&rate, &s, &out) < 0 && !memcmp(&saved, &rate, sizeof(rate))); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS rate: cap, pending, no route, queue bound, warmup, probe and local hold"); +} + +/* Нормальный inflight на длинном RTT не должен выглядеть как перегрузка. */ +static void test_long_rtt(void) { + struct call_video_rate rate; + struct call_video_rate_result out; + struct call_video_rate_sample s = sample(0); + s.applied_bps = s.maximum_bps = 1600000; + s.rtt_us = s.baseline_rtt_us = 600000; + s.produced_bytes = s.delivered_bytes = 100000; + s.debt_bytes = 120000; + call_video_rate_init(&rate, 0); + for (unsigned i = 0; i < 200; i++) { + s.now_us = s.window_end_us = (uint64_t)i * 50000; + CHECK(call_video_rate_step(&rate, &s, &out) == 0); + CHECK(out.target_bps == s.applied_bps && out.admit && !out.excess_bytes); + } + DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS rate: 600ms RTT inflight is not congestion"); +} + +/* Объём каждого шага сохраняется до ACK; delivered задержан на два шага (100мс RTT). */ +static void test_channel(int static_scene) { + struct call_video_rate rate; + struct call_video_rate_sample s = sample(0); + struct call_video_rate_result out = {.admit = 1}; + uint64_t produced[10] = {0}, delivered[10] = {0}, flight[2] = {0}; + uint64_t queue = 0, debt = 0, max_debt = 0; + uint32_t before_increase = 0; + unsigned cuts = 0, probes = 0; + call_video_rate_init(&rate, 0); + for (unsigned i = 0; i < 2400; i++) { + uint64_t generated = out.admit ? s.applied_bps / (static_scene ? 16000 : 160) : 0; + queue += generated; debt += generated; + uint64_t acked = flight[i % 2]; debt -= acked; + uint64_t capacity = (i < 1200 ? 1500000 : 3000000) / 160; + uint64_t sent = queue < capacity ? queue : capacity; + queue -= sent; flight[i % 2] = sent; + produced[i % 10] = generated; delivered[i % 10] = acked; + s.produced_bytes = s.delivered_bytes = 0; + for (unsigned j = 0; j < 10; j++) { s.produced_bytes += produced[j]; s.delivered_bytes += delivered[j]; } + s.now_us = s.window_end_us = (uint64_t)(i + 1) * 50000; + s.valid = i >= 9; s.debt_bytes = debt; + CHECK(call_video_rate_step(&rate, &s, &out) == 0); + if (out.reason == CALL_VIDEO_RATE_DRAIN) cuts++; + if (out.reason == CALL_VIDEO_RATE_PROBE) probes++; + s.applied_bps = out.target_bps; + CHECK(s.applied_bps >= s.minimum_bps && s.applied_bps <= s.maximum_bps); + if (debt > max_debt) max_debt = debt; + if (i == 1199) before_increase = s.applied_bps; + } + DEBUG_INFO(DEBUG_CATEGORY_CALL, "rate model static=%d max_debt=%llu cuts=%u probes=%u before=%u final=%u", + static_scene, (unsigned long long)max_debt, cuts, probes, before_increase, s.applied_bps); + CHECK(max_debt < 250000 && probes > 10); + if (static_scene) CHECK(cuts == 0 && s.applied_bps == s.maximum_bps); + else CHECK(cuts > 0 && before_increase < 1800000 && s.applied_bps > 2000000 && s.applied_bps < 3400000); +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); + test_gates(); test_long_rtt(); test_channel(0); test_channel(1); + return 0; +}