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