[perf] Cache dependency checks in HeaderChecker

This optimization introduces a thread-safe cache for IsDependencyOf
results in HeaderChecker.

On large projects like Chromium, 'gn gen --check' performs a massive
number of include validations. Many of these involve checking
reachability between the same pairs of targets over and over again. Each
check previously required a full BFS traversal of the dependency graph.

By caching whether target A can depend on target B (and if the path is
publicly permitted), we avoid the vast majority of these expensive
traversals.

Performance on Chromium 'gn gen --check':
- Before: ~125 seconds
- After: ~66 seconds (approx. 47% faster)
- CPU user time reduced from ~16 minutes to ~8 minutes.

Bug: 484862257
Change-Id: Ib46fef088039a145a53f1eb541c27f296a4df3ba
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/21001
Commit-Queue: Takuto Ikuta <tikuta@google.com>
Reviewed-by: David Turner <digit@google.com>
diff --git a/src/gn/header_checker.cc b/src/gn/header_checker.cc
index 1ecc90a..20ed4bc 100644
--- a/src/gn/header_checker.cc
+++ b/src/gn/header_checker.cc
@@ -186,7 +186,7 @@
   task_count_.Decrement();
 
   // Wait for all tasks posted by this method to complete.
-  std::unique_lock<std::mutex> auto_lock(lock_);
+  std::unique_lock<std::mutex> auto_lock(task_count_lock_);
   while (!task_count_.IsZero())
     task_count_cv_.wait(auto_lock);
 }
@@ -194,13 +194,13 @@
 void HeaderChecker::DoWork(const Target* target, const SourceFile& file) {
   std::vector<Err> errors;
   if (!CheckFile(target, file, &errors)) {
-    std::lock_guard<std::mutex> lock(lock_);
+    std::lock_guard<std::shared_mutex> lock(lock_);
     errors_.insert(errors_.end(), errors.begin(), errors.end());
   }
 
   if (!task_count_.Decrement()) {
     // Signal |task_count_cv_| when |task_count_| becomes zero.
-    std::unique_lock<std::mutex> auto_lock(lock_);
+    std::unique_lock<std::mutex> auto_lock(task_count_lock_);
     task_count_cv_.notify_one();
   }
 }
@@ -528,19 +528,48 @@
     return false;
   }
 
+  {
+    std::shared_lock<std::shared_mutex> lock(lock_);
+    auto it = dependency_cache_.find(std::make_pair(search_for, search_from));
+    if (it != dependency_cache_.end()) {
+      if (it->second == DependencyState::kNotADependency) {
+        *is_permitted = false;
+        return false;
+      }
+      *is_permitted = (it->second == DependencyState::kPermittedDependency);
+      if (*is_permitted) {
+        // For permitted chains, we often don't need the chain itself (it's
+        // only used for error reporting). If the caller provided a null
+        // chain, we can return immediately.
+        if (!chain)
+          return true;
+      }
+      // If we need the chain, we have to re-run the BFS.
+    }
+  }
+
   // Find the shortest public dependency chain.
   if (IsDependencyOf(search_for, search_from, true, chain)) {
     *is_permitted = true;
+    std::unique_lock<std::shared_mutex> lock(lock_);
+    dependency_cache_[std::make_pair(search_for, search_from)] =
+        DependencyState::kPermittedDependency;
     return true;
   }
 
   // If not, try to find any dependency chain at all.
   if (IsDependencyOf(search_for, search_from, false, chain)) {
     *is_permitted = false;
+    std::unique_lock<std::shared_mutex> lock(lock_);
+    dependency_cache_[std::make_pair(search_for, search_from)] =
+        DependencyState::kNonPermittedDependency;
     return true;
   }
 
   *is_permitted = false;
+  std::unique_lock<std::shared_mutex> lock(lock_);
+  dependency_cache_[std::make_pair(search_for, search_from)] =
+      DependencyState::kNotADependency;
   return false;
 }
 
diff --git a/src/gn/header_checker.h b/src/gn/header_checker.h
index fd053dd..0c88149 100644
--- a/src/gn/header_checker.h
+++ b/src/gn/header_checker.h
@@ -10,6 +10,7 @@
 #include <map>
 #include <mutex>
 #include <set>
+#include <shared_mutex>
 #include <string_view>
 #include <vector>
 
@@ -191,14 +192,29 @@
   // execution.
   base::AtomicRefCount task_count_;
 
+  // Maps (target_to, target_from) -> is_permitted.
+  enum class DependencyState {
+    kNotADependency,
+    kNonPermittedDependency,
+    kPermittedDependency,
+  };
+  using DependencyCache =
+      std::map<std::pair<const Target*, const Target*>, DependencyState>;
+
   // Locked variables ----------------------------------------------------------
   //
   // These are mutable during runtime and require locking.
 
-  std::mutex lock_;
+  mutable std::shared_mutex lock_;
 
   std::vector<Err> errors_;
 
+  mutable DependencyCache dependency_cache_;
+
+  // Separate lock for task count synchronization since std::condition_variable
+  // only works with std::unique_lock<std::mutex>.
+  std::mutex task_count_lock_;
+
   // Signaled when |task_count_| becomes zero.
   std::condition_variable task_count_cv_;