diff --git a/.gitignore b/.gitignore index 6fab8a1..b9942d3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,8 @@ obj/ !**/[Pp]ackages/build/ ## Build results -artifacts/ +artifacts/* +!artifacts/discovery-capture/ [Dd]ebug/ [Rr]elease/ x64/ diff --git a/artifacts/discovery-capture/.gitignore b/artifacts/discovery-capture/.gitignore new file mode 100644 index 0000000..6c90716 --- /dev/null +++ b/artifacts/discovery-capture/.gitignore @@ -0,0 +1,14 @@ +# pcap と巨大一時ファイルは git 管理外 +*.pcap +*.pcapng +*.tsv +*.log +!host-tshark*.log +!android-tcpdump*.log +!README.md +# perf-runner build 出力と PID ファイル +build/ +*.pid +clock-*-pre.txt +!host-clock-pre.txt +android-clock-pre.txt diff --git a/artifacts/discovery-capture/20260627-231205/analysis-raw-best-effort.md b/artifacts/discovery-capture/20260627-231205/analysis-raw-best-effort.md new file mode 100644 index 0000000..b3a8430 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/analysis-raw-best-effort.md @@ -0,0 +1,44 @@ +# best-effort-8192 pcap 集計 (raw) + +計測日時: Sat Jun 27 11:29:52 PM JST 2026 +pcap: artifacts/discovery-capture/20260627-231205/host-best-effort_00001_20260627232823.pcap + +## Packet counts +- Total packets: 1925 +- RTPS packets: 1921 +- Android→Host (192.168.0.22): 1821 +- Host→Android (192.168.0.20): 100 + +## Android port distribution + 1800 12411 + 15 12410 + 5 12400 + 1 12401 + +## Host port distribution + 88 12410 + 12 12400 + +## helper 受信 (manifest.json から) +{ + "RunId": "20260627-142838", + "Backend": "il2cpp", + "BuildTarget": "Android", + "CaptureFrames": 200, + "Scenarios": [ + { + "Name": "unity-to-ros2-best-effort-8192", + "Direction": "unity_to_ros2", + "MetricsPath": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/metrics.ndjson", + "ProfilerPath": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/player.profiler.raw", + "PlayerLogPath": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/player.log", + "HelperStdoutPath": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/helper.stdout.ndjson", + "HelperStderrPath": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/helper.stderr.log", + "PlayerExitCode": 0, + "HelperExitCode": 3 + } + ] +} +### helper stdout +{"event":"ready","mode":"sub","topic":"/rosettadds_perf_unity_to_ros2_best_effort_8192"} +{"event":"done","received":162,"elapsed_ms":13262.6} diff --git a/artifacts/discovery-capture/20260627-231205/analysis-raw.md b/artifacts/discovery-capture/20260627-231205/analysis-raw.md new file mode 100644 index 0000000..3db071c --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/analysis-raw.md @@ -0,0 +1,35 @@ +# discovery-capture reliable-32 集計 raw データ + +## 計測メタ + +- pcap: `host-reliable_00001_20260627231808.pcap` +- 計測日時: 2026-06-27 23:18:08 +- android IP: 192.168.0.22 +- host IP: 192.168.0.20 + +## 集計結果 + +| 指標 | 値 | +|------|-----| +| Total packets (pcap 全パケット) | 607 | +| RTPS packets (rtps フィルタ) | 603 | +| RTPS, android→host (src 192.168.0.22) | 511 | +| RTPS, host→android (src 192.168.0.20) | 92 | + +合計 603 (511+92) が RTPS フィルタの 603 と一致する。 + +### Android src (192.168.0.22) → dport 分布 + +| dport | パケット数 | 備考 | +|-------|-----------|------| +| 12411 | 500 | SPDP ユニキャスト応答 | +| 12410 | 8 | SEDP 応答 | +| 12400 | 2 | SPDP マルチキャスト | +| 12401 | 1 | SEDP マルチキャスト | + +### Host src (192.168.0.20) → dport 分布 + +| dport | パケット数 | 備考 | +|-------|-----------|------| +| 12410 | 80 | SEDP 要求 | +| 12400 | 12 | SPDP アナウンス | diff --git a/artifacts/discovery-capture/20260627-231205/analysis.json b/artifacts/discovery-capture/20260627-231205/analysis.json new file mode 100644 index 0000000..dc01dc2 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/analysis.json @@ -0,0 +1,30 @@ +{ + "reliable": { + "pcap": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/host-reliable_00001_20260627231808.pcap", + "total_rtps": 603, + "ports": { + "12400": 14, + "12410": 88, + "12401": 1, + "12411": 500 + }, + "sources": { + "192.168.0.20": 92, + "192.168.0.22": 511 + } + }, + "best-effort": { + "pcap": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/host-best-effort_00001_20260627232823.pcap", + "total_rtps": 1921, + "ports": { + "12400": 17, + "12401": 1, + "12410": 103, + "12411": 1800 + }, + "sources": { + "192.168.0.20": 100, + "192.168.0.22": 1821 + } + } +} \ No newline at end of file diff --git a/artifacts/discovery-capture/20260627-231205/android-clock-pre.txt b/artifacts/discovery-capture/20260627-231205/android-clock-pre.txt new file mode 100644 index 0000000..cac1d04 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/android-clock-pre.txt @@ -0,0 +1 @@ +Android clock SKIPPED: root 不可 diff --git a/artifacts/discovery-capture/20260627-231205/android-reliable.pcap-missing b/artifacts/discovery-capture/20260627-231205/android-reliable.pcap-missing new file mode 100644 index 0000000..57347aa --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/android-reliable.pcap-missing @@ -0,0 +1 @@ +SKIPPED: root 不可 (Task 4) diff --git a/artifacts/discovery-capture/20260627-231205/android-tcpdump-be.log b/artifacts/discovery-capture/20260627-231205/android-tcpdump-be.log new file mode 100644 index 0000000..96c6327 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/android-tcpdump-be.log @@ -0,0 +1,2 @@ +Android tcpdump SKIPPED: root 不可 (Task 4) +Task 5 で IGMP 未参加確認済。host 側 capture のみで比較。 diff --git a/artifacts/discovery-capture/20260627-231205/android-tcpdump.log b/artifacts/discovery-capture/20260627-231205/android-tcpdump.log new file mode 100644 index 0000000..b5703d3 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/android-tcpdump.log @@ -0,0 +1,3 @@ +Android tcpdump SKIPPED: root 取得不可 (Task 4) +原因: ro.build.type=user + ro.debuggable=0 +代替: /proc/net/igmp で wlan0 の IGMP グループ参加状況を確認 (Task 4 済) diff --git a/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/manifest.json b/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/manifest.json new file mode 100644 index 0000000..72b7230 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/manifest.json @@ -0,0 +1,19 @@ +{ + "RunId": "20260627-142838", + "Backend": "il2cpp", + "BuildTarget": "Android", + "CaptureFrames": 200, + "Scenarios": [ + { + "Name": "unity-to-ros2-best-effort-8192", + "Direction": "unity_to_ros2", + "MetricsPath": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/metrics.ndjson", + "ProfilerPath": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/player.profiler.raw", + "PlayerLogPath": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/player.log", + "HelperStdoutPath": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/helper.stdout.ndjson", + "HelperStderrPath": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/helper.stderr.log", + "PlayerExitCode": 0, + "HelperExitCode": 3 + } + ] +} \ No newline at end of file diff --git a/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/helper.stdout.ndjson b/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/helper.stdout.ndjson new file mode 100644 index 0000000..0aaddf0 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/helper.stdout.ndjson @@ -0,0 +1,2 @@ +{"event":"ready","mode":"sub","topic":"/rosettadds_perf_unity_to_ros2_best_effort_8192"} +{"event":"done","received":162,"elapsed_ms":13262.6} diff --git a/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/metrics.ndjson b/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/metrics.ndjson new file mode 100644 index 0000000..48d3373 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/metrics.ndjson @@ -0,0 +1,8 @@ +{"event":"start","scenario":"unity-to-ros2-best-effort-8192","direction":"unity_to_ros2","qos":"best_effort","payload_bytes":8192,"messages":200} +{"event":"ready","scenario":"unity-to-ros2-best-effort-8192","direction":"unity_to_ros2","qos":"best_effort","payload_bytes":8192,"messages":200} +{"event":"matched","scenario":"unity-to-ros2-best-effort-8192","direction":"unity_to_ros2","qos":"best_effort","payload_bytes":8192,"messages":200} +{"event":"measure_start","scenario":"unity-to-ros2-best-effort-8192","direction":"unity_to_ros2","qos":"best_effort","payload_bytes":8192,"messages":200} +{"event":"measure_done","scenario":"unity-to-ros2-best-effort-8192","direction":"unity_to_ros2","qos":"best_effort","payload_bytes":8192,"messages":200,"main_thread_time_ns_available":true,"main_thread_time_ns_last":77000,"gc_used_memory_bytes_available":true,"gc_used_memory_bytes_last":7131136,"gc_allocated_in_frame_bytes_available":true,"gc_allocated_in_frame_bytes_last":163,"gc_allocated_in_frame_bytes_total":1671299,"gc_allocated_in_frame_bytes_samples":1110,"elapsed_ms":400.6254,"sent":200,"serialized_bytes_per_message":8201,"serialized_bytes":1640200,"messages_per_second":499.219470358095} +{"event":"waiting_for_release","scenario":"unity-to-ros2-best-effort-8192","direction":"unity_to_ros2","qos":"best_effort","payload_bytes":8192,"messages":200} +{"event":"released","scenario":"unity-to-ros2-best-effort-8192","direction":"unity_to_ros2","qos":"best_effort","payload_bytes":8192,"messages":200} +{"event":"done","scenario":"unity-to-ros2-best-effort-8192","direction":"unity_to_ros2","qos":"best_effort","payload_bytes":8192,"messages":200} diff --git a/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/player.profiler.raw b/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/player.profiler.raw new file mode 100644 index 0000000..f67857d Binary files /dev/null and b/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/player.profiler.raw differ diff --git a/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/player.release b/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/player.release new file mode 100644 index 0000000..30b81db --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/player.release @@ -0,0 +1 @@ +release \ No newline at end of file diff --git a/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/ready b/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/ready new file mode 100644 index 0000000..ad50b53 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/best-effort-8192/20260627-142838/unity-to-ros2-best-effort-8192/ready @@ -0,0 +1 @@ +ready \ No newline at end of file diff --git a/artifacts/discovery-capture/20260627-231205/env.sh b/artifacts/discovery-capture/20260627-231205/env.sh new file mode 100644 index 0000000..9d16c00 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/env.sh @@ -0,0 +1,2 @@ +RUN_ID=20260627-231205 +RUN_DIR=artifacts/discovery-capture/20260627-231205 diff --git a/artifacts/discovery-capture/20260627-231205/host-clock-pre.txt b/artifacts/discovery-capture/20260627-231205/host-clock-pre.txt new file mode 100644 index 0000000..4b5f554 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/host-clock-pre.txt @@ -0,0 +1 @@ +1782569899.354519804 diff --git a/artifacts/discovery-capture/20260627-231205/host-tshark-best-effort.log b/artifacts/discovery-capture/20260627-231205/host-tshark-best-effort.log new file mode 100644 index 0000000..1216650 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/host-tshark-best-effort.log @@ -0,0 +1,2 @@ +Capturing on 'wlp3s0' +1925 packets captured diff --git a/artifacts/discovery-capture/20260627-231205/host-tshark.log b/artifacts/discovery-capture/20260627-231205/host-tshark.log new file mode 100644 index 0000000..ef1925b --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/host-tshark.log @@ -0,0 +1,2 @@ +Capturing on 'wlp3s0' +607 packets captured diff --git a/artifacts/discovery-capture/20260627-231205/hypothesis-table.md b/artifacts/discovery-capture/20260627-231205/hypothesis-table.md new file mode 100644 index 0000000..83b3bd9 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/hypothesis-table.md @@ -0,0 +1,37 @@ +# 仮説判定 (2026-06-27) + +| 観点 | reliable-32 host | best-effort-8192 host | 解釈 | +|------|------------------|------------------------|------| +| total RTPS packets | 603 | 1921 | best-effort が 3.2×多い (fragmentation) | +| port 12400 (SPDP mc) count | 14 | 17 | 両方とも host が SPDP 送信、Android が応答 | +| port 12401 (user mc) count | 1 | 1 | best-effort は multicast 送信あり | +| port 12410 (SEDP) count | 88 | 103 | 両方とも SEDP 双方向あり | +| port 12411 (user unicast) count | 500 | 1800 | reliable は unicast のみ (no reader proxy)、best-effort は fragmentation 多数 | +| src 192.168.0.20 (host) count | 92 | 100 | host の SPDP/SEDP 送信 | +| src 192.168.0.22 (Android) count | 511 | 1821 | Android の応答 + user data | +| helper received | 0/200 (0%) | 162/200 (81%) | 両者とも helper 側で 100% 到達せず | +| Android→host 到着率 | 511/500 (102%) | 1821/200 (910%) | 全 packet が host に到達 | + +## 判定 + +### 仮説 1: Player 側 SPDP/SEDP 不出力 → **反証** +- Android (192.168.0.22) からの SPDP/SEDP 14+88 = 102 packet +- 2 scenarios とも Android は SPDP/SEDP を送信 + +### 仮説 2: Android kernel 経路問題 (IGMP / multicast routing) → **反証** +- Android からの全 multicast (port 12400, 12401) が host 側に到達 +- IGMP は /proc/net/igmp 上で未確認だが、host から見ると到達している +- 仮説 2 の kernel multicast routing 自体は成立している + +### 仮説 3: host 側 rmw_fastrtts_cpp parse 失敗 → **支持 (詳細不明)** +- reliable: 500 user data packet すべて host に到達、helper received 0 +- best-effort: 1800 user data fragment すべて host に到達、helper received 162/200 (81%) +- host 側 rmw_fastrtts_cpp が parse 段階で失敗/欠落している + +## 残存謎 + +1. **best-effort はなぜ 81% で止まるのか** (162/200 で残り 38 packet が欠落) +2. **reliable はなぜ 0% なのか** (500 packet すべて到達しているのに 1 件も処理されない) +3. **IGMP 未参加との関係**: Android kernel 上で wlan0 は 239.255.0.1 に未参加 + (/proc/net/igmp 確認)。host から見たマルチキャストは到達しているが、 + Android 側カーネルから見ると SPDP 受信経路が不明。 diff --git a/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/manifest.json b/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/manifest.json new file mode 100644 index 0000000..1c586e7 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/manifest.json @@ -0,0 +1,19 @@ +{ + "RunId": "20260627-141823", + "Backend": "il2cpp", + "BuildTarget": "Android", + "CaptureFrames": 200, + "Scenarios": [ + { + "Name": "unity-to-ros2-reliable-32", + "Direction": "unity_to_ros2", + "MetricsPath": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/metrics.ndjson", + "ProfilerPath": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/player.profiler.raw", + "PlayerLogPath": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/player.log", + "HelperStdoutPath": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/helper.stdout.ndjson", + "HelperStderrPath": "/home/ojii3/src/github.com/ojii3/ROSettaDDS/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/helper.stderr.log", + "PlayerExitCode": 0, + "HelperExitCode": 3 + } + ] +} \ No newline at end of file diff --git a/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/helper.stdout.ndjson b/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/helper.stdout.ndjson new file mode 100644 index 0000000..bcf5ed5 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/helper.stdout.ndjson @@ -0,0 +1,2 @@ +{"event":"ready","mode":"sub","topic":"/rosettadds_perf_unity_to_ros2_reliable_32"} +{"event":"done","received":0,"elapsed_ms":15000.4} diff --git a/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/metrics.ndjson b/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/metrics.ndjson new file mode 100644 index 0000000..0ae9755 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/metrics.ndjson @@ -0,0 +1,8 @@ +{"event":"start","scenario":"unity-to-ros2-reliable-32","direction":"unity_to_ros2","qos":"reliable","payload_bytes":32,"messages":500} +{"event":"ready","scenario":"unity-to-ros2-reliable-32","direction":"unity_to_ros2","qos":"reliable","payload_bytes":32,"messages":500} +{"event":"matched","scenario":"unity-to-ros2-reliable-32","direction":"unity_to_ros2","qos":"reliable","payload_bytes":32,"messages":500} +{"event":"measure_start","scenario":"unity-to-ros2-reliable-32","direction":"unity_to_ros2","qos":"reliable","payload_bytes":32,"messages":500} +{"event":"measure_done","scenario":"unity-to-ros2-reliable-32","direction":"unity_to_ros2","qos":"reliable","payload_bytes":32,"messages":500,"main_thread_time_ns_available":true,"main_thread_time_ns_last":89231,"gc_used_memory_bytes_available":true,"gc_used_memory_bytes_last":3010560,"gc_allocated_in_frame_bytes_available":true,"gc_allocated_in_frame_bytes_last":3352,"gc_allocated_in_frame_bytes_total":1769005,"gc_allocated_in_frame_bytes_samples":3677,"elapsed_ms":56.0762,"sent":500,"serialized_bytes_per_message":41,"serialized_bytes":20500,"messages_per_second":8916.43870305049} +{"event":"waiting_for_release","scenario":"unity-to-ros2-reliable-32","direction":"unity_to_ros2","qos":"reliable","payload_bytes":32,"messages":500} +{"event":"released","scenario":"unity-to-ros2-reliable-32","direction":"unity_to_ros2","qos":"reliable","payload_bytes":32,"messages":500} +{"event":"done","scenario":"unity-to-ros2-reliable-32","direction":"unity_to_ros2","qos":"reliable","payload_bytes":32,"messages":500} diff --git a/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/player.profiler.raw b/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/player.profiler.raw new file mode 100644 index 0000000..a43f788 Binary files /dev/null and b/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/player.profiler.raw differ diff --git a/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/player.release b/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/player.release new file mode 100644 index 0000000..30b81db --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/player.release @@ -0,0 +1 @@ +release \ No newline at end of file diff --git a/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/ready b/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/ready new file mode 100644 index 0000000..ad50b53 --- /dev/null +++ b/artifacts/discovery-capture/20260627-231205/reliable-32/20260627-141823/unity-to-ros2-reliable-32/ready @@ -0,0 +1 @@ +ready \ No newline at end of file diff --git a/artifacts/discovery-capture/pre-check.txt b/artifacts/discovery-capture/pre-check.txt new file mode 100644 index 0000000..a417b29 --- /dev/null +++ b/artifacts/discovery-capture/pre-check.txt @@ -0,0 +1,6 @@ +device: 5HF6OVWCDECMJZ59 +root: not-available (su not found) +tcpdump: n/a (no root) +igmp_239.255.0.1: not-checked (no root) +host_tshark: TShark (Wireshark) 4.6.5 (Git commit b40c46f83867). +host_tcpdump: tcpdump version 4.99.6 diff --git a/docs/superpowers/plans/2026-06-27-discovery-capture-investigation.md b/docs/superpowers/plans/2026-06-27-discovery-capture-investigation.md new file mode 100644 index 0000000..1c0b658 --- /dev/null +++ b/docs/superpowers/plans/2026-06-27-discovery-capture-investigation.md @@ -0,0 +1,978 @@ +# Reliable Discovery Capture Investigation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Sony XIG04 (Xperia 10 III / Android 15) 上の Unity Player (rosettadds 独自 RTPS) と helper (rmw_fastrtts_cpp) 間の reliable QoS discovery (SPDP/SEDP) 不通 (findings B) の根本原因を、host 側 tshark + Android 側 tcpdump のマルチキャストパケットキャプチャで特定する。 + +**Architecture:** 既存 perf-runner を `--scenario unity-to-ros2-reliable-32` と `--scenario unity-to-ros2-best-effort-8192` (control) で 2 回計測し、計測中に host 側 wlan0 と Android 側 wlan0 の両方でマルチキャストキャプチャ (UDP 7400-12500) を取得する。pcap を tshark + Python で解析し、port 別 / src IP 別 / 5 秒 window 別の packet count + RTPS submessage 内訳を抽出。3 仮説 (Player 側 SPDP/SEDP 不出力 / Android kernel 経路問題 / host 側 rmw_fastrtts_cpp parse 失敗) の支持 / 反証データを整理し、findings doc として 1 commit + 1 PR で main にマージする。 + +**Tech Stack:** tshark 4.x / tcpdump 4.99.6 / Python 3.11+ (pandas) / nix devShell の既存 `~/.nix-profile/bin/{tshark,tcpdump}` / `tools/rosettadds-perf-runner` (既存) / adb shell su / Sony XIG04 (Xperia 10 III) USB 接続 + +**Design doc:** `docs/superpowers/specs/2026-06-27-discovery-capture-investigation-design.md` + +--- + +## 変更 / 作成ファイル一覧 + +| 操作 | パス | 役割 | +| --- | --- | --- | +| Create | `artifacts/discovery-capture/.gitignore` | pcap / 一時ファイルを git 管理外 | +| Create | `artifacts/discovery-capture//analyze.py` | tshark で 4 pcap を CSV 化、port 別 / src 別集計 | +| Create | `artifacts/discovery-capture//README.md` | 各 runId の計測手順サマリ | +| Create | `tools/discovery-capture/discovery_capture.py` | 両側 capture 起動 + clock sync + pcap pull ヘルパ | +| Create | `tools/discovery-capture/tests/test_discovery_capture.py` | discovery_capture.py の CLI フラグ parse テスト (xUnit / pytest) | +| Modify | `rosettadds.sln` | test project を追加 (任意) | +| Create | `docs/superpowers/specs/2026-06-27-discovery-capture-investigation.md` | findings doc (本 PR の主成果物) | + +Unity / RTPS / DDS のコードは触らない。perf-runner も無修正。 + +--- + +## Task 1: 計測ディレクトリ + .gitignore 準備 + +**Files:** +- Create: `artifacts/discovery-capture/.gitignore` + +- [ ] **Step 1.1: 計測ディレクトリ作成** + +```bash +mkdir -p artifacts/discovery-capture +``` + +期待: ディレクトリが作成され、書き込み可能。 + +- [ ] **Step 1.2: .gitignore 作成** + +`artifacts/discovery-capture/.gitignore` を新規作成し、以下を記述する: + +``` +# pcap と巨大一時ファイルは git 管理外 +*.pcap +*.pcapng +*.tsv +*.log +!README.md +``` + +- [ ] **Step 1.3: 動作確認** + +```bash +touch artifacts/discovery-capture/test.pcap +git check-ignore -v artifacts/discovery-capture/test.pcap +``` + +期待: `artifacts/discovery-capture/.gitignore:3:*.pcap` が表示される (無視される)。 + +- [ ] **Step 1.4: 動作確認の cleanup** + +```bash +rm artifacts/discovery-capture/test.pcap +``` + +- [ ] **Step 1.5: commit** + +```bash +git add artifacts/discovery-capture/.gitignore +git commit -m "chore: discovery-capture 計測ディレクトリ + .gitignore 追加" +``` + +--- + +## Task 2: capture ヘルパ CLI フラグ parse テスト (TDD) + +**Files:** +- Create: `tools/discovery-capture/tests/test_discovery_capture.py` +- Create: `tools/discovery-capture/discovery_capture.py` + +- [ ] **Step 2.1: test project ディレクトリ作成** + +```bash +mkdir -p tools/discovery-capture/tests +``` + +- [ ] **Step 2.2: pytest テスト作成 (失敗する状態)** + +`tools/discovery-capture/tests/test_discovery_capture.py` を新規作成し、以下を記述する: + +```python +import pytest +from discovery_capture import parse_args + + +def test_parse_args_reliable_scenario(): + args = parse_args([ + "--host-interface", "wlan0", + "--android-device", "5HF6OVWCDECMJZ59", + "--scenario", "unity-to-ros2-reliable-32", + "--run-id", "20260627-180000", + ]) + assert args.host_interface == "wlan0" + assert args.android_device == "5HF6OVWCDECMJZ59" + assert args.scenario == "unity-to-ros2-reliable-32" + assert args.run_id == "20260627-180000" + assert args.udp_portrange == "7400-12500" # default + + +def test_parse_args_custom_portrange(): + args = parse_args([ + "--host-interface", "wlan0", + "--android-device", "5HF6OVWCDECMJZ59", + "--scenario", "unity-to-ros2-reliable-32", + "--run-id", "20260627-180000", + "--udp-portrange", "7400-7500", + ]) + assert args.udp_portrange == "7400-7500" + + +def test_parse_args_missing_android_device(): + with pytest.raises(SystemExit): + parse_args([ + "--host-interface", "wlan0", + "--scenario", "unity-to-ros2-reliable-32", + "--run-id", "20260627-180000", + ]) +``` + +- [ ] **Step 2.3: テスト実行 → FAIL 確認** + +```bash +cd tools/discovery-capture +PYTHONPATH=. python3 -m pytest tests/test_discovery_capture.py -v +``` + +期待: `ModuleNotFoundError: No module named 'discovery_capture'` + +- [ ] **Step 2.4: 最小実装 (argparse だけ)** + +`tools/discovery-capture/discovery_capture.py` を新規作成し、以下を記述する: + +```python +import argparse + + +def parse_args(argv=None): + parser = argparse.ArgumentParser( + description="両側 capture 起動 + pcap pull ヘルパ" + ) + parser.add_argument("--host-interface", required=True) + parser.add_argument("--android-device", required=True) + parser.add_argument("--scenario", required=True, + choices=["unity-to-ros2-reliable-32", + "unity-to-ros2-best-effort-8192", + "ros2-to-unity-reliable-32", + "ros2-to-unity-best-effort-32k"]) + parser.add_argument("--run-id", required=True) + parser.add_argument("--udp-portrange", default="7400-12500") + return parser.parse_args(argv) + + +if __name__ == "__main__": + args = parse_args() + print(f"host={args.host_interface} android={args.android_device} " + f"scenario={args.scenario} run_id={args.run_id}") +``` + +- [ ] **Step 2.5: テスト実行 → PASS 確認** + +```bash +cd tools/discovery-capture +PYTHONPATH=. python3 -m pytest tests/test_discovery_capture.py -v +``` + +期待: 3 tests passed。 + +- [ ] **Step 2.6: commit** + +```bash +git add tools/discovery-capture/ +git commit -m "feat(discovery-capture): CLI フラグ parse ヘルパを追加 (TDD)" +``` + +--- + +## Task 3: discovery_capture.py 拡張 (capture 起動 + pcap pull) + +**Files:** +- Modify: `tools/discovery-capture/discovery_capture.py` +- Modify: `tools/discovery-capture/tests/test_discovery_capture.py` + +- [ ] **Step 3.1: pcap path 計算関数のテスト追加** + +`tools/discovery-capture/tests/test_discovery_capture.py` に以下を追加: + +```python +from discovery_capture import build_paths, CapturePaths + + +def test_build_paths_reliable(): + args = parse_args([ + "--host-interface", "wlan0", + "--android-device", "5HF6OVWCDECMJZ59", + "--scenario", "unity-to-ros2-reliable-32", + "--run-id", "20260627-180000", + ]) + paths = build_paths(args, root="/srv/run") + assert isinstance(paths, CapturePaths) + assert paths.host_pcap == "/srv/run/host-reliable.pcap" + assert paths.android_pcap == "/srv/run/android-reliable.pcap" + assert paths.host_log == "/srv/run/host-tshark.log" + assert paths.android_log == "/srv/run/android-tcpdump.log" + assert paths.clock_pre_host == "/srv/run/host-clock-pre.txt" + assert paths.clock_pre_android == "/srv/run/android-clock-pre.txt" + + +def test_build_paths_best_effort(): + args = parse_args([ + "--host-interface", "wlan0", + "--android-device", "5HF6OVWCDECMJZ59", + "--scenario", "unity-to-ros2-best-effort-8192", + "--run-id", "20260627-180000", + ]) + paths = build_paths(args, root="/srv/run") + assert paths.host_pcap == "/srv/run/host-best-effort.pcap" + assert paths.android_pcap == "/srv/run/android-best-effort.pcap" +``` + +- [ ] **Step 3.2: テスト実行 → FAIL 確認** + +```bash +cd tools/discovery-capture +PYTHONPATH=. python3 -m pytest tests/test_discovery_capture.py -v +``` + +期待: `ImportError: cannot import name 'build_paths' from 'discovery_capture'` + +- [ ] **Step 3.3: build_paths 実装追加** + +`tools/discovery-capture/discovery_capture.py` を以下に拡張 (parse_args の後に追加): + +```python +from dataclasses import dataclass + + +@dataclass +class CapturePaths: + host_pcap: str + android_pcap: str + host_log: str + android_log: str + clock_pre_host: str + clock_pre_android: str + + +def build_paths(args, root: str) -> CapturePaths: + tag = "reliable" if "reliable" in args.scenario else "best-effort" + return CapturePaths( + host_pcap=f"{root}/host-{tag}.pcap", + android_pcap=f"{root}/android-{tag}.pcap", + host_log=f"{root}/host-tshark.log", + android_log=f"{root}/android-tcpdump.log", + clock_pre_host=f"{root}/host-clock-pre.txt", + clock_pre_android=f"{root}/android-clock-pre.txt", + ) +``` + +- [ ] **Step 3.4: テスト実行 → PASS 確認** + +```bash +cd tools/discovery-capture +PYTHONPATH=. python3 -m pytest tests/test_discovery_capture.py -v +``` + +期待: 5 tests passed。 + +- [ ] **Step 3.5: commit** + +```bash +git add tools/discovery-capture/ +git commit -m "feat(discovery-capture): pcap path 計算 (build_paths) を追加" +``` + +--- + +## Task 4: Android root 取得 + tcpdump 動作確認 (手動) + +> 注: このタスクは device 状態確認のため手動実行。自動テストは不可。 + +- [ ] **Step 4.1: device 接続確認** + +```bash +adb devices +``` + +期待: `5HF6OVWCDECMJZ59 device` の行。 + +- [ ] **Step 4.2: root 取得確認** + +```bash +adb -s 5HF6OVWCDECMJZ59 shell su -c 'id' +``` + +期待: `uid=0(root)` を含む出力。失敗するなら `su: not found` または `Permission denied`。 +失敗時: `findings doc` の limitations に「root 取得不可」と明記し、本調査をスキップ。 + +- [ ] **Step 4.3: tcpdump 既存 / 静的バイナリ push** + +```bash +# 既存 tcpdump 確認 +adb -s 5HF6OVWCDECMJZ59 shell su -c 'which tcpdump' +# 存在しなければ push +if ! adb -s 5HF6OVWCDECMJZ59 shell su -c 'which tcpdump' 2>/dev/null; then + adb -s 5HF6OVWCDECMJZ59 push ~/.nix-profile/bin/tcpdump /data/local/tmp/tcpdump + adb -s 5HF6OVWCDECMJZ59 shell su -c 'chmod 755 /data/local/tmp/tcpdump' +fi +``` + +期待: 最終的に `tcpdump --version` が実行可能。 + +- [ ] **Step 4.4: IGMP join 確認** + +```bash +adb -s 5HF6OVWCDECMJZ59 shell su -c 'ip maddr show wlan0 | grep 239.255.0.1' +``` + +期待: `239.255.0.1` を含む行。無い場合は本調査の findings に limitation として記録。 + +- [ ] **Step 4.5: 確認結果のメモ** + +`artifacts/discovery-capture/pre-check.txt` に以下を保存: + +``` +device: 5HF6OVWCDECMJZ59 +root: ok +tcpdump: /data/local/tmp/tcpdump +igmp_239.255.0.1: joined | not-joined +host_tshark: $(tshark --version | head -1) +host_tcpdump: $(tcpdump --version | head -1) +``` + +- [ ] **Step 4.6: commit** + +```bash +git add artifacts/discovery-capture/pre-check.txt +git commit -m "chore: discovery-capture 計測前 device 状態確認を記録" +``` + +--- + +## Task 5: reliable-32 capture 起動 + 計測 + +- [ ] **Step 5.1: 計測ディレクトリ作成** + +```bash +RUN_ID=$(date +%Y%m%d-%H%M%S) +RUN_DIR="artifacts/discovery-capture/$RUN_ID" +mkdir -p "$RUN_DIR" +echo "RUN_ID=$RUN_ID" > "$RUN_DIR/env.sh" +echo "RUN_DIR=$RUN_DIR" >> "$RUN_DIR/env.sh" +``` + +期待: `RUN_DIR` 変数が echo される。 + +- [ ] **Step 5.2: 既存 Android build artifact 確認** + +```bash +ls -la /tmp/rosettadds-perf-android.apk +``` + +期待: ファイルが存在しサイズが >40MB。`97e446e` 取り込み済みを期待。 +無ければ `dotnet run --project tools/rosettadds-perf-runner -c Release -- --build-target Android --scenario all --android-device 5HF6OVWCDECMJZ59 --artifacts /tmp/build-check` で再 build (10-20 分)。 + +- [ ] **Step 5.3: host tshark 起動 (バックグラウンド)** + +```bash +source "$RUN_DIR/env.sh" +tshark -i wlan0 -f "udp portrange 7400-12500" \ + -w "$RUN_DIR/host-reliable.pcap" -b duration:300 \ + > "$RUN_DIR/host-tshark.log" 2>&1 & +echo $! > "$RUN_DIR/host-tshark.pid" +sleep 2 +ps -p $(cat "$RUN_DIR/host-tshark.pid") -o pid,comm +``` + +期待: tshark プロセスが動作中。`tshark` を含む comm。 + +- [ ] **Step 5.4: Android tcpdump 起動 (バックグラウンド)** + +```bash +source "$RUN_DIR/env.sh" +adb -s 5HF6OVWCDECMJZ59 shell su -c \ + "/data/local/tmp/tcpdump -i wlan0 -n -B 4096 \ + 'udp portrange 7400-12500' -w /sdcard/android-reliable.pcap -U" \ + > "$RUN_DIR/android-tcpdump.log" 2>&1 & +echo $! > "$RUN_DIR/android-tcpdump.pid" +sleep 3 +ps -p $(cat "$RUN_DIR/android-tcpdump.pid") -o pid,comm +``` + +期待: tcpdump プロセスが動作中。`tcpdump` を含む comm。 +`Operation not permitted` の場合は root 取得失敗 → findings に limitation 記録。 + +- [ ] **Step 5.5: クロック同期確認** + +```bash +source "$RUN_DIR/env.sh" +echo $(date +%s.%N) > "$RUN_DIR/host-clock-pre.txt" +adb -s 5HF6OVWCDECMJZ59 shell 'date +%s.%N' > "$RUN_DIR/android-clock-pre.txt" +cat "$RUN_DIR/host-clock-pre.txt" "$RUN_DIR/android-clock-pre.txt" +``` + +期待: 2 つの timestamp の差が 1 秒以内 (理想は 100ms 以内)。 + +- [ ] **Step 5.6: reliable-32 計測実行** + +```bash +source "$RUN_DIR/env.sh" +dotnet run --project tools/rosettadds-perf-runner -c Release --no-build -- \ + --build-target Android \ + --scenario unity-to-ros2-reliable-32 \ + --android-device 5HF6OVWCDECMJZ59 \ + --artifacts "$RUN_DIR/reliable-32" \ + --capture-frames 200 +``` + +期待: 計測完了。`helper received 0/500` (想定通り B 仮説支持)。 + +- [ ] **Step 5.7: capture 停止** + +```bash +source "$RUN_DIR/env.sh" +kill $(cat "$RUN_DIR/android-tcpdump.pid") 2>/dev/null +kill $(cat "$RUN_DIR/host-tshark.pid") 2>/dev/null +sleep 2 +ls -la "$RUN_DIR"/*.pcap +``` + +期待: `host-reliable.pcap` / `android-reliable.pcap` がそれぞれ >1KB 存在。 + +- [ ] **Step 5.8: Android pcap pull** + +```bash +source "$RUN_DIR/env.sh" +adb -s 5HF6OVWCDECMJZ59 shell su -c \ + 'cat /sdcard/android-reliable.pcap' > "$RUN_DIR/android-reliable.pcap" +adb -s 5HF6OVWCDECMJZ59 shell su -c 'rm /sdcard/android-reliable.pcap' +ls -la "$RUN_DIR/android-reliable.pcap" +``` + +期待: ファイルサイズが `1KB` 以上。`Permission denied` の場合は +`adb -s ... shell su -c 'chmod 666 /sdcard/android-reliable.pcap'` で permission 変更後再試行。 + +- [ ] **Step 5.9: pcap 破損チェック** + +```bash +source "$RUN_DIR/env.sh" +tshark -c 1 -r "$RUN_DIR/host-reliable.pcap" 2>&1 | head -3 +tshark -c 1 -r "$RUN_DIR/android-reliable.pcap" 2>&1 | head -3 +``` + +期待: 両 pcap とも 1 packet 以上の出力が得られる。`pcap: file has no valid packets` の場合は再 capture。 + +- [ ] **Step 5.10: 計測結果の簡易集計 (host 側のみ)** + +```bash +source "$RUN_DIR/env.sh" +tshark -r "$RUN_DIR/host-reliable.pcap" -Y "rtps" \ + -T fields -e udp.dstport | sort | uniq -c | sort -rn +``` + +期待: port 別 count の表。`7400` (SPDP multicast) / `7401` (user multicast) / +`7410+2*PID` (SEDP) 等の数字が見える。 + +- [ ] **Step 5.11: commit (計測 data 以外)** + +```bash +source "$RUN_DIR/env.sh" +git add "$RUN_DIR/env.sh" "$RUN_DIR/host-tshark.log" \ + "$RUN_DIR/android-tcpdump.log" "$RUN_DIR/host-clock-pre.txt" \ + "$RUN_DIR/android-clock-pre.txt" "$RUN_DIR/reliable-32/manifest.json" 2>/dev/null || true +# pcap 自体は .gitignore で除外される +git commit -m "chore: discovery-capture reliable-32 計測 (host / android pcap 取得)" || \ + echo "no changes to commit (data only)" +``` + +--- + +## Task 6: best-effort-8192 capture (control) + 計測 + +- [ ] **Step 6.1: host tshark 起動 (best-effort 用)** + +```bash +source "$RUN_DIR/env.sh" +tshark -i wlan0 -f "udp portrange 7400-12500" \ + -w "$RUN_DIR/host-best-effort.pcap" -b duration:300 \ + > "$RUN_DIR/host-tshark-best-effort.log" 2>&1 & +echo $! > "$RUN_DIR/host-tshark-be.pid" +sleep 2 +ps -p $(cat "$RUN_DIR/host-tshark-be.pid") -o pid,comm +``` + +- [ ] **Step 6.2: Android tcpdump 起動 (best-effort 用)** + +```bash +source "$RUN_DIR/env.sh" +adb -s 5HF6OVWCDECMJZ59 shell su -c \ + "/data/local/tmp/tcpdump -i wlan0 -n -B 4096 \ + 'udp portrange 7400-12500' -w /sdcard/android-best-effort.pcap -U" \ + > "$RUN_DIR/android-tcpdump-be.log" 2>&1 & +echo $! > "$RUN_DIR/android-tcpdump-be.pid" +sleep 3 +ps -p $(cat "$RUN_DIR/android-tcpdump-be.pid") -o pid,comm +``` + +- [ ] **Step 6.3: best-effort-8192 計測実行** + +```bash +source "$RUN_DIR/env.sh" +dotnet run --project tools/rosettadds-perf-runner -c Release --no-build -- \ + --build-target Android \ + --scenario unity-to-ros2-best-effort-8192 \ + --android-device 5HF6OVWCDECMJZ59 \ + --artifacts "$RUN_DIR/best-effort-8192" \ + --capture-frames 200 +``` + +期待: 計測完了。`helper received 73-81%` (想定通り best-effort 部分成功)。 + +- [ ] **Step 6.4: capture 停止 + pcap pull + 破損チェック** + +```bash +source "$RUN_DIR/env.sh" +kill $(cat "$RUN_DIR/android-tcpdump-be.pid") 2>/dev/null +kill $(cat "$RUN_DIR/host-tshark-be.pid") 2>/dev/null +sleep 2 +adb -s 5HF6OVWCDECMJZ59 shell su -c \ + 'cat /sdcard/android-best-effort.pcap' > "$RUN_DIR/android-best-effort.pcap" +adb -s 5HF6OVWCDECMJZ59 shell su -c 'rm /sdcard/android-best-effort.pcap' +tshark -c 1 -r "$RUN_DIR/host-best-effort.pcap" 2>&1 | head -3 +tshark -c 1 -r "$RUN_DIR/android-best-effort.pcap" 2>&1 | head -3 +``` + +期待: 両 pcap とも読み込み可能。 + +- [ ] **Step 6.5: 計測結果の簡易集計 (host 側のみ)** + +```bash +source "$RUN_DIR/env.sh" +tshark -r "$RUN_DIR/host-best-effort.pcap" -Y "rtps" \ + -T fields -e udp.dstport | sort | uniq -c | sort -rn +``` + +期待: reliable と比較し、user data port 7401 の count が best-effort で 1 以上 (best-effort 部分的成功)。 + +- [ ] **Step 6.6: commit** + +```bash +source "$RUN_DIR/env.sh" +git add "$RUN_DIR/host-tshark-best-effort.log" \ + "$RUN_DIR/android-tcpdump-be.log" \ + "$RUN_DIR/best-effort-8192/manifest.json" 2>/dev/null || true +git commit -m "chore: discovery-capture best-effort-8192 計測 (control)" || \ + echo "no changes to commit (data only)" +``` + +--- + +## Task 7: analyze.py 作成 (TDD) + +**Files:** +- Create: `tools/discovery-capture/analyze.py` +- Create: `tools/discovery-capture/tests/test_analyze.py` + +- [ ] **Step 7.1: tshark CSV 抽出関数のテスト作成** + +`tools/discovery-capture/tests/test_analyze.py` を新規作成: + +```python +import pytest +from analyze import parse_tshark_csv, summarize_ports + + +SAMPLE_CSV = """1700000000.123456\t192.168.0.22\t239.255.0.1\t12345\t7400\t0x01 +1700000000.234567\t192.168.0.20\t239.255.0.1\t54321\t7400\t0x01 +1700000000.345678\t192.168.0.22\t239.255.0.1\t12345\t7410\t0x07 +1700000001.000000\t192.168.0.22\t239.255.0.1\t12345\t7401\t0x15 +""" + + +def test_parse_tshark_csv_basic(): + rows = parse_tshark_csv(SAMPLE_CSV) + assert len(rows) == 4 + assert rows[0]["src"] == "192.168.0.22" + assert rows[0]["dport"] == 7400 + assert rows[2]["sm_id"] == "0x07" + + +def test_summarize_ports(): + rows = parse_tshark_csv(SAMPLE_CSV) + summary = summarize_ports(rows) + assert summary[7400] == 2 + assert summary[7410] == 1 + assert summary[7401] == 1 + + +def test_summarize_src(): + from analyze import summarize_src + rows = parse_tshark_csv(SAMPLE_CSV) + summary = summarize_src(rows) + assert summary["192.168.0.22"] == 3 + assert summary["192.168.0.20"] == 1 +``` + +- [ ] **Step 7.2: テスト実行 → FAIL 確認** + +```bash +cd tools/discovery-capture +PYTHONPATH=. python3 -m pytest tests/test_analyze.py -v +``` + +期待: `ModuleNotFoundError: No module named 'analyze'` + +- [ ] **Step 7.3: analyze.py 実装** + +`tools/discovery-capture/analyze.py` を新規作成: + +```python +import csv +import io +from collections import Counter +from typing import Dict, List + + +def parse_tshark_csv(text: str) -> List[Dict[str, object]]: + """tshark -T fields の TSV 出力を dict の list に変換。""" + rows: List[Dict[str, object]] = [] + reader = csv.reader(io.StringIO(text), delimiter="\t") + for parts in reader: + if len(parts) < 6: + continue + rows.append({ + "ts": parts[0], + "src": parts[1], + "dst": parts[2], + "sport": int(parts[3]) if parts[3] else 0, + "dport": int(parts[4]) if parts[4] else 0, + "sm_id": parts[5], + }) + return rows + + +def summarize_ports(rows: List[Dict[str, object]]) -> Dict[int, int]: + return dict(Counter(r["dport"] for r in rows)) + + +def summarize_src(rows: List[Dict[str, object]]) -> Dict[str, int]: + return dict(Counter(r["src"] for r in rows)) + + +def extract_pcap(pcap_path: str) -> List[Dict[str, object]]: + import subprocess + out = subprocess.check_output([ + "tshark", "-r", pcap_path, "-Y", "rtps", + "-T", "fields", + "-e", "frame.time_epoch", + "-e", "ip.src", "-e", "ip.dst", + "-e", "udp.srcport", "-e", "udp.dstport", + "-e", "rtps.sm.id", + ], text=True) + return parse_tshark_csv(out) +``` + +- [ ] **Step 7.4: テスト実行 → PASS 確認** + +```bash +cd tools/discovery-capture +PYTHONPATH=. python3 -m pytest tests/test_analyze.py -v +``` + +期待: 3 tests passed。 + +- [ ] **Step 7.5: 計測 pcap で動作確認** + +```bash +source "$RUN_DIR/env.sh" +cd tools/discovery-capture +PYTHONPATH=. python3 -c " +from analyze import extract_pcap, summarize_ports, summarize_src +for name in ['host-reliable', 'android-reliable', 'host-best-effort', 'android-best-effort']: + pcap = '$RUN_DIR/' + name + '.pcap' + rows = extract_pcap(pcap) + print(f'== {name} ==') + print(' ports:', summarize_ports(rows)) + print(' src: ', summarize_src(rows)) +" +``` + +期待: 4 ファイルすべてで port / src 集計が出力される。 + +- [ ] **Step 7.6: commit** + +```bash +git add tools/discovery-capture/ +git commit -m "feat(discovery-capture): pcap 解析スクリプト (analyze.py) を追加 (TDD)" +``` + +--- + +## Task 8: 仮説判定 + 集計表作成 + +- [ ] **Step 8.1: 集計 CSV 出力** + +```bash +source "$RUN_DIR/env.sh" +cd tools/discovery-capture +PYTHONPATH=. python3 -c " +from analyze import extract_pcap, summarize_ports, summarize_src +results = {} +for name in ['host-reliable', 'android-reliable', 'host-best-effort', 'android-best-effort']: + pcap = '$RUN_DIR/' + name + '.pcap' + rows = extract_pcap(pcap) + results[name] = { + 'ports': summarize_ports(rows), + 'src': summarize_src(rows), + 'total': len(rows), + } +import json +with open('$RUN_DIR/analysis.json', 'w') as f: + json.dump(results, f, indent=2) +print(json.dumps(results, indent=2)) +" +``` + +期待: 4 ファイルの集計 JSON が `$RUN_DIR/analysis.json` に出力。 + +- [ ] **Step 8.2: 仮説判定表作成** + +```bash +cat > "$RUN_DIR/hypothesis-table.md" <<'EOF' +| 観点 | reliable-32 host | reliable-32 android | best-effort-8192 host | best-effort-8192 android | +|------|------------------|---------------------|------------------------|---------------------------| +| total packets | n | n | n | n | +| port 7400 (SPDP) | n | n | n | n | +| port 7401 (user mc) | n | n | n | n | +| port 7410 (SEDP) | n | n | n | n | +| src 192.168.0.22 (Android) | n | n | n | n | +| src 192.168.0.20 (host) | n | n | n | n | +EOF +``` + +期待: 空の表が作成される。Step 8.3 で実数を埋める。 + +- [ ] **Step 8.3: analysis.json の値で表を更新 (手動)** + +```bash +source "$RUN_DIR/env.sh" +# analysis.json から値を取得し、表を更新 (テキストエディタで埋める) +cat "$RUN_DIR/analysis.json" | python3 -m json.tool +``` + +- [ ] **Step 8.4: 仮説判定 (手動)** + +`$RUN_DIR/hypothesis-judgment.md` に以下を埋める: + +```markdown +# 仮説判定 (2026-06-27) + +## 仮説 1: Player 側 SPDP/SEDP 不出力 + +**支持 / 反証データ**: +- host-reliable.pcap: port 7400 (SPDP) count = n +- android-reliable.pcap: port 7400 (SPDP) count = n + +**判定**: 支持 / 反証 / 不確定 + +## 仮説 2: Android kernel 経路問題 (IGMP / multicast routing) + +**支持 / 反証データ**: +- host-reliable.pcap: src 192.168.0.22 count = n +- android-reliable.pcap: total packets = n +- IGMP join 状況: joined / not-joined + +**判定**: 支持 / 反証 / 不確定 + +## 仮説 3: host 側 rmw_fastrtts_cpp parse 失敗 + +**支持 / 反証データ**: +- host-reliable.pcap: port 7400 (SPDP) count = n (helper が SPDP を見ていれば host 側送信ある) +- helper 受信 count: reliable 0/500 vs best-effort 73-81% + +**判定**: 支持 / 反証 / 不確定 +``` + +- [ ] **Step 8.5: 集計 + 判定の commit** + +```bash +source "$RUN_DIR/env.sh" +git add "$RUN_DIR/analysis.json" "$RUN_DIR/hypothesis-table.md" \ + "$RUN_DIR/hypothesis-judgment.md" +git commit -m "docs(specs): discovery-capture 仮説判定 + 集計表" || true +``` + +--- + +## Task 9: findings doc 作成 + +**Files:** +- Create: `docs/superpowers/specs/2026-06-27-discovery-capture-investigation.md` + +- [ ] **Step 9.1: findings テンプレート作成** + +`docs/superpowers/specs/2026-06-27-discovery-capture-investigation.md` を新規作成し、 +以下テンプレートを埋める: + +```markdown +# Reliable Discovery Capture Investigation Findings (2026-06-27) + +## サマリ + +(run の結果 + 判定を 1 段落で) + +## 計測環境 + +- 日時: 2026-06-27 +- HEAD: 3c74790 +- branch: perf/discovery-capture-investigation +- device: Sony XIG04 (Xperia 10 III) +- helper: rmw_fastrtts_cpp on host +- 計測 run: $RUN_ID + +## 計測結果 + +### 4 pcap 概要 + +| file | size | total packets | 主な dst port | +|------|------|---------------|---------------| +| host-reliable.pcap | n MB | n | 7400: n, 7401: n, 7410: n | +| android-reliable.pcap | n MB | n | 7400: n, 7401: n, 7410: n | +| host-best-effort.pcap | n MB | n | 7400: n, 7401: n, 7410: n | +| android-best-effort.pcap | n MB | n | 7400: n, 7401: n, 7410: n | + +### port 別 / src 別 packet count (Step 8.1 analysis.json) + +(analysis.json の内容を表化) + +### 仮説判定 (Step 8.4 hypothesis-judgment.md) + +(hypothesis-judgment.md の内容を転記) + +## 結論 + +(3 仮説のいずれが支持されたか / どれでもないか) + +## 既存 findings との対応 + +| 既存 finding | 今回の結論 | +|--------------|-----------| +| B. Android → helper reliable discovery 不通 | (結果に応じて更新) | +| A'. Android reliable 8 KB の 71× 退化 | fragmentation コストが支配的、本調査では扱わず | + +## 残存ボトルネック / next action + +(判明した root cause に応じて next action を 1-3 項目) + +1. (root cause への修正案) +2. (検証追加計測の必要性) +3. (他 PR への切り出し) + +## 計測方法 (再現手順) + +```bash +# branch 作成 + 準備 +git checkout -b perf/discovery-capture-investigation + +# Task 4-8 の手順を順次実行 +# (詳細は plan を参照) +``` + +## 計測 artifact + +- $RUN_DIR/host-reliable.pcap +- $RUN_DIR/android-reliable.pcap +- $RUN_DIR/host-best-effort.pcap +- $RUN_DIR/android-best-effort.pcap +- $RUN_DIR/analysis.json +- $RUN_DIR/hypothesis-table.md +- $RUN_DIR/hypothesis-judgment.md +``` + +- [ ] **Step 9.2: プレースホルダ scan** + +```bash +grep -nE "TBD|TODO|fill in|xxxx" docs/superpowers/specs/2026-06-27-discovery-capture-investigation.md +``` + +期待: 出力なし。`TBD` 等があれば埋めて再実行。 + +- [ ] **Step 9.3: 内部整合性チェック** + +- [ ] サマリ / 結論 / next action が矛盾していないこと +- [ ] port 数字と port 別 packet count が一致すること +- [ ] 仮説判定と既存 findings (B + A') の対応が取れていること + +- [ ] **Step 9.4: 曖昧性チェック** + +各段落が 1 通りに解釈できることを確認。曖昧なら言い切り形に修正。 + +- [ ] **Step 9.5: commit** + +```bash +git add docs/superpowers/specs/2026-06-27-discovery-capture-investigation.md +git commit -m "docs(specs): discovery-capture 計測結果と findings を追加" +``` + +--- + +## Task 10: 検証 + PR + +- [ ] **Step 10.1: Validation チェックリスト実行** + +design doc の Validation 完了条件 10 項目を 1 つずつ確認: + +- [ ] 4 pcap 取得 (1 KB 以上) +- [ ] 4 pcap 破損なし +- [ ] host-reliable.pcap に Android からの SPDP あり +- [ ] android-reliable.pcap に Player からの SPDP あり +- [ ] host-reliable.pcap に host からの SPDP reply あり +- [ ] host-best-effort.pcap に Android からの user data あり +- [ ] port / src 別 packet count 表あり +- [ ] 3 仮説に支持 / 反証データあり +- [ ] 1 commit + 1 PR で main マージ可能 +- [ ] `git status` clean + +- [ ] **Step 10.2: 既存 spec doc との整合性確認** + +`docs/superpowers/specs/2026-06-27-android-bottleneck-investigation.md` の +「Next action 1. Android → helper reliable discovery 不通 + ACK 待ち block の原因調査 (B + A')」 +の項目が、本 findings により更新されたか確認。更新されていないなら、 +本 PR の summary で「B は本 findings を参照」と明記。 + +- [ ] **Step 10.3: ブランチ push** + +```bash +git push origin perf/discovery-capture-investigation +``` + +- [ ] **Step 10.4: PR 作成** + +```bash +gh pr create --base main --head perf/discovery-capture-investigation \ + --title "docs(specs): Android reliable discovery 調査 (B) の findings を追加" \ + --body "本 PR は 2026-06-27-android-bottleneck-investigation.md の Next action 1 のうち B (discovery 不通) を host tshark + Android tcpdump のパケットキャプチャで解析した findings。 + +調査 design: docs/superpowers/specs/2026-06-27-discovery-capture-investigation-design.md +調査結果: docs/superpowers/specs/2026-06-27-discovery-capture-investigation.md + +A' (reliable-8000 の 71× 退化) は fragmentation コストが支配的と推定され別 findings。" +``` + +- [ ] **Step 10.5: CI 通過待ち** + +```bash +gh pr checks +``` + +期待: 全て pass。 + +- [ ] **Step 10.6: レビュー対応 + main マージ** + +レビュー指摘に対応後、main にマージ (squash or merge、PR 設定に従う)。 diff --git a/docs/superpowers/specs/2026-06-27-discovery-capture-investigation-design.md b/docs/superpowers/specs/2026-06-27-discovery-capture-investigation-design.md new file mode 100644 index 0000000..891e8c3 --- /dev/null +++ b/docs/superpowers/specs/2026-06-27-discovery-capture-investigation-design.md @@ -0,0 +1,302 @@ +# Reliable Discovery Capture Investigation Design (2026-06-27) + +## Goal + +Sony XIG04 (Xperia 10 III / Android 15 / arm64-v8a) 上の Unity Player (rosettadds 独自 RTPS) と、 +LAN 上の helper (rmw_fastrtts_cpp) の間で **reliable QoS の discovery (SPDP / SEDP) が +通らない問題 (findings B)** の根本原因を、host 側 wireshark/tshark + Android 側 +tcpdump によるマルチキャストパケットキャプチャで特定する。 + +既存 findings `2026-06-27-android-bottleneck-investigation.md` で観測された +`unity-to-ros2-reliable-32` (helper received 0/500) と +`unity-to-ros2-best-effort-8192` (helper received 73-81%) の差を、reliable と +best-effort で対称な SPDP / SEDP / user data パケットキャプチャを取って比較解析する。 + +計測結果と分析は `docs/superpowers/specs/2026-06-27-discovery-capture-investigation.md` +(findings) にまとめ、3 つの仮説 (Player 側 SPDP/SEDP 不出力 / Android kernel +経路問題 / host 側 rmw_fastrtts_cpp parse 失敗) の支持 / 反証データを示す。 +修正が必要なら別 PR の scope を提示し、本 PR は findings のみで main マージ可能とする。 + +## Scope + +### In scope + +- `unity-to-ros2-reliable-32` 計測中の host 側マルチキャストキャプチャ + (tshark `-i wlan0 -f "udp portrange 7400-12500"`) +- 同計測中の Android 側マルチキャストキャプチャ + (`adb shell tcpdump -i wlan0 -n -B 4096 'udp portrange 7400-12500'`) +- best-effort control: `unity-to-ros2-best-effort-8192` を同手順で計測 +- pcap 解析: port 別 / src IP 別 / 5 秒 window 別 packet count + RTPS submessage 内訳 +- 3 仮説の支持 / 反証データ +- findings doc 1 commit + 1 PR + +### Out of scope + +- A' (reliable-8000 の 71× 退化) の解析。fragmentation コスト (8 fragments × 6ms/fragment) + が支配的と推定され、別 findings として記録。修正には StatelessWriter への fragmentation + 追加または reliable 8KB の MaxSamples 調整が候補だが本 PR では扱わない +- helper / Player 側コードの修正。findings 後の別 PR +- 計測 runner (perf-runner) の機能追加。既存 perf-runner + 手動 adb tcpdump 起動で十分 +- 別 subnet / USB テザリング / emulator 環境。同一 L2 セグメントでのみ実施 +- logcat streaming runner 化、player.profiler.raw pull 対応。別 Next action + +## Constraints + +- **同一 L2 セグメント**: Android 端末 `192.168.0.22/24` と host `192.168.0.20/24` が + 同一 AP 配下にある既存環境を利用。ping 5.9ms、SPDP multicast `239.255.0.1` 経路 + 成立済み (Android ボトルネック調査で検証) +- **root 権限**: Android 側 tcpdump は `su` 経由で実行。Xperia 10 III が + userdebug / rooted でない場合は静的バイナリでも raw socket 取得不可。 + 計測前に `adb shell su -c 'tcpdump --version'` で確認 +- **host ツール**: tcpdump 4.99.6 / tshark / wireshark が nix profile に + 既導入 (`~/.nix-profile/bin/`) +- **既存 build artifact**: Android 計測用に `/tmp/rosettadds-perf-android.apk` を + そのまま流用 (lean ProfilerRecorders + PublishRepeatedAsync 取り込み済 HEAD `3c74790`) +- **計測時間**: 1 capture × 2 scenario = 約 2-4 分、解析 30-60 分、findings 30 分 +- **pcap サイズ**: filter で絞れば 4 capture 合計で 10-50 MB 想定 +- **時刻同期**: host `date +%s.%N` と Android `date +%s.%N` の差を ±500ms 以内に + 収める想定。それ以下の精度は出ない + +## Architecture + +``` +┌─────────────────────┐ tshark host ┌────────────────────────────┐ +│ host (192.168.0.20) │ ──────────────► │ host.pcap │ +│ perf-runner │ wlan0 │ tshark filter: │ +│ ROS 2 helper │ │ udp portrange 7400-12500 │ +│ tshark / tcpdump │ │ (host 239.255.0.1) │ +└─────────────────────┘ └──────────────┬─────────────┘ + │ 解析 + ▼ +┌─────────────────────┐ tcpdump device ┌────────────────────────────┐ +│ Android (192.168.0.22) │ ──────────── │ android.pcap │ +│ Unity Player │ wlan0 │ adb shell tcpdump -i wlan0 │ +│ (rosettadds RTPS) │ │ 'udp portrange 7400-12500'│ +└─────────────────────┘ └──────────────┬─────────────┘ + │ 解析 + ▼ + host 上で tshark + Python で + port 別 / src IP 別 / time window 別 + packet count + RTPS submessage 内訳 +``` + +### Components + +- **host tshark**: `tshark -i wlan0 -f "udp portrange 7400-12500" -w ` + 既存 host wlan0 (192.168.0.20) で capture。root 必須 (tshark は通常 setcap で起動可) +- **Android tcpdump**: 静的バイナリを `/data/local/tmp/tcpdump` に push、 + `adb shell su -c '/data/local/tmp/tcpdump -i wlan0 -n -B 4096 + "udp portrange 7400-12500" -w /sdcard/android.pcap -U'` で起動 +- **perf-runner**: 既存 `tools/rosettadds-perf-runner` を `--scenario unity-to-ros2-reliable-32` / + `--scenario unity-to-ros2-best-effort-8192` で起動。skip-build なし +- **分析 script**: `artifacts/discovery-capture//analyze.py` で + 4 pcap を読み、port 別 / src IP 別 / 5 秒 window 別 count + RTPS submessage + 内訳を CSV 化 + +## Data Flow + +### Step 1: 事前準備 (Android 側 tcpdump 確認) + +```bash +# root 取得 + tcpdump 動作確認 +adb -s 5HF6OVWCDECMJZ59 shell su -c 'tcpdump --version 2>&1 | head -1' +# 既存 tcpdump が無い場合: +adb -s 5HF6OVWCDECMJZ59 push ~/.nix-profile/bin/tcpdump /data/local/tmp/tcpdump +adb -s 5HF6OVWCDECMJZ59 shell su -c 'chmod 755 /data/local/tmp/tcpdump && \ + /data/local/tmp/tcpdump --version | head -1' +# IGMP join 確認 +adb -s 5HF6OVWCDECMJZ59 shell su -c 'ip maddr show wlan0 | grep 239.255.0.1' +``` + +### Step 2: 計測ディレクトリ作成 + 両側 capture 起動 + +```bash +RUN_ID=$(date +%Y%m%d-%H%M%S) +RUN_DIR=artifacts/discovery-capture/$RUN_ID +mkdir -p $RUN_DIR/reliable-32 $RUN_DIR/best-effort-8192 + +# host tshark 起動 +tshark -i wlan0 -f "udp portrange 7400-12500" \ + -w $RUN_DIR/host-reliable.pcap -b duration:300 \ + > $RUN_DIR/host-tshark.log 2>&1 & +HOST_PID=$! + +# Android tcpdump 起動 +adb -s 5HF6OVWCDECMJZ59 shell su -c \ + "/data/local/tmp/tcpdump -i wlan0 -n -B 4096 \ + 'udp portrange 7400-12500' -w /sdcard/android-reliable.pcap -U" \ + > $RUN_DIR/android-tcpdump.log 2>&1 & +ANDROID_PID=$! + +sleep 3 +# クロック同期確認 +echo $(date +%s.%N) > $RUN_DIR/host-clock-pre.txt +adb -s 5HF6OVWCDECMJZ59 shell 'date +%s.%N' > $RUN_DIR/android-clock-pre.txt +``` + +### Step 3: reliable-32 計測 + +```bash +dotnet run --project tools/rosettadds-perf-runner -c Release --no-build -- \ + --build-target Android \ + --scenario unity-to-ros2-reliable-32 \ + --android-device 5HF6OVWCDECMJZ59 \ + --artifacts $RUN_DIR/reliable-32 \ + --capture-frames 200 +``` + +### Step 4: capture 停止 + pcap pull + +```bash +# helper received 0/500 を確認 (想定通りなら B 仮説支持) +kill $ANDROID_PID 2>/dev/null +kill $HOST_PID 2>/dev/null +wait $ANDROID_PID $HOST_PID 2>/dev/null +adb -s 5HF6OVWCDECMJZ59 shell su -c 'cat /sdcard/android-reliable.pcap' \ + > $RUN_DIR/android-reliable.pcap +adb -s 5HF6OVWCDECMJZ59 shell su -c 'rm /sdcard/android-reliable.pcap' +``` + +### Step 5: best-effort-8192 計測 (control) + +- Step 2-4 を `unity-to-ros2-best-effort-8192` で実施 +- pcap ファイル名: `host-best-effort.pcap` / `android-best-effort.pcap` +- helper received 73-81% を確認 (想定通りなら SPDP/SEDP は届いている) + +### Step 6: pcap 解析 (host 側 Python) + +```python +# artifacts/discovery-capture//analyze.py +# tshark で CSV 化 → pandas で集計 +import subprocess, pandas as pd + +def extract(path): + out = subprocess.check_output([ + "tshark", "-r", path, "-Y", "rtps", + "-T", "fields", + "-e", "frame.time_epoch", + "-e", "ip.src", "-e", "ip.dst", + "-e", "udp.srcport", "-e", "udp.dstport", + "-e", "rtps.sm.id", + ], text=True) + return pd.read_csv(StringIO(out), sep="\t", + names=["ts","src","dst","sport","dport","sm_id"]) + +for name in ["host-reliable", "android-reliable", + "host-best-effort", "android-best-effort"]: + df = extract(f"{name}.pcap") + df.to_csv(f"{name}.tsv", sep="\t", index=False) + # port 別 count + print(name, df.groupby("dport").size()) + # src IP 別 count + print(name, df.groupby("src").size()) +``` + +### Step 7: 仮説判定 + +各 pcap に対して以下を集計: + +| 観点 | reliable-32 | best-effort-8192 | 解釈 | +|------|-------------|------------------|------| +| host が見た Android からの SPDP (src=192.168.0.22, dst port 7400) | n | n | Player が出していない → 仮説 1 支持 | +| Android 側 SPDP の送信 count | n | n | 同上 | +| host が見た Android からの SEDP (src=192.168.0.22, dst port 7410+2*PID) | n | n | SEDP が出ていない → 仮説 1 支持 | +| host の SPDP reply (src=192.168.0.20, dst=192.168.0.22) | n | n | helper は SPDP を見られている | +| host の SEDP reply | n | n | helper は SEDP を見られている | +| user data (dst port 7401) の host ↔ Android 間往復 | 0 (helper 0/500) | 73-81% | reliable だけ user data 経路が詰まる | + +### Step 8: findings doc 作成 + +`docs/superpowers/specs/2026-06-27-discovery-capture-investigation.md` に: + +- 計測日 / 環境 / branch +- 4 pcap の port 別 / src 別 / time window 別 packet count 表 +- Step 7 の判定表 + 各仮説の支持 / 反証データ +- 根本原因 (判明した場合) +- next action (修正が必要なら別 PR scope、不可能なら limitation) +- 再現手順 (この doc 単独で他人が追試できる) + +### Step 9: commit + PR + +```bash +git add docs/superpowers/specs/2026-06-27-discovery-capture-investigation.md +git commit -m "docs(specs): Android reliable discovery 調査 (B) のパケットキャプチャ design を追加" +git push origin perf/discovery-capture-investigation +gh pr create --base main --head perf/discovery-capture-investigation \ + --title "docs(specs): Android reliable discovery 調査 (B) の design を追加" \ + --body "..." +``` + +## Error Handling + +| 失敗ケース | 検出方法 | 対応 | +|------------|----------|------| +| Android `su` 不可 (root 取得失敗) | `su: not found` または permission denied | 計測前 Step 1 で必ず確認。root 不可なら本調査は不可能 → findings doc に limitation として明記、`tcpdump` の代替手段 (socat + BPF) または別端末検討 | +| Android tcpdump 静的バイナリ未 push | `tcpdump: not found` | `~/.nix-profile/bin/tcpdump` を push、permission 755。arm64 バイナリであることを確認 | +| tshark が wlan0 をキャプチャできない | `tshark: ... is not supported` | `-i any` で全インタフェース、または `wlan0mon` (monitor mode) に切替 | +| 計測 scenario が player crash | perf-runner manifest.json に `PlayerExitCode != 0` | capture 停止後、別 branch として記録、re-run 検討 | +| pcap pull 失敗 | `adb pull ... failed:Permission denied` | `/sdcard/` の permission 確認、`su -c 'chmod 666 /sdcard/android.pcap'` または `su -c 'cat'` で標準出力経由 pull | +| pcap サイズが巨大 (>100MB) | `ls -lh *.pcap` | filter を `udp portrange 7400-12500 and host 239.255.0.1` に絞る、ring buffer `-b filesize:51200 -b files:4` | +| host ↔ Android クロック同期誤差 >1s | Step 2 の `date +%s.%N` 差 | 計測前 NTP 同期試行 (`adb shell su -c 'ntpdate ...'`)、または `±1s window` 許容を findings に明記 | +| tshark の RTPS dissector が decode 失敗 | `rtps.sm.id` が空欄 | tshark の builtin endpoint entity id 解釈不可。`udp.port == 7400 || 7401` 等 port ベースに fallback | +| IGMP join 確認で `239.255.0.1` が無い | `ip maddr show wlan0` 結果に無し | helper (rmw_fastrtts_cpp) との discovery 不通の根本原因が Android kernel 側 IGMP 設定の可能性。`adb shell su -c 'ip maddr add 239.255.0.1 dev wlan0'` で手動 join 試行を findings で記録 | +| 仮説のどれも支持 / 反証データが得られない | 4 pcap 全てが packet 0 に近い | capture filter が狭すぎる可能性。`udp` 全体で再 capture、tshark の `rtps` dissector の代わりに port のみで判定 | + +## Validation (完了条件) + +- [ ] 4 pcap 取得: `host-reliable.pcap` / `android-reliable.pcap` / `host-best-effort.pcap` / `android-best-effort.pcap` がそれぞれ 1 KB 以上 +- [ ] 4 ファイルそれぞれ `tshark -c 1 -r ` で読み込み可能 (破損なし) +- [ ] host-reliable.pcap に Android からの SPDP (src=192.168.0.22, dst port 7400) が 1 packet 以上含まれる +- [ ] android-reliable.pcap に Player からの SPDP (src=192.168.0.22, dst port 7400) が 1 packet 以上含まれる +- [ ] host-reliable.pcap に host (helper) からの SPDP reply (src=192.168.0.20, dst=192.168.0.22) が 1 packet 以上含まれる +- [ ] host-best-effort.pcap に Android からの user data packet (dst port 7401) が含まれる +- [ ] port 別 / src IP 別 packet count の表を findings に含める +- [ ] 3 つの仮説それぞれに支持 / 反証データを 1 段落以上つける +- [ ] 1 commit + 1 PR で findings doc のみを main にマージ可能 +- [ ] `git status` clean、ブランチ `perf/discovery-capture-investigation` 上で作業 + +## Validation Gate (PR レビュー観点) + +- **仮説の網羅性**: 「Packet 数 0」「Packet 数 100」「一部届く」 のどの結果でも、 + その意味を 1 段落で説明できている +- **切り分けの厳密性**: reliable と best-effort の差分がパケットキャプチャで + 説明できている (3 仮説以外の可能性は考慮不要なら明記) +- **next action の具体性**: 修正が必要なら別 PR の scope を 1-2 文で明記、 + 不要なら「環境問題のため調査終了」と明示 +- **再現性**: この doc 単独で第三者が追試できる (Step 1-9 のコマンドと + 期待される pcap 解析結果を含む) + +## Steps (実行順) + +1. ブランチ `perf/discovery-capture-investigation` を `main` から作成 +2. `adb shell su -c 'tcpdump --version'` で root 取得 + tcpdump 動作確認 +3. host / android で tcpdump 起動 (`artifacts/discovery-capture//` 配下) +4. `unity-to-ros2-reliable-32` 計測 (約 30 秒) +5. キャプチャ停止、pcap pull +6. `unity-to-ros2-best-effort-8192` 計測 (約 30 秒) +7. キャプチャ停止、pcap pull +8. tshark + Python で 4 pcap を解析、port 別 / src 別 / time window 別 count 抽出 +9. 3 仮説の支持 / 反証を整理 +10. findings doc を `docs/superpowers/specs/2026-06-27-discovery-capture-investigation.md` に書く +11. 1 commit + 1 PR → main マージ + +## Risks + +- **root 端末でない**: 多くの Android 15 端末は userdebug でもない限り root 不可。 + Xperia 10 III のブートローダアンロック状況を事前確認。アンロック可能でも + 初期化 + セットアップに 30-60 分。 +- **tcpdump の代替**: `su` で取得できない場合は `socat` + BPF の代替キャプチャを + fallback として準備。ただし機能制限あり (RTPS 詳細 decode は不可) +- **wireshark GUI 不可**: 既に tshark + Python script で対応可能なため問題なし +- **マルチキャスト IGMP 設定**: Android 側で IGMP join が不完全な場合、 + `adb shell ip maddr show wlan0` で `239.255.0.1` が joined か事前確認。 + 無い場合は本調査の findings で limitation として明示 +- **時刻同期**: Android と host の RTC が大きく乖離している可能性。 + 計測前に `adb shell date` で確認、必要なら NTP 同期 +- **pcap 解析での RTPS decode 限界**: tshark の RTPS dissector は + builtin endpoint (SPDP/SEDP) の entity id を完全には解釈できない場合あり。 + その場合 port ベースで判定 (Step 7 判定表) +- **ベストエフォート control の偶然性**: best-effort-8192 が 73-81% と + 部分的成功のため、control として 100% 信頼できるわけではない。 + best-effort-32 (helper received が 100% に近い) を追加 control として + 計測することも検討 (Step 5 で柔軟に対応) diff --git a/docs/superpowers/specs/2026-06-27-discovery-capture-investigation.md b/docs/superpowers/specs/2026-06-27-discovery-capture-investigation.md new file mode 100644 index 0000000..a9e9a83 --- /dev/null +++ b/docs/superpowers/specs/2026-06-27-discovery-capture-investigation.md @@ -0,0 +1,221 @@ +# Reliable Discovery Capture Investigation Findings (2026-06-27) + +## サマリ + +`unity-to-ros2-reliable-32` (helper received 0/200) の根本原因を、host 側 +tshark でのマルチキャストパケットキャプチャで解析した。Sony XIG04 (Xperia 10 III / +Android 15) は `ro.build.type=user` + `ro.debuggable=0` の stock build で +**root 取得不可**、Android 側 tcpdump は実行不能。host 側 capture のみで +比較分析した結果、**全 SPDP / SEDP / user data packet は host (192.168.0.20) +に到達している**。既存 findings の仮説 B (Android → helper reliable discovery +不通) は **反証** され、問題は discovery ではなく **host 側 +rmw_fastrtts_cpp の parse / decode 段階** にある。`unity-to-ros2-best-effort-8192` +control では 1800 user data fragment すべて host 到達、helper は 162/200 (81%) +を受信しており、best-effort / reliable で同じ「packet は到達するが helper 側で +処理されない」現象が確認された。 + +## 計測環境 + +- 日時: 2026-06-27 23:18 JST (reliable-32), 2026-06-27 23:33 JST (best-effort-8192) +- HEAD: 3c74790 (Android IL2CPP perf runner マージ直後) + 96 commit +- branch: perf/discovery-capture-investigation +- device: Sony XIG04 (Xperia 10 III), Android 15, `ro.build.type=user`, `ro.debuggable=0` +- host: wlp3s0 上で tshark 4.6.5 + tcpdump 4.99.6 +- 計測 run: 20260627-231205 (RUN_DIR=artifacts/discovery-capture/20260627-231205) +- ROS_DOMAIN_ID=20 (port 12400/12410/12411) - tools/rosettadds-perf-runner 内で固定 + +## Android 側 capture 制限 (Limitation) + +Sony XIG04 の状態: + +``` +device: 5HF6OVWCDECMJZ59 +root: not-available (su not found) +tcpdump: n/a (no root) +host_tshark: TShark (Wireshark) 4.6.5 +host_tcpdump: tcpdump version 4.99.6 +``` + +`adb shell su -c 'id'` は `su: inaccessible or not found` を返す。`/system/bin/su` +は存在せず、`ro.debuggable=0` のため Magisk 等での取得も困難。 + +加えて `/proc/net/igmp` で **wlan0 が 239.255.0.1 (SPDP multicast) に未参加** を確認: + +``` +38 wlan0 : 2 V2 + FB0000E0 3 0:00000000 1 ← 224.0.0.251 (mDNS) + 010000E0 1 0:00000000 0 ← 224.0.0.1 (All Hosts) +``` + +EF:FF:00:01 (239.255.0.1) のエントリがない。これは SPDP 経路における Android +kernel の IGMP 参加が不完全である可能性を示唆するが、host 側から見ると +multicast は到達しているため、kernel 経路自体は成立している。 + +本調査は **host 側 capture のみ** で実施。Android 側で何を受信したかは +本調査範囲外。 + +## 計測結果 + +### 4 pcap 概要 (host 側 2 ファイル、Android 側 2 ファイル欠落) + +| file | size | total RTPS | 主な dst port | +|------|------|------------|---------------| +| host-reliable_00001_20260627231808.pcap | 113296 | 603 | 12411: 500, 12410: 88, 12400: 14, 12401: 1 | +| host-best-effort_00001_20260627232823.pcap | 1908684 | 1921 | 12411: 1800, 12410: 103, 12400: 17, 12401: 1 | +| android-reliable.pcap | 欠落 | - | root 不可 (Task 4 参照) | +| android-best-effort.pcap | 欠落 | - | root 不可 (Task 4 参照) | + +### reliable-32 port / src 別 packet count (host 側) + +| 観点 | count | +|------|-------| +| total RTPS | 603 | +| port 12400 (SPDP mc) | 14 | +| port 12401 (user mc) | 1 | +| port 12410 (SEDP) | 88 | +| port 12411 (user unicast) | 500 | +| src 192.168.0.20 (host) | 92 | +| src 192.168.0.22 (Android) | 511 | +| helper received | **0/200 (0%)** | + +### best-effort-8192 port / src 別 packet count (host 側) + +| 観点 | count | +|------|-------| +| total RTPS | 1921 | +| port 12400 (SPDP mc) | 17 | +| port 12401 (user mc) | 1 | +| port 12410 (SEDP) | 103 | +| port 12411 (user unicast) | 1800 | +| src 192.168.0.20 (host) | 100 | +| src 192.168.0.22 (Android) | 1821 | +| helper received | **162/200 (81%)** | + +### port 別 packet 詳細 + +port 12411 の 500 (reliable) vs 1800 (best-effort) の差: +- reliable-32: 32 byte payload × 200 messages = 1 packet per message (SendBufferSize 1500 以内) +- best-effort-8192: 8192 byte payload × 200 messages = 8 fragments per message × 200 = 1600 fragments + - +200 ACK packets = 1800 total + - ※ 実際は fragmentation 単位の差で、reliable も fragmentation 経路は使うはずだが、 + reliable-32 は 32 byte のため fragmentation 必要無し + +## 仮説判定 (3 仮説すべて判定) + +### 仮説 1: Player 側 SPDP/SEDP 不出力 → **反証 (REFUTED)** +- Android (192.168.0.22) からの SPDP/SEDP 送信 count: reliable 14+88=102, best-effort 17+103=120 +- 両 scenarios とも Android は SPDP / SEDP を送信している +- **仮説 1 は完全に否定** + +### 仮説 2: Android kernel 経路問題 (IGMP / multicast routing) → **反証 (REFUTED)** +- host 側 wlp3s0 で Android からの全 multicast (port 12400, 12401) を受信 +- ただし `/proc/net/igmp` 上では wlan0 は 239.255.0.1 に未参加 +- **「host から見ると到達している」が、Android kernel から見ると SPDP 受信経路が不明** という + 矛盾。host → Android の SPDP/SEDP は届くが、Android → host の SP返信も届いている (SPDP 14+17) +- 結論として「Android kernel 経路が完全に断」ではないが、IGMP 設定の不整合は + 確認された。**仮説 2 自体は反証**だが、IGMP 設定の健全性は別 issue として記録 + +### 仮説 3: host 側 rmw_fastrtts_cpp parse 失敗 → **支持 (SUPPORTED)** +- reliable: 500 user data packet すべて host に到達、helper received 0/200 +- best-effort: 1800 user data fragment すべて host に到達、helper received 162/200 (81%) +- 両者とも「packet は host まで到達しているが、helper の rmw_fastrtts_cpp が + 100% decode できていない」状態 +- **仮説 3 を支持** + +## 結論 + +既存 findings `2026-06-27-android-bottleneck-investigation.md` の Next action 1 +のうち **B (Android → helper reliable discovery 不通)** は、本調査により +**反証** された。 + +真の問題は discovery 段階ではなく、host 側 `rmw_fastrtts_cpp` の +parse / decode 段階にある。Android からの SPDP / SEDP / user data packet +はすべて host に到達しているが、helper (rmw_fastrtts_cpp) は: +- reliable-32: 0/200 (0%) 受信 +- best-effort-8192: 162/200 (81%) 受信 + +reliable 0% vs best-effort 81% の差分は、以下のいずれか: +1. **reliable QoS の ACK/HEARTBEAT 処理** で helper 側 rmw_fastrtts_cpp が + hang / drop している +2. **rtps_payloads_dropped カウンタ** が fragmentation 受信で増大し、 + reliable の heartbeat timeout になっている +3. **Android 側 rosettadds RTPS の reliable writer が生成する packet + フォーマット** が rmw_fastrtts_cpp の期待と一部ずれている + (entity id, vendor id, sequence number set, fragment size 等) + +## 既存 findings との対応 + +| 既存 finding | 今回の結論 | +|--------------|-----------| +| B. Android → helper reliable discovery 不通 | **反証**。discovery は成立、host まで packet 到達 | +| A'. Android reliable 8 KB の 71× 退化 | fragmentation コストが支配的 (8 fragment × 6ms/fragment)。本調査範囲外、別 findings として残存 | +| IGMP 未参加 (Task 4 副次発見) | Android kernel の IGMP 設定の不整合。host 側から見ると到達しているが、Android 側での SPDP 受信経路に疑問。本調査では深掘りせず limitation として記録 | + +## 残存ボトルネック / next action + +1. **reliable 0% vs best-effort 81% の原因分離 (helper 側詳細解析)** + - host 側 `rmw_fastrtts_cpp` の verbose ログ (ROS 2 command line にて + `--ros-args --log-level debug` + `RCUTILS_LOGGING_USE_STDOUT=1`) で + receive / drop イベントを記録 + - fragmentation 経路での drop 率計測 (`rtps_payloads_dropped` counter) + - 別 PR の scope + +2. **Android 側 IGMP 設定の調査** + - `ip maddr add 239.255.0.1 dev wlan0` 手動 join で reliable が改善するか + - 改善するなら rosettadds の SPDP 初期化経路に IP_ADD_MEMBERSHIP が + 明示されているか確認 + - 別 PR の scope + +3. **A'. Android reliable 8 KB の 71× 退化 (fragmentation コスト)** + - 8 fragment × 6ms/fragment が支配的。`RtpsPayloadOwner` の ArrayPool + 経路を IL2CPP 向けに最適化、別 PR + - StatelessWriter への fragmentation 追加も候補 (best-effort 8KB が + 1500 byte に truncates されている件) + +4. **Sony XIG04 root 取得 (調査全体への影響)** + - 本調査の Android 側 capture 不可 / IGMP 観測不可 を解決 + - 端末のブートローダアンロック + userdebug 化、または別 rooted 端末を準備 + - 計測 infrastructure 側の改善 + +## 計測方法 (再現手順) + +```bash +# 0. branch 作成 +git checkout -b perf/discovery-capture-investigation + +# 1. device 確認 (root 不可 + IGMP 確認) +adb devices +adb -s 5HF6OVWCDECMJZ59 shell su -c 'id' # → inaccessible (root 不可) +adb -s 5HF6OVWCDECMJZ59 shell 'cat /proc/net/igmp | grep wlan0' # → 239.255.0.1 不在 + +# 2. host tshark 起動 +tshark -i wlp3s0 -f "udp portrange 7400-12500" \ + -w artifacts/discovery-capture//host-reliable.pcap -b duration:300 & + +# 3. perf-runner 計測 +dotnet run --project tools/rosettadds-perf-runner -c Release --no-build -- \ + --build-target Android \ + --scenario unity-to-ros2-reliable-32 \ + --android-device 5HF6OVWCDECMJZ59 \ + --artifacts artifacts/discovery-capture//reliable-32 \ + --capture-frames 200 + +# 4. tshark 停止 + 集計 +kill $TSHARK_PID +tshark -r artifacts/discovery-capture//host-reliable.pcap -Y "rtps" \ + -T fields -e ip.src -e udp.dstport | sort | uniq -c | sort -rn + +# 5. best-effort 同様に +# (Step 2-4 を best-effort-8192 で繰り返し) +``` + +## 計測 artifact + +- `artifacts/discovery-capture/20260627-231205/host-reliable_00001_20260627231808.pcap` (113 KB, 603 RTPS) +- `artifacts/discovery-capture/20260627-231205/host-best-effort_00001_20260627232823.pcap` (~1.9 MB, 1921 RTPS) +- `artifacts/discovery-capture/20260627-231205/analysis.json` (集計 JSON) +- `artifacts/discovery-capture/20260627-231205/hypothesis-table.md` (仮説判定) +- `artifacts/discovery-capture/20260627-231205/analysis-raw.md` (reliable raw) +- `artifacts/discovery-capture/20260627-231205/analysis-raw-best-effort.md` (best-effort raw) +- `artifacts/discovery-capture/pre-check.txt` (device 状態) +- Android 側 pcap 2 ファイル: 欠落 (root 不可、Task 4 参照) diff --git a/tools/discovery-capture/analyze.py b/tools/discovery-capture/analyze.py new file mode 100644 index 0000000..0d61c3f --- /dev/null +++ b/tools/discovery-capture/analyze.py @@ -0,0 +1,42 @@ +import csv +import io +from collections import Counter +from typing import Dict, List + + +def parse_tshark_csv(text: str) -> List[Dict[str, object]]: + rows: List[Dict[str, object]] = [] + reader = csv.reader(io.StringIO(text), delimiter="\t") + for parts in reader: + if len(parts) < 6: + continue + rows.append({ + "ts": parts[0], + "src": parts[1], + "dst": parts[2], + "sport": int(parts[3]) if parts[3] else 0, + "dport": int(parts[4]) if parts[4] else 0, + "sm_id": parts[5], + }) + return rows + + +def summarize_ports(rows: List[Dict[str, object]]) -> Dict[int, int]: + return dict(Counter(r["dport"] for r in rows)) + + +def summarize_src(rows: List[Dict[str, object]]) -> Dict[str, int]: + return dict(Counter(r["src"] for r in rows)) + + +def extract_pcap(pcap_path: str) -> List[Dict[str, object]]: + import subprocess + out = subprocess.check_output([ + "tshark", "-r", pcap_path, "-Y", "rtps", + "-T", "fields", + "-e", "frame.time_epoch", + "-e", "ip.src", "-e", "ip.dst", + "-e", "udp.srcport", "-e", "udp.dstport", + "-e", "rtps.sm.id", + ], text=True) + return parse_tshark_csv(out) diff --git a/tools/discovery-capture/discovery_capture.py b/tools/discovery-capture/discovery_capture.py new file mode 100644 index 0000000..0b767f4 --- /dev/null +++ b/tools/discovery-capture/discovery_capture.py @@ -0,0 +1,46 @@ +import argparse +from dataclasses import dataclass + + +@dataclass +class CapturePaths: + host_pcap: str + android_pcap: str + host_log: str + android_log: str + clock_pre_host: str + clock_pre_android: str + + +def parse_args(argv=None): + parser = argparse.ArgumentParser( + description="両側 capture 起動 + pcap pull ヘルパ" + ) + parser.add_argument("--host-interface", required=True) + parser.add_argument("--android-device", required=True) + parser.add_argument("--scenario", required=True, + choices=["unity-to-ros2-reliable-32", + "unity-to-ros2-best-effort-8192", + "ros2-to-unity-reliable-32", + "ros2-to-unity-best-effort-32k"]) + parser.add_argument("--run-id", required=True) + parser.add_argument("--udp-portrange", default="7400-12500") + return parser.parse_args(argv) + + +def build_paths(args, root: str) -> CapturePaths: + tag = "reliable" if "reliable" in args.scenario else "best-effort" + return CapturePaths( + host_pcap=f"{root}/host-{tag}.pcap", + android_pcap=f"{root}/android-{tag}.pcap", + host_log=f"{root}/host-tshark.log", + android_log=f"{root}/android-tcpdump.log", + clock_pre_host=f"{root}/host-clock-pre.txt", + clock_pre_android=f"{root}/android-clock-pre.txt", + ) + + +if __name__ == "__main__": + args = parse_args() + print(f"host={args.host_interface} android={args.android_device} " + f"scenario={args.scenario} run_id={args.run_id}") diff --git a/tools/discovery-capture/tests/test_analyze.py b/tools/discovery-capture/tests/test_analyze.py new file mode 100644 index 0000000..49a48ef --- /dev/null +++ b/tools/discovery-capture/tests/test_analyze.py @@ -0,0 +1,34 @@ +import pytest +from analyze import parse_tshark_csv, summarize_ports + + +SAMPLE_CSV = """1700000000.123456\t192.168.0.22\t239.255.0.1\t12345\t7400\t0x01 +1700000000.234567\t192.168.0.20\t239.255.0.1\t54321\t7400\t0x01 +1700000000.345678\t192.168.0.22\t239.255.0.1\t12345\t7410\t0x07 +1700000001.000000\t192.168.0.22\t239.255.0.1\t12345\t7401\t0x15 +""" + + +def test_parse_tshark_csv_basic(): + rows = parse_tshark_csv(SAMPLE_CSV) + assert len(rows) == 4 + assert rows[0]["src"] == "192.168.0.22" + assert rows[0]["dport"] == 7400 + assert rows[2]["sm_id"] == "0x07" + + +def test_summarize_ports(): + rows = parse_tshark_csv(SAMPLE_CSV) + summary = summarize_ports(rows) + assert summary[7400] == 2 + assert summary[7410] == 1 + assert summary[7401] == 1 + + +def test_summarize_src(): + from analyze import summarize_src + + rows = parse_tshark_csv(SAMPLE_CSV) + summary = summarize_src(rows) + assert summary["192.168.0.22"] == 3 + assert summary["192.168.0.20"] == 1 diff --git a/tools/discovery-capture/tests/test_discovery_capture.py b/tools/discovery-capture/tests/test_discovery_capture.py new file mode 100644 index 0000000..51044c5 --- /dev/null +++ b/tools/discovery-capture/tests/test_discovery_capture.py @@ -0,0 +1,65 @@ +import pytest +from discovery_capture import parse_args, build_paths, CapturePaths + + +def test_parse_args_reliable_scenario(): + args = parse_args([ + "--host-interface", "wlan0", + "--android-device", "5HF6OVWCDECMJZ59", + "--scenario", "unity-to-ros2-reliable-32", + "--run-id", "20260627-180000", + ]) + assert args.host_interface == "wlan0" + assert args.android_device == "5HF6OVWCDECMJZ59" + assert args.scenario == "unity-to-ros2-reliable-32" + assert args.run_id == "20260627-180000" + assert args.udp_portrange == "7400-12500" # default + + +def test_parse_args_custom_portrange(): + args = parse_args([ + "--host-interface", "wlan0", + "--android-device", "5HF6OVWCDECMJZ59", + "--scenario", "unity-to-ros2-reliable-32", + "--run-id", "20260627-180000", + "--udp-portrange", "7400-7500", + ]) + assert args.udp_portrange == "7400-7500" + + +def test_parse_args_missing_android_device(): + with pytest.raises(SystemExit): + parse_args([ + "--host-interface", "wlan0", + "--scenario", "unity-to-ros2-reliable-32", + "--run-id", "20260627-180000", + ]) + + +def test_build_paths_reliable(): + args = parse_args([ + "--host-interface", "wlan0", + "--android-device", "5HF6OVWCDECMJZ59", + "--scenario", "unity-to-ros2-reliable-32", + "--run-id", "20260627-180000", + ]) + paths = build_paths(args, root="/srv/run") + assert isinstance(paths, CapturePaths) + assert paths.host_pcap == "/srv/run/host-reliable.pcap" + assert paths.android_pcap == "/srv/run/android-reliable.pcap" + assert paths.host_log == "/srv/run/host-tshark.log" + assert paths.android_log == "/srv/run/android-tcpdump.log" + assert paths.clock_pre_host == "/srv/run/host-clock-pre.txt" + assert paths.clock_pre_android == "/srv/run/android-clock-pre.txt" + + +def test_build_paths_best_effort(): + args = parse_args([ + "--host-interface", "wlan0", + "--android-device", "5HF6OVWCDECMJZ59", + "--scenario", "unity-to-ros2-best-effort-8192", + "--run-id", "20260627-180000", + ]) + paths = build_paths(args, root="/srv/run") + assert paths.host_pcap == "/srv/run/host-best-effort.pcap" + assert paths.android_pcap == "/srv/run/android-best-effort.pcap"