Lab035: add proactive video frame admission

This commit is contained in:
LittleSam129
2026-08-03 15:42:16 +03:00
parent b63e36abdb
commit 22c2eeab8c
16 changed files with 1279 additions and 0 deletions

View File

@@ -213,3 +213,29 @@ git version 2.51.1.windows.1
- Подтверждено, что короткая очередь сама по себе не означает свежее изображение. - Подтверждено, что короткая очередь сама по себе не означает свежее изображение.
- Текущая реактивная политика не принята. - Текущая реактивная политика не принята.
- Следующий этап посвящён упреждающему допуску кадров и обслуживанию видео целыми кадрами. - Следующий этап посвящён упреждающему допуску кадров и обслуживанию видео целыми кадрами.
---
# Запись 010
## Дата
3 августа 2026 года
## Тема
Завершение Lab035: упреждающий допуск и обслуживание видео целыми составными кадрами.
## Выполнено
- Lab035 исследует упреждающий допуск и обслуживание видео целыми составными кадрами.
- Начатый кадр всегда передаётся полностью.
- Команды и телеметрия могут передаваться между его пакетами.
- Реактивное удаление Lab034 окончательно не принято.
- Основная политика — хранение только самого свежего не начатого кадра.
- При 230 кбит/с опубликовано 56 из 63 кадров с частотой около 2,6 кадра/с.
- Бесполезно переданных видеобайтов нет.
- Очередь из двух кадров увеличивает возраст изображения.
- Прогноз 1000 мс не дал преимущества над более простой политикой.
- Прогноз 500 мс чрезмерно снижает частоту обновления.
- Следующая лабораторная проверяет долговременную устойчивость и изменение пропускной способности канала.

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View 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
1 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
2 300.0 no_drop 17.46633333333314 17.78700000000022 0 65.7596666666671 1.7066666666671892 True none 0.0 0
3 300.0 reactive_1500ms 17.46633333333314 17.78700000000022 0 65.7596666666671 1.7066666666671892 True none 0.0 0
4 300.0 latest_only 17.46633333333314 17.78700000000022 0 65.7596666666671 1.7066666666671892 True none 0.0 0
5 300.0 two_waiting 17.46633333333314 17.78700000000022 0 65.7596666666671 1.7066666666671892 True none 0.0 0
6 300.0 predict_1000ms 17.46633333333314 17.78700000000022 0 65.7596666666671 1.7066666666671892 True none 0.0 0
7 300.0 predict_500ms 17.46633333333314 17.78700000000022 0 65.7596666666671 1.7066666666671892 True none 0.0 0
8 260.0 no_drop 18.907692307691043 20.492307692309808 0 67.81538461538617 1.969230769230279 True none 0.0 0
9 260.0 reactive_1500ms 18.907692307691043 20.492307692309808 0 67.81538461538617 1.969230769230279 True none 0.0 0
10 260.0 latest_only 18.907692307691043 20.492307692309808 0 67.81538461538617 1.969230769230279 True none 0.0 0
11 260.0 two_waiting 18.907692307691043 20.492307692309808 0 67.81538461538617 1.969230769230279 True none 0.0 0
12 260.0 predict_1000ms 18.907692307691043 20.492307692309808 0 67.81538461538617 1.969230769230279 True none 0.0 0
13 260.0 predict_500ms 18.907692307691043 20.533000000078516 0 67.81538461538617 1.969230769230279 True none 0.0 0
14 230.0 no_drop 22.499999999942013 24.556521739064863 0 69.00869565217427 22.330434782542596 True video 20.10434782602033 0
15 230.0 reactive_1500ms 22.460869565154475 24.556521739064863 0 69.00869565217427 22.330434782542596 True video 20.10434782602033 0
16 230.0 latest_only 22.043478260820315 23.339130434699484 0 69.00869565217427 6.2260869564561006 True video 3.9999999999338343 0
17 230.0 two_waiting 22.53913043472089 23.33913043474567 0 69.00869565217427 6.991304347760519 True video 4.765217391238252 0
18 230.0 predict_1000ms 22.317391304289558 23.35652173909253 0 69.00869565217427 2.365217391240293 True video 0.13913043471802666 0
19 230.0 predict_500ms 22.211510869569295 23.304347826089256 0 69.00869565217427 5.739130434786688 True video 3.5130434782644215 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View 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
1 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
2 300.0 no_drop 0 0 0.0 0.0 0.0 0 0
3 300.0 reactive_1500ms 0 0 0.0 0.0 0.0 0 0
4 300.0 latest_only 0 0 0.0 0.0 0.0 0 0
5 300.0 two_waiting 0 0 0.0 0.0 0.0 0 0
6 300.0 predict_1000ms 63 0 0.0 0.0 0.0 0 0
7 300.0 predict_500ms 63 0 0.0 0.0 0.0 0 0
8 260.0 no_drop 0 0 0.0 0.0 0.0 0 0
9 260.0 reactive_1500ms 0 0 0.0 0.0 0.0 0 0
10 260.0 latest_only 0 0 0.0 0.0 0.0 0 0
11 260.0 two_waiting 0 0 0.0 0.0 0.0 0 0
12 260.0 predict_1000ms 63 0 0.0 0.0 0.0 0 0
13 260.0 predict_500ms 62 1 0.0 0.0 0.0 0 0
14 230.0 no_drop 0 0 0.0 0.0 0.0 0 0
15 230.0 reactive_1500ms 0 0 0.0 0.0 0.0 0 0
16 230.0 latest_only 0 0 0.0 0.0 0.0 0 0
17 230.0 two_waiting 0 0 0.0 0.0 0.0 0 0
18 230.0 predict_1000ms 56 7 0.0 0.0 0.0 0 0
19 230.0 predict_500ms 47 16 0.0 0.0 0.0 0 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View 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

View 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
1 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
2 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
3 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
4 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
5 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
6 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
7 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
8 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
9 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
10 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
11 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
12 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
13 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
14 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
15 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
16 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
17 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
18 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
19 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View 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
1 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
2 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
3 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
4 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
5 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
6 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
7 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
8 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
9 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
10 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
11 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
12 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
13 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
14 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
15 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
16 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
17 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
18 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
19 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

View 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,
)

View 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()