feature/statistics uitags/3.0
@@ -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<string, string>; | |||
using DataList = List<KeyValuePair<string, string>>; | |||
using Statistics = Dictionary<string, List<AvailabilityStatistics.RawStatisticsData>>; | |||
using Statistics = Dictionary<string, List<StatisticsRecord>>; | |||
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<Server> _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<string, List<int>> _latencyRecords = new ConcurrentDictionary<string, List<int>>(); | |||
//speed in KiB/s | |||
private readonly ConcurrentDictionary<string, long> _inboundCounter = new ConcurrentDictionary<string, long>(); | |||
private readonly ConcurrentDictionary<string, long> _lastInboundCounter = new ConcurrentDictionary<string, long>(); | |||
private readonly ConcurrentDictionary<string, List<int>> _inboundSpeedRecords = new ConcurrentDictionary<string, List<int>>(); | |||
private readonly ConcurrentDictionary<string, long> _outboundCounter = new ConcurrentDictionary<string, long>(); | |||
private readonly ConcurrentDictionary<string, long> _lastOutboundCounter = new ConcurrentDictionary<string, long>(); | |||
private readonly ConcurrentDictionary<string, List<int>> _outboundSpeedRecords = new ConcurrentDictionary<string, List<int>>(); | |||
//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<DataList> 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<DataList> 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<int> {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<int> {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<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, Timeout); | |||
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>("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<string, StatisticsRecord>(); | |||
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<int> inboundSpeedRecords = null; | |||
List<int> outboundSpeedRecords = null; | |||
List<int> 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<DataUnit> 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<StatisticsRecord> 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<StatisticsRecord>(); | |||
} | |||
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<RawStatisticsData> 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<RawStatisticsData> ?? 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<Statistics>(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<int?> RoundtripTime = new List<int?>(); | |||
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<int> records; | |||
_latencyRecords.TryGetValue(server.Identifier(), out records); | |||
if (records == null) | |||
{ | |||
records = new List<int>(); | |||
} | |||
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; | |||
} | |||
} | |||
} |
@@ -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 | |||
{ | |||
@@ -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); | |||
} | |||
} | |||
} | |||
} |
@@ -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<string, List<StatisticsRecord>>; | |||
internal class StatisticsStrategy : IStrategy, IDisposable | |||
{ | |||
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; | |||
@@ -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<string, List<AvailabilityStatistics.RawStatisticsData>>(); | |||
_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<StatisticsRecord> records) | |||
{ | |||
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 == 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(); | |||
} | |||
} | |||
} |
@@ -30,7 +30,7 @@ Quit=退出 | |||
Edit Servers=编辑服务器 | |||
Load Balance=负载均衡 | |||
High Availability=高可用 | |||
Choose By Total Package Loss=累计丢包率 | |||
Choose by statistics=根据统计 | |||
# Config Form | |||
@@ -95,5 +95,10 @@ namespace Shadowsocks.Model | |||
throw new FormatException(); | |||
} | |||
} | |||
public string Identifier() | |||
{ | |||
return server + ':' + server_port; | |||
} | |||
} | |||
} |
@@ -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<int> inboundSpeedRecords, ICollection<int> outboundSpeedRecords, ICollection<int> 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<int?> responseRecords) | |||
{ | |||
ServerIdentifier = identifier; | |||
SetResponse(responseRecords); | |||
} | |||
public void SetResponse(ICollection<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; | |||
} | |||
} | |||
} |
@@ -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; } | |||
} | |||
} | |||
} |
@@ -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(); | |||
@@ -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; | |||
} | |||
} |
@@ -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<string> _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<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) | |||
{ | |||
_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; | |||
} | |||
} | |||
} |
@@ -118,9 +118,12 @@ | |||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | |||
</resheader> | |||
<metadata name="bindingConfiguration.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> | |||
<value>1, 30</value> | |||
<value>4, 5</value> | |||
</metadata> | |||
<metadata name="CalculatinTip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> | |||
<value>238, 6</value> | |||
</metadata> | |||
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | |||
<value>63</value> | |||
<value>191</value> | |||
</metadata> | |||
</root> |
@@ -60,6 +60,7 @@ | |||
<CodeAnalysisRuleSet>ManagedMinimumRules.ruleset</CodeAnalysisRuleSet> | |||
<Prefer32Bit>false</Prefer32Bit> | |||
<DebugSymbols>true</DebugSymbols> | |||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> | |||
</PropertyGroup> | |||
<PropertyGroup> | |||
<ApplicationManifest>app.manifest</ApplicationManifest> | |||
@@ -195,6 +196,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> | |||
@@ -340,13 +342,12 @@ | |||
<Files Output="false" Required="true" ParameterType="Microsoft.Build.Framework.ITaskItem[]" /> | |||
</ParameterGroup> | |||
<Task Evaluate="true"> | |||
<Reference xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Include="System.Xml" /> | |||
<Reference xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Include="System.Xml.Linq" /> | |||
<Using xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Namespace="System" /> | |||
<Using xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Namespace="System.IO" /> | |||
<Using xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Namespace="System.Xml.Linq" /> | |||
<Code xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Type="Fragment" Language="cs"> | |||
<![CDATA[ | |||
<Reference xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Include="System.Xml" /> | |||
<Reference xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Include="System.Xml.Linq" /> | |||
<Using xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Namespace="System" /> | |||
<Using xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Namespace="System.IO" /> | |||
<Using xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Namespace="System.Xml.Linq" /> | |||
<Code xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Type="Fragment" Language="cs"><![CDATA[ | |||
var config = XElement.Load(Config.ItemSpec).Elements("Costura").FirstOrDefault(); | |||
if (config == null) return true; | |||
@@ -365,8 +366,8 @@ var filesToCleanup = Files.Select(f => f.ItemSpec).Where(f => !excludedAssemblie | |||
foreach (var item in filesToCleanup) | |||
File.Delete(item); | |||
]]> | |||
</Code></Task> | |||
]]></Code> | |||
</Task> | |||
</UsingTask> | |||
<Target Name="CleanReferenceCopyLocalPaths" AfterTargets="AfterBuild;NonWinFodyTarget"> | |||
<CosturaCleanup Config="FodyWeavers.xml" Files="@(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')" /> | |||