diff --git a/sycl/doc/design/KernelProgramCache.md b/sycl/doc/design/KernelProgramCache.md index fd242e7d9749d..487e427980f16 100644 --- a/sycl/doc/design/KernelProgramCache.md +++ b/sycl/doc/design/KernelProgramCache.md @@ -415,15 +415,16 @@ When adding a new program to cache, we check if the size of the program cache ex #### Persistent cache eviction -Persistent cache eviction is going to be applied based on file last access -(read/write) date (access time). On SYCL application shutdown phase cache -eviction process is initiated which walks through cache directories as follows: - -- if the file is locked, go to the next file; -- otherwise check file access time: - - if file access time is above threshold, delete the file and remove parent - directory while they are unlocked and empty; - - otherwise do nothing. +Persistent cache eviction can be enabled using the SYCL_CACHE_MAX_SIZE environment variable and is based on the LRU strategy. + +- A new file, called `cache_size.txt`, is created at the root of the persistent cache directory. This file contains the total size of the cache in bytes. When a new item is added to the cache, the size of the item is added to the total size in the `cache_size.txt` file. When the total size exceeds the threshold, the eviction process is initiated. + +- Whenever a cache entry is added or accessed, the corresponding cache item directory is updated with the current time. This is done by creating a new file, called `_access_time.txt`, in the cache item directory. This file contains the current time in nanoseconds since the epoch. When the eviction process is initiated, we use this file to determine the last access time of the cache item. + +- When a new item is added to the cache, we check if the total size exceeds the threshold. If so, we iterate through the cache item directories and delete the least recently accessed items until the total size is below half the cache size. + +Note that once the eviction is triggered, the cache size is reduced to half the cache size to avoid frequent eviction. + ## Cache limitations diff --git a/sycl/include/sycl/detail/os_util.hpp b/sycl/include/sycl/detail/os_util.hpp index c77cdb4913c36..a0c3a8483373e 100644 --- a/sycl/include/sycl/detail/os_util.hpp +++ b/sycl/include/sycl/detail/os_util.hpp @@ -12,7 +12,8 @@ #include // for __SYCL_EXPORT -#include // for size_t +#include // for size_t +#include #include // for string #include // for stat @@ -90,6 +91,20 @@ class __SYCL_EXPORT OSUtil { } }; +// These functions are not a part of OSUtils class to prevent +// exporting them as ABI. They are only used in persistent cache +// implementation and should not be exposed to the end users. +// Get size of directory in bytes. +size_t getDirectorySize(const std::string &Path); + +// Get size of file in bytes. +size_t getFileSize(const std::string &Path); + +// Function to recursively iterate over the directory and execute +// 'Func' on each regular file. +void fileTreeWalk(const std::string Path, + std::function Func); + } // namespace detail } // namespace _V1 } // namespace sycl diff --git a/sycl/source/detail/config.hpp b/sycl/source/detail/config.hpp index ace69d0a9420e..3e0a591e27d14 100644 --- a/sycl/source/detail/config.hpp +++ b/sycl/source/detail/config.hpp @@ -806,6 +806,56 @@ template <> class SYCLConfig { } }; +// SYCL_CACHE_MAX_SIZE accepts an integer that specifies +// the maximum size of the on-disk Program cache. +// Cache eviction is performed when the cache size exceeds the threshold. +// The thresholds are specified in bytes. +// The default value is "0" which means that eviction is disabled. +template <> class SYCLConfig { + using BaseT = SYCLConfigBase; + +public: + static long long get() { return getCachedValue(); } + static void reset() { (void)getCachedValue(true); } + + static long long getProgramCacheSize() { return getCachedValue(); } + + static bool isPersistentCacheEvictionEnabled() { + return getProgramCacheSize() > 0; + } + +private: + static long long getCachedValue(bool ResetCache = false) { + const auto Parser = []() { + const char *ValStr = BaseT::getRawValue(); + + // Disable eviction by default. + if (!ValStr) + return (long long)0; + + long long CacheSize = 0; + try { + CacheSize = std::stoll(ValStr); + if (CacheSize < 0) + throw INVALID_CONFIG_EXCEPTION(BaseT, "Value must be non-negative"); + } catch (...) { + std::string Msg = + std::string{"Invalid input to SYCL_CACHE_MAX_SIZE. Please try " + "a positive integer."}; + throw exception(make_error_code(errc::runtime), Msg); + } + + return CacheSize; + }; + + static auto EvictionThresholds = Parser(); + if (ResetCache) + EvictionThresholds = Parser(); + + return EvictionThresholds; + } +}; + #undef INVALID_CONFIG_EXCEPTION } // namespace detail diff --git a/sycl/source/detail/os_util.cpp b/sycl/source/detail/os_util.cpp index 018ba97cff05c..8c4c0bff1293d 100644 --- a/sycl/source/detail/os_util.cpp +++ b/sycl/source/detail/os_util.cpp @@ -7,14 +7,20 @@ //===----------------------------------------------------------------------===// #include +#include #include #include -#if __GNUC__ && __GNUC__ < 8 -// Don't include for GCC versions less than 8 +// For GCC versions less than 8, use experimental/filesystem. +#if defined(__has_include) && __has_include() +#include +namespace fs = std::filesystem; +#elif defined(__has_include) && __has_include() +#include +namespace fs = std::experimental::filesystem; #else -#include // C++ 17 std::create_directories +#error "OSUtils requires C++ filesystem support" #endif #if defined(__SYCL_RT_OS_LINUX) @@ -277,6 +283,45 @@ int OSUtil::makeDir(const char *Dir) { return 0; } +// Get size of file in bytes. +size_t getFileSize(const std::string &Path) { + return static_cast(fs::file_size(Path)); +} + +// Function to recursively iterate over the directory and execute +// 'Func' on each regular file. +void fileTreeWalk(const std::string Path, + std::function Func) { + + std::error_code EC; + for (auto It = fs::recursive_directory_iterator(Path, EC); + It != fs::recursive_directory_iterator(); It.increment(EC)) { + + // Errors can happen if a file was removed/added during the iteration. + if (EC) + throw sycl::exception( + make_error_code(errc::runtime), + "Failed to do File Tree Walk. Ensure that the directory is not " + "getting updated while FileTreeWalk is in progress.: " + + Path + "\n" + EC.message()); + + if (fs::is_regular_file(It->path())) + Func(It->path().string()); + } +} + +// Get size of a directory in bytes. +size_t getDirectorySize(const std::string &Path) { + size_t DirSizeVar = 0; + + auto CollectFIleSize = [&DirSizeVar](const std::string Path) { + DirSizeVar += getFileSize(Path); + }; + fileTreeWalk(Path, CollectFIleSize); + + return DirSizeVar; +} + } // namespace detail } // namespace _V1 } // namespace sycl diff --git a/sycl/source/detail/persistent_device_code_cache.cpp b/sycl/source/detail/persistent_device_code_cache.cpp index 40e8318f92c81..47adbf133e85c 100644 --- a/sycl/source/detail/persistent_device_code_cache.cpp +++ b/sycl/source/detail/persistent_device_code_cache.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -178,6 +179,246 @@ getProgramBinaryData(const ur_program_handle_t &NativePrg, return Result; } +// Save the current time in a file. +void PersistentDeviceCodeCache::saveCurrentTimeInAFile(std::string FileName) { + // Lock the file to prevent concurrent writes. + LockCacheItem Lock{FileName}; + if (Lock.isOwned()) { + try { + std::ofstream FileStream{FileName, std::ios::trunc}; + FileStream << std::chrono::high_resolution_clock::now() + .time_since_epoch() + .count(); + FileStream.close(); + } catch (std::exception &e) { + throw sycl::exception(make_error_code(errc::runtime), + "Failed to save current time in a file: " + + FileName + "\n" + std::string(e.what())); + } + } +} + +// Check if cache_size.txt file is present in the cache root directory. +// If not, create it and populate it with the size of the cache directory. +void PersistentDeviceCodeCache::repopulateCacheSizeFile( + const std::string &CacheRoot) { + + // No need to store cache size if eviction is disabled. + if (!isEvictionEnabled()) + return; + + const std::string CacheSizeFileName = "cache_size.txt"; + const std::string CacheSizeFile = CacheRoot + "/" + CacheSizeFileName; + + // If the cache size file is not present, calculate the size of the cache size + // directory and write it to the file. + if (!OSUtil::isPathPresent(CacheSizeFile)) { + PersistentDeviceCodeCache::trace( + "Cache size file not present. Creating one."); + + // Take the lock to write the cache size to the file. + { + LockCacheItem Lock{CacheSizeFile}; + if (!Lock.isOwned()) { + // If some other process is writing the cache size, do not write it. + PersistentDeviceCodeCache::trace("Didnot create the cache size file. " + "Some other process is creating one."); + + // Stall until the other process creates the file. Stalling is important + // to prevent race between one process that's calculating the directory + // size and another process that's trying to create a new cache entry. + while (!OSUtil::isPathPresent(CacheSizeFile)) + continue; + } else { + // Calculate the size of the cache directory. + // During directory size calculation, do not add anything + // in the cache. Otherwise, we'll get a std::fs_error. + size_t CacheSize = getDirectorySize(CacheRoot); + + std::ofstream FileStream{CacheSizeFile}; + FileStream << CacheSize; + FileStream.close(); + PersistentDeviceCodeCache::trace("Cache size file created."); + } + } + } +} + +void PersistentDeviceCodeCache::evictItemsFromCache( + const std::string &CacheRoot, size_t CacheSize, size_t MaxCacheSize) { + PersistentDeviceCodeCache::trace("Cache eviction triggered."); + + // EVict half of the cache. + constexpr float HowMuchCacheToEvict = 0.5; + + // Create a file eviction_in_progress.lock to indicate that eviction is in + // progress. This file is used to prevent two processes from evicting the + // cache at the same time. + LockCacheItem Lock{CacheRoot + EvictionInProgressFileSuffix}; + if (!Lock.isOwned()) { + // If some other process is evicting the cache, return. + PersistentDeviceCodeCache::trace( + "Another process is evicting the cache. Returning."); + return; + } + + // Get the list of all files in the cache directory along with their last + // modification time. + std::vector> FilesWithAccessTime; + + auto CollectFileAccessTime = [&FilesWithAccessTime](const std::string File) { + if (File.find(CacheEntryAccessTimeSuffix) != std::string::npos) { + std::ifstream FileStream{File}; + uint64_t AccessTime; + FileStream >> AccessTime; + FilesWithAccessTime.push_back({AccessTime, File}); + } + }; + + // fileTreeWalk can throw if any new file is created or removed during the + // iteration. Retry in that case. When eviction is in progress, we don't + // insert any new item but processes can still read the cache. Reading from + // cache can create/remove .lock file which can cause the exception. + while (true) { + try { + fileTreeWalk(CacheRoot, CollectFileAccessTime); + break; + } catch (...) { + FilesWithAccessTime.clear(); + // If the cache directory is removed during the iteration, retry. + continue; + } + } + + // Sort the files in the cache directory based on their last access time. + std::sort(FilesWithAccessTime.begin(), FilesWithAccessTime.end(), + [](const std::pair &A, + const std::pair &B) { + return A.first < B.first; + }); + + // Evict files from the cache directory until the cache size is less than the + // threshold. + size_t CurrCacheSize = CacheSize; + for (const auto &File : FilesWithAccessTime) { + + int pos = File.second.find(CacheEntryAccessTimeSuffix); + const std::string FileNameWOExt = File.second.substr(0, pos); + const std::string BinFile = FileNameWOExt + ".bin"; + const std::string SrcFile = FileNameWOExt + ".src"; + + while (OSUtil::isPathPresent(BinFile) || OSUtil::isPathPresent(SrcFile)) { + + // Lock to prevent race between writer and eviction thread. + LockCacheItem Lock{FileNameWOExt}; + if (Lock.isOwned()) { + // Remove the file and subtract its size from the cache size. + auto RemoveFileAndSubtractSize = [&CurrCacheSize]( + const std::string &FileName) { + // If the file is not present, return. + if (!OSUtil::isPathPresent(FileName)) + return; + + auto FileSize = getFileSize(FileName); + if (std::remove(FileName.c_str())) { + throw sycl::exception(make_error_code(errc::runtime), + "Failed to evict cache entry: " + FileName); + } else { + PersistentDeviceCodeCache::trace("File removed: " + FileName); + CurrCacheSize -= FileSize; + } + }; + + // If removal fails due to a race, retry. + // Races are rare, but can happen if another process is reading the + // file. Locking down the entire cache and blocking all readers would be + // inefficient. + try { + RemoveFileAndSubtractSize(SrcFile); + RemoveFileAndSubtractSize(BinFile); + } catch (...) { + continue; + } + } + } + + // If the cache size is less than the threshold, break. + if (CurrCacheSize <= (size_t)(HowMuchCacheToEvict * MaxCacheSize)) + break; + } + + // Update the cache size file with the new cache size. + { + const std::string CacheSizeFileName = "cache_size.txt"; + const std::string CacheSizeFile = CacheRoot + "/" + CacheSizeFileName; + while (true) { + LockCacheItem Lock{CacheSizeFile}; + if (!Lock.isOwned()) { + // If some other process is writing the cache size, spin lock. + continue; + } else { + std::fstream FileStream; + FileStream.open(CacheSizeFile, std::ios::out | std::ios::trunc); + FileStream << CurrCacheSize; + FileStream.close(); + + PersistentDeviceCodeCache::trace( + "Updating the cache size file after eviction. New size: " + + std::to_string(CurrCacheSize)); + break; + } + } + } +} + +// Update the cache size file and trigger cache eviction if needed. +void PersistentDeviceCodeCache::updateCacheFileSizeAndTriggerEviction( + const std::string &CacheRoot, size_t ItemSize) { + + // No need to store cache size if eviction is disabled. + if (!isEvictionEnabled()) + return; + + const std::string CacheSizeFileName = "cache_size.txt"; + const std::string CacheSizeFile = CacheRoot + "/" + CacheSizeFileName; + size_t CurrentCacheSize = 0; + // Read the cache size from the file. + while (true) { + LockCacheItem Lock{CacheSizeFile}; + if (!Lock.isOwned()) { + // If some other process is writing the cache size, spin lock. + continue; + } else { + PersistentDeviceCodeCache::trace("Updating the cache size file."); + std::fstream FileStream; + FileStream.open(CacheSizeFile, std::ios::in); + + // Read the cache size from the file; + std::string line; + if (std::getline(FileStream, line)) { + CurrentCacheSize = std::stoull(line); + } + FileStream.close(); + + CurrentCacheSize += ItemSize; + + // Write the updated cache size to the file. + FileStream.open(CacheSizeFile, std::ios::out | std::ios::trunc); + FileStream << CurrentCacheSize; + FileStream.close(); + break; + } + } + + // Check if the cache size exceeds the threshold and trigger cache eviction if + // needed. + size_t MaxCacheSize = SYCLConfig::getProgramCacheSize(); + if (CurrentCacheSize > MaxCacheSize) { + // Trigger cache eviction. + evictItemsFromCache(CacheRoot, CurrentCacheSize, MaxCacheSize); + } +} + /* Stores built program in persistent cache. We will put the binary for each * device in the list to a separate file. */ @@ -190,8 +431,21 @@ void PersistentDeviceCodeCache::putItemToDisc( if (!areImagesCacheable(Imgs)) return; + repopulateCacheSizeFile(getRootDir()); + + // Do not insert any new item if eviction is in progress. + // Since evictions are rare, we can afford to spin lock here. + const std::string EvictionInProgressFile = + getRootDir() + EvictionInProgressFileSuffix; + // Stall until the other process finishes eviction. + while (OSUtil::isPathPresent(EvictionInProgressFile)) + continue; + std::vector SortedImgs = getSortedImages(Imgs); auto BinaryData = getProgramBinaryData(NativePrg, Devices); + + // Total size of the item that we just wrote to the cache. + size_t TotalSize = 0; for (size_t DeviceIndex = 0; DeviceIndex < Devices.size(); DeviceIndex++) { // If we don't have binary for the device, skip it. if (BinaryData[DeviceIndex].empty()) @@ -202,9 +456,10 @@ void PersistentDeviceCodeCache::putItemToDisc( if (DirName.empty()) return; + std::string FileName; try { OSUtil::makeDir(DirName.c_str()); - std::string FileName = getUniqueFilename(DirName); + FileName = getUniqueFilename(DirName); LockCacheItem Lock{FileName}; if (Lock.isOwned()) { std::string FullFileName = FileName + ".bin"; @@ -212,6 +467,12 @@ void PersistentDeviceCodeCache::putItemToDisc( trace("device binary has been cached: " + FullFileName); writeSourceItem(FileName + ".src", Devices[DeviceIndex], SortedImgs, SpecConsts, BuildOptionsString); + + // Update Total cache size after adding the new items. + TotalSize += getFileSize(FileName + ".src"); + TotalSize += getFileSize(FileName + ".bin"); + + saveCurrentTimeInAFile(FileName + CacheEntryAccessTimeSuffix); } else { PersistentDeviceCodeCache::trace("cache lock not owned " + FileName); } @@ -225,6 +486,10 @@ void PersistentDeviceCodeCache::putItemToDisc( std::strerror(errno)); } } + + // Update the cache size file and trigger cache eviction if needed. + if (TotalSize) + updateCacheFileSizeAndTriggerEviction(getRootDir(), TotalSize); } void PersistentDeviceCodeCache::putCompiledKernelToDisc( @@ -298,6 +563,12 @@ std::vector> PersistentDeviceCodeCache::getItemFromDisc( try { std::string FullFileName = FileName + ".bin"; Binaries[DeviceIndex] = readBinaryDataFromFile(FullFileName); + + // Explicitly update the access time of the file. This is required for + // eviction. + if (isEvictionEnabled()) + saveCurrentTimeInAFile(FileName + CacheEntryAccessTimeSuffix); + FileNames += FullFileName + ";"; break; } catch (...) { @@ -423,8 +694,8 @@ PersistentDeviceCodeCache::readBinaryDataFromFile(const std::string &FileName) { } /* Writing cache item key sources to be used for reliable identification - * Format: Four pairs of [size, value] for device, build options, specialization - * constant values, device code SPIR-V images. + * Format: Four pairs of [size, value] for device, build options, + * specialization constant values, device code SPIR-V images. */ void PersistentDeviceCodeCache::writeSourceItem( const std::string &FileName, const device &Device, diff --git a/sycl/source/detail/persistent_device_code_cache.hpp b/sycl/source/detail/persistent_device_code_cache.hpp index a7c57625f81f4..c51e5e55bc22b 100644 --- a/sycl/source/detail/persistent_device_code_cache.hpp +++ b/sycl/source/detail/persistent_device_code_cache.hpp @@ -124,9 +124,6 @@ class PersistentDeviceCodeCache { const std::vector &SortedImgs, const SerializedObj &SpecConsts, const std::string &BuildOptionsString); - /* Returns the path to directory storing persistent device code cache.*/ - static std::string getRootDir(); - /* Form string representing device version */ static std::string getDeviceIDString(const device &Device); @@ -158,6 +155,9 @@ class PersistentDeviceCodeCache { 1024 * 1024 * 1024; public: + /* Returns the path to directory storing persistent device code cache.*/ + static std::string getRootDir(); + /* Check if on-disk cache enabled. */ static bool isEnabled(); @@ -220,6 +220,34 @@ class PersistentDeviceCodeCache { if (traceEnabled) std::cerr << "[kernel_compiler Persistent Cache]: " << msg << std::endl; } + +private: + // Check if cache_size.lock file is present in the cache root directory. + // If not, create it and populate it with the size of the cache directory. + static void repopulateCacheSizeFile(const std::string &CacheRoot); + + // Update the cache size file and trigger cache eviction if needed. + static void + updateCacheFileSizeAndTriggerEviction(const std::string &CacheRoot, + size_t CacheSize); + + // Evict LRU items from the cache to make space for new items. + static void evictItemsFromCache(const std::string &CacheRoot, + size_t CacheSize, size_t MaxCacheSize); + + static void saveCurrentTimeInAFile(std::string FileName); + + // Check if eviction is enabled. + static bool isEvictionEnabled() { + return SYCLConfig::isPersistentCacheEvictionEnabled(); + } + + // Suffix for access time file. Every cache entry will have one. + static inline std::string CacheEntryAccessTimeSuffix = "_access_time.txt"; + // Suffix for eviction in progress file. It is created when eviction is + // triggered and removed when eviction is done. + static inline std::string EvictionInProgressFileSuffix = + "_eviction_in_progress"; }; } // namespace detail } // namespace _V1 diff --git a/sycl/unittests/config/ConfigTests.cpp b/sycl/unittests/config/ConfigTests.cpp index 756a340c8f82d..0f990bc3c9847 100644 --- a/sycl/unittests/config/ConfigTests.cpp +++ b/sycl/unittests/config/ConfigTests.cpp @@ -387,3 +387,65 @@ TEST(ConfigTests, CheckSyclCacheEvictionThresholdTest) { InMemEvicType::reset(); TestConfig(0); } + +// SYCL_CACHE_MAX_SIZE accepts an integer that specifies +// the maximum size of the persistent Program cache. +// Cache eviction is performed when the cache size exceeds the threshold. +// The thresholds are specified in bytes. +// The default value is "0" which means that eviction is disabled. +TEST(ConfigTests, CheckPersistentCacheEvictionThresholdTest) { + + using OnDiskEvicType = sycl::detail::SYCLConfig; + + // Lambda to test parsing of SYCL_CACHE_MAX_SIZE. + auto TestConfig = [](int expectedProgramCacheSize) { + EXPECT_EQ(expectedProgramCacheSize, OnDiskEvicType::getProgramCacheSize()); + EXPECT_EQ(expectedProgramCacheSize > 0, + OnDiskEvicType::isPersistentCacheEvictionEnabled()); + }; + + // Lambda to set SYCL_CACHE_MAX_SIZE. + auto SetSyclDiskCacheEvictionThresholdEnv = [](const char *value) { +#ifdef _WIN32 + _putenv_s("SYCL_CACHE_MAX_SIZE", value); +#else + setenv("SYCL_CACHE_MAX_SIZE", value, 1); +#endif + }; + + // Lambda to test invalid inputs. An exception should be thrown + // when parsing invalid values. + auto TestInvalidValues = [&](const char *value, const char *errMsg) { + SetSyclDiskCacheEvictionThresholdEnv(value); + try { + OnDiskEvicType::reset(); + TestConfig(0); + FAIL() << errMsg; + } catch (...) { + } + }; + + // Test eviction threshold with zero. + SetSyclDiskCacheEvictionThresholdEnv("0"); + sycl::detail::readConfig(true); + TestConfig(0); + + // Test invalid values. + TestInvalidValues("-1", "Should throw exception for negative value"); + TestInvalidValues("a", "Should throw exception for non-integer value"); + + // Test valid values. + SetSyclDiskCacheEvictionThresholdEnv("1024"); + OnDiskEvicType::reset(); + TestConfig(1024); + + // When SYCL_CACHE_MAX_SIZE is not set, it should default to + // 0:0:0. +#ifdef _WIN32 + _putenv_s("SYCL_CACHE_MAX_SIZE", ""); +#else + unsetenv("SYCL_CACHE_MAX_SIZE"); +#endif + OnDiskEvicType::reset(); + TestConfig(0); +} diff --git a/sycl/unittests/kernel-and-program/PersistentDeviceCodeCache.cpp b/sycl/unittests/kernel-and-program/PersistentDeviceCodeCache.cpp index 85ac0cd62c6a0..a9c37c00b562c 100644 --- a/sycl/unittests/kernel-and-program/PersistentDeviceCodeCache.cpp +++ b/sycl/unittests/kernel-and-program/PersistentDeviceCodeCache.cpp @@ -135,6 +135,12 @@ class PersistentDeviceCodeCache SYCLCachePersistentChanged = true; } + // Set SYCL_CACHE_MAX_SIZE. + void SetDiskCacheEvictionEnv(const char *NewValue) { + set_env("SYCL_CACHE_MAX_SIZE", NewValue); + sycl::detail::SYCLConfig::reset(); + } + void AppendToSYCLCacheDirEnv(const char *SubDir) { std::string NewSYCLCacheDirPath{RootSYCLCacheDir}; if (NewSYCLCacheDirPath.back() != '\\' && NewSYCLCacheDirPath.back() != '/') @@ -144,6 +150,24 @@ class PersistentDeviceCodeCache sycl::detail::SYCLConfig::reset(); } + // Get the list of binary files in the cache directory. + std::vector getBinaryFileNames(std::string CachePath) { + + std::vector FileNames; + std::error_code EC; + for (llvm::sys::fs::directory_iterator DirIt(CachePath, EC); + DirIt != llvm::sys::fs::directory_iterator(); DirIt.increment(EC)) { + // Check if the file is a binary file. + std::string filename = DirIt->path(); + if (filename.find(".bin") != std::string::npos) { + // Just return the file name without the path. + FileNames.push_back(filename.substr(filename.find_last_of("/\\") + 1)); + } + } + + return FileNames; + } + void ResetSYCLCacheDirEnv() { set_env("SYCL_CACHE_DIR", RootSYCLCacheDir.c_str()); sycl::detail::SYCLConfig::reset(); @@ -169,6 +193,9 @@ class PersistentDeviceCodeCache SetSYCLCachePersistentEnv(SYCLCachePersistentBefore ? SYCLCachePersistentBefore->c_str() : nullptr); + + // Reset SYCL_CACHE_MAX_SIZE. + SetDiskCacheEvictionEnv(nullptr); ResetSYCLCacheDirEnv(); } @@ -536,6 +563,92 @@ TEST_P(PersistentDeviceCodeCache, AccessDeniedForCacheDir) { } #endif //_WIN32 +// Unit tests for testing eviction in persistent cache. +TEST_P(PersistentDeviceCodeCache, BasicEviction) { + + // Cleanup the cache directory. + std::string CacheRoot = detail::PersistentDeviceCodeCache::getRootDir(); + ASSERT_NO_ERROR(llvm::sys::fs::remove_directories(CacheRoot)); + ASSERT_NO_ERROR(llvm::sys::fs::create_directories(CacheRoot)); + + // Disable eviction for the time being. + SetDiskCacheEvictionEnv("9000000"); + + std::string BuildOptions{"--eviction"}; + // Put 3 items to the cache. + detail::PersistentDeviceCodeCache::putItemToDisc({Dev}, {&Img}, {}, + BuildOptions, NativeProg); + + std::string ItemDir = detail::PersistentDeviceCodeCache::getCacheItemPath( + Dev, {&Img}, {}, BuildOptions); + size_t SizeOfOneEntry = (size_t)(detail::getDirectorySize(ItemDir)); + + detail::PersistentDeviceCodeCache::putItemToDisc({Dev}, {&Img}, {}, + BuildOptions, NativeProg); + + detail::PersistentDeviceCodeCache::putItemToDisc({Dev}, {&Img}, {}, + BuildOptions, NativeProg); + + // Retrieve 0.bin from the cache. + auto Res = detail::PersistentDeviceCodeCache::getItemFromDisc( + {Dev}, {&Img}, {}, BuildOptions); + + // Get the number of binary files in the cached item folder. + auto BinFiles = getBinaryFileNames(ItemDir); + EXPECT_EQ(BinFiles.size(), static_cast(3)) + << "Missing binary files. Eviction should not have happened."; + + // Set SYCL_CACHE_MAX_SIZE. + SetDiskCacheEvictionEnv(std::to_string(3 * SizeOfOneEntry).c_str()); + + // Put 4th item to the cache. This should trigger eviction. Three of the + // items should be evicted as we evict till the size of cache is less than + // the half of cache size. + detail::PersistentDeviceCodeCache::putItemToDisc({Dev}, {&Img}, {}, + BuildOptions, NativeProg); + + // We should have two binary files: 0.bin, 3.bin. + BinFiles = getBinaryFileNames(ItemDir); + EXPECT_EQ(BinFiles.size(), static_cast(1)) + << "Eviction failed. Wrong number of binary files in the cache."; + + // Check that 1.bin, 2.bin, and 0.bin was evicted. + for (const auto &File : BinFiles) { + EXPECT_NE(File, "1.bin") + << "Eviction failed. 1.bin should have been evicted."; + EXPECT_NE(File, "2.bin") + << "Eviction failed. 2.bin should have been evicted."; + EXPECT_NE(File, "0.bin") + << "Eviction failed. 0.bin should have been evicted."; + } + + ASSERT_NO_ERROR(llvm::sys::fs::remove_directories(ItemDir)); +} + +// Unit test for testing size file creation and update, concurrently. +TEST_P(PersistentDeviceCodeCache, ConcurentReadWriteCacheFileSize) { + // Cleanup the cache directory. + std::string CacheRoot = detail::PersistentDeviceCodeCache::getRootDir(); + ASSERT_NO_ERROR(llvm::sys::fs::remove_directories(CacheRoot)); + ASSERT_NO_ERROR(llvm::sys::fs::create_directories(CacheRoot)); + + // Insanely large value (1GB) to not trigger eviction. This test just + // checks for deadlocks/crashes when updating the size file concurrently. + SetDiskCacheEvictionEnv("1000000000"); + ConcurentReadWriteCache(1, 100); +} + +// Unit test for adding and evicting cache, concurrently. +TEST_P(PersistentDeviceCodeCache, ConcurentReadWriteCacheEviction) { + // Cleanup the cache directory. + std::string CacheRoot = detail::PersistentDeviceCodeCache::getRootDir(); + ASSERT_NO_ERROR(llvm::sys::fs::remove_directories(CacheRoot)); + ASSERT_NO_ERROR(llvm::sys::fs::create_directories(CacheRoot)); + + SetDiskCacheEvictionEnv("1000"); + ConcurentReadWriteCache(2, 100); +} + INSTANTIATE_TEST_SUITE_P(PersistentDeviceCodeCacheImpl, PersistentDeviceCodeCache, ::testing::Values(SYCL_DEVICE_BINARY_TYPE_SPIRV,