cbb2aba8b0
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>
173 lines
4.8 KiB
Dart
173 lines
4.8 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:iperf3gui/data/services/iperf3_service.dart';
|
|
import 'package:iperf3gui/domain/models/iperf3_event.dart';
|
|
import 'package:iperf3gui/domain/models/iperf3_result.dart';
|
|
import 'package:iperf3gui/domain/models/test_configuration.dart';
|
|
import 'package:iperf3gui/ui/features/test_run/iperf3_event_formatter.dart';
|
|
|
|
/// State of the test run.
|
|
enum TestRunState { idle, running, completed, error }
|
|
|
|
/// ViewModel for managing test run state.
|
|
///
|
|
/// Subscribes to [Iperf3Service.events] so the transcript and the interval
|
|
/// series both grow while the test runs, rather than appearing at the end.
|
|
class TestRunViewModel extends ChangeNotifier {
|
|
final Iperf3Service iperf3Service;
|
|
|
|
/// How often to notify listeners while output is arriving. iperf3 can report
|
|
/// several times a second across many streams, and each rebuild re-renders
|
|
/// the whole transcript — so repaints are coalesced instead of one per event.
|
|
static const Duration displayInterval = Duration(milliseconds: 250);
|
|
|
|
TestRunViewModel({required this.iperf3Service}) {
|
|
_eventSubscription = iperf3Service.events.listen(_onEvent);
|
|
}
|
|
|
|
StreamSubscription<Iperf3Event>? _eventSubscription;
|
|
Timer? _repaintTimer;
|
|
bool _dirty = false;
|
|
|
|
TestRunState _state = TestRunState.idle;
|
|
TestRunState get state => _state;
|
|
|
|
Iperf3Result? _result;
|
|
Iperf3Result? get result => _result;
|
|
|
|
TestConfiguration? _currentConfig;
|
|
TestConfiguration? get currentConfig => _currentConfig;
|
|
|
|
final StringBuffer _transcript = StringBuffer();
|
|
|
|
/// Human-readable output accumulated so far.
|
|
String get output => _transcript.toString();
|
|
|
|
final List<Iperf3Interval> _intervals = [];
|
|
|
|
/// Per-interval throughput samples, in arrival order. Structured, so a chart
|
|
/// can be driven from this without re-parsing text.
|
|
List<Iperf3Interval> get intervals => List.unmodifiable(_intervals);
|
|
|
|
Iperf3Start? _start;
|
|
|
|
/// Connection details reported at the start of the run.
|
|
Iperf3Start? get start => _start;
|
|
|
|
Iperf3End? _summary;
|
|
|
|
/// Totals reported at the end of the run.
|
|
Iperf3End? get summary => _summary;
|
|
|
|
/// Whether a test is currently in flight.
|
|
bool get isRunning => _state == TestRunState.running;
|
|
|
|
/// Whether the most recent finished run ended because the user stopped it.
|
|
bool get wasStopped => _result?.stopped ?? false;
|
|
|
|
/// Start a test with the given configuration.
|
|
Future<void> runTest(TestConfiguration config) async {
|
|
if (_state == TestRunState.running) return;
|
|
|
|
if (!config.isValid) {
|
|
_resetRunData();
|
|
_transcript
|
|
.writeln('Invalid configuration: check the server address and port.');
|
|
_state = TestRunState.error;
|
|
notifyListeners();
|
|
return;
|
|
}
|
|
|
|
_resetRunData();
|
|
_state = TestRunState.running;
|
|
_currentConfig = config;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final result = await iperf3Service.run(config);
|
|
_result = result;
|
|
if (_transcript.isEmpty && result.rawOutput.isNotEmpty) {
|
|
_transcript.write(result.rawOutput);
|
|
}
|
|
_state =
|
|
result.exitCode == 0 ? TestRunState.completed : TestRunState.error;
|
|
} catch (e) {
|
|
_transcript.writeln('Test failed: $e');
|
|
_state = TestRunState.error;
|
|
} finally {
|
|
_stopRepaintTimer();
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Request that a running test stop early.
|
|
Future<void> stopTest() async {
|
|
if (_state != TestRunState.running) return;
|
|
await iperf3Service.stop();
|
|
}
|
|
|
|
/// Reset to idle state.
|
|
void reset() {
|
|
_resetRunData();
|
|
_state = TestRunState.idle;
|
|
_currentConfig = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
void _resetRunData() {
|
|
_result = null;
|
|
_transcript.clear();
|
|
_intervals.clear();
|
|
_start = null;
|
|
_summary = null;
|
|
_dirty = false;
|
|
}
|
|
|
|
void _onEvent(Iperf3Event event) {
|
|
switch (event) {
|
|
case Iperf3Start e:
|
|
_start = e;
|
|
case Iperf3Interval e:
|
|
// Omitted intervals are warm-up (-O) and are not part of the series.
|
|
if (!e.omitted) _intervals.add(e);
|
|
case Iperf3End e:
|
|
_summary = e;
|
|
case Iperf3Error _:
|
|
case Iperf3Stopped _:
|
|
case Iperf3LogLine _:
|
|
break;
|
|
}
|
|
|
|
final line = formatIperf3Event(event);
|
|
if (line != null && line.isNotEmpty) {
|
|
_transcript.writeln(line);
|
|
}
|
|
_scheduleRepaint();
|
|
}
|
|
|
|
/// Coalesce repaints: mark dirty and let a timer flush at [displayInterval].
|
|
void _scheduleRepaint() {
|
|
_dirty = true;
|
|
_repaintTimer ??= Timer.periodic(displayInterval, (_) {
|
|
if (!_dirty) return;
|
|
_dirty = false;
|
|
notifyListeners();
|
|
});
|
|
}
|
|
|
|
void _stopRepaintTimer() {
|
|
_repaintTimer?.cancel();
|
|
_repaintTimer = null;
|
|
_dirty = false;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_stopRepaintTimer();
|
|
_eventSubscription?.cancel();
|
|
iperf3Service.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|