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

9 years ago
9 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  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. Save();
  129. Reset();
  130. FilterRawStatistics();
  131. }
  132. private void UpdateRecords()
  133. {
  134. var records = new Dictionary<string, StatisticsRecord>();
  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, record);
  153. ping.Completed += ping_Completed;
  154. ping.Start();
  155. }
  156. }
  157. foreach (var kv in records.Where(kv => !kv.Value.IsEmptyData()))
  158. {
  159. AppendRecord(kv.Key, kv.Value);
  160. }
  161. }
  162. private void ping_Completed(object sender, MyPing.CompletedEventArgs e)
  163. {
  164. Server server = ((MyPing)sender).server;
  165. StatisticsRecord record = (StatisticsRecord)((MyPing)sender).userstate;
  166. record.SetResponse(e.RoundtripTime);
  167. 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");
  168. }
  169. private void AppendRecord(string serverIdentifier, StatisticsRecord record)
  170. {
  171. List<StatisticsRecord> records;
  172. if (!RawStatistics.TryGetValue(serverIdentifier, out records))
  173. {
  174. records = new List<StatisticsRecord>();
  175. RawStatistics[serverIdentifier] = records;
  176. }
  177. records.Add(record);
  178. }
  179. private void Save()
  180. {
  181. if (RawStatistics.Count == 0)
  182. {
  183. return;
  184. }
  185. try
  186. {
  187. var content = JsonConvert.SerializeObject(RawStatistics, Formatting.None);
  188. File.WriteAllText(AvailabilityStatisticsFile, content);
  189. }
  190. catch (IOException e)
  191. {
  192. Logging.LogUsefulException(e);
  193. }
  194. }
  195. private bool IsValidRecord(StatisticsRecord record)
  196. {
  197. if (Config.ByHourOfDay)
  198. {
  199. if (!record.Timestamp.Hour.Equals(DateTime.Now.Hour)) return false;
  200. }
  201. return true;
  202. }
  203. private void FilterRawStatistics()
  204. {
  205. if (RawStatistics == null) return;
  206. if (FilteredStatistics == null)
  207. {
  208. FilteredStatistics = new Statistics();
  209. }
  210. foreach (var serverAndRecords in RawStatistics)
  211. {
  212. var server = serverAndRecords.Key;
  213. var filteredRecords = serverAndRecords.Value.FindAll(IsValidRecord);
  214. FilteredStatistics[server] = filteredRecords;
  215. }
  216. }
  217. private void LoadRawStatistics()
  218. {
  219. try
  220. {
  221. var path = AvailabilityStatisticsFile;
  222. Logging.Debug($"loading statistics from {path}");
  223. if (!File.Exists(path))
  224. {
  225. using (File.Create(path))
  226. {
  227. //do nothing
  228. }
  229. }
  230. var content = File.ReadAllText(path);
  231. RawStatistics = JsonConvert.DeserializeObject<Statistics>(content) ?? RawStatistics;
  232. }
  233. catch (Exception e)
  234. {
  235. Logging.LogUsefulException(e);
  236. Console.WriteLine($"failed to load statistics; try to reload {_retryInterval.TotalMinutes} minutes later");
  237. _recorder.Change(_retryInterval, RecordingInterval);
  238. }
  239. }
  240. private static int GetSpeedInKiBPerSecond(long bytes, double seconds)
  241. {
  242. var result = (int)(bytes / seconds) / 1024;
  243. return result;
  244. }
  245. public void Dispose()
  246. {
  247. _recorder.Dispose();
  248. _speedMonior.Dispose();
  249. }
  250. public void UpdateLatency(Server server, int latency)
  251. {
  252. _latencyRecords.GetOrAdd(server.Identifier(), (k) =>
  253. {
  254. List<int> records = new List<int>();
  255. records.Add(latency);
  256. return records;
  257. });
  258. }
  259. public void UpdateInboundCounter(Server server, long n)
  260. {
  261. _inboundCounter.AddOrUpdate(server.Identifier(), (k) =>
  262. {
  263. _lastInboundCounter.GetOrAdd(server.Identifier(), 0);
  264. return n;
  265. }, (k, v) => (v + n));
  266. }
  267. public void UpdateOutboundCounter(Server server, long n)
  268. {
  269. _outboundCounter.AddOrUpdate(server.Identifier(), (k) =>
  270. {
  271. _lastOutboundCounter.GetOrAdd(server.Identifier(), 0);
  272. return n;
  273. }, (k, v) => (v + n));
  274. }
  275. class MyPing
  276. {
  277. //arguments for ICMP tests
  278. public const int TimeoutMilliseconds = 500;
  279. public EventHandler<CompletedEventArgs> Completed;
  280. public Server server;
  281. public object userstate;
  282. private int repeat;
  283. private IPAddress ip;
  284. private Ping ping;
  285. private List<int?> RoundtripTime;
  286. public MyPing(Server server, int repeat, object userstate)
  287. {
  288. this.server = server;
  289. this.repeat = repeat;
  290. this.userstate = userstate;
  291. RoundtripTime = new List<int?>(repeat);
  292. ping = new Ping();
  293. ping.PingCompleted += Ping_PingCompleted;
  294. }
  295. public void Start()
  296. {
  297. if (server.server == "")
  298. return;
  299. new Task(() => ICMPTest(0)).Start();
  300. }
  301. private void ICMPTest(int delay)
  302. {
  303. try
  304. {
  305. Logging.Debug($"Ping {server.FriendlyName()}");
  306. if (ip == null)
  307. {
  308. ip = Dns.GetHostAddresses(server.server)
  309. .First(
  310. ip =>
  311. ip.AddressFamily == AddressFamily.InterNetwork ||
  312. ip.AddressFamily == AddressFamily.InterNetworkV6);
  313. }
  314. repeat--;
  315. if (delay > 0)
  316. Thread.Sleep(delay);
  317. ping.SendAsync(ip, TimeoutMilliseconds, null);
  318. }
  319. catch (Exception e)
  320. {
  321. Logging.Error($"An exception occured while eveluating {server.FriendlyName()}");
  322. Logging.LogUsefulException(e);
  323. }
  324. }
  325. private void Ping_PingCompleted(object sender, PingCompletedEventArgs e)
  326. {
  327. try
  328. {
  329. if (e.Reply.Status == IPStatus.Success)
  330. {
  331. Logging.Debug($"Ping {server.FriendlyName()} {e.Reply.RoundtripTime} ms");
  332. RoundtripTime.Add((int?)e.Reply.RoundtripTime);
  333. }
  334. else
  335. {
  336. Logging.Debug($"Ping {server.FriendlyName()} timeout");
  337. RoundtripTime.Add(null);
  338. }
  339. TestNext();
  340. }
  341. catch (Exception ex)
  342. {
  343. Logging.Error($"An exception occured while eveluating {server.FriendlyName()}");
  344. Logging.LogUsefulException(ex);
  345. }
  346. }
  347. private void TestNext()
  348. {
  349. if (repeat > 0)
  350. {
  351. //Do ICMPTest in a random frequency
  352. int delay = TimeoutMilliseconds + new Random().Next() % TimeoutMilliseconds;
  353. new Task(() => ICMPTest(delay)).Start();
  354. }
  355. else
  356. {
  357. Completed?.Invoke(this, new CompletedEventArgs
  358. {
  359. RoundtripTime = RoundtripTime
  360. });
  361. }
  362. }
  363. public class CompletedEventArgs : EventArgs
  364. {
  365. public List<int?> RoundtripTime;
  366. }
  367. }
  368. }
  369. }