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 19 kB

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