Slightly less naive cache

The simple cache ihas a number of issues
* the "max cache size" is not the max cache size
* the purge typically only purges DOWN to the max cache size
* once you reach max you are basically purging constantly.
This change addresses that a bit by adding highwater threshold at 95% leaving a bit of headroom for the incoming cache updates while we purge, and a low water threshold, that we purge down to (70%) .
This means people can now set their cache to a ramdisk of they really want and it won't (normally) overflow.
it reduces the cache thrashing, this is not a speedup on the main thread really, but just a less naive process.
This does not address the fact that every time we check the cache it has to enumerate every single file (beyond stupid) even though most have not changed. This is TBD.
master
Beq 2023-06-27 19:12:58 +01:00
parent e9c4e1942b
commit de702e112a
6 changed files with 241 additions and 57 deletions

View File

@ -44,7 +44,12 @@ static const char* subdirs = "0123456789abcdef";
LLDiskCache::LLDiskCache(const std::string cache_dir,
const uintmax_t max_size_bytes,
const bool enable_cache_debug_info) :
const bool enable_cache_debug_info
// <FS:Beq> Add High/Low water mark support
,const F32 highwater_mark_percent
,const F32 lowwater_mark_percent
// </FS:Beq>
) :
mCacheDir(cache_dir),
mMaxSizeBytes(max_size_bytes),
mEnableCacheDebugInfo(enable_cache_debug_info)
@ -113,6 +118,8 @@ void LLDiskCache::purge()
#else
std::string cache_path(mCacheDir);
#endif
uintmax_t file_size_total = 0; // <FS:Beq/> try to make simple cache less naive.
if (boost::filesystem::is_directory(cache_path, ec) && !ec.failed())
{
// <FS:Ansariel> Optimize asset simple disk cache
@ -137,6 +144,7 @@ void LLDiskCache::purge()
{
continue;
}
file_size_total += file_size; // <FS:Beq/> try to make simple cache less naive.
file_info.push_back(file_info_t(file_time, { file_size, file_path }));
}
@ -145,52 +153,106 @@ void LLDiskCache::purge()
}
}
// <FS:Beq> add high water/low water thresholds to reduce the churn in the cache.
LL_DEBUGS("LLDiskCache") << "Cache is " << (int)(((F32)file_size_total)/mMaxSizeBytes*100.0) << "% full" << LL_ENDL;
if( file_size_total < mMaxSizeBytes * (mHighPercent/100) )
{
// Nothing to do here
LL_DEBUGS("LLDiskCache") << "Not exceded high water - do nothing" << LL_ENDL;
return;
}
// If we reach here we are above the trigger level so we must purge until we've removed enough to take us down to the low water mark.
// </FS:Beq>
std::sort(file_info.begin(), file_info.end(), [](file_info_t& x, file_info_t& y)
{
return x.first > y.first;
return x.first < y.first; // <FS:Beq/> sort oldest to newest, to we can remove the oldest files first.
});
LL_INFOS() << "Purging cache to a maximum of " << mMaxSizeBytes << " bytes" << LL_ENDL;
// <FS:Beq> add high water/low water thresholds to reduce the churn in the cache.
auto target_size = (uintmax_t)(mMaxSizeBytes * (mLowPercent/100));
LL_INFOS() << "Purging cache to a maximum of " << target_size << " bytes" << LL_ENDL;
// </FS:Beq>
// <FS:Beq> Extra accounting to track the retention of static assets
//std::vector<bool> file_removed;
std::vector<S32> file_removed;
int keep{0};
int del{0};
int skip{0};
enum class purge_action { delete_file=0, keep_file, skip_file };
std::map<std::string,purge_action> file_removed;
auto keep{file_info.size()};
auto del{0};
auto skip{0};
// </FS:Beq>
if (mEnableCacheDebugInfo)
{
file_removed.reserve(file_info.size());
}
uintmax_t file_size_total = 0;
// <FS:Beq> revised purge logic to track amount removed not retained to shortern loop
// uintmax_t file_size_total = 0;
// if (mEnableCacheDebugInfo)
// {
// file_removed.reserve(file_info.size());
// }
// uintmax_t file_size_total = 0;
// for (file_info_t& entry : file_info)
// {
// file_size_total += entry.second.first;
// bool should_remove = file_size_total > mMaxSizeBytes;
// // <FS> Make sure static assets are not eliminated
// S32 action{ should_remove ? 0 : 1 };
// if (should_remove)
// {
// auto uuid_as_string = gDirUtilp->getBaseFileName(entry.second.second, true);
// uuid_as_string = uuid_as_string.substr(mCacheFilenamePrefix.size() + 1, 36);// skip "sl_cache_" and trailing "_N"
// // LL_INFOS() << "checking UUID=" <<uuid_as_string<< LL_ENDL;
// if (std::find(mSkipList.begin(), mSkipList.end(), uuid_as_string) != mSkipList.end())
// {
// // this is one of our protected items so no purging
// should_remove = false;
// action = 2;
// updateFileAccessTime(entry.second.second); // force these to the front of the list next time so that purge size works
// }
// }
// // </FS>
// if (mEnableCacheDebugInfo)
// {
// // <FS> Static asset stuff
// //file_removed.push_back(should_remove);
// file_removed.push_back(action);
// }
uintmax_t deleted_size_total = 0;
for (file_info_t& entry : file_info)
{
file_size_total += entry.second.first;
// first check if we still need to delete more files
bool should_remove = (file_size_total - deleted_size_total) > target_size;
bool should_remove = file_size_total > mMaxSizeBytes;
// <FS> Make sure static assets are not eliminated
S32 action{ should_remove ? 0 : 1 };
if (should_remove)
auto action{ should_remove ? purge_action::delete_file : purge_action::keep_file };
if (!should_remove)
{
auto uuid_as_string = gDirUtilp->getBaseFileName(entry.second.second, true);
uuid_as_string = uuid_as_string.substr(mCacheFilenamePrefix.size() + 1, 36);// skip "sl_cache_" and trailing "_N"
// LL_INFOS() << "checking UUID=" <<uuid_as_string<< LL_ENDL;
if (std::find(mSkipList.begin(), mSkipList.end(), uuid_as_string) != mSkipList.end())
{
// this is one of our protected items so no purging
should_remove = false;
action = 2;
updateFileAccessTime(entry.second.second); // force these to the front of the list next time so that purge size works
}
break;
}
// </FS>
auto this_file_size = entry.second.first;
deleted_size_total += this_file_size;
auto uuid_as_string = gDirUtilp->getBaseFileName(entry.second.second, true);
uuid_as_string = uuid_as_string.substr(mCacheFilenamePrefix.size() + 1, 36);// skip "sl_cache_" and trailing "_N"
// LL_INFOS() << "checking UUID=" <<uuid_as_string<< LL_ENDL;
if (std::find(mSkipList.begin(), mSkipList.end(), uuid_as_string) != mSkipList.end())
{
// this is one of our protected items so no purging
should_remove = false;
action = purge_action::skip_file;
updateFileAccessTime(entry.second.second); // force these to the front of the list next time so that purge size works
skip++;
}
else{
del++;
}
keep--;
if (mEnableCacheDebugInfo)
{
// <FS> Static asset stuff
//file_removed.push_back(should_remove);
file_removed.push_back(action);
// <FS> Static asset stuff
//file_removed.push_back(should_remove);
file_removed.emplace(entry.second.second, action);
}
// </FS>
if (should_remove)
{
boost::filesystem::remove(entry.second.second, ec);
@ -200,37 +262,52 @@ void LLDiskCache::purge()
}
}
}
// <FS:Beq> update the debug logging to be more useful
auto end_time = std::chrono::high_resolution_clock::now();
auto execute_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
// </FS:Beq>
if (mEnableCacheDebugInfo)
{
auto end_time = std::chrono::high_resolution_clock::now();
auto execute_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
// <FS:Beq> update the debug logging to be more useful
// auto end_time = std::chrono::high_resolution_clock::now();
// auto execute_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
// </FS:Beq>
// Log afterward so it doesn't affect the time measurement
// Logging thousands of file results can take hundreds of milliseconds
auto deleted_so_far = 0; // <FS:Beq/> update the debug logging to be more useful
for (size_t i = 0; i < file_info.size(); ++i)
{
const file_info_t& entry = file_info[i];
// <FS> Static asset stuff
deleted_so_far += entry.second.first; // <FS:Beq/> update the debug logging to be more useful
//const bool removed = file_removed[i];
//const std::string action = removed ? "DELETE:" : "KEEP:";
std::string action{};
switch (file_removed[i])
{
default:
case 0:
action = "KEEP";
keep++;
break;
case 1:
// Check if the file exists in the map
auto& filename{ entry.second.second };
if (file_removed.find(filename) != file_removed.end()) {
// File found in the map, retrieve the corresponding enum value
switch (file_removed[filename]) {
case purge_action::delete_file:
action = "DELETE";
del++;
break;
case 2:
break;
case purge_action::skip_file:
action = "STATIC";
skip++;
break;
break;
default:
// Handle any unexpected enum value
action = "UNKNOWN";
break;
}
}
else
{
action = "KEEP";
}
// </FS>
// have to do this because of LL_INFO/LL_END weirdness
@ -240,14 +317,22 @@ void LLDiskCache::purge()
line << entry.first << " ";
line << entry.second.first << " ";
line << entry.second.second;
line << " (" << file_size_total << "/" << mMaxSizeBytes << ")";
line << " (" << file_size_total - deleted_so_far << "/" << mMaxSizeBytes << ")"; // <FS:Beq/> update the debug logging to be more useful
LL_INFOS() << line.str() << LL_ENDL;
}
LL_INFOS() << "Total dir size after purge is " << dirFileSize(mCacheDir) << LL_ENDL;
LL_INFOS() << "Cache purge took " << execute_time << " ms to execute for " << file_info.size() << " files" << LL_ENDL;
LL_INFOS() << "Deleted: " << del << " Skipped: " << skip << " Kept: " << keep << LL_ENDL; // <FS:Beq/> Extra accounting to track the retention of static assets
// <FS:Beq> make the summary stats more easily enabled.
}
// <FS:Beq> update the debug logging to be more useful
// LL_INFOS() << "Total dir size after purge is " << dirFileSize(mCacheDir) << LL_ENDL;
// LL_INFOS() << "Cache purge took " << execute_time << " ms to execute for " << file_info.size() << " files" << LL_ENDL;
auto newCacheSize = updateCacheSize(file_size_total - deleted_size_total);
LL_INFOS("LLDiskCache") << "Total dir size after purge is " << newCacheSize << LL_ENDL;
LL_INFOS("LLDiskCache") << "Cache purge took " << execute_time << " ms to execute for " << file_info.size() << " files" << LL_ENDL;
// </FS:Beq>
LL_INFOS("LLDiskCache") << "Deleted: " << del << " Skipped: " << skip << " Kept: " << keep << LL_ENDL; // <FS:Beq/> Extra accounting to track the retention of static assets
LL_INFOS("LLDiskCache") << "Total of " << deleted_size_total << " bytes removed." << LL_ENDL; // <FS:Beq/> Extra accounting to track the retention of static assets
// } <FS:Beq/> this bracket was moved up a few lines.
}
const std::string LLDiskCache::assetTypeToString(LLAssetType::EType at)
@ -534,8 +619,32 @@ void LLDiskCache::removeOldVFSFiles()
}
}
uintmax_t LLDiskCache::dirFileSize(const std::string& dir)
// <FS:Beq> Lets not scan every single time if we can avoid it eh?
// uintmax_t LLDiskCache::dirFileSize(const std::string& dir)
// {
uintmax_t LLDiskCache::updateCacheSize(const uintmax_t newsize)
{
mStoredCacheSize = newsize;
mLastScanTime = system_clock::now();
return mStoredCacheSize;
}
uintmax_t LLDiskCache::dirFileSize(const std::string& dir, bool force )
{
using namespace std::chrono;
const seconds cache_duration{ 120 };// A rather arbitrary number. it takes 5 seconds+ on a fast drive to scan 80K+ items. purge runs every minute and will update. so 120 should mean we never need a superfluous cache scan.
const auto current_time = system_clock::now();
const auto time_difference = duration_cast<seconds>(current_time - mLastScanTime);
// Check if the cached result can be used
if( !force && time_difference < cache_duration )
{
LL_DEBUGS("LLDiskCache") << "Using cached result: " << mStoredCacheSize << LL_ENDL;
return mStoredCacheSize;
}
// </FS:Beq>
uintmax_t total_file_size = 0;
/**
@ -577,7 +686,10 @@ uintmax_t LLDiskCache::dirFileSize(const std::string& dir)
}
}
return total_file_size;
// <FS:Beq> Lets not scan every single time if we can avoid it eh?
// return total_file_size;
return updateCacheSize(total_file_size);
// </FS:Beq>
}
LLPurgeDiskCacheThread::LLPurgeDiskCacheThread() :

View File

@ -63,6 +63,9 @@
#define _LLDISKCACHE
#include "llsingleton.h"
#include <chrono>
using namespace std::chrono;
class LLDiskCache :
public LLParamSingleton<LLDiskCache>
@ -92,7 +95,18 @@ class LLDiskCache :
* if there are bugs, we can ask uses to enable this
* setting and send us their logs
*/
const bool enable_cache_debug_info);
const bool enable_cache_debug_info,
// <FS:Beq> Add high/low threshold controls for cache purging
/**
* A floating point percentage of the max_size_bytes above which the cache purge will trigger.
*/
const F32 highwater_mark_percent,
/**
* A floating point percentage of the max_size_bytes which the cache purge will aim to reach once triggered.
*/
const F32 lowwater_mark_percent
// </FS:Beq>
);
virtual ~LLDiskCache() = default;
@ -157,6 +171,10 @@ class LLDiskCache :
// <FS:Ansariel> Better asset cache size control
void setMaxSizeBytes(uintmax_t size) { mMaxSizeBytes = size; }
// <FS:Beq> High/Low water control
void setHighWaterPercentage(F32 HiPct) { mHighPercent = llclamp(HiPct, mLowPercent, 100.0); };
void setLowWaterPercentage(F32 LowPct) { mLowPercent = llclamp(LowPct, 0.0, mHighPercent); };
// </FS:Beq>
private:
/**
@ -164,7 +182,8 @@ class LLDiskCache :
* directory. Primarily used here to determine the directory size
* before and after the cache purge
*/
uintmax_t dirFileSize(const std::string& dir);
uintmax_t updateCacheSize(const uintmax_t newsize); // <FS:Beq/> enable time based caching of dirfilesize except when force is true.
uintmax_t dirFileSize(const std::string& dir, bool force=false); // <FS:Beq/> enable time based caching of dirfilesize except when force is true.
/**
* Utility function to convert an LLAssetType enum into a
@ -172,6 +191,13 @@ class LLDiskCache :
*/
const std::string assetTypeToString(LLAssetType::EType at);
/**
* cache the directory size cos it takes forever to calculate it
*
*/
uintmax_t mStoredCacheSize{ 0 };
time_point<system_clock> mLastScanTime{ };
private:
/**
* The maximum size of the cache in bytes. After purge is called, the
@ -179,6 +205,10 @@ class LLDiskCache :
* less than this value
*/
uintmax_t mMaxSizeBytes;
// <FS:Beq> High/Low water control
F32 mHighPercent { 95.0 };
F32 mLowPercent { 70.0 };
// </FS:Beq>
/**
* The folder that holds the cached files. The consumer of this

View File

@ -2952,6 +2952,28 @@
<key>Value</key>
<integer>2048</integer>
</map>
<key>FSDiskCacheHighWaterPercent</key>
<map>
<key>Comment</key>
<string>Trigger point above which we should start to clear out older cache entries</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
<integer>95.0</integer>
</map>
<key>FSDiskCacheLowWaterPercent</key>
<map>
<key>Comment</key>
<string>Level to drain cache to once it goes over the high water limit.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
<integer>70.0</integer>
</map>
<key>CacheLocation</key>
<map>
<key>Comment</key>

View File

@ -5195,7 +5195,10 @@ bool LLAppViewer::initCache()
// </FS:Ansariel>
const std::string cache_dir = gDirUtilp->getExpandedFilename(LL_PATH_CACHE, cache_dir_name);
LLDiskCache::initParamSingleton(cache_dir, disk_cache_size, enable_cache_debug_info);
// <FS:Beq> Improve cache purge triggering
// LLDiskCache::initParamSingleton(cache_dir, disk_cache_size, enable_cache_debug_info);
LLDiskCache::initParamSingleton(cache_dir, disk_cache_size, enable_cache_debug_info, gSavedSettings.getF32("FSDiskCacheHighWaterPercent"), gSavedSettings.getF32("FSDiskCacheLowWaterPercent"));
// </FS:Beq>
if (!read_only)
{

View File

@ -1196,7 +1196,7 @@ DWORD WINAPI purgeThread( LPVOID lpParameter )
for( auto dir : vctDirs )
{
LL_INFOS("CachePurge") << "Removing an old cache" << LL_ENDL;
LL_INFOS("LLDiskCache") << "Removing an old cache" << LL_ENDL; // <FS:Beq/> consistent tagging to help searching log files
deleteCacheDirectory( dir );
}
@ -1214,7 +1214,7 @@ void LLAppViewerWin32::startCachePurge()
if( !hThread )
{
LL_WARNS("CachePurge") << "CreateThread failed: " << GetLastError() << LL_ENDL;
LL_WARNS("LLDiskCache") << "CreateThread failed: " << GetLastError() << LL_ENDL; // <FS:Beq/> consistent tagging to help searching log files
}
else
SetThreadPriority( hThread, THREAD_MODE_BACKGROUND_BEGIN );

View File

@ -1087,6 +1087,20 @@ void handleDiskCacheSizeChanged(const LLSD& newValue)
}
// </FS:Ansariel>
// <FS:Beq> Better asset cache purge control
void handleDiskCacheHighWaterPctChanged(const LLSD& newValue)
{
const auto new_high = newValue.asReal();
LLDiskCache::getInstance()->setHighWaterPercentage(new_high);
}
void handleDiskCacheLowWaterPctChanged(const LLSD& newValue)
{
const auto new_low = newValue.asReal();
LLDiskCache::getInstance()->setHighWaterPercentage(new_low);
}
// </FS:Beq>
void handleTargetFPSChanged(const LLSD& newValue)
{
const auto targetFPS = gSavedSettings.getU32("TargetFPS");
@ -1464,7 +1478,10 @@ void settings_setup_listeners()
// <FS:Ansariel> Better asset cache size control
setting_setup_signal_listener(gSavedSettings, "FSDiskCacheSize", handleDiskCacheSizeChanged);
// <FS:Beq> Better asset cache purge control
setting_setup_signal_listener(gSavedSettings, "FSDiskCacheHighWaterPercent", handleDiskCacheHighWaterPctChanged);
setting_setup_signal_listener(gSavedSettings, "FSDiskCacheHighWaterPercent", handleDiskCacheLowWaterPctChanged);
// </FS:Beq>
// <FS:Zi> Handle IME text input getting enabled or disabled
#if LL_SDL2