Files
clice/src/syntax/dependency_graph.cpp
ykiko 17e68010a0 feat(server): improve configuration file handling (#423)
## Summary

- **`[[rules]]`**: TOML array-of-tables config for per-file compilation
flag rules with glob pattern matching (`append`/`remove`). Patterns are
pre-compiled at config load time. Rules whose patterns all fail to
compile are dropped entirely (no silent no-op entries), and rules now
apply uniformly to every compilation — including the header-context
fallback path used when editing a header without its own CDB entry.
- **CDB auto-scan**: Default search scans workspace root + all immediate
subdirectories for `compile_commands.json`, replacing the hardcoded
directory list.
- **LSP `initializationOptions`**: Clients can pass config as JSON via
the LSP initialize request; priority is `initializationOptions >
clice.toml > defaults`.
- **XDG cache paths**: Default cache/index/logging paths prefer
`$XDG_CACHE_HOME/clice/<workspace-hash>/`; fall back to
`$HOME/.cache/clice/<hash>/`, then `<workspace>/.clice/`.
- **`${workspace}` substitution**: supported in `cache_dir`,
`index_dir`, `logging_dir`, and every `compile_commands_paths` entry.
No-op when `workspace_root` is empty.
- **Partial config support**: All TOML/JSON fields are optional via
`kota::meta::defaulted<T>`, so minimal config files work correctly.
- **Detailed diagnostics**: malformed `clice.toml` now logs line, column
and parser description (via toml++ direct parse); a malformed workspace
config surfaces a clear fallback warning instead of silently reverting
to defaults.

## Test plan

- [x] 28 unit tests for config (full suite 545 unit tests pass, Debug)
- [x] 119 integration tests pass
- [x] 2 smoke tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* XDG-based, workspace-scoped project cache (PCH/PCM and header caches
moved under project cache) with workspace fallback
* Initialization options JSON can override config (takes precedence over
file/defaults)
* Per-file pattern rules to append/remove compile flags; expanded
discovery of compilation databases (multiple paths)

* **Refactor**
* Configuration fields reorganized under a project scope; runtime
behavior now respects project-scoped values

* **Tests**
* New unit and integration tests for config parsing, rule matching, and
persistent cache behavior

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 00:21:31 +08:00

847 lines
35 KiB
C++

#include "syntax/dependency_graph.h"
#include <algorithm>
#include <chrono>
#include "command/toolchain.h"
#include "support/logging.h"
#include "syntax/include_resolver.h"
#include "syntax/scan.h"
#include "kota/async/async.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/StringSet.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/StringSaver.h"
namespace clice {
// DependencyGraph implementation
void DependencyGraph::add_module(llvm::StringRef module_name, std::uint32_t path_id) {
auto& ids = module_to_path[module_name];
if(llvm::find(ids, path_id) == ids.end()) {
ids.push_back(path_id);
}
}
llvm::ArrayRef<std::uint32_t> DependencyGraph::lookup_module(llvm::StringRef module_name) const {
auto it = module_to_path.find(module_name);
if(it != module_to_path.end()) {
return it->second;
}
return {};
}
void DependencyGraph::set_includes(std::uint32_t path_id,
std::uint32_t config_id,
llvm::SmallVector<std::uint32_t> included_ids) {
IncludeKey key{path_id, config_id};
includes[key] = std::move(included_ids);
auto& configs = file_configs[path_id];
if(std::find(configs.begin(), configs.end(), config_id) == configs.end()) {
configs.push_back(config_id);
}
}
llvm::ArrayRef<std::uint32_t> DependencyGraph::get_includes(std::uint32_t path_id,
std::uint32_t config_id) const {
auto it = includes.find(IncludeKey{path_id, config_id});
if(it != includes.end()) {
return it->second;
}
return {};
}
llvm::SmallVector<std::uint32_t> DependencyGraph::get_all_includes(std::uint32_t path_id) const {
llvm::DenseMap<std::uint32_t, std::size_t> seen; // raw_id -> index in result
llvm::SmallVector<std::uint32_t> result;
auto fc_it = file_configs.find(path_id);
if(fc_it == file_configs.end()) {
return result;
}
for(auto config_id: fc_it->second) {
auto it = includes.find(IncludeKey{path_id, config_id});
if(it != includes.end()) {
for(auto id: it->second) {
auto raw_id = id & PATH_ID_MASK;
auto [sit, inserted] = seen.try_emplace(raw_id, result.size());
if(inserted) {
result.push_back(id);
} else if(!(id & CONDITIONAL_FLAG)) {
// Unconditional include wins over conditional.
result[sit->second] = raw_id;
}
}
}
}
return result;
}
std::size_t DependencyGraph::file_count() const {
return file_configs.size();
}
std::size_t DependencyGraph::module_count() const {
return module_to_path.size();
}
std::size_t DependencyGraph::edge_count() const {
std::size_t count = 0;
for(auto& [key, ids]: includes) {
count += ids.size();
}
return count;
}
void DependencyGraph::build_reverse_map() {
reverse_includes_.clear();
for(auto& [key, ids]: includes) {
for(auto flagged_id: ids) {
auto included_id = flagged_id & PATH_ID_MASK;
auto& vec = reverse_includes_[included_id];
if(llvm::find(vec, key.path_id) == vec.end()) {
vec.push_back(key.path_id);
}
}
}
}
llvm::ArrayRef<std::uint32_t> DependencyGraph::get_includers(std::uint32_t path_id) const {
auto it = reverse_includes_.find(path_id);
if(it != reverse_includes_.end()) {
return it->second;
}
return {};
}
llvm::SmallVector<std::uint32_t, 4>
DependencyGraph::find_host_sources(std::uint32_t header_path_id) const {
llvm::SmallVector<std::uint32_t, 4> result;
llvm::DenseSet<std::uint32_t> visited;
llvm::SmallVector<std::uint32_t, 16> queue;
queue.push_back(header_path_id);
visited.insert(header_path_id);
while(!queue.empty()) {
auto current = queue.pop_back_val();
auto includers = get_includers(current);
if(includers.empty()) {
// No includers: this is a root (source file).
// Exclude the starting header itself.
if(current != header_path_id) {
result.push_back(current);
}
continue;
}
for(auto includer: includers) {
if(visited.insert(includer).second) {
queue.push_back(includer);
}
}
}
return result;
}
std::vector<std::uint32_t> DependencyGraph::find_include_chain(std::uint32_t host_path_id,
std::uint32_t target_path_id) const {
if(host_path_id == target_path_id) {
return {host_path_id};
}
// BFS: predecessor map for path reconstruction.
llvm::DenseMap<std::uint32_t, std::uint32_t> prev;
llvm::SmallVector<std::uint32_t, 16> queue;
prev[host_path_id] = host_path_id;
queue.push_back(host_path_id);
bool found = false;
while(!queue.empty() && !found) {
llvm::SmallVector<std::uint32_t, 16> next_queue;
for(auto current: queue) {
auto includes_union = get_all_includes(current);
for(auto flagged_id: includes_union) {
auto child = flagged_id & PATH_ID_MASK;
if(prev.find(child) == prev.end()) {
prev[child] = current;
if(child == target_path_id) {
found = true;
break;
}
next_queue.push_back(child);
}
}
if(found) {
break;
}
}
queue = std::move(next_queue);
}
if(!found) {
return {};
}
// Reconstruct path from target back to host.
std::vector<std::uint32_t> chain;
auto node = target_path_id;
while(node != host_path_id) {
chain.push_back(node);
node = prev[node];
}
chain.push_back(host_path_id);
std::reverse(chain.begin(), chain.end());
return chain;
}
// Wavefront BFS scanner — async implementation
namespace {
/// Result of scanning a single file (returned from worker thread).
struct FileScanResult {
const char* path; // Stable pointer from PathPool.
std::uint32_t path_id;
std::uint32_t config_id;
ScanResult scan_result;
bool read_failed = false;
std::int64_t read_us = 0;
std::int64_t scan_us = 0;
};
/// Scan a single file: read content + lexer scan.
/// Runs on libuv worker thread via queue().
/// @param path Stable pointer from PathPool (must outlive the task).
FileScanResult scan_file_worker(const char* path, std::uint32_t path_id, std::uint32_t config_id) {
FileScanResult result;
result.path = path;
result.path_id = path_id;
result.config_id = config_id;
auto t0 = std::chrono::steady_clock::now();
// Force read() instead of mmap: RequiresNullTerminator=true makes LLVM
// fall back to read() for page-aligned files, and IsVolatile=true forces
// read() unconditionally — bypassing mmap entirely. This separates
// actual I/O cost from page-fault cost that was previously hidden inside
// the lexer timing.
auto buf = llvm::MemoryBuffer::getFile(result.path,
/*FileSize=*/-1,
/*RequiresNullTerminator=*/true,
/*IsVolatile=*/true);
auto t1 = std::chrono::steady_clock::now();
result.read_us = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
if(!buf) {
result.read_failed = true;
return result;
}
result.scan_result = scan((*buf)->getBuffer());
auto t2 = std::chrono::steady_clock::now();
result.scan_us = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count();
return result;
}
/// The async scan implementation that runs on a local event loop.
kota::task<> scan_impl(CompilationDatabase& cdb,
PathPool& path_pool,
DependencyGraph& graph,
ScanReport& report,
ScanCache* ext_cache,
kota::event_loop& loop,
const RuleMatcher& rule_matcher) {
auto start_time = std::chrono::steady_clock::now();
// Reuse context groups and configs from cache when available (warm runs).
// On the first call (or when cache is null) we build everything from scratch.
const bool have_config_cache =
ext_cache && !ext_cache->context_groups.empty() && !ext_cache->configs.empty();
// Provide local storage when not using the persistent cache.
llvm::DenseMap<const CompilationInfo*, llvm::SmallVector<std::uint32_t>> local_context_groups;
llvm::DenseMap<const CompilationInfo*, std::uint32_t> local_context_to_config_id;
llvm::DenseMap<std::uint32_t, SearchConfig> local_configs;
// When ext_cache is provided, write directly into it so that the data
// survives across calls (making have_config_cache true on run 2+).
llvm::DenseMap<const CompilationInfo*, llvm::SmallVector<std::uint32_t>>& context_groups =
ext_cache ? ext_cache->context_groups : local_context_groups;
llvm::DenseMap<const CompilationInfo*, std::uint32_t>& context_to_config_id =
ext_cache ? ext_cache->context_to_config_id : local_context_to_config_id;
llvm::DenseMap<std::uint32_t, SearchConfig>& configs =
ext_cache ? ext_cache->configs : local_configs;
auto config_start = std::chrono::steady_clock::now();
if(!have_config_cache) {
// Group files by CompilationInfo pointer to identify unique compilation commands.
// Convert CDB path IDs to PathPool IDs.
for(auto& entry: cdb.get_entries()) {
auto path = cdb.resolve_path(entry.file);
auto pool_id = path_pool.intern(path);
context_groups[entry.info.ptr].push_back(pool_id);
}
// Pre-warm toolchain cache: extract unique queries, execute in parallel.
// Skip entirely when configs are already cached (warm runs), since the
// toolchain cache is necessarily also populated from the previous scan.
auto prewarm_start = std::chrono::steady_clock::now();
if(!cdb.has_cached_configs()) {
std::vector<CompilationDatabase::PendingEntry> pending_entries;
for(auto& [info_ptr, file_ids]: context_groups) {
auto representative_path = path_pool.resolve(file_ids[0]);
CompilationDatabase::PendingEntry pe;
pe.file = representative_path;
pe.directory = info_ptr->directory;
// Reconstruct arguments: canonical args + patch args.
for(auto arg: info_ptr->canonical->arguments) {
pe.arguments.push_back(arg);
}
for(auto arg: info_ptr->patch) {
pe.arguments.push_back(arg);
}
pending_entries.push_back(std::move(pe));
}
auto pending = cdb.get_pending_queries(pending_entries);
if(!pending.empty()) {
LOG_INFO("Warming toolchain cache: {} unique queries", pending.size());
std::vector<kota::task<ToolchainResult, kota::error>> tasks;
tasks.reserve(pending.size());
for(auto& query: pending) {
tasks.push_back(kota::queue(
[q = std::move(query)]() -> ToolchainResult {
ToolchainResult result;
result.key = q.key;
llvm::BumpPtrAllocator alloc;
llvm::StringSaver saver(alloc);
toolchain::query_toolchain({q.file,
q.directory,
q.query_args,
[&](const char* s) -> const char* {
result.cc1_args.push_back(s);
return saver.save(s).data();
}});
return result;
},
loop));
}
auto outcome = co_await kota::when_all(std::move(tasks));
if(outcome.has_value()) {
cdb.inject_results(*outcome);
} else {
LOG_ERROR("Parallel toolchain query failed: {}", outcome.error().message());
}
}
}
auto prewarm_end = std::chrono::steady_clock::now();
report.prewarm_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(prewarm_end - prewarm_start)
.count();
// Extract SearchConfig for each unique context.
std::uint32_t next_config_id = 0;
std::int64_t lookup_us = 0;
for(auto& [context, file_ids]: context_groups) {
std::uint32_t config_id = next_config_id++;
context_to_config_id[context] = config_id;
auto representative_path = path_pool.resolve(file_ids[0]);
// Apply per-file rules so that `[[rules]]`-modified -I/-isystem/-std
// flags are reflected in the search config used by the scan.
// Rules are applied to the representative file and assumed to hold
// for the whole context group (same CompilationInfo).
std::vector<std::string> rule_append, rule_remove;
if(rule_matcher)
rule_matcher(representative_path, rule_append, rule_remove);
auto t0 = std::chrono::steady_clock::now();
configs[config_id] = cdb.lookup_search_config(
representative_path,
{.query_toolchain = true, .remove = rule_remove, .append = rule_append});
auto t1 = std::chrono::steady_clock::now();
lookup_us += std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
}
report.config_loop_ms = lookup_us / 1000;
LOG_INFO("Config extracted: {} groups, {:.1f}ms", configs.size(), lookup_us / 1000.0);
}
auto config_end = std::chrono::steady_clock::now();
report.config_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(config_end - config_start).count();
// Use external persistent cache when provided, otherwise create a local one.
DirListingCache local_dir_cache;
DirListingCache& dir_cache = ext_cache ? ext_cache->dir_cache : local_dir_cache;
llvm::StringMap<ScanCache::CachedInclude> local_include_cache;
llvm::StringMap<ScanCache::CachedInclude>& include_cache =
ext_cache ? ext_cache->include_cache : local_include_cache;
// Collect all unique search dirs and launch readdir tasks on the
// thread pool. Tasks start executing immediately but are NOT awaited
// here — instead they run concurrently with Wave 0's file scanning
// (Optimization 1: overlap dir cache with Phase 1). We only await
// them before Phase 2 of Wave 0, which is the first consumer.
struct DirEntry {
std::string dir_path;
llvm::StringSet<> entries;
};
std::vector<kota::task<DirEntry, kota::error>> pending_dir_tasks;
if(dir_cache.dirs.empty()) {
llvm::StringSet<> unique_dirs;
for(auto& [config_id, config]: configs) {
for(auto& dir: config.dirs) {
unique_dirs.insert(dir.path);
}
}
// Also prefetch parent directories of source files (for quoted include resolution).
for(auto& [context, file_ids]: context_groups) {
for(auto path_id: file_ids) {
auto dir = llvm::sys::path::parent_path(path_pool.resolve(path_id));
if(!dir.empty()) {
unique_dirs.insert(dir);
}
}
}
pending_dir_tasks.reserve(unique_dirs.size());
for(auto& entry: unique_dirs) {
auto dir_path = entry.getKey().str();
pending_dir_tasks.push_back(kota::queue(
[dir_path = std::move(dir_path)]() -> DirEntry {
DirEntry result;
result.dir_path = dir_path;
std::error_code ec;
llvm::sys::fs::directory_iterator di(result.dir_path, ec);
for(; !ec && di != llvm::sys::fs::directory_iterator(); di.increment(ec)) {
result.entries.insert(llvm::sys::path::filename(di->path()));
}
return result;
},
loop));
}
LOG_INFO("Launched {} dir cache tasks (running in background)", pending_dir_tasks.size());
}
// Track which files have been scanned (by path_id — cheaper than string hash).
// Value: found_dir_idx needed for #include_next.
llvm::DenseMap<std::uint32_t, unsigned> scanned_files;
// Wave 0: all source files from CDB.
// Re-use the cached initial_wave when available to avoid re-iterating context_groups.
std::vector<WaveEntry> current_wave;
const bool have_initial_wave_cache = ext_cache && !ext_cache->initial_wave.empty();
if(have_initial_wave_cache) {
current_wave = ext_cache->initial_wave;
for(auto& entry: current_wave) {
scanned_files.try_emplace(entry.path_id, entry.found_dir_idx);
}
} else {
current_wave.reserve(cdb.get_entries().size());
for(auto& [context, file_ids]: context_groups) {
auto config_id = context_to_config_id[context];
for(auto path_id: file_ids) {
scanned_files.try_emplace(path_id, 0u);
current_wave.push_back({path_id, config_id, /*found_dir_idx=*/0});
}
}
if(ext_cache) {
ext_cache->initial_wave = current_wave;
}
}
report.source_files = current_wave.size();
std::size_t wave_num = 0;
// Optimization 2: prefetch scan tasks.
// During Phase 2 of wave N, newly discovered files are immediately
// queued for scanning on the thread pool. When wave N+1 starts,
// these tasks are already running (or finished), eliminating most
// of the Phase 1 wait time for subsequent waves.
std::vector<kota::task<FileScanResult, kota::error>> prefetch_tasks;
// Pre-resolved search configs: built once after dir cache is populated,
// then reused for all waves. Eliminates StringMap lookups in Phase 2.
llvm::DenseMap<std::uint32_t, ResolvedSearchConfig> resolved_configs;
while(!current_wave.empty()) {
auto wave_start = std::chrono::steady_clock::now();
// Phase 1: Read + scan all files in parallel on the thread pool.
// Files with a cached ScanResult skip I/O and lexing entirely.
// For waves > 0, files discovered during the previous wave's Phase 2
// already have running scan tasks in prefetch_tasks.
std::vector<FileScanResult> scan_results;
scan_results.reserve(current_wave.size());
std::size_t wave_cache_hits = 0;
// Collect cache hits first (applies to all waves).
for(auto& entry: current_wave) {
if(ext_cache) {
auto it = ext_cache->scan_results.find(entry.path_id);
if(it != ext_cache->scan_results.end()) {
scan_results.push_back({path_pool.resolve(entry.path_id).data(),
entry.path_id,
entry.config_id,
it->second,
false,
0,
0});
report.scan_cache_hits++;
wave_cache_hits++;
}
}
}
if(!prefetch_tasks.empty()) {
// Waves 1+: await prefetched scan tasks from previous Phase 2.
auto scan_outcome = co_await kota::when_all(std::move(prefetch_tasks));
prefetch_tasks.clear();
if(scan_outcome.has_error()) {
LOG_ERROR("Prefetch scan failed: {}", scan_outcome.error().message());
break;
}
for(auto& r: *scan_outcome) {
if(!r.read_failed && ext_cache) {
ext_cache->scan_results.try_emplace(r.path_id, r.scan_result);
}
scan_results.push_back(std::move(r));
}
} else {
// Wave 0 (or warm run with all cache hits): create scan tasks now.
std::vector<kota::task<FileScanResult, kota::error>> scan_tasks;
scan_tasks.reserve(current_wave.size());
for(auto& entry: current_wave) {
auto pid = entry.path_id;
auto cid = entry.config_id;
// Skip files already served from cache above.
if(ext_cache && ext_cache->scan_results.count(pid)) {
continue;
}
auto path = path_pool.resolve(pid).data();
scan_tasks.push_back(
kota::queue([path, pid, cid]() { return scan_file_worker(path, pid, cid); },
loop));
}
// Optimization 1: await dir cache tasks concurrently with scan tasks.
// Both sets of tasks run on the same thread pool. By awaiting dir
// tasks first (while scan tasks continue in the background), we pay
// max(dir_time, scan_time) instead of dir_time + scan_time.
if(!pending_dir_tasks.empty()) {
auto dir_t0 = std::chrono::steady_clock::now();
auto dir_outcome = co_await kota::when_all(std::move(pending_dir_tasks));
pending_dir_tasks.clear();
if(dir_outcome.has_value()) {
for(auto& entry: *dir_outcome) {
dir_cache.dirs.try_emplace(entry.dir_path, std::move(entry.entries));
}
LOG_INFO("Pre-populated dir cache: {} directories", dir_outcome->size());
}
auto dir_t1 = std::chrono::steady_clock::now();
report.dir_cache_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(dir_t1 - dir_t0).count();
}
if(!scan_tasks.empty()) {
auto scan_outcome = co_await kota::when_all(std::move(scan_tasks));
if(scan_outcome.has_error()) {
LOG_ERROR("Parallel scan failed: {}", scan_outcome.error().message());
break;
}
for(auto& r: *scan_outcome) {
if(!r.read_failed && ext_cache) {
ext_cache->scan_results.try_emplace(r.path_id, r.scan_result);
}
scan_results.push_back(std::move(r));
}
}
}
auto phase1_end = std::chrono::steady_clock::now();
// Accumulate per-file read/scan timing into report.
for(auto& sr: scan_results) {
report.read_us += sr.read_us;
report.scan_us += sr.scan_us;
}
// Pre-resolve search configs once after dir cache is populated (wave 0).
// Converts StringMap lookups into direct pointer dereferences for Phase 2.
if(resolved_configs.empty()) {
for(auto& [config_id, config]: configs) {
resolved_configs[config_id] = resolve_search_config(config, dir_cache);
}
}
// Phase 2+3: Resolve includes, intern paths, build graph, collect next wave.
// Merged into a single pass to avoid intermediate string allocations.
// Optimization 2: newly discovered files are immediately queued for
// scanning (prefetch_tasks), overlapping Phase 1 of the next wave
// with Phase 2 of the current wave.
std::vector<WaveEntry> next_wave;
next_wave.reserve(current_wave.size()); // Heuristic: next wave ≤ current wave.
StatCounters wave_stat_counters;
for(auto& scan_result: scan_results) {
report.total_files++;
if(scan_result.read_failed) {
LOG_WARN("Failed to read file for scanning: {}", scan_result.path);
continue;
}
auto rc_it = resolved_configs.find(scan_result.config_id);
if(rc_it == resolved_configs.end()) {
continue;
}
auto& resolved_config = rc_it->second;
auto includer_dir = llvm::sys::path::parent_path(scan_result.path);
auto* includer_entries = resolve_dir(includer_dir, dir_cache, &wave_stat_counters);
// Look up the found_dir_idx for this file (stored when it was discovered).
unsigned includer_found_dir_idx = 0;
auto sf_it = scanned_files.find(scan_result.path_id);
if(sf_it != scanned_files.end()) {
includer_found_dir_idx = sf_it->second;
}
// Record module interface unit mapping.
// When the module declaration is inside a conditional directive
// (need_preprocess=true), fall back to scan_module_decl() which
// runs a lightweight preprocessor pass to resolve the actual
// module name. This only applies to source files (wave 0) since
// headers cannot contain module declarations.
if(scan_result.scan_result.need_preprocess && wave_num == 0) {
auto file_path = llvm::StringRef(scan_result.path);
auto contexts =
cdb.lookup(file_path, {.query_toolchain = true, .suppress_logging = true});
if(!contexts.empty()) {
auto& cmd = contexts[0];
auto fallback =
scan_module_decl(cmd.to_argv(), cmd.resolved.directory, /*content=*/{});
if(!fallback.module_name.empty()) {
scan_result.scan_result.module_name = std::move(fallback.module_name);
scan_result.scan_result.is_interface_unit = fallback.is_interface_unit;
// Update cache so warm runs don't re-trigger fallback.
if(ext_cache) {
auto cache_it = ext_cache->scan_results.find(scan_result.path_id);
if(cache_it != ext_cache->scan_results.end()) {
cache_it->second.module_name = scan_result.scan_result.module_name;
cache_it->second.is_interface_unit =
scan_result.scan_result.is_interface_unit;
cache_it->second.need_preprocess = false;
}
}
}
}
}
if(scan_result.scan_result.is_interface_unit) {
graph.add_module(scan_result.scan_result.module_name, scan_result.path_id);
}
report.includes_found += scan_result.scan_result.includes.size();
llvm::SmallVector<std::uint32_t> include_ids;
include_ids.reserve(scan_result.scan_result.includes.size());
for(auto& inc: scan_result.scan_result.includes) {
// For angled includes, resolution depends only on config (not includer dir).
// Cache these to skip redundant directory searches across files.
bool cache_eligible = inc.is_angled && !inc.is_include_next;
llvm::SmallString<80> cache_key;
if(cache_eligible) {
cache_key.append(reinterpret_cast<const char*>(&scan_result.config_id),
reinterpret_cast<const char*>(&scan_result.config_id) +
sizeof(std::uint32_t));
cache_key += inc.path;
auto cache_it = include_cache.find(cache_key);
if(cache_it != include_cache.end()) {
report.include_cache_hits++;
auto& cached = cache_it->second;
if(cached.path_id == UINT32_MAX) {
report.unresolved.push_back({
std::move(inc.path),
std::string(path_pool.resolve(scan_result.path_id)),
inc.is_angled,
inc.conditional,
});
continue;
}
report.includes_resolved++;
// Jump directly to edge building with cached path_id.
std::uint32_t flagged_id = cached.path_id;
if(inc.conditional) {
flagged_id |= DependencyGraph::CONDITIONAL_FLAG;
report.conditional_edges++;
} else {
report.unconditional_edges++;
}
report.total_edges++;
include_ids.push_back(flagged_id);
if(scanned_files.try_emplace(cached.path_id, cached.found_dir_idx).second) {
next_wave.push_back(
{cached.path_id, scan_result.config_id, cached.found_dir_idx});
}
continue;
}
}
auto r_t0 = std::chrono::steady_clock::now();
auto resolved = resolve_include(inc.path,
inc.is_angled,
includer_entries,
includer_dir,
inc.is_include_next,
includer_found_dir_idx,
resolved_config,
dir_cache,
&wave_stat_counters);
auto r_t1 = std::chrono::steady_clock::now();
report.p2_resolve_us +=
std::chrono::duration_cast<std::chrono::microseconds>(r_t1 - r_t0).count();
if(!resolved.has_value()) {
if(cache_eligible) {
include_cache.try_emplace(cache_key,
ScanCache::CachedInclude{UINT32_MAX, 0});
}
report.unresolved.push_back({
std::move(inc.path),
std::string(path_pool.resolve(scan_result.path_id)),
inc.is_angled,
inc.conditional,
});
continue;
}
auto inc_path_id = path_pool.intern(resolved->path);
report.includes_resolved++;
if(cache_eligible) {
include_cache.try_emplace(
cache_key,
ScanCache::CachedInclude{inc_path_id, resolved->found_dir_idx});
}
std::uint32_t flagged_id = inc_path_id;
if(inc.conditional) {
flagged_id |= DependencyGraph::CONDITIONAL_FLAG;
report.conditional_edges++;
} else {
report.unconditional_edges++;
}
report.total_edges++;
include_ids.push_back(flagged_id);
if(scanned_files.try_emplace(inc_path_id, resolved->found_dir_idx).second) {
next_wave.push_back(
{inc_path_id, scan_result.config_id, resolved->found_dir_idx});
// Prefetch: start scanning this file immediately on the
// thread pool so it's ready when the next wave begins.
if(!ext_cache ||
ext_cache->scan_results.find(inc_path_id) == ext_cache->scan_results.end()) {
auto inc_path = path_pool.resolve(inc_path_id).data();
prefetch_tasks.push_back(kota::queue(
[inc_path, inc_path_id, cid = scan_result.config_id]() {
return scan_file_worker(inc_path, inc_path_id, cid);
},
loop));
}
}
}
graph.set_includes(scan_result.path_id, scan_result.config_id, std::move(include_ids));
}
report.dir_listings += wave_stat_counters.dir_listings;
report.dir_hits += wave_stat_counters.dir_hits;
report.fs_lookups += wave_stat_counters.lookups;
report.fs_us += wave_stat_counters.us;
auto phase2_end = std::chrono::steady_clock::now();
auto phase3_end = phase2_end;
auto p1 =
std::chrono::duration_cast<std::chrono::milliseconds>(phase1_end - wave_start).count();
auto p2 =
std::chrono::duration_cast<std::chrono::milliseconds>(phase2_end - phase1_end).count();
auto p3 =
std::chrono::duration_cast<std::chrono::milliseconds>(phase3_end - phase2_end).count();
report.phase1_ms += p1;
report.phase2_ms += p2;
report.phase3_ms += p3;
// Record per-wave stats for cold start analysis.
ScanReport::WaveStats ws;
ws.files = current_wave.size();
ws.phase1_ms = p1;
ws.phase2_ms = p2;
ws.next_files = next_wave.size();
ws.prefetch_count = prefetch_tasks.size();
ws.dir_listings = wave_stat_counters.dir_listings;
ws.dir_hits = wave_stat_counters.dir_hits;
ws.cache_hits = wave_cache_hits;
report.wave_stats.push_back(ws);
LOG_INFO(
"Wave {}: {} files | read+scan={}ms resolve={}ms graph={}ms | next={} " "prefetch={}",
wave_num,
current_wave.size(),
p1,
p2,
p3,
next_wave.size(),
prefetch_tasks.size());
current_wave = std::move(next_wave);
wave_num++;
}
auto end_time = std::chrono::steady_clock::now();
report.elapsed_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
report.header_files = report.total_files - report.source_files;
report.modules = graph.module_count();
report.waves = wave_num;
}
} // namespace
// Public sync entry point
ScanReport scan_dependency_graph(CompilationDatabase& cdb,
PathPool& path_pool,
DependencyGraph& graph,
ScanCache* cache,
const RuleMatcher& rule_matcher) {
ScanReport report;
if(cdb.get_entries().empty()) {
return report;
}
kota::event_loop loop;
loop.schedule(scan_impl(cdb, path_pool, graph, report, cache, loop, rule_matcher));
loop.run();
return report;
}
} // namespace clice