diff --git a/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs b/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs index 3a89ed8b..790f5d1b 100644 --- a/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs +++ b/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs @@ -1,159 +1,157 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Net; -using System.Net.Http; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; - using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - using Shadowsocks.Model; using Shadowsocks.Util; namespace Shadowsocks.Controller { - using DataUnit = KeyValuePair; - using DataList = List>; - - using Statistics = Dictionary>; + using Statistics = Dictionary>; - public class AvailabilityStatistics + public sealed class AvailabilityStatistics : IDisposable { - public static readonly 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; - 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 Timer _timer; - private State _state; - private List _servers; - private StatisticsStrategyConfiguration _config; - + public const string DateTimePattern = "yyyy-MM-dd HH:mm:ss"; + private const string StatisticsFilesName = "shadowsocks.availability.json"; public static string AvailabilityStatisticsFile; - //static constructor to initialize every public static fields before refereced static AvailabilityStatistics() { AvailabilityStatisticsFile = Utils.GetTempPath(StatisticsFilesName); } - public AvailabilityStatistics(Configuration config, StatisticsStrategyConfiguration statisticsConfig) + //arguments for ICMP tests + private int Repeat => Config.RepeatTimesNum; + public const int TimeoutMilliseconds = 500; + + //records cache for current server in {_monitorInterval} minutes + private readonly ConcurrentDictionary> _latencyRecords = new ConcurrentDictionary>(); + //speed in KiB/s + private readonly ConcurrentDictionary _inboundCounter = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _lastInboundCounter = new ConcurrentDictionary(); + private readonly ConcurrentDictionary> _inboundSpeedRecords = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary _outboundCounter = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _lastOutboundCounter = new ConcurrentDictionary(); + private readonly ConcurrentDictionary> _outboundSpeedRecords = new ConcurrentDictionary>(); + + //tasks + private readonly TimeSpan _delayBeforeStart = TimeSpan.FromSeconds(1); + private readonly TimeSpan _retryInterval = TimeSpan.FromMinutes(2); + private Timer _recorder; //analyze and save cached records to RawStatistics and filter records + private TimeSpan RecordingInterval => TimeSpan.FromMinutes(Config.DataCollectionMinutes); + private Timer _speedMonior; + private readonly TimeSpan _monitorInterval = TimeSpan.FromSeconds(1); + //private Timer _writer; //write RawStatistics to file + //private readonly TimeSpan _writingInterval = TimeSpan.FromMinutes(1); + + private ShadowsocksController _controller; + private StatisticsStrategyConfiguration Config => _controller.StatisticsConfiguration; + + // Static Singleton Initialization + public static AvailabilityStatistics Instance { get; } = new AvailabilityStatistics(); + public Statistics RawStatistics { get; private set; } + public Statistics FilteredStatistics { get; private set; } + + private AvailabilityStatistics() { - UpdateConfiguration(config, statisticsConfig); + RawStatistics = new Statistics(); } - public bool Set(StatisticsStrategyConfiguration config) + internal void UpdateConfiguration(ShadowsocksController controller) { - _config = config; + _controller = controller; + Reset(); try { - if (config.StatisticsEnabled) + if (Config.StatisticsEnabled) { - if (_timer?.Change(DelayBeforeStart, Interval) == null) - { - _state = new State(); - _timer = new Timer(Run, _state, DelayBeforeStart, Interval); - } + StartTimerWithoutState(ref _recorder, Run, RecordingInterval); + LoadRawStatistics(); + StartTimerWithoutState(ref _speedMonior, UpdateSpeed, _monitorInterval); } else { - _timer?.Dispose(); + _recorder?.Dispose(); + _speedMonior?.Dispose(); } - return true; } catch (Exception e) { Logging.LogUsefulException(e); - return false; } } - //hardcode - //TODO: backup reliable isp&geolocation provider or a local database is required - public static async Task GetGeolocationAndIsp() + private void StartTimerWithoutState(ref Timer timer, TimerCallback callback, TimeSpan interval) { - Logging.Debug("Retrive information of geolocation and isp"); - const string API = "http://ip-api.com/json"; - const string alternativeAPI = "http://www.telize.com/geoip"; //must be comptible with current API - var result = await GetInfoFromAPI(API); - if (result != null) return result; - result = await GetInfoFromAPI(alternativeAPI); - if (result != null) return result; - return new DataList + if (timer?.Change(_delayBeforeStart, interval) == null) { - new DataUnit(State.Geolocation, State.Unknown), - new DataUnit(State.ISP, State.Unknown) - }; + timer = new Timer(callback, null, _delayBeforeStart, interval); + } } - private static async Task GetInfoFromAPI(string API) + private void UpdateSpeed(object _) { - string jsonString; - try - { - jsonString = await new HttpClient().GetStringAsync(API); - } - catch (HttpRequestException e) + foreach (var kv in _lastInboundCounter) { - Logging.LogUsefulException(e); - return null; - } - JObject obj; - try - { - obj = JObject.Parse(jsonString); - } - catch (JsonReaderException) - { - return null; + var id = kv.Key; + + var lastInbound = kv.Value; + var inbound = _inboundCounter[id]; + var bytes = inbound - lastInbound; + _lastInboundCounter[id] = inbound; + var inboundSpeed = GetSpeedInKiBPerSecond(bytes, _monitorInterval.TotalSeconds); + _inboundSpeedRecords.GetOrAdd(id, new List {inboundSpeed}).Add(inboundSpeed); + + var lastOutbound = _lastOutboundCounter[id]; + var outbound = _outboundCounter[id]; + bytes = outbound - lastOutbound; + _lastOutboundCounter[id] = outbound; + var outboundSpeed = GetSpeedInKiBPerSecond(bytes, _monitorInterval.TotalSeconds); + _outboundSpeedRecords.GetOrAdd(id, new List {outboundSpeed}).Add(outboundSpeed); + + Logging.Debug( + $"{id}: current/max inbound {inboundSpeed}/{_inboundSpeedRecords[id].Max()} KiB/s, current/max outbound {outboundSpeed}/{_outboundSpeedRecords[id].Max()} KiB/s"); } - string country = (string)obj["country"]; - string city = (string)obj["city"]; - string isp = (string)obj["isp"]; - if (country == null || city == null || isp == null) return null; - return new DataList { - new DataUnit(State.Geolocation, $"\"{country} {city}\""), - new DataUnit(State.ISP, $"\"{isp}\"") - }; } - private async Task> ICMPTest(Server server) + private async Task ICMPTest(Server server) { Logging.Debug("Ping " + server.FriendlyName()); if (server.server == "") return null; - var ret = new List(); - try { - var IP = Dns.GetHostAddresses(server.server).First(ip => (ip.AddressFamily == AddressFamily.InterNetwork || ip.AddressFamily == AddressFamily.InterNetworkV6)); + var result = new ICMPResult(server); + try + { + var IP = + Dns.GetHostAddresses(server.server) + .First( + ip => + ip.AddressFamily == AddressFamily.InterNetwork || + ip.AddressFamily == AddressFamily.InterNetworkV6); var ping = new Ping(); - foreach (var timestamp in Enumerable.Range(0, Repeat).Select(_ => DateTime.Now.ToString(DateTimePattern))) + foreach (var _ in Enumerable.Range(0, Repeat)) { - //ICMP echo. we can also set options and special bytes try { - var reply = await ping.SendTaskAsync(IP, Timeout); - ret.Add(new List> - { - new KeyValuePair("Timestamp", timestamp), - new KeyValuePair("Server", server.FriendlyName()), - new KeyValuePair("Status", reply?.Status.ToString()), - new KeyValuePair("RoundtripTime", reply?.RoundtripTime.ToString()) - //new KeyValuePair("data", reply.Buffer.ToString()); // The data of reply - }); - Thread.Sleep(Timeout + new Random().Next() % Timeout); + var reply = await ping.SendTaskAsync(IP, TimeoutMilliseconds); + if (reply.Status.Equals(IPStatus.Success)) + { + result.RoundtripTime.Add((int?) reply.RoundtripTime); + } + else + { + result.RoundtripTime.Add(null); + } + //Do ICMPTest in a random frequency + Thread.Sleep(TimeoutMilliseconds + new Random().Next()%TimeoutMilliseconds); } catch (Exception e) { @@ -161,52 +159,82 @@ namespace Shadowsocks.Controller Logging.LogUsefulException(e); } } - }catch(Exception e) + } + catch (Exception e) { Logging.Error($"An exception occured while eveluating {server.FriendlyName()}"); Logging.LogUsefulException(e); } - return ret; + return result; } - private void Run(object obj) + private void Reset() { - LoadRawStatistics(); + _inboundSpeedRecords.Clear(); + _outboundSpeedRecords.Clear(); + _latencyRecords.Clear(); + } + + private void Run(object _) + { + UpdateRecords(); + Save(); + Reset(); FilterRawStatistics(); - evaluate(); } - private async void evaluate() + private async void UpdateRecords() { - var geolocationAndIsp = GetGeolocationAndIsp(); - foreach (var dataLists in await TaskEx.WhenAll(_servers.Select(ICMPTest))) + var records = new Dictionary(); + + foreach (var server in _controller.GetCurrentConfiguration().configs) { - if (dataLists == null) continue; - foreach (var dataList in dataLists.Where(dataList => dataList != null)) + var id = server.Identifier(); + List inboundSpeedRecords = null; + List outboundSpeedRecords = null; + List latencyRecords = null; + _inboundSpeedRecords.TryGetValue(id, out inboundSpeedRecords); + _outboundSpeedRecords.TryGetValue(id, out outboundSpeedRecords); + _latencyRecords.TryGetValue(id, out latencyRecords); + records.Add(id, new StatisticsRecord(id, inboundSpeedRecords, outboundSpeedRecords, latencyRecords)); + } + + if (Config.Ping) + { + var icmpResults = await TaskEx.WhenAll(_controller.GetCurrentConfiguration().configs.Select(ICMPTest)); + foreach (var result in icmpResults.Where(result => result != null)) { - await geolocationAndIsp; - Append(dataList, geolocationAndIsp.Result); + records[result.Server.Identifier()].SetResponse(result.RoundtripTime); } } + + foreach (var kv in records.Where(kv => !kv.Value.IsEmptyData())) + { + AppendRecord(kv.Key, kv.Value); + } } - private static void Append(DataList dataList, IEnumerable extra) + private void AppendRecord(string serverIdentifier, StatisticsRecord record) { - var data = dataList.Concat(extra); - var dataLine = string.Join(Delimiter, data.Select(kv => kv.Value).ToArray()); - string[] lines; - if (!File.Exists(AvailabilityStatisticsFile)) + List records; + if (!RawStatistics.TryGetValue(serverIdentifier, out records)) { - var headerLine = string.Join(Delimiter, data.Select(kv => kv.Key).ToArray()); - lines = new[] { headerLine, dataLine }; + records = new List(); } - else + records.Add(record); + RawStatistics[serverIdentifier] = records; + } + + private void Save() + { + if (RawStatistics.Count == 0) { - lines = new[] { dataLine }; + return; } try { - File.AppendAllLines(AvailabilityStatisticsFile, lines); + var content = JsonConvert.SerializeObject(RawStatistics, Formatting.None); + File.WriteAllText(AvailabilityStatisticsFile, content); } catch (IOException e) { @@ -214,46 +242,28 @@ namespace Shadowsocks.Controller } } - internal void UpdateConfiguration(Configuration config, StatisticsStrategyConfiguration statisticsConfig) + private bool IsValidRecord(StatisticsRecord record) { - Set(statisticsConfig); - _servers = config.configs; + if (Config.ByHourOfDay) + { + if (!record.Timestamp.Hour.Equals(DateTime.Now.Hour)) return false; + } + return true; } - private async void FilterRawStatistics() + private void FilterRawStatistics() { if (RawStatistics == null) return; if (FilteredStatistics == null) { FilteredStatistics = new Statistics(); } - foreach (IEnumerable rawData in RawStatistics.Values) + + foreach (var serverAndRecords in RawStatistics) { - var filteredData = rawData; - if (_config.ByIsp) - { - var current = await GetGeolocationAndIsp(); - filteredData = - filteredData.Where( - data => - data.Geolocation == current[0].Value || - data.Geolocation == State.Unknown); - filteredData = - filteredData.Where( - data => data.ISP == current[1].Value || data.ISP == State.Unknown); - if (filteredData.LongCount() == 0) return; - } - if (_config.ByHourOfDay) - { - var currentHour = DateTime.Now.Hour; - filteredData = filteredData.Where(data => - data.Timestamp != UnknownDateTime && data.Timestamp.Hour == currentHour - ); - if (filteredData.LongCount() == 0) return; - } - var dataList = filteredData as List ?? filteredData.ToList(); - var serverName = dataList[0].ServerName; - FilteredStatistics[serverName] = dataList; + var server = serverAndRecords.Key; + var filteredRecords = serverAndRecords.Value.FindAll(IsValidRecord); + FilteredStatistics[server] = filteredRecords; } } @@ -265,77 +275,85 @@ namespace Shadowsocks.Controller Logging.Debug($"loading statistics from {path}"); if (!File.Exists(path)) { - try { - using (FileStream fs = File.Create(path)) - { - //do nothing - } - }catch(Exception e) + using (File.Create(path)) { - Logging.LogUsefulException(e); - } - if (!File.Exists(path)) { - Console.WriteLine($"statistics file does not exist, try to reload {RetryInterval / 60 / 1000} minutes later"); - _timer.Change(RetryInterval, Interval); - return; + //do nothing } } - RawStatistics = (from l in File.ReadAllLines(path).Skip(1) - let strings = l.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) - let rawData = new RawStatisticsData - { - Timestamp = ParseExactOrUnknown(strings[0]), - ServerName = strings[1], - ICMPStatus = strings[2], - RoundtripTime = int.Parse(strings[3]), - Geolocation = 5 > strings.Length ? - null - : strings[4], - ISP = 6 > strings.Length ? null : strings[5] - } - group rawData by rawData.ServerName into server - select new - { - ServerName = server.Key, - data = server.ToList() - }).ToDictionary(server => server.ServerName, server => server.data); + var content = File.ReadAllText(path); + RawStatistics = JsonConvert.DeserializeObject(content) ?? RawStatistics; } catch (Exception e) { Logging.LogUsefulException(e); + Console.WriteLine($"failed to load statistics; try to reload {_retryInterval.TotalMinutes} minutes later"); + _recorder.Change(_retryInterval, RecordingInterval); } } - private DateTime ParseExactOrUnknown(string str) + private static int GetSpeedInKiBPerSecond(long bytes, double seconds) { - DateTime dateTime; - return !DateTime.TryParseExact(str, DateTimePattern, null, DateTimeStyles.None, out dateTime) ? UnknownDateTime : dateTime; + var result = (int) (bytes/seconds)/1024; + return result; } - public class State + private class ICMPResult { - public DataList dataList = new DataList(); - public const string Geolocation = "Geolocation"; - public const string ISP = "ISP"; - public const string Unknown = "Unknown"; + internal readonly List RoundtripTime = new List(); + internal readonly Server Server; + + internal ICMPResult(Server server) + { + Server = server; + } } - public class RawStatisticsData + public void Dispose() { - public DateTime Timestamp; - public string ServerName; - public string ICMPStatus; - public int RoundtripTime; - public string Geolocation; - public string ISP; + _recorder.Dispose(); + _speedMonior.Dispose(); } - public class StatisticsData + public void UpdateLatency(Server server, int latency) { - public float PackageLoss; - public int AverageResponse; - public int MinResponse; - public int MaxResponse; + List records; + _latencyRecords.TryGetValue(server.Identifier(), out records); + if (records == null) + { + records = new List(); + } + records.Add(latency); + _latencyRecords[server.Identifier()] = records; + } + + public void UpdateInboundCounter(Server server, long n) + { + long count; + if (_inboundCounter.TryGetValue(server.Identifier(), out count)) + { + count += n; + } + else + { + count = n; + _lastInboundCounter[server.Identifier()] = 0; + } + _inboundCounter[server.Identifier()] = count; + } + + public void UpdateOutboundCounter(Server server, long n) + { + long count; + if (_outboundCounter.TryGetValue(server.Identifier(), out count)) + { + count += n; + } + else + { + count = n; + _lastOutboundCounter[server.Identifier()] = 0; + } + _outboundCounter[server.Identifier()] = count; } } } diff --git a/shadowsocks-csharp/Controller/Service/TCPRelay.cs b/shadowsocks-csharp/Controller/Service/TCPRelay.cs index 2fd3c149..9c162780 100644 --- a/shadowsocks-csharp/Controller/Service/TCPRelay.cs +++ b/shadowsocks-csharp/Controller/Service/TCPRelay.cs @@ -69,14 +69,19 @@ namespace Shadowsocks.Controller return true; } - public void UpdateInboundCounter(long n) + public void UpdateInboundCounter(Server server, long n) { - _controller.UpdateInboundCounter(n); + _controller.UpdateInboundCounter(server, n); } - public void UpdateOutboundCounter(long n) + public void UpdateOutboundCounter(Server server, long n) { - _controller.UpdateOutboundCounter(n); + _controller.UpdateOutboundCounter(server, n); + } + + public void UpdateLatency(Server server, TimeSpan latency) + { + _controller.UpdateLatency(server, latency); } } @@ -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) @@ -480,10 +488,8 @@ namespace Shadowsocks.Controller var latency = DateTime.Now - _startConnectTime; IStrategy strategy = controller.GetCurrentStrategy(); - if (strategy != null) - { - strategy.UpdateLatency(server, latency); - } + strategy?.UpdateLatency(server, latency); + tcprelay.UpdateLatency(server, latency); StartPipe(); } @@ -513,6 +519,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); } @@ -533,7 +540,7 @@ namespace Shadowsocks.Controller { int bytesRead = remote.EndReceive(ar); totalRead += bytesRead; - tcprelay.UpdateInboundCounter(bytesRead); + tcprelay.UpdateInboundCounter(server, bytesRead); if (bytesRead > 0) { @@ -600,14 +607,13 @@ namespace Shadowsocks.Controller encryptor.Encrypt(connetionRecvBuffer, bytesRead, connetionSendBuffer, out bytesToSend); } Logging.Debug(remote, bytesToSend, "TCP Relay", "@PipeConnectionReceiveCallback() (upload)"); - tcprelay.UpdateOutboundCounter(bytesToSend); + tcprelay.UpdateOutboundCounter(server, 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 4b86a204..13c6650d 100755 --- a/shadowsocks-csharp/Controller/ShadowsocksController.cs +++ b/shadowsocks-csharp/Controller/ShadowsocksController.cs @@ -5,7 +5,7 @@ using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; - +using System.Threading.Tasks; using Newtonsoft.Json; using Shadowsocks.Controller.Strategy; @@ -30,7 +30,7 @@ namespace Shadowsocks.Controller private StrategyManager _strategyManager; private PolipoRunner polipoRunner; private GFWListUpdater gfwListUpdater; - public AvailabilityStatistics availabilityStatistics { get; private set; } + public AvailabilityStatistics availabilityStatistics = AvailabilityStatistics.Instance; public StatisticsStrategyConfiguration StatisticsConfiguration { get; private set; } public long inboundCounter = 0; @@ -268,7 +268,7 @@ namespace Shadowsocks.Controller public void UpdateStatisticsConfiguration(bool enabled) { if (availabilityStatistics == null) return; - availabilityStatistics.UpdateConfiguration(_config, StatisticsConfiguration); + availabilityStatistics.UpdateConfiguration(this); _config.availabilityStatistics = enabled; SaveConfig(_config); } @@ -307,14 +307,30 @@ namespace Shadowsocks.Controller Configuration.Save(_config); } - public void UpdateInboundCounter(long n) + public void UpdateLatency(Server server, TimeSpan latency) + { + if (_config.availabilityStatistics) + { + new Task(() => availabilityStatistics.UpdateLatency(server, (int) latency.TotalMilliseconds)).Start(); + } + } + + public void UpdateInboundCounter(Server server, long n) { Interlocked.Add(ref inboundCounter, n); + if (_config.availabilityStatistics) + { + new Task(() => availabilityStatistics.UpdateInboundCounter(server, n)).Start(); + } } - public void UpdateOutboundCounter(long n) + public void UpdateOutboundCounter(Server server, long n) { Interlocked.Add(ref outboundCounter, n); + if (_config.availabilityStatistics) + { + new Task(() => availabilityStatistics.UpdateOutboundCounter(server, n)).Start(); + } } protected void Reload() @@ -341,11 +357,7 @@ namespace Shadowsocks.Controller gfwListUpdater.Error += pacServer_PACUpdateError; } - if (availabilityStatistics == null) - { - availabilityStatistics = new AvailabilityStatistics(_config, StatisticsConfiguration); - } - availabilityStatistics.UpdateConfiguration(_config, StatisticsConfiguration); + availabilityStatistics.UpdateConfiguration(this); if (_listener != null) { @@ -501,5 +513,6 @@ namespace Shadowsocks.Controller Thread.Sleep(30 * 1000); } } + } } diff --git a/shadowsocks-csharp/Controller/Strategy/StatisticsStrategy.cs b/shadowsocks-csharp/Controller/Strategy/StatisticsStrategy.cs index 70da0ac8..9679f681 100644 --- a/shadowsocks-csharp/Controller/Strategy/StatisticsStrategy.cs +++ b/shadowsocks-csharp/Controller/Strategy/StatisticsStrategy.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using System.Net.NetworkInformation; using System.Threading; using Newtonsoft.Json; @@ -11,12 +10,15 @@ using Shadowsocks.Model; namespace Shadowsocks.Controller.Strategy { - class StatisticsStrategy : IStrategy + using Statistics = Dictionary>; + + internal class StatisticsStrategy : IStrategy, IDisposable { private readonly ShadowsocksController _controller; private Server _currentServer; private readonly Timer _timer; - private Dictionary> _filteredStatistics; + private Statistics _filteredStatistics; + private AvailabilityStatistics Service => _controller.availabilityStatistics; private int ChoiceKeptMilliseconds => (int)TimeSpan.FromMinutes(_controller.StatisticsConfiguration.ChoiceKeptMinutes).TotalMilliseconds; @@ -24,7 +26,7 @@ namespace Shadowsocks.Controller.Strategy { _controller = controller; var servers = controller.GetCurrentConfiguration().configs; - var randomIndex = new Random().Next() % servers.Count(); + var randomIndex = new Random().Next() % servers.Count; _currentServer = servers[randomIndex]; //choose a server randomly at first _timer = new Timer(ReloadStatisticsAndChooseAServer); } @@ -39,36 +41,40 @@ namespace Shadowsocks.Controller.Strategy private void LoadStatistics() { - _filteredStatistics = _controller.availabilityStatistics.RawStatistics ?? _filteredStatistics ?? new Dictionary>(); + _filteredStatistics = + Service.FilteredStatistics ?? + Service.RawStatistics ?? + _filteredStatistics; } //return the score by data //server with highest score will be choosen - private float GetScore(string serverName) + private float? GetScore(string identifier, List records) { var config = _controller.StatisticsConfiguration; - List dataList; - if (_filteredStatistics == null || !_filteredStatistics.TryGetValue(serverName, out dataList)) return 0; - var successTimes = (float)dataList.Count(data => data.ICMPStatus == IPStatus.Success.ToString()); - var timedOutTimes = (float)dataList.Count(data => data.ICMPStatus == IPStatus.TimedOut.ToString()); - var statisticsData = new AvailabilityStatistics.StatisticsData + float? score = null; + + var averageRecord = new StatisticsRecord(identifier, + records.Where(record => record.MaxInboundSpeed != null).Select(record => record.MaxInboundSpeed.Value).ToList(), + records.Where(record => record.MaxOutboundSpeed != null).Select(record => record.MaxOutboundSpeed.Value).ToList(), + records.Where(record => record.AverageLatency != null).Select(record => record.AverageLatency.Value).ToList()); + averageRecord.SetResponse(records.Select(record => record.AverageResponse).ToList()); + + foreach (var calculation in config.Calculations) + { + var name = calculation.Key; + var field = typeof (StatisticsRecord).GetField(name); + dynamic value = field?.GetValue(averageRecord); + var factor = calculation.Value; + if (value == null || factor.Equals(0)) continue; + score = score ?? 0; + score += value * factor; + } + + if (score != null) { - PackageLoss = timedOutTimes / (successTimes + timedOutTimes) * 100, - AverageResponse = Convert.ToInt32(dataList.Average(data => data.RoundtripTime)), - MinResponse = dataList.Min(data => data.RoundtripTime), - MaxResponse = dataList.Max(data => data.RoundtripTime) - }; - float factor; - float score = 0; - if (!config.Calculations.TryGetValue("PackageLoss", out factor)) factor = 0; - score += statisticsData.PackageLoss * factor; - if (!config.Calculations.TryGetValue("AverageResponse", out factor)) factor = 0; - score += statisticsData.AverageResponse * factor; - if (!config.Calculations.TryGetValue("MinResponse", out factor)) factor = 0; - score += statisticsData.MinResponse * factor; - if (!config.Calculations.TryGetValue("MaxResponse", out factor)) factor = 0; - score += statisticsData.MaxResponse * factor; - Logging.Debug($"{serverName} {JsonConvert.SerializeObject(statisticsData)}"); + Logging.Debug($"Highest score: {score} {JsonConvert.SerializeObject(averageRecord, Formatting.Indented)}"); + } return score; } @@ -80,15 +86,25 @@ namespace Shadowsocks.Controller.Strategy } try { - var bestResult = (from server in servers - let name = server.FriendlyName() - where _filteredStatistics.ContainsKey(name) - select new - { - server, - score = GetScore(name) - } - ).Aggregate((result1, result2) => result1.score > result2.score ? result1 : result2); + var serversWithStatistics = (from server in servers + let id = server.Identifier() + where _filteredStatistics.ContainsKey(id) + let score = GetScore(server.Identifier(), _filteredStatistics[server.Identifier()]) + where score != null + select new + { + server, + score + }).ToArray(); + + if (serversWithStatistics.Length < 2) + { + LogWhenEnabled("no enough statistics data or all factors in calculations are 0"); + return; + } + + var bestResult = serversWithStatistics + .Aggregate((server1, server2) => server1.score > server2.score ? server1 : server2); LogWhenEnabled($"Switch to server: {bestResult.server.FriendlyName()} by statistics: score {bestResult.score}"); _currentServer = bestResult.server; @@ -109,18 +125,14 @@ namespace Shadowsocks.Controller.Strategy public string ID => "com.shadowsocks.strategy.scbs"; - public string Name => I18N.GetString("Choose By Total Package Loss"); + public string Name => I18N.GetString("Choose by statistics"); public Server GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint) { - var oldServer = _currentServer; - if (oldServer == null) + if (_currentServer == null) { ChooseNewServer(_controller.GetCurrentConfiguration().configs); } - if (oldServer != _currentServer) - { - } return _currentServer; //current server cached for CachedInterval } @@ -137,17 +149,19 @@ namespace Shadowsocks.Controller.Strategy public void UpdateLastRead(Server server) { - //TODO: combine this part of data with ICMP statics } public void UpdateLastWrite(Server server) { - //TODO: combine this part of data with ICMP statics } public void UpdateLatency(Server server, TimeSpan latency) { - //TODO: combine this part of data with ICMP statics + } + + public void Dispose() + { + _timer.Dispose(); } } } diff --git a/shadowsocks-csharp/Data/cn.txt b/shadowsocks-csharp/Data/cn.txt index a6d77156..68af293b 100644 --- a/shadowsocks-csharp/Data/cn.txt +++ b/shadowsocks-csharp/Data/cn.txt @@ -30,7 +30,7 @@ Quit=退出 Edit Servers=编辑服务器 Load Balance=负载均衡 High Availability=高可用 -Choose By Total Package Loss=累计丢包率 +Choose by statistics=根据统计 # Config Form diff --git a/shadowsocks-csharp/Model/Server.cs b/shadowsocks-csharp/Model/Server.cs index 69682302..a42cbbf3 100755 --- a/shadowsocks-csharp/Model/Server.cs +++ b/shadowsocks-csharp/Model/Server.cs @@ -95,5 +95,10 @@ namespace Shadowsocks.Model throw new FormatException(); } } + + public string Identifier() + { + return server + ':' + server_port; + } } } diff --git a/shadowsocks-csharp/Model/StatisticsRecord.cs b/shadowsocks-csharp/Model/StatisticsRecord.cs new file mode 100644 index 00000000..5c1051a4 --- /dev/null +++ b/shadowsocks-csharp/Model/StatisticsRecord.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Shadowsocks.Model +{ + // Simple processed records for a short period of time + public class StatisticsRecord + { + public DateTime Timestamp { get; set; } = DateTime.Now; + public string ServerIdentifier { get; set; } + + // in ping-only records, these fields would be null + public int? AverageLatency; + public int? MinLatency; + public int? MaxLatency; + + private bool EmptyLatencyData => (AverageLatency == null) && (MinLatency == null) && (MaxLatency == null); + + public int? AverageInboundSpeed; + public int? MinInboundSpeed; + public int? MaxInboundSpeed; + + private bool EmptyInboundSpeedData + => (AverageInboundSpeed == null) && (MinInboundSpeed == null) && (MaxInboundSpeed == null); + + public int? AverageOutboundSpeed; + public int? MinOutboundSpeed; + public int? MaxOutboundSpeed; + + private bool EmptyOutboundSpeedData + => (AverageOutboundSpeed == null) && (MinOutboundSpeed == null) && (MaxOutboundSpeed == null); + + // if user disabled ping test, response would be null + public int? AverageResponse; + public int? MinResponse; + public int? MaxResponse; + public float? PackageLoss; + + private bool EmptyResponseData + => (AverageResponse == null) && (MinResponse == null) && (MaxResponse == null) && (PackageLoss == null); + + public bool IsEmptyData() { + return EmptyInboundSpeedData && EmptyOutboundSpeedData && EmptyResponseData && EmptyLatencyData; + } + + public StatisticsRecord() + { + } + + public StatisticsRecord(string identifier, ICollection inboundSpeedRecords, ICollection outboundSpeedRecords, ICollection latencyRecords) + { + ServerIdentifier = identifier; + var inbound = inboundSpeedRecords?.Where(s => s > 0).ToList(); + if (inbound != null && inbound.Any()) + { + AverageInboundSpeed = (int) inbound.Average(); + MinInboundSpeed = inbound.Min(); + MaxInboundSpeed = inbound.Max(); + } + var outbound = outboundSpeedRecords?.Where(s => s > 0).ToList(); + if (outbound!= null && outbound.Any()) + { + AverageOutboundSpeed = (int) outbound.Average(); + MinOutboundSpeed = outbound.Min(); + MaxOutboundSpeed = outbound.Max(); + } + var latency = latencyRecords?.Where(s => s > 0).ToList(); + if (latency!= null && latency.Any()) + { + AverageLatency = (int) latency.Average(); + MinLatency = latency.Min(); + MaxLatency = latency.Max(); + } + } + + public StatisticsRecord(string identifier, ICollection responseRecords) + { + ServerIdentifier = identifier; + SetResponse(responseRecords); + } + + public void SetResponse(ICollection responseRecords) + { + if (responseRecords == null) return; + var records = responseRecords.Where(response => response != null).Select(response => response.Value).ToList(); + if (!records.Any()) return; + AverageResponse = (int?) records.Average(); + MinResponse = records.Min(); + MaxResponse = records.Max(); + PackageLoss = responseRecords.Count(response => response != null)/(float) responseRecords.Count; + } + } +} diff --git a/shadowsocks-csharp/Model/StatisticsStrategyConfiguration.cs b/shadowsocks-csharp/Model/StatisticsStrategyConfiguration.cs index 70433117..4a654814 100644 --- a/shadowsocks-csharp/Model/StatisticsStrategyConfiguration.cs +++ b/shadowsocks-csharp/Model/StatisticsStrategyConfiguration.cs @@ -13,13 +13,13 @@ namespace Shadowsocks.Model [Serializable] public class StatisticsStrategyConfiguration { - public static readonly string ID = "com.shadowsocks.strategy.statistics"; - private bool _statisticsEnabled = true; - private bool _byIsp = false; - private bool _byHourOfDay = false; - private int _choiceKeptMinutes = 10; - private int _dataCollectionMinutes = 10; - private int _repeatTimesNum = 4; + public static readonly string ID = "com.shadowsocks.strategy.statistics"; + public bool StatisticsEnabled { get; set; } = true; + public bool ByHourOfDay { get; set; } = true; + public bool Ping { get; set; } + public int ChoiceKeptMinutes { get; set; } = 10; + public int DataCollectionMinutes { get; set; } = 10; + public int RepeatTimesNum { get; set; } = 4; private const string ConfigFile = "statistics-config.json"; @@ -61,46 +61,8 @@ namespace Shadowsocks.Model public StatisticsStrategyConfiguration() { - var availabilityStatisticsType = typeof(AvailabilityStatistics); - var statisticsData = availabilityStatisticsType.GetNestedType("StatisticsData"); - var properties = statisticsData.GetFields(BindingFlags.Instance | BindingFlags.Public); + var properties = typeof(StatisticsRecord).GetFields(BindingFlags.Instance | BindingFlags.Public); Calculations = properties.ToDictionary(p => p.Name, _ => (float)0); } - - public bool StatisticsEnabled - { - get { return _statisticsEnabled; } - set { _statisticsEnabled = value; } - } - - public bool ByIsp - { - get { return _byIsp; } - set { _byIsp = value; } - } - - public bool ByHourOfDay - { - get { return _byHourOfDay; } - set { _byHourOfDay = value; } - } - - public int ChoiceKeptMinutes - { - get { return _choiceKeptMinutes; } - set { _choiceKeptMinutes = value; } - } - - public int DataCollectionMinutes - { - get { return _dataCollectionMinutes; } - set { _dataCollectionMinutes = value; } - } - - public int RepeatTimesNum - { - get { return _repeatTimesNum; } - set { _repeatTimesNum = value; } - } } } diff --git a/shadowsocks-csharp/View/CalculationControl.Designer.cs b/shadowsocks-csharp/View/CalculationControl.Designer.cs index b5a9bfab..9995bb05 100644 --- a/shadowsocks-csharp/View/CalculationControl.Designer.cs +++ b/shadowsocks-csharp/View/CalculationControl.Designer.cs @@ -38,13 +38,14 @@ // factorNum // this.factorNum.DecimalPlaces = 2; + this.factorNum.Dock = System.Windows.Forms.DockStyle.Right; this.factorNum.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); this.factorNum.Increment = new decimal(new int[] { 1, 0, 0, 131072}); - this.factorNum.Location = new System.Drawing.Point(285, 5); + this.factorNum.Location = new System.Drawing.Point(236, 0); this.factorNum.Minimum = new decimal(new int[] { 1000, 0, @@ -58,7 +59,7 @@ // this.multiply.AutoSize = true; this.multiply.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.multiply.Location = new System.Drawing.Point(251, 7); + this.multiply.Location = new System.Drawing.Point(202, 2); this.multiply.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); this.multiply.Name = "multiply"; this.multiply.Size = new System.Drawing.Size(26, 28); @@ -69,7 +70,7 @@ // this.plus.AutoSize = true; this.plus.Font = new System.Drawing.Font("Segoe UI", 10F); - this.plus.Location = new System.Drawing.Point(5, 7); + this.plus.Location = new System.Drawing.Point(5, 2); this.plus.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); this.plus.Name = "plus"; this.plus.Size = new System.Drawing.Size(26, 28); @@ -80,7 +81,7 @@ // this.valueLabel.AutoSize = true; this.valueLabel.Font = new System.Drawing.Font("Microsoft YaHei", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.valueLabel.Location = new System.Drawing.Point(39, 11); + this.valueLabel.Location = new System.Drawing.Point(39, 6); this.valueLabel.Name = "valueLabel"; this.valueLabel.Size = new System.Drawing.Size(118, 24); this.valueLabel.TabIndex = 7; @@ -88,14 +89,15 @@ // // CalculationControl // - this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 18F); + this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.Controls.Add(this.valueLabel); this.Controls.Add(this.factorNum); this.Controls.Add(this.multiply); this.Controls.Add(this.plus); + this.Margin = new System.Windows.Forms.Padding(0); this.Name = "CalculationControl"; - this.Size = new System.Drawing.Size(380, 46); + this.Size = new System.Drawing.Size(322, 34); ((System.ComponentModel.ISupportInitialize)(this.factorNum)).EndInit(); this.ResumeLayout(false); this.PerformLayout(); diff --git a/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.Designer.cs b/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.Designer.cs index a207f309..19cc6731 100644 --- a/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.Designer.cs +++ b/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.Designer.cs @@ -33,11 +33,12 @@ System.Windows.Forms.DataVisualization.Charting.Legend legend1 = new System.Windows.Forms.DataVisualization.Charting.Legend(); System.Windows.Forms.DataVisualization.Charting.Series series1 = new System.Windows.Forms.DataVisualization.Charting.Series(); System.Windows.Forms.DataVisualization.Charting.Series series2 = new System.Windows.Forms.DataVisualization.Charting.Series(); + System.Windows.Forms.DataVisualization.Charting.Series series3 = new System.Windows.Forms.DataVisualization.Charting.Series(); this.StatisticsChart = new System.Windows.Forms.DataVisualization.Charting.Chart(); - this.byISPCheckBox = new System.Windows.Forms.CheckBox(); + this.PingCheckBox = new System.Windows.Forms.CheckBox(); + this.bindingConfiguration = new System.Windows.Forms.BindingSource(this.components); this.label2 = new System.Windows.Forms.Label(); this.label3 = new System.Windows.Forms.Label(); - this.label4 = new System.Windows.Forms.Label(); this.chartModeSelector = new System.Windows.Forms.GroupBox(); this.allMode = new System.Windows.Forms.RadioButton(); this.dayMode = new System.Windows.Forms.RadioButton(); @@ -57,8 +58,9 @@ this.serverSelector = new System.Windows.Forms.ComboBox(); this.CancelButton = new System.Windows.Forms.Button(); this.OKButton = new System.Windows.Forms.Button(); - this.bindingConfiguration = new System.Windows.Forms.BindingSource(this.components); + this.CalculatinTip = new System.Windows.Forms.ToolTip(this.components); ((System.ComponentModel.ISupportInitialize)(this.StatisticsChart)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.bindingConfiguration)).BeginInit(); this.chartModeSelector.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit(); this.splitContainer1.Panel1.SuspendLayout(); @@ -75,7 +77,6 @@ this.splitContainer3.Panel1.SuspendLayout(); this.splitContainer3.Panel2.SuspendLayout(); this.splitContainer3.SuspendLayout(); - ((System.ComponentModel.ISupportInitialize)(this.bindingConfiguration)).BeginInit(); this.SuspendLayout(); // // StatisticsChart @@ -83,6 +84,7 @@ this.StatisticsChart.BackColor = System.Drawing.Color.Transparent; chartArea1.AxisX.MajorGrid.Enabled = false; chartArea1.AxisY.MajorGrid.Enabled = false; + chartArea1.AxisY2.Enabled = System.Windows.Forms.DataVisualization.Charting.AxisEnabled.False; chartArea1.AxisY2.MajorGrid.Enabled = false; chartArea1.BackColor = System.Drawing.Color.Transparent; chartArea1.Name = "DataArea"; @@ -96,40 +98,58 @@ this.StatisticsChart.Name = "StatisticsChart"; this.StatisticsChart.Palette = System.Windows.Forms.DataVisualization.Charting.ChartColorPalette.Pastel; series1.ChartArea = "DataArea"; - series1.ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Bubble; - series1.Color = System.Drawing.Color.FromArgb(((int)(((byte)(221)))), ((int)(((byte)(88)))), ((int)(((byte)(0))))); + series1.Color = System.Drawing.Color.DarkGray; series1.Legend = "ChartLegend"; - series1.Name = "Package Loss"; - series1.XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.DateTime; - series1.YValuesPerPoint = 2; - series2.BorderWidth = 4; + series1.Name = "Speed"; + series1.ToolTip = "#VALX\\nMax inbound speed\\n#VAL KiB/s"; + series1.XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.Time; series2.ChartArea = "DataArea"; - series2.ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line; - series2.Color = System.Drawing.Color.FromArgb(((int)(((byte)(155)))), ((int)(((byte)(77)))), ((int)(((byte)(150))))); + series2.ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Bubble; + series2.Color = System.Drawing.Color.Crimson; + series2.CustomProperties = "EmptyPointValue=Zero"; series2.Legend = "ChartLegend"; - series2.Name = "Ping"; - series2.XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.DateTime; + series2.Name = "Package Loss"; + series2.ToolTip = "#VALX\\n#VAL%"; + series2.XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.Time; + series2.YAxisType = System.Windows.Forms.DataVisualization.Charting.AxisType.Secondary; + series2.YValuesPerPoint = 2; + series3.BorderWidth = 5; + series3.ChartArea = "DataArea"; + series3.ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line; + series3.Color = System.Drawing.Color.DodgerBlue; + series3.Legend = "ChartLegend"; + series3.MarkerSize = 10; + series3.MarkerStyle = System.Windows.Forms.DataVisualization.Charting.MarkerStyle.Circle; + series3.Name = "Ping"; + series3.ToolTip = "#VALX\\n#VAL ms"; + series3.XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.Time; this.StatisticsChart.Series.Add(series1); this.StatisticsChart.Series.Add(series2); - this.StatisticsChart.Size = new System.Drawing.Size(1077, 303); + this.StatisticsChart.Series.Add(series3); + this.StatisticsChart.Size = new System.Drawing.Size(978, 429); this.StatisticsChart.TabIndex = 2; // - // byISPCheckBox + // PingCheckBox // - this.byISPCheckBox.AutoSize = true; - this.byISPCheckBox.DataBindings.Add(new System.Windows.Forms.Binding("Checked", this.bindingConfiguration, "ByIsp", true)); - this.byISPCheckBox.Location = new System.Drawing.Point(13, 54); - this.byISPCheckBox.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); - this.byISPCheckBox.Name = "byISPCheckBox"; - this.byISPCheckBox.Size = new System.Drawing.Size(220, 31); - this.byISPCheckBox.TabIndex = 5; - this.byISPCheckBox.Text = "By ISP/geolocation"; - this.byISPCheckBox.UseVisualStyleBackColor = true; + this.PingCheckBox.AutoSize = true; + this.PingCheckBox.DataBindings.Add(new System.Windows.Forms.Binding("Checked", this.bindingConfiguration, "Ping", true)); + this.PingCheckBox.Location = new System.Drawing.Point(13, 54); + this.PingCheckBox.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); + this.PingCheckBox.Name = "PingCheckBox"; + this.PingCheckBox.Size = new System.Drawing.Size(124, 31); + this.PingCheckBox.TabIndex = 5; + this.PingCheckBox.Text = "Ping Test"; + this.PingCheckBox.UseVisualStyleBackColor = true; + this.PingCheckBox.CheckedChanged += new System.EventHandler(this.PingCheckBox_CheckedChanged); + // + // bindingConfiguration + // + this.bindingConfiguration.DataSource = typeof(Shadowsocks.Model.StatisticsStrategyConfiguration); // // label2 // this.label2.AutoSize = true; - this.label2.Location = new System.Drawing.Point(8, 136); + this.label2.Location = new System.Drawing.Point(9, 206); this.label2.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); this.label2.Name = "label2"; this.label2.Size = new System.Drawing.Size(167, 27); @@ -139,48 +159,35 @@ // label3 // this.label3.AutoSize = true; - this.label3.Location = new System.Drawing.Point(285, 136); + this.label3.Location = new System.Drawing.Point(286, 206); this.label3.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); this.label3.Name = "label3"; this.label3.Size = new System.Drawing.Size(87, 27); this.label3.TabIndex = 9; this.label3.Text = "minutes"; // - // label4 - // - this.label4.AutoSize = true; - this.label4.Location = new System.Drawing.Point(8, 218); - this.label4.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); - this.label4.Name = "label4"; - this.label4.Size = new System.Drawing.Size(54, 27); - this.label4.TabIndex = 10; - this.label4.Text = "Ping"; - // // chartModeSelector // this.chartModeSelector.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.chartModeSelector.Controls.Add(this.allMode); this.chartModeSelector.Controls.Add(this.dayMode); - this.chartModeSelector.Location = new System.Drawing.Point(801, 104); + this.chartModeSelector.Location = new System.Drawing.Point(729, 188); this.chartModeSelector.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); this.chartModeSelector.Name = "chartModeSelector"; this.chartModeSelector.Padding = new System.Windows.Forms.Padding(5, 10, 5, 10); - this.chartModeSelector.Size = new System.Drawing.Size(261, 103); + this.chartModeSelector.Size = new System.Drawing.Size(234, 103); this.chartModeSelector.TabIndex = 3; this.chartModeSelector.TabStop = false; this.chartModeSelector.Text = "Chart Mode"; - this.chartModeSelector.Enter += new System.EventHandler(this.chartModeSelector_Enter); // // allMode // this.allMode.AutoSize = true; - this.allMode.Checked = true; this.allMode.Location = new System.Drawing.Point(11, 61); this.allMode.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); this.allMode.Name = "allMode"; this.allMode.Size = new System.Drawing.Size(58, 31); this.allMode.TabIndex = 1; - this.allMode.TabStop = true; this.allMode.Text = "all"; this.allMode.UseVisualStyleBackColor = true; this.allMode.CheckedChanged += new System.EventHandler(this.allMode_CheckedChanged); @@ -188,11 +195,13 @@ // dayMode // this.dayMode.AutoSize = true; + this.dayMode.Checked = true; this.dayMode.Location = new System.Drawing.Point(11, 29); this.dayMode.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); this.dayMode.Name = "dayMode"; this.dayMode.Size = new System.Drawing.Size(73, 31); this.dayMode.TabIndex = 0; + this.dayMode.TabStop = true; this.dayMode.Text = "24h"; this.dayMode.UseVisualStyleBackColor = true; this.dayMode.CheckedChanged += new System.EventHandler(this.dayMode_CheckedChanged); @@ -217,8 +226,8 @@ this.splitContainer1.Panel2.Controls.Add(this.OKButton); this.splitContainer1.Panel2.Controls.Add(this.chartModeSelector); this.splitContainer1.Panel2.Controls.Add(this.StatisticsChart); - this.splitContainer1.Size = new System.Drawing.Size(1077, 614); - this.splitContainer1.SplitterDistance = 301; + this.splitContainer1.Size = new System.Drawing.Size(978, 744); + this.splitContainer1.SplitterDistance = 305; this.splitContainer1.SplitterWidth = 10; this.splitContainer1.TabIndex = 12; // @@ -242,14 +251,13 @@ this.splitContainer2.Panel1.Controls.Add(this.repeatTimesNum); this.splitContainer2.Panel1.Controls.Add(this.label6); this.splitContainer2.Panel1.Controls.Add(this.label2); - this.splitContainer2.Panel1.Controls.Add(this.label4); - this.splitContainer2.Panel1.Controls.Add(this.byISPCheckBox); + this.splitContainer2.Panel1.Controls.Add(this.PingCheckBox); this.splitContainer2.Panel1.Controls.Add(this.label3); // // splitContainer2.Panel2 // this.splitContainer2.Panel2.Controls.Add(this.splitContainer3); - this.splitContainer2.Size = new System.Drawing.Size(1077, 301); + this.splitContainer2.Size = new System.Drawing.Size(978, 305); this.splitContainer2.SplitterDistance = 384; this.splitContainer2.SplitterWidth = 5; this.splitContainer2.TabIndex = 7; @@ -257,7 +265,7 @@ // label9 // this.label9.AutoSize = true; - this.label9.Location = new System.Drawing.Point(8, 175); + this.label9.Location = new System.Drawing.Point(9, 164); this.label9.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); this.label9.Name = "label9"; this.label9.Size = new System.Drawing.Size(162, 27); @@ -268,7 +276,7 @@ // this.label8.AutoSize = true; this.label8.Font = new System.Drawing.Font("Microsoft YaHei", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label8.Location = new System.Drawing.Point(285, 176); + this.label8.Location = new System.Drawing.Point(286, 165); this.label8.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); this.label8.Name = "label8"; this.label8.Size = new System.Drawing.Size(87, 27); @@ -283,7 +291,7 @@ 0, 0, 0}); - this.dataCollectionMinutesNum.Location = new System.Drawing.Point(176, 173); + this.dataCollectionMinutesNum.Location = new System.Drawing.Point(177, 162); this.dataCollectionMinutesNum.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.dataCollectionMinutesNum.Maximum = new decimal(new int[] { 120, @@ -291,7 +299,7 @@ 0, 0}); this.dataCollectionMinutesNum.Minimum = new decimal(new int[] { - 5, + 1, 0, 0, 0}); @@ -324,7 +332,7 @@ 0, 0, 0}); - this.choiceKeptMinutesNum.Location = new System.Drawing.Point(176, 134); + this.choiceKeptMinutesNum.Location = new System.Drawing.Point(177, 204); this.choiceKeptMinutesNum.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.choiceKeptMinutesNum.Maximum = new decimal(new int[] { 120, @@ -332,7 +340,7 @@ 0, 0}); this.choiceKeptMinutesNum.Minimum = new decimal(new int[] { - 5, + 1, 0, 0, 0}); @@ -349,7 +357,7 @@ // this.byHourOfDayCheckBox.AutoSize = true; this.byHourOfDayCheckBox.DataBindings.Add(new System.Windows.Forms.Binding("Checked", this.bindingConfiguration, "ByHourOfDay", true)); - this.byHourOfDayCheckBox.Location = new System.Drawing.Point(13, 95); + this.byHourOfDayCheckBox.Location = new System.Drawing.Point(13, 127); this.byHourOfDayCheckBox.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); this.byHourOfDayCheckBox.Name = "byHourOfDayCheckBox"; this.byHourOfDayCheckBox.Size = new System.Drawing.Size(180, 31); @@ -360,7 +368,7 @@ // repeatTimesNum // this.repeatTimesNum.DataBindings.Add(new System.Windows.Forms.Binding("Value", this.bindingConfiguration, "RepeatTimesNum", true)); - this.repeatTimesNum.Location = new System.Drawing.Point(72, 216); + this.repeatTimesNum.Location = new System.Drawing.Point(34, 84); this.repeatTimesNum.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.repeatTimesNum.Maximum = new decimal(new int[] { 10, @@ -380,7 +388,7 @@ // this.label6.AutoSize = true; this.label6.Font = new System.Drawing.Font("Microsoft YaHei", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label6.Location = new System.Drawing.Point(178, 218); + this.label6.Location = new System.Drawing.Point(139, 86); this.label6.Name = "label6"; this.label6.Size = new System.Drawing.Size(201, 27); this.label6.TabIndex = 13; @@ -403,20 +411,21 @@ // splitContainer3.Panel2 // this.splitContainer3.Panel2.Controls.Add(this.calculationContainer); - this.splitContainer3.Size = new System.Drawing.Size(688, 301); - this.splitContainer3.SplitterDistance = 46; - this.splitContainer3.SplitterWidth = 10; + this.splitContainer3.Size = new System.Drawing.Size(589, 305); + this.splitContainer3.SplitterDistance = 42; + this.splitContainer3.SplitterWidth = 1; this.splitContainer3.TabIndex = 6; // // label1 // this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(5, 12); + this.label1.Location = new System.Drawing.Point(5, 9); this.label1.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(262, 27); + this.label1.Size = new System.Drawing.Size(137, 27); this.label1.TabIndex = 0; - this.label1.Text = "Design evaluation method"; + this.label1.Text = "Final Score ="; + this.CalculatinTip.SetToolTip(this.label1, "(The server with the highest score would be choosen)"); // // calculationContainer // @@ -425,23 +434,23 @@ this.calculationContainer.Location = new System.Drawing.Point(0, 0); this.calculationContainer.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); this.calculationContainer.Name = "calculationContainer"; - this.calculationContainer.Size = new System.Drawing.Size(688, 245); + this.calculationContainer.Size = new System.Drawing.Size(589, 262); this.calculationContainer.TabIndex = 1; // // serverSelector // this.serverSelector.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.serverSelector.FormattingEnabled = true; - this.serverSelector.Location = new System.Drawing.Point(801, 67); + this.serverSelector.Location = new System.Drawing.Point(729, 151); this.serverSelector.Name = "serverSelector"; - this.serverSelector.Size = new System.Drawing.Size(260, 35); + this.serverSelector.Size = new System.Drawing.Size(233, 35); this.serverSelector.TabIndex = 6; - this.serverSelector.SelectedIndexChanged += new System.EventHandler(this.serverSelector_SelectedIndexChanged); + this.serverSelector.SelectionChangeCommitted += new System.EventHandler(this.serverSelector_SelectionChangeCommitted); // // CancelButton // this.CancelButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.CancelButton.Location = new System.Drawing.Point(960, 220); + this.CancelButton.Location = new System.Drawing.Point(861, 370); this.CancelButton.Name = "CancelButton"; this.CancelButton.Size = new System.Drawing.Size(101, 41); this.CancelButton.TabIndex = 5; @@ -452,7 +461,7 @@ // OKButton // this.OKButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.OKButton.Location = new System.Drawing.Point(852, 220); + this.OKButton.Location = new System.Drawing.Point(754, 370); this.OKButton.Name = "OKButton"; this.OKButton.Size = new System.Drawing.Size(101, 41); this.OKButton.TabIndex = 4; @@ -460,23 +469,20 @@ this.OKButton.UseVisualStyleBackColor = true; this.OKButton.Click += new System.EventHandler(this.OKButton_Click); // - // bindingConfiguration - // - this.bindingConfiguration.DataSource = typeof(Shadowsocks.Model.StatisticsStrategyConfiguration); - // // StatisticsStrategyConfigurationForm // this.AutoScaleDimensions = new System.Drawing.SizeF(12F, 27F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoSize = true; - this.ClientSize = new System.Drawing.Size(1077, 614); + this.ClientSize = new System.Drawing.Size(978, 744); this.Controls.Add(this.splitContainer1); this.Font = new System.Drawing.Font("Microsoft YaHei", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); this.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); - this.MinimumSize = new System.Drawing.Size(1059, 498); + this.MinimumSize = new System.Drawing.Size(1000, 800); this.Name = "StatisticsStrategyConfigurationForm"; this.Text = "StatisticsStrategyConfigurationForm"; ((System.ComponentModel.ISupportInitialize)(this.StatisticsChart)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.bindingConfiguration)).EndInit(); this.chartModeSelector.ResumeLayout(false); this.chartModeSelector.PerformLayout(); this.splitContainer1.Panel1.ResumeLayout(false); @@ -496,17 +502,15 @@ this.splitContainer3.Panel2.ResumeLayout(false); ((System.ComponentModel.ISupportInitialize)(this.splitContainer3)).EndInit(); this.splitContainer3.ResumeLayout(false); - ((System.ComponentModel.ISupportInitialize)(this.bindingConfiguration)).EndInit(); this.ResumeLayout(false); } #endregion private System.Windows.Forms.DataVisualization.Charting.Chart StatisticsChart; - private System.Windows.Forms.CheckBox byISPCheckBox; + private System.Windows.Forms.CheckBox PingCheckBox; private System.Windows.Forms.Label label2; private System.Windows.Forms.Label label3; - private System.Windows.Forms.Label label4; private System.Windows.Forms.GroupBox chartModeSelector; private System.Windows.Forms.RadioButton allMode; private System.Windows.Forms.RadioButton dayMode; @@ -527,5 +531,6 @@ private new System.Windows.Forms.Button CancelButton; private System.Windows.Forms.Button OKButton; private System.Windows.Forms.ComboBox serverSelector; + private System.Windows.Forms.ToolTip CalculatinTip; } } \ No newline at end of file diff --git a/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.cs b/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.cs index 02b57cd4..4f1b95ae 100644 --- a/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.cs +++ b/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.cs @@ -2,9 +2,8 @@ using System.Collections.Generic; using System.Data; using System.Linq; -using System.Net.NetworkInformation; using System.Windows.Forms; - +using System.Windows.Forms.DataVisualization.Charting; using Shadowsocks.Controller; using Shadowsocks.Model; @@ -14,23 +13,30 @@ namespace Shadowsocks.View { private readonly ShadowsocksController _controller; private StatisticsStrategyConfiguration _configuration; - private DataTable _dataTable = new DataTable(); + private readonly DataTable _dataTable = new DataTable(); private List _servers; + private readonly Series _speedSeries; + private readonly Series _packageLossSeries; + private readonly Series _pingSeries; public StatisticsStrategyConfigurationForm(ShadowsocksController controller) { if (controller == null) return; InitializeComponent(); + _speedSeries = StatisticsChart.Series["Speed"]; + _packageLossSeries = StatisticsChart.Series["Package Loss"]; + _pingSeries = StatisticsChart.Series["Ping"]; _controller = controller; _controller.ConfigChanged += (sender, args) => LoadConfiguration(); LoadConfiguration(); Load += (sender, args) => InitData(); + } private void LoadConfiguration() { var configs = _controller.GetCurrentConfiguration().configs; - _servers = configs.Select(server => server.FriendlyName()).ToList(); + _servers = configs.Select(server => server.Identifier()).ToList(); _configuration = _controller.StatisticsConfiguration ?? new StatisticsStrategyConfiguration(); if (_configuration.Calculations == null) @@ -51,15 +57,20 @@ namespace Shadowsocks.View serverSelector.DataSource = _servers; _dataTable.Columns.Add("Timestamp", typeof(DateTime)); - _dataTable.Columns.Add("Package Loss", typeof(int)); - _dataTable.Columns.Add("Ping", typeof(int)); + _dataTable.Columns.Add("Speed", typeof (int)); + _speedSeries.XValueMember = "Timestamp"; + _speedSeries.YValueMembers = "Speed"; + + // might be empty + _dataTable.Columns.Add("Package Loss", typeof (int)); + _dataTable.Columns.Add("Ping", typeof (int)); + _packageLossSeries.XValueMember = "Timestamp"; + _packageLossSeries.YValueMembers = "Package Loss"; + _pingSeries.XValueMember = "Timestamp"; + _pingSeries.YValueMembers = "Ping"; - StatisticsChart.Series["Package Loss"].XValueMember = "Timestamp"; - StatisticsChart.Series["Package Loss"].YValueMembers = "Package Loss"; - StatisticsChart.Series["Ping"].XValueMember = "Timestamp"; - StatisticsChart.Series["Ping"].YValueMembers = "Ping"; StatisticsChart.DataSource = _dataTable; - loadChartData(); + LoadChartData(); StatisticsChart.DataBind(); } @@ -79,24 +90,30 @@ namespace Shadowsocks.View Close(); } - private void loadChartData() + private void LoadChartData() { - string serverName = _servers[serverSelector.SelectedIndex]; + var serverName = _servers[serverSelector.SelectedIndex]; _dataTable.Rows.Clear(); //return directly when no data is usable if (_controller.availabilityStatistics?.FilteredStatistics == null) return; - List statistics; + List statistics; if (!_controller.availabilityStatistics.FilteredStatistics.TryGetValue(serverName, out statistics)) return; - IEnumerable> dataGroups; + IEnumerable> dataGroups; if (allMode.Checked) { + _pingSeries.XValueType = ChartValueType.DateTime; + _packageLossSeries.XValueType = ChartValueType.DateTime; + _speedSeries.XValueType = ChartValueType.DateTime; dataGroups = statistics.GroupBy(data => data.Timestamp.DayOfYear); - StatisticsChart.ChartAreas["DataArea"].AxisX.LabelStyle.Format = "MM/dd/yyyy"; - StatisticsChart.ChartAreas["DataArea"].AxisX2.LabelStyle.Format = "MM/dd/yyyy"; + StatisticsChart.ChartAreas["DataArea"].AxisX.LabelStyle.Format = "g"; + StatisticsChart.ChartAreas["DataArea"].AxisX2.LabelStyle.Format = "g"; } else { + _pingSeries.XValueType = ChartValueType.Time; + _packageLossSeries.XValueType = ChartValueType.Time; + _speedSeries.XValueType = ChartValueType.Time; dataGroups = statistics.GroupBy(data => data.Timestamp.Hour); StatisticsChart.ChartAreas["DataArea"].AxisX.LabelStyle.Format = "HH:00"; StatisticsChart.ChartAreas["DataArea"].AxisX2.LabelStyle.Format = "HH:00"; @@ -105,37 +122,36 @@ namespace Shadowsocks.View orderby dataGroup.Key select new { - Timestamp = dataGroup.First().Timestamp, - Ping = (int)dataGroup.Average(data => data.RoundtripTime), - PackageLoss = (int) - (dataGroup.Count(data => data.ICMPStatus == IPStatus.TimedOut.ToString()) - / (float)dataGroup.Count() * 100) + dataGroup.First().Timestamp, + Speed = dataGroup.Max(data => data.MaxInboundSpeed) ?? 0, + Ping = (int) (dataGroup.Average(data => data.AverageResponse) ?? 0), + PackageLossPercentage = (int) (dataGroup.Average(data => data.PackageLoss) ?? 0) * 100 }; - foreach (var data in finalData) + foreach (var data in finalData.Where(data => data.Speed != 0 || data.PackageLossPercentage != 0 || data.Ping != 0)) { - _dataTable.Rows.Add(data.Timestamp, data.PackageLoss, data.Ping); + _dataTable.Rows.Add(data.Timestamp, data.Speed, data.PackageLossPercentage, data.Ping); } StatisticsChart.DataBind(); } - private void serverSelector_SelectedIndexChanged(object sender, EventArgs e) + private void serverSelector_SelectionChangeCommitted(object sender, EventArgs e) { - loadChartData(); + LoadChartData(); } - private void chartModeSelector_Enter(object sender, EventArgs e) + private void dayMode_CheckedChanged(object sender, EventArgs e) { - + LoadChartData(); } - private void dayMode_CheckedChanged(object sender, EventArgs e) + private void allMode_CheckedChanged(object sender, EventArgs e) { - loadChartData(); + LoadChartData(); } - private void allMode_CheckedChanged(object sender, EventArgs e) + private void PingCheckBox_CheckedChanged(object sender, EventArgs e) { - loadChartData(); + repeatTimesNum.ReadOnly = !PingCheckBox.Checked; } } } diff --git a/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.resx b/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.resx index 5f9d5c44..8360b4c2 100644 --- a/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.resx +++ b/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.resx @@ -118,9 +118,12 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - 1, 30 + 4, 5 + + + 238, 6 - 63 + 191 \ No newline at end of file diff --git a/shadowsocks-csharp/shadowsocks-csharp.csproj b/shadowsocks-csharp/shadowsocks-csharp.csproj index d401a59a..2f43d366 100644 --- a/shadowsocks-csharp/shadowsocks-csharp.csproj +++ b/shadowsocks-csharp/shadowsocks-csharp.csproj @@ -60,6 +60,7 @@ ManagedMinimumRules.ruleset false true + true app.manifest @@ -195,6 +196,7 @@ + True @@ -340,13 +342,12 @@ - - - - - - - + + + + + f.ItemSpec).Where(f => !excludedAssemblie foreach (var item in filesToCleanup) File.Delete(item); -]]> - +]]> +