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:
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:iperf3gui/data/services/iperf3_service.dart';
|
||||
import 'package:iperf3gui/domain/models/test_configuration.dart';
|
||||
import 'package:iperf3gui/ui/features/test_run/iperf3_event_formatter.dart';
|
||||
import 'package:iperf3gui/ui/features/test_run/test_run_view_model.dart';
|
||||
|
||||
/// Main test run screen.
|
||||
@@ -193,6 +194,35 @@ class _TestRunContentState extends State<_TestRunContent> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Live throughput figures, derived from the parsed interval series
|
||||
Consumer<TestRunViewModel>(
|
||||
builder: (context, vm, _) {
|
||||
final intervals = vm.intervals;
|
||||
if (intervals.isEmpty) return const SizedBox.shrink();
|
||||
final rates =
|
||||
intervals.map((i) => i.bitsPerSecond).toList(growable: false);
|
||||
final latest = rates.last;
|
||||
final peak = rates.reduce((a, b) => a > b ? a : b);
|
||||
final mean = rates.reduce((a, b) => a + b) / rates.length;
|
||||
final summary = vm.summary?.sent;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_Stat(
|
||||
label: summary != null ? 'total' : 'now',
|
||||
value: formatBitrate(summary?.bitsPerSecond ?? latest),
|
||||
),
|
||||
_Stat(label: 'avg', value: formatBitrate(mean)),
|
||||
_Stat(label: 'peak', value: formatBitrate(peak)),
|
||||
_Stat(label: 'samples', value: '${rates.length}'),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// Live output
|
||||
Expanded(
|
||||
child: Container(
|
||||
@@ -229,3 +259,32 @@ class _TestRunContentState extends State<_TestRunContent> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// One figure in the throughput row.
|
||||
class _Stat extends StatelessWidget {
|
||||
const _Stat({required this.label, required this.value});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.labelSmall
|
||||
?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: theme.textTheme.titleSmall
|
||||
?.copyWith(fontFeatures: const [FontFeature.tabularFigures()]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user