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:
@@ -1,13 +1,16 @@
|
|||||||
# iperf3-gui
|
# 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
|
## Features
|
||||||
|
|
||||||
- Client-mode network throughput testing
|
- Client-mode network throughput testing
|
||||||
- Simple UI: enter server address, port, and press GO
|
- Configurable server, port, duration and parallel streams
|
||||||
- Raw iperf3 output display
|
- Live iperf3 output, streamed line by line while the test runs
|
||||||
- Bundled iperf3 v3.21 binaries for arm64-v8a and armeabi-v7a
|
- 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
|
## Architecture
|
||||||
|
|
||||||
@@ -15,33 +18,62 @@ A Flutter-based Android GUI wrapper for iperf3, the popular network performance
|
|||||||
lib/
|
lib/
|
||||||
├── domain/
|
├── domain/
|
||||||
│ ├── models/ # TestConfiguration, Iperf3Result
|
│ ├── models/ # TestConfiguration, Iperf3Result
|
||||||
│ └── services/ # Iperf3Service (Process runner)
|
├── data/
|
||||||
|
│ └── services/ # Iperf3Service (MethodChannel → JNI)
|
||||||
├── ui/
|
├── ui/
|
||||||
│ └── features/
|
│ └── features/
|
||||||
│ └── test_run/ # TestRunScreen, TestRunViewModel
|
│ └── test_run/ # TestRunScreen, TestRunViewModel
|
||||||
├── main.dart # App entry point
|
└── main.dart # App entry point
|
||||||
└── pubspec.yaml # Dependencies: provider, path_provider
|
|
||||||
|
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
|
## Building
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
- Flutter 3.47+
|
- Flutter 3.47+
|
||||||
- Android SDK with platform android-36
|
- Android SDK with platform android-36
|
||||||
- JDK 17
|
- JDK 17
|
||||||
|
- NDK r28+ and CMake 3.22.1 (for the native build)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
flutter create . --org novoyuuparosk --project-name iperf3gui
|
|
||||||
flutter pub get
|
flutter pub get
|
||||||
flutter analyze
|
flutter analyze
|
||||||
|
flutter test
|
||||||
flutter build apk --debug
|
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)
|
```bash
|
||||||
and bundled in `assets/iperf3/` (arm64-v8a and armeabi-v7a for Android 28+).
|
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
|
## License
|
||||||
|
|
||||||
MIT — bundled iperf3 v3.21 is BSD-3-Clause licensed.
|
MIT — bundled iperf3 3.19 is BSD-3-Clause licensed.
|
||||||
|
|||||||
+31
-9
@@ -11,9 +11,9 @@ lib/ # Flutter/Dart application layer
|
|||||||
│ ├── models/
|
│ ├── models/
|
||||||
│ │ ├── test_configuration.dart # Server, port, duration, interval, parallel streams
|
│ │ ├── test_configuration.dart # Server, port, duration, interval, parallel streams
|
||||||
│ │ └── iperf3_result.dart # Raw output + exit code
|
│ │ └── iperf3_result.dart # Raw output + exit code
|
||||||
|
├── data/
|
||||||
│ └── services/
|
│ └── services/
|
||||||
│ ├── iperf3_service.dart # Dart → MethodChannel bridge (async with Completer)
|
│ └── iperf3_service.dart # Dart → MethodChannel bridge (async with Completer)
|
||||||
│ └── iperf3_binary_path_provider.dart # Deprecated (kept for reference)
|
|
||||||
└── ui/ # Presentation layer
|
└── ui/ # Presentation layer
|
||||||
└── features/
|
└── features/
|
||||||
└── test_run/
|
└── test_run/
|
||||||
@@ -39,10 +39,11 @@ android/app/src/main/
|
|||||||
User taps GO
|
User taps GO
|
||||||
→ Dart TestRunViewModel.runTest(config)
|
→ Dart TestRunViewModel.runTest(config)
|
||||||
→ Iperf3Service.run(config) via MethodChannel
|
→ Iperf3Service.run(config) via MethodChannel
|
||||||
→ Kotlin IperfRunner.runIperfLive(args, callback)
|
→ Kotlin IperfRunner: submits to the iperf3-run worker thread, returns at once
|
||||||
→ JNI C: java_string_array_to_argv() → iperf_parse_arguments()
|
→ JNI C: build_argv() (prepends argv[0]) → iperf_parse_arguments()
|
||||||
→ iperf_run_client(test)
|
→ iperf_run_client(test) [blocks on the worker thread]
|
||||||
→ Reader thread pipes stdout → CallVoidMethod → Dart onOutput callbacks
|
→ 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
|
→ On completion: Dart Completer resolves with Iperf3Result
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -51,11 +52,31 @@ User taps GO
|
|||||||
### JNI Over Process Execution
|
### 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.
|
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
|
### 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.
|
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
|
### 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
|
### 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.
|
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 |
|
| 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() |
|
| `forceStopIperfTest(IperfCallback)` | `forceStopIperfTest` | Graceful stop via shutdown() |
|
||||||
| `isTestRunning()` | `isTestRunning` | Check if test is in progress |
|
| `isTestRunning()` | `isTestRunning` | Check if test is in progress |
|
||||||
|
| `setTempDir(String)` | `setTempDir` | Point `mkstemp()` at the app cache dir |
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
@@ -93,7 +115,7 @@ adb install build/app/outputs/flutter-apk/app-debug.apk
|
|||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- Dart: `provider`, `path_provider`
|
- Dart: `provider`
|
||||||
- Android: CMake external native build
|
- Android: CMake external native build
|
||||||
- Native: iperf3 3.19, cJSON (single file)
|
- Native: iperf3 3.19, cJSON (single file)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user