PageRenderTime 46ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/Cache.cs

https://bitbucket.org/rstarkov/tankiconmaker
C# | 313 lines | 234 code | 32 blank | 47 comment | 33 complexity | 8fcb0b3af5b782a663a828ffaa5702ce MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause, GPL-3.0, CC-BY-SA-3.0
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Windows.Media.Imaging;
  6. using ICSharpCode.SharpZipLib.Zip;
  7. using RT.Util;
  8. using RT.Util.ExtensionMethods;
  9. namespace TankIconMaker
  10. {
  11. /// <summary>A strongly-typed wrapper around <see cref="WeakReference"/>.</summary>
  12. struct WeakReference<T> where T : class
  13. {
  14. private WeakReference _ref;
  15. public WeakReference(T value) { _ref = new WeakReference(value); }
  16. public T Target { get { return _ref == null ? null : (T) _ref.Target; } }
  17. public bool IsAlive { get { return _ref != null && _ref.IsAlive; } }
  18. }
  19. /// <summary>Base class for all cache entries.</summary>
  20. abstract class CacheEntry
  21. {
  22. /// <summary>Ensures that the entry is up-to-date, reloading the data if necessary. Not called again within a short timeout period.</summary>
  23. public abstract void Refresh();
  24. /// <summary>Gets the approximate size of this entry, in the same units as the <see cref="Cache.MaximumSize"/> property.</summary>
  25. public abstract long Size { get; }
  26. }
  27. /// <summary>
  28. /// Implements a strongly-typed cache which stores all entries up to a certain size, and above that evicts little-used items randomly.
  29. /// Evicted items will remain accessible, however, until the garbage collector actually collects them. All public methods are thread-safe.
  30. /// </summary>
  31. sealed class Cache<TKey, TEntry> where TEntry : CacheEntry
  32. {
  33. /// <summary>Keeps track of various information related to the cache entry, as well as a weak and, optionally, a strong reference to it.</summary>
  34. private class Container { public WeakReference<TEntry> Weak; public TEntry Strong; public int UseCount; public DateTime ValidStamp; }
  35. /// <summary>The actual keyed cache.</summary>
  36. private Dictionary<TKey, Container> _cache = new Dictionary<TKey, Container>();
  37. /// <summary>The root for all strongly-referenced entries.</summary>
  38. private HashSet<Container> _strong = new HashSet<Container>();
  39. /// <summary>Incremented every time an entry is looked up and an existing entry is already available.</summary>
  40. public int Hits { get; private set; }
  41. /// <summary>Incremented every time an entry is looked up but a new entry must be created.</summary>
  42. public int Misses { get; private set; }
  43. /// <summary>Incremented every time a strong reference is evicted due to the cache size going over the quota.</summary>
  44. public int Evictions { get; private set; }
  45. /// <summary>The maximum total size of the strongly-referenced entries that the cache is allowed to have. Units are up to <typeparamref name="TEntry"/>.</summary>
  46. public long MaximumSize { get; set; }
  47. /// <summary>The total size of the strongly-referenced entries that the cache currently has. Units are up to <typeparamref name="TEntry"/>.</summary>
  48. public long CurrentSize { get; set; }
  49. /// <summary>Gets an entry associated with the specified key. Returns a valid entry regardless of whether it was in the cache.</summary>
  50. /// <param name="key">The key that the entry is identified by.</param>
  51. /// <param name="createEntry">The function that instantiates a new entry in case there is no cached entry available.</param>
  52. public TEntry GetEntry(TKey key, Func<TEntry> createEntry)
  53. {
  54. var now = DateTime.UtcNow;
  55. lock (_cache)
  56. {
  57. Container c;
  58. if (!_cache.TryGetValue(key, out c))
  59. _cache[key] = c = new Container();
  60. // Gets are counted to prioritize eviction; the count is maintained even if the weak reference gets GC’d
  61. c.UseCount++;
  62. // Retrieve the actual entry and ensure it’s up-to-date
  63. long wasSize = 0;
  64. var entry = c.Weak.Target; // grab a strong reference, if any
  65. if (entry == null)
  66. {
  67. Misses++;
  68. entry = createEntry();
  69. entry.Refresh();
  70. c.ValidStamp = now;
  71. c.Weak = new WeakReference<TEntry>(entry);
  72. }
  73. else
  74. {
  75. Hits++;
  76. wasSize = entry.Size;
  77. if (now - c.ValidStamp > TimeSpan.FromSeconds(1))
  78. {
  79. entry.Refresh();
  80. c.ValidStamp = now;
  81. }
  82. }
  83. // Update the strong reference list
  84. long nowSize = entry.Size;
  85. if (c.Strong != null)
  86. CurrentSize += nowSize - wasSize;
  87. else if (Rnd.NextDouble() > Math.Min(0.5, CurrentSize / MaximumSize))
  88. {
  89. c.Strong = entry;
  90. _strong.Add(c);
  91. CurrentSize += nowSize;
  92. }
  93. if (CurrentSize > MaximumSize)
  94. evictStrong();
  95. return entry;
  96. }
  97. }
  98. /// <summary>Evicts entries from the strongly-referenced cache until the <see cref="MaximumSize"/> is satisfied.</summary>
  99. private void evictStrong()
  100. {
  101. while (CurrentSize > MaximumSize && _strong.Count > 0)
  102. {
  103. // Pick two random entries and evict the one that's been used the least.
  104. var item1 = _strong.Skip(Rnd.Next(_strong.Count)).First();
  105. var item2 = _strong.Skip(Rnd.Next(_strong.Count)).First();
  106. if (item1.UseCount < item2.UseCount)
  107. {
  108. _strong.Remove(item1);
  109. CurrentSize -= item1.Strong.Size;
  110. item1.Strong = null;
  111. }
  112. else
  113. {
  114. _strong.Remove(item2);
  115. CurrentSize -= item2.Strong.Size;
  116. item2.Strong = null;
  117. }
  118. Evictions++;
  119. }
  120. }
  121. /// <summary>
  122. /// Removes all the metadata associated with entries which have been evicted and garbage-collected. Note that this wipes
  123. /// the metadata which helps ensure that frequently evicted and re-requested items eventually stop being evicted from the strong cache.
  124. /// </summary>
  125. public void Collect()
  126. {
  127. lock (_cache)
  128. {
  129. var removeKeys = _cache.Where(kvp => !kvp.Value.Weak.IsAlive).Select(kvp => kvp.Key).ToArray();
  130. foreach (var key in removeKeys)
  131. _cache.Remove(key);
  132. }
  133. }
  134. /// <summary>
  135. /// Empties the cache completely, resetting it to blank state.
  136. /// </summary>
  137. public void Clear()
  138. {
  139. lock (_cache)
  140. {
  141. _cache.Clear();
  142. _strong.Clear();
  143. Hits = Misses = Evictions = 0;
  144. }
  145. }
  146. }
  147. /// <summary>
  148. /// Implements a cache for <see cref="ZipFile"/> instances.
  149. /// </summary>
  150. static class ZipCache
  151. {
  152. private static Cache<string, ZipCacheEntry> _cache = new Cache<string, ZipCacheEntry> { MaximumSize = 1 * 1024 * 1024 };
  153. /// <summary>Empties the cache completely, resetting it to blank state.</summary>
  154. public static void Clear() { _cache.Clear(); }
  155. /// <summary>
  156. /// Opens a file inside a zip file, returning the stream for reading its contents. The stream must be disposed after use.
  157. /// Returns null if the zip file or the file inside it does not exist.
  158. /// </summary>
  159. public static Stream GetZipFileStream(CompositePath path)
  160. {
  161. var zipfile = _cache.GetEntry(path.File.ToLowerInvariant(), () => new ZipCacheEntry(path.File)).Zip;
  162. if (zipfile == null)
  163. return null;
  164. var entry = zipfile.GetEntry(path.InnerFile.Replace('\\', '/'));
  165. if (entry == null)
  166. return null;
  167. else
  168. return zipfile.GetInputStream(entry);
  169. }
  170. }
  171. /// <summary>
  172. /// Implements a zip file cache entry.
  173. /// </summary>
  174. sealed class ZipCacheEntry : CacheEntry
  175. {
  176. public ZipFile Zip { get; private set; }
  177. private string _path;
  178. private DateTime _lastModified;
  179. public ZipCacheEntry(string path)
  180. {
  181. _path = path;
  182. }
  183. public override void Refresh()
  184. {
  185. if (!File.Exists(_path))
  186. Zip = null;
  187. else
  188. try
  189. {
  190. var modified = File.GetLastWriteTimeUtc(_path);
  191. if (_lastModified == modified)
  192. return;
  193. _lastModified = modified;
  194. Zip = new ZipFile(_path);
  195. }
  196. catch (FileNotFoundException) { Zip = null; }
  197. catch (DirectoryNotFoundException) { Zip = null; }
  198. }
  199. public override long Size
  200. {
  201. get { return IntPtr.Size * 6 + (Zip == null ? 0 : (IntPtr.Size * Zip.Count)); } // very approximate
  202. }
  203. }
  204. /// <summary>
  205. /// Implements a cache for images loaded from a variety of formats and, optionally, from inside zip files.
  206. /// </summary>
  207. static class ImageCache
  208. {
  209. private static Cache<string, ImageEntry> _cache = new Cache<string, ImageEntry> { MaximumSize = 10 * 1024 * 1024 };
  210. /// <summary>Empties the cache completely, resetting it to blank state.</summary>
  211. public static void Clear() { _cache.Clear(); }
  212. /// <summary>Retrieves an image which may optionally be stored inside a zip file.</summary>
  213. public static BitmapRam GetImage(CompositePath path)
  214. {
  215. return _cache.GetEntry(path.ToString(),
  216. () => path.InnerFile == null ? (ImageEntry) new FileImageEntry(path.File) : new ZipImageEntry(path)).Image;
  217. }
  218. }
  219. abstract class ImageEntry : CacheEntry
  220. {
  221. public BitmapRam Image;
  222. protected void LoadImage(Stream file, string extension)
  223. {
  224. if (extension == ".tga")
  225. Image = Targa.Load(file);
  226. else
  227. {
  228. if (!file.CanSeek) // http://stackoverflow.com/questions/14286462/how-to-use-bitmapdecoder-with-a-non-seekable-stream
  229. file = new MemoryStream(file.ReadAllBytes());
  230. Image = BitmapDecoder.Create(file, BitmapCreateOptions.None, BitmapCacheOption.None).Frames[0].ToBitmapRam();
  231. }
  232. Image.MarkReadOnly();
  233. }
  234. public override long Size
  235. {
  236. get { return 10 + (Image == null ? 0 : (Image.Width * Image.Height * 4)); } // very approximate
  237. }
  238. }
  239. sealed class ZipImageEntry : ImageEntry
  240. {
  241. private CompositePath _path;
  242. public ZipImageEntry(CompositePath path)
  243. {
  244. _path = path;
  245. }
  246. public override void Refresh()
  247. {
  248. using (var stream = ZipCache.GetZipFileStream(_path))
  249. if (stream == null)
  250. Image = null;
  251. else
  252. LoadImage(stream, Path.GetExtension(_path.InnerFile).ToLowerInvariant());
  253. }
  254. }
  255. sealed class FileImageEntry : ImageEntry
  256. {
  257. private string _path;
  258. private DateTime _lastModified;
  259. public FileImageEntry(string path)
  260. {
  261. _path = path;
  262. }
  263. public override void Refresh()
  264. {
  265. if (!File.Exists(_path))
  266. Image = null;
  267. else
  268. try
  269. {
  270. var modified = File.GetLastWriteTimeUtc(_path);
  271. if (_lastModified == modified)
  272. return;
  273. _lastModified = modified;
  274. using (var file = File.Open(_path, FileMode.Open, FileAccess.Read, FileShare.Read))
  275. LoadImage(file, Path.GetExtension(_path).ToLowerInvariant());
  276. }
  277. catch (FileNotFoundException) { Image = null; }
  278. catch (DirectoryNotFoundException) { Image = null; }
  279. }
  280. }
  281. }