diff --git a/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs b/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs index 3bf0d13e..6a41210a 100644 --- a/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs +++ b/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs @@ -8,53 +8,40 @@ using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json; using Shadowsocks.Model; using Shadowsocks.Util; namespace Shadowsocks.Controller { - using DataUnit = KeyValuePair; - using DataList = List>; - - using Statistics = Dictionary>; - + using Statistics = Dictionary>; public sealed class AvailabilityStatistics { - // Static Singleton Initialization - public static AvailabilityStatistics Instance { get; } = new AvailabilityStatistics(); - private AvailabilityStatistics() { } - public const string DateTimePattern = "yyyy-MM-dd HH:mm:ss"; - private const string StatisticsFilesName = "shadowsocks.availability.csv"; - private const string Delimiter = ","; + private const string StatisticsFilesName = "shadowsocks.availability.json"; private const int TimeoutMilliseconds = 500; - 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 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 readonly DateTime UnknownDateTime = new DateTime(1970, 1, 1); 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 ShadowsocksController _controller; - private long _lastInboundCounter = 0; - private long _lastOutboundCounter = 0; + private readonly TimeSpan _delayBeforeStart = TimeSpan.FromSeconds(1); private readonly TimeSpan _monitorInterval = TimeSpan.FromSeconds(1); + private readonly TimeSpan _retryInterval = TimeSpan.FromMinutes(2); //retry 2 minutes after failed + private readonly TimeSpan _writingInterval = TimeSpan.FromMinutes(1); + private StatisticsStrategyConfiguration _config; + private ShadowsocksController _controller; + private Server _currentServer; + //speed in KiB/s + private List _inboundSpeedRecords; + private long _lastInboundCounter; + private long _lastOutboundCounter; + private List _latencyRecords; + private List _outboundSpeedRecords; + private Timer _recorder; + private List _servers; + private Timer _speedMonior; + private Timer _writer; //static constructor to initialize every public static fields before refereced static AvailabilityStatistics() @@ -62,6 +49,18 @@ namespace Shadowsocks.Controller AvailabilityStatisticsFile = Utils.GetTempPath(StatisticsFilesName); } + private AvailabilityStatistics() + { + RawStatistics = new Statistics(); + } + + // Static Singleton Initialization + public static AvailabilityStatistics Instance { get; } = new AvailabilityStatistics(); + public Statistics RawStatistics { get; private set; } + public Statistics FilteredStatistics { get; private set; } + private int Repeat => _config.RepeatTimesNum; + private TimeSpan RecordingInterval => TimeSpan.FromMinutes(_config.DataCollectionMinutes); + public bool Set(StatisticsStrategyConfiguration config) { _config = config; @@ -69,15 +68,23 @@ namespace Shadowsocks.Controller { if (config.StatisticsEnabled) { - if (_timer?.Change(_delayBeforeStart, Interval) == null) + if (_recorder?.Change(_delayBeforeStart, RecordingInterval) == null) + { + _recorder = new Timer(Run, null, _delayBeforeStart, RecordingInterval); + } + LoadRawStatistics(); + if (_speedMonior?.Change(_delayBeforeStart, _monitorInterval) == null) + { + _speedMonior = new Timer(UpdateSpeed, null, _delayBeforeStart, _monitorInterval); + } + if (_writer?.Change(_delayBeforeStart, RecordingInterval) == null) { - _state = new State(); - _timer = new Timer(Run, _state, _delayBeforeStart, Interval); + _writer = new Timer(Save, null, _delayBeforeStart, RecordingInterval); } } else { - _timer?.Dispose(); + _recorder?.Dispose(); _speedMonior?.Dispose(); } return true; @@ -93,51 +100,49 @@ namespace Shadowsocks.Controller { var bytes = _controller.inboundCounter - _lastInboundCounter; _lastInboundCounter = _controller.inboundCounter; - var inboundSpeed = GetSpeedInKiBPerSecond(bytes ,_monitorInterval.TotalSeconds); + var inboundSpeed = GetSpeedInKiBPerSecond(bytes, _monitorInterval.TotalSeconds); + _inboundSpeedRecords.Add(inboundSpeed); bytes = _controller.outboundCounter - _lastOutboundCounter; _lastOutboundCounter = _controller.outboundCounter; var outboundSpeed = GetSpeedInKiBPerSecond(bytes, _monitorInterval.TotalSeconds); + _outboundSpeedRecords.Add(outboundSpeed); - 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"); + Logging.Debug( + $"{_currentServer.FriendlyName()}: current/max inbound {inboundSpeed}/{_inboundSpeedRecords.Max()} KiB/s, current/max outbound {outboundSpeed}/{_outboundSpeedRecords.Max()} KiB/s"); } - 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, TimeoutMilliseconds); - 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("Latency", GetRecentLatency(server)), - new KeyValuePair("InboundSpeed", GetRecentInboundSpeed(server)), - new KeyValuePair("OutboundSpeed", GetRecentOutboundSpeed(server)) - //new KeyValuePair("data", reply.Buffer.ToString()); // The data of reply - }); - Thread.Sleep(TimeoutMilliseconds + new Random().Next() % 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) { @@ -145,63 +150,73 @@ 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 string GetRecentOutboundSpeed(Server server) + private void Reset() { - return server != _currentServer ? Empty : _outboundSpeed.ToString(); + _inboundSpeedRecords = new List(); + _outboundSpeedRecords = new List(); + _latencyRecords = new List(); } - private string GetRecentInboundSpeed(Server server) + private void Run(object _) { - return server != _currentServer ? Empty : _inboundSpeed.ToString(); + AppendRecord(); + Reset(); + FilterRawStatistics(); } - private string GetRecentLatency(Server server) + private async void AppendRecord() { - if (server != _currentServer) return Empty; - return _latency == null ? Empty : _latency.ToString(); - } + //todo: option for icmp test + var icmpResults = TaskEx.WhenAll(_servers.Select(ICMPTest)); - private void ResetSpeed() - { - _currentServer = _controller.GetCurrentServer(); - _latency = null; - _inboundSpeed = 0; - _outboundSpeed = 0; - } + var currentServerRecord = new StatisticsRecord(_currentServer.Identifier(), + _inboundSpeedRecords, _outboundSpeedRecords, _latencyRecords); - private void Run(object obj) - { - if (_speedMonior?.Change(_delayBeforeStart, _monitorInterval) == null) + foreach (var result in (await icmpResults).Where(result => result != null)) { - _speedMonior = new Timer(UpdateSpeed, null, _delayBeforeStart, _monitorInterval); + List records; + if (!RawStatistics.TryGetValue(result.Server.Identifier(), out records)) + { + records = new List(); + } + + if (result.Server.Equals(_currentServer)) + { + currentServerRecord.setResponse(result.RoundtripTime); + records.Add(currentServerRecord); + } + else + { + records.Add(new StatisticsRecord(result.Server.Identifier(), result.RoundtripTime)); + } + RawStatistics[result.Server.Identifier()] = records; } - LoadRawStatistics(); - FilterRawStatistics(); - Evaluate(); - ResetSpeed(); } - private async void Evaluate() + private void Save(object _) { - foreach (var dataLists in await TaskEx.WhenAll(_servers.Select(ICMPTest))) + try { - if (dataLists == null) continue; - foreach (var dataList in dataLists.Where(dataList => dataList != null)) - { - Append(dataList, Enumerable.Empty()); - } + File.WriteAllText(AvailabilityStatisticsFile, + JsonConvert.SerializeObject(RawStatistics, Formatting.None)); + } + catch (IOException e) + { + Logging.LogUsefulException(e); + _writer.Change(_retryInterval, _writingInterval); } } + /* private static void Append(DataList dataList, IEnumerable extra) { var data = dataList.Concat(extra); @@ -225,15 +240,28 @@ namespace Shadowsocks.Controller Logging.LogUsefulException(e); } } + */ internal void UpdateConfiguration(ShadowsocksController controller) { _controller = controller; - ResetSpeed(); + _currentServer = _controller.GetCurrentServer(); + Reset(); Set(controller.StatisticsConfiguration); _servers = _controller.GetCurrentConfiguration().configs; } + private bool IsValidRecord(StatisticsRecord record) + { + if (_config.ByHourOfDay) + { + var currentHour = DateTime.Now.Hour; + if (record.Timestamp == UnknownDateTime) return false; + if (!record.Timestamp.Hour.Equals(DateTime.Now.Hour)) return false; + } + return true; + } + private void FilterRawStatistics() { if (RawStatistics == null) return; @@ -241,20 +269,12 @@ namespace Shadowsocks.Controller { FilteredStatistics = new Statistics(); } - foreach (IEnumerable rawData in RawStatistics.Values) + + foreach (var serverAndRecords in RawStatistics) { - var filteredData = rawData; - if (_config.ByHourOfDay) - { - var currentHour = DateTime.Now.Hour; - filteredData = filteredData.Where(data => - data.Timestamp != UnknownDateTime && data.Timestamp.Hour.Equals(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; } } @@ -266,36 +286,26 @@ namespace Shadowsocks.Controller Logging.Debug($"loading statistics from {path}"); if (!File.Exists(path)) { - try { + try + { using (var fs = File.Create(path)) { //do nothing } - }catch(Exception e) + } + catch (Exception e) { Logging.LogUsefulException(e); } - if (!File.Exists(path)) { - Console.WriteLine($"statistics file does not exist, try to reload {_retryInterval.TotalMinutes} minutes later"); - _timer.Change(_retryInterval, Interval); + if (!File.Exists(path)) + { + Console.WriteLine( + $"statistics file does not exist, try to reload {_retryInterval.TotalMinutes} minutes later"); + _recorder.Change(_retryInterval, RecordingInterval); return; } } - 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]) - } - group rawData by rawData.ServerName into server - select new - { - ServerName = server.Key, - data = server.ToList() - }).ToDictionary(server => server.ServerName, server => server.data); + RawStatistics = JsonConvert.DeserializeObject(File.ReadAllText(path)) ?? RawStatistics; } catch (Exception e) { @@ -306,41 +316,31 @@ namespace Shadowsocks.Controller private DateTime ParseExactOrUnknown(string str) { DateTime dateTime; - return !DateTime.TryParseExact(str, DateTimePattern, null, DateTimeStyles.None, out dateTime) ? UnknownDateTime : dateTime; + return !DateTime.TryParseExact(str, DateTimePattern, null, DateTimeStyles.None, out dateTime) + ? UnknownDateTime + : dateTime; } - public class State + public void UpdateLatency(int latency) { - public DataList dataList = new DataList(); - public const string Unknown = "Unknown"; + _latencyRecords.Add(latency); } - //TODO: redesign model - public class RawStatisticsData + private static int GetSpeedInKiBPerSecond(long bytes, double seconds) { - public DateTime Timestamp; - public string ServerName; - public string ICMPStatus; - public int RoundtripTime; + var result = (int) (bytes/seconds)/1024; + return result; } - public class StatisticsData + private class ICMPResult { - public float PackageLoss; - public int AverageResponse; - public int MinResponse; - public int MaxResponse; - } + internal readonly List RoundtripTime = new List(); + internal readonly Server Server; - public void UpdateLatency(int latency) - { - _latency = latency; - } - - private static int GetSpeedInKiBPerSecond(long bytes, double seconds) - { - var result = (int) (bytes / seconds) / 1024; - return result; + internal ICMPResult(Server server) + { + Server = server; + } } } -} +} \ No newline at end of file diff --git a/shadowsocks-csharp/Controller/Strategy/StatisticsStrategy.cs b/shadowsocks-csharp/Controller/Strategy/StatisticsStrategy.cs index df34c994..3567291e 100644 --- a/shadowsocks-csharp/Controller/Strategy/StatisticsStrategy.cs +++ b/shadowsocks-csharp/Controller/Strategy/StatisticsStrategy.cs @@ -11,12 +11,14 @@ using Shadowsocks.Model; namespace Shadowsocks.Controller.Strategy { + using Statistics = Dictionary>; class StatisticsStrategy : IStrategy { 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; @@ -39,7 +41,10 @@ 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 @@ -47,28 +52,26 @@ namespace Shadowsocks.Controller.Strategy private float GetScore(string serverName) { var config = _controller.StatisticsConfiguration; - List dataList; - if (_filteredStatistics == null || !_filteredStatistics.TryGetValue(serverName, out dataList)) return 0; - var successTimes = (float) dataList.Count(data => data.ICMPStatus.Equals(IPStatus.Success.ToString())); - var timedOutTimes = (float) dataList.Count(data => data.ICMPStatus.Equals(IPStatus.TimedOut.ToString())); - var statisticsData = new AvailabilityStatistics.StatisticsData - { - 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) - }; + List records; + if (_filteredStatistics == null || !_filteredStatistics.TryGetValue(serverName, out records)) return 0; float factor; float score = 0; + + var averageRecord = new StatisticsRecord(serverName, + records.FindAll(record => record.MaxInboundSpeed != null).Select(record => record.MaxInboundSpeed.Value), + records.FindAll(record => record.MaxOutboundSpeed != null).Select(record => record.MaxOutboundSpeed.Value), + records.FindAll(record => record.AverageLatency != null).Select(record => record.AverageLatency.Value)); + averageRecord.setResponse(records.Select(record => record.AverageResponse)); + if (!config.Calculations.TryGetValue("PackageLoss", out factor)) factor = 0; - score += statisticsData.PackageLoss*factor; + score += averageRecord.PackageLoss*factor ?? 0; if (!config.Calculations.TryGetValue("AverageResponse", out factor)) factor = 0; - score += statisticsData.AverageResponse*factor; + score += averageRecord.AverageResponse*factor ?? 0; if (!config.Calculations.TryGetValue("MinResponse", out factor)) factor = 0; - score += statisticsData.MinResponse*factor; + score += averageRecord.MinResponse*factor ?? 0; if (!config.Calculations.TryGetValue("MaxResponse", out factor)) factor = 0; - score += statisticsData.MaxResponse*factor; - Logging.Debug($"{serverName} {JsonConvert.SerializeObject(statisticsData)}"); + score += averageRecord.MaxResponse*factor ?? 0; + Logging.Debug($"{JsonConvert.SerializeObject(averageRecord, Formatting.Indented)}"); return score; } diff --git a/shadowsocks-csharp/Model/Server.cs b/shadowsocks-csharp/Model/Server.cs index 8306d9e3..54c29b6f 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..181567ce --- /dev/null +++ b/shadowsocks-csharp/Model/StatisticsRecord.cs @@ -0,0 +1,76 @@ +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; + + public string ServerName; + + // these fields ping-only records would be null + public int? AverageLatency; + public int? MinLatency; + public int? MaxLatency; + + public int? AverageInboundSpeed; + public int? MinInboundSpeed; + public int? MaxInboundSpeed; + + public int? AverageOutboundSpeed; + public int? MinOutboundSpeed; + public int? MaxOutboundSpeed; + + // if user disabled ping test, response would be null + public int? AverageResponse; + public int? MinResponse; + public int? MaxResponse; + public float? PackageLoss; + + public StatisticsRecord(string identifier, IEnumerable inboundSpeedRecords, IEnumerable outboundSpeedRecords, IEnumerable latencyRecords) + { + Timestamp = DateTime.Now; + ServerName = identifier; + if (inboundSpeedRecords != null && inboundSpeedRecords.Any()) + { + AverageInboundSpeed = (int) inboundSpeedRecords.Average(); + MinInboundSpeed = inboundSpeedRecords.Min(); + MaxInboundSpeed = inboundSpeedRecords.Max(); + } + if (outboundSpeedRecords != null && outboundSpeedRecords.Any()) + { + AverageOutboundSpeed = (int) outboundSpeedRecords.Average(); + MinOutboundSpeed = outboundSpeedRecords.Min(); + MaxOutboundSpeed = outboundSpeedRecords.Max(); + } + if (latencyRecords != null && latencyRecords.Any()) + { + AverageLatency = (int) latencyRecords.Average(); + MinLatency = latencyRecords.Min(); + MaxLatency = latencyRecords.Max(); + } + } + + public StatisticsRecord(string identifier, IEnumerable responseRecords) + { + Timestamp = DateTime.Now; + ServerName = identifier; + setResponse(responseRecords); + } + + public void setResponse(IEnumerable 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..e59fcde5 100644 --- a/shadowsocks-csharp/Model/StatisticsStrategyConfiguration.cs +++ b/shadowsocks-csharp/Model/StatisticsStrategyConfiguration.cs @@ -61,9 +61,7 @@ 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); } diff --git a/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.cs b/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.cs index aae33546..9db17302 100644 --- a/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.cs +++ b/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.cs @@ -10,6 +10,7 @@ using Shadowsocks.Model; namespace Shadowsocks.View { + using Statistics = Dictionary>; public partial class StatisticsStrategyConfigurationForm : Form { private readonly ShadowsocksController _controller; @@ -86,9 +87,9 @@ namespace Shadowsocks.View //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) { dataGroups = statistics.GroupBy(data => data.Timestamp.DayOfYear); @@ -105,12 +106,9 @@ 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.Equals(IPStatus.TimedOut.ToString())) - / (float)dataGroup.Count() * 100) - }; + dataGroup.First().Timestamp, + Ping = (int)dataGroup.Average(data => data.AverageResponse), + PackageLoss = dataGroup.Average(data => data.PackageLoss)}; foreach (var data in finalData) { _dataTable.Rows.Add(data.Timestamp, data.PackageLoss, data.Ping); diff --git a/shadowsocks-csharp/shadowsocks-csharp.csproj b/shadowsocks-csharp/shadowsocks-csharp.csproj index 69f7b546..389c1122 100644 --- a/shadowsocks-csharp/shadowsocks-csharp.csproj +++ b/shadowsocks-csharp/shadowsocks-csharp.csproj @@ -195,6 +195,7 @@ + True