Skip to content

Commit 8cf1955

Browse files
Feature: Improve bandwidth meter & fix bugs (#3457)
* Feature: Migrate to LiveCharts2 * Fix: Bugs based on claude review * Docs: #3457 * Fix: Copilot feedback * Update network-interface--bandwidth.png * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Update NetworkInterfaceViewModel.cs * Update NetworkInterfaceView.xaml --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent d04c260 commit 8cf1955

23 files changed

Lines changed: 750 additions & 231 deletions
Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
1-
using System;
1+
using System;
22
using System.Globalization;
33
using System.Windows.Data;
44
using NETworkManager.Utilities;
55

66
namespace NETworkManager.Converters;
77

8+
/// <summary>
9+
/// Converts a byte-per-second value to a human-readable speed string.
10+
/// Pass the converter parameter "bytes" to format as byte/s (e.g. "1.54 MB/s");
11+
/// any other value (default) formats as bit/s (e.g. "12.3 Mbit/s") to match the chart axis.
12+
/// </summary>
813
public sealed class BandwidthBytesToSpeedConverter : IValueConverter
914
{
1015
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
1116
{
12-
return value != null
13-
? $"{FileSizeConverter.GetBytesReadable((long)value * 8)}it/s ({FileSizeConverter.GetBytesReadable((long)value)}/s)"
14-
: "-/-";
17+
if (value == null)
18+
return "-/-";
19+
20+
var bytesPerSecond = (long)value;
21+
22+
return parameter as string == "bytes"
23+
? $"{FileSizeConverter.GetBytesReadable(bytesPerSecond)}/s"
24+
: $"{FileSizeConverter.GetBytesReadable(bytesPerSecond * 8)}it/s";
1525
}
1626

1727
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
1828
{
1929
throw new NotImplementedException();
2030
}
21-
}
31+
}

‎Source/NETworkManager.Converters/BandwidthBytesWithSizeConverter.cs‎

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System;
2+
using System.Globalization;
3+
using System.Windows.Data;
4+
using NETworkManager.Localization.Resources;
5+
6+
namespace NETworkManager.Converters;
7+
8+
/// <summary>
9+
/// Converts a byte count to its exact value with thousands separators and a "Bytes" unit
10+
/// (e.g. "6,783,176,192 Bytes"). Intended for tooltips that complement a human-readable size.
11+
/// </summary>
12+
public sealed class BytesToExactStringConverter : IValueConverter
13+
{
14+
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
15+
{
16+
return value != null ? string.Format(culture, "{0:N0} {1}", (long)value, Strings.Bytes) : "-/-";
17+
}
18+
19+
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
20+
{
21+
throw new NotImplementedException();
22+
}
23+
}

‎Source/NETworkManager.Converters/LvlChartsBandwidthValueConverter.cs‎

Lines changed: 0 additions & 29 deletions
This file was deleted.

‎Source/NETworkManager.Documentation/DocumentationManager.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ public static DocumentationIdentifier GetIdentifierBySettingsName(SettingsName n
255255
SettingsName.Profiles => DocumentationIdentifier.SettingsProfiles,
256256
SettingsName.Settings => DocumentationIdentifier.SettingsSettings,
257257
SettingsName.Dashboard => GetIdentifierByApplicationName(ApplicationName.Dashboard),
258+
SettingsName.NetworkInterface => GetIdentifierByApplicationName(ApplicationName.NetworkInterface),
258259
SettingsName.IPScanner => GetIdentifierByApplicationName(ApplicationName.IPScanner),
259260
SettingsName.PortScanner => GetIdentifierByApplicationName(ApplicationName.PortScanner),
260261
SettingsName.PingMonitor => GetIdentifierByApplicationName(ApplicationName.PingMonitor),

‎Source/NETworkManager.Models/Network/BandwidthMeter.cs‎

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
using System;
1+
using System;
2+
using System.Diagnostics;
23
using System.Linq;
4+
using System.Net.NetworkInformation;
35
using System.Windows.Threading;
46

57
namespace NETworkManager.Models.Network;
@@ -30,6 +32,12 @@ private void Timer_Tick(object sender, EventArgs e)
3032

3133
#region Variables
3234

35+
/// <summary>
36+
/// The default sample interval in milliseconds. Kept as a constant because the speed
37+
/// calculation and the consuming view model derive timing from it.
38+
/// </summary>
39+
public const double DefaultUpdateInterval = 1000;
40+
3341
public double UpdateInterval
3442
{
3543
get;
@@ -42,12 +50,13 @@ public double UpdateInterval
4250

4351
field = value;
4452
}
45-
} = 1000;
53+
} = DefaultUpdateInterval;
4654

4755
public bool IsRunning => _timer.IsEnabled;
4856

4957
private readonly DispatcherTimer _timer = new();
5058
private readonly System.Net.NetworkInformation.NetworkInterface _networkInterface;
59+
private readonly Stopwatch _stopwatch = new();
5160
private long _previousBytesSent;
5261
private long _previousBytesReceived;
5362
private bool _canUpdate; // Collect initial data for correct calculation
@@ -76,34 +85,60 @@ public void Stop()
7685
{
7786
_timer.Stop();
7887

79-
// Reset
88+
// Reset, so the next sample re-seeds the baseline (avoids a speed spike after a pause).
8089
_canUpdate = false;
8190
}
8291

