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>
This commit is contained in:
+47
-11
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user