#include "syntax/dependency_graph.h" #include #include #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 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 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 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 DependencyGraph::get_all_includes(std::uint32_t path_id) const { llvm::DenseMap seen; // raw_id -> index in result llvm::SmallVector 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 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 DependencyGraph::find_host_sources(std::uint32_t header_path_id) const { llvm::SmallVector result; llvm::DenseSet visited; llvm::SmallVector 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 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 prev; llvm::SmallVector queue; prev[host_path_id] = host_path_id; queue.push_back(host_path_id); bool found = false; while(!queue.empty() && !found) { llvm::SmallVector 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 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(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(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> local_context_groups; llvm::DenseMap local_context_to_config_id; llvm::DenseMap 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>& context_groups = ext_cache ? ext_cache->context_groups : local_context_groups; llvm::DenseMap& context_to_config_id = ext_cache ? ext_cache->context_to_config_id : local_context_to_config_id; llvm::DenseMap& 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 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> 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(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 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(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(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 local_include_cache; llvm::StringMap& 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> 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 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 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> 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 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 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> 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(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 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 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(&scan_result.config_id), reinterpret_cast(&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(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(phase1_end - wave_start).count(); auto p2 = std::chrono::duration_cast(phase2_end - phase1_end).count(); auto p3 = std::chrono::duration_cast(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(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