Files
iperf3-gui/docs
mikkeli fe042efc29 docs: record the layering rule and where it is enforced
The architecture section listed files but said nothing about which layer may
import what, so the direction of dependency was only visible by reading the
imports. States the rule as a table, notes that the UI depends on the
Iperf3Runner contract rather than on a transport and what that buys, and points
at the test that enforces it so the rule is not just prose.

Also brings the tree up to date: it was missing the domain services directory,
the event model and the formatter.

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

iperf3-gui

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

Architecture

lib/                          # Flutter/Dart application layer
├── main.dart                 # Composition root: picks the Iperf3Runner impl
├── domain/                   # Pure Dart — no Flutter import
│   ├── models/
│   │   ├── test_configuration.dart   # Server, port, duration, interval, parallel streams
│   │   ├── iperf3_result.dart        # Raw output, exit code, stopped flag
│   │   └── iperf3_event.dart         # Parsed --json-stream reports
│   └── services/
│       └── iperf3_runner.dart        # The contract the UI depends on
├── data/                     # Transport — flutter/services only
│   └── services/
│       └── iperf3_service.dart       # Iperf3Runner over MethodChannel → JNI
└── ui/                       # Presentation
    └── features/
        └── test_run/
            ├── test_run_screen.dart            # Inputs, GO/STOP, live transcript
            ├── test_run_view_model.dart        # idle → running → completed/error
            └── iperf3_event_formatter.dart     # Events → iperf3-style text

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

Layering

Dependencies point inward: ui → domain ← data, never the reverse.

Layer May import Holds
domain nothing else in this package, no Flutter models and the Iperf3Runner contract
data domain, package:flutter/services.dart the MethodChannel/JNI implementation
ui domain widgets, view model, formatting
main.dart everything the single place that picks an implementation

The UI depends on Iperf3Runner, not on Iperf3Service, so presentation names no transport. That is what lets test/widget_test.dart drive the real widget tree against an in-memory runner with no MethodChannel, native library or device — and what keeps a test double at five stable members instead of mirroring the transport's whole surface.

test/architecture_test.dart enforces all of this by reading the actual imports under lib/, including a check that no fourth layer directory has appeared unnoticed. Change the layering there when the layering should change; don't work around it.

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.