]> git.ipfire.org Git - thirdparty/ccache.git/commitdiff
feat: Improve automatic cache cleanup mechanism
authorJoel Rosdahl <joel@rosdahl.net>
Thu, 5 Jan 2023 18:14:27 +0000 (19:14 +0100)
committerJoel Rosdahl <joel@rosdahl.net>
Tue, 17 Jan 2023 19:25:06 +0000 (20:25 +0100)
The cache cleanup mechanism has worked essentially the same ever since
ccache was initially created in 2002:

- The total number and size of all files in one of the 16 subdirectories
  (AKA level 1) are kept in the stats file in said subdirectory.
- On a cache miss, the new compilation result file is written (based on
  the first digits of the hash) to a subdirectory of one of those 16
  subdirectories, and the stats file is updated accordingly.
- Automatic cleanup is triggered if the size of the level 1 subdirectory
  becomes larger than max_size / 16.
- ccache then lists all files in the subdirectory recursively, stats
  them to check their size and mtime, sorts the file list on mtime and
  deletes the 20% oldest files.

Some problems with the approach described above:

- (A) If several concurrent ccache invocations result in a cache miss
  and write their results to the same subdirectory then all of them will
  start cleaning up the same subdirectory simultaneously, doing
  unnecessary work.
- (B) The ccache invocation that resulted in a cache miss will perform
  cleanup and then exit, which means that an arbitrary ccache process
  that happens to trigger cleanup will take a long time to finish.
- (C) Listing all files in a subdirectory of a large cache can be quite
  slow.
- (D) stat-ing all files in a subdirectory of a large cache can be quite
  slow.
- (E) Deleting many files can be quite slow.
- (F) Since a cleanup by default removes 20% of the files in a
  subdirectory, the actual cache size will (once the cache limit is
  reached) on average hover around 90% of the configured maximum size,
  which can be confusing.

This commit solves or improves on all of the listed problems:

- Before starting automatic cleanup, a global "auto cleanup" lock is
  acquired (non-blocking) so that at most one process is performing
  cleanup at a time. This solves the potential "cache cleanup stampede"
  described in (A).
- Automatic cleanup is now performed in just one of the 256 level 2
  directories. This means that a single cleanup on average will be 16
  times faster than before. This improves on (B), (C), (D) and (E) since
  the cleanup made by a single compilation will not have to access a
  large part of the cache. On the other hand, cleanups will be triggered
  16 times more often, but the cleanup duty will be more evenly spread
  out during a build.
- The total cache size is calculated and compared with the configured
  maximum size before starting automatic cleanup. This, in combination
  with performing cleanup on level 2, means that the actual cache size
  will stay very close to the maximum size instead of about 90%. This
  solves (F).

The limit_multiple configuration option has been removed since it is no
longer used.

Closes #417.

18 files changed:
doc/MANUAL.adoc
src/.clang-tidy
src/Config.cpp
src/Config.hpp
src/core/Statistic.hpp
src/core/Statistics.cpp
src/storage/local/LocalStorage.cpp
src/storage/local/LocalStorage.hpp
src/storage/local/util.cpp
src/storage/local/util.hpp
src/test_lockfile.cpp
src/util/LockFile.hpp
src/util/LongLivedLockFileManager.cpp
src/util/LongLivedLockFileManager.hpp
test/suites/cleanup.bash
unittest/test_Config.cpp
unittest/test_storage_local_util.cpp
unittest/test_util_LockFile.cpp

index 4d3aab8b76af7607477dd962bd673d532abe02ea..b6d52f1adf9b96e38463b13f2079870673647507 100644 (file)
@@ -84,7 +84,7 @@ documentation.
     cache file count and size totals. Normally, there is no need to initiate
     cleanup manually as ccache keeps the cache below the specified limits at
     runtime and keeps statistics up to date on each compilation. Forcing a
-    cleanup is mostly useful if you manually modify the cache contents or
+    cleanup is mostly useful if you have modified the cache contents manually or
     believe that the cache size statistics may be inaccurate.
 
 *-C*, *--clear*::
@@ -791,13 +791,6 @@ NOTE: The inode cache feature is currently not available on Windows.
     output. The default is false. This can be used to check documentation with
     `-Wdocumentation`.
 
