aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/Region/CoreModules/Asset
diff options
context:
space:
mode:
Diffstat (limited to 'OpenSim/Region/CoreModules/Asset')
-rw-r--r--OpenSim/Region/CoreModules/Asset/FlotsamAssetCache.cs961
1 files changed, 523 insertions, 438 deletions
diff --git a/OpenSim/Region/CoreModules/Asset/FlotsamAssetCache.cs b/OpenSim/Region/CoreModules/Asset/FlotsamAssetCache.cs
index 1d122ee..99282a3 100644
--- a/OpenSim/Region/CoreModules/Asset/FlotsamAssetCache.cs
+++ b/OpenSim/Region/CoreModules/Asset/FlotsamAssetCache.cs
@@ -1,438 +1,523 @@
1/* 1/*
2 * Copyright (c) Contributors, http://opensimulator.org/ 2Copyright (c) Contributors, http://osflotsam.org/
3 * See CONTRIBUTORS.TXT for a full list of copyright holders. 3See CONTRIBUTORS.TXT for a full list of copyright holders.
4 * 4
5 * Redistribution and use in source and binary forms, with or without 5Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met: 6modification, are permitted provided that the following conditions are met:
7 * * Redistributions of source code must retain the above copyright 7 * Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer. 8 notice, this list of conditions and the following disclaimer.
9 * * Redistributions in binary form must reproduce the above copyright 9 * Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the 10 notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution. 11 documentation and/or other materials provided with the distribution.
12 * * Neither the name of the OpenSim Project nor the 12 * Neither the name of the Flotsam Project nor the
13 * names of its contributors may be used to endorse or promote products 13 names of its contributors may be used to endorse or promote products
14 * derived from this software without specific prior written permission. 14 derived from this software without specific prior written permission.
15 * 15
16 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY 16THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY EXPRESS OR
17 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY 19DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
20 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
22 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
26 */ 26ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
27 27
28using System; 28// Uncomment to make asset Get requests for existing
29using System.IO; 29// #define WAIT_ON_INPROGRESS_REQUESTS
30using System.Collections.Generic; 30
31using System.Reflection; 31using System;
32using System.Runtime.Serialization; 32using System.IO;
33using System.Runtime.Serialization.Formatters.Binary; 33using System.Collections.Generic;
34using System.Threading; 34using System.Reflection;
35using System.Timers; 35using System.Runtime.Serialization;
36 36using System.Runtime.Serialization.Formatters.Binary;
37 37using System.Threading;
38using GlynnTucker.Cache; 38using System.Timers;
39using log4net; 39
40using Nini.Config; 40using log4net;
41using Mono.Addins; 41using Nini.Config;
42 42using Mono.Addins;
43using OpenSim.Framework; 43using OpenMetaverse;
44using OpenSim.Region.Framework.Interfaces; 44
45using OpenSim.Region.Framework.Scenes; 45using OpenSim.Framework;
46using OpenSim.Services.Interfaces; 46using OpenSim.Region.Framework.Interfaces;
47 47using OpenSim.Region.Framework.Scenes;
48[assembly: Addin("FlotsamAssetCache", "1.0")] 48using OpenSim.Services.Interfaces;
49[assembly: AddinDependency("OpenSim", "0.5")] 49
50 50
51namespace OpenSim.Region.CoreModules.Asset 51[assembly: Addin("FlotsamAssetCache", "1.1")]
52{ 52[assembly: AddinDependency("OpenSim", "0.5")]
53 /// <summary> 53
54 /// OpenSim.ini Options: 54namespace Flotsam.RegionModules.AssetCache
55 /// ------- 55{
56 /// [Modules] 56 /// <summary>
57 /// AssetCaching = "FlotsamAssetCache" 57 /// OpenSim.ini Options:
58 /// 58 /// -------
59 /// [AssetCache] 59 /// [Modules]
60 /// ; cache directory can be shared by multiple instances 60 /// AssetCaching = "FlotsamAssetCache"
61 /// CacheDirectory = /directory/writable/by/OpenSim/instance 61 ///
62 /// 62 /// [AssetCache]
63 /// ; Set to false for disk cache only. 63 /// ; cache directory can be shared by multiple instances
64 /// MemoryCacheEnabled = true 64 /// CacheDirectory = /directory/writable/by/OpenSim/instance
65 /// 65 ///
66 /// ; How long {in hours} to keep assets cached in memory, .5 == 30 minutes 66 /// ; Log level
67 /// MemoryCacheTimeout = 2 67 /// ; 0 - (Error) Errors only
68 /// 68 /// ; 1 - (Info) Hit Rate Stats + Level 0
69 /// ; How long {in hours} to keep assets cached on disk, .5 == 30 minutes 69 /// ; 2 - (Debug) Cache Activity (Reads/Writes) + Level 1
70 /// ; Specify 0 if you do not want your disk cache to expire 70 /// ;
71 /// FileCacheTimeout = 0 71 /// LogLevel = 1
72 /// 72 ///
73 /// ; How often {in hours} should the disk be checked for expired filed 73 /// ; How often should hit rates be displayed (given in AssetRequests)
74 /// ; Specify 0 to disable expiration checking 74 /// ; 0 to disable
75 /// FileCleanupTimer = .166 ;roughly every 10 minutes 75 /// HitRateDisplay = 100
76 /// ------- 76 ///
77 /// </summary> 77 /// ; Set to false for disk cache only.
78 78 /// MemoryCacheEnabled = true
79 [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule")] 79 ///
80 public class FlotsamAssetCache : ISharedRegionModule, IImprovedAssetCache 80 /// ; How long {in hours} to keep assets cached in memory, .5 == 30 minutes
81 { 81 /// MemoryCacheTimeout = 2
82 private static readonly ILog m_log = 82 ///
83 LogManager.GetLogger( 83 /// ; How long {in hours} to keep assets cached on disk, .5 == 30 minutes
84 MethodBase.GetCurrentMethod().DeclaringType); 84 /// ; Specify 0 if you do not want your disk cache to expire
85 85 /// FileCacheTimeout = 0
86 private bool m_Enabled = false; 86 ///
87 87 /// ; How often {in hours} should the disk be checked for expired filed
88 private const string m_ModuleName = "FlotsamAssetCache"; 88 /// ; Specify 0 to disable expiration checking
89 private const string m_DefaultCacheDirectory = m_ModuleName; 89 /// FileCleanupTimer = .166 ;roughly every 10 minutes
90 private string m_CacheDirectory = m_DefaultCacheDirectory; 90 ///
91 91 /// ; If WAIT_ON_INPROGRESS_REQUESTS has been defined then this specifies how
92 92 /// ; long (in miliseconds) to block a request thread while trying to complete
93 private List<char> m_InvalidChars = new List<char>(); 93 /// ; writing to disk.
94 94 /// WaitOnInprogressTimeout = 3000
95 private uint m_DebugRate = 1; // How often to display hit statistics, given in requests 95 /// -------
96 96 /// </summary>
97 private static ulong m_Requests = 0; 97
98 private static ulong m_FileHits = 0; 98 [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule")]
99 private static ulong m_MemoryHits = 0; 99 public class FlotsamAssetCache : ISharedRegionModule, IImprovedAssetCache
100 private static double m_HitRateMemory = 0.0; 100 {
101 private static double m_HitRateFile = 0.0; 101 private static readonly ILog m_log =
102 102 LogManager.GetLogger(
103 private List<string> m_CurrentlyWriting = new List<string>(); 103 MethodBase.GetCurrentMethod().DeclaringType);
104 104
105 delegate void AsyncWriteDelegate(string file, AssetBase obj); 105 private bool m_Enabled = false;
106 106
107 private ICache m_MemoryCache = new GlynnTucker.Cache.SimpleMemoryCache(); 107 private const string m_ModuleName = "FlotsamAssetCache";
108 private bool m_MemoryCacheEnabled = true; 108 private const string m_DefaultCacheDirectory = m_ModuleName;
109 109 private string m_CacheDirectory = m_DefaultCacheDirectory;
110 // Expiration is expressed in hours. 110
111 private const double m_DefaultMemoryExpiration = 1.0; 111
112 private const double m_DefaultFileExpiration = 48; 112 private List<char> m_InvalidChars = new List<char>();
113 private TimeSpan m_MemoryExpiration = TimeSpan.Zero; 113
114 private TimeSpan m_FileExpiration = TimeSpan.Zero; 114 private int m_LogLevel = 1;
115 private TimeSpan m_FileExpirationCleanupTimer = TimeSpan.Zero; 115 private ulong m_HitRateDisplay = 1; // How often to display hit statistics, given in requests
116 116
117 private System.Timers.Timer m_CachCleanTimer = new System.Timers.Timer(); 117 private static ulong m_Requests = 0;
118 118 private static ulong m_RequestsForInprogress = 0;
119 public FlotsamAssetCache() 119 private static ulong m_DiskHits = 0;
120 { 120 private static ulong m_MemoryHits = 0;
121 m_InvalidChars.AddRange(Path.GetInvalidPathChars()); 121 private static double m_HitRateMemory = 0.0;
122 m_InvalidChars.AddRange(Path.GetInvalidFileNameChars()); 122 private static double m_HitRateFile = 0.0;
123 } 123
124 124#if WAIT_ON_INPROGRESS_REQUESTS
125 public string Name 125 private Dictionary<string, ManualResetEvent> m_CurrentlyWriting = new Dictionary<string, ManualResetEvent>();
126 { 126 private int m_WaitOnInprogressTimeout = 3000;
127 get { return m_ModuleName; } 127#else
128 } 128 private List<string> m_CurrentlyWriting = new List<string>();
129 129#endif
130 public void Initialise(IConfigSource source) 130
131 { 131 private ExpiringCache<string, AssetBase> m_MemoryCache = new ExpiringCache<string, AssetBase>();
132 IConfig moduleConfig = source.Configs["Modules"]; 132 private bool m_MemoryCacheEnabled = true;
133 133
134 if (moduleConfig != null) 134 // Expiration is expressed in hours.
135 { 135 private const double m_DefaultMemoryExpiration = 1.0;
136 string name = moduleConfig.GetString("AssetCaching", this.Name); 136 private const double m_DefaultFileExpiration = 48;
137 m_log.DebugFormat("[XXX] name = {0} (this module's name: {1}", name, Name); 137 private TimeSpan m_MemoryExpiration = TimeSpan.Zero;
138 138 private TimeSpan m_FileExpiration = TimeSpan.Zero;
139 if (name == Name) 139 private TimeSpan m_FileExpirationCleanupTimer = TimeSpan.Zero;
140 { 140
141 IConfig assetConfig = source.Configs["AssetCache"]; 141 private System.Timers.Timer m_CachCleanTimer = new System.Timers.Timer();
142 if (assetConfig == null) 142
143 { 143 public FlotsamAssetCache()
144 m_log.Error("[ASSET CACHE]: AssetCache missing from OpenSim.ini"); 144 {
145 return; 145 m_InvalidChars.AddRange(Path.GetInvalidPathChars());
146 } 146 m_InvalidChars.AddRange(Path.GetInvalidFileNameChars());
147 147 }
148 m_Enabled = true; 148
149 149 public string Name
150 m_log.InfoFormat("[ASSET CACHE]: {0} enabled", this.Name); 150 {
151 151 get { return m_ModuleName; }
152 m_CacheDirectory = assetConfig.GetString("CacheDirectory", m_DefaultCacheDirectory); 152 }
153 m_log.InfoFormat("[ASSET CACHE]: Cache Directory", m_DefaultCacheDirectory); 153
154 154 public void Initialise(IConfigSource source)
155 m_MemoryCacheEnabled = assetConfig.GetBoolean("MemoryCacheEnabled", true); 155 {
156 m_MemoryExpiration = TimeSpan.FromHours(assetConfig.GetDouble("MemoryCacheTimeout", m_DefaultMemoryExpiration)); 156 IConfig moduleConfig = source.Configs["Modules"];
157 157
158 158 if (moduleConfig != null)
159 m_FileExpiration = TimeSpan.FromHours(assetConfig.GetDouble("FileCacheTimeout", m_DefaultFileExpiration)); 159 {
160 m_FileExpirationCleanupTimer = TimeSpan.FromHours(assetConfig.GetDouble("FileCleanupTimer", m_DefaultFileExpiration)); 160 string name = moduleConfig.GetString("AssetCaching", this.Name);
161 if ((m_FileExpiration > TimeSpan.Zero) && (m_FileExpirationCleanupTimer > TimeSpan.Zero)) 161
162 { 162 if (name == Name)
163 m_CachCleanTimer.Interval = m_FileExpirationCleanupTimer.TotalMilliseconds; 163 {
164 m_CachCleanTimer.AutoReset = true; 164 IConfig assetConfig = source.Configs["AssetCache"];
165 m_CachCleanTimer.Elapsed += CleanupExpiredFiles; 165 if (assetConfig == null)
166 m_CachCleanTimer.Enabled = true; 166 {
167 m_CachCleanTimer.Start(); 167 m_log.Error("[ASSET CACHE]: AssetCache missing from OpenSim.ini");
168 } 168 return;
169 else 169 }
170 { 170
171 m_CachCleanTimer.Enabled = false; 171 m_Enabled = true;
172 } 172
173 } 173 m_log.InfoFormat("[ASSET CACHE]: {0} enabled", this.Name);
174 } 174
175 } 175 m_CacheDirectory = assetConfig.GetString("CacheDirectory", m_DefaultCacheDirectory);
176 176 m_log.InfoFormat("[ASSET CACHE]: Cache Directory", m_DefaultCacheDirectory);
177 public void PostInitialise() 177
178 { 178 m_MemoryCacheEnabled = assetConfig.GetBoolean("MemoryCacheEnabled", true);
179 } 179 m_MemoryExpiration = TimeSpan.FromHours(assetConfig.GetDouble("MemoryCacheTimeout", m_DefaultMemoryExpiration));
180 180
181 public void Close() 181#if WAIT_ON_INPROGRESS_REQUESTS
182 { 182 m_WaitOnInprogressTimeout = assetConfig.GetInt("WaitOnInprogressTimeout", 3000);
183 } 183#endif
184 184
185 public void AddRegion(Scene scene) 185 m_HitRateDisplay = (ulong)assetConfig.GetInt("HitRateDisplay", 1);
186 { 186
187 if (m_Enabled) 187 m_FileExpiration = TimeSpan.FromHours(assetConfig.GetDouble("FileCacheTimeout", m_DefaultFileExpiration));
188 scene.RegisterModuleInterface<IImprovedAssetCache>(this); 188 m_FileExpirationCleanupTimer = TimeSpan.FromHours(assetConfig.GetDouble("FileCleanupTimer", m_DefaultFileExpiration));
189 } 189 if ((m_FileExpiration > TimeSpan.Zero) && (m_FileExpirationCleanupTimer > TimeSpan.Zero))
190 190 {
191 public void RemoveRegion(Scene scene) 191 m_CachCleanTimer.Interval = m_FileExpirationCleanupTimer.TotalMilliseconds;
192 { 192 m_CachCleanTimer.AutoReset = true;
193 } 193 m_CachCleanTimer.Elapsed += CleanupExpiredFiles;
194 194 m_CachCleanTimer.Enabled = true;
195 public void RegionLoaded(Scene scene) 195 m_CachCleanTimer.Start();
196 { 196 }
197 } 197 else
198 198 {
199 //////////////////////////////////////////////////////////// 199 m_CachCleanTimer.Enabled = false;
200 // IImprovedAssetCache 200 }
201 // 201 }
202 202 }
203 private void UpdateMemoryCache(string key, AssetBase asset) 203 }
204 { 204
205 if( m_MemoryCacheEnabled ) 205 public void PostInitialise()
206 { 206 {
207 if (m_MemoryExpiration > TimeSpan.Zero) 207 }
208 { 208
209 m_MemoryCache.AddOrUpdate(key, asset, m_MemoryExpiration); 209 public void Close()
210 } 210 {
211 else 211 }
212 { 212
213 m_MemoryCache.AddOrUpdate(key, asset); 213 public void AddRegion(Scene scene)
214 } 214 {
215 } 215 if (m_Enabled)
216 } 216 scene.RegisterModuleInterface<IImprovedAssetCache>(this);
217 217 }
218 public void Cache(AssetBase asset) 218
219 { 219 public void RemoveRegion(Scene scene)
220 // TODO: Spawn this off to some seperate thread to do the actual writing 220 {
221 if (asset != null) 221 }
222 { 222
223 UpdateMemoryCache(asset.ID, asset); 223 public void RegionLoaded(Scene scene)
224 224 {
225 string filename = GetFileName(asset.ID); 225 }
226 226
227 try 227 ////////////////////////////////////////////////////////////
228 { 228 // IImprovedAssetCache
229 // If the file is already cached, don't cache it, just touch it so access time is updated 229 //
230 if (File.Exists(filename)) 230
231 { 231 private void UpdateMemoryCache(string key, AssetBase asset)
232 File.SetLastAccessTime(filename, DateTime.Now); 232 {
233 } else { 233 if( m_MemoryCacheEnabled )
234 234 {
235 // Once we start writing, make sure we flag that we're writing 235 if (m_MemoryExpiration > TimeSpan.Zero)
236 // that object to the cache so that we don't try to write the 236 {
237 // same file multiple times. 237 m_MemoryCache.AddOrUpdate(key, asset, m_MemoryExpiration);
238 lock (m_CurrentlyWriting) 238 }
239 { 239 else
240 if (m_CurrentlyWriting.Contains(filename)) 240 {
241 { 241 m_MemoryCache.AddOrUpdate(key, asset, DateTime.MaxValue);
242 return; 242 }
243 } 243 }
244 else 244 }
245 { 245
246 m_CurrentlyWriting.Add(filename); 246 public void Cache(AssetBase asset)
247 } 247 {
248 } 248 // TODO: Spawn this off to some seperate thread to do the actual writing
249 249 if (asset != null)
250 // Setup the actual writing so that it happens asynchronously 250 {
251 AsyncWriteDelegate awd = delegate( string file, AssetBase obj ) 251 UpdateMemoryCache(asset.ID, asset);
252 { 252
253 WriteFileCache(file, obj); 253 string filename = GetFileName(asset.ID);
254 }; 254
255 255 try
256 // Go ahead and cache it to disk 256 {
257 awd.BeginInvoke(filename, asset, null, null); 257 // If the file is already cached, don't cache it, just touch it so access time is updated
258 } 258 if (File.Exists(filename))
259 } 259 {
260 catch (Exception e) 260 File.SetLastAccessTime(filename, DateTime.Now);
261 { 261 } else {
262 string[] text = e.ToString().Split(new char[] { '\n' }); 262
263 foreach (string t in text) 263 // Once we start writing, make sure we flag that we're writing
264 { 264 // that object to the cache so that we don't try to write the
265 m_log.InfoFormat("[ASSET CACHE]: {0} ", t); 265 // same file multiple times.
266 } 266 lock (m_CurrentlyWriting)
267 } 267 {
268 } 268#if WAIT_ON_INPROGRESS_REQUESTS
269 } 269 if (m_CurrentlyWriting.ContainsKey(filename))
270 270 {
271 public AssetBase Get(string id) 271 return;
272 { 272 }
273 m_Requests++; 273 else
274 274 {
275 AssetBase asset = null; 275 m_CurrentlyWriting.Add(filename, new ManualResetEvent(false));
276 276 }
277 object obj; 277
278 if (m_MemoryCacheEnabled && m_MemoryCache.TryGet(id, out obj)) 278#else
279 { 279 if (m_CurrentlyWriting.Contains(filename))
280 asset = (AssetBase)obj; 280 {
281 m_MemoryHits++; 281 return;
282 } 282 }
283 else 283 else
284 { 284 {
285 try 285 m_CurrentlyWriting.Add(filename);
286 { 286 }
287 string filename = GetFileName(id); 287#endif
288 if (File.Exists(filename)) 288
289 { 289 }
290 FileStream stream = File.Open(filename, FileMode.Open); 290
291 BinaryFormatter bformatter = new BinaryFormatter(); 291 ThreadPool.QueueUserWorkItem(
292 292 delegate
293 asset = (AssetBase)bformatter.Deserialize(stream); 293 {
294 stream.Close(); 294 WriteFileCache(filename, asset);
295 295 }
296 UpdateMemoryCache(id, asset); 296 );
297 297 }
298 m_FileHits++; 298 }
299 } 299 catch (Exception e)
300 } 300 {
301 catch (Exception e) 301 LogException(e);
302 { 302 }
303 string[] text = e.ToString().Split(new char[] { '\n' }); 303 }
304 foreach (string t in text) 304 }
305 { 305
306 m_log.InfoFormat("[ASSET CACHE]: {0} ", t); 306 public AssetBase Get(string id)
307 } 307 {
308 } 308 m_Requests++;
309 } 309
310 310 AssetBase asset = null;
311 if (m_Requests % m_DebugRate == 0) 311
312 { 312 if (m_MemoryCacheEnabled && m_MemoryCache.TryGetValue(id, out asset))
313 m_HitRateFile = (double)m_FileHits / m_Requests * 100.0; 313 {
314 314 m_MemoryHits++;
315 m_log.DebugFormat("[ASSET CACHE]: Cache Get :: {0} :: {1}", id, asset == null ? "Miss" : "Hit"); 315 }
316 m_log.DebugFormat("[ASSET CACHE]: File Hit Rate {0}% for {1} requests", m_HitRateFile.ToString("0.00"), m_Requests); 316 else
317 317 {
318 if (m_MemoryCacheEnabled) 318 string filename = GetFileName(id);
319 { 319 if (File.Exists(filename))
320 m_HitRateMemory = (double)m_MemoryHits / m_Requests * 100.0; 320 {
321 m_log.DebugFormat("[ASSET CACHE]: Memory Hit Rate {0}% for {1} requests", m_HitRateMemory.ToString("0.00"), m_Requests); 321 try
322 } 322 {
323 } 323 FileStream stream = File.Open(filename, FileMode.Open);
324 324 BinaryFormatter bformatter = new BinaryFormatter();
325 return asset; 325
326 } 326 asset = (AssetBase)bformatter.Deserialize(stream);
327 327 stream.Close();
328 public void Expire(string id) 328
329 { 329 UpdateMemoryCache(id, asset);
330 try 330
331 { 331 m_DiskHits++;
332 string filename = GetFileName(id); 332 }
333 if (File.Exists(filename)) 333 catch (System.Runtime.Serialization.SerializationException e)
334 { 334 {
335 File.Delete(filename); 335 LogException(e);
336 } 336
337 337 // If there was a problem deserializing the asset, the asset may
338 if( m_MemoryCacheEnabled ) 338 // either be corrupted OR was serialized under an old format
339 m_MemoryCache.Remove(id); 339 // {different version of AssetBase} -- we should attempt to
340 } 340 // delete it and re-cache
341 catch (Exception e) 341 File.Delete(filename);
342 { 342 }
343 string[] text = e.ToString().Split(new char[] { '\n' }); 343 catch (Exception e)
344 foreach (string t in text) 344 {
345 { 345 LogException(e);
346 m_log.InfoFormat("[ASSET CACHE]: {0} ", t); 346 }
347 } 347 }
348 } 348
349 } 349
350 350#if WAIT_ON_INPROGRESS_REQUESTS
351 public void Clear() 351 // Check if we're already downloading this asset. If so, try to wait for it to
352 { 352 // download.
353 foreach (string dir in Directory.GetDirectories(m_CacheDirectory)) 353 if (m_WaitOnInprogressTimeout > 0)
354 { 354 {
355 Directory.Delete(dir); 355 m_RequestsForInprogress++;
356 } 356
357 357 ManualResetEvent waitEvent;
358 if( m_MemoryCacheEnabled ) 358 if (m_CurrentlyWriting.TryGetValue(filename, out waitEvent))
359 m_MemoryCache.Clear(); 359 {
360 } 360 waitEvent.WaitOne(m_WaitOnInprogressTimeout);
361 361 return Get(id);
362 private void CleanupExpiredFiles(object source, ElapsedEventArgs e) 362 }
363 { 363 }
364 foreach (string dir in Directory.GetDirectories(m_CacheDirectory)) 364#else
365 { 365 // Track how often we have the problem that an asset is requested while
366 foreach (string file in Directory.GetFiles(dir)) 366 // it is still being downloaded by a previous request.
367 { 367 if (m_CurrentlyWriting.Contains(filename))
368 if (DateTime.Now - File.GetLastAccessTime(file) > m_FileExpiration) 368 {
369 { 369 m_RequestsForInprogress++;
370 File.Delete(file); 370 }
371 } 371#endif
372 } 372 }
373 } 373
374 } 374 if (((m_LogLevel >= 1)) && (m_HitRateDisplay != 0) && (m_Requests % m_HitRateDisplay == 0))
375 375 {
376 private string GetFileName(string id) 376 m_HitRateFile = (double)m_DiskHits / m_Requests * 100.0;
377 { 377
378 // Would it be faster to just hash the darn thing? 378 m_log.InfoFormat("[ASSET CACHE]: Cache Get :: {0} :: {1}", id, asset == null ? "Miss" : "Hit");
379 foreach (char c in m_InvalidChars) 379 m_log.InfoFormat("[ASSET CACHE]: File Hit Rate {0}% for {1} requests", m_HitRateFile.ToString("0.00"), m_Requests);
380 { 380
381 id = id.Replace(c, '_'); 381 if (m_MemoryCacheEnabled)
382 } 382 {
383 383 m_HitRateMemory = (double)m_MemoryHits / m_Requests * 100.0;
384 string p = id.Substring(id.Length - 4); 384 m_log.InfoFormat("[ASSET CACHE]: Memory Hit Rate {0}% for {1} requests", m_HitRateMemory.ToString("0.00"), m_Requests);
385 p = Path.Combine(p, id); 385 }
386 return Path.Combine(m_CacheDirectory, p); 386
387 } 387 m_log.InfoFormat("[ASSET CACHE]: {0} unnessesary requests due to requests for assets that are currently downloading.", m_RequestsForInprogress);
388 388
389 private void WriteFileCache(string filename, AssetBase asset) 389 }
390 { 390
391 try 391 return asset;
392 { 392 }
393 // Make sure the target cache directory exists 393
394 string directory = Path.GetDirectoryName(filename); 394 public void Expire(string id)
395 if (!Directory.Exists(directory)) 395 {
396 { 396 if (m_LogLevel >= 2)
397 Directory.CreateDirectory(directory); 397 m_log.DebugFormat("[ASSET CACHE]: Expiring Asset {0}.", id);
398 } 398
399 399 try
400 // Write file first to a temp name, so that it doesn't look 400 {
401 // like it's already cached while it's still writing. 401 string filename = GetFileName(id);
402 string tempname = Path.Combine(directory, Path.GetRandomFileName()); 402 if (File.Exists(filename))
403 Stream stream = File.Open(tempname, FileMode.Create); 403 {
404 BinaryFormatter bformatter = new BinaryFormatter(); 404 File.Delete(filename);
405 bformatter.Serialize(stream, asset); 405 }
406 stream.Close(); 406
407 407 if( m_MemoryCacheEnabled )
408 // Now that it's written, rename it so that it can be found. 408 m_MemoryCache.Remove(id);
409 File.Move(tempname, filename); 409 }
410 410 catch (Exception e)
411 m_log.DebugFormat("[ASSET CACHE]: Cache Stored :: {0}", asset.ID); 411 {
412 } 412 LogException(e);
413 catch (Exception e) 413 }
414 { 414 }
415 string[] text = e.ToString().Split(new char[] { '\n' }); 415
416 foreach (string t in text) 416 public void Clear()
417 { 417 {
418 m_log.InfoFormat("[ASSET CACHE]: {0} ", t); 418 if (m_LogLevel >= 2)
419 } 419 m_log.Debug("[ASSET CACHE]: Clearing Cache.");
420 420
421 } 421 foreach (string dir in Directory.GetDirectories(m_CacheDirectory))
422 finally 422 {
423 { 423 Directory.Delete(dir);
424 // Even if the write fails with an exception, we need to make sure 424 }
425 // that we release the lock on that file, otherwise it'll never get 425
426 // cached 426 if( m_MemoryCacheEnabled )
427 lock (m_CurrentlyWriting) 427 m_MemoryCache.Clear();
428 { 428 }
429 if (m_CurrentlyWriting.Contains(filename)) 429
430 { 430 private void CleanupExpiredFiles(object source, ElapsedEventArgs e)
431 m_CurrentlyWriting.Remove(filename); 431 {
432 } 432 if (m_LogLevel >= 2)
433 } 433 m_log.DebugFormat("[ASSET CACHE]: Checking for expired files older then {0}.", m_FileExpiration.ToString());
434 434
435 } 435 foreach (string dir in Directory.GetDirectories(m_CacheDirectory))
436 } 436 {
437 } 437 foreach (string file in Directory.GetFiles(dir))
438} 438 {
439 if (DateTime.Now - File.GetLastAccessTime(file) > m_FileExpiration)
440 {
441 File.Delete(file);
442 }
443 }
444 }
445 }
446
447 private string GetFileName(string id)
448 {
449 // Would it be faster to just hash the darn thing?
450 foreach (char c in m_InvalidChars)
451 {
452 id = id.Replace(c, '_');
453 }
454
455 string p = id.Substring(id.Length - 4);
456 p = Path.Combine(p, id);
457 return Path.Combine(m_CacheDirectory, p);
458 }
459
460 private void WriteFileCache(string filename, AssetBase asset)
461 {
462 try
463 {
464 // Make sure the target cache directory exists
465 string directory = Path.GetDirectoryName(filename);
466 if (!Directory.Exists(directory))
467 {
468 Directory.CreateDirectory(directory);
469 }
470
471 // Write file first to a temp name, so that it doesn't look
472 // like it's already cached while it's still writing.
473 string tempname = Path.Combine(directory, Path.GetRandomFileName());
474 Stream stream = File.Open(tempname, FileMode.Create);
475 BinaryFormatter bformatter = new BinaryFormatter();
476 bformatter.Serialize(stream, asset);
477 stream.Close();
478
479 // Now that it's written, rename it so that it can be found.
480 File.Move(tempname, filename);
481
482 if (m_LogLevel >= 2)
483 m_log.DebugFormat("[ASSET CACHE]: Cache Stored :: {0}", asset.ID);
484 }
485 catch (Exception e)
486 {
487 LogException(e);
488 }
489 finally
490 {
491 // Even if the write fails with an exception, we need to make sure
492 // that we release the lock on that file, otherwise it'll never get
493 // cached
494 lock (m_CurrentlyWriting)
495 {
496#if WAIT_ON_INPROGRESS_REQUESTS
497 ManualResetEvent waitEvent;
498 if (m_CurrentlyWriting.TryGetValue(filename, out waitEvent))
499 {
500 m_CurrentlyWriting.Remove(filename);
501 waitEvent.Set();
502 }
503#else
504 if (m_CurrentlyWriting.Contains(filename))
505 {
506 m_CurrentlyWriting.Remove(filename);
507 }
508#endif
509 }
510
511 }
512 }
513
514 private static void LogException(Exception e)
515 {
516 string[] text = e.ToString().Split(new char[] { '\n' });
517 foreach (string t in text)
518 {
519 m_log.ErrorFormat("[ASSET CACHE]: {0} ", t);
520 }
521 }
522 }
523}