Correct AvailabilityStatistics's behavior under special strategies like "LoadBalance", which may switch the server several times in a short period.tags/3.0
@@ -1,4 +1,5 @@ | |||||
using System; | using System; | ||||
using System.Collections.Concurrent; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.IO; | using System.IO; | ||||
using System.Linq; | using System.Linq; | ||||
@@ -31,12 +32,14 @@ namespace Shadowsocks.Controller | |||||
public const int TimeoutMilliseconds = 500; | public const int TimeoutMilliseconds = 500; | ||||
//records cache for current server in {_monitorInterval} minutes | //records cache for current server in {_monitorInterval} minutes | ||||
private List<int> _latencyRecords; | |||||
private readonly ConcurrentDictionary<string, List<int>> _latencyRecords = new ConcurrentDictionary<string, List<int>>(); | |||||
//speed in KiB/s | //speed in KiB/s | ||||
private long _lastInboundCounter; | |||||
private List<int> _inboundSpeedRecords; | |||||
private long _lastOutboundCounter; | |||||
private List<int> _outboundSpeedRecords; | |||||
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 | //tasks | ||||
private readonly TimeSpan _delayBeforeStart = TimeSpan.FromSeconds(1); | private readonly TimeSpan _delayBeforeStart = TimeSpan.FromSeconds(1); | ||||
@@ -45,12 +48,11 @@ namespace Shadowsocks.Controller | |||||
private TimeSpan RecordingInterval => TimeSpan.FromMinutes(Config.DataCollectionMinutes); | private TimeSpan RecordingInterval => TimeSpan.FromMinutes(Config.DataCollectionMinutes); | ||||
private Timer _speedMonior; | private Timer _speedMonior; | ||||
private readonly TimeSpan _monitorInterval = TimeSpan.FromSeconds(1); | private readonly TimeSpan _monitorInterval = TimeSpan.FromSeconds(1); | ||||
private Timer _writer; //write RawStatistics to file | |||||
private readonly TimeSpan _writingInterval = TimeSpan.FromMinutes(1); | |||||
//private Timer _writer; //write RawStatistics to file | |||||
//private readonly TimeSpan _writingInterval = TimeSpan.FromMinutes(1); | |||||
private ShadowsocksController _controller; | private ShadowsocksController _controller; | ||||
private StatisticsStrategyConfiguration Config => _controller.StatisticsConfiguration; | private StatisticsStrategyConfiguration Config => _controller.StatisticsConfiguration; | ||||
private Server CurrentServer => _controller.GetCurrentServer(); | |||||
// Static Singleton Initialization | // Static Singleton Initialization | ||||
public static AvailabilityStatistics Instance { get; } = new AvailabilityStatistics(); | public static AvailabilityStatistics Instance { get; } = new AvailabilityStatistics(); | ||||
@@ -73,13 +75,11 @@ namespace Shadowsocks.Controller | |||||
StartTimerWithoutState(ref _recorder, Run, RecordingInterval); | StartTimerWithoutState(ref _recorder, Run, RecordingInterval); | ||||
LoadRawStatistics(); | LoadRawStatistics(); | ||||
StartTimerWithoutState(ref _speedMonior, UpdateSpeed, _monitorInterval); | StartTimerWithoutState(ref _speedMonior, UpdateSpeed, _monitorInterval); | ||||
StartTimerWithoutState(ref _writer, Save, _writingInterval); | |||||
} | } | ||||
else | else | ||||
{ | { | ||||
_recorder?.Dispose(); | _recorder?.Dispose(); | ||||
_speedMonior?.Dispose(); | _speedMonior?.Dispose(); | ||||
_writer?.Dispose(); | |||||
} | } | ||||
} | } | ||||
catch (Exception e) | catch (Exception e) | ||||
@@ -98,18 +98,27 @@ namespace Shadowsocks.Controller | |||||
private void UpdateSpeed(object _) | private void UpdateSpeed(object _) | ||||
{ | { | ||||
var bytes = _controller.inboundCounter - _lastInboundCounter; | |||||
_lastInboundCounter = _controller.inboundCounter; | |||||
var inboundSpeed = GetSpeedInKiBPerSecond(bytes, _monitorInterval.TotalSeconds); | |||||
_inboundSpeedRecords.Add(inboundSpeed); | |||||
bytes = _controller.outboundCounter - _lastOutboundCounter; | |||||
_lastOutboundCounter = _controller.outboundCounter; | |||||
var outboundSpeed = GetSpeedInKiBPerSecond(bytes, _monitorInterval.TotalSeconds); | |||||
_outboundSpeedRecords.Add(outboundSpeed); | |||||
Logging.Debug( | |||||
$"{CurrentServer.FriendlyName()}: current/max inbound {inboundSpeed}/{_inboundSpeedRecords.Max()} KiB/s, current/max outbound {outboundSpeed}/{_outboundSpeedRecords.Max()} KiB/s"); | |||||
foreach (var kv in _lastInboundCounter) | |||||
{ | |||||
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"); | |||||
} | |||||
} | } | ||||
private async Task<ICMPResult> ICMPTest(Server server) | private async Task<ICMPResult> ICMPTest(Server server) | ||||
@@ -161,66 +170,75 @@ namespace Shadowsocks.Controller | |||||
private void Reset() | private void Reset() | ||||
{ | { | ||||
_inboundSpeedRecords = new List<int>(); | |||||
_outboundSpeedRecords = new List<int>(); | |||||
_latencyRecords = new List<int>(); | |||||
_inboundSpeedRecords.Clear(); | |||||
_outboundSpeedRecords.Clear(); | |||||
_latencyRecords.Clear(); | |||||
} | } | ||||
private void Run(object _) | private void Run(object _) | ||||
{ | { | ||||
UpdateRecords(); | UpdateRecords(); | ||||
Save(); | |||||
Reset(); | Reset(); | ||||
FilterRawStatistics(); | FilterRawStatistics(); | ||||
} | } | ||||
private async void UpdateRecords() | private async void UpdateRecords() | ||||
{ | { | ||||
var currentServerRecord = new StatisticsRecord(CurrentServer.Identifier(), _inboundSpeedRecords, _outboundSpeedRecords, _latencyRecords); | |||||
var records = new Dictionary<string, StatisticsRecord>(); | |||||
if (!Config.Ping) | |||||
foreach (var server in _controller.GetCurrentConfiguration().configs) | |||||
{ | { | ||||
AppendRecord(CurrentServer, currentServerRecord); | |||||
return; | |||||
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)); | |||||
} | } | ||||
var icmpResults = TaskEx.WhenAll(_controller.GetCurrentConfiguration().configs.Select(ICMPTest)); | |||||
foreach (var result in (await icmpResults).Where(result => result != null)) | |||||
if (Config.Ping) | |||||
{ | { | ||||
if (result.Server.Equals(CurrentServer)) | |||||
var icmpResults = await TaskEx.WhenAll(_controller.GetCurrentConfiguration().configs.Select(ICMPTest)); | |||||
foreach (var result in icmpResults.Where(result => result != null)) | |||||
{ | { | ||||
currentServerRecord.setResponse(result.RoundtripTime); | |||||
AppendRecord(CurrentServer, currentServerRecord); | |||||
} | |||||
else | |||||
{ | |||||
AppendRecord(result.Server, new StatisticsRecord(result.Server.Identifier(), result.RoundtripTime)); | |||||
records[result.Server.Identifier()].SetResponse(result.RoundtripTime); | |||||
} | } | ||||
} | } | ||||
foreach (var kv in records.Where(kv => !kv.Value.IsEmptyData())) | |||||
{ | |||||
AppendRecord(kv.Key, kv.Value); | |||||
} | |||||
} | } | ||||
private void AppendRecord(Server server, StatisticsRecord record) | |||||
private void AppendRecord(string serverIdentifier, StatisticsRecord record) | |||||
{ | { | ||||
List<StatisticsRecord> records; | List<StatisticsRecord> records; | ||||
if (!RawStatistics.TryGetValue(server.Identifier(), out records)) | |||||
if (!RawStatistics.TryGetValue(serverIdentifier, out records)) | |||||
{ | { | ||||
records = new List<StatisticsRecord>(); | records = new List<StatisticsRecord>(); | ||||
} | } | ||||
records.Add(record); | records.Add(record); | ||||
RawStatistics[server.Identifier()] = records; | |||||
RawStatistics[serverIdentifier] = records; | |||||
} | } | ||||
private void Save(object _) | |||||
private void Save() | |||||
{ | { | ||||
if (RawStatistics.Count == 0) | |||||
{ | |||||
return; | |||||
} | |||||
try | try | ||||
{ | { | ||||
File.WriteAllText(AvailabilityStatisticsFile, | |||||
JsonConvert.SerializeObject(RawStatistics, Formatting.None)); | |||||
var content = JsonConvert.SerializeObject(RawStatistics, Formatting.None); | |||||
File.WriteAllText(AvailabilityStatisticsFile, content); | |||||
} | } | ||||
catch (IOException e) | catch (IOException e) | ||||
{ | { | ||||
Logging.LogUsefulException(e); | Logging.LogUsefulException(e); | ||||
_writer.Change(_retryInterval, _writingInterval); | |||||
} | } | ||||
} | } | ||||
@@ -273,11 +291,6 @@ namespace Shadowsocks.Controller | |||||
} | } | ||||
} | } | ||||
public void UpdateLatency(int latency) | |||||
{ | |||||
_latencyRecords.Add(latency); | |||||
} | |||||
private static int GetSpeedInKiBPerSecond(long bytes, double seconds) | private static int GetSpeedInKiBPerSecond(long bytes, double seconds) | ||||
{ | { | ||||
var result = (int) (bytes/seconds)/1024; | var result = (int) (bytes/seconds)/1024; | ||||
@@ -298,8 +311,49 @@ namespace Shadowsocks.Controller | |||||
public void Dispose() | public void Dispose() | ||||
{ | { | ||||
_recorder.Dispose(); | _recorder.Dispose(); | ||||
_writer.Dispose(); | |||||
_speedMonior.Dispose(); | _speedMonior.Dispose(); | ||||
} | } | ||||
public void UpdateLatency(Server server, int latency) | |||||
{ | |||||
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,14 @@ namespace Shadowsocks.Controller | |||||
return true; | 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) | public void UpdateLatency(Server server, TimeSpan latency) | ||||
@@ -488,10 +488,7 @@ namespace Shadowsocks.Controller | |||||
var latency = DateTime.Now - _startConnectTime; | var latency = DateTime.Now - _startConnectTime; | ||||
IStrategy strategy = controller.GetCurrentStrategy(); | IStrategy strategy = controller.GetCurrentStrategy(); | ||||
if (strategy != null) | |||||
{ | |||||
strategy.UpdateLatency(server, latency); | |||||
} | |||||
strategy?.UpdateLatency(server, latency); | |||||
tcprelay.UpdateLatency(server, latency); | tcprelay.UpdateLatency(server, latency); | ||||
StartPipe(); | StartPipe(); | ||||
@@ -543,7 +540,7 @@ namespace Shadowsocks.Controller | |||||
{ | { | ||||
int bytesRead = remote.EndReceive(ar); | int bytesRead = remote.EndReceive(ar); | ||||
totalRead += bytesRead; | totalRead += bytesRead; | ||||
tcprelay.UpdateInboundCounter(bytesRead); | |||||
tcprelay.UpdateInboundCounter(server, bytesRead); | |||||
if (bytesRead > 0) | if (bytesRead > 0) | ||||
{ | { | ||||
@@ -610,7 +607,7 @@ namespace Shadowsocks.Controller | |||||
encryptor.Encrypt(connetionRecvBuffer, bytesRead, connetionSendBuffer, out bytesToSend); | encryptor.Encrypt(connetionRecvBuffer, bytesRead, connetionSendBuffer, out bytesToSend); | ||||
} | } | ||||
Logging.Debug(remote, bytesToSend, "TCP Relay", "@PipeConnectionReceiveCallback() (upload)"); | Logging.Debug(remote, bytesToSend, "TCP Relay", "@PipeConnectionReceiveCallback() (upload)"); | ||||
tcprelay.UpdateOutboundCounter(bytesToSend); | |||||
tcprelay.UpdateOutboundCounter(server, bytesToSend); | |||||
_startSendingTime = DateTime.Now; | _startSendingTime = DateTime.Now; | ||||
_bytesToSend = bytesToSend; | _bytesToSend = bytesToSend; | ||||
remote.BeginSend(connetionSendBuffer, 0, bytesToSend, 0, new AsyncCallback(PipeRemoteSendCallback), null); | remote.BeginSend(connetionSendBuffer, 0, bytesToSend, 0, new AsyncCallback(PipeRemoteSendCallback), null); | ||||
@@ -5,7 +5,7 @@ using System.Net; | |||||
using System.Net.Sockets; | using System.Net.Sockets; | ||||
using System.Text; | using System.Text; | ||||
using System.Threading; | using System.Threading; | ||||
using System.Threading.Tasks; | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using Shadowsocks.Controller.Strategy; | using Shadowsocks.Controller.Strategy; | ||||
@@ -307,14 +307,30 @@ namespace Shadowsocks.Controller | |||||
Configuration.Save(_config); | 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); | 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); | Interlocked.Add(ref outboundCounter, n); | ||||
if (_config.availabilityStatistics) | |||||
{ | |||||
new Task(() => availabilityStatistics.UpdateOutboundCounter(server, n)).Start(); | |||||
} | |||||
} | } | ||||
protected void Reload() | protected void Reload() | ||||
@@ -498,12 +514,5 @@ namespace Shadowsocks.Controller | |||||
} | } | ||||
} | } | ||||
public void UpdateLatency(Server server, TimeSpan latency) | |||||
{ | |||||
if (_config.availabilityStatistics) | |||||
{ | |||||
availabilityStatistics.UpdateLatency((int) latency.TotalMilliseconds); | |||||
} | |||||
} | |||||
} | } | ||||
} | } |
@@ -49,29 +49,32 @@ namespace Shadowsocks.Controller.Strategy | |||||
//return the score by data | //return the score by data | ||||
//server with highest score will be choosen | //server with highest score will be choosen | ||||
private float GetScore(string serverName) | |||||
private float? GetScore(string identifier, List<StatisticsRecord> records) | |||||
{ | { | ||||
var config = _controller.StatisticsConfiguration; | var config = _controller.StatisticsConfiguration; | ||||
List<StatisticsRecord> records; | |||||
if (_filteredStatistics == null || !_filteredStatistics.TryGetValue(serverName, out records)) return 0; | |||||
float factor; | |||||
float score = 0; | |||||
var averageRecord = new StatisticsRecord(serverName, | |||||
records.FindAll(record => record.MaxInboundSpeed != null).Select(record => record.MaxInboundSpeed.Value), | |||||
records.FindAll(record => record.MaxOutboundSpeed != null).Select(record => record.MaxOutboundSpeed.Value), | |||||
records.FindAll(record => record.AverageLatency != null).Select(record => record.AverageLatency.Value)); | |||||
averageRecord.setResponse(records.Select(record => record.AverageResponse)); | |||||
if (!config.Calculations.TryGetValue("PackageLoss", out factor)) factor = 0; | |||||
score += averageRecord.PackageLoss * factor ?? 0; | |||||
if (!config.Calculations.TryGetValue("AverageResponse", out factor)) factor = 0; | |||||
score += averageRecord.AverageResponse * factor ?? 0; | |||||
if (!config.Calculations.TryGetValue("MinResponse", out factor)) factor = 0; | |||||
score += averageRecord.MinResponse * factor ?? 0; | |||||
if (!config.Calculations.TryGetValue("MaxResponse", out factor)) factor = 0; | |||||
score += averageRecord.MaxResponse * factor ?? 0; | |||||
Logging.Debug($"Highest score: {score} {JsonConvert.SerializeObject(averageRecord, Formatting.Indented)}"); | |||||
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) | |||||
{ | |||||
Logging.Debug($"Highest score: {score} {JsonConvert.SerializeObject(averageRecord, Formatting.Indented)}"); | |||||
} | |||||
return score; | return score; | ||||
} | } | ||||
@@ -83,15 +86,25 @@ namespace Shadowsocks.Controller.Strategy | |||||
} | } | ||||
try | 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 for evaluation"); | |||||
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}"); | LogWhenEnabled($"Switch to server: {bestResult.server.FriendlyName()} by statistics: score {bestResult.score}"); | ||||
_currentServer = bestResult.server; | _currentServer = bestResult.server; | ||||
@@ -112,7 +125,7 @@ namespace Shadowsocks.Controller.Strategy | |||||
public string ID => "com.shadowsocks.strategy.scbs"; | 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) | public Server GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint) | ||||
{ | { | ||||
@@ -30,7 +30,7 @@ Quit=退出 | |||||
Edit Servers=编辑服务器 | Edit Servers=编辑服务器 | ||||
Load Balance=负载均衡 | Load Balance=负载均衡 | ||||
High Availability=高可用 | High Availability=高可用 | ||||
Choose By Total Package Loss=累计丢包率 | |||||
Choose by statistics=根据统计 | |||||
# Config Form | # Config Form | ||||
@@ -9,34 +9,49 @@ namespace Shadowsocks.Model | |||||
public class StatisticsRecord | public class StatisticsRecord | ||||
{ | { | ||||
public DateTime Timestamp { get; set; } = DateTime.Now; | public DateTime Timestamp { get; set; } = DateTime.Now; | ||||
public string ServerName { get; set; } | |||||
public string ServerIdentifier { get; set; } | |||||
// in ping-only records, these fields would be null | // in ping-only records, these fields would be null | ||||
public int? AverageLatency; | public int? AverageLatency; | ||||
public int? MinLatency; | public int? MinLatency; | ||||
public int? MaxLatency; | public int? MaxLatency; | ||||
private bool EmptyLatencyData => (AverageLatency == null) && (MinLatency == null) && (MaxLatency == null); | |||||
public int? AverageInboundSpeed; | public int? AverageInboundSpeed; | ||||
public int? MinInboundSpeed; | public int? MinInboundSpeed; | ||||
public int? MaxInboundSpeed; | public int? MaxInboundSpeed; | ||||
private bool EmptyInboundSpeedData | |||||
=> (AverageInboundSpeed == null) && (MinInboundSpeed == null) && (MaxInboundSpeed == null); | |||||
public int? AverageOutboundSpeed; | public int? AverageOutboundSpeed; | ||||
public int? MinOutboundSpeed; | public int? MinOutboundSpeed; | ||||
public int? MaxOutboundSpeed; | public int? MaxOutboundSpeed; | ||||
private bool EmptyOutboundSpeedData | |||||
=> (AverageOutboundSpeed == null) && (MinOutboundSpeed == null) && (MaxOutboundSpeed == null); | |||||
// if user disabled ping test, response would be null | // if user disabled ping test, response would be null | ||||
public int? AverageResponse; | public int? AverageResponse; | ||||
public int? MinResponse; | public int? MinResponse; | ||||
public int? MaxResponse; | public int? MaxResponse; | ||||
public float? PackageLoss; | 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() | ||||
{ | { | ||||
} | } | ||||
public StatisticsRecord(string identifier, IEnumerable<int> inboundSpeedRecords, IEnumerable<int> outboundSpeedRecords, IEnumerable<int> latencyRecords) | |||||
public StatisticsRecord(string identifier, ICollection<int> inboundSpeedRecords, ICollection<int> outboundSpeedRecords, ICollection<int> latencyRecords) | |||||
{ | { | ||||
ServerName = identifier; | |||||
ServerIdentifier = identifier; | |||||
if (inboundSpeedRecords != null && inboundSpeedRecords.Any()) | if (inboundSpeedRecords != null && inboundSpeedRecords.Any()) | ||||
{ | { | ||||
AverageInboundSpeed = (int) inboundSpeedRecords.Average(); | AverageInboundSpeed = (int) inboundSpeedRecords.Average(); | ||||
@@ -57,13 +72,13 @@ namespace Shadowsocks.Model | |||||
} | } | ||||
} | } | ||||
public StatisticsRecord(string identifier, IEnumerable<int?> responseRecords) | |||||
public StatisticsRecord(string identifier, ICollection<int?> responseRecords) | |||||
{ | { | ||||
ServerName = identifier; | |||||
setResponse(responseRecords); | |||||
ServerIdentifier = identifier; | |||||
SetResponse(responseRecords); | |||||
} | } | ||||
public void setResponse(IEnumerable<int?> responseRecords) | |||||
public void SetResponse(ICollection<int?> responseRecords) | |||||
{ | { | ||||
if (responseRecords == null) return; | if (responseRecords == null) return; | ||||
var records = responseRecords.Where(response => response != null).Select(response => response.Value).ToList(); | var records = responseRecords.Where(response => response != null).Select(response => response.Value).ToList(); | ||||
@@ -71,7 +86,7 @@ namespace Shadowsocks.Model | |||||
AverageResponse = (int?) records.Average(); | AverageResponse = (int?) records.Average(); | ||||
MinResponse = records.Min(); | MinResponse = records.Min(); | ||||
MaxResponse = records.Max(); | MaxResponse = records.Max(); | ||||
PackageLoss = responseRecords.Count(response => response != null)/(float) responseRecords.Count(); | |||||
PackageLoss = responseRecords.Count(response => response != null)/(float) responseRecords.Count; | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -36,6 +36,7 @@ | |||||
System.Windows.Forms.DataVisualization.Charting.Series series3 = 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.StatisticsChart = new System.Windows.Forms.DataVisualization.Charting.Chart(); | ||||
this.PingCheckBox = 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.label2 = new System.Windows.Forms.Label(); | ||||
this.label3 = new System.Windows.Forms.Label(); | this.label3 = new System.Windows.Forms.Label(); | ||||
this.chartModeSelector = new System.Windows.Forms.GroupBox(); | this.chartModeSelector = new System.Windows.Forms.GroupBox(); | ||||
@@ -58,8 +59,8 @@ | |||||
this.CancelButton = new System.Windows.Forms.Button(); | this.CancelButton = new System.Windows.Forms.Button(); | ||||
this.OKButton = new System.Windows.Forms.Button(); | this.OKButton = new System.Windows.Forms.Button(); | ||||
this.CalculatinTip = new System.Windows.Forms.ToolTip(this.components); | this.CalculatinTip = new System.Windows.Forms.ToolTip(this.components); | ||||
this.bindingConfiguration = new System.Windows.Forms.BindingSource(this.components); | |||||
((System.ComponentModel.ISupportInitialize)(this.StatisticsChart)).BeginInit(); | ((System.ComponentModel.ISupportInitialize)(this.StatisticsChart)).BeginInit(); | ||||
((System.ComponentModel.ISupportInitialize)(this.bindingConfiguration)).BeginInit(); | |||||
this.chartModeSelector.SuspendLayout(); | this.chartModeSelector.SuspendLayout(); | ||||
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit(); | ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit(); | ||||
this.splitContainer1.Panel1.SuspendLayout(); | this.splitContainer1.Panel1.SuspendLayout(); | ||||
@@ -76,7 +77,6 @@ | |||||
this.splitContainer3.Panel1.SuspendLayout(); | this.splitContainer3.Panel1.SuspendLayout(); | ||||
this.splitContainer3.Panel2.SuspendLayout(); | this.splitContainer3.Panel2.SuspendLayout(); | ||||
this.splitContainer3.SuspendLayout(); | this.splitContainer3.SuspendLayout(); | ||||
((System.ComponentModel.ISupportInitialize)(this.bindingConfiguration)).BeginInit(); | |||||
this.SuspendLayout(); | this.SuspendLayout(); | ||||
// | // | ||||
// StatisticsChart | // StatisticsChart | ||||
@@ -142,6 +142,10 @@ | |||||
this.PingCheckBox.UseVisualStyleBackColor = true; | this.PingCheckBox.UseVisualStyleBackColor = true; | ||||
this.PingCheckBox.CheckedChanged += new System.EventHandler(this.PingCheckBox_CheckedChanged); | this.PingCheckBox.CheckedChanged += new System.EventHandler(this.PingCheckBox_CheckedChanged); | ||||
// | // | ||||
// bindingConfiguration | |||||
// | |||||
this.bindingConfiguration.DataSource = typeof(Shadowsocks.Model.StatisticsStrategyConfiguration); | |||||
// | |||||
// label2 | // label2 | ||||
// | // | ||||
this.label2.AutoSize = true; | this.label2.AutoSize = true; | ||||
@@ -167,7 +171,7 @@ | |||||
this.chartModeSelector.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); | 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.allMode); | ||||
this.chartModeSelector.Controls.Add(this.dayMode); | this.chartModeSelector.Controls.Add(this.dayMode); | ||||
this.chartModeSelector.Location = new System.Drawing.Point(729, 194); | |||||
this.chartModeSelector.Location = new System.Drawing.Point(729, 188); | |||||
this.chartModeSelector.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); | this.chartModeSelector.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); | ||||
this.chartModeSelector.Name = "chartModeSelector"; | this.chartModeSelector.Name = "chartModeSelector"; | ||||
this.chartModeSelector.Padding = new System.Windows.Forms.Padding(5, 10, 5, 10); | this.chartModeSelector.Padding = new System.Windows.Forms.Padding(5, 10, 5, 10); | ||||
@@ -437,7 +441,7 @@ | |||||
// | // | ||||
this.serverSelector.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); | this.serverSelector.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); | ||||
this.serverSelector.FormattingEnabled = true; | this.serverSelector.FormattingEnabled = true; | ||||
this.serverSelector.Location = new System.Drawing.Point(729, 157); | |||||
this.serverSelector.Location = new System.Drawing.Point(729, 151); | |||||
this.serverSelector.Name = "serverSelector"; | this.serverSelector.Name = "serverSelector"; | ||||
this.serverSelector.Size = new System.Drawing.Size(233, 35); | this.serverSelector.Size = new System.Drawing.Size(233, 35); | ||||
this.serverSelector.TabIndex = 6; | this.serverSelector.TabIndex = 6; | ||||
@@ -446,7 +450,7 @@ | |||||
// CancelButton | // CancelButton | ||||
// | // | ||||
this.CancelButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); | this.CancelButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); | ||||
this.CancelButton.Location = new System.Drawing.Point(861, 376); | |||||
this.CancelButton.Location = new System.Drawing.Point(861, 370); | |||||
this.CancelButton.Name = "CancelButton"; | this.CancelButton.Name = "CancelButton"; | ||||
this.CancelButton.Size = new System.Drawing.Size(101, 41); | this.CancelButton.Size = new System.Drawing.Size(101, 41); | ||||
this.CancelButton.TabIndex = 5; | this.CancelButton.TabIndex = 5; | ||||
@@ -457,7 +461,7 @@ | |||||
// OKButton | // OKButton | ||||
// | // | ||||
this.OKButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); | this.OKButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); | ||||
this.OKButton.Location = new System.Drawing.Point(754, 376); | |||||
this.OKButton.Location = new System.Drawing.Point(754, 370); | |||||
this.OKButton.Name = "OKButton"; | this.OKButton.Name = "OKButton"; | ||||
this.OKButton.Size = new System.Drawing.Size(101, 41); | this.OKButton.Size = new System.Drawing.Size(101, 41); | ||||
this.OKButton.TabIndex = 4; | this.OKButton.TabIndex = 4; | ||||
@@ -465,12 +469,6 @@ | |||||
this.OKButton.UseVisualStyleBackColor = true; | this.OKButton.UseVisualStyleBackColor = true; | ||||
this.OKButton.Click += new System.EventHandler(this.OKButton_Click); | this.OKButton.Click += new System.EventHandler(this.OKButton_Click); | ||||
// | // | ||||
// bindingConfiguration | |||||
// | |||||
this.bindingConfiguration.DataSource = typeof(Shadowsocks.Model.StatisticsStrategyConfiguration); | |||||
this.bindingConfiguration.BindingComplete += new System.Windows.Forms.BindingCompleteEventHandler(this.bindingConfiguration_BindingComplete); | |||||
this.bindingConfiguration.CurrentItemChanged += new System.EventHandler(this.bindingConfiguration_CurrentItemChanged); | |||||
// | |||||
// StatisticsStrategyConfigurationForm | // StatisticsStrategyConfigurationForm | ||||
// | // | ||||
this.AutoScaleDimensions = new System.Drawing.SizeF(12F, 27F); | this.AutoScaleDimensions = new System.Drawing.SizeF(12F, 27F); | ||||
@@ -484,6 +482,7 @@ | |||||
this.Name = "StatisticsStrategyConfigurationForm"; | this.Name = "StatisticsStrategyConfigurationForm"; | ||||
this.Text = "StatisticsStrategyConfigurationForm"; | this.Text = "StatisticsStrategyConfigurationForm"; | ||||
((System.ComponentModel.ISupportInitialize)(this.StatisticsChart)).EndInit(); | ((System.ComponentModel.ISupportInitialize)(this.StatisticsChart)).EndInit(); | ||||
((System.ComponentModel.ISupportInitialize)(this.bindingConfiguration)).EndInit(); | |||||
this.chartModeSelector.ResumeLayout(false); | this.chartModeSelector.ResumeLayout(false); | ||||
this.chartModeSelector.PerformLayout(); | this.chartModeSelector.PerformLayout(); | ||||
this.splitContainer1.Panel1.ResumeLayout(false); | this.splitContainer1.Panel1.ResumeLayout(false); | ||||
@@ -503,7 +502,6 @@ | |||||
this.splitContainer3.Panel2.ResumeLayout(false); | this.splitContainer3.Panel2.ResumeLayout(false); | ||||
((System.ComponentModel.ISupportInitialize)(this.splitContainer3)).EndInit(); | ((System.ComponentModel.ISupportInitialize)(this.splitContainer3)).EndInit(); | ||||
this.splitContainer3.ResumeLayout(false); | this.splitContainer3.ResumeLayout(false); | ||||
((System.ComponentModel.ISupportInitialize)(this.bindingConfiguration)).EndInit(); | |||||
this.ResumeLayout(false); | this.ResumeLayout(false); | ||||
} | } | ||||
@@ -36,7 +36,7 @@ namespace Shadowsocks.View | |||||
private void LoadConfiguration() | private void LoadConfiguration() | ||||
{ | { | ||||
var configs = _controller.GetCurrentConfiguration().configs; | var configs = _controller.GetCurrentConfiguration().configs; | ||||
_servers = configs.Select(server => server.FriendlyName()).ToList(); | |||||
_servers = configs.Select(server => server.Identifier()).ToList(); | |||||
_configuration = _controller.StatisticsConfiguration | _configuration = _controller.StatisticsConfiguration | ||||
?? new StatisticsStrategyConfiguration(); | ?? new StatisticsStrategyConfiguration(); | ||||
if (_configuration.Calculations == null) | if (_configuration.Calculations == null) | ||||
@@ -153,15 +153,5 @@ namespace Shadowsocks.View | |||||
{ | { | ||||
repeatTimesNum.ReadOnly = !PingCheckBox.Checked; | repeatTimesNum.ReadOnly = !PingCheckBox.Checked; | ||||
} | } | ||||
private void bindingConfiguration_CurrentItemChanged(object sender, EventArgs e) | |||||
{ | |||||
Logging.Info("?"); | |||||
} | |||||
private void bindingConfiguration_BindingComplete(object sender, BindingCompleteEventArgs e) | |||||
{ | |||||
Logging.Info("?"); | |||||
} | |||||
} | } | ||||
} | } |
@@ -118,12 +118,12 @@ | |||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||||
</resheader> | </resheader> | ||||
<metadata name="bindingConfiguration.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> | <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> | ||||
<metadata name="CalculatinTip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> | <metadata name="CalculatinTip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> | ||||
<value>238, 6</value> | <value>238, 6</value> | ||||
</metadata> | </metadata> | ||||
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | <metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||||
<value>37</value> | |||||
<value>191</value> | |||||
</metadata> | </metadata> | ||||
</root> | </root> |
@@ -53,13 +53,14 @@ | |||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'"> | <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'"> | ||||
<OutputPath>bin\x86\Release\</OutputPath> | <OutputPath>bin\x86\Release\</OutputPath> | ||||
<DefineConstants>TRACE</DefineConstants> | <DefineConstants>TRACE</DefineConstants> | ||||
<Optimize>true</Optimize> | |||||
<Optimize>false</Optimize> | |||||
<DebugType>pdbonly</DebugType> | <DebugType>pdbonly</DebugType> | ||||
<PlatformTarget>x86</PlatformTarget> | <PlatformTarget>x86</PlatformTarget> | ||||
<ErrorReport>prompt</ErrorReport> | <ErrorReport>prompt</ErrorReport> | ||||
<CodeAnalysisRuleSet>ManagedMinimumRules.ruleset</CodeAnalysisRuleSet> | <CodeAnalysisRuleSet>ManagedMinimumRules.ruleset</CodeAnalysisRuleSet> | ||||
<Prefer32Bit>false</Prefer32Bit> | <Prefer32Bit>false</Prefer32Bit> | ||||
<DebugSymbols>true</DebugSymbols> | <DebugSymbols>true</DebugSymbols> | ||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> | |||||
</PropertyGroup> | </PropertyGroup> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<ApplicationManifest>app.manifest</ApplicationManifest> | <ApplicationManifest>app.manifest</ApplicationManifest> | ||||
@@ -341,13 +342,12 @@ | |||||
<Files Output="false" Required="true" ParameterType="Microsoft.Build.Framework.ITaskItem[]" /> | <Files Output="false" Required="true" ParameterType="Microsoft.Build.Framework.ITaskItem[]" /> | ||||
</ParameterGroup> | </ParameterGroup> | ||||
<Task Evaluate="true"> | <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(); | var config = XElement.Load(Config.ItemSpec).Elements("Costura").FirstOrDefault(); | ||||
if (config == null) return true; | if (config == null) return true; | ||||
@@ -366,8 +366,8 @@ var filesToCleanup = Files.Select(f => f.ItemSpec).Where(f => !excludedAssemblie | |||||
foreach (var item in filesToCleanup) | foreach (var item in filesToCleanup) | ||||
File.Delete(item); | File.Delete(item); | ||||
]]> | |||||
</Code></Task> | |||||
]]></Code> | |||||
</Task> | |||||
</UsingTask> | </UsingTask> | ||||
<Target Name="CleanReferenceCopyLocalPaths" AfterTargets="AfterBuild;NonWinFodyTarget"> | <Target Name="CleanReferenceCopyLocalPaths" AfterTargets="AfterBuild;NonWinFodyTarget"> | ||||
<CosturaCleanup Config="FodyWeavers.xml" Files="@(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')" /> | <CosturaCleanup Config="FodyWeavers.xml" Files="@(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')" /> | ||||