import 'package:iperf3gui/domain/models/iperf3_event.dart'; /// The accumulated state of one iperf3 run. /// /// Fed the event stream one report at a time, this is the answer to "what /// happened during this test" — the interval series, the end totals, any /// errors, and the statistics derived from them. /// /// It lives in the domain layer on purpose: it is what has to be *correct*, /// independent of how anything chooses to draw it. A different UI, a headless /// run or a future export path all consume the same object, and none of them /// re-derive a mean or re-decide whether a warm-up interval counts. class Iperf3Session { Iperf3Start? _start; /// Connection details, once the run has reported them. Iperf3Start? get start => _start; final List _intervals = []; /// Throughput samples in arrival order, warm-up intervals excluded. List get intervals => List.unmodifiable(_intervals); Iperf3End? _summary; /// Sent/received totals, once the run has finished. Iperf3End? get summary => _summary; final List _errors = []; /// Errors reported during the run, in order. List get errors => List.unmodifiable(_errors); bool _stopped = false; /// Whether the run ended because it was asked to stop. bool get stopped => _stopped; /// Whether iperf3 reported any error. bool get hasError => _errors.isNotEmpty; /// Whether anything at all has been recorded. bool get isEmpty => _start == null && _intervals.isEmpty && _summary == null && !hasError; /// How many throughput samples have been recorded. int get sampleCount => _intervals.length; /// Throughput of the most recent sample, or null before the first one. double? get latestBitsPerSecond => _intervals.isEmpty ? null : _intervals.last.bitsPerSecond; /// Mean throughput across the samples, or null before the first one. /// /// This is the mean of the per-interval rates, which is not identical to the /// run's overall rate when intervals differ in length — prefer /// [summary]'s totals for the headline figure once it is available. double? get meanBitsPerSecond { if (_intervals.isEmpty) return null; final total = _intervals.fold(0, (sum, i) => sum + i.bitsPerSecond); return total / _intervals.length; } /// Highest sampled throughput, or null before the first sample. double? get peakBitsPerSecond { if (_intervals.isEmpty) return null; return _intervals .map((i) => i.bitsPerSecond) .reduce((a, b) => a > b ? a : b); } /// Total bytes across the recorded samples. int get sampledBytes => _intervals.fold(0, (sum, i) => sum + i.bytes); /// Total retransmits across the samples, or null if none were reported. /// /// UDP tests report no retransmit counts, so null means "not measured" /// rather than zero. int? get totalRetransmits { final reported = _intervals.map((i) => i.retransmits).whereType().toList(); if (reported.isEmpty) return null; return reported.fold(0, (sum, r) => sum + r); } /// Record one report. void add(Iperf3Event event) { switch (event) { case Iperf3Start e: _start = e; case Iperf3Interval e: // Warm-up intervals (-O) are marked omitted by iperf3 and must not // drag the statistics down. if (!e.omitted) _intervals.add(e); case Iperf3End e: _summary = e; case Iperf3Error e: _errors.add(e.message); case Iperf3Stopped _: _stopped = true; case Iperf3LogLine _: break; } } /// Forget everything, ready for another run. void clear() { _start = null; _intervals.clear(); _summary = null; _errors.clear(); _stopped = false; } }