You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

AvailabilityStatistics.cs 17 kB

9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Net;
  7. using System.Net.NetworkInformation;
  8. using System.Net.Sockets;
  9. using System.Threading;
  10. using System.Threading.Tasks;
  11. using Newtonsoft.Json;
  12. using Shadowsocks.Model;
  13. using Shadowsocks.Util;
  14. namespace Shadowsocks.Controller
  15. {
  16. using Statistics = Dictionary<string, List<StatisticsRecord>>;
  17. public sealed class AvailabilityStatistics : IDisposable
  18. {
  19. public const string DateTimePattern = "yyyy-MM-dd HH:mm:ss";
  20. private const string StatisticsFilesName = "shadowsocks.availability.json";
  21. public static string AvailabilityStatisticsFile;
  22. //static constructor to initialize every public static fields before refereced
  23. static AvailabilityStatistics()
  24. {
  25. AvailabilityStatisticsFile = Utils.GetTempPath(StatisticsFilesName);
  26. }
  27. //arguments for ICMP tests
  28. private int Repeat => Config.RepeatTimesNum;
  29. public const int TimeoutMilliseconds = 500;
  30. //records cache for current server in {_monitorInterval} minutes
  31. private readonly ConcurrentDictionary<string, List<int>> _latencyRecords = new ConcurrentDictionary<string, List<int>>();
  32. //speed in KiB/s
  33. private readonly ConcurrentDictionary<string, long> _inboundCounter = new ConcurrentDictionary<string, long>();
  34. private readonly ConcurrentDictionary<string, long> _lastInboundCounter = new ConcurrentDictionary<string, long>();
  35. private readonly ConcurrentDictionary<string, List<int>> _inboundSpeedRecords = new ConcurrentDictionary<string, List<int>>();
  36. private readonly ConcurrentDictionary<string, long> _outboundCounter = new ConcurrentDictionary<string, long>();
  37. private readonly ConcurrentDictionary<string, long> _lastOutboundCounter = new ConcurrentDictionary<string, long>();
  38. private readonly ConcurrentDictionary<string, List<int>> _outboundSpeedRecords = new ConcurrentDictionary<string, List<int>>();
  39. //tasks
  40. private readonly TimeSpan _delayBeforeStart = TimeSpan.FromSeconds(1);
  41. private readonly TimeSpan _retryInterval = TimeSpan.FromMinutes(2);
  42. private Timer _recorder; //analyze and save cached records to RawStatistics and filter records
  43. private TimeSpan RecordingInterval => TimeSpan.FromMinutes(Config.DataCollectionMinutes);
  44. private Timer _speedMonior;
  45. private readonly TimeSpan _monitorInterval = TimeSpan.FromSeconds(1);
  46. //private Timer _writer; //write RawStatistics to file
  47. //private readonly TimeSpan _writingInterval = TimeSpan.FromMinutes(1);
  48. private ShadowsocksController _controller;
  49. private StatisticsStrategyConfiguration Config => _controller.StatisticsConfiguration;
  50. // Static Singleton Initialization
  51. public static AvailabilityStatistics Instance { get; } = new AvailabilityStatistics();
  52. public Statistics RawStatistics { get; private set; }
  53. public Statistics FilteredStatistics { get; private set; }
  54. private AvailabilityStatistics()
  55. {
  56. RawStatistics = new Statistics();
  57. }
  58. internal void UpdateConfiguration(ShadowsocksController controller)
  59. {
  60. _controller = controller;
  61. Reset();
  62. try
  63. {
  64. if (Config.StatisticsEnabled)
  65. {
  66. StartTimerWithoutState(ref _recorder, Run, RecordingInterval);
  67. LoadRawStatistics();
  68. StartTimerWithoutState(ref _speedMonior, UpdateSpeed, _monitorInterval);
  69. }
  70. else
  71. {
  72. _recorder?.Dispose();
  73. _speedMonior?.Dispose();
  74. }
  75. }
  76. catch (Exception e)
  77. {
  78. Logging.LogUsefulException(e);
  79. }
  80. }
  81. private void StartTimerWithoutState(ref Timer timer, TimerCallback callback, TimeSpan interval)
  82. {
  83. if (timer?.Change(_delayBeforeStart, interval) == null)
  84. {
  85. timer = new Timer(callback, null, _delayBeforeStart, interval);
  86. }
  87. }
  88. private void UpdateSpeed(object _)
  89. {
  90. foreach (var kv in _lastInboundCounter)
  91. {
  92. var id = kv.Key;
  93. var lastInbound = kv.Value;
  94. var inbound = _inboundCounter[id];
  95. var bytes = inbound - lastInbound;
  96. _lastInboundCounter[id] = inbound;
  97. var inboundSpeed = GetSpeedInKiBPerSecond(bytes, _monitorInterval.TotalSeconds);
  98. _inboundSpeedRecords.GetOrAdd(id, (k) =>
  99. {
  100. List<int> records = new List<int>();
  101. records.Add(inboundSpeed);
  102. return records;
  103. });
  104. var lastOutbound = _lastOutboundCounter[id];
  105. var outbound = _outboundCounter[id];
  106. bytes = outbound - lastOutbound;
  107. _lastOutboundCounter[id] = outbound;
  108. var outboundSpeed = GetSpeedInKiBPerSecond(bytes, _monitorInterval.TotalSeconds);
  109. _outboundSpeedRecords.GetOrAdd(id, (k) =>
  110. {
  111. List<int> records = new List<int>();
  112. records.Add(outboundSpeed);
  113. return records;
  114. });
  115. Logging.Debug(
  116. $"{id}: current/max inbound {inboundSpeed}/{_inboundSpeedRecords[id].Max()} KiB/s, current/max outbound {outboundSpeed}/{_outboundSpeedRecords[id].Max()} KiB/s");
  117. }
  118. }
  119. private void Reset()
  120. {
  121. _inboundSpeedRecords.Clear();
  122. _outboundSpeedRecords.Clear();
  123. _latencyRecords.Clear();
  124. }
  125. private void Run(object _)
  126. {
  127. UpdateRecords();
  128. Reset();
  129. }
  130. private void UpdateRecords()
  131. {
  132. var records = new Dictionary<string, StatisticsRecord>();
  133. UpdateRecordsState state = new UpdateRecordsState();
  134. state.counter = _controller.GetCurrentConfiguration().configs.Count;
  135. foreach (var server in _controller.GetCurrentConfiguration().configs)
  136. {
  137. var id = server.Identifier();
  138. List<int> inboundSpeedRecords = null;
  139. List<int> outboundSpeedRecords = null;
  140. List<int> latencyRecords = null;
  141. _inboundSpeedRecords.TryGetValue(id, out inboundSpeedRecords);
  142. _outboundSpeedRecords.TryGetValue(id, out outboundSpeedRecords);
  143. _latencyRecords.TryGetValue(id, out latencyRecords);
  144. StatisticsRecord record = new StatisticsRecord(id, inboundSpeedRecords, outboundSpeedRecords, latencyRecords);
  145. /* duplicate server identifier */
  146. if (records.ContainsKey(id))
  147. records[id] = record;
  148. else
  149. records.Add(id, record);
  150. if (Config.Ping)
  151. {
  152. MyPing ping = new MyPing(server, Repeat);
  153. ping.Completed += ping_Completed;
  154. ping.Start(new PingState { state = state, record = record });
  155. }
  156. else if (!record.IsEmptyData())
  157. {
  158. AppendRecord(id, record);
  159. }
  160. }
  161. if (!Config.Ping)
  162. {
  163. Save();
  164. FilterRawStatistics();
  165. }
  166. }
  167. private void ping_Completed(object sender, MyPing.CompletedEventArgs e)
  168. {
  169. PingState pingState = (PingState)e.UserState;
  170. UpdateRecordsState state = pingState.state;
  171. Server server = e.Server;
  172. StatisticsRecord record = pingState.record;
  173. record.SetResponse(e.RoundtripTime);
  174. if (!record.IsEmptyData())
  175. {
  176. AppendRecord(server.Identifier(), record);
  177. }
  178. 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");
  179. if (Interlocked.Decrement(ref state.counter) == 0)
  180. {
  181. Save();
  182. FilterRawStatistics();
  183. }
  184. }
  185. private void AppendRecord(string serverIdentifier, StatisticsRecord record)
  186. {
  187. try
  188. {
  189. List<StatisticsRecord> records;
  190. lock (RawStatistics)
  191. {
  192. if (!RawStatistics.TryGetValue(serverIdentifier, out records))
  193. {
  194. records = new List<StatisticsRecord>();
  195. RawStatistics[serverIdentifier] = records;
  196. }
  197. }
  198. records.Add(record);
  199. }
  200. catch (Exception e)
  201. {
  202. Logging.LogUsefulException(e);
  203. }
  204. }
  205. private void Save()
  206. {
  207. Logging.Debug($"save statistics to {AvailabilityStatisticsFile}");
  208. if (RawStatistics.Count == 0)
  209. {
  210. return;
  211. }
  212. try
  213. {
  214. string content;
  215. #if DEBUG
  216. content = JsonConvert.SerializeObject(RawStatistics, Formatting.Indented);
  217. #else
  218. content = JsonConvert.SerializeObject(RawStatistics, Formatting.None);
  219. #endif
  220. File.WriteAllText(AvailabilityStatisticsFile, content);
  221. }
  222. catch (IOException e)
  223. {
  224. Logging.LogUsefulException(e);
  225. }
  226. }
  227. private bool IsValidRecord(StatisticsRecord record)
  228. {
  229. if (Config.ByHourOfDay)
  230. {
  231. if (!record.Timestamp.Hour.Equals(DateTime.Now.Hour)) return false;
  232. }
  233. return true;
  234. }
  235. private void FilterRawStatistics()
  236. {
  237. try
  238. {
  239. Logging.Debug("filter raw statistics");
  240. if (RawStatistics == null) return;
  241. if (FilteredStatistics == null)
  242. {
  243. FilteredStatistics = new Statistics();
  244. }
  245. foreach (var serverAndRecords in RawStatistics)
  246. {
  247. var server = serverAndRecords.Key;
  248. var filteredRecords = serverAndRecords.Value.FindAll(IsValidRecord);
  249. FilteredStatistics[server] = filteredRecords;
  250. }
  251. }
  252. catch (Exception e)
  253. {
  254. Logging.LogUsefulException(e);
  255. }
  256. }
  257. private void LoadRawStatistics()
  258. {
  259. try
  260. {
  261. var path = AvailabilityStatisticsFile;
  262. Logging.Debug($"loading statistics from {path}");
  263. if (!File.Exists(path))
  264. {
  265. using (File.Create(path))
  266. {
  267. //do nothing
  268. }
  269. }
  270. var content = File.ReadAllText(path);
  271. RawStatistics = JsonConvert.DeserializeObject<Statistics>(content) ?? RawStatistics;
  272. }
  273. catch (Exception e)
  274. {
  275. Logging.LogUsefulException(e);
  276. Console.WriteLine($"failed to load statistics; try to reload {_retryInterval.TotalMinutes} minutes later");
  277. _recorder.Change(_retryInterval, RecordingInterval);
  278. }
  279. }
  280. private static int GetSpeedInKiBPerSecond(long bytes, double seconds)
  281. {
  282. var result = (int)(bytes / seconds) / 1024;
  283. return result;
  284. }
  285. public void Dispose()
  286. {
  287. _recorder.Dispose();
  288. _speedMonior.Dispose();
  289. }
  290. public void UpdateLatency(Server server, int latency)
  291. {
  292. _latencyRecords.GetOrAdd(server.Identifier(), (k) =>
  293. {
  294. List<int> records = new List<int>();
  295. records.Add(latency);
  296. return records;
  297. });
  298. }
  299. public void UpdateInboundCounter(Server server, long n)
  300. {
  301. _inboundCounter.AddOrUpdate(server.Identifier(), (k) =>
  302. {
  303. _lastInboundCounter.GetOrAdd(server.Identifier(), 0);
  304. return n;
  305. }, (k, v) => (v + n));
  306. }
  307. public void UpdateOutboundCounter(Server server, long n)
  308. {
  309. _outboundCounter.AddOrUpdate(server.Identifier(), (k) =>
  310. {
  311. _lastOutboundCounter.GetOrAdd(server.Identifier(), 0);
  312. return n;
  313. }, (k, v) => (v + n));
  314. }
  315. class UpdateRecordsState
  316. {
  317. public int counter;
  318. }
  319. class PingState
  320. {
  321. public UpdateRecordsState state;
  322. public StatisticsRecord record;
  323. }
  324. class MyPing
  325. {
  326. //arguments for ICMP tests
  327. public const int TimeoutMilliseconds = 500;
  328. public EventHandler<CompletedEventArgs> Completed;
  329. private Server server;
  330. private int repeat;
  331. private IPAddress ip;
  332. private Ping ping;
  333. private List<int?> RoundtripTime;
  334. public MyPing(Server server, int repeat)
  335. {
  336. this.server = server;
  337. this.repeat = repeat;
  338. RoundtripTime = new List<int?>(repeat);
  339. ping = new Ping();
  340. ping.PingCompleted += Ping_PingCompleted;
  341. }
  342. public void Start(object userstate)
  343. {
  344. if (server.server == "")
  345. {
  346. FireCompleted(new Exception("Invalid Server"), userstate);
  347. return;
  348. }
  349. new Task(() => ICMPTest(0, userstate)).Start();
  350. }
  351. private void ICMPTest(int delay, object userstate)
  352. {
  353. try
  354. {
  355. Logging.Debug($"Ping {server.FriendlyName()}");
  356. if (ip == null)
  357. {
  358. ip = Dns.GetHostAddresses(server.server)
  359. .First(
  360. ip =>
  361. ip.AddressFamily == AddressFamily.InterNetwork ||
  362. ip.AddressFamily == AddressFamily.InterNetworkV6);
  363. }
  364. repeat--;
  365. if (delay > 0)
  366. Thread.Sleep(delay);
  367. ping.SendAsync(ip, TimeoutMilliseconds, userstate);
  368. }
  369. catch (Exception e)
  370. {
  371. Logging.Error($"An exception occured while eveluating {server.FriendlyName()}");
  372. Logging.LogUsefulException(e);
  373. FireCompleted(e, userstate);
  374. }
  375. }
  376. private void Ping_PingCompleted(object sender, PingCompletedEventArgs e)
  377. {
  378. try
  379. {
  380. if (e.Reply.Status == IPStatus.Success)
  381. {
  382. Logging.Debug($"Ping {server.FriendlyName()} {e.Reply.RoundtripTime} ms");
  383. RoundtripTime.Add((int?)e.Reply.RoundtripTime);
  384. }
  385. else
  386. {
  387. Logging.Debug($"Ping {server.FriendlyName()} timeout");
  388. RoundtripTime.Add(null);
  389. }
  390. TestNext(e.UserState);
  391. }
  392. catch (Exception ex)
  393. {
  394. Logging.Error($"An exception occured while eveluating {server.FriendlyName()}");
  395. Logging.LogUsefulException(ex);
  396. FireCompleted(ex, e.UserState);
  397. }
  398. }
  399. private void TestNext(object userstate)
  400. {
  401. if (repeat > 0)
  402. {
  403. //Do ICMPTest in a random frequency
  404. int delay = TimeoutMilliseconds + new Random().Next() % TimeoutMilliseconds;
  405. new Task(() => ICMPTest(delay, userstate)).Start();
  406. }
  407. else
  408. {
  409. FireCompleted(null, userstate);
  410. }
  411. }
  412. private void FireCompleted(Exception error, object userstate)
  413. {
  414. Completed?.Invoke(this, new CompletedEventArgs
  415. {
  416. Error = error,
  417. Server = server,
  418. RoundtripTime = RoundtripTime,
  419. UserState = userstate
  420. });
  421. }
  422. public class CompletedEventArgs : EventArgs
  423. {
  424. public Exception Error;
  425. public Server Server;
  426. public List<int?> RoundtripTime;
  427. public object UserState;
  428. }
  429. }
  430. }
  431. }