## 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>
847 lines
35 KiB
C++
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
|