docs: record that the core runs without the UI, and the CPU cost of the UI

States where run data lives now that Iperf3Session owns it, so the next person
does not put accumulation back in the view model, and documents the headless
entry point as the way to check that claim rather than trust it.

Includes the measurement that motivates using it: 30.2% local CPU headless
versus 79.9% through the UI on a comparable 4-stream run. That gap is worth
recording, because it means throughput read off the app's own screen is
pessimistic at high rates and should not be compared against a headless or
desktop figure without noting which path produced it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 20:25:06 +09:00
parent fc15897541
commit 6e35d724dc
+24 -1
View File
@@ -7,11 +7,13 @@ A Flutter-based Android GUI wrapper for iperf3, the popular network performance
``` ```
lib/ # Flutter/Dart application layer lib/ # Flutter/Dart application layer
├── main.dart # Composition root: picks the Iperf3Runner impl ├── main.dart # Composition root: picks the Iperf3Runner impl
├── main_headless.dart # Runs a test with no widgets (see Layering)
├── domain/ # Pure Dart — no Flutter import ├── domain/ # Pure Dart — no Flutter import
│ ├── models/ │ ├── models/
│ │ ├── test_configuration.dart # Server, port, duration, interval, parallel streams │ │ ├── test_configuration.dart # Server, port, duration, interval, parallel streams
│ │ ├── iperf3_result.dart # Raw output, exit code, stopped flag │ │ ├── iperf3_result.dart # Raw output, exit code, stopped flag
│ │ └── iperf3_event.dart # Parsed --json-stream reports │ │ ├── iperf3_event.dart # Parsed --json-stream reports
│ │ └── iperf3_session.dart # One run accumulated: samples, totals, stats
│ └── services/ │ └── services/
│ └── iperf3_runner.dart # The contract the UI depends on │ └── iperf3_runner.dart # The contract the UI depends on
├── data/ # Transport — flutter/services only ├── data/ # Transport — flutter/services only
@@ -69,6 +71,27 @@ 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 device — and what keeps a test double at five stable members instead of
mirroring the transport's whole surface. mirroring the transport's whole surface.
**The core does not need the UI.** What happened during a run is accumulated by
`Iperf3Session` in the domain layer — samples, totals, errors, and the mean,
peak and retransmit figures over them. The view model holds no run data of its
own and the screen derives no numbers; both read the session. Replacing the
presentation layer therefore means rewriting rendering, not re-deriving results.
`lib/main_headless.dart` is the standing proof: it runs a full test through
`Iperf3Service` and `Iperf3Session` with `WidgetsFlutterBinding` but no
`runApp` — no widget tree, no render surface. If the run path ever starts
needing the UI, it stops working.
```bash
flutter run -t lib/main_headless.dart \
--dart-define=IPERF_SERVER=192.168.2.182 --dart-define=IPERF_STREAMS=4
```
Use it when a figure has to be trustworthy. A 4-stream local run reports
**30.2% local CPU headless against 79.9% through the UI** — the phone is near
CPU-bound at gigabit rates, so a repainting transcript measurably depresses
throughput. Numbers taken through the UI are pessimistic.
`test/architecture_test.dart` enforces all of this by reading the actual `test/architecture_test.dart` enforces all of this by reading the actual
imports under `lib/`, including a check that no fourth layer directory has imports under `lib/`, including a check that no fourth layer directory has
appeared unnoticed. Change the layering there when the layering should change; appeared unnoticed. Change the layering there when the layering should change;