migrate to record data model
This commit is contained in:
@@ -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<string, string>;
|
||||
using DataList = List<KeyValuePair<string, string>>;
|
||||
|
||||
using Statistics = Dictionary<string, List<AvailabilityStatistics.RawStatisticsData>>;
|
||||
|
||||
using Statistics = Dictionary<string, List<StatisticsRecord>>;
|
||||
|
||||
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<Server> _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<int> _inboundSpeedRecords;
|
||||
private long _lastInboundCounter;
|
||||
private long _lastOutboundCounter;
|
||||
private List<int> _latencyRecords;
|
||||
private List<int> _outboundSpeedRecords;
|
||||
private Timer _recorder;
|
||||
private List<Server> _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)
|
||||
{
|
||||
_state = new State();
|
||||
_timer = new Timer(Run, _state, _delayBeforeStart, Interval);
|
||||
_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)
|
||||
{
|
||||
_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<List<DataList>> ICMPTest(Server server)
|
||||
private async Task<ICMPResult> ICMPTest(Server server)
|
||||
{
|
||||
Logging.Debug("Ping " + server.FriendlyName());
|
||||
if (server.server == "") return null;
|
||||
var ret = new List<DataList>();
|
||||
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<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>("Timestamp", timestamp),
|
||||
new KeyValuePair<string, string>("Server", server.FriendlyName()),
|
||||
new KeyValuePair<string, string>("Status", reply?.Status.ToString()),
|
||||
new KeyValuePair<string, string>("RoundtripTime", reply?.RoundtripTime.ToString()),
|
||||
new KeyValuePair<string, string>("Latency", GetRecentLatency(server)),
|
||||
new KeyValuePair<string, string>("InboundSpeed", GetRecentInboundSpeed(server)),
|
||||
new KeyValuePair<string, string>("OutboundSpeed", GetRecentOutboundSpeed(server))
|
||||
//new KeyValuePair<string, string>("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<int>();
|
||||
_outboundSpeedRecords = new List<int>();
|
||||
_latencyRecords = new List<int>();
|
||||
}
|
||||
|
||||
private string GetRecentInboundSpeed(Server server)
|
||||
private void Run(object _)
|
||||
{
|
||||
return server != _currentServer ? Empty : _inboundSpeed.ToString();
|
||||
}
|
||||
|
||||
private string GetRecentLatency(Server server)
|
||||
{
|
||||
if (server != _currentServer) return Empty;
|
||||
return _latency == null ? Empty : _latency.ToString();
|
||||
}
|
||||
|
||||
private void ResetSpeed()
|
||||
{
|
||||
_currentServer = _controller.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();
|
||||
AppendRecord();
|
||||
Reset();
|
||||
FilterRawStatistics();
|
||||
Evaluate();
|
||||
ResetSpeed();
|
||||
}
|
||||
|
||||
private async void Evaluate()
|
||||
private async void AppendRecord()
|
||||
{
|
||||
foreach (var dataLists in await TaskEx.WhenAll(_servers.Select(ICMPTest)))
|
||||
//todo: option for icmp test
|
||||
var icmpResults = TaskEx.WhenAll(_servers.Select(ICMPTest));
|
||||
|
||||
var currentServerRecord = new StatisticsRecord(_currentServer.Identifier(),
|
||||
_inboundSpeedRecords, _outboundSpeedRecords, _latencyRecords);
|
||||
|
||||
foreach (var result in (await icmpResults).Where(result => result != null))
|
||||
{
|
||||
if (dataLists == null) continue;
|
||||
foreach (var dataList in dataLists.Where(dataList => dataList != null))
|
||||
List<StatisticsRecord> records;
|
||||
if (!RawStatistics.TryGetValue(result.Server.Identifier(), out records))
|
||||
{
|
||||
Append(dataList, Enumerable.Empty<DataUnit>());
|
||||
records = new List<StatisticsRecord>();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private void Save(object _)
|
||||
{
|
||||
try
|
||||
{
|
||||
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<DataUnit> 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<RawStatisticsData> 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<RawStatisticsData> ?? 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<Statistics>(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;
|
||||
}
|
||||
|
||||
public class State
|
||||
{
|
||||
public DataList dataList = new DataList();
|
||||
public const string Unknown = "Unknown";
|
||||
}
|
||||
|
||||
//TODO: redesign model
|
||||
public class RawStatisticsData
|
||||
{
|
||||
public DateTime Timestamp;
|
||||
public string ServerName;
|
||||
public string ICMPStatus;
|
||||
public int RoundtripTime;
|
||||
}
|
||||
|
||||
public class StatisticsData
|
||||
{
|
||||
public float PackageLoss;
|
||||
public int AverageResponse;
|
||||
public int MinResponse;
|
||||
public int MaxResponse;
|
||||
return !DateTime.TryParseExact(str, DateTimePattern, null, DateTimeStyles.None, out dateTime)
|
||||
? UnknownDateTime
|
||||
: dateTime;
|
||||
}
|
||||
|
||||
public void UpdateLatency(int latency)
|
||||
{
|
||||
_latency = latency;
|
||||
_latencyRecords.Add(latency);
|
||||
}
|
||||
|
||||
private static int GetSpeedInKiBPerSecond(long bytes, double seconds)
|
||||
{
|
||||
var result = (int) (bytes / seconds) / 1024;
|
||||
var result = (int) (bytes/seconds)/1024;
|
||||
return result;
|
||||
}
|
||||
|
||||
private class ICMPResult
|
||||
{
|
||||
internal readonly List<int?> RoundtripTime = new List<int?>();
|
||||
internal readonly Server Server;
|
||||
|
||||
internal ICMPResult(Server server)
|
||||
{
|
||||
Server = server;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,14 @@ using Shadowsocks.Model;
|
||||
|
||||
namespace Shadowsocks.Controller.Strategy
|
||||
{
|
||||
using Statistics = Dictionary<string, List<StatisticsRecord>>;
|
||||
class StatisticsStrategy : IStrategy
|
||||
{
|
||||
private readonly ShadowsocksController _controller;
|
||||
private Server _currentServer;
|
||||
private readonly Timer _timer;
|
||||
private Dictionary<string, List<AvailabilityStatistics.RawStatisticsData>> _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<string, List<AvailabilityStatistics.RawStatisticsData>>();
|
||||
_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<AvailabilityStatistics.RawStatisticsData> 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<StatisticsRecord> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,5 +95,10 @@ namespace Shadowsocks.Model
|
||||
throw new FormatException();
|
||||
}
|
||||
}
|
||||
|
||||
public string Identifier()
|
||||
{
|
||||
return server + ':' + server_port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<int> inboundSpeedRecords, IEnumerable<int> outboundSpeedRecords, IEnumerable<int> 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<int?> responseRecords)
|
||||
{
|
||||
Timestamp = DateTime.Now;
|
||||
ServerName = identifier;
|
||||
setResponse(responseRecords);
|
||||
}
|
||||
|
||||
public void setResponse(IEnumerable<int?> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using Shadowsocks.Model;
|
||||
|
||||
namespace Shadowsocks.View
|
||||
{
|
||||
using Statistics = Dictionary<string, List<StatisticsRecord>>;
|
||||
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<AvailabilityStatistics.RawStatisticsData> statistics;
|
||||
List<StatisticsRecord> statistics;
|
||||
if (!_controller.availabilityStatistics.FilteredStatistics.TryGetValue(serverName, out statistics)) return;
|
||||
IEnumerable<IGrouping<int, AvailabilityStatistics.RawStatisticsData>> dataGroups;
|
||||
IEnumerable<IGrouping<int, StatisticsRecord>> 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);
|
||||
|
||||
@@ -195,6 +195,7 @@
|
||||
<Compile Include="Model\LogViewerConfig.cs" />
|
||||
<Compile Include="Model\Server.cs" />
|
||||
<Compile Include="Model\Configuration.cs" />
|
||||
<Compile Include="Model\StatisticsRecord.cs" />
|
||||
<Compile Include="Model\StatisticsStrategyConfiguration.cs" />
|
||||
<Compile Include="Properties\Resources.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
|
||||
Reference in New Issue
Block a user