33 using System.Collections.Generic;
35 using System.Reflection;
36 using System.Runtime.Serialization;
37 using System.Runtime.Serialization.Formatters.Binary;
38 using System.Threading;
44 using OpenSim.Framework;
45 using OpenSim.Framework.Console;
46 using OpenSim.Framework.Monitoring;
47 using OpenSim.Region.Framework.Interfaces;
48 using OpenSim.Region.Framework.Scenes;
49 using OpenSim.Services.Interfaces;
57 [Extension(Path =
"/OpenSim/RegionModules", NodeName =
"RegionModule", Id =
"FlotsamAssetCache")]
60 private static readonly ILog m_log =
62 MethodBase.GetCurrentMethod().DeclaringType);
64 private bool m_Enabled;
66 private const string m_ModuleName =
"FlotsamAssetCache";
67 private const string m_DefaultCacheDirectory =
"./assetcache";
68 private string m_CacheDirectory = m_DefaultCacheDirectory;
70 private readonly List<char> m_InvalidChars =
new List<char>();
72 private int m_LogLevel = 0;
73 private ulong m_HitRateDisplay = 100;
75 private static ulong m_Requests;
76 private static ulong m_RequestsForInprogress;
77 private static ulong m_DiskHits;
78 private static ulong m_MemoryHits;
80 #if WAIT_ON_INPROGRESS_REQUESTS
81 private Dictionary<string, ManualResetEvent> m_CurrentlyWriting =
new Dictionary<string, ManualResetEvent>();
82 private int m_WaitOnInprogressTimeout = 3000;
84 private HashSet<string> m_CurrentlyWriting =
new HashSet<string>();
87 private bool m_FileCacheEnabled =
true;
89 private ExpiringCache<string, AssetBase> m_MemoryCache;
90 private bool m_MemoryCacheEnabled =
false;
93 private const double m_DefaultMemoryExpiration = 2;
94 private const double m_DefaultFileExpiration = 48;
95 private TimeSpan m_MemoryExpiration = TimeSpan.FromHours(m_DefaultMemoryExpiration);
96 private TimeSpan m_FileExpiration = TimeSpan.FromHours(m_DefaultFileExpiration);
97 private TimeSpan m_FileExpirationCleanupTimer = TimeSpan.FromHours(0.166);
99 private static int m_CacheDirectoryTiers = 1;
100 private static int m_CacheDirectoryTierLen = 3;
101 private static int m_CacheWarnAt = 30000;
103 private System.Timers.Timer m_CacheCleanTimer;
106 private List<Scene> m_Scenes =
new List<Scene>();
110 m_InvalidChars.AddRange(Path.GetInvalidPathChars());
111 m_InvalidChars.AddRange(Path.GetInvalidFileNameChars());
114 public Type ReplaceableInterface
121 get {
return m_ModuleName; }
126 IConfig moduleConfig = source.Configs[
"Modules"];
128 if (moduleConfig != null)
130 string name = moduleConfig.GetString(
"AssetCaching", String.Empty);
134 m_MemoryCache =
new ExpiringCache<string, AssetBase>();
137 m_log.InfoFormat(
"[FLOTSAM ASSET CACHE]: {0} enabled", this.Name);
139 IConfig assetConfig = source.Configs[
"AssetCache"];
140 if (assetConfig == null)
143 "[FLOTSAM ASSET CACHE]: AssetCache section missing from config (not copied config-include/FlotsamCache.ini.example? Using defaults.");
147 m_FileCacheEnabled = assetConfig.GetBoolean(
"FileCacheEnabled", m_FileCacheEnabled);
148 m_CacheDirectory = assetConfig.GetString(
"CacheDirectory", m_DefaultCacheDirectory);
150 m_MemoryCacheEnabled = assetConfig.GetBoolean(
"MemoryCacheEnabled", m_MemoryCacheEnabled);
151 m_MemoryExpiration = TimeSpan.FromHours(assetConfig.GetDouble(
"MemoryCacheTimeout", m_DefaultMemoryExpiration));
153 #if WAIT_ON_INPROGRESS_REQUESTS
154 m_WaitOnInprogressTimeout = assetConfig.GetInt(
"WaitOnInprogressTimeout", 3000);
157 m_LogLevel = assetConfig.GetInt(
"LogLevel", m_LogLevel);
158 m_HitRateDisplay = (ulong)assetConfig.GetLong(
"HitRateDisplay", (
long)m_HitRateDisplay);
160 m_FileExpiration = TimeSpan.FromHours(assetConfig.GetDouble(
"FileCacheTimeout", m_DefaultFileExpiration));
161 m_FileExpirationCleanupTimer
162 = TimeSpan.FromHours(
163 assetConfig.GetDouble(
"FileCleanupTimer", m_FileExpirationCleanupTimer.TotalHours));
165 m_CacheDirectoryTiers = assetConfig.GetInt(
"CacheDirectoryTiers", m_CacheDirectoryTiers);
166 m_CacheDirectoryTierLen = assetConfig.GetInt(
"CacheDirectoryTierLength", m_CacheDirectoryTierLen);
168 m_CacheWarnAt = assetConfig.GetInt(
"CacheWarnAt", m_CacheWarnAt);
171 m_log.InfoFormat(
"[FLOTSAM ASSET CACHE]: Cache Directory {0}", m_CacheDirectory);
173 if (m_FileCacheEnabled && (m_FileExpiration > TimeSpan.Zero) && (m_FileExpirationCleanupTimer > TimeSpan.Zero))
175 m_CacheCleanTimer =
new System.Timers.Timer(m_FileExpirationCleanupTimer.TotalMilliseconds);
176 m_CacheCleanTimer.AutoReset =
true;
177 m_CacheCleanTimer.Elapsed += CleanupExpiredFiles;
178 lock (m_CacheCleanTimer)
179 m_CacheCleanTimer.Start();
182 if (m_CacheDirectoryTiers < 1)
184 m_CacheDirectoryTiers = 1;
186 else if (m_CacheDirectoryTiers > 3)
188 m_CacheDirectoryTiers = 3;
191 if (m_CacheDirectoryTierLen < 1)
193 m_CacheDirectoryTierLen = 1;
195 else if (m_CacheDirectoryTierLen > 4)
197 m_CacheDirectoryTierLen = 4;
200 MainConsole.Instance.Commands.AddCommand(
"Assets",
true,
"fcache status",
"fcache status",
"Display cache status", HandleConsoleCommand);
201 MainConsole.Instance.Commands.AddCommand(
"Assets",
true,
"fcache clear",
"fcache clear [file] [memory]",
"Remove all assets in the cache. If file or memory is specified then only this cache is cleared.", HandleConsoleCommand);
202 MainConsole.Instance.Commands.AddCommand(
"Assets",
true,
"fcache assets",
"fcache assets",
"Attempt a deep scan and cache of all assets in all scenes", HandleConsoleCommand);
203 MainConsole.Instance.Commands.AddCommand(
"Assets",
true,
"fcache expire",
"fcache expire <datetime>",
"Purge cached assets older then the specified date/time", HandleConsoleCommand);
231 m_Scenes.Remove(scene);
237 if (m_Enabled && m_AssetService == null)
238 m_AssetService = scene.RequestModuleInterface<
IAssetService>();
245 private void UpdateMemoryCache(
string key,
AssetBase asset)
247 m_MemoryCache.AddOrUpdate(
key, asset, m_MemoryExpiration);
250 private void UpdateFileCache(
string key,
AssetBase asset)
252 string filename = GetFileName(key);
257 if (
File.Exists(filename))
259 UpdateFileLastAccessTime(filename);
266 lock (m_CurrentlyWriting)
268 #if WAIT_ON_INPROGRESS_REQUESTS
269 if (m_CurrentlyWriting.ContainsKey(filename))
275 m_CurrentlyWriting.Add(filename,
new ManualResetEvent(
false));
279 if (m_CurrentlyWriting.Contains(filename))
285 m_CurrentlyWriting.Add(filename);
292 delegate { WriteFileCache(filename, asset); }, null,
"FlotsamAssetCache.UpdateFileCache");
298 "[FLOTSAM ASSET CACHE]: Failed to update cache for asset {0}. Exception {1} {2}",
299 asset.ID, e.Message, e.StackTrace);
310 if (m_MemoryCacheEnabled)
311 UpdateMemoryCache(asset.
ID, asset);
313 if (m_FileCacheEnabled)
314 UpdateFileCache(asset.
ID, asset);
323 private bool UpdateFileLastAccessTime(
string filename)
327 File.SetLastAccessTime(filename, DateTime.Now);
341 private AssetBase GetFromMemoryCache(
string id)
345 if (m_MemoryCache.TryGetValue(
id, out asset))
351 private bool CheckFromMemoryCache(
string id)
353 return m_MemoryCache.Contains(id);
361 private AssetBase GetFromFileCache(
string id)
363 string filename = GetFileName(
id);
365 #if WAIT_ON_INPROGRESS_REQUESTS
368 if (m_WaitOnInprogressTimeout > 0)
370 m_RequestsForInprogress++;
372 ManualResetEvent waitEvent;
373 if (m_CurrentlyWriting.TryGetValue(filename, out waitEvent))
375 waitEvent.WaitOne(m_WaitOnInprogressTimeout);
382 if (m_CurrentlyWriting.Contains(filename))
384 m_RequestsForInprogress++;
391 if (
File.Exists(filename))
395 using (FileStream stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read))
397 BinaryFormatter bformatter =
new BinaryFormatter();
399 asset = (
AssetBase)bformatter.Deserialize(stream);
404 catch (System.Runtime.Serialization.SerializationException e)
407 "[FLOTSAM ASSET CACHE]: Failed to get file {0} for asset {1}. Exception {2} {3}",
408 filename, id, e.Message, e.StackTrace);
414 File.Delete(filename);
419 "[FLOTSAM ASSET CACHE]: Failed to get file {0} for asset {1}. Exception {2} {3}",
420 filename, id, e.Message, e.StackTrace);
427 private bool CheckFromFileCache(
string id)
431 string filename = GetFileName(
id);
433 if (
File.Exists(filename))
437 using (FileStream stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read))
446 "[FLOTSAM ASSET CACHE]: Failed to check file {0} for asset {1}. Exception {2} {3}",
447 filename, id, e.Message, e.StackTrace);
460 if (m_MemoryCacheEnabled)
461 asset = GetFromMemoryCache(
id);
463 if (asset == null && m_FileCacheEnabled)
465 asset = GetFromFileCache(
id);
467 if (m_MemoryCacheEnabled && asset != null)
468 UpdateMemoryCache(
id, asset);
471 if (((m_LogLevel >= 1)) && (m_HitRateDisplay != 0) && (m_Requests % m_HitRateDisplay == 0))
473 m_log.InfoFormat(
"[FLOTSAM ASSET CACHE]: Cache Get :: {0} :: {1}", id, asset == null ?
"Miss" :
"Hit");
475 GenerateCacheHitReport().ForEach(l => m_log.InfoFormat(
"[FLOTSAM ASSET CACHE]: {0}", l));
483 if (m_MemoryCacheEnabled && CheckFromMemoryCache(
id))
486 if (m_FileCacheEnabled && CheckFromFileCache(
id))
499 m_log.DebugFormat(
"[FLOTSAM ASSET CACHE]: Expiring Asset {0}", id);
503 if (m_FileCacheEnabled)
505 string filename = GetFileName(
id);
506 if (
File.Exists(filename))
508 File.Delete(filename);
512 if (m_MemoryCacheEnabled)
513 m_MemoryCache.Remove(id);
518 "[FLOTSAM ASSET CACHE]: Failed to expire cached file {0}. Exception {1} {2}",
519 id, e.Message, e.StackTrace);
526 m_log.Debug(
"[FLOTSAM ASSET CACHE]: Clearing caches.");
528 if (m_FileCacheEnabled)
530 foreach (
string dir
in Directory.GetDirectories(m_CacheDirectory))
532 Directory.Delete(dir);
536 if (m_MemoryCacheEnabled)
537 m_MemoryCache.Clear();
540 private void CleanupExpiredFiles(
object source, ElapsedEventArgs e)
543 m_log.DebugFormat(
"[FLOTSAM ASSET CACHE]: Checking for expired files older then {0}.", m_FileExpiration);
546 DateTime purgeLine = DateTime.Now - m_FileExpiration;
551 TouchAllSceneAssets(
false);
553 foreach (
string dir
in Directory.GetDirectories(m_CacheDirectory))
555 CleanExpiredFiles(dir, purgeLine);
566 private void CleanExpiredFiles(
string dir, DateTime purgeLine)
570 foreach (
string file
in Directory.GetFiles(dir))
572 if (
File.GetLastAccessTime(file) < purgeLine)
579 foreach (
string subdir
in Directory.GetDirectories(dir))
581 CleanExpiredFiles(subdir, purgeLine);
585 int dirSize = Directory.GetFiles(dir).Length + Directory.GetDirectories(dir).Length;
588 Directory.Delete(dir);
590 else if (dirSize >= m_CacheWarnAt)
593 "[FLOTSAM ASSET CACHE]: Cache folder exceeded CacheWarnAt limit {0} {1}. Suggest increasing tiers, tier length, or reducing cache expiration",
597 catch (DirectoryNotFoundException)
605 string.Format(
"[FLOTSAM ASSET CACHE]: Could not complete clean of expired files in {0}, exception ", dir), e);
614 private string GetFileName(
string id)
617 foreach (
char c
in m_InvalidChars)
619 id = id.Replace(c,
'_');
622 string path = m_CacheDirectory;
623 for (
int p = 1; p <= m_CacheDirectoryTiers; p++)
625 string pathPart = id.Substring((p - 1) * m_CacheDirectoryTierLen, m_CacheDirectoryTierLen);
626 path = Path.Combine(path, pathPart);
629 return Path.Combine(path, id);
638 private void WriteFileCache(
string filename,
AssetBase asset)
640 Stream stream = null;
643 string directory = Path.GetDirectoryName(filename);
647 string tempname = Path.Combine(directory, Path.GetRandomFileName());
653 if (!Directory.Exists(directory))
655 Directory.CreateDirectory(directory);
658 stream = File.Open(tempname, FileMode.Create);
659 BinaryFormatter bformatter =
new BinaryFormatter();
660 bformatter.Serialize(stream, asset);
662 catch (IOException e)
665 "[FLOTSAM ASSET CACHE]: Failed to write asset {0} to temporary location {1} (final {2}) on cache in {3}. Exception {4} {5}.",
666 asset.ID, tempname, filename, directory, e.Message, e.StackTrace);
690 File.Move(tempname, filename);
693 m_log.DebugFormat(
"[FLOTSAM ASSET CACHE]: Cache Stored :: {0}", asset.ID);
707 lock (m_CurrentlyWriting)
709 #if WAIT_ON_INPROGRESS_REQUESTS
710 ManualResetEvent waitEvent;
711 if (m_CurrentlyWriting.TryGetValue(filename, out waitEvent))
713 m_CurrentlyWriting.Remove(filename);
717 m_CurrentlyWriting.Remove(filename);
728 private int GetFileCacheCount(
string dir)
730 int count = Directory.GetFiles(dir).Length;
732 foreach (
string subdir
in Directory.GetDirectories(dir))
734 count += GetFileCacheCount(subdir);
744 private void StampRegionStatusFile(UUID regionID)
746 string RegionCacheStatusFile = Path.Combine(m_CacheDirectory,
"RegionStatus_" + regionID.ToString() +
".fac");
750 if (
File.Exists(RegionCacheStatusFile))
752 File.SetLastWriteTime(RegionCacheStatusFile, DateTime.Now);
757 RegionCacheStatusFile,
758 "Please do not delete this file unless you are manually clearing your Flotsam Asset Cache.");
765 "[FLOTSAM ASSET CACHE]: Could not stamp region status file for region {0}. Exception ",
780 private int TouchAllSceneAssets(
bool storeUncached)
784 Dictionary<UUID, bool> assetsFound =
new Dictionary<UUID, bool>();
786 foreach (
Scene s
in m_Scenes)
792 gatherer.AddForInspection(e);
793 gatherer.GatherAll();
797 if (!assetsFound.ContainsKey(assetID))
799 string filename = GetFileName(assetID.ToString());
801 if (
File.Exists(filename))
803 UpdateFileLastAccessTime(filename);
805 else if (storeUncached)
807 AssetBase cachedAsset = m_AssetService.Get(assetID.ToString());
808 if (cachedAsset == null && gatherer.
GatheredUuids[assetID] != (sbyte)AssetType.Unknown)
809 assetsFound[assetID] =
false;
811 assetsFound[assetID] =
true;
814 else if (!assetsFound[assetID])
817 "[FLOTSAM ASSET CACHE]: Could not find asset {0}, type {1} referenced by object {2} at {3} in scene {4} when pre-caching all scene assets",
818 assetID, gatherer.GatheredUuids[assetID], e.Name, e.AbsolutePosition, s.Name);
822 gatherer.GatheredUuids.Clear();
826 return assetsFound.Count;
832 private void ClearFileCache()
834 foreach (
string dir
in Directory.GetDirectories(m_CacheDirectory))
838 Directory.Delete(dir,
true);
843 "[FLOTSAM ASSET CACHE]: Couldn't clear asset cache directory {0} from {1}. Exception {2} {3}",
844 dir, m_CacheDirectory, e.Message, e.StackTrace);
848 foreach (
string file
in Directory.GetFiles(m_CacheDirectory))
857 "[FLOTSAM ASSET CACHE]: Couldn't clear asset cache file {0} from {1}. Exception {1} {2}",
858 file, m_CacheDirectory, e.Message, e.StackTrace);
863 private List<string> GenerateCacheHitReport()
865 List<string> outputLines =
new List<string>();
867 double fileHitRate = (double)m_DiskHits / m_Requests * 100.0;
869 string.Format(
"File Hit Rate: {0}% for {1} requests", fileHitRate.ToString(
"0.00"), m_Requests));
871 if (m_MemoryCacheEnabled)
873 double memHitRate = (double)m_MemoryHits / m_Requests * 100.0;
876 string.Format(
"Memory Hit Rate: {0}% for {1} requests", memHitRate.ToString(
"0.00"), m_Requests));
881 "Unnecessary requests due to requests for assets that are currently downloading: {0}",
882 m_RequestsForInprogress));
887 #region Console Commands
888 private void HandleConsoleCommand(
string module,
string[] cmdparams)
892 if (cmdparams.Length >= 2)
894 string cmd = cmdparams[1];
899 if (m_MemoryCacheEnabled)
900 con.OutputFormat(
"Memory Cache: {0} assets", m_MemoryCache.Count);
902 con.OutputFormat(
"Memory cache disabled");
904 if (m_FileCacheEnabled)
906 int fileCount = GetFileCacheCount(m_CacheDirectory);
907 con.OutputFormat(
"File Cache: {0} assets", fileCount);
911 con.Output(
"File cache disabled");
914 GenerateCacheHitReport().ForEach(l => con.Output(l));
916 if (m_FileCacheEnabled)
918 con.Output(
"Deep scans have previously been performed on the following regions:");
920 foreach (
string s
in Directory.GetFiles(m_CacheDirectory,
"*.fac"))
922 string RegionID = s.Remove(0,s.IndexOf(
"_")).Replace(
".fac",
"");
923 DateTime RegionDeepScanTMStamp = File.GetLastWriteTime(s);
924 con.OutputFormat(
"Region: {0}, {1}", RegionID, RegionDeepScanTMStamp.ToString(
"MM/dd/yyyy hh:mm:ss"));
931 if (cmdparams.Length < 2)
933 con.Output(
"Usage is fcache clear [file] [memory]");
937 bool clearMemory =
false, clearFile =
false;
939 if (cmdparams.Length == 2)
944 foreach (
string s
in cmdparams)
946 if (s.ToLower() ==
"memory")
948 else if (s.ToLower() ==
"file")
954 if (m_MemoryCacheEnabled)
956 m_MemoryCache.Clear();
957 con.Output(
"Memory cache cleared.");
961 con.Output(
"Memory cache not enabled.");
967 if (m_FileCacheEnabled)
970 con.Output(
"File cache cleared.");
974 con.Output(
"File cache not enabled.");
981 con.Output(
"Ensuring assets are cached for all scenes.");
983 WorkManager.RunInThread(delegate
985 int assetReferenceTotal = TouchAllSceneAssets(
true);
986 con.OutputFormat(
"Completed check with {0} assets.", assetReferenceTotal);
987 }, null,
"TouchAllSceneAssets");
992 if (cmdparams.Length < 3)
994 con.OutputFormat(
"Invalid parameters for Expire, please specify a valid date & time", cmd);
998 string s_expirationDate =
"";
999 DateTime expirationDate;
1001 if (cmdparams.Length > 3)
1003 s_expirationDate = string.Join(
" ", cmdparams, 2, cmdparams.Length - 2);
1007 s_expirationDate = cmdparams[2];
1010 if (!DateTime.TryParse(s_expirationDate, out expirationDate))
1012 con.OutputFormat(
"{0} is not a valid date & time", cmd);
1016 if (m_FileCacheEnabled)
1017 CleanExpiredFiles(m_CacheDirectory, expirationDate);
1019 con.OutputFormat(
"File cache not active, not clearing.");
1023 con.OutputFormat(
"Unknown command {0}", cmd);
1027 else if (cmdparams.Length == 1)
1029 con.Output(
"fcache assets - Attempt a deep cache of all assets in all scenes");
1030 con.Output(
"fcache expire <datetime> - Purge assets older then the specified date & time");
1031 con.Output(
"fcache clear [file] [memory] - Remove cached assets");
1032 con.Output(
"fcache status - Display cache status");
1038 #region IAssetService Members
1043 return asset.Metadata;
1055 handler(
id, sender, asset);
1061 bool[] exist =
new bool[ids.Length];
1063 for (
int i = 0; i < ids.Length; i++)
1065 exist[i] =
Check(ids[i]);
1075 asset.FullID = UUID.Random();
void Expire(string id)
Expire an asset from the cache.
bool[] AssetsExist(string[] ids)
Check if assets exist in the database.
void Initialise(IConfigSource source)
This is called to initialize the region module. For shared modules, this is called exactly once...
Gather uuids for a given entity.
bool Check(string id)
Check whether an asset with the specified id exists in the cache.
AssetBase GetCached(string id)
Synchronously fetches an asset from the local cache only.
bool Get(string id, object sender, AssetRetrieved handler)
void RemoveRegion(Scene scene)
This is called whenever a Scene is removed. For shared modules, this can happen several times...
byte[] GetData(string id)
Get an asset's data, ignoring the metadata.
A scene object group is conceptually an object in the scene. The object is constituted of SceneObject...
AssetMetadata GetMetadata(string id)
Get an asset's metadata
void Cache(AssetBase asset)
Cache the specified asset.
OpenSim.Region.ScriptEngine.Shared.LSL_Types.LSLString key
Asset class. All Assets are reference by this class or a class derived from this class ...
bool Delete(string id)
Delete an asset
void RegionLoaded(Scene scene)
This will be called once for every scene loaded. In a shared module this will be multiple times in on...
IDictionary< UUID, sbyte > GatheredUuids
The dictionary of UUIDs gathered so far. If Complete == true then this is all the reachable UUIDs...
bool UpdateContent(string id, byte[] data)
Update an asset's content
void Clear()
Clear the cache.
void Close()
This is the inverse to Initialise. After a Close(), this instance won't be usable anymore...
delegate void AssetRetrieved(string id, Object sender, AssetBase asset)
void PostInitialise()
This is called exactly once after all the shared region-modules have been instanciated and IRegionMod...
virtual RegionInfo RegionInfo
AssetBase Get(string id)
Get an asset by its id.
void AddRegion(Scene scene)
This is called whenever a Scene is added. For shared modules, this can happen several times...
string ID
Asset MetaData ID (transferring from UUID to string ID)
string Store(AssetBase asset)
Creates a new asset