Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions clang/include/clang/Driver/Options.td
Original file line number Diff line number Diff line change
Expand Up @@ -3489,6 +3489,12 @@ def fmodule_output : Flag<["-"], "fmodule-output">, Flags<[NoXarchOption]>,
Visibility<[ClangOption, CLOption, CC1Option]>,
HelpText<"Save intermediate module file results when compiling a standard C++ module unit.">;

def fimplicit_modules_lock_timeout_EQ : Joined<["-"], "fimplicit-modules-lock-timeout=">,
Flags<[NoXarchOption]>, Visibility<[ClangOption, CLOption, CC1Option]>,
MetaVarName<"<seconds>">,
HelpText<"Time to wait on an implicit module lock before timing out">,
MarshallingInfoInt<FrontendOpts<"ImplicitModulesLockTimeoutSeconds">, "90">;

defm skip_odr_check_in_gmf : BoolOption<"f", "skip-odr-check-in-gmf",
LangOpts<"SkipODRCheckInGMF">, DefaultFalse,
PosFlag<SetTrue, [], [CC1Option],
Expand Down
3 changes: 3 additions & 0 deletions clang/include/clang/Frontend/FrontendOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,9 @@ class FrontendOptions {
/// The list of files to embed into the compiled module file.
std::vector<std::string> ModulesEmbedFiles;

/// The time in seconds to wait on an implicit module lock before timing out.
unsigned ImplicitModulesLockTimeoutSeconds = 90;

/// The list of AST files to merge.
std::vector<std::string> ASTMergeFiles;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@
#include "clang/Serialization/ModuleCache.h"
#include "llvm/ADT/StringMap.h"

#include <atomic>
#include <condition_variable>
#include <mutex>
#include <shared_mutex>

namespace clang {
namespace tooling {
namespace dependencies {
struct ModuleCacheEntry {
std::shared_mutex CompilationMutex;
std::mutex Mutex;
std::condition_variable CondVar;
bool Locked = false;
unsigned Generation = 0;

std::atomic<std::time_t> Timestamp = 0;
};

Expand Down
2 changes: 2 additions & 0 deletions clang/lib/Driver/ToolChains/Clang.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4041,6 +4041,8 @@ static bool RenderModulesOptions(Compilation &C, const Driver &D,
Path.insert(Path.begin(), Arg, Arg + strlen(Arg));
CmdArgs.push_back(Args.MakeArgString(Path));
}

Args.AddLastArg(CmdArgs, options::OPT_fimplicit_modules_lock_timeout_EQ);
}

if (HaveModules) {
Expand Down
8 changes: 5 additions & 3 deletions clang/lib/Frontend/CompilerInstance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1763,19 +1763,21 @@ static bool compileModuleAndReadASTBehindLock(

// Someone else is responsible for building the module. Wait for them to
// finish.
switch (Lock->waitForUnlockFor(std::chrono::seconds(90))) {
unsigned Timeout =
ImportingInstance.getFrontendOpts().ImplicitModulesLockTimeoutSeconds;
switch (Lock->waitForUnlockFor(std::chrono::seconds(Timeout))) {
case llvm::WaitForUnlockResult::Success:
break; // The interesting case.
case llvm::WaitForUnlockResult::OwnerDied:
continue; // try again to get the lock.
case llvm::WaitForUnlockResult::Timeout:
// Since the InMemoryModuleCache takes care of correctness, we try waiting
// for someone else to complete the build so that it does not happen
// twice. In case of timeout, build it ourselves.
// twice. In case of timeout, try to build it ourselves again.
Diags.Report(ModuleNameLoc, diag::remark_module_lock_timeout)
<< Module->Name;
// Clear the lock file so that future invocations can make progress.
Lock->unsafeMaybeUnlock();
Lock->unsafeUnlock();
continue;
}

Expand Down
3 changes: 3 additions & 0 deletions clang/lib/Lex/Pragma.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
#include <cstdint>
#include <optional>
#include <string>
#include <thread>
#include <utility>
#include <vector>

Expand Down Expand Up @@ -1069,6 +1070,8 @@ struct PragmaDebugHandler : public PragmaHandler {
Crasher.setAnnotationRange(SourceRange(Tok.getLocation()));
PP.EnterToken(Crasher, /*IsReinject*/ false);
}
} else if (II->isStr("sleep")) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
} else if (II->isStr("dump")) {
Token DumpAnnot;
DumpAnnot.startToken();
Expand Down
67 changes: 44 additions & 23 deletions clang/lib/Tooling/DependencyScanning/InProcessModuleCache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,63 @@
#include "llvm/Support/AdvisoryLock.h"
#include "llvm/Support/Chrono.h"

#include <mutex>

using namespace clang;
using namespace tooling;
using namespace dependencies;

namespace {
class ReaderWriterLock : public llvm::AdvisoryLock {
// TODO: Consider using std::atomic::{wait,notify_all} when we move to C++20.
std::unique_lock<std::shared_mutex> OwningLock;
ModuleCacheEntry &Entry;
std::optional<unsigned> OwnedGeneration;

public:
ReaderWriterLock(std::shared_mutex &Mutex)
: OwningLock(Mutex, std::defer_lock) {}

Expected<bool> tryLock() override { return OwningLock.try_lock(); }
ReaderWriterLock(ModuleCacheEntry &Entry) : Entry(Entry) {}

Expected<bool> tryLock() override {
std::lock_guard<std::mutex> Lock(Entry.Mutex);
if (Entry.Locked)
return false;
Entry.Locked = true;
OwnedGeneration = Entry.Generation;
return true;
}

llvm::WaitForUnlockResult
waitForUnlockFor(std::chrono::seconds MaxSeconds) override {
assert(!OwningLock);
// We do not respect the timeout here. It's very generous for implicit
// modules, so we'd typically only reach it if the owner crashed (but so did
// we, since we run in the same process), or encountered deadlock.
(void)MaxSeconds;
std::shared_lock<std::shared_mutex> Lock(*OwningLock.mutex());
return llvm::WaitForUnlockResult::Success;
assert(!OwnedGeneration);
std::unique_lock<std::mutex> Lock(Entry.Mutex);
unsigned CurrentGeneration = Entry.Generation;
bool Success = Entry.CondVar.wait_for(Lock, MaxSeconds, [&] {
// We check not only Locked, but also Generation to break the wait in case
// of unsafeUnlock() and successful tryLock().
return !Entry.Locked || Entry.Generation != CurrentGeneration;
});
return Success ? llvm::WaitForUnlockResult::Success
: llvm::WaitForUnlockResult::Timeout;
}

std::error_code unsafeMaybeUnlock() override {
// Unlocking the mutex here would trigger UB and we don't expect this to be
// actually called when compiling scanning modules due to the no-timeout
// guarantee above.
std::error_code unsafeUnlock() override {
{
std::lock_guard<std::mutex> Lock(Entry.Mutex);
Entry.Generation += 1;
Entry.Locked = false;
}
Entry.CondVar.notify_all();
return {};
}

~ReaderWriterLock() override = default;
~ReaderWriterLock() override {
if (OwnedGeneration) {
{
std::lock_guard<std::mutex> Lock(Entry.Mutex);
// Avoid stomping over the state managed by someone else after
// unsafeUnlock() and successful tryLock().
if (*OwnedGeneration == Entry.Generation)
Entry.Locked = false;
}
Entry.CondVar.notify_all();
}
}
};

class InProcessModuleCache : public ModuleCache {
Expand All @@ -65,14 +86,14 @@ class InProcessModuleCache : public ModuleCache {
void prepareForGetLock(StringRef Filename) override {}

std::unique_ptr<llvm::AdvisoryLock> getLock(StringRef Filename) override {
auto &CompilationMutex = [&]() -> std::shared_mutex & {
auto &Entry = [&]() -> ModuleCacheEntry & {
std::lock_guard<std::mutex> Lock(Entries.Mutex);
auto &Entry = Entries.Map[Filename];
if (!Entry)
Entry = std::make_unique<ModuleCacheEntry>();
return Entry->CompilationMutex;
return *Entry;
}();
return std::make_unique<ReaderWriterLock>(CompilationMutex);
return std::make_unique<ReaderWriterLock>(Entry);
}

std::time_t getModuleTimestamp(StringRef Filename) override {
Expand Down
37 changes: 37 additions & 0 deletions clang/test/ClangScanDeps/modules-cycle-deadlock.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// This test checks that implicit modules do not encounter a deadlock on a dependency cycle.

// RUN: rm -rf %t
// RUN: split-file %s %t
// RUN: sed "s|DIR|%/t|g" %t/cdb.json.in > %t/cdb.json
// RUN: not clang-scan-deps -mode preprocess -format experimental-full \
// RUN: -compilation-database %t/cdb.json -j 2 -o %t/out 2> %t/err
// RUN: FileCheck %s --input-file %t/err
// CHECK-DAG: fatal error: cyclic dependency in module 'M': M -> N -> M
// CHECK-DAG: fatal error: cyclic dependency in module 'N': N -> M -> N

//--- cdb.json.in
[
{
"file": "DIR/tu1.c",
"directory": "DIR",
"command": "clang -fmodules -fmodules-cache-path=DIR/cache -fimplicit-modules-lock-timeout=3 DIR/tu1.c -o DIR/tu1.o"
},
{
"file": "DIR/tu2.c",
"directory": "DIR",
"command": "clang -fmodules -fmodules-cache-path=DIR/cache -fimplicit-modules-lock-timeout=3 DIR/tu2.c -o DIR/tu2.o"
}
]
//--- tu1.c
#include "m.h"
//--- tu2.c
#include "n.h"
//--- module.modulemap
module M { header "m.h" }
module N { header "n.h" }
//--- m.h
#pragma clang __debug sleep // Give enough time for tu2.c to start building N.
#include "n.h"
//--- n.h
#pragma clang __debug sleep // Give enough time for tu1.c to start building M.
#include "m.h"
36 changes: 36 additions & 0 deletions clang/test/Modules/implicit-cycle-deadlock.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// This test checks that implicit modules do not encounter a deadlock on a dependency cycle.

// RUN: rm -rf %t
// RUN: split-file %s %t

// RUN: %python %t/run_concurrently.py \
// RUN: "not %clang -fsyntax-only -fmodules -fmodules-cache-path=%t/cache -fimplicit-modules-lock-timeout=3 %t/tu1.c 2> %t/err1" \
// RUN: "not %clang -fsyntax-only -fmodules -fmodules-cache-path=%t/cache -fimplicit-modules-lock-timeout=3 %t/tu2.c 2> %t/err2"

// RUN: FileCheck %s --input-file %t/err1 --check-prefix=CHECK1
// RUN: FileCheck %s --input-file %t/err2 --check-prefix=CHECK2
// CHECK1: fatal error: cyclic dependency in module 'M': M -> N -> M
// CHECK2: fatal error: cyclic dependency in module 'N': N -> M -> N

//--- run_concurrently.py
import subprocess, sys, threading

def run(cmd):
subprocess.run(cmd, shell=True)

threads = [threading.Thread(target=run, args=(cmd,)) for cmd in sys.argv[1:]]
for t in threads: t.start()
for t in threads: t.join()
//--- tu1.c
#include "m.h"
//--- tu2.c
#include "n.h"
//--- module.modulemap
module M { header "m.h" }
module N { header "n.h" }
//--- m.h
#pragma clang __debug sleep // Give enough time for tu2.c to start building N.
#include "n.h"
//--- n.h
#pragma clang __debug sleep // Give enough time for tu1.c to start building M.
#include "m.h"
2 changes: 1 addition & 1 deletion llvm/include/llvm/Support/AdvisoryLock.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class AdvisoryLock {
/// For a lock owned by someone else, unlock it. A permitted side-effect is
/// that another thread/process may acquire ownership of the lock before the
/// existing owner unlocks it. This is an unsafe operation.
virtual std::error_code unsafeMaybeUnlock() = 0;
virtual std::error_code unsafeUnlock() = 0;

/// Unlocks the lock if its ownership was previously acquired by \c tryLock().
virtual ~AdvisoryLock() = default;
Expand Down
2 changes: 1 addition & 1 deletion llvm/include/llvm/Support/LockFileManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class LLVM_ABI LockFileManager : public AdvisoryLock {

/// Remove the lock file. This may delete a different lock file than
/// the one previously read if there is a race.
std::error_code unsafeMaybeUnlock() override;
std::error_code unsafeUnlock() override;

/// Unlocks the lock if previously acquired by \c tryLock().
~LockFileManager() override;
Expand Down
2 changes: 1 addition & 1 deletion llvm/lib/Support/LockFileManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,6 @@ LockFileManager::waitForUnlockFor(std::chrono::seconds MaxSeconds) {
return WaitForUnlockResult::Timeout;
}

std::error_code LockFileManager::unsafeMaybeUnlock() {
std::error_code LockFileManager::unsafeUnlock() {
return sys::fs::remove(LockFileName);
}
6 changes: 3 additions & 3 deletions llvm/lib/Support/VirtualOutputBackends.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ Error OnDiskOutputFile::keep() {
if (Error Err = Lock.tryLock().moveInto(Owned)) {
// If we error acquiring a lock, we cannot ensure appends
// to the trace file are atomic - cannot ensure output correctness.
Lock.unsafeMaybeUnlock();
Lock.unsafeUnlock();
return convertToOutputError(
OutputPath, std::make_error_code(std::errc::no_lock_available));
}
Expand All @@ -477,7 +477,7 @@ Error OnDiskOutputFile::keep() {
return convertToOutputError(OutputPath, EC);
Out << (*Content)->getBuffer();
Out.close();
Lock.unsafeMaybeUnlock();
Lock.unsafeUnlock();
if (Out.has_error())
return convertToOutputError(OutputPath, Out.error());
// Remove temp file and done.
Expand All @@ -497,7 +497,7 @@ Error OnDiskOutputFile::keep() {
// the lock, causing other waiting processes to time-out. Let's clear
// the lock and try again right away. If we do start seeing compiler
// hangs in this location, we will need to re-consider.
Lock.unsafeMaybeUnlock();
Lock.unsafeUnlock();
continue;
}
}
Expand Down
2 changes: 1 addition & 1 deletion llvm/lib/Target/AMDGPU/AMDGPUSplitModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1577,7 +1577,7 @@ PreservedAnalyses AMDGPUSplitModulePass::run(Module &M,
dbgs()
<< "[amdgpu-split-module] unable to acquire lockfile, debug "
"output may be mangled by other processes\n");
Lock.unsafeMaybeUnlock();
Lock.unsafeUnlock();
break; // give up
}
}
Expand Down