Files
iperf3-gui/docs/README.md
T
mikkeli 857b79fbaf docs: replace the pipe design with json_callback and note the embedding costs
The output-streaming section described the pipe and reader thread that no
longer exist, including the join-ordering rule that was the reason they were
dangerous. Replaced with how the json_callback path works and why, and an
explicit note not to reintroduce the pipe.

Adds a section on what embedding a CLI in a long-lived app process actually
costs, since that is the recurring trap here: iperf3's main() can leave
process-global state behind because it is about to exit, and this app cannot.
Records what iperf3 does clean up — joined stream threads, closed fds, an
immediately unlinked temp file — with the measurements taken on device, so the
next person does not have to re-derive whether repeated runs leak.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:35:41 +09:00

7.6 KiB

iperf3-gui

A Flutter-based Android GUI wrapper for iperf3, the popular network performance measurement tool.

Architecture

lib/                          # Flutter/Dart application layer
├── main.dart                 # App entry point (MaterialApp)
├── domain/                   # Business logic (platform-independent)
│   ├── 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)
└── ui/                       # Presentation layer
    └── features/
        └── test_run/
            ├── test_run_screen.dart          # Server/port inputs + GO button
            └── test_run_view_model.dart      # State machine: idle → running → completed/error

android/app/src/main/
├── kotlin/                     # Kotlin plugin layer
│   └── com/novoyuuparosk/iperf3gui/
│       ├── MainActivity.kt     # Registers IperfRunner as FlutterPlugin
│       └── IperfRunner.kt      # MethodChannel handler → native JNI calls
└── cpp/                        # Native iperf3 library
    ├── CMakeLists.txt          # Compiles iperf3 + cJSON + JNI into libiperf3jni.so
    ├── iperf_jni.c             # JNI bridge: Java String[] → iperf3 API
    ├── iperf/                  # iperf3 3.19 C source tree (see below)
    ├── iperf_config.h          # Generated Android-compatible config header
    └── iperf_config_android.h  # Android-specific compile options

Data Flow

User taps GO
  → Dart TestRunViewModel.runTest(config)
  → Iperf3Service.run(config) via MethodChannel
  → 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]
  → iperf3 json_callback → one JSON event per report → CallVoidMethod
  → main-thread hop → Dart onOutput → Iperf3Event.parse()
  → Iperf3Service.events → view model: transcript + interval series + summary
  → On completion: Dart Completer resolves with Iperf3Result

Key Design Decisions

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 jmethodIDs 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 via json_callback, not a pipe

The test runs with json_output and json_stream set and a json_callback installed, so iperf3 hands each report to the bridge as a JSON string and never writes to test->outfile. iperf.h describes the hook as "allow user apps to receive the JSON strings, instead of writing them to the output file" — this is its intended use.

An earlier design piped test->outfile to a reader thread. That imposed two constraints, both of which were gotten wrong and deadlocked every test: the pipe's write end had to be closed before joining a reader blocked in fgets(), and exactly one side could own the shared state struct. Reading from the callback removes the pipe, the reader thread and both constraints. Don't reintroduce it.

json_callback carries no user-data pointer, so the run state is reached through a global published before iperf_run_client() and cleared afterwards; only one test may run at a time, which runIperfLive enforces. The callback fires on iperf3's reporting thread, so it resolves a JNIEnv for the calling thread rather than reusing a stored one.

Dart parses each event into a sealed Iperf3Event; unrecognised output becomes an Iperf3LogLine rather than being discarded. The UI rebuilds iperf3's text layout from the events and keeps the interval series as data.

Embedding a CLI in a long-lived process

iperf3's main() runs one test and exits, so anything it leaves behind is reclaimed by the OS. Linked into an app process that runs tests repeatedly for hours, every such leftover becomes this app's problem. What that costs in practice:

  • Signal handlers are process-global. iperf_catch_sigend() and signal(SIGPIPE, SIG_IGN) change behaviour for the whole process, Flutter and ART included. Handlers are restored to SIG_DFL when a test ends.
  • g_sigend_jmp_buf outlives its frame. Harmless in a process about to exit; a jump into a dead stack frame otherwise. Same fix as above.
  • Globals mean one test at a time. i_errno and the bridge's own statics hold a single test's worth of state, so concurrent runs are refused.

What iperf3 itself handles correctly, verified rather than assumed: per-stream threads are joined (iperf_client_api.c), stream buffer fds are closed, and the mkstemp temp file is unlinked immediately after creation (iperf_api.c:4652), so nothing accumulates in the cache dir. Measured across four consecutive 4-stream runs on device, thread count returned to baseline (33 → 40 mid-run → 34) and fd count returned exactly to baseline (175 → 183 → 175).

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.

iperf3 Source

  • Version: 3.19
  • Location: android/app/src/main/cpp/iperf/
  • Files: Full C source tree including cJSON, headers, man pages, PEM keys
  • Build: CMake compiles into libiperf3jni.so for armeabi-v7a and arm64-v8a
  • Config: iperf_config.h generated from iperf_config.h.in with Android-compatible POSIX/threading macros

Native JNI API

Kotlin Method JNI Signature Purpose
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

Prerequisites:

  • Flutter 3.47+
  • Android SDK with platform android-36
  • JDK 17
  • NDK r28+
  • CMake 3.22.1
flutter pub get
flutter build apk --debug
adb install build/app/outputs/flutter-apk/app-debug.apk

Dependencies

  • Dart: provider
  • Android: CMake external native build
  • Native: iperf3 3.19, cJSON (single file)

License

MIT — bundled iperf3 3.19 is BSD-3-Clause licensed.