475 lines
17 KiB
C#
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;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|