-[#config_limit_multiple]
-*limit_multiple* (*CCACHE_LIMIT_MULTIPLE*)::
-
-    Sets the limit when cleaning up. Files are deleted (in LRU order) until the
-    levels are below the limit. The default is 0.8 (= 80%). See
-    _<<Automatic cleanup>>_ for more information.
-
 [#config_log_file]
 *log_file* (*CCACHE_LOGFILE*)::
 
@@ -1282,45 +1275,20 @@ Cleanup can be triggered in two different ways: automatic and manual.
 
 === Automatic cleanup
 
-Ccache maintains counters for various statistics about the cache, including the
-size and number of all cached files. In order to improve performance and reduce
-issues with concurrent ccache invocations, there is one statistics file for
-each of the sixteen subdirectories in the cache.
-
-After a new compilation result has been written to the cache, ccache will
-update the size and file number statistics for the subdirectory (one of
-sixteen) to which the result was written. Then, if the size counter for said
-subdirectory is greater than *max_size / 16* or the file number counter is
-greater than *max_files / 16*, automatic cleanup is triggered.
-
-When automatic cleanup is triggered for a subdirectory in the cache, ccache
-will:
-
-1. Count all files in the subdirectory and compute their aggregated size.
-2. Remove files in LRU (least recently used) order until the size is at most
-   *limit_multiple * max_size / 16* and the number of files is at most
-   *limit_multiple * max_files / 16*, where
-   <<config_limit_multiple,*limit_multiple*>>, <<config_max_size,*max_size*>>
-   and <<config_max_files,*max_files*>> are configuration options.
-3. Set the size and file number counters to match the files that were kept.
-
-The reason for removing more files than just those needed to not exceed the max
-limits is that a cleanup is a fairly slow operation, so it would not be a good
-idea to trigger it often, like after each cache miss.
-
-The LRU cleanup makes use of the file modification time (mtime) of cache
-entries; ccache updates mtime of the cache entries read on a cache hit to mark
-them as "recently used".
+After a new compilation result has been written to the local cache, ccache will
+trigger an automatic cleanup if <<config_max_size,*max_size*>> or
+<<config_max_files,*max_files*>> is exceeded. The cleanup removes cache entries
+in LRU (least recently used) order based on the modification time (mtime) of
+files in the cache. For this reason, ccache updates mtime of the cache files
+read on a cache hit to mark them as recently used.
 
 
 === Manual cleanup
 
-You can run `ccache -c/--cleanup` to force cleanup of the whole cache, i.e. all
-of the sixteen subdirectories. This will recalculate the statistics counters
-and make sure that the configuration options *max_size* and
-<<config_max_files,*max_files*>> are not exceeded. Note that
-<<config_limit_multiple,*limit_multiple*>> is not taken into account for manual
-cleanup.
+You can run `ccache -c/--cleanup` to force cleanup of the whole cache. This will
+recalculate the cache size information and also make sure that the cache size
+does not exceed <<config_max_size,*max_size*>> and
+<<config_max_files,*max_files*>>.
 
 
 == Cache compression
index 48c5e0aa1ff976a060b923cee705c2d7f6912e60..e0a7e652458d13678fb0eb090eeae18a0fa0ad1e 100644 (file)
@@ -10,6 +10,7 @@
 ---
 Checks:          '-*,
                   readability-*,
+                  -readability-convert-member-functions-to-static,
                   -readability-implicit-bool-conversion,
                   -readability-magic-numbers,
                   -readability-else-after-return,
index 75a1358d70aa3404dff2a2a4039fa9a51b59acd0..a12180a86252d8efea0aff92c9e3242d6e6b3dec 100644 (file)
@@ -80,7 +80,6 @@ enum class ConfigItem {
   ignore_options,
   inode_cache,
   keep_comments_cpp,
-  limit_multiple,
   log_file,
   max_files,
   max_size,
@@ -136,7 +135,6 @@ const std::unordered_map<std::string, ConfigKeyTableEntry> k_config_key_table =
     {"ignore_options", {ConfigItem::ignore_options}},
     {"inode_cache", {ConfigItem::inode_cache}},
     {"keep_comments_cpp", {ConfigItem::keep_comments_cpp}},
-    {"limit_multiple", {ConfigItem::limit_multiple}},
     {"log_file", {ConfigItem::log_file}},
     {"max_files", {ConfigItem::max_files}},
     {"max_size", {ConfigItem::max_size}},
@@ -186,7 +184,6 @@ const std::unordered_map<std::string, std::string> k_env_variable_table = {
   {"IGNOREHEADERS", "ignore_headers_in_manifest"},
   {"IGNOREOPTIONS", "ignore_options"},
   {"INODECACHE", "inode_cache"},
-  {"LIMIT_MULTIPLE", "limit_multiple"},
   {"LOGFILE", "log_file"},
   {"MAXFILES", "max_files"},
   {"MAXSIZE", "max_size"},
@@ -747,9 +744,6 @@ Config::get_string_value(const std::string& key) const
   case ConfigItem::keep_comments_cpp:
     return format_bool(m_keep_comments_cpp);
 
-  case ConfigItem::limit_multiple:
-    return FMT("{:.1f}", m_limit_multiple);
-
   case ConfigItem::log_file:
     return m_log_file;
 
@@ -992,11 +986,6 @@ Config::set_item(const std::string& key,
     m_keep_comments_cpp = parse_bool(value, env_var_key, negate);
     break;
 
-  case ConfigItem::limit_multiple:
-    m_limit_multiple = std::clamp(
-      util::value_or_throw<core::Error>(util::parse_double(value)), 0.0, 1.0);
-    break;
-
   case ConfigItem::log_file:
     m_log_file = Util::expand_environment_variables(value);
     break;
index b75ff43c475329fe4232766bf192de642eb644df..b1d2e101c27e22727daeb12f1c595d9de8363dd5 100644 (file)
@@ -73,7 +73,6 @@ public:
   const std::string& ignore_options() const;
   bool inode_cache() const;
   bool keep_comments_cpp() const;
-  double limit_multiple() const;
   const std::string& log_file() const;
   uint64_t max_files() const;
   uint64_t max_size() const;
@@ -183,7 +182,6 @@ private:
   std::string m_ignore_options;
   bool m_inode_cache = true;
   bool m_keep_comments_cpp = false;
-  double m_limit_multiple = 0.8;
   std::string m_log_file;
   uint64_t m_max_files = 0;
   uint64_t m_max_size = 5ULL * 1000 * 1000 * 1000;
@@ -364,12 +362,6 @@ Config::keep_comments_cpp() const
   return m_keep_comments_cpp;
 }
 
-inline double
-Config::limit_multiple() const
-{
-  return m_limit_multiple;
-}
-
 inline const std::string&
 Config::log_file() const
 {
index 1f4fb921c2debb0198babb1b770256e447c2f281..ba39cfea0934f3c9fb15cae11bd3b113c6a72760 100644 (file)
@@ -72,7 +72,13 @@ enum class Statistic {
   remote_storage_hit = 47,
   remote_storage_miss = 48,
 
-  END
+  // 49-64: files in level 2 subdirs 0-f
+  subdir_files_base = 49,
+
+  // 65-80: size (KiB) in level 2 subdirs 0-f
+  subdir_size_kibibyte_base = 65,
+
+  END = 81
 };
 
 } // namespace core
index e76ea5e013f1696414f7ab2de442db00cc9f4516..459d1f135dae7714c92af00a3c0195e80b6e4e15 100644 (file)
@@ -127,10 +127,14 @@ const StatisticsField k_statistics_fields[] = {
   FIELD(unsupported_source_language,
         "Unsupported source language",
         FLAG_UNCACHEABLE),
+
+  // subdir_files_base and subdir_size_kibibyte_base are intentionally omitted
+  // since they are not interesting to show.
 };
 
 static_assert(sizeof(k_statistics_fields) / sizeof(k_statistics_fields[0])
-              == static_cast<size_t>(Statistic::END) - 1);
+              == static_cast<size_t>(Statistic::END)
+                   - (/*none*/ 1 + /*subdir files*/ 16 + /*subdir size*/ 16));
 
 static std::string
 format_timestamp(const util::TimePoint& value)
@@ -333,7 +337,7 @@ Statistics::format_human_readable(const Config& config,
     }
     table.add_row(size_cells);
 
-    if (verbosity > 0) {
+    if (verbosity > 0 || config.max_files() > 0) {
       std::vector<C> files_cells{"  Files:", S(files_in_cache)};
       if (config.max_files() > 0) {
         files_cells.emplace_back("/");
index 50f6b1c9a65bedb12826ea61a00ececf751dfe6d..5134c0b7f47189908008c67a2f291dea05523925 100644 (file)
 #include <algorithm>
 #include <atomic>
 #include <memory>
+#include <numeric>
 #include <string>
+#include <utility>
 
 #ifdef HAVE_UNISTD_H
 #  include <unistd.h>
 #endif
 
 using core::Statistic;
+using core::StatisticsCounters;
 
 namespace storage::local {
 
@@ -89,32 +92,62 @@ const uint8_t k_max_cache_levels = 4;
 
 namespace {
 
-struct SubdirCounters
+struct Level2Counters
 {
   uint64_t files = 0;
   uint64_t size = 0;
-  uint64_t cleanups_performed = 0;
 
-  SubdirCounters&
-  operator+=(const SubdirCounters& other)
+  Level2Counters&
+  operator+=(const Level2Counters& other)
   {
     files += other.files;
     size += other.size;
-    cleanups_performed += other.cleanups_performed;
     return *this;
   }
 };
 
+struct Level1Counters
+{
+  Level2Counters level_2_counters[16] = {};
+  uint64_t cleanups = 0;
+
+  uint64_t
+  files() const
+  {
+    uint64_t sum = 0;
+    for (const auto& cs : level_2_counters) {
+      sum += cs.files;
+    }
+    return sum;
+  }
+
+  uint64_t
+  size() const
+  {
+    uint64_t sum = 0;
+    for (const auto& cs : level_2_counters) {
+      sum += cs.size;
+    }
+    return sum;
+  }
+};
+
 } // namespace
 
 static void
-set_counters(const std::string& level_1_dir, const SubdirCounters& level_1_cs)
+set_counters(const StatsFile& stats_file, const Level1Counters& level_1_cs)
 {
-  const std::string stats_file = level_1_dir + "/stats";
-  StatsFile(stats_file).update([&](auto& cs) {
-    cs.increment(Statistic::cleanups_performed, level_1_cs.cleanups_performed);
-    cs.set(Statistic::files_in_cache, level_1_cs.files);
-    cs.set(Statistic::cache_size_kibibyte, level_1_cs.size / 1024);
+  stats_file.update([&](auto& cs) {
+    cs.set(Statistic::files_in_cache, level_1_cs.files());
+    cs.set(Statistic::cache_size_kibibyte, level_1_cs.size() / 1024);
+    for_each_cache_subdir([&](uint8_t i) {
+      cs.set_offsetted(
+        Statistic::subdir_files_base, i, level_1_cs.level_2_counters[i].files);
+      cs.set_offsetted(Statistic::subdir_size_kibibyte_base,
+                       i,
+                       level_1_cs.level_2_counters[i].size / 1024);
+    });
+    cs.increment(Statistic::cleanups_performed, level_1_cs.cleanups);
   });
 }
 
@@ -148,33 +181,40 @@ calculate_wanted_cache_level(const uint64_t files_in_level_1)
 static void
 delete_file(const std::string& path,
             const uint64_t size,
-            uint64_t* cache_size,
-            uint64_t* files_in_cache)
+            uint64_t& cache_size,
+            uint64_t& files_in_cache)
 {
   const bool deleted = Util::unlink_safe(path, Util::UnlinkLog::ignore_failure);
   if (!deleted && errno != ENOENT && errno != ESTALE) {
     LOG("Failed to unlink {} ({})", path, strerror(errno));
-  } else if (cache_size && files_in_cache) {
+  } else {
     // The counters are intentionally subtracted even if there was no file to
     // delete since the final cache size calculation will be incorrect if they
     // aren't. (This can happen when there are several parallel ongoing
     // cleanups of the same directory.)
-    *cache_size -= size;
-    --*files_in_cache;
+    cache_size -= size;
+    --files_in_cache;
   }
 }
 
-static SubdirCounters
-clean_dir(const std::string& subdir,
-          const uint64_t max_size,
-          const uint64_t max_files,
-          const std::optional<uint64_t> max_age,
-          const std::optional<std::string> namespace_,
-          const ProgressReceiver& progress_receiver)
+struct CleanDirResult
+{
+  Level2Counters before;
+  Level2Counters after;
+};
+
+static CleanDirResult
+clean_dir(
+  const std::string& l2_dir,
+  const uint64_t max_size,
+  const uint64_t max_files,
+  const std::optional<uint64_t> max_age = std::nullopt,
+  const std::optional<std::string> namespace_ = std::nullopt,
+  const ProgressReceiver& progress_receiver = [](double /*progress*/) {})
 {
-  LOG("Cleaning up cache directory {}", subdir);
+  LOG("Cleaning up cache directory {}", l2_dir);
 
-  auto files = get_cache_dir_files(subdir);
+  auto files = get_cache_dir_files(l2_dir);
   progress_receiver(1.0 / 3);
 
   uint64_t cache_size = 0;
@@ -218,6 +258,7 @@ clean_dir(const std::string& subdir,
   LOG("Before cleanup: {:.0f} KiB, {:.0f} files",
       static_cast<double>(cache_size) / 1024,
       static_cast<double>(files_in_cache));
+  Level2Counters counters_before{files_in_cache, cache_size};
 
   bool cleaned = false;
   for (size_t i = 0; i < files.size();
@@ -255,26 +296,27 @@ clean_dir(const std::string& subdir,
           for (const auto& raw_file : entry->second) {
             delete_file(raw_file,
                         Stat::lstat(raw_file).size_on_disk(),
-                        &cache_size,
-                        &files_in_cache);
+                        cache_size,
+                        files_in_cache);
           }
         }
       }
     }
 
-    delete_file(file.path(), file.size_on_disk(), &cache_size, &files_in_cache);
+    delete_file(file.path(), file.size_on_disk(), cache_size, files_in_cache);
     cleaned = true;
   }
 
   LOG("After cleanup: {:.0f} KiB, {:.0f} files",
       static_cast<double>(cache_size) / 1024,
       static_cast<double>(files_in_cache));
+  Level2Counters counters_after{files_in_cache, cache_size};
 
   if (cleaned) {
-    LOG("Cleaned up cache directory {}", subdir);
+    LOG("Cleaned up cache directory {}", l2_dir);
   }
 
-  return SubdirCounters{files_in_cache, cache_size, cleaned ? 1U : 0U};
+  return {counters_before, counters_after};
 }
 
 FileType
@@ -298,83 +340,23 @@ LocalStorage::LocalStorage(const Config& config) : m_config(config)
 void
 LocalStorage::finalize()
 {
-  if (m_config.temporary_dir() == m_config.default_temporary_dir()) {
-    clean_internal_tempdir();
-  }
-
-  if (!m_config.stats()) {
-    return;
-  }
-
-  if (m_manifest_key) {
-    // A manifest entry was written.
-    ASSERT(!m_manifest_path.empty());
-    update_stats_and_maybe_move_cache_file(*m_manifest_key,
-                                           m_manifest_path,
-                                           m_manifest_counter_updates,
-                                           core::CacheEntryType::manifest);
-  }
-
-  if (!m_result_key) {
-    // No result entry was written, so we just choose one of the stats files in
-    // the 256 level 2 directories.
-
-    ASSERT(m_result_counter_updates.get(Statistic::cache_size_kibibyte) == 0);
-    ASSERT(m_result_counter_updates.get(Statistic::files_in_cache) == 0);
-
+  if (m_config.stats()) {
+    // Pseudo-randomly choose one of the stats files in the 256 level 2
+    // directories.
     const auto bucket = getpid() % 256;
     const auto stats_file =
       FMT("{}/{:x}/{:x}/stats", m_config.cache_dir(), bucket / 16, bucket % 16);
     StatsFile(stats_file).update([&](auto& cs) {
-      cs.increment(m_result_counter_updates);
+      cs.increment(m_counter_updates);
     });
-    return;
-  }
 
-  ASSERT(!m_result_path.empty());
-
-  const auto counters =
-    update_stats_and_maybe_move_cache_file(*m_result_key,
-                                           m_result_path,
-                                           m_result_counter_updates,
-                                           core::CacheEntryType::result);
-  if (!counters) {
-    return;
-  }
-
-  const auto subdir =
-    FMT("{}/{:x}", m_config.cache_dir(), m_result_key->bytes()[0] >> 4);
-  bool need_cleanup = false;
-
-  if (m_config.max_files() != 0
-      && counters->get(Statistic::files_in_cache) > m_config.max_files() / 16) {
-    LOG("Need to clean up {} since it holds {} files (limit: {} files)",
-        subdir,
-        counters->get(Statistic::files_in_cache),
-        m_config.max_files() / 16);
-    need_cleanup = true;
-  }
-  if (m_config.max_size() != 0
-      && counters->get(Statistic::cache_size_kibibyte)
-           > m_config.max_size() / 1024 / 16) {
-    LOG("Need to clean up {} since it holds {} KiB (limit: {} KiB)",
-        subdir,
-        counters->get(Statistic::cache_size_kibibyte),
-        m_config.max_size() / 1024 / 16);
-    need_cleanup = true;
+    if (m_stored_data) {
+      perform_automatic_cleanup();
+    }
   }
 
-  if (need_cleanup) {
-    const double factor = m_config.limit_multiple() / 16;
-    const uint64_t max_size = round(m_config.max_size() * factor);
-    const uint64_t max_files = round(m_config.max_files() * factor);
-    auto level_1_cs = clean_dir(subdir,
-                                max_size,
-                                max_files,
-                                std::nullopt,
-                                std::nullopt,
-                                [](double /*progress*/) {});
-    set_counters(subdir, level_1_cs);
+  if (m_config.temporary_dir() == m_config.default_temporary_dir()) {
+    clean_internal_tempdir();
   }
 }
 
@@ -404,10 +386,10 @@ LocalStorage::get(const Digest& key, const core::CacheEntryType type)
     LOG("No {} in local storage", key.to_string());
   }
 
-  increment_statistic(return_value ? core::Statistic::local_storage_read_hit
-                                   : core::Statistic::local_storage_read_miss);
+  increment_statistic(return_value ? Statistic::local_storage_read_hit
+                                   : Statistic::local_storage_read_miss);
   if (return_value && type == core::CacheEntryType::result) {
-    increment_statistic(core::Statistic::local_storage_hit);
+    increment_statistic(Statistic::local_storage_hit);
   }
 
   return return_value;
@@ -428,43 +410,49 @@ LocalStorage::put(const Digest& key,
     return;
   }
 
-  switch (type) {
-  case core::CacheEntryType::manifest:
-    m_manifest_key = key;
-    m_manifest_path = cache_file.path;
-    break;
-
-  case core::CacheEntryType::result:
-    m_result_key = key;
-    m_result_path = cache_file.path;
-    break;
-  }
+  auto l2_content_lock = get_level_2_content_lock(key);
 
   try {
-    increment_statistic(core::Statistic::local_storage_write);
     AtomicFile result_file(cache_file.path, AtomicFile::Mode::binary);
     result_file.write(value);
+    result_file.flush();
+    if (!l2_content_lock.acquire()) {
+      LOG("Not storing {} due to lock failure", cache_file.path);
+      return;
+    }
     result_file.commit();
   } catch (core::Error& e) {
     LOG("Failed to write to {}: {}", cache_file.path, e.what());
     return;
   }
 
+  LOG("Stored {} in local storage ({})", key.to_string(), cache_file.path);
+  m_stored_data = true;
+
+  if (!m_config.stats()) {
+    return;
+  }
+
+  increment_statistic(Statistic::local_storage_write);
+
   const auto new_stat = Stat::stat(cache_file.path, Stat::OnError::log);
   if (!new_stat) {
-    LOG("Failed to stat {}: {}", cache_file.path, strerror(errno));
     return;
   }
 
-  LOG("Stored {} in local storage ({})", key.to_string(), cache_file.path);
+  int64_t files_change = cache_file.stat ? 0 : 1;
+  int64_t size_change_kibibyte =
+    Util::size_change_kibibyte(cache_file.stat, new_stat);
+  auto counters =
+    increment_level_2_counters(key, files_change, size_change_kibibyte);
+
+  l2_content_lock.release();
+
+  if (!counters) {
+    return;
+  }
 
-  auto& counter_updates = (type == core::CacheEntryType::manifest)
-                            ? m_manifest_counter_updates
-                            : m_result_counter_updates;
-  counter_updates.increment(
-    Statistic::cache_size_kibibyte,
-    Util::size_change_kibibyte(cache_file.stat, new_stat));
-  counter_updates.increment(Statistic::files_in_cache, cache_file.stat ? 0 : 1);
+  move_to_wanted_cache_level(*counters, key, type, cache_file.path);
 
   // Make sure we have a CACHEDIR.TAG in the cache part of cache_dir. This can
   // be done almost anywhere, but we might as well do it near the end as we save
@@ -479,13 +467,24 @@ LocalStorage::remove(const Digest& key, const core::CacheEntryType type)
   MTR_SCOPE("local_storage", "remove");
 
   const auto cache_file = look_up_cache_file(key, type);
-  if (cache_file.stat) {
-    increment_statistic(core::Statistic::local_storage_write);
-    Util::unlink_safe(cache_file.path);
-    LOG("Removed {} from local storage ({})", key.to_string(), cache_file.path);
-  } else {
+  if (!cache_file.stat) {
     LOG("No {} to remove from local storage", key.to_string());
+    return;
   }
+
+  increment_statistic(Statistic::local_storage_write);
+
+  {
+    auto l2_content_lock = get_level_2_content_lock(key);
+    if (!l2_content_lock.acquire()) {
+      LOG("Not removing {} due to lock failure", cache_file.path);
+    }
+    Util::unlink_safe(cache_file.path);
+  }
+
+  LOG("Removed {} from local storage ({})", key.to_string(), cache_file.path);
+  increment_level_2_counters(
+    key, -1, -static_cast<int64_t>(cache_file.stat.size_on_disk() / 1024));
 }
 
 std::string
@@ -546,13 +545,17 @@ void
 LocalStorage::increment_statistic(const Statistic statistic,
                                   const int64_t value)
 {
-  m_result_counter_updates.increment(statistic, value);
+  if (m_config.stats()) {
+    m_counter_updates.increment(statistic, value);
+  }
 }
 
 void
-LocalStorage::increment_statistics(const core::StatisticsCounters& statistics)
+LocalStorage::increment_statistics(const StatisticsCounters& statistics)
 {
-  m_result_counter_updates.increment(statistics);
+  if (m_config.stats()) {
+    m_counter_updates.increment(statistics);
+  }
 }
 
 // Zero all statistics counters except those tracking cache size and number of
@@ -569,30 +572,30 @@ LocalStorage::zero_all_statistics()
         for (const auto statistic : zeroable_fields) {
           cs.set(statistic, 0);
         }
-        cs.set(core::Statistic::stats_zeroed_timestamp, now.sec());
+        cs.set(Statistic::stats_zeroed_timestamp, now.sec());
       });
     });
 }
 
 // Get statistics and last time of update for the whole local storage cache.
-std::pair<core::StatisticsCounters, util::TimePoint>
+std::pair<StatisticsCounters, util::TimePoint>
 LocalStorage::get_all_statistics() const
 {
-  core::StatisticsCounters counters;
+  StatisticsCounters counters;
   uint64_t zero_timestamp = 0;
   util::TimePoint last_updated;
 
   // Add up the stats in each directory.
   for_each_level_1_and_2_stats_file(
     m_config.cache_dir(), [&](const auto& path) {
-      counters.set(core::Statistic::stats_zeroed_timestamp, 0); // Don't add
+      counters.set(Statistic::stats_zeroed_timestamp, 0); // Don't add
       counters.increment(StatsFile(path).read());
-      zero_timestamp = std::max(
-        counters.get(core::Statistic::stats_zeroed_timestamp), zero_timestamp);
+      zero_timestamp = std::max(counters.get(Statistic::stats_zeroed_timestamp),
+                                zero_timestamp);
       last_updated = std::max(last_updated, Stat::stat(path).mtime());
     });
 
-  counters.set(core::Statistic::stats_zeroed_timestamp, zero_timestamp);
+  counters.set(Statistic::stats_zeroed_timestamp, zero_timestamp);
   return std::make_pair(counters, last_updated);
 }
 
@@ -601,80 +604,49 @@ LocalStorage::evict(const ProgressReceiver& progress_receiver,
                     std::optional<uint64_t> max_age,
                     std::optional<std::string> namespace_)
 {
-  for_each_cache_subdir(
-    m_config.cache_dir(),
-    progress_receiver,
-    [&](const auto& level_1_dir, const auto& level_1_progress_receiver) {
-      SubdirCounters counters;
-      for_each_cache_subdir(
-        level_1_dir,
-        level_1_progress_receiver,
-        [&](const std::string& level_2_dir,
-            const ProgressReceiver& level_2_progress_receiver) {
-          counters += clean_dir(
-            level_2_dir, 0, 0, max_age, namespace_, level_2_progress_receiver);
-        });
-
-      set_counters(level_1_dir, counters);
-    });
+  return do_clean_all(progress_receiver, 0, 0, max_age, namespace_);
 }
 
-// Clean up all cache subdirectories.
 void
 LocalStorage::clean_all(const ProgressReceiver& progress_receiver)
 {
-  for_each_cache_subdir(
-    m_config.cache_dir(),
-    progress_receiver,
-    [&](const auto& level_1_dir, const auto& level_1_progress_receiver) {
-      SubdirCounters counters;
-      for_each_cache_subdir(
-        level_1_dir,
-        level_1_progress_receiver,
-        [&](const std::string& level_2_dir,
-            const ProgressReceiver& level_2_progress_receiver) {
-          counters += clean_dir(level_2_dir,
-                                m_config.max_size() / 256,
-                                m_config.max_files() / 16,
-                                std::nullopt,
-                                std::nullopt,
-                                level_2_progress_receiver);
-        });
-      set_counters(level_1_dir, counters);
-    });
+  return do_clean_all(progress_receiver,
+                      m_config.max_size(),
+                      m_config.max_files(),
+                      std::nullopt,
+                      std::nullopt);
 }
 
 // Wipe all cached files in all subdirectories.
 void
 LocalStorage::wipe_all(const ProgressReceiver& progress_receiver)
 {
+  util::LongLivedLockFileManager lock_manager;
+
   for_each_cache_subdir(
-    m_config.cache_dir(),
-    progress_receiver,
-    [&](const auto& level_1_dir, const auto& level_1_progress_receiver) {
-      uint64_t cleanups = 0;
-      for_each_cache_subdir(
-        level_1_dir,
-        level_1_progress_receiver,
-        [&](const std::string& level_2_dir,
-            const ProgressReceiver& level_2_progress_receiver) {
-          LOG("Clearing out cache directory {}", level_2_dir);
+    progress_receiver, [&](uint8_t l1_index, const auto& l1_progress_receiver) {
+      auto acquired_locks =
+        acquire_all_level_2_content_locks(lock_manager, l1_index);
+      Level1Counters level_1_counters;
 
-          const auto files = get_cache_dir_files(level_2_dir);
-          level_2_progress_receiver(0.5);
+      for_each_cache_subdir(
+        l1_progress_receiver,
+        [&](uint8_t l2_index, const ProgressReceiver& l2_progress_receiver) {
+          auto l2_dir = get_subdir(l1_index, l2_index);
+          const auto files = get_cache_dir_files(l2_dir);
+          l2_progress_receiver(0.5);
 
           for (size_t i = 0; i < files.size(); ++i) {
             Util::unlink_safe(files[i].path());
-            level_2_progress_receiver(0.5 + 0.5 * i / files.size());
+            l2_progress_receiver(0.5 + 0.5 * i / files.size());
           }
 
           if (!files.empty()) {
-            ++cleanups;
-            LOG("Cleared out cache directory {}", level_2_dir);
+            ++level_1_counters.cleanups;
           }
         });
 
-      set_counters(level_1_dir, SubdirCounters{0, 0, cleanups});
+      set_counters(get_stats_file(l1_index), level_1_counters);
     });
 }
 
@@ -685,15 +657,14 @@ LocalStorage::get_compression_statistics(
   CompressionStatistics cs{};
 
   for_each_cache_subdir(
-    m_config.cache_dir(),
     progress_receiver,
-    [&](const auto& level_1_dir, const auto& level_1_progress_receiver) {
+    [&](const auto& l1_index, const auto& l1_progress_receiver) {
       for_each_cache_subdir(
-        level_1_dir,
-        level_1_progress_receiver,
-        [&](const auto& level_2_dir, const auto& level_2_progress_receiver) {
-          const auto files = get_cache_dir_files(level_2_dir);
-          level_2_progress_receiver(0.2);
+        l1_progress_receiver,
+        [&](const auto& l2_index, const auto& l2_progress_receiver) {
+          auto l2_dir = get_subdir(l1_index, l2_index);
+          const auto files = get_cache_dir_files(l2_dir);
+          l2_progress_receiver(0.2);
 
           for (size_t i = 0; i < files.size(); ++i) {
             const auto& cache_file = files[i];
@@ -705,7 +676,7 @@ LocalStorage::get_compression_statistics(
             } catch (core::Error&) {
               cs.incompr_size += cache_file.size();
             }
-            level_2_progress_receiver(0.2 + 0.8 * i / files.size());
+            l2_progress_receiver(0.2 + 0.8 * i / files.size());
           }
         });
     });
@@ -724,52 +695,60 @@ LocalStorage::recompress(const std::optional<int8_t> level,
   core::FileRecompressor recompressor;
 
   std::atomic<uint64_t> incompressible_size = 0;
+  util::LongLivedLockFileManager lock_manager;
 
   for_each_cache_subdir(
-    m_config.cache_dir(),
     progress_receiver,
-    [&](const auto& level_1_dir, const auto& level_1_progress_receiver) {
+    [&](const auto& l1_index, const auto& l1_progress_receiver) {
       for_each_cache_subdir(
-        level_1_dir,
-        level_1_progress_receiver,
-        [&](const auto& level_2_dir, const auto& level_2_progress_receiver) {
-          (void)level_2_progress_receiver;
-          (void)level_2_dir;
-          auto files = get_cache_dir_files(level_2_dir);
-          level_2_progress_receiver(0.1);
+        l1_progress_receiver,
+        [&](const auto& l2_index, const auto& l2_progress_receiver) {
+          auto l2_content_lock = get_level_2_content_lock(l1_index, l2_index);
+          l2_content_lock.make_long_lived(lock_manager);
+          if (!l2_content_lock.acquire()) {
+            LOG("Failed to acquire content lock for {}/{}", l1_index, l2_index);
+            return;
+          }
+
+          auto l2_dir = get_subdir(l1_index, l2_index);
+          auto files = get_cache_dir_files(l2_dir);
+          l2_progress_receiver(0.1);
 
-          auto stats_file = FMT("{}/stats", Util::dir_name(level_1_dir));
+          auto stats_file = get_stats_file(l1_index);
 
           for (size_t i = 0; i < files.size(); ++i) {
             const auto& file = files[i];
 
             if (file_type_from_path(file.path()) != FileType::unknown) {
-              thread_pool.enqueue(
-                [&recompressor, &incompressible_size, level, stats_file, file] {
-                  try {
-                    Stat new_stat = recompressor.recompress(
-                      file, level, core::FileRecompressor::KeepAtime::no);
-                    auto size_change_kibibyte =
-                      Util::size_change_kibibyte(file, new_stat);
-                    if (size_change_kibibyte != 0) {
-                      StatsFile(stats_file).update([=](auto& cs) {
-                        cs.increment(core::Statistic::cache_size_kibibyte,
-                                     size_change_kibibyte);
-                      });
-                    }
-                  } catch (core::Error&) {
-                    // Ignore for now.
-                    incompressible_size += file.size_on_disk();
+              thread_pool.enqueue([=, &recompressor, &incompressible_size] {
+                try {
+                  Stat new_stat = recompressor.recompress(
+                    file, level, core::FileRecompressor::KeepAtime::no);
+                  auto size_change_kibibyte =
+                    Util::size_change_kibibyte(file, new_stat);
+                  if (size_change_kibibyte != 0) {
+                    StatsFile(stats_file).update([=](auto& cs) {
+                      cs.increment(Statistic::cache_size_kibibyte,
+                                   size_change_kibibyte);
+                      cs.increment_offsetted(
+                        Statistic::subdir_size_kibibyte_base,
+                        l2_index,
+                        size_change_kibibyte);
+                    });
                   }
-                });
+                } catch (core::Error&) {
+                  // Ignore for now.
+                  incompressible_size += file.size_on_disk();
+                }
+              });
             } else if (!TemporaryFile::is_tmp_file(file.path())) {
               incompressible_size += file.size_on_disk();
             }
 
-            level_2_progress_receiver(0.1 + 0.9 * i / files.size());
+            l2_progress_receiver(0.1 + 0.9 * i / files.size());
           }
 
-          if (util::ends_with(level_2_dir, "f/f")) {
+          if (util::ends_with(l2_dir, "f/f")) {
             // Wait here instead of after for_each_cache_subdir to avoid
             // updating the progress bar to 100% before all work is done.
             thread_pool.shut_down();
@@ -836,6 +815,18 @@ LocalStorage::recompress(const std::optional<int8_t> level,
 
 // Private methods
 
+std::string
+LocalStorage::get_subdir(uint8_t l1_index) const
+{
+  return FMT("{}/{:x}", m_config.cache_dir(), l1_index);
+}
+
+std::string
+LocalStorage::get_subdir(uint8_t l1_index, uint8_t l2_index) const
+{
+  return FMT("{}/{:x}/{:x}", m_config.cache_dir(), l1_index, l2_index);
+}
+
 LocalStorage::LookUpCacheFileResult
 LocalStorage::look_up_cache_file(const Digest& key,
                                  const core::CacheEntryType type) const
@@ -856,6 +847,387 @@ LocalStorage::look_up_cache_file(const Digest& key,
   return {shallowest_path, Stat(), k_min_cache_levels};
 }
 
+StatsFile
+LocalStorage::get_stats_file(uint8_t l1_index) const
+{
+  return StatsFile(FMT("{}/{:x}/stats", m_config.cache_dir(), l1_index));
+}
+
+StatsFile
+LocalStorage::get_stats_file(uint8_t l1_index, uint8_t l2_index) const
+{
+  return StatsFile(
+    FMT("{}/{:x}/{:x}/stats", m_config.cache_dir(), l1_index, l2_index));
+}
+
+void
+LocalStorage::move_to_wanted_cache_level(const StatisticsCounters& counters,
+                                         const Digest& key,
+                                         core::CacheEntryType type,
+                                         const std::string& cache_file_path)
+{
+  const auto wanted_level =
+    calculate_wanted_cache_level(counters.get(Statistic::files_in_cache));
+  const auto wanted_path =
+    get_path_in_cache(wanted_level, key.to_string() + suffix_from_type(type));
+  if (cache_file_path != wanted_path) {
+    Util::ensure_dir_exists(Util::dir_name(wanted_path));
+    LOG("Moving {} to {}", cache_file_path, wanted_path);
+    try {
+      Util::rename(cache_file_path, wanted_path);
+    } catch (const core::Error&) {
+      // Two ccache processes may move the file at the same time, so failure
+      // to rename is OK.
+    }
+    for (const auto& raw_file : m_added_raw_files) {
+      try {
+        Util::rename(
+          raw_file,
+          FMT("{}/{}", Util::dir_name(wanted_path), Util::base_name(raw_file)));
+      } catch (const core::Error&) {
+        // Two ccache processes may move the file at the same time, so failure
+        // to rename is OK.
+      }
+    }
+  }
+}
+
+void
+LocalStorage::recount_level_1_dir(util::LongLivedLockFileManager& lock_manager,
+                                  uint8_t l1_index)
+{
+  auto acquired_locks =
+    acquire_all_level_2_content_locks(lock_manager, l1_index);
+  Level1Counters level_1_counters;
+
+  for_each_cache_subdir([&](uint8_t l2_index) {
+    auto files = get_cache_dir_files(get_subdir(l1_index, l2_index));
+    auto& level_2_counters = level_1_counters.level_2_counters[l2_index];
+    level_2_counters.files = files.size();
+    for (const auto& file : files) {
+      level_2_counters.size += file.size_on_disk();
+    }
+  });
+
+  set_counters(get_stats_file(l1_index), level_1_counters);
+}
+
+std::optional<core::StatisticsCounters>
+LocalStorage::increment_level_2_counters(const Digest& key,
+                                         int64_t files,
+                                         int64_t size_kibibyte)
+{
+  uint8_t l1_index = key.bytes()[0] >> 4;
+  uint8_t l2_index = key.bytes()[0] & 0xF;
+  const auto level_1_stats_file = get_stats_file(l1_index);
+  return level_1_stats_file.update([&](auto& cs) {
+    // Level 1 counters:
+    cs.increment(Statistic::files_in_cache, files);
+    cs.increment(Statistic::cache_size_kibibyte, size_kibibyte);
+
+    // Level 2 counters:
+    cs.increment_offsetted(Statistic::subdir_files_base, l2_index, files);
+    cs.increment_offsetted(
+      Statistic::subdir_size_kibibyte_base, l2_index, size_kibibyte);
+  });
+}
+
+static uint8_t
+get_largest_level_2_index(const StatisticsCounters& counters)
+{
+  uint64_t largest_level_2_files = 0;
+  uint8_t largest_level_2_index = 0;
+  for_each_cache_subdir([&](uint8_t i) {
+    uint64_t l2_files =
+      1024 * counters.get_offsetted(Statistic::subdir_files_base, i);
+    if (l2_files > largest_level_2_files) {
+      largest_level_2_files = l2_files;
+      largest_level_2_index = i;
+    }
+  });
+  return largest_level_2_index;
+}
+
+static bool
+has_consistent_counters(const StatisticsCounters& counters)
+{
+  uint64_t level_2_files = 0;
+  uint64_t level_2_size_kibibyte = 0;
+  for_each_cache_subdir([&](uint8_t i) {
+    level_2_files += counters.get_offsetted(Statistic::subdir_files_base, i);
+    level_2_size_kibibyte +=
+      counters.get_offsetted(Statistic::subdir_size_kibibyte_base, i);
+  });
+  return level_2_files == counters.get(Statistic::files_in_cache)
+         && level_2_size_kibibyte
+              == counters.get(Statistic::cache_size_kibibyte);
+}
+
+void
+LocalStorage::perform_automatic_cleanup()
+{
+  util::LongLivedLockFileManager lock_manager;
+  auto auto_cleanup_lock = get_auto_cleanup_lock();
+  if (!auto_cleanup_lock.try_acquire()) {
+    // Somebody else is already performing automatic cleanup.
+    return;
+  }
+
+  // Intentionally not acquiring content locks here to avoid write contention
+  // since precision is not important. It doesn't matter if some compilation
+  // sneaks in a new result during our calculation - if maximum cache size
+  // becomes exceeded it will be taken care of the next time instead.
+  auto evaluation = evaluate_cleanup();
+  if (!evaluation) {
+    // No cleanup needed.
+    return;
+  }
+
+  auto_cleanup_lock.make_long_lived(lock_manager);
+
+  if (!has_consistent_counters(evaluation->l1_counters)) {
+    LOG("Recounting {} due to inconsistent counters", evaluation->l1_path);
+    recount_level_1_dir(lock_manager, evaluation->l1_index);
+    evaluation->l1_counters = get_stats_file(evaluation->l1_index).read();
+  }
+
+  uint8_t largest_level_2_index =
+    get_largest_level_2_index(evaluation->l1_counters);
+
+  auto l2_content_lock =
+    get_level_2_content_lock(evaluation->l1_index, largest_level_2_index);
+  l2_content_lock.make_long_lived(lock_manager);
+  if (!l2_content_lock.acquire()) {
+    LOG("Failed to acquire content lock for {}/{}",
+        evaluation->l1_index,
+        largest_level_2_index);
+    return;
+  }
+
+  // Need to reread the counters again after acquiring the lock since another
+  // compilation may have modified the size since evaluation->l1_counters was
+  // read.
+  auto stats_file = get_stats_file(evaluation->l1_index);
+  auto counters = stats_file.read();
+  if (!has_consistent_counters(counters)) {
+    // The cache_size_kibibyte counters doesn't match the 16
+    // subdir_size_kibibyte_base+i counters. This should only happen if an older
+    // ccache version (before introduction of the subdir_size_kibibyte_base
+    // counters) has modified the cache size after the recount_level_1_dir call
+    // above. Bail out now and leave it to the next ccache invocation to clean
+    // up the inconsistency.
+    LOG("Inconsistent counters in {}, bailing out", evaluation->l1_path);
+    return;
+  }
+
+  // Since counting files and their sizes is costly, remove more than needed to
+  // amortize the cost. Trimming the directory down to 90% of the max size means
+  // that statistically about 20% of the directory content will be removed each
+  // automatic cleanup (since subdirectories will be between 90% and about 110%
+  // filled at steady state).
+  //
+  // We trim based on number of files instead of size. The main reason for this
+  // is to be more forgiving if there is one or a few large cache entries among
+  // many smaller. For example, say that there's a single 100 MB entry (maybe
+  // the result of a precompiled header) and lots of small 50 kB files as well.
+  // If the large file is the oldest in the subdirectory that is chosen for
+  // cleanup, only one file would be removed, thus wasting most of the effort of
+  // stat-ing all files. On the other hand, if the large file is the newest, all
+  // or a large portion of the other files would be removed on cleanup, thus in
+  // practice removing much newer entries than the oldest in other
+  // subdirectories. By doing cleanup based on the number of files, both example
+  // scenarios are improved.
+  const uint64_t target_files = 0.9 * evaluation->total_files / 256;
+
+  auto clean_dir_result = clean_dir(
+    get_subdir(evaluation->l1_index, largest_level_2_index), 0, target_files);
+
+  stats_file.update([&](auto& cs) {
+    const auto old_files =
+      cs.get_offsetted(Statistic::subdir_files_base, largest_level_2_index);
+    const auto old_size_kibibyte = cs.get_offsetted(
+      Statistic::subdir_size_kibibyte_base, largest_level_2_index);
+    const auto new_files = clean_dir_result.after.files;
+    const auto new_size_kibibyte = clean_dir_result.after.size / 1024;
+    const int64_t cleanups =
+      clean_dir_result.after.size != clean_dir_result.before.size ? 1 : 0;
+
+    cs.increment(Statistic::files_in_cache, new_files - old_files);
+    cs.increment(Statistic::cache_size_kibibyte,
+                 new_size_kibibyte - old_size_kibibyte);
+    cs.set_offsetted(
+      Statistic::subdir_files_base, largest_level_2_index, new_files);
+    cs.set_offsetted(Statistic::subdir_size_kibibyte_base,
+                     largest_level_2_index,
+                     new_size_kibibyte);
+    cs.increment(Statistic::cleanups_performed, cleanups);
+  });
+}
+
+void
+LocalStorage::do_clean_all(const ProgressReceiver& progress_receiver,
+                           uint64_t max_size,
+                           uint64_t max_files,
+                           std::optional<uint64_t> max_age,
+                           std::optional<std::string> namespace_)
+{
+  util::LongLivedLockFileManager lock_manager;
+
+  uint64_t current_size = 0;
+  uint64_t current_files = 0;
+  if (max_size > 0 || max_files > 0) {
+    for_each_cache_subdir([&](uint8_t i) {
+      auto counters = get_stats_file(i).read();
+      current_size += 1024 * counters.get(Statistic::cache_size_kibibyte);
+      current_files += counters.get(Statistic::files_in_cache);
+    });
+  }
+
+  for_each_cache_subdir(
+    progress_receiver, [&](uint8_t l1_index, const auto& l1_progress_receiver) {
+      auto acquired_locks =
+        acquire_all_level_2_content_locks(lock_manager, l1_index);
+      Level1Counters level_1_counters;
+
+      for_each_cache_subdir(
+        l1_progress_receiver,
+        [&](uint8_t l2_index, const ProgressReceiver& l2_progress_receiver) {
+          uint64_t level_2_max_size =
+            current_size > max_size ? max_size / 256 : 0;
+          uint64_t level_2_max_files =
+            current_files > max_files ? max_files / 256 : 0;
+          auto clean_dir_result = clean_dir(get_subdir(l1_index, l2_index),
+                                            level_2_max_size,
+                                            level_2_max_files,
+                                            max_age,
+                                            namespace_,
+                                            l2_progress_receiver);
+          uint64_t removed_size =
+            clean_dir_result.before.size - clean_dir_result.after.size;
+          uint64_t removed_files =
+            clean_dir_result.before.files - clean_dir_result.after.files;
+
+          // removed_size/remove_files should never be larger than
+          // current_size/current_files, but in case there's some error we
+          // certainly don't want to underflow, so better safe than sorry.
+          current_size -= std::min(removed_size, current_size);
+          current_files -= std::min(removed_files, current_files);
+
+          level_1_counters.level_2_counters[l2_index] = clean_dir_result.after;
+          if (clean_dir_result.after.files != clean_dir_result.before.files) {
+            ++level_1_counters.cleanups;
+          }
+        });
+
+      set_counters(get_stats_file(l1_index), level_1_counters);
+    });
+}
+
+std::optional<LocalStorage::EvaluateCleanupResult>
+LocalStorage::evaluate_cleanup()
+{
+  // We trust that the L1 size and files counters are correct, but the L2 size
+  // and files counters may be inconsistent if older ccache versions have been
+  // used. If all L2 counters are consistent, we choose the L1 directory with
+  // the largest L2 directory, otherwise we just choose the largest L1 directory
+  // since we can't trust the L2 counters.
+
+  std::vector<StatisticsCounters> counters;
+  counters.reserve(16);
+  for_each_cache_subdir([&](uint8_t l1_index) {
+    counters.emplace_back(get_stats_file(l1_index).read());
+  });
+  ASSERT(counters.size() == 16);
+
+  uint64_t largest_l1_dir_files = 0;
+  uint64_t largest_l2_dir_files = 0;
+  uint8_t largest_l1_dir = 0;
+  uint8_t l1_dir_with_largest_l2 = 0;
+  uint8_t largest_l2_dir = 0;
+  bool l2_counters_consistent = true;
+  uint64_t total_files = 0;
+  uint64_t total_size = 0;
+  for_each_cache_subdir([&](uint8_t i) {
+    auto l1_files = counters[i].get(Statistic::files_in_cache);
+    auto l1_size = 1024 * counters[i].get(Statistic::cache_size_kibibyte);
+    total_files += l1_files;
+    total_size += l1_size;
+    if (l1_files > largest_l1_dir_files) {
+      largest_l1_dir_files = l1_files;
+      largest_l1_dir = i;
+    }
+
+    if (l2_counters_consistent && has_consistent_counters(counters[i])) {
+      for_each_cache_subdir([&](uint8_t j) {
+        auto l2_files =
+          counters[i].get_offsetted(Statistic::subdir_files_base, j);
+        if (l2_files > largest_l2_dir_files) {
+          largest_l2_dir_files = l2_files;
+          l1_dir_with_largest_l2 = i;
+          largest_l2_dir = j;
+        }
+      });
+    } else {
+      l2_counters_consistent = false;
+    }
+  });
+
+  std::string max_size_str =
+    m_config.max_size() > 0 ? FMT(
+      ", max size {}", Util::format_human_readable_size(m_config.max_size()))
+                            : "";
+  std::string max_files_str =
+    m_config.max_files() > 0 ? FMT(", max files {}", m_config.max_files()) : "";
+  std::string info_str = FMT("size {}, files {}{}{}",
+                             Util::format_human_readable_size(total_size),
+                             total_files,
+                             max_size_str,
+                             max_files_str);
+  if ((m_config.max_size() == 0 || total_size <= m_config.max_size())
+      && (m_config.max_files() == 0 || total_files <= m_config.max_files())) {
+    LOG("No automatic cleanup needed ({})", info_str);
+    return std::nullopt;
+  }
+
+  LOG("Need to clean up local cache ({})", info_str);
+
+  uint8_t chosen_l1_dir =
+    l2_counters_consistent ? l1_dir_with_largest_l2 : largest_l1_dir;
+  auto largest_level_1_dir_path = get_subdir(chosen_l1_dir);
+  LOG("Choosing {} for cleanup (counters {}, files {}{})",
+      largest_level_1_dir_path,
+      has_consistent_counters(counters[chosen_l1_dir]) ? "consistent"
+                                                       : "inconsistent",
+      largest_l1_dir_files,
+      l2_counters_consistent
+        ? FMT(", subdir {:x} files {}", largest_l2_dir, largest_l2_dir_files)
+        : std::string());
+
+  return EvaluateCleanupResult{chosen_l1_dir,
+                               largest_level_1_dir_path,
+                               counters[chosen_l1_dir],
+                               total_files};
+}
+
+std::vector<util::LockFile>
+LocalStorage::acquire_all_level_2_content_locks(
+  util::LongLivedLockFileManager& lock_manager, uint8_t l1_index)
+{
+  std::vector<util::LockFile> locks;
+
+  for_each_cache_subdir([&](uint8_t l2_index) {
+    auto lock = get_level_2_content_lock(l1_index, l2_index);
+    lock.make_long_lived(lock_manager);
+
+    // Not much to do on failure except treating the lock as acquired.
+    (void)lock.acquire();
+
+    locks.push_back(std::move(lock));
+  });
+
+  return locks;
+}
+
 void
 LocalStorage::clean_internal_tempdir()
 {
@@ -886,70 +1258,6 @@ LocalStorage::clean_internal_tempdir()
   util::write_file(cleaned_stamp, "");
 }
 
-std::optional<core::StatisticsCounters>
-LocalStorage::update_stats_and_maybe_move_cache_file(
-  const Digest& key,
-  const std::string& current_path,
-  const core::StatisticsCounters& counter_updates,
-  const core::CacheEntryType type)
-{
-  if (counter_updates.all_zero()) {
-    return std::nullopt;
-  }
-
-  // Use stats file in the level one subdirectory for cache bookkeeping counters
-  // since cleanup is performed on level one. Use stats file in the level two
-  // subdirectory for other counters to reduce lock contention.
-  const bool use_stats_on_level_1 =
-    counter_updates.get(Statistic::cache_size_kibibyte) != 0
-    || counter_updates.get(Statistic::files_in_cache) != 0;
-  std::string level_string = FMT("{:x}", key.bytes()[0] >> 4);
-  if (!use_stats_on_level_1) {
-    level_string += FMT("/{:x}", key.bytes()[0] & 0xF);
-  }
-
-  const auto stats_file =
-    FMT("{}/{}/stats", m_config.cache_dir(), level_string);
-  auto counters = StatsFile(stats_file).update([&counter_updates](auto& cs) {
-    cs.increment(counter_updates);
-  });
-  if (!counters) {
-    return std::nullopt;
-  }
-
-  if (use_stats_on_level_1) {
-    // Only consider moving the cache file to another level when we have read
-    // the level 1 stats file since it's only then we know the proper
-    // files_in_cache value.
-    const auto wanted_level =
-      calculate_wanted_cache_level(counters->get(Statistic::files_in_cache));
-    const auto wanted_path =
-      get_path_in_cache(wanted_level, key.to_string() + suffix_from_type(type));
-    if (current_path != wanted_path) {
-      Util::ensure_dir_exists(Util::dir_name(wanted_path));
-      LOG("Moving {} to {}", current_path, wanted_path);
-      try {
-        Util::rename(current_path, wanted_path);
-      } catch (const core::Error&) {
-        // Two ccache processes may move the file at the same time, so failure
-        // to rename is OK.
-      }
-      for (const auto& raw_file : m_added_raw_files) {
-        try {
-          Util::rename(raw_file,
-                       FMT("{}/{}",
-                           Util::dir_name(wanted_path),
-                           Util::base_name(raw_file)));
-        } catch (const core::Error&) {
-          // Two ccache processes may move the file at the same time, so failure
-          // to rename is OK.
-        }
-      }
-    }
-  }
-  return counters;
-}
-
 std::string
 LocalStorage::get_path_in_cache(const uint8_t level,
                                 const std::string_view name) const
@@ -972,4 +1280,31 @@ LocalStorage::get_path_in_cache(const uint8_t level,
   return path;
 }
 
+std::string
+LocalStorage::get_lock_path(const std::string& name) const
+{
+  auto path = FMT("{}/lock/{}", m_config.cache_dir(), name);
+  Util::ensure_dir_exists(Util::dir_name(path));
+  return path;
+}
+
+util::LockFile
+LocalStorage::get_auto_cleanup_lock() const
+{
+  return util::LockFile(get_lock_path("auto_cleanup"));
+}
+
+util::LockFile
+LocalStorage::get_level_2_content_lock(const Digest& key) const
+{
+  return get_level_2_content_lock(key.bytes()[0] >> 4, key.bytes()[0] & 0xF);
+}
+
+util::LockFile
+LocalStorage::get_level_2_content_lock(uint8_t l1_index, uint8_t l2_index) const
+{
+  return util::LockFile(
+    get_lock_path(FMT("subdir_{:x}{:x}", l1_index, l2_index)));
+}
+
 } // namespace storage::local
index e79e64786d9dc82ceeb75f5dffd24ae7dfc147a5..4989ef09f91c78a6c9af08c0c9e76e17d3de5bfb 100644 (file)
 #include <core/Result.hpp>
 #include <core/StatisticsCounters.hpp>
 #include <core/types.hpp>
+#include <storage/local/StatsFile.hpp>
 #include <storage/local/util.hpp>
 #include <storage/types.hpp>
 #include <util/Bytes.hpp>
+#include <util/LockFile.hpp>
 #include <util/TimePoint.hpp>
 
 #include <third_party/nonstd/span.hpp>
@@ -36,8 +38,7 @@
 
 class Config;
 
-namespace storage {
-namespace local {
+namespace storage::local {
 
 struct CompressionStatistics
 {
@@ -115,23 +116,12 @@ public:
 private:
   const Config& m_config;
 
-  // Main statistics updates (result statistics and size/count change for result
-  // file) which get written into the statistics file belonging to the result
-  // file.
-  core::StatisticsCounters m_result_counter_updates;
-
-  // Statistics updates (only for manifest size/count change) which get written
-  // into the statistics file belonging to the manifest.
-  core::StatisticsCounters m_manifest_counter_updates;
-
-  // The manifest and result keys and paths are stored by put() so that
-  // finalize() can use them to move the files in place.
-  std::optional<Digest> m_manifest_key;
-  std::optional<Digest> m_result_key;
-  std::string m_manifest_path;
-  std::string m_result_path;
+  // Statistics updates (excluding size/count changes) that will get written to
+  // a statistics file in the finalize method.
+  core::StatisticsCounters m_counter_updates;
 
   std::vector<std::string> m_added_raw_files;
+  bool m_stored_data = false;
 
   struct LookUpCacheFileResult
   {
@@ -143,19 +133,63 @@ private:
   LookUpCacheFileResult look_up_cache_file(const Digest& key,
                                            core::CacheEntryType type) const;
 
-  void clean_internal_tempdir();
+  std::string get_subdir(uint8_t l1_index) const;
+  std::string get_subdir(uint8_t l1_index, uint8_t l2_index) const;
+
+  StatsFile get_stats_file(uint8_t l1_index) const;
+  StatsFile get_stats_file(uint8_t l1_index, uint8_t l2_index) const;
+
+  void move_to_wanted_cache_level(const core::StatisticsCounters& counters,
+                                  const Digest& key,
+                                  core::CacheEntryType type,
+                                  const std::string& cache_file_path);
+
+  void recount_level_1_dir(util::LongLivedLockFileManager& lock_manager,
+                           uint8_t l1_index);
+
+  std::optional<core::StatisticsCounters> increment_level_2_counters(
+    const Digest& key, int64_t files, int64_t size_kibibyte);
+
+  void perform_automatic_cleanup();
+
+  void do_clean_all(const ProgressReceiver& progress_receiver,
+                    uint64_t max_size,
+                    uint64_t max_files,
+                    std::optional<uint64_t> max_age,
+                    std::optional<std::string> namespace_);
 
-  std::optional<core::StatisticsCounters>
-  update_stats_and_maybe_move_cache_file(
-    const Digest& key,
-    const std::string& current_path,
-    const core::StatisticsCounters& counter_updates,
-    core::CacheEntryType type);
+  struct EvaluateCleanupResult
+  {
+    uint8_t l1_index;
+    std::string l1_path;
+    core::StatisticsCounters l1_counters;
+    uint64_t total_files;
+  };
+
+  std::optional<EvaluateCleanupResult> evaluate_cleanup();
+
+  std::vector<util::LockFile> acquire_all_level_2_content_locks(
+    util::LongLivedLockFileManager& lock_manager, uint8_t l1_index);
+
+  void clean_internal_tempdir();
 
   // Join the cache directory, a '/' and `name` into a single path and return
   // it. Additionally, `level` single-character, '/'-separated subpaths are
   // split from the beginning of `name` before joining them all.
   std::string get_path_in_cache(uint8_t level, std::string_view name) const;
+
+  std::string get_lock_path(const std::string& name) const;
+
+  util::LockFile get_auto_cleanup_lock() const;
+
+  // A level 2 content lock grants exclusive access to a level 2 directory in
+  // the cache. It must be acquired before adding, removing or recounting files
+  // in the directory (including any subdirectories). However, the lock does not
+  // have to be acquired to update a level 2 stats file since level 2 content
+  // size and file count are stored in the parent (level 1) stats file.
+  util::LockFile get_level_2_content_lock(const Digest& key) const;
+  util::LockFile get_level_2_content_lock(uint8_t l1_index,
+                                          uint8_t l2_index) const;
 };
 
 // --- Inline implementations ---
@@ -163,8 +197,7 @@ private:
 inline const core::StatisticsCounters&
 LocalStorage::get_statistics_updates() const
 {
-  return m_result_counter_updates;
+  return m_counter_updates;
 }
 
-} // namespace local
-} // namespace storage
+} // namespace storage::local
index 8f2c7eca8076382ed516cc73491dc4c582e946f7..f994daef872d42c9e288c9fc4afc5527b34b17b2 100644 (file)
 namespace storage::local {
 
 void
-for_each_cache_subdir(const std::string& cache_dir,
-                      const ProgressReceiver& progress_receiver,
-                      const SubdirVisitor& visitor)
+for_each_cache_subdir(const SubdirVisitor& visitor)
+{
+  for (uint8_t i = 0; i < 16; ++i) {
+    visitor(i);
+  }
+}
+
+void
+for_each_cache_subdir(const ProgressReceiver& progress_receiver,
+                      const SubdirProgressVisitor& visitor)
 {
   for (uint8_t i = 0; i < 16; ++i) {
     double progress = i / 16.0;
     progress_receiver(progress);
-    std::string subdir_path = FMT("{}/{:x}", cache_dir, i);
-    visitor(subdir_path, [&](double inner_progress) {
+    visitor(i, [&](double inner_progress) {
       progress_receiver(progress + inner_progress / 16);
     });
   }
index 084d725586700fe63c667f650aa45f61f4e9cfa9..ed2ada401f0e347216f09ff926a5a61161b097af 100644 (file)
 namespace storage::local {
 
 using ProgressReceiver = std::function<void(double progress)>;
-using SubdirVisitor = std::function<void(
-  const std::string& dir_path, const ProgressReceiver& progress_receiver)>;
+using SubdirVisitor = std::function<void(uint8_t subdir_index)>;
+using SubdirProgressVisitor = std::function<void(
+  uint8_t subdir_index, const ProgressReceiver& progress_receiver)>;
 
 // Call `visitor` for each subdirectory (0-9a-f) in `cache_dir`.
-void for_each_cache_subdir(const std::string& cache_dir,
-                           const ProgressReceiver& progress_receiver,
-                           const SubdirVisitor& visitor);
+void for_each_cache_subdir(const SubdirVisitor& visitor);
+void for_each_cache_subdir(const ProgressReceiver& progress_receiver,
+                           const SubdirProgressVisitor& visitor);
 
 void for_each_level_1_and_2_stats_file(
   const std::string& cache_dir,
index 68286c99eba3ada7bbcdc062ea58156283a434d7..62fc535978031cc7425ad7a9e4c80d39371400f0 100644 (file)
@@ -51,26 +51,27 @@ main(int argc, char** argv)
 
   util::LongLivedLockFileManager lock_manager;
   util::LockFile lock(path);
+  bool acquired = false;
   if (blocking) {
     PRINT_RAW(stdout, "Acquiring\n");
-    lock.acquire();
+    acquired = lock.acquire();
   } else {
     PRINT_RAW(stdout, "Trying to acquire\n");
-    lock.try_acquire();
+    acquired = lock.try_acquire();
   }
-  if (lock.acquired()) {
-    if (long_lived) {
-      lock.make_long_lived(lock_manager);
-    }
-    PRINT_RAW(stdout, "Acquired\n");
-    PRINT(stdout, "Sleeping {} second{}\n", *seconds, *seconds == 1 ? "" : "s");
-    std::this_thread::sleep_for(std::chrono::seconds{*seconds});
-  } else {
+
+  if (!acquired) {
     PRINT(stdout, "{} acquire\n", blocking ? "Failed to" : "Did not");
+    return 1;
   }
-  if (lock.acquired()) {
-    PRINT_RAW(stdout, "Releasing\n");
-    lock.release();
-    PRINT_RAW(stdout, "Released\n");
+
+  PRINT_RAW(stdout, "Acquired\n");
+  if (long_lived) {
+    lock.make_long_lived(lock_manager);
   }
+  PRINT(stdout, "Sleeping {} second{}\n", *seconds, *seconds == 1 ? "" : "s");
+  std::this_thread::sleep_for(std::chrono::seconds{*seconds});
+  PRINT_RAW(stdout, "Releasing\n");
+  lock.release();
+  PRINT_RAW(stdout, "Released\n");
 }
index 67d1cb043fe884457aeb4d2bf19f266466688c65..865b9ddcd5662cf476eea37e688fa20ec2ec0438 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2020-2022 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2023 Joel Rosdahl and other contributors
 //
 // See doc/AUTHORS.adoc for a complete list of contributors.
 //
@@ -46,10 +46,10 @@ public:
   void make_long_lived(LongLivedLockFileManager& lock_manager);
 
   // Acquire lock, blocking. Returns true if acquired, otherwise false.
-  bool acquire();
+  [[nodiscard]] bool acquire();
 
   // Acquire lock, non-blocking. Returns true if acquired, otherwise false.
-  bool try_acquire();
+  [[nodiscard]] bool try_acquire();
 
   // Release lock early. If not previously acquired, nothing happens.
   void release();
index 71dc0eb3d66b122e588ab8cea09b0c3e47754f78..4c2d87d9d1053e4894705d33d4ecc6870d0f9496 100644 (file)
@@ -30,10 +30,6 @@ std::chrono::milliseconds k_keep_alive_interval{500};
 
 namespace util {
 
-LongLivedLockFileManager::LongLivedLockFileManager()
-{
-}
-
 LongLivedLockFileManager::~LongLivedLockFileManager()
 {
 #ifndef _WIN32
index 5b118cec2039d3d7ff309a58cb12105f8c7fa6b4..212097358d8aad8f49ae6738e4428cf40eaa19a7 100644 (file)
@@ -31,7 +31,7 @@ namespace util {
 class LongLivedLockFileManager : NonCopyable
 {
 public:
-  LongLivedLockFileManager();
+  LongLivedLockFileManager() = default;
   ~LongLivedLockFileManager();
 
   void register_alive_file(const std::string& path);
index a765bfca243a145da651aa87c91282d4baf47e64..53189f268f3c2b83fefdad74ddb922cf84525c7e 100644 (file)
+SUITE_cleanup_PROBE() {
+    # NOTE: This test suite is known to fail on filesystems that have unusual
+    # block sizes, including ecryptfs. The workaround is to place the test
+    # directory elsewhere:
+    #
+    #     cd /tmp
+    #     CCACHE=$DIR/ccache $DIR/test.sh
+    if [ -z "$ENABLE_CACHE_CLEANUP_TESTS" ]; then
+        echo "ENABLE_CACHE_CLEANUP_TESTS is not set"
+    fi
+}
+
 SUITE_cleanup_SETUP() {
-    local l1_dir=$CCACHE_DIR/a
-    local l2_dir=$l1_dir/b
-    local i
-
-    rm -rf $l2_dir
-    mkdir -p $l2_dir
-    for ((i = 0; i < 10; ++i)); do
-        printf 'A%.0s' {1..4017} >$l2_dir/result${i}R
-        backdate $((3 * i + 1)) $l2_dir/result${i}R
+    mkdir -p $CCACHE_DIR/0/0
+    printf 'A%.0s' {1..4017} >"$CCACHE_DIR/0/0/result0R"
+    backdate "$CCACHE_DIR/0/0/result0R"
+    for ((i = 1; i < 10; ++i )); do
+        cp -a "$CCACHE_DIR/0/0/result0R" "$CCACHE_DIR/0/0/result${i}R"
+    done
+
+    subdirs=(1 2 3 4 5 6 7 8 9 a b c d e f)
+    for c in "${subdirs[@]}"; do
+        cp -a "$CCACHE_DIR/0/0" "$CCACHE_DIR/0/${c}"
+    done
+
+    for c in "${subdirs[@]}"; do
+        cp -a "$CCACHE_DIR/0" "$CCACHE_DIR/${c}"
     done
-    # NUMFILES: 10, TOTALSIZE: 13 KiB, MAXFILES: 0, MAXSIZE: 0
-    echo "0 0 0 0 0 0 0 0 0 0 0 10 13 0 0" >$l1_dir/stats
+
+    $CCACHE -c >/dev/null
+
+    # We have now created 16 * 16 * 10 = 2560 files, each 4017 bytes big (4096
+    # bytes on disk), totalling (counting disk blocks) 2560 * 4096 = 10 MiB =
+    # 10240 KiB.
 }
 
 SUITE_cleanup() {
     # -------------------------------------------------------------------------
     TEST "Clear cache"
 
+    expect_stat cleanups_performed 0
     $CCACHE -C >/dev/null
     expect_file_count 0 '*R' $CCACHE_DIR
     expect_stat files_in_cache 0
-    expect_stat cleanups_performed 1
+    expect_stat cleanups_performed 256
 
     # -------------------------------------------------------------------------
-    TEST "Forced cache cleanup, no limits"
+    TEST "Forced cache cleanup, no size limit"
 
-    $CCACHE -F 0 -M 0 >/dev/null
-    $CCACHE -c >/dev/null
-    expect_file_count 10 '*R' $CCACHE_DIR
-    expect_stat files_in_cache 10
+    $CCACHE -M 0 -c >/dev/null
+    expect_file_count 2560 '*R' $CCACHE_DIR
+    expect_stat files_in_cache 2560
     expect_stat cleanups_performed 0
 
     # -------------------------------------------------------------------------
     TEST "Forced cache cleanup, file limit"
 
-    # No cleanup needed.
-    #
-    # 10 * 16 = 160
-    $CCACHE -F 160 -M 0 >/dev/null
-    $CCACHE -c >/dev/null
-    expect_file_count 10 '*R' $CCACHE_DIR
-    expect_stat files_in_cache 10
-    expect_stat cleanups_performed 0
+    $CCACHE -F 2543 -c >/dev/null
 
-    # Reduce file limit
-    #
-    # 7 * 16 = 112
-    $CCACHE -F 112 -M 0 >/dev/null
-    $CCACHE -c >/dev/null
-    expect_file_count 7 '*R' $CCACHE_DIR
-    expect_stat files_in_cache 7
-    expect_stat cleanups_performed 1
-    for i in 0 1 2; do
-        file=$CCACHE_DIR/a/b/result${i}R
-        expect_missing $CCACHE_DIR/a/result${i}R
-    done
-    for i in 3 4 5 6 7 8 9; do
-        file=$CCACHE_DIR/a/b/result${i}R
-        expect_exists $file
-    done
+    expect_file_count 2543 '*R' $CCACHE_DIR
+    expect_stat files_in_cache 2543
+    expect_stat cleanups_performed 17
 
     # -------------------------------------------------------------------------
-    if [ -n "$ENABLE_CACHE_CLEANUP_TESTS" ]; then
-        TEST "Forced cache cleanup, size limit"
-
-        # NOTE: This test is known to fail on filesystems that have unusual block
-        # sizes, including ecryptfs. The workaround is to place the test directory
-        # elsewhere:
-        #
-        #     cd /tmp
-        #     CCACHE=$DIR/ccache $DIR/test.sh
-
-        $CCACHE -F 0 -M 4096K >/dev/null
-        $CCACHE -c >/dev/null
-        expect_file_count 3 '*R' $CCACHE_DIR
-        expect_stat files_in_cache 3
-        expect_stat cleanups_performed 1
-        for i in 0 1 2 3 4 5 6; do
-            file=$CCACHE_DIR/a/b/result${i}R
-            expect_missing $file
-        done
-        for i in 7 8 9; do
-            file=$CCACHE_DIR/a/b/result${i}R
-            expect_exists $file
-        done
-    fi
+    TEST "Forced cache cleanup, size limit"
+
+    # 10240 KiB - 10230 KiB = 10 KiB, so we need to remove 3 files of 4 KiB byte
+    # to get under the limit. Each cleanup only removes one file since there are
+    # only 10 files in each directory, so there are 3 cleanups.
+    $CCACHE -M 10230KiB -c >/dev/null
+
+    expect_file_count 2557 '*R' $CCACHE_DIR
+    expect_stat files_in_cache 2557
+    expect_stat cleanups_performed 3
 
     # -------------------------------------------------------------------------
-    TEST "No cleanup of new unknown file"
+    TEST "Automatic cache cleanup, file limit"
 
-    touch $CCACHE_DIR/a/b/abcd.unknown
-    $CCACHE -F 0 -M 0 -c >/dev/null # update counters
-    expect_stat files_in_cache 11
+    $CCACHE -F 2543 >/dev/null
 
-    $CCACHE -F 160 -M 0 >/dev/null
-    $CCACHE -c >/dev/null
-    expect_exists $CCACHE_DIR/a/b/abcd.unknown
-    expect_stat files_in_cache 10
+    touch test.c
+    $CCACHE_COMPILE -c test.c
+    expect_stat files_in_cache 2559
+    expect_stat cleanups_performed 1
 
     # -------------------------------------------------------------------------
-    TEST "Cleanup of old unknown file"
+    TEST "Automatic cache cleanup, size limit"
 
-    $CCACHE -F 160 -M 0 >/dev/null
-    touch $CCACHE_DIR/a/b/abcd.unknown
-    backdate $CCACHE_DIR/a/b/abcd.unknown
-    $CCACHE -F 0 -M 0 -c >/dev/null # update counters
-    expect_stat files_in_cache 11
+    $CCACHE -M 10230KiB >/dev/null
 
-    $CCACHE -F 160 -M 0 -c >/dev/null
-    expect_missing $CCACHE_DIR/a/b/abcd.unknown
-    expect_stat files_in_cache 10
+    # Automatic cleanup triggers one cleanup. The directory where the result
+    # ended up will have 11 files and will be trimmed down to floor(0.9 * 2561 /
+    # 256) = 9 files.
+
+    touch test.c
+    $CCACHE_COMPILE -c test.c
+    expect_stat files_in_cache 2559
+    expect_stat cleanups_performed 1
 
     # -------------------------------------------------------------------------
     TEST "Cleanup of tmp file"
 
-    mkdir -p $CCACHE_DIR/a
-    touch $CCACHE_DIR/a/b/abcd.tmp.efgh
+    mkdir -p $CCACHE_DIR/a/a
+    touch $CCACHE_DIR/a/a/abcd.tmp.efgh
     $CCACHE -c >/dev/null # update counters
-    expect_stat files_in_cache 11
-    backdate $CCACHE_DIR/a/b/abcd.tmp.efgh
+    expect_stat files_in_cache 2561
+
+    backdate $CCACHE_DIR/a/a/abcd.tmp.efgh
     $CCACHE -c >/dev/null
-    expect_missing $CCACHE_DIR/a/b/abcd.tmp.efgh
-    expect_stat files_in_cache 10
+    expect_missing $CCACHE_DIR/a/a/abcd.tmp.efgh
+    expect_stat files_in_cache 2560
 
     # -------------------------------------------------------------------------
     TEST "No cleanup of .nfs* files"
 
-    touch $CCACHE_DIR/a/.nfs0123456789
-    $CCACHE -F 0 -M 0 >/dev/null
+    mkdir -p $CCACHE_DIR/a/a
+    touch $CCACHE_DIR/a/a/.nfs0123456789
     $CCACHE -c >/dev/null
     expect_file_count 1 '.nfs*' $CCACHE_DIR
-    expect_stat files_in_cache 10
+    expect_stat files_in_cache 2560
 
     # -------------------------------------------------------------------------
     TEST "Cleanup of old files by age"
 
-    touch $CCACHE_DIR/a/b/nowR
-    $CCACHE -F 0 -M 0 >/dev/null
+    mkdir -p $CCACHE_DIR/a/a
+    touch $CCACHE_DIR/a/a/nowR
 
     $CCACHE --evict-older-than 1d >/dev/null
     expect_file_count 1 '*R' $CCACHE_DIR
@@ -146,7 +133,7 @@ SUITE_cleanup() {
     expect_file_count 1 '*R' $CCACHE_DIR
     expect_stat files_in_cache 1
 
-    backdate $CCACHE_DIR/a/b/nowR
+    backdate $CCACHE_DIR/a/a/nowR
     $CCACHE --evict-older-than 10s  >/dev/null
     expect_stat files_in_cache 0
 }
index 70a66dd28c908a1a9f13aad778a833233613c012..3bf27d19b3ca67dbe173447328f61945231f24c8 100644 (file)
@@ -60,7 +60,6 @@ TEST_CASE("Config: default values")
   CHECK(config.ignore_headers_in_manifest().empty());
   CHECK(config.ignore_options().empty());
   CHECK_FALSE(config.keep_comments_cpp());
-  CHECK(config.limit_multiple() == Approx(0.8));
   CHECK(config.log_file().empty());
   CHECK(config.max_files() == 0);
   CHECK(config.max_size() == static_cast<uint64_t>(5) * 1000 * 1000 * 1000);
@@ -119,7 +118,6 @@ TEST_CASE("Config::update_from_file")
     "ignore_headers_in_manifest = a:b/c\n"
     "ignore_options = -a=* -b\n"
     "keep_comments_cpp = true\n"
-    "limit_multiple = 1.0\n"
     "log_file = $USER${USER} \n"
     "max_files = 17\n"
     "max_size = 123M\n"
@@ -161,7 +159,6 @@ TEST_CASE("Config::update_from_file")
   CHECK(config.ignore_headers_in_manifest() == "a:b/c");
   CHECK(config.ignore_options() == "-a=* -b");
   CHECK(config.keep_comments_cpp());
-  CHECK(config.limit_multiple() == Approx(1.0));
   CHECK(config.log_file() == FMT("{0}{0}", user));
   CHECK(config.max_files() == 17);
   CHECK(config.max_size() == 123 * 1000 * 1000);
@@ -406,7 +403,6 @@ TEST_CASE("Config::visit_items")
     "ignore_options = -a=* -b\n"
     "inode_cache = false\n"
     "keep_comments_cpp = true\n"
-    "limit_multiple = 0.0\n"
     "log_file = lf\n"
     "max_files = 4711\n"
     "max_size = 98.7M\n"
@@ -468,7 +464,6 @@ TEST_CASE("Config::visit_items")
     "(test.conf) ignore_options = -a=* -b",
     "(test.conf) inode_cache = false",
     "(test.conf) keep_comments_cpp = true",
-    "(test.conf) limit_multiple = 0.0",
     "(test.conf) log_file = lf",
     "(test.conf) max_files = 4711",
     "(test.conf) max_size = 98.7M",
index d9c86d18960c2b7ccc6c0f99c8bb5249a5b5e3d1..40b3e6f83fc50ee6af99669ea9c9574addcddae1 100644 (file)
@@ -44,30 +44,13 @@ TEST_SUITE_BEGIN("storage::local::util");
 
 TEST_CASE("storage::local::for_each_cache_subdir")
 {
-  std::vector<std::string> actual;
+  std::vector<uint8_t> actual;
   storage::local::for_each_cache_subdir(
-    "cache_dir",
     [](double) {},
-    [&](const auto& subdir, const auto&) { actual.push_back(subdir); });
-
-  std::vector<std::string> expected = {
-    "cache_dir/0",
-    "cache_dir/1",
-    "cache_dir/2",
-    "cache_dir/3",
-    "cache_dir/4",
-    "cache_dir/5",
-    "cache_dir/6",
-    "cache_dir/7",
-    "cache_dir/8",
-    "cache_dir/9",
-    "cache_dir/a",
-    "cache_dir/b",
-    "cache_dir/c",
-    "cache_dir/d",
-    "cache_dir/e",
-    "cache_dir/f",
-  };
+    [&](uint8_t index, const auto&) { actual.push_back(index); });
+
+  std::vector<uint8_t> expected = {
+    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
   CHECK(actual == expected);
 }
 
index 6a91841c7bf133061bbbb9816c83221fcd29d034..c5c30d9c8c08b38b3dc87e5446365475ca9fbc0d 100644 (file)
@@ -48,10 +48,10 @@ TEST_CASE("Acquire and release short-lived lock file")
 
     CHECK(lock.acquire());
     CHECK(lock.acquired());
-    CHECK(Stat::lstat("test.alive"));
     const auto st = Stat::lstat("test.lock");
     CHECK(st);
 #ifndef _WIN32
+    CHECK(Stat::lstat("test.alive"));
     CHECK(st.is_symlink());
 #else
     CHECK(st.is_regular());