Commit Graph

51 Commits

Author SHA1 Message Date
mikkeli 66826bbce6 ci: add a provisioning script for an x86_64 Android build runner
Gitea runs on an aarch64 host, and Flutter cannot build an Android APK on a
Linux arm64 host at all: the SDK ships no arm64-host gen_snapshot
(flutter/flutter#189724, #75864, open as of July 2026). So CI for this repo was
failing for two independent reasons, and fixing the runner's container network
would only have exposed the second. An x86_64 runner is a requirement here, not
a preference.

Registers at android-stuff org scope rather than repo scope, so any Android
repo in the org can use the same runner, and labels it by capability
(flutter-android) rather than by hostname, so the workflow does not encode
which machine happens to provide it.

Host mode rather than a container: the machine already has a proven Flutter,
JDK and Android SDK/NDK, so jobs build with the same toolchain that produced
the verified release and skip a per-run NDK download. The trade is isolation,
which the header states plainly — workflows run as that user on that machine.

Refuses to register unless the toolchain it would advertise is actually
present, so a missing SDK fails at setup with a list rather than inside every
later job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 23:21:17 +09:00
mikkeli 85dc74247c Merge branch 'ci/release-apk-and-jni-name-guard'
CI / Analyze & Test (push) Failing after 28s
CI / Release APK (push) Failing after 1m48s
Add a CI job that builds the release APK and verifies the names JNI resolves
survived R8, so the broken-release-APK failure mode cannot recur silently.

CI's existing failures are unrelated to workflow content: every run has failed
at actions/checkout because the runner's job containers cannot resolve the
'gitea' hostname. That needs container.network set in the runner's config.yaml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 22:54:33 +09:00
mikkeli 94d96297f8 ci: build the release APK and check the JNI names
CI only ran analyze and test, so it would have passed for every commit that
carried the broken release APK. Adds a release-apk job that builds the
minified artifact, runs tool/check_jni_keep_rules.sh against it, and uploads
it — turning "the shipping artifact works" into something checked rather than
assumed at tag time.

Enables flutter-action's dependency cache on both jobs, and notes that the
test step now includes the layering guard.

This does not fix the current failures: every run so far has failed at
actions/checkout, before Flutter is reached, because the runner's job
containers cannot resolve the `gitea` hostname. That is runner configuration,
not workflow content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 22:54:19 +09:00
mikkeli a96f302dc7 test: guard the JNI-visible names against R8 renaming
A renamed JNI callback does not fail the build — it produces a release APK
that aborts on the first callback of every test. That shipped once, because
every on-device check used the debug build, which does not minify.

This reads R8's own mapping file for the classes and callback methods, and the
DEX string pool for the native entry points. Native methods are deliberately
not checked against mapping.txt: R8 only emits entries for members with a body
or a rename, so a native method is absent either way and absence would prove
nothing.

Verified by deleting the callback keep rules and rebuilding: the check reports
MainThreadCallback renamed along with onOutput, onError and onComplete, which
is exactly the failure that shipped — with no device involved.

Also avoids `... | strings | grep -q` and dumps to a file first. Under
`set -o pipefail`, grep -q exits at the first match and SIGPIPEs strings, so
the pipeline reports failure for a name found early and success for one found
late. It reported two of four names missing while all four were present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 22:54:19 +09:00
mikkeli 7ca74366a3 Merge branch 'fix/release-build-jni-keep-rules'
CI / Analyze & Test (push) Failing after 30s
Release builds aborted on the first callback of every test: R8 renamed the
IperfCallback implementation and its methods, so the JNI GetMethodID lookups
resolved nothing. Adds the keep rules, and makes the native side report a
failed lookup instead of aborting the process on the next JNI call.

Debug builds do not minify, so every on-device check until now passed while the
shipping artifact was broken. Found by running the release APK during release
preparation.

Verified with the release APK on an XQ-AS72: clean run at 806 Mbits/sec,
matching the server's own figure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.1.0
2026-08-26 22:43:52 +09:00
mikkeli ff13d63333 fix(jni): fail cleanly when a callback method does not resolve
GetMethodID raises NoSuchMethodError when a lookup fails, and any further JNI
call made while an exception is pending aborts the process. runIperfLive
resolved three method IDs back to back without checking, so the first failure
made the second call fatal — turning a diagnosable configuration problem into
a SIGABRT with a stack that pointed at the JNI entry rather than the cause.

resolve_method() clears the pending exception and returns NULL, and the caller
now bails out with a log naming which lookups failed and pointing at the keep
rules. The R8 problem this exposed is fixed separately; this is about the
failure being reportable rather than fatal, whatever the cause.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 22:43:35 +09:00
mikkeli 2b3f20d3a0 fix(android): keep the JNI callback names through R8
Release builds aborted on the first callback of every test:

  NoSuchMethodError: no non-static method "Lp0;.onOutput(Ljava/lang/String;)V"

R8 had renamed MainThreadCallback to p0 along with its methods, so
GetMethodID(env, cls, "onOutput", ...) in iperf_jni.c resolved nothing. The
names are a contract between C and Kotlin that the minifier cannot see.

Adds keep rules for IperfRunner, the IperfCallback interface and — the part
that matters — everything implementing it, since the lookup happens on the
concrete object's class rather than on the interface. A general rule keeps any
class declaring a native method, so a future one is covered without a new rule.

Debug builds do not minify, which is why every on-device check so far passed
while the shipping artifact was broken. Found by running the release APK
during release preparation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 22:43:35 +09:00
mikkeli 894712b7a8 chore: set version to 0.1.0 for the first release
pubspec.yaml still carried flutter create's placeholder 1.0.0+1, which would
have had the APK reporting 1.0.0 while the release was tagged v0.1.0. Matches
the version the tag records.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 22:38:48 +09:00
mikkeli 895b963745 fix: restore speedtest.uztelecom.uz as the default server
Consolidating the UI's seed values onto TestConfiguration's own defaults also
silently changed the default target to iperf.es.net, which was not asked for.
Sets it back in TestConfiguration, which is now the single definition the
screen and the headless entry point both read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 22:38:19 +09:00
mikkeli b76098c7e6 Merge branch 'refactor/core-independent-of-ui'
CI / Analyze & Test (push) Failing after 30s
Make the core work independently of the UI: Iperf3Session accumulates a run in
the domain layer, so the view model holds no run data and the screen derives no
numbers. Replacing the presentation layer now means rewriting rendering, not
re-deriving results.

Adds lib/main_headless.dart, which runs a full test through Iperf3Service and
Iperf3Session with no widget tree, as a standing check on that claim — and as
the accurate way to measure: 30.2% local CPU headless against 79.9% through the
UI on a comparable 4-stream run.

Verified on an XQ-AS72: headless run clean at 859 Mbit/s mean, matching the
server's own 859 Mbits/sec receiver figure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 20:25:24 +09:00
mikkeli 6e35d724dc 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>
2026-08-26 20:25:06 +09:00
mikkeli fc15897541 feat: add a headless entry point that runs a test with no UI
Separation that nothing exercises is separation that quietly stops being true,
so this 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, this stops working. An architecture test
additionally forbids it importing lib/ui.

It is also the accurate way to measure. A 4-stream run against a local server
reports 30.2% local CPU here, against 79.9% for a comparable run with the
transcript repainting alongside — the UI was costing roughly fifty points of
CPU on a phone already near CPU-bound at gigabit rates. Throughput figures
taken through the UI are therefore pessimistic, and this is the path to use
when the number matters.

Parameters come from --dart-define so a run needs no code edit:
  flutter run -t lib/main_headless.dart --dart-define=IPERF_SERVER=…

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 20:24:44 +09:00
mikkeli 9485049b57 refactor: move run accumulation out of the view model into the domain
The view model was doing two jobs. Alongside presentation state it held what
happened during a run — the interval series, the rule that iperf3's omitted
warm-up intervals do not count, the retained end summary — and the screen
computed mean and peak inline in a widget builder. All of that is the part
that has to be correct, and all of it would have been rewritten by anyone
replacing the UI.

Iperf3Session accumulates a run in the domain layer: samples, totals, errors,
whether it was stopped, and the statistics over them. The view model now feeds
it events and keeps only a formatted transcript and the repaint schedule; the
screen reads every displayed number off the session and derives none.

Two details the session now owns rather than leaving to a caller. Omitted
intervals are excluded, so a warm-up cannot drag the mean down. And
totalRetransmits stays null when no interval reported a count — UDP does not
measure retransmits, and "not measured" must not render as zero.

The aggregation tests move with the logic, into a pure-Dart test with no
widgets or bindings involved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 20:24:44 +09:00
mikkeli 838c883cc0 Merge branch 'refactor/separate-ui-from-data'
CI / Analyze & Test (push) Failing after 38s
Separate presentation from the data layer: the UI depends on an Iperf3Runner
interface declared in the domain layer, and main.dart is the only file that
names a concrete implementation.

Adds widget tests that drive the real UI against an in-memory runner with no
MethodChannel or native library, and an architecture test that reads the
imports under lib/ and fails the build when a layer boundary is crossed.

Verified on an XQ-AS72 after the change: clean run, iperf_run_client returned
0, server logged 608 Mbits/sec, no ANR or native abort.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:56:41 +09:00
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
mikkeli 0cf019379a test: fail the build when a layer boundary is crossed
The layering only holds if something checks it, so these tests read the real
import statements under lib/ and assert the direction: domain imports no
Flutter and nothing else in the package, data never reaches into ui and may
import flutter/services alone, and ui never names a concrete transport.

A final test asserts the set of directories under lib/ is exactly the three
layers, so a new one cannot appear and silently escape every rule above.

Each violation reports the offending file and import with the reason, since a
guard that only says "expected empty" costs more than it saves. Verified by
introducing a UI-to-data import and a Flutter import in domain and confirming
both fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:56:03 +09:00
mikkeli 95acf5ea38 test: drive the real UI against an in-memory runner
FakeIperf3Runner implements Iperf3Runner, so the widget tests exercise the
actual widget tree — GO, STOP, the status line, the stat row, validation —
with no MethodChannel and no native library. That was impossible while the
screen constructed its own Iperf3Service.

The view model tests share the same fake instead of each file carrying a
double that had to mirror the concrete service.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:56:03 +09:00
mikkeli 314a3ea059 refactor: depend on an Iperf3Runner interface instead of the service
The presentation layer imported Iperf3Service directly, so the UI was welded
to the MethodChannel transport: the view model named it, and the screen
constructed one. Dependency direction was already inward-only, but the UI
depended on an implementation rather than a contract.

Iperf3Runner states that contract in the domain layer in five members, in pure
Dart with no Flutter import, and Iperf3Service implements it. The view model
and screen now take an Iperf3Runner; main.dart is the only file that names a
concrete one, which makes it the composition root.

Two consequences worth having. The UI can be driven against an in-memory
runner with no MethodChannel, native library or device involved. And a test
double implements five stable members rather than tracking every public member
the transport happens to expose — the previous double, forced to implement the
concrete class, broke twice while this branch's work was going on.

Also drops flutter/foundation from the data layer: debugPrint gives way to
dart:developer, leaving flutter/services (where MethodChannel lives) as that
layer's only Flutter dependency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:56:03 +09:00
mikkeli defd64a073 Merge branch 'refactor/json-stream-events'
CI / Analyze & Test (push) Failing after 34s
Replace the stdout pipe with iperf3's json_callback hook and consume the
resulting --json-stream events as structured data: interval samples with
retransmits and per-stream detail, and an end summary with sent/received
totals and CPU utilisation.

Deletes the pipe, the reader thread and the join-ordering and ownership
constraints that came with them.

Verified on an XQ-AS72 against a local iperf3 server: 4 streams at 889
Mbits/sec, matching the server's own 887 Mbits/sec receiver figure. Across
four consecutive runs thread count returned to baseline and fd count returned
exactly to baseline, with no ANR or native abort.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:35:55 +09:00
mikkeli 857b79fbaf 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>
2026-08-26 19:35:41 +09:00
mikkeli cbb2aba8b0 feat(ui): render the event stream and show live throughput figures
Consuming JSON means the familiar iperf3 text layout has to be rebuilt, so a
pure formatter turns each event back into a line — binary units for transfer
totals, decimal units for bitrates, matching how iperf3 quotes each. The view
model keeps the interval series and the end summary as data alongside the
transcript, and a stat row shows current/total, mean, peak and sample count
off that series. A chart can be driven from the same list without re-parsing
text.

Repaints are coalesced on a 250ms timer rather than one per event. Reports can
now arrive several times a second across many streams, and each rebuild
re-renders the whole transcript, so notifying per event made the cost of
drawing scale with the report rate — which competes with the test itself for
CPU and skews the measurement.

Warm-up intervals (-O) are marked omitted by iperf3 and are kept out of the
series so they cannot drag the mean down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:35:10 +09:00
mikkeli 7d8d06e2e5 feat(app): parse json-stream reports into structured events
The native layer now emits one JSON object per report, so the service parses
each into a sealed Iperf3Event — start, interval, end, error — and publishes
those instead of opaque lines. Interval samples carry bytes, bits per second,
retransmits and per-stream socket/cwnd/rtt detail; the end event carries sent
and received totals and CPU utilisation. None of that was available while the
output was scraped text.

Anything unrecognised — non-JSON, malformed JSON, an event name a future
iperf3 adds — becomes an Iperf3LogLine rather than being dropped, so
unexpected output still reaches the user.

Failures arrive as an `error` event rather than a non-zero return from
iperf_run_client, so that event is what now marks a run as failed. The
result's rawOutput keeps the JSON lines verbatim, since that is what the
native layer actually produced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:35:10 +09:00
mikkeli 598a2d99c9 refactor(jni): take results from json_callback instead of piping stdout
iperf3 exposes a hook for exactly this use — iperf.h describes json_callback
as "allow user apps to receive the JSON strings, instead of writing them to
the output file". Setting it alongside --json-stream's two flags makes iperf3
hand each report ("start", "interval", "end", "error") straight to the bridge
as a JSON string, and test->outfile is never touched.

That removes the pipe, the reader thread, the fdopen/fgets loop, and
AttachCurrentThread from the output path — roughly 90 lines. It also removes
the two constraints that came with them and were both gotten wrong: that the
write end must be closed before joining a reader blocked in fgets(), and that
exactly one side may own the shared state struct. Neither can be violated now
because neither exists.

json_callback carries no user-data pointer, so the run state is reached
through a global published before iperf_run_client() and cleared after it.
Only one test runs at a time, which runIperfLive already enforced. The
callback fires on iperf3's reporting thread, so it resolves a JNIEnv for the
calling thread rather than trusting a stored one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:35:10 +09:00
mikkeli d448d5f885 Merge branch 'fix/iperf3-test-execution'
CI / Analyze & Test (push) Failing after 34s
Make iperf3 tests actually run on device: fix the end-of-test deadlock and
memory ownership in the JNI bridge, move the blocking test run off the
Flutter platform thread, stream output to the UI as it arrives, and report a
user-requested stop as a stop rather than a failure.

Verified end to end on an XQ-AS72 against a local iperf3 server: 790 Mbits/sec
over a full 10s run, mid-run stop, and back-to-back runs, with no ANR or
native abort in logcat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:08:38 +09:00
mikkeli 0dc747edef docs: describe the JNI architecture the app actually uses
The root README still documented the abandoned approach: iperf3 3.21 binaries
bundled in assets and run through a Process runner, and a lib/ layout that no
longer matched. It now covers the JNI design, the current feature set and how
to test against a local iperf3 server, and says plainly that the binaries
still sitting in assets/iperf3/ are undeclared in pubspec.yaml and therefore
never reach the APK.

docs/README.md gains the threading, pipe-shutdown and stop-handling
constraints that the fixes on this branch depend on, so the next change does
not have to rediscover why the join order and the worker thread matter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:03:59 +09:00
mikkeli 6f809913bd chore: drop leftovers from the pre-JNI binary-execution approach
iperf3_binary_path_provider.dart held no code, only a comment saying the
extract-and-exec approach it belonged to had been replaced. path_provider was
a dependency of that same approach and no longer has a single usage; removing
it regenerates the desktop plugin registrants.

Also drops an unused import in main.dart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:03:59 +09:00
mikkeli 02a8ba9c18 fix: report a user-requested stop as stopped rather than failed
Stopping a test shuts down the control socket, so iperf_run_client() returns
-1 with "unable to send control message ... Broken pipe". That is the
expected consequence of the stop, not a fault, but the bridge read the
negative return as a failure — so pressing STOP left the UI showing
"Error (exit 1)" above a broken-pipe message.

The bridge now consults its own g_stop flag before classifying the outcome,
and the distinction travels up as Iperf3Result.stopped so the status line can
say "Stopped". A stop is reported with exit code 0: the run ended the way the
user asked it to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:03:59 +09:00
mikkeli fb6ec3af37 feat(ui): show live output, add a stop button and duration/stream inputs
The screen could only start a 10s single-stream test and read the result
afterwards. It now renders the transcript as it arrives in a scrolling
monospace view that follows the newest line, exposes the duration and
parallel-stream settings TestConfiguration already carried, can stop a run in
progress, and copies the transcript to the clipboard.

The output field previously built a TextEditingController inside build(), so
every rebuild — one per output line now that output streams — discarded and
replaced it. Replaced with SelectableText, which needs no controller.

Seeds the inputs from TestConfiguration's own defaults rather than a second
hardcoded server address, so there is one default to change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:03:22 +09:00
mikkeli 3b01b0c4a6 fix(app): stream native output and release run state on every exit path
_isRunning was never cleared on the timeout path, so a single timed-out run
locked the app out of starting another until it was restarted. It is now
cleared in a finally block covering all three exits.

Output was buffered inside the service and only handed over once the run
finished, so the UI showed nothing until the end despite the native layer
streaming line by line. The service republishes each line on outputLines and
the view model appends to a transcript as they arrive.

The channel handler was re-registered on every run and dereferenced
_runCompleter with `!`, which threw once a late callback arrived after the
field had been nulled. It is registered once in the constructor and reads the
completer through a local.

Also drops the duplicated argument list in favour of
TestConfiguration.toCliArgs(), so the flags the UI sends have one definition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:03:22 +09:00
mikkeli f410a7e0d3 fix(android): run iperf3 tests off the Flutter platform thread
runIperfLive() blocks for the full duration of a test, but the MethodChannel
handler called it directly on the platform thread. That froze the UI for the
whole run, and because output reaches Dart by posting to the main looper, it
also meant no line could be delivered until the test was already over — the
live output the bridge streams had nowhere to go. Tests now run on a
dedicated single-thread executor and the channel call returns immediately.

MainThreadCallback's constructor properties were named onOutput/onError/
onComplete, the same names as the interface methods overriding them, so
`mainHandler.post { onOutput(line) }` resolved to the member function rather
than the lambda and re-posted itself instead of reaching the channel. Renamed
to emit* so the call cannot bind to the wrong declaration.

A failed System.loadLibrary now surfaces as a NATIVE_LIB_MISSING channel
error rather than leaving every later call to hang, and detaching from the
engine stops an in-flight test whose callbacks would fire into a dead
channel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:03:22 +09:00
mikkeli 2bba647a7b fix(jni): close the output pipe before joining the reader thread
Every test hung on the thread that started it. iperf_cleanup_and_signal()
called pthread_join() on the reader thread before closing the pipe's write
end, but the reader blocks in fgets() until EOF and EOF only arrives once
that write end is closed — two lines further down, after the join. Neither
side could ever make progress.

The reader also freed the shared state struct on exit while the caller went
on to read state->outfile and state->callback_ref and free it a second time,
so any path that got past the join corrupted the heap. Ownership now sits
solely with the JNI caller, which frees the struct after the join.

On the longjmp path outfile was fclose'd twice, once inline and once inside
the cleanup helper, so close_outfile() is idempotent and the hand-rolled
unwinding is gone. Signal handlers are restored to SIG_DFL once a test ends,
because leaving them armed pointed a later SIGTERM at a jmp_buf whose stack
frame no longer existed.

g_test is now guarded by a mutex and a second concurrent run is refused: a
stop request arrives on the platform thread while the test runs elsewhere,
which would otherwise race the worker retiring the pointer. Startup failures
report through onError/onComplete instead of returning silently, so Dart
resolves its completer rather than waiting out a timeout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 19:03:22 +09:00
irene af26a1c385 fix(jni): set app cache dir as temp dir and wrap callbacks in Handler
CI / Analyze & Test (push) Failing after 43s
- Set app cache dir as temp directory for mkstemp() to avoid EACCES
  on Android sandbox (iperf_set_test_template).
- Wrap all MethodChannel callbacks in MainThreadCallback with
  Handler(Looper.getMainLooper()) to prevent SIGABRT from calling
  invokeMethod from native reader thread.
- Add setTempDir JNI function and logging for test return value.
2026-08-26 18:30:20 +09:00
irene a31cdef775 fix: resolve argv[0] misparse and SIGSEGV hang in JNI bridge
Two root causes prevented iperf3 tests from running:

1. argv[0] bug: iperf_parse_arguments() expects argv[0] to be the
   program name. We passed Dart args directly, so argv[0] was '-c',
   which iperf3 misinterpreted as the program name → 'Argument parse
   error: must either be a client (-c) or server (-s)'. Fixed by
   prepending "iperf3" as argv[0] and shifting user args to argv[1+].

2. Signal handling: iperf_catch_sigend() installs handlers that use
   longjmp() to a jmp_buf, but we never initialized the buffer with
   setjmp(). Any signal during iperf_run_client() caused SIGSEGV or
   hang. Added setjmp/longjmp wrapper matching main.c's pattern,
   SIGPIPE=SIG_IGN, and a 30s connect_timeout to avoid DNS hangs.

Also extracted iperf_cleanup_and_signal() to deduplicate the
normal-return and longjmp paths. Added diagnostic logging to
IperfRunner.kt's library loading for faster future debugging.
2026-08-26 01:52:37 +09:00
irene 7263808918 docs: add current structure/implementation/design documentation
CI / Analyze & Test (push) Failing after 27s
2026-08-25 19:25:40 +09:00
irene 0dd202291c fix: resolve SIGBUS crash in JNI bridge — use CallVoidMethod instead of function pointer casts
The root cause of the SIGBUS (alignment fault) crash was casting jmethodID
values to function pointers and invoking them directly. jmethodID is an
opaque JNI type that must only be used via CallVoidMethod/CallObjectMethod
etc. Calling it as a function pointer dereferences garbage memory, causing
SIGBUS at address 0xb1.

Changes:
- Replace function pointer typedefs (output_callback_t etc.) with a struct
  storing JNIEnv + callback global ref + jmethodIDs
- All callbacks now go through CallVoidMethod(env, ref, mid, ...)
- Remove getTestProgress() (never used, unused start_time workaround)
- Fix Kotlin arg cast: List<String> -> toTypedArray() (ClassCastException)
- Update UI default server to speedtest.uztelecom.uz
2026-08-25 18:59:42 +09:00
irene d551f41b41 feat: switch iperf3 to JNI with 3.19 source
- Switch from bundled iperf3 binary (Process.start) to JNI compilation
- iperf3 3.21 source API was incompatible (start_time, iperf_strerror, etc.)
- iperf3 3.19 source is proven to work (used by CellularLab)
- CMakeLists.txt updated: config.h generated into cpp/iperf/ for correct include paths
- iperf_jni.c fixed for 3.19 API (no start_time on iperf_test)
- getTestProgress() returns g_test->duration as fallback (no real-time elapsed tracking yet)
- NDK CMake pinned to 3.22.1
- UI layer updated: removed Iperf3BinaryPathProvider references
- Tests updated: TestRunViewModelTest matches new service interface
- pubspec.yaml: removed iperf3 assets bundle, added http dependency
- AndroidManifest: INTERNET permission (needed for hostname resolution)
2026-08-25 13:46:33 +09:00
irene 27e8e50469 feat: add INTERNET permission to AndroidManifest
Required for iperf3 client mode to resolve hostnames and connect to servers. Without this, the app silently fails to make network connections.
2026-08-24 13:11:37 +09:00
irene 11ada3e5a8 fix: use runInShell=false for iperf3 binary execution and add logging
CI / Analyze & Test (push) Failing after 27s
- Android toybox ash cannot exec ELF binaries even with -rwx permissions
- runInShell: true wraps in /system/bin/sh -c which refuses ELF
- runInShell: false calls execve() directly, bypassing sh
- Added debugPrint logging to capture actual output in logcat
2026-08-24 11:38:34 +09:00
irene d5cd8177b2 docs: add test output excerpt to implementation plan
CI / Analyze & Test (push) Failing after 31s
2026-08-24 10:47:45 +09:00
irene 909d7c707e docs: add on-device verification results to implementation plan
CI / Analyze & Test (push) Failing after 36s
- Added static analysis results (flutter analyze + flutter test)
- Added on-device test results from Sony XQ-AS72 (QV7100JQ52)
- iperf3 v3.21 executes successfully without permission errors
- Raw output displayed in text box, status shows 'Test complete'
2026-08-24 10:46:30 +09:00
irene 369e42d06a fix: use /system/bin/sh for chmod to fix iperf3 binary permission on Android
CI / Analyze & Test (push) Failing after 31s
- The original chmod call used Process.run('chmod', ...) which fails on Android
  because chmod is not available in the app sandbox PATH
- /system/bin/sh is always present on Android and can execute chmod on the
  extracted binary, setting executable permissions before the first run
2026-08-24 10:22:16 +09:00
irene dc41c82d43 fix: move MainActivity to match package namespace com.novoyuuparosk.iperf3gui
CI / Analyze & Test (push) Failing after 38s
- Move MainActivity.kt from novoyuuparosk.iperf3gui to
  com.novoyuuparosk.iperf3gui package directory
- Update package declaration to match namespace
- This fixes the ClassNotFoundException crash on launch
2026-08-24 09:02:24 +09:00
irene ae627c437f fix: sync namespace with applicationId to match package name
CI / Analyze & Test (push) Failing after 41s
2026-08-24 08:45:17 +09:00
irene 7488e1458c docs: update implementation plan to mark all tasks completed
CI / Analyze & Test (push) Failing after 37s
2026-08-24 08:38:29 +09:00
irene 6c3ebf72c6 feat: bundle iperf3 v3.21 binaries from davidBar-On/android-iperf3
CI / Analyze & Test (push) Failing after 35s
- Add arm64-v8a and armeabi-v7a iperf3.21 binaries
- Update pubspec.yaml with assets section
- Update README with binary source information
2026-08-24 08:35:59 +09:00
irene 3ba11330cc feat: add CI workflow and update README
CI / Analyze & Test (push) Failing after 2m25s
- Add GitHub Actions CI workflow for analyze and tests
- Update README with project structure, build instructions, and license info
- Document iperf3 binary bundling process
2026-08-24 08:24:39 +09:00
irene 815fc48a15 refactor: fix lint issues and wire up main screen
- Use const Iperf3Result constructor in tests
- Remove unused import from widget_test.dart
- Fix unnecessary underscore warnings in Consumer builders
- Use initializing formal for iperf3Service field
- Wire up main.dart to call Iperf3BinaryPathProvider.initialize()
2026-08-24 06:51:06 +09:00
irene a6922b6a27 feat: add TestRunViewModel with state management 2026-08-24 06:43:37 +09:00
irene ef194af782 feat: add domain models for test configuration and results 2026-08-24 06:41:47 +09:00
irene 5a7c36a798 chore: scaffold Flutter project with com.novoyuuparosk.iperf3gui package 2026-08-24 06:37:11 +09:00