From 0dc747edefa37cc13c1f747bf4221f7f2001b66d Mon Sep 17 00:00:00 2001 From: Mikkeli Date: Wed, 26 Aug 2026 19:03:59 +0900 Subject: [PATCH] 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 --- README.md | 58 +++++++++++++++++++++++++++++++++++++++----------- docs/README.md | 40 ++++++++++++++++++++++++++-------- 2 files changed, 76 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index ae83ea0..dde45a0 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ # iperf3-gui -A Flutter-based Android GUI wrapper for iperf3, the popular network performance measurement tool. +A Flutter-based Android GUI wrapper for iperf3, the popular network performance +measurement tool. ## Features - Client-mode network throughput testing -- Simple UI: enter server address, port, and press GO -- Raw iperf3 output display -- Bundled iperf3 v3.21 binaries for arm64-v8a and armeabi-v7a +- Configurable server, port, duration and parallel streams +- Live iperf3 output, streamed line by line while the test runs +- Stop a running test early +- Copy the transcript to the clipboard +- iperf3 3.19 compiled into the app as a native library — no external binary ## Architecture @@ -15,33 +18,62 @@ A Flutter-based Android GUI wrapper for iperf3, the popular network performance lib/ ├── domain/ │ ├── models/ # TestConfiguration, Iperf3Result -│ └── services/ # Iperf3Service (Process runner) +├── data/ +│ └── services/ # Iperf3Service (MethodChannel → JNI) ├── ui/ │ └── features/ │ └── test_run/ # TestRunScreen, TestRunViewModel -├── main.dart # App entry point -└── pubspec.yaml # Dependencies: provider, path_provider +└── main.dart # App entry point + +android/app/src/main/ +├── kotlin/…/IperfRunner.kt # MethodChannel handler, runs tests off the platform thread +└── cpp/iperf_jni.c # JNI bridge into the iperf3 API ``` +See [docs/README.md](docs/README.md) for the data flow, the native API surface +and the design decisions behind them. + +## Why JNI instead of executing a binary + +Android SELinux in Enforcing mode blocks `execve()` from the `untrusted_app` +domain, so shipping an iperf3 ELF in assets and running it via `Process.start()` +fails with "Permission denied". iperf3 is therefore compiled as a shared library +(`libiperf3jni.so`) and called in-process over JNI. + +`assets/iperf3/` still holds the pre-compiled binaries from that earlier +approach. They are not declared in `pubspec.yaml`, so they are not packaged into +the APK, and nothing in the app reads them. + ## Building Prerequisites: -- Flutter 3.47+ +- Flutter 3.47+ - Android SDK with platform android-36 - JDK 17 +- NDK r28+ and CMake 3.22.1 (for the native build) ```bash -flutter create . --org novoyuuparosk --project-name iperf3gui flutter pub get flutter analyze +flutter test flutter build apk --debug +adb install -r build/app/outputs/flutter-apk/app-debug.apk ``` -### iperf3 Binaries +## Testing against a local server -Pre-compiled binaries are sourced from [davidBar-On/android-iperf3](https://github.com/davidBar-On/android-iperf3) -and bundled in `assets/iperf3/` (arm64-v8a and armeabi-v7a for Android 28+). +```bash +iperf3 -s -p 5201 # on a machine on the same network +ip -4 addr show # note its address +``` + +Enter that address in the app, then press GO. + +## Dependencies + +- Dart: `provider` +- Native: iperf3 3.19 + cJSON, built by CMake for `armeabi-v7a` and `arm64-v8a` ## License -MIT — bundled iperf3 v3.21 is BSD-3-Clause licensed. +MIT — bundled iperf3 3.19 is BSD-3-Clause licensed. diff --git a/docs/README.md b/docs/README.md index a0bdc5d..321cfbc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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` 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` 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)