8392
private void Update()
8493
{
85-
var stats = _networkInterface.GetIPv4Statistics();
94+
// The interface may have been removed/disabled after this meter was created.
95+
if (_networkInterface == null)
96+
return;
8697

87-
var totalBytesSent = stats.BytesSent;
88-
var totalBytesReceived = stats.BytesReceived;
98+
IPInterfaceStatistics stats;
8999

90-
var byteSentSpeed = totalBytesSent - _previousBytesSent;
91-
var byteReceivedSpeed = totalBytesReceived - _previousBytesReceived;
100+
try
101+
{
102+
// IPStatistics covers both IPv4 and IPv6 traffic on the interface.
103+
stats = _networkInterface.GetIPStatistics();
104+
}
105+
catch (NetworkInformationException)
106+
{
107+
// Transient failure (e.g. adapter going down) - skip this tick.
108+
return;
109+
}
92110

93-
_previousBytesSent = stats.BytesSent;
94-
_previousBytesReceived = stats.BytesReceived;
111+
var totalBytesSent = stats.BytesSent;
112+
var totalBytesReceived = stats.BytesReceived;
95113

96-
// Need to collect initial data for correct calculation...
114+
// First sample after start/resume: seed the baseline and start timing, no speed yet.
97115
if (!_canUpdate)
98116
{
117+
_previousBytesSent = totalBytesSent;
118+
_previousBytesReceived = totalBytesReceived;
119+
_stopwatch.Restart();
99120
_canUpdate = true;
100121

101122
return;
102123
}
103124

104-
OnUpdateSpeed(new BandwidthMeterSpeedArgs(DateTime.Now, totalBytesReceived, totalBytesSent, byteReceivedSpeed,
105-
byteSentSpeed));
125+
var elapsedSeconds = _stopwatch.Elapsed.TotalSeconds;
126+
_stopwatch.Restart();
127+
128+
// Clamp negative deltas: cumulative counters can reset/wrap (driver reset, disable/enable).
129+
var deltaSent = Math.Max(0, totalBytesSent - _previousBytesSent);
130+
var deltaReceived = Math.Max(0, totalBytesReceived - _previousBytesReceived);
131+
132+
// Derive a true per-second rate from the measured elapsed time (robust against timer jitter).
133+
var byteSentSpeed = elapsedSeconds > 0 ? (long)(deltaSent / elapsedSeconds) : 0;
134+
var byteReceivedSpeed = elapsedSeconds > 0 ? (long)(deltaReceived / elapsedSeconds) : 0;
135+
136+
_previousBytesSent = totalBytesSent;
137+
_previousBytesReceived = totalBytesReceived;
138+
139+
OnUpdateSpeed(new BandwidthMeterSpeedArgs(DateTime.Now, totalBytesReceived, totalBytesSent,
140+
byteReceivedSpeed, byteSentSpeed));
106141
}
107142

108143
#endregion
109-
}
144+
}

‎Source/NETworkManager.Settings/GlobalStaticConfiguration.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ public static class GlobalStaticConfiguration
104104

105105
// Application: Network Interface
106106
public static ExportFileType NetworkInterface_ExportFileType => ExportFileType.Csv;
107+
public static int NetworkInterface_BandwidthChartTime => 60;
107108

108109
// Application: WiFi
109110
public static bool WiFi_Show2dot4GHzNetworks => true;

‎Source/NETworkManager.Settings/SettingsInfo.cs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,22 @@ public ExportFileType NetworkInterface_ExportFileType
894894
}
895895
} = GlobalStaticConfiguration.NetworkInterface_ExportFileType;
896896

897+
/// <summary>
898+
/// Gets or sets the bandwidth chart time window in seconds.
899+
/// </summary>
900+
public int NetworkInterface_BandwidthChartTime
901+
{
902+
get;
903+
set
904+
{
905+
if (value == field)
906+
return;
907+
908+
field = value;
909+
OnPropertyChanged();
910+
}
911+
} = GlobalStaticConfiguration.NetworkInterface_BandwidthChartTime;
912+
897913
#endregion
898914

899915
#region WiFi

‎Source/NETworkManager.Settings/SettingsName.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public enum SettingsName
1414
Profiles,
1515
Settings,
1616
Dashboard,
17+
NetworkInterface,
1718
IPScanner,
1819
PortScanner,
1920
PingMonitor,

‎Source/NETworkManager.Settings/SettingsViewManager.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public static class SettingsViewManager
4848
// Applications
4949
new(SettingsName.Dashboard, ApplicationManager.GetIcon(ApplicationName.Dashboard),
5050
SettingsGroup.Application),
51+
new(SettingsName.NetworkInterface, ApplicationManager.GetIcon(ApplicationName.NetworkInterface),
52+
SettingsGroup.Application),
5153
new(SettingsName.IPScanner, ApplicationManager.GetIcon(ApplicationName.IPScanner),
5254
SettingsGroup.Application),
5355
new(SettingsName.PortScanner, ApplicationManager.GetIcon(ApplicationName.PortScanner),

0 commit comments

Comments
 (0)