Compare commits
3 Commits
3dbb7afa83
...
22c2eeab8c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22c2eeab8c | ||
|
|
b63e36abdb | ||
|
|
21751f41dd |
@@ -163,3 +163,79 @@ git version 2.51.1.windows.1
|
||||
- Режим 8+3 не принят из-за отсутствия устойчивого преимущества.
|
||||
- Подтверждено, что продолжительные помехи нельзя компенсировать только увеличением числа проверочных пакетов.
|
||||
- Следующий этап посвящён совместной передаче команд, телеметрии и видео с приоритетным обслуживанием.
|
||||
|
||||
---
|
||||
|
||||
# Запись 008
|
||||
|
||||
## Дата
|
||||
|
||||
3 августа 2026 года
|
||||
|
||||
## Тема
|
||||
|
||||
Завершение Lab033: общий пакет канала и приоритетное обслуживание.
|
||||
|
||||
## Выполнено
|
||||
|
||||
- Добавлен общий 32-байтный заголовок канала для команд, телеметрии и видео.
|
||||
- Общая предложенная нагрузка составляет 258,214 кбит/с.
|
||||
- Подтверждено, что FIFO не обеспечивает требуемую задержку команд.
|
||||
- Выбран строгий приоритет с заменой устаревших состояний.
|
||||
- Аварийная команда имеет наивысший приоритет.
|
||||
- Скорость 260 кбит/с практически полностью загружена.
|
||||
- При 230 кбит/с видеоданные накапливаются и устаревают.
|
||||
- Итоговое восстановление всех кадров после освобождения очереди не означает работу видео в реальном времени.
|
||||
- Расчёты и графики выполнены с Matplotlib 3.11.1.
|
||||
- Следующий этап посвящён отбрасыванию устаревших видеокадров.
|
||||
|
||||
---
|
||||
|
||||
# Запись 009
|
||||
|
||||
## Дата
|
||||
|
||||
3 августа 2026 года
|
||||
|
||||
## Тема
|
||||
|
||||
Завершение Lab034: реактивное отбрасывание устаревших видеокадров.
|
||||
|
||||
## Выполнено
|
||||
|
||||
- Сравнены непрерывные FEC-блоки и блоки, выровненные по составным кадрам.
|
||||
- Выравнивание увеличивает видеопоток на 5,975 кбит/с, или на 2,633%.
|
||||
- При 300 кбит/с удаление кадров не требуется.
|
||||
- При 260 кбит/с ограничения 1000 и 1500 мс не срабатывают, а ограничение 500 мс уже чрезмерно.
|
||||
- При 230 кбит/с реактивное удаление устаревших кадров уменьшает очередь, но резко снижает частоту обновления.
|
||||
- Ограничение 500 мс оставляет только 3 из 63 кадров.
|
||||
- Ограничение 1500 мс оставляет 27 из 63 кадров.
|
||||
- Подтверждено, что короткая очередь сама по себе не означает свежее изображение.
|
||||
- Текущая реактивная политика не принята.
|
||||
- Следующий этап посвящён упреждающему допуску кадров и обслуживанию видео целыми кадрами.
|
||||
|
||||
---
|
||||
|
||||
# Запись 010
|
||||
|
||||
## Дата
|
||||
|
||||
3 августа 2026 года
|
||||
|
||||
## Тема
|
||||
|
||||
Завершение Lab035: упреждающий допуск и обслуживание видео целыми составными кадрами.
|
||||
|
||||
## Выполнено
|
||||
|
||||
- Lab035 исследует упреждающий допуск и обслуживание видео целыми составными кадрами.
|
||||
- Начатый кадр всегда передаётся полностью.
|
||||
- Команды и телеметрия могут передаваться между его пакетами.
|
||||
- Реактивное удаление Lab034 окончательно не принято.
|
||||
- Основная политика — хранение только самого свежего не начатого кадра.
|
||||
- При 230 кбит/с опубликовано 56 из 63 кадров с частотой около 2,6 кадра/с.
|
||||
- Бесполезно переданных видеобайтов нет.
|
||||
- Очередь из двух кадров увеличивает возраст изображения.
|
||||
- Прогноз 1000 мс не дал преимущества над более простой политикой.
|
||||
- Прогноз 500 мс чрезмерно снижает частоту обновления.
|
||||
- Следующая лабораторная проверяет долговременную устойчивость и изменение пропускной способности канала.
|
||||
|
||||
37
data/processed/lab033/lab033_class_metrics.csv
Normal file
@@ -0,0 +1,37 @@
|
||||
channel_kbps,scheduler,traffic_class,offered_load_kbps,created_packets,transmitted_packets,replaced_packets,remaining_at_source_end_packets,mean_start_delay_ms,p95_start_delay_ms,max_start_delay_ms,mean_total_delay_ms,p95_total_delay_ms,max_total_delay_ms,deadline_misses,deadline_miss_fraction,mean_blocking_delay_ms,max_blocking_delay_ms
|
||||
300.0,fifo,emergency,0.02465489566613162,1,1,0,0,0.0,0.0,0.0,1.7066666666671892,1.7066666666671892,1.7066666666671892,0,0.0,0.0,0.0
|
||||
300.0,fifo,control,10.256436597110755,416,416,0,2,104.3649927884586,236.92691666666235,294.47966666665957,106.0716594551255,238.6335833333294,296.1863333333259,203,0.4879807692307692,5.879864583331442,16.212999999996036
|
||||
300.0,fifo,telemetry,7.692327447833066,208,208,0,1,98.47181249999751,231.12033333332866,294.26699999999425,101.03181249999754,233.68033333332914,296.8269999999933,0,0.0,5.797325320511048,16.212999999996036
|
||||
300.0,fifo,video,240.24038523274479,1078,1078,0,15,128.3044211502762,254.33199999999601,309.6799999999931,143.7311125541102,264.7599999999962,325.89333333332604,0,0.0,0.0026771799628383768,0.16033333332998723
|
||||
300.0,strict_priority,emergency,0.02465489566613162,1,1,0,0,0.0,0.0,0.0,1.7066666666671892,1.7066666666671892,1.7066666666671892,0,0.0,0.0,0.0
|
||||
300.0,strict_priority,control,10.256436597110755,416,416,0,0,7.733262019229404,15.759666666666755,16.106333333334,9.439928685896334,17.46633333333346,17.813000000000745,0,0.0,7.7291594551268386,16.106333333334
|
||||
300.0,strict_priority,telemetry,7.692327447833066,208,208,0,0,9.864120192306657,17.546883333334474,17.813000000000745,12.424120192306697,20.10688333333437,20.37300000000064,0,0.0,8.149248397434599,16.106333333334
|
||||
300.0,strict_priority,video,240.24038523274479,1078,1078,0,15,135.37173098330103,269.12799999999464,323.36000000000143,150.79842238713505,279.70533333332844,333.4933333333332,0,0.0,0.0026771799628976987,0.16033333333353994
|
||||
300.0,latest_state,emergency,0.02465489566613162,1,1,0,0,0.0,0.0,0.0,1.7066666666671892,1.7066666666671892,1.7066666666671892,0,0.0,0.0,0.0
|
||||
300.0,latest_state,control,10.256436597110755,416,416,0,0,7.733262019229404,15.759666666666755,16.106333333334,9.439928685896334,17.46633333333346,17.813000000000745,0,0.0,7.7291594551268386,16.106333333334
|
||||
300.0,latest_state,telemetry,7.692327447833066,208,208,0,0,9.864120192306657,17.546883333334474,17.813000000000745,12.424120192306697,20.10688333333437,20.37300000000064,0,0.0,8.149248397434599,16.106333333334
|
||||
300.0,latest_state,video,240.24038523274479,1078,1078,0,15,135.37173098330103,269.12799999999464,323.36000000000143,150.79842238713505,279.70533333332844,333.4933333333332,0,0.0,0.0026771799628976987,0.16033333333353994
|
||||
260.0,fifo,emergency,0.02465489566613162,1,1,0,0,0.0,0.0,0.0,1.969230769230279,1.969230769230279,1.969230769230279,0,0.0,0.0,0.0
|
||||
260.0,fifo,control,10.256436597110755,416,416,0,7,167.10386353552764,331.0900769231244,458.97469230786925,169.07309430475814,333.0593076923547,460.9439230770995,298,0.7163461538461539,7.857339866886274,18.615384615390695
|
||||
260.0,fifo,telemetry,7.692327447833066,208,208,0,4,160.36350517753885,328.8285384616073,460.9439230770995,163.31735133138503,331.7823846154539,463.8977692309467,0,0.0,7.732144230791379,18.615384615390695
|
||||
260.0,fifo,video,240.24038523274479,1078,1078,0,21,175.94024482662746,343.1754384616482,473.6000000001752,193.74027336951343,357.6154384615683,492.3076923078682,0,0.0,3.3139568289080654,18.257076923131166
|
||||
260.0,strict_priority,emergency,0.02465489566613162,1,1,0,0,0.0,0.0,0.0,1.969230769230279,1.969230769230279,1.969230769230279,0,0.0,0.0,0.0
|
||||
260.0,strict_priority,control,10.256436597110755,416,416,0,0,8.313035133156095,16.784365384612144,18.69776923077815,10.282265902386632,18.753596153843198,20.66700000000843,0,0.0,8.308301405345444,18.69776923077815
|
||||
260.0,strict_priority,telemetry,7.692327447833066,208,208,0,0,10.909659023688215,19.493946153856623,20.66700000000843,13.863505177534366,22.44779230770204,23.620846153853847,0,0.0,8.930960798836377,18.69776923077815
|
||||
260.0,strict_priority,video,240.24038523274479,1078,1078,0,23,187.65972534610552,365.73226153856297,480.49230769247583,205.4597538889915,380.5630769230985,499.20000000016887,0,0.0,3.9965313971947523,18.25707692318801
|
||||
260.0,latest_state,emergency,0.02465489566613162,1,1,0,0,0.0,0.0,0.0,1.969230769230279,1.969230769230279,1.969230769230279,0,0.0,0.0,0.0
|
||||
260.0,latest_state,control,10.256436597110755,416,416,0,0,8.313035133156095,16.784365384612144,18.69776923077815,10.282265902386632,18.753596153843198,20.66700000000843,0,0.0,8.308301405345444,18.69776923077815
|
||||
260.0,latest_state,telemetry,7.692327447833066,208,208,0,0,10.909659023688215,19.493946153856623,20.66700000000843,13.863505177534366,22.44779230770204,23.620846153853847,0,0.0,8.930960798836377,18.69776923077815
|
||||
260.0,latest_state,video,240.24038523274479,1078,1078,0,23,187.65972534610552,365.73226153856297,480.49230769247583,205.4597538889915,380.5630769230985,499.20000000016887,0,0.0,3.9965313971947523,18.25707692318801
|
||||
230.0,fifo,emergency,0.02465489566613162,1,1,0,0,878.8869565216775,878.8869565216775,878.8869565216775,881.1130434781998,881.1130434781998,881.1130434781998,1,1.0,0.4173913042819777,0.4173913042819777
|
||||
230.0,fifo,control,10.256436597110755,416,416,0,42,1172.0617892976043,2238.3086956520533,2606.2956521737738,1174.2878762541263,2240.5347826085754,2608.5217391302963,414,0.9951923076923077,9.870735785906158,21.13043478255605
|
||||
230.0,fifo,telemetry,7.692327447833066,208,208,0,21,1161.6391304347285,2222.267826086846,2608.5217391302963,1164.978260869511,2225.6069565216276,2611.860869565078,177,0.8509615384615384,9.380434782561796,21.043478260802218
|
||||
230.0,fifo,video,240.24038523274479,1078,1078,0,118,1186.612664354224,2267.033969565095,2618.4808260868167,1206.7344357505297,2288.1817956520513,2639.628652173773,0,0.0,8.20556586266141,21.136565217338088
|
||||
230.0,strict_priority,emergency,0.02465489566613162,1,1,0,0,9.32173913036749,9.32173913036749,9.32173913036749,11.547826086889756,11.547826086889756,11.547826086889756,0,0.0,9.32173913036749,9.32173913036749
|
||||
230.0,strict_priority,control,10.256436597110755,416,416,0,1,10.02424749162144,19.62173913038057,21.113043478251825,12.250334448143445,21.84782608690261,23.33913043477409,0,0.0,10.018896321052877,21.113043478251825
|
||||
230.0,strict_priority,telemetry,7.692327447833066,208,208,0,0,12.175083612023139,21.772173913030322,23.33913043477409,15.51421404680579,25.111304347813142,26.678260869555714,0,0.0,9.938294314364006,21.113043478251825
|
||||
230.0,strict_priority,video,240.24038523274479,1078,1078,0,126,1273.2989271597767,2364.9321739130005,2626.272130434721,1293.4206985560818,2379.542608695629,2647.4199565216772,0,0.0,9.628813422585635,19.802565217331036
|
||||
230.0,latest_state,emergency,0.02465489566613162,1,1,0,0,9.32173913036749,9.32173913036749,9.32173913036749,11.547826086889756,11.547826086889756,11.547826086889756,0,0.0,9.32173913036749,9.32173913036749
|
||||
230.0,latest_state,control,10.256436597110755,416,416,0,1,10.02424749162144,19.62173913038057,21.113043478251825,12.250334448143445,21.84782608690261,23.33913043477409,0,0.0,10.018896321052877,21.113043478251825
|
||||
230.0,latest_state,telemetry,7.692327447833066,208,208,0,0,12.175083612023139,21.772173913030322,23.33913043477409,15.51421404680579,25.111304347813142,26.678260869555714,0,0.0,9.938294314364006,21.113043478251825
|
||||
230.0,latest_state,video,240.24038523274479,1078,1078,0,126,1273.2989271597767,2364.9321739130005,2626.272130434721,1293.4206985560818,2379.542608695629,2647.4199565216772,0,0.0,9.628813422585635,19.802565217331036
|
||||
|
BIN
data/processed/lab033/lab033_control_delay.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
data/processed/lab033/lab033_deadline_misses.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
data/processed/lab033/lab033_emergency_delay.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
data/processed/lab033/lab033_queue_length.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
165
data/processed/lab033/lab033_report.txt
Normal file
@@ -0,0 +1,165 @@
|
||||
Lab033. Общий пакет канала и приоритетное обслуживание
|
||||
|
||||
1. Git и конфигурация
|
||||
- Git-корень: C:/Users/user/Desktop/projects/SDR_Rover.
|
||||
- Ветка: main; HEAD: 3dbb7afa8332b49535d867f1eabca7bdd2b7cdde.
|
||||
- Исходное видео: 20.766667 с, 63 составных кадров, 3 кадра/с.
|
||||
- BASE 240x135 grayscale JPEG Q23; ROI 320x180 grayscale JPEG Q33.
|
||||
- Lab028: payload 512 байт; Lab030: FEC 12+3; Lab031: D=1.
|
||||
- Фактический внешний видеопоток до Lab033: 226.951 кбит/с; с общим заголовком: 240.240 кбит/с.
|
||||
- Аварийная команда: 0.024655; обычные команды: 10.256; телеметрия: 7.692 кбит/с.
|
||||
- Общая предложенная нагрузка: 258.214 кбит/с; суммарная служебная доля: 34.923%.
|
||||
|
||||
2. Формат общего заголовка
|
||||
- struct: !4sBBBBHIQHII; размер: 32 байта; network byte order, неявного padding нет.
|
||||
- Смещения: magic[4]@0, version:u8@4, traffic_class:u8@5, direction:u8@6, flags:u8@7, stream_id:u16@8, sequence_number:u32@10, generation_time_us:u64@14, deadline_ms:u16@22, payload_length:u32@24, packet_crc32:u32@28.
|
||||
- traffic_class: 1=EMERGENCY, 2=CONTROL, 3=TELEMETRY, 4=VIDEO; direction: 1=GROUND_TO_ROVER, 2=ROVER_TO_GROUND; flags в версии 1 равен нулю.
|
||||
- CRC32 защищает заголовок с нулевым packet_crc32 и всю полезную нагрузку.
|
||||
|
||||
3. Девять сочетаний скорости и планировщика
|
||||
speed | scheduler | offered/capacity | control P95 ms | emergency ms | telemetry P95 ms | video P95 ms | queue@end packets | drain extra s | final video
|
||||
300 | FIFO | 0.8607 | 238.634 | 1.707 | 233.680 | 304.579 | 18 | 0.231867 | 1.000000
|
||||
300 | Строгий приоритет | 0.8607 | 17.466 | 1.707 | 20.107 | 322.072 | 15 | 0.231867 | 1.000000
|
||||
300 | Приоритет + замена | 0.8607 | 17.466 | 1.707 | 20.107 | 322.072 | 15 | 0.231867 | 1.000000
|
||||
260 | FIFO | 0.9931 | 333.059 | 1.969 | 331.782 | 420.025 | 32 | 0.399200 | 1.000000
|
||||
260 | Строгий приоритет | 0.9931 | 18.754 | 1.969 | 22.448 | 445.846 | 23 | 0.399200 | 1.000000
|
||||
260 | Приоритет + замена | 0.9931 | 18.754 | 1.969 | 22.448 | 445.846 | 23 | 0.399200 | 1.000000
|
||||
230 | FIFO | 1.1227 | 2240.535 | 881.113 | 2225.607 | 2417.731 | 181 | 2.547420 | 1.000000
|
||||
230 | Строгий приоритет | 1.1227 | 21.848 | 11.548 | 25.111 | 2505.995 | 127 | 2.547420 | 1.000000
|
||||
230 | Приоритет + замена | 1.1227 | 21.848 | 11.548 | 25.111 | 2505.995 | 127 | 2.547420 | 1.000000
|
||||
|
||||
4. Подробные показатели
|
||||
[300 кбит/с; FIFO]
|
||||
- Передано 1703 пакетов, 670280 байт; средняя/максимальная очередь 10.478/22 пакетов и 4452.4/12221 байт.
|
||||
- На конце видео: 18 пакетов, 8695.0 байт; полное освобождение 20.998534 с, дополнительно 0.231867 с.
|
||||
- Команды: mean/P95/max 106.072/238.634/296.186 мс; превышений 100 мс 203; промежутков >100 мс 63; max gap 344.480 мс; заменено 0.
|
||||
- Аварийная команда: start/total 0.000/1.707 мс; deadline 50 мс соблюдён; блокировал none, 0 байт, seq=-1, ожидание 0.000 мс.
|
||||
- Телеметрия: mean/P95/max 101.032/233.680/296.827 мс; превышений 500 мс 0 (0.000000); заменено 0.
|
||||
- Видео: опубликовано к концу источника 0.984127, после drain 1.000000, итог 1.000000; delay mean/P95/max 261.559/304.579/311.146 мс.
|
||||
- Возраст изображения mean/P95/max 423.846/580.000/643.333 мс; доля >1 с 0.000000; поздних кадров 0, max серия 0.
|
||||
[300 кбит/с; Строгий приоритет]
|
||||
- Передано 1703 пакетов, 670280 байт; средняя/максимальная очередь 8.050/22 пакетов и 4452.4/12221 байт.
|
||||
- На конце видео: 15 пакетов, 8695.0 байт; полное освобождение 20.998534 с, дополнительно 0.231867 с.
|
||||
- Команды: mean/P95/max 9.440/17.466/17.813 мс; превышений 100 мс 0; промежутков >100 мс 0; max gap 65.760 мс; заменено 0.
|
||||
- Аварийная команда: start/total 0.000/1.707 мс; deadline 50 мс соблюдён; блокировал none, 0 байт, seq=-1, ожидание 0.000 мс.
|
||||
- Телеметрия: mean/P95/max 12.424/20.107/20.373 мс; превышений 500 мс 0 (0.000000); заменено 0.
|
||||
- Видео: опубликовано к концу источника 0.984127, после drain 1.000000, итог 1.000000; delay mean/P95/max 276.337/322.072/330.773 мс.
|
||||
- Возраст изображения mean/P95/max 437.649/593.333/660.000 мс; доля >1 с 0.000000; поздних кадров 0, max серия 0.
|
||||
[300 кбит/с; Приоритет + замена]
|
||||
- Передано 1703 пакетов, 670280 байт; средняя/максимальная очередь 8.050/22 пакетов и 4452.4/12221 байт.
|
||||
- На конце видео: 15 пакетов, 8695.0 байт; полное освобождение 20.998534 с, дополнительно 0.231867 с.
|
||||
- Команды: mean/P95/max 9.440/17.466/17.813 мс; превышений 100 мс 0; промежутков >100 мс 0; max gap 65.760 мс; заменено 0.
|
||||
- Аварийная команда: start/total 0.000/1.707 мс; deadline 50 мс соблюдён; блокировал none, 0 байт, seq=-1, ожидание 0.000 мс.
|
||||
- Телеметрия: mean/P95/max 12.424/20.107/20.373 мс; превышений 500 мс 0 (0.000000); заменено 0.
|
||||
- Видео: опубликовано к концу источника 0.984127, после drain 1.000000, итог 1.000000; delay mean/P95/max 276.337/322.072/330.773 мс.
|
||||
- Возраст изображения mean/P95/max 437.649/593.333/660.000 мс; доля >1 с 0.000000; поздних кадров 0, max серия 0.
|
||||
[260 кбит/с; FIFO]
|
||||
- Передано 1703 пакетов, 670280 байт; средняя/максимальная очередь 14.800/37 пакетов и 6010.5/16512 байт.
|
||||
- На конце видео: 32 пакетов, 12974.0 байт; полное освобождение 21.165867 с, дополнительно 0.399200 с.
|
||||
- Команды: mean/P95/max 169.073/333.059/460.944 мс; превышений 100 мс 298; промежутков >100 мс 63; max gap 384.348 мс; заменено 0.
|
||||
- Аварийная команда: start/total 0.000/1.969 мс; deadline 50 мс соблюдён; блокировал none, 0 байт, seq=-1, ожидание 0.000 мс.
|
||||
- Телеметрия: mean/P95/max 163.317/331.782/463.898 мс; превышений 500 мс 0 (0.000000); заменено 0.
|
||||
- Видео: опубликовано к концу источника 0.984127, после drain 1.000000, итог 1.000000; delay mean/P95/max 329.072/420.025/436.185 мс.
|
||||
- Возраст изображения mean/P95/max 489.627/666.667/766.667 мс; доля >1 с 0.000000; поздних кадров 0, max серия 0.
|
||||
[260 кбит/с; Строгий приоритет]
|
||||
- Передано 1703 пакетов, 670280 байт; средняя/максимальная очередь 10.783/28 пакетов и 6010.2/16384 байт.
|
||||
- На конце видео: 23 пакетов, 12974.0 байт; полное освобождение 21.165867 с, дополнительно 0.399200 с.
|
||||
- Команды: mean/P95/max 10.282/18.754/20.667 мс; превышений 100 мс 0; промежутков >100 мс 0; max gap 63.477 мс; заменено 0.
|
||||
- Аварийная команда: start/total 0.000/1.969 мс; deadline 50 мс соблюдён; блокировал none, 0 байт, seq=-1, ожидание 0.000 мс.
|
||||
- Телеметрия: mean/P95/max 13.864/22.448/23.621 мс; превышений 500 мс 0 (0.000000); заменено 0.
|
||||
- Видео: опубликовано к концу источника 0.968254, после drain 1.000000, итог 1.000000; delay mean/P95/max 351.218/445.846/459.559 мс.
|
||||
- Возраст изображения mean/P95/max 511.188/693.333/783.333 мс; доля >1 с 0.000000; поздних кадров 0, max серия 0.
|
||||
[260 кбит/с; Приоритет + замена]
|
||||
- Передано 1703 пакетов, 670280 байт; средняя/максимальная очередь 10.783/28 пакетов и 6010.2/16384 байт.
|
||||
- На конце видео: 23 пакетов, 12974.0 байт; полное освобождение 21.165867 с, дополнительно 0.399200 с.
|
||||
- Команды: mean/P95/max 10.282/18.754/20.667 мс; превышений 100 мс 0; промежутков >100 мс 0; max gap 63.477 мс; заменено 0.
|
||||
- Аварийная команда: start/total 0.000/1.969 мс; deadline 50 мс соблюдён; блокировал none, 0 байт, seq=-1, ожидание 0.000 мс.
|
||||
- Телеметрия: mean/P95/max 13.864/22.448/23.621 мс; превышений 500 мс 0 (0.000000); заменено 0.
|
||||
- Видео: опубликовано к концу источника 0.968254, после drain 1.000000, итог 1.000000; delay mean/P95/max 351.218/445.846/459.559 мс.
|
||||
- Возраст изображения mean/P95/max 511.188/693.333/783.333 мс; доля >1 с 0.000000; поздних кадров 0, max серия 0.
|
||||
[230 кбит/с; FIFO]
|
||||
- Передано 1703 пакетов, 670280 байт; средняя/максимальная очередь 86.915/184 пакетов и 34309.1/76497 байт.
|
||||
- На конце видео: 181 пакетов, 73238.3 байт; полное освобождение 23.314087 с, дополнительно 2.547420 с.
|
||||
- Команды: mean/P95/max 1174.288/2240.535/2608.522 мс; превышений 100 мс 414; промежутков >100 мс 63; max gap 427.304 мс; заменено 0.
|
||||
- Аварийная команда: start/total 878.887/881.113 мс; deadline 50 мс НАРУШЕН; блокировал video, 608 байт, seq=461, ожидание 0.417 мс.
|
||||
- Телеметрия: mean/P95/max 1164.978/2225.607/2611.861 мс; превышений 500 мс 177 (0.850962); заменено 0.
|
||||
- Видео: опубликовано к концу источника 0.888889, после drain 1.000000, итог 1.000000; delay mean/P95/max 1347.217/2417.731/2576.186 мс.
|
||||
- Возраст изображения mean/P95/max 1506.487/2588.167/2906.667 мс; доля >1 с 0.771870; поздних кадров 42, max серия 42.
|
||||
[230 кбит/с; Строгий приоритет]
|
||||
- Передано 1703 пакетов, 670280 байт; средняя/максимальная очередь 59.750/132 пакетов и 34307.1/75969 байт.
|
||||
- На конце видео: 127 пакетов, 73238.3 байт; полное освобождение 23.314087 с, дополнительно 2.547420 с.
|
||||
- Команды: mean/P95/max 12.250/21.848/23.339 мс; превышений 100 мс 0; промежутков >100 мс 0; max gap 69.009 мс; заменено 0.
|
||||
- Аварийная команда: start/total 9.322/11.548 мс; deadline 50 мс соблюдён; блокировал video, 608 байт, seq=458, ожидание 9.322 мс.
|
||||
- Телеметрия: mean/P95/max 15.514/25.111/26.678 мс; превышений 500 мс 0 (0.000000); заменено 0.
|
||||
- Видео: опубликовано к концу источника 0.888889, после drain 1.000000, итог 1.000000; delay mean/P95/max 1445.995/2505.995/2583.977 мс.
|
||||
- Возраст изображения mean/P95/max 1594.537/2678.167/2916.667 мс; доля >1 с 0.804460; поздних кадров 47, max серия 47.
|
||||
[230 кбит/с; Приоритет + замена]
|
||||
- Передано 1703 пакетов, 670280 байт; средняя/максимальная очередь 59.750/132 пакетов и 34307.1/75969 байт.
|
||||
- На конце видео: 127 пакетов, 73238.3 байт; полное освобождение 23.314087 с, дополнительно 2.547420 с.
|
||||
- Команды: mean/P95/max 12.250/21.848/23.339 мс; превышений 100 мс 0; промежутков >100 мс 0; max gap 69.009 мс; заменено 0.
|
||||
- Аварийная команда: start/total 9.322/11.548 мс; deadline 50 мс соблюдён; блокировал video, 608 байт, seq=458, ожидание 9.322 мс.
|
||||
- Телеметрия: mean/P95/max 15.514/25.111/26.678 мс; превышений 500 мс 0 (0.000000); заменено 0.
|
||||
- Видео: опубликовано к концу источника 0.888889, после drain 1.000000, итог 1.000000; delay mean/P95/max 1445.995/2505.995/2583.977 мс.
|
||||
- Возраст изображения mean/P95/max 1594.537/2678.167/2916.667 мс; доля >1 с 0.804460; поздних кадров 47, max серия 47.
|
||||
|
||||
5. Интерпретация планировщиков
|
||||
- FIFO задерживает команды, потому что все ранее поступившие внешние видеопакеты остаются перед ними независимо от срочности.
|
||||
- Строгий приоритет после каждого окончания пакета выбирает управление раньше телеметрии и видео, поэтому сокращает очередь команд.
|
||||
- Уже начатая передача не прерывается: модель сериализует целый пакет как атомарную единицу и не задаёт фрагментацию общего пакета или возобновление передачи.
|
||||
- Поэтому нижняя граница задержки срочной команды включает остаток времени самого длинного пакета, уже занявшего ресурс.
|
||||
- Замена старого состояния новым не тратит канал на запоздалую команду, которая к моменту приёма уже не отражает актуальное управление.
|
||||
- Строгий приоритет перераспределяет порядок, но не уменьшает объём видео; при нагрузке выше пропускной способности видеопакеты продолжают накапливаться.
|
||||
- Длинная очередь видео увеличивает возраст изображения даже без потерь: полные, но старые кадры публикуются позже времени формирования.
|
||||
- Восстановление 100% кадров после drain подтверждает целостность, но не пригодность задержек канала для управления в реальном времени.
|
||||
|
||||
6. Допущения модели
|
||||
- Один общий абстрактный ресурс и одна расчётная скорость для обоих направлений; переключение направления не задерживает передачу.
|
||||
- Не выбран физический полудуплекс/дуплекс, TDD/FDD, модуляция, ретранслятор или физическое разделение восходящего и нисходящего каналов.
|
||||
- Ошибки и помехи отсутствуют; полностью переданный пакет доставляется без повреждений; видеопакеты не отбрасываются по возрасту.
|
||||
- После конца исходного видео новые данные не создаются, а очередь полностью освобождается.
|
||||
- При совпадении времени FIFO использует traffic_class, stream_id и sequence_number как устойчивое правило разрешения совпадений.
|
||||
- Средняя очередь рассчитана по времени на основном интервале и включает пакет, находящийся в передатчике; остаток байтов активного пакета учитывается пропорционально недопереданному времени.
|
||||
|
||||
7. Функциональные проверки
|
||||
- PASS 01_header_roundtrip: PASS
|
||||
- PASS 02_header_crc_detection: PASS
|
||||
- PASS 03_payload_crc_detection: PASS
|
||||
- PASS 04_video_wrapper_byte_exact: PASS
|
||||
- PASS 05_lab030_crc: PASS
|
||||
- PASS 06_lab028_crc: PASS
|
||||
- PASS 07_fifo_global_order: PASS
|
||||
- PASS 08_strict_priority: PASS
|
||||
- PASS 09_non_preemptive: PASS
|
||||
- PASS 10_intra_class_sequence: PASS
|
||||
- PASS 11_latest_state_scope: PASS
|
||||
- PASS 12_active_state_not_removed: PASS
|
||||
- PASS 13_emergency_never_replaced: PASS
|
||||
- PASS 14_300kbps_all_video: PASS
|
||||
- PASS 15_overload_visible: PASS
|
||||
- PASS 16_exact_transmission_duration: PASS
|
||||
- PASS 17_total_bits_accounting: PASS
|
||||
- PASS 18_reproducible_schedule: PASS
|
||||
- PASS 19_no_video_control_isolated: PASS
|
||||
- PASS 20_latest_state_reaches_receiver: PASS
|
||||
|
||||
8. Созданные файлы и итоговый Git status
|
||||
- protocol/link_packet.py
|
||||
- protocol/priority_scheduler.py
|
||||
- tests/lab033_priority_channel_scheduler.py
|
||||
- data/processed/lab033/lab033_summary.csv
|
||||
- data/processed/lab033/lab033_class_metrics.csv
|
||||
- data/processed/lab033/lab033_report.txt
|
||||
- data/processed/lab033/lab033_control_delay.png
|
||||
- data/processed/lab033/lab033_emergency_delay.png
|
||||
- data/processed/lab033/lab033_video_delay.png
|
||||
- data/processed/lab033/lab033_queue_length.png
|
||||
- data/processed/lab033/lab033_deadline_misses.png
|
||||
- data/processed/lab033/lab033_scheduler_comparison.png
|
||||
- Lab028-Lab032 и их сохранённые результаты не изменены.
|
||||
- Lab033 не добавлена в индекс и не закоммичена.
|
||||
|
||||
## main...origin/main
|
||||
?? data/processed/lab033/
|
||||
?? protocol/link_packet.py
|
||||
?? protocol/priority_scheduler.py
|
||||
?? tests/lab033_priority_channel_scheduler.py
|
||||
BIN
data/processed/lab033/lab033_scheduler_comparison.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
10
data/processed/lab033/lab033_summary.csv
Normal file
@@ -0,0 +1,10 @@
|
||||
channel_kbps,scheduler,offered_load_kbps,emergency_load_kbps,control_load_kbps,telemetry_load_kbps,video_load_kbps,service_share_percent,offered_to_capacity_ratio,transmitted_packets,transmitted_bytes,source_duration_seconds,drain_end_seconds,additional_drain_seconds,queue_at_source_end_packets,queue_at_source_end_bytes,mean_queue_packets,max_queue_packets,mean_queue_bytes,max_queue_bytes,control_mean_age_ms,control_p95_age_ms,control_max_age_ms,control_gaps_over_100ms,control_max_receive_gap_ms,control_replaced,emergency_start_delay_ms,emergency_total_delay_ms,emergency_deadline_met,emergency_blocker_class,emergency_blocker_size_bytes,emergency_blocker_sequence,emergency_blocking_delay_ms,telemetry_mean_age_ms,telemetry_p95_age_ms,telemetry_max_age_ms,telemetry_misses_500ms,telemetry_miss_fraction,telemetry_replaced,video_published_at_source_end_fraction,video_published_after_drain_fraction,video_final_recovery_fraction,video_mean_publication_delay_ms,video_p95_publication_delay_ms,video_max_publication_delay_ms,video_mean_display_age_ms,video_p95_display_age_ms,video_max_display_age_ms,video_age_over_1s_fraction,video_late_frame_count,video_max_late_run
|
||||
300.0,fifo,258.21380417335473,0.02465489566613162,10.256436597110755,7.692327447833066,240.24038523274479,34.922718863758426,0.8607126805778491,1703,670280,20.766666666666666,20.99853366666666,0.23186699999999405,18,8695.012499999913,10.477643691813581,22,4452.409985633955,12221,106.0716594551255,238.6335833333294,296.1863333333259,63,344.4796666666594,0,0.0,1.7066666666671892,True,none,0,-1,0.0,101.03181249999754,233.68033333332914,296.8269999999933,0,0.0,0,0.9841269841269841,1.0,1.0,261.5593703703662,304.57866666666183,311.1463333333262,423.8457877201335,580.0,643.3333333333333,0.0,0,0
|
||||
300.0,strict_priority,258.21380417335473,0.02465489566613162,10.256436597110755,7.692327447833066,240.24038523274479,34.922718863758426,0.8607126805778491,1703,670280,20.766666666666666,20.99853366666666,0.23186699999999405,15,8695.012499999915,8.05016637239154,22,4452.387224542477,12221,9.439928685896334,17.46633333333346,17.813000000000745,0,65.7596666666671,0,0.0,1.7066666666671892,True,none,0,-1,0.0,12.424120192306697,20.10688333333437,20.37300000000064,0,0.0,0,0.9841269841269841,1.0,1.0,276.3369365079338,322.0720000000001,330.77299999999,437.6487386958594,593.3333333333334,660.0000000000001,0.0,0,0
|
||||
300.0,latest_state,258.21380417335473,0.02465489566613162,10.256436597110755,7.692327447833066,240.24038523274479,34.922718863758426,0.8607126805778491,1703,670280,20.766666666666666,20.99853366666666,0.23186699999999405,15,8695.012499999915,8.05016637239154,22,4452.387224542477,12221,9.439928685896334,17.46633333333346,17.813000000000745,0,65.7596666666671,0,0.0,1.7066666666671892,True,none,0,-1,0.0,12.424120192306697,20.10688333333437,20.37300000000064,0,0.0,0,0.9841269841269841,1.0,1.0,276.3369365079338,322.0720000000001,330.77299999999,437.6487386958594,593.3333333333334,660.0000000000001,0.0,0,0
|
||||
260.0,fifo,258.21380417335473,0.02465489566613162,10.256436597110755,7.692327447833066,240.24038523274479,34.922718863758426,0.9931300160513643,1703,670280,20.766666666666666,21.165867000000176,0.39920033333351057,32,12974.010833338622,14.800483013954153,37,6010.5496338357525,16512,169.07309430475814,333.0593076923547,460.9439230770995,63,384.34838461538055,0,0.0,1.969230769230279,True,none,0,-1,0.0,163.31735133138503,331.7823846154539,463.8977692309467,0,0.0,0,0.9841269841269841,1.0,1.0,329.07164590967284,420.0249487180816,436.18494871812175,489.62700661000974,666.6666666666679,766.6666666666692,0.0,0,0
|
||||
260.0,strict_priority,258.21380417335473,0.02465489566613162,10.256436597110755,7.692327447833066,240.24038523274479,34.922718863758426,0.9931300160513643,1703,670280,20.766666666666666,21.16586700000017,0.39920033333350347,23,12974.010833338338,10.783079034450305,28,6010.224630522928,16384,10.282265902386632,18.753596153843198,20.66700000000843,0,63.476923076922546,0,0.0,1.969230769230279,True,none,0,-1,0.0,13.863505177534366,22.44779230770204,23.620846153853847,0,0.0,0,0.9682539682539683,1.0,1.0,351.2176776557015,445.84648717956554,459.55930769245157,511.18822788794483,693.3333333333316,783.3333333333314,0.0,0,0
|
||||
260.0,latest_state,258.21380417335473,0.02465489566613162,10.256436597110755,7.692327447833066,240.24038523274479,34.922718863758426,0.9931300160513643,1703,670280,20.766666666666666,21.16586700000017,0.39920033333350347,23,12974.010833338338,10.783079034450305,28,6010.224630522928,16384,10.282265902386632,18.753596153843198,20.66700000000843,0,63.476923076922546,0,0.0,1.969230769230279,True,none,0,-1,0.0,13.863505177534366,22.44779230770204,23.620846153853847,0,0.0,0,0.9682539682539683,1.0,1.0,351.2176776557015,445.84648717956554,459.55930769245157,511.18822788794483,693.3333333333316,783.3333333333314,0.0,0,0
|
||||
230.0,fifo,258.21380417335473,0.02465489566613162,10.256436597110755,7.692327447833066,240.24038523274479,34.922718863758426,1.1226687137971945,1703,670280,20.766666666666666,23.3140869565216,2.5474202898549336,181,73238.33333333043,86.91502882266383,184,34309.08168164904,76497,1174.2878762541263,2240.5347826085754,2608.5217391302963,63,427.3043478260803,0,878.8869565216775,881.1130434781998,False,video,608,461,0.4173913042819777,1164.978260869511,2225.6069565216276,2611.860869565078,177,0.8509615384615384,0,0.8888888888888888,1.0,1.0,1347.216563146941,2417.731014492629,2576.1855072462367,1506.486563750715,2588.1666666666674,2906.6666666666697,0.7718696397941681,42,42
|
||||
230.0,strict_priority,258.21380417335473,0.02465489566613162,10.256436597110755,7.692327447833066,240.24038523274479,34.922718863758426,1.1226687137971945,1703,670280,20.766666666666666,23.314086956521678,2.547420289855012,127,73238.33333333276,59.749842696627866,132,34307.118394888734,75969,12.250334448143445,21.84782608690261,23.33913043477409,0,69.00869565217427,0,9.32173913036749,11.547826086889756,True,video,608,458,9.32173913036749,15.51421404680579,25.111304347813142,26.678260869555714,0,0.0,0,0.8888888888888888,1.0,1.0,1445.9947550034299,2505.9953623187953,2583.976811594141,1594.536878216124,2678.1666666666674,2916.666666666668,0.8044596912521441,47,47
|
||||
230.0,latest_state,258.21380417335473,0.02465489566613162,10.256436597110755,7.692327447833066,240.24038523274479,34.922718863758426,1.1226687137971945,1703,670280,20.766666666666666,23.314086956521678,2.547420289855012,127,73238.33333333276,59.749842696627866,132,34307.118394888734,75969,12.250334448143445,21.84782608690261,23.33913043477409,0,69.00869565217427,0,9.32173913036749,11.547826086889756,True,video,608,458,9.32173913036749,15.51421404680579,25.111304347813142,26.678260869555714,0,0.0,0,0.8888888888888888,1.0,1.0,1445.9947550034299,2505.9953623187953,2583.976811594141,1594.536878216124,2678.1666666666674,2916.666666666668,0.8044596912521441,47,47
|
||||
|
BIN
data/processed/lab033/lab033_video_delay.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
data/processed/lab034/lab034_control_delay.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
16
data/processed/lab034/lab034_control_metrics.csv
Normal file
@@ -0,0 +1,16 @@
|
||||
channel_kbps,policy,control_mean_age_ms,control_p95_age_ms,control_max_age_ms,control_deadline_misses,control_max_receive_gap_ms,control_replaced,emergency_total_delay_ms,emergency_deadline_met,emergency_blocker_class,emergency_blocker_size_bytes,emergency_blocker_sequence,emergency_blocking_delay_ms,telemetry_mean_age_ms,telemetry_p95_age_ms,telemetry_max_age_ms,telemetry_deadline_misses,telemetry_replaced
|
||||
300.0,continuous_no_drop,9.439928685896334,17.46633333333346,17.813000000000745,0,65.7596666666671,0,1.7066666666671892,True,none,0,-1,0.0,12.424120192306697,20.10688333333437,20.37300000000064,0,0
|
||||
300.0,aligned_no_drop,10.09563301281934,17.46633333333314,17.78700000000022,0,65.7596666666671,0,1.7066666666671892,True,none,0,-1,0.0,12.858206730768112,19.573666666667933,20.160000000000622,0,0
|
||||
300.0,aligned_1500ms,10.09563301281934,17.46633333333314,17.78700000000022,0,65.7596666666671,0,1.7066666666671892,True,none,0,-1,0.0,12.858206730768112,19.573666666667933,20.160000000000622,0,0
|
||||
300.0,aligned_1000ms,10.09563301281934,17.46633333333314,17.78700000000022,0,65.7596666666671,0,1.7066666666671892,True,none,0,-1,0.0,12.858206730768112,19.573666666667933,20.160000000000622,0,0
|
||||
300.0,aligned_500ms,10.09563301281934,17.46633333333314,17.78700000000022,0,65.7596666666671,0,1.7066666666671892,True,none,0,-1,0.0,12.858206730768112,19.573666666667933,20.160000000000622,0,0
|
||||
260.0,continuous_no_drop,10.282265902386632,18.753596153843198,20.66700000000843,0,63.476923076922546,0,1.969230769230279,True,none,0,-1,0.0,13.863505177534366,22.44779230770204,23.620846153853847,0,0
|
||||
260.0,aligned_no_drop,10.671617788489147,18.907692307691043,20.492307692309808,0,67.81538461538617,0,1.969230769230279,True,none,0,-1,0.0,13.490528846181197,21.89960384616611,23.323076923056796,0,0
|
||||
260.0,aligned_1500ms,10.671617788489147,18.907692307691043,20.492307692309808,0,67.81538461538617,0,1.969230769230279,True,none,0,-1,0.0,13.490528846181197,21.89960384616611,23.323076923056796,0,0
|
||||
260.0,aligned_1000ms,10.671617788489147,18.907692307691043,20.492307692309808,0,67.81538461538617,0,1.969230769230279,True,none,0,-1,0.0,13.490528846181197,21.89960384616611,23.323076923056796,0,0
|
||||
260.0,aligned_500ms,10.71385151629976,18.907692307691043,20.492307692309808,0,67.81538461538617,0,1.969230769230279,True,none,0,-1,0.0,13.61641642014566,22.039999999996684,23.323076923056796,0,0
|
||||
230.0,continuous_no_drop,12.250334448143445,21.84782608690261,23.33913043477409,0,69.00869565217427,0,11.547826086889756,True,video,608,458,9.32173913036749,15.51421404680579,25.111304347813142,26.678260869555714,0,0
|
||||
230.0,aligned_no_drop,12.729598662164767,22.499999999942013,24.556521739064863,0,69.00869565217427,0,22.330434782542596,True,video,608,463,20.10434782602033,15.594983277549522,25.377391304327652,27.895652173848262,0,0
|
||||
230.0,aligned_1500ms,12.613043478216982,22.460869565154475,24.556521739064863,0,69.00869565217427,0,22.330434782542596,True,video,608,463,20.10434782602033,15.613377926377664,25.426086956510073,27.895652173848262,0,0
|
||||
230.0,aligned_1000ms,12.76596989961911,22.578260869525746,23.356521739078318,0,69.00869565217427,0,11.652173912974462,True,video,608,484,9.426086956452195,15.511371237411904,25.37739130434555,26.608695652086567,0,0
|
||||
230.0,aligned_500ms,12.377759197289913,22.230434782573028,23.339130434790967,0,69.00869565217427,0,3.6869565216850475,True,video,608,511,1.4608695651627812,15.598996655484031,24.913043478221116,26.678260869573478,0,0
|
||||
|
BIN
data/processed/lab034/lab034_fec_alignment_overhead.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
data/processed/lab034/lab034_frame_outcomes.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
data/processed/lab034/lab034_image_age.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
data/processed/lab034/lab034_policy_comparison.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
data/processed/lab034/lab034_queue_size.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
98
data/processed/lab034/lab034_report.txt
Normal file
@@ -0,0 +1,98 @@
|
||||
Lab034. Ограничение возраста видеоданных и отбрасывание устаревших кадров
|
||||
|
||||
1. Исходное состояние
|
||||
- Commit Lab033: 21751f41dd0af022fc1f935c67e795e3ae4257c5.
|
||||
- Перед Lab034 рабочее дерево было чистым; main опережала origin/main на один commit.
|
||||
- Lab028-Lab033 и их результаты не изменяются.
|
||||
|
||||
2. Формирование FEC-блоков
|
||||
layout | source | parity | blocks | partial | cross-frame | frames/block mean/max | before/after Lab033 kbps
|
||||
Непрерывные | 862 | 216 | 72 | 1 | 58 | 1.806/2 | 226.951/240.240
|
||||
По кадрам | 862 | 251 | 125 | 62 | 0 | 1.000/1 | 232.926/246.647
|
||||
- Дополнительная избыточность выравнивания: 5.975 кбит/с (2.633%).
|
||||
- Средняя эквивалентная группа кадра: непрерывные 17.111 пакета / 263.968 мс; выровненные 17.667 пакета / 271.007 мс при 300 кбит/с.
|
||||
|
||||
3. Пятнадцать сочетаний
|
||||
speed | policy | offered/capacity | published/drop/partial | update fps | age P95 ms | control P95 ms | emergency ms | queue max/end | drain s | wasted bytes
|
||||
300 | Непрерывные, без отбрасывания | 0.8607 | 63/0/0 | 2.986 | 593.333 | 17.466 | 1.707 | 22/15 | 0.231867 | 0
|
||||
300 | По кадрам, без отбрасывания | 0.8821 | 63/0/0 | 2.986 | 590.000 | 17.466 | 1.707 | 21/13 | 0.199440 | 0
|
||||
300 | По кадрам, 1500 мс | 0.8821 | 63/0/0 | 2.986 | 590.000 | 17.466 | 1.707 | 21/13 | 0.199440 | 0
|
||||
300 | По кадрам, 1000 мс | 0.8821 | 63/0/0 | 2.986 | 590.000 | 17.466 | 1.707 | 21/13 | 0.199440 | 0
|
||||
300 | По кадрам, 500 мс | 0.8821 | 63/0/0 | 2.986 | 590.000 | 17.466 | 1.707 | 21/13 | 0.199440 | 0
|
||||
260 | Непрерывные, без отбрасывания | 0.9931 | 63/0/0 | 2.937 | 690.000 | 18.754 | 1.969 | 28/23 | 0.399200 | 0
|
||||
260 | По кадрам, без отбрасывания | 1.0178 | 63/0/0 | 2.937 | 773.833 | 18.908 | 1.969 | 37/30 | 0.539293 | 0
|
||||
260 | По кадрам, 1500 мс | 1.0178 | 63/0/0 | 2.937 | 773.833 | 18.908 | 1.969 | 37/30 | 0.539293 | 0
|
||||
260 | По кадрам, 1000 мс | 1.0178 | 63/0/0 | 2.937 | 773.833 | 18.908 | 1.969 | 37/30 | 0.539293 | 0
|
||||
260 | По кадрам, 500 мс | 1.0178 | 56/7/7 | 2.648 | 1053.833 | 18.908 | 1.969 | 31/24 | 0.403939 | 69597
|
||||
230 | Непрерывные, без отбрасывания | 1.1227 | 63/0/0 | 2.697 | 2443.833 | 21.848 | 11.548 | 132/127 | 2.547420 | 0
|
||||
230 | По кадрам, без отбрасывания | 1.1505 | 63/0/0 | 2.600 | 2913.333 | 22.500 | 22.330 | 160/155 | 3.125855 | 0
|
||||
230 | По кадрам, 1500 мс | 1.1505 | 27/36/36 | 1.300 | 11064.833 | 22.461 | 22.330 | 87/81 | 1.418064 | 321929
|
||||
230 | По кадрам, 1000 мс | 1.1505 | 14/49/49 | 0.674 | 11731.500 | 22.578 | 11.652 | 63/53 | 0.904881 | 434885
|
||||
230 | По кадрам, 500 мс | 1.1505 | 3/60/60 | 0.144 | 11731.500 | 22.230 | 3.687 | 32/27 | 0.419072 | 532167
|
||||
|
||||
4. Практический смысл ограничений возраста
|
||||
- При 300 кбит/с очередь обслуживается достаточно быстро: ограничения 500, 1000 и 1500 мс не срабатывают и не дают выигрыша.
|
||||
- При 260 кбит/с пределы 1000 и 1500 мс также не срабатывают; предел 500 мс публикует 56 из 63 кадров и отбрасывает 7, поэтому он уже чрезмерно жёсткий для части кадров.
|
||||
- При 230 кбит/с предел 1500 мс сохраняет 27 кадров, 1000 мс — 14, 500 мс — только 3; более строгий предел лучше ограничивает очередь, но резко уменьшает частоту обновления.
|
||||
- В исследованной перегрузке 1500 мс является наиболее мягким из конечных ограничений; 1000 мс сильнее улучшает свежесть ценой обновлений, а 500 мс чрезмерно жёсток.
|
||||
|
||||
5. Интерпретация
|
||||
- Произвольное удаление символа из непрерывного FEC-блока опасно: блок может содержать соседние кадры, и нехватка символов затрагивает их совместное декодирование.
|
||||
- Выравнивание изолирует кадр в собственных блоках, поэтому все ожидающие символы устаревшего кадра можно удалить без повреждения соседей.
|
||||
- Цена изоляции — 5.975 кбит/с дополнительного внешнего потока из-за большего числа неполных блоков.
|
||||
- Полная доставка после освобождения очереди не является целью операторского видео: поздний полный кадр уже не описывает текущее состояние ровера.
|
||||
- При перегрузке отбрасывание старого кадра освобождает ресурс для более свежего изображения и не затрагивает команды.
|
||||
- При 230 кбит/с выравнивание меняет фазу активного видеопакета около 10,0 с; фактическая задержка аварийной команды может отличаться от Lab033, но отбрасывание её не увеличивает, а непереключаемая верхняя граница остаётся прежней, поскольку максимальный размер пакета не изменён.
|
||||
- Слишком малый возраст может отменять кадры быстрее их полной передачи и снижать фактическую частоту обновления.
|
||||
- Поэтому возраст изображения оценивается вместе с частотой обновления: хорошее значение одного показателя не гарантирует полезный видеопоток.
|
||||
- Замена команд допустима только для текущего состояния (скорость, поворот, торможение, телеметрия). Дискретные события заменять нельзя; в Lab034 они не моделируются.
|
||||
|
||||
6. Допущения
|
||||
- Один общий абстрактный ресурс, одна скорость для обоих направлений, без времени переключения и без выбора физического duplex/TDD/FDD.
|
||||
- Ошибки и помехи отсутствуют; начатый пакет не прерывается; форматы Lab028, Lab030 и Lab033 неизменны.
|
||||
- Возраст считается от формирования составного кадра по generation_time_us; служебные frame_id и состояние отмены не сериализуются.
|
||||
- Видеокадр с удалёнными пакетами очищается в приёмнике и никогда не публикуется.
|
||||
- Допустимые возраста 500, 1000 и 1500 мс заданы явно и автоматически не выбираются.
|
||||
|
||||
7. Функциональные проверки
|
||||
- PASS 01_continuous_matches_lab033: PASS
|
||||
- PASS 02_aligned_blocks_one_frame: PASS
|
||||
- PASS 03_aligned_no_drop_recovers_all: PASS
|
||||
- PASS 04_whole_frame_drop_isolated: PASS
|
||||
- PASS 05_only_waiting_video_removed: PASS
|
||||
- PASS 06_active_packet_not_preempted: PASS
|
||||
- PASS 07_partial_cancel_not_published: PASS
|
||||
- PASS 08_age_uses_frame_generation: PASS
|
||||
- PASS 09_receiver_cleans_stale_state: PASS
|
||||
- PASS 10_emergency_never_deleted: PASS
|
||||
- PASS 11_high_priority_before_video: PASS
|
||||
- PASS 12_230kbps_finite_age_bounds_queue: PASS
|
||||
- PASS 13_packet_accounting: PASS
|
||||
- PASS 14_byte_accounting: PASS
|
||||
- PASS 15_all_crc_layers_pass: PASS
|
||||
- PASS 16_incomplete_frame_never_published: PASS
|
||||
- PASS 17_reproducible: PASS
|
||||
- PASS 18_drop_does_not_worsen_emergency: PASS
|
||||
|
||||
8. Созданные файлы
|
||||
- protocol/video_age_policy.py
|
||||
- tests/lab034_stale_video_drop.py
|
||||
- data/processed/lab034/lab034_summary.csv
|
||||
- data/processed/lab034/lab034_video_metrics.csv
|
||||
- data/processed/lab034/lab034_control_metrics.csv
|
||||
- data/processed/lab034/lab034_report.txt
|
||||
- data/processed/lab034/lab034_image_age.png
|
||||
- data/processed/lab034/lab034_frame_outcomes.png
|
||||
- data/processed/lab034/lab034_queue_size.png
|
||||
- data/processed/lab034/lab034_control_delay.png
|
||||
- data/processed/lab034/lab034_update_rate.png
|
||||
- data/processed/lab034/lab034_fec_alignment_overhead.png
|
||||
- data/processed/lab034/lab034_policy_comparison.png
|
||||
|
||||
9. Итоговый Git status
|
||||
- Lab034 не добавлена в индекс и не закоммичена.
|
||||
|
||||
## main...origin/main [ahead 1]
|
||||
?? data/processed/lab034/
|
||||
?? protocol/video_age_policy.py
|
||||
?? tests/lab034_stale_video_drop.py
|
||||
16
data/processed/lab034/lab034_summary.csv
Normal file
@@ -0,0 +1,16 @@
|
||||
channel_kbps,policy,layout,max_video_age_ms,offered_load_kbps,offered_to_capacity_ratio,transmitted_packets,transmitted_bytes,dropped_packets,dropped_bytes,wasted_transmitted_bytes,mean_queue_packets,max_queue_packets,mean_queue_bytes,max_queue_bytes,queue_at_source_end_packets,additional_drain_seconds,channel_utilization_fraction
|
||||
300.0,continuous_no_drop,continuous,-1,258.21380417335473,0.8607126805778491,1703,670280,0,0,0,8.05016637239154,22,4452.387224542477,12221,15,0.23186699999999405,0.8512086423304386
|
||||
300.0,aligned_no_drop,aligned,-1,264.62022471910115,0.8820674157303372,1738,686910,0,0,0,8.518393226324118,21,4685.69717834665,11415,13,0.1994403333333281,0.8724635473515175
|
||||
300.0,aligned_1500ms,aligned,1500,264.62022471910115,0.8820674157303372,1738,686910,0,0,0,8.518393226324118,21,4685.69717834665,11415,13,0.1994403333333281,0.8724635473515175
|
||||
300.0,aligned_1000ms,aligned,1000,264.62022471910115,0.8820674157303372,1738,686910,0,0,0,8.518393226324118,21,4685.69717834665,11415,13,0.1994403333333281,0.8724635473515175
|
||||
300.0,aligned_500ms,aligned,500,264.62022471910115,0.8820674157303372,1738,686910,0,0,0,8.518393226324118,21,4685.69717834665,11415,13,0.1994403333333281,0.8724635473515175
|
||||
260.0,continuous_no_drop,continuous,-1,258.21380417335473,0.9931300160513643,1703,670280,0,0,0,10.783079034450305,28,6010.224630522928,16384,23,0.39920033333350347,0.9743990170589202
|
||||
260.0,aligned_no_drop,aligned,-1,264.62022471910115,1.017770095073466,1738,686910,0,0,0,13.695290291396285,37,7655.6639836816,21131,30,0.5392926410258561,0.991800947030499
|
||||
260.0,aligned_1500ms,aligned,1500,264.62022471910115,1.017770095073466,1738,686910,0,0,0,13.695290291396285,37,7655.6639836816,21131,30,0.5392926410258561,0.991800947030499
|
||||
260.0,aligned_1000ms,aligned,1000,264.62022471910115,1.017770095073466,1738,686910,0,0,0,13.695290291396285,37,7655.6639836816,21131,30,0.5392926410258561,0.991800947030499
|
||||
260.0,aligned_500ms,aligned,500,264.62022471910115,1.017770095073466,1730,682511,8,4399,69597,13.261018996174682,31,7402.747049027978,16992,24,0.4039387948720048,0.991800947030499
|
||||
230.0,continuous_no_drop,continuous,-1,258.21380417335473,1.1226687137971945,1703,670280,0,0,0,59.749842696627866,132,34307.118394888734,75969,127,2.547420289855012,1.0
|
||||
230.0,aligned_no_drop,aligned,-1,264.62022471910115,1.1505227161700051,1738,686910,0,0,0,76.17493190801534,160,43586.68565075366,92568,155,3.1258550724636294,1.0
|
||||
230.0,aligned_1500ms,aligned,1500,264.62022471910115,1.1505227161700051,1641,637811,97,49099,321929,59.44779896084508,87,33911.67327808277,50035,81,1.4180637681158217,1.0
|
||||
230.0,aligned_1000ms,aligned,1000,264.62022471910115,1.1505227161700051,1609,623057,129,63853,434885,42.60156656500422,63,24223.970784957004,35495,53,0.9048811594201744,1.0
|
||||
230.0,aligned_500ms,aligned,500,264.62022471910115,1.1505227161700051,1580,609090,158,77820,532167,20.03526158908224,32,11270.42233914812,18208,27,0.41907246376802476,0.9999999999999993
|
||||
|
BIN
data/processed/lab034/lab034_update_rate.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
16
data/processed/lab034/lab034_video_metrics.csv
Normal file
@@ -0,0 +1,16 @@
|
||||
channel_kbps,policy,created_frames,published_frames,intentionally_dropped_frames,partially_transmitted_cancelled_frames,published_after_deadline_frames,published_fraction,actual_update_fps,mean_publication_delay_ms,p95_publication_delay_ms,max_publication_delay_ms,mean_display_age_ms,p95_display_age_ms,max_display_age_ms,display_age_over_500ms_fraction,display_age_over_1000ms_fraction,mean_no_update_duration_ms,p95_no_update_duration_ms,max_no_update_duration_ms,mean_missing_run_frames,p95_missing_run_frames,max_missing_run_frames,delivered_useful_video_kbps,wasted_transmitted_video_bytes,receiver_cleared_frames
|
||||
300.0,continuous_no_drop,63,63,0,0,0,1.0,2.985553772070626,276.3369365079338,322.0720000000001,330.77299999999,437.2938723131218,593.3333333333333,660.0000000000001,0.3142444658325313,0.0,329.6296296296296,388.9899000000025,397.1729999999969,0.0,0.0,0,157.76975922953451,0,0
|
||||
300.0,aligned_no_drop,63,63,0,0,0,1.0,2.985553772070626,272.2167195767169,293.13066666666225,301.84000000000003,433.2836060314408,589.9999999999999,623.3333333333348,0.30221366698748797,0.0,329.6296296296296,355.05666666666684,406.506333333331,0.0,0.0,0,157.76975922953451,0,0
|
||||
300.0,aligned_1500ms,63,63,0,0,0,1.0,2.985553772070626,272.2167195767169,293.13066666666225,301.84000000000003,433.2836060314408,589.9999999999999,623.3333333333348,0.30221366698748797,0.0,329.6296296296296,355.05666666666684,406.506333333331,0.0,0.0,0,157.76975922953451,0,0
|
||||
300.0,aligned_1000ms,63,63,0,0,0,1.0,2.985553772070626,272.2167195767169,293.13066666666225,301.84000000000003,433.2836060314408,589.9999999999999,623.3333333333348,0.30221366698748797,0.0,329.6296296296296,355.05666666666684,406.506333333331,0.0,0.0,0,157.76975922953451,0,0
|
||||
300.0,aligned_500ms,63,63,0,0,0,1.0,2.985553772070626,272.2167195767169,293.13066666666225,301.84000000000003,433.2836060314408,589.9999999999999,623.3333333333348,0.30221366698748797,0.0,329.6296296296296,355.05666666666684,406.506333333331,0.0,0.0,0,157.76975922953451,0,0
|
||||
260.0,continuous_no_drop,63,63,0,0,0,1.0,2.937399678972713,351.2176776557015,445.84648717956554,459.55930769245157,509.63907603464884,690.0000000000005,783.3333333333314,0.5312800769971127,0.0,334.9462365591397,401.4781615384583,411.91761538461515,0.0,0.0,0,157.76975922953451,0,0
|
||||
260.0,aligned_no_drop,63,63,0,0,0,1.0,2.937399678972713,393.259570207603,579.4598205129968,620.584948718161,546.5335258261151,773.833333333333,943.3333333333316,0.62223291626564,0.0,334.9462365591397,363.58000000001095,417.7637692307723,0.0,0.0,0,157.76975922953451,0,0
|
||||
260.0,aligned_1500ms,63,63,0,0,0,1.0,2.937399678972713,393.259570207603,579.4598205129968,620.584948718161,546.5335258261151,773.833333333333,943.3333333333316,0.62223291626564,0.0,334.9462365591397,363.58000000001095,417.7637692307723,0.0,0.0,0,157.76975922953451,0,0
|
||||
260.0,aligned_1000ms,63,63,0,0,0,1.0,2.937399678972713,393.259570207603,579.4598205129968,620.584948718161,546.5335258261151,773.833333333333,943.3333333333316,0.62223291626564,0.0,334.9462365591397,363.58000000001095,417.7637692307723,0.0,0.0,0,157.76975922953451,0,0
|
||||
260.0,aligned_500ms,63,56,7,7,0,0.8888888888888888,2.648475120385233,367.5917000915918,493.7670000001404,498.6567435899403,584.550850176452,1053.8333333333312,1769.9999999999995,0.6212704523580366,0.060153994225216556,370.8333333333333,664.0000000000219,1270.1227435895887,1.4,2.5999999999999996,3,139.5205136436597,69597,7
|
||||
230.0,continuous_no_drop,63,63,0,0,0,1.0,2.696629213483146,1445.9947550034299,2505.9953623187953,2583.976811594141,1464.8845043310876,2443.8333333333317,2740.000000000002,0.9716073147256978,0.78055822906641,364.32748538011697,437.67652173912944,464.34782608695,0.0,0.0,0,157.76975922953451,0,0
|
||||
230.0,aligned_no_drop,63,63,0,0,0,1.0,2.6003210272873196,1777.4951000689596,3067.511884057848,3204.707246376671,1723.7872954764196,2913.333333333334,3206.666666666667,0.9735322425409048,0.8267564966313763,377.57575757575756,412.5426086956473,424.00000000000125,0.0,0.0,0,157.76975922953451,0,0
|
||||
230.0,aligned_1500ms,63,27,36,36,0,0.42857142857142855,1.3001605136436598,1010.3600644122084,1414.757101449207,1473.0202898550094,4028.4071222329167,11064.833333333332,12103.333333333334,0.9735322425409048,0.8267564966313763,741.6666666666666,416.659130434783,10626.97971014499,36.0,36.0,36,66.39987158908507,321929,36
|
||||
230.0,aligned_1000ms,63,14,49,49,0,0.2222222222222222,0.6741573033707865,734.7155279503047,977.28927536231,995.617391304334,4667.483156881616,11731.499999999998,12770.0,0.9735322425409048,0.8233878729547642,1384.4444444444443,6308.219130434754,11828.788405797173,24.5,36.65,38,34.951781701444624,434885,49
|
||||
230.0,aligned_500ms,63,3,60,60,0,0.047619047619047616,0.14446227929373998,429.5149758453909,457.5026086956464,460.9971014492756,5508.034969521976,11731.499999999998,12770.0,0.9696823869104908,0.9076034648700674,5191.666666666666,11634.280579710185,12340.614492753682,30.0,37.2,38,7.567126805778492,532167,60
|
||||
|
BIN
data/processed/lab035/lab035_control_delay.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
19
data/processed/lab035/lab035_control_metrics.csv
Normal file
@@ -0,0 +1,19 @@
|
||||
channel_kbps,policy,control_p95_delay_ms,control_max_delay_ms,control_deadline_misses,control_max_receive_gap_ms,emergency_delay_ms,emergency_deadline_met,emergency_blocker_class,emergency_blocking_delay_ms,telemetry_deadline_misses
|
||||
300.0,no_drop,17.46633333333314,17.78700000000022,0,65.7596666666671,1.7066666666671892,True,none,0.0,0
|
||||
300.0,reactive_1500ms,17.46633333333314,17.78700000000022,0,65.7596666666671,1.7066666666671892,True,none,0.0,0
|
||||
300.0,latest_only,17.46633333333314,17.78700000000022,0,65.7596666666671,1.7066666666671892,True,none,0.0,0
|
||||
300.0,two_waiting,17.46633333333314,17.78700000000022,0,65.7596666666671,1.7066666666671892,True,none,0.0,0
|
||||
300.0,predict_1000ms,17.46633333333314,17.78700000000022,0,65.7596666666671,1.7066666666671892,True,none,0.0,0
|
||||
300.0,predict_500ms,17.46633333333314,17.78700000000022,0,65.7596666666671,1.7066666666671892,True,none,0.0,0
|
||||
260.0,no_drop,18.907692307691043,20.492307692309808,0,67.81538461538617,1.969230769230279,True,none,0.0,0
|
||||
260.0,reactive_1500ms,18.907692307691043,20.492307692309808,0,67.81538461538617,1.969230769230279,True,none,0.0,0
|
||||
260.0,latest_only,18.907692307691043,20.492307692309808,0,67.81538461538617,1.969230769230279,True,none,0.0,0
|
||||
260.0,two_waiting,18.907692307691043,20.492307692309808,0,67.81538461538617,1.969230769230279,True,none,0.0,0
|
||||
260.0,predict_1000ms,18.907692307691043,20.492307692309808,0,67.81538461538617,1.969230769230279,True,none,0.0,0
|
||||
260.0,predict_500ms,18.907692307691043,20.533000000078516,0,67.81538461538617,1.969230769230279,True,none,0.0,0
|
||||
230.0,no_drop,22.499999999942013,24.556521739064863,0,69.00869565217427,22.330434782542596,True,video,20.10434782602033,0
|
||||
230.0,reactive_1500ms,22.460869565154475,24.556521739064863,0,69.00869565217427,22.330434782542596,True,video,20.10434782602033,0
|
||||
230.0,latest_only,22.043478260820315,23.339130434699484,0,69.00869565217427,6.2260869564561006,True,video,3.9999999999338343,0
|
||||
230.0,two_waiting,22.53913043472089,23.33913043474567,0,69.00869565217427,6.991304347760519,True,video,4.765217391238252,0
|
||||
230.0,predict_1000ms,22.317391304289558,23.35652173909253,0,69.00869565217427,2.365217391240293,True,video,0.13913043471802666,0
|
||||
230.0,predict_500ms,22.211510869569295,23.304347826089256,0,69.00869565217427,5.739130434786688,True,video,3.5130434782644215,0
|
||||
|
BIN
data/processed/lab035/lab035_frame_outcomes.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
data/processed/lab035/lab035_image_age.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
data/processed/lab035/lab035_policy_comparison.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
data/processed/lab035/lab035_prediction_accuracy.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
19
data/processed/lab035/lab035_prediction_metrics.csv
Normal file
@@ -0,0 +1,19 @@
|
||||
channel_kbps,policy,admitted_frames,prediction_rejected_frames,mean_absolute_error_ms,p95_absolute_error_ms,max_absolute_error_ms,published_after_deadline_frames,false_rejections
|
||||
300.0,no_drop,0,0,0.0,0.0,0.0,0,0
|
||||
300.0,reactive_1500ms,0,0,0.0,0.0,0.0,0,0
|
||||
300.0,latest_only,0,0,0.0,0.0,0.0,0,0
|
||||
300.0,two_waiting,0,0,0.0,0.0,0.0,0,0
|
||||
300.0,predict_1000ms,63,0,0.0,0.0,0.0,0,0
|
||||
300.0,predict_500ms,63,0,0.0,0.0,0.0,0,0
|
||||
260.0,no_drop,0,0,0.0,0.0,0.0,0,0
|
||||
260.0,reactive_1500ms,0,0,0.0,0.0,0.0,0,0
|
||||
260.0,latest_only,0,0,0.0,0.0,0.0,0,0
|
||||
260.0,two_waiting,0,0,0.0,0.0,0.0,0,0
|
||||
260.0,predict_1000ms,63,0,0.0,0.0,0.0,0,0
|
||||
260.0,predict_500ms,62,1,0.0,0.0,0.0,0,0
|
||||
230.0,no_drop,0,0,0.0,0.0,0.0,0,0
|
||||
230.0,reactive_1500ms,0,0,0.0,0.0,0.0,0,0
|
||||
230.0,latest_only,0,0,0.0,0.0,0.0,0,0
|
||||
230.0,two_waiting,0,0,0.0,0.0,0.0,0,0
|
||||
230.0,predict_1000ms,56,7,0.0,0.0,0.0,0,0
|
||||
230.0,predict_500ms,47,16,0.0,0.0,0.0,0,0
|
||||
|
BIN
data/processed/lab035/lab035_publication_delay.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
data/processed/lab035/lab035_queue_size.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
99
data/processed/lab035/lab035_report.txt
Normal file
@@ -0,0 +1,99 @@
|
||||
Lab035. Упреждающий допуск видеокадров и обслуживание видео целыми кадрами
|
||||
|
||||
1. Исходное состояние
|
||||
- Commit Lab034: b63e36abdb7031d642de8b8138b43cc29e94b759.
|
||||
- Перед Lab035 рабочее дерево было чистым; main опережала origin/main на два commit.
|
||||
|
||||
2. Правило обслуживания
|
||||
- После первого видеопакета кадр становится активным и не удаляется.
|
||||
- При повторном выборе видео передаётся следующий пакет активного кадра; команды и телеметрия могут передаваться между пакетами.
|
||||
- Новый видеокадр начинается только после полного завершения активного; видеопакеты разных кадров не чередуются; отдельный пакет не прерывается.
|
||||
- Только неактивные кадры могут быть удалены до передачи первого пакета.
|
||||
|
||||
3. Теоретическая пропускная способность
|
||||
speed | nonvideo kbps | remaining video kbps | remaining/aligned | minimum skip | maximum fps
|
||||
300 | 17.973 | 282.027 | 1.143443 | 0.000000 | 3.000
|
||||
260 | 17.973 | 242.027 | 0.981268 | 0.018732 | 2.944
|
||||
230 | 17.973 | 212.027 | 0.859636 | 0.140364 | 2.579
|
||||
|
||||
4. Восемнадцать сочетаний
|
||||
speed | policy | published/drop/partial | fps | age P95 ms | no-update max ms | queue max/waiting frames | waste bytes | prediction MAE/P95/max ms | control P95/max ms | emergency ms
|
||||
300 | Без удаления | 63/0/0 | 2.986 | 590.000 | 406.506 | 21/1 | 0 | 0.000000/0.000000/0.000000 | 17.466/17.787 | 1.707
|
||||
300 | Реактивная 1500 мс | 63/0/0 | 2.986 | 590.000 | 406.506 | 21/1 | 0 | 0.000000/0.000000/0.000000 | 17.466/17.787 | 1.707
|
||||
300 | Самый свежий | 63/0/0 | 2.986 | 590.000 | 406.506 | 21/1 | 0 | 0.000000/0.000000/0.000000 | 17.466/17.787 | 1.707
|
||||
300 | Два ожидающих | 63/0/0 | 2.986 | 590.000 | 406.506 | 21/1 | 0 | 0.000000/0.000000/0.000000 | 17.466/17.787 | 1.707
|
||||
300 | Прогноз 1000 мс | 63/0/0 | 2.986 | 590.000 | 406.506 | 21/1 | 0 | 0.000000/0.000000/0.000000 | 17.466/17.787 | 1.707
|
||||
300 | Прогноз 500 мс | 63/0/0 | 2.986 | 590.000 | 406.506 | 21/1 | 0 | 0.000000/0.000000/0.000000 | 17.466/17.787 | 1.707
|
||||
260 | Без удаления | 63/0/0 | 2.937 | 773.833 | 417.764 | 37/1 | 0 | 0.000000/0.000000/0.000000 | 18.908/20.492 | 1.969
|
||||
260 | Реактивная 1500 мс | 63/0/0 | 2.937 | 773.833 | 417.764 | 37/1 | 0 | 0.000000/0.000000/0.000000 | 18.908/20.492 | 1.969
|
||||
260 | Самый свежий | 63/0/0 | 2.937 | 773.833 | 417.764 | 37/1 | 0 | 0.000000/0.000000/0.000000 | 18.908/20.492 | 1.969
|
||||
260 | Два ожидающих | 63/0/0 | 2.937 | 773.833 | 417.764 | 37/1 | 0 | 0.000000/0.000000/0.000000 | 18.908/20.492 | 1.969
|
||||
260 | Прогноз 1000 мс | 63/0/0 | 2.937 | 773.833 | 417.764 | 37/1 | 0 | 0.000000/0.000000/0.000000 | 18.908/20.492 | 1.969
|
||||
260 | Прогноз 500 мс | 62/1/0 | 2.889 | 703.333 | 524.266 | 29/1 | 0 | 0.000000/0.000000/0.000000 | 18.908/20.533 | 1.969
|
||||
230 | Без удаления | 63/0/0 | 2.600 | 2913.333 | 424.000 | 160/8 | 0 | 0.000000/0.000000/0.000000 | 22.500/24.557 | 22.330
|
||||
230 | Реактивная 1500 мс | 27/0/36 | 1.300 | 11064.833 | 10626.980 | 87/4 | 321929 | 0.000000/0.000000/0.000000 | 22.461/24.557 | 22.330
|
||||
230 | Самый свежий | 56/7/0 | 2.600 | 960.000 | 426.226 | 37/1 | 0 | 0.000000/0.000000/0.000000 | 22.043/23.339 | 6.226
|
||||
230 | Два ожидающих | 57/6/0 | 2.600 | 1286.667 | 421.183 | 56/2 | 0 | 0.000000/0.000000/0.000000 | 22.539/23.339 | 6.991
|
||||
230 | Прогноз 1000 мс | 56/7/0 | 2.600 | 1226.667 | 426.226 | 53/2 | 0 | 0.000000/0.000000/0.000000 | 22.317/23.357 | 2.365
|
||||
230 | Прогноз 500 мс | 47/16/0 | 2.215 | 966.667 | 622.817 | 28/1 | 0 | 0.000000/0.000000/0.000000 | 22.212/23.304 | 5.739
|
||||
|
||||
5. Интерпретация
|
||||
- Реактивная Lab034 начинает кадр без гарантии завершения, затем удаляет остаток: уже переданные байты становятся бесполезными, а обновление не публикуется.
|
||||
- Удаление до первого пакета исключает бесполезную передачу; обслуживание целыми кадрами гарантирует, что начатый кадр будет опубликован.
|
||||
- Политика самого свежего уменьшает задержку ожидающих данных, но удаляет больше промежуточных кадров; очередь из двух кадров сохраняет больше последовательных обновлений ценой возраста.
|
||||
- Прогноз полного завершения учитывает весь размер кадра и будущую периодическую высокоприоритетную нагрузку, поэтому полезнее проверки только текущего возраста.
|
||||
- В модели точно известны команды 20 Гц, телеметрия 10 Гц и аварийная команда 10,0 с; неизвестные будущие дискретные события не моделируются и в реальной системе потребовали бы запаса.
|
||||
- При устойчивой перегрузке невозможно одновременно сохранить все кадры, исходное JPEG-качество и малую задержку; требуется уменьшить частоту, качество или заранее пропускать кадры.
|
||||
- Частота обновления, возраст изображения и длительность отсутствия нового изображения оцениваются одновременно: оптимизация одного показателя может ухудшить остальные.
|
||||
|
||||
6. Допущения
|
||||
- Ошибки и помехи отсутствуют; один общий абстрактный ресурс, форматы Lab028-Lab034 неизменны, активный пакет не прерывается.
|
||||
- Прогноз не изменяет настоящую очередь; для допущенных кадров сохраняются только агрегированные ошибки, без подробного журнала.
|
||||
- Политика автоматически не выбирается.
|
||||
|
||||
7. Функциональные проверки
|
||||
- PASS 01_video_frames_do_not_interleave: PASS
|
||||
- PASS 02_started_frame_never_dropped: PASS
|
||||
- PASS 03_high_priority_between_frame_packets: PASS
|
||||
- PASS 04_latest_drops_only_unstarted: PASS
|
||||
- PASS 05_two_waiting_limit: PASS
|
||||
- PASS 06_prediction_is_pure: PASS
|
||||
- PASS 07_prediction_uses_actual_sizes: PASS
|
||||
- PASS 08_prestart_drop_has_no_waste: PASS
|
||||
- PASS 09_partial_only_reactive: PASS
|
||||
- PASS 10_incomplete_not_published: PASS
|
||||
- PASS 11_crc_layers_pass: PASS
|
||||
- PASS 12_emergency_never_deleted: PASS
|
||||
- PASS 13_priority_above_video: PASS
|
||||
- PASS 14_300kbps_no_unnecessary_loss: PASS
|
||||
- PASS 15_230kbps_bounded_queue: PASS
|
||||
- PASS 16_frame_accounting: PASS
|
||||
- PASS 17_byte_accounting: PASS
|
||||
- PASS 18_reproducible: PASS
|
||||
- PASS 19_predict_1000_never_known_late: PASS
|
||||
- PASS 20_command_delay_bound: PASS
|
||||
|
||||
8. Созданные файлы
|
||||
- protocol/video_frame_scheduler.py
|
||||
- tests/lab035_video_frame_admission.py
|
||||
- data/processed/lab035/lab035_summary.csv
|
||||
- data/processed/lab035/lab035_video_metrics.csv
|
||||
- data/processed/lab035/lab035_control_metrics.csv
|
||||
- data/processed/lab035/lab035_prediction_metrics.csv
|
||||
- data/processed/lab035/lab035_report.txt
|
||||
- data/processed/lab035/lab035_update_rate.png
|
||||
- data/processed/lab035/lab035_image_age.png
|
||||
- data/processed/lab035/lab035_publication_delay.png
|
||||
- data/processed/lab035/lab035_frame_outcomes.png
|
||||
- data/processed/lab035/lab035_queue_size.png
|
||||
- data/processed/lab035/lab035_prediction_accuracy.png
|
||||
- data/processed/lab035/lab035_control_delay.png
|
||||
- data/processed/lab035/lab035_policy_comparison.png
|
||||
|
||||
9. Итоговый Git status
|
||||
- Lab035 не добавлена в индекс и не закоммичена.
|
||||
|
||||
## main...origin/main [ahead 2]
|
||||
?? data/processed/lab035/
|
||||
?? protocol/video_frame_scheduler.py
|
||||
?? tests/lab035_video_frame_admission.py
|
||||
19
data/processed/lab035/lab035_summary.csv
Normal file
@@ -0,0 +1,19 @@
|
||||
channel_kbps,policy,offered_load_kbps,offered_to_capacity_ratio,transmitted_packets,transmitted_bytes,dropped_before_start_packets,dropped_before_start_bytes,wasted_transmitted_bytes,mean_queue_packets,max_queue_packets,mean_queue_bytes,max_queue_bytes,mean_waiting_video_frames,max_waiting_video_frames,queue_at_source_end_packets,additional_drain_seconds,remaining_video_capacity_kbps,theoretical_minimum_skip_fraction,theoretical_maximum_update_fps
|
||||
300.0,no_drop,264.62022471910115,0.8820674157303372,1738,686910,0,0,0,8.518393226324118,21,4685.69717834665,11415,0.0043967897271271275,1,13,0.1994403333333281,282.02658105939,0.0,3.0
|
||||
300.0,reactive_1500ms,264.62022471910115,0.8820674157303372,1738,686910,0,0,0,8.518393226324118,21,4685.69717834665,11415,0.004397126805778977,1,13,0.1994403333333281,282.02658105939,0.0,3.0
|
||||
300.0,latest_only,264.62022471910115,0.8820674157303372,1738,686910,0,0,0,8.518393226324118,21,4685.69717834665,11415,0.0043967897271271275,1,13,0.1994403333333281,282.02658105939,0.0,3.0
|
||||
300.0,two_waiting,264.62022471910115,0.8820674157303372,1738,686910,0,0,0,8.518393226324118,21,4685.69717834665,11415,0.0043967897271271275,1,13,0.1994403333333281,282.02658105939,0.0,3.0
|
||||
300.0,predict_1000ms,264.62022471910115,0.8820674157303372,1738,686910,0,0,0,8.518393226324118,21,4685.69717834665,11415,0.0043967897271271275,1,13,0.1994403333333281,282.02658105939,0.0,3.0
|
||||
300.0,predict_500ms,264.62022471910115,0.8820674157303372,1738,686910,0,0,0,8.518393226324118,21,4685.69717834665,11415,0.0043967897271271275,1,13,0.1994403333333281,282.02658105939,0.0,3.0
|
||||
260.0,no_drop,264.62022471910115,1.017770095073466,1738,686910,0,0,0,13.695290291396285,37,7655.6639836816,21131,0.22409427213244032,1,30,0.5392926410258561,242.02658105939005,0.0187321490116944,2.943803552964917
|
||||
260.0,reactive_1500ms,264.62022471910115,1.017770095073466,1738,686910,0,0,0,13.695290291396285,37,7655.6639836816,21131,0.22409428818380472,1,30,0.5392926410258561,242.02658105939005,0.0187321490116944,2.943803552964917
|
||||
260.0,latest_only,264.62022471910115,1.017770095073466,1738,686910,0,0,0,13.695290291396285,37,7655.6639836816,21131,0.22409427213244032,1,30,0.5392926410258561,242.02658105939005,0.0187321490116944,2.943803552964917
|
||||
260.0,two_waiting,264.62022471910115,1.017770095073466,1738,686910,0,0,0,13.695290291396285,37,7655.6639836816,21131,0.22409427213244032,1,30,0.5392926410258561,242.02658105939005,0.0187321490116944,2.943803552964917
|
||||
260.0,predict_1000ms,264.62022471910115,1.017770095073466,1738,686910,0,0,0,13.695290291396285,37,7655.6639836816,21131,0.22409427213244032,1,30,0.5392926410258561,242.02658105939005,0.0187321490116944,2.943803552964917
|
||||
260.0,predict_500ms,264.62022471910115,1.017770095073466,1719,675994,19,10916,0,12.011204749970242,29,6682.897892161229,15804,0.13726051487841454,1,21,0.37186633333345753,242.02658105939005,0.0187321490116944,2.943803552964917
|
||||
230.0,no_drop,264.62022471910115,1.1505227161700051,1738,686910,0,0,0,76.17493190801534,160,43586.68565075366,92568,3.7476585944586778,8,155,3.1258550724636294,212.02658105939005,0.14036356404385353,2.5789093078684395
|
||||
230.0,reactive_1500ms,264.62022471910115,1.1505227161700051,1641,637811,0,0,321929,59.44779896084508,87,33911.67327808277,50035,2.785503524321179,4,81,1.4180637681158217,212.02658105939005,0.14036356404385353,2.5789093078684395
|
||||
230.0,latest_only,264.62022471910115,1.1505227161700051,1614,615407,124,71503,0,19.887121174537647,37,11167.829372157905,21067,0.5720064205456188,1,33,0.6387942028984455,212.02658105939005,0.14036356404385353,2.5789093078684395
|
||||
230.0,two_waiting,264.62022471910115,1.1505227161700051,1633,626345,105,60565,0,35.493155370922615,56,20128.047561843177,32209,1.456761532556227,2,52,1.0192463768114841,212.02658105939005,0.14036356404385353,2.5789093078684395
|
||||
230.0,predict_1000ms,264.62022471910115,1.1505227161700051,1612,613851,126,73059,0,32.84338218367999,53,18617.01722113549,30172,1.3064353409168972,2,49,0.5846724637680119,212.02658105939005,0.14036356404385353,2.5789093078684395
|
||||
230.0,predict_500ms,264.62022471910115,1.1505227161700051,1452,521373,286,165537,0,12.440944832856212,28,6904.035936733742,15168,0.22863162956241578,1,22,0.04693333333332461,212.02658105939005,0.14036356404385353,2.5789093078684395
|
||||
|
BIN
data/processed/lab035/lab035_update_rate.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
19
data/processed/lab035/lab035_video_metrics.csv
Normal file
@@ -0,0 +1,19 @@
|
||||
channel_kbps,policy,created_frames,started_frames,published_frames,dropped_before_start_frames,partially_transmitted_cancelled_frames,published_fraction,actual_update_fps,mean_publication_delay_ms,p95_publication_delay_ms,max_publication_delay_ms,mean_display_age_ms,p95_display_age_ms,max_display_age_ms,display_age_over_500ms_fraction,display_age_over_1000ms_fraction,mean_no_update_duration_ms,p95_no_update_duration_ms,max_no_update_duration_ms,mean_missing_run_frames,p95_missing_run_frames,max_missing_run_frames,mean_publication_gap_ms,max_publication_gap_ms,transmitted_video_bytes,dropped_before_start_video_bytes,wasted_transmitted_video_bytes,delivered_useful_video_kbps
|
||||
300.0,no_drop,63,63,63,0,0,1.0,2.985553772070626,272.2167195767169,293.13066666666225,301.84000000000003,433.2836060314408,589.9999999999999,623.3333333333348,0.30221366698748797,0.0,329.6296296296296,355.05666666666684,406.506333333331,0.0,0.0,0,333.07759016393436,406.506333333331,640254,0,0,157.76975922953451
|
||||
300.0,reactive_1500ms,63,63,63,0,0,1.0,2.985553772070626,272.2167195767169,293.13066666666225,301.84000000000003,433.2836060314408,589.9999999999999,623.3333333333348,0.30221366698748797,0.0,329.6296296296296,355.05666666666684,406.506333333331,0.0,0.0,0,333.03312365591387,406.506333333331,640254,0,0,157.76975922953451
|
||||
300.0,latest_only,63,63,63,0,0,1.0,2.985553772070626,272.2167195767169,293.13066666666225,301.84000000000003,433.2836060314408,589.9999999999999,623.3333333333348,0.30221366698748797,0.0,329.6296296296296,355.05666666666684,406.506333333331,0.0,0.0,0,333.07759016393436,406.506333333331,640254,0,0,157.76975922953451
|
||||
300.0,two_waiting,63,63,63,0,0,1.0,2.985553772070626,272.2167195767169,293.13066666666225,301.84000000000003,433.2836060314408,589.9999999999999,623.3333333333348,0.30221366698748797,0.0,329.6296296296296,355.05666666666684,406.506333333331,0.0,0.0,0,333.07759016393436,406.506333333331,640254,0,0,157.76975922953451
|
||||
300.0,predict_1000ms,63,63,63,0,0,1.0,2.985553772070626,272.2167195767169,293.13066666666225,301.84000000000003,433.2836060314408,589.9999999999999,623.3333333333348,0.30221366698748797,0.0,329.6296296296296,355.05666666666684,406.506333333331,0.0,0.0,0,333.07759016393436,406.506333333331,640254,0,0,157.76975922953451
|
||||
300.0,predict_500ms,63,63,63,0,0,1.0,2.985553772070626,272.2167195767169,293.13066666666225,301.84000000000003,433.2836060314408,589.9999999999999,623.3333333333348,0.30221366698748797,0.0,329.6296296296296,355.05666666666684,406.506333333331,0.0,0.0,0,333.07759016393436,406.506333333331,640254,0,0,157.76975922953451
|
||||
260.0,no_drop,63,63,63,0,0,1.0,2.937399678972713,393.259570207603,579.4598205129968,620.584948718161,546.5335258261151,773.833333333333,943.3333333333316,0.62223291626564,0.0,334.9462365591397,363.58000000001095,417.7637692307723,0.0,0.0,0,337.6377833333365,417.7637692307723,640254,0,0,157.76975922953451
|
||||
260.0,reactive_1500ms,63,63,63,0,0,1.0,2.937399678972713,393.259570207603,579.4598205129968,620.584948718161,546.5335258261151,773.833333333333,943.3333333333316,0.62223291626564,0.0,334.9462365591397,363.58000000001095,417.7637692307723,0.0,0.0,0,337.64599379652947,417.7637692307723,640254,0,0,157.76975922953451
|
||||
260.0,latest_only,63,63,63,0,0,1.0,2.937399678972713,393.259570207603,579.4598205129968,620.584948718161,546.5335258261151,773.833333333333,943.3333333333316,0.62223291626564,0.0,334.9462365591397,363.58000000001095,417.7637692307723,0.0,0.0,0,337.6377833333365,417.7637692307723,640254,0,0,157.76975922953451
|
||||
260.0,two_waiting,63,63,63,0,0,1.0,2.937399678972713,393.259570207603,579.4598205129968,620.584948718161,546.5335258261151,773.833333333333,943.3333333333316,0.62223291626564,0.0,334.9462365591397,363.58000000001095,417.7637692307723,0.0,0.0,0,337.6377833333365,417.7637692307723,640254,0,0,157.76975922953451
|
||||
260.0,predict_1000ms,63,63,63,0,0,1.0,2.937399678972713,393.259570207603,579.4598205129968,620.584948718161,546.5335258261151,773.833333333333,943.3333333333316,0.62223291626564,0.0,334.9462365591397,363.58000000001095,417.7637692307723,0.0,0.0,0,337.6377833333365,417.7637692307723,640254,0,0,157.76975922953451
|
||||
260.0,predict_500ms,63,62,62,1,0,0.9841269841269841,2.8892455858747996,360.2011848635393,447.1807256411333,465.7234102564871,523.434392043632,703.3333333333336,983.3333333333343,0.5654475457170356,0.0,340.4371584699453,365.69230769231353,524.2659999999332,1.0,1.0,1,340.28908604954535,524.2659999999332,629338,10916,0,155.0561797752809
|
||||
230.0,no_drop,63,63,63,0,0,1.0,2.6003210272873196,1777.4951000689596,3067.511884057848,3204.707246376671,1723.7872954764196,2913.333333333334,3206.666666666667,0.9735322425409048,0.8267564966313763,377.57575757575756,412.5426086956473,424.00000000000125,0.0,0.0,0,380.14569319113855,424.00000000000125,640254,0,0,157.76975922953451
|
||||
230.0,reactive_1500ms,63,63,27,0,36,0.42857142857142855,1.3001605136436598,1010.3600644122084,1414.757101449207,1473.0202898550094,4028.4071222329167,11064.833333333332,12103.333333333334,0.9735322425409048,0.8267564966313763,741.6666666666666,416.659130434783,10626.97971014499,36.0,36.0,36,374.5458193979908,424.00000000000125,591155,0,321929,66.39987158908507
|
||||
230.0,latest_only,63,56,56,7,0,0.8888888888888888,2.6003210272873196,536.8126293995401,685.881159420235,717.6463768114871,709.5107475136352,960.0000000000001,1076.6666666666679,0.9100096246390761,0.02165543792107796,377.5757575757575,416.6017391304294,426.22608695652355,1.0,1.0,1,381.4884331419178,426.22608695652355,568751,71503,0,140.1904333868379
|
||||
230.0,two_waiting,63,57,57,6,0,0.9047619047619048,2.6003210272873196,838.9013984235485,1026.1286956521203,1098.0985507245257,998.5707410972092,1286.6666666666665,1440.0000000000014,0.9735322425409048,0.546679499518768,377.5757575757576,406.0417391304327,421.1826086956538,1.0,1.0,1,381.49433962263976,421.1826086956538,579689,60565,0,142.8577849117175
|
||||
230.0,predict_1000ms,63,56,56,7,0,0.8888888888888888,2.6003210272873196,771.422153209065,952.4115942028234,971.5478260868871,943.7102983638115,1226.6666666666665,1359.9999999999995,0.9735322425409048,0.41193455245428295,377.57575757575756,405.373913043476,426.22608695652355,1.0,1.0,1,380.15028712058887,426.22608695652355,567195,73059,0,139.75743178170146
|
||||
230.0,predict_500ms,63,47,47,16,0,0.746031746031746,2.215088282504013,412.55253931544405,466.07430289855006,482.2141594203,641.0153994225219,966.6666666666686,1056.6666666666683,0.7776708373435997,0.026467757459095284,441.84397163120565,614.827726086956,622.8173913043555,1.0,1.0,1,444.19246376811583,622.8173913043555,474717,165537,0,117.06375601926163
|
||||
|
195
protocol/link_packet.py
Normal file
@@ -0,0 +1,195 @@
|
||||
"""Common Lab033 link packet shared by commands, telemetry, and video.
|
||||
|
||||
The fixed 32-byte header uses network byte order and no implicit padding::
|
||||
|
||||
!4sBBBBHIQHII
|
||||
|
||||
Offset Size Field
|
||||
0 4 magic (b"SLP1")
|
||||
4 1 version
|
||||
5 1 traffic_class
|
||||
6 1 direction
|
||||
7 1 flags
|
||||
8 2 stream_id
|
||||
10 4 sequence_number
|
||||
14 8 generation_time_us
|
||||
22 2 deadline_ms
|
||||
24 4 payload_length
|
||||
28 4 packet_crc32
|
||||
|
||||
``packet_crc32`` is IEEE CRC32 over the complete header with that field set
|
||||
to zero, followed by the complete payload.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
from enum import IntEnum
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
|
||||
MAGIC = b"SLP1"
|
||||
VERSION = 1
|
||||
HEADER_FORMAT = "!4sBBBBHIQHII"
|
||||
HEADER_SIZE = struct.calcsize(HEADER_FORMAT)
|
||||
SUPPORTED_FLAGS_MASK = 0
|
||||
|
||||
|
||||
class TrafficClass(IntEnum):
|
||||
"""Traffic classes in descending scheduling priority."""
|
||||
|
||||
EMERGENCY = 1
|
||||
CONTROL = 2
|
||||
TELEMETRY = 3
|
||||
VIDEO = 4
|
||||
|
||||
|
||||
class Direction(IntEnum):
|
||||
"""Logical direction of a packet on the shared modelled resource."""
|
||||
|
||||
GROUND_TO_ROVER = 1
|
||||
ROVER_TO_GROUND = 2
|
||||
|
||||
|
||||
class LinkPacketError(ValueError):
|
||||
"""Base class for malformed Lab033 link packets."""
|
||||
|
||||
|
||||
class LinkPacketCRCError(LinkPacketError):
|
||||
"""The Lab033 packet header or payload failed CRC32 validation."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LinkPacket:
|
||||
"""Decoded common packet, including its CRC-validated payload."""
|
||||
|
||||
traffic_class: TrafficClass
|
||||
direction: Direction
|
||||
stream_id: int
|
||||
sequence_number: int
|
||||
generation_time_us: int
|
||||
deadline_ms: int
|
||||
payload: bytes
|
||||
flags: int = 0
|
||||
version: int = VERSION
|
||||
packet_crc32: int = 0
|
||||
|
||||
|
||||
def crc32(data: bytes) -> int:
|
||||
"""Return unsigned IEEE CRC32."""
|
||||
|
||||
return zlib.crc32(data) & 0xFFFFFFFF
|
||||
|
||||
|
||||
def _validated(packet: LinkPacket) -> LinkPacket:
|
||||
if not isinstance(packet, LinkPacket):
|
||||
raise TypeError("packet must be LinkPacket")
|
||||
try:
|
||||
traffic_class = TrafficClass(packet.traffic_class)
|
||||
direction = Direction(packet.direction)
|
||||
except ValueError as error:
|
||||
raise LinkPacketError("unsupported enumeration value") from error
|
||||
if packet.version != VERSION:
|
||||
raise LinkPacketError("unsupported link packet version")
|
||||
if packet.flags & ~SUPPORTED_FLAGS_MASK:
|
||||
raise LinkPacketError("unsupported link packet flags")
|
||||
if not 0 <= packet.stream_id <= 0xFFFF:
|
||||
raise LinkPacketError("stream_id is outside uint16")
|
||||
if not 0 <= packet.sequence_number <= 0xFFFFFFFF:
|
||||
raise LinkPacketError("sequence_number is outside uint32")
|
||||
if not 0 <= packet.generation_time_us <= 0xFFFFFFFFFFFFFFFF:
|
||||
raise LinkPacketError("generation_time_us is outside uint64")
|
||||
if not 0 <= packet.deadline_ms <= 0xFFFF:
|
||||
raise LinkPacketError("deadline_ms is outside uint16")
|
||||
if not isinstance(packet.payload, (bytes, bytearray)):
|
||||
raise TypeError("payload must be bytes or bytearray")
|
||||
payload = bytes(packet.payload)
|
||||
if len(payload) > 0xFFFFFFFF:
|
||||
raise LinkPacketError("payload is outside uint32 length")
|
||||
return replace(
|
||||
packet,
|
||||
traffic_class=traffic_class,
|
||||
direction=direction,
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
|
||||
def _pack_header(packet: LinkPacket, crc_value: int) -> bytes:
|
||||
return struct.pack(
|
||||
HEADER_FORMAT,
|
||||
MAGIC,
|
||||
packet.version,
|
||||
int(packet.traffic_class),
|
||||
int(packet.direction),
|
||||
packet.flags,
|
||||
packet.stream_id,
|
||||
packet.sequence_number,
|
||||
packet.generation_time_us,
|
||||
packet.deadline_ms,
|
||||
len(packet.payload),
|
||||
crc_value,
|
||||
)
|
||||
|
||||
|
||||
def encode_link_packet(packet: LinkPacket) -> bytes:
|
||||
"""Serialize a common packet and calculate its CRC32."""
|
||||
|
||||
packet = _validated(packet)
|
||||
header_without_crc = _pack_header(packet, 0)
|
||||
packet_crc32 = crc32(header_without_crc + packet.payload)
|
||||
return _pack_header(packet, packet_crc32) + packet.payload
|
||||
|
||||
|
||||
def decode_link_packet(wire_packet: bytes) -> LinkPacket:
|
||||
"""Deserialize a common packet and validate its format and CRC32."""
|
||||
|
||||
if not isinstance(wire_packet, (bytes, bytearray)):
|
||||
raise TypeError("wire_packet must be bytes or bytearray")
|
||||
wire_packet = bytes(wire_packet)
|
||||
if len(wire_packet) < HEADER_SIZE:
|
||||
raise LinkPacketError("link packet is shorter than its header")
|
||||
(
|
||||
magic,
|
||||
version,
|
||||
traffic_class,
|
||||
direction,
|
||||
flags,
|
||||
stream_id,
|
||||
sequence_number,
|
||||
generation_time_us,
|
||||
deadline_ms,
|
||||
payload_length,
|
||||
received_crc32,
|
||||
) = struct.unpack(HEADER_FORMAT, wire_packet[:HEADER_SIZE])
|
||||
if magic != MAGIC:
|
||||
raise LinkPacketError("invalid link packet magic")
|
||||
if len(wire_packet) != HEADER_SIZE + payload_length:
|
||||
raise LinkPacketError("payload_length does not match packet length")
|
||||
try:
|
||||
packet = LinkPacket(
|
||||
traffic_class=TrafficClass(traffic_class),
|
||||
direction=Direction(direction),
|
||||
stream_id=stream_id,
|
||||
sequence_number=sequence_number,
|
||||
generation_time_us=generation_time_us,
|
||||
deadline_ms=deadline_ms,
|
||||
payload=wire_packet[HEADER_SIZE:],
|
||||
flags=flags,
|
||||
version=version,
|
||||
packet_crc32=received_crc32,
|
||||
)
|
||||
except ValueError as error:
|
||||
raise LinkPacketError("unsupported enumeration value") from error
|
||||
packet = _validated(packet)
|
||||
calculated_crc32 = crc32(_pack_header(packet, 0) + packet.payload)
|
||||
if calculated_crc32 != received_crc32:
|
||||
raise LinkPacketCRCError(
|
||||
"link packet CRC mismatch: "
|
||||
f"received 0x{received_crc32:08X}, "
|
||||
f"calculated 0x{calculated_crc32:08X}"
|
||||
)
|
||||
return packet
|
||||
|
||||
|
||||
assert HEADER_SIZE == 32
|
||||
222
protocol/priority_scheduler.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""Deterministic non-preemptive queue schedulers for Lab033."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Iterable
|
||||
|
||||
from protocol.link_packet import (
|
||||
HEADER_SIZE,
|
||||
LinkPacket,
|
||||
TrafficClass,
|
||||
encode_link_packet,
|
||||
)
|
||||
|
||||
|
||||
TIME_EPSILON_SECONDS = 1e-12
|
||||
|
||||
|
||||
class SchedulerMode(str, Enum):
|
||||
FIFO = "fifo"
|
||||
STRICT_PRIORITY = "strict_priority"
|
||||
LATEST_STATE = "latest_state"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class QueuedPacket:
|
||||
"""A packet plus a stable global arrival-order tie breaker."""
|
||||
|
||||
packet: LinkPacket
|
||||
arrival_order: int
|
||||
|
||||
@property
|
||||
def generation_time_seconds(self) -> float:
|
||||
return self.packet.generation_time_us / 1_000_000.0
|
||||
|
||||
@property
|
||||
def wire_size_bytes(self) -> int:
|
||||
return HEADER_SIZE + len(self.packet.payload)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Replacement:
|
||||
removed: QueuedPacket
|
||||
replacement: QueuedPacket
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ScheduledPacket:
|
||||
queued: QueuedPacket
|
||||
start_seconds: float
|
||||
end_seconds: float
|
||||
blocked_by: QueuedPacket | None
|
||||
blocking_delay_seconds: float
|
||||
wire_packet: bytes
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class QueueSample:
|
||||
time_seconds: float
|
||||
packets: int
|
||||
bytes: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ScheduleResult:
|
||||
mode: SchedulerMode
|
||||
channel_bitrate_bps: float
|
||||
transmitted: tuple[ScheduledPacket, ...]
|
||||
replacements: tuple[Replacement, ...]
|
||||
queue_samples: tuple[QueueSample, ...]
|
||||
|
||||
|
||||
def transmission_duration_seconds(
|
||||
packet_size_bytes: int,
|
||||
channel_bitrate_bps: float,
|
||||
) -> float:
|
||||
"""Return exact serialization duration for a complete packet."""
|
||||
|
||||
if packet_size_bytes < 0:
|
||||
raise ValueError("packet size must not be negative")
|
||||
if channel_bitrate_bps <= 0.0:
|
||||
raise ValueError("channel bitrate must be positive")
|
||||
return packet_size_bytes * 8.0 / channel_bitrate_bps
|
||||
|
||||
|
||||
def _selection_key(item: QueuedPacket, mode: SchedulerMode) -> tuple[int, int]:
|
||||
if mode is SchedulerMode.FIFO:
|
||||
return (0, item.arrival_order)
|
||||
return (int(item.packet.traffic_class), item.arrival_order)
|
||||
|
||||
|
||||
def _enqueue(
|
||||
ready: list[QueuedPacket],
|
||||
item: QueuedPacket,
|
||||
mode: SchedulerMode,
|
||||
replacements: list[Replacement],
|
||||
) -> None:
|
||||
if (
|
||||
mode is SchedulerMode.LATEST_STATE
|
||||
and item.packet.traffic_class
|
||||
in (TrafficClass.CONTROL, TrafficClass.TELEMETRY)
|
||||
):
|
||||
retained = []
|
||||
for old in ready:
|
||||
if (
|
||||
old.packet.traffic_class == item.packet.traffic_class
|
||||
and old.packet.stream_id == item.packet.stream_id
|
||||
):
|
||||
replacements.append(Replacement(old, item))
|
||||
else:
|
||||
retained.append(old)
|
||||
ready[:] = retained
|
||||
ready.append(item)
|
||||
|
||||
|
||||
def schedule_packets(
|
||||
packets: Iterable[QueuedPacket],
|
||||
mode: SchedulerMode,
|
||||
channel_bitrate_bps: float,
|
||||
) -> ScheduleResult:
|
||||
"""Serialize all packets through one non-preemptive shared resource.
|
||||
|
||||
The caller supplies a unique ``arrival_order``. Packets are admitted by
|
||||
``(generation_time, arrival_order)``. FIFO selects the smallest arrival
|
||||
order; priority modes select the smallest ``TrafficClass`` and preserve
|
||||
arrival order within a class. Latest-state replacement only touches items
|
||||
still waiting in ``ready`` and therefore can never remove an active packet.
|
||||
"""
|
||||
|
||||
try:
|
||||
mode = SchedulerMode(mode)
|
||||
except ValueError as error:
|
||||
raise ValueError("unsupported scheduler mode") from error
|
||||
if channel_bitrate_bps <= 0.0:
|
||||
raise ValueError("channel bitrate must be positive")
|
||||
arrivals = sorted(
|
||||
tuple(packets),
|
||||
key=lambda item: (
|
||||
item.generation_time_seconds,
|
||||
item.arrival_order,
|
||||
),
|
||||
)
|
||||
if len({item.arrival_order for item in arrivals}) != len(arrivals):
|
||||
raise ValueError("arrival_order values must be unique")
|
||||
|
||||
ready: list[QueuedPacket] = []
|
||||
transmitted: list[ScheduledPacket] = []
|
||||
replacements: list[Replacement] = []
|
||||
samples: list[QueueSample] = []
|
||||
blocker_by_order: dict[int, tuple[QueuedPacket, float]] = {}
|
||||
cursor = 0.0
|
||||
arrival_index = 0
|
||||
|
||||
def sample(time_seconds: float) -> None:
|
||||
samples.append(
|
||||
QueueSample(
|
||||
time_seconds=time_seconds,
|
||||
packets=len(ready),
|
||||
bytes=sum(item.wire_size_bytes for item in ready),
|
||||
)
|
||||
)
|
||||
|
||||
while arrival_index < len(arrivals) or ready:
|
||||
if not ready:
|
||||
cursor = max(cursor, arrivals[arrival_index].generation_time_seconds)
|
||||
while (
|
||||
arrival_index < len(arrivals)
|
||||
and arrivals[arrival_index].generation_time_seconds
|
||||
<= cursor + TIME_EPSILON_SECONDS
|
||||
):
|
||||
_enqueue(ready, arrivals[arrival_index], mode, replacements)
|
||||
arrival_index += 1
|
||||
sample(cursor)
|
||||
selected_index = min(
|
||||
range(len(ready)),
|
||||
key=lambda index: _selection_key(ready[index], mode),
|
||||
)
|
||||
selected = ready.pop(selected_index)
|
||||
start = max(cursor, selected.generation_time_seconds)
|
||||
wire_packet = encode_link_packet(selected.packet)
|
||||
end = start + transmission_duration_seconds(
|
||||
len(wire_packet), channel_bitrate_bps
|
||||
)
|
||||
|
||||
while (
|
||||
arrival_index < len(arrivals)
|
||||
and arrivals[arrival_index].generation_time_seconds
|
||||
<= end + TIME_EPSILON_SECONDS
|
||||
):
|
||||
arriving = arrivals[arrival_index]
|
||||
if arriving.generation_time_seconds > start + TIME_EPSILON_SECONDS:
|
||||
blocker_by_order[arriving.arrival_order] = (
|
||||
selected,
|
||||
max(0.0, end - arriving.generation_time_seconds),
|
||||
)
|
||||
_enqueue(ready, arriving, mode, replacements)
|
||||
arrival_index += 1
|
||||
|
||||
blocker, blocking_delay = blocker_by_order.get(
|
||||
selected.arrival_order, (None, 0.0)
|
||||
)
|
||||
transmitted.append(
|
||||
ScheduledPacket(
|
||||
queued=selected,
|
||||
start_seconds=start,
|
||||
end_seconds=end,
|
||||
blocked_by=blocker,
|
||||
blocking_delay_seconds=blocking_delay,
|
||||
wire_packet=wire_packet,
|
||||
)
|
||||
)
|
||||
cursor = end
|
||||
sample(cursor)
|
||||
|
||||
return ScheduleResult(
|
||||
mode=mode,
|
||||
channel_bitrate_bps=channel_bitrate_bps,
|
||||
transmitted=tuple(transmitted),
|
||||
replacements=tuple(replacements),
|
||||
queue_samples=tuple(samples),
|
||||
)
|
||||
224
protocol/video_age_policy.py
Normal file
@@ -0,0 +1,224 @@
|
||||
"""Frame-aware stale-video policy for the Lab034 shared link queue.
|
||||
|
||||
The scheduler remains strict-priority, state-replacing, and non-preemptive.
|
||||
Unlike Lab033, packet availability is separate from the timestamp stored in
|
||||
the common header. Every aligned video packet therefore carries the source
|
||||
composite-frame generation time while FEC construction metadata remains local
|
||||
to the scheduler.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable
|
||||
|
||||
from protocol.link_packet import HEADER_SIZE, LinkPacket, TrafficClass, encode_link_packet
|
||||
from protocol.priority_scheduler import transmission_duration_seconds
|
||||
|
||||
|
||||
TIME_EPSILON_SECONDS = 1e-12
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgePolicyPacket:
|
||||
"""A common packet plus non-serialized availability/frame metadata."""
|
||||
|
||||
packet: LinkPacket
|
||||
arrival_order: int
|
||||
available_time_us: int
|
||||
composite_frame_id: int | None = None
|
||||
|
||||
@property
|
||||
def available_time_seconds(self) -> float:
|
||||
return self.available_time_us / 1_000_000.0
|
||||
|
||||
@property
|
||||
def frame_generation_seconds(self) -> float:
|
||||
return self.packet.generation_time_us / 1_000_000.0
|
||||
|
||||
@property
|
||||
def wire_size_bytes(self) -> int:
|
||||
return HEADER_SIZE + len(self.packet.payload)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StateReplacement:
|
||||
removed: AgePolicyPacket
|
||||
replacement: AgePolicyPacket
|
||||
time_seconds: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DroppedVideoPacket:
|
||||
item: AgePolicyPacket
|
||||
drop_time_seconds: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgeScheduledPacket:
|
||||
item: AgePolicyPacket
|
||||
start_seconds: float
|
||||
end_seconds: float
|
||||
blocked_by: AgePolicyPacket | None
|
||||
blocking_delay_seconds: float
|
||||
wire_packet: bytes
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgeScheduleResult:
|
||||
channel_bitrate_bps: float
|
||||
max_video_age_seconds: float | None
|
||||
transmitted: tuple[AgeScheduledPacket, ...]
|
||||
dropped_video: tuple[DroppedVideoPacket, ...]
|
||||
replacements: tuple[StateReplacement, ...]
|
||||
|
||||
|
||||
def _enqueue(
|
||||
ready: list[AgePolicyPacket],
|
||||
item: AgePolicyPacket,
|
||||
replacements: list[StateReplacement],
|
||||
) -> None:
|
||||
if item.packet.traffic_class in (TrafficClass.CONTROL, TrafficClass.TELEMETRY):
|
||||
retained = []
|
||||
for old in ready:
|
||||
if (
|
||||
old.packet.traffic_class == item.packet.traffic_class
|
||||
and old.packet.stream_id == item.packet.stream_id
|
||||
):
|
||||
replacements.append(
|
||||
StateReplacement(old, item, item.available_time_seconds)
|
||||
)
|
||||
else:
|
||||
retained.append(old)
|
||||
ready[:] = retained
|
||||
ready.append(item)
|
||||
|
||||
|
||||
def schedule_with_video_age(
|
||||
packets: Iterable[AgePolicyPacket],
|
||||
channel_bitrate_bps: float,
|
||||
max_video_age_seconds: float | None,
|
||||
) -> AgeScheduleResult:
|
||||
"""Run a strict-priority non-preemptive scheduler with frame drops.
|
||||
|
||||
At every packet-selection instant all queued packets belonging to a video
|
||||
frame older than ``max_video_age_seconds`` are removed together. An active
|
||||
packet has already left the queue and is never interrupted.
|
||||
"""
|
||||
|
||||
if channel_bitrate_bps <= 0.0:
|
||||
raise ValueError("channel bitrate must be positive")
|
||||
if max_video_age_seconds is not None and max_video_age_seconds <= 0.0:
|
||||
raise ValueError("maximum video age must be positive")
|
||||
arrivals = sorted(
|
||||
tuple(packets),
|
||||
key=lambda item: (item.available_time_seconds, item.arrival_order),
|
||||
)
|
||||
if len({item.arrival_order for item in arrivals}) != len(arrivals):
|
||||
raise ValueError("arrival_order values must be unique")
|
||||
for item in arrivals:
|
||||
if item.packet.traffic_class is TrafficClass.VIDEO:
|
||||
if item.composite_frame_id is None:
|
||||
raise ValueError("video packet lacks composite_frame_id")
|
||||
elif item.composite_frame_id is not None:
|
||||
raise ValueError("non-video packet has composite_frame_id")
|
||||
|
||||
ready: list[AgePolicyPacket] = []
|
||||
transmitted: list[AgeScheduledPacket] = []
|
||||
dropped: list[DroppedVideoPacket] = []
|
||||
replacements: list[StateReplacement] = []
|
||||
blocker_by_order: dict[int, tuple[AgePolicyPacket, float]] = {}
|
||||
dropped_frames: set[int] = set()
|
||||
arrival_index = 0
|
||||
cursor = 0.0
|
||||
|
||||
def admit_until(limit: float, active: AgePolicyPacket | None = None, active_end: float = 0.0) -> None:
|
||||
nonlocal arrival_index
|
||||
while (
|
||||
arrival_index < len(arrivals)
|
||||
and arrivals[arrival_index].available_time_seconds
|
||||
<= limit + TIME_EPSILON_SECONDS
|
||||
):
|
||||
item = arrivals[arrival_index]
|
||||
arrival_index += 1
|
||||
if (
|
||||
active is not None
|
||||
and item.available_time_seconds > cursor + TIME_EPSILON_SECONDS
|
||||
):
|
||||
blocker_by_order[item.arrival_order] = (
|
||||
active,
|
||||
max(0.0, active_end - item.available_time_seconds),
|
||||
)
|
||||
if item.composite_frame_id in dropped_frames:
|
||||
dropped.append(DroppedVideoPacket(item, limit))
|
||||
else:
|
||||
_enqueue(ready, item, replacements)
|
||||
|
||||
def expire_video(now: float) -> None:
|
||||
if max_video_age_seconds is None:
|
||||
return
|
||||
expired_frames = {
|
||||
item.composite_frame_id
|
||||
for item in ready
|
||||
if (
|
||||
item.packet.traffic_class is TrafficClass.VIDEO
|
||||
and now - item.frame_generation_seconds
|
||||
> max_video_age_seconds + TIME_EPSILON_SECONDS
|
||||
)
|
||||
}
|
||||
if not expired_frames:
|
||||
return
|
||||
retained = []
|
||||
for item in ready:
|
||||
if item.composite_frame_id in expired_frames:
|
||||
dropped.append(DroppedVideoPacket(item, now))
|
||||
assert item.composite_frame_id is not None
|
||||
dropped_frames.add(item.composite_frame_id)
|
||||
else:
|
||||
retained.append(item)
|
||||
ready[:] = retained
|
||||
|
||||
while arrival_index < len(arrivals) or ready:
|
||||
if not ready:
|
||||
cursor = max(cursor, arrivals[arrival_index].available_time_seconds)
|
||||
admit_until(cursor)
|
||||
expire_video(cursor)
|
||||
if not ready:
|
||||
continue
|
||||
selected_index = min(
|
||||
range(len(ready)),
|
||||
key=lambda index: (
|
||||
int(ready[index].packet.traffic_class),
|
||||
ready[index].arrival_order,
|
||||
),
|
||||
)
|
||||
selected = ready.pop(selected_index)
|
||||
start = max(cursor, selected.available_time_seconds)
|
||||
wire_packet = encode_link_packet(selected.packet)
|
||||
end = start + transmission_duration_seconds(
|
||||
len(wire_packet), channel_bitrate_bps
|
||||
)
|
||||
cursor = start
|
||||
admit_until(end, selected, end)
|
||||
blocker, blocking_delay = blocker_by_order.get(
|
||||
selected.arrival_order, (None, 0.0)
|
||||
)
|
||||
transmitted.append(
|
||||
AgeScheduledPacket(
|
||||
item=selected,
|
||||
start_seconds=start,
|
||||
end_seconds=end,
|
||||
blocked_by=blocker,
|
||||
blocking_delay_seconds=blocking_delay,
|
||||
wire_packet=wire_packet,
|
||||
)
|
||||
)
|
||||
cursor = end
|
||||
|
||||
return AgeScheduleResult(
|
||||
channel_bitrate_bps=channel_bitrate_bps,
|
||||
max_video_age_seconds=max_video_age_seconds,
|
||||
transmitted=tuple(transmitted),
|
||||
dropped_video=tuple(dropped),
|
||||
replacements=tuple(replacements),
|
||||
)
|
||||
330
protocol/video_frame_scheduler.py
Normal file
@@ -0,0 +1,330 @@
|
||||
"""Whole-frame video scheduling and predictive admission for Lab035."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Iterable
|
||||
|
||||
from protocol.link_packet import TrafficClass, encode_link_packet
|
||||
from protocol.priority_scheduler import transmission_duration_seconds
|
||||
from protocol.video_age_policy import AgePolicyPacket
|
||||
|
||||
|
||||
TIME_EPSILON_SECONDS = 1e-12
|
||||
|
||||
|
||||
class FramePolicy(str, Enum):
|
||||
NO_DROP = "no_drop"
|
||||
LATEST_ONLY = "latest_only"
|
||||
TWO_WAITING = "two_waiting"
|
||||
PREDICT_1000MS = "predict_1000ms"
|
||||
PREDICT_500MS = "predict_500ms"
|
||||
|
||||
@property
|
||||
def deadline_seconds(self) -> float | None:
|
||||
if self is FramePolicy.PREDICT_1000MS:
|
||||
return 1.0
|
||||
if self is FramePolicy.PREDICT_500MS:
|
||||
return 0.5
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VideoFrameGroup:
|
||||
composite_frame_id: int
|
||||
generation_time_us: int
|
||||
packets: tuple[AgePolicyPacket, ...]
|
||||
|
||||
@property
|
||||
def generation_time_seconds(self) -> float:
|
||||
return self.generation_time_us / 1_000_000.0
|
||||
|
||||
@property
|
||||
def wire_size_bytes(self) -> int:
|
||||
return sum(packet.wire_size_bytes for packet in self.packets)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrameDrop:
|
||||
frame: VideoFrameGroup
|
||||
drop_time_seconds: float
|
||||
reason: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StateReplacement:
|
||||
removed: AgePolicyPacket
|
||||
replacement: AgePolicyPacket
|
||||
time_seconds: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrameScheduledPacket:
|
||||
item: AgePolicyPacket
|
||||
start_seconds: float
|
||||
end_seconds: float
|
||||
blocked_by: AgePolicyPacket | None
|
||||
blocking_delay_seconds: float
|
||||
wire_packet: bytes
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrameAdmission:
|
||||
composite_frame_id: int
|
||||
predicted_completion_seconds: float
|
||||
actual_completion_seconds: float
|
||||
|
||||
@property
|
||||
def prediction_error_seconds(self) -> float:
|
||||
return self.actual_completion_seconds - self.predicted_completion_seconds
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrameScheduleResult:
|
||||
policy: FramePolicy
|
||||
channel_bitrate_bps: float
|
||||
transmitted: tuple[FrameScheduledPacket, ...]
|
||||
dropped_frames: tuple[FrameDrop, ...]
|
||||
replacements: tuple[StateReplacement, ...]
|
||||
started_frame_ids: tuple[int, ...]
|
||||
completed_frame_ids: tuple[int, ...]
|
||||
admissions: tuple[FrameAdmission, ...]
|
||||
|
||||
|
||||
def _replace_state(
|
||||
ready: list[AgePolicyPacket],
|
||||
item: AgePolicyPacket,
|
||||
replacements: list[StateReplacement],
|
||||
) -> None:
|
||||
if item.packet.traffic_class in (TrafficClass.CONTROL, TrafficClass.TELEMETRY):
|
||||
retained = []
|
||||
for old in ready:
|
||||
if (
|
||||
old.packet.traffic_class == item.packet.traffic_class
|
||||
and old.packet.stream_id == item.packet.stream_id
|
||||
):
|
||||
replacements.append(StateReplacement(old, item, item.available_time_seconds))
|
||||
else:
|
||||
retained.append(old)
|
||||
ready[:] = retained
|
||||
ready.append(item)
|
||||
|
||||
|
||||
def _serve_high_priority(
|
||||
cursor: float,
|
||||
ready: list[AgePolicyPacket],
|
||||
future: tuple[AgePolicyPacket, ...],
|
||||
future_index: int,
|
||||
channel_bitrate_bps: float,
|
||||
) -> tuple[float, int, list[AgePolicyPacket]]:
|
||||
"""Pure local helper used only by the completion predictor."""
|
||||
|
||||
copied_ready = list(ready)
|
||||
index = future_index
|
||||
while index < len(future) and future[index].available_time_seconds <= cursor + TIME_EPSILON_SECONDS:
|
||||
item = future[index]
|
||||
index += 1
|
||||
copied_ready = [
|
||||
old for old in copied_ready
|
||||
if not (
|
||||
old.packet.traffic_class == item.packet.traffic_class
|
||||
and old.packet.stream_id == item.packet.stream_id
|
||||
and item.packet.traffic_class in (TrafficClass.CONTROL, TrafficClass.TELEMETRY)
|
||||
)
|
||||
]
|
||||
copied_ready.append(item)
|
||||
while copied_ready:
|
||||
selected = min(
|
||||
copied_ready,
|
||||
key=lambda item: (int(item.packet.traffic_class), item.arrival_order),
|
||||
)
|
||||
copied_ready.remove(selected)
|
||||
cursor += transmission_duration_seconds(selected.wire_size_bytes, channel_bitrate_bps)
|
||||
while index < len(future) and future[index].available_time_seconds <= cursor + TIME_EPSILON_SECONDS:
|
||||
item = future[index]
|
||||
index += 1
|
||||
copied_ready = [
|
||||
old for old in copied_ready
|
||||
if not (
|
||||
old.packet.traffic_class == item.packet.traffic_class
|
||||
and old.packet.stream_id == item.packet.stream_id
|
||||
and item.packet.traffic_class in (TrafficClass.CONTROL, TrafficClass.TELEMETRY)
|
||||
)
|
||||
]
|
||||
copied_ready.append(item)
|
||||
return cursor, index, copied_ready
|
||||
|
||||
|
||||
def predict_frame_completion(
|
||||
current_time_seconds: float,
|
||||
frame: VideoFrameGroup,
|
||||
channel_bitrate_bps: float,
|
||||
ready_high_priority: Iterable[AgePolicyPacket],
|
||||
future_high_priority: Iterable[AgePolicyPacket],
|
||||
active_packet_remaining_seconds: float = 0.0,
|
||||
) -> float:
|
||||
"""Predict full-frame completion without mutating any caller collection.
|
||||
|
||||
Known periodic commands, telemetry, and the scheduled emergency event are
|
||||
simulated exactly at packet boundaries. Unknown future discrete events are
|
||||
outside the Lab035 traffic model and therefore cannot be included.
|
||||
"""
|
||||
|
||||
if channel_bitrate_bps <= 0.0:
|
||||
raise ValueError("channel bitrate must be positive")
|
||||
if active_packet_remaining_seconds < 0.0:
|
||||
raise ValueError("active packet remainder must not be negative")
|
||||
cursor = current_time_seconds + active_packet_remaining_seconds
|
||||
ready = list(ready_high_priority)
|
||||
future = tuple(sorted(future_high_priority, key=lambda item: (item.available_time_seconds, item.arrival_order)))
|
||||
future_index = 0
|
||||
for video_packet in frame.packets:
|
||||
cursor, future_index, ready = _serve_high_priority(
|
||||
cursor, ready, future, future_index, channel_bitrate_bps
|
||||
)
|
||||
cursor += transmission_duration_seconds(video_packet.wire_size_bytes, channel_bitrate_bps)
|
||||
while future_index < len(future) and future[future_index].available_time_seconds <= cursor + TIME_EPSILON_SECONDS:
|
||||
ready.append(future[future_index])
|
||||
future_index += 1
|
||||
return cursor
|
||||
|
||||
|
||||
def schedule_video_frames(
|
||||
frames: Iterable[VideoFrameGroup],
|
||||
high_priority_packets: Iterable[AgePolicyPacket],
|
||||
policy: FramePolicy,
|
||||
channel_bitrate_bps: float,
|
||||
) -> FrameScheduleResult:
|
||||
"""Run strict-priority service while keeping video frames contiguous."""
|
||||
|
||||
policy = FramePolicy(policy)
|
||||
if channel_bitrate_bps <= 0.0:
|
||||
raise ValueError("channel bitrate must be positive")
|
||||
frame_arrivals = tuple(sorted(frames, key=lambda frame: (frame.generation_time_seconds, frame.composite_frame_id)))
|
||||
high_arrivals = tuple(sorted(high_priority_packets, key=lambda item: (item.available_time_seconds, item.arrival_order)))
|
||||
ready_high: list[AgePolicyPacket] = []
|
||||
pending_frames: list[VideoFrameGroup] = []
|
||||
transmitted: list[FrameScheduledPacket] = []
|
||||
dropped: list[FrameDrop] = []
|
||||
replacements: list[StateReplacement] = []
|
||||
started: list[int] = []
|
||||
completed: list[int] = []
|
||||
predicted_by_frame: dict[int, float] = {}
|
||||
actual_by_frame: dict[int, float] = {}
|
||||
active: VideoFrameGroup | None = None
|
||||
active_index = 0
|
||||
cursor = 0.0
|
||||
frame_index = high_index = 0
|
||||
blocker_by_order: dict[int, tuple[AgePolicyPacket, float]] = {}
|
||||
|
||||
def drop_frame(frame: VideoFrameGroup, reason: str) -> None:
|
||||
dropped.append(FrameDrop(frame, cursor, reason))
|
||||
|
||||
def admit(now: float, active_packet: AgePolicyPacket | None = None, active_end: float = 0.0) -> None:
|
||||
nonlocal frame_index, high_index
|
||||
while high_index < len(high_arrivals) and high_arrivals[high_index].available_time_seconds <= now + TIME_EPSILON_SECONDS:
|
||||
item = high_arrivals[high_index]
|
||||
high_index += 1
|
||||
if active_packet is not None and item.available_time_seconds > cursor + TIME_EPSILON_SECONDS:
|
||||
blocker_by_order[item.arrival_order] = (
|
||||
active_packet,
|
||||
max(0.0, active_end - item.available_time_seconds),
|
||||
)
|
||||
_replace_state(ready_high, item, replacements)
|
||||
while frame_index < len(frame_arrivals) and frame_arrivals[frame_index].generation_time_seconds <= now + TIME_EPSILON_SECONDS:
|
||||
frame = frame_arrivals[frame_index]
|
||||
frame_index += 1
|
||||
if policy is FramePolicy.LATEST_ONLY:
|
||||
for old in pending_frames:
|
||||
drop_frame(old, "replaced_by_newest")
|
||||
pending_frames[:] = [frame]
|
||||
elif policy is FramePolicy.TWO_WAITING:
|
||||
pending_frames.append(frame)
|
||||
while len(pending_frames) > 2:
|
||||
drop_frame(pending_frames.pop(0), "waiting_limit")
|
||||
else:
|
||||
pending_frames.append(frame)
|
||||
|
||||
while (
|
||||
frame_index < len(frame_arrivals)
|
||||
or high_index < len(high_arrivals)
|
||||
or ready_high
|
||||
or pending_frames
|
||||
or active is not None
|
||||
):
|
||||
if not ready_high and not pending_frames and active is None:
|
||||
next_times = []
|
||||
if frame_index < len(frame_arrivals):
|
||||
next_times.append(frame_arrivals[frame_index].generation_time_seconds)
|
||||
if high_index < len(high_arrivals):
|
||||
next_times.append(high_arrivals[high_index].available_time_seconds)
|
||||
cursor = max(cursor, min(next_times))
|
||||
admit(cursor)
|
||||
|
||||
selected: AgePolicyPacket | None = None
|
||||
if ready_high:
|
||||
selected = min(
|
||||
ready_high,
|
||||
key=lambda item: (int(item.packet.traffic_class), item.arrival_order),
|
||||
)
|
||||
ready_high.remove(selected)
|
||||
else:
|
||||
if active is None:
|
||||
while pending_frames and active is None:
|
||||
candidate = pending_frames.pop(0)
|
||||
deadline = policy.deadline_seconds
|
||||
if deadline is not None:
|
||||
future_high = high_arrivals[high_index:]
|
||||
predicted = predict_frame_completion(
|
||||
cursor,
|
||||
candidate,
|
||||
channel_bitrate_bps,
|
||||
tuple(ready_high),
|
||||
future_high,
|
||||
)
|
||||
if predicted - candidate.generation_time_seconds > deadline + TIME_EPSILON_SECONDS:
|
||||
drop_frame(candidate, "prediction_reject")
|
||||
continue
|
||||
predicted_by_frame[candidate.composite_frame_id] = predicted
|
||||
active = candidate
|
||||
active_index = 0
|
||||
started.append(active.composite_frame_id)
|
||||
if active is not None:
|
||||
selected = active.packets[active_index]
|
||||
|
||||
if selected is None:
|
||||
continue
|
||||
start = max(cursor, selected.available_time_seconds)
|
||||
wire_packet = encode_link_packet(selected.packet)
|
||||
end = start + transmission_duration_seconds(len(wire_packet), channel_bitrate_bps)
|
||||
cursor = start
|
||||
admit(end, selected, end)
|
||||
blocker, blocking_delay = blocker_by_order.get(selected.arrival_order, (None, 0.0))
|
||||
transmitted.append(
|
||||
FrameScheduledPacket(selected, start, end, blocker, blocking_delay, wire_packet)
|
||||
)
|
||||
cursor = end
|
||||
if selected.packet.traffic_class is TrafficClass.VIDEO:
|
||||
assert active is not None
|
||||
active_index += 1
|
||||
if active_index == len(active.packets):
|
||||
completed.append(active.composite_frame_id)
|
||||
actual_by_frame[active.composite_frame_id] = end
|
||||
active = None
|
||||
active_index = 0
|
||||
|
||||
admissions = tuple(
|
||||
FrameAdmission(frame_id, predicted, actual_by_frame[frame_id])
|
||||
for frame_id, predicted in sorted(predicted_by_frame.items())
|
||||
)
|
||||
return FrameScheduleResult(
|
||||
policy=policy,
|
||||
channel_bitrate_bps=channel_bitrate_bps,
|
||||
transmitted=tuple(transmitted),
|
||||
dropped_frames=tuple(dropped),
|
||||
replacements=tuple(replacements),
|
||||
started_frame_ids=tuple(started),
|
||||
completed_frame_ids=tuple(completed),
|
||||
admissions=admissions,
|
||||
)
|
||||
1043
tests/lab033_priority_channel_scheduler.py
Normal file
1020
tests/lab034_stale_video_drop.py
Normal file
748
tests/lab035_video_frame_admission.py
Normal file
@@ -0,0 +1,748 @@
|
||||
"""Lab035: predictive admission and whole-frame video service."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import Iterable
|
||||
|
||||
import cv2
|
||||
import matplotlib
|
||||
import numpy as np
|
||||
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from protocol.link_packet import HEADER_SIZE, TrafficClass, decode_link_packet
|
||||
from protocol.packet_erasure_fec import decode_fec_block, decode_outer_symbol
|
||||
from protocol.video_frame_scheduler import (
|
||||
FramePolicy,
|
||||
FrameScheduleResult,
|
||||
VideoFrameGroup,
|
||||
predict_frame_completion,
|
||||
schedule_video_frames,
|
||||
)
|
||||
from protocol.video_packet import CompositeReassembler, decode_packet as decode_inner_packet
|
||||
from tests.lab028_video_packetization import COMPOSITE_FPS
|
||||
from tests.lab034_stale_video_drop import (
|
||||
PolicyDefinition as Lab034Policy,
|
||||
build_aligned_workload,
|
||||
build_lab033_workload,
|
||||
percentile,
|
||||
simulate_aligned,
|
||||
)
|
||||
|
||||
|
||||
OUTPUT_DIRECTORY = Path("data/processed/lab035")
|
||||
SUMMARY_CSV_PATH = OUTPUT_DIRECTORY / "lab035_summary.csv"
|
||||
VIDEO_CSV_PATH = OUTPUT_DIRECTORY / "lab035_video_metrics.csv"
|
||||
CONTROL_CSV_PATH = OUTPUT_DIRECTORY / "lab035_control_metrics.csv"
|
||||
PREDICTION_CSV_PATH = OUTPUT_DIRECTORY / "lab035_prediction_metrics.csv"
|
||||
REPORT_PATH = OUTPUT_DIRECTORY / "lab035_report.txt"
|
||||
UPDATE_PLOT_PATH = OUTPUT_DIRECTORY / "lab035_update_rate.png"
|
||||
AGE_PLOT_PATH = OUTPUT_DIRECTORY / "lab035_image_age.png"
|
||||
PUBLICATION_PLOT_PATH = OUTPUT_DIRECTORY / "lab035_publication_delay.png"
|
||||
OUTCOME_PLOT_PATH = OUTPUT_DIRECTORY / "lab035_frame_outcomes.png"
|
||||
QUEUE_PLOT_PATH = OUTPUT_DIRECTORY / "lab035_queue_size.png"
|
||||
PREDICTION_PLOT_PATH = OUTPUT_DIRECTORY / "lab035_prediction_accuracy.png"
|
||||
CONTROL_PLOT_PATH = OUTPUT_DIRECTORY / "lab035_control_delay.png"
|
||||
COMPARISON_PLOT_PATH = OUTPUT_DIRECTORY / "lab035_policy_comparison.png"
|
||||
PLOT_PATHS = (
|
||||
UPDATE_PLOT_PATH,
|
||||
AGE_PLOT_PATH,
|
||||
PUBLICATION_PLOT_PATH,
|
||||
OUTCOME_PLOT_PATH,
|
||||
QUEUE_PLOT_PATH,
|
||||
PREDICTION_PLOT_PATH,
|
||||
CONTROL_PLOT_PATH,
|
||||
COMPARISON_PLOT_PATH,
|
||||
)
|
||||
LAB034_COMMIT = "b63e36abdb7031d642de8b8138b43cc29e94b759"
|
||||
CHANNEL_RATES_KBPS = (300.0, 260.0, 230.0)
|
||||
TIME_EPSILON_SECONDS = 1e-9
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PolicyDefinition:
|
||||
name: str
|
||||
label: str
|
||||
scheduler_policy: FramePolicy | None
|
||||
reactive: bool = False
|
||||
|
||||
|
||||
POLICIES = (
|
||||
PolicyDefinition("no_drop", "Без удаления", FramePolicy.NO_DROP),
|
||||
PolicyDefinition("reactive_1500ms", "Реактивная 1500 мс", None, True),
|
||||
PolicyDefinition("latest_only", "Самый свежий", FramePolicy.LATEST_ONLY),
|
||||
PolicyDefinition("two_waiting", "Два ожидающих", FramePolicy.TWO_WAITING),
|
||||
PolicyDefinition("predict_1000ms", "Прогноз 1000 мс", FramePolicy.PREDICT_1000MS),
|
||||
PolicyDefinition("predict_500ms", "Прогноз 500 мс", FramePolicy.PREDICT_500MS),
|
||||
)
|
||||
POLICY_BY_NAME = {policy.name: policy for policy in POLICIES}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TheoreticalCapacity:
|
||||
channel_kbps: float
|
||||
nonvideo_load_kbps: float
|
||||
remaining_video_kbps: float
|
||||
video_capacity_ratio: float
|
||||
minimum_skip_fraction: float
|
||||
maximum_update_fps: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SummaryMetrics:
|
||||
channel_kbps: float
|
||||
policy: str
|
||||
offered_load_kbps: float
|
||||
offered_to_capacity_ratio: float
|
||||
transmitted_packets: int
|
||||
transmitted_bytes: int
|
||||
dropped_before_start_packets: int
|
||||
dropped_before_start_bytes: int
|
||||
wasted_transmitted_bytes: int
|
||||
mean_queue_packets: float
|
||||
max_queue_packets: int
|
||||
mean_queue_bytes: float
|
||||
max_queue_bytes: int
|
||||
mean_waiting_video_frames: float
|
||||
max_waiting_video_frames: int
|
||||
queue_at_source_end_packets: int
|
||||
additional_drain_seconds: float
|
||||
remaining_video_capacity_kbps: float
|
||||
theoretical_minimum_skip_fraction: float
|
||||
theoretical_maximum_update_fps: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VideoMetrics:
|
||||
channel_kbps: float
|
||||
policy: str
|
||||
created_frames: int
|
||||
started_frames: int
|
||||
published_frames: int
|
||||
dropped_before_start_frames: int
|
||||
partially_transmitted_cancelled_frames: int
|
||||
published_fraction: float
|
||||
actual_update_fps: float
|
||||
mean_publication_delay_ms: float
|
||||
p95_publication_delay_ms: float
|
||||
max_publication_delay_ms: float
|
||||
mean_display_age_ms: float
|
||||
p95_display_age_ms: float
|
||||
max_display_age_ms: float
|
||||
display_age_over_500ms_fraction: float
|
||||
display_age_over_1000ms_fraction: float
|
||||
mean_no_update_duration_ms: float
|
||||
p95_no_update_duration_ms: float
|
||||
max_no_update_duration_ms: float
|
||||
mean_missing_run_frames: float
|
||||
p95_missing_run_frames: float
|
||||
max_missing_run_frames: int
|
||||
mean_publication_gap_ms: float
|
||||
max_publication_gap_ms: float
|
||||
transmitted_video_bytes: int
|
||||
dropped_before_start_video_bytes: int
|
||||
wasted_transmitted_video_bytes: int
|
||||
delivered_useful_video_kbps: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ControlMetrics:
|
||||
channel_kbps: float
|
||||
policy: str
|
||||
control_p95_delay_ms: float
|
||||
control_max_delay_ms: float
|
||||
control_deadline_misses: int
|
||||
control_max_receive_gap_ms: float
|
||||
emergency_delay_ms: float
|
||||
emergency_deadline_met: bool
|
||||
emergency_blocker_class: str
|
||||
emergency_blocking_delay_ms: float
|
||||
telemetry_deadline_misses: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PredictionMetrics:
|
||||
channel_kbps: float
|
||||
policy: str
|
||||
admitted_frames: int
|
||||
prediction_rejected_frames: int
|
||||
mean_absolute_error_ms: float
|
||||
p95_absolute_error_ms: float
|
||||
max_absolute_error_ms: float
|
||||
published_after_deadline_frames: int
|
||||
false_rejections: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ScenarioResult:
|
||||
summary: SummaryMetrics
|
||||
video: VideoMetrics
|
||||
control: ControlMetrics
|
||||
prediction: PredictionMetrics
|
||||
publication_times: dict[int, float]
|
||||
schedule: FrameScheduleResult | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FunctionalTestResult:
|
||||
name: str
|
||||
passed: bool
|
||||
detail: str
|
||||
|
||||
|
||||
def build_frame_groups(aligned) -> tuple[VideoFrameGroup, ...]:
|
||||
grouped: dict[int, list] = {}
|
||||
for item in aligned.packets:
|
||||
if item.composite_frame_id is not None:
|
||||
grouped.setdefault(item.composite_frame_id, []).append(item)
|
||||
return tuple(
|
||||
VideoFrameGroup(
|
||||
composite_frame_id=frame_id,
|
||||
generation_time_us=packets[0].packet.generation_time_us,
|
||||
packets=tuple(sorted(packets, key=lambda item: item.packet.sequence_number)),
|
||||
)
|
||||
for frame_id, packets in sorted(grouped.items())
|
||||
)
|
||||
|
||||
|
||||
def high_priority_packets(aligned) -> tuple:
|
||||
return tuple(
|
||||
item for item in aligned.packets
|
||||
if item.packet.traffic_class is not TrafficClass.VIDEO
|
||||
)
|
||||
|
||||
|
||||
def theoretical_capacity(aligned, rate: float) -> TheoreticalCapacity:
|
||||
duration = aligned.lab033.metadata.duration_seconds
|
||||
nonvideo_bytes = sum(
|
||||
item.wire_size_bytes for item in aligned.packets
|
||||
if item.packet.traffic_class is not TrafficClass.VIDEO
|
||||
)
|
||||
nonvideo_kbps = nonvideo_bytes * 8.0 / duration / 1000.0
|
||||
video_kbps = aligned.layout_metrics.after_link_kbps
|
||||
remaining = max(0.0, rate - nonvideo_kbps)
|
||||
ratio = remaining / video_kbps
|
||||
skip = max(0.0, 1.0 - ratio)
|
||||
return TheoreticalCapacity(
|
||||
rate,
|
||||
nonvideo_kbps,
|
||||
remaining,
|
||||
ratio,
|
||||
skip,
|
||||
COMPOSITE_FPS * min(1.0, ratio),
|
||||
)
|
||||
|
||||
|
||||
def receive_whole_frames(schedule: FrameScheduleResult) -> dict[int, float]:
|
||||
dropped = {item.frame.composite_frame_id for item in schedule.dropped_frames}
|
||||
receiver = CompositeReassembler()
|
||||
publication_times: dict[int, float] = {}
|
||||
symbols_by_block: dict[int, list[bytes]] = {}
|
||||
decoded_blocks: set[int] = set()
|
||||
for sent in schedule.transmitted:
|
||||
link = decode_link_packet(sent.wire_packet)
|
||||
if link.traffic_class is not TrafficClass.VIDEO:
|
||||
continue
|
||||
frame_id = sent.item.composite_frame_id
|
||||
assert frame_id is not None and frame_id not in dropped
|
||||
outer = decode_outer_symbol(link.payload)
|
||||
symbols = symbols_by_block.setdefault(outer.block_id, [])
|
||||
symbols.append(link.payload)
|
||||
if not outer.is_parity:
|
||||
completed = receiver.ingest(outer.data)
|
||||
if completed is not None:
|
||||
publication_times[completed.composite_frame_id] = sent.end_seconds
|
||||
if outer.block_id not in decoded_blocks and len(symbols) >= outer.source_count:
|
||||
decoded = decode_fec_block(tuple(symbols))
|
||||
for inner in decoded.source_packets:
|
||||
decode_inner_packet(inner)
|
||||
decoded_blocks.add(outer.block_id)
|
||||
if set(publication_times) != set(schedule.completed_frame_ids):
|
||||
raise AssertionError("published frames differ from completed whole frames")
|
||||
return publication_times
|
||||
|
||||
|
||||
def display_and_gap_metrics(publications: dict[int, float], source_end: float):
|
||||
events = sorted((time, frame) for frame, time in publications.items() if time <= source_end + TIME_EPSILON_SECONDS)
|
||||
samples = np.arange(0.0, source_end + 0.005, 0.01)
|
||||
ages = []
|
||||
index = 0
|
||||
last_frame = None
|
||||
for time in samples:
|
||||
while index < len(events) and events[index][0] <= time:
|
||||
last_frame = events[index][1]; index += 1
|
||||
generation = 0.0 if last_frame is None else last_frame / COMPOSITE_FPS
|
||||
ages.append(max(0.0, time - generation))
|
||||
update_times = [0.0] + [time for time, _ in events] + [source_end]
|
||||
no_update = tuple(max(0.0, right - left) for left, right in zip(update_times, update_times[1:]))
|
||||
publication_gaps = tuple(right[0] - left[0] for left, right in zip(events, events[1:]))
|
||||
return ages, no_update, publication_gaps
|
||||
|
||||
|
||||
def missing_runs(frame_count: int, published: set[int]) -> tuple[int, ...]:
|
||||
runs = []
|
||||
current = 0
|
||||
for frame_id in range(frame_count):
|
||||
if frame_id not in published:
|
||||
current += 1
|
||||
elif current:
|
||||
runs.append(current); current = 0
|
||||
if current:
|
||||
runs.append(current)
|
||||
return tuple(runs)
|
||||
|
||||
|
||||
def queue_metrics(schedule: FrameScheduleResult, frames, source_end: float):
|
||||
intervals = []
|
||||
first_start: dict[int, float] = {}
|
||||
for sent in schedule.transmitted:
|
||||
intervals.append((sent.item.available_time_seconds, sent.end_seconds, sent.item.wire_size_bytes))
|
||||
if sent.item.composite_frame_id is not None:
|
||||
first_start.setdefault(sent.item.composite_frame_id, sent.start_seconds)
|
||||
drop_time = {item.frame.composite_frame_id: item.drop_time_seconds for item in schedule.dropped_frames}
|
||||
for item in schedule.dropped_frames:
|
||||
for packet in item.frame.packets:
|
||||
intervals.append((packet.available_time_seconds, item.drop_time_seconds, packet.wire_size_bytes))
|
||||
for item in schedule.replacements:
|
||||
intervals.append((item.removed.available_time_seconds, item.time_seconds, item.removed.wire_size_bytes))
|
||||
packet_area = byte_area = 0.0
|
||||
events: dict[float, list[int]] = {}
|
||||
remaining = 0
|
||||
for start, end, size in intervals:
|
||||
if start <= source_end + TIME_EPSILON_SECONDS < end - TIME_EPSILON_SECONDS:
|
||||
remaining += 1
|
||||
left, right = max(0.0, start), min(source_end, end)
|
||||
if right <= left + TIME_EPSILON_SECONDS:
|
||||
continue
|
||||
packet_area += right - left; byte_area += (right - left) * size
|
||||
events.setdefault(left, [0, 0])[0] += 1; events[left][1] += size
|
||||
events.setdefault(right, [0, 0])[0] -= 1; events[right][1] -= size
|
||||
count = size_now = max_count = max_size = 0
|
||||
for time in sorted(events):
|
||||
count += events[time][0]; size_now += events[time][1]
|
||||
max_count = max(max_count, count); max_size = max(max_size, size_now)
|
||||
frame_intervals = []
|
||||
for frame in frames:
|
||||
end = first_start.get(frame.composite_frame_id, drop_time.get(frame.composite_frame_id, source_end))
|
||||
frame_intervals.append((frame.generation_time_seconds, end))
|
||||
frame_area = 0.0; frame_events: dict[float, int] = {}
|
||||
for start, end in frame_intervals:
|
||||
left, right = max(0.0, start), min(source_end, end)
|
||||
if right <= left + TIME_EPSILON_SECONDS: continue
|
||||
frame_area += right - left
|
||||
frame_events[left] = frame_events.get(left, 0) + 1
|
||||
frame_events[right] = frame_events.get(right, 0) - 1
|
||||
waiting = max_waiting = 0
|
||||
for time in sorted(frame_events):
|
||||
waiting += frame_events[time]; max_waiting = max(max_waiting, waiting)
|
||||
finish = max([source_end] + [item.end_seconds for item in schedule.transmitted] + [item.drop_time_seconds for item in schedule.dropped_frames])
|
||||
return (
|
||||
packet_area / source_end, max_count, byte_area / source_end, max_size,
|
||||
frame_area / source_end, max_waiting, remaining, max(0.0, finish - source_end),
|
||||
)
|
||||
|
||||
|
||||
def control_metrics(schedule: FrameScheduleResult, rate: float, policy: str) -> ControlMetrics:
|
||||
by_class = {
|
||||
traffic: [item for item in schedule.transmitted if item.item.packet.traffic_class is traffic]
|
||||
for traffic in (TrafficClass.CONTROL, TrafficClass.EMERGENCY, TrafficClass.TELEMETRY)
|
||||
}
|
||||
control = by_class[TrafficClass.CONTROL]
|
||||
control_delays = [item.end_seconds - item.item.packet.generation_time_us / 1_000_000.0 for item in control]
|
||||
gaps = [right.end_seconds - left.end_seconds for left, right in zip(control, control[1:])]
|
||||
telemetry_delays = [item.end_seconds - item.item.packet.generation_time_us / 1_000_000.0 for item in by_class[TrafficClass.TELEMETRY]]
|
||||
emergency = by_class[TrafficClass.EMERGENCY]
|
||||
if len(emergency) != 1: raise AssertionError("exactly one emergency command is required")
|
||||
urgent = emergency[0]
|
||||
urgent_delay = urgent.end_seconds - urgent.item.packet.generation_time_us / 1_000_000.0
|
||||
return ControlMetrics(
|
||||
rate, policy,
|
||||
percentile(control_delays, 95) * 1000.0,
|
||||
max(control_delays) * 1000.0,
|
||||
sum(delay > 0.1 + TIME_EPSILON_SECONDS for delay in control_delays),
|
||||
max(gaps, default=0.0) * 1000.0,
|
||||
urgent_delay * 1000.0,
|
||||
urgent_delay <= 0.05 + TIME_EPSILON_SECONDS,
|
||||
urgent.blocked_by.packet.traffic_class.name.lower() if urgent.blocked_by else "none",
|
||||
urgent.blocking_delay_seconds * 1000.0,
|
||||
sum(delay > 0.5 + TIME_EPSILON_SECONDS for delay in telemetry_delays),
|
||||
)
|
||||
|
||||
|
||||
def video_metrics(aligned, schedule, publications, rate, policy):
|
||||
source_end = aligned.lab033.metadata.duration_seconds
|
||||
published = set(publications)
|
||||
dropped = {item.frame.composite_frame_id for item in schedule.dropped_frames}
|
||||
delays = [publications[frame] - frame / COMPOSITE_FPS for frame in sorted(published)]
|
||||
ages, no_update, publication_gaps = display_and_gap_metrics(publications, source_end)
|
||||
runs = missing_runs(len(aligned.lab033.composites), published)
|
||||
transmitted_video_bytes = sum(
|
||||
len(item.wire_packet) for item in schedule.transmitted
|
||||
if item.item.packet.traffic_class is TrafficClass.VIDEO
|
||||
)
|
||||
dropped_bytes = sum(item.frame.wire_size_bytes for item in schedule.dropped_frames)
|
||||
useful = sum(
|
||||
len(aligned.lab033.composites[frame].base_jpeg) + len(aligned.lab033.composites[frame].roi_jpeg)
|
||||
for frame in published
|
||||
)
|
||||
return VideoMetrics(
|
||||
rate, policy, len(aligned.lab033.composites), len(schedule.started_frame_ids),
|
||||
len(published), len(dropped), 0, len(published) / len(aligned.lab033.composites),
|
||||
sum(time <= source_end + TIME_EPSILON_SECONDS for time in publications.values()) / source_end,
|
||||
float(np.mean(delays)) * 1000.0 if delays else 0.0,
|
||||
percentile(delays, 95) * 1000.0, max(delays, default=0.0) * 1000.0,
|
||||
float(np.mean(ages)) * 1000.0, percentile(ages, 95) * 1000.0,
|
||||
max(ages, default=0.0) * 1000.0,
|
||||
sum(age > 0.5 for age in ages) / len(ages),
|
||||
sum(age > 1.0 for age in ages) / len(ages),
|
||||
float(np.mean(no_update)) * 1000.0, percentile(no_update, 95) * 1000.0,
|
||||
max(no_update, default=0.0) * 1000.0,
|
||||
float(np.mean(runs)) if runs else 0.0, percentile(runs, 95), max(runs, default=0),
|
||||
float(np.mean(publication_gaps)) * 1000.0 if publication_gaps else 0.0,
|
||||
max(publication_gaps, default=0.0) * 1000.0,
|
||||
transmitted_video_bytes, dropped_bytes, 0,
|
||||
useful * 8.0 / source_end / 1000.0,
|
||||
)
|
||||
|
||||
|
||||
def prediction_metrics(schedule, rate, policy):
|
||||
errors = [abs(item.prediction_error_seconds) for item in schedule.admissions]
|
||||
deadline = schedule.policy.deadline_seconds
|
||||
rejected = [item for item in schedule.dropped_frames if item.reason == "prediction_reject"]
|
||||
late = sum(
|
||||
item.actual_completion_seconds - item.composite_frame_id / COMPOSITE_FPS
|
||||
> deadline + TIME_EPSILON_SECONDS
|
||||
for item in schedule.admissions
|
||||
) if deadline is not None else 0
|
||||
return PredictionMetrics(
|
||||
rate, policy, len(schedule.admissions), len(rejected),
|
||||
float(np.mean(errors)) * 1000.0 if errors else 0.0,
|
||||
percentile(errors, 95) * 1000.0, max(errors, default=0.0) * 1000.0,
|
||||
late, 0,
|
||||
)
|
||||
|
||||
|
||||
def whole_frame_result(aligned, frames, high, rate, policy_def):
|
||||
schedule = schedule_video_frames(frames, high, policy_def.scheduler_policy, rate * 1000.0)
|
||||
publications = receive_whole_frames(schedule)
|
||||
video = video_metrics(aligned, schedule, publications, rate, policy_def.name)
|
||||
control = control_metrics(schedule, rate, policy_def.name)
|
||||
prediction = prediction_metrics(schedule, rate, policy_def.name)
|
||||
source_end = aligned.lab033.metadata.duration_seconds
|
||||
qp, qmax, qb, qbmax, fq, fqmax, remaining, drain = queue_metrics(schedule, frames, source_end)
|
||||
capacity = theoretical_capacity(aligned, rate)
|
||||
offered_bytes = sum(item.wire_size_bytes for item in aligned.packets)
|
||||
summary = SummaryMetrics(
|
||||
rate, policy_def.name,
|
||||
offered_bytes * 8.0 / source_end / 1000.0,
|
||||
offered_bytes * 8.0 / source_end / (rate * 1000.0),
|
||||
len(schedule.transmitted), sum(len(item.wire_packet) for item in schedule.transmitted),
|
||||
sum(len(item.frame.packets) for item in schedule.dropped_frames),
|
||||
sum(item.frame.wire_size_bytes for item in schedule.dropped_frames), 0,
|
||||
qp, qmax, qb, qbmax, fq, fqmax, remaining, drain,
|
||||
capacity.remaining_video_kbps, capacity.minimum_skip_fraction,
|
||||
capacity.maximum_update_fps,
|
||||
)
|
||||
return ScenarioResult(summary, video, control, prediction, publications, schedule)
|
||||
|
||||
|
||||
def reactive_result(aligned, rate):
|
||||
old_policy = Lab034Policy("aligned_1500ms", "По кадрам, 1500 мс", "aligned", 1500)
|
||||
old = simulate_aligned(aligned, rate, old_policy)
|
||||
capacity = theoretical_capacity(aligned, rate)
|
||||
s, v, c = old.summary, old.video, old.control
|
||||
source_end = aligned.lab033.metadata.duration_seconds
|
||||
dropped_frames = set(old.dropped_frames)
|
||||
partial_frames = {
|
||||
frame_id for frame_id in dropped_frames
|
||||
if old.transmitted_video_by_frame.get(frame_id, 0) > 0
|
||||
}
|
||||
before_start_frames = dropped_frames - partial_frames
|
||||
dropped_before_packets = [
|
||||
item for item in old.schedule.dropped_video
|
||||
if item.item.composite_frame_id in before_start_frames
|
||||
]
|
||||
first_start = {}
|
||||
for item in old.schedule.transmitted:
|
||||
if item.item.composite_frame_id is not None:
|
||||
first_start.setdefault(item.item.composite_frame_id, item.start_seconds)
|
||||
frame_drop_time = {}
|
||||
for item in old.schedule.dropped_video:
|
||||
assert item.item.composite_frame_id is not None
|
||||
frame_drop_time.setdefault(item.item.composite_frame_id, item.drop_time_seconds)
|
||||
frame_events = {}
|
||||
frame_area = 0.0
|
||||
for frame_id in range(len(aligned.lab033.composites)):
|
||||
start = frame_id / COMPOSITE_FPS
|
||||
end = first_start.get(frame_id, frame_drop_time.get(frame_id, start))
|
||||
left, right = max(0.0, start), min(source_end, end)
|
||||
if right <= left + TIME_EPSILON_SECONDS:
|
||||
continue
|
||||
frame_area += right - left
|
||||
frame_events[left] = frame_events.get(left, 0) + 1
|
||||
frame_events[right] = frame_events.get(right, 0) - 1
|
||||
waiting = max_waiting = 0
|
||||
for time in sorted(frame_events):
|
||||
waiting += frame_events[time]
|
||||
max_waiting = max(max_waiting, waiting)
|
||||
publication_events = sorted(old.publication_times.values())
|
||||
publication_gaps = [
|
||||
right - left for left, right in zip(publication_events, publication_events[1:])
|
||||
]
|
||||
summary = SummaryMetrics(
|
||||
rate, "reactive_1500ms", s.offered_load_kbps, s.offered_to_capacity_ratio,
|
||||
s.transmitted_packets, s.transmitted_bytes,
|
||||
len(dropped_before_packets),
|
||||
sum(item.item.wire_size_bytes for item in dropped_before_packets),
|
||||
s.wasted_transmitted_bytes, s.mean_queue_packets, s.max_queue_packets,
|
||||
s.mean_queue_bytes, s.max_queue_bytes,
|
||||
frame_area / source_end, max_waiting,
|
||||
s.queue_at_source_end_packets, s.additional_drain_seconds,
|
||||
capacity.remaining_video_kbps, capacity.minimum_skip_fraction,
|
||||
capacity.maximum_update_fps,
|
||||
)
|
||||
video = VideoMetrics(
|
||||
rate, "reactive_1500ms", v.created_frames,
|
||||
v.published_frames + v.partially_transmitted_cancelled_frames,
|
||||
v.published_frames,
|
||||
v.intentionally_dropped_frames - v.partially_transmitted_cancelled_frames,
|
||||
v.partially_transmitted_cancelled_frames,
|
||||
v.published_fraction, v.actual_update_fps,
|
||||
v.mean_publication_delay_ms, v.p95_publication_delay_ms, v.max_publication_delay_ms,
|
||||
v.mean_display_age_ms, v.p95_display_age_ms, v.max_display_age_ms,
|
||||
v.display_age_over_500ms_fraction, v.display_age_over_1000ms_fraction,
|
||||
v.mean_no_update_duration_ms, v.p95_no_update_duration_ms, v.max_no_update_duration_ms,
|
||||
v.mean_missing_run_frames, v.p95_missing_run_frames, v.max_missing_run_frames,
|
||||
float(np.mean(publication_gaps)) * 1000.0 if publication_gaps else 0.0,
|
||||
max(publication_gaps, default=0.0) * 1000.0,
|
||||
sum(len(item.wire_packet) for item in old.schedule.transmitted if item.item.packet.traffic_class is TrafficClass.VIDEO),
|
||||
0, v.wasted_transmitted_video_bytes, v.delivered_useful_video_kbps,
|
||||
)
|
||||
control = ControlMetrics(
|
||||
rate, "reactive_1500ms", c.control_p95_age_ms, c.control_max_age_ms,
|
||||
c.control_deadline_misses, c.control_max_receive_gap_ms,
|
||||
c.emergency_total_delay_ms, c.emergency_deadline_met,
|
||||
c.emergency_blocker_class, c.emergency_blocking_delay_ms,
|
||||
c.telemetry_deadline_misses,
|
||||
)
|
||||
prediction = PredictionMetrics(rate, "reactive_1500ms", 0, 0, 0.0, 0.0, 0.0, 0, 0)
|
||||
return ScenarioResult(summary, video, control, prediction, old.publication_times, None)
|
||||
|
||||
|
||||
def run_experiment(aligned, frames, high):
|
||||
results = []
|
||||
for rate in CHANNEL_RATES_KBPS:
|
||||
for policy in POLICIES:
|
||||
results.append(
|
||||
reactive_result(aligned, rate)
|
||||
if policy.reactive
|
||||
else whole_frame_result(aligned, frames, high, rate, policy)
|
||||
)
|
||||
return tuple(results)
|
||||
|
||||
|
||||
def run_functional_tests(aligned, frames, high, results):
|
||||
lookup = {(r.summary.channel_kbps, r.summary.policy): r for r in results}
|
||||
checks = []
|
||||
def check(name):
|
||||
def decorator(function): checks.append((name, function)); return function
|
||||
return decorator
|
||||
|
||||
whole = [result for result in results if result.schedule is not None]
|
||||
|
||||
@check("01_video_frames_do_not_interleave")
|
||||
def _():
|
||||
for result in whole:
|
||||
sequence = [item.item.composite_frame_id for item in result.schedule.transmitted if item.item.composite_frame_id is not None]
|
||||
compressed = [frame for index, frame in enumerate(sequence) if index == 0 or frame != sequence[index - 1]]
|
||||
assert len(compressed) == len(set(compressed))
|
||||
|
||||
@check("02_started_frame_never_dropped")
|
||||
def _():
|
||||
for result in whole:
|
||||
assert not (set(result.schedule.started_frame_ids) & {item.frame.composite_frame_id for item in result.schedule.dropped_frames})
|
||||
|
||||
@check("03_high_priority_between_frame_packets")
|
||||
def _():
|
||||
assert any(
|
||||
any(item.item.packet.traffic_class is not TrafficClass.VIDEO for item in result.schedule.transmitted[left + 1:right])
|
||||
for result in whole
|
||||
for left, right in zip(
|
||||
[i for i, item in enumerate(result.schedule.transmitted) if item.item.composite_frame_id is not None][:-1],
|
||||
[i for i, item in enumerate(result.schedule.transmitted) if item.item.composite_frame_id is not None][1:],
|
||||
)
|
||||
if result.schedule.transmitted[left].item.composite_frame_id == result.schedule.transmitted[right].item.composite_frame_id
|
||||
)
|
||||
|
||||
@check("04_latest_drops_only_unstarted")
|
||||
def _():
|
||||
for rate in CHANNEL_RATES_KBPS:
|
||||
result = lookup[(rate, "latest_only")]
|
||||
assert not (set(result.schedule.started_frame_ids) & {item.frame.composite_frame_id for item in result.schedule.dropped_frames})
|
||||
|
||||
@check("05_two_waiting_limit")
|
||||
def _(): assert all(lookup[(rate, "two_waiting")].summary.max_waiting_video_frames <= 2 for rate in CHANNEL_RATES_KBPS)
|
||||
|
||||
@check("06_prediction_is_pure")
|
||||
def _():
|
||||
ready = list(high[:2]); future = list(high[2:20]); ready_before=list(ready); future_before=list(future)
|
||||
predict_frame_completion(0.0, frames[0], 230_000.0, ready, future)
|
||||
assert ready == ready_before and future == future_before
|
||||
|
||||
@check("07_prediction_uses_actual_sizes")
|
||||
def _():
|
||||
small = VideoFrameGroup(999, 0, (frames[0].packets[0],))
|
||||
full = predict_frame_completion(0.0, frames[0], 300_000.0, (), ())
|
||||
one = predict_frame_completion(0.0, small, 300_000.0, (), ())
|
||||
assert full > one and abs(one - small.wire_size_bytes * 8.0 / 300_000.0) < 1e-12
|
||||
|
||||
@check("08_prestart_drop_has_no_waste")
|
||||
def _(): assert all(result.video.wasted_transmitted_video_bytes == 0 for result in whole)
|
||||
|
||||
@check("09_partial_only_reactive")
|
||||
def _():
|
||||
assert all(result.video.partially_transmitted_cancelled_frames == 0 for result in whole)
|
||||
assert lookup[(230.0, "reactive_1500ms")].video.partially_transmitted_cancelled_frames > 0
|
||||
|
||||
@check("10_incomplete_not_published")
|
||||
def _():
|
||||
for result in whole:
|
||||
dropped = {item.frame.composite_frame_id for item in result.schedule.dropped_frames}
|
||||
assert not (dropped & set(result.publication_times))
|
||||
|
||||
@check("11_crc_layers_pass")
|
||||
def _(): assert all(result.video.published_frames == len(result.publication_times) for result in results)
|
||||
|
||||
@check("12_emergency_never_deleted")
|
||||
def _(): assert all(result.control.emergency_deadline_met for result in results)
|
||||
|
||||
@check("13_priority_above_video")
|
||||
def _(): assert all(result.control.control_deadline_misses == 0 and result.control.telemetry_deadline_misses == 0 for result in results)
|
||||
|
||||
@check("14_300kbps_no_unnecessary_loss")
|
||||
def _(): assert all(lookup[(300.0, policy.name)].video.published_frames == 63 for policy in POLICIES)
|
||||
|
||||
@check("15_230kbps_bounded_queue")
|
||||
def _():
|
||||
baseline = lookup[(230.0, "no_drop")].summary.max_queue_packets
|
||||
assert all(lookup[(230.0, name)].summary.max_queue_packets < baseline for name in ("latest_only", "two_waiting", "predict_1000ms", "predict_500ms"))
|
||||
|
||||
@check("16_frame_accounting")
|
||||
def _():
|
||||
for result in results:
|
||||
assert result.video.published_frames + result.video.dropped_before_start_frames + result.video.partially_transmitted_cancelled_frames == 63
|
||||
|
||||
@check("17_byte_accounting")
|
||||
def _():
|
||||
for result in whole:
|
||||
assert result.summary.transmitted_bytes == sum(len(item.wire_packet) for item in result.schedule.transmitted)
|
||||
assert result.summary.dropped_before_start_bytes == sum(item.frame.wire_size_bytes for item in result.schedule.dropped_frames)
|
||||
|
||||
@check("18_reproducible")
|
||||
def _():
|
||||
original = lookup[(230.0, "predict_1000ms")].schedule
|
||||
repeated = schedule_video_frames(frames, high, FramePolicy.PREDICT_1000MS, 230_000.0)
|
||||
assert [(x.item.arrival_order,x.start_seconds,x.end_seconds) for x in original.transmitted] == [(x.item.arrival_order,x.start_seconds,x.end_seconds) for x in repeated.transmitted]
|
||||
|
||||
@check("19_predict_1000_never_known_late")
|
||||
def _(): assert all(lookup[(rate, "predict_1000ms")].prediction.published_after_deadline_frames == 0 for rate in CHANNEL_RATES_KBPS)
|
||||
|
||||
@check("20_command_delay_bound")
|
||||
def _():
|
||||
lab033 = {}
|
||||
with Path("data/processed/lab033/lab033_summary.csv").open(encoding="utf-8") as file:
|
||||
for row in csv.DictReader(file):
|
||||
if row["scheduler"] == "latest_state": lab033[float(row["channel_kbps"])] = float(row["control_max_age_ms"])
|
||||
max_video_bytes = max(packet.wire_size_bytes for frame in frames for packet in frame.packets)
|
||||
for result in results:
|
||||
bound = lab033[result.summary.channel_kbps] + max_video_bytes * 8.0 / (result.summary.channel_kbps * 1000.0) * 1000.0
|
||||
assert result.control.control_max_delay_ms <= bound + 1e-9
|
||||
|
||||
output=[]
|
||||
for name,function in checks:
|
||||
try: function(); output.append(FunctionalTestResult(name,True,"PASS"))
|
||||
except Exception as error: output.append(FunctionalTestResult(name,False,f"{type(error).__name__}: {error}"))
|
||||
if not all(item.passed for item in output): raise AssertionError("functional checks failed: "+", ".join(item.name for item in output if not item.passed))
|
||||
return tuple(output)
|
||||
|
||||
|
||||
def save_csv(results):
|
||||
OUTPUT_DIRECTORY.mkdir(parents=True, exist_ok=True)
|
||||
for path, cls, rows in (
|
||||
(SUMMARY_CSV_PATH, SummaryMetrics, (r.summary for r in results)),
|
||||
(VIDEO_CSV_PATH, VideoMetrics, (r.video for r in results)),
|
||||
(CONTROL_CSV_PATH, ControlMetrics, (r.control for r in results)),
|
||||
(PREDICTION_CSV_PATH, PredictionMetrics, (r.prediction for r in results)),
|
||||
):
|
||||
with path.open("w",encoding="utf-8",newline="") as file:
|
||||
writer=csv.DictWriter(file,fieldnames=list(cls.__dataclass_fields__)); writer.writeheader(); writer.writerows(asdict(row) for row in rows)
|
||||
|
||||
|
||||
def grouped_plot(results,value,ylabel,title,path):
|
||||
x=np.arange(len(CHANNEL_RATES_KBPS)); width=.13
|
||||
fig,axis=plt.subplots(figsize=(12,5.5))
|
||||
for index,policy in enumerate(POLICIES):
|
||||
rows=[r for r in results if r.summary.policy==policy.name]
|
||||
axis.bar(x+(index-2.5)*width,[value(r) for r in rows],width,label=policy.label)
|
||||
axis.set_xticks(x,[f"{rate:.0f}" for rate in CHANNEL_RATES_KBPS]); axis.set_xlabel("Скорость, кбит/с"); axis.set_ylabel(ylabel); axis.set_title(title); axis.grid(axis="y",alpha=.3); axis.legend(fontsize=8); fig.tight_layout(); fig.savefig(path,dpi=150); plt.close(fig)
|
||||
|
||||
|
||||
def save_plots(results):
|
||||
grouped_plot(results,lambda r:r.video.actual_update_fps,"Обновлений/с","Фактическая частота обновления",UPDATE_PLOT_PATH)
|
||||
grouped_plot(results,lambda r:r.video.p95_display_age_ms,"P95 возраста, мс","Возраст отображаемого изображения",AGE_PLOT_PATH)
|
||||
grouped_plot(results,lambda r:r.video.p95_publication_delay_ms,"P95 задержки, мс","Задержка публикации",PUBLICATION_PLOT_PATH)
|
||||
grouped_plot(results,lambda r:r.video.published_frames,"Кадров","Опубликованные кадры",OUTCOME_PLOT_PATH)
|
||||
grouped_plot(results,lambda r:r.summary.max_queue_packets,"Пакетов","Максимальный размер очереди",QUEUE_PLOT_PATH)
|
||||
grouped_plot(results,lambda r:r.prediction.p95_absolute_error_ms,"P95 ошибки, мс","Точность прогноза",PREDICTION_PLOT_PATH)
|
||||
grouped_plot(results,lambda r:r.control.control_p95_delay_ms,"P95, мс","Задержка команд",CONTROL_PLOT_PATH)
|
||||
grouped_plot(results,lambda r:r.video.dropped_before_start_frames,"Кадров","Сравнение политик упреждающего удаления",COMPARISON_PLOT_PATH)
|
||||
|
||||
|
||||
def write_report(aligned,results,tests):
|
||||
git_status=subprocess.run(("git","status","--short","--branch"),check=True,capture_output=True,text=True,encoding="utf-8").stdout.rstrip()
|
||||
capacities={rate:theoretical_capacity(aligned,rate) for rate in CHANNEL_RATES_KBPS}
|
||||
lines=[
|
||||
"Lab035. Упреждающий допуск видеокадров и обслуживание видео целыми кадрами","",
|
||||
"1. Исходное состояние",f"- Commit Lab034: {LAB034_COMMIT}.","- Перед Lab035 рабочее дерево было чистым; main опережала origin/main на два commit.","",
|
||||
"2. Правило обслуживания","- После первого видеопакета кадр становится активным и не удаляется.","- При повторном выборе видео передаётся следующий пакет активного кадра; команды и телеметрия могут передаваться между пакетами.","- Новый видеокадр начинается только после полного завершения активного; видеопакеты разных кадров не чередуются; отдельный пакет не прерывается.","- Только неактивные кадры могут быть удалены до передачи первого пакета.","",
|
||||
"3. Теоретическая пропускная способность","speed | nonvideo kbps | remaining video kbps | remaining/aligned | minimum skip | maximum fps",
|
||||
]
|
||||
for rate in CHANNEL_RATES_KBPS:
|
||||
c=capacities[rate]; lines.append(f"{rate:.0f} | {c.nonvideo_load_kbps:.3f} | {c.remaining_video_kbps:.3f} | {c.video_capacity_ratio:.6f} | {c.minimum_skip_fraction:.6f} | {c.maximum_update_fps:.3f}")
|
||||
lines.extend(["","4. Восемнадцать сочетаний","speed | policy | published/drop/partial | fps | age P95 ms | no-update max ms | queue max/waiting frames | waste bytes | prediction MAE/P95/max ms | control P95/max ms | emergency ms"])
|
||||
for r in results:
|
||||
s,v,c,p=r.summary,r.video,r.control,r.prediction
|
||||
lines.append(f"{s.channel_kbps:.0f} | {POLICY_BY_NAME[s.policy].label} | {v.published_frames}/{v.dropped_before_start_frames}/{v.partially_transmitted_cancelled_frames} | {v.actual_update_fps:.3f} | {v.p95_display_age_ms:.3f} | {v.max_no_update_duration_ms:.3f} | {s.max_queue_packets}/{s.max_waiting_video_frames} | {v.wasted_transmitted_video_bytes} | {p.mean_absolute_error_ms:.6f}/{p.p95_absolute_error_ms:.6f}/{p.max_absolute_error_ms:.6f} | {c.control_p95_delay_ms:.3f}/{c.control_max_delay_ms:.3f} | {c.emergency_delay_ms:.3f}")
|
||||
lines.extend(["","5. Интерпретация","- Реактивная Lab034 начинает кадр без гарантии завершения, затем удаляет остаток: уже переданные байты становятся бесполезными, а обновление не публикуется.","- Удаление до первого пакета исключает бесполезную передачу; обслуживание целыми кадрами гарантирует, что начатый кадр будет опубликован.","- Политика самого свежего уменьшает задержку ожидающих данных, но удаляет больше промежуточных кадров; очередь из двух кадров сохраняет больше последовательных обновлений ценой возраста.","- Прогноз полного завершения учитывает весь размер кадра и будущую периодическую высокоприоритетную нагрузку, поэтому полезнее проверки только текущего возраста.","- В модели точно известны команды 20 Гц, телеметрия 10 Гц и аварийная команда 10,0 с; неизвестные будущие дискретные события не моделируются и в реальной системе потребовали бы запаса.","- При устойчивой перегрузке невозможно одновременно сохранить все кадры, исходное JPEG-качество и малую задержку; требуется уменьшить частоту, качество или заранее пропускать кадры.","- Частота обновления, возраст изображения и длительность отсутствия нового изображения оцениваются одновременно: оптимизация одного показателя может ухудшить остальные.","","6. Допущения","- Ошибки и помехи отсутствуют; один общий абстрактный ресурс, форматы Lab028-Lab034 неизменны, активный пакет не прерывается.","- Прогноз не изменяет настоящую очередь; для допущенных кадров сохраняются только агрегированные ошибки, без подробного журнала.","- Политика автоматически не выбирается.","","7. Функциональные проверки"])
|
||||
lines.extend(f"- {'PASS' if item.passed else 'FAIL'} {item.name}: {item.detail}" for item in tests)
|
||||
lines.extend(["","8. Созданные файлы"])
|
||||
lines.extend(f"- {path.as_posix()}" for path in (Path("protocol/video_frame_scheduler.py"),Path("tests/lab035_video_frame_admission.py"),SUMMARY_CSV_PATH,VIDEO_CSV_PATH,CONTROL_CSV_PATH,PREDICTION_CSV_PATH,REPORT_PATH,*PLOT_PATHS))
|
||||
lines.extend(["","9. Итоговый Git status","- Lab035 не добавлена в индекс и не закоммичена.","",git_status])
|
||||
REPORT_PATH.write_text("\n".join(lines)+"\n",encoding="utf-8")
|
||||
|
||||
|
||||
def validate_outputs():
|
||||
for path in (SUMMARY_CSV_PATH,VIDEO_CSV_PATH,CONTROL_CSV_PATH,PREDICTION_CSV_PATH):
|
||||
with path.open(encoding="utf-8",newline="") as file: rows=list(csv.DictReader(file))
|
||||
if len(rows)!=18: raise AssertionError(f"{path} must contain 18 rows")
|
||||
if "Lab035" not in REPORT_PATH.read_text(encoding="utf-8"): raise AssertionError("invalid report")
|
||||
for path in PLOT_PATHS:
|
||||
image=cv2.imread(str(path),cv2.IMREAD_UNCHANGED)
|
||||
if image is None or image.size==0: raise AssertionError(f"OpenCV could not read {path}")
|
||||
|
||||
|
||||
def main():
|
||||
lab033=build_lab033_workload(); aligned=build_aligned_workload(lab033)
|
||||
frames=build_frame_groups(aligned); high=high_priority_packets(aligned)
|
||||
results=run_experiment(aligned,frames,high)
|
||||
tests=run_functional_tests(aligned,frames,high,results)
|
||||
save_csv(results); save_plots(results); write_report(aligned,results,tests); validate_outputs()
|
||||
print(f"Lab035 complete: {len(results)} scenarios, {len(tests)} checks")
|
||||
|
||||
|
||||
if __name__=="__main__": main()
|
||||