docs: describe the JNI architecture the app actually uses

The root README still documented the abandoned approach: iperf3 3.21 binaries
bundled in assets and run through a Process runner, and a lib/ layout that no
longer matched. It now covers the JNI design, the current feature set and how
to test against a local iperf3 server, and says plainly that the binaries
still sitting in assets/iperf3/ are undeclared in pubspec.yaml and therefore
never reach the APK.

docs/README.md gains the threading, pipe-shutdown and stop-handling
constraints that the fixes on this branch depend on, so the next change does
not have to rediscover why the join order and the worker thread matter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 19:03:59 +09:00
parent 6f809913bd
commit 0dc747edef
2 changed files with 76 additions and 22 deletions
+31 -9
View File
@@ -11,9 +11,9 @@ lib/ # Flutter/Dart application layer
│ ├── models/
│ │ ├── test_configuration.dart # Server, port, duration, interval, parallel streams
│ │ └── iperf3_result.dart # Raw output + exit code
├── data/
│ └── services/
│ ├── iperf3_service.dart # Dart → MethodChannel bridge (async with Completer)
│ └── iperf3_binary_path_provider.dart # Deprecated (kept for reference)
│ └── iperf3_service.dart # Dart → MethodChannel bridge (async with Completer)
└── ui/ # Presentation layer
└── features/
└── test_run/
@@ -39,10 +39,11 @@ android/app/src/main/
User taps GO
→ Dart TestRunViewModel.runTest(config)
→ Iperf3Service.run(config) via MethodChannel
→ Kotlin IperfRunner.runIperfLive(args, callback)
→ JNI C: java_string_array_to_argv() → iperf_parse_arguments()
→ iperf_run_client(test)
→ Reader thread pipes stdout → CallVoidMethod → Dart onOutput callbacks
→ Kotlin IperfRunner: submits to the iperf3-run worker thread, returns at once
→ JNI C: build_argv() (prepends argv[0]) → iperf_parse_arguments()
→ iperf_run_client(test) [blocks on the worker thread]
→ Reader thread pipes output → CallVoidMethod → main-thread hop → Dart onOutput
→ Dart streams each line to the UI via Iperf3Service.outputLines
→ On completion: Dart Completer resolves with Iperf3Result
```
@@ -51,11 +52,31 @@ User taps GO
### JNI Over Process Execution
Android SELinux Enforcing mode blocks `execve()` from the `untrusted_app` domain, making Process.start() of external binaries fail with Permission denied. Compiling iperf3 as a shared library linked into the app process eliminates this restriction entirely — iperf3 code runs in-process with no domain transitions needed.
### Threading
`runIperfLive()` blocks for the whole test, so Kotlin submits it to a dedicated
single-thread executor and hands control straight back to Dart. Running it on the
Flutter platform thread froze the UI and — because output is delivered by posting
to the main looper — stalled every callback until the test was already over.
### Callback Mechanism
The JNI bridge uses `CallVoidMethod` on stored `jmethodID`s to invoke the Kotlin `IperfCallback` interface, NOT function pointer casts. Direct function pointer casts caused SIGBUS crashes on ARM due to JNINativeInterface method ID layout differences.
### Output Streaming
iperf3 output is redirected via pipe to a background reader thread that attaches to the Java VM via `AttachCurrentThread` and invokes callbacks via `CallVoidMethod`. The reader thread frees the state struct on exit; the JNI caller owns the callback object's lifetime.
iperf3 output is redirected via pipe to a background reader thread that attaches
to the Java VM via `AttachCurrentThread` and invokes callbacks via
`CallVoidMethod`. Dart re-publishes each line on `Iperf3Service.outputLines`, so
the transcript grows while the test runs.
The reader blocks in `fgets()` and only unblocks on EOF, so the pipe's write end
must be closed **before** `pthread_join()` — joining first deadlocks the worker
thread. The JNI caller solely owns the run-state struct and frees it after the
join; the reader must never free it.
### Stop Handling
A stop shuts down the control socket, so `iperf_run_client()` returns -1 with
"Broken pipe". That is the expected outcome of a stop, not a failure, so the
bridge checks its own `g_stop` flag and reports a stop rather than an error; the
flag travels up to the UI as `Iperf3Result.stopped`.
### Async Pattern
Dart uses `Completer<Iperf3Result>` to await native completion. The MethodChannel callback handler resolves the completer on `onComplete`, with a timeout fallback that calls `stopIperf` on the native layer.
@@ -72,9 +93,10 @@ Dart uses `Completer<Iperf3Result>` to await native completion. The MethodChanne
| Kotlin Method | JNI Signature | Purpose |
|---|---|---|
| `runIperfLive(String[], IperfCallback)` | `runIperfLive` | Start test with args and callback |
| `runIperfLive(String[], IperfCallback)` | `runIperfLive` | Run a test to completion — blocks, call off the platform thread |
| `forceStopIperfTest(IperfCallback)` | `forceStopIperfTest` | Graceful stop via shutdown() |
| `isTestRunning()` | `isTestRunning` | Check if test is in progress |
| `setTempDir(String)` | `setTempDir` | Point `mkstemp()` at the app cache dir |
## Building
@@ -93,7 +115,7 @@ adb install build/app/outputs/flutter-apk/app-debug.apk
## Dependencies
- Dart: `provider`, `path_provider`
- Dart: `provider`
- Android: CMake external native build
- Native: iperf3 3.19, cJSON (single file)