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>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import 'package:iperf3gui/domain/models/iperf3_event.dart';
|
||||
|
||||
/// Renders parsed iperf3 events as the kind of line iperf3 itself would print.
|
||||
///
|
||||
/// The app consumes `--json-stream`, so the familiar text layout has to be
|
||||
/// rebuilt here. Keeping it in one pure function makes it testable and leaves
|
||||
/// the structured events untouched for anything else that wants them.
|
||||
String? formatIperf3Event(Iperf3Event event) {
|
||||
return switch (event) {
|
||||
Iperf3Start e => _formatStart(e),
|
||||
Iperf3Interval e => _formatInterval(e),
|
||||
Iperf3End e => _formatEnd(e),
|
||||
Iperf3Error e => 'iperf3 error: ${e.message}',
|
||||
Iperf3Stopped _ => 'Stopped by user.',
|
||||
Iperf3LogLine e => e.text.isEmpty ? null : e.text,
|
||||
};
|
||||
}
|
||||
|
||||
String _formatStart(Iperf3Start e) {
|
||||
final parts = <String>[];
|
||||
if (e.targetHost != null) {
|
||||
final port = e.targetPort != null ? ', port ${e.targetPort}' : '';
|
||||
parts.add('Connecting to host ${e.targetHost}$port');
|
||||
}
|
||||
final detail = <String>[
|
||||
if (e.protocol != null) e.protocol!,
|
||||
if (e.streamCount != null)
|
||||
'${e.streamCount} stream${e.streamCount == 1 ? '' : 's'}',
|
||||
if (e.durationSeconds != null) '${e.durationSeconds}s',
|
||||
];
|
||||
if (detail.isNotEmpty) parts.add(detail.join(' · '));
|
||||
return parts.isEmpty ? '' : parts.join('\n');
|
||||
}
|
||||
|
||||
String _formatInterval(Iperf3Interval e) {
|
||||
final window = '${_seconds(e.start)}-${_seconds(e.end)} sec';
|
||||
final line = StringBuffer('[SUM] $window '
|
||||
'${formatBytes(e.bytes)} ${formatBitrate(e.bitsPerSecond)}');
|
||||
if (e.retransmits != null) {
|
||||
line.write(' ${e.retransmits} retr');
|
||||
}
|
||||
if (e.omitted) line.write(' (omitted)');
|
||||
return line.toString();
|
||||
}
|
||||
|
||||
String _formatEnd(Iperf3End e) {
|
||||
final lines = <String>['- ' * 20];
|
||||
final sent = e.sent;
|
||||
final received = e.received;
|
||||
if (sent != null) {
|
||||
lines.add('[SUM] 0.00-${_seconds(sent.seconds)} sec '
|
||||
'${formatBytes(sent.bytes)} ${formatBitrate(sent.bitsPerSecond)}'
|
||||
'${sent.retransmits != null ? ' ${sent.retransmits} retr' : ''}'
|
||||
' sender');
|
||||
}
|
||||
if (received != null) {
|
||||
lines.add('[SUM] 0.00-${_seconds(received.seconds)} sec '
|
||||
'${formatBytes(received.bytes)} '
|
||||
'${formatBitrate(received.bitsPerSecond)} receiver');
|
||||
}
|
||||
if (e.hostCpuPercent != null) {
|
||||
lines.add('CPU: local ${e.hostCpuPercent!.toStringAsFixed(1)}%'
|
||||
'${e.remoteCpuPercent != null ? ', remote ${e.remoteCpuPercent!.toStringAsFixed(1)}%' : ''}');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
String _seconds(double value) => value.toStringAsFixed(2);
|
||||
|
||||
/// Byte totals the way iperf3 prints them — `93.8 MBytes`.
|
||||
String formatBytes(int bytes) {
|
||||
const units = ['Bytes', 'KBytes', 'MBytes', 'GBytes', 'TBytes'];
|
||||
var value = bytes.toDouble();
|
||||
var unit = 0;
|
||||
while (value >= 1024 && unit < units.length - 1) {
|
||||
value /= 1024;
|
||||
unit++;
|
||||
}
|
||||
final precision = value >= 100 ? 0 : (value >= 10 ? 1 : 2);
|
||||
return '${value.toStringAsFixed(precision)} ${units[unit]}';
|
||||
}
|
||||
|
||||
/// Bitrates the way iperf3 prints them — `786 Mbits/sec`. Decimal units, since
|
||||
/// network rates are powers of ten while transfer totals are powers of two.
|
||||
String formatBitrate(double bitsPerSecond) {
|
||||
const units = ['bits/sec', 'Kbits/sec', 'Mbits/sec', 'Gbits/sec'];
|
||||
var value = bitsPerSecond;
|
||||
var unit = 0;
|
||||
while (value >= 1000 && unit < units.length - 1) {
|
||||
value /= 1000;
|
||||
unit++;
|
||||
}
|
||||
final precision = value >= 100 ? 0 : (value >= 10 ? 1 : 2);
|
||||
return '${value.toStringAsFixed(precision)} ${units[unit]}';
|
||||
}
|
||||
Reference in New Issue
Block a user