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

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