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