https://github.com/qiongsiwu updated https://github.com/llvm/llvm-project/pull/195885
>From 6d77d8a1d969a9b6db8f96de0e0ffe845ef5b677 Mon Sep 17 00:00:00 2001 From: Qiongsi Wu <[email protected]> Date: Thu, 30 Apr 2026 14:59:00 -0700 Subject: [PATCH 1/3] AtomicLineLogger with unit tests. --- clang/include/clang/Basic/AtomicLineLogger.h | 66 ++++++ clang/lib/Basic/AtomicLineLogger.cpp | 77 +++++++ clang/lib/Basic/CMakeLists.txt | 1 + .../unittests/Basic/AtomicLineLoggerTest.cpp | 213 ++++++++++++++++++ clang/unittests/Basic/CMakeLists.txt | 1 + 5 files changed, 358 insertions(+) create mode 100644 clang/include/clang/Basic/AtomicLineLogger.h create mode 100644 clang/lib/Basic/AtomicLineLogger.cpp create mode 100644 clang/unittests/Basic/AtomicLineLoggerTest.cpp diff --git a/clang/include/clang/Basic/AtomicLineLogger.h b/clang/include/clang/Basic/AtomicLineLogger.h new file mode 100644 index 0000000000000..84c5d2d1f8dd6 --- /dev/null +++ b/clang/include/clang/Basic/AtomicLineLogger.h @@ -0,0 +1,66 @@ +//===- AtomicLineLogger.h ---------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +/// \file +/// Defines a logger where each line is written atomically to the file. It is +/// safe to share a logger instance across threads. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_BASIC_ATOMICLINELOGGER_H +#define LLVM_CLANG_BASIC_ATOMICLINELOGGER_H + +#include "clang/Basic/LLVM.h" +#include "llvm/Support/raw_ostream.h" +#include <memory> + +namespace clang { + +class LogLine { + SmallString<128> Buffer; + std::optional<llvm::raw_svector_ostream> FormattingOS; + raw_ostream *Dest = nullptr; + +public: + explicit LogLine(raw_ostream &Dest); + LogLine() {} + LogLine(LogLine &&Other); + LogLine(const LogLine &) = delete; + LogLine &operator=(const LogLine &) = delete; + LogLine &operator=(LogLine &&) = delete; + + ~LogLine() { + if (Dest) { + assert(FormattingOS && "Cannot have uninitialized FormattingOS"); + *FormattingOS << '\n'; + Dest->write(Buffer.data(), Buffer.size()); + } + } + + template <typename T> LogLine &operator<<(const T &Val) { + if (Dest) { + assert(FormattingOS && "Cannot have uninitialized FormattingOS"); + *FormattingOS << Val; + } + return *this; + } +}; + +class AtomicLineLogger { + std::unique_ptr<llvm::raw_fd_ostream> OS; + +public: + AtomicLineLogger() {} + AtomicLineLogger(StringRef LogFilePath); + + LogLine log(); +}; + +} // namespace clang + +#endif \ No newline at end of file diff --git a/clang/lib/Basic/AtomicLineLogger.cpp b/clang/lib/Basic/AtomicLineLogger.cpp new file mode 100644 index 0000000000000..d287ebd75c1b1 --- /dev/null +++ b/clang/lib/Basic/AtomicLineLogger.cpp @@ -0,0 +1,77 @@ +//===- AtomicLineLogger.cpp -----------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This file defines the implementation of an AtomicLineLogger and the relevant +// supporting classes. +// +//===----------------------------------------------------------------------===// + +#include "clang/Basic/AtomicLineLogger.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Process.h" +#include "llvm/Support/Threading.h" +#ifdef __APPLE__ +#include <sys/time.h> +#endif + +using namespace clang; + +static uint64_t getTimestampMillis() { +#ifdef __APPLE__ + // Using chrono is roughly 50% slower. + struct timeval T; + gettimeofday(&T, 0); + return T.tv_sec * 1000 + T.tv_usec / 1000; +#else + auto Time = std::chrono::system_clock::now(); + auto Millis = std::chrono::duration_cast<std::chrono::milliseconds>( + Time.time_since_epoch()); + return Millis.count(); +#endif +} + +LogLine::LogLine(raw_ostream &Dest) : FormattingOS(Buffer), Dest(&Dest) { + auto Millis = getTimestampMillis(); + assert(FormattingOS && "Cannot have unintialized FormattingOS"); + *FormattingOS << llvm::format("[%lld.%0.3lld]", Millis / 1000, Millis % 1000); + *FormattingOS << ' ' << llvm::sys::Process::getProcessId() << ' ' + << llvm::get_threadid() << ": "; +} + +LogLine::LogLine(LogLine &&Other) + : Buffer(std::move(Other.Buffer)), Dest(Other.Dest) { + if (Dest) + FormattingOS.emplace(Buffer); + Other.Dest = nullptr; +} + +AtomicLineLogger::AtomicLineLogger(StringRef LogFilePath) { + if (LogFilePath.empty()) + return; + + std::error_code EC; + OS = std::make_unique<llvm::raw_fd_ostream>( + LogFilePath, EC, llvm::sys::fs::CD_OpenAlways, llvm::sys::fs::FA_Write, + llvm::sys::fs::OF_Append); + if (EC) { + llvm::errs() << "warning: unable to open log file '" << LogFilePath + << "': " << EC.message() << "\n"; + OS.reset(); + return; + } + + // We need to set the OS to unbuffered, so LogLine's destructor can write + // a single line as an atomic operation. + OS->SetUnbuffered(); +} + +LogLine AtomicLineLogger::log() { + if (OS) + return LogLine(*OS); + return LogLine(); +} \ No newline at end of file diff --git a/clang/lib/Basic/CMakeLists.txt b/clang/lib/Basic/CMakeLists.txt index 350c77696bcc4..21851e79b9d15 100644 --- a/clang/lib/Basic/CMakeLists.txt +++ b/clang/lib/Basic/CMakeLists.txt @@ -57,6 +57,7 @@ endif() add_clang_library(clangBasic ASTSourceDescriptor.cpp Attributes.cpp + AtomicLineLogger.cpp Builtins.cpp CLWarnings.cpp CharInfo.cpp diff --git a/clang/unittests/Basic/AtomicLineLoggerTest.cpp b/clang/unittests/Basic/AtomicLineLoggerTest.cpp new file mode 100644 index 0000000000000..0909723e7a174 --- /dev/null +++ b/clang/unittests/Basic/AtomicLineLoggerTest.cpp @@ -0,0 +1,213 @@ +//===- unittests/Basic/AtomicLineLoggerTest.cpp ---------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/Basic/AtomicLineLogger.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Process.h" +#include "llvm/Support/Threading.h" +#include "llvm/Testing/Support/SupportHelpers.h" +#include "gtest/gtest.h" +#include <thread> + +using namespace clang; + +TEST(LogLineTest, NoOpLogLineProducesNoOutput) { + LogLine() << "stream to empty line"; + // Check to make sure streaming into Empty does not lead to crashes. + EXPECT_TRUE(true); +} + +TEST(LogLineTest, MoveConstructor) { + SmallString<128> Buffer; + llvm::raw_svector_ostream OS(Buffer); + + { + LogLine Original(OS); + LogLine Moved(std::move(Original)); + Moved << "after_move"; + } + + StringRef Output = OS.str(); + + // Only one line should be written (from Moved, not from Original). + EXPECT_EQ(Output.count('\n'), 1u); + EXPECT_TRUE(Output.contains("after_move")); +} + +TEST(LogLineTest, ActiveLogLinePIDTIDMsg) { + SmallString<128> Buffer; + llvm::raw_svector_ostream OS(Buffer); + LogLine(OS) << "test_event: " << "some_file.pcm"; + + StringRef Output = OS.str(); + + // Ends with message + newline. + EXPECT_TRUE(Output.ends_with("test_event: some_file.pcm\n")); + + // Prefix has the form: "<timestamp> <pid> <tid>: " + // Verify PID matches this process. + std::string ExpectedPID = std::to_string(llvm::sys::Process::getProcessId()); + EXPECT_TRUE(Output.contains(ExpectedPID)); + + // Verify TID is present. + std::string ExpectedTID = std::to_string(llvm::get_threadid()); + EXPECT_TRUE(Output.contains(ExpectedTID)); +} + +TEST(LogLineTest, ActiveLogLineTimestamp) { + SmallString<128> Buffer; + llvm::raw_svector_ostream OS(Buffer); + // Test that the timestamp generated is always sandwiched between Before + // and After to verify the correctness. + auto Before = std::chrono::duration_cast<std::chrono::milliseconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + LogLine(OS) << "test_event"; + + auto After = std::chrono::duration_cast<std::chrono::milliseconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + StringRef Output = OS.str(); + + // Extract timestamp from "[<seconds>.<millis>]" prefix. + ASSERT_TRUE(Output.starts_with("[")); + size_t CloseBracket = Output.find(']'); + ASSERT_NE(CloseBracket, StringRef::npos); + StringRef TimestampStr = Output.slice(1, CloseBracket); + + // Parse "<seconds>.<millis>". + auto [SecStr, MillisStr] = TimestampStr.split('.'); + uint64_t Seconds, Millis; + ASSERT_FALSE(SecStr.getAsInteger(10, Seconds)); + ASSERT_FALSE(MillisStr.getAsInteger(10, Millis)); + + uint64_t LoggedMillis = Seconds * 1000 + Millis; + EXPECT_GE(LoggedMillis, (uint64_t)Before); + EXPECT_LE(LoggedMillis, (uint64_t)After); +} + +TEST(AtomicLineLoggerTest, DisabledLoggerDoesNotCrash) { + AtomicLineLogger Logger; + Logger.log() << "this goes nowhere"; + + // An empty logger should not crash. + EXPECT_TRUE(true); +} + +TEST(AtomicLineLoggerTest, SingleLineWrittenToFile) { + // Create a temp directory and build a log file path inside it. + llvm::unittest::TempDir Dir("atomic-logger-test", /*Unique=*/true); + SmallString<128> LogPath(Dir.path()); + llvm::sys::path::append(LogPath, "test.log"); + + { + AtomicLineLogger Logger(LogPath); + Logger.log() << "pcm_write: module.pcm"; + } + // Logger destroyed here. Log file is written to disk. + + // Read the file back. + auto BufOrErr = llvm::MemoryBuffer::getFile(LogPath); + ASSERT_TRUE(BufOrErr) << "Failed to read log file"; + StringRef Content = (*BufOrErr)->getBuffer(); + + // Verify the message is present and the line ends with a newline. + EXPECT_TRUE(Content.contains("pcm_write: module.pcm")); + EXPECT_TRUE(Content.ends_with("\n")); + + // Verify there is exactly one line. + EXPECT_EQ(Content.count('\n'), 1u); +} + +TEST(AtomicLineLoggerTest, ConcurrentWritesProduceCompleteLines) { + llvm::unittest::TempDir Dir("atomic-logger-concurrent", /*Unique=*/true); + SmallString<128> LogPath(Dir.path()); + llvm::sys::path::append(LogPath, "concurrent.log"); + + // Testing concurrent writing of the log file. + // Each logged message starts with the string `thread_`, and the message is + // always 32 characters long. + const unsigned NumThreads = 8; + const unsigned LinesPerThread = 100; + // Fixed-width message: "thread_XX_line_XXX" padded to 32 chars with '_'. + const unsigned MessageLen = 32; + + { + AtomicLineLogger Logger(LogPath); + + std::vector<std::thread> Threads; + for (unsigned I = 0; I < NumThreads; ++I) { + Threads.emplace_back([&Logger, I] { + for (unsigned J = 0; J < LinesPerThread; ++J) { + SmallString<64> Msg; + llvm::raw_svector_ostream MsgOS(Msg); + MsgOS << "thread_" << llvm::format("%02u", I) << "_line_" + << llvm::format("%03u", J); + // Pad to fixed width. + while (Msg.size() < MessageLen) + MsgOS << '_'; + Logger.log() << Msg; + } + }); + } + for (auto &T : Threads) + T.join(); + } + // Logger destroyed here. Log file is written to disk. + + auto BufOrErr = llvm::MemoryBuffer::getFile(LogPath); + ASSERT_TRUE(BufOrErr) << "Failed to read log file"; + StringRef Content = (*BufOrErr)->getBuffer(); + + SmallVector<StringRef> Lines; + Content.split(Lines, '\n', /*MaxSplit=*/-1, /*KeepEmpty=*/false); + + EXPECT_EQ(Lines.size(), (size_t)(NumThreads * LinesPerThread)); + + for (const auto &Line : Lines) { + // For each line, we check the separator, message length, message start and + // the prefix format to make sure no lines are interleved. + + // Split at ": " to separate prefix from message body. + auto [Prefix, Body] = Line.split(": "); + ASSERT_FALSE(Body.empty()) + << "Malformed line (no ': ' separator): " << Line.str(); + + // Message body checks. + EXPECT_EQ(Body.size(), (size_t)MessageLen) + << "Wrong message length (interleaving?): " << Line.str(); + EXPECT_TRUE(Body.starts_with("thread_")) + << "Corrupted message body: " << Line.str(); + + // Prefix format: "[<seconds>.<millis>] <pid> <tid>" + // Parse timestamp. + EXPECT_TRUE(Prefix.starts_with("[")) << "Missing '[': " << Prefix.str(); + size_t CloseBracket = Prefix.find(']'); + ASSERT_NE(CloseBracket, StringRef::npos) << "Missing ']': " << Prefix.str(); + + StringRef TimestampStr = Prefix.slice(1, CloseBracket); + auto [SecStr, MillisStr] = TimestampStr.split('.'); + uint64_t Seconds, Millis; + EXPECT_FALSE(SecStr.getAsInteger(10, Seconds)) + << "Bad seconds: " << SecStr.str(); + EXPECT_FALSE(MillisStr.getAsInteger(10, Millis)) + << "Bad millis: " << MillisStr.str(); + + // Parse PID and TID from the rest: " <pid> <tid>" + StringRef Rest = Prefix.substr(CloseBracket + 1).ltrim(); + auto [PidStr, TidStr] = Rest.split(' '); + uint64_t Pid, Tid; + EXPECT_FALSE(PidStr.getAsInteger(10, Pid)) << "Bad PID: " << PidStr.str(); + EXPECT_FALSE(TidStr.getAsInteger(10, Tid)) << "Bad TID: " << TidStr.str(); + + // PID should match this process. + EXPECT_EQ(Pid, (uint64_t)llvm::sys::Process::getProcessId()); + } +} \ No newline at end of file diff --git a/clang/unittests/Basic/CMakeLists.txt b/clang/unittests/Basic/CMakeLists.txt index 058243fd3fdba..32dce09866892 100644 --- a/clang/unittests/Basic/CMakeLists.txt +++ b/clang/unittests/Basic/CMakeLists.txt @@ -1,6 +1,7 @@ # Basic tests have few LLVM and Clang dependencies, so linking it as a # distinct target enables faster iteration times at low cost. add_distinct_clang_unittest(BasicTests + AtomicLineLoggerTest.cpp CharInfoTest.cpp DarwinSDKInfoTest.cpp DiagnosticTest.cpp >From d7e2f553a1a02511965e2b1c0732b9672d28e933 Mon Sep 17 00:00:00 2001 From: Qiongsi Wu <[email protected]> Date: Tue, 5 May 2026 10:08:17 -0700 Subject: [PATCH 2/3] Adding new lines to the ends of new files. --- clang/include/clang/Basic/AtomicLineLogger.h | 3 ++- clang/lib/Basic/AtomicLineLogger.cpp | 3 ++- clang/unittests/Basic/AtomicLineLoggerTest.cpp | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/clang/include/clang/Basic/AtomicLineLogger.h b/clang/include/clang/Basic/AtomicLineLogger.h index 84c5d2d1f8dd6..cc933cb8b7215 100644 --- a/clang/include/clang/Basic/AtomicLineLogger.h +++ b/clang/include/clang/Basic/AtomicLineLogger.h @@ -63,4 +63,5 @@ class AtomicLineLogger { } // namespace clang -#endif \ No newline at end of file +#endif + diff --git a/clang/lib/Basic/AtomicLineLogger.cpp b/clang/lib/Basic/AtomicLineLogger.cpp index d287ebd75c1b1..e5c4c4772cc3c 100644 --- a/clang/lib/Basic/AtomicLineLogger.cpp +++ b/clang/lib/Basic/AtomicLineLogger.cpp @@ -74,4 +74,5 @@ LogLine AtomicLineLogger::log() { if (OS) return LogLine(*OS); return LogLine(); -} \ No newline at end of file +} + diff --git a/clang/unittests/Basic/AtomicLineLoggerTest.cpp b/clang/unittests/Basic/AtomicLineLoggerTest.cpp index 0909723e7a174..4b2c13c25d4e9 100644 --- a/clang/unittests/Basic/AtomicLineLoggerTest.cpp +++ b/clang/unittests/Basic/AtomicLineLoggerTest.cpp @@ -210,4 +210,5 @@ TEST(AtomicLineLoggerTest, ConcurrentWritesProduceCompleteLines) { // PID should match this process. EXPECT_EQ(Pid, (uint64_t)llvm::sys::Process::getProcessId()); } -} \ No newline at end of file +} + >From 8d69aad69c6576f9eb723561984d96b666067f1f Mon Sep 17 00:00:00 2001 From: Qiongsi Wu <[email protected]> Date: Tue, 5 May 2026 11:22:28 -0700 Subject: [PATCH 3/3] Adding missing header include for SmallString. --- clang/include/clang/Basic/AtomicLineLogger.h | 1 + 1 file changed, 1 insertion(+) diff --git a/clang/include/clang/Basic/AtomicLineLogger.h b/clang/include/clang/Basic/AtomicLineLogger.h index cc933cb8b7215..8c30070d1ec2c 100644 --- a/clang/include/clang/Basic/AtomicLineLogger.h +++ b/clang/include/clang/Basic/AtomicLineLogger.h @@ -16,6 +16,7 @@ #define LLVM_CLANG_BASIC_ATOMICLINELOGGER_H #include "clang/Basic/LLVM.h" +#include "llvm/ADT/SmallString.h" #include "llvm/Support/raw_ostream.h" #include <memory> _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
