[Lldb-commits] [lldb] [lldb] Add an artifact provider mechanism to Diagnostics (PR #210417)
Jonas Devlieghere via lldb-commits
lldb-commits at lists.llvm.org
Fri Jul 17 14:03:50 PDT 2026
https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/210417
>From b41eda2098d782e1ed07f9dc1a4e02f0786587ee Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Fri, 17 Jul 2026 11:53:09 -0700
Subject: [PATCH] [lldb] Add an artifact provider mechanism to Diagnostics
Add a hook that lets a subsystem contribute a file to the diagnostics
bundle, without breaking layering and making Core depend on it. A plugin registers an
ArtifactProvider callback and a file name via AddArtifactProvider.
Providers run when a bundle is collected, and each one that yields
content is written into the bundle and recorded in the report's
attachments.
This restores, in a simpler form, the collection point that the removed
Diagnostics callback mechanism offered. The motivating consumer is the
downstream Swift health check, which registers a provider to write its
"swift-healthcheck.log" into the bundle. Unlike the old callback, a
provider does not open or write its own file: it just returns the
contents as a string and Diagnostics handles the rest.
I have a follow-up PR that takes advantage of this new mechanism
upstream to collect the GDB remote packet log.
---
lldb/include/lldb/Core/Diagnostics.h | 30 +++++
lldb/source/Core/Diagnostics.cpp | 30 +++++
lldb/unittests/Core/CMakeLists.txt | 1 +
lldb/unittests/Core/DiagnosticsTest.cpp | 140 ++++++++++++++++++++++++
4 files changed, 201 insertions(+)
create mode 100644 lldb/unittests/Core/DiagnosticsTest.cpp
diff --git a/lldb/include/lldb/Core/Diagnostics.h b/lldb/include/lldb/Core/Diagnostics.h
index 554314e32e8ce..bcacf3b9c301d 100644
--- a/lldb/include/lldb/Core/Diagnostics.h
+++ b/lldb/include/lldb/Core/Diagnostics.h
@@ -14,6 +14,8 @@
#include "lldb/Utility/Log.h"
#include "llvm/Support/Error.h"
+#include <functional>
+#include <mutex>
#include <optional>
#include <string>
#include <vector>
@@ -90,6 +92,19 @@ class Diagnostics {
/// Record a diagnostic message into the always-on, in-memory log.
void Record(llvm::StringRef message);
+ /// Supplies an artifact's contents on demand. Subsystems register a provider
+ /// so Core need not depend on them. Each runs when a bundle is collected.
+ using ArtifactProvider = std::function<std::string()>;
+ using ArtifactProviderID = uint64_t;
+
+ /// Register \p provider to contribute file \p name. Returns an id for
+ /// RemoveArtifactProvider. Thread-safe.
+ ArtifactProviderID AddArtifactProvider(std::string name,
+ ArtifactProvider provider);
+
+ /// Unregister a provider. Thread-safe.
+ void RemoveArtifactProvider(ArtifactProviderID id);
+
static Diagnostics &Instance();
static DiagnosticsProperties &GetGlobalProperties();
@@ -122,6 +137,8 @@ class Diagnostics {
static void CollectBinaries(const ExecutionContext &exe_ctx,
const FileSpec &dir,
std::vector<std::string> &files);
+ void CollectArtifactProviders(const FileSpec &dir,
+ std::vector<std::string> &files);
/// @}
/// Scalars carried in the report rather than written as files.
@@ -131,6 +148,19 @@ class Diagnostics {
/// @}
RotatingLogHandler m_log_handler;
+
+ struct ArtifactProviderEntry {
+ ArtifactProviderID id;
+ std::string name;
+ ArtifactProvider provider;
+ };
+
+ /// Registered artifact providers, guarded by the mutex.
+ /// @{
+ ArtifactProviderID m_next_artifact_provider_id = 0;
+ std::vector<ArtifactProviderEntry> m_artifact_providers;
+ std::mutex m_artifact_providers_mutex;
+ /// @}
};
/// Render a diagnostics report as JSON, for `diagnostics dump`'s terminal
diff --git a/lldb/source/Core/Diagnostics.cpp b/lldb/source/Core/Diagnostics.cpp
index 311d8fab2b1bc..08adcaebdf038 100644
--- a/lldb/source/Core/Diagnostics.cpp
+++ b/lldb/source/Core/Diagnostics.cpp
@@ -25,12 +25,14 @@
#include "lldb/Utility/ProcessInfo.h"
#include "lldb/Version/Version.h"
+#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/JSON.h"
#include "llvm/Support/raw_ostream.h"
+#include <mutex>
#include <optional>
#include <string>
#include <vector>
@@ -149,6 +151,20 @@ void Diagnostics::Record(llvm::StringRef message) {
m_log_handler.Emit(message);
}
+Diagnostics::ArtifactProviderID
+Diagnostics::AddArtifactProvider(std::string name, ArtifactProvider provider) {
+ std::lock_guard<std::mutex> guard(m_artifact_providers_mutex);
+ ArtifactProviderID id = m_next_artifact_provider_id++;
+ m_artifact_providers.push_back({id, std::move(name), std::move(provider)});
+ return id;
+}
+
+void Diagnostics::RemoveArtifactProvider(ArtifactProviderID id) {
+ std::lock_guard<std::mutex> guard(m_artifact_providers_mutex);
+ llvm::erase_if(m_artifact_providers,
+ [id](const ArtifactProviderEntry &e) { return e.id == id; });
+}
+
// Write a single artifact into the bundle and, on success, record its name in
// \p files. Best-effort: a write failure leaves the file out of the list, so a
// missing artifact stays visible. The file is made owner-only because the
@@ -253,6 +269,7 @@ Diagnostics::Collect(Debugger &debugger, const ExecutionContext &exe_ctx,
CollectCommands(debugger, exe_ctx, dir, report.attachments.files);
if (GetGlobalProperties().GetCollectBinaries())
CollectBinaries(exe_ctx, dir, report.attachments.files);
+ CollectArtifactProviders(dir, report.attachments.files);
report.version = lldb_private::GetVersion();
report.os = GetHostDescription(exe_ctx);
@@ -317,6 +334,19 @@ void Diagnostics::CollectBinaries(const ExecutionContext &exe_ctx,
CopyBinary(process->GetCoreFile(), dir, files);
}
+void Diagnostics::CollectArtifactProviders(const FileSpec &dir,
+ std::vector<std::string> &files) {
+ // Snapshot under the lock, then run providers without it: a provider can be
+ // slow and must not block registration or removal.
+ std::vector<ArtifactProviderEntry> providers;
+ {
+ std::lock_guard<std::mutex> guard(m_artifact_providers_mutex);
+ providers = m_artifact_providers;
+ }
+ for (const ArtifactProviderEntry &entry : providers)
+ WriteArtifact(dir, entry.name, entry.provider(), files);
+}
+
std::string Diagnostics::GetHostDescription(const ExecutionContext &exe_ctx) {
std::string os = HostInfo::GetTargetTriple().str();
Target *target = exe_ctx.GetTargetPtr();
diff --git a/lldb/unittests/Core/CMakeLists.txt b/lldb/unittests/Core/CMakeLists.txt
index d69432d332f44..959793d7d4de0 100644
--- a/lldb/unittests/Core/CMakeLists.txt
+++ b/lldb/unittests/Core/CMakeLists.txt
@@ -3,6 +3,7 @@ add_lldb_unittest(LLDBCoreTests
DebuggerTest.cpp
CommunicationTest.cpp
DiagnosticEventTest.cpp
+ DiagnosticsTest.cpp
DumpDataExtractorTest.cpp
DumpRegisterInfoTest.cpp
FormatEntityTest.cpp
diff --git a/lldb/unittests/Core/DiagnosticsTest.cpp b/lldb/unittests/Core/DiagnosticsTest.cpp
new file mode 100644
index 0000000000000..6bf0e7720b8fe
--- /dev/null
+++ b/lldb/unittests/Core/DiagnosticsTest.cpp
@@ -0,0 +1,140 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "lldb/Core/Diagnostics.h"
+#include "Plugins/Platform/MacOSX/PlatformMacOSX.h"
+#include "Plugins/Platform/MacOSX/PlatformRemoteMacOSX.h"
+#include "TestingSupport/TestUtilities.h"
+#include "lldb/Core/Debugger.h"
+#include "lldb/Host/FileSystem.h"
+#include "lldb/Host/HostInfo.h"
+#include "lldb/Target/ExecutionContext.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gtest/gtest.h"
+
+#include <mutex>
+
+using namespace lldb;
+using namespace lldb_private;
+
+namespace {
+class DiagnosticsTest : public ::testing::Test {
+public:
+ void SetUp() override {
+ FileSystem::Initialize();
+ HostInfo::Initialize();
+ PlatformMacOSX::Initialize();
+ std::call_once(TestUtilities::g_debugger_initialize_flag,
+ []() { Debugger::Initialize(nullptr); });
+ ArchSpec arch("x86_64-apple-macosx-");
+ Platform::SetHostPlatform(
+ PlatformRemoteMacOSX::CreateInstance(true, &arch));
+ }
+ void TearDown() override {
+ PlatformMacOSX::Terminate();
+ HostInfo::Terminate();
+ FileSystem::Terminate();
+ }
+};
+
+// Read a file from the bundle directory, or "" if it cannot be read.
+std::string ReadBundleFile(const FileSpec &dir, llvm::StringRef name) {
+ FileSpec path = dir.CopyByAppendingPathComponent(name);
+ llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> buffer =
+ llvm::MemoryBuffer::getFile(path.GetPath());
+ if (!buffer)
+ return "";
+ return (*buffer)->getBuffer().str();
+}
+} // namespace
+
+TEST_F(DiagnosticsTest, ArtifactProviderIDsAreUnique) {
+ Diagnostics diagnostics;
+ Diagnostics::ArtifactProviderID id0 =
+ diagnostics.AddArtifactProvider("a.txt", [] { return "a"; });
+ Diagnostics::ArtifactProviderID id1 =
+ diagnostics.AddArtifactProvider("b.txt", [] { return "b"; });
+ EXPECT_NE(id0, id1);
+}
+
+TEST_F(DiagnosticsTest, ArtifactProviderContributesToBundle) {
+ DebuggerSP debugger_sp = Debugger::CreateInstance();
+ ASSERT_TRUE(debugger_sp);
+
+ Diagnostics diagnostics;
+ int call_count = 0;
+ Diagnostics::ArtifactProviderID id = diagnostics.AddArtifactProvider(
+ "my-artifact.txt", [&call_count]() -> std::string {
+ ++call_count;
+ return "hello diagnostics";
+ });
+
+ ExecutionContext exe_ctx;
+
+ // A registered provider is invoked and its content lands in the bundle and
+ // in the report's attachments.
+ {
+ llvm::Expected<FileSpec> dir = Diagnostics::CreateUniqueDirectory();
+ ASSERT_THAT_EXPECTED(dir, llvm::Succeeded());
+ llvm::Expected<Diagnostics::Report> report =
+ diagnostics.Collect(*debugger_sp, exe_ctx, *dir);
+ ASSERT_THAT_EXPECTED(report, llvm::Succeeded());
+
+ EXPECT_EQ(call_count, 1);
+ EXPECT_TRUE(
+ llvm::is_contained(report->attachments.files, "my-artifact.txt"));
+ EXPECT_EQ(ReadBundleFile(*dir, "my-artifact.txt"), "hello diagnostics");
+
+ llvm::sys::fs::remove_directories(dir->GetPath());
+ }
+
+ // After removal the provider is neither invoked nor present in the bundle.
+ diagnostics.RemoveArtifactProvider(id);
+ call_count = 0;
+ {
+ llvm::Expected<FileSpec> dir = Diagnostics::CreateUniqueDirectory();
+ ASSERT_THAT_EXPECTED(dir, llvm::Succeeded());
+ llvm::Expected<Diagnostics::Report> report =
+ diagnostics.Collect(*debugger_sp, exe_ctx, *dir);
+ ASSERT_THAT_EXPECTED(report, llvm::Succeeded());
+
+ EXPECT_EQ(call_count, 0);
+ EXPECT_FALSE(
+ llvm::is_contained(report->attachments.files, "my-artifact.txt"));
+
+ llvm::sys::fs::remove_directories(dir->GetPath());
+ }
+
+ Debugger::Destroy(debugger_sp);
+}
+
+TEST_F(DiagnosticsTest, RemoveArtifactProviderIgnoresUnknownID) {
+ Diagnostics diagnostics;
+ Diagnostics::ArtifactProviderID id =
+ diagnostics.AddArtifactProvider("kept.txt", [] { return "kept"; });
+
+ // Removing an id that was never handed out must not disturb other providers.
+ diagnostics.RemoveArtifactProvider(id + 1);
+
+ DebuggerSP debugger_sp = Debugger::CreateInstance();
+ ASSERT_TRUE(debugger_sp);
+ ExecutionContext exe_ctx;
+ llvm::Expected<FileSpec> dir = Diagnostics::CreateUniqueDirectory();
+ ASSERT_THAT_EXPECTED(dir, llvm::Succeeded());
+ llvm::Expected<Diagnostics::Report> report =
+ diagnostics.Collect(*debugger_sp, exe_ctx, *dir);
+ ASSERT_THAT_EXPECTED(report, llvm::Succeeded());
+
+ EXPECT_TRUE(llvm::is_contained(report->attachments.files, "kept.txt"));
+
+ llvm::sys::fs::remove_directories(dir->GetPath());
+ Debugger::Destroy(debugger_sp);
+}
More information about the lldb-commits
mailing list