StatCache: Provide timestamp cache abstraction
This introduces a new class named StatCache to abstract
timestamp cache implementations from DiskInterface itself.
For now, this only moves the Win32-specific implementation
to stat_cache-win32.cc, and provides a non-caching Posix
backend. This does not modifies Ninja's behavior in any
way.
In particular, this does not improve the performance
of the Win32 StatCache instance, which is only a
"timestamp preloader", and is not capable of detecting
changes between incremental builds.
+ Introduces the DiskInterface::Sync() method which can
be used to ensure that changes to the filesystem that
happened outside of DiskInterface() calls are properly
accounted for.
+ Change StatFile() to GetFileTimestamp() in util.h
to avoid confusion with the StatFile() method of the
StatCache class.
A future CL will add an inotify-based cache to speed
up incremental builds in persistent mode on Linux.
Fuchsia-Topic: persistent-mode
Original-Change-Id: I0f68e8165b8f269decea5d4ee52aa2f25349e44d
Change-Id: Ib3b7748f882d5affe7f9a209f8de8619d22975ed
Reviewed-on: https://fuchsia-review.googlesource.com/c/third_party/github.com/ninja-build/ninja/+/1071431
Reviewed-by: Tyler Mandry <tmandry@google.com>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 82980a9..c148ce1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -202,6 +202,16 @@
endif()
endif()
+if(WIN32)
+ target_sources(libninja PRIVATE
+ src/stat_cache-win32.cc
+ )
+else()
+ target_sources(libninja PRIVATE
+ src/stat_cache-posix.cc
+ )
+endif()
+
target_compile_features(libninja PUBLIC cxx_std_11)
#Fixes GetActiveProcessorCount on MinGW
@@ -299,6 +309,7 @@
src/persistent_mode_test.cc
src/persistent_service_test.cc
src/process_utils_test.cc
+ src/stat_cache_test.cc
src/state_test.cc
src/status_table_test.cc
src/stdio_redirection_test.cc
diff --git a/configure.py b/configure.py
index 4ecb0e3..2fc87c0 100755
--- a/configure.py
+++ b/configure.py
@@ -568,12 +568,12 @@
'version']:
objs += cxx(name, variables=cxxvariables)
if platform.is_windows():
- for name in ['subprocess-win32',
- 'includes_normalize-win32',
+ for name in ['includes_normalize-win32',
'interrupt_handling-win32',
'ipc_handle-win32',
'msvc_helper-win32',
- 'msvc_helper_main-win32']:
+ 'msvc_helper_main-win32',
+ 'subprocess-win32']:
objs += cxx(name, variables=cxxvariables)
if platform.is_msvc():
objs += cxx('minidump-win32', variables=cxxvariables)
@@ -583,6 +583,12 @@
'ipc_handle-posix',
'subprocess-posix']:
objs += cxx(name, variables=cxxvariables)
+
+if platform.is_windows():
+ objs += cxx('stat_cache-win32', variables=cxxvariables)
+else:
+ objs += cxx('stat_cache-posix', variables=cxxvariables)
+
if platform.is_aix():
objs += cc('getopt')
if platform.is_msvc():
diff --git a/src/build.cc b/src/build.cc
index 5e38802..a62e352 100644
--- a/src/build.cc
+++ b/src/build.cc
@@ -595,8 +595,10 @@
}
struct RealCommandRunner : public CommandRunner {
- explicit RealCommandRunner(const BuildConfig& config)
- : config_(config), subprocs_(config_.environment) {}
+ explicit RealCommandRunner(const BuildConfig& config,
+ DiskInterface& disk_interface)
+ : config_(config), subprocs_(config_.environment),
+ disk_interface_(disk_interface) {}
virtual ~RealCommandRunner() {}
size_t CanRunMore() const override;
bool StartCommand(Edge* edge) override;
@@ -605,6 +607,7 @@
void Abort() override;
const BuildConfig& config_;
+ DiskInterface& disk_interface_;
SubprocessSet subprocs_;
map<const Subprocess*, Edge*> subproc_to_edge_;
};
@@ -656,7 +659,12 @@
bool RealCommandRunner::WaitForCommand(Result* result) {
std::unique_ptr<Subprocess> subproc;
while (!(subproc = subprocs_.NextFinished())) {
- if (subprocs_.DoWork()) {
+ bool interrupted = subprocs_.DoWork();
+
+ // Launched sub-commands may have modified file timestamps.
+ disk_interface_.Sync();
+
+ if (interrupted) {
result->status = ExitInterrupted;
return false;
}
@@ -721,9 +729,11 @@
}
}
+ // Remove lock file now.
string err;
- if (disk_interface_->Stat(lock_file_path_, &err) > 0)
+ if (disk_interface_->Stat(lock_file_path_, &err) > 0) {
disk_interface_->RemoveFile(lock_file_path_);
+ }
}
Node* Builder::AddTarget(const string& name, string* err) {
@@ -780,7 +790,7 @@
if (config_.dry_run)
command_runner_.reset(new DryRunCommandRunner);
else
- command_runner_.reset(new RealCommandRunner(config_));
+ command_runner_.reset(new RealCommandRunner(config_, *disk_interface_));
}
// We are about to start the build process.
diff --git a/src/disk_interface.cc b/src/disk_interface.cc
index 7c325de..e8e0dd7 100644
--- a/src/disk_interface.cc
+++ b/src/disk_interface.cc
@@ -63,54 +63,6 @@
#endif
}
-#ifdef _WIN32
-bool IsWindows7OrLater() {
- OSVERSIONINFOEX version_info =
- { sizeof(OSVERSIONINFOEX), 6, 1, 0, 0, {0}, 0, 0, 0, 0, 0};
- DWORDLONG comparison = 0;
- VER_SET_CONDITION(comparison, VER_MAJORVERSION, VER_GREATER_EQUAL);
- VER_SET_CONDITION(comparison, VER_MINORVERSION, VER_GREATER_EQUAL);
- return VerifyVersionInfo(
- &version_info, VER_MAJORVERSION | VER_MINORVERSION, comparison);
-}
-
-bool StatAllFilesInDir(const string& dir, map<string, TimeStamp>* stamps,
- string* err) {
- // FindExInfoBasic is 30% faster than FindExInfoStandard.
- static bool can_use_basic_info = IsWindows7OrLater();
- // This is not in earlier SDKs.
- const FINDEX_INFO_LEVELS kFindExInfoBasic =
- static_cast<FINDEX_INFO_LEVELS>(1);
- FINDEX_INFO_LEVELS level =
- can_use_basic_info ? kFindExInfoBasic : FindExInfoStandard;
- WIN32_FIND_DATAA ffd;
- HANDLE find_handle = FindFirstFileExA((dir + "\\*").c_str(), level, &ffd,
- FindExSearchNameMatch, NULL, 0);
-
- if (find_handle == INVALID_HANDLE_VALUE) {
- DWORD win_err = GetLastError();
- if (win_err == ERROR_FILE_NOT_FOUND || win_err == ERROR_PATH_NOT_FOUND ||
- win_err == ERROR_DIRECTORY)
- return true;
- *err = "FindFirstFileExA(" + dir + "): " + GetLastErrorString();
- return false;
- }
- do {
- string lowername = ffd.cFileName;
- if (lowername == "..") {
- // Seems to just copy the timestamp for ".." from ".", which is wrong.
- // This is the case at least on NTFS under Windows 7.
- continue;
- }
- transform(lowername.begin(), lowername.end(), lowername.begin(), ::tolower);
- stamps->insert(make_pair(lowername,
- TimeStampFromFileTime(ffd.ftLastWriteTime)));
- } while (FindNextFileA(find_handle, &ffd));
- FindClose(find_handle);
- return true;
-}
-#endif // _WIN32
-
} // namespace
// DiskInterface ---------------------------------------------------------------
@@ -138,7 +90,7 @@
// RealDiskInterface -----------------------------------------------------------
RealDiskInterface::RealDiskInterface()
#ifdef _WIN32
- : use_cache_(false), long_paths_enabled_(AreWin32LongPathsEnabled()) {
+ : long_paths_enabled_(AreWin32LongPathsEnabled()) {
}
#else
{}
@@ -146,48 +98,7 @@
TimeStamp RealDiskInterface::Stat(const string& path, string* err) const {
METRIC_RECORD("node stat");
-#ifdef _WIN32
- // MSDN: "Naming Files, Paths, and Namespaces"
- // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
- if (!path.empty() && !AreLongPathsEnabled() && path[0] != '\\' &&
- path.size() > MAX_PATH) {
- ostringstream err_stream;
- err_stream << "Stat(" << path << "): Filename longer than " << MAX_PATH
- << " characters";
- *err = err_stream.str();
- return -1;
- }
- if (!use_cache_)
- return GetFileTimestamp(path, err);
-
- string dir = DirName(path);
- string base(path.substr(dir.size() ? dir.size() + 1 : 0));
- if (base == "..") {
- // StatAllFilesInDir does not report any information for base = "..".
- base = ".";
- dir = path;
- }
-
- string dir_lowercase = dir;
- transform(dir.begin(), dir.end(), dir_lowercase.begin(), ::tolower);
- transform(base.begin(), base.end(), base.begin(), ::tolower);
-
- Cache::iterator ci = cache_.find(dir_lowercase);
- if (ci == cache_.end()) {
- ci = cache_.insert(make_pair(dir_lowercase, DirCache())).first;
- if (!StatAllFilesInDir(dir.empty() ? "." : dir, &ci->second, err)) {
- cache_.erase(ci);
- return -1;
- }
- }
- DirCache::iterator di = ci->second.find(base);
- return di != ci->second.end() ? di->second : 0;
-#else // !_WIN32
- int64_t ret = GetFileTimestamp(path, err);
- if (ret < 0)
- return -1;
- return ret;
-#endif // !_WIN32
+ return stat_cache_.Stat(path, err);
}
bool RealDiskInterface::WriteFile(const string& path, const string& contents) {
@@ -198,6 +109,7 @@
return false;
}
+ stat_cache_.Invalidate(path);
if (fwrite(contents.data(), 1, contents.length(), fp) < contents.length()) {
Error("WriteFile(%s): Unable to write to the file. %s",
path.c_str(), strerror(errno));
@@ -215,11 +127,15 @@
}
bool RealDiskInterface::MakeDir(const string& path) {
- if (::MakeDir(path) < 0) {
- if (errno == EEXIST) {
- return true;
- }
- Error("mkdir(%s): %s", path.c_str(), strerror(errno));
+ int ret = ::MakeDir(path);
+ if (ret < 0 && errno == EEXIST)
+ return true;
+
+ int saved_errno = errno;
+ // Invalidate just in case the call modified something on the disk.
+ stat_cache_.Invalidate(path);
+ if (ret < 0) {
+ Error("mkdir(%s): %s", path.c_str(), strerror(saved_errno));
return false;
}
return true;
@@ -236,6 +152,7 @@
}
int RealDiskInterface::RemoveFile(const string& path) {
+ stat_cache_.Invalidate(path);
#ifdef _WIN32
DWORD attributes = GetFileAttributesA(path.c_str());
if (attributes == INVALID_FILE_ATTRIBUTES) {
@@ -291,11 +208,11 @@
}
void RealDiskInterface::AllowStatCache(bool allow) {
-#ifdef _WIN32
- use_cache_ = allow;
- if (!use_cache_)
- cache_.clear();
-#endif
+ stat_cache_.Enable(allow);
+}
+
+void RealDiskInterface::Sync() {
+ stat_cache_.Sync();
}
#ifdef _WIN32
@@ -305,7 +222,5 @@
#endif
void RealDiskInterface::FlushCache() {
-#ifdef _WIN32
- cache_.clear();
-#endif
+ stat_cache_.Flush();
}
diff --git a/src/disk_interface.h b/src/disk_interface.h
index 3e814f8..8fcb2a1 100644
--- a/src/disk_interface.h
+++ b/src/disk_interface.h
@@ -19,6 +19,7 @@
#include <string>
#include <vector>
+#include "stat_cache.h"
#include "timestamp.h"
/// Interface for reading files from disk. See DiskInterface for details.
@@ -66,6 +67,13 @@
/// Create all the parent directories for path; like mkdir -p
/// `basename path`.
bool MakeDirs(const std::string& path);
+
+ /// Sync with real filesystem state, which is useful when a timestamp cache
+ /// is implemented using a directory-watching system service. Calling this
+ /// is only needed when calling Stat() on files that may have been modified
+ /// through a different interface (e.g. files modified by command
+ /// sub-processes).
+ virtual void Sync() {}
};
/// Implementation of FileReader that tracks opened file paths and their
@@ -115,13 +123,13 @@
/// Implementation of DiskInterface that actually hits the disk.
struct RealDiskInterface : public DiskInterface {
RealDiskInterface();
- virtual ~RealDiskInterface() {}
- virtual TimeStamp Stat(const std::string& path, std::string* err) const;
- virtual bool MakeDir(const std::string& path);
- virtual bool WriteFile(const std::string& path, const std::string& contents);
- virtual Status ReadFile(const std::string& path, std::string* contents,
- std::string* err);
- virtual int RemoveFile(const std::string& path);
+ virtual ~RealDiskInterface() = default;
+ TimeStamp Stat(const std::string& path, std::string* err) const override;
+ bool MakeDir(const std::string& path) override;
+ bool WriteFile(const std::string& path, const std::string& contents) override;
+ Status ReadFile(const std::string& path, std::string* contents,
+ std::string* err) override;
+ int RemoveFile(const std::string& path) override;
/// Whether stat information can be cached. Only has an effect on Windows.
void AllowStatCache(bool allow);
@@ -131,26 +139,20 @@
bool AreLongPathsEnabled() const;
#endif
- /// Ensure stat cache is synchronized with the filesystem.
- void SyncCache() { AllowStatCache(false); }
+ /// If StatCache is allowed, synchronize it with the current filesystem
+ /// state. This is used to drain pending events from filesystem monitoring
+ /// services like inotify on Linux.
+ void Sync() override;
/// Remove all cached stat information, if any.
void FlushCache();
private:
#ifdef _WIN32
- /// Whether stat information can be cached.
- bool use_cache_;
-
/// Whether long paths are enabled.
bool long_paths_enabled_;
-
- typedef std::map<std::string, TimeStamp> DirCache;
- // TODO: Neither a map nor a hashmap seems ideal here. If the statcache
- // works out, come up with a better data structure.
- typedef std::map<std::string, DirCache> Cache;
- mutable Cache cache_;
-#endif
+#endif // _WIN32
+ StatCache stat_cache_;
};
#endif // NINJA_DISK_INTERFACE_H_
diff --git a/src/ninja.cc b/src/ninja.cc
index 8debbd6..9a5a3c7 100644
--- a/src/ninja.cc
+++ b/src/ninja.cc
@@ -140,6 +140,7 @@
// Reset status, since stdio was redirected when in persistent
// server process.
status_.reset(Status::factory(config));
+ disk_interface_.Sync();
}
/// Loaded state (rules, nodes).
@@ -1515,7 +1516,6 @@
}
disk_interface_.AllowStatCache(g_experimental_statcache);
- disk_interface_.SyncCache();
Builder builder(&state_, *config_, &build_log_, &deps_log_, &disk_interface_,
status(), start_time_millis_);
diff --git a/src/stat_cache-posix.cc b/src/stat_cache-posix.cc
new file mode 100644
index 0000000..2855df3
--- /dev/null
+++ b/src/stat_cache-posix.cc
@@ -0,0 +1,51 @@
+// Copyright 2023 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <errno.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "stat_cache.h"
+#include "util.h"
+
+// This implementation calls stat() directly and never caches anything.
+class StatCache::Impl {
+ public:
+ void Enable(bool) {}
+
+ TimeStamp Stat(const std::string& path, std::string* err) const {
+ return ::GetFileTimestamp(path, err);
+ }
+};
+
+StatCache::StatCache() : impl_(new StatCache::Impl()) {}
+
+StatCache::~StatCache() = default;
+
+void StatCache::Enable(bool enabled) {
+ impl_->Enable(enabled);
+}
+
+void StatCache::Invalidate(const std::string&) {
+ // Nothing to do here.
+}
+
+TimeStamp StatCache::Stat(const std::string& path, std::string* err) const {
+ return impl_->Stat(path, err);
+}
+
+void StatCache::Sync() {}
+
+void StatCache::Flush() {}
diff --git a/src/stat_cache-win32.cc b/src/stat_cache-win32.cc
new file mode 100644
index 0000000..9b7339e
--- /dev/null
+++ b/src/stat_cache-win32.cc
@@ -0,0 +1,199 @@
+// Copyright 2023 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <windows.h>
+
+#include <algorithm>
+#include <map>
+
+#include "stat_cache.h"
+#include "util.h"
+
+namespace {
+
+bool IsPathSeparator(char ch) {
+ return (ch == '/' || ch == '\\');
+}
+
+// Compute the dirname and basename of a given path.
+// If there is no directory separator, set |*dir| to ".".
+void DecomposePath(const std::string& path, std::string* dir,
+ std::string* base) {
+ size_t pos = path.size();
+
+ // Find first directory separator before base name.
+ while (pos > 0 && !IsPathSeparator(path[pos - 1]))
+ --pos;
+
+ *base = path.substr(pos);
+
+ // Skip over separator(s) to find directory name, might be empty
+ while (pos > 1 && IsPathSeparator(path[pos - 2]))
+ --pos;
+
+ *dir = path.substr(0, pos);
+}
+
+bool IsWindows7OrLater() {
+ OSVERSIONINFOEX version_info = {
+ sizeof(OSVERSIONINFOEX), 6, 1, 0, 0, { 0 }, 0, 0, 0, 0, 0
+ };
+ DWORDLONG comparison = 0;
+ VER_SET_CONDITION(comparison, VER_MAJORVERSION, VER_GREATER_EQUAL);
+ VER_SET_CONDITION(comparison, VER_MINORVERSION, VER_GREATER_EQUAL);
+ return VerifyVersionInfo(&version_info, VER_MAJORVERSION | VER_MINORVERSION,
+ comparison);
+}
+
+using DirCache = std::map<std::string, TimeStamp>;
+
+// TODO: Neither a map nor a hashmap seems ideal here. If the statcache
+// works out, come up with a better data structure.
+using Cache = std::map<std::string, DirCache>;
+
+bool StatAllFilesInDir(const std::string& dir, DirCache* stamps,
+ std::string* err) {
+ // FindExInfoBasic is 30% faster than FindExInfoStandard.
+ static bool can_use_basic_info = IsWindows7OrLater();
+ // This is not in earlier SDKs.
+ const FINDEX_INFO_LEVELS kFindExInfoBasic =
+ static_cast<FINDEX_INFO_LEVELS>(1);
+ FINDEX_INFO_LEVELS level =
+ can_use_basic_info ? kFindExInfoBasic : FindExInfoStandard;
+ WIN32_FIND_DATAA ffd;
+ HANDLE find_handle = FindFirstFileExA((dir + "\\*").c_str(), level, &ffd,
+ FindExSearchNameMatch, NULL, 0);
+
+ if (find_handle == INVALID_HANDLE_VALUE) {
+ DWORD win_err = GetLastError();
+ if (win_err == ERROR_FILE_NOT_FOUND || win_err == ERROR_PATH_NOT_FOUND ||
+ win_err == ERROR_DIRECTORY)
+ return true;
+ *err = StringFormat("FindFirstFileExA(%s): %s", dir.c_str(),
+ GetLastErrorString().c_str());
+ return false;
+ }
+ do {
+ std::string lowername = ffd.cFileName;
+ if (lowername == "..") {
+ // Seems to just copy the timestamp for ".." from ".", which is wrong.
+ // This is the case at least on NTFS under Windows 7.
+ continue;
+ }
+ std::transform(lowername.begin(), lowername.end(), lowername.begin(),
+ ::tolower);
+ // C++11 equivalent to
+ // stamps->try_emplace(std::move(lowername), TimeStampFromFile(...))
+ stamps->emplace(
+ std::piecewise_construct, std::forward_as_tuple(std::move(lowername)),
+ std::forward_as_tuple(TimeStampFromFileTime(ffd.ftLastWriteTime)));
+ } while (FindNextFileA(find_handle, &ffd));
+ FindClose(find_handle);
+ return true;
+}
+
+} // namespace
+
+class StatCache::Impl {
+ public:
+ void Enable(bool enabled) {
+ if (!enabled)
+ cache_.clear();
+
+ enabled_ = enabled;
+ }
+
+ TimeStamp Stat(const std::string& path, std::string* err) const {
+ // MSDN: "Naming Files, Paths, and Namespaces"
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
+ if (!path.empty() && !long_path_enabled_ && path[0] != '\\' &&
+ path.size() > MAX_PATH) {
+ *err = StringFormat("Stat(%s): Filename longer than %u characters",
+ path.c_str(), MAX_PATH);
+ return -1;
+ }
+ if (!enabled_)
+ return ::GetFileTimestamp(path, err);
+
+ std::string dir, base;
+ DecomposePath(path, &dir, &base);
+ if (base == "..") {
+ // StatAllFilesInDir does not report any information for base = "..".
+ base = ".";
+ dir = path;
+ }
+
+ std::string dir_lowercase = dir;
+ std::transform(dir.begin(), dir.end(), dir_lowercase.begin(), ::tolower);
+ std::transform(base.begin(), base.end(), base.begin(), ::tolower);
+
+ // NOTE: The following is the C++11 equivalent of
+ // cache_.try_emplace(std::move(dir_lowercase))
+ auto ret = cache_.emplace(std::piecewise_construct,
+ std::forward_as_tuple(std::move(dir_lowercase)),
+ std::forward_as_tuple());
+ Cache::iterator ci = ret.first;
+ if (ret.second) {
+ if (!StatAllFilesInDir(dir.empty() ? "." : dir, &ci->second, err)) {
+ cache_.erase(ci);
+ return -1;
+ }
+ }
+ DirCache::iterator di = ci->second.find(base);
+ return di != ci->second.end() ? di->second : 0;
+ }
+
+ void Sync() {
+ // TODO(digit): Implement this properly!
+ // FOR NOW, drop everything from the cache to pass the unit-tests!
+ cache_.clear();
+ }
+
+ void Flush() { cache_.clear(); }
+
+ private:
+ /// Whether stat information can be cached.
+ bool enabled_ = false;
+
+ /// Wether long paths are enabled on this machine.
+ bool long_path_enabled_ = AreWin32LongPathsEnabled();
+
+ /// The cache itself.
+ mutable Cache cache_;
+};
+
+StatCache::StatCache() : impl_(new StatCache::Impl()) {}
+
+StatCache::~StatCache() = default;
+
+void StatCache::Enable(bool enabled) {
+ impl_->Enable(enabled);
+}
+
+TimeStamp StatCache::Stat(const std::string& path, std::string* err) const {
+ return impl_->Stat(path, err);
+}
+
+void StatCache::Invalidate(const std::string& path) {
+ // TODO(digit): Only remove entries from the path's parent directory.
+ impl_->Sync();
+}
+
+void StatCache::Sync() {
+ impl_->Sync();
+}
+
+void StatCache::Flush() {
+ impl_->Flush();
+}
diff --git a/src/stat_cache.h b/src/stat_cache.h
new file mode 100644
index 0000000..7d6322d
--- /dev/null
+++ b/src/stat_cache.h
@@ -0,0 +1,56 @@
+// Copyright 2023 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef NINJA_STAT_CACHE_H_
+#define NINJA_STAT_CACHE_H_
+
+#include <memory>
+#include <string>
+
+#include "timestamp.h"
+
+/// Implement a cache for file path timestamps.
+class StatCache {
+ public:
+ /// Constructor
+ StatCache();
+
+ /// Destructor
+ ~StatCache();
+
+ /// Enable caching of timestamps. If false (the default), the Stat() performs
+ /// a single stat() operation per file and no caching is performed.
+ void Enable(bool enabled);
+
+ /// Mark a path as invalid.
+ void Invalidate(const std::string& path);
+
+ /// Return the timestamp of a given file path. On error, set |*err| then
+ /// return -1. Otherwise, return 0 if the file is missing, or a strictly
+ /// positive value if it exists.
+ TimeStamp Stat(const std::string& path, std::string* err) const;
+
+ /// Synchronize the cache state with recent filesystem events.
+ /// Call this before one or more Stat() calls.
+ void Sync();
+
+ /// Remove all entries from the cache.
+ void Flush();
+
+ private:
+ class Impl;
+ std::unique_ptr<Impl> impl_;
+};
+
+#endif // NINJA_STAT_CACHE_H_
diff --git a/src/stat_cache_test.cc b/src/stat_cache_test.cc
new file mode 100644
index 0000000..f6b8455
--- /dev/null
+++ b/src/stat_cache_test.cc
@@ -0,0 +1,128 @@
+// Copyright 2023 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "stat_cache.h"
+
+#include <stdio.h>
+
+#include "test.h"
+#include "util.h"
+
+namespace {
+
+void WriteFile(const std::string& path, const std::string& content) {
+ FILE* f = fopen(path.c_str(), "wb");
+ if (!f)
+ ErrnoFatal("fopen", path.c_str());
+
+ size_t ret = fwrite(content.c_str(), content.size(), 1, f);
+ if (ret != 1)
+ ErrnoFatal("Could not write to %s", path.c_str());
+ fclose(f);
+}
+
+void RemoveFile(const std::string& path) {
+ ::remove(path.c_str());
+}
+
+} // namespace
+
+TEST(StatCache, CheckMissingTimestamp) {
+ ScopedTempDir temp_dir;
+ temp_dir.CreateAndEnter("StatCacheTest");
+ auto top_dir = GetCurrentDir();
+
+ StatCache cache;
+ cache.Enable(true);
+
+ std::string err;
+ TimeStamp t = cache.Stat(top_dir + "/foo", &err);
+ EXPECT_EQ(0, t);
+ EXPECT_TRUE(err.empty());
+
+ t = cache.Stat(top_dir + "/bar", &err);
+ EXPECT_EQ(0, t);
+ EXPECT_TRUE(err.empty());
+
+ // Try foo again, just in case.
+ t = cache.Stat(top_dir + "/foo", &err);
+ EXPECT_EQ(0, t);
+ EXPECT_TRUE(err.empty());
+}
+
+TEST(StatCache, CheckExistingTimestamp) {
+ ScopedTempDir temp_dir;
+ temp_dir.CreateAndEnter("StatCacheTest");
+ auto top_dir = GetCurrentDir();
+
+ StatCache cache;
+ cache.Enable(true);
+
+ std::string foo_path = top_dir + "/foo";
+
+ WriteFile(foo_path, "foo!!");
+
+ std::string err;
+ TimeStamp t = cache.Stat(foo_path, &err);
+ EXPECT_GE(t, 0);
+ EXPECT_TRUE(err.empty());
+}
+
+TEST(StatCache, CheckCreatedTimestamp) {
+ ScopedTempDir temp_dir;
+ temp_dir.CreateAndEnter("StatCacheTest");
+ auto top_dir = GetCurrentDir();
+
+ StatCache cache;
+ cache.Enable(true);
+
+ std::string foo_path = top_dir + "/foo";
+
+ std::string err;
+ TimeStamp t = cache.Stat(foo_path, &err);
+ EXPECT_EQ(0, t);
+ EXPECT_TRUE(err.empty());
+
+ WriteFile(foo_path, "foo!!");
+ cache.Sync();
+
+ TimeStamp t2 = cache.Stat(foo_path, &err);
+ EXPECT_NE(0, t2);
+ EXPECT_TRUE(err.empty());
+}
+
+TEST(StatCache, CheckDeletedTimestamp) {
+ ScopedTempDir temp_dir;
+ temp_dir.CreateAndEnter("StatCacheTest");
+ auto top_dir = GetCurrentDir();
+
+ StatCache cache;
+ cache.Enable(true);
+
+ std::string foo_path = top_dir + "/foo";
+ WriteFile(foo_path, "foo!!");
+
+ std::string err;
+ TimeStamp t = cache.Stat(foo_path, &err);
+ EXPECT_NE(0, t);
+ EXPECT_TRUE(err.empty());
+
+ RemoveFile(foo_path);
+
+ cache.Sync();
+
+ TimeStamp t2 = cache.Stat(foo_path, &err);
+ EXPECT_EQ(0, t2);
+ EXPECT_TRUE(err.empty());
+}