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
|
→ Kotlin IperfRunner: submits to the iperf3-run worker thread, returns at once
|
||||||
→ JNI C: build_argv() (prepends argv[0]) → iperf_parse_arguments()
|
→ JNI C: build_argv() (prepends argv[0]) → iperf_parse_arguments()
|
||||||
→ iperf_run_client(test) [blocks on the worker thread]
|
→ iperf_run_client(test) [blocks on the worker thread]
|
||||||
→ Reader thread pipes output → CallVoidMethod → main-thread hop → Dart onOutput
|
→ iperf3 json_callback → one JSON event per report → CallVoidMethod
|
||||||
→ Dart streams each line to the UI via Iperf3Service.outputLines
|
→ main-thread hop → Dart onOutput → Iperf3Event.parse()
|
||||||
|
→ Iperf3Service.events → view model: transcript + interval series + summary
|
||||||
→ On completion: Dart Completer resolves with Iperf3Result
|
→ 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
|
### 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 via json_callback, not a pipe
|
||||||
iperf3 output is redirected via pipe to a background reader thread that attaches
|
The test runs with `json_output` and `json_stream` set and a `json_callback`
|
||||||
to the Java VM via `AttachCurrentThread` and invokes callbacks via
|
installed, so iperf3 hands each report to the bridge as a JSON string and never
|
||||||
`CallVoidMethod`. Dart re-publishes each line on `Iperf3Service.outputLines`, so
|
writes to `test->outfile`. `iperf.h` describes the hook as "allow user apps to
|
||||||
the transcript grows while the test runs.
|
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
|
An earlier design piped `test->outfile` to a reader thread. That imposed two
|
||||||
must be closed **before** `pthread_join()` — joining first deadlocks the worker
|
constraints, both of which were gotten wrong and deadlocked every test: the
|
||||||
thread. The JNI caller solely owns the run-state struct and frees it after the
|
pipe's write end had to be closed *before* joining a reader blocked in
|
||||||
join; the reader must never free it.
|
`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
|
### Stop Handling
|
||||||
A stop shuts down the control socket, so `iperf_run_client()` returns -1 with
|
A stop shuts down the control socket, so `iperf_run_client()` returns -1 with
|
||||||
|
|||||||
Reference in New Issue
Block a user