FlotsamAssetCache.cs 63 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723
  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 OpenSimulator 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. using System;
  28. using System.IO;
  29. using System.Collections.Generic;
  30. using System.Reflection;
  31. using System.Runtime.Serialization.Formatters.Binary;
  32. using System.Text;
  33. using System.Threading;
  34. using System.Timers;
  35. using log4net;
  36. using Nini.Config;
  37. using Mono.Addins;
  38. using OpenMetaverse;
  39. using OpenSim.Framework;
  40. using OpenSim.Framework.Monitoring;
  41. using OpenSim.Region.Framework.Interfaces;
  42. using OpenSim.Region.Framework.Scenes;
  43. using OpenSim.Server.Base;
  44. using OpenSim.Services.Interfaces;
  45. using System.Runtime.InteropServices;
  46. namespace OpenSim.Region.CoreModules.Asset
  47. {
  48. [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "FlotsamAssetCache")]
  49. public class FlotsamAssetCache : ISharedRegionModule, IAssetCache, IAssetService
  50. {
  51. private struct WriteAssetInfo
  52. {
  53. public string filename;
  54. public AssetBase asset;
  55. public bool replace;
  56. }
  57. private static readonly ILog m_log = LogManager.GetLogger( MethodBase.GetCurrentMethod().DeclaringType);
  58. private bool m_Enabled;
  59. private bool m_timerRunning;
  60. private bool m_cleanupRunning;
  61. private const string m_ModuleName = "FlotsamAssetCache";
  62. private string m_CacheDirectory = "assetcache";
  63. private string m_assetLoader;
  64. private string m_assetLoaderArgs;
  65. private readonly char[] m_InvalidChars;
  66. private int m_LogLevel = 0;
  67. private ulong m_HitRateDisplay = 100; // How often to display hit statistics, given in requests
  68. private ulong m_Requests;
  69. private ulong m_RequestsForInprogress;
  70. private ulong m_DiskHits;
  71. private ulong m_MemoryHits;
  72. private ulong m_weakRefHits;
  73. private static readonly HashSet<string> m_CurrentlyWriting = new();
  74. private static ObjectJobEngine m_assetFileWriteWorker = null;
  75. private static HashSet<string> m_defaultAssets = new();
  76. private bool m_FileCacheEnabled = true;
  77. private ExpiringCacheOS<string, AssetBase> m_MemoryCache;
  78. private bool m_MemoryCacheEnabled = false;
  79. private ExpiringKey<string> m_negativeCache;
  80. private bool m_negativeCacheEnabled = true;
  81. // Expiration is expressed in hours.
  82. private double m_MemoryExpiration = 0.016;
  83. private const double m_DefaultFileExpiration = 48;
  84. // Negative cache is in seconds
  85. private int m_negativeExpiration = 120;
  86. private TimeSpan m_FileExpiration = TimeSpan.FromHours(m_DefaultFileExpiration);
  87. private TimeSpan m_FileExpirationCleanupTimer = TimeSpan.FromHours(1.0);
  88. private static int m_CacheDirectoryTiers = 1;
  89. private static int m_CacheDirectoryTierLen = 3;
  90. private static int m_CacheWarnAt = 30000;
  91. private System.Timers.Timer m_CacheCleanTimer;
  92. private IAssetService m_AssetService;
  93. private readonly List<Scene> m_Scenes = new();
  94. private readonly object timerLock = new();
  95. private Dictionary<string,WeakReference> weakAssetReferences = new();
  96. private readonly object weakAssetReferencesLock = new();
  97. private static bool m_updateFileTimeOnCacheHit = false;
  98. private static ExpiringKey<string> m_lastFileAccessTimeChange = null;
  99. public FlotsamAssetCache()
  100. {
  101. List<char> invalidChars = new();
  102. invalidChars.AddRange(Path.GetInvalidPathChars());
  103. invalidChars.AddRange(Path.GetInvalidFileNameChars());
  104. m_InvalidChars = invalidChars.ToArray();
  105. }
  106. public Type ReplaceableInterface
  107. {
  108. get { return null; }
  109. }
  110. public string Name
  111. {
  112. get { return m_ModuleName; }
  113. }
  114. public void Initialise(IConfigSource source)
  115. {
  116. IConfig moduleConfig = source.Configs["Modules"];
  117. if (moduleConfig is not null)
  118. {
  119. string name = moduleConfig.GetString("AssetCaching", string.Empty);
  120. if (name == Name)
  121. {
  122. m_negativeCache = new ExpiringKey<string>(2000);
  123. m_Enabled = true;
  124. m_log.Info($"[FLOTSAM ASSET CACHE]: {this.Name} enabled");
  125. IConfig assetConfig = source.Configs["AssetCache"];
  126. if (assetConfig is null)
  127. {
  128. m_log.Debug(
  129. "[FLOTSAM ASSET CACHE]: AssetCache section missing from config (not copied config-include/FlotsamCache.ini.example? Using defaults.");
  130. }
  131. else
  132. {
  133. m_FileCacheEnabled = assetConfig.GetBoolean("FileCacheEnabled", m_FileCacheEnabled);
  134. m_CacheDirectory = assetConfig.GetString("CacheDirectory", m_CacheDirectory);
  135. m_CacheDirectory = Path.GetFullPath(m_CacheDirectory);
  136. m_MemoryCacheEnabled = assetConfig.GetBoolean("MemoryCacheEnabled", m_MemoryCacheEnabled);
  137. m_MemoryExpiration = assetConfig.GetDouble("MemoryCacheTimeout", m_MemoryExpiration);
  138. m_MemoryExpiration *= 3600.0; // config in hours to seconds
  139. m_negativeCacheEnabled = assetConfig.GetBoolean("NegativeCacheEnabled", m_negativeCacheEnabled);
  140. m_negativeExpiration = assetConfig.GetInt("NegativeCacheTimeout", m_negativeExpiration);
  141. m_updateFileTimeOnCacheHit = assetConfig.GetBoolean("UpdateFileTimeOnCacheHit", m_updateFileTimeOnCacheHit);
  142. m_updateFileTimeOnCacheHit &= m_FileCacheEnabled;
  143. m_LogLevel = assetConfig.GetInt("LogLevel", m_LogLevel);
  144. m_HitRateDisplay = (ulong)assetConfig.GetLong("HitRateDisplay", (long)m_HitRateDisplay);
  145. m_FileExpiration = TimeSpan.FromHours(assetConfig.GetDouble("FileCacheTimeout", m_DefaultFileExpiration));
  146. m_FileExpirationCleanupTimer = TimeSpan.FromHours(
  147. assetConfig.GetDouble("FileCleanupTimer", m_FileExpirationCleanupTimer.TotalHours));
  148. m_CacheDirectoryTiers = assetConfig.GetInt("CacheDirectoryTiers", m_CacheDirectoryTiers);
  149. m_CacheDirectoryTierLen = assetConfig.GetInt("CacheDirectoryTierLength", m_CacheDirectoryTierLen);
  150. m_CacheWarnAt = assetConfig.GetInt("CacheWarnAt", m_CacheWarnAt);
  151. }
  152. if(m_updateFileTimeOnCacheHit)
  153. m_lastFileAccessTimeChange = new ExpiringKey<string>(300000);
  154. if(m_MemoryCacheEnabled)
  155. m_MemoryCache = new ExpiringCacheOS<string, AssetBase>((int)m_MemoryExpiration * 500);
  156. m_log.Info($"[FLOTSAM ASSET CACHE]: Cache Directory {m_CacheDirectory}");
  157. if (m_CacheDirectoryTiers < 1)
  158. m_CacheDirectoryTiers = 1;
  159. else if (m_CacheDirectoryTiers > 3)
  160. m_CacheDirectoryTiers = 3;
  161. if (m_CacheDirectoryTierLen < 1)
  162. m_CacheDirectoryTierLen = 1;
  163. else if (m_CacheDirectoryTierLen > 4)
  164. m_CacheDirectoryTierLen = 4;
  165. m_negativeExpiration *= 1000;
  166. assetConfig = source.Configs["AssetService"];
  167. if(assetConfig is not null)
  168. {
  169. m_assetLoader = assetConfig.GetString("DefaultAssetLoader", string.Empty);
  170. m_assetLoaderArgs = assetConfig.GetString("AssetLoaderArgs", string.Empty);
  171. if (string.IsNullOrWhiteSpace(m_assetLoaderArgs))
  172. m_assetLoader = string.Empty;
  173. }
  174. MainConsole.Instance.Commands.AddCommand("Assets", true, "fcache status", "fcache status", "Display cache status", HandleConsoleCommand);
  175. 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);
  176. MainConsole.Instance.Commands.AddCommand("Assets", true, "fcache assets", "fcache assets", "Attempt a deep scan and cache of all assets in all scenes", HandleConsoleCommand);
  177. MainConsole.Instance.Commands.AddCommand("Assets", true, "fcache expire", "fcache expire <datetime(mm/dd/YYYY)>", "Purge cached assets older than the specified date/time", HandleConsoleCommand);
  178. if (!string.IsNullOrWhiteSpace(m_assetLoader))
  179. {
  180. MainConsole.Instance.Commands.AddCommand("Assets", true, "fcache cachedefaultassets", "fcache cachedefaultassets", "loads local default assets to cache. This may override grid ones. use with care", HandleConsoleCommand);
  181. MainConsole.Instance.Commands.AddCommand("Assets", true, "fcache deletedefaultassets", "fcache deletedefaultassets", "deletes default local assets from cache so they can be refreshed from grid. use with care", HandleConsoleCommand);
  182. }
  183. }
  184. }
  185. }
  186. public void PostInitialise()
  187. {
  188. }
  189. public void Close()
  190. {
  191. if(m_Scenes.Count <= 0)
  192. {
  193. lock (timerLock)
  194. {
  195. m_cleanupRunning = false;
  196. if (m_timerRunning)
  197. {
  198. m_timerRunning = false;
  199. m_CacheCleanTimer.Stop();
  200. m_CacheCleanTimer.Close();
  201. }
  202. if (m_assetFileWriteWorker != null)
  203. {
  204. m_assetFileWriteWorker.Dispose();
  205. m_assetFileWriteWorker = null;
  206. }
  207. }
  208. }
  209. }
  210. public void AddRegion(Scene scene)
  211. {
  212. if (m_Enabled)
  213. {
  214. scene.RegisterModuleInterface<IAssetCache>(this);
  215. m_Scenes.Add(scene);
  216. }
  217. }
  218. public void RemoveRegion(Scene scene)
  219. {
  220. if (m_Enabled)
  221. {
  222. scene.UnregisterModuleInterface<IAssetCache>(this);
  223. m_Scenes.Remove(scene);
  224. lock(timerLock)
  225. {
  226. if(m_Scenes.Count <= 0)
  227. {
  228. m_cleanupRunning = false;
  229. if (m_timerRunning)
  230. {
  231. m_timerRunning = false;
  232. m_CacheCleanTimer.Stop();
  233. m_CacheCleanTimer.Close();
  234. }
  235. if (m_assetFileWriteWorker != null)
  236. {
  237. m_assetFileWriteWorker.Dispose();
  238. m_assetFileWriteWorker = null;
  239. }
  240. }
  241. }
  242. }
  243. }
  244. public void RegionLoaded(Scene scene)
  245. {
  246. if (m_Enabled)
  247. {
  248. m_AssetService ??= scene.RequestModuleInterface<IAssetService>();
  249. lock(timerLock)
  250. {
  251. if(!m_timerRunning)
  252. {
  253. if (m_FileCacheEnabled && (m_FileExpiration > TimeSpan.Zero) && (m_FileExpirationCleanupTimer > TimeSpan.Zero))
  254. {
  255. m_CacheCleanTimer = new System.Timers.Timer(m_FileExpirationCleanupTimer.TotalMilliseconds)
  256. {
  257. AutoReset = false
  258. };
  259. m_CacheCleanTimer.Elapsed += CleanupExpiredFiles;
  260. m_CacheCleanTimer.Start();
  261. m_timerRunning = true;
  262. }
  263. }
  264. if (m_FileCacheEnabled && m_assetFileWriteWorker == null)
  265. {
  266. m_assetFileWriteWorker = new ObjectJobEngine(ProcessWrites, "FloatsamCacheWriter", 1000, 1);
  267. }
  268. if (!string.IsNullOrWhiteSpace(m_assetLoader) && scene.RegionInfo.RegionID == m_Scenes[0].RegionInfo.RegionID)
  269. {
  270. IAssetLoader assetLoader = ServerUtils.LoadPlugin<IAssetLoader>(m_assetLoader, Array.Empty<object>());
  271. if (assetLoader is not null)
  272. {
  273. HashSet<string> ids = new();
  274. assetLoader.ForEachDefaultXmlAsset(
  275. m_assetLoaderArgs,
  276. delegate (AssetBase a)
  277. {
  278. Cache(a, true);
  279. ids.Add(a.ID);
  280. });
  281. m_defaultAssets = ids;
  282. }
  283. }
  284. }
  285. }
  286. }
  287. private void ProcessWrites(object o)
  288. {
  289. try
  290. {
  291. WriteAssetInfo wai = (WriteAssetInfo)o;
  292. WriteFileCache(wai.filename, wai.asset, wai.replace);
  293. wai.asset = null;
  294. Thread.Yield();
  295. }
  296. catch { }
  297. }
  298. ////////////////////////////////////////////////////////////
  299. // IAssetCache
  300. //
  301. private void UpdateWeakReference(string key, AssetBase asset)
  302. {
  303. lock(weakAssetReferencesLock)
  304. {
  305. ref WeakReference aref = ref CollectionsMarshal.GetValueRefOrAddDefault(weakAssetReferences, key, out bool ex);
  306. if(ex)
  307. aref.Target = asset;
  308. else
  309. aref = new WeakReference(asset);
  310. }
  311. }
  312. private void UpdateMemoryCache(string key, AssetBase asset)
  313. {
  314. m_MemoryCache.AddOrUpdate(key, asset, m_MemoryExpiration);
  315. }
  316. private void UpdateFileCache(string key, AssetBase asset, bool replace = false)
  317. {
  318. if(m_assetFileWriteWorker is null)
  319. return;
  320. string filename = GetFileName(key);
  321. try
  322. {
  323. // Once we start writing, make sure we flag that we're writing
  324. // that object to the cache so that we don't try to write the
  325. // same file multiple times.
  326. lock (m_CurrentlyWriting)
  327. {
  328. if (!m_CurrentlyWriting.Add(filename))
  329. return;
  330. }
  331. if (m_assetFileWriteWorker is not null)
  332. {
  333. WriteAssetInfo wai = new()
  334. {
  335. filename = filename,
  336. asset = asset,
  337. replace = replace
  338. };
  339. m_assetFileWriteWorker.Enqueue(wai);
  340. }
  341. }
  342. catch (Exception e)
  343. {
  344. m_log.Warn($"[FLOTSAM ASSET CACHE]: Failed to update cache for asset {asset.ID}: {e.Message}");
  345. }
  346. }
  347. public void Cache(AssetBase asset, bool replace = false)
  348. {
  349. if (asset is not null)
  350. {
  351. //m_log.DebugFormat("[FLOTSAM ASSET CACHE]: Caching asset with id {0}", asset.ID);
  352. UpdateWeakReference(asset.ID, asset);
  353. if (m_MemoryCacheEnabled)
  354. UpdateMemoryCache(asset.ID, asset);
  355. if (m_FileCacheEnabled)
  356. UpdateFileCache(asset.ID, asset, replace);
  357. if (m_negativeCacheEnabled)
  358. m_negativeCache.Remove(asset.ID);
  359. }
  360. }
  361. public void CacheNegative(string id)
  362. {
  363. if (m_negativeCacheEnabled)
  364. m_negativeCache.Add(id, m_negativeExpiration);
  365. }
  366. /// <summary>
  367. /// Updates the cached file with the current time.
  368. /// </summary>
  369. /// <param name="filename">Filename.</param>
  370. /// <returns><c>true</c>, if the update was successful, false otherwise.</returns>
  371. private static bool CheckUpdateFileLastAccessTime(string filename)
  372. {
  373. try
  374. {
  375. File.SetLastAccessTime(filename, DateTime.Now);
  376. m_lastFileAccessTimeChange?.Add(filename, 900000);
  377. return true;
  378. }
  379. catch (FileNotFoundException)
  380. {
  381. return false;
  382. }
  383. catch
  384. {
  385. return true; // ignore other errors
  386. }
  387. }
  388. private static void UpdateFileLastAccessTime(string filename)
  389. {
  390. try
  391. {
  392. if(!m_lastFileAccessTimeChange.ContainsKey(filename))
  393. {
  394. File.SetLastAccessTime(filename, DateTime.Now);
  395. m_lastFileAccessTimeChange.Add(filename, 900000);
  396. }
  397. }
  398. catch
  399. {
  400. }
  401. }
  402. private AssetBase GetFromWeakReference(string id)
  403. {
  404. lock(weakAssetReferencesLock)
  405. {
  406. if (weakAssetReferences.TryGetValue(id, out WeakReference aref))
  407. {
  408. if (aref.Target is AssetBase asset)
  409. {
  410. m_weakRefHits++;
  411. return asset;
  412. }
  413. }
  414. }
  415. return null;
  416. }
  417. /// <summary>
  418. /// Try to get an asset from the in-memory cache.
  419. /// </summary>
  420. /// <param name="id"></param>
  421. /// <returns></returns>
  422. private AssetBase GetFromMemoryCache(string id)
  423. {
  424. if (m_MemoryCache.TryGetValue(id, out AssetBase asset))
  425. {
  426. m_MemoryHits++;
  427. return asset;
  428. }
  429. return null;
  430. }
  431. private bool CheckFromMemoryCache(string id)
  432. {
  433. return m_MemoryCache.Contains(id);
  434. }
  435. /// <summary>
  436. /// Try to get an asset from the file cache.
  437. /// </summary>
  438. /// <param name="id"></param>
  439. /// <returns>An asset retrieved from the file cache. null if there was a problem retrieving an asset.</returns>
  440. private AssetBase GetFromFileCache(string id)
  441. {
  442. string filename = GetFileName(id);
  443. // Track how often we have the problem that an asset is requested while
  444. // it is still being downloaded by a previous request.
  445. if (m_CurrentlyWriting.Contains(filename))
  446. {
  447. m_RequestsForInprogress++;
  448. return null;
  449. }
  450. AssetBase asset = null;
  451. try
  452. {
  453. using FileStream stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read);
  454. if (stream.Length == 0) // Empty file will trigger exception below
  455. return null;
  456. BinaryFormatter bformatter = new();
  457. asset = (AssetBase)bformatter.Deserialize(stream);
  458. m_DiskHits++;
  459. }
  460. catch (FileNotFoundException)
  461. {
  462. }
  463. catch (DirectoryNotFoundException)
  464. {
  465. }
  466. catch (System.Runtime.Serialization.SerializationException e)
  467. {
  468. m_log.Warn($"[FLOTSAM ASSET CACHE]: Failed to get file {filename} for asset {id}: {e.Message}");
  469. // If there was a problem deserializing the asset, the asset may
  470. // either be corrupted OR was serialized under an old format
  471. // {different version of AssetBase} -- we should attempt to
  472. // delete it and re-cache
  473. File.Delete(filename);
  474. }
  475. catch (Exception e)
  476. {
  477. m_log.Warn($"[FLOTSAM ASSET CACHE]: Failed to get file {filename} for asset {id}: {e.Message}");
  478. }
  479. return asset;
  480. }
  481. private bool CheckFromFileCache(string id)
  482. {
  483. try
  484. {
  485. return File.Exists(GetFileName(id));
  486. }
  487. catch
  488. {
  489. }
  490. return false;
  491. }
  492. // For IAssetService
  493. public AssetBase Get(string id)
  494. {
  495. Get(id, out AssetBase asset);
  496. return asset;
  497. }
  498. public AssetBase Get(string id, string ForeignAssetService, bool dummy)
  499. {
  500. return null;
  501. }
  502. public bool Get(string id, out AssetBase asset)
  503. {
  504. asset = null;
  505. m_Requests++;
  506. if (id.Equals(UUID.ZeroString))
  507. return false;
  508. if (m_negativeCache.ContainsKey(id))
  509. return false;
  510. asset = GetFromWeakReference(id);
  511. if (asset is not null)
  512. {
  513. if(m_updateFileTimeOnCacheHit)
  514. UpdateFileLastAccessTime(GetFileName(id));
  515. if (m_MemoryCacheEnabled)
  516. UpdateMemoryCache(id, asset);
  517. return true;
  518. }
  519. if (m_MemoryCacheEnabled)
  520. {
  521. asset = GetFromMemoryCache(id);
  522. if(asset is not null)
  523. {
  524. UpdateWeakReference(id,asset);
  525. if (m_updateFileTimeOnCacheHit)
  526. UpdateFileLastAccessTime(GetFileName(id));
  527. return true;
  528. }
  529. }
  530. if (m_FileCacheEnabled)
  531. {
  532. asset = GetFromFileCache(id);
  533. if(asset is not null)
  534. {
  535. UpdateWeakReference(id,asset);
  536. if (m_MemoryCacheEnabled)
  537. UpdateMemoryCache(id, asset);
  538. }
  539. }
  540. return true;
  541. }
  542. public bool GetFromMemory(string id, out AssetBase asset)
  543. {
  544. asset = null;
  545. m_Requests++;
  546. if (id.Equals(Util.UUIDZeroString))
  547. return false;
  548. if (m_negativeCache.ContainsKey(id))
  549. return false;
  550. asset = GetFromWeakReference(id);
  551. if (asset != null)
  552. {
  553. if (m_updateFileTimeOnCacheHit)
  554. {
  555. string filename = GetFileName(id);
  556. UpdateFileLastAccessTime(filename);
  557. }
  558. if (m_MemoryCacheEnabled)
  559. UpdateMemoryCache(id, asset);
  560. return true;
  561. }
  562. if (m_MemoryCacheEnabled)
  563. {
  564. asset = GetFromMemoryCache(id);
  565. if (asset != null)
  566. {
  567. UpdateWeakReference(id, asset);
  568. if (m_updateFileTimeOnCacheHit)
  569. {
  570. string filename = GetFileName(id);
  571. UpdateFileLastAccessTime(filename);
  572. }
  573. return true;
  574. }
  575. }
  576. return true;
  577. }
  578. public bool Check(string id)
  579. {
  580. if(GetFromWeakReference(id) is not null)
  581. return true;
  582. if (m_MemoryCacheEnabled && CheckFromMemoryCache(id))
  583. return true;
  584. if (m_FileCacheEnabled && CheckFromFileCache(id))
  585. return true;
  586. return false;
  587. }
  588. // does not check negative cache
  589. public AssetBase GetCached(string id)
  590. {
  591. m_Requests++;
  592. AssetBase asset = GetFromWeakReference(id);
  593. if (asset is not null)
  594. {
  595. if (m_updateFileTimeOnCacheHit)
  596. UpdateFileLastAccessTime(GetFileName(id));
  597. if (m_MemoryCacheEnabled)
  598. UpdateMemoryCache(id, asset);
  599. return asset;
  600. }
  601. if (m_MemoryCacheEnabled)
  602. {
  603. asset = GetFromMemoryCache(id);
  604. if (asset is not null)
  605. {
  606. UpdateWeakReference(id, asset);
  607. if (m_updateFileTimeOnCacheHit)
  608. UpdateFileLastAccessTime(GetFileName(id));
  609. return asset;
  610. }
  611. }
  612. if (m_FileCacheEnabled)
  613. {
  614. asset = GetFromFileCache(id);
  615. if (asset is not null)
  616. {
  617. UpdateWeakReference(id, asset);
  618. if (m_MemoryCacheEnabled)
  619. UpdateMemoryCache(id, asset);
  620. }
  621. }
  622. return asset;
  623. }
  624. public void Expire(string id)
  625. {
  626. if (m_LogLevel >= 2)
  627. m_log.Debug($"[FLOTSAM ASSET CACHE]: Expiring Asset {id}");
  628. try
  629. {
  630. lock (weakAssetReferencesLock)
  631. weakAssetReferences.Remove(id);
  632. if (m_MemoryCacheEnabled)
  633. m_MemoryCache.Remove(id);
  634. if (m_negativeCacheEnabled)
  635. m_negativeCache.Remove(id);
  636. if (m_FileCacheEnabled)
  637. File.Delete(GetFileName(id));
  638. }
  639. catch (Exception e)
  640. {
  641. if (m_LogLevel >= 2)
  642. m_log.Warn($"[FLOTSAM ASSET CACHE]: Failed to expire cached file {id}: {e.Message}");
  643. }
  644. }
  645. public void Clear()
  646. {
  647. if (m_LogLevel >= 2)
  648. m_log.Debug("[FLOTSAM ASSET CACHE]: Clearing caches.");
  649. if (m_FileCacheEnabled && Directory.Exists(m_CacheDirectory))
  650. {
  651. foreach (string dir in Directory.GetDirectories(m_CacheDirectory))
  652. {
  653. try
  654. {
  655. Directory.Delete(dir, true);
  656. }
  657. catch { }
  658. }
  659. }
  660. if (m_MemoryCacheEnabled)
  661. {
  662. m_MemoryCache.Dispose();
  663. m_MemoryCache = new ExpiringCacheOS<string, AssetBase>((int)m_MemoryExpiration * 500);
  664. }
  665. if (m_negativeCacheEnabled)
  666. {
  667. m_negativeCache.Dispose();
  668. m_negativeCache = new ExpiringKey<string>(2000);
  669. }
  670. lock (weakAssetReferencesLock)
  671. weakAssetReferences = new Dictionary<string, WeakReference>();
  672. }
  673. private void CleanupExpiredFiles(object source, ElapsedEventArgs e)
  674. {
  675. lock (timerLock)
  676. {
  677. if (!m_timerRunning || m_cleanupRunning || !Directory.Exists(m_CacheDirectory))
  678. return;
  679. m_cleanupRunning = true;
  680. }
  681. // Purge all files last accessed prior to this point
  682. DoCleanExpiredFiles(DateTime.Now - m_FileExpiration);
  683. }
  684. private void DoCleanExpiredFiles(DateTime purgeLine)
  685. {
  686. //long heap = 0;
  687. //if (m_LogLevel >= 2)
  688. //{
  689. m_log.Info($"[FLOTSAM ASSET CACHE]: Start background expiring files older than {purgeLine}");
  690. long heap = GC.GetTotalMemory(false);
  691. //}
  692. // An asset cache may contain local non-temporary assets that are not in the asset service. Therefore,
  693. // before cleaning up expired files we must scan the objects in the scene to make sure that we retain
  694. // such local assets if they have not been recently accessed.
  695. Dictionary<UUID,sbyte> gids = GatherSceneAssets();
  696. int cooldown = 0;
  697. m_log.Info("[FLOTSAM ASSET CACHE] start asset files expire");
  698. foreach (string subdir in Directory.GetDirectories(m_CacheDirectory))
  699. {
  700. if(!m_cleanupRunning)
  701. break;
  702. cooldown = CleanExpiredFiles(subdir, gids, purgeLine, cooldown);
  703. if (++cooldown >= 10)
  704. {
  705. Thread.Sleep(120);
  706. cooldown = 0;
  707. }
  708. }
  709. lock (weakAssetReferencesLock)
  710. {
  711. weakAssetReferences = new Dictionary<string, WeakReference>();
  712. m_weakRefHits=0;
  713. }
  714. lock (timerLock)
  715. {
  716. if (m_timerRunning)
  717. m_CacheCleanTimer.Start();
  718. m_cleanupRunning = false;
  719. }
  720. //if (m_LogLevel >= 2)
  721. {
  722. heap = GC.GetTotalMemory(false) - heap;
  723. double fheap = Math.Round((double)(heap / (1024 * 1024)), 3);
  724. m_log.Info($"[FLOTSAM ASSET CACHE]: Finished expiring files, heap delta: {fheap}MB.");
  725. }
  726. }
  727. /// <summary>
  728. /// Recurses through specified directory checking for asset files last
  729. /// accessed prior to the specified purge line and deletes them. Also
  730. /// removes empty tier directories.
  731. /// </summary>
  732. /// <param name="dir"></param>
  733. /// <param name="purgeTimeline"></param>
  734. private int CleanExpiredFiles(string dir, Dictionary<UUID, sbyte> gids, DateTime purgeTimeline, int cooldown)
  735. {
  736. try
  737. {
  738. if (!m_cleanupRunning)
  739. return cooldown;
  740. int dirSize = 0;
  741. // Recurse into lower tiers
  742. foreach (string subdir in Directory.GetDirectories(dir))
  743. {
  744. if (!m_cleanupRunning)
  745. return cooldown;
  746. ++dirSize;
  747. cooldown = CleanExpiredFiles(subdir, gids, purgeTimeline, cooldown);
  748. if (++cooldown > 10)
  749. {
  750. Thread.Sleep(60);
  751. cooldown = 0;
  752. }
  753. }
  754. foreach (string file in Directory.GetFiles(dir))
  755. {
  756. if (!m_cleanupRunning)
  757. return cooldown;
  758. ++dirSize;
  759. string id = Path.GetFileName(file);
  760. if (string.IsNullOrEmpty(id))
  761. continue; //??
  762. if (m_defaultAssets.Contains(id) || (UUID.TryParse(id, out UUID uid) && gids.ContainsKey(uid)))
  763. {
  764. ++cooldown;
  765. continue;
  766. }
  767. if (File.GetLastAccessTime(file) < purgeTimeline)
  768. {
  769. try
  770. {
  771. File.Delete(file);
  772. lock (weakAssetReferencesLock)
  773. weakAssetReferences.Remove(id);
  774. }
  775. catch { }
  776. cooldown += 5;
  777. --dirSize;
  778. }
  779. if (++cooldown >= 20)
  780. {
  781. Thread.Sleep(60);
  782. cooldown = 0;
  783. }
  784. }
  785. // Check if a tier directory is empty, if so, delete it
  786. if (m_cleanupRunning && dirSize == 0)
  787. {
  788. try
  789. {
  790. Directory.Delete(dir);
  791. }
  792. catch { }
  793. cooldown += 5;
  794. if (cooldown >= 20)
  795. {
  796. Thread.Sleep(60);
  797. cooldown = 0;
  798. }
  799. }
  800. else if (dirSize >= m_CacheWarnAt)
  801. {
  802. m_log.Warn(
  803. $"[FLOTSAM ASSET CACHE]: Cache folder exceeded CacheWarnAt limit {dir} {dirSize}. Suggest increasing tiers, tier length, or reducing cache expiration");
  804. }
  805. }
  806. catch (DirectoryNotFoundException)
  807. {
  808. // If we get here, another node on the same box has
  809. // already removed the directory. Continue with next.
  810. }
  811. catch (Exception e)
  812. {
  813. m_log.Warn($"[FLOTSAM ASSET CACHE]: Could not complete clean of expired files in {dir}: {e.Message}");
  814. }
  815. return cooldown;
  816. }
  817. /// <summary>
  818. /// Determines the filename for an AssetID stored in the file cache
  819. /// </summary>
  820. /// <param name="id"></param>
  821. /// <returns></returns>
  822. private string GetFileName(string id)
  823. {
  824. StringBuilder sb = osStringBuilderCache.Acquire();
  825. int indx = id.IndexOfAny(m_InvalidChars);
  826. if (indx >= 0)
  827. {
  828. sb.Append(id);
  829. int sublen = id.Length - indx;
  830. for(int i = 0; i < m_InvalidChars.Length; ++i)
  831. {
  832. sb.Replace(m_InvalidChars[i], '_', indx, sublen);
  833. }
  834. id = sb.ToString();
  835. sb.Clear();
  836. }
  837. if(m_CacheDirectoryTiers == 1)
  838. {
  839. sb.Append(id.AsSpan(0, m_CacheDirectoryTierLen));
  840. sb.Append(Path.DirectorySeparatorChar);
  841. }
  842. else
  843. {
  844. for (int p = 0; p < m_CacheDirectoryTiers * m_CacheDirectoryTierLen; p += m_CacheDirectoryTierLen)
  845. {
  846. sb.Append(id.AsSpan(p, m_CacheDirectoryTierLen));
  847. sb.Append(Path.DirectorySeparatorChar);
  848. }
  849. }
  850. sb.Append(id);
  851. return Path.Combine(m_CacheDirectory, osStringBuilderCache.GetStringAndRelease(sb));
  852. }
  853. /// <summary>
  854. /// Writes a file to the file cache, creating any necessary
  855. /// tier directories along the way
  856. /// </summary>
  857. /// <param name="filename"></param>
  858. /// <param name="asset"></param>
  859. private static void WriteFileCache(string filename, AssetBase asset, bool replace)
  860. {
  861. try
  862. {
  863. // If the file is already cached, don't cache it, just touch it so access time is updated
  864. if (!replace && File.Exists(filename))
  865. {
  866. if (m_updateFileTimeOnCacheHit)
  867. UpdateFileLastAccessTime(filename);
  868. return;
  869. }
  870. string directory = Path.GetDirectoryName(filename);
  871. string tempname = Path.Combine(directory, Path.GetRandomFileName());
  872. try
  873. {
  874. if (!Directory.Exists(directory))
  875. {
  876. Directory.CreateDirectory(directory);
  877. }
  878. using (Stream stream = File.Open(tempname, FileMode.Create))
  879. {
  880. BinaryFormatter bformatter = new();
  881. bformatter.Serialize(stream, asset);
  882. stream.Flush();
  883. }
  884. m_lastFileAccessTimeChange?.Add(filename, 900000);
  885. }
  886. catch (IOException e)
  887. {
  888. m_log.Warn(
  889. $"[FLOTSAM ASSET CACHE]: Failed to write asset {asset.ID} to temporary location {tempname} (final {filename}) on cache in {directory}: {e.Message}");
  890. return;
  891. }
  892. catch (UnauthorizedAccessException)
  893. {
  894. }
  895. try
  896. {
  897. if(replace)
  898. File.Delete(filename);
  899. File.Move(tempname, filename);
  900. }
  901. catch
  902. {
  903. try
  904. {
  905. File.Delete(tempname);
  906. }
  907. catch{ }
  908. // If we see an IOException here it's likely that some other competing thread has written the
  909. // cache file first, so ignore. Other IOException errors (e.g. filesystem full) should be
  910. // signally by the earlier temporary file writing code.
  911. }
  912. }
  913. finally
  914. {
  915. // Even if the write fails with an exception, we need to make sure
  916. // that we release the lock on that file, otherwise it'll never get
  917. // cached
  918. lock (m_CurrentlyWriting)
  919. {
  920. m_CurrentlyWriting.Remove(filename);
  921. }
  922. }
  923. }
  924. /// <summary>
  925. /// Scan through the file cache, and return number of assets currently cached.
  926. /// </summary>
  927. /// <param name="dir"></param>
  928. /// <returns></returns>
  929. private int GetFileCacheCount(string dir)
  930. {
  931. try
  932. {
  933. int count = 0;
  934. int cooldown = 0;
  935. foreach (string subdir in Directory.GetDirectories(dir))
  936. {
  937. count += GetFileCacheCount(subdir);
  938. ++cooldown;
  939. if(cooldown > 50)
  940. {
  941. Thread.Sleep(100);
  942. cooldown = 0;
  943. }
  944. }
  945. return count + Directory.GetFiles(dir).Length;
  946. }
  947. catch
  948. {
  949. return 0;
  950. }
  951. }
  952. /// <summary>
  953. /// This notes the last time the Region had a deep asset scan performed on it.
  954. /// </summary>
  955. /// <param name="regionID"></param>
  956. private void StampRegionStatusFile(UUID regionID)
  957. {
  958. string RegionCacheStatusFile = Path.Combine(m_CacheDirectory, $"RegionStatus_{regionID}.fac");
  959. try
  960. {
  961. if (File.Exists(RegionCacheStatusFile))
  962. {
  963. File.SetLastWriteTime(RegionCacheStatusFile, DateTime.Now);
  964. }
  965. else
  966. {
  967. File.WriteAllText(
  968. RegionCacheStatusFile,
  969. "Please do not delete this file unless you are manually clearing your Flotsam Asset Cache.");
  970. }
  971. }
  972. catch (Exception e)
  973. {
  974. m_log.Warn($"[FLOTSAM ASSET CACHE]: Could not stamp region status file for region {regionID}: {e. Message}");
  975. }
  976. }
  977. /// <summary>
  978. /// Iterates through all Scenes, doing a deep scan through assets
  979. /// to update the access time of all assets present in the scene or referenced by assets
  980. /// in the scene.
  981. /// </summary>
  982. /// <param name="tryGetUncached">
  983. /// If true, then assets scanned which are not found in cache are added to the cache.
  984. /// </param>
  985. /// <returns>Number of distinct asset references found in the scene.</returns>
  986. private int TouchAllSceneAssets(bool tryGetUncached)
  987. {
  988. m_log.Info("[FLOTSAM ASSET CACHE] start touch files of assets in use");
  989. Dictionary<UUID,sbyte> gatheredids = GatherSceneAssets();
  990. int cooldown = 0;
  991. foreach(UUID id in gatheredids.Keys)
  992. {
  993. if (!m_cleanupRunning)
  994. break;
  995. string idstr = id.ToString();
  996. if (!CheckUpdateFileLastAccessTime(GetFileName(idstr)) && tryGetUncached)
  997. {
  998. cooldown += 5;
  999. m_AssetService.Get(idstr);
  1000. }
  1001. if (++cooldown > 50)
  1002. {
  1003. Thread.Sleep(50);
  1004. cooldown = 0;
  1005. }
  1006. }
  1007. return gatheredids.Count;
  1008. }
  1009. private Dictionary<UUID, sbyte> GatherSceneAssets()
  1010. {
  1011. m_log.Info("[FLOTSAM ASSET CACHE] gather assets in use");
  1012. Dictionary<UUID, sbyte> gatheredids = new();
  1013. UuidGatherer gatherer = new(m_AssetService, gatheredids);
  1014. int cooldown = 0;
  1015. foreach (Scene s in m_Scenes)
  1016. {
  1017. gatherer.AddGathered(s.RegionInfo.RegionSettings.TerrainTexture1, (sbyte)AssetType.Texture);
  1018. gatherer.AddGathered(s.RegionInfo.RegionSettings.TerrainTexture2, (sbyte)AssetType.Texture);
  1019. gatherer.AddGathered(s.RegionInfo.RegionSettings.TerrainTexture3, (sbyte)AssetType.Texture);
  1020. gatherer.AddGathered(s.RegionInfo.RegionSettings.TerrainTexture4, (sbyte)AssetType.Texture);
  1021. gatherer.AddGathered(s.RegionInfo.RegionSettings.TerrainPBR1, (sbyte)AssetType.Texture);
  1022. gatherer.AddGathered(s.RegionInfo.RegionSettings.TerrainPBR2, (sbyte)AssetType.Texture);
  1023. gatherer.AddGathered(s.RegionInfo.RegionSettings.TerrainPBR3, (sbyte)AssetType.Texture);
  1024. gatherer.AddGathered(s.RegionInfo.RegionSettings.TerrainPBR4, (sbyte)AssetType.Texture);
  1025. gatherer.AddGathered(s.RegionInfo.RegionSettings.TerrainImageID, (sbyte)AssetType.Texture);
  1026. s.RegionEnvironment?.GatherAssets(gatheredids);
  1027. if (s.LandChannel is not null)
  1028. {
  1029. List<ILandObject> landObjects = s.LandChannel.AllParcels();
  1030. foreach (ILandObject lo in landObjects)
  1031. {
  1032. if (lo.LandData is not null && lo.LandData.Environment is not null)
  1033. lo.LandData.Environment.GatherAssets(gatheredids);
  1034. }
  1035. }
  1036. EntityBase[] entities = s.Entities.GetEntities();
  1037. foreach (EntityBase entity in entities.AsSpan())
  1038. {
  1039. if (!m_cleanupRunning)
  1040. break;
  1041. if (entity is SceneObjectGroup sog)
  1042. {
  1043. if (sog.IsDeleted)
  1044. continue;
  1045. gatherer.AddForInspection(sog);
  1046. while (gatherer.GatherNext())
  1047. {
  1048. if (++cooldown > 50)
  1049. {
  1050. Thread.Sleep(60);
  1051. cooldown = 0;
  1052. }
  1053. }
  1054. if (++cooldown > 25)
  1055. {
  1056. Thread.Sleep(60);
  1057. cooldown = 0;
  1058. }
  1059. }
  1060. else if( entity is ScenePresence sp)
  1061. {
  1062. if (sp.IsChildAgent || sp.IsDeleted || sp.Appearance is null)
  1063. continue;
  1064. Primitive.TextureEntry Texture = sp.Appearance.Texture;
  1065. if (Texture is null)
  1066. continue;
  1067. Primitive.TextureEntryFace[] FaceTextures = Texture.FaceTextures;
  1068. if (FaceTextures is null)
  1069. continue;
  1070. for (int it = 0; it < AvatarAppearance.BAKE_INDICES.Length; it++)
  1071. {
  1072. int idx = AvatarAppearance.BAKE_INDICES[it];
  1073. if(idx < FaceTextures.Length)
  1074. {
  1075. Primitive.TextureEntryFace face = FaceTextures[idx];
  1076. if (face is null)
  1077. continue;
  1078. if (face.TextureID.IsZero() || face.TextureID.Equals(AppearanceManager.DEFAULT_AVATAR_TEXTURE))
  1079. continue;
  1080. gatherer.AddGathered(face.TextureID, (sbyte)AssetType.Texture);
  1081. }
  1082. }
  1083. }
  1084. }
  1085. entities = null;
  1086. if (!m_cleanupRunning)
  1087. break;
  1088. StampRegionStatusFile(s.RegionInfo.RegionID);
  1089. }
  1090. gatherer.GatherAll();
  1091. gatherer.FailedUUIDs.Clear();
  1092. gatherer.UncertainAssetsUUIDs.Clear();
  1093. m_log.Info($"[FLOTSAM ASSET CACHE] found {gatheredids.Count} possible assets in use)");
  1094. return gatheredids;
  1095. }
  1096. /// <summary>
  1097. /// Deletes all cache contents
  1098. /// </summary>
  1099. private void ClearFileCache()
  1100. {
  1101. if(!Directory.Exists(m_CacheDirectory))
  1102. return;
  1103. foreach (string dir in Directory.GetDirectories(m_CacheDirectory))
  1104. {
  1105. try
  1106. {
  1107. Directory.Delete(dir, true);
  1108. }
  1109. catch (Exception e)
  1110. {
  1111. m_log.Warn($"[FLOTSAM ASSET CACHE]: Couldn't clear asset cache directory {dir} from {m_CacheDirectory}: {e.Message}");
  1112. }
  1113. }
  1114. foreach (string file in Directory.GetFiles(m_CacheDirectory))
  1115. {
  1116. try
  1117. {
  1118. File.Delete(file);
  1119. }
  1120. catch (Exception e)
  1121. {
  1122. m_log.Warn($"[FLOTSAM ASSET CACHE]: Couldn't clear asset cache file {file} from {m_CacheDirectory}: {e.Message}");
  1123. }
  1124. }
  1125. }
  1126. private List<string> GenerateCacheHitReport()
  1127. {
  1128. List<string> outputLines = new();
  1129. double invReq = 100.0 / m_Requests;
  1130. double weakHitRate = m_weakRefHits * invReq;
  1131. int weakEntriesAlive = 0;
  1132. lock(weakAssetReferencesLock)
  1133. {
  1134. foreach(WeakReference aref in weakAssetReferences.Values)
  1135. {
  1136. if (aref.IsAlive)
  1137. ++weakEntriesAlive;
  1138. }
  1139. }
  1140. int weakEntries = weakAssetReferences.Count;
  1141. double fileHitRate = m_DiskHits * invReq;
  1142. double TotalHitRate = weakHitRate + fileHitRate;
  1143. outputLines.Add($"Total requests: {m_Requests}");
  1144. outputLines.Add($"unCollected Hit Rate: {weakHitRate:0.00}% ({weakEntries} entries {weakEntriesAlive} alive)");
  1145. outputLines.Add($"File Hit Rate: {fileHitRate:0.00}%");
  1146. if (m_MemoryCacheEnabled)
  1147. {
  1148. double HitRate = m_MemoryHits * invReq;
  1149. outputLines.Add($"Memory Hit Rate: {HitRate:0.00}%");
  1150. TotalHitRate += HitRate;
  1151. }
  1152. outputLines.Add($"Total Hit Rate: {TotalHitRate:0.00}%");
  1153. outputLines.Add($"Requests overlap during file writing: {m_RequestsForInprogress}");
  1154. return outputLines;
  1155. }
  1156. #region Console Commands
  1157. private void HandleConsoleCommand(string module, string[] cmdparams)
  1158. {
  1159. ICommandConsole con = MainConsole.Instance;
  1160. if (cmdparams.Length >= 2)
  1161. {
  1162. string cmd = cmdparams[1];
  1163. switch (cmd)
  1164. {
  1165. case "status":
  1166. {
  1167. WorkManager.RunInThreadPool(delegate
  1168. {
  1169. if (m_MemoryCacheEnabled)
  1170. con.Output("[FLOTSAM ASSET CACHE] Memory Cache: {0} assets", m_MemoryCache.Count);
  1171. else
  1172. con.Output("[FLOTSAM ASSET CACHE] Memory cache disabled");
  1173. if (m_FileCacheEnabled)
  1174. {
  1175. bool doingscan;
  1176. lock (timerLock)
  1177. {
  1178. doingscan = m_cleanupRunning;
  1179. }
  1180. if(doingscan)
  1181. {
  1182. con.Output("[FLOTSAM ASSET CACHE] a deep scan is in progress, skipping file cache assets count");
  1183. }
  1184. else
  1185. {
  1186. con.Output("[FLOTSAM ASSET CACHE] counting file cache assets");
  1187. int fileCount = GetFileCacheCount(m_CacheDirectory);
  1188. con.Output("[FLOTSAM ASSET CACHE] File Cache: {0} assets", fileCount);
  1189. }
  1190. }
  1191. else
  1192. {
  1193. con.Output("[FLOTSAM ASSET CACHE] File cache disabled");
  1194. }
  1195. GenerateCacheHitReport().ForEach(l => con.Output(l));
  1196. if (m_FileCacheEnabled)
  1197. {
  1198. con.Output("[FLOTSAM ASSET CACHE] Deep scans have previously been performed on the following regions:");
  1199. foreach (string s in Directory.GetFiles(m_CacheDirectory, "*.fac"))
  1200. {
  1201. int start = s.IndexOf('_');
  1202. int end = s.IndexOf('.');
  1203. if(start > 0 && end > 0)
  1204. {
  1205. string RegionID = s.Substring(start + 1, end - start);
  1206. DateTime RegionDeepScanTMStamp = File.GetLastWriteTime(s);
  1207. con.Output("[FLOTSAM ASSET CACHE] Region: {0}, {1}", RegionID, RegionDeepScanTMStamp.ToString("MM/dd/yyyy hh:mm:ss"));
  1208. }
  1209. }
  1210. }
  1211. }, null, "CacheStatus", false);
  1212. break;
  1213. }
  1214. case "clear":
  1215. if (cmdparams.Length < 2)
  1216. {
  1217. con.Output("Usage is fcache clear [file] [memory]");
  1218. break;
  1219. }
  1220. bool clearMemory = false, clearFile = false;
  1221. if (cmdparams.Length == 2)
  1222. {
  1223. clearMemory = true;
  1224. clearFile = true;
  1225. }
  1226. foreach (string s in cmdparams)
  1227. {
  1228. if (s.ToLower() == "memory")
  1229. clearMemory = true;
  1230. else if (s.ToLower() == "file")
  1231. clearFile = true;
  1232. }
  1233. if (clearMemory)
  1234. {
  1235. if (m_MemoryCacheEnabled)
  1236. {
  1237. m_MemoryCache.Clear();
  1238. con.Output("Memory cache cleared.");
  1239. }
  1240. else
  1241. {
  1242. con.Output("Memory cache not enabled.");
  1243. }
  1244. }
  1245. if (clearFile)
  1246. {
  1247. if (m_FileCacheEnabled)
  1248. {
  1249. ClearFileCache();
  1250. con.Output("File cache cleared.");
  1251. }
  1252. else
  1253. {
  1254. con.Output("File cache not enabled.");
  1255. }
  1256. }
  1257. break;
  1258. case "assets":
  1259. lock (timerLock)
  1260. {
  1261. if (m_cleanupRunning)
  1262. {
  1263. con.Output("Flotsam assets check already running");
  1264. return;
  1265. }
  1266. m_cleanupRunning = true;
  1267. }
  1268. con.Output("Flotsam Ensuring assets are cached for all scenes.");
  1269. WorkManager.RunInThreadPool(delegate
  1270. {
  1271. bool wasRunning= false;
  1272. lock(timerLock)
  1273. {
  1274. if(m_timerRunning)
  1275. {
  1276. m_CacheCleanTimer.Stop();
  1277. m_timerRunning = false;
  1278. wasRunning = true;
  1279. }
  1280. }
  1281. if (wasRunning)
  1282. Thread.Sleep(120);
  1283. int assetReferenceTotal = TouchAllSceneAssets(true);
  1284. lock(timerLock)
  1285. {
  1286. if(wasRunning)
  1287. {
  1288. m_CacheCleanTimer.Start();
  1289. m_timerRunning = true;
  1290. }
  1291. m_cleanupRunning = false;
  1292. }
  1293. con.Output("Completed check with {0} assets.", assetReferenceTotal);
  1294. }, null, "TouchAllSceneAssets", false);
  1295. break;
  1296. case "expire":
  1297. lock (timerLock)
  1298. {
  1299. if (m_cleanupRunning)
  1300. {
  1301. con.Output("Flotsam assets check already running");
  1302. return;
  1303. }
  1304. m_cleanupRunning = true;
  1305. }
  1306. if (cmdparams.Length < 3)
  1307. {
  1308. con.Output("Invalid parameters for Expire, please specify a valid date & time");
  1309. m_cleanupRunning = false;
  1310. break;
  1311. }
  1312. string s_expirationDate = "";
  1313. DateTime expirationDate;
  1314. if (cmdparams.Length > 3)
  1315. {
  1316. s_expirationDate = string.Join(" ", cmdparams, 2, cmdparams.Length - 2);
  1317. }
  1318. else
  1319. {
  1320. s_expirationDate = cmdparams[2];
  1321. }
  1322. if(s_expirationDate.Equals("now", StringComparison.InvariantCultureIgnoreCase))
  1323. expirationDate = DateTime.Now;
  1324. else
  1325. {
  1326. if (!DateTime.TryParse(s_expirationDate, out expirationDate))
  1327. {
  1328. con.Output("{0} is not a valid date & time", cmd);
  1329. m_cleanupRunning = false;
  1330. break;
  1331. }
  1332. if (expirationDate >= DateTime.Now)
  1333. {
  1334. con.Output("{0} date & time must be in past", cmd);
  1335. m_cleanupRunning = false;
  1336. break;
  1337. }
  1338. }
  1339. if (m_FileCacheEnabled)
  1340. {
  1341. WorkManager.RunInThreadPool(delegate
  1342. {
  1343. bool wasRunning = false;
  1344. lock (timerLock)
  1345. {
  1346. if (m_timerRunning)
  1347. {
  1348. m_CacheCleanTimer.Stop();
  1349. m_timerRunning = false;
  1350. wasRunning = true;
  1351. }
  1352. }
  1353. if(wasRunning)
  1354. Thread.Sleep(120);
  1355. DoCleanExpiredFiles(expirationDate);
  1356. lock (timerLock)
  1357. {
  1358. if (wasRunning)
  1359. {
  1360. m_CacheCleanTimer.Start();
  1361. m_timerRunning = true;
  1362. }
  1363. m_cleanupRunning = false;
  1364. }
  1365. }, null, "ExpireSceneAssets", false);
  1366. }
  1367. else
  1368. con.Output("File cache not active, not clearing.");
  1369. break;
  1370. case "cachedefaultassets":
  1371. HandleLoadDefaultAssets();
  1372. break;
  1373. case "deletedefaultassets":
  1374. HandleDeleteDefaultAssets();
  1375. break;
  1376. default:
  1377. con.Output("Unknown command {0}", cmd);
  1378. break;
  1379. }
  1380. }
  1381. else if (cmdparams.Length == 1)
  1382. {
  1383. con.Output("fcache assets - Attempt a deep cache of all assets in all scenes");
  1384. con.Output("fcache expire <datetime> - Purge assets older than the specified date & time");
  1385. con.Output("fcache clear [file] [memory] - Remove cached assets");
  1386. con.Output("fcache status - Display cache status");
  1387. con.Output("fcache cachedefaultassets - loads default assets to cache replacing existent ones, this may override grid assets. Use with care");
  1388. con.Output("fcache deletedefaultassets - deletes default local assets from cache so they can be refreshed from grid");
  1389. }
  1390. }
  1391. #endregion
  1392. #region IAssetService Members
  1393. public AssetMetadata GetMetadata(string id)
  1394. {
  1395. Get(id, out AssetBase asset);
  1396. if (asset == null)
  1397. return null;
  1398. return asset.Metadata;
  1399. }
  1400. public byte[] GetData(string id)
  1401. {
  1402. Get(id, out AssetBase asset);
  1403. if (asset == null)
  1404. return null;
  1405. return asset.Data;
  1406. }
  1407. public bool Get(string id, object sender, AssetRetrieved handler)
  1408. {
  1409. if (!Get(id, out AssetBase asset))
  1410. return false;
  1411. handler(id, sender, asset);
  1412. return true;
  1413. }
  1414. public void Get(string id, string ForeignAssetService, bool StoreOnLocalGrid, SimpleAssetRetrieved callBack)
  1415. {
  1416. }
  1417. public bool[] AssetsExist(string[] ids)
  1418. {
  1419. bool[] exist = new bool[ids.Length];
  1420. for (int i = 0; i < ids.Length; i++)
  1421. {
  1422. exist[i] = Check(ids[i]);
  1423. }
  1424. return exist;
  1425. }
  1426. public string Store(AssetBase asset)
  1427. {
  1428. if (asset.FullID.IsZero())
  1429. {
  1430. asset.FullID = UUID.Random();
  1431. }
  1432. Cache(asset);
  1433. return asset.ID;
  1434. }
  1435. public bool UpdateContent(string id, byte[] data)
  1436. {
  1437. if (!Get(id, out AssetBase asset))
  1438. return false;
  1439. asset.Data = data;
  1440. Cache(asset, true);
  1441. return true;
  1442. }
  1443. public bool Delete(string id)
  1444. {
  1445. Expire(id);
  1446. return true;
  1447. }
  1448. private void HandleLoadDefaultAssets()
  1449. {
  1450. if (string.IsNullOrWhiteSpace(m_assetLoader))
  1451. {
  1452. m_log.Info("[FLOTSAM ASSET CACHE] default assets loader not defined");
  1453. return;
  1454. }
  1455. IAssetLoader assetLoader = ServerUtils.LoadPlugin<IAssetLoader>(m_assetLoader, Array.Empty<object>());
  1456. if (assetLoader == null)
  1457. {
  1458. m_log.Info("[FLOTSAM ASSET CACHE] default assets loader not found");
  1459. return;
  1460. }
  1461. m_log.Info("[FLOTSAM ASSET CACHE] start loading local default assets");
  1462. int count = 0;
  1463. HashSet<string> ids = new();
  1464. assetLoader.ForEachDefaultXmlAsset(
  1465. m_assetLoaderArgs,
  1466. delegate (AssetBase a)
  1467. {
  1468. Cache(a, true);
  1469. ids.Add(a.ID);
  1470. ++count;
  1471. });
  1472. m_defaultAssets = ids;
  1473. m_log.Info($"[FLOTSAM ASSET CACHE] loaded {count} local default assets");
  1474. }
  1475. private void HandleDeleteDefaultAssets()
  1476. {
  1477. if (string.IsNullOrWhiteSpace(m_assetLoader))
  1478. {
  1479. m_log.Info("[FLOTSAM ASSET CACHE] default assets loader not defined");
  1480. return;
  1481. }
  1482. IAssetLoader assetLoader = ServerUtils.LoadPlugin<IAssetLoader>(m_assetLoader, Array.Empty<object>());
  1483. if (assetLoader is null)
  1484. {
  1485. m_log.Info("[FLOTSAM ASSET CACHE] default assets loader not found");
  1486. return;
  1487. }
  1488. m_log.Info("[FLOTSAM ASSET CACHE] started deleting local default assets");
  1489. int count = 0;
  1490. assetLoader.ForEachDefaultXmlAsset(
  1491. m_assetLoaderArgs,
  1492. delegate (AssetBase a)
  1493. {
  1494. Expire(a.ID);
  1495. ++count;
  1496. });
  1497. m_defaultAssets = new HashSet<string>();
  1498. m_log.Info($"[FLOTSAM ASSET CACHE] deleted {count} local default assets");
  1499. }
  1500. #endregion
  1501. }
  1502. }