Files
shadowsocks-windows/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs
T
Syrone Wong 6a8a09983e Merge fixes from @GangZhuo, close #478 (#638)
Squashed commit of the following:

commit e280c2385c
Merge: 579039f 87aa9eb
Author: Gang Zhuo <gang.zhuo@gmail.com>
Date:   Wed Mar 16 04:02:27 2016 -0400

    Merge remote-tracking branch 'origin/master'

commit 579039fe40
Author: Gang Zhuo <gang.zhuo@gmail.com>
Date:   Fri Mar 4 21:45:23 2016 +0800

    log unhandle exception

commit 8d863f1d5f
Author: Gang Zhuo <gang.zhuo@gmail.com>
Date:   Fri Mar 4 21:44:54 2016 +0800

    fix Availability Statistics

commit b3ce1c698a
Author: Gang Zhuo <gang.zhuo@gmail.com>
Date:   Fri Mar 4 21:07:54 2016 +0800

    tiny refactor

commit 0f7d39e27e
Merge: bd7078a b01aced
Author: Gang Zhuo <gang.zhuo@gmail.com>
Date:   Fri Mar 4 20:02:58 2016 +0800

    Merge remote-tracking branch 'origin/master'

commit bd7078aa4f
Author: Gang Zhuo <gang.zhuo@gmail.com>
Date:   Fri Mar 4 20:01:58 2016 +0800

    tiny refactor

commit 49adb95b6e
Author: Gang Zhuo <gang.zhuo@gmail.com>
Date:   Fri Mar 4 03:44:26 2016 -0500

    log more Ping information for debug

commit 2aa0620ddd
Author: Gang Zhuo <gang.zhuo@gmail.com>
Date:   Fri Mar 4 03:04:10 2016 -0500

    reduce 3rd packages

[solve conflicts and squash to make history clean]
Signed-off-by: Syrone Wong <wong.syrone@gmail.com>
2016-08-05 11:05:16 +08:00

