From 857b79fbaf56d61cffb7734b0f0de8bf40995071 Mon Sep 17 00:00:00 2001 From: Mikkeli Date: Wed, 26 Aug 2026 19:35:41 +0900 Subject: [PATCH] docs: replace the pipe design with json_callback and note the embedding costs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/README.md | 58 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/docs/README.md b/docs/README.md index 321cfbc..9728d37 100644 --- a/docs/README.md +++ b/docs/README.md @@ -42,8 +42,9 @@ User taps GO → 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 + → 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 ``` @@ -61,16 +62,51 @@ 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`. Dart re-publishes each line on `Iperf3Service.outputLines`, so -the transcript grows while the test runs. +### 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. -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. +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 `unlink`ed 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