Path: blob/main/contrib/llvm-project/llvm/lib/Support/CachePruning.cpp
35232 views
//===-CachePruning.cpp - LLVM Cache Directory Pruning ---------------------===//1//2// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.3// See https://llvm.org/LICENSE.txt for license information.4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception5//6//===----------------------------------------------------------------------===//7//8// This file implements the pruning of a directory based on least recently used.9//10//===----------------------------------------------------------------------===//1112#include "llvm/Support/CachePruning.h"13#include "llvm/ADT/StringRef.h"14#include "llvm/Support/Debug.h"15#include "llvm/Support/Errc.h"16#include "llvm/Support/Error.h"17#include "llvm/Support/FileSystem.h"18#include "llvm/Support/Path.h"19#include "llvm/Support/WithColor.h"20#include "llvm/Support/raw_ostream.h"2122#define DEBUG_TYPE "cache-pruning"2324#include <set>25#include <system_error>2627using namespace llvm;2829namespace {30struct FileInfo {31sys::TimePoint<> Time;32uint64_t Size;33std::string Path;3435/// Used to determine which files to prune first. Also used to determine36/// set membership, so must take into account all fields.37bool operator<(const FileInfo &Other) const {38return std::tie(Time, Other.Size, Path) <39std::tie(Other.Time, Size, Other.Path);40}41};42} // anonymous namespace4344/// Write a new timestamp file with the given path. This is used for the pruning45/// interval option.46static void writeTimestampFile(StringRef TimestampFile) {47std::error_code EC;48raw_fd_ostream Out(TimestampFile.str(), EC, sys::fs::OF_None);49}5051static Expected<std::chrono::seconds> parseDuration(StringRef Duration) {52if (Duration.empty())53return make_error<StringError>("Duration must not be empty",54inconvertibleErrorCode());5556StringRef NumStr = Duration.slice(0, Duration.size()-1);57uint64_t Num;58if (NumStr.getAsInteger(0, Num))59return make_error<StringError>("'" + NumStr + "' not an integer",60inconvertibleErrorCode());6162switch (Duration.back()) {63case 's':64return std::chrono::seconds(Num);65case 'm':66return std::chrono::minutes(Num);67case 'h':68return std::chrono::hours(Num);69default:70return make_error<StringError>("'" + Duration +71"' must end with one of 's', 'm' or 'h'",72inconvertibleErrorCode());73}74}7576Expected<CachePruningPolicy>77llvm::parseCachePruningPolicy(StringRef PolicyStr) {78CachePruningPolicy Policy;79std::pair<StringRef, StringRef> P = {"", PolicyStr};80while (!P.second.empty()) {81P = P.second.split(':');8283StringRef Key, Value;84std::tie(Key, Value) = P.first.split('=');85if (Key == "prune_interval") {86auto DurationOrErr = parseDuration(Value);87if (!DurationOrErr)88return DurationOrErr.takeError();89Policy.Interval = *DurationOrErr;90} else if (Key == "prune_after") {91auto DurationOrErr = parseDuration(Value);92if (!DurationOrErr)93return DurationOrErr.takeError();94Policy.Expiration = *DurationOrErr;95} else if (Key == "cache_size") {96if (Value.back() != '%')97return make_error<StringError>("'" + Value + "' must be a percentage",98inconvertibleErrorCode());99StringRef SizeStr = Value.drop_back();100uint64_t Size;101if (SizeStr.getAsInteger(0, Size))102return make_error<StringError>("'" + SizeStr + "' not an integer",103inconvertibleErrorCode());104if (Size > 100)105return make_error<StringError>("'" + SizeStr +106"' must be between 0 and 100",107inconvertibleErrorCode());108Policy.MaxSizePercentageOfAvailableSpace = Size;109} else if (Key == "cache_size_bytes") {110uint64_t Mult = 1;111switch (tolower(Value.back())) {112case 'k':113Mult = 1024;114Value = Value.drop_back();115break;116case 'm':117Mult = 1024 * 1024;118Value = Value.drop_back();119break;120case 'g':121Mult = 1024 * 1024 * 1024;122Value = Value.drop_back();123break;124}125uint64_t Size;126if (Value.getAsInteger(0, Size))127return make_error<StringError>("'" + Value + "' not an integer",128inconvertibleErrorCode());129Policy.MaxSizeBytes = Size * Mult;130} else if (Key == "cache_size_files") {131if (Value.getAsInteger(0, Policy.MaxSizeFiles))132return make_error<StringError>("'" + Value + "' not an integer",133inconvertibleErrorCode());134} else {135return make_error<StringError>("Unknown key: '" + Key + "'",136inconvertibleErrorCode());137}138}139140return Policy;141}142143/// Prune the cache of files that haven't been accessed in a long time.144bool llvm::pruneCache(StringRef Path, CachePruningPolicy Policy,145const std::vector<std::unique_ptr<MemoryBuffer>> &Files) {146using namespace std::chrono;147148if (Path.empty())149return false;150151bool isPathDir;152if (sys::fs::is_directory(Path, isPathDir))153return false;154155if (!isPathDir)156return false;157158Policy.MaxSizePercentageOfAvailableSpace =159std::min(Policy.MaxSizePercentageOfAvailableSpace, 100u);160161if (Policy.Expiration == seconds(0) &&162Policy.MaxSizePercentageOfAvailableSpace == 0 &&163Policy.MaxSizeBytes == 0 && Policy.MaxSizeFiles == 0) {164LLVM_DEBUG(dbgs() << "No pruning settings set, exit early\n");165// Nothing will be pruned, early exit166return false;167}168169// Try to stat() the timestamp file.170SmallString<128> TimestampFile(Path);171sys::path::append(TimestampFile, "llvmcache.timestamp");172sys::fs::file_status FileStatus;173const auto CurrentTime = system_clock::now();174if (auto EC = sys::fs::status(TimestampFile, FileStatus)) {175if (EC == errc::no_such_file_or_directory) {176// If the timestamp file wasn't there, create one now.177writeTimestampFile(TimestampFile);178} else {179// Unknown error?180return false;181}182} else {183if (!Policy.Interval)184return false;185if (Policy.Interval != seconds(0)) {186// Check whether the time stamp is older than our pruning interval.187// If not, do nothing.188const auto TimeStampModTime = FileStatus.getLastModificationTime();189auto TimeStampAge = CurrentTime - TimeStampModTime;190if (TimeStampAge <= *Policy.Interval) {191LLVM_DEBUG(dbgs() << "Timestamp file too recent ("192<< duration_cast<seconds>(TimeStampAge).count()193<< "s old), do not prune.\n");194return false;195}196}197// Write a new timestamp file so that nobody else attempts to prune.198// There is a benign race condition here, if two processes happen to199// notice at the same time that the timestamp is out-of-date.200writeTimestampFile(TimestampFile);201}202203// Keep track of files to delete to get below the size limit.204// Order by time of last use so that recently used files are preserved.205std::set<FileInfo> FileInfos;206uint64_t TotalSize = 0;207208// Walk the entire directory cache, looking for unused files.209std::error_code EC;210SmallString<128> CachePathNative;211sys::path::native(Path, CachePathNative);212// Walk all of the files within this directory.213for (sys::fs::directory_iterator File(CachePathNative, EC), FileEnd;214File != FileEnd && !EC; File.increment(EC)) {215// Ignore filenames not beginning with "llvmcache-" or "Thin-". This216// includes the timestamp file as well as any files created by the user.217// This acts as a safeguard against data loss if the user specifies the218// wrong directory as their cache directory.219StringRef filename = sys::path::filename(File->path());220if (!filename.starts_with("llvmcache-") && !filename.starts_with("Thin-"))221continue;222223// Look at this file. If we can't stat it, there's nothing interesting224// there.225ErrorOr<sys::fs::basic_file_status> StatusOrErr = File->status();226if (!StatusOrErr) {227LLVM_DEBUG(dbgs() << "Ignore " << File->path() << " (can't stat)\n");228continue;229}230231// If the file hasn't been used recently enough, delete it232const auto FileAccessTime = StatusOrErr->getLastAccessedTime();233auto FileAge = CurrentTime - FileAccessTime;234if (Policy.Expiration != seconds(0) && FileAge > Policy.Expiration) {235LLVM_DEBUG(dbgs() << "Remove " << File->path() << " ("236<< duration_cast<seconds>(FileAge).count()237<< "s old)\n");238sys::fs::remove(File->path());239continue;240}241242// Leave it here for now, but add it to the list of size-based pruning.243TotalSize += StatusOrErr->getSize();244FileInfos.insert({FileAccessTime, StatusOrErr->getSize(), File->path()});245}246247auto FileInfo = FileInfos.begin();248size_t NumFiles = FileInfos.size();249250auto RemoveCacheFile = [&]() {251// Remove the file.252sys::fs::remove(FileInfo->Path);253// Update size254TotalSize -= FileInfo->Size;255NumFiles--;256LLVM_DEBUG(dbgs() << " - Remove " << FileInfo->Path << " (size "257<< FileInfo->Size << "), new occupancy is " << TotalSize258<< "%\n");259++FileInfo;260};261262// files.size() is greater the number of inputs by one. However, a timestamp263// file is created and stored in the cache directory if --thinlto-cache-policy264// option is used. Therefore, files.size() is used as ActualNums.265const size_t ActualNums = Files.size();266if (Policy.MaxSizeFiles && ActualNums > Policy.MaxSizeFiles)267WithColor::warning()268<< "ThinLTO cache pruning happens since the number of created files ("269<< ActualNums << ") exceeds the maximum number of files ("270<< Policy.MaxSizeFiles271<< "); consider adjusting --thinlto-cache-policy\n";272273// Prune for number of files.274if (Policy.MaxSizeFiles)275while (NumFiles > Policy.MaxSizeFiles)276RemoveCacheFile();277278// Prune for size now if needed279if (Policy.MaxSizePercentageOfAvailableSpace > 0 || Policy.MaxSizeBytes > 0) {280auto ErrOrSpaceInfo = sys::fs::disk_space(Path);281if (!ErrOrSpaceInfo) {282report_fatal_error("Can't get available size");283}284sys::fs::space_info SpaceInfo = ErrOrSpaceInfo.get();285auto AvailableSpace = TotalSize + SpaceInfo.free;286287if (Policy.MaxSizePercentageOfAvailableSpace == 0)288Policy.MaxSizePercentageOfAvailableSpace = 100;289if (Policy.MaxSizeBytes == 0)290Policy.MaxSizeBytes = AvailableSpace;291auto TotalSizeTarget = std::min<uint64_t>(292AvailableSpace * Policy.MaxSizePercentageOfAvailableSpace / 100ull,293Policy.MaxSizeBytes);294295LLVM_DEBUG(dbgs() << "Occupancy: " << ((100 * TotalSize) / AvailableSpace)296<< "% target is: "297<< Policy.MaxSizePercentageOfAvailableSpace << "%, "298<< Policy.MaxSizeBytes << " bytes\n");299300size_t ActualSizes = 0;301for (const auto &File : Files)302if (File)303ActualSizes += File->getBufferSize();304305if (ActualSizes > TotalSizeTarget)306WithColor::warning()307<< "ThinLTO cache pruning happens since the total size of the cache "308"files consumed by the current link job ("309<< ActualSizes << " bytes) exceeds maximum cache size ("310<< TotalSizeTarget311<< " bytes); consider adjusting --thinlto-cache-policy\n";312313// Remove the oldest accessed files first, till we get below the threshold.314while (TotalSize > TotalSizeTarget && FileInfo != FileInfos.end())315RemoveCacheFile();316}317return true;318}319320321