475 lines
17 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Shadowsocks.Model;
using Shadowsocks.Util;
namespace Shadowsocks.Controller
{
using Statistics = Dictionary<string, List<StatisticsRecord>>;
public sealed class AvailabilityStatistics : IDisposable
{
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);
}
//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()
{
RawStatistics = new Statistics();
}
internal void UpdateConfiguration(ShadowsocksController controller)
{
_controller = controller;
Reset();
try
{
if (Config.StatisticsEnabled)
{
StartTimerWithoutState(ref _recorder, Run, RecordingInterval);
LoadRawStatistics();
StartTimerWithoutState(ref _speedMonior, UpdateSpeed, _monitorInterval);
}
else
{
_recorder?.Dispose();
_speedMonior?.Dispose();
}
}
catch (Exception e)
{
Logging.LogUsefulException(e);
}
}
private void StartTimerWithoutState(ref Timer timer, TimerCallback callback, TimeSpan interval)
{
if (timer?.Change(_delayBeforeStart, interval) == null)
{
timer = new Timer(callback, null, _delayBeforeStart, interval);
}
}
private void UpdateSpeed(object _)
{
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, (k) =>
{
List<int> records = new List<int>();
records.Add(inboundSpeed);
return records;
});
var lastOutbound = _lastOutboundCounter[id];
var outbound = _outboundCounter[id];
bytes = outbound - lastOutbound;
_lastOutboundCounter[id] = outbound;
var outboundSpeed = GetSpeedInKiBPerSecond(bytes, _monitorInterval.TotalSeconds);
_outboundSpeedRecords.GetOrAdd(id, (k) =>
{
List<int> records = new List<int>();
records.Add(outboundSpeed);
return records;
});
Logging.Debug(
$"{id}: current/max inbound {inboundSpeed}/{_inboundSpeedRecords[id].Max()} KiB/s, current/max outbound {outboundSpeed}/{_outboundSpeedRecords[id].Max()} KiB/s");
}
}
private void Reset()
{
_inboundSpeedRecords.Clear();
_outboundSpeedRecords.Clear();
_latencyRecords.Clear();
}
private void Run(object _)
{
UpdateRecords();
Reset();
}
private void UpdateRecords()
{
var records = new Dictionary<string, StatisticsRecord>();
UpdateRecordsState state = new UpdateRecordsState();
state.counter = _controller.GetCurrentConfiguration().configs.Count;
foreach (var server in _controller.GetCurrentConfiguration().configs)
{
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);
StatisticsRecord record = new StatisticsRecord(id, inboundSpeedRecords, outboundSpeedRecords, latencyRecords);
/* duplicate server identifier */
if (records.ContainsKey(id))
records[id] = record;
else
records.Add(id, record);
if (Config.Ping)
{
MyPing ping = new MyPing(server, Repeat);
ping.Completed += ping_Completed;
ping.Start(new PingState { state = state, record = record });
}
else if (!record.IsEmptyData())
{
AppendRecord(id, record);
}
}
if (!Config.Ping)
{
Save();
FilterRawStatistics();
}
}
private void ping_Completed(object sender, MyPing.CompletedEventArgs e)
{
PingState pingState = (PingState)e.UserState;
UpdateRecordsState state = pingState.state;
Server server = e.Server;
StatisticsRecord record = pingState.record;
record.SetResponse(e.RoundtripTime);
if (!record.IsEmptyData())
{
AppendRecord(server.Identifier(), record);
}
Logging.Debug($"Ping {server.FriendlyName()} {e.RoundtripTime.Count} times, {(100 - record.PackageLoss * 100)}% packages loss, min {record.MinResponse} ms, max {record.MaxResponse} ms, avg {record.AverageResponse} ms");
if (Interlocked.Decrement(ref state.counter) == 0)
{
Save();
FilterRawStatistics();
}
}
private void AppendRecord(string serverIdentifier, StatisticsRecord record)
{
try
{
List<StatisticsRecord> records;
lock (RawStatistics)
{
if (!RawStatistics.TryGetValue(serverIdentifier, out records))
{
records = new List<StatisticsRecord>();
RawStatistics[serverIdentifier] = records;
}
}
records.Add(record);
}
catch (Exception e)
{
Logging.LogUsefulException(e);
}
}
private void Save()
{
Logging.Debug($"save statistics to {AvailabilityStatisticsFile}");
if (RawStatistics.Count == 0)
{
return;
}
try
{
string content;
#if DEBUG
content = JsonConvert.SerializeObject(RawStatistics, Formatting.Indented);
#else
content = JsonConvert.SerializeObject(RawStatistics, Formatting.None);
#endif
File.WriteAllText(AvailabilityStatisticsFile, content);
}
catch (IOException e)
{
Logging.LogUsefulException(e);
}
}
private bool IsValidRecord(StatisticsRecord record)
{
if (Config.ByHourOfDay)
{
if (!record.Timestamp.Hour.Equals(DateTime.Now.Hour)) return false;
}
return true;
}
private void FilterRawStatistics()
{
try
{
Logging.Debug("filter raw statistics");
if (RawStatistics == null) return;
if (FilteredStatistics == null)
{
FilteredStatistics = new Statistics();
}
foreach (var serverAndRecords in RawStatistics)
{
var server = serverAndRecords.Key;
var filteredRecords = serverAndRecords.Value.FindAll(IsValidRecord);
FilteredStatistics[server] = filteredRecords;
}
}
catch (Exception e)
{
Logging.LogUsefulException(e);
}
}
private void LoadRawStatistics()
{
try
{
var path = AvailabilityStatisticsFile;
Logging.Debug($"loading statistics from {path}");
if (!File.Exists(path))
{
using (File.Create(path))
{
//do nothing
}
}
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 static int GetSpeedInKiBPerSecond(long bytes, double seconds)
{
var result = (int)(bytes / seconds) / 1024;
return result;
}
public void Dispose()
{
_recorder.Dispose();
_speedMonior.Dispose();
}
public void UpdateLatency(Server server, int latency)
{
_latencyRecords.GetOrAdd(server.Identifier(), (k) =>
{
List<int> records = new List<int>();
records.Add(latency);
return records;
});
}
public void UpdateInboundCounter(Server server, long n)
{
_inboundCounter.AddOrUpdate(server.Identifier(), (k) =>
{
_lastInboundCounter.GetOrAdd(server.Identifier(), 0);
return n;
}, (k, v) => (v + n));
}
public void UpdateOutboundCounter(Server server, long n)
{
_outboundCounter.AddOrUpdate(server.Identifier(), (k) =>
{
_lastOutboundCounter.GetOrAdd(server.Identifier(), 0);
return n;
}, (k, v) => (v + n));
}
class UpdateRecordsState
{
public int counter;
}
class PingState
{
public UpdateRecordsState state;
public StatisticsRecord record;
}
class MyPing
{
//arguments for ICMP tests
public const int TimeoutMilliseconds = 500;
public EventHandler<CompletedEventArgs> Completed;
private Server server;
private int repeat;
private IPAddress ip;
private Ping ping;
private List<int?> RoundtripTime;
public MyPing(Server server, int repeat)
{
this.server = server;
this.repeat = repeat;
RoundtripTime = new List<int?>(repeat);
ping = new Ping();
ping.PingCompleted += Ping_PingCompleted;
}
public void Start(object userstate)
{
if (server.server == "")
{
FireCompleted(new Exception("Invalid Server"), userstate);
return;
}
new Task(() => ICMPTest(0, userstate)).Start();
}
private void ICMPTest(int delay, object userstate)
{
try
{
Logging.Debug($"Ping {server.FriendlyName()}");
if (ip == null)
{
ip = Dns.GetHostAddresses(server.server)
.First(
ip =>
ip.AddressFamily == AddressFamily.InterNetwork ||
ip.AddressFamily == AddressFamily.InterNetworkV6);
}
repeat--;
if (delay > 0)
Thread.Sleep(delay);
ping.SendAsync(ip, TimeoutMilliseconds, userstate);
}
catch (Exception e)
{
Logging.Error($"An exception occured while eveluating {server.FriendlyName()}");
Logging.LogUsefulException(e);
FireCompleted(e, userstate);
}
}
private void Ping_PingCompleted(object sender, PingCompletedEventArgs e)
{
try
{
if (e.Reply.Status == IPStatus.Success)
{
Logging.Debug($"Ping {server.FriendlyName()} {e.Reply.RoundtripTime} ms");
RoundtripTime.Add((int?)e.Reply.RoundtripTime);
}
else
{
Logging.Debug($"Ping {server.FriendlyName()} timeout");
RoundtripTime.Add(null);
}
TestNext(e.UserState);
}
catch (Exception ex)
{
Logging.Error($"An exception occured while eveluating {server.FriendlyName()}");
Logging.LogUsefulException(ex);
FireCompleted(ex, e.UserState);
}
}
private void TestNext(object userstate)
{
if (repeat > 0)
{
//Do ICMPTest in a random frequency
int delay = TimeoutMilliseconds + new Random().Next() % TimeoutMilliseconds;
new Task(() => ICMPTest(delay, userstate)).Start();
}
else
{
FireCompleted(null, userstate);
}
}
private void FireCompleted(Exception error, object userstate)
{
Completed?.Invoke(this, new CompletedEventArgs
{
Error = error,
Server = server,
RoundtripTime = RoundtripTime,
UserState = userstate
});
}
public class CompletedEventArgs : EventArgs
{
public Exception Error;
public Server Server;
public List<int?> RoundtripTime;
public object UserState;
}
}
}
}