Merge pull request #457 from icylogic/feature/statistics_ui
feature/statistics ui
This commit is contained in:
@@ -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<StatisticsRecord>>;
|
||||
|
||||
using Statistics = Dictionary<string, List<AvailabilityStatistics.RawStatisticsData>>;
|
||||
|
||||
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
|
||||
foreach (var kv in _lastInboundCounter)
|
||||
{
|
||||
jsonString = await new HttpClient().GetStringAsync(API);
|
||||
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");
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
Logging.LogUsefulException(e);
|
||||
return null;
|
||||
}
|
||||
JObject obj;
|
||||
try
|
||||
{
|
||||
obj = JObject.Parse(jsonString);
|
||||
}
|
||||
catch (JsonReaderException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
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)
|
||||
{
|
||||
Interlocked.Add(ref inboundCounter, n);
|
||||
if (_config.availabilityStatistics)
|
||||
{
|
||||
new Task(() => availabilityStatistics.UpdateLatency(server, (int) latency.TotalMilliseconds)).Start();
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateOutboundCounter(long n)
|
||||
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(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)
|
||||
{
|
||||
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)}");
|
||||
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)
|
||||
{
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-6
@@ -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();
|
||||
}
|
||||
|
||||
private void chartModeSelector_Enter(object sender, EventArgs e)
|
||||
{
|
||||
|
||||
LoadChartData();
|
||||
}
|
||||
|
||||
private void dayMode_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
loadChartData();
|
||||
LoadChartData();
|
||||
}
|
||||
|
||||
private void allMode_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
loadChartData();
|
||||
LoadChartData();
|
||||
}
|
||||
|
||||
private void PingCheckBox_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
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)')" />
|
||||
|
||||
Reference in New Issue
Block a user