diff --git a/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs b/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs index f9c3c3d7..30a339a0 100644 --- a/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs +++ b/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -23,25 +24,39 @@ namespace Shadowsocks.Controller using Statistics = Dictionary>; + //TODO: change to singleton public class AvailabilityStatistics { - public static readonly string DateTimePattern = "yyyy-MM-dd HH:mm:ss"; + public const string DateTimePattern = "yyyy-MM-dd HH:mm:ss"; private const string StatisticsFilesName = "shadowsocks.availability.csv"; private const string Delimiter = ","; private const int Timeout = 500; - private const int DelayBeforeStart = 1000; + private readonly TimeSpan DelayBeforeStart = TimeSpan.FromSeconds(1); public Statistics RawStatistics { get; private set; } public Statistics FilteredStatistics { get; private set; } public static readonly DateTime UnknownDateTime = new DateTime(1970, 1, 1); private int Repeat => _config.RepeatTimesNum; - private const int RetryInterval = 2 * 60 * 1000; //retry 2 minutes after failed - private int Interval => (int)TimeSpan.FromMinutes(_config.DataCollectionMinutes).TotalMilliseconds; + private readonly TimeSpan RetryInterval = TimeSpan.FromMinutes(2); //retry 2 minutes after failed + private TimeSpan Interval => TimeSpan.FromMinutes(_config.DataCollectionMinutes); private Timer _timer; + private Timer _speedMonior; private State _state; private List _servers; private StatisticsStrategyConfiguration _config; + private const string Empty = ""; + public static string AvailabilityStatisticsFile; + //speed in KiB/s + private int _inboundSpeed = 0; + private int _outboundSpeed = 0; + private int? _latency = 0; + private Server _currentServer; + private Configuration _globalConfig; + private readonly ShadowsocksController _controller; + private long _lastInboundCounter = 0; + private long _lastOutboundCounter = 0; + private readonly TimeSpan MonitorInterval = TimeSpan.FromSeconds(1); //static constructor to initialize every public static fields before refereced static AvailabilityStatistics() @@ -49,9 +64,11 @@ namespace Shadowsocks.Controller AvailabilityStatisticsFile = Utils.GetTempPath(StatisticsFilesName); } - public AvailabilityStatistics(Configuration config, StatisticsStrategyConfiguration statisticsConfig) + public AvailabilityStatistics(ShadowsocksController controller) { - UpdateConfiguration(config, statisticsConfig); + _controller = controller; + _globalConfig = controller.GetCurrentConfiguration(); + UpdateConfiguration(_globalConfig, controller.StatisticsConfiguration); } public bool Set(StatisticsStrategyConfiguration config) @@ -70,6 +87,7 @@ namespace Shadowsocks.Controller else { _timer?.Dispose(); + _speedMonior?.Dispose(); } return true; } @@ -80,6 +98,27 @@ namespace Shadowsocks.Controller } } + private void UpdateSpeed(object state) + { + var bytes = _controller.inboundCounter - _lastInboundCounter; + _lastInboundCounter = _controller.inboundCounter; + var inboundSpeed = GetSpeedInKiBPerSecond(bytes ,MonitorInterval.TotalSeconds); + + bytes = _controller.outboundCounter - _lastOutboundCounter; + _lastOutboundCounter = _controller.outboundCounter; + var outboundSpeed = GetSpeedInKiBPerSecond(bytes, MonitorInterval.TotalSeconds); + + if (inboundSpeed > _inboundSpeed) + { + _inboundSpeed = inboundSpeed; + } + if (outboundSpeed > _outboundSpeed) + { + _outboundSpeed = outboundSpeed; + } + Logging.Debug($"{_currentServer.FriendlyName()}: current/max inbound {inboundSpeed}/{_inboundSpeed} KiB/s, current/max outbound {outboundSpeed}/{_outboundSpeed} KiB/s"); + } + //hardcode //TODO: backup reliable isp&geolocation provider or a local database is required public static async Task GetGeolocationAndIsp() @@ -98,6 +137,7 @@ namespace Shadowsocks.Controller }; } + //TODO: remove geolocation private static async Task GetInfoFromAPI(string API) { string jsonString; @@ -146,6 +186,9 @@ namespace Shadowsocks.Controller { new KeyValuePair("Timestamp", timestamp), new KeyValuePair("Server", server.FriendlyName()), + new KeyValuePair("Latency", GetRecentLatency(server)), + new KeyValuePair("InboundSpeed", GetRecentInboundSpeed(server)), + new KeyValuePair("OutboundSpeed", GetRecentOutboundSpeed(server)), new KeyValuePair("Status", reply?.Status.ToString()), new KeyValuePair("RoundtripTime", reply?.RoundtripTime.ToString()) //new KeyValuePair("data", reply.Buffer.ToString()); // The data of reply @@ -162,11 +205,43 @@ namespace Shadowsocks.Controller return ret; } + + private string GetRecentOutboundSpeed(Server server) + { + if (server != _currentServer) return Empty; + return _outboundSpeed.ToString(); + } + + private string GetRecentInboundSpeed(Server server) + { + if (server != _currentServer) return Empty; + return _inboundSpeed.ToString(); + } + + private string GetRecentLatency(Server server) + { + if (server != _currentServer) return Empty; + return _latency == null ? Empty : _latency.ToString(); + } + + private void ResetSpeed() + { + _currentServer = _globalConfig.GetCurrentServer(); + _latency = null; + _inboundSpeed = 0; + _outboundSpeed = 0; + } + private void Run(object obj) { + if (_speedMonior?.Change(DelayBeforeStart, MonitorInterval) == null) + { + _speedMonior = new Timer(UpdateSpeed, null, DelayBeforeStart, MonitorInterval); + } LoadRawStatistics(); FilterRawStatistics(); evaluate(); + ResetSpeed(); } private async void evaluate() @@ -177,7 +252,6 @@ namespace Shadowsocks.Controller if (dataLists == null) continue; foreach (var dataList in dataLists.Where(dataList => dataList != null)) { - await geolocationAndIsp; Append(dataList, geolocationAndIsp.Result); } } @@ -211,6 +285,7 @@ namespace Shadowsocks.Controller { Set(statisticsConfig); _servers = config.configs; + ResetSpeed(); } private async void FilterRawStatistics() @@ -258,7 +333,7 @@ namespace Shadowsocks.Controller Logging.Debug($"loading statistics from {path}"); if (!File.Exists(path)) { - Console.WriteLine($"statistics file does not exist, try to reload {RetryInterval / 60 / 1000} minutes later"); + Console.WriteLine($"statistics file does not exist, try to reload {RetryInterval.TotalMinutes} minutes later"); _timer.Change(RetryInterval, Interval); return; } @@ -302,6 +377,7 @@ namespace Shadowsocks.Controller public const string Unknown = "Unknown"; } + //TODO: redesign model public class RawStatisticsData { public DateTime Timestamp; @@ -319,5 +395,16 @@ namespace Shadowsocks.Controller public int MinResponse; public int MaxResponse; } + + public void UpdateLatency(int latency) + { + _latency = latency; + } + + private static int GetSpeedInKiBPerSecond(long bytes, double seconds) + { + var result = (int) (bytes / seconds) / 1024; + return result; + } } } diff --git a/shadowsocks-csharp/Controller/Service/TCPRelay.cs b/shadowsocks-csharp/Controller/Service/TCPRelay.cs index 2fd3c149..d5d82ed5 100644 --- a/shadowsocks-csharp/Controller/Service/TCPRelay.cs +++ b/shadowsocks-csharp/Controller/Service/TCPRelay.cs @@ -78,6 +78,11 @@ namespace Shadowsocks.Controller { _controller.UpdateOutboundCounter(n); } + + public void UpdateLatency(Server server, TimeSpan latency) + { + _controller.UpdateLatency(server, latency); + } } class TCPHandler @@ -126,6 +131,9 @@ namespace Shadowsocks.Controller private object decryptionLock = new object(); private DateTime _startConnectTime; + private DateTime _startReceivingTime; + private DateTime _startSendingTime; + private int _bytesToSend; private TCPRelay tcprelay; // TODO: tcprelay ?= relay public TCPHandler(TCPRelay tcprelay) @@ -484,6 +492,7 @@ namespace Shadowsocks.Controller { strategy.UpdateLatency(server, latency); } + tcprelay.UpdateLatency(server, latency); StartPipe(); } @@ -513,6 +522,7 @@ namespace Shadowsocks.Controller } try { + _startReceivingTime = DateTime.Now; remote.BeginReceive(remoteRecvBuffer, 0, RecvSize, 0, new AsyncCallback(PipeRemoteReceiveCallback), null); connection.BeginReceive(connetionRecvBuffer, 0, RecvSize, 0, new AsyncCallback(PipeConnectionReceiveCallback), null); } @@ -601,13 +611,12 @@ namespace Shadowsocks.Controller } Logging.Debug(remote, bytesToSend, "TCP Relay", "@PipeConnectionReceiveCallback() (upload)"); tcprelay.UpdateOutboundCounter(bytesToSend); + _startSendingTime = DateTime.Now; + _bytesToSend = bytesToSend; remote.BeginSend(connetionSendBuffer, 0, bytesToSend, 0, new AsyncCallback(PipeRemoteSendCallback), null); IStrategy strategy = controller.GetCurrentStrategy(); - if (strategy != null) - { - strategy.UpdateLastWrite(server); - } + strategy?.UpdateLastWrite(server); } else { diff --git a/shadowsocks-csharp/Controller/ShadowsocksController.cs b/shadowsocks-csharp/Controller/ShadowsocksController.cs index f7d50bd3..9b3206c1 100755 --- a/shadowsocks-csharp/Controller/ShadowsocksController.cs +++ b/shadowsocks-csharp/Controller/ShadowsocksController.cs @@ -343,7 +343,7 @@ namespace Shadowsocks.Controller if (availabilityStatistics == null) { - availabilityStatistics = new AvailabilityStatistics(_config, StatisticsConfiguration); + availabilityStatistics = new AvailabilityStatistics(this); } availabilityStatistics.UpdateConfiguration(_config, StatisticsConfiguration); @@ -498,5 +498,13 @@ namespace Shadowsocks.Controller Thread.Sleep(30 * 1000); } } + + public void UpdateLatency(Server server, TimeSpan latency) + { + if (_config.availabilityStatistics) + { + availabilityStatistics.UpdateLatency((int) latency.TotalMilliseconds); + } + } } }