From dacaf41c33f4aef5ec8c57421b6e509f3419cf4b Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 12 Nov 2026 06:56:20 +0100 Subject: [PATCH 1/5] build(deps): pin coretrace-compiler to its per-compilation working directory The pinned commit, from CoreTrace/coretrace-compiler#262, gives each compilation a file system of its own, whose working directory -working-directory moves while the process's stays. The analyzer does not use it yet: it still moves the process around each compilation, under its lock. Refs #271 --- cmake/compiler/coretrace-compiler.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/compiler/coretrace-compiler.cmake b/cmake/compiler/coretrace-compiler.cmake index fb853b9..70a3bbb 100644 --- a/cmake/compiler/coretrace-compiler.cmake +++ b/cmake/compiler/coretrace-compiler.cmake @@ -13,7 +13,7 @@ endif() FetchContent_Declare( cc GIT_REPOSITORY https://github.com/CoreTrace/coretrace-compiler.git - GIT_TAG b4ad33638e9020a93f052a7a299e4105bd48f975 # v0.10.0 + GIT_TAG b05bd18b35f5c4c42f3d9b245aecd2c464ccb52b # CoreTrace/coretrace-compiler#262 ) FetchContent_MakeAvailable(cc) From f56e9792ee1011590c8df14ce3df2bd97165dc12 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 12 Nov 2026 06:56:20 +0100 Subject: [PATCH 2/5] perf(input): compile sources in parallel, each in its directory Every compilation held one global lock and moved the whole process to its compile command's directory: the --jobs threads compiled one source at a time. coretrace-compiler now gives each compilation the directory of its -working-directory without moving the process: the analyzer passes the directory that way, and compilations share the lock. Some compilations still hold it alone, since they change state that the whole process shares: those with an option that Clang applies to LLVM's state (-mllvm, -mdebug-pass, -mlimit-float-precision, -ftime-report), and those with -fno-integrated-cc1, whose jobs run as programs, for which coretrace-compiler moves the process. A compile command whose directory does not exist still fails the analysis: coretrace-compiler reports a directory it cannot enter, but compiles to memory all the same, so the analyzer checks the directory itself, with the message it gave before. Refs #271 --- src/analysis/InputPipeline.cpp | 114 +++++++++++++++------------------ 1 file changed, 52 insertions(+), 62 deletions(-) diff --git a/src/analysis/InputPipeline.cpp b/src/analysis/InputPipeline.cpp index 0828f54..a723665 100644 --- a/src/analysis/InputPipeline.cpp +++ b/src/analysis/InputPipeline.cpp @@ -14,7 +14,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -40,15 +42,29 @@ namespace ctrace::stack::analysis { namespace { - // compilerlib drives an in-process clang CompilerInstance. clang/LLVM keep global - // mutable state (target registry, cl options, timers); two worker threads compiling - // at once hung intermittently on the first compile of a run. Compilation is - // serialised here; module analysis stays parallel, and the IR cache makes the - // compile step cheap on repeated runs. A compile that needs its compile command's - // directory changes the working directory of the whole process: it holds the same - // lock from before that change until it is undone, so no other compile runs in a - // directory that is not its own, and two changes never interleave. - std::mutex gCompileInvokeMutex; + // coretrace-compiler compiles from several threads at once, each compilation in the + // directory of its -working-directory, without moving the process: compilations share + // this lock. Some change state that the whole process shares, and hold it alone: those + // with an option that Clang applies to LLVM's state, and those with -fno-integrated-cc1, + // whose jobs run as programs, for which coretrace-compiler moves the process to the + // directory. + std::shared_mutex gCompileInvokeMutex; + + bool compilesAlone(const std::vector& args) + { + static constexpr std::string_view kOptionsOfLLVMState[] = { + "-mllvm", "-mdebug-pass", "-mlimit-float-precision", "-ftime-report"}; + return std::any_of(args.begin(), args.end(), + [](const std::string& arg) + { + for (std::string_view option : kOptionsOfLLVMState) + { + if (std::string_view(arg).starts_with(option)) + return true; + } + return arg == "-fno-integrated-cc1"; + }); + } void appendIfMissing(std::vector& args, const std::string& flag) { @@ -745,43 +761,6 @@ namespace ctrace::stack::analysis return true; } - class ScopedCurrentPath - { - public: - explicit ScopedCurrentPath(const std::string& path, std::string& error) - { - if (path.empty()) - return; - std::error_code ec; - previousPath_ = std::filesystem::current_path(ec); - if (ec) - { - error = "Failed to read current working directory"; - return; - } - std::filesystem::current_path(path, ec); - if (ec) - { - error = "Failed to change working directory to: " + path; - return; - } - active_ = true; - } - - ~ScopedCurrentPath() - { - if (!active_) - return; - std::error_code ec; - std::filesystem::current_path(previousPath_, ec); - } - - private: - std::filesystem::path previousPath_; - std::uint64_t active_ : 1 = false; - std::uint64_t reservedFlags_ : 63 = 0; - }; - class ScopedFileCleanup { public: @@ -859,9 +838,7 @@ namespace ctrace::stack::analysis using ctrace::stack::analyzer::ScopedHotspot; ModuleLoadResult result; const ScopedHotspot totalHotspot(config.timing, "input.load_module.total"); - // A relative input resolves from the directory the analysis started in, not from the - // current one: another thread may be compiling from its compile command's directory, - // which the whole process shares meanwhile. + // A relative input resolves from the directory the analysis started in. std::error_code cwdErr; const std::filesystem::path baseDir = config.inputBaseDir.empty() ? std::filesystem::current_path(cwdErr) @@ -923,22 +900,35 @@ namespace ctrace::stack::analysis [&](const std::vector& compileArgs, compilerlib::OutputMode outputMode) -> std::optional { - std::lock_guard compileLock(gCompileInvokeMutex); - if (workingDir.empty()) + std::vector argsInWorkingDir; + argsInWorkingDir.reserve(compileArgs.size() + 2); + if (!workingDir.empty()) { - const ScopedHotspot hotspot(config.timing, "input.compiler.invoke"); - return compilerlib::compile(compileArgs, outputMode); + // coretrace-compiler reports a directory it cannot enter, yet compiles to + // memory all the same: the compilation fails here instead. + std::error_code directoryError; + if (!std::filesystem::is_directory(workingDir, directoryError)) + { + result.error = + "Failed to change working directory to: " + workingDir + "\n"; + return std::nullopt; + } + argsInWorkingDir.push_back("-working-directory"); + argsInWorkingDir.push_back(workingDir); } + argsInWorkingDir.insert(argsInWorkingDir.end(), compileArgs.begin(), + compileArgs.end()); - std::string cwdError; - ScopedCurrentPath cwdGuard(workingDir, cwdError); - if (!cwdError.empty()) - { - result.error = cwdError + "\n"; - return std::nullopt; - } - const ScopedHotspot hotspot(config.timing, "input.compiler.invoke.cwd"); - return compilerlib::compile(compileArgs, outputMode); + std::shared_lock shared(gCompileInvokeMutex, std::defer_lock); + std::unique_lock alone(gCompileInvokeMutex, std::defer_lock); + if (compilesAlone(compileArgs)) + alone.lock(); + else + shared.lock(); + const ScopedHotspot hotspot(config.timing, workingDir.empty() + ? "input.compiler.invoke" + : "input.compiler.invoke.cwd"); + return compilerlib::compile(argsInWorkingDir, outputMode); }; if (cachePaths.enabled) From dd3ba4157309e81dc38c0242cfc601f2268a43b8 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 12 Nov 2026 06:56:20 +0100 Subject: [PATCH 3/5] docs(readme): say that --jobs compiles the sources in parallel Refs #271 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 420cbbd..236ed64 100644 --- a/README.md +++ b/README.md @@ -295,7 +295,7 @@ To generate `compile_commands.json` with CMake, configure with If analysis feels slow, `--compdb-fast` disables heavy flags (optimizations, sanitizers, profiling) while keeping include paths and macros. -For multi-file runs, `--jobs=` parallelizes input loading; with cross-TU enabled it also parallelizes summary construction. +For multi-file runs, `--jobs=` parallelizes input loading, compilations included; with cross-TU enabled it also parallelizes summary construction. `--compile-ir-cache-dir=` reuses compiled LLVM IR for unchanged translation units based on source/dependency stamps, which reduces repeated C/C++ frontend cost across runs. IR that another compiler produced (another coretrace-compiler revision or LLVM version) is compiled again. From d6420395fc47013b9106841edf49e0e181b28fd3 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 12 Nov 2026 06:56:20 +0100 Subject: [PATCH 4/5] docs(changelog): announce that sources compile in parallel Refs #271 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2ad54f..fe38468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,12 @@ Notable changes are grouped by release. See [all releases](https://github.com/Co - Added an authors file and linked the repository documents from the README. - SARIF rules carry a title (`shortDescription`) and a description (`fullDescription`) instead of their id, and `describeRule` (`RuleCatalog.hpp`) gives them to programs linking the analyzer. +- With `--jobs` above 1, sources compile in parallel. Each compilation runs in its compile + command's directory through coretrace-compiler's `-working-directory`, instead of moving the + whole process there under a lock that let one source compile at a time. Compilations that pass + `-mllvm`, `-mdebug-pass`, `-mlimit-float-precision`, `-ftime-report` or `-fno-integrated-cc1` + still run one at a time. coretrace-compiler is pinned to a commit of + CoreTrace/coretrace-compiler#262, which gives each compilation a working directory of its own. ## v0.22.0 From e6ae53edbc1d1f92303fc47f4ba83de6de87f347 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 12 Nov 2026 07:14:36 +0100 Subject: [PATCH 5/5] build(deps): pin coretrace-compiler to its fix of the files -MJ writes The newer commit of CoreTrace/coretrace-compiler#262 also writes the fragment of -MJ, the statistics of -save-stats, the graph of -dependency-dot and the module cache in the directory of -working-directory, as the analyzer did when it moved the process there. Refs #271 --- cmake/compiler/coretrace-compiler.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/compiler/coretrace-compiler.cmake b/cmake/compiler/coretrace-compiler.cmake index 70a3bbb..706a1b5 100644 --- a/cmake/compiler/coretrace-compiler.cmake +++ b/cmake/compiler/coretrace-compiler.cmake @@ -13,7 +13,7 @@ endif() FetchContent_Declare( cc GIT_REPOSITORY https://github.com/CoreTrace/coretrace-compiler.git - GIT_TAG b05bd18b35f5c4c42f3d9b245aecd2c464ccb52b # CoreTrace/coretrace-compiler#262 + GIT_TAG 8a99cf3d7be21dd6e15c1cc8f9523c685fd307ef # CoreTrace/coretrace-compiler#262 ) FetchContent_MakeAvailable(cc)