From dbb8b15edb5e63f37a66dd15e67d46ee1b4f6c1b Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 7 Aug 2024 11:08:44 -0400 Subject: [PATCH 01/30] [llvm][lib]Propose a simple Telemetry framework. Objective: - Provide a common framework in LLVM for collecting various usage metrics - Characteristics: + Extensible and configurable by: * tools in LLVM that want to use it * vendors in their downstream codebase * tools users (as allowed by vendor) Background: The framework was originally proposed only for LLDB, but there were quite a few requests that it should be moved to llvm/lib given telemetry is a common usage to a lot of tools, not just LLDB. See more details on the design and discussions here on the RFC: https://discourse.llvm.org/t/rfc-lldb-telemetry-metrics/64588/20?u=oontvoo --- llvm/include/llvm/Telemetry/Telemetry.h | 99 +++++++++++++++++++++++++ llvm/lib/CMakeLists.txt | 1 + llvm/lib/Telemetry/CMakeLists.txt | 6 ++ llvm/lib/Telemetry/Telemetry.cpp | 32 ++++++++ 4 files changed, 138 insertions(+) create mode 100644 llvm/include/llvm/Telemetry/Telemetry.h create mode 100644 llvm/lib/Telemetry/CMakeLists.txt create mode 100644 llvm/lib/Telemetry/Telemetry.cpp diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h new file mode 100644 index 0000000000000..e34b228b219c1 --- /dev/null +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -0,0 +1,99 @@ +#ifndef LVM_TELEMETRY_TELEMETRY_H +#define LVM_TELEMETRY_TELEMETRY_H + +#include +#include +#include +#include +#include + +#include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/JSON.h" + +namespace llvm { +namespace telemetry { + +using SteadyTimePoint = std::chrono::time_point; + +struct TelemetryConfig { + // If true, telemetry will be enabled. + bool enable_telemetry; + + // Additional destinations to send the logged entries. + // Could be stdout, stderr, or some local paths. + // Note: these are destinations are __in addition to__ whatever the default + // destination(s) are, as implemented by vendors. + std::vector additional_destinations; +}; + +struct TelemetryEventStats { + // REQUIRED: Start time of event + SteadyTimePoint m_start; + // OPTIONAL: End time of event - may be empty if not meaningful. + std::optional m_end; + // TBD: could add some memory stats here too? + + TelemetryEventStats() = default; + TelemetryEventStats(SteadyTimePoint start) : m_start(start) {} + TelemetryEventStats(SteadyTimePoint start, SteadyTimePoint end) + : m_start(start), m_end(end) {} + + std::string ToString() const; +}; + +struct ExitDescription { + int exit_code; + std::string description; + + std::string ToString() const; +}; + +// The base class contains the basic set of data. +// Downstream implementations can add more fields as needed. +struct TelemetryInfo { + // A "session" corresponds to every time the tool starts. + // All entries emitted for the same session will have + // the same session_uuid + std::string session_uuid; + + TelemetryEventStats stats; + + std::optional exit_description; + + // Counting number of entries. + // (For each set of entries with the same session_uuid, this value should + // be unique for each entry) + size_t counter; + + TelemetryInfo() = default; + ~TelemetryInfo() = default; + virtual std::string ToString() const; +}; + +// Where/how to send the telemetry entries. +class TelemetryDestination { +public: + virtual ~TelemetryDestination() = default; + virtual Error EmitEntry(const TelemetryInfo *entry) = 0; + virtual std::string name() const = 0; +}; + +class Telemeter { +public: + virtual ~Telemeter() = default; + + // Invoked upon tool startup + virtual void LogStartup(llvm::StringRef tool_path, TelemetryInfo *entry) = 0; + + // Invoked upon tool exit. + virtual void LogExit(llvm::StringRef tool_path, TelemetryInfo *entry) = 0; + + virtual void AddDestination(TelemetryDestination *destination) = 0; +}; + +} // namespace telemetry +} // namespace llvm + +#endif // LVM_TELEMETRY_TELEMETRY_H diff --git a/llvm/lib/CMakeLists.txt b/llvm/lib/CMakeLists.txt index 638c3bd6f90f5..1d2fb32922648 100644 --- a/llvm/lib/CMakeLists.txt +++ b/llvm/lib/CMakeLists.txt @@ -41,6 +41,7 @@ add_subdirectory(ProfileData) add_subdirectory(Passes) add_subdirectory(TargetParser) add_subdirectory(TextAPI) +add_subdirectory(Telemetry) add_subdirectory(ToolDrivers) add_subdirectory(XRay) if (LLVM_INCLUDE_TESTS) diff --git a/llvm/lib/Telemetry/CMakeLists.txt b/llvm/lib/Telemetry/CMakeLists.txt new file mode 100644 index 0000000000000..8208bdadb05e9 --- /dev/null +++ b/llvm/lib/Telemetry/CMakeLists.txt @@ -0,0 +1,6 @@ +add_llvm_component_library(LLVMTelemetry + Telemetry.cpp + + ADDITIONAL_HEADER_DIRS + "${LLVM_MAIN_INCLUDE_DIR}/llvm/Telemetry" +) diff --git a/llvm/lib/Telemetry/Telemetry.cpp b/llvm/lib/Telemetry/Telemetry.cpp new file mode 100644 index 0000000000000..f7100685ee2d2 --- /dev/null +++ b/llvm/lib/Telemetry/Telemetry.cpp @@ -0,0 +1,32 @@ +#include "llvm/Telemetry/Telemetry.h" + +namespace llvm { +namespace telemetry { + +std::string TelemetryEventStats::ToString() const { + std::string result; + llvm::raw_string_ostream os(result); + os << "start_timestamp: " << m_start.time_since_epoch().count() + << ", end_timestamp: " + << (m_end.has_value() ? std::to_string(m_end->time_since_epoch().count()) + : ""); + return result; +} + +std::string ExitDescription::ToString() const { + return "exit_code: " + std::to_string(exit_code) + + ", description: " + description + "\n"; +} + +std::string TelemetryInfo::ToString() const { + return "[TelemetryInfo]\n" + (" session_uuid:" + session_uuid + "\n") + + (" stats: " + stats.ToString() + "\n") + + (" exit_description: " + + (exit_description.has_value() ? exit_description->ToString() + : "") + + "\n") + + (" counter: " + std::to_string(counter) + "\n"); +} + +} // namespace telemetry +} // namespace llvm \ No newline at end of file From 6e43f67c9f0412fbf0f4a16f2be68daa40d4b6f4 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 7 Aug 2024 13:21:32 -0400 Subject: [PATCH 02/30] add header --- llvm/include/llvm/Telemetry/Telemetry.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index e34b228b219c1..fab170c61c02e 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -1,3 +1,15 @@ +//===- llvm/Telemetry/Telemetry.h - Telemetry -------------------*- 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 +// +//===----------------------------------------------------------------------===// +// +// Defines the commom Telemetry framework. +// +//===----------------------------------------------------------------------===// + #ifndef LVM_TELEMETRY_TELEMETRY_H #define LVM_TELEMETRY_TELEMETRY_H From e25f5fcd79f84086f4fddb7288d353cf0c0858c0 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 7 Aug 2024 14:25:22 -0400 Subject: [PATCH 03/30] fixed typo --- llvm/include/llvm/Telemetry/Telemetry.h | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index fab170c61c02e..dc24f9c3fc3fb 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -5,13 +5,9 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// -// -// Defines the commom Telemetry framework. -// -//===----------------------------------------------------------------------===// -#ifndef LVM_TELEMETRY_TELEMETRY_H -#define LVM_TELEMETRY_TELEMETRY_H +#ifndef LLVM_TELEMETRY_TELEMETRY_H +#define LLVM_TELEMETRY_TELEMETRY_H #include #include @@ -108,4 +104,4 @@ class Telemeter { } // namespace telemetry } // namespace llvm -#endif // LVM_TELEMETRY_TELEMETRY_H +#endif // LLVM_TELEMETRY_TELEMETRY_H From 0057bcf63de085a4d41184eac7d4d4fe9ba7f299 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 28 Aug 2024 22:12:30 -0400 Subject: [PATCH 04/30] add tests and addressed review comments --- llvm/include/llvm/Telemetry/Telemetry.h | 53 +- llvm/lib/Telemetry/Telemetry.cpp | 31 +- llvm/unittests/CMakeLists.txt | 1 + llvm/unittests/Telemetry/CMakeLists.txt | 9 + llvm/unittests/Telemetry/TelemetryTest.cpp | 606 +++++++++++++++++++++ 5 files changed, 652 insertions(+), 48 deletions(-) create mode 100644 llvm/unittests/Telemetry/CMakeLists.txt create mode 100644 llvm/unittests/Telemetry/TelemetryTest.cpp diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index dc24f9c3fc3fb..935b1feddbed6 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -27,35 +27,37 @@ using SteadyTimePoint = std::chrono::time_point; struct TelemetryConfig { // If true, telemetry will be enabled. - bool enable_telemetry; + bool EnableTelemetry; // Additional destinations to send the logged entries. // Could be stdout, stderr, or some local paths. // Note: these are destinations are __in addition to__ whatever the default // destination(s) are, as implemented by vendors. - std::vector additional_destinations; + std::vector AdditionalDestinations; }; struct TelemetryEventStats { // REQUIRED: Start time of event - SteadyTimePoint m_start; + SteadyTimePoint Start; // OPTIONAL: End time of event - may be empty if not meaningful. - std::optional m_end; + std::optional End; // TBD: could add some memory stats here too? TelemetryEventStats() = default; - TelemetryEventStats(SteadyTimePoint start) : m_start(start) {} - TelemetryEventStats(SteadyTimePoint start, SteadyTimePoint end) - : m_start(start), m_end(end) {} - - std::string ToString() const; + TelemetryEventStats(SteadyTimePoint Start) : Start(Start) {} + TelemetryEventStats(SteadyTimePoint Start, SteadyTimePoint End) + : Start(Start), End(End) {} }; struct ExitDescription { - int exit_code; - std::string description; + int ExitCode; + std::string Description; +}; - std::string ToString() const; +// For isa, dyn_cast, etc operations on TelemetryInfo. +typedef unsigned KindType; +struct EntryKind { + static const KindType Base = 0; }; // The base class contains the basic set of data. @@ -64,41 +66,46 @@ struct TelemetryInfo { // A "session" corresponds to every time the tool starts. // All entries emitted for the same session will have // the same session_uuid - std::string session_uuid; + std::string SessionUuid; - TelemetryEventStats stats; + TelemetryEventStats Stats; - std::optional exit_description; + std::optional ExitDesc; // Counting number of entries. // (For each set of entries with the same session_uuid, this value should // be unique for each entry) - size_t counter; + size_t Counter; TelemetryInfo() = default; ~TelemetryInfo() = default; - virtual std::string ToString() const; + + virtual json::Object serializeToJson() const; + + // For isa, dyn_cast, etc, operations. + virtual KindType getEntryKind() const { return EntryKind::Base; } + static bool classof(const TelemetryInfo* T) { + return T->getEntryKind() == EntryKind::Base; + } }; // Where/how to send the telemetry entries. class TelemetryDestination { public: virtual ~TelemetryDestination() = default; - virtual Error EmitEntry(const TelemetryInfo *entry) = 0; + virtual Error emitEntry(const TelemetryInfo *Entry) = 0; virtual std::string name() const = 0; }; class Telemeter { public: - virtual ~Telemeter() = default; - // Invoked upon tool startup - virtual void LogStartup(llvm::StringRef tool_path, TelemetryInfo *entry) = 0; + virtual void logStartup(llvm::StringRef ToolPath, TelemetryInfo *Entry) = 0; // Invoked upon tool exit. - virtual void LogExit(llvm::StringRef tool_path, TelemetryInfo *entry) = 0; + virtual void logExit(llvm::StringRef ToolPath, TelemetryInfo *Entry) = 0; - virtual void AddDestination(TelemetryDestination *destination) = 0; + virtual void addDestination(TelemetryDestination *Destination) = 0; }; } // namespace telemetry diff --git a/llvm/lib/Telemetry/Telemetry.cpp b/llvm/lib/Telemetry/Telemetry.cpp index f7100685ee2d2..b9605c4a38ecd 100644 --- a/llvm/lib/Telemetry/Telemetry.cpp +++ b/llvm/lib/Telemetry/Telemetry.cpp @@ -3,30 +3,11 @@ namespace llvm { namespace telemetry { -std::string TelemetryEventStats::ToString() const { - std::string result; - llvm::raw_string_ostream os(result); - os << "start_timestamp: " << m_start.time_since_epoch().count() - << ", end_timestamp: " - << (m_end.has_value() ? std::to_string(m_end->time_since_epoch().count()) - : ""); - return result; -} - -std::string ExitDescription::ToString() const { - return "exit_code: " + std::to_string(exit_code) + - ", description: " + description + "\n"; -} - -std::string TelemetryInfo::ToString() const { - return "[TelemetryInfo]\n" + (" session_uuid:" + session_uuid + "\n") + - (" stats: " + stats.ToString() + "\n") + - (" exit_description: " + - (exit_description.has_value() ? exit_description->ToString() - : "") + - "\n") + - (" counter: " + std::to_string(counter) + "\n"); -} +llvm::json::Object TelemetryInfo::serializeToJson() const { + return json::Object { + {"UUID", SessionUuid}, + }; +}; } // namespace telemetry -} // namespace llvm \ No newline at end of file +} // namespace llvm diff --git a/llvm/unittests/CMakeLists.txt b/llvm/unittests/CMakeLists.txt index 911ede701982f..9d6b3999c4395 100644 --- a/llvm/unittests/CMakeLists.txt +++ b/llvm/unittests/CMakeLists.txt @@ -49,6 +49,7 @@ add_subdirectory(Support) add_subdirectory(TableGen) add_subdirectory(Target) add_subdirectory(TargetParser) +add_subdirectory(Telemetry) add_subdirectory(Testing) add_subdirectory(TextAPI) add_subdirectory(Transforms) diff --git a/llvm/unittests/Telemetry/CMakeLists.txt b/llvm/unittests/Telemetry/CMakeLists.txt new file mode 100644 index 0000000000000..a40ae4b2f5560 --- /dev/null +++ b/llvm/unittests/Telemetry/CMakeLists.txt @@ -0,0 +1,9 @@ +set(LLVM_LINK_COMPONENTS + Telemetry + Core + Support + ) + +add_llvm_unittest(TelemetryTests + TelemetryTest.cpp + ) diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp new file mode 100644 index 0000000000000..36a4daf55e101 --- /dev/null +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -0,0 +1,606 @@ +//===- llvm/unittest/Telemetry/TelemetryTest.cpp - Telemetry unittests ---===// +// +// 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 "llvm/Telemetry/Telemetry.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/IR/Constants.h" +#include "llvm/IR/DataLayout.h" +#include "llvm/IR/DebugInfoMetadata.h" +#include "llvm/IR/LLVMContext.h" +#include "llvm/IR/Module.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/SourceMgr.h" +#include "llvm/Support/raw_ostream.h" +#include "gtest/gtest.h" + +#include +#include +#include +#include + +#include // TODO: remove this + +// Testing parameters.ve +static thread_local bool HasExitError = false; +static thread_local std::string ExitMsg = ""; +static thread_local bool HasVendorConfig = false; +static thread_local bool SanitizeData = false; +static thread_local std::string Buffer = ""; +static thread_local std::vector EmittedJsons; + +namespace llvm { +namespace telemetry { +namespace vendor_code { + +// Generate unique (but deterministic "uuid" for testing purposes). +static std::string nextUuid() { + static size_t seed = 1111; + return std::to_string(seed++); +} + +struct VendorEntryKind { + // TODO: should avoid dup with other vendors' Types? + static const KindType VendorCommon = 0b010101000; + static const KindType Startup = 0b010101001; + static const KindType Exit = 0b010101010; + }; + + +// Demonstrates that the TelemetryInfo (data courier) struct can be extended +// by downstream code to store additional data as needed. +// It can also define additional data serialization method. +struct VendorCommonTelemetryInfo : public TelemetryInfo { + + static bool classof(const TelemetryInfo* T) { + // Subclasses of this is also acceptable. + return (T->getEntryKind() & VendorEntryKind::VendorCommon) == VendorEntryKind::VendorCommon; + + } + + KindType getEntryKind() const override { return VendorEntryKind::VendorCommon;} + + virtual void serializeToStream(llvm::raw_ostream &OS) const = 0; +}; + + +struct StartupEvent : public VendorCommonTelemetryInfo { + std::string MagicStartupMsg; + + StartupEvent() = default; + StartupEvent(const StartupEvent &E) { + SessionUuid = E.SessionUuid; + Stats = E.Stats; + ExitDesc = E.ExitDesc; + Counter = E.Counter; + + MagicStartupMsg = E.MagicStartupMsg; + } + + static bool classof(const TelemetryInfo* T) { + return T->getEntryKind() == VendorEntryKind::Startup; + } + + KindType getEntryKind() const override { return VendorEntryKind::Startup;} + + void serializeToStream(llvm::raw_ostream &OS) const override { + OS<< "UUID:" << SessionUuid << "\n"; + OS << "MagicStartupMsg:" << MagicStartupMsg << "\n"; + } + + json::Object serializeToJson() const override { + return json::Object{ + {"Startup", { {"UUID", SessionUuid}, + {"MagicStartupMsg", MagicStartupMsg}}}, + }; + } +}; + +struct ExitEvent : public VendorCommonTelemetryInfo { + std::string MagicExitMsg; + + ExitEvent() = default; + // Provide a copy ctor because we may need to make a copy + // before sanitizing the Entry. + ExitEvent(const ExitEvent &E) { + SessionUuid = E.SessionUuid; + Stats = E.Stats; + ExitDesc = E.ExitDesc; + Counter = E.Counter; + + MagicExitMsg = E.MagicExitMsg; + } + + static bool classof(const TelemetryInfo* T) { + return T->getEntryKind() == VendorEntryKind::Exit; + } + + unsigned getEntryKind() const override { return VendorEntryKind::Exit;} + + void serializeToStream(llvm::raw_ostream &OS) const override { + OS << "UUID:" << SessionUuid << "\n"; + if (ExitDesc.has_value()) + OS << "ExitCode:" << ExitDesc->ExitCode << "\n"; + OS << "MagicExitMsg:" << MagicExitMsg << "\n"; + } + + json::Object serializeToJson() const override { + json::Array I = json::Array{ + {"UUID", SessionUuid}, + {"MagicExitMsg", MagicExitMsg}, + }; + if (ExitDesc.has_value()) + I.push_back(json::Value({"ExitCode", ExitDesc->ExitCode})); + return json::Object { + {"Exit", std::move(I)}, + }; + } +}; + +struct CustomTelemetryEvent : public VendorCommonTelemetryInfo { + std::vector Msgs; + + CustomTelemetryEvent() = default; + CustomTelemetryEvent(const CustomTelemetryEvent &E) { + SessionUuid = E.SessionUuid; + Stats = E.Stats; + ExitDesc = E.ExitDesc; + Counter = E.Counter; + + Msgs = E.Msgs; + } + + void serializeToStream(llvm::raw_ostream &OS) const override { + int I = 0; + for (const std::string &M : Msgs) { + OS << "MSG_" << I << ":" << M << "\n"; + ++I; + } + } + + json::Object serializeToJson() const override { + json::Object Inner; + int I = 0; + for (const std::string &M : Msgs) { + Inner.try_emplace(("MSG_" + llvm::Twine(I)).str(), M); + ++I; + } + return json::Object { + {"Midpoint", std::move(Inner)}} + ; + } +}; + + +// The following classes demonstrate how downstream code can +// define one or more custom TelemetryDestination(s) to handle +// Telemetry data differently, specifically: +// + which data to send (fullset or sanitized) +// + where to send the data +// + in what form + +const std::string STRING_DEST( "STRING"); +const std::string JSON_DEST ("JSON"); + +// This Destination sends data to a std::string given at ctor. +class StringDestination : public TelemetryDestination { +public: + // ShouldSanitize: if true, sanitize the data before emitting, otherwise, emit + // the full set. + StringDestination(bool ShouldSanitize, std::string& Buf) + : ShouldSanitize(ShouldSanitize), OS(Buf) { + } + + Error emitEntry(const TelemetryInfo *Entry) override { + if (isa(Entry)) { + if (auto *E = dyn_cast(Entry)) { + if (ShouldSanitize) { + if (isa(E) || isa(E)) { + // There is nothing to sanitize for this type of data, so keep as-is. + E->serializeToStream(OS); + } else if (isa(E)) { + auto Sanitized = sanitizeFields(dyn_cast(E)); + Sanitized.serializeToStream(OS); + } else { + llvm_unreachable("unexpected type"); + } + } else { + E->serializeToStream(OS); + } + } + } else { + // Unfamiliar entries, just send the entry's UUID + OS << "UUID:" << Entry->SessionUuid << "\n"; + } + return Error::success(); + } + + std::string name() const override { return STRING_DEST;} + +private: + // Returns a copy of the given entry, but with some fields sanitized. + CustomTelemetryEvent sanitizeFields(const CustomTelemetryEvent* Entry) { + CustomTelemetryEvent Sanitized(*Entry); + // Pretend that messages stored at ODD positions are "sensitive", + // hence need to be sanitized away. + int S = Sanitized.Msgs.size() - 1; + for (int I = S % 2 == 0 ? S - 1 : S; I >= 0; I -= 2) + Sanitized.Msgs[I]=""; + return Sanitized; + } + + bool ShouldSanitize; + llvm::raw_string_ostream OS; + +}; + +// This Destination sends data to some "blackbox" in form of JSON. +class JsonStreamDestination : public TelemetryDestination { +public: + JsonStreamDestination(bool ShouldSanitize) + : ShouldSanitize(ShouldSanitize) {} + + Error emitEntry(const TelemetryInfo *Entry) override { + if (auto *E = dyn_cast(Entry)) { + if (ShouldSanitize) { + if (isa(E) || isa(E)) { + // There is nothing to sanitize for this type of data, so keep as-is. + return SendToBlackbox(E->serializeToJson()); + } else if (isa(E)) { + auto Sanitized = sanitizeFields(dyn_cast(E)); + return SendToBlackbox(Sanitized.serializeToJson()); + } else { + llvm_unreachable("unexpected type"); + } + } else { + return SendToBlackbox(E->serializeToJson()); + } + } else { + // Unfamiliar entries, just send the entry's UUID + return SendToBlackbox(json::Object{{"UUID", Entry->SessionUuid}}); + } + return make_error("unhandled codepath in emitEntry", + inconvertibleErrorCode()); + } + + std::string name() const override { return JSON_DEST;} +private: + + // Returns a copy of the given entry, but with some fields sanitized. + CustomTelemetryEvent sanitizeFields(const CustomTelemetryEvent* Entry) { + CustomTelemetryEvent Sanitized(*Entry); + // Pretend that messages stored at EVEN positions are "sensitive", + // hence need to be sanitized away. + int S = Sanitized.Msgs.size() - 1; + for (int I = S % 2 == 0 ? S : S - 1; I >= 0; I -= 2) + Sanitized.Msgs[I]=""; + + return Sanitized; + } + + llvm::Error SendToBlackbox(json::Object O) { + // Here is where the vendor-defined Destination class can + // send the data to some internal storage. + // For testing purposes, we just queue up the entries to + // the vector for validation. + EmittedJsons.push_back(std::move(O)); + return Error::success(); + } + bool ShouldSanitize; +}; + +// Custom vendor-defined Telemeter that has additional data-collection point. +class TestTelemeter : public Telemeter { +public: + TestTelemeter(std::string SessionUuid) : Uuid(SessionUuid), Counter(0) {} + + static std::unique_ptr createInstance(TelemetryConfig *config) { + if (!config->EnableTelemetry) return std::unique_ptr(nullptr); + std::unique_ptr Telemeter = std::make_unique(nextUuid()); + // Set up Destination based on the given config. + for (const std::string &Dest : config->AdditionalDestinations) { + // The destination(s) are ALSO defined by vendor, so it should understand + // what the name of each destination signifies. + if (Dest == JSON_DEST) { + Telemeter->addDestination(new vendor_code::JsonStreamDestination(SanitizeData)); + } else if (Dest == STRING_DEST) { + Telemeter->addDestination(new vendor_code::StringDestination(SanitizeData, Buffer)); + } else { + llvm_unreachable(llvm::Twine("unknown destination: ", Dest).str().c_str()); + } + } + return Telemeter; + } + + void logStartup(llvm::StringRef ToolPath, TelemetryInfo *Entry) override { + ToolName = ToolPath.str(); + + // The vendor can add additional stuff to the entry before logging. + if (auto* S = dyn_cast(Entry)) { + S->MagicStartupMsg = llvm::Twine("One_", ToolPath).str(); + } + emitToDestinations(Entry); + } + + void logExit(llvm::StringRef ToolPath, TelemetryInfo *Entry) override { + // Ensure we're shutting down the same tool we started with. + if (ToolPath != ToolName){ + std::string Str; + raw_string_ostream OS(Str); + OS << "Expected tool with name" << ToolName << ", but got " << ToolPath; + llvm_unreachable(Str.c_str()); + } + + // The vendor can add additional stuff to the entry before logging. + if (auto * E = dyn_cast(Entry)) { + E->MagicExitMsg = llvm::Twine("Three_", ToolPath).str(); + } + + emitToDestinations(Entry); + } + + void addDestination(TelemetryDestination* Dest) override { + Destinations.push_back(Dest); + } + + void logMidpoint(TelemetryInfo *Entry) { + // The custom Telemeter can record and send additional data. + if (auto * C = dyn_cast(Entry)) { + C->Msgs.push_back("Two"); + C->Msgs.push_back("Deux"); + C->Msgs.push_back("Zwei"); + } + + emitToDestinations(Entry); + } + + ~TestTelemeter() { + for (auto* Dest : Destinations) + delete Dest; + } + + template + T makeDefaultTelemetryInfo() { + T Ret; + Ret.SessionUuid = Uuid; + Ret.Counter = Counter++; + return Ret; + } +private: + + void emitToDestinations(TelemetryInfo *Entry) { + for (TelemetryDestination *Dest : Destinations) { + llvm::Error err = Dest->emitEntry(Entry); + if(err) { + // Log it and move on. + } + } + } + + const std::string Uuid; + size_t Counter; + std::string ToolName; + std::vector Destinations; +}; + +// Pretend to be a "weakly" defined vendor-specific function. +void ApplyVendorSpecificConfigs(TelemetryConfig *config) { + config->EnableTelemetry = true; +} + +} // namespace vendor_code +} // namespace telemetry +} // namespace llvm + +namespace { + +void ApplyCommonConfig(llvm::telemetry::TelemetryConfig* config) { + // Any shareable configs for the upstream tool can go here. + // ..... +} + +std::shared_ptr GetTelemetryConfig() { + // Telemetry is disabled by default. + // The vendor can enable in their config. + auto Config = std::make_shared(); + Config->EnableTelemetry = false; + + ApplyCommonConfig(Config.get()); + + // Apply vendor specific config, if present. + // In practice, this would be a build-time param. + // Eg: + // + // #ifdef HAS_VENDOR_TELEMETRY_CONFIG + // llvm::telemetry::vendor_code::ApplyVendorSpecificConfigs(config.get()); + // #endif + // But for unit testing, we use the testing params defined at the top. + if (HasVendorConfig) { + llvm::telemetry::vendor_code::ApplyVendorSpecificConfigs(Config.get()); + } + return Config; +} + +using namespace llvm; +using namespace llvm::telemetry; + +// For deterministic tests, pre-defined certain important time-points +// rather than using now(). +// +// Preset StartTime to EPOCH. +auto StartTime = std::chrono::time_point{}; +// Pretend the time it takes for the tool's initialization is EPOCH + 5 milliseconds +auto InitCompleteTime = StartTime + std::chrono::milliseconds(5); +auto MidPointTime = StartTime + std::chrono::milliseconds(10); +auto MidPointCompleteTime = MidPointTime + std::chrono::milliseconds(5); +// Preset ExitTime to EPOCH + 20 milliseconds +auto ExitTime = StartTime + std::chrono::milliseconds(20); +// Pretend the time it takes to complete tearing down the tool is 10 milliseconds. +auto ExitCompleteTime = ExitTime + std::chrono::milliseconds(10); + +void AtToolStart(std::string ToolName, vendor_code::TestTelemeter* T) { + vendor_code::StartupEvent Entry = T->makeDefaultTelemetryInfo(); + Entry.Stats = {StartTime, InitCompleteTime}; + T->logStartup(ToolName, &Entry); +} + +void AtToolExit(std::string ToolName, vendor_code::TestTelemeter* T) { + vendor_code::ExitEvent Entry = T->makeDefaultTelemetryInfo(); + Entry.Stats = {ExitTime, ExitCompleteTime}; + + if (HasExitError) { + Entry.ExitDesc = {1, ExitMsg}; + } + T->logExit(ToolName, &Entry); +} + +void AtToolMidPoint (vendor_code::TestTelemeter* T) { + vendor_code::CustomTelemetryEvent Entry = T->makeDefaultTelemetryInfo(); + Entry.Stats = {MidPointTime, MidPointCompleteTime}; + T->logMidpoint(&Entry); +} + +// Helper function to print the given object content to string. +static std::string ValueToString(const json::Value* V) { + std::string Ret; + llvm::raw_string_ostream P(Ret); + P << *V; + return Ret; +} + +// Without vendor's implementation, telemetry is not enabled by default. +TEST(TelemetryTest, TelemetryDefault) { + HasVendorConfig = false; + std::shared_ptr Config = GetTelemetryConfig(); + auto Tool = vendor_code::TestTelemeter::createInstance(Config.get()); + + EXPECT_EQ(nullptr, Tool.get()); +} + +TEST(TelemetryTest, TelemetryEnabled) { + const std::string ToolName = "TestToolOne"; + + // Preset some test params. + HasVendorConfig = true; + SanitizeData = false; + Buffer = ""; + EmittedJsons.clear(); + + std::shared_ptr Config = GetTelemetryConfig(); + + // Add some destinations + Config->AdditionalDestinations.push_back(vendor_code::STRING_DEST); + Config->AdditionalDestinations.push_back(vendor_code::JSON_DEST); + + auto Tool = vendor_code::TestTelemeter::createInstance(Config.get()); + + AtToolStart(ToolName, Tool.get()); + AtToolMidPoint(Tool.get()); + AtToolExit(ToolName, Tool.get()); + + + // Check that the StringDestination emitted properly + { + std::string ExpectedBuff = "UUID:1111\n" + "MagicStartupMsg:One_TestToolOne\n" + "MSG_0:Two\n" + "MSG_1:Deux\n" + "MSG_2:Zwei\n" + "UUID:1111\n" + "MagicExitMsg:Three_TestToolOne\n"; + + EXPECT_STREQ(ExpectedBuff.c_str(), Buffer.c_str()); + } + + // Check that the JsonDestination emitted properly + { + + // There should be 3 events emitted by the Telemeter (start, midpoint, exit) + EXPECT_EQ(3, EmittedJsons.size()); + + const json::Value* StartupEntry = EmittedJsons[0].get("Startup"); + ASSERT_NE(StartupEntry, nullptr); + EXPECT_STREQ("[[\"UUID\",\"1111\"],[\"MagicStartupMsg\",\"One_TestToolOne\"]]", + ValueToString(StartupEntry).c_str()); + + const json::Value* MidpointEntry = EmittedJsons[1].get("Midpoint"); + ASSERT_NE(MidpointEntry, nullptr); + EXPECT_STREQ("{\"MSG_0\":\"Two\",\"MSG_1\":\"Deux\",\"MSG_2\":\"Zwei\"}", + ValueToString(MidpointEntry).c_str()); + + const json::Value* ExitEntry = EmittedJsons[2].get("Exit"); + ASSERT_NE(ExitEntry, nullptr); + EXPECT_STREQ("[[\"UUID\",\"1111\"],[\"MagicExitMsg\",\"Three_TestToolOne\"]]", + ValueToString(ExitEntry).c_str()); + } +} + +// Similar to previous tests, but toggling the data-sanitization option ON. +// The recorded data should have some fields removed. +TEST(TelemetryTest, TelemetryEnabledSanitizeData) { + const std::string ToolName = "TestToolOne"; + + // Preset some test params. + HasVendorConfig = true; + SanitizeData = true; + Buffer = ""; + EmittedJsons.clear(); + + std::shared_ptr Config = GetTelemetryConfig(); + + // Add some destinations + Config->AdditionalDestinations.push_back(vendor_code::STRING_DEST); + Config->AdditionalDestinations.push_back(vendor_code::JSON_DEST); + + auto Tool = vendor_code::TestTelemeter::createInstance(Config.get()); + + AtToolStart(ToolName, Tool.get()); + AtToolMidPoint(Tool.get()); + AtToolExit(ToolName, Tool.get()); + + + // Check that the StringDestination emitted properly + { + // The StringDestination should have removed the odd-positioned msgs. + std::string ExpectedBuff = "UUID:1111\n" + "MagicStartupMsg:One_TestToolOne\n" + "MSG_0:Two\n" + "MSG_1:\n" // was sannitized away. + "MSG_2:Zwei\n" + "UUID:1111\n" + "MagicExitMsg:Three_TestToolOne\n"; + + EXPECT_STREQ(ExpectedBuff.c_str(), Buffer.c_str()); + } + + // Check that the JsonDestination emitted properly + { + + // There should be 3 events emitted by the Telemeter (start, midpoint, exit) + EXPECT_EQ(3, EmittedJsons.size()); + + const json::Value* StartupEntry = EmittedJsons[0].get("Startup"); + ASSERT_NE(StartupEntry, nullptr); + EXPECT_STREQ("[[\"UUID\",\"1111\"],[\"MagicStartupMsg\",\"One_TestToolOne\"]]", + ValueToString(StartupEntry).c_str()); + + const json::Value* MidpointEntry = EmittedJsons[1].get("Midpoint"); + ASSERT_NE(MidpointEntry, nullptr); + // The JsonDestination should have removed the even-positioned msgs. + EXPECT_STREQ("{\"MSG_0\":\"\",\"MSG_1\":\"Deux\",\"MSG_2\":\"\"}", + ValueToString(MidpointEntry).c_str()); + + const json::Value* ExitEntry = EmittedJsons[2].get("Exit"); + ASSERT_NE(ExitEntry, nullptr); + EXPECT_STREQ("[[\"UUID\",\"1111\"],[\"MagicExitMsg\",\"Three_TestToolOne\"]]", + ValueToString(ExitEntry).c_str()); + } +} + +} // namespace From 750e4ac6af7bda76fb730b44b16b86279df42e15 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 28 Aug 2024 22:24:43 -0400 Subject: [PATCH 05/30] formatting changes --- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- llvm/lib/Telemetry/Telemetry.cpp | 4 +- llvm/unittests/Telemetry/TelemetryTest.cpp | 236 +++++++++++---------- 3 files changed, 125 insertions(+), 117 deletions(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 935b1feddbed6..7084b81c0304a 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -84,7 +84,7 @@ struct TelemetryInfo { // For isa, dyn_cast, etc, operations. virtual KindType getEntryKind() const { return EntryKind::Base; } - static bool classof(const TelemetryInfo* T) { + static bool classof(const TelemetryInfo *T) { return T->getEntryKind() == EntryKind::Base; } }; diff --git a/llvm/lib/Telemetry/Telemetry.cpp b/llvm/lib/Telemetry/Telemetry.cpp index b9605c4a38ecd..f49b88e07f136 100644 --- a/llvm/lib/Telemetry/Telemetry.cpp +++ b/llvm/lib/Telemetry/Telemetry.cpp @@ -4,8 +4,8 @@ namespace llvm { namespace telemetry { llvm::json::Object TelemetryInfo::serializeToJson() const { - return json::Object { - {"UUID", SessionUuid}, + return json::Object{ + {"UUID", SessionUuid}, }; }; diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 36a4daf55e101..b89bc584717da 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -20,12 +20,10 @@ #include "llvm/Support/raw_ostream.h" #include "gtest/gtest.h" -#include -#include #include #include +#include -#include // TODO: remove this // Testing parameters.ve static thread_local bool HasExitError = false; @@ -46,30 +44,30 @@ static std::string nextUuid() { } struct VendorEntryKind { - // TODO: should avoid dup with other vendors' Types? + // TODO: should avoid dup with other vendors' Types? static const KindType VendorCommon = 0b010101000; - static const KindType Startup = 0b010101001; - static const KindType Exit = 0b010101010; - }; - + static const KindType Startup = 0b010101001; + static const KindType Exit = 0b010101010; +}; // Demonstrates that the TelemetryInfo (data courier) struct can be extended // by downstream code to store additional data as needed. // It can also define additional data serialization method. struct VendorCommonTelemetryInfo : public TelemetryInfo { - static bool classof(const TelemetryInfo* T) { + static bool classof(const TelemetryInfo *T) { // Subclasses of this is also acceptable. - return (T->getEntryKind() & VendorEntryKind::VendorCommon) == VendorEntryKind::VendorCommon; - + return (T->getEntryKind() & VendorEntryKind::VendorCommon) == + VendorEntryKind::VendorCommon; } - KindType getEntryKind() const override { return VendorEntryKind::VendorCommon;} + KindType getEntryKind() const override { + return VendorEntryKind::VendorCommon; + } virtual void serializeToStream(llvm::raw_ostream &OS) const = 0; }; - struct StartupEvent : public VendorCommonTelemetryInfo { std::string MagicStartupMsg; @@ -83,21 +81,21 @@ struct StartupEvent : public VendorCommonTelemetryInfo { MagicStartupMsg = E.MagicStartupMsg; } - static bool classof(const TelemetryInfo* T) { + static bool classof(const TelemetryInfo *T) { return T->getEntryKind() == VendorEntryKind::Startup; } - KindType getEntryKind() const override { return VendorEntryKind::Startup;} + KindType getEntryKind() const override { return VendorEntryKind::Startup; } void serializeToStream(llvm::raw_ostream &OS) const override { - OS<< "UUID:" << SessionUuid << "\n"; + OS << "UUID:" << SessionUuid << "\n"; OS << "MagicStartupMsg:" << MagicStartupMsg << "\n"; } json::Object serializeToJson() const override { return json::Object{ - {"Startup", { {"UUID", SessionUuid}, - {"MagicStartupMsg", MagicStartupMsg}}}, + {"Startup", + {{"UUID", SessionUuid}, {"MagicStartupMsg", MagicStartupMsg}}}, }; } }; @@ -117,11 +115,11 @@ struct ExitEvent : public VendorCommonTelemetryInfo { MagicExitMsg = E.MagicExitMsg; } - static bool classof(const TelemetryInfo* T) { + static bool classof(const TelemetryInfo *T) { return T->getEntryKind() == VendorEntryKind::Exit; } - unsigned getEntryKind() const override { return VendorEntryKind::Exit;} + unsigned getEntryKind() const override { return VendorEntryKind::Exit; } void serializeToStream(llvm::raw_ostream &OS) const override { OS << "UUID:" << SessionUuid << "\n"; @@ -131,14 +129,14 @@ struct ExitEvent : public VendorCommonTelemetryInfo { } json::Object serializeToJson() const override { - json::Array I = json::Array{ + json::Array I = json::Array{ {"UUID", SessionUuid}, {"MagicExitMsg", MagicExitMsg}, }; if (ExitDesc.has_value()) I.push_back(json::Value({"ExitCode", ExitDesc->ExitCode})); - return json::Object { - {"Exit", std::move(I)}, + return json::Object{ + {"Exit", std::move(I)}, }; } }; @@ -164,20 +162,17 @@ struct CustomTelemetryEvent : public VendorCommonTelemetryInfo { } } - json::Object serializeToJson() const override { + json::Object serializeToJson() const override { json::Object Inner; int I = 0; for (const std::string &M : Msgs) { Inner.try_emplace(("MSG_" + llvm::Twine(I)).str(), M); ++I; } - return json::Object { - {"Midpoint", std::move(Inner)}} - ; + return json::Object{{"Midpoint", std::move(Inner)}}; } }; - // The following classes demonstrate how downstream code can // define one or more custom TelemetryDestination(s) to handle // Telemetry data differently, specifically: @@ -185,24 +180,24 @@ struct CustomTelemetryEvent : public VendorCommonTelemetryInfo { // + where to send the data // + in what form -const std::string STRING_DEST( "STRING"); -const std::string JSON_DEST ("JSON"); +const std::string STRING_DEST("STRING"); +const std::string JSON_DEST("JSON"); // This Destination sends data to a std::string given at ctor. class StringDestination : public TelemetryDestination { public: // ShouldSanitize: if true, sanitize the data before emitting, otherwise, emit // the full set. - StringDestination(bool ShouldSanitize, std::string& Buf) - : ShouldSanitize(ShouldSanitize), OS(Buf) { - } + StringDestination(bool ShouldSanitize, std::string &Buf) + : ShouldSanitize(ShouldSanitize), OS(Buf) {} Error emitEntry(const TelemetryInfo *Entry) override { if (isa(Entry)) { if (auto *E = dyn_cast(Entry)) { if (ShouldSanitize) { if (isa(E) || isa(E)) { - // There is nothing to sanitize for this type of data, so keep as-is. + // There is nothing to sanitize for this type of data, so keep + // as-is. E->serializeToStream(OS); } else if (isa(E)) { auto Sanitized = sanitizeFields(dyn_cast(E)); @@ -221,65 +216,63 @@ class StringDestination : public TelemetryDestination { return Error::success(); } - std::string name() const override { return STRING_DEST;} + std::string name() const override { return STRING_DEST; } private: // Returns a copy of the given entry, but with some fields sanitized. - CustomTelemetryEvent sanitizeFields(const CustomTelemetryEvent* Entry) { + CustomTelemetryEvent sanitizeFields(const CustomTelemetryEvent *Entry) { CustomTelemetryEvent Sanitized(*Entry); // Pretend that messages stored at ODD positions are "sensitive", // hence need to be sanitized away. int S = Sanitized.Msgs.size() - 1; for (int I = S % 2 == 0 ? S - 1 : S; I >= 0; I -= 2) - Sanitized.Msgs[I]=""; - return Sanitized; + Sanitized.Msgs[I] = ""; + return Sanitized; } bool ShouldSanitize; llvm::raw_string_ostream OS; - }; // This Destination sends data to some "blackbox" in form of JSON. class JsonStreamDestination : public TelemetryDestination { public: - JsonStreamDestination(bool ShouldSanitize) - : ShouldSanitize(ShouldSanitize) {} + JsonStreamDestination(bool ShouldSanitize) : ShouldSanitize(ShouldSanitize) {} Error emitEntry(const TelemetryInfo *Entry) override { if (auto *E = dyn_cast(Entry)) { - if (ShouldSanitize) { - if (isa(E) || isa(E)) { - // There is nothing to sanitize for this type of data, so keep as-is. - return SendToBlackbox(E->serializeToJson()); - } else if (isa(E)) { - auto Sanitized = sanitizeFields(dyn_cast(E)); - return SendToBlackbox(Sanitized.serializeToJson()); - } else { - llvm_unreachable("unexpected type"); - } - } else { + if (ShouldSanitize) { + if (isa(E) || isa(E)) { + // There is nothing to sanitize for this type of data, so keep as-is. return SendToBlackbox(E->serializeToJson()); + } else if (isa(E)) { + auto Sanitized = sanitizeFields(dyn_cast(E)); + return SendToBlackbox(Sanitized.serializeToJson()); + } else { + llvm_unreachable("unexpected type"); } - } else { + } else { + return SendToBlackbox(E->serializeToJson()); + } + } else { // Unfamiliar entries, just send the entry's UUID - return SendToBlackbox(json::Object{{"UUID", Entry->SessionUuid}}); + return SendToBlackbox(json::Object{{"UUID", Entry->SessionUuid}}); } return make_error("unhandled codepath in emitEntry", - inconvertibleErrorCode()); + inconvertibleErrorCode()); } - std::string name() const override { return JSON_DEST;} -private: + std::string name() const override { return JSON_DEST; } +private: // Returns a copy of the given entry, but with some fields sanitized. - CustomTelemetryEvent sanitizeFields(const CustomTelemetryEvent* Entry) { + CustomTelemetryEvent sanitizeFields(const CustomTelemetryEvent *Entry) { CustomTelemetryEvent Sanitized(*Entry); // Pretend that messages stored at EVEN positions are "sensitive", // hence need to be sanitized away. int S = Sanitized.Msgs.size() - 1; for (int I = S % 2 == 0 ? S : S - 1; I >= 0; I -= 2) - Sanitized.Msgs[I]=""; + Sanitized.Msgs[I] = ""; return Sanitized; } @@ -300,29 +293,35 @@ class TestTelemeter : public Telemeter { public: TestTelemeter(std::string SessionUuid) : Uuid(SessionUuid), Counter(0) {} - static std::unique_ptr createInstance(TelemetryConfig *config) { - if (!config->EnableTelemetry) return std::unique_ptr(nullptr); - std::unique_ptr Telemeter = std::make_unique(nextUuid()); - // Set up Destination based on the given config. - for (const std::string &Dest : config->AdditionalDestinations) { - // The destination(s) are ALSO defined by vendor, so it should understand - // what the name of each destination signifies. - if (Dest == JSON_DEST) { - Telemeter->addDestination(new vendor_code::JsonStreamDestination(SanitizeData)); - } else if (Dest == STRING_DEST) { - Telemeter->addDestination(new vendor_code::StringDestination(SanitizeData, Buffer)); - } else { - llvm_unreachable(llvm::Twine("unknown destination: ", Dest).str().c_str()); - } + static std::unique_ptr + createInstance(TelemetryConfig *config) { + if (!config->EnableTelemetry) + return std::unique_ptr(nullptr); + std::unique_ptr Telemeter = + std::make_unique(nextUuid()); + // Set up Destination based on the given config. + for (const std::string &Dest : config->AdditionalDestinations) { + // The destination(s) are ALSO defined by vendor, so it should understand + // what the name of each destination signifies. + if (Dest == JSON_DEST) { + Telemeter->addDestination( + new vendor_code::JsonStreamDestination(SanitizeData)); + } else if (Dest == STRING_DEST) { + Telemeter->addDestination( + new vendor_code::StringDestination(SanitizeData, Buffer)); + } else { + llvm_unreachable( + llvm::Twine("unknown destination: ", Dest).str().c_str()); } - return Telemeter; + } + return Telemeter; } void logStartup(llvm::StringRef ToolPath, TelemetryInfo *Entry) override { ToolName = ToolPath.str(); // The vendor can add additional stuff to the entry before logging. - if (auto* S = dyn_cast(Entry)) { + if (auto *S = dyn_cast(Entry)) { S->MagicStartupMsg = llvm::Twine("One_", ToolPath).str(); } emitToDestinations(Entry); @@ -330,7 +329,7 @@ class TestTelemeter : public Telemeter { void logExit(llvm::StringRef ToolPath, TelemetryInfo *Entry) override { // Ensure we're shutting down the same tool we started with. - if (ToolPath != ToolName){ + if (ToolPath != ToolName) { std::string Str; raw_string_ostream OS(Str); OS << "Expected tool with name" << ToolName << ", but got " << ToolPath; @@ -338,20 +337,20 @@ class TestTelemeter : public Telemeter { } // The vendor can add additional stuff to the entry before logging. - if (auto * E = dyn_cast(Entry)) { + if (auto *E = dyn_cast(Entry)) { E->MagicExitMsg = llvm::Twine("Three_", ToolPath).str(); } emitToDestinations(Entry); } - void addDestination(TelemetryDestination* Dest) override { + void addDestination(TelemetryDestination *Dest) override { Destinations.push_back(Dest); } void logMidpoint(TelemetryInfo *Entry) { // The custom Telemeter can record and send additional data. - if (auto * C = dyn_cast(Entry)) { + if (auto *C = dyn_cast(Entry)) { C->Msgs.push_back("Two"); C->Msgs.push_back("Deux"); C->Msgs.push_back("Zwei"); @@ -361,23 +360,22 @@ class TestTelemeter : public Telemeter { } ~TestTelemeter() { - for (auto* Dest : Destinations) + for (auto *Dest : Destinations) delete Dest; } - template - T makeDefaultTelemetryInfo() { + template T makeDefaultTelemetryInfo() { T Ret; Ret.SessionUuid = Uuid; Ret.Counter = Counter++; return Ret; } -private: +private: void emitToDestinations(TelemetryInfo *Entry) { for (TelemetryDestination *Dest : Destinations) { llvm::Error err = Dest->emitEntry(Entry); - if(err) { + if (err) { // Log it and move on. } } @@ -400,7 +398,7 @@ void ApplyVendorSpecificConfigs(TelemetryConfig *config) { namespace { -void ApplyCommonConfig(llvm::telemetry::TelemetryConfig* config) { +void ApplyCommonConfig(llvm::telemetry::TelemetryConfig *config) { // Any shareable configs for the upstream tool can go here. // ..... } @@ -435,23 +433,27 @@ using namespace llvm::telemetry; // // Preset StartTime to EPOCH. auto StartTime = std::chrono::time_point{}; -// Pretend the time it takes for the tool's initialization is EPOCH + 5 milliseconds +// Pretend the time it takes for the tool's initialization is EPOCH + 5 +// milliseconds auto InitCompleteTime = StartTime + std::chrono::milliseconds(5); auto MidPointTime = StartTime + std::chrono::milliseconds(10); auto MidPointCompleteTime = MidPointTime + std::chrono::milliseconds(5); // Preset ExitTime to EPOCH + 20 milliseconds auto ExitTime = StartTime + std::chrono::milliseconds(20); -// Pretend the time it takes to complete tearing down the tool is 10 milliseconds. +// Pretend the time it takes to complete tearing down the tool is 10 +// milliseconds. auto ExitCompleteTime = ExitTime + std::chrono::milliseconds(10); -void AtToolStart(std::string ToolName, vendor_code::TestTelemeter* T) { - vendor_code::StartupEvent Entry = T->makeDefaultTelemetryInfo(); +void AtToolStart(std::string ToolName, vendor_code::TestTelemeter *T) { + vendor_code::StartupEvent Entry = + T->makeDefaultTelemetryInfo(); Entry.Stats = {StartTime, InitCompleteTime}; T->logStartup(ToolName, &Entry); } -void AtToolExit(std::string ToolName, vendor_code::TestTelemeter* T) { - vendor_code::ExitEvent Entry = T->makeDefaultTelemetryInfo(); +void AtToolExit(std::string ToolName, vendor_code::TestTelemeter *T) { + vendor_code::ExitEvent Entry = + T->makeDefaultTelemetryInfo(); Entry.Stats = {ExitTime, ExitCompleteTime}; if (HasExitError) { @@ -460,14 +462,15 @@ void AtToolExit(std::string ToolName, vendor_code::TestTelemeter* T) { T->logExit(ToolName, &Entry); } -void AtToolMidPoint (vendor_code::TestTelemeter* T) { - vendor_code::CustomTelemetryEvent Entry = T->makeDefaultTelemetryInfo(); +void AtToolMidPoint(vendor_code::TestTelemeter *T) { + vendor_code::CustomTelemetryEvent Entry = + T->makeDefaultTelemetryInfo(); Entry.Stats = {MidPointTime, MidPointCompleteTime}; T->logMidpoint(&Entry); } // Helper function to print the given object content to string. -static std::string ValueToString(const json::Value* V) { +static std::string ValueToString(const json::Value *V) { std::string Ret; llvm::raw_string_ostream P(Ret); P << *V; @@ -477,7 +480,8 @@ static std::string ValueToString(const json::Value* V) { // Without vendor's implementation, telemetry is not enabled by default. TEST(TelemetryTest, TelemetryDefault) { HasVendorConfig = false; - std::shared_ptr Config = GetTelemetryConfig(); + std::shared_ptr Config = + GetTelemetryConfig(); auto Tool = vendor_code::TestTelemeter::createInstance(Config.get()); EXPECT_EQ(nullptr, Tool.get()); @@ -492,7 +496,8 @@ TEST(TelemetryTest, TelemetryEnabled) { Buffer = ""; EmittedJsons.clear(); - std::shared_ptr Config = GetTelemetryConfig(); + std::shared_ptr Config = + GetTelemetryConfig(); // Add some destinations Config->AdditionalDestinations.push_back(vendor_code::STRING_DEST); @@ -504,7 +509,6 @@ TEST(TelemetryTest, TelemetryEnabled) { AtToolMidPoint(Tool.get()); AtToolExit(ToolName, Tool.get()); - // Check that the StringDestination emitted properly { std::string ExpectedBuff = "UUID:1111\n" @@ -524,20 +528,22 @@ TEST(TelemetryTest, TelemetryEnabled) { // There should be 3 events emitted by the Telemeter (start, midpoint, exit) EXPECT_EQ(3, EmittedJsons.size()); - const json::Value* StartupEntry = EmittedJsons[0].get("Startup"); + const json::Value *StartupEntry = EmittedJsons[0].get("Startup"); ASSERT_NE(StartupEntry, nullptr); - EXPECT_STREQ("[[\"UUID\",\"1111\"],[\"MagicStartupMsg\",\"One_TestToolOne\"]]", - ValueToString(StartupEntry).c_str()); + EXPECT_STREQ( + "[[\"UUID\",\"1111\"],[\"MagicStartupMsg\",\"One_TestToolOne\"]]", + ValueToString(StartupEntry).c_str()); - const json::Value* MidpointEntry = EmittedJsons[1].get("Midpoint"); + const json::Value *MidpointEntry = EmittedJsons[1].get("Midpoint"); ASSERT_NE(MidpointEntry, nullptr); EXPECT_STREQ("{\"MSG_0\":\"Two\",\"MSG_1\":\"Deux\",\"MSG_2\":\"Zwei\"}", ValueToString(MidpointEntry).c_str()); - const json::Value* ExitEntry = EmittedJsons[2].get("Exit"); + const json::Value *ExitEntry = EmittedJsons[2].get("Exit"); ASSERT_NE(ExitEntry, nullptr); - EXPECT_STREQ("[[\"UUID\",\"1111\"],[\"MagicExitMsg\",\"Three_TestToolOne\"]]", - ValueToString(ExitEntry).c_str()); + EXPECT_STREQ( + "[[\"UUID\",\"1111\"],[\"MagicExitMsg\",\"Three_TestToolOne\"]]", + ValueToString(ExitEntry).c_str()); } } @@ -552,7 +558,8 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { Buffer = ""; EmittedJsons.clear(); - std::shared_ptr Config = GetTelemetryConfig(); + std::shared_ptr Config = + GetTelemetryConfig(); // Add some destinations Config->AdditionalDestinations.push_back(vendor_code::STRING_DEST); @@ -564,14 +571,13 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { AtToolMidPoint(Tool.get()); AtToolExit(ToolName, Tool.get()); - // Check that the StringDestination emitted properly { // The StringDestination should have removed the odd-positioned msgs. std::string ExpectedBuff = "UUID:1111\n" "MagicStartupMsg:One_TestToolOne\n" "MSG_0:Two\n" - "MSG_1:\n" // was sannitized away. + "MSG_1:\n" // was sannitized away. "MSG_2:Zwei\n" "UUID:1111\n" "MagicExitMsg:Three_TestToolOne\n"; @@ -585,21 +591,23 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { // There should be 3 events emitted by the Telemeter (start, midpoint, exit) EXPECT_EQ(3, EmittedJsons.size()); - const json::Value* StartupEntry = EmittedJsons[0].get("Startup"); + const json::Value *StartupEntry = EmittedJsons[0].get("Startup"); ASSERT_NE(StartupEntry, nullptr); - EXPECT_STREQ("[[\"UUID\",\"1111\"],[\"MagicStartupMsg\",\"One_TestToolOne\"]]", - ValueToString(StartupEntry).c_str()); + EXPECT_STREQ( + "[[\"UUID\",\"1111\"],[\"MagicStartupMsg\",\"One_TestToolOne\"]]", + ValueToString(StartupEntry).c_str()); - const json::Value* MidpointEntry = EmittedJsons[1].get("Midpoint"); + const json::Value *MidpointEntry = EmittedJsons[1].get("Midpoint"); ASSERT_NE(MidpointEntry, nullptr); // The JsonDestination should have removed the even-positioned msgs. EXPECT_STREQ("{\"MSG_0\":\"\",\"MSG_1\":\"Deux\",\"MSG_2\":\"\"}", ValueToString(MidpointEntry).c_str()); - const json::Value* ExitEntry = EmittedJsons[2].get("Exit"); + const json::Value *ExitEntry = EmittedJsons[2].get("Exit"); ASSERT_NE(ExitEntry, nullptr); - EXPECT_STREQ("[[\"UUID\",\"1111\"],[\"MagicExitMsg\",\"Three_TestToolOne\"]]", - ValueToString(ExitEntry).c_str()); + EXPECT_STREQ( + "[[\"UUID\",\"1111\"],[\"MagicExitMsg\",\"Three_TestToolOne\"]]", + ValueToString(ExitEntry).c_str()); } } From 1378ed4c0358b2d9ff51a3a15766e20ce3472687 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 29 Aug 2024 11:32:13 -0400 Subject: [PATCH 06/30] more formatting --- llvm/unittests/Telemetry/TelemetryTest.cpp | 88 +++++++++++++--------- 1 file changed, 54 insertions(+), 34 deletions(-) diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index b89bc584717da..cd3c61478c0b3 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -24,14 +24,17 @@ #include #include - -// Testing parameters.ve +// Testing parameters. +// These are set by each test to force certain outcomes. +// Since the tests may be run in parellel, these should probably +// be thread_local. static thread_local bool HasExitError = false; static thread_local std::string ExitMsg = ""; static thread_local bool HasVendorConfig = false; static thread_local bool SanitizeData = false; static thread_local std::string Buffer = ""; static thread_local std::vector EmittedJsons; +static thread_local std::string ExpectedUuid = ""; namespace llvm { namespace telemetry { @@ -39,8 +42,8 @@ namespace vendor_code { // Generate unique (but deterministic "uuid" for testing purposes). static std::string nextUuid() { - static size_t seed = 1111; - return std::to_string(seed++); + static std::atomic seed = 1111; + return std::to_string(seed.fetch_add(1, std::memory_order_acquire)); } struct VendorEntryKind { @@ -54,7 +57,6 @@ struct VendorEntryKind { // by downstream code to store additional data as needed. // It can also define additional data serialization method. struct VendorCommonTelemetryInfo : public TelemetryInfo { - static bool classof(const TelemetryInfo *T) { // Subclasses of this is also acceptable. return (T->getEntryKind() & VendorEntryKind::VendorCommon) == @@ -155,6 +157,7 @@ struct CustomTelemetryEvent : public VendorCommonTelemetryInfo { } void serializeToStream(llvm::raw_ostream &OS) const override { + OS << "UUID:" << SessionUuid << "\n"; int I = 0; for (const std::string &M : Msgs) { OS << "MSG_" << I << ":" << M << "\n"; @@ -164,11 +167,13 @@ struct CustomTelemetryEvent : public VendorCommonTelemetryInfo { json::Object serializeToJson() const override { json::Object Inner; + Inner.try_emplace ("UUID", SessionUuid); int I = 0; for (const std::string &M : Msgs) { Inner.try_emplace(("MSG_" + llvm::Twine(I)).str(), M); ++I; } + return json::Object{{"Midpoint", std::move(Inner)}}; } }; @@ -295,10 +300,12 @@ class TestTelemeter : public Telemeter { static std::unique_ptr createInstance(TelemetryConfig *config) { + llvm::errs() << "============================== createInstance is called" << "\n"; if (!config->EnableTelemetry) - return std::unique_ptr(nullptr); + return nullptr; + ExpectedUuid = nextUuid(); std::unique_ptr Telemeter = - std::make_unique(nextUuid()); + std::make_unique(ExpectedUuid); // Set up Destination based on the given config. for (const std::string &Dest : config->AdditionalDestinations) { // The destination(s) are ALSO defined by vendor, so it should understand @@ -359,6 +366,8 @@ class TestTelemeter : public Telemeter { emitToDestinations(Entry); } + const std::string& getUuid() const {return Uuid;} + ~TestTelemeter() { for (auto *Dest : Destinations) delete Dest; @@ -412,12 +421,13 @@ std::shared_ptr GetTelemetryConfig() { ApplyCommonConfig(Config.get()); // Apply vendor specific config, if present. - // In practice, this would be a build-time param. + // In principle, this would be a build-time param, configured by the vendor. // Eg: // // #ifdef HAS_VENDOR_TELEMETRY_CONFIG // llvm::telemetry::vendor_code::ApplyVendorSpecificConfigs(config.get()); // #endif + // // But for unit testing, we use the testing params defined at the top. if (HasVendorConfig) { llvm::telemetry::vendor_code::ApplyVendorSpecificConfigs(Config.get()); @@ -488,12 +498,12 @@ TEST(TelemetryTest, TelemetryDefault) { } TEST(TelemetryTest, TelemetryEnabled) { - const std::string ToolName = "TestToolOne"; + const std::string ToolName = "TelemetryTest"; // Preset some test params. HasVendorConfig = true; SanitizeData = false; - Buffer = ""; + Buffer.clear(); EmittedJsons.clear(); std::shared_ptr Config = @@ -509,17 +519,21 @@ TEST(TelemetryTest, TelemetryEnabled) { AtToolMidPoint(Tool.get()); AtToolExit(ToolName, Tool.get()); + // Check that the Tool uses the expected UUID. + EXPECT_STREQ(Tool->getUuid().c_str(), ExpectedUuid.c_str()); + // Check that the StringDestination emitted properly { - std::string ExpectedBuff = "UUID:1111\n" - "MagicStartupMsg:One_TestToolOne\n" - "MSG_0:Two\n" - "MSG_1:Deux\n" - "MSG_2:Zwei\n" - "UUID:1111\n" - "MagicExitMsg:Three_TestToolOne\n"; + std::string ExpectedBuffer = ("UUID:" + llvm::Twine(ExpectedUuid) + "\n" + + "MagicStartupMsg:One_" + llvm::Twine(ToolName) + "\n" + + "UUID:" + llvm::Twine(ExpectedUuid) + "\n" + + "MSG_0:Two\n" + + "MSG_1:Deux\n" + + "MSG_2:Zwei\n" + + "UUID:" + llvm::Twine(ExpectedUuid) + "\n" + + "MagicExitMsg:Three_" + llvm::Twine(ToolName) + "\n").str(); - EXPECT_STREQ(ExpectedBuff.c_str(), Buffer.c_str()); + EXPECT_STREQ(ExpectedBuffer.c_str(), Buffer.c_str()); } // Check that the JsonDestination emitted properly @@ -531,18 +545,21 @@ TEST(TelemetryTest, TelemetryEnabled) { const json::Value *StartupEntry = EmittedJsons[0].get("Startup"); ASSERT_NE(StartupEntry, nullptr); EXPECT_STREQ( - "[[\"UUID\",\"1111\"],[\"MagicStartupMsg\",\"One_TestToolOne\"]]", + ("[[\"UUID\",\"" + llvm::Twine(ExpectedUuid) + "\"],[\"MagicStartupMsg\",\"One_" + llvm::Twine(ToolName)+"\"]]").str().c_str(), ValueToString(StartupEntry).c_str()); const json::Value *MidpointEntry = EmittedJsons[1].get("Midpoint"); ASSERT_NE(MidpointEntry, nullptr); - EXPECT_STREQ("{\"MSG_0\":\"Two\",\"MSG_1\":\"Deux\",\"MSG_2\":\"Zwei\"}", + // TODO: This is a bit flaky in that the json string printer sort the entries (for now), + // so the "UUID" field is put at the end of the array even though it was emitted first. + EXPECT_STREQ(("{\"MSG_0\":\"Two\",\"MSG_1\":\"Deux\",\"MSG_2\":\"Zwei\",\"UUID\":\"" + + llvm::Twine(ExpectedUuid) + "\"}").str().c_str(), ValueToString(MidpointEntry).c_str()); const json::Value *ExitEntry = EmittedJsons[2].get("Exit"); ASSERT_NE(ExitEntry, nullptr); EXPECT_STREQ( - "[[\"UUID\",\"1111\"],[\"MagicExitMsg\",\"Three_TestToolOne\"]]", + ("[[\"UUID\",\"" + llvm::Twine(ExpectedUuid) + "\"],[\"MagicExitMsg\",\"Three_" + llvm::Twine(ToolName)+"\"]]").str().c_str(), ValueToString(ExitEntry).c_str()); } } @@ -550,12 +567,12 @@ TEST(TelemetryTest, TelemetryEnabled) { // Similar to previous tests, but toggling the data-sanitization option ON. // The recorded data should have some fields removed. TEST(TelemetryTest, TelemetryEnabledSanitizeData) { - const std::string ToolName = "TestToolOne"; + const std::string ToolName = "TelemetryTest_SanitizedData"; // Preset some test params. HasVendorConfig = true; SanitizeData = true; - Buffer = ""; + Buffer.clear(); EmittedJsons.clear(); std::shared_ptr Config = @@ -574,15 +591,16 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { // Check that the StringDestination emitted properly { // The StringDestination should have removed the odd-positioned msgs. - std::string ExpectedBuff = "UUID:1111\n" - "MagicStartupMsg:One_TestToolOne\n" - "MSG_0:Two\n" - "MSG_1:\n" // was sannitized away. - "MSG_2:Zwei\n" - "UUID:1111\n" - "MagicExitMsg:Three_TestToolOne\n"; - EXPECT_STREQ(ExpectedBuff.c_str(), Buffer.c_str()); + std::string ExpectedBuffer = ("UUID:" + llvm::Twine(ExpectedUuid) + "\n" + + "MagicStartupMsg:One_" + llvm::Twine(ToolName) + "\n" + + "UUID:" + llvm::Twine(ExpectedUuid) + "\n" + + "MSG_0:Two\n" + + "MSG_1:\n" + // <<< was sanitized away. + "MSG_2:Zwei\n" + + "UUID:" + llvm::Twine(ExpectedUuid) + "\n" + + "MagicExitMsg:Three_" + llvm::Twine(ToolName) + "\n").str(); + EXPECT_STREQ(ExpectedBuffer.c_str(), Buffer.c_str()); } // Check that the JsonDestination emitted properly @@ -594,19 +612,21 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { const json::Value *StartupEntry = EmittedJsons[0].get("Startup"); ASSERT_NE(StartupEntry, nullptr); EXPECT_STREQ( - "[[\"UUID\",\"1111\"],[\"MagicStartupMsg\",\"One_TestToolOne\"]]", + ("[[\"UUID\",\"" + llvm::Twine(ExpectedUuid) + "\"],[\"MagicStartupMsg\",\"One_" + llvm::Twine(ToolName)+"\"]]").str().c_str(), ValueToString(StartupEntry).c_str()); const json::Value *MidpointEntry = EmittedJsons[1].get("Midpoint"); ASSERT_NE(MidpointEntry, nullptr); // The JsonDestination should have removed the even-positioned msgs. - EXPECT_STREQ("{\"MSG_0\":\"\",\"MSG_1\":\"Deux\",\"MSG_2\":\"\"}", + EXPECT_STREQ(("{\"MSG_0\":\"\",\"MSG_1\":\"Deux\",\"MSG_2\":\"\",\"UUID\":\"" + + llvm::Twine(ExpectedUuid) + "\"}").str().c_str(), ValueToString(MidpointEntry).c_str()); + const json::Value *ExitEntry = EmittedJsons[2].get("Exit"); ASSERT_NE(ExitEntry, nullptr); EXPECT_STREQ( - "[[\"UUID\",\"1111\"],[\"MagicExitMsg\",\"Three_TestToolOne\"]]", + ("[[\"UUID\",\"" + llvm::Twine(ExpectedUuid) + "\"],[\"MagicExitMsg\",\"Three_" + llvm::Twine(ToolName)+"\"]]").str().c_str(), ValueToString(ExitEntry).c_str()); } } From 02e750ec91015fba0acaee1b3527f790417c0cb8 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 29 Aug 2024 12:47:46 -0400 Subject: [PATCH 07/30] more formatting changes --- llvm/unittests/Telemetry/TelemetryTest.cpp | 95 +++++++++++++--------- 1 file changed, 56 insertions(+), 39 deletions(-) diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index cd3c61478c0b3..fde7e31802f68 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -167,7 +167,7 @@ struct CustomTelemetryEvent : public VendorCommonTelemetryInfo { json::Object serializeToJson() const override { json::Object Inner; - Inner.try_emplace ("UUID", SessionUuid); + Inner.try_emplace("UUID", SessionUuid); int I = 0; for (const std::string &M : Msgs) { Inner.try_emplace(("MSG_" + llvm::Twine(I)).str(), M); @@ -300,7 +300,8 @@ class TestTelemeter : public Telemeter { static std::unique_ptr createInstance(TelemetryConfig *config) { - llvm::errs() << "============================== createInstance is called" << "\n"; + llvm::errs() << "============================== createInstance is called" + << "\n"; if (!config->EnableTelemetry) return nullptr; ExpectedUuid = nextUuid(); @@ -366,7 +367,7 @@ class TestTelemeter : public Telemeter { emitToDestinations(Entry); } - const std::string& getUuid() const {return Uuid;} + const std::string &getUuid() const { return Uuid; } ~TestTelemeter() { for (auto *Dest : Destinations) @@ -524,14 +525,13 @@ TEST(TelemetryTest, TelemetryEnabled) { // Check that the StringDestination emitted properly { - std::string ExpectedBuffer = ("UUID:" + llvm::Twine(ExpectedUuid) + "\n" + - "MagicStartupMsg:One_" + llvm::Twine(ToolName) + "\n" + - "UUID:" + llvm::Twine(ExpectedUuid) + "\n" + - "MSG_0:Two\n" + - "MSG_1:Deux\n" + - "MSG_2:Zwei\n" + - "UUID:" + llvm::Twine(ExpectedUuid) + "\n" + - "MagicExitMsg:Three_" + llvm::Twine(ToolName) + "\n").str(); + std::string ExpectedBuffer = + ("UUID:" + llvm::Twine(ExpectedUuid) + "\n" + "MagicStartupMsg:One_" + + llvm::Twine(ToolName) + "\n" + "UUID:" + llvm::Twine(ExpectedUuid) + + "\n" + "MSG_0:Two\n" + "MSG_1:Deux\n" + "MSG_2:Zwei\n" + + "UUID:" + llvm::Twine(ExpectedUuid) + "\n" + "MagicExitMsg:Three_" + + llvm::Twine(ToolName) + "\n") + .str(); EXPECT_STREQ(ExpectedBuffer.c_str(), Buffer.c_str()); } @@ -544,23 +544,33 @@ TEST(TelemetryTest, TelemetryEnabled) { const json::Value *StartupEntry = EmittedJsons[0].get("Startup"); ASSERT_NE(StartupEntry, nullptr); - EXPECT_STREQ( - ("[[\"UUID\",\"" + llvm::Twine(ExpectedUuid) + "\"],[\"MagicStartupMsg\",\"One_" + llvm::Twine(ToolName)+"\"]]").str().c_str(), - ValueToString(StartupEntry).c_str()); + EXPECT_STREQ(("[[\"UUID\",\"" + llvm::Twine(ExpectedUuid) + + "\"],[\"MagicStartupMsg\",\"One_" + llvm::Twine(ToolName) + + "\"]]") + .str() + .c_str(), + ValueToString(StartupEntry).c_str()); const json::Value *MidpointEntry = EmittedJsons[1].get("Midpoint"); ASSERT_NE(MidpointEntry, nullptr); - // TODO: This is a bit flaky in that the json string printer sort the entries (for now), - // so the "UUID" field is put at the end of the array even though it was emitted first. - EXPECT_STREQ(("{\"MSG_0\":\"Two\",\"MSG_1\":\"Deux\",\"MSG_2\":\"Zwei\",\"UUID\":\"" - + llvm::Twine(ExpectedUuid) + "\"}").str().c_str(), + // TODO: This is a bit flaky in that the json string printer sort the + // entries (for now), so the "UUID" field is put at the end of the array + // even though it was emitted first. + EXPECT_STREQ(("{\"MSG_0\":\"Two\",\"MSG_1\":\"Deux\",\"MSG_2\":\"Zwei\"," + "\"UUID\":\"" + + llvm::Twine(ExpectedUuid) + "\"}") + .str() + .c_str(), ValueToString(MidpointEntry).c_str()); const json::Value *ExitEntry = EmittedJsons[2].get("Exit"); ASSERT_NE(ExitEntry, nullptr); - EXPECT_STREQ( - ("[[\"UUID\",\"" + llvm::Twine(ExpectedUuid) + "\"],[\"MagicExitMsg\",\"Three_" + llvm::Twine(ToolName)+"\"]]").str().c_str(), - ValueToString(ExitEntry).c_str()); + EXPECT_STREQ(("[[\"UUID\",\"" + llvm::Twine(ExpectedUuid) + + "\"],[\"MagicExitMsg\",\"Three_" + llvm::Twine(ToolName) + + "\"]]") + .str() + .c_str(), + ValueToString(ExitEntry).c_str()); } } @@ -592,14 +602,13 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { { // The StringDestination should have removed the odd-positioned msgs. - std::string ExpectedBuffer = ("UUID:" + llvm::Twine(ExpectedUuid) + "\n" + - "MagicStartupMsg:One_" + llvm::Twine(ToolName) + "\n" + - "UUID:" + llvm::Twine(ExpectedUuid) + "\n" + - "MSG_0:Two\n" + - "MSG_1:\n" + // <<< was sanitized away. - "MSG_2:Zwei\n" + - "UUID:" + llvm::Twine(ExpectedUuid) + "\n" + - "MagicExitMsg:Three_" + llvm::Twine(ToolName) + "\n").str(); + std::string ExpectedBuffer = + ("UUID:" + llvm::Twine(ExpectedUuid) + "\n" + "MagicStartupMsg:One_" + + llvm::Twine(ToolName) + "\n" + "UUID:" + llvm::Twine(ExpectedUuid) + + "\n" + "MSG_0:Two\n" + "MSG_1:\n" + // <<< was sanitized away. + "MSG_2:Zwei\n" + "UUID:" + llvm::Twine(ExpectedUuid) + "\n" + + "MagicExitMsg:Three_" + llvm::Twine(ToolName) + "\n") + .str(); EXPECT_STREQ(ExpectedBuffer.c_str(), Buffer.c_str()); } @@ -611,23 +620,31 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { const json::Value *StartupEntry = EmittedJsons[0].get("Startup"); ASSERT_NE(StartupEntry, nullptr); - EXPECT_STREQ( - ("[[\"UUID\",\"" + llvm::Twine(ExpectedUuid) + "\"],[\"MagicStartupMsg\",\"One_" + llvm::Twine(ToolName)+"\"]]").str().c_str(), - ValueToString(StartupEntry).c_str()); + EXPECT_STREQ(("[[\"UUID\",\"" + llvm::Twine(ExpectedUuid) + + "\"],[\"MagicStartupMsg\",\"One_" + llvm::Twine(ToolName) + + "\"]]") + .str() + .c_str(), + ValueToString(StartupEntry).c_str()); const json::Value *MidpointEntry = EmittedJsons[1].get("Midpoint"); ASSERT_NE(MidpointEntry, nullptr); // The JsonDestination should have removed the even-positioned msgs. - EXPECT_STREQ(("{\"MSG_0\":\"\",\"MSG_1\":\"Deux\",\"MSG_2\":\"\",\"UUID\":\"" - + llvm::Twine(ExpectedUuid) + "\"}").str().c_str(), - ValueToString(MidpointEntry).c_str()); - + EXPECT_STREQ( + ("{\"MSG_0\":\"\",\"MSG_1\":\"Deux\",\"MSG_2\":\"\",\"UUID\":\"" + + llvm::Twine(ExpectedUuid) + "\"}") + .str() + .c_str(), + ValueToString(MidpointEntry).c_str()); const json::Value *ExitEntry = EmittedJsons[2].get("Exit"); ASSERT_NE(ExitEntry, nullptr); - EXPECT_STREQ( - ("[[\"UUID\",\"" + llvm::Twine(ExpectedUuid) + "\"],[\"MagicExitMsg\",\"Three_" + llvm::Twine(ToolName)+"\"]]").str().c_str(), - ValueToString(ExitEntry).c_str()); + EXPECT_STREQ(("[[\"UUID\",\"" + llvm::Twine(ExpectedUuid) + + "\"],[\"MagicExitMsg\",\"Three_" + llvm::Twine(ToolName) + + "\"]]") + .str() + .c_str(), + ValueToString(ExitEntry).c_str()); } } From 63e99fc09412fa6b8f23f9820eb1810dc7012f3d Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 29 Aug 2024 15:00:29 -0400 Subject: [PATCH 08/30] Added header comment to describe the package --- llvm/include/llvm/Telemetry/Telemetry.h | 66 ++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 7084b81c0304a..d4f192ab9159c 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -5,6 +5,50 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// +/// +/// \file +/// This file provides the basic framework for Telemetry +/// +/// It comprises of three important structs/classes: +/// +/// - Telemeter: The class responsible for collecting and forwarding +/// telemery data. +/// - TelemetryInfo: data courier +/// - TelemetryConfig: this stores configurations on Telemeter. +/// +/// This framework is intended to be configurable and extensible: +/// - Any LLVM tool that wants to use Telemetry can extend/customize it. +/// - Toolchain vendors can also provide custom implementation/config of the +/// Telemetry library, which could either overrides or extends the given tool's +/// upstream implementation, to best fit their organization's usage and +/// security models. +/// - End users of such tool can also configure telemetry (as allowed +/// by their vendor). +/// +/// Note: There are two important points to highlight about this package: +/// +/// (0) There is (currently) no concrete implementation of Telemetry in +/// upstream LLVM. We only provide the abstract API here. Any tool +/// that wants telemetry will have to implement one. +/// +/// The reason for this is because all the tools in llvm are +/// very different in what they care about (what/when/where to instrument) +/// Hence it might not be practical to a single implementation. +/// However, if in the future when we see any common pattern, we can +/// extract them into a shared place. That is TBD - contributions welcomed. +/// +/// (1) No implementation of Telemetry in upstream LLVM shall directly store +/// any of the collected data due to privacy and security reasons: +/// + Different organizations have different opinions on which data +/// is sensitive and which is not. +/// + Data ownerships and data collection consents are hare to +/// accommodate from LLVM developers' point of view. +/// (Eg., the data collected by Telemetry framework is NOT neccessarily +/// owned by the user of a LLVM tool with Telemetry enabled, hence +/// their consent to data collection isn't meaningful. On the other +/// hand, we have no practical way to request consent from "real" owners. +//===---------------------------------------------------------------------===// + #ifndef LLVM_TELEMETRY_TELEMETRY_H #define LLVM_TELEMETRY_TELEMETRY_H @@ -23,23 +67,26 @@ namespace llvm { namespace telemetry { -using SteadyTimePoint = std::chrono::time_point; struct TelemetryConfig { // If true, telemetry will be enabled. bool EnableTelemetry; - // Additional destinations to send the logged entries. - // Could be stdout, stderr, or some local paths. - // Note: these are destinations are __in addition to__ whatever the default - // destination(s) are, as implemented by vendors. + // Implementation-defined names of additional destinations to send + // telemetry data (Could be stdout, stderr, or some local paths, etc). + // + // These strings will be interpreted by the vendor's code. + // So the users must pick the from their vendor's pre-defined + // set of Destinations. std::vector AdditionalDestinations; }; +using SteadyTimePoint = std::chrono::time_point; + struct TelemetryEventStats { - // REQUIRED: Start time of event + // REQUIRED: Start time of an event SteadyTimePoint Start; - // OPTIONAL: End time of event - may be empty if not meaningful. + // OPTIONAL: End time of an event - may be empty if not meaningful. std::optional End; // TBD: could add some memory stats here too? @@ -60,7 +107,10 @@ struct EntryKind { static const KindType Base = 0; }; -// The base class contains the basic set of data. +// TelemetryInfo is the data courier, used to forward data from +// the tool being monitored and the Telemery framework. +// +// This base class contains only the basic set of telemetry data. // Downstream implementations can add more fields as needed. struct TelemetryInfo { // A "session" corresponds to every time the tool starts. From fa885126c6d7e5b0caf6e2f2411dcd034081e187 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 29 Aug 2024 15:08:18 -0400 Subject: [PATCH 09/30] reformat header --- llvm/include/llvm/Telemetry/Telemetry.h | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index d4f192ab9159c..cbc374df63212 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -19,9 +19,9 @@ /// This framework is intended to be configurable and extensible: /// - Any LLVM tool that wants to use Telemetry can extend/customize it. /// - Toolchain vendors can also provide custom implementation/config of the -/// Telemetry library, which could either overrides or extends the given tool's -/// upstream implementation, to best fit their organization's usage and -/// security models. +/// Telemetry library, which could either overrides or extends the given +/// tool's upstream implementation, to best fit their organization's usage +/// and security models. /// - End users of such tool can also configure telemetry (as allowed /// by their vendor). /// @@ -41,15 +41,15 @@ /// any of the collected data due to privacy and security reasons: /// + Different organizations have different opinions on which data /// is sensitive and which is not. -/// + Data ownerships and data collection consents are hare to +/// + Data ownerships and data collection consents are hard to /// accommodate from LLVM developers' point of view. /// (Eg., the data collected by Telemetry framework is NOT neccessarily /// owned by the user of a LLVM tool with Telemetry enabled, hence /// their consent to data collection isn't meaningful. On the other -/// hand, we have no practical way to request consent from "real" owners. +/// hand, we have no practical way to request consent from "real" +/// owners. //===---------------------------------------------------------------------===// - #ifndef LLVM_TELEMETRY_TELEMETRY_H #define LLVM_TELEMETRY_TELEMETRY_H @@ -67,7 +67,6 @@ namespace llvm { namespace telemetry { - struct TelemetryConfig { // If true, telemetry will be enabled. bool EnableTelemetry; From 0866f64dc2863fad28d2027a08e01ab414c6b46a Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 3 Sep 2024 13:52:20 -0400 Subject: [PATCH 10/30] addressed review comments and added separate docs in llvm/docs/ --- llvm/docs/Telemetry.rst | 70 ++++++++++++++++++++++ llvm/docs/UserGuides.rst | 3 + llvm/include/llvm/Telemetry/Telemetry.h | 77 +++++++++++-------------- 3 files changed, 106 insertions(+), 44 deletions(-) create mode 100644 llvm/docs/Telemetry.rst diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst new file mode 100644 index 0000000000000..997436e78735d --- /dev/null +++ b/llvm/docs/Telemetry.rst @@ -0,0 +1,70 @@ +=========================== +Telemetry framework in LLVM +=========================== + +.. contents:: + :local: + +.. toctree:: + :hidden: + +Objective +========= + +Provides a common framework in LLVM for collecting various usage and performance +metrics. +It is located at `llvm/Telemetry/Telemetry.h` + +Characteristics +--------------- +* Configurable and extensible by: + * Tools: any tool that wants to use Telemetry can extend and customize it. + * Vendors: Toolchain vendors can also provide custom implementation of the + library, which could either override or extend the given tool's upstream + implementation, to best fit their organization's usage and privacy models. + * End users of such tool can also configure Telemetry(as allowed by their + vendor). + + +Important notes +---------------- + +* There is no concrete implementation of a Telemetry library in upstream LLVM. + We only provide the abstract API here. Any tool that wants telemetry will + implement one. + The rationale for this is that, all the tools in llvm are very different in + what they care about(what/where/when to instrument data). Hence, it might not + be practical to have a single implementation. + However, in the future, if we see enough common pattern, we can extract them + into a shared place. This is TBD - contributions are welcomed. + +* No implementation of Telemetry in upstream LLVM shall store any of the + collected data due to privacy and security reasons: + * Different organizations have different privacy models: + * Which data is sensitive, which is not? + * Whether it is acceptable for instrumented data to be stored anywhere? + (to a local file, what not?) + * Data ownership and data collection consents are hard to accommodate from + LLVM developers' point of view: + * Eg., data collected by Telemetry is not neccessarily owned by the user + of an LLVM tool with Telemetry enabled, hence the user's consent to data + collection is not meaningful. On the other hand, LLVM developers have no + practical way to request consent from the "real" owners. + + +High-level design +================= + +Key components +-------------- + +The framework is consisted of three important classes: +* `llvm::telemetry::Telemeter`: The class responsible for collecting and + forwarding telemetry data. This is the main point of interaction between + the framework and any tool that wants to enable telemery. +* `llvm::telemetry::TelemetryInfo`: Data courier +* `llvm::telemetry::Config`: Configurations on the `Telemeter`. + + +// + diff --git a/llvm/docs/UserGuides.rst b/llvm/docs/UserGuides.rst index 86101ffbd9ca5..171da2053e731 100644 --- a/llvm/docs/UserGuides.rst +++ b/llvm/docs/UserGuides.rst @@ -293,3 +293,6 @@ Additional Topics :doc:`Sandbox IR ` This document describes the design and usage of Sandbox IR, a transactional layer over LLVM IR. + +:doc:`Telemetry` + This document describes the Telemetry framework in LLVM. diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index cbc374df63212..55a915a4fb894 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -16,38 +16,7 @@ /// - TelemetryInfo: data courier /// - TelemetryConfig: this stores configurations on Telemeter. /// -/// This framework is intended to be configurable and extensible: -/// - Any LLVM tool that wants to use Telemetry can extend/customize it. -/// - Toolchain vendors can also provide custom implementation/config of the -/// Telemetry library, which could either overrides or extends the given -/// tool's upstream implementation, to best fit their organization's usage -/// and security models. -/// - End users of such tool can also configure telemetry (as allowed -/// by their vendor). -/// -/// Note: There are two important points to highlight about this package: -/// -/// (0) There is (currently) no concrete implementation of Telemetry in -/// upstream LLVM. We only provide the abstract API here. Any tool -/// that wants telemetry will have to implement one. -/// -/// The reason for this is because all the tools in llvm are -/// very different in what they care about (what/when/where to instrument) -/// Hence it might not be practical to a single implementation. -/// However, if in the future when we see any common pattern, we can -/// extract them into a shared place. That is TBD - contributions welcomed. -/// -/// (1) No implementation of Telemetry in upstream LLVM shall directly store -/// any of the collected data due to privacy and security reasons: -/// + Different organizations have different opinions on which data -/// is sensitive and which is not. -/// + Data ownerships and data collection consents are hard to -/// accommodate from LLVM developers' point of view. -/// (Eg., the data collected by Telemetry framework is NOT neccessarily -/// owned by the user of a LLVM tool with Telemetry enabled, hence -/// their consent to data collection isn't meaningful. On the other -/// hand, we have no practical way to request consent from "real" -/// owners. +/// Refer to its documentation at llvm/docs/Telemetry.rst for more details. //===---------------------------------------------------------------------===// #ifndef LLVM_TELEMETRY_TELEMETRY_H @@ -67,7 +36,9 @@ namespace llvm { namespace telemetry { -struct TelemetryConfig { +// Configuration for the Telemeter class. +// This struct can be extended as needed. +struct Config { // If true, telemetry will be enabled. bool EnableTelemetry; @@ -80,9 +51,12 @@ struct TelemetryConfig { std::vector AdditionalDestinations; }; +// Defines a convenient type for timestamp of various events. +// This is used by the EventStats below. using SteadyTimePoint = std::chrono::time_point; -struct TelemetryEventStats { +// Various time (and possibly memory) statistics of an event. +struct EventStats { // REQUIRED: Start time of an event SteadyTimePoint Start; // OPTIONAL: End time of an event - may be empty if not meaningful. @@ -107,22 +81,28 @@ struct EntryKind { }; // TelemetryInfo is the data courier, used to forward data from -// the tool being monitored and the Telemery framework. +// the tool being monitored to the Telemery framework. // // This base class contains only the basic set of telemetry data. // Downstream implementations can add more fields as needed. struct TelemetryInfo { - // A "session" corresponds to every time the tool starts. - // All entries emitted for the same session will have - // the same session_uuid - std::string SessionUuid; + // This represents a unique-id, conventionally corresponding to + // a tools' session - ie., every time the tool starts until it exits. + // + // Note: a tool could have mutliple sessions running at once, in which + // case, these shall be multiple sets of TelemetryInfo with multiple unique + // ids. + // + // Different usages can assign different types of IDs to this field. + std::string SessionId; + // Time/memory statistics of this event. TelemetryEventStats Stats; std::optional ExitDesc; // Counting number of entries. - // (For each set of entries with the same session_uuid, this value should + // (For each set of entries with the same SessionId, this value should // be unique for each entry) size_t Counter; @@ -132,20 +112,29 @@ struct TelemetryInfo { virtual json::Object serializeToJson() const; // For isa, dyn_cast, etc, operations. - virtual KindType getEntryKind() const { return EntryKind::Base; } + virtual KindType getKind() const { return EntryKind::Base; } static bool classof(const TelemetryInfo *T) { - return T->getEntryKind() == EntryKind::Base; + return T->getKind() == EntryKind::Base; } }; -// Where/how to send the telemetry entries. -class TelemetryDestination { +// This class presents a data sink to which the Telemetry framework +// sends data. +// +// Its implementation is transparent to the framework. +// It is up to the vendor to decide which pieces of data to forward +// and where to forward them. +class Destination { public: virtual ~TelemetryDestination() = default; virtual Error emitEntry(const TelemetryInfo *Entry) = 0; virtual std::string name() const = 0; }; +// This class is the main interaction point between any LLVM tool +// and this framework. +// It is responsible for collecting telemetry data from the tool being +// monitored. class Telemeter { public: // Invoked upon tool startup From a8523f731c8bb1f6384407e9c3eca2bdfb949da2 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 3 Sep 2024 21:00:50 -0400 Subject: [PATCH 11/30] updated field names in tests --- llvm/include/llvm/Telemetry/Telemetry.h | 17 ++-- llvm/lib/Telemetry/Telemetry.cpp | 2 +- llvm/unittests/Telemetry/TelemetryTest.cpp | 103 +++++++++++---------- 3 files changed, 66 insertions(+), 56 deletions(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 55a915a4fb894..8f26f6ba9ec26 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -63,9 +63,9 @@ struct EventStats { std::optional End; // TBD: could add some memory stats here too? - TelemetryEventStats() = default; - TelemetryEventStats(SteadyTimePoint Start) : Start(Start) {} - TelemetryEventStats(SteadyTimePoint Start, SteadyTimePoint End) + EventStats() = default; + EventStats(SteadyTimePoint Start) : Start(Start) {} + EventStats(SteadyTimePoint Start, SteadyTimePoint End) : Start(Start), End(End) {} }; @@ -76,6 +76,9 @@ struct ExitDescription { // For isa, dyn_cast, etc operations on TelemetryInfo. typedef unsigned KindType; +// The EntryKind is defined as a struct because it is expectend to be +// extended by subclasses which may have additional TelemetryInfo +// types defined. struct EntryKind { static const KindType Base = 0; }; @@ -97,7 +100,7 @@ struct TelemetryInfo { std::string SessionId; // Time/memory statistics of this event. - TelemetryEventStats Stats; + EventStats Stats; std::optional ExitDesc; @@ -114,6 +117,8 @@ struct TelemetryInfo { // For isa, dyn_cast, etc, operations. virtual KindType getKind() const { return EntryKind::Base; } static bool classof(const TelemetryInfo *T) { + if (T == nullptr) + return false; return T->getKind() == EntryKind::Base; } }; @@ -126,7 +131,7 @@ struct TelemetryInfo { // and where to forward them. class Destination { public: - virtual ~TelemetryDestination() = default; + virtual ~Destination() = default; virtual Error emitEntry(const TelemetryInfo *Entry) = 0; virtual std::string name() const = 0; }; @@ -143,7 +148,7 @@ class Telemeter { // Invoked upon tool exit. virtual void logExit(llvm::StringRef ToolPath, TelemetryInfo *Entry) = 0; - virtual void addDestination(TelemetryDestination *Destination) = 0; + virtual void addDestination(Destination *Destination) = 0; }; } // namespace telemetry diff --git a/llvm/lib/Telemetry/Telemetry.cpp b/llvm/lib/Telemetry/Telemetry.cpp index f49b88e07f136..b05a7483c7408 100644 --- a/llvm/lib/Telemetry/Telemetry.cpp +++ b/llvm/lib/Telemetry/Telemetry.cpp @@ -5,7 +5,7 @@ namespace telemetry { llvm::json::Object TelemetryInfo::serializeToJson() const { return json::Object{ - {"UUID", SessionUuid}, + {"SessionId", SessionId}, }; }; diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index fde7e31802f68..a2ded3cb5a239 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -47,10 +47,9 @@ static std::string nextUuid() { } struct VendorEntryKind { - // TODO: should avoid dup with other vendors' Types? static const KindType VendorCommon = 0b010101000; - static const KindType Startup = 0b010101001; - static const KindType Exit = 0b010101010; + static const KindType Startup = 0b010101001; + static const KindType Exit = 0b010101010; }; // Demonstrates that the TelemetryInfo (data courier) struct can be extended @@ -58,12 +57,14 @@ struct VendorEntryKind { // It can also define additional data serialization method. struct VendorCommonTelemetryInfo : public TelemetryInfo { static bool classof(const TelemetryInfo *T) { + if (T == nullptr) + return false; // Subclasses of this is also acceptable. - return (T->getEntryKind() & VendorEntryKind::VendorCommon) == + return (T->getKind() & VendorEntryKind::VendorCommon) == VendorEntryKind::VendorCommon; } - KindType getEntryKind() const override { + KindType getKind() const override { return VendorEntryKind::VendorCommon; } @@ -75,7 +76,7 @@ struct StartupEvent : public VendorCommonTelemetryInfo { StartupEvent() = default; StartupEvent(const StartupEvent &E) { - SessionUuid = E.SessionUuid; + SessionId = E.SessionId; Stats = E.Stats; ExitDesc = E.ExitDesc; Counter = E.Counter; @@ -84,20 +85,22 @@ struct StartupEvent : public VendorCommonTelemetryInfo { } static bool classof(const TelemetryInfo *T) { - return T->getEntryKind() == VendorEntryKind::Startup; + if (T == nullptr) + return false; + return T->getKind() == VendorEntryKind::Startup; } - KindType getEntryKind() const override { return VendorEntryKind::Startup; } + KindType getKind() const override { return VendorEntryKind::Startup; } void serializeToStream(llvm::raw_ostream &OS) const override { - OS << "UUID:" << SessionUuid << "\n"; + OS << "SessionId:" << SessionId << "\n"; OS << "MagicStartupMsg:" << MagicStartupMsg << "\n"; } json::Object serializeToJson() const override { return json::Object{ {"Startup", - {{"UUID", SessionUuid}, {"MagicStartupMsg", MagicStartupMsg}}}, + {{"SessionId", SessionId}, {"MagicStartupMsg", MagicStartupMsg}}}, }; } }; @@ -109,7 +112,7 @@ struct ExitEvent : public VendorCommonTelemetryInfo { // Provide a copy ctor because we may need to make a copy // before sanitizing the Entry. ExitEvent(const ExitEvent &E) { - SessionUuid = E.SessionUuid; + SessionId = E.SessionId; Stats = E.Stats; ExitDesc = E.ExitDesc; Counter = E.Counter; @@ -118,13 +121,15 @@ struct ExitEvent : public VendorCommonTelemetryInfo { } static bool classof(const TelemetryInfo *T) { - return T->getEntryKind() == VendorEntryKind::Exit; + if (T == nullptr) + return false; + return T->getKind() == VendorEntryKind::Exit; } - unsigned getEntryKind() const override { return VendorEntryKind::Exit; } + unsigned getKind() const override { return VendorEntryKind::Exit; } void serializeToStream(llvm::raw_ostream &OS) const override { - OS << "UUID:" << SessionUuid << "\n"; + OS << "SessionId:" << SessionId << "\n"; if (ExitDesc.has_value()) OS << "ExitCode:" << ExitDesc->ExitCode << "\n"; OS << "MagicExitMsg:" << MagicExitMsg << "\n"; @@ -132,7 +137,7 @@ struct ExitEvent : public VendorCommonTelemetryInfo { json::Object serializeToJson() const override { json::Array I = json::Array{ - {"UUID", SessionUuid}, + {"SessionId", SessionId}, {"MagicExitMsg", MagicExitMsg}, }; if (ExitDesc.has_value()) @@ -148,7 +153,7 @@ struct CustomTelemetryEvent : public VendorCommonTelemetryInfo { CustomTelemetryEvent() = default; CustomTelemetryEvent(const CustomTelemetryEvent &E) { - SessionUuid = E.SessionUuid; + SessionId = E.SessionId; Stats = E.Stats; ExitDesc = E.ExitDesc; Counter = E.Counter; @@ -157,7 +162,7 @@ struct CustomTelemetryEvent : public VendorCommonTelemetryInfo { } void serializeToStream(llvm::raw_ostream &OS) const override { - OS << "UUID:" << SessionUuid << "\n"; + OS << "SessionId:" << SessionId << "\n"; int I = 0; for (const std::string &M : Msgs) { OS << "MSG_" << I << ":" << M << "\n"; @@ -167,7 +172,7 @@ struct CustomTelemetryEvent : public VendorCommonTelemetryInfo { json::Object serializeToJson() const override { json::Object Inner; - Inner.try_emplace("UUID", SessionUuid); + Inner.try_emplace("SessionId", SessionId); int I = 0; for (const std::string &M : Msgs) { Inner.try_emplace(("MSG_" + llvm::Twine(I)).str(), M); @@ -179,7 +184,7 @@ struct CustomTelemetryEvent : public VendorCommonTelemetryInfo { }; // The following classes demonstrate how downstream code can -// define one or more custom TelemetryDestination(s) to handle +// define one or more custom Destination(s) to handle // Telemetry data differently, specifically: // + which data to send (fullset or sanitized) // + where to send the data @@ -189,7 +194,7 @@ const std::string STRING_DEST("STRING"); const std::string JSON_DEST("JSON"); // This Destination sends data to a std::string given at ctor. -class StringDestination : public TelemetryDestination { +class StringDestination : public Destination { public: // ShouldSanitize: if true, sanitize the data before emitting, otherwise, emit // the full set. @@ -216,7 +221,7 @@ class StringDestination : public TelemetryDestination { } } else { // Unfamiliar entries, just send the entry's UUID - OS << "UUID:" << Entry->SessionUuid << "\n"; + OS << "SessionId:" << Entry->SessionId << "\n"; } return Error::success(); } @@ -240,7 +245,7 @@ class StringDestination : public TelemetryDestination { }; // This Destination sends data to some "blackbox" in form of JSON. -class JsonStreamDestination : public TelemetryDestination { +class JsonStreamDestination : public Destination { public: JsonStreamDestination(bool ShouldSanitize) : ShouldSanitize(ShouldSanitize) {} @@ -260,8 +265,8 @@ class JsonStreamDestination : public TelemetryDestination { return SendToBlackbox(E->serializeToJson()); } } else { - // Unfamiliar entries, just send the entry's UUID - return SendToBlackbox(json::Object{{"UUID", Entry->SessionUuid}}); + // Unfamiliar entries, just send the entry's ID + return SendToBlackbox(json::Object{{"SessionId", Entry->SessionId}}); } return make_error("unhandled codepath in emitEntry", inconvertibleErrorCode()); @@ -296,10 +301,10 @@ class JsonStreamDestination : public TelemetryDestination { // Custom vendor-defined Telemeter that has additional data-collection point. class TestTelemeter : public Telemeter { public: - TestTelemeter(std::string SessionUuid) : Uuid(SessionUuid), Counter(0) {} + TestTelemeter(std::string SessionId) : Uuid(SessionId), Counter(0) {} static std::unique_ptr - createInstance(TelemetryConfig *config) { + createInstance(Config *config) { llvm::errs() << "============================== createInstance is called" << "\n"; if (!config->EnableTelemetry) @@ -352,7 +357,7 @@ class TestTelemeter : public Telemeter { emitToDestinations(Entry); } - void addDestination(TelemetryDestination *Dest) override { + void addDestination(Destination *Dest) override { Destinations.push_back(Dest); } @@ -376,14 +381,14 @@ class TestTelemeter : public Telemeter { template T makeDefaultTelemetryInfo() { T Ret; - Ret.SessionUuid = Uuid; + Ret.SessionId = Uuid; Ret.Counter = Counter++; return Ret; } private: void emitToDestinations(TelemetryInfo *Entry) { - for (TelemetryDestination *Dest : Destinations) { + for (Destination *Dest : Destinations) { llvm::Error err = Dest->emitEntry(Entry); if (err) { // Log it and move on. @@ -394,11 +399,11 @@ class TestTelemeter : public Telemeter { const std::string Uuid; size_t Counter; std::string ToolName; - std::vector Destinations; + std::vector Destinations; }; // Pretend to be a "weakly" defined vendor-specific function. -void ApplyVendorSpecificConfigs(TelemetryConfig *config) { +void ApplyVendorSpecificConfigs(Config *config) { config->EnableTelemetry = true; } @@ -408,15 +413,15 @@ void ApplyVendorSpecificConfigs(TelemetryConfig *config) { namespace { -void ApplyCommonConfig(llvm::telemetry::TelemetryConfig *config) { +void ApplyCommonConfig(llvm::telemetry::Config *config) { // Any shareable configs for the upstream tool can go here. // ..... } -std::shared_ptr GetTelemetryConfig() { +std::shared_ptr GetTelemetryConfig() { // Telemetry is disabled by default. // The vendor can enable in their config. - auto Config = std::make_shared(); + auto Config = std::make_shared(); Config->EnableTelemetry = false; ApplyCommonConfig(Config.get()); @@ -491,7 +496,7 @@ static std::string ValueToString(const json::Value *V) { // Without vendor's implementation, telemetry is not enabled by default. TEST(TelemetryTest, TelemetryDefault) { HasVendorConfig = false; - std::shared_ptr Config = + std::shared_ptr Config = GetTelemetryConfig(); auto Tool = vendor_code::TestTelemeter::createInstance(Config.get()); @@ -507,7 +512,7 @@ TEST(TelemetryTest, TelemetryEnabled) { Buffer.clear(); EmittedJsons.clear(); - std::shared_ptr Config = + std::shared_ptr Config = GetTelemetryConfig(); // Add some destinations @@ -526,10 +531,10 @@ TEST(TelemetryTest, TelemetryEnabled) { // Check that the StringDestination emitted properly { std::string ExpectedBuffer = - ("UUID:" + llvm::Twine(ExpectedUuid) + "\n" + "MagicStartupMsg:One_" + - llvm::Twine(ToolName) + "\n" + "UUID:" + llvm::Twine(ExpectedUuid) + + ("SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + "MagicStartupMsg:One_" + + llvm::Twine(ToolName) + "\n" + "SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + "MSG_0:Two\n" + "MSG_1:Deux\n" + "MSG_2:Zwei\n" + - "UUID:" + llvm::Twine(ExpectedUuid) + "\n" + "MagicExitMsg:Three_" + + "SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + "MagicExitMsg:Three_" + llvm::Twine(ToolName) + "\n") .str(); @@ -544,7 +549,7 @@ TEST(TelemetryTest, TelemetryEnabled) { const json::Value *StartupEntry = EmittedJsons[0].get("Startup"); ASSERT_NE(StartupEntry, nullptr); - EXPECT_STREQ(("[[\"UUID\",\"" + llvm::Twine(ExpectedUuid) + + EXPECT_STREQ(("[[\"SessionId\",\"" + llvm::Twine(ExpectedUuid) + "\"],[\"MagicStartupMsg\",\"One_" + llvm::Twine(ToolName) + "\"]]") .str() @@ -557,7 +562,7 @@ TEST(TelemetryTest, TelemetryEnabled) { // entries (for now), so the "UUID" field is put at the end of the array // even though it was emitted first. EXPECT_STREQ(("{\"MSG_0\":\"Two\",\"MSG_1\":\"Deux\",\"MSG_2\":\"Zwei\"," - "\"UUID\":\"" + + "\"SessionId\":\"" + llvm::Twine(ExpectedUuid) + "\"}") .str() .c_str(), @@ -565,7 +570,7 @@ TEST(TelemetryTest, TelemetryEnabled) { const json::Value *ExitEntry = EmittedJsons[2].get("Exit"); ASSERT_NE(ExitEntry, nullptr); - EXPECT_STREQ(("[[\"UUID\",\"" + llvm::Twine(ExpectedUuid) + + EXPECT_STREQ(("[[\"SessionId\",\"" + llvm::Twine(ExpectedUuid) + "\"],[\"MagicExitMsg\",\"Three_" + llvm::Twine(ToolName) + "\"]]") .str() @@ -585,7 +590,7 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { Buffer.clear(); EmittedJsons.clear(); - std::shared_ptr Config = + std::shared_ptr Config = GetTelemetryConfig(); // Add some destinations @@ -603,10 +608,10 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { // The StringDestination should have removed the odd-positioned msgs. std::string ExpectedBuffer = - ("UUID:" + llvm::Twine(ExpectedUuid) + "\n" + "MagicStartupMsg:One_" + - llvm::Twine(ToolName) + "\n" + "UUID:" + llvm::Twine(ExpectedUuid) + + ("SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + "MagicStartupMsg:One_" + + llvm::Twine(ToolName) + "\n" + "SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + "MSG_0:Two\n" + "MSG_1:\n" + // <<< was sanitized away. - "MSG_2:Zwei\n" + "UUID:" + llvm::Twine(ExpectedUuid) + "\n" + + "MSG_2:Zwei\n" + "SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + "MagicExitMsg:Three_" + llvm::Twine(ToolName) + "\n") .str(); EXPECT_STREQ(ExpectedBuffer.c_str(), Buffer.c_str()); @@ -620,7 +625,7 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { const json::Value *StartupEntry = EmittedJsons[0].get("Startup"); ASSERT_NE(StartupEntry, nullptr); - EXPECT_STREQ(("[[\"UUID\",\"" + llvm::Twine(ExpectedUuid) + + EXPECT_STREQ(("[[\"SessionId\",\"" + llvm::Twine(ExpectedUuid) + "\"],[\"MagicStartupMsg\",\"One_" + llvm::Twine(ToolName) + "\"]]") .str() @@ -631,7 +636,7 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { ASSERT_NE(MidpointEntry, nullptr); // The JsonDestination should have removed the even-positioned msgs. EXPECT_STREQ( - ("{\"MSG_0\":\"\",\"MSG_1\":\"Deux\",\"MSG_2\":\"\",\"UUID\":\"" + + ("{\"MSG_0\":\"\",\"MSG_1\":\"Deux\",\"MSG_2\":\"\",\"SessionId\":\"" + llvm::Twine(ExpectedUuid) + "\"}") .str() .c_str(), @@ -639,7 +644,7 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { const json::Value *ExitEntry = EmittedJsons[2].get("Exit"); ASSERT_NE(ExitEntry, nullptr); - EXPECT_STREQ(("[[\"UUID\",\"" + llvm::Twine(ExpectedUuid) + + EXPECT_STREQ(("[[\"SessionId\",\"" + llvm::Twine(ExpectedUuid) + "\"],[\"MagicExitMsg\",\"Three_" + llvm::Twine(ToolName) + "\"]]") .str() From 47e8b06e52aa074e1d8047fb5ec3332829f9cdb0 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 4 Sep 2024 12:23:24 -0400 Subject: [PATCH 12/30] reformated doc and added additional details --- llvm/docs/Telemetry.rst | 24 ++++++++++++++++-------- llvm/docs/llvm_telemery_design.png | Bin 0 -> 94299 bytes 2 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 llvm/docs/llvm_telemery_design.png diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 997436e78735d..bca8eda9d13d1 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -8,6 +8,10 @@ Telemetry framework in LLVM .. toctree:: :hidden: +=========================== +Telemetry framework in LLVM +=========================== + Objective ========= @@ -18,6 +22,7 @@ It is located at `llvm/Telemetry/Telemetry.h` Characteristics --------------- * Configurable and extensible by: + * Tools: any tool that wants to use Telemetry can extend and customize it. * Vendors: Toolchain vendors can also provide custom implementation of the library, which could either override or extend the given tool's upstream @@ -32,6 +37,7 @@ Important notes * There is no concrete implementation of a Telemetry library in upstream LLVM. We only provide the abstract API here. Any tool that wants telemetry will implement one. + The rationale for this is that, all the tools in llvm are very different in what they care about(what/where/when to instrument data). Hence, it might not be practical to have a single implementation. @@ -40,18 +46,21 @@ Important notes * No implementation of Telemetry in upstream LLVM shall store any of the collected data due to privacy and security reasons: + * Different organizations have different privacy models: + * Which data is sensitive, which is not? * Whether it is acceptable for instrumented data to be stored anywhere? (to a local file, what not?) + * Data ownership and data collection consents are hard to accommodate from LLVM developers' point of view: * Eg., data collected by Telemetry is not neccessarily owned by the user of an LLVM tool with Telemetry enabled, hence the user's consent to data collection is not meaningful. On the other hand, LLVM developers have no - practical way to request consent from the "real" owners. - - + reasonable ways to request consent from the "real" owners. + + High-level design ================= @@ -59,12 +68,11 @@ Key components -------------- The framework is consisted of three important classes: + * `llvm::telemetry::Telemeter`: The class responsible for collecting and - forwarding telemetry data. This is the main point of interaction between - the framework and any tool that wants to enable telemery. + forwarding telemetry data. This is the main point of interaction between the + framework and any tool that wants to enable telemery. * `llvm::telemetry::TelemetryInfo`: Data courier * `llvm::telemetry::Config`: Configurations on the `Telemeter`. - -// - +.. image:: llvm_telemetry_design.png diff --git a/llvm/docs/llvm_telemery_design.png b/llvm/docs/llvm_telemery_design.png new file mode 100644 index 0000000000000000000000000000000000000000..0ccb75cf8e30b662c3dcac0faf009bd0753df7c8 GIT binary patch literal 94299 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

=OX9)WnFFprt4a$S!=mRFHou2- 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

%0U7*l* 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 V$Qk)Rarpf&xAlx~RO#49{T~zh??nIr literal 0 HcmV?d00001 From 690c6ab3f6fb14968b06bd9d8e8a9b7b0d4b0d4a Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 4 Sep 2024 12:43:16 -0400 Subject: [PATCH 13/30] fix formatting --- llvm/docs/Telemetry.rst | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index bca8eda9d13d1..be7d7c673c21e 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -55,10 +55,10 @@ Important notes * Data ownership and data collection consents are hard to accommodate from LLVM developers' point of view: - * Eg., data collected by Telemetry is not neccessarily owned by the user - of an LLVM tool with Telemetry enabled, hence the user's consent to data - collection is not meaningful. On the other hand, LLVM developers have no - reasonable ways to request consent from the "real" owners. + * Eg., data collected by Telemetry is not neccessarily owned by the user + of an LLVM tool with Telemetry enabled, hence the user's consent to data + collection is not meaningful. On the other hand, LLVM developers have no + reasonable ways to request consent from the "real" owners. High-level design @@ -67,7 +67,7 @@ High-level design Key components -------------- -The framework is consisted of three important classes: +The framework is consisted of four important classes: * `llvm::telemetry::Telemeter`: The class responsible for collecting and forwarding telemetry data. This is the main point of interaction between the @@ -76,3 +76,8 @@ The framework is consisted of three important classes: * `llvm::telemetry::Config`: Configurations on the `Telemeter`. .. image:: llvm_telemetry_design.png + +How to implement and interact with the API +------------------------------------------ + +// TODO: walk through a simple usage here. From db668f4847d7a8adb5587b24fe11e8b5edff0eee Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 4 Sep 2024 13:52:55 -0400 Subject: [PATCH 14/30] details --- llvm/docs/Telemetry.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index be7d7c673c21e..afce9af040a6e 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -70,11 +70,16 @@ Key components The framework is consisted of four important classes: * `llvm::telemetry::Telemeter`: The class responsible for collecting and - forwarding telemetry data. This is the main point of interaction between the + transmitting telemetry data. This is the main point of interaction between the framework and any tool that wants to enable telemery. * `llvm::telemetry::TelemetryInfo`: Data courier +* `llvm::telemetry::Destination`: Data sink to which the Telemetry framework + sends data. + Its implementation is transparent to the framework. + It is up to the vendor to decide which pieces of data to forward and where + to forward them to their final storage. * `llvm::telemetry::Config`: Configurations on the `Telemeter`. - + .. image:: llvm_telemetry_design.png How to implement and interact with the API From 0290a1491593135da6db2838a5ab019361cd6f2c Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 4 Sep 2024 14:33:52 -0400 Subject: [PATCH 15/30] formatted TelemetryTest.cpp --- llvm/unittests/Telemetry/TelemetryTest.cpp | 39 ++++++++++------------ 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index a2ded3cb5a239..fcd1f4c750d31 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -47,9 +47,9 @@ static std::string nextUuid() { } struct VendorEntryKind { - static const KindType VendorCommon = 0b010101000; - static const KindType Startup = 0b010101001; - static const KindType Exit = 0b010101010; + static const KindType VendorCommon = 168; // 0b010101000 + static const KindType Startup = 169; // 0b010101001 + static const KindType Exit = 170; // 0b010101010 }; // Demonstrates that the TelemetryInfo (data courier) struct can be extended @@ -64,9 +64,7 @@ struct VendorCommonTelemetryInfo : public TelemetryInfo { VendorEntryKind::VendorCommon; } - KindType getKind() const override { - return VendorEntryKind::VendorCommon; - } + KindType getKind() const override { return VendorEntryKind::VendorCommon; } virtual void serializeToStream(llvm::raw_ostream &OS) const = 0; }; @@ -303,8 +301,7 @@ class TestTelemeter : public Telemeter { public: TestTelemeter(std::string SessionId) : Uuid(SessionId), Counter(0) {} - static std::unique_ptr - createInstance(Config *config) { + static std::unique_ptr createInstance(Config *config) { llvm::errs() << "============================== createInstance is called" << "\n"; if (!config->EnableTelemetry) @@ -496,8 +493,7 @@ static std::string ValueToString(const json::Value *V) { // Without vendor's implementation, telemetry is not enabled by default. TEST(TelemetryTest, TelemetryDefault) { HasVendorConfig = false; - std::shared_ptr Config = - GetTelemetryConfig(); + std::shared_ptr Config = GetTelemetryConfig(); auto Tool = vendor_code::TestTelemeter::createInstance(Config.get()); EXPECT_EQ(nullptr, Tool.get()); @@ -512,8 +508,7 @@ TEST(TelemetryTest, TelemetryEnabled) { Buffer.clear(); EmittedJsons.clear(); - std::shared_ptr Config = - GetTelemetryConfig(); + std::shared_ptr Config = GetTelemetryConfig(); // Add some destinations Config->AdditionalDestinations.push_back(vendor_code::STRING_DEST); @@ -531,11 +526,11 @@ TEST(TelemetryTest, TelemetryEnabled) { // Check that the StringDestination emitted properly { std::string ExpectedBuffer = - ("SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + "MagicStartupMsg:One_" + - llvm::Twine(ToolName) + "\n" + "SessionId:" + llvm::Twine(ExpectedUuid) + - "\n" + "MSG_0:Two\n" + "MSG_1:Deux\n" + "MSG_2:Zwei\n" + - "SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + "MagicExitMsg:Three_" + - llvm::Twine(ToolName) + "\n") + ("SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + + "MagicStartupMsg:One_" + llvm::Twine(ToolName) + "\n" + "SessionId:" + + llvm::Twine(ExpectedUuid) + "\n" + "MSG_0:Two\n" + "MSG_1:Deux\n" + + "MSG_2:Zwei\n" + "SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + + "MagicExitMsg:Three_" + llvm::Twine(ToolName) + "\n") .str(); EXPECT_STREQ(ExpectedBuffer.c_str(), Buffer.c_str()); @@ -590,8 +585,7 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { Buffer.clear(); EmittedJsons.clear(); - std::shared_ptr Config = - GetTelemetryConfig(); + std::shared_ptr Config = GetTelemetryConfig(); // Add some destinations Config->AdditionalDestinations.push_back(vendor_code::STRING_DEST); @@ -608,9 +602,10 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { // The StringDestination should have removed the odd-positioned msgs. std::string ExpectedBuffer = - ("SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + "MagicStartupMsg:One_" + - llvm::Twine(ToolName) + "\n" + "SessionId:" + llvm::Twine(ExpectedUuid) + - "\n" + "MSG_0:Two\n" + "MSG_1:\n" + // <<< was sanitized away. + ("SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + + "MagicStartupMsg:One_" + llvm::Twine(ToolName) + "\n" + + "SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + "MSG_0:Two\n" + + "MSG_1:\n" + // <<< was sanitized away. "MSG_2:Zwei\n" + "SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + "MagicExitMsg:Three_" + llvm::Twine(ToolName) + "\n") .str(); From 14b52340b67aa7cce366717b35aa1327dd4b48d5 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 4 Sep 2024 14:36:58 -0400 Subject: [PATCH 16/30] fix formatting in docs --- llvm/docs/Telemetry.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index afce9af040a6e..4dc305362cfed 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -55,6 +55,7 @@ Important notes * Data ownership and data collection consents are hard to accommodate from LLVM developers' point of view: + * Eg., data collected by Telemetry is not neccessarily owned by the user of an LLVM tool with Telemetry enabled, hence the user's consent to data collection is not meaningful. On the other hand, LLVM developers have no From 6ca87e532a66fdb61b6c924f84368d062353da91 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 4 Sep 2024 14:57:38 -0400 Subject: [PATCH 17/30] convert comments to doxygen style --- ...y_design.png => llvm_telemetry_design.png} | Bin llvm/include/llvm/Telemetry/Telemetry.h | 62 +++++++++++------- llvm/unittests/Telemetry/TelemetryTest.cpp | 1 - 3 files changed, 38 insertions(+), 25 deletions(-) rename llvm/docs/{llvm_telemery_design.png => llvm_telemetry_design.png} (100%) diff --git a/llvm/docs/llvm_telemery_design.png b/llvm/docs/llvm_telemetry_design.png similarity index 100% rename from llvm/docs/llvm_telemery_design.png rename to llvm/docs/llvm_telemetry_design.png diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 8f26f6ba9ec26..9febb08c9caed 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -36,8 +36,13 @@ namespace llvm { namespace telemetry { -// Configuration for the Telemeter class. -// This struct can be extended as needed. +/// Configuration for the Telemeter class. +/// This stores configurations from both users and vendors and is passed +/// to the Telemeter upon contruction. (Any changes to the config after +/// the Telemeter's construction will not have effect on it). +/// +/// This struct can be extended as needed to add additional configuration +/// points specific to a vendor's implementation. struct Config { // If true, telemetry will be enabled. bool EnableTelemetry; @@ -51,11 +56,11 @@ struct Config { std::vector AdditionalDestinations; }; -// Defines a convenient type for timestamp of various events. -// This is used by the EventStats below. +/// Defines a convenient type for timestamp of various events. +/// This is used by the EventStats below. using SteadyTimePoint = std::chrono::time_point; -// Various time (and possibly memory) statistics of an event. +/// Various time (and possibly memory) statistics of an event. struct EventStats { // REQUIRED: Start time of an event SteadyTimePoint Start; @@ -69,25 +74,34 @@ struct EventStats { : Start(Start), End(End) {} }; +/// Describes the exit signal of an event. +/// This is used by TelemetryInfo below. struct ExitDescription { int ExitCode; std::string Description; }; -// For isa, dyn_cast, etc operations on TelemetryInfo. +/// For isa, dyn_cast, etc operations on TelemetryInfo. typedef unsigned KindType; -// The EntryKind is defined as a struct because it is expectend to be -// extended by subclasses which may have additional TelemetryInfo -// types defined. +/// This struct is used by TelemetryInfo to support isa<>, dyn_cast<> +/// operations. +/// It is defined as a struct(rather than an enum) because it is +/// expectend to be extended by subclasses which may have +/// additional TelemetryInfo types defined to describe different events. struct EntryKind { static const KindType Base = 0; }; -// TelemetryInfo is the data courier, used to forward data from -// the tool being monitored to the Telemery framework. -// -// This base class contains only the basic set of telemetry data. -// Downstream implementations can add more fields as needed. +/// TelemetryInfo is the data courier, used to move instrumented data +/// the tool being monitored to the Telemery framework. +/// +/// This base class contains only the basic set of telemetry data. +/// Downstream implementations can add more fields as needed to describe +/// additional events. +/// +/// For eg., The LLDB debugger can define a DebugCommandInfo subclass +/// which has additional fields about the debug-command being instrumented, +/// such as `CommandArguments` or `CommandName`. struct TelemetryInfo { // This represents a unique-id, conventionally corresponding to // a tools' session - ie., every time the tool starts until it exits. @@ -123,12 +137,12 @@ struct TelemetryInfo { } }; -// This class presents a data sink to which the Telemetry framework -// sends data. -// -// Its implementation is transparent to the framework. -// It is up to the vendor to decide which pieces of data to forward -// and where to forward them. +/// This class presents a data sink to which the Telemetry framework +/// sends data. +/// +/// Its implementation is transparent to the framework. +/// It is up to the vendor to decide which pieces of data to forward +/// and where to forward them. class Destination { public: virtual ~Destination() = default; @@ -136,10 +150,10 @@ class Destination { virtual std::string name() const = 0; }; -// This class is the main interaction point between any LLVM tool -// and this framework. -// It is responsible for collecting telemetry data from the tool being -// monitored. +/// This class is the main interaction point between any LLVM tool +/// and this framework. +/// It is responsible for collecting telemetry data from the tool being +/// monitored and transmitting the data elsewhere. class Telemeter { public: // Invoked upon tool startup diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index fcd1f4c750d31..82bdaaa8876c1 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -19,7 +19,6 @@ #include "llvm/Support/SourceMgr.h" #include "llvm/Support/raw_ostream.h" #include "gtest/gtest.h" - #include #include #include From 4155adde942153cffbbeab94ff4009f2f822ac74 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 4 Sep 2024 15:14:35 -0400 Subject: [PATCH 18/30] fix doc toc --- llvm/docs/UserGuides.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/llvm/docs/UserGuides.rst b/llvm/docs/UserGuides.rst index 171da2053e731..feb07bcf0a7ef 100644 --- a/llvm/docs/UserGuides.rst +++ b/llvm/docs/UserGuides.rst @@ -72,6 +72,7 @@ intermediate LLVM representation. SupportLibrary TableGen/index TableGenFundamentals + Telemetry Vectorizers WritingAnLLVMPass WritingAnLLVMNewPMPass From 11ead4adede52690936936f3ed2784fce4bedaaa Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 5 Sep 2024 13:53:00 -0400 Subject: [PATCH 19/30] group all the testing-params into a Context struct --- llvm/unittests/Telemetry/TelemetryTest.cpp | 178 ++++++++++++--------- 1 file changed, 104 insertions(+), 74 deletions(-) diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 82bdaaa8876c1..d46543beec87f 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -25,15 +25,32 @@ // Testing parameters. // These are set by each test to force certain outcomes. -// Since the tests may be run in parellel, these should probably -// be thread_local. -static thread_local bool HasExitError = false; -static thread_local std::string ExitMsg = ""; -static thread_local bool HasVendorConfig = false; -static thread_local bool SanitizeData = false; -static thread_local std::string Buffer = ""; -static thread_local std::vector EmittedJsons; -static thread_local std::string ExpectedUuid = ""; +// Since the tests may run in parallel, each test will have +// its own TestContext populated. +struct TestContext { + // Controlling whether there should be an Exit error (if so, what the + // expected exit message/description should be). + bool HasExitError = false; + std::string ExitMsg = ""; + + // Controllilng whether there is a vendor-provided config for + // Telemetry. + bool HasVendorConfig = false; + + // Controlling whether the data should be sanitized. + bool SanitizeData = false; + + // These two fields data emitted by the framework for later + // verifications by the tests. + std::string Buffer = ""; + std::vector EmittedJsons; + + // The expected Uuid generated by the fake tool. + std::string ExpectedUuid = ""; +}; + +// This is set by the test body. +static thread_local TestContext *CurrentContext = nullptr; namespace llvm { namespace telemetry { @@ -289,7 +306,7 @@ class JsonStreamDestination : public Destination { // send the data to some internal storage. // For testing purposes, we just queue up the entries to // the vector for validation. - EmittedJsons.push_back(std::move(O)); + CurrentContext->EmittedJsons.push_back(std::move(O)); return Error::success(); } bool ShouldSanitize; @@ -305,19 +322,19 @@ class TestTelemeter : public Telemeter { << "\n"; if (!config->EnableTelemetry) return nullptr; - ExpectedUuid = nextUuid(); + CurrentContext->ExpectedUuid = nextUuid(); std::unique_ptr Telemeter = - std::make_unique(ExpectedUuid); + std::make_unique(CurrentContext->ExpectedUuid); // Set up Destination based on the given config. for (const std::string &Dest : config->AdditionalDestinations) { // The destination(s) are ALSO defined by vendor, so it should understand // what the name of each destination signifies. if (Dest == JSON_DEST) { - Telemeter->addDestination( - new vendor_code::JsonStreamDestination(SanitizeData)); + Telemeter->addDestination(new vendor_code::JsonStreamDestination( + CurrentContext->SanitizeData)); } else if (Dest == STRING_DEST) { - Telemeter->addDestination( - new vendor_code::StringDestination(SanitizeData, Buffer)); + Telemeter->addDestination(new vendor_code::StringDestination( + CurrentContext->SanitizeData, CurrentContext->Buffer)); } else { llvm_unreachable( llvm::Twine("unknown destination: ", Dest).str().c_str()); @@ -431,7 +448,7 @@ std::shared_ptr GetTelemetryConfig() { // #endif // // But for unit testing, we use the testing params defined at the top. - if (HasVendorConfig) { + if (CurrentContext->HasVendorConfig) { llvm::telemetry::vendor_code::ApplyVendorSpecificConfigs(Config.get()); } return Config; @@ -468,8 +485,8 @@ void AtToolExit(std::string ToolName, vendor_code::TestTelemeter *T) { T->makeDefaultTelemetryInfo(); Entry.Stats = {ExitTime, ExitCompleteTime}; - if (HasExitError) { - Entry.ExitDesc = {1, ExitMsg}; + if (CurrentContext->HasExitError) { + Entry.ExitDesc = {1, CurrentContext->ExitMsg}; } T->logExit(ToolName, &Entry); } @@ -491,7 +508,11 @@ static std::string ValueToString(const json::Value *V) { // Without vendor's implementation, telemetry is not enabled by default. TEST(TelemetryTest, TelemetryDefault) { - HasVendorConfig = false; + // Preset some test params. + TestContext Context; + Context.HasVendorConfig = false; + CurrentContext = &Context; + std::shared_ptr Config = GetTelemetryConfig(); auto Tool = vendor_code::TestTelemeter::createInstance(Config.get()); @@ -502,10 +523,12 @@ TEST(TelemetryTest, TelemetryEnabled) { const std::string ToolName = "TelemetryTest"; // Preset some test params. - HasVendorConfig = true; - SanitizeData = false; - Buffer.clear(); - EmittedJsons.clear(); + TestContext Context; + Context.HasVendorConfig = true; + Context.SanitizeData = false; + Context.Buffer.clear(); + Context.EmittedJsons.clear(); + CurrentContext = &Context; std::shared_ptr Config = GetTelemetryConfig(); @@ -520,56 +543,59 @@ TEST(TelemetryTest, TelemetryEnabled) { AtToolExit(ToolName, Tool.get()); // Check that the Tool uses the expected UUID. - EXPECT_STREQ(Tool->getUuid().c_str(), ExpectedUuid.c_str()); + EXPECT_STREQ(Tool->getUuid().c_str(), CurrentContext->ExpectedUuid.c_str()); // Check that the StringDestination emitted properly { std::string ExpectedBuffer = - ("SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + - "MagicStartupMsg:One_" + llvm::Twine(ToolName) + "\n" + "SessionId:" + - llvm::Twine(ExpectedUuid) + "\n" + "MSG_0:Two\n" + "MSG_1:Deux\n" + - "MSG_2:Zwei\n" + "SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + + ("SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" + + "MagicStartupMsg:One_" + llvm::Twine(ToolName) + "\n" + + "SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" + + "MSG_0:Two\n" + "MSG_1:Deux\n" + "MSG_2:Zwei\n" + + "SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" + "MagicExitMsg:Three_" + llvm::Twine(ToolName) + "\n") .str(); - EXPECT_STREQ(ExpectedBuffer.c_str(), Buffer.c_str()); + EXPECT_STREQ(ExpectedBuffer.c_str(), CurrentContext->Buffer.c_str()); } // Check that the JsonDestination emitted properly { // There should be 3 events emitted by the Telemeter (start, midpoint, exit) - EXPECT_EQ(3, EmittedJsons.size()); + EXPECT_EQ(3, CurrentContext->EmittedJsons.size()); - const json::Value *StartupEntry = EmittedJsons[0].get("Startup"); + const json::Value *StartupEntry = + CurrentContext->EmittedJsons[0].get("Startup"); ASSERT_NE(StartupEntry, nullptr); - EXPECT_STREQ(("[[\"SessionId\",\"" + llvm::Twine(ExpectedUuid) + - "\"],[\"MagicStartupMsg\",\"One_" + llvm::Twine(ToolName) + - "\"]]") - .str() - .c_str(), - ValueToString(StartupEntry).c_str()); + EXPECT_STREQ( + ("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) + + "\"],[\"MagicStartupMsg\",\"One_" + llvm::Twine(ToolName) + "\"]]") + .str() + .c_str(), + ValueToString(StartupEntry).c_str()); - const json::Value *MidpointEntry = EmittedJsons[1].get("Midpoint"); + const json::Value *MidpointEntry = + CurrentContext->EmittedJsons[1].get("Midpoint"); ASSERT_NE(MidpointEntry, nullptr); // TODO: This is a bit flaky in that the json string printer sort the // entries (for now), so the "UUID" field is put at the end of the array // even though it was emitted first. EXPECT_STREQ(("{\"MSG_0\":\"Two\",\"MSG_1\":\"Deux\",\"MSG_2\":\"Zwei\"," "\"SessionId\":\"" + - llvm::Twine(ExpectedUuid) + "\"}") + llvm::Twine(CurrentContext->ExpectedUuid) + "\"}") .str() .c_str(), ValueToString(MidpointEntry).c_str()); - const json::Value *ExitEntry = EmittedJsons[2].get("Exit"); + const json::Value *ExitEntry = CurrentContext->EmittedJsons[2].get("Exit"); ASSERT_NE(ExitEntry, nullptr); - EXPECT_STREQ(("[[\"SessionId\",\"" + llvm::Twine(ExpectedUuid) + - "\"],[\"MagicExitMsg\",\"Three_" + llvm::Twine(ToolName) + - "\"]]") - .str() - .c_str(), - ValueToString(ExitEntry).c_str()); + EXPECT_STREQ( + ("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) + + "\"],[\"MagicExitMsg\",\"Three_" + llvm::Twine(ToolName) + "\"]]") + .str() + .c_str(), + ValueToString(ExitEntry).c_str()); } } @@ -579,10 +605,12 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { const std::string ToolName = "TelemetryTest_SanitizedData"; // Preset some test params. - HasVendorConfig = true; - SanitizeData = true; - Buffer.clear(); - EmittedJsons.clear(); + TestContext Context; + Context.HasVendorConfig = true; + Context.SanitizeData = true; + Context.Buffer.clear(); + Context.EmittedJsons.clear(); + CurrentContext = &Context; std::shared_ptr Config = GetTelemetryConfig(); @@ -599,51 +627,53 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { // Check that the StringDestination emitted properly { // The StringDestination should have removed the odd-positioned msgs. - std::string ExpectedBuffer = - ("SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + + ("SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" + "MagicStartupMsg:One_" + llvm::Twine(ToolName) + "\n" + - "SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + "MSG_0:Two\n" + - "MSG_1:\n" + // <<< was sanitized away. - "MSG_2:Zwei\n" + "SessionId:" + llvm::Twine(ExpectedUuid) + "\n" + + "SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" + + "MSG_0:Two\n" + "MSG_1:\n" + // <<< was sanitized away. + "MSG_2:Zwei\n" + + "SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" + "MagicExitMsg:Three_" + llvm::Twine(ToolName) + "\n") .str(); - EXPECT_STREQ(ExpectedBuffer.c_str(), Buffer.c_str()); + EXPECT_STREQ(ExpectedBuffer.c_str(), CurrentContext->Buffer.c_str()); } // Check that the JsonDestination emitted properly { // There should be 3 events emitted by the Telemeter (start, midpoint, exit) - EXPECT_EQ(3, EmittedJsons.size()); + EXPECT_EQ(3, CurrentContext->EmittedJsons.size()); - const json::Value *StartupEntry = EmittedJsons[0].get("Startup"); + const json::Value *StartupEntry = + CurrentContext->EmittedJsons[0].get("Startup"); ASSERT_NE(StartupEntry, nullptr); - EXPECT_STREQ(("[[\"SessionId\",\"" + llvm::Twine(ExpectedUuid) + - "\"],[\"MagicStartupMsg\",\"One_" + llvm::Twine(ToolName) + - "\"]]") - .str() - .c_str(), - ValueToString(StartupEntry).c_str()); + EXPECT_STREQ( + ("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) + + "\"],[\"MagicStartupMsg\",\"One_" + llvm::Twine(ToolName) + "\"]]") + .str() + .c_str(), + ValueToString(StartupEntry).c_str()); - const json::Value *MidpointEntry = EmittedJsons[1].get("Midpoint"); + const json::Value *MidpointEntry = + CurrentContext->EmittedJsons[1].get("Midpoint"); ASSERT_NE(MidpointEntry, nullptr); // The JsonDestination should have removed the even-positioned msgs. EXPECT_STREQ( ("{\"MSG_0\":\"\",\"MSG_1\":\"Deux\",\"MSG_2\":\"\",\"SessionId\":\"" + - llvm::Twine(ExpectedUuid) + "\"}") + llvm::Twine(CurrentContext->ExpectedUuid) + "\"}") .str() .c_str(), ValueToString(MidpointEntry).c_str()); - const json::Value *ExitEntry = EmittedJsons[2].get("Exit"); + const json::Value *ExitEntry = CurrentContext->EmittedJsons[2].get("Exit"); ASSERT_NE(ExitEntry, nullptr); - EXPECT_STREQ(("[[\"SessionId\",\"" + llvm::Twine(ExpectedUuid) + - "\"],[\"MagicExitMsg\",\"Three_" + llvm::Twine(ToolName) + - "\"]]") - .str() - .c_str(), - ValueToString(ExitEntry).c_str()); + EXPECT_STREQ( + ("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) + + "\"],[\"MagicExitMsg\",\"Three_" + llvm::Twine(ToolName) + "\"]]") + .str() + .c_str(), + ValueToString(ExitEntry).c_str()); } } From 48228ee076d15fe4bcab5e0843a5adbd5c7636f2 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 5 Sep 2024 14:14:56 -0400 Subject: [PATCH 20/30] fix conversion warnings --- llvm/unittests/Telemetry/TelemetryTest.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index d46543beec87f..3497cd0b139c0 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -563,7 +563,7 @@ TEST(TelemetryTest, TelemetryEnabled) { { // There should be 3 events emitted by the Telemeter (start, midpoint, exit) - EXPECT_EQ(3, CurrentContext->EmittedJsons.size()); + EXPECT_EQ(static_cast(3), CurrentContext->EmittedJsons.size()); const json::Value *StartupEntry = CurrentContext->EmittedJsons[0].get("Startup"); @@ -643,7 +643,7 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { { // There should be 3 events emitted by the Telemeter (start, midpoint, exit) - EXPECT_EQ(3, CurrentContext->EmittedJsons.size()); + EXPECT_EQ(static_cast(3), CurrentContext->EmittedJsons.size()); const json::Value *StartupEntry = CurrentContext->EmittedJsons[0].get("Startup"); From 990e1ca8520349cf154351ae9a6ceb6f8bac396b Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 5 Sep 2024 15:33:44 -0400 Subject: [PATCH 21/30] finish up todos --- llvm/docs/Telemetry.rst | 126 ++++++++++++++++++++- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- llvm/unittests/Telemetry/TelemetryTest.cpp | 22 ++-- 3 files changed, 136 insertions(+), 14 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 4dc305362cfed..af976058e66bc 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -86,4 +86,128 @@ The framework is consisted of four important classes: How to implement and interact with the API ------------------------------------------ -// TODO: walk through a simple usage here. +To use Telemetry in your tool, you need to provide a concrete implementation of the `Telemeter` class and `Destination`. + +1) Define a custom `Telemeter` and `Destination` + +.. code-block:: c++ + // This destiantion just prints the given entry to a stdout. + // In "real life", this would be where you forward the data to your + // custom data storage. + class MyStdoutDestination : public llvm::telemetry::Destiantion { + public: + Error emitEntry(const TelemetryInfo* Entry) override { + return sendToBlackBox(Entry); + + } + + private: + Error sendToBlackBox(const TelemetryInfo* Entry) { + // This could send the data anywhere. + // But we're simply sending it to stdout for the example. + llvm::outs() << entryToString(Entry) << "\n"; + return llvm::success(); + } + + std::string entryToString(const TelemetryInfo* Entry) { + // make a string-representation of the given entry. + } + }; + + // This defines a custom TelemetryInfo that has an addition Msg field. + struct MyTelemetryInfo : public llvm::telemetry::TelemetryInfo { + std::string Msg; + + json::Object serializeToJson() const { + json::Object Ret = TelemeteryInfo::serializeToJson(); + Ret.emplace_back("MyMsg", Msg); + return std::move(Ret); + } + + // TODO: implement getKind() and classof() to support dyn_cast operations. + }; + + class MyTelemeter : public llvm::telemery::Telemeter { + public: + static std::unique_ptr createInstatnce(llvm::telemetry::Config* config) { + // If Telemetry is not enabled, then just return null; + if (!config->EnableTelemetry) return nullptr; + + std::make_unique(); + } + MyTelemeter() = default; + + void logStartup(llvm::StringRef ToolName, TelemetryInfo* Entry) override { + if (MyTelemetryInfo* M = dyn_cast(Entry)) { + M->Msg = "Starting up tool with name: " + ToolName; + emitToAllDestinations(M); + } else { + emitToAllDestinations(Entry); + } + } + + void logExit(llvm::StringRef ToolName, TelemetryInfo* Entry) override { + if (MyTelemetryInfo* M = dyn_cast(Entry)) { + M->Msg = "Exitting tool with name: " + ToolName; + emitToAllDestinations(M); + } else { + emitToAllDestinations(Entry); + } + } + + void addDestination(Destination* dest) override { + destinations.push_back(dest); + } + + // You can also define additional instrumentation points.) + void logAdditionalPoint(TelemetryInfo* Entry) { + // .... code here + } + private: + void emitToAllDestinations(const TelemetryInfo* Entry) { + // Note: could do this in paralle, if needed. + for (Destination* Dest : Destinations) + Dest->emitEntry(Entry); + } + std::vector Destinations; + }; + +2) Use the library in your tool. + +Logging the tool init-process: + +.. code-block:: c++ + + // At tool's init code + auto StartTime = std::chrono::time_point::now(); + llvm::telemetry::Config MyConfig = makeConfig(); // build up the appropriate Config struct here. + auto Telemeter = MyTelemeter::createInstance(&MyConfig); + std::string CurrentSessionId = ...; // Make some unique ID corresponding to the current session here. + + // Any other tool's init code can go here + // ... + + // Finally, take a snapshot of the time now so we know how long it took the + // init process to finish + auto EndTime = std::chrono::time_point::now(); + MyTelemetryInfo Entry; + Entry.SessionId = CurrentSessionId ; // Assign some unique ID here. + Entry.Stats = {StartTime, EndTime}; + Telemeter->logStartup("MyTool", &Entry); + +Similar code can be used for logging the tool's exit. + +Additionall, at any other point in the tool's lifetime, it can also log telemetry: + +.. code-block:: c++ + + // At some execution point: + auto StartTime = std::chrono::time_point::now(); + + // ... other events happening here + + auto EndTime = std::chrono::time_point::now(); + MyTelemetryInfo Entry; + Entry.SessionId = CurrentSessionId ; // Assign some unique ID here. + Entry.Stats = {StartTime, EndTime}; + Telemeter->logAdditionalPoint(&Entry); diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 9febb08c9caed..f682e1cdf5caf 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -93,7 +93,7 @@ struct EntryKind { }; /// TelemetryInfo is the data courier, used to move instrumented data -/// the tool being monitored to the Telemery framework. +/// from the tool being monitored to the Telemery framework. /// /// This base class contains only the basic set of telemetry data. /// Downstream implementations can add more fields as needed to describe diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 3497cd0b139c0..173976bdb1650 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -318,8 +318,6 @@ class TestTelemeter : public Telemeter { TestTelemeter(std::string SessionId) : Uuid(SessionId), Counter(0) {} static std::unique_ptr createInstance(Config *config) { - llvm::errs() << "============================== createInstance is called" - << "\n"; if (!config->EnableTelemetry) return nullptr; CurrentContext->ExpectedUuid = nextUuid(); @@ -348,7 +346,7 @@ class TestTelemeter : public Telemeter { // The vendor can add additional stuff to the entry before logging. if (auto *S = dyn_cast(Entry)) { - S->MagicStartupMsg = llvm::Twine("One_", ToolPath).str(); + S->MagicStartupMsg = llvm::Twine("Startup_", ToolPath).str(); } emitToDestinations(Entry); } @@ -364,7 +362,7 @@ class TestTelemeter : public Telemeter { // The vendor can add additional stuff to the entry before logging. if (auto *E = dyn_cast(Entry)) { - E->MagicExitMsg = llvm::Twine("Three_", ToolPath).str(); + E->MagicExitMsg = llvm::Twine("Exit_", ToolPath).str(); } emitToDestinations(Entry); @@ -549,11 +547,11 @@ TEST(TelemetryTest, TelemetryEnabled) { { std::string ExpectedBuffer = ("SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" + - "MagicStartupMsg:One_" + llvm::Twine(ToolName) + "\n" + + "MagicStartupMsg:Startup_" + llvm::Twine(ToolName) + "\n" + "SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" + "MSG_0:Two\n" + "MSG_1:Deux\n" + "MSG_2:Zwei\n" + "SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" + - "MagicExitMsg:Three_" + llvm::Twine(ToolName) + "\n") + "MagicExitMsg:Exit_" + llvm::Twine(ToolName) + "\n") .str(); EXPECT_STREQ(ExpectedBuffer.c_str(), CurrentContext->Buffer.c_str()); @@ -570,7 +568,7 @@ TEST(TelemetryTest, TelemetryEnabled) { ASSERT_NE(StartupEntry, nullptr); EXPECT_STREQ( ("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) + - "\"],[\"MagicStartupMsg\",\"One_" + llvm::Twine(ToolName) + "\"]]") + "\"],[\"MagicStartupMsg\",\"Startup_" + llvm::Twine(ToolName) + "\"]]") .str() .c_str(), ValueToString(StartupEntry).c_str()); @@ -592,7 +590,7 @@ TEST(TelemetryTest, TelemetryEnabled) { ASSERT_NE(ExitEntry, nullptr); EXPECT_STREQ( ("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) + - "\"],[\"MagicExitMsg\",\"Three_" + llvm::Twine(ToolName) + "\"]]") + "\"],[\"MagicExitMsg\",\"Exit_" + llvm::Twine(ToolName) + "\"]]") .str() .c_str(), ValueToString(ExitEntry).c_str()); @@ -629,12 +627,12 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { // The StringDestination should have removed the odd-positioned msgs. std::string ExpectedBuffer = ("SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" + - "MagicStartupMsg:One_" + llvm::Twine(ToolName) + "\n" + + "MagicStartupMsg:Startup_" + llvm::Twine(ToolName) + "\n" + "SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" + "MSG_0:Two\n" + "MSG_1:\n" + // <<< was sanitized away. "MSG_2:Zwei\n" + "SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" + - "MagicExitMsg:Three_" + llvm::Twine(ToolName) + "\n") + "MagicExitMsg:Exit_" + llvm::Twine(ToolName) + "\n") .str(); EXPECT_STREQ(ExpectedBuffer.c_str(), CurrentContext->Buffer.c_str()); } @@ -650,7 +648,7 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { ASSERT_NE(StartupEntry, nullptr); EXPECT_STREQ( ("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) + - "\"],[\"MagicStartupMsg\",\"One_" + llvm::Twine(ToolName) + "\"]]") + "\"],[\"MagicStartupMsg\",\"Startup_" + llvm::Twine(ToolName) + "\"]]") .str() .c_str(), ValueToString(StartupEntry).c_str()); @@ -670,7 +668,7 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { ASSERT_NE(ExitEntry, nullptr); EXPECT_STREQ( ("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) + - "\"],[\"MagicExitMsg\",\"Three_" + llvm::Twine(ToolName) + "\"]]") + "\"],[\"MagicExitMsg\",\"Exit_" + llvm::Twine(ToolName) + "\"]]") .str() .c_str(), ValueToString(ExitEntry).c_str()); From 42781f792a953d41d6ae0181f6d4f1a78b0b37fc Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 12 Sep 2024 14:55:57 -0400 Subject: [PATCH 22/30] [lldb]Implement LLDB Telemetry : Provide the concrete implementation for telemetry in LLDB. (This is follow-up patch to PR/102323) --- lldb/include/lldb/API/SBDebugger.h | 4 + lldb/include/lldb/Core/Debugger.h | 10 + lldb/include/lldb/Core/Telemetry.h | 292 +++++++ lldb/include/lldb/lldb-enumerations.h | 4 +- lldb/source/API/SBDebugger.cpp | 10 + lldb/source/Core/CMakeLists.txt | 2 + lldb/source/Core/Debugger.cpp | 22 +- lldb/source/Core/Telemetry.cpp | 722 ++++++++++++++++++ .../source/Interpreter/CommandInterpreter.cpp | 45 +- lldb/source/Target/Process.cpp | 11 + lldb/source/Target/Target.cpp | 15 + lldb/tools/lldb-dap/DAP.cpp | 15 + 12 files changed, 1139 insertions(+), 13 deletions(-) create mode 100644 lldb/include/lldb/Core/Telemetry.h create mode 100644 lldb/source/Core/Telemetry.cpp diff --git a/lldb/include/lldb/API/SBDebugger.h b/lldb/include/lldb/API/SBDebugger.h index 84ea9c0f772e1..7ec69dc89b3f3 100644 --- a/lldb/include/lldb/API/SBDebugger.h +++ b/lldb/include/lldb/API/SBDebugger.h @@ -13,6 +13,8 @@ #include "lldb/API/SBDefines.h" #include "lldb/API/SBPlatform.h" +#include "lldb/API/SBStructuredData.h" +#include "llvm/Support/JSON.h" namespace lldb_private { class CommandPluginInterfaceImplementation; @@ -245,6 +247,8 @@ class LLDB_API SBDebugger { lldb::SBTarget GetDummyTarget(); + void SendTelemetry(const llvm::json::Object &entry); + // Return true if target is deleted from the target list of the debugger. bool DeleteTarget(lldb::SBTarget &target); diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h index a72c2596cc2c5..d0e40e415c097 100644 --- a/lldb/include/lldb/Core/Debugger.h +++ b/lldb/include/lldb/Core/Debugger.h @@ -19,6 +19,7 @@ #include "lldb/Core/FormatEntity.h" #include "lldb/Core/IOHandler.h" #include "lldb/Core/SourceManager.h" +#include "lldb/Core/Telemetry.h" #include "lldb/Core/UserSettingsController.h" #include "lldb/Host/HostThread.h" #include "lldb/Host/StreamFile.h" @@ -31,6 +32,7 @@ #include "lldb/Utility/Diagnostics.h" #include "lldb/Utility/FileSpec.h" #include "lldb/Utility/Status.h" +#include "lldb/Utility/StructuredData.h" #include "lldb/Utility/UserID.h" #include "lldb/lldb-defines.h" #include "lldb/lldb-enumerations.h" @@ -45,7 +47,9 @@ #include "llvm/ADT/StringRef.h" #include "llvm/Support/DynamicLibrary.h" #include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/JSON.h" #include "llvm/Support/Threading.h" +#include "llvm/Telemetry/Telemetry.h" #include #include @@ -149,6 +153,10 @@ class Debugger : public std::enable_shared_from_this, repro::DataRecorder *GetInputRecorder(); + LldbTelemeter *GetTelemeter() { return m_telemeter.get(); } + + void SendClientTelemetry(const llvm::json::Object &entry); + Status SetInputString(const char *data); void SetInputFile(lldb::FileSP file); @@ -759,6 +767,8 @@ class Debugger : public std::enable_shared_from_this, eBroadcastBitEventThreadIsListening = (1 << 0), }; + std::unique_ptr m_telemeter; + private: // Use Debugger::CreateInstance() to get a shared pointer to a new debugger // object diff --git a/lldb/include/lldb/Core/Telemetry.h b/lldb/include/lldb/Core/Telemetry.h new file mode 100644 index 0000000000000..bc07369722a12 --- /dev/null +++ b/lldb/include/lldb/Core/Telemetry.h @@ -0,0 +1,292 @@ +//===-- Telemetry.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 +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_CORE_TELEMETRY_H +#define LLDB_CORE_TELEMETRY_H + +#include +#include +#include +#include +#include +#include + +#include "lldb/Interpreter/CommandReturnObject.h" +#include "lldb/Utility/StructuredData.h" +#include "lldb/lldb-forward.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/JSON.h" +#include "llvm/Telemetry/Telemetry.h" + +namespace lldb_private { + +using llvm::telemetry::KindType; + +struct LldbEntryKind : public ::llvm::telemetry::EntryKind { + static const KindType BaseInfo = 0b11000; + static const KindType DebuggerInfo = 0b11001; + static const KindType TargetInfo = 0b11010; + static const KindType ClientInfo = 0b11100; + static const KindType CommandInfo = 0b11101; + static const KindType MiscInfo = 0b11110; +}; + +struct LldbBaseTelemetryInfo : public ::llvm::telemetry::TelemetryInfo { + // For dyn_cast, isa, etc operations. + KindType getKind() const override { return LldbEntryKind::BaseInfo; } + + static bool classof(const TelemetryInfo *T) { + if (T == nullptr) + return false; + // Subclasses of this is also acceptable. + return (T->getKind() & LldbEntryKind::BaseInfo) == LldbEntryKind::BaseInfo; + } + + // Returns a human-readable string description of the struct. + // This is for debugging purposes only. + // It is NOT meant as a data-serialisation method. + virtual std::string ToString() const; +}; + +struct DebuggerTelemetryInfo : public LldbBaseTelemetryInfo { + std::string username; + std::string lldb_git_sha; + std::string lldb_path; + std::string cwd; + + DebuggerTelemetryInfo() = default; + + // Provide a copy ctor because we may need to make a copy before + // sanitizing the data. + // (The sanitization might differ between different Destination classes). + DebuggerTelemetryInfo(const DebuggerTelemetryInfo &other) { + username = other.username; + lldb_git_sha = other.lldb_git_sha; + lldb_path = other.lldb_path; + cwd = other.cwd; + }; + + KindType getKind() const override { return LldbEntryKind::DebuggerInfo; } + + static bool classof(const TelemetryInfo *T) { + if (T == nullptr) + return false; + return T->getKind() == LldbEntryKind::DebuggerInfo; + } + + llvm::json::Object serializeToJson() const override; + + std::string ToString() const override; +}; + +struct TargetTelemetryInfo : public LldbBaseTelemetryInfo { + // The same as the executable-module's UUID. + std::string target_uuid; + std::string file_format; + + std::string binary_path; + size_t binary_size; + + TargetTelemetryInfo() = default; + + TargetTelemetryInfo(const TargetTelemetryInfo &other) { + target_uuid = other.target_uuid; + file_format = other.file_format; + binary_path = other.binary_path; + binary_size = other.binary_size; + } + + KindType getKind() const override { return LldbEntryKind::TargetInfo; } + + static bool classof(const TelemetryInfo *T) { + if (T == nullptr) + return false; + return T->getKind() == LldbEntryKind::TargetInfo; + } + + llvm::json::Object serializeToJson() const override; + + std::string ToString() const override; +}; + +// Entry from client (eg., SB-API) +struct ClientTelemetryInfo : public LldbBaseTelemetryInfo { + std::string request_name; + std::string error_msg; + + ClientTelemetryInfo() = default; + + ClientTelemetryInfo(const ClientTelemetryInfo &other) { + request_name = other.request_name; + error_msg = other.error_msg; + } + + KindType getKind() const override { return LldbEntryKind::ClientInfo; } + + static bool classof(const TelemetryInfo *T) { + if (T == nullptr) + return false; + return T->getKind() == LldbEntryKind::ClientInfo; + } + + llvm::json::Object serializeToJson() const override; + + std::string ToString() const override; +}; + +struct CommandExitDescription : public ::llvm::telemetry::ExitDescription { + lldb::ReturnStatus ret_status; + CommandExitDescription(int ret_code, std::string ret_desc, + lldb::ReturnStatus status) { + ExitCode = ret_code; + Description = std::move(ret_desc); + ret_status = status; + } +}; + +struct CommandTelemetryInfo : public LldbBaseTelemetryInfo { + // If the command is/can be associated with a target entry, + // this field contains that target's UUID. + // otherwise. + std::string target_uuid; + std::string command_uuid; + + // Eg., "breakpoint set" + std::string command_name; + + // !!NOTE!!: The following fields may be omitted due to PII risk. + // (Configurable via the telemery::Config struct) + std::string original_command; + std::string args; + + lldb::ReturnStatus ret_status; + + CommandTelemetryInfo() = default; + + CommandTelemetryInfo(const CommandTelemetryInfo &other) { + target_uuid = other.target_uuid; + command_uuid = other.command_uuid; + command_name = other.command_name; + original_command = other.original_command; + args = other.args; + } + + KindType getKind() const override { return LldbEntryKind::CommandInfo; } + + static bool classof(const TelemetryInfo *T) { + if (T == nullptr) + return false; + return T->getKind() == LldbEntryKind::CommandInfo; + } + + llvm::json::Object serializeToJson() const override; + + std::string ToString() const override; +}; + +// The "catch-all" entry to store a set of custom/non-standard +// data. +struct MiscTelemetryInfo : public LldbBaseTelemetryInfo { + // If the event is/can be associated with a target entry, + // this field contains that target's UUID. + // otherwise. + std::string target_uuid; + + // Set of key-value pairs for any optional (or impl-specific) data + std::unordered_map meta_data; + + MiscTelemetryInfo() = default; + + MiscTelemetryInfo(const MiscTelemetryInfo &other) { + target_uuid = other.target_uuid; + meta_data = other.meta_data; + } + + KindType getKind() const override { return LldbEntryKind::MiscInfo; } + + static bool classof(const TelemetryInfo *T) { + if (T == nullptr) + return false; + return T->getKind() == LldbEntryKind::MiscInfo; + } + + llvm::json::Object serializeToJson() const override; + + std::string ToString() const override; +}; + +class LldbTelemeter : public llvm::telemetry::Telemeter { +public: + static std::unique_ptr CreateInstance(Debugger *); + + virtual ~LldbTelemeter() = default; + + // Invoked upon process exit + virtual void LogProcessExit(int status, llvm::StringRef exit_string, + llvm::telemetry::EventStats stats, + Target *target_ptr) = 0; + + // Invoked upon loading the main executable module + // We log in a fire-n-forget fashion so that if the load + // crashes, we don't lose the entry. + virtual void + LogMainExecutableLoadStart(lldb::ModuleSP exec_mod, + llvm::telemetry::EventStats stats) = 0; + virtual void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod, + llvm::telemetry::EventStats stats) = 0; + + // Invoked for each command + // We log in a fire-n-forget fashion so that if the command execution + // crashes, we don't lose the entry. + virtual void LogCommandStart(llvm::StringRef uuid, + llvm::StringRef original_command, + llvm::telemetry::EventStats stats, + Target *target_ptr) = 0; + virtual void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name, + llvm::StringRef command_args, + llvm::telemetry::EventStats stats, + Target *target_ptr, + CommandReturnObject *result) = 0; + + virtual std::string GetNextUUID() = 0; + + // For client (eg., SB API) to send telemetry entries. + virtual void LogClientTelemetry(const llvm::json::Object &entry) = 0; +}; + +// Logger configs: LLDB users can also supply their own configs via: +// $HOME/.lldb_telemetry_config +// +// We can propose simple syntax: +// Eg., +// enable_telemetry:true +// destination:stdout +// destination:stderr +// destination:/path/to/some/file +// +// The allowed field_name values are: +// * enable_telemetry +// If the fields are specified more than once, the last line will take +// precedence If enable_logging is set to false, no logging will occur. +// * destination. +// This is allowed to be specified multiple times - it will add to the +// default (ie, specified by vendor) list of destinations. +// The value can be either: +// + one of the two magic values "stdout" or "stderr". +// + a path to a local file +// !!NOTE!!: We decided to use a separate file instead of the existing settings +// file because that file is parsed too late in the process and by the +// there might have been lots of telemetry-entries that need to be +// sent already. +// This approach avoid losing log entries if LLDB crashes during init. +llvm::telemetry::Config *GetTelemetryConfig(); + +} // namespace lldb_private +#endif // LLDB_CORE_TELEMETRY_H diff --git a/lldb/include/lldb/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h index 7bfde8b9de127..3ff3aecec783e 100644 --- a/lldb/include/lldb/lldb-enumerations.h +++ b/lldb/include/lldb/lldb-enumerations.h @@ -257,8 +257,8 @@ enum StopReason { }; /// Command Return Status Types. -enum ReturnStatus { - eReturnStatusInvalid, +enum ReturnStatus : int { + eReturnStatusInvalid = 0, eReturnStatusSuccessFinishNoResult, eReturnStatusSuccessFinishResult, eReturnStatusSuccessContinuingNoResult, diff --git a/lldb/source/API/SBDebugger.cpp b/lldb/source/API/SBDebugger.cpp index 72501570320d5..950a84cbb53f1 100644 --- a/lldb/source/API/SBDebugger.cpp +++ b/lldb/source/API/SBDebugger.cpp @@ -972,6 +972,16 @@ SBTarget SBDebugger::GetDummyTarget() { return sb_target; } +void SBDebugger::SendTelemetry(const llvm::json::Object &entry) { + if (lldb_private::Debugger *debugger = this->get()) { + debugger->SendClientTelemetry(entry); + } else { + Log *log = GetLog(LLDBLog::API); + LLDB_LOGF(log, + "Could not send telemetry from SBDebugger - debugger was null."); + } +} + bool SBDebugger::DeleteTarget(lldb::SBTarget &target) { LLDB_INSTRUMENT_VA(this, target); diff --git a/lldb/source/Core/CMakeLists.txt b/lldb/source/Core/CMakeLists.txt index dbc620b91b1ed..4a02f7f1fc85e 100644 --- a/lldb/source/Core/CMakeLists.txt +++ b/lldb/source/Core/CMakeLists.txt @@ -51,6 +51,7 @@ add_lldb_library(lldbCore Section.cpp SourceLocationSpec.cpp SourceManager.cpp + Telemetry.cpp StreamAsynchronousIO.cpp ThreadedCommunication.cpp UserSettingsController.cpp @@ -94,6 +95,7 @@ add_lldb_library(lldbCore Support Demangle TargetParser + Telemetry ) add_dependencies(lldbCore diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp index 1266355578e32..167a49f9ee8fc 100644 --- a/lldb/source/Core/Debugger.cpp +++ b/lldb/source/Core/Debugger.cpp @@ -69,6 +69,7 @@ #include "llvm/Support/Threading.h" #include "llvm/Support/raw_ostream.h" +#include #include #include #include @@ -733,12 +734,18 @@ void Debugger::InstanceInitialize() { DebuggerSP Debugger::CreateInstance(lldb::LogOutputCallback log_callback, void *baton) { + llvm::telemetry::SteadyTimePoint start_time = + std::chrono::steady_clock::now(); DebuggerSP debugger_sp(new Debugger(log_callback, baton)); if (g_debugger_list_ptr && g_debugger_list_mutex_ptr) { std::lock_guard guard(*g_debugger_list_mutex_ptr); g_debugger_list_ptr->push_back(debugger_sp); } debugger_sp->InstanceInitialize(); + llvm::telemetry::TelemetryInfo entry; + entry.Stats = {start_time, std::chrono::steady_clock::now()}; + debugger_sp->m_telemeter->logStartup(HostInfo::GetProgramFileSpec().GetPath(), + &entry); return debugger_sp; } @@ -860,7 +867,8 @@ Debugger::Debugger(lldb::LogOutputCallback log_callback, void *baton) m_sync_broadcaster(nullptr, "lldb.debugger.sync"), m_broadcaster(m_broadcaster_manager_sp, GetStaticBroadcasterClass().str()), - m_forward_listener_sp(), m_clear_once() { + m_forward_listener_sp(), m_clear_once(), + m_telemeter(LldbTelemeter::CreateInstance(this)) { // Initialize the debugger properties as early as possible as other parts of // LLDB will start querying them during construction. m_collection_sp->Initialize(g_debugger_properties); @@ -952,6 +960,8 @@ void Debugger::Clear() { // static void Debugger::Destroy(lldb::DebuggerSP &debugger_sp); // static void Debugger::Terminate(); llvm::call_once(m_clear_once, [this]() { + llvm::telemetry::SteadyTimePoint quit_start_time = + std::chrono::steady_clock::now(); ClearIOHandlers(); StopIOHandlerThread(); StopEventHandlerThread(); @@ -974,6 +984,12 @@ void Debugger::Clear() { if (Diagnostics::Enabled()) Diagnostics::Instance().RemoveCallback(m_diagnostics_callback_id); + + // Log the "quit" event (including stats on how long the teardown took) + // TBD: We *may* have to send off the log BEFORE the ClearIOHanders()? + llvm::telemetry::TelemetryInfo entry; + entry.Stats = {quit_start_time, std::chrono::steady_clock::now()}; + m_telemeter->logExit(HostInfo::GetProgramFileSpec().GetPath(), &entry); }); } @@ -2239,3 +2255,7 @@ llvm::ThreadPoolInterface &Debugger::GetThreadPool() { "Debugger::GetThreadPool called before Debugger::Initialize"); return *g_thread_pool; } + +void Debugger::SendClientTelemetry(const llvm::json::Object &entry) { + m_telemeter->LogClientTelemetry(entry); +} diff --git a/lldb/source/Core/Telemetry.cpp b/lldb/source/Core/Telemetry.cpp new file mode 100644 index 0000000000000..d1d0c010395ae --- /dev/null +++ b/lldb/source/Core/Telemetry.cpp @@ -0,0 +1,722 @@ + +//===-- Telemetry.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 "lldb/Core/Telemetry.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lldb/API/SBDebugger.h" +#include "lldb/API/SBProcess.h" +#include "lldb/Core/Debugger.h" +#include "lldb/Core/Module.h" +#include "lldb/Host/FileSystem.h" +#include "lldb/Host/HostInfo.h" +#include "lldb/Interpreter/CommandInterpreter.h" +#include "lldb/Target/Process.h" +#include "lldb/Target/Statistics.h" +#include "lldb/Utility/ConstString.h" +#include "lldb/Utility/FileSpec.h" +#include "lldb/Utility/LLDBLog.h" +#include "lldb/Utility/UUID.h" +#include "lldb/Version/Version.h" +#include "lldb/lldb-enumerations.h" +#include "lldb/lldb-forward.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/Twine.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/LineIterator.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/RandomNumberGenerator.h" +#include "llvm/Support/raw_ostream.h" +#include "llvm/Telemetry/Telemetry.h" + +#ifdef HAS_VENDOR_TELEMETRY_PLUGINS +// TODO: could make this path a build-variable rather than hard-coded header +// path +#include "lldb/Core/VendorTelemetryPlugin.h" + +namespace vendor_specific { + +// Set any additional configurations, as needed. +extern void ApplyVendorSpecificConfigs( + llvm::telemetry::TelemetryConfig *config) /* __attribute__((weak))*/; + +// Return a copy of the given entry, but with certain fields that are deemed +// PII risk removed. +extern std::shared_ptr SanitizeSensitiveFields( + const llvm::telemetry::TelemetryInfo *entry) /*__attribute__((weak))*/; + +extern std::shared_ptr +CreateVendorSpecificTelemeter( + llvm::telemetry::TelemetryConfig *config) /*__attribute__((weak))*/; +} // namespace vendor_specific +#endif + +namespace lldb_private { + +using ::llvm::telemetry::Destination; +using ::llvm::telemetry::EventStats; +using ::llvm::telemetry::ExitDescription; +using ::llvm::telemetry::SteadyTimePoint; +using ::llvm::telemetry::TelemetryInfo; + +static std::string ExitDescToString(const ExitDescription *desc) { + return ("ExitCode:" + desc->ExitCode) + + (" ExixitDescription: " + desc->Description); +} + +static std::string GetDuration(const EventStats &stats) { + if (stats.End.has_value()) + return std::to_string((stats.End.value() - stats.Start).count()) + + "(nanosec)"; + return ""; +} + +std::string LldbBaseTelemetryInfo::ToString() const { + return ("[LldbBaseTelemetryInfo]\n") + (" SessionId: " + SessionId + "\n"); +} + +std::string DebuggerTelemetryInfo::ToString() const { + std::string duration_desc = + (ExitDesc.has_value() ? " lldb session duration: " + : " lldb startup duration: ") + + std::to_string((Stats.End.value() - Stats.Start).count()) + "(nanosec)\n"; + + return LldbBaseTelemetryInfo::ToString() + "\n" + + ("[DebuggerTelemetryInfo]\n") + (" username: " + username + "\n") + + (" lldb_git_sha: " + lldb_git_sha + "\n") + + (" lldb_path: " + lldb_path + "\n") + (" cwd: " + cwd + "\n") + + duration_desc + "\n"; +} + +static size_t ToNanosecOrZero(const std::optional &Point) { + if (!Point.has_value()) + return 0; + + return Point.value().time_since_epoch().count(); +} + +llvm::json::Object DebuggerTelemetryInfo::serializeToJson() const { + return llvm::json::Object{ + {"DebuggerInfo", + { + {"SessionId", SessionId}, + {"username", username}, + {"lldb_git_sha", lldb_git_sha}, + {"lldb_path", lldb_path}, + {"cwd", cwd}, + { + "EventStats", + { + {"Start", Stats.Start.time_since_epoch().count()}, + {"End", ToNanosecOrZero(Stats.End)}, + }, + }, + // TODO: fill in more? + }}}; +} + +std::string ClientTelemetryInfo::ToString() const { + return LldbBaseTelemetryInfo::ToString() + "\n" + + ("[DapRequestInfoEntry]\n") + + (" request_name: " + request_name + "\n") + + (" request_duration: " + GetDuration(Stats) + "(nanosec)\n") + + (" error_msg: " + error_msg + "\n"); +} + +llvm::json::Object ClientTelemetryInfo::serializeToJson() const { + return llvm::json::Object{ + {"ClientInfo", + { + {"SessionId", SessionId}, + {"request_name", request_name}, + {"error_msg", error_msg}, + { + "EventStats", + { + {"Start", Stats.Start.time_since_epoch().count()}, + {"End", ToNanosecOrZero(Stats.End)}, + }, + }, + }}}; +} + +std::string TargetTelemetryInfo::ToString() const { + std::string exit_or_load_desc; + if (ExitDesc.has_value()) { + // If this entry was emitted for an exit + exit_or_load_desc = " process_duration: " + GetDuration(Stats) + + ExitDescToString(&(ExitDesc.value())) + "\n"; + } else { + // This was emitted for a load event. + // See if it was the start-load or end-load entry + if (Stats.End.has_value()) { + exit_or_load_desc = + " startup_init_duration: " + GetDuration(Stats) + "\n"; + } else { + exit_or_load_desc = " startup_init_start\n"; + } + } + return LldbBaseTelemetryInfo::ToString() + "\n" + + ("[TargetTelemetryInfo]\n") + + (" target_uuid: " + target_uuid + "\n") + + (" file_format: " + file_format + "\n") + + (" binary_path: " + binary_path + "\n") + + (" binary_size: " + std::to_string(binary_size) + "\n") + + exit_or_load_desc; +} + +llvm::json::Object TargetTelemetryInfo::serializeToJson() const { + return llvm::json::Object{{ + "TargetInfo", + { + {"SessionId", SessionId}, + {"target_uuid", target_uuid}, + {"binary_path", binary_path}, + {"binary_size", binary_size}, + // TODO: fill in more + }, + }}; +} + +std::string CommandTelemetryInfo::ToString() const { + // Whether this entry was emitted at the start or at the end of the + // command-execution. + if (Stats.End.has_value()) { + return LldbBaseTelemetryInfo::ToString() + "\n" + + ("[CommandTelemetryInfo] - END\n") + + (" target_uuid: " + target_uuid + "\n") + + (" command_uuid: " + command_uuid + "\n") + + (" command_name: " + command_name + "\n") + + (" args: " + args + "\n") + + (" command_runtime: " + GetDuration(Stats) + "\n") + + (ExitDesc.has_value() ? ExitDescToString(&(ExitDesc.value())) + : "no exit-description") + + "\n"; + } else { + return LldbBaseTelemetryInfo::ToString() + "\n" + + ("[CommandTelemetryInfo] - START\n") + + (" target_uuid: " + target_uuid + "\n") + + (" command_uuid: " + command_uuid + "\n") + + (" original_command: " + original_command + "\n"); + } +} + +llvm::json::Object CommandTelemetryInfo::serializeToJson() const { + llvm::json::Object inner; + + inner.insert({"SessionId", SessionId}); + inner.insert({"target_uuid", target_uuid}); + inner.insert({"command_uuid", command_uuid}); + inner.insert({"args", args}); + inner.insert({"original_command", original_command}); + inner.insert({ + "EventStats", + { + {"Start", Stats.Start.time_since_epoch().count()}, + {"End", ToNanosecOrZero(Stats.End)}, + }, + }); + + // If this entry was emitted at the end of the command-execution, + // then calculate the runtime too. + if (Stats.End.has_value()) { + inner.insert( + {"command_runtime", (Stats.End.value() - Stats.Start).count()}); + if (ExitDesc.has_value()) { + inner.insert({"exit_code", ExitDesc->ExitCode}); + inner.insert({"exit_msg", ExitDesc->Description}); + inner.insert({"return_status", static_cast(ret_status)}); + } + } + + return llvm::json::Object{{"CommandInfo", std::move(inner)}}; +} + +std::string MiscTelemetryInfo::ToString() const { + std::string ret; + llvm::raw_string_ostream ret_strm(ret); + ret_strm << LldbBaseTelemetryInfo::ToString() << "\n[MiscTelemetryInfo]\n" + << " target_uuid: " << target_uuid + "\n" + << " meta_data:\n"; + for (const auto &kv : meta_data) { + ret_strm << " " << kv.first << ": " << kv.second << "\n"; + } + return ret; +} + +llvm::json::Object MiscTelemetryInfo::serializeToJson() const { + llvm::json::Object meta_data_obj; + + for (const auto &kv : meta_data) + meta_data_obj.insert({kv.first, kv.second}); + + return llvm::json::Object{{ + "MiscInfo", + { + {"SessionId", SessionId}, + {"target_uuid", target_uuid}, + {"meta_data", std::move(meta_data_obj)}, + }, + }}; +} + +class StreamTelemetryDestination : public Destination { +public: + StreamTelemetryDestination(llvm::raw_ostream &os, std::string desc) + : os(os), desc(desc) {} + llvm::Error emitEntry(const llvm::telemetry::TelemetryInfo *entry) override { + // Unless there exists a custom (vendor-defined) data-cleanup + // for printing, upstream Telemetry should not leak anything other than the + // basic. +#ifdef HAS_TELEMETRY_FIELDS_PRINTER + os << SanitizeSensitiveFields(entry)->ToString() << "\n"; +#else + os << "session_uuid: " << entry->SessionId + << "\n"; +#endif + os.flush(); + return llvm::ErrorSuccess(); + } + + std::string name() const override { return desc; } + +private: + llvm::raw_ostream &os; + const std::string desc; +}; + +// No-op logger to use when users disable telemetry +class NoOpTelemeter : public LldbTelemeter { +public: + static std::unique_ptr CreateInstance(Debugger *debugger) { + return std::unique_ptr(new NoOpTelemeter(debugger)); + } + + NoOpTelemeter(Debugger *debugger) {} + void logStartup(llvm::StringRef tool_path, TelemetryInfo *entry) override {} + void logExit(llvm::StringRef tool_path, TelemetryInfo *entry) override {} + + void LogProcessExit(int status, llvm::StringRef exit_string, EventStats stats, + Target *target_ptr) override {} + void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod, + EventStats stats) override {} + void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod, + EventStats stats) override {} + + void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command, + EventStats stats, Target *target_ptr) override {} + void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name, + llvm::StringRef command_args, EventStats stats, + Target *target_ptr, CommandReturnObject *result) override { + } + + void LogClientTelemetry(const llvm::json::Object &entry) override {} + + void addDestination(llvm::telemetry::Destination *destination) override {} + std::string GetNextUUID() override { return ""; } +}; + +class BasicTelemeter : public LldbTelemeter { +public: + static std::unique_ptr CreateInstance(Debugger *); + + virtual ~BasicTelemeter() = default; + + void logStartup(llvm::StringRef lldb_path, TelemetryInfo *entry) override; + void logExit(llvm::StringRef lldb_path, TelemetryInfo *entry) override; + + void LogProcessExit(int status, llvm::StringRef exit_string, EventStats stats, + Target *target_ptr) override; + void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod, + EventStats stats) override; + void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod, + EventStats stats) override; + + void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command, + EventStats stats, Target *target_ptr) override; + void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name, + llvm::StringRef command_args, EventStats stats, + Target *target_ptr, CommandReturnObject *result) override; + + void LogClientTelemetry(const llvm::json::Object &entry) override; + + void addDestination(Destination *destination) override { + m_destinations.push_back(destination); + } + + std::string GetNextUUID() override { + return std::to_string(uuid_seed.fetch_add(1)); + } + +protected: + BasicTelemeter(Debugger *debugger); + + void CollectMiscBuildInfo(); + +private: + template EntrySubType MakeBaseEntry() { + EntrySubType entry; + entry.SessionId = m_session_uuid; + entry.Counter = counter.fetch_add(1); + return entry; + } + + void EmitToDestinations(const TelemetryInfo *entry); + + Debugger *m_debugger; + const std::string m_session_uuid; + std::string startup_lldb_path; + + // counting number of entries. + std::atomic counter = 0; + + std::vector m_destinations; + + std::atomic uuid_seed = 0; +}; + +static std::string MakeUUID(lldb_private::Debugger *debugger) { + std::string ret; + uint8_t random_bytes[16]; + if (auto ec = llvm::getRandomBytes(random_bytes, 16)) { + LLDB_LOG(GetLog(LLDBLog::Object), + "Failed to generate random bytes for UUID: {0}", ec.message()); + // fallback to using timestamp + debugger ID. + ret = std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count()) + + "_" + std::to_string(debugger->GetID()); + } else { + ret = lldb_private::UUID(random_bytes).GetAsString(); + } + + return ret; +} + +BasicTelemeter::BasicTelemeter(lldb_private::Debugger *debugger) + : m_debugger(debugger), m_session_uuid(MakeUUID(debugger)) {} + +std::unique_ptr +BasicTelemeter::CreateInstance(lldb_private::Debugger *debugger) { + auto *config = GetTelemetryConfig(); + + BasicTelemeter *ins = new BasicTelemeter(debugger); + for (const std ::string &dest : config->AdditionalDestinations) { + if (dest == "stdout") { + ins->addDestination( + new StreamTelemetryDestination(llvm::outs(), "stdout")); + } else if (dest == "stderr") { + ins->addDestination( + new StreamTelemetryDestination(llvm::errs(), "stderr")); + } else { + // TODO: handle custom values as needed? + } + } + + return std::unique_ptr(ins); +} + +void BasicTelemeter::EmitToDestinations(const TelemetryInfo *entry) { + // TODO: can do this in a separate thread (need to own the ptrs!). + for (Destination *destination : m_destinations) { + llvm::Error err = destination->emitEntry(entry); + if (err) { + LLDB_LOG(GetLog(LLDBLog::Object), + "Error emitting to destination(name = {0})", + destination->name()); + } + } +} + +void BasicTelemeter::logStartup(llvm::StringRef lldb_path, + TelemetryInfo *entry) { + startup_lldb_path = lldb_path.str(); + lldb_private::DebuggerTelemetryInfo startup_info = + MakeBaseEntry(); + + UserIDResolver &resolver = lldb_private::HostInfo::GetUserIDResolver(); + std::optional opt_username = + resolver.GetUserName(lldb_private::HostInfo::GetUserID()); + if (opt_username) + startup_info.username = *opt_username; + + startup_info.lldb_git_sha = + lldb_private::GetVersion(); // TODO: find the real git sha + startup_info.lldb_path = startup_lldb_path; + startup_info.Stats = entry->Stats; + + llvm::SmallString<64> cwd; + if (!llvm::sys::fs::current_path(cwd)) { + startup_info.cwd = cwd.c_str(); + } else { + MiscTelemetryInfo misc_info = MakeBaseEntry(); + misc_info.meta_data["internal_errors"] = "Cannot determine CWD"; + EmitToDestinations(&misc_info); + } + + EmitToDestinations(&startup_info); + + // Optional part + CollectMiscBuildInfo(); +} + +void BasicTelemeter::logExit(llvm::StringRef lldb_path, TelemetryInfo *entry) { + // we should be shutting down the same instance that we started?! + // llvm::Assert(startup_lldb_path == lldb_path.str()); + + lldb_private::DebuggerTelemetryInfo exit_info = + MakeBaseEntry(); + exit_info.Stats = entry->Stats; + exit_info.lldb_path = startup_lldb_path; + if (auto *selected_target = + m_debugger->GetSelectedExecutionContext().GetTargetPtr()) { + if (!selected_target->IsDummyTarget()) { + const lldb::ProcessSP proc = selected_target->GetProcessSP(); + if (proc == nullptr) { + // no process has been launched yet. + exit_info.ExitDesc = {-1, "no process launched."}; + } else { + exit_info.ExitDesc = {proc->GetExitStatus(), ""}; + if (const char *description = proc->GetExitDescription()) + exit_info.ExitDesc->Description = std::string(description); + } + } + } + EmitToDestinations(&exit_info); +} + +void BasicTelemeter::LogProcessExit(int status, llvm::StringRef exit_string, + EventStats stats, Target *target_ptr) { + lldb_private::TargetTelemetryInfo exit_info = + MakeBaseEntry(); + exit_info.Stats = std::move(stats); + exit_info.target_uuid = + target_ptr && !target_ptr->IsDummyTarget() + ? target_ptr->GetExecutableModule()->GetUUID().GetAsString() + : ""; + exit_info.ExitDesc = {status, exit_string.str()}; + + EmitToDestinations(&exit_info); +} + +void BasicTelemeter::CollectMiscBuildInfo() { + // collecting use-case specific data +} + +void BasicTelemeter::LogMainExecutableLoadStart(lldb::ModuleSP exec_mod, + EventStats stats) { + TargetTelemetryInfo target_info = MakeBaseEntry(); + target_info.Stats = std::move(stats); + target_info.binary_path = + exec_mod->GetFileSpec().GetPathAsConstString().GetCString(); + target_info.file_format = exec_mod->GetArchitecture().GetArchitectureName(); + target_info.target_uuid = exec_mod->GetUUID().GetAsString(); + if (auto err = llvm::sys::fs::file_size(exec_mod->GetFileSpec().GetPath(), + target_info.binary_size)) { + // If there was error obtaining it, just reset the size to 0. + // Maybe log the error too? + target_info.binary_size = 0; + } + EmitToDestinations(&target_info); +} + +void BasicTelemeter::LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod, + EventStats stats) { + TargetTelemetryInfo target_info = MakeBaseEntry(); + target_info.Stats = std::move(stats); + target_info.binary_path = + exec_mod->GetFileSpec().GetPathAsConstString().GetCString(); + target_info.file_format = exec_mod->GetArchitecture().GetArchitectureName(); + target_info.target_uuid = exec_mod->GetUUID().GetAsString(); + target_info.binary_size = exec_mod->GetObjectFile()->GetByteSize(); + + EmitToDestinations(&target_info); + + // Collect some more info, might be useful? + MiscTelemetryInfo misc_info = MakeBaseEntry(); + misc_info.target_uuid = exec_mod->GetUUID().GetAsString(); + misc_info.meta_data["symtab_index_time"] = + std::to_string(exec_mod->GetSymtabIndexTime().get().count()); + misc_info.meta_data["symtab_parse_time"] = + std::to_string(exec_mod->GetSymtabParseTime().get().count()); + EmitToDestinations(&misc_info); +} + +void BasicTelemeter::LogClientTelemetry(const llvm::json::Object &entry) { + ClientTelemetryInfo client_info = MakeBaseEntry(); + + std::optional request_name = entry.getString("request_name"); + if (!request_name.has_value()) { + MiscTelemetryInfo misc_info = MakeBaseEntry(); + misc_info.meta_data["internal_errors"] = + "Cannot determine request name from client entry"; + // TODO: Dump the errornous entry to stderr too? + EmitToDestinations(&misc_info); + return; + } + client_info.request_name = request_name->str(); + + std::optional start_time = entry.getInteger("start_time"); + std::optional end_time = entry.getInteger("end_time"); + + if (!start_time.has_value() || !end_time.has_value()) { + MiscTelemetryInfo misc_info = MakeBaseEntry(); + misc_info.meta_data["internal_errors"] = + "Cannot determine start/end time from client entry"; + EmitToDestinations(&misc_info); + return; + } + + SteadyTimePoint epoch; + client_info.Stats.Start = + epoch + std::chrono::nanoseconds(static_cast(*start_time)); + client_info.Stats.End = + epoch + std::chrono::nanoseconds(static_cast(*end_time)); + + std::optional error_msg = entry.getString("error"); + if (error_msg.has_value()) + client_info.error_msg = error_msg->str(); + + EmitToDestinations(&client_info); +} + +void BasicTelemeter::LogCommandStart(llvm::StringRef uuid, + llvm::StringRef original_command, + EventStats stats, Target *target_ptr) { + + lldb_private::CommandTelemetryInfo command_info = + MakeBaseEntry(); + + // If we have a target attached to this command, then get the UUID. + command_info.target_uuid = ""; + if (target_ptr && target_ptr->GetExecutableModule() != nullptr) { + command_info.target_uuid = + target_ptr->GetExecutableModule()->GetUUID().GetAsString(); + } + command_info.command_uuid = uuid.str(); + command_info.original_command = original_command.str(); + command_info.Stats = std::move(stats); + + EmitToDestinations(&command_info); +} + +void BasicTelemeter::LogCommandEnd(llvm::StringRef uuid, + llvm::StringRef command_name, + llvm::StringRef command_args, + EventStats stats, Target *target_ptr, + CommandReturnObject *result) { + + lldb_private::CommandTelemetryInfo command_info = + MakeBaseEntry(); + + // If we have a target attached to this command, then get the UUID. + command_info.target_uuid = ""; + if (target_ptr && target_ptr->GetExecutableModule() != nullptr) { + command_info.target_uuid = + target_ptr->GetExecutableModule()->GetUUID().GetAsString(); + } + command_info.command_uuid = uuid.str(); + command_info.command_name = command_name.str(); + command_info.args = command_args.str(); + command_info.Stats = std::move(stats); + command_info.ExitDesc = {result->Succeeded() ? 0 : -1, ""}; + if (llvm::StringRef error_data = result->GetErrorData(); + !error_data.empty()) { + command_info.ExitDesc->Description = error_data.str(); + } + command_info.ret_status = result->GetStatus(); + EmitToDestinations(&command_info); +} + +llvm::StringRef parse_value(llvm::StringRef str, llvm::StringRef label) { + return str.substr(label.size()).trim(); +} + +bool parse_field(llvm::StringRef str, llvm::StringRef label) { + if (parse_value(str, label) == "true") + return true; + return false; +} + +llvm::telemetry::Config *MakeTelemetryConfig() { + bool enable_telemetry = false; + std::vector additional_destinations; + + // Look in the $HOME/.lldb_telemetry_config file to populate the struct + llvm::SmallString<64> init_file; + FileSystem::Instance().GetHomeDirectory(init_file); + llvm::sys::path::append(init_file, ".lldb_telemetry_config"); + FileSystem::Instance().Resolve(init_file); + if (llvm::sys::fs::exists(init_file)) { + auto contents = llvm::MemoryBuffer::getFile(init_file, /*IsText*/ true); + if (contents) { + llvm::line_iterator iter = + llvm::line_iterator(contents->get()->getMemBufferRef()); + for (; !iter.is_at_eof(); ++iter) { + if (iter->starts_with("enable_telemetry:")) { + enable_telemetry = parse_field(*iter, "enable_telemetry:"); + } else if (iter->starts_with("destination:")) { + llvm::StringRef dest = parse_value(*iter, "destination:"); + if (dest == "stdout") { + additional_destinations.push_back("stdout"); + } else if (dest == "stderr") { + additional_destinations.push_back("stderr"); + } else { + additional_destinations.push_back(dest.str()); + } + } + } + } else { + LLDB_LOG(GetLog(LLDBLog::Object), "Error reading config file at {0}", + init_file.c_str()); + } + } + + auto *ret = + new llvm::telemetry::Config{enable_telemetry, additional_destinations}; +#ifdef HAS_VENDOR_TELEMETRY_PLUGINS + vendor_specific::ApplyVendorSpecificConfigs(ret); +#endif + return ret; +} + +llvm::telemetry::Config *GetTelemetryConfig() { + static llvm::telemetry::Config *config = MakeTelemetryConfig(); + return config; +} + +std::unique_ptr +LldbTelemeter::CreateInstance(lldb_private::Debugger *debugger) { + auto *config = GetTelemetryConfig(); + if (!config->EnableTelemetry) { + return NoOpTelemeter::CreateInstance(debugger); + } + +#ifdef HAS_VENDOR_TELEMETRY_PLUGINS + return vendor_specific::CreateVendorSpecificTelemeter(config); +#else + return BasicTelemeter::CreateInstance(debugger); +#endif +} +} // namespace lldb_private diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp index df539d5f5bcee..05b399ceab9a4 100644 --- a/lldb/source/Interpreter/CommandInterpreter.cpp +++ b/lldb/source/Interpreter/CommandInterpreter.cpp @@ -1886,8 +1886,34 @@ bool CommandInterpreter::HandleCommand(const char *command_line, LazyBool lazy_add_to_history, CommandReturnObject &result, bool force_repeat_command) { + llvm::telemetry::EventStats start_command_stats( + std::chrono::steady_clock::now()); + LldbTelemeter *telemeter = GetDebugger().GetTelemeter(); + // Generate a UUID for this command so the logger can match + // the start/end entries correctly. + const std::string command_uuid = telemeter->GetNextUUID(); + + telemeter->LogCommandStart(command_uuid, command_line, start_command_stats, + GetExecutionContext().GetTargetPtr()); + std::string command_string(command_line); std::string original_command_string(command_line); + std::string parsed_command_args; + CommandObject *cmd_obj = nullptr; + + auto log_on_exit = llvm::make_scope_exit([&]() { + llvm::telemetry::EventStats end_command_stats( + start_command_stats.Start, std::chrono::steady_clock::now()); + + llvm::StringRef command_name = + cmd_obj ? cmd_obj->GetCommandName() : ""; + // TODO: this is logging the time the command-handler finishes. + // But we may want a finer-grain durations too? + // (ie., the execute_time recorded below?) + telemeter->LogCommandEnd(command_uuid, command_name, parsed_command_args, + end_command_stats, + GetExecutionContext().GetTargetPtr(), &result); + }); Log *log = GetLog(LLDBLog::Commands); llvm::PrettyStackTraceFormat stack_trace("HandleCommand(command = \"%s\")", @@ -1925,9 +1951,9 @@ bool CommandInterpreter::HandleCommand(const char *command_line, bool empty_command = false; bool comment_command = false; - if (command_string.empty()) + if (command_string.empty()) { empty_command = true; - else { + } else { const char *k_space_characters = "\t\n\v\f\r "; size_t non_space = command_string.find_first_not_of(k_space_characters); @@ -1992,7 +2018,7 @@ bool CommandInterpreter::HandleCommand(const char *command_line, // From 1 above, we can determine whether the Execute function wants raw // input or not. - CommandObject *cmd_obj = ResolveCommandImpl(command_string, result); + cmd_obj = ResolveCommandImpl(command_string, result); // We have to preprocess the whole command string for Raw commands, since we // don't know the structure of the command. For parsed commands, we only @@ -2053,30 +2079,29 @@ bool CommandInterpreter::HandleCommand(const char *command_line, if (add_to_history) m_command_history.AppendString(original_command_string); - std::string remainder; const std::size_t actual_cmd_name_len = cmd_obj->GetCommandName().size(); if (actual_cmd_name_len < command_string.length()) - remainder = command_string.substr(actual_cmd_name_len); + parsed_command_args = command_string.substr(actual_cmd_name_len); // Remove any initial spaces - size_t pos = remainder.find_first_not_of(k_white_space); + size_t pos = parsed_command_args.find_first_not_of(k_white_space); if (pos != 0 && pos != std::string::npos) - remainder.erase(0, pos); + parsed_command_args.erase(0, pos); LLDB_LOGF( log, "HandleCommand, command line after removing command name(s): '%s'", - remainder.c_str()); + parsed_command_args.c_str()); // To test whether or not transcript should be saved, `transcript_item` is // used instead of `GetSaveTrasncript()`. This is because the latter will // fail when the command is "settings set interpreter.save-transcript true". if (transcript_item) { transcript_item->AddStringItem("commandName", cmd_obj->GetCommandName()); - transcript_item->AddStringItem("commandArguments", remainder); + transcript_item->AddStringItem("commandArguments", parsed_command_args); } ElapsedTime elapsed(execute_time); - cmd_obj->Execute(remainder.c_str(), result); + cmd_obj->Execute(parsed_command_args.c_str(), result); } LLDB_LOGF(log, "HandleCommand, command %s", diff --git a/lldb/source/Target/Process.cpp b/lldb/source/Target/Process.cpp index 97ce2c14458e9..a21c5adf9b2f2 100644 --- a/lldb/source/Target/Process.cpp +++ b/lldb/source/Target/Process.cpp @@ -22,6 +22,7 @@ #include "lldb/Core/ModuleSpec.h" #include "lldb/Core/PluginManager.h" #include "lldb/Core/Progress.h" +#include "lldb/Core/Telemetry.h" #include "lldb/Expression/DiagnosticManager.h" #include "lldb/Expression/DynamicCheckerFunctions.h" #include "lldb/Expression/UserExpression.h" @@ -74,6 +75,8 @@ #include "lldb/Utility/SelectHelper.h" #include "lldb/Utility/State.h" #include "lldb/Utility/Timer.h" +#include "llvm/Telemetry/Telemetry.h" +#include using namespace lldb; using namespace lldb_private; @@ -1065,6 +1068,9 @@ bool Process::SetExitStatus(int status, llvm::StringRef exit_string) { // Use a mutex to protect setting the exit status. std::lock_guard guard(m_exit_status_mutex); + llvm::telemetry::SteadyTimePoint start_time = + std::chrono::steady_clock::now(); + Log *log(GetLog(LLDBLog::State | LLDBLog::Process)); LLDB_LOG(log, "(plugin = {0} status = {1} ({1:x8}), description=\"{2}\")", GetPluginName(), status, exit_string); @@ -1094,6 +1100,11 @@ bool Process::SetExitStatus(int status, llvm::StringRef exit_string) { // Allow subclasses to do some cleanup DidExit(); + llvm::telemetry::EventStats stats = {start_time, + std::chrono::steady_clock::now()}; + GetTarget().GetDebugger().GetTelemeter()->LogProcessExit(status, exit_string, + stats, &GetTarget()); + return true; } diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp index fcac0a48f46e6..6ff01d142a7cf 100644 --- a/lldb/source/Target/Target.cpp +++ b/lldb/source/Target/Target.cpp @@ -24,6 +24,7 @@ #include "lldb/Core/Section.h" #include "lldb/Core/SourceManager.h" #include "lldb/Core/StructuredDataImpl.h" +#include "lldb/Core/Telemetry.h" #include "lldb/Core/ValueObject.h" #include "lldb/Core/ValueObjectConstResult.h" #include "lldb/Expression/DiagnosticManager.h" @@ -67,7 +68,9 @@ #include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/SetVector.h" +#include "llvm/Telemetry/Telemetry.h" +#include #include #include #include @@ -1477,9 +1480,21 @@ void Target::DidExec() { void Target::SetExecutableModule(ModuleSP &executable_sp, LoadDependentFiles load_dependent_files) { + llvm::telemetry::EventStats load_executable_stats( + std::chrono::steady_clock::now()); Log *log = GetLog(LLDBLog::Target); ClearModules(false); + if (executable_sp) { + m_debugger.GetTelemeter()->LogMainExecutableLoadStart( + executable_sp, load_executable_stats); + + auto log_on_exit = llvm::make_scope_exit([&]() { + load_executable_stats.End = std::chrono::steady_clock::now(); + m_debugger.GetTelemeter()->LogMainExecutableLoadEnd( + executable_sp, load_executable_stats); + }); + } if (executable_sp) { ElapsedTime elapsed(m_stats.GetCreateTime()); LLDB_SCOPED_TIMERF("Target::SetExecutableModule (executable = '%s')", diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp index 6012ee52110b7..eb4be3ac64aa1 100644 --- a/lldb/tools/lldb-dap/DAP.cpp +++ b/lldb/tools/lldb-dap/DAP.cpp @@ -682,17 +682,32 @@ PacketStatus DAP::GetNextObject(llvm::json::Object &object) { } bool DAP::HandleObject(const llvm::json::Object &object) { + auto start_time = std::chrono::steady_clock::now(); const auto packet_type = GetString(object, "type"); if (packet_type == "request") { const auto command = GetString(object, "command"); auto handler_pos = request_handlers.find(std::string(command)); + llvm::json::Object telemetry_entry; + telemetry_entry.insert({"request_name", std::string(command)}); + telemetry_entry.insert( + {"start_time", start_time.time_since_epoch().count()}); + if (handler_pos != request_handlers.end()) { handler_pos->second(object); + auto end_time = std::chrono::steady_clock::now(); + telemetry_entry.insert({"end_time", end_time.time_since_epoch().count()}); + debugger.SendTelemetry(telemetry_entry); return true; // Success } else { if (log) *log << "error: unhandled command \"" << command.data() << "\"" << std::endl; + auto end_time = std::chrono::steady_clock::now(); + telemetry_entry.insert({"end_time", end_time.time_since_epoch().count()}); + telemetry_entry.insert( + {"error", "unhandled-command:" + std::string(command)}); + debugger.SendTelemetry(telemetry_entry); + debugger.SendTelemetry(telemetry_entry); return false; // Fail } } From 04b7da899d6544fac977321f6e9c00f14d42446e Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 17 Sep 2024 15:21:44 -0400 Subject: [PATCH 23/30] change the data type from json to structureddata --- lldb/include/lldb/API/SBDebugger.h | 3 +-- lldb/include/lldb/Core/Debugger.h | 4 ++-- lldb/include/lldb/Core/Telemetry.h | 4 +++- lldb/source/API/SBDebugger.cpp | 4 ++-- lldb/source/Core/Debugger.cpp | 3 ++- lldb/source/Core/Telemetry.cpp | 13 ++++++---- lldb/tools/lldb-dap/DAP.cpp | 38 +++++++++++++++++++++++------- 7 files changed, 48 insertions(+), 21 deletions(-) diff --git a/lldb/include/lldb/API/SBDebugger.h b/lldb/include/lldb/API/SBDebugger.h index 7ec69dc89b3f3..b12c4623de285 100644 --- a/lldb/include/lldb/API/SBDebugger.h +++ b/lldb/include/lldb/API/SBDebugger.h @@ -14,7 +14,6 @@ #include "lldb/API/SBDefines.h" #include "lldb/API/SBPlatform.h" #include "lldb/API/SBStructuredData.h" -#include "llvm/Support/JSON.h" namespace lldb_private { class CommandPluginInterfaceImplementation; @@ -247,7 +246,7 @@ class LLDB_API SBDebugger { lldb::SBTarget GetDummyTarget(); - void SendTelemetry(const llvm::json::Object &entry); + void SendTelemetry(const lldb::SBStructuredData &entry); // Return true if target is deleted from the target list of the debugger. bool DeleteTarget(lldb::SBTarget &target); diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h index d0e40e415c097..7a500aa9cd787 100644 --- a/lldb/include/lldb/Core/Debugger.h +++ b/lldb/include/lldb/Core/Debugger.h @@ -19,6 +19,7 @@ #include "lldb/Core/FormatEntity.h" #include "lldb/Core/IOHandler.h" #include "lldb/Core/SourceManager.h" +#include "lldb/Core/StructuredDataImpl.h" #include "lldb/Core/Telemetry.h" #include "lldb/Core/UserSettingsController.h" #include "lldb/Host/HostThread.h" @@ -47,7 +48,6 @@ #include "llvm/ADT/StringRef.h" #include "llvm/Support/DynamicLibrary.h" #include "llvm/Support/FormatVariadic.h" -#include "llvm/Support/JSON.h" #include "llvm/Support/Threading.h" #include "llvm/Telemetry/Telemetry.h" @@ -155,7 +155,7 @@ class Debugger : public std::enable_shared_from_this, LldbTelemeter *GetTelemeter() { return m_telemeter.get(); } - void SendClientTelemetry(const llvm::json::Object &entry); + void SendClientTelemetry(const lldb_private::StructuredDataImpl &entry); Status SetInputString(const char *data); diff --git a/lldb/include/lldb/Core/Telemetry.h b/lldb/include/lldb/Core/Telemetry.h index bc07369722a12..61cdca59f31fc 100644 --- a/lldb/include/lldb/Core/Telemetry.h +++ b/lldb/include/lldb/Core/Telemetry.h @@ -17,6 +17,7 @@ #include #include +#include "lldb/Core/StructuredDataImpl.h" #include "lldb/Interpreter/CommandReturnObject.h" #include "lldb/Utility/StructuredData.h" #include "lldb/lldb-forward.h" @@ -258,7 +259,8 @@ class LldbTelemeter : public llvm::telemetry::Telemeter { virtual std::string GetNextUUID() = 0; // For client (eg., SB API) to send telemetry entries. - virtual void LogClientTelemetry(const llvm::json::Object &entry) = 0; + virtual void + LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) = 0; }; // Logger configs: LLDB users can also supply their own configs via: diff --git a/lldb/source/API/SBDebugger.cpp b/lldb/source/API/SBDebugger.cpp index 950a84cbb53f1..d9cfaf3c51470 100644 --- a/lldb/source/API/SBDebugger.cpp +++ b/lldb/source/API/SBDebugger.cpp @@ -972,9 +972,9 @@ SBTarget SBDebugger::GetDummyTarget() { return sb_target; } -void SBDebugger::SendTelemetry(const llvm::json::Object &entry) { +void SBDebugger::SendTelemetry(const lldb::SBStructuredData &entry) { if (lldb_private::Debugger *debugger = this->get()) { - debugger->SendClientTelemetry(entry); + debugger->SendClientTelemetry(*(entry.m_impl_up.get())); } else { Log *log = GetLog(LLDBLog::API); LLDB_LOGF(log, diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp index 167a49f9ee8fc..b1cfca0653938 100644 --- a/lldb/source/Core/Debugger.cpp +++ b/lldb/source/Core/Debugger.cpp @@ -2256,6 +2256,7 @@ llvm::ThreadPoolInterface &Debugger::GetThreadPool() { return *g_thread_pool; } -void Debugger::SendClientTelemetry(const llvm::json::Object &entry) { +void Debugger::SendClientTelemetry( + const lldb_private::StructuredDataImpl &entry) { m_telemeter->LogClientTelemetry(entry); } diff --git a/lldb/source/Core/Telemetry.cpp b/lldb/source/Core/Telemetry.cpp index d1d0c010395ae..795065a0e25e0 100644 --- a/lldb/source/Core/Telemetry.cpp +++ b/lldb/source/Core/Telemetry.cpp @@ -332,7 +332,8 @@ class NoOpTelemeter : public LldbTelemeter { Target *target_ptr, CommandReturnObject *result) override { } - void LogClientTelemetry(const llvm::json::Object &entry) override {} + void + LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override {} void addDestination(llvm::telemetry::Destination *destination) override {} std::string GetNextUUID() override { return ""; } @@ -360,7 +361,8 @@ class BasicTelemeter : public LldbTelemeter { llvm::StringRef command_args, EventStats stats, Target *target_ptr, CommandReturnObject *result) override; - void LogClientTelemetry(const llvm::json::Object &entry) override; + void + LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override; void addDestination(Destination *destination) override { m_destinations.push_back(destination); @@ -563,9 +565,11 @@ void BasicTelemeter::LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod, EmitToDestinations(&misc_info); } -void BasicTelemeter::LogClientTelemetry(const llvm::json::Object &entry) { +void BasicTelemeter::LogClientTelemetry( + const lldb_private::StructuredDataImpl &entry) { + // TODO: pull the dictionary out of entry ClientTelemetryInfo client_info = MakeBaseEntry(); - + /* std::optional request_name = entry.getString("request_name"); if (!request_name.has_value()) { MiscTelemetryInfo misc_info = MakeBaseEntry(); @@ -597,6 +601,7 @@ void BasicTelemeter::LogClientTelemetry(const llvm::json::Object &entry) { std::optional error_msg = entry.getString("error"); if (error_msg.has_value()) client_info.error_msg = error_msg->str(); + */ EmitToDestinations(&client_info); } diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp index eb4be3ac64aa1..584cac90b2143 100644 --- a/lldb/tools/lldb-dap/DAP.cpp +++ b/lldb/tools/lldb-dap/DAP.cpp @@ -13,6 +13,7 @@ #include #include "DAP.h" +#include "JSONUtils.h" #include "LLDBUtils.h" #include "lldb/API/SBCommandInterpreter.h" #include "llvm/ADT/StringExtras.h" @@ -687,15 +688,30 @@ bool DAP::HandleObject(const llvm::json::Object &object) { if (packet_type == "request") { const auto command = GetString(object, "command"); auto handler_pos = request_handlers.find(std::string(command)); - llvm::json::Object telemetry_entry; - telemetry_entry.insert({"request_name", std::string(command)}); - telemetry_entry.insert( - {"start_time", start_time.time_since_epoch().count()}); + lldb::SBStructuredData telemetry_entry; + + // There does not seem to be a direct way to construct an SBStructuredData. + // So we first create a json::Array object, + // then we serialize it to a string, + // and finally call SBStructuredData::SetFromJSON(string). + // + // TODO: This seems unnecessarily complex. Ideally, we should be able to + // just send a json::Object directly? Does the SB API allow json? + // + llvm::json::Array telemetry_array({ + {"request_name", std::string(command)}, + {"start_time", start_time.time_since_epoch().count()}, + }); if (handler_pos != request_handlers.end()) { handler_pos->second(object); auto end_time = std::chrono::steady_clock::now(); - telemetry_entry.insert({"end_time", end_time.time_since_epoch().count()}); + telemetry_array.push_back( + llvm::json::Value{"end_time", end_time.time_since_epoch().count()}); + + llvm::json::Value val(std::move(telemetry_array)); + std::string string_rep = lldb_dap::JSONToString(val); + telemetry_entry.SetFromJSON(string_rep.c_str()); debugger.SendTelemetry(telemetry_entry); return true; // Success } else { @@ -703,10 +719,14 @@ bool DAP::HandleObject(const llvm::json::Object &object) { *log << "error: unhandled command \"" << command.data() << "\"" << std::endl; auto end_time = std::chrono::steady_clock::now(); - telemetry_entry.insert({"end_time", end_time.time_since_epoch().count()}); - telemetry_entry.insert( - {"error", "unhandled-command:" + std::string(command)}); - debugger.SendTelemetry(telemetry_entry); + telemetry_array.push_back( + llvm::json::Value{"end_time", end_time.time_since_epoch().count()}); + telemetry_array.push_back(llvm::json::Value{ + "error", llvm::Twine("unhandled-command:" + command).str()}); + + llvm::json::Value val(std::move(telemetry_array)); + std::string string_rep = lldb_dap::JSONToString(val); + telemetry_entry.SetFromJSON(string_rep.c_str()); debugger.SendTelemetry(telemetry_entry); return false; // Fail } From 0e49b932f3b92b3ae6744a3b86499c95b70f97cf Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Fri, 20 Sep 2024 10:55:55 -0400 Subject: [PATCH 24/30] Rework the default impl of Telemetry in LLDB as "Plugin" to allow for testing --- lldb/include/lldb/Core/PluginManager.h | 9 + lldb/include/lldb/Core/Telemetry.h | 90 ++-- lldb/include/lldb/Core/TelemetryVendor.h | 46 ++ lldb/include/lldb/lldb-forward.h | 1 + lldb/include/lldb/lldb-private-interfaces.h | 1 + lldb/source/Core/CMakeLists.txt | 1 + lldb/source/Core/PluginManager.cpp | 27 + lldb/source/Core/Telemetry.cpp | 476 +---------------- lldb/source/Core/TelemetryVendor.cpp | 546 ++++++++++++++++++++ lldb/test/CMakeLists.txt | 3 + 10 files changed, 695 insertions(+), 505 deletions(-) create mode 100644 lldb/include/lldb/Core/TelemetryVendor.h create mode 100644 lldb/source/Core/TelemetryVendor.cpp diff --git a/lldb/include/lldb/Core/PluginManager.h b/lldb/include/lldb/Core/PluginManager.h index e4e0c3eea67f8..f17104cfdc796 100644 --- a/lldb/include/lldb/Core/PluginManager.h +++ b/lldb/include/lldb/Core/PluginManager.h @@ -379,6 +379,15 @@ class PluginManager { const UUID *uuid, const ArchSpec *arch); + // TelemetryVendor + static bool RegisterPlugin(llvm::StringRef name, llvm::StringRef description, + TelemetryVendorCreateInstance create_callback); + + static bool UnregisterPlugin(TelemetryVendorCreateInstance create_callback); + + static TelemetryVendorCreateInstance + GetTelemetryVendorCreateCallbackAtIndex(uint32_t idx); + // Trace static bool RegisterPlugin( llvm::StringRef name, llvm::StringRef description, diff --git a/lldb/include/lldb/Core/Telemetry.h b/lldb/include/lldb/Core/Telemetry.h index 61cdca59f31fc..3c6118414bb28 100644 --- a/lldb/include/lldb/Core/Telemetry.h +++ b/lldb/include/lldb/Core/Telemetry.h @@ -192,15 +192,15 @@ struct CommandTelemetryInfo : public LldbBaseTelemetryInfo { std::string ToString() const override; }; -// The "catch-all" entry to store a set of custom/non-standard -// data. +/// The "catch-all" entry to store a set of custom/non-standard +/// data. struct MiscTelemetryInfo : public LldbBaseTelemetryInfo { - // If the event is/can be associated with a target entry, - // this field contains that target's UUID. - // otherwise. + /// If the event is/can be associated with a target entry, + /// this field contains that target's UUID. + /// otherwise. std::string target_uuid; - // Set of key-value pairs for any optional (or impl-specific) data + /// Set of key-value pairs for any optional (or impl-specific) data std::unordered_map meta_data; MiscTelemetryInfo() = default; @@ -223,29 +223,39 @@ struct MiscTelemetryInfo : public LldbBaseTelemetryInfo { std::string ToString() const override; }; +/// The base Telemeter instance in LLDB. +/// This class declares additional instrumentation points +/// applicable to LLDB. class LldbTelemeter : public llvm::telemetry::Telemeter { public: - static std::unique_ptr CreateInstance(Debugger *); + /// Creates an instance of LldbTelemeter. + /// This uses the plugin registry to find an instance: + /// - If a vendor supplies a implementation, it will use it. + /// - If not, it will either return a no-op instance or a basic + /// implementation for testing. + /// + /// See also lldb_private::TelemetryVendor. + static std::unique_ptr CreateInstance(Debugger *debugger); virtual ~LldbTelemeter() = default; - // Invoked upon process exit + /// Invoked upon process exit virtual void LogProcessExit(int status, llvm::StringRef exit_string, llvm::telemetry::EventStats stats, Target *target_ptr) = 0; - // Invoked upon loading the main executable module - // We log in a fire-n-forget fashion so that if the load - // crashes, we don't lose the entry. + /// Invoked upon loading the main executable module + /// We log in a fire-n-forget fashion so that if the load + /// crashes, we don't lose the entry. virtual void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod, llvm::telemetry::EventStats stats) = 0; virtual void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod, llvm::telemetry::EventStats stats) = 0; - // Invoked for each command - // We log in a fire-n-forget fashion so that if the command execution - // crashes, we don't lose the entry. + /// Invoked for each command + /// We log in a fire-n-forget fashion so that if the command execution + /// crashes, we don't lose the entry. virtual void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command, llvm::telemetry::EventStats stats, @@ -258,36 +268,36 @@ class LldbTelemeter : public llvm::telemetry::Telemeter { virtual std::string GetNextUUID() = 0; - // For client (eg., SB API) to send telemetry entries. + /// For client (eg., SB API) to send telemetry entries. virtual void LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) = 0; }; -// Logger configs: LLDB users can also supply their own configs via: -// $HOME/.lldb_telemetry_config -// -// We can propose simple syntax: -// Eg., -// enable_telemetry:true -// destination:stdout -// destination:stderr -// destination:/path/to/some/file -// -// The allowed field_name values are: -// * enable_telemetry -// If the fields are specified more than once, the last line will take -// precedence If enable_logging is set to false, no logging will occur. -// * destination. -// This is allowed to be specified multiple times - it will add to the -// default (ie, specified by vendor) list of destinations. -// The value can be either: -// + one of the two magic values "stdout" or "stderr". -// + a path to a local file -// !!NOTE!!: We decided to use a separate file instead of the existing settings -// file because that file is parsed too late in the process and by the -// there might have been lots of telemetry-entries that need to be -// sent already. -// This approach avoid losing log entries if LLDB crashes during init. +/// Logger configs: LLDB users can also supply their own configs via: +/// $HOME/.lldb_telemetry_config +/// +/// We can propose simple syntax: +/// Eg., +/// enable_telemetry:true +/// destination:stdout +/// destination:stderr +/// destination:/path/to/some/file +/// +/// The allowed field_name values are: +/// * enable_telemetry +/// If the fields are specified more than once, the last line will take +/// precedence If enable_logging is set to false, no logging will occur. +/// * destination. +/// This is allowed to be specified multiple times - it will add to the +/// default (ie, specified by vendor) list of destinations. +/// The value can be either: +/// + one of the two magic values "stdout" or "stderr". +/// + a path to a local file +/// !!NOTE!!: We decided to use a separate file instead of the existing settings +/// file because that file is parsed too late in the process and by the +/// there might have been lots of telemetry-entries that need to be +/// sent already. +/// This approach avoid losing log entries if LLDB crashes during init. llvm::telemetry::Config *GetTelemetryConfig(); } // namespace lldb_private diff --git a/lldb/include/lldb/Core/TelemetryVendor.h b/lldb/include/lldb/Core/TelemetryVendor.h new file mode 100644 index 0000000000000..f8a57980db7ad --- /dev/null +++ b/lldb/include/lldb/Core/TelemetryVendor.h @@ -0,0 +1,46 @@ +//===-- TelemetryVendor.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 +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_CORE_TELEMETRYVENDOR_H +#define LLDB_CORE_TELEMETRYVENDOR_H + +#include "lldb/Core/PluginInterface.h" +#include "lldb/Core/Telemetry.h" +#include "llvm/Telemetry/Telemetry.h" + +#include + +namespace lldb_private { + +class TelemetryVendor : public PluginInterface { +public: + static TelemetryVendor *FindPlugin(); + + TelemetryVendor() = default; + + llvm::StringRef GetPluginName() override; + + std::unique_ptr GetTelemetryConfig(); + + // Creates an LldbTelemeter instance. + // Vendor plugins can override this to create customized instance as needed. + virtual std::unique_ptr + CreateTelemeter(lldb_private::Debugger *debugger); + + // TODO: move most of the basictelemeter concrete impl here to the plug in (to + // its .ccpp file that is) +protected: + // Returns a vendor-specific config which may or may not be the same as the + // given "default_config". Downstream implementation can define their + // configugrations in addition to OR overriding the default option. + virtual std::unique_ptr GetVendorSpecificConfig( + std::unique_ptr default_config); +}; + +} // namespace lldb_private +#endif // LLDB_CORE_TELEMETRYVENDOR_H diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h index 337eff696fcf3..6efc265870f68 100644 --- a/lldb/include/lldb/lldb-forward.h +++ b/lldb/include/lldb/lldb-forward.h @@ -234,6 +234,7 @@ class SystemRuntime; class Target; class TargetList; class TargetProperties; +class TelemetryVendor; class Thread; class ThreadCollection; class ThreadList; diff --git a/lldb/include/lldb/lldb-private-interfaces.h b/lldb/include/lldb/lldb-private-interfaces.h index b3c8cda899b95..dcf27a3e160fc 100644 --- a/lldb/include/lldb/lldb-private-interfaces.h +++ b/lldb/include/lldb/lldb-private-interfaces.h @@ -129,6 +129,7 @@ typedef bool (*ScriptedInterfaceCreateInstance)(lldb::ScriptLanguage language, ScriptedInterfaceUsages usages); typedef int (*ComparisonFunction)(const void *, const void *); typedef void (*DebuggerInitializeCallback)(Debugger &debugger); +typedef TelemetryVendor *(*TelemetryVendorCreateInstance)(); /// Trace /// \{ typedef llvm::Expected (*TraceCreateInstanceFromBundle)( diff --git a/lldb/source/Core/CMakeLists.txt b/lldb/source/Core/CMakeLists.txt index 4a02f7f1fc85e..0632c42e78c65 100644 --- a/lldb/source/Core/CMakeLists.txt +++ b/lldb/source/Core/CMakeLists.txt @@ -52,6 +52,7 @@ add_lldb_library(lldbCore SourceLocationSpec.cpp SourceManager.cpp Telemetry.cpp + TelemetryVendor.cpp StreamAsynchronousIO.cpp ThreadedCommunication.cpp UserSettingsController.cpp diff --git a/lldb/source/Core/PluginManager.cpp b/lldb/source/Core/PluginManager.cpp index fd5cb792c101a..7b3abb6868f80 100644 --- a/lldb/source/Core/PluginManager.cpp +++ b/lldb/source/Core/PluginManager.cpp @@ -1224,6 +1224,33 @@ FileSpec PluginManager::FindSymbolFileInBundle(const FileSpec &symfile_bundle, return {}; } +#pragma mark TelemetryVendor + +typedef PluginInstance TelemetryVendorInstance; +typedef PluginInstances TelemetryVendorInstances; + +static TelemetryVendorInstances &GetTelemetryVendorInstances() { + static TelemetryVendorInstances g_instances; + return g_instances; +} + +bool PluginManager::RegisterPlugin( + llvm::StringRef name, llvm::StringRef description, + TelemetryVendorCreateInstance create_callback) { + return GetTelemetryVendorInstances().RegisterPlugin(name, description, + create_callback); +} + +bool PluginManager::UnregisterPlugin( + TelemetryVendorCreateInstance create_callback) { + return GetTelemetryVendorInstances().UnregisterPlugin(create_callback); +} + +TelemetryVendorCreateInstance +PluginManager::GetTelemetryVendorCreateCallbackAtIndex(uint32_t idx) { + return GetTelemetryVendorInstances().GetCallbackAtIndex(idx); +} + #pragma mark Trace struct TraceInstance diff --git a/lldb/source/Core/Telemetry.cpp b/lldb/source/Core/Telemetry.cpp index 795065a0e25e0..9c4ab9dadc745 100644 --- a/lldb/source/Core/Telemetry.cpp +++ b/lldb/source/Core/Telemetry.cpp @@ -8,15 +8,11 @@ //===----------------------------------------------------------------------===// #include "lldb/Core/Telemetry.h" -#include -#include - -#include - #include #include #include #include +#include #include #include #include @@ -26,6 +22,7 @@ #include "lldb/API/SBProcess.h" #include "lldb/Core/Debugger.h" #include "lldb/Core/Module.h" +#include "lldb/Core/TelemetryVendor.h" #include "lldb/Host/FileSystem.h" #include "lldb/Host/HostInfo.h" #include "lldb/Interpreter/CommandInterpreter.h" @@ -51,28 +48,6 @@ #include "llvm/Support/raw_ostream.h" #include "llvm/Telemetry/Telemetry.h" -#ifdef HAS_VENDOR_TELEMETRY_PLUGINS -// TODO: could make this path a build-variable rather than hard-coded header -// path -#include "lldb/Core/VendorTelemetryPlugin.h" - -namespace vendor_specific { - -// Set any additional configurations, as needed. -extern void ApplyVendorSpecificConfigs( - llvm::telemetry::TelemetryConfig *config) /* __attribute__((weak))*/; - -// Return a copy of the given entry, but with certain fields that are deemed -// PII risk removed. -extern std::shared_ptr SanitizeSensitiveFields( - const llvm::telemetry::TelemetryInfo *entry) /*__attribute__((weak))*/; - -extern std::shared_ptr -CreateVendorSpecificTelemeter( - llvm::telemetry::TelemetryConfig *config) /*__attribute__((weak))*/; -} // namespace vendor_specific -#endif - namespace lldb_private { using ::llvm::telemetry::Destination; @@ -81,7 +56,8 @@ using ::llvm::telemetry::ExitDescription; using ::llvm::telemetry::SteadyTimePoint; using ::llvm::telemetry::TelemetryInfo; -static std::string ExitDescToString(const ExitDescription *desc) { +static std::string +ExitDescToString(const llvm::telemetry::ExitDescription *desc) { return ("ExitCode:" + desc->ExitCode) + (" ExixitDescription: " + desc->Description); } @@ -282,446 +258,16 @@ llvm::json::Object MiscTelemetryInfo::serializeToJson() const { }}; } -class StreamTelemetryDestination : public Destination { -public: - StreamTelemetryDestination(llvm::raw_ostream &os, std::string desc) - : os(os), desc(desc) {} - llvm::Error emitEntry(const llvm::telemetry::TelemetryInfo *entry) override { - // Unless there exists a custom (vendor-defined) data-cleanup - // for printing, upstream Telemetry should not leak anything other than the - // basic. -#ifdef HAS_TELEMETRY_FIELDS_PRINTER - os << SanitizeSensitiveFields(entry)->ToString() << "\n"; -#else - os << "session_uuid: " << entry->SessionId - << "\n"; -#endif - os.flush(); - return llvm::ErrorSuccess(); - } - - std::string name() const override { return desc; } - -private: - llvm::raw_ostream &os; - const std::string desc; -}; - -// No-op logger to use when users disable telemetry -class NoOpTelemeter : public LldbTelemeter { -public: - static std::unique_ptr CreateInstance(Debugger *debugger) { - return std::unique_ptr(new NoOpTelemeter(debugger)); - } - - NoOpTelemeter(Debugger *debugger) {} - void logStartup(llvm::StringRef tool_path, TelemetryInfo *entry) override {} - void logExit(llvm::StringRef tool_path, TelemetryInfo *entry) override {} - - void LogProcessExit(int status, llvm::StringRef exit_string, EventStats stats, - Target *target_ptr) override {} - void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod, - EventStats stats) override {} - void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod, - EventStats stats) override {} - - void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command, - EventStats stats, Target *target_ptr) override {} - void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name, - llvm::StringRef command_args, EventStats stats, - Target *target_ptr, CommandReturnObject *result) override { - } - - void - LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override {} - - void addDestination(llvm::telemetry::Destination *destination) override {} - std::string GetNextUUID() override { return ""; } -}; - -class BasicTelemeter : public LldbTelemeter { -public: - static std::unique_ptr CreateInstance(Debugger *); - - virtual ~BasicTelemeter() = default; - - void logStartup(llvm::StringRef lldb_path, TelemetryInfo *entry) override; - void logExit(llvm::StringRef lldb_path, TelemetryInfo *entry) override; - - void LogProcessExit(int status, llvm::StringRef exit_string, EventStats stats, - Target *target_ptr) override; - void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod, - EventStats stats) override; - void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod, - EventStats stats) override; - - void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command, - EventStats stats, Target *target_ptr) override; - void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name, - llvm::StringRef command_args, EventStats stats, - Target *target_ptr, CommandReturnObject *result) override; - - void - LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override; - - void addDestination(Destination *destination) override { - m_destinations.push_back(destination); - } - - std::string GetNextUUID() override { - return std::to_string(uuid_seed.fetch_add(1)); - } - -protected: - BasicTelemeter(Debugger *debugger); - - void CollectMiscBuildInfo(); - -private: - template EntrySubType MakeBaseEntry() { - EntrySubType entry; - entry.SessionId = m_session_uuid; - entry.Counter = counter.fetch_add(1); - return entry; - } - - void EmitToDestinations(const TelemetryInfo *entry); - - Debugger *m_debugger; - const std::string m_session_uuid; - std::string startup_lldb_path; - - // counting number of entries. - std::atomic counter = 0; - - std::vector m_destinations; - - std::atomic uuid_seed = 0; -}; - -static std::string MakeUUID(lldb_private::Debugger *debugger) { - std::string ret; - uint8_t random_bytes[16]; - if (auto ec = llvm::getRandomBytes(random_bytes, 16)) { - LLDB_LOG(GetLog(LLDBLog::Object), - "Failed to generate random bytes for UUID: {0}", ec.message()); - // fallback to using timestamp + debugger ID. - ret = std::to_string( - std::chrono::steady_clock::now().time_since_epoch().count()) + - "_" + std::to_string(debugger->GetID()); - } else { - ret = lldb_private::UUID(random_bytes).GetAsString(); - } - - return ret; -} - -BasicTelemeter::BasicTelemeter(lldb_private::Debugger *debugger) - : m_debugger(debugger), m_session_uuid(MakeUUID(debugger)) {} - -std::unique_ptr -BasicTelemeter::CreateInstance(lldb_private::Debugger *debugger) { - auto *config = GetTelemetryConfig(); - - BasicTelemeter *ins = new BasicTelemeter(debugger); - for (const std ::string &dest : config->AdditionalDestinations) { - if (dest == "stdout") { - ins->addDestination( - new StreamTelemetryDestination(llvm::outs(), "stdout")); - } else if (dest == "stderr") { - ins->addDestination( - new StreamTelemetryDestination(llvm::errs(), "stderr")); - } else { - // TODO: handle custom values as needed? - } - } - - return std::unique_ptr(ins); -} - -void BasicTelemeter::EmitToDestinations(const TelemetryInfo *entry) { - // TODO: can do this in a separate thread (need to own the ptrs!). - for (Destination *destination : m_destinations) { - llvm::Error err = destination->emitEntry(entry); - if (err) { - LLDB_LOG(GetLog(LLDBLog::Object), - "Error emitting to destination(name = {0})", - destination->name()); - } - } -} - -void BasicTelemeter::logStartup(llvm::StringRef lldb_path, - TelemetryInfo *entry) { - startup_lldb_path = lldb_path.str(); - lldb_private::DebuggerTelemetryInfo startup_info = - MakeBaseEntry(); - - UserIDResolver &resolver = lldb_private::HostInfo::GetUserIDResolver(); - std::optional opt_username = - resolver.GetUserName(lldb_private::HostInfo::GetUserID()); - if (opt_username) - startup_info.username = *opt_username; - - startup_info.lldb_git_sha = - lldb_private::GetVersion(); // TODO: find the real git sha - startup_info.lldb_path = startup_lldb_path; - startup_info.Stats = entry->Stats; - - llvm::SmallString<64> cwd; - if (!llvm::sys::fs::current_path(cwd)) { - startup_info.cwd = cwd.c_str(); - } else { - MiscTelemetryInfo misc_info = MakeBaseEntry(); - misc_info.meta_data["internal_errors"] = "Cannot determine CWD"; - EmitToDestinations(&misc_info); - } - - EmitToDestinations(&startup_info); - - // Optional part - CollectMiscBuildInfo(); -} - -void BasicTelemeter::logExit(llvm::StringRef lldb_path, TelemetryInfo *entry) { - // we should be shutting down the same instance that we started?! - // llvm::Assert(startup_lldb_path == lldb_path.str()); - - lldb_private::DebuggerTelemetryInfo exit_info = - MakeBaseEntry(); - exit_info.Stats = entry->Stats; - exit_info.lldb_path = startup_lldb_path; - if (auto *selected_target = - m_debugger->GetSelectedExecutionContext().GetTargetPtr()) { - if (!selected_target->IsDummyTarget()) { - const lldb::ProcessSP proc = selected_target->GetProcessSP(); - if (proc == nullptr) { - // no process has been launched yet. - exit_info.ExitDesc = {-1, "no process launched."}; - } else { - exit_info.ExitDesc = {proc->GetExitStatus(), ""}; - if (const char *description = proc->GetExitDescription()) - exit_info.ExitDesc->Description = std::string(description); - } - } - } - EmitToDestinations(&exit_info); -} - -void BasicTelemeter::LogProcessExit(int status, llvm::StringRef exit_string, - EventStats stats, Target *target_ptr) { - lldb_private::TargetTelemetryInfo exit_info = - MakeBaseEntry(); - exit_info.Stats = std::move(stats); - exit_info.target_uuid = - target_ptr && !target_ptr->IsDummyTarget() - ? target_ptr->GetExecutableModule()->GetUUID().GetAsString() - : ""; - exit_info.ExitDesc = {status, exit_string.str()}; - - EmitToDestinations(&exit_info); -} - -void BasicTelemeter::CollectMiscBuildInfo() { - // collecting use-case specific data -} - -void BasicTelemeter::LogMainExecutableLoadStart(lldb::ModuleSP exec_mod, - EventStats stats) { - TargetTelemetryInfo target_info = MakeBaseEntry(); - target_info.Stats = std::move(stats); - target_info.binary_path = - exec_mod->GetFileSpec().GetPathAsConstString().GetCString(); - target_info.file_format = exec_mod->GetArchitecture().GetArchitectureName(); - target_info.target_uuid = exec_mod->GetUUID().GetAsString(); - if (auto err = llvm::sys::fs::file_size(exec_mod->GetFileSpec().GetPath(), - target_info.binary_size)) { - // If there was error obtaining it, just reset the size to 0. - // Maybe log the error too? - target_info.binary_size = 0; - } - EmitToDestinations(&target_info); -} - -void BasicTelemeter::LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod, - EventStats stats) { - TargetTelemetryInfo target_info = MakeBaseEntry(); - target_info.Stats = std::move(stats); - target_info.binary_path = - exec_mod->GetFileSpec().GetPathAsConstString().GetCString(); - target_info.file_format = exec_mod->GetArchitecture().GetArchitectureName(); - target_info.target_uuid = exec_mod->GetUUID().GetAsString(); - target_info.binary_size = exec_mod->GetObjectFile()->GetByteSize(); - - EmitToDestinations(&target_info); - - // Collect some more info, might be useful? - MiscTelemetryInfo misc_info = MakeBaseEntry(); - misc_info.target_uuid = exec_mod->GetUUID().GetAsString(); - misc_info.meta_data["symtab_index_time"] = - std::to_string(exec_mod->GetSymtabIndexTime().get().count()); - misc_info.meta_data["symtab_parse_time"] = - std::to_string(exec_mod->GetSymtabParseTime().get().count()); - EmitToDestinations(&misc_info); -} - -void BasicTelemeter::LogClientTelemetry( - const lldb_private::StructuredDataImpl &entry) { - // TODO: pull the dictionary out of entry - ClientTelemetryInfo client_info = MakeBaseEntry(); - /* - std::optional request_name = entry.getString("request_name"); - if (!request_name.has_value()) { - MiscTelemetryInfo misc_info = MakeBaseEntry(); - misc_info.meta_data["internal_errors"] = - "Cannot determine request name from client entry"; - // TODO: Dump the errornous entry to stderr too? - EmitToDestinations(&misc_info); - return; - } - client_info.request_name = request_name->str(); - - std::optional start_time = entry.getInteger("start_time"); - std::optional end_time = entry.getInteger("end_time"); - - if (!start_time.has_value() || !end_time.has_value()) { - MiscTelemetryInfo misc_info = MakeBaseEntry(); - misc_info.meta_data["internal_errors"] = - "Cannot determine start/end time from client entry"; - EmitToDestinations(&misc_info); - return; - } - - SteadyTimePoint epoch; - client_info.Stats.Start = - epoch + std::chrono::nanoseconds(static_cast(*start_time)); - client_info.Stats.End = - epoch + std::chrono::nanoseconds(static_cast(*end_time)); - - std::optional error_msg = entry.getString("error"); - if (error_msg.has_value()) - client_info.error_msg = error_msg->str(); - */ - - EmitToDestinations(&client_info); -} - -void BasicTelemeter::LogCommandStart(llvm::StringRef uuid, - llvm::StringRef original_command, - EventStats stats, Target *target_ptr) { - - lldb_private::CommandTelemetryInfo command_info = - MakeBaseEntry(); - - // If we have a target attached to this command, then get the UUID. - command_info.target_uuid = ""; - if (target_ptr && target_ptr->GetExecutableModule() != nullptr) { - command_info.target_uuid = - target_ptr->GetExecutableModule()->GetUUID().GetAsString(); - } - command_info.command_uuid = uuid.str(); - command_info.original_command = original_command.str(); - command_info.Stats = std::move(stats); - - EmitToDestinations(&command_info); -} - -void BasicTelemeter::LogCommandEnd(llvm::StringRef uuid, - llvm::StringRef command_name, - llvm::StringRef command_args, - EventStats stats, Target *target_ptr, - CommandReturnObject *result) { - - lldb_private::CommandTelemetryInfo command_info = - MakeBaseEntry(); - - // If we have a target attached to this command, then get the UUID. - command_info.target_uuid = ""; - if (target_ptr && target_ptr->GetExecutableModule() != nullptr) { - command_info.target_uuid = - target_ptr->GetExecutableModule()->GetUUID().GetAsString(); - } - command_info.command_uuid = uuid.str(); - command_info.command_name = command_name.str(); - command_info.args = command_args.str(); - command_info.Stats = std::move(stats); - command_info.ExitDesc = {result->Succeeded() ? 0 : -1, ""}; - if (llvm::StringRef error_data = result->GetErrorData(); - !error_data.empty()) { - command_info.ExitDesc->Description = error_data.str(); - } - command_info.ret_status = result->GetStatus(); - EmitToDestinations(&command_info); -} - -llvm::StringRef parse_value(llvm::StringRef str, llvm::StringRef label) { - return str.substr(label.size()).trim(); -} - -bool parse_field(llvm::StringRef str, llvm::StringRef label) { - if (parse_value(str, label) == "true") - return true; - return false; -} - -llvm::telemetry::Config *MakeTelemetryConfig() { - bool enable_telemetry = false; - std::vector additional_destinations; - - // Look in the $HOME/.lldb_telemetry_config file to populate the struct - llvm::SmallString<64> init_file; - FileSystem::Instance().GetHomeDirectory(init_file); - llvm::sys::path::append(init_file, ".lldb_telemetry_config"); - FileSystem::Instance().Resolve(init_file); - if (llvm::sys::fs::exists(init_file)) { - auto contents = llvm::MemoryBuffer::getFile(init_file, /*IsText*/ true); - if (contents) { - llvm::line_iterator iter = - llvm::line_iterator(contents->get()->getMemBufferRef()); - for (; !iter.is_at_eof(); ++iter) { - if (iter->starts_with("enable_telemetry:")) { - enable_telemetry = parse_field(*iter, "enable_telemetry:"); - } else if (iter->starts_with("destination:")) { - llvm::StringRef dest = parse_value(*iter, "destination:"); - if (dest == "stdout") { - additional_destinations.push_back("stdout"); - } else if (dest == "stderr") { - additional_destinations.push_back("stderr"); - } else { - additional_destinations.push_back(dest.str()); - } - } - } - } else { - LLDB_LOG(GetLog(LLDBLog::Object), "Error reading config file at {0}", - init_file.c_str()); - } - } - - auto *ret = - new llvm::telemetry::Config{enable_telemetry, additional_destinations}; -#ifdef HAS_VENDOR_TELEMETRY_PLUGINS - vendor_specific::ApplyVendorSpecificConfigs(ret); -#endif - return ret; -} - -llvm::telemetry::Config *GetTelemetryConfig() { - static llvm::telemetry::Config *config = MakeTelemetryConfig(); - return config; -} - std::unique_ptr LldbTelemeter::CreateInstance(lldb_private::Debugger *debugger) { - auto *config = GetTelemetryConfig(); - if (!config->EnableTelemetry) { - return NoOpTelemeter::CreateInstance(debugger); + // TODO: do we cache the plugin? + TelemetryVendor *vendor = TelemetryVendor::FindPlugin(); + if (vendor == nullptr) { + LLDB_LOG(GetLog(LLDBLog::Object), + "Failed to find a TelemetryVendor plugin instance"); + return nullptr; } -#ifdef HAS_VENDOR_TELEMETRY_PLUGINS - return vendor_specific::CreateVendorSpecificTelemeter(config); -#else - return BasicTelemeter::CreateInstance(debugger); -#endif + return vendor->CreateTelemeter(debugger); } } // namespace lldb_private diff --git a/lldb/source/Core/TelemetryVendor.cpp b/lldb/source/Core/TelemetryVendor.cpp new file mode 100644 index 0000000000000..fa40ee2ad045e --- /dev/null +++ b/lldb/source/Core/TelemetryVendor.cpp @@ -0,0 +1,546 @@ +//===-- TelemetryVendor.cpp -------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +#include "lldb/Core/TelemetryVendor.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lldb/API/SBDebugger.h" +#include "lldb/API/SBProcess.h" +#include "lldb/Core/Debugger.h" +#include "lldb/Core/Module.h" +#include "lldb/Core/PluginManager.h" +#include "lldb/Core/Telemetry.h" +#include "lldb/Host/FileSystem.h" +#include "lldb/Host/HostInfo.h" +#include "lldb/Interpreter/CommandInterpreter.h" +#include "lldb/Target/Process.h" +#include "lldb/Target/Statistics.h" +#include "lldb/Utility/ConstString.h" +#include "lldb/Utility/FileSpec.h" +#include "lldb/Utility/LLDBLog.h" +#include "lldb/Utility/UUID.h" +#include "lldb/Version/Version.h" +#include "lldb/lldb-enumerations.h" +#include "lldb/lldb-forward.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/Twine.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/LineIterator.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/RandomNumberGenerator.h" +#include "llvm/Support/raw_ostream.h" +#include "llvm/Telemetry/Telemetry.h" + +using namespace lldb; +using namespace lldb_private; + +namespace { + +using ::llvm::telemetry::Destination; +using ::llvm::telemetry::EventStats; +using ::llvm::telemetry::ExitDescription; +using ::llvm::telemetry::SteadyTimePoint; +using ::llvm::telemetry::TelemetryInfo; + +// No-op logger to use when users disable telemetry +class NoOpTelemeter : public LldbTelemeter { +public: + static std::unique_ptr CreateInstance(Debugger *debugger) { + return std::unique_ptr(new NoOpTelemeter(debugger)); + } + + NoOpTelemeter(Debugger *debugger) {} + void logStartup(llvm::StringRef tool_path, TelemetryInfo *entry) override {} + void logExit(llvm::StringRef tool_path, TelemetryInfo *entry) override {} + + void LogProcessExit(int status, llvm::StringRef exit_string, EventStats stats, + Target *target_ptr) override {} + void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod, + EventStats stats) override {} + void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod, + EventStats stats) override {} + + void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command, + EventStats stats, Target *target_ptr) override {} + void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name, + llvm::StringRef command_args, EventStats stats, + Target *target_ptr, CommandReturnObject *result) override { + } + + void + LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override {} + + void addDestination(llvm::telemetry::Destination *destination) override {} + std::string GetNextUUID() override { return ""; } +}; + +class BasicTelemeter : public LldbTelemeter { +public: + static std::unique_ptr + CreateInstance(std::unique_ptr config, + Debugger *debugger); + + virtual ~BasicTelemeter() = default; + + void logStartup(llvm::StringRef lldb_path, TelemetryInfo *entry) override; + void logExit(llvm::StringRef lldb_path, TelemetryInfo *entry) override; + + void LogProcessExit(int status, llvm::StringRef exit_string, EventStats stats, + Target *target_ptr) override; + void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod, + EventStats stats) override; + void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod, + EventStats stats) override; + + void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command, + EventStats stats, Target *target_ptr) override; + void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name, + llvm::StringRef command_args, EventStats stats, + Target *target_ptr, CommandReturnObject *result) override; + + void + LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override; + + void addDestination(Destination *destination) override { + m_destinations.push_back(destination); + } + + std::string GetNextUUID() override { + return std::to_string(uuid_seed.fetch_add(1)); + } + +protected: + BasicTelemeter(std::unique_ptr config, + Debugger *debugger); + + void CollectMiscBuildInfo(); + +private: + template EntrySubType MakeBaseEntry() { + EntrySubType entry; + entry.SessionId = m_session_uuid; + entry.Counter = counter.fetch_add(1); + return entry; + } + + void EmitToDestinations(const TelemetryInfo *entry); + + std::unique_ptr m_config; + Debugger *m_debugger; + const std::string m_session_uuid; + std::string startup_lldb_path; + + // counting number of entries. + std::atomic counter = 0; + + std::vector m_destinations; + + std::atomic uuid_seed = 0; +}; + +class StreamTelemetryDestination : public Destination { +public: + StreamTelemetryDestination(llvm::raw_ostream &os, std::string desc) + : os(os), desc(desc) {} + llvm::Error emitEntry(const llvm::telemetry::TelemetryInfo *entry) override { + // Upstream Telemetry should not leak anything other than the + // basic data, unless running in test mode. +#ifdef TEST_TELEMETRY + os << entry->ToString() << "\n"; +#else + os << "session_uuid: " << entry->SessionId + << "\n"; +#endif + os.flush(); + return llvm::ErrorSuccess(); + } + + std::string name() const override { return desc; } + +private: + llvm::raw_ostream &os; + const std::string desc; +}; + +static std::string MakeUUID(lldb_private::Debugger *debugger) { + std::string ret; + uint8_t random_bytes[16]; + if (auto ec = llvm::getRandomBytes(random_bytes, 16)) { + LLDB_LOG(GetLog(LLDBLog::Object), + "Failed to generate random bytes for UUID: {0}", ec.message()); + // fallback to using timestamp + debugger ID. + ret = std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count()) + + "_" + std::to_string(debugger->GetID()); + } else { + ret = lldb_private::UUID(random_bytes).GetAsString(); + } + + return ret; +} + +BasicTelemeter::BasicTelemeter(std::unique_ptr config, + lldb_private::Debugger *debugger) + : m_config(std::move(config)), m_debugger(debugger), + m_session_uuid(MakeUUID(debugger)) {} + +std::unique_ptr +BasicTelemeter::CreateInstance(std::unique_ptr config, + lldb_private::Debugger *debugger) { + + BasicTelemeter *ins = new BasicTelemeter(std::move(config), debugger); + for (const std ::string &dest : config->AdditionalDestinations) { + if (dest == "stdout") { + ins->addDestination( + new StreamTelemetryDestination(llvm::outs(), "stdout")); + } else if (dest == "stderr") { + ins->addDestination( + new StreamTelemetryDestination(llvm::errs(), "stderr")); + } else { + // TODO: handle custom values as needed? + } + } + + return std::unique_ptr(ins); +} + +void BasicTelemeter::EmitToDestinations(const TelemetryInfo *entry) { + // TODO: can do this in a separate thread (need to own the ptrs!). + for (Destination *destination : m_destinations) { + llvm::Error err = destination->emitEntry(entry); + if (err) { + LLDB_LOG(GetLog(LLDBLog::Object), + "Error emitting to destination(name = {0})", + destination->name()); + } + } +} + +void BasicTelemeter::logStartup(llvm::StringRef lldb_path, + TelemetryInfo *entry) { + startup_lldb_path = lldb_path.str(); + lldb_private::DebuggerTelemetryInfo startup_info = + MakeBaseEntry(); + + UserIDResolver &resolver = lldb_private::HostInfo::GetUserIDResolver(); + std::optional opt_username = + resolver.GetUserName(lldb_private::HostInfo::GetUserID()); + if (opt_username) + startup_info.username = *opt_username; + + startup_info.lldb_git_sha = + lldb_private::GetVersion(); // TODO: find the real git sha + startup_info.lldb_path = startup_lldb_path; + startup_info.Stats = entry->Stats; + + llvm::SmallString<64> cwd; + if (!llvm::sys::fs::current_path(cwd)) { + startup_info.cwd = cwd.c_str(); + } else { + MiscTelemetryInfo misc_info = MakeBaseEntry(); + misc_info.meta_data["internal_errors"] = "Cannot determine CWD"; + EmitToDestinations(&misc_info); + } + + EmitToDestinations(&startup_info); + + // Optional part + CollectMiscBuildInfo(); +} + +void BasicTelemeter::logExit(llvm::StringRef lldb_path, TelemetryInfo *entry) { + // we should be shutting down the same instance that we started?! + // llvm::Assert(startup_lldb_path == lldb_path.str()); + + lldb_private::DebuggerTelemetryInfo exit_info = + MakeBaseEntry(); + exit_info.Stats = entry->Stats; + exit_info.lldb_path = startup_lldb_path; + if (auto *selected_target = + m_debugger->GetSelectedExecutionContext().GetTargetPtr()) { + if (!selected_target->IsDummyTarget()) { + const lldb::ProcessSP proc = selected_target->GetProcessSP(); + if (proc == nullptr) { + // no process has been launched yet. + exit_info.ExitDesc = {-1, "no process launched."}; + } else { + exit_info.ExitDesc = {proc->GetExitStatus(), ""}; + if (const char *description = proc->GetExitDescription()) + exit_info.ExitDesc->Description = std::string(description); + } + } + } + EmitToDestinations(&exit_info); +} + +void BasicTelemeter::LogProcessExit(int status, llvm::StringRef exit_string, + EventStats stats, Target *target_ptr) { + lldb_private::TargetTelemetryInfo exit_info = + MakeBaseEntry(); + exit_info.Stats = std::move(stats); + exit_info.target_uuid = + target_ptr && !target_ptr->IsDummyTarget() + ? target_ptr->GetExecutableModule()->GetUUID().GetAsString() + : ""; + exit_info.ExitDesc = {status, exit_string.str()}; + + EmitToDestinations(&exit_info); +} + +void BasicTelemeter::CollectMiscBuildInfo() { + // collecting use-case specific data +} + +void BasicTelemeter::LogMainExecutableLoadStart(lldb::ModuleSP exec_mod, + EventStats stats) { + TargetTelemetryInfo target_info = MakeBaseEntry(); + target_info.Stats = std::move(stats); + target_info.binary_path = + exec_mod->GetFileSpec().GetPathAsConstString().GetCString(); + target_info.file_format = exec_mod->GetArchitecture().GetArchitectureName(); + target_info.target_uuid = exec_mod->GetUUID().GetAsString(); + if (auto err = llvm::sys::fs::file_size(exec_mod->GetFileSpec().GetPath(), + target_info.binary_size)) { + // If there was error obtaining it, just reset the size to 0. + // Maybe log the error too? + target_info.binary_size = 0; + } + EmitToDestinations(&target_info); +} + +void BasicTelemeter::LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod, + EventStats stats) { + TargetTelemetryInfo target_info = MakeBaseEntry(); + target_info.Stats = std::move(stats); + target_info.binary_path = + exec_mod->GetFileSpec().GetPathAsConstString().GetCString(); + target_info.file_format = exec_mod->GetArchitecture().GetArchitectureName(); + target_info.target_uuid = exec_mod->GetUUID().GetAsString(); + target_info.binary_size = exec_mod->GetObjectFile()->GetByteSize(); + + EmitToDestinations(&target_info); + + // Collect some more info, might be useful? + MiscTelemetryInfo misc_info = MakeBaseEntry(); + misc_info.target_uuid = exec_mod->GetUUID().GetAsString(); + misc_info.meta_data["symtab_index_time"] = + std::to_string(exec_mod->GetSymtabIndexTime().get().count()); + misc_info.meta_data["symtab_parse_time"] = + std::to_string(exec_mod->GetSymtabParseTime().get().count()); + EmitToDestinations(&misc_info); +} + +void BasicTelemeter::LogClientTelemetry( + const lldb_private::StructuredDataImpl &entry) { + // TODO: pull the dictionary out of entry + ClientTelemetryInfo client_info = MakeBaseEntry(); + /* + std::optional request_name = entry.getString("request_name"); + if (!request_name.has_value()) { + MiscTelemetryInfo misc_info = MakeBaseEntry(); + misc_info.meta_data["internal_errors"] = + "Cannot determine request name from client entry"; + // TODO: Dump the errornous entry to stderr too? + EmitToDestinations(&misc_info); + return; + } + client_info.request_name = request_name->str(); + + std::optional start_time = entry.getInteger("start_time"); + std::optional end_time = entry.getInteger("end_time"); + + if (!start_time.has_value() || !end_time.has_value()) { + MiscTelemetryInfo misc_info = MakeBaseEntry(); + misc_info.meta_data["internal_errors"] = + "Cannot determine start/end time from client entry"; + EmitToDestinations(&misc_info); + return; + } + + SteadyTimePoint epoch; + client_info.Stats.Start = + epoch + std::chrono::nanoseconds(static_cast(*start_time)); + client_info.Stats.End = + epoch + std::chrono::nanoseconds(static_cast(*end_time)); + + std::optional error_msg = entry.getString("error"); + if (error_msg.has_value()) + client_info.error_msg = error_msg->str(); + */ + + EmitToDestinations(&client_info); +} + +void BasicTelemeter::LogCommandStart(llvm::StringRef uuid, + llvm::StringRef original_command, + EventStats stats, Target *target_ptr) { + + lldb_private::CommandTelemetryInfo command_info = + MakeBaseEntry(); + + // If we have a target attached to this command, then get the UUID. + command_info.target_uuid = ""; + if (target_ptr && target_ptr->GetExecutableModule() != nullptr) { + command_info.target_uuid = + target_ptr->GetExecutableModule()->GetUUID().GetAsString(); + } + command_info.command_uuid = uuid.str(); + command_info.original_command = original_command.str(); + command_info.Stats = std::move(stats); + + EmitToDestinations(&command_info); +} + +void BasicTelemeter::LogCommandEnd(llvm::StringRef uuid, + llvm::StringRef command_name, + llvm::StringRef command_args, + EventStats stats, Target *target_ptr, + CommandReturnObject *result) { + + lldb_private::CommandTelemetryInfo command_info = + MakeBaseEntry(); + + // If we have a target attached to this command, then get the UUID. + command_info.target_uuid = ""; + if (target_ptr && target_ptr->GetExecutableModule() != nullptr) { + command_info.target_uuid = + target_ptr->GetExecutableModule()->GetUUID().GetAsString(); + } + command_info.command_uuid = uuid.str(); + command_info.command_name = command_name.str(); + command_info.args = command_args.str(); + command_info.Stats = std::move(stats); + command_info.ExitDesc = {result->Succeeded() ? 0 : -1, ""}; + if (llvm::StringRef error_data = result->GetErrorData(); + !error_data.empty()) { + command_info.ExitDesc->Description = error_data.str(); + } + command_info.ret_status = result->GetStatus(); + EmitToDestinations(&command_info); +} + +} // namespace + +TelemetryVendor *TelemetryVendor::FindPlugin() { + // The default implementation (ie., upstream impl) returns + // the basic instance. + // + // Vendors can provide their plugins as needed. + + std::unique_ptr instance_up; + TelemetryVendorCreateInstance create_callback; + + for (size_t idx = 0; + (create_callback = + PluginManager::GetTelemetryVendorCreateCallbackAtIndex(idx)) != + nullptr; + ++idx) { + instance_up.reset(create_callback()); + + if (instance_up) { + return instance_up.release(); + } + } + + return new TelemetryVendor(); +} + +llvm::StringRef TelemetryVendor::GetPluginName() { + return "DefaultTelemetryVendor"; +} + +static llvm::StringRef ParseValue(llvm::StringRef str, llvm::StringRef label) { + return str.substr(label.size()).trim(); +} + +static bool ParseBoolValue(llvm::StringRef str, llvm::StringRef label) { + if (ParseValue(str, label) == "true") + return true; + return false; +} + +std::unique_ptr TelemetryVendor::GetTelemetryConfig() { + // Telemetry is disabled by default. + bool enable_telemetry = false; + std::vector additional_destinations; + + // Look in the $HOME/.lldb_telemetry_config file to populate the struct + llvm::SmallString<64> init_file; + FileSystem::Instance().GetHomeDirectory(init_file); + llvm::sys::path::append(init_file, ".lldb_telemetry_config"); + FileSystem::Instance().Resolve(init_file); + if (llvm::sys::fs::exists(init_file)) { + auto contents = llvm::MemoryBuffer::getFile(init_file, /*IsText*/ true); + if (contents) { + llvm::line_iterator iter = + llvm::line_iterator(contents->get()->getMemBufferRef()); + for (; !iter.is_at_eof(); ++iter) { + if (iter->starts_with("enable_telemetry:")) { + enable_telemetry = ParseBoolValue(*iter, "enable_telemetry:"); + } else if (iter->starts_with("destination:")) { + llvm::StringRef dest = ParseValue(*iter, "destination:"); + if (dest == "stdout") { + additional_destinations.push_back("stdout"); + } else if (dest == "stderr") { + additional_destinations.push_back("stderr"); + } else { + additional_destinations.push_back(dest.str()); + } + } + } + } else { + LLDB_LOG(GetLog(LLDBLog::Object), "Error reading config file at {0}", + init_file.c_str()); + } + } + +// Enable Telemetry in upstream config only if we are running tests. +#ifdef TEST_TELEMETRY + enable_telemetry = true; +#endif + + auto config = std::make_unique(); + config->EnableTelemetry = enable_telemetry; + config->AdditionalDestinations = std::move(additional_destinations); + + // Now apply any additional vendor config, if available. + // TODO: cache the Config? (given it's not going to change after LLDB starts + // up) However, it's possible we want to supporting restarting the Telemeter + // with new config? + return GetVendorSpecificConfig(std::move(config)); +} + +std::unique_ptr +TelemetryVendor::GetVendorSpecificConfig( + std::unique_ptr default_config) { + return std::move(default_config); +} + +std::unique_ptr +TelemetryVendor::CreateTelemeter(Debugger *debugger) { + auto config = GetTelemetryConfig(); + + if (!config->EnableTelemetry) { + return NoOpTelemeter::CreateInstance(debugger); + } + + return BasicTelemeter::CreateInstance(std::move(config), debugger); +} diff --git a/lldb/test/CMakeLists.txt b/lldb/test/CMakeLists.txt index 5ac474736eb63..a5a342da7cfaa 100644 --- a/lldb/test/CMakeLists.txt +++ b/lldb/test/CMakeLists.txt @@ -108,6 +108,9 @@ endfunction(add_lldb_test_dependency) add_lldb_test_dependency(lldb) add_lldb_test_dependency(lldb-test) +# Enable Telemetry for testing. +target_compile_definitions(lldb PRIVATE -DTEST_TELEMETRY) + # On Darwin, darwin-debug is an hard dependency for the testsuites. if (CMAKE_SYSTEM_NAME MATCHES "Darwin") add_lldb_test_dependency(darwin-debug) From f85900d3c4ed552681117baa03ec1afda5d6962a Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 11 Dec 2024 10:58:25 -0500 Subject: [PATCH 25/30] UPdate LLDB implementation to match changes in LLVM's telemetry --- lldb/include/lldb/Core/Telemetry.h | 154 ++++---- lldb/include/lldb/Core/TelemetryVendor.h | 2 - lldb/source/Core/Debugger.cpp | 21 +- lldb/source/Core/Telemetry.cpp | 219 +++-------- lldb/source/Core/TelemetryVendor.cpp | 344 +++++++++--------- .../source/Interpreter/CommandInterpreter.cpp | 26 +- lldb/source/Target/Process.cpp | 12 +- lldb/source/Target/Target.cpp | 18 +- llvm/include/llvm/Telemetry/Telemetry.h | 118 ++---- llvm/lib/Telemetry/Telemetry.cpp | 8 +- 10 files changed, 372 insertions(+), 550 deletions(-) diff --git a/lldb/include/lldb/Core/Telemetry.h b/lldb/include/lldb/Core/Telemetry.h index 3c6118414bb28..bc28f8356bba4 100644 --- a/lldb/include/lldb/Core/Telemetry.h +++ b/lldb/include/lldb/Core/Telemetry.h @@ -28,7 +28,10 @@ namespace lldb_private { +using llvm::telemetry::Destination; using llvm::telemetry::KindType; +using llvm::telemetry::Serializer; +using llvm::telemetry::TelemetryInfo; struct LldbEntryKind : public ::llvm::telemetry::EntryKind { static const KindType BaseInfo = 0b11000; @@ -39,21 +42,44 @@ struct LldbEntryKind : public ::llvm::telemetry::EntryKind { static const KindType MiscInfo = 0b11110; }; -struct LldbBaseTelemetryInfo : public ::llvm::telemetry::TelemetryInfo { +/// Defines a convenient type for timestamp of various events. +/// This is used by the EventStats below. +using SteadyTimePoint = std::chrono::time_point; + +/// Various time (and possibly memory) statistics of an event. +struct EventStats { + // REQUIRED: Start time of an event + SteadyTimePoint start; + // OPTIONAL: End time of an event - may be empty if not meaningful. + std::optional end; + // TBD: could add some memory stats here too? + + EventStats() = default; + EventStats(SteadyTimePoint start) : start(start) {} + EventStats(SteadyTimePoint start, SteadyTimePoint end) + : start(start), end(end) {} +}; + +/// Describes the exit signal of an event. +struct ExitDescription { + int exit_code; + std::string description; +}; + +struct LldbBaseTelemetryInfo : public TelemetryInfo { + EventStats stats; + // For dyn_cast, isa, etc operations. KindType getKind() const override { return LldbEntryKind::BaseInfo; } - static bool classof(const TelemetryInfo *T) { - if (T == nullptr) + static bool classof(const TelemetryInfo *t) { + if (t == nullptr) return false; // Subclasses of this is also acceptable. - return (T->getKind() & LldbEntryKind::BaseInfo) == LldbEntryKind::BaseInfo; + return (t->getKind() & LldbEntryKind::BaseInfo) == LldbEntryKind::BaseInfo; } - // Returns a human-readable string description of the struct. - // This is for debugging purposes only. - // It is NOT meant as a data-serialisation method. - virtual std::string ToString() const; + void serialize(Serializer &serializer) const override; }; struct DebuggerTelemetryInfo : public LldbBaseTelemetryInfo { @@ -62,6 +88,7 @@ struct DebuggerTelemetryInfo : public LldbBaseTelemetryInfo { std::string lldb_path; std::string cwd; + std::optional exit_desc; DebuggerTelemetryInfo() = default; // Provide a copy ctor because we may need to make a copy before @@ -82,12 +109,13 @@ struct DebuggerTelemetryInfo : public LldbBaseTelemetryInfo { return T->getKind() == LldbEntryKind::DebuggerInfo; } - llvm::json::Object serializeToJson() const override; - - std::string ToString() const override; + void serialize(Serializer &serializer) const override; }; struct TargetTelemetryInfo : public LldbBaseTelemetryInfo { + lldb::ModuleSP exec_mod; + Target *target_ptr; + // The same as the executable-module's UUID. std::string target_uuid; std::string file_format; @@ -95,13 +123,16 @@ struct TargetTelemetryInfo : public LldbBaseTelemetryInfo { std::string binary_path; size_t binary_size; + std::optional exit_desc; TargetTelemetryInfo() = default; TargetTelemetryInfo(const TargetTelemetryInfo &other) { + exec_mod = other.exec_mod; target_uuid = other.target_uuid; file_format = other.file_format; binary_path = other.binary_path; binary_size = other.binary_size; + exit_desc = other.exit_desc; } KindType getKind() const override { return LldbEntryKind::TargetInfo; } @@ -112,9 +143,7 @@ struct TargetTelemetryInfo : public LldbBaseTelemetryInfo { return T->getKind() == LldbEntryKind::TargetInfo; } - llvm::json::Object serializeToJson() const override; - - std::string ToString() const override; + void serialize(Serializer &serializer) const override; }; // Entry from client (eg., SB-API) @@ -137,22 +166,13 @@ struct ClientTelemetryInfo : public LldbBaseTelemetryInfo { return T->getKind() == LldbEntryKind::ClientInfo; } - llvm::json::Object serializeToJson() const override; - - std::string ToString() const override; -}; - -struct CommandExitDescription : public ::llvm::telemetry::ExitDescription { - lldb::ReturnStatus ret_status; - CommandExitDescription(int ret_code, std::string ret_desc, - lldb::ReturnStatus status) { - ExitCode = ret_code; - Description = std::move(ret_desc); - ret_status = status; - } + void serialize(Serializer &serializer) const override; }; struct CommandTelemetryInfo : public LldbBaseTelemetryInfo { + Target *target_ptr; + CommandReturnObject *result; + // If the command is/can be associated with a target entry, // this field contains that target's UUID. // otherwise. @@ -167,6 +187,7 @@ struct CommandTelemetryInfo : public LldbBaseTelemetryInfo { std::string original_command; std::string args; + std::optional exit_desc; lldb::ReturnStatus ret_status; CommandTelemetryInfo() = default; @@ -177,6 +198,8 @@ struct CommandTelemetryInfo : public LldbBaseTelemetryInfo { command_name = other.command_name; original_command = other.original_command; args = other.args; + exit_desc = other.exit_desc; + ret_status = other.ret_status; } KindType getKind() const override { return LldbEntryKind::CommandInfo; } @@ -187,9 +210,7 @@ struct CommandTelemetryInfo : public LldbBaseTelemetryInfo { return T->getKind() == LldbEntryKind::CommandInfo; } - llvm::json::Object serializeToJson() const override; - - std::string ToString() const override; + void serialize(Serializer &serializer) const override; }; /// The "catch-all" entry to store a set of custom/non-standard @@ -201,7 +222,7 @@ struct MiscTelemetryInfo : public LldbBaseTelemetryInfo { std::string target_uuid; /// Set of key-value pairs for any optional (or impl-specific) data - std::unordered_map meta_data; + std::map meta_data; MiscTelemetryInfo() = default; @@ -218,15 +239,13 @@ struct MiscTelemetryInfo : public LldbBaseTelemetryInfo { return T->getKind() == LldbEntryKind::MiscInfo; } - llvm::json::Object serializeToJson() const override; - - std::string ToString() const override; + void serialize(Serializer &serializer) const override; }; -/// The base Telemeter instance in LLDB. +/// The base Telemetry manager instance in LLDB /// This class declares additional instrumentation points /// applicable to LLDB. -class LldbTelemeter : public llvm::telemetry::Telemeter { +class LldbTelemeter : public llvm::telemetry::Manager { public: /// Creates an instance of LldbTelemeter. /// This uses the plugin registry to find an instance: @@ -239,65 +258,40 @@ class LldbTelemeter : public llvm::telemetry::Telemeter { virtual ~LldbTelemeter() = default; - /// Invoked upon process exit - virtual void LogProcessExit(int status, llvm::StringRef exit_string, - llvm::telemetry::EventStats stats, - Target *target_ptr) = 0; + /// To be invoked upon LLDB startup. + virtual void LogStartup(DebuggerTelemetryInfo *entry) = 0; + + /// To be invoked upon LLDB exit. + virtual void LogExit(DebuggerTelemetryInfo *entry) = 0; - /// Invoked upon loading the main executable module + /// To be invoked upon loading the main executable module. /// We log in a fire-n-forget fashion so that if the load /// crashes, we don't lose the entry. - virtual void - LogMainExecutableLoadStart(lldb::ModuleSP exec_mod, - llvm::telemetry::EventStats stats) = 0; - virtual void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod, - llvm::telemetry::EventStats stats) = 0; + virtual void LogMainExecutableLoadStart(TargetTelemetryInfo *entry) = 0; + virtual void LogMainExecutableLoadEnd(TargetTelemetryInfo *entry) = 0; + + /// To be invoked upon process exit. + virtual void LogProcessExit(TargetTelemetryInfo *entry); /// Invoked for each command /// We log in a fire-n-forget fashion so that if the command execution /// crashes, we don't lose the entry. - virtual void LogCommandStart(llvm::StringRef uuid, - llvm::StringRef original_command, - llvm::telemetry::EventStats stats, - Target *target_ptr) = 0; - virtual void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name, - llvm::StringRef command_args, - llvm::telemetry::EventStats stats, - Target *target_ptr, - CommandReturnObject *result) = 0; + virtual void LogCommandStart(CommandTelemetryInfo *entry) = 0; + virtual void LogCommandEnd(CommandTelemetryInfo *entry) = 0; virtual std::string GetNextUUID() = 0; /// For client (eg., SB API) to send telemetry entries. virtual void LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) = 0; + +private: + const std::string SessionId; + std::vector> destinations; }; -/// Logger configs: LLDB users can also supply their own configs via: -/// $HOME/.lldb_telemetry_config -/// -/// We can propose simple syntax: -/// Eg., -/// enable_telemetry:true -/// destination:stdout -/// destination:stderr -/// destination:/path/to/some/file -/// -/// The allowed field_name values are: -/// * enable_telemetry -/// If the fields are specified more than once, the last line will take -/// precedence If enable_logging is set to false, no logging will occur. -/// * destination. -/// This is allowed to be specified multiple times - it will add to the -/// default (ie, specified by vendor) list of destinations. -/// The value can be either: -/// + one of the two magic values "stdout" or "stderr". -/// + a path to a local file -/// !!NOTE!!: We decided to use a separate file instead of the existing settings -/// file because that file is parsed too late in the process and by the -/// there might have been lots of telemetry-entries that need to be -/// sent already. -/// This approach avoid losing log entries if LLDB crashes during init. +/// Logger configs. This should be overriden by vendor's specific config. +/// The default (upstream) config will have telemetry disabled. llvm::telemetry::Config *GetTelemetryConfig(); } // namespace lldb_private diff --git a/lldb/include/lldb/Core/TelemetryVendor.h b/lldb/include/lldb/Core/TelemetryVendor.h index f8a57980db7ad..3adc77af07e4f 100644 --- a/lldb/include/lldb/Core/TelemetryVendor.h +++ b/lldb/include/lldb/Core/TelemetryVendor.h @@ -32,8 +32,6 @@ class TelemetryVendor : public PluginInterface { virtual std::unique_ptr CreateTelemeter(lldb_private::Debugger *debugger); - // TODO: move most of the basictelemeter concrete impl here to the plug in (to - // its .ccpp file that is) protected: // Returns a vendor-specific config which may or may not be the same as the // given "default_config". Downstream implementation can define their diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp index 83f74cbd4935a..4e664f24cb2bb 100644 --- a/lldb/source/Core/Debugger.cpp +++ b/lldb/source/Core/Debugger.cpp @@ -734,18 +734,17 @@ void Debugger::InstanceInitialize() { DebuggerSP Debugger::CreateInstance(lldb::LogOutputCallback log_callback, void *baton) { - llvm::telemetry::SteadyTimePoint start_time = - std::chrono::steady_clock::now(); + SteadyTimePoint start_time = std::chrono::steady_clock::now(); DebuggerSP debugger_sp(new Debugger(log_callback, baton)); if (g_debugger_list_ptr && g_debugger_list_mutex_ptr) { std::lock_guard guard(*g_debugger_list_mutex_ptr); g_debugger_list_ptr->push_back(debugger_sp); } debugger_sp->InstanceInitialize(); - llvm::telemetry::TelemetryInfo entry; - entry.Stats = {start_time, std::chrono::steady_clock::now()}; - debugger_sp->m_telemeter->logStartup(HostInfo::GetProgramFileSpec().GetPath(), - &entry); + DebuggerTelemetryInfo entry; + entry.lldb_path = HostInfo::GetProgramFileSpec().GetPath(); + entry.stats = {start_time, std::chrono::steady_clock::now()}; + debugger_sp->m_telemeter->LogStartup(&entry); return debugger_sp; } @@ -960,8 +959,7 @@ void Debugger::Clear() { // static void Debugger::Destroy(lldb::DebuggerSP &debugger_sp); // static void Debugger::Terminate(); llvm::call_once(m_clear_once, [this]() { - llvm::telemetry::SteadyTimePoint quit_start_time = - std::chrono::steady_clock::now(); + SteadyTimePoint quit_start_time = std::chrono::steady_clock::now(); ClearIOHandlers(); StopIOHandlerThread(); StopEventHandlerThread(); @@ -987,9 +985,10 @@ void Debugger::Clear() { // Log the "quit" event (including stats on how long the teardown took) // TBD: We *may* have to send off the log BEFORE the ClearIOHanders()? - llvm::telemetry::TelemetryInfo entry; - entry.Stats = {quit_start_time, std::chrono::steady_clock::now()}; - m_telemeter->logExit(HostInfo::GetProgramFileSpec().GetPath(), &entry); + DebuggerTelemetryInfo entry; + entry.stats = {quit_start_time, std::chrono::steady_clock::now()}; + entry.lldb_path = HostInfo::GetProgramFileSpec().GetPath(); + m_telemeter->LogExit(&entry); }); } diff --git a/lldb/source/Core/Telemetry.cpp b/lldb/source/Core/Telemetry.cpp index 9c4ab9dadc745..2bcf00ae6a261 100644 --- a/lldb/source/Core/Telemetry.cpp +++ b/lldb/source/Core/Telemetry.cpp @@ -51,41 +51,15 @@ namespace lldb_private { using ::llvm::telemetry::Destination; -using ::llvm::telemetry::EventStats; -using ::llvm::telemetry::ExitDescription; -using ::llvm::telemetry::SteadyTimePoint; using ::llvm::telemetry::TelemetryInfo; -static std::string -ExitDescToString(const llvm::telemetry::ExitDescription *desc) { - return ("ExitCode:" + desc->ExitCode) + - (" ExixitDescription: " + desc->Description); -} - static std::string GetDuration(const EventStats &stats) { - if (stats.End.has_value()) - return std::to_string((stats.End.value() - stats.Start).count()) + + if (stats.end.has_value()) + return std::to_string((stats.end.value() - stats.start).count()) + "(nanosec)"; return ""; } -std::string LldbBaseTelemetryInfo::ToString() const { - return ("[LldbBaseTelemetryInfo]\n") + (" SessionId: " + SessionId + "\n"); -} - -std::string DebuggerTelemetryInfo::ToString() const { - std::string duration_desc = - (ExitDesc.has_value() ? " lldb session duration: " - : " lldb startup duration: ") + - std::to_string((Stats.End.value() - Stats.Start).count()) + "(nanosec)\n"; - - return LldbBaseTelemetryInfo::ToString() + "\n" + - ("[DebuggerTelemetryInfo]\n") + (" username: " + username + "\n") + - (" lldb_git_sha: " + lldb_git_sha + "\n") + - (" lldb_path: " + lldb_path + "\n") + (" cwd: " + cwd + "\n") + - duration_desc + "\n"; -} - static size_t ToNanosecOrZero(const std::optional &Point) { if (!Point.has_value()) return 0; @@ -93,169 +67,61 @@ static size_t ToNanosecOrZero(const std::optional &Point) { return Point.value().time_since_epoch().count(); } -llvm::json::Object DebuggerTelemetryInfo::serializeToJson() const { - return llvm::json::Object{ - {"DebuggerInfo", - { - {"SessionId", SessionId}, - {"username", username}, - {"lldb_git_sha", lldb_git_sha}, - {"lldb_path", lldb_path}, - {"cwd", cwd}, - { - "EventStats", - { - {"Start", Stats.Start.time_since_epoch().count()}, - {"End", ToNanosecOrZero(Stats.End)}, - }, - }, - // TODO: fill in more? - }}}; -} - -std::string ClientTelemetryInfo::ToString() const { - return LldbBaseTelemetryInfo::ToString() + "\n" + - ("[DapRequestInfoEntry]\n") + - (" request_name: " + request_name + "\n") + - (" request_duration: " + GetDuration(Stats) + "(nanosec)\n") + - (" error_msg: " + error_msg + "\n"); -} - -llvm::json::Object ClientTelemetryInfo::serializeToJson() const { - return llvm::json::Object{ - {"ClientInfo", - { - {"SessionId", SessionId}, - {"request_name", request_name}, - {"error_msg", error_msg}, - { - "EventStats", - { - {"Start", Stats.Start.time_since_epoch().count()}, - {"End", ToNanosecOrZero(Stats.End)}, - }, - }, - }}}; +void LldbBaseTelemetryInfo::serialize(Serializer &serializer) const { + serializer.writeInt32("EntryKind", getKind()); + serializer.writeString("SessionId", SessionId); } -std::string TargetTelemetryInfo::ToString() const { - std::string exit_or_load_desc; - if (ExitDesc.has_value()) { - // If this entry was emitted for an exit - exit_or_load_desc = " process_duration: " + GetDuration(Stats) + - ExitDescToString(&(ExitDesc.value())) + "\n"; - } else { - // This was emitted for a load event. - // See if it was the start-load or end-load entry - if (Stats.End.has_value()) { - exit_or_load_desc = - " startup_init_duration: " + GetDuration(Stats) + "\n"; - } else { - exit_or_load_desc = " startup_init_start\n"; - } - } - return LldbBaseTelemetryInfo::ToString() + "\n" + - ("[TargetTelemetryInfo]\n") + - (" target_uuid: " + target_uuid + "\n") + - (" file_format: " + file_format + "\n") + - (" binary_path: " + binary_path + "\n") + - (" binary_size: " + std::to_string(binary_size) + "\n") + - exit_or_load_desc; +void DebuggerTelemetryInfo::serialize(Serializer &serializer) const { + LldbBaseTelemetryInfo::serialize(serializer); + serializer.writeString("username", username); + serializer.writeString("lldb_path", lldb_path); + serializer.writeString("cwd", cwd); + serializer.writeSizeT("start", stats.start.time_since_epoch().count()); + serializer.writeSizeT("end", ToNanosecOrZero(stats.end)); } -llvm::json::Object TargetTelemetryInfo::serializeToJson() const { - return llvm::json::Object{{ - "TargetInfo", - { - {"SessionId", SessionId}, - {"target_uuid", target_uuid}, - {"binary_path", binary_path}, - {"binary_size", binary_size}, - // TODO: fill in more - }, - }}; +void ClientTelemetryInfo::serialize(Serializer &serializer) const { + LldbBaseTelemetryInfo::serialize(serializer); + serializer.writeString("request_name", request_name); + serializer.writeString("error_msg", error_msg); + serializer.writeSizeT("start", stats.start.time_since_epoch().count()); + serializer.writeSizeT("end", ToNanosecOrZero(stats.end)); } -std::string CommandTelemetryInfo::ToString() const { - // Whether this entry was emitted at the start or at the end of the - // command-execution. - if (Stats.End.has_value()) { - return LldbBaseTelemetryInfo::ToString() + "\n" + - ("[CommandTelemetryInfo] - END\n") + - (" target_uuid: " + target_uuid + "\n") + - (" command_uuid: " + command_uuid + "\n") + - (" command_name: " + command_name + "\n") + - (" args: " + args + "\n") + - (" command_runtime: " + GetDuration(Stats) + "\n") + - (ExitDesc.has_value() ? ExitDescToString(&(ExitDesc.value())) - : "no exit-description") + - "\n"; - } else { - return LldbBaseTelemetryInfo::ToString() + "\n" + - ("[CommandTelemetryInfo] - START\n") + - (" target_uuid: " + target_uuid + "\n") + - (" command_uuid: " + command_uuid + "\n") + - (" original_command: " + original_command + "\n"); - } +void TargetTelemetryInfo::serialize(Serializer &serializer) const { + LldbBaseTelemetryInfo::serialize(serializer); + serializer.writeString("target_uuid", target_uuid); + serializer.writeString("binary_path", binary_path); + serializer.writeSizeT("binary_size", binary_size); } -llvm::json::Object CommandTelemetryInfo::serializeToJson() const { - llvm::json::Object inner; - - inner.insert({"SessionId", SessionId}); - inner.insert({"target_uuid", target_uuid}); - inner.insert({"command_uuid", command_uuid}); - inner.insert({"args", args}); - inner.insert({"original_command", original_command}); - inner.insert({ - "EventStats", - { - {"Start", Stats.Start.time_since_epoch().count()}, - {"End", ToNanosecOrZero(Stats.End)}, - }, - }); +void CommandTelemetryInfo::serialize(Serializer &serializer) const { + LldbBaseTelemetryInfo::serialize(serializer); + serializer.writeString("target_uuid", target_uuid); + serializer.writeString("command_uuid", command_uuid); + serializer.writeString("args", args); + serializer.writeString("original_command", original_command); + serializer.writeSizeT("start", stats.start.time_since_epoch().count()); + serializer.writeSizeT("end", ToNanosecOrZero(stats.end)); // If this entry was emitted at the end of the command-execution, // then calculate the runtime too. - if (Stats.End.has_value()) { - inner.insert( - {"command_runtime", (Stats.End.value() - Stats.Start).count()}); - if (ExitDesc.has_value()) { - inner.insert({"exit_code", ExitDesc->ExitCode}); - inner.insert({"exit_msg", ExitDesc->Description}); - inner.insert({"return_status", static_cast(ret_status)}); + if (stats.end.has_value()) { + serializer.writeSizeT("command_runtime", + (stats.end.value() - stats.start).count()); + if (exit_desc.has_value()) { + serializer.writeInt32("exit_code", exit_desc->exit_code); + serializer.writeString("exit_msg", exit_desc->description); + serializer.writeInt32("return_status", static_cast(ret_status)); } } - - return llvm::json::Object{{"CommandInfo", std::move(inner)}}; } -std::string MiscTelemetryInfo::ToString() const { - std::string ret; - llvm::raw_string_ostream ret_strm(ret); - ret_strm << LldbBaseTelemetryInfo::ToString() << "\n[MiscTelemetryInfo]\n" - << " target_uuid: " << target_uuid + "\n" - << " meta_data:\n"; - for (const auto &kv : meta_data) { - ret_strm << " " << kv.first << ": " << kv.second << "\n"; - } - return ret; -} - -llvm::json::Object MiscTelemetryInfo::serializeToJson() const { - llvm::json::Object meta_data_obj; - - for (const auto &kv : meta_data) - meta_data_obj.insert({kv.first, kv.second}); - - return llvm::json::Object{{ - "MiscInfo", - { - {"SessionId", SessionId}, - {"target_uuid", target_uuid}, - {"meta_data", std::move(meta_data_obj)}, - }, - }}; +void MiscTelemetryInfo::serialize(Serializer &serializer) const { + LldbBaseTelemetryInfo::serialize(serializer); + serializer.writeString("target_uuid", target_uuid); + serializer.writeKeyValueMap("meta_data", meta_data); } std::unique_ptr @@ -270,4 +136,5 @@ LldbTelemeter::CreateInstance(lldb_private::Debugger *debugger) { return vendor->CreateTelemeter(debugger); } + } // namespace lldb_private diff --git a/lldb/source/Core/TelemetryVendor.cpp b/lldb/source/Core/TelemetryVendor.cpp index fa40ee2ad045e..170f2bbe239ab 100644 --- a/lldb/source/Core/TelemetryVendor.cpp +++ b/lldb/source/Core/TelemetryVendor.cpp @@ -40,6 +40,7 @@ #include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/Twine.h" +#include "llvm/Support/Error.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/JSON.h" #include "llvm/Support/LineIterator.h" @@ -54,10 +55,9 @@ using namespace lldb_private; namespace { +using ::llvm::Error; +using ::llvm::StringRef; using ::llvm::telemetry::Destination; -using ::llvm::telemetry::EventStats; -using ::llvm::telemetry::ExitDescription; -using ::llvm::telemetry::SteadyTimePoint; using ::llvm::telemetry::TelemetryInfo; // No-op logger to use when users disable telemetry @@ -68,27 +68,21 @@ class NoOpTelemeter : public LldbTelemeter { } NoOpTelemeter(Debugger *debugger) {} - void logStartup(llvm::StringRef tool_path, TelemetryInfo *entry) override {} - void logExit(llvm::StringRef tool_path, TelemetryInfo *entry) override {} - - void LogProcessExit(int status, llvm::StringRef exit_string, EventStats stats, - Target *target_ptr) override {} - void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod, - EventStats stats) override {} - void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod, - EventStats stats) override {} - - void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command, - EventStats stats, Target *target_ptr) override {} - void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name, - llvm::StringRef command_args, EventStats stats, - Target *target_ptr, CommandReturnObject *result) override { - } + void addDestination(std::unique_ptr destination) override {} + llvm::Error dispatch(TelemetryInfo *entry) override { + return Error::success(); + } + void LogStartup(DebuggerTelemetryInfo *entry) override {} + void LogExit(DebuggerTelemetryInfo *entry) override {} + void LogMainExecutableLoadStart(TargetTelemetryInfo *entry) override {} + void LogMainExecutableLoadEnd(TargetTelemetryInfo *entry) override {} + void LogProcessExit(TargetTelemetryInfo *entry) override {} + void LogCommandStart(CommandTelemetryInfo *entry) override {} + void LogCommandEnd(CommandTelemetryInfo *entry) override {} void LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override {} - void addDestination(llvm::telemetry::Destination *destination) override {} std::string GetNextUUID() override { return ""; } }; @@ -100,33 +94,26 @@ class BasicTelemeter : public LldbTelemeter { virtual ~BasicTelemeter() = default; - void logStartup(llvm::StringRef lldb_path, TelemetryInfo *entry) override; - void logExit(llvm::StringRef lldb_path, TelemetryInfo *entry) override; - - void LogProcessExit(int status, llvm::StringRef exit_string, EventStats stats, - Target *target_ptr) override; - void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod, - EventStats stats) override; - void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod, - EventStats stats) override; - - void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command, - EventStats stats, Target *target_ptr) override; - void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name, - llvm::StringRef command_args, EventStats stats, - Target *target_ptr, CommandReturnObject *result) override; - + void LogStartup(DebuggerTelemetryInfo *entry) override; + void LogExit(DebuggerTelemetryInfo *entry) override; + void LogMainExecutableLoadStart(TargetTelemetryInfo *entry) override; + void LogMainExecutableLoadEnd(TargetTelemetryInfo *entry) override; + void LogProcessExit(TargetTelemetryInfo *entry) override; + void LogCommandStart(CommandTelemetryInfo *entry) override; + void LogCommandEnd(CommandTelemetryInfo *entry) override; void LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override; - void addDestination(Destination *destination) override { - m_destinations.push_back(destination); + void addDestination(std::unique_ptr destination) override { + m_destinations.push_back(std::move(destination)); } std::string GetNextUUID() override { return std::to_string(uuid_seed.fetch_add(1)); } + llvm::Error dispatch(TelemetryInfo *entry) override; + protected: BasicTelemeter(std::unique_ptr config, Debugger *debugger); @@ -137,12 +124,9 @@ class BasicTelemeter : public LldbTelemeter { template EntrySubType MakeBaseEntry() { EntrySubType entry; entry.SessionId = m_session_uuid; - entry.Counter = counter.fetch_add(1); return entry; } - void EmitToDestinations(const TelemetryInfo *entry); - std::unique_ptr m_config; Debugger *m_debugger; const std::string m_session_uuid; @@ -151,20 +135,86 @@ class BasicTelemeter : public LldbTelemeter { // counting number of entries. std::atomic counter = 0; - std::vector m_destinations; + std::vector> m_destinations; std::atomic uuid_seed = 0; }; +class BasicSerializer : public Serializer { +public: + const std::string &getString() { return Buffer; } + + llvm::Error start() override { + if (started) + return llvm::createStringError("Serializer already in use"); + started = true; + Buffer.clear(); + return Error::success(); + } + + void writeBool(StringRef KeyName, bool Value) override { + writeHelper(KeyName, Value); + } + + void writeInt32(StringRef KeyName, int Value) override { + writeHelper(KeyName, Value); + } + + void writeSizeT(StringRef KeyName, size_t Value) override { + writeHelper(KeyName, Value); + } + void writeString(StringRef KeyName, StringRef Value) override { + assert(started && "serializer not started"); + } + + void + writeKeyValueMap(StringRef KeyName, + const std::map &Value) override { + std::string Inner; + for (auto kv : Value) { + writeHelper(StringRef(kv.first), StringRef(kv.second), &Inner); + } + writeHelper(KeyName, StringRef(Inner)); + } + + llvm::Error finish() override { + if (!started) + return llvm::createStringError("Serializer not currently in use"); + started = false; + return Error::success(); + } + +private: + template + void writeHelper(StringRef Name, T Value, std::string *Buff) { + assert(started && "serializer not started"); + Buff->append((Name + ":" + llvm::Twine(Value) + "\n").str()); + } + + template void writeHelper(StringRef Name, T Value) { + writeHelper(Name, Value, &Buffer); + } + + bool started = false; + std::string Buffer; +}; + class StreamTelemetryDestination : public Destination { public: - StreamTelemetryDestination(llvm::raw_ostream &os, std::string desc) - : os(os), desc(desc) {} - llvm::Error emitEntry(const llvm::telemetry::TelemetryInfo *entry) override { + StreamTelemetryDestination(llvm::raw_ostream &os) : os(os) {} + llvm::Error + receiveEntry(const llvm::telemetry::TelemetryInfo *entry) override { // Upstream Telemetry should not leak anything other than the // basic data, unless running in test mode. #ifdef TEST_TELEMETRY - os << entry->ToString() << "\n"; + if (Error err = serializer.start()) { + return err; + } + entry->serialize(serializer); + if (Error err = serializer.finish()) { + return err; + } + os << serializer.getString() << "\n"; #else os << "session_uuid: " << entry->SessionId << "\n"; @@ -173,11 +223,11 @@ class StreamTelemetryDestination : public Destination { return llvm::ErrorSuccess(); } - std::string name() const override { return desc; } + llvm::StringLiteral name() const override { return "StreamDestination"; } private: llvm::raw_ostream &os; - const std::string desc; + BasicSerializer serializer; }; static std::string MakeUUID(lldb_private::Debugger *debugger) { @@ -207,145 +257,115 @@ BasicTelemeter::CreateInstance(std::unique_ptr config, lldb_private::Debugger *debugger) { BasicTelemeter *ins = new BasicTelemeter(std::move(config), debugger); - for (const std ::string &dest : config->AdditionalDestinations) { - if (dest == "stdout") { - ins->addDestination( - new StreamTelemetryDestination(llvm::outs(), "stdout")); - } else if (dest == "stderr") { - ins->addDestination( - new StreamTelemetryDestination(llvm::errs(), "stderr")); - } else { - // TODO: handle custom values as needed? - } - } return std::unique_ptr(ins); } -void BasicTelemeter::EmitToDestinations(const TelemetryInfo *entry) { - // TODO: can do this in a separate thread (need to own the ptrs!). - for (Destination *destination : m_destinations) { - llvm::Error err = destination->emitEntry(entry); +llvm::Error BasicTelemeter::dispatch(TelemetryInfo *entry) { + entry->SessionId = m_session_uuid; + + for (auto &destination : m_destinations) { + llvm::Error err = destination->receiveEntry(entry); if (err) { - LLDB_LOG(GetLog(LLDBLog::Object), - "Error emitting to destination(name = {0})", - destination->name()); + return std::move(err); } } + return Error::success(); } -void BasicTelemeter::logStartup(llvm::StringRef lldb_path, - TelemetryInfo *entry) { - startup_lldb_path = lldb_path.str(); - lldb_private::DebuggerTelemetryInfo startup_info = - MakeBaseEntry(); - +void BasicTelemeter::LogStartup(DebuggerTelemetryInfo *entry) { UserIDResolver &resolver = lldb_private::HostInfo::GetUserIDResolver(); std::optional opt_username = resolver.GetUserName(lldb_private::HostInfo::GetUserID()); if (opt_username) - startup_info.username = *opt_username; + entry->username = *opt_username; - startup_info.lldb_git_sha = - lldb_private::GetVersion(); // TODO: find the real git sha - startup_info.lldb_path = startup_lldb_path; - startup_info.Stats = entry->Stats; + entry->lldb_git_sha = + lldb_private::GetVersion(); // TODO: find the real git sha? llvm::SmallString<64> cwd; if (!llvm::sys::fs::current_path(cwd)) { - startup_info.cwd = cwd.c_str(); + entry->cwd = cwd.c_str(); } else { MiscTelemetryInfo misc_info = MakeBaseEntry(); misc_info.meta_data["internal_errors"] = "Cannot determine CWD"; - EmitToDestinations(&misc_info); + if (auto er = dispatch(&misc_info)) { + LLDB_LOG(GetLog(LLDBLog::Object), + "Failed to dispatch misc-info from startup"); + } } - EmitToDestinations(&startup_info); + if (auto er = dispatch(entry)) { + LLDB_LOG(GetLog(LLDBLog::Object), "Failed to dispatch entry from startup"); + } // Optional part CollectMiscBuildInfo(); } -void BasicTelemeter::logExit(llvm::StringRef lldb_path, TelemetryInfo *entry) { - // we should be shutting down the same instance that we started?! - // llvm::Assert(startup_lldb_path == lldb_path.str()); - - lldb_private::DebuggerTelemetryInfo exit_info = - MakeBaseEntry(); - exit_info.Stats = entry->Stats; - exit_info.lldb_path = startup_lldb_path; +void BasicTelemeter::LogExit(DebuggerTelemetryInfo *entry) { if (auto *selected_target = m_debugger->GetSelectedExecutionContext().GetTargetPtr()) { if (!selected_target->IsDummyTarget()) { const lldb::ProcessSP proc = selected_target->GetProcessSP(); if (proc == nullptr) { // no process has been launched yet. - exit_info.ExitDesc = {-1, "no process launched."}; + entry->exit_desc = {-1, "no process launched."}; } else { - exit_info.ExitDesc = {proc->GetExitStatus(), ""}; + entry->exit_desc = {proc->GetExitStatus(), ""}; if (const char *description = proc->GetExitDescription()) - exit_info.ExitDesc->Description = std::string(description); + entry->exit_desc->description = std::string(description); } } } - EmitToDestinations(&exit_info); + dispatch(entry); } -void BasicTelemeter::LogProcessExit(int status, llvm::StringRef exit_string, - EventStats stats, Target *target_ptr) { - lldb_private::TargetTelemetryInfo exit_info = - MakeBaseEntry(); - exit_info.Stats = std::move(stats); - exit_info.target_uuid = - target_ptr && !target_ptr->IsDummyTarget() - ? target_ptr->GetExecutableModule()->GetUUID().GetAsString() +void BasicTelemeter::LogProcessExit(TargetTelemetryInfo *entry) { + entry->target_uuid = + entry->target_ptr && !entry->target_ptr->IsDummyTarget() + ? entry->target_ptr->GetExecutableModule()->GetUUID().GetAsString() : ""; - exit_info.ExitDesc = {status, exit_string.str()}; - EmitToDestinations(&exit_info); + dispatch(entry); } void BasicTelemeter::CollectMiscBuildInfo() { // collecting use-case specific data } -void BasicTelemeter::LogMainExecutableLoadStart(lldb::ModuleSP exec_mod, - EventStats stats) { - TargetTelemetryInfo target_info = MakeBaseEntry(); - target_info.Stats = std::move(stats); - target_info.binary_path = - exec_mod->GetFileSpec().GetPathAsConstString().GetCString(); - target_info.file_format = exec_mod->GetArchitecture().GetArchitectureName(); - target_info.target_uuid = exec_mod->GetUUID().GetAsString(); - if (auto err = llvm::sys::fs::file_size(exec_mod->GetFileSpec().GetPath(), - target_info.binary_size)) { +void BasicTelemeter::LogMainExecutableLoadStart(TargetTelemetryInfo *entry) { + entry->binary_path = + entry->exec_mod->GetFileSpec().GetPathAsConstString().GetCString(); + entry->file_format = entry->exec_mod->GetArchitecture().GetArchitectureName(); + entry->target_uuid = entry->exec_mod->GetUUID().GetAsString(); + if (auto err = llvm::sys::fs::file_size( + entry->exec_mod->GetFileSpec().GetPath(), entry->binary_size)) { // If there was error obtaining it, just reset the size to 0. // Maybe log the error too? - target_info.binary_size = 0; + entry->binary_size = 0; } - EmitToDestinations(&target_info); + dispatch(entry); } -void BasicTelemeter::LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod, - EventStats stats) { - TargetTelemetryInfo target_info = MakeBaseEntry(); - target_info.Stats = std::move(stats); - target_info.binary_path = +void BasicTelemeter::LogMainExecutableLoadEnd(TargetTelemetryInfo *entry) { + lldb::ModuleSP exec_mod = entry->exec_mod; + entry->binary_path = exec_mod->GetFileSpec().GetPathAsConstString().GetCString(); - target_info.file_format = exec_mod->GetArchitecture().GetArchitectureName(); - target_info.target_uuid = exec_mod->GetUUID().GetAsString(); - target_info.binary_size = exec_mod->GetObjectFile()->GetByteSize(); + entry->file_format = exec_mod->GetArchitecture().GetArchitectureName(); + entry->target_uuid = exec_mod->GetUUID().GetAsString(); + entry->binary_size = exec_mod->GetObjectFile()->GetByteSize(); - EmitToDestinations(&target_info); + dispatch(entry); - // Collect some more info, might be useful? + // Collect some more info, might be useful? MiscTelemetryInfo misc_info = MakeBaseEntry(); misc_info.target_uuid = exec_mod->GetUUID().GetAsString(); misc_info.meta_data["symtab_index_time"] = std::to_string(exec_mod->GetSymtabIndexTime().get().count()); misc_info.meta_data["symtab_parse_time"] = std::to_string(exec_mod->GetSymtabParseTime().get().count()); - EmitToDestinations(&misc_info); + dispatch(&misc_info); } void BasicTelemeter::LogClientTelemetry( @@ -386,55 +406,39 @@ void BasicTelemeter::LogClientTelemetry( client_info.error_msg = error_msg->str(); */ - EmitToDestinations(&client_info); + dispatch(&client_info); } -void BasicTelemeter::LogCommandStart(llvm::StringRef uuid, - llvm::StringRef original_command, - EventStats stats, Target *target_ptr) { - - lldb_private::CommandTelemetryInfo command_info = - MakeBaseEntry(); - +void BasicTelemeter::LogCommandStart(CommandTelemetryInfo *entry) { // If we have a target attached to this command, then get the UUID. - command_info.target_uuid = ""; - if (target_ptr && target_ptr->GetExecutableModule() != nullptr) { - command_info.target_uuid = - target_ptr->GetExecutableModule()->GetUUID().GetAsString(); + if (entry->target_ptr && + entry->target_ptr->GetExecutableModule() != nullptr) { + entry->target_uuid = + entry->target_ptr->GetExecutableModule()->GetUUID().GetAsString(); + } else { + entry->target_uuid = ""; } - command_info.command_uuid = uuid.str(); - command_info.original_command = original_command.str(); - command_info.Stats = std::move(stats); - EmitToDestinations(&command_info); + dispatch(entry); } -void BasicTelemeter::LogCommandEnd(llvm::StringRef uuid, - llvm::StringRef command_name, - llvm::StringRef command_args, - EventStats stats, Target *target_ptr, - CommandReturnObject *result) { - - lldb_private::CommandTelemetryInfo command_info = - MakeBaseEntry(); - +void BasicTelemeter::LogCommandEnd(CommandTelemetryInfo *entry) { // If we have a target attached to this command, then get the UUID. - command_info.target_uuid = ""; - if (target_ptr && target_ptr->GetExecutableModule() != nullptr) { - command_info.target_uuid = - target_ptr->GetExecutableModule()->GetUUID().GetAsString(); + if (entry->target_ptr && + entry->target_ptr->GetExecutableModule() != nullptr) { + entry->target_uuid = + entry->target_ptr->GetExecutableModule()->GetUUID().GetAsString(); + } else { + entry->target_uuid = ""; } - command_info.command_uuid = uuid.str(); - command_info.command_name = command_name.str(); - command_info.args = command_args.str(); - command_info.Stats = std::move(stats); - command_info.ExitDesc = {result->Succeeded() ? 0 : -1, ""}; - if (llvm::StringRef error_data = result->GetErrorData(); + + entry->exit_desc = {entry->result->Succeeded() ? 0 : -1, ""}; + if (llvm::StringRef error_data = entry->result->GetErrorData(); !error_data.empty()) { - command_info.ExitDesc->Description = error_data.str(); + entry->exit_desc->description = error_data.str(); } - command_info.ret_status = result->GetStatus(); - EmitToDestinations(&command_info); + entry->ret_status = entry->result->GetStatus(); + dispatch(entry); } } // namespace @@ -480,7 +484,6 @@ static bool ParseBoolValue(llvm::StringRef str, llvm::StringRef label) { std::unique_ptr TelemetryVendor::GetTelemetryConfig() { // Telemetry is disabled by default. bool enable_telemetry = false; - std::vector additional_destinations; // Look in the $HOME/.lldb_telemetry_config file to populate the struct llvm::SmallString<64> init_file; @@ -495,15 +498,6 @@ std::unique_ptr TelemetryVendor::GetTelemetryConfig() { for (; !iter.is_at_eof(); ++iter) { if (iter->starts_with("enable_telemetry:")) { enable_telemetry = ParseBoolValue(*iter, "enable_telemetry:"); - } else if (iter->starts_with("destination:")) { - llvm::StringRef dest = ParseValue(*iter, "destination:"); - if (dest == "stdout") { - additional_destinations.push_back("stdout"); - } else if (dest == "stderr") { - additional_destinations.push_back("stderr"); - } else { - additional_destinations.push_back(dest.str()); - } } } } else { @@ -517,9 +511,7 @@ std::unique_ptr TelemetryVendor::GetTelemetryConfig() { enable_telemetry = true; #endif - auto config = std::make_unique(); - config->EnableTelemetry = enable_telemetry; - config->AdditionalDestinations = std::move(additional_destinations); + auto config = std::make_unique(enable_telemetry); // Now apply any additional vendor config, if available. // TODO: cache the Config? (given it's not going to change after LLDB starts diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp index 3df40629792ce..75819382d8e61 100644 --- a/lldb/source/Interpreter/CommandInterpreter.cpp +++ b/lldb/source/Interpreter/CommandInterpreter.cpp @@ -1886,15 +1886,18 @@ bool CommandInterpreter::HandleCommand(const char *command_line, LazyBool lazy_add_to_history, CommandReturnObject &result, bool force_repeat_command) { - llvm::telemetry::EventStats start_command_stats( - std::chrono::steady_clock::now()); + EventStats start_command_stats(std::chrono::steady_clock::now()); LldbTelemeter *telemeter = GetDebugger().GetTelemeter(); // Generate a UUID for this command so the logger can match // the start/end entries correctly. const std::string command_uuid = telemeter->GetNextUUID(); - telemeter->LogCommandStart(command_uuid, command_line, start_command_stats, - GetExecutionContext().GetTargetPtr()); + CommandTelemetryInfo start_entry; + start_entry.stats = start_command_stats; + start_entry.command_uuid = command_uuid; + start_entry.target_ptr = GetExecutionContext().GetTargetPtr(); + + telemeter->LogCommandStart(&start_entry); std::string command_string(command_line); std::string original_command_string(command_line); @@ -1902,17 +1905,22 @@ bool CommandInterpreter::HandleCommand(const char *command_line, CommandObject *cmd_obj = nullptr; auto log_on_exit = llvm::make_scope_exit([&]() { - llvm::telemetry::EventStats end_command_stats( - start_command_stats.Start, std::chrono::steady_clock::now()); + EventStats end_command_stats(start_command_stats.start, + std::chrono::steady_clock::now()); llvm::StringRef command_name = cmd_obj ? cmd_obj->GetCommandName() : ""; // TODO: this is logging the time the command-handler finishes. // But we may want a finer-grain durations too? // (ie., the execute_time recorded below?) - telemeter->LogCommandEnd(command_uuid, command_name, parsed_command_args, - end_command_stats, - GetExecutionContext().GetTargetPtr(), &result); + CommandTelemetryInfo end_entry; + end_entry.stats = end_command_stats; + end_entry.command_uuid = command_uuid; + end_entry.command_name = command_name.str(); + end_entry.args = parsed_command_args; + end_entry.target_ptr = GetExecutionContext().GetTargetPtr(); + end_entry.result = &result; + telemeter->LogCommandEnd(&end_entry); }); Log *log = GetLog(LLDBLog::Commands); diff --git a/lldb/source/Target/Process.cpp b/lldb/source/Target/Process.cpp index 7e88851430b2b..65731826e81f2 100644 --- a/lldb/source/Target/Process.cpp +++ b/lldb/source/Target/Process.cpp @@ -1068,8 +1068,7 @@ bool Process::SetExitStatus(int status, llvm::StringRef exit_string) { // Use a mutex to protect setting the exit status. std::lock_guard guard(m_exit_status_mutex); - llvm::telemetry::SteadyTimePoint start_time = - std::chrono::steady_clock::now(); + SteadyTimePoint start_time = std::chrono::steady_clock::now(); Log *log(GetLog(LLDBLog::State | LLDBLog::Process)); LLDB_LOG(log, "(plugin = {0} status = {1} ({1:x8}), description=\"{2}\")", @@ -1100,10 +1099,11 @@ bool Process::SetExitStatus(int status, llvm::StringRef exit_string) { // Allow subclasses to do some cleanup DidExit(); - llvm::telemetry::EventStats stats = {start_time, - std::chrono::steady_clock::now()}; - GetTarget().GetDebugger().GetTelemeter()->LogProcessExit(status, exit_string, - stats, &GetTarget()); + TargetTelemetryInfo entry; + entry.stats = {start_time, std::chrono::steady_clock::now()}; + entry.exit_desc = {status, exit_string.str()}; + entry.target_ptr = &GetTarget(); + GetTarget().GetDebugger().GetTelemeter()->LogProcessExit(&entry); return true; } diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp index 3e36dd8046fc5..760dd37f89214 100644 --- a/lldb/source/Target/Target.cpp +++ b/lldb/source/Target/Target.cpp @@ -1555,19 +1555,23 @@ void Target::DidExec() { void Target::SetExecutableModule(ModuleSP &executable_sp, LoadDependentFiles load_dependent_files) { - llvm::telemetry::EventStats load_executable_stats( - std::chrono::steady_clock::now()); + EventStats load_executable_stats(std::chrono::steady_clock::now()); Log *log = GetLog(LLDBLog::Target); ClearModules(false); if (executable_sp) { - m_debugger.GetTelemeter()->LogMainExecutableLoadStart( - executable_sp, load_executable_stats); + TargetTelemetryInfo load_start; + load_start.stats = load_executable_stats; + load_start.exec_mod = executable_sp; + + m_debugger.GetTelemeter()->LogMainExecutableLoadStart(&load_start); auto log_on_exit = llvm::make_scope_exit([&]() { - load_executable_stats.End = std::chrono::steady_clock::now(); - m_debugger.GetTelemeter()->LogMainExecutableLoadEnd( - executable_sp, load_executable_stats); + load_executable_stats.end = std::chrono::steady_clock::now(); + TargetTelemetryInfo load_end; + load_end.stats = load_executable_stats; + load_end.exec_mod = executable_sp; + m_debugger.GetTelemeter()->LogMainExecutableLoadEnd(&load_end); }); } if (executable_sp) { diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index f682e1cdf5caf..f6198bd4d3401 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -8,125 +8,87 @@ /// /// \file /// This file provides the basic framework for Telemetry -/// -/// It comprises of three important structs/classes: -/// -/// - Telemeter: The class responsible for collecting and forwarding -/// telemery data. -/// - TelemetryInfo: data courier -/// - TelemetryConfig: this stores configurations on Telemeter. -/// /// Refer to its documentation at llvm/docs/Telemetry.rst for more details. //===---------------------------------------------------------------------===// #ifndef LLVM_TELEMETRY_TELEMETRY_H #define LLVM_TELEMETRY_TELEMETRY_H -#include -#include -#include -#include -#include - #include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Error.h" #include "llvm/Support/JSON.h" +#include +#include +#include namespace llvm { namespace telemetry { +class Serializer { +public: + virtual llvm::Error start() = 0; + virtual void writeBool(StringRef KeyName, bool Value) = 0; + virtual void writeInt32(StringRef KeyName, int Value) = 0; + virtual void writeSizeT(StringRef KeyName, size_t Value) = 0; + virtual void writeString(StringRef KeyName, StringRef Value) = 0; + virtual void + writeKeyValueMap(StringRef KeyName, + const std::map &Value) = 0; + virtual llvm::Error finish() = 0; +}; + /// Configuration for the Telemeter class. /// This stores configurations from both users and vendors and is passed -/// to the Telemeter upon contruction. (Any changes to the config after -/// the Telemeter's construction will not have effect on it). +/// to the Telemeter upon construction. (Any changes to the config after +/// the Telemeter's construction will not have any effect on it). /// /// This struct can be extended as needed to add additional configuration /// points specific to a vendor's implementation. struct Config { // If true, telemetry will be enabled. - bool EnableTelemetry; - - // Implementation-defined names of additional destinations to send - // telemetry data (Could be stdout, stderr, or some local paths, etc). - // - // These strings will be interpreted by the vendor's code. - // So the users must pick the from their vendor's pre-defined - // set of Destinations. - std::vector AdditionalDestinations; -}; + const bool EnableTelemetry; + Config(bool E) : EnableTelemetry(E) {} -/// Defines a convenient type for timestamp of various events. -/// This is used by the EventStats below. -using SteadyTimePoint = std::chrono::time_point; - -/// Various time (and possibly memory) statistics of an event. -struct EventStats { - // REQUIRED: Start time of an event - SteadyTimePoint Start; - // OPTIONAL: End time of an event - may be empty if not meaningful. - std::optional End; - // TBD: could add some memory stats here too? - - EventStats() = default; - EventStats(SteadyTimePoint Start) : Start(Start) {} - EventStats(SteadyTimePoint Start, SteadyTimePoint End) - : Start(Start), End(End) {} -}; - -/// Describes the exit signal of an event. -/// This is used by TelemetryInfo below. -struct ExitDescription { - int ExitCode; - std::string Description; + virtual std::string makeSessionId() { return "0"; } }; /// For isa, dyn_cast, etc operations on TelemetryInfo. typedef unsigned KindType; /// This struct is used by TelemetryInfo to support isa<>, dyn_cast<> /// operations. -/// It is defined as a struct(rather than an enum) because it is -/// expectend to be extended by subclasses which may have +/// It is defined as a struct (rather than an enum) because it is +/// expected to be extended by subclasses which may have /// additional TelemetryInfo types defined to describe different events. struct EntryKind { static const KindType Base = 0; }; /// TelemetryInfo is the data courier, used to move instrumented data -/// from the tool being monitored to the Telemery framework. +/// from the tool being monitored to the Telemetry framework. /// /// This base class contains only the basic set of telemetry data. -/// Downstream implementations can add more fields as needed to describe -/// additional events. +/// Downstream implementations can define more subclasses with +/// additional fields to describe different events and concepts. /// -/// For eg., The LLDB debugger can define a DebugCommandInfo subclass +/// For example, The LLDB debugger can define a DebugCommandInfo subclass /// which has additional fields about the debug-command being instrumented, /// such as `CommandArguments` or `CommandName`. struct TelemetryInfo { // This represents a unique-id, conventionally corresponding to - // a tools' session - ie., every time the tool starts until it exits. + // a tool's session - i.e., every time the tool starts until it exits. // - // Note: a tool could have mutliple sessions running at once, in which + // Note: a tool could have multiple sessions running at once, in which // case, these shall be multiple sets of TelemetryInfo with multiple unique // ids. // // Different usages can assign different types of IDs to this field. std::string SessionId; - // Time/memory statistics of this event. - EventStats Stats; - - std::optional ExitDesc; - - // Counting number of entries. - // (For each set of entries with the same SessionId, this value should - // be unique for each entry) - size_t Counter; - TelemetryInfo() = default; - ~TelemetryInfo() = default; + virtual ~TelemetryInfo() = default; - virtual json::Object serializeToJson() const; + virtual void serialize(Serializer &serializer) const; // For isa, dyn_cast, etc, operations. virtual KindType getKind() const { return EntryKind::Base; } @@ -146,23 +108,23 @@ struct TelemetryInfo { class Destination { public: virtual ~Destination() = default; - virtual Error emitEntry(const TelemetryInfo *Entry) = 0; - virtual std::string name() const = 0; + virtual Error receiveEntry(const TelemetryInfo *Entry) = 0; + virtual llvm::StringLiteral name() const = 0; }; /// This class is the main interaction point between any LLVM tool /// and this framework. /// It is responsible for collecting telemetry data from the tool being /// monitored and transmitting the data elsewhere. -class Telemeter { +class Manager { public: - // Invoked upon tool startup - virtual void logStartup(llvm::StringRef ToolPath, TelemetryInfo *Entry) = 0; - - // Invoked upon tool exit. - virtual void logExit(llvm::StringRef ToolPath, TelemetryInfo *Entry) = 0; + // Dispatch Telemetry data to the Destination(s). + // The argument is non-const because the Manager may add or remove + // data from the entry. + virtual Error dispatch(TelemetryInfo *Entry) = 0; - virtual void addDestination(Destination *Destination) = 0; + // Register a Destination. + virtual void addDestination(std::unique_ptr Destination) = 0; }; } // namespace telemetry diff --git a/llvm/lib/Telemetry/Telemetry.cpp b/llvm/lib/Telemetry/Telemetry.cpp index b05a7483c7408..b7ee3c2bb1778 100644 --- a/llvm/lib/Telemetry/Telemetry.cpp +++ b/llvm/lib/Telemetry/Telemetry.cpp @@ -3,11 +3,9 @@ namespace llvm { namespace telemetry { -llvm::json::Object TelemetryInfo::serializeToJson() const { - return json::Object{ - {"SessionId", SessionId}, - }; -}; +void TelemetryInfo::serialize(Serializer &serializer) const { + serializer.writeString("SessionId", SessionId); +} } // namespace telemetry } // namespace llvm From 66b77aacd97f810de54bb43f4581c3af5a38d0f2 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 11 Dec 2024 15:02:20 -0500 Subject: [PATCH 26/30] fix vtable issue --- lldb/include/lldb/Core/Telemetry.h | 47 +++-- lldb/source/Core/Debugger.cpp | 3 +- lldb/source/Core/Telemetry.cpp | 220 +++++++++++++++++++++- lldb/source/Core/TelemetryVendor.cpp | 271 +-------------------------- 4 files changed, 247 insertions(+), 294 deletions(-) diff --git a/lldb/include/lldb/Core/Telemetry.h b/lldb/include/lldb/Core/Telemetry.h index bc28f8356bba4..3c25e47bf8f98 100644 --- a/lldb/include/lldb/Core/Telemetry.h +++ b/lldb/include/lldb/Core/Telemetry.h @@ -10,6 +10,7 @@ #ifndef LLDB_CORE_TELEMETRY_H #define LLDB_CORE_TELEMETRY_H +#include #include #include #include @@ -254,21 +255,21 @@ class LldbTelemeter : public llvm::telemetry::Manager { /// implementation for testing. /// /// See also lldb_private::TelemetryVendor. - static std::unique_ptr CreateInstance(Debugger *debugger); - - virtual ~LldbTelemeter() = default; + static std::unique_ptr + CreateInstance(std::unique_ptr config, + Debugger *debugger); /// To be invoked upon LLDB startup. - virtual void LogStartup(DebuggerTelemetryInfo *entry) = 0; + virtual void LogStartup(DebuggerTelemetryInfo *entry); /// To be invoked upon LLDB exit. - virtual void LogExit(DebuggerTelemetryInfo *entry) = 0; + virtual void LogExit(DebuggerTelemetryInfo *entry); /// To be invoked upon loading the main executable module. /// We log in a fire-n-forget fashion so that if the load /// crashes, we don't lose the entry. - virtual void LogMainExecutableLoadStart(TargetTelemetryInfo *entry) = 0; - virtual void LogMainExecutableLoadEnd(TargetTelemetryInfo *entry) = 0; + virtual void LogMainExecutableLoadStart(TargetTelemetryInfo *entry); + virtual void LogMainExecutableLoadEnd(TargetTelemetryInfo *entry); /// To be invoked upon process exit. virtual void LogProcessExit(TargetTelemetryInfo *entry); @@ -276,23 +277,33 @@ class LldbTelemeter : public llvm::telemetry::Manager { /// Invoked for each command /// We log in a fire-n-forget fashion so that if the command execution /// crashes, we don't lose the entry. - virtual void LogCommandStart(CommandTelemetryInfo *entry) = 0; - virtual void LogCommandEnd(CommandTelemetryInfo *entry) = 0; - - virtual std::string GetNextUUID() = 0; + virtual void LogCommandStart(CommandTelemetryInfo *entry); + virtual void LogCommandEnd(CommandTelemetryInfo *entry); /// For client (eg., SB API) to send telemetry entries. virtual void - LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) = 0; + LogClientTelemetry(const lldb_private::StructuredDataImpl &entry); + + virtual std::string GetNextUUID() { + return std::to_string(uuid_seed.fetch_add(1)); + } + + llvm::Error dispatch(TelemetryInfo *entry) override; + void addDestination(std::unique_ptr destination) override; + +protected: + LldbTelemeter(std::unique_ptr config, + Debugger *debugger); + LldbTelemeter() = default; + virtual void CollectMiscBuildInfo(); private: - const std::string SessionId; - std::vector> destinations; + std::atomic uuid_seed = 0; + std::unique_ptr m_config; + const std::string m_session_uuid; + Debugger *m_debugger; + std::vector> m_destinations; }; -/// Logger configs. This should be overriden by vendor's specific config. -/// The default (upstream) config will have telemetry disabled. -llvm::telemetry::Config *GetTelemetryConfig(); - } // namespace lldb_private #endif // LLDB_CORE_TELEMETRY_H diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp index 4e664f24cb2bb..55bd02f85818e 100644 --- a/lldb/source/Core/Debugger.cpp +++ b/lldb/source/Core/Debugger.cpp @@ -17,6 +17,7 @@ #include "lldb/Core/PluginManager.h" #include "lldb/Core/Progress.h" #include "lldb/Core/StreamAsynchronousIO.h" +#include "lldb/Core/TelemetryVendor.h" #include "lldb/DataFormatters/DataVisualization.h" #include "lldb/Expression/REPL.h" #include "lldb/Host/File.h" @@ -867,7 +868,7 @@ Debugger::Debugger(lldb::LogOutputCallback log_callback, void *baton) m_broadcaster(m_broadcaster_manager_sp, GetStaticBroadcasterClass().str()), m_forward_listener_sp(), m_clear_once(), - m_telemeter(LldbTelemeter::CreateInstance(this)) { + m_telemeter(TelemetryVendor::FindPlugin()->CreateTelemeter(this)) { // Initialize the debugger properties as early as possible as other parts of // LLDB will start querying them during construction. m_collection_sp->Initialize(g_debugger_properties); diff --git a/lldb/source/Core/Telemetry.cpp b/lldb/source/Core/Telemetry.cpp index 2bcf00ae6a261..093c12e1d428b 100644 --- a/lldb/source/Core/Telemetry.cpp +++ b/lldb/source/Core/Telemetry.cpp @@ -39,6 +39,7 @@ #include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/Twine.h" +#include "llvm/Support/Error.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/JSON.h" #include "llvm/Support/LineIterator.h" @@ -50,6 +51,7 @@ namespace lldb_private { +using ::llvm::Error; using ::llvm::telemetry::Destination; using ::llvm::telemetry::TelemetryInfo; @@ -124,17 +126,219 @@ void MiscTelemetryInfo::serialize(Serializer &serializer) const { serializer.writeKeyValueMap("meta_data", meta_data); } -std::unique_ptr -LldbTelemeter::CreateInstance(lldb_private::Debugger *debugger) { - // TODO: do we cache the plugin? - TelemetryVendor *vendor = TelemetryVendor::FindPlugin(); - if (vendor == nullptr) { +static std::string MakeUUID(lldb_private::Debugger *debugger) { + std::string ret; + uint8_t random_bytes[16]; + if (auto ec = llvm::getRandomBytes(random_bytes, 16)) { LLDB_LOG(GetLog(LLDBLog::Object), - "Failed to find a TelemetryVendor plugin instance"); - return nullptr; + "Failed to generate random bytes for UUID: {0}", ec.message()); + // fallback to using timestamp + debugger ID. + ret = std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count()) + + "_" + std::to_string(debugger->GetID()); + } else { + ret = lldb_private::UUID(random_bytes).GetAsString(); } - return vendor->CreateTelemeter(debugger); + return ret; +} + +LldbTelemeter::LldbTelemeter(std::unique_ptr config, + lldb_private::Debugger *debugger) + : m_config(std::move(config)), m_debugger(debugger), + m_session_uuid(MakeUUID(debugger)) {} + +std::unique_ptr +LldbTelemeter::CreateInstance(std::unique_ptr config, + lldb_private::Debugger *debugger) { + + LldbTelemeter *ins = new LldbTelemeter(std::move(config), debugger); + + return std::unique_ptr(ins); +} + +llvm::Error LldbTelemeter::dispatch(TelemetryInfo *entry) { + entry->SessionId = m_session_uuid; + + for (auto &destination : m_destinations) { + llvm::Error err = destination->receiveEntry(entry); + if (err) { + return std::move(err); + } + } + return Error::success(); +} + +void LldbTelemeter::addDestination(std::unique_ptr destination) { + m_destinations.push_back(std::move(destination)); +} + +void LldbTelemeter::LogStartup(DebuggerTelemetryInfo *entry) { + UserIDResolver &resolver = lldb_private::HostInfo::GetUserIDResolver(); + std::optional opt_username = + resolver.GetUserName(lldb_private::HostInfo::GetUserID()); + if (opt_username) + entry->username = *opt_username; + + entry->lldb_git_sha = + lldb_private::GetVersion(); // TODO: find the real git sha? + + llvm::SmallString<64> cwd; + if (!llvm::sys::fs::current_path(cwd)) { + entry->cwd = cwd.c_str(); + } else { + MiscTelemetryInfo misc_info; + misc_info.meta_data["internal_errors"] = "Cannot determine CWD"; + if (auto er = dispatch(&misc_info)) { + LLDB_LOG(GetLog(LLDBLog::Object), + "Failed to dispatch misc-info from startup"); + } + } + + if (auto er = dispatch(entry)) { + LLDB_LOG(GetLog(LLDBLog::Object), "Failed to dispatch entry from startup"); + } + + // Optional part + CollectMiscBuildInfo(); +} + +void LldbTelemeter::LogExit(DebuggerTelemetryInfo *entry) { + if (auto *selected_target = + m_debugger->GetSelectedExecutionContext().GetTargetPtr()) { + if (!selected_target->IsDummyTarget()) { + const lldb::ProcessSP proc = selected_target->GetProcessSP(); + if (proc == nullptr) { + // no process has been launched yet. + entry->exit_desc = {-1, "no process launched."}; + } else { + entry->exit_desc = {proc->GetExitStatus(), ""}; + if (const char *description = proc->GetExitDescription()) + entry->exit_desc->description = std::string(description); + } + } + } + dispatch(entry); +} + +void LldbTelemeter::LogProcessExit(TargetTelemetryInfo *entry) { + entry->target_uuid = + entry->target_ptr && !entry->target_ptr->IsDummyTarget() + ? entry->target_ptr->GetExecutableModule()->GetUUID().GetAsString() + : ""; + + dispatch(entry); +} + +void LldbTelemeter::CollectMiscBuildInfo() { + // collecting use-case specific data +} + +void LldbTelemeter::LogMainExecutableLoadStart(TargetTelemetryInfo *entry) { + entry->binary_path = + entry->exec_mod->GetFileSpec().GetPathAsConstString().GetCString(); + entry->file_format = entry->exec_mod->GetArchitecture().GetArchitectureName(); + entry->target_uuid = entry->exec_mod->GetUUID().GetAsString(); + if (auto err = llvm::sys::fs::file_size( + entry->exec_mod->GetFileSpec().GetPath(), entry->binary_size)) { + // If there was error obtaining it, just reset the size to 0. + // Maybe log the error too? + entry->binary_size = 0; + } + dispatch(entry); +} + +void LldbTelemeter::LogMainExecutableLoadEnd(TargetTelemetryInfo *entry) { + lldb::ModuleSP exec_mod = entry->exec_mod; + entry->binary_path = + exec_mod->GetFileSpec().GetPathAsConstString().GetCString(); + entry->file_format = exec_mod->GetArchitecture().GetArchitectureName(); + entry->target_uuid = exec_mod->GetUUID().GetAsString(); + entry->binary_size = exec_mod->GetObjectFile()->GetByteSize(); + + dispatch(entry); + + // Collect some more info, might be useful? + MiscTelemetryInfo misc_info; + misc_info.target_uuid = exec_mod->GetUUID().GetAsString(); + misc_info.meta_data["symtab_index_time"] = + std::to_string(exec_mod->GetSymtabIndexTime().get().count()); + misc_info.meta_data["symtab_parse_time"] = + std::to_string(exec_mod->GetSymtabParseTime().get().count()); + dispatch(&misc_info); +} + +void LldbTelemeter::LogClientTelemetry( + const lldb_private::StructuredDataImpl &entry) { + // TODO: pull the dictionary out of entry + ClientTelemetryInfo client_info; + /* + std::optional request_name = entry.getString("request_name"); + if (!request_name.has_value()) { + MiscTelemetryInfo misc_info = MakeBaseEntry(); + misc_info.meta_data["internal_errors"] = + "Cannot determine request name from client entry"; + // TODO: Dump the errornous entry to stderr too? + EmitToDestinations(&misc_info); + return; + } + client_info.request_name = request_name->str(); + + std::optional start_time = entry.getInteger("start_time"); + std::optional end_time = entry.getInteger("end_time"); + + if (!start_time.has_value() || !end_time.has_value()) { + MiscTelemetryInfo misc_info = MakeBaseEntry(); + misc_info.meta_data["internal_errors"] = + "Cannot determine start/end time from client entry"; + EmitToDestinations(&misc_info); + return; + } + + SteadyTimePoint epoch; + client_info.Stats.Start = + epoch + std::chrono::nanoseconds(static_cast(*start_time)); + client_info.Stats.End = + epoch + std::chrono::nanoseconds(static_cast(*end_time)); + + std::optional error_msg = entry.getString("error"); + if (error_msg.has_value()) + client_info.error_msg = error_msg->str(); + */ + + dispatch(&client_info); +} + +void LldbTelemeter::LogCommandStart(CommandTelemetryInfo *entry) { + // If we have a target attached to this command, then get the UUID. + if (entry->target_ptr && + entry->target_ptr->GetExecutableModule() != nullptr) { + entry->target_uuid = + entry->target_ptr->GetExecutableModule()->GetUUID().GetAsString(); + } else { + entry->target_uuid = ""; + } + + dispatch(entry); +} + +void LldbTelemeter::LogCommandEnd(CommandTelemetryInfo *entry) { + // If we have a target attached to this command, then get the UUID. + if (entry->target_ptr && + entry->target_ptr->GetExecutableModule() != nullptr) { + entry->target_uuid = + entry->target_ptr->GetExecutableModule()->GetUUID().GetAsString(); + } else { + entry->target_uuid = ""; + } + + entry->exit_desc = {entry->result->Succeeded() ? 0 : -1, ""}; + if (llvm::StringRef error_data = entry->result->GetErrorData(); + !error_data.empty()) { + entry->exit_desc->description = error_data.str(); + } + entry->ret_status = entry->result->GetStatus(); + dispatch(entry); } } // namespace lldb_private diff --git a/lldb/source/Core/TelemetryVendor.cpp b/lldb/source/Core/TelemetryVendor.cpp index 170f2bbe239ab..ec2f23830ea44 100644 --- a/lldb/source/Core/TelemetryVendor.cpp +++ b/lldb/source/Core/TelemetryVendor.cpp @@ -8,6 +8,7 @@ #include "lldb/Core/TelemetryVendor.h" +#include #include #include #include @@ -68,6 +69,8 @@ class NoOpTelemeter : public LldbTelemeter { } NoOpTelemeter(Debugger *debugger) {} + NoOpTelemeter() = default; + ~NoOpTelemeter() = default; void addDestination(std::unique_ptr destination) override {} llvm::Error dispatch(TelemetryInfo *entry) override { @@ -86,60 +89,6 @@ class NoOpTelemeter : public LldbTelemeter { std::string GetNextUUID() override { return ""; } }; -class BasicTelemeter : public LldbTelemeter { -public: - static std::unique_ptr - CreateInstance(std::unique_ptr config, - Debugger *debugger); - - virtual ~BasicTelemeter() = default; - - void LogStartup(DebuggerTelemetryInfo *entry) override; - void LogExit(DebuggerTelemetryInfo *entry) override; - void LogMainExecutableLoadStart(TargetTelemetryInfo *entry) override; - void LogMainExecutableLoadEnd(TargetTelemetryInfo *entry) override; - void LogProcessExit(TargetTelemetryInfo *entry) override; - void LogCommandStart(CommandTelemetryInfo *entry) override; - void LogCommandEnd(CommandTelemetryInfo *entry) override; - void - LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override; - - void addDestination(std::unique_ptr destination) override { - m_destinations.push_back(std::move(destination)); - } - - std::string GetNextUUID() override { - return std::to_string(uuid_seed.fetch_add(1)); - } - - llvm::Error dispatch(TelemetryInfo *entry) override; - -protected: - BasicTelemeter(std::unique_ptr config, - Debugger *debugger); - - void CollectMiscBuildInfo(); - -private: - template EntrySubType MakeBaseEntry() { - EntrySubType entry; - entry.SessionId = m_session_uuid; - return entry; - } - - std::unique_ptr m_config; - Debugger *m_debugger; - const std::string m_session_uuid; - std::string startup_lldb_path; - - // counting number of entries. - std::atomic counter = 0; - - std::vector> m_destinations; - - std::atomic uuid_seed = 0; -}; - class BasicSerializer : public Serializer { public: const std::string &getString() { return Buffer; } @@ -229,218 +178,6 @@ class StreamTelemetryDestination : public Destination { llvm::raw_ostream &os; BasicSerializer serializer; }; - -static std::string MakeUUID(lldb_private::Debugger *debugger) { - std::string ret; - uint8_t random_bytes[16]; - if (auto ec = llvm::getRandomBytes(random_bytes, 16)) { - LLDB_LOG(GetLog(LLDBLog::Object), - "Failed to generate random bytes for UUID: {0}", ec.message()); - // fallback to using timestamp + debugger ID. - ret = std::to_string( - std::chrono::steady_clock::now().time_since_epoch().count()) + - "_" + std::to_string(debugger->GetID()); - } else { - ret = lldb_private::UUID(random_bytes).GetAsString(); - } - - return ret; -} - -BasicTelemeter::BasicTelemeter(std::unique_ptr config, - lldb_private::Debugger *debugger) - : m_config(std::move(config)), m_debugger(debugger), - m_session_uuid(MakeUUID(debugger)) {} - -std::unique_ptr -BasicTelemeter::CreateInstance(std::unique_ptr config, - lldb_private::Debugger *debugger) { - - BasicTelemeter *ins = new BasicTelemeter(std::move(config), debugger); - - return std::unique_ptr(ins); -} - -llvm::Error BasicTelemeter::dispatch(TelemetryInfo *entry) { - entry->SessionId = m_session_uuid; - - for (auto &destination : m_destinations) { - llvm::Error err = destination->receiveEntry(entry); - if (err) { - return std::move(err); - } - } - return Error::success(); -} - -void BasicTelemeter::LogStartup(DebuggerTelemetryInfo *entry) { - UserIDResolver &resolver = lldb_private::HostInfo::GetUserIDResolver(); - std::optional opt_username = - resolver.GetUserName(lldb_private::HostInfo::GetUserID()); - if (opt_username) - entry->username = *opt_username; - - entry->lldb_git_sha = - lldb_private::GetVersion(); // TODO: find the real git sha? - - llvm::SmallString<64> cwd; - if (!llvm::sys::fs::current_path(cwd)) { - entry->cwd = cwd.c_str(); - } else { - MiscTelemetryInfo misc_info = MakeBaseEntry(); - misc_info.meta_data["internal_errors"] = "Cannot determine CWD"; - if (auto er = dispatch(&misc_info)) { - LLDB_LOG(GetLog(LLDBLog::Object), - "Failed to dispatch misc-info from startup"); - } - } - - if (auto er = dispatch(entry)) { - LLDB_LOG(GetLog(LLDBLog::Object), "Failed to dispatch entry from startup"); - } - - // Optional part - CollectMiscBuildInfo(); -} - -void BasicTelemeter::LogExit(DebuggerTelemetryInfo *entry) { - if (auto *selected_target = - m_debugger->GetSelectedExecutionContext().GetTargetPtr()) { - if (!selected_target->IsDummyTarget()) { - const lldb::ProcessSP proc = selected_target->GetProcessSP(); - if (proc == nullptr) { - // no process has been launched yet. - entry->exit_desc = {-1, "no process launched."}; - } else { - entry->exit_desc = {proc->GetExitStatus(), ""}; - if (const char *description = proc->GetExitDescription()) - entry->exit_desc->description = std::string(description); - } - } - } - dispatch(entry); -} - -void BasicTelemeter::LogProcessExit(TargetTelemetryInfo *entry) { - entry->target_uuid = - entry->target_ptr && !entry->target_ptr->IsDummyTarget() - ? entry->target_ptr->GetExecutableModule()->GetUUID().GetAsString() - : ""; - - dispatch(entry); -} - -void BasicTelemeter::CollectMiscBuildInfo() { - // collecting use-case specific data -} - -void BasicTelemeter::LogMainExecutableLoadStart(TargetTelemetryInfo *entry) { - entry->binary_path = - entry->exec_mod->GetFileSpec().GetPathAsConstString().GetCString(); - entry->file_format = entry->exec_mod->GetArchitecture().GetArchitectureName(); - entry->target_uuid = entry->exec_mod->GetUUID().GetAsString(); - if (auto err = llvm::sys::fs::file_size( - entry->exec_mod->GetFileSpec().GetPath(), entry->binary_size)) { - // If there was error obtaining it, just reset the size to 0. - // Maybe log the error too? - entry->binary_size = 0; - } - dispatch(entry); -} - -void BasicTelemeter::LogMainExecutableLoadEnd(TargetTelemetryInfo *entry) { - lldb::ModuleSP exec_mod = entry->exec_mod; - entry->binary_path = - exec_mod->GetFileSpec().GetPathAsConstString().GetCString(); - entry->file_format = exec_mod->GetArchitecture().GetArchitectureName(); - entry->target_uuid = exec_mod->GetUUID().GetAsString(); - entry->binary_size = exec_mod->GetObjectFile()->GetByteSize(); - - dispatch(entry); - - // Collect some more info, might be useful? - MiscTelemetryInfo misc_info = MakeBaseEntry(); - misc_info.target_uuid = exec_mod->GetUUID().GetAsString(); - misc_info.meta_data["symtab_index_time"] = - std::to_string(exec_mod->GetSymtabIndexTime().get().count()); - misc_info.meta_data["symtab_parse_time"] = - std::to_string(exec_mod->GetSymtabParseTime().get().count()); - dispatch(&misc_info); -} - -void BasicTelemeter::LogClientTelemetry( - const lldb_private::StructuredDataImpl &entry) { - // TODO: pull the dictionary out of entry - ClientTelemetryInfo client_info = MakeBaseEntry(); - /* - std::optional request_name = entry.getString("request_name"); - if (!request_name.has_value()) { - MiscTelemetryInfo misc_info = MakeBaseEntry(); - misc_info.meta_data["internal_errors"] = - "Cannot determine request name from client entry"; - // TODO: Dump the errornous entry to stderr too? - EmitToDestinations(&misc_info); - return; - } - client_info.request_name = request_name->str(); - - std::optional start_time = entry.getInteger("start_time"); - std::optional end_time = entry.getInteger("end_time"); - - if (!start_time.has_value() || !end_time.has_value()) { - MiscTelemetryInfo misc_info = MakeBaseEntry(); - misc_info.meta_data["internal_errors"] = - "Cannot determine start/end time from client entry"; - EmitToDestinations(&misc_info); - return; - } - - SteadyTimePoint epoch; - client_info.Stats.Start = - epoch + std::chrono::nanoseconds(static_cast(*start_time)); - client_info.Stats.End = - epoch + std::chrono::nanoseconds(static_cast(*end_time)); - - std::optional error_msg = entry.getString("error"); - if (error_msg.has_value()) - client_info.error_msg = error_msg->str(); - */ - - dispatch(&client_info); -} - -void BasicTelemeter::LogCommandStart(CommandTelemetryInfo *entry) { - // If we have a target attached to this command, then get the UUID. - if (entry->target_ptr && - entry->target_ptr->GetExecutableModule() != nullptr) { - entry->target_uuid = - entry->target_ptr->GetExecutableModule()->GetUUID().GetAsString(); - } else { - entry->target_uuid = ""; - } - - dispatch(entry); -} - -void BasicTelemeter::LogCommandEnd(CommandTelemetryInfo *entry) { - // If we have a target attached to this command, then get the UUID. - if (entry->target_ptr && - entry->target_ptr->GetExecutableModule() != nullptr) { - entry->target_uuid = - entry->target_ptr->GetExecutableModule()->GetUUID().GetAsString(); - } else { - entry->target_uuid = ""; - } - - entry->exit_desc = {entry->result->Succeeded() ? 0 : -1, ""}; - if (llvm::StringRef error_data = entry->result->GetErrorData(); - !error_data.empty()) { - entry->exit_desc->description = error_data.str(); - } - entry->ret_status = entry->result->GetStatus(); - dispatch(entry); -} - } // namespace TelemetryVendor *TelemetryVendor::FindPlugin() { @@ -534,5 +271,5 @@ TelemetryVendor::CreateTelemeter(Debugger *debugger) { return NoOpTelemeter::CreateInstance(debugger); } - return BasicTelemeter::CreateInstance(std::move(config), debugger); + return LldbTelemeter::CreateInstance(std::move(config), debugger); } From 67a0ed0be4394d406740bc20a1af7f539b8281f3 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 11 Dec 2024 15:13:54 -0500 Subject: [PATCH 27/30] more renaming for consistency --- lldb/include/lldb/Core/Debugger.h | 4 +- lldb/include/lldb/Core/Telemetry.h | 14 +++---- lldb/include/lldb/Core/TelemetryVendor.h | 6 +-- lldb/source/Core/Debugger.cpp | 9 +++-- lldb/source/Core/Telemetry.cpp | 38 ++++++++++--------- lldb/source/Core/TelemetryVendor.cpp | 12 +++--- .../source/Interpreter/CommandInterpreter.cpp | 8 ++-- lldb/source/Target/Process.cpp | 2 +- lldb/source/Target/Target.cpp | 4 +- 9 files changed, 50 insertions(+), 47 deletions(-) diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h index 7a500aa9cd787..82f0806e96e3b 100644 --- a/lldb/include/lldb/Core/Debugger.h +++ b/lldb/include/lldb/Core/Debugger.h @@ -153,7 +153,7 @@ class Debugger : public std::enable_shared_from_this, repro::DataRecorder *GetInputRecorder(); - LldbTelemeter *GetTelemeter() { return m_telemeter.get(); } + TelemetryManager *GetTelemetryManager() { return m_telemetry_manager.get(); } void SendClientTelemetry(const lldb_private::StructuredDataImpl &entry); @@ -767,7 +767,7 @@ class Debugger : public std::enable_shared_from_this, eBroadcastBitEventThreadIsListening = (1 << 0), }; - std::unique_ptr m_telemeter; + std::unique_ptr m_telemetry_manager; private: // Use Debugger::CreateInstance() to get a shared pointer to a new debugger diff --git a/lldb/include/lldb/Core/Telemetry.h b/lldb/include/lldb/Core/Telemetry.h index 3c25e47bf8f98..241d957672b6c 100644 --- a/lldb/include/lldb/Core/Telemetry.h +++ b/lldb/include/lldb/Core/Telemetry.h @@ -246,16 +246,16 @@ struct MiscTelemetryInfo : public LldbBaseTelemetryInfo { /// The base Telemetry manager instance in LLDB /// This class declares additional instrumentation points /// applicable to LLDB. -class LldbTelemeter : public llvm::telemetry::Manager { +class TelemetryManager : public llvm::telemetry::Manager { public: - /// Creates an instance of LldbTelemeter. + /// Creates an instance of TelemetryManager. /// This uses the plugin registry to find an instance: /// - If a vendor supplies a implementation, it will use it. /// - If not, it will either return a no-op instance or a basic /// implementation for testing. /// /// See also lldb_private::TelemetryVendor. - static std::unique_ptr + static std::unique_ptr CreateInstance(std::unique_ptr config, Debugger *debugger); @@ -292,16 +292,16 @@ class LldbTelemeter : public llvm::telemetry::Manager { void addDestination(std::unique_ptr destination) override; protected: - LldbTelemeter(std::unique_ptr config, - Debugger *debugger); - LldbTelemeter() = default; + TelemetryManager(std::unique_ptr config, + Debugger *debugger); + TelemetryManager() = default; virtual void CollectMiscBuildInfo(); private: std::atomic uuid_seed = 0; std::unique_ptr m_config; - const std::string m_session_uuid; Debugger *m_debugger; + const std::string m_session_uuid; std::vector> m_destinations; }; diff --git a/lldb/include/lldb/Core/TelemetryVendor.h b/lldb/include/lldb/Core/TelemetryVendor.h index 3adc77af07e4f..e484dca07296b 100644 --- a/lldb/include/lldb/Core/TelemetryVendor.h +++ b/lldb/include/lldb/Core/TelemetryVendor.h @@ -27,10 +27,10 @@ class TelemetryVendor : public PluginInterface { std::unique_ptr GetTelemetryConfig(); - // Creates an LldbTelemeter instance. + // Creates a TelemetryManager instance. // Vendor plugins can override this to create customized instance as needed. - virtual std::unique_ptr - CreateTelemeter(lldb_private::Debugger *debugger); + virtual std::unique_ptr + CreateTelemetryManager(lldb_private::Debugger *debugger); protected: // Returns a vendor-specific config which may or may not be the same as the diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp index 55bd02f85818e..9a56dbdb048da 100644 --- a/lldb/source/Core/Debugger.cpp +++ b/lldb/source/Core/Debugger.cpp @@ -745,7 +745,7 @@ DebuggerSP Debugger::CreateInstance(lldb::LogOutputCallback log_callback, DebuggerTelemetryInfo entry; entry.lldb_path = HostInfo::GetProgramFileSpec().GetPath(); entry.stats = {start_time, std::chrono::steady_clock::now()}; - debugger_sp->m_telemeter->LogStartup(&entry); + debugger_sp->m_telemetry_manager->LogStartup(&entry); return debugger_sp; } @@ -868,7 +868,8 @@ Debugger::Debugger(lldb::LogOutputCallback log_callback, void *baton) m_broadcaster(m_broadcaster_manager_sp, GetStaticBroadcasterClass().str()), m_forward_listener_sp(), m_clear_once(), - m_telemeter(TelemetryVendor::FindPlugin()->CreateTelemeter(this)) { + m_telemetry_manager( + TelemetryVendor::FindPlugin()->CreateTelemetryManager(this)) { // Initialize the debugger properties as early as possible as other parts of // LLDB will start querying them during construction. m_collection_sp->Initialize(g_debugger_properties); @@ -989,7 +990,7 @@ void Debugger::Clear() { DebuggerTelemetryInfo entry; entry.stats = {quit_start_time, std::chrono::steady_clock::now()}; entry.lldb_path = HostInfo::GetProgramFileSpec().GetPath(); - m_telemeter->LogExit(&entry); + m_telemetry_manager->LogExit(&entry); }); } @@ -2258,5 +2259,5 @@ llvm::ThreadPoolInterface &Debugger::GetThreadPool() { void Debugger::SendClientTelemetry( const lldb_private::StructuredDataImpl &entry) { - m_telemeter->LogClientTelemetry(entry); + m_telemetry_manager->LogClientTelemetry(entry); } diff --git a/lldb/source/Core/Telemetry.cpp b/lldb/source/Core/Telemetry.cpp index 093c12e1d428b..52b767d663ba6 100644 --- a/lldb/source/Core/Telemetry.cpp +++ b/lldb/source/Core/Telemetry.cpp @@ -143,21 +143,22 @@ static std::string MakeUUID(lldb_private::Debugger *debugger) { return ret; } -LldbTelemeter::LldbTelemeter(std::unique_ptr config, - lldb_private::Debugger *debugger) +TelemetryManager::TelemetryManager( + std::unique_ptr config, + lldb_private::Debugger *debugger) : m_config(std::move(config)), m_debugger(debugger), m_session_uuid(MakeUUID(debugger)) {} -std::unique_ptr -LldbTelemeter::CreateInstance(std::unique_ptr config, - lldb_private::Debugger *debugger) { +std::unique_ptr TelemetryManager::CreateInstance( + std::unique_ptr config, + lldb_private::Debugger *debugger) { - LldbTelemeter *ins = new LldbTelemeter(std::move(config), debugger); + TelemetryManager *ins = new TelemetryManager(std::move(config), debugger); - return std::unique_ptr(ins); + return std::unique_ptr(ins); } -llvm::Error LldbTelemeter::dispatch(TelemetryInfo *entry) { +llvm::Error TelemetryManager::dispatch(TelemetryInfo *entry) { entry->SessionId = m_session_uuid; for (auto &destination : m_destinations) { @@ -169,11 +170,12 @@ llvm::Error LldbTelemeter::dispatch(TelemetryInfo *entry) { return Error::success(); } -void LldbTelemeter::addDestination(std::unique_ptr destination) { +void TelemetryManager::addDestination( + std::unique_ptr destination) { m_destinations.push_back(std::move(destination)); } -void LldbTelemeter::LogStartup(DebuggerTelemetryInfo *entry) { +void TelemetryManager::LogStartup(DebuggerTelemetryInfo *entry) { UserIDResolver &resolver = lldb_private::HostInfo::GetUserIDResolver(); std::optional opt_username = resolver.GetUserName(lldb_private::HostInfo::GetUserID()); @@ -203,7 +205,7 @@ void LldbTelemeter::LogStartup(DebuggerTelemetryInfo *entry) { CollectMiscBuildInfo(); } -void LldbTelemeter::LogExit(DebuggerTelemetryInfo *entry) { +void TelemetryManager::LogExit(DebuggerTelemetryInfo *entry) { if (auto *selected_target = m_debugger->GetSelectedExecutionContext().GetTargetPtr()) { if (!selected_target->IsDummyTarget()) { @@ -221,7 +223,7 @@ void LldbTelemeter::LogExit(DebuggerTelemetryInfo *entry) { dispatch(entry); } -void LldbTelemeter::LogProcessExit(TargetTelemetryInfo *entry) { +void TelemetryManager::LogProcessExit(TargetTelemetryInfo *entry) { entry->target_uuid = entry->target_ptr && !entry->target_ptr->IsDummyTarget() ? entry->target_ptr->GetExecutableModule()->GetUUID().GetAsString() @@ -230,11 +232,11 @@ void LldbTelemeter::LogProcessExit(TargetTelemetryInfo *entry) { dispatch(entry); } -void LldbTelemeter::CollectMiscBuildInfo() { +void TelemetryManager::CollectMiscBuildInfo() { // collecting use-case specific data } -void LldbTelemeter::LogMainExecutableLoadStart(TargetTelemetryInfo *entry) { +void TelemetryManager::LogMainExecutableLoadStart(TargetTelemetryInfo *entry) { entry->binary_path = entry->exec_mod->GetFileSpec().GetPathAsConstString().GetCString(); entry->file_format = entry->exec_mod->GetArchitecture().GetArchitectureName(); @@ -248,7 +250,7 @@ void LldbTelemeter::LogMainExecutableLoadStart(TargetTelemetryInfo *entry) { dispatch(entry); } -void LldbTelemeter::LogMainExecutableLoadEnd(TargetTelemetryInfo *entry) { +void TelemetryManager::LogMainExecutableLoadEnd(TargetTelemetryInfo *entry) { lldb::ModuleSP exec_mod = entry->exec_mod; entry->binary_path = exec_mod->GetFileSpec().GetPathAsConstString().GetCString(); @@ -268,7 +270,7 @@ void LldbTelemeter::LogMainExecutableLoadEnd(TargetTelemetryInfo *entry) { dispatch(&misc_info); } -void LldbTelemeter::LogClientTelemetry( +void TelemetryManager::LogClientTelemetry( const lldb_private::StructuredDataImpl &entry) { // TODO: pull the dictionary out of entry ClientTelemetryInfo client_info; @@ -309,7 +311,7 @@ void LldbTelemeter::LogClientTelemetry( dispatch(&client_info); } -void LldbTelemeter::LogCommandStart(CommandTelemetryInfo *entry) { +void TelemetryManager::LogCommandStart(CommandTelemetryInfo *entry) { // If we have a target attached to this command, then get the UUID. if (entry->target_ptr && entry->target_ptr->GetExecutableModule() != nullptr) { @@ -322,7 +324,7 @@ void LldbTelemeter::LogCommandStart(CommandTelemetryInfo *entry) { dispatch(entry); } -void LldbTelemeter::LogCommandEnd(CommandTelemetryInfo *entry) { +void TelemetryManager::LogCommandEnd(CommandTelemetryInfo *entry) { // If we have a target attached to this command, then get the UUID. if (entry->target_ptr && entry->target_ptr->GetExecutableModule() != nullptr) { diff --git a/lldb/source/Core/TelemetryVendor.cpp b/lldb/source/Core/TelemetryVendor.cpp index ec2f23830ea44..63bca9352e17e 100644 --- a/lldb/source/Core/TelemetryVendor.cpp +++ b/lldb/source/Core/TelemetryVendor.cpp @@ -62,10 +62,10 @@ using ::llvm::telemetry::Destination; using ::llvm::telemetry::TelemetryInfo; // No-op logger to use when users disable telemetry -class NoOpTelemeter : public LldbTelemeter { +class NoOpTelemeter : public TelemetryManager { public: - static std::unique_ptr CreateInstance(Debugger *debugger) { - return std::unique_ptr(new NoOpTelemeter(debugger)); + static std::unique_ptr CreateInstance(Debugger *debugger) { + return std::unique_ptr(new NoOpTelemeter(debugger)); } NoOpTelemeter(Debugger *debugger) {} @@ -263,13 +263,13 @@ TelemetryVendor::GetVendorSpecificConfig( return std::move(default_config); } -std::unique_ptr -TelemetryVendor::CreateTelemeter(Debugger *debugger) { +std::unique_ptr +TelemetryVendor::CreateTelemetryManager(Debugger *debugger) { auto config = GetTelemetryConfig(); if (!config->EnableTelemetry) { return NoOpTelemeter::CreateInstance(debugger); } - return LldbTelemeter::CreateInstance(std::move(config), debugger); + return TelemetryManager::CreateInstance(std::move(config), debugger); } diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp index 75819382d8e61..518113d354e5b 100644 --- a/lldb/source/Interpreter/CommandInterpreter.cpp +++ b/lldb/source/Interpreter/CommandInterpreter.cpp @@ -1887,17 +1887,17 @@ bool CommandInterpreter::HandleCommand(const char *command_line, CommandReturnObject &result, bool force_repeat_command) { EventStats start_command_stats(std::chrono::steady_clock::now()); - LldbTelemeter *telemeter = GetDebugger().GetTelemeter(); + TelemetryManager *manager = GetDebugger().GetTelemetryManager(); // Generate a UUID for this command so the logger can match // the start/end entries correctly. - const std::string command_uuid = telemeter->GetNextUUID(); + const std::string command_uuid = manager->GetNextUUID(); CommandTelemetryInfo start_entry; start_entry.stats = start_command_stats; start_entry.command_uuid = command_uuid; start_entry.target_ptr = GetExecutionContext().GetTargetPtr(); - telemeter->LogCommandStart(&start_entry); + manager->LogCommandStart(&start_entry); std::string command_string(command_line); std::string original_command_string(command_line); @@ -1920,7 +1920,7 @@ bool CommandInterpreter::HandleCommand(const char *command_line, end_entry.args = parsed_command_args; end_entry.target_ptr = GetExecutionContext().GetTargetPtr(); end_entry.result = &result; - telemeter->LogCommandEnd(&end_entry); + manager->LogCommandEnd(&end_entry); }); Log *log = GetLog(LLDBLog::Commands); diff --git a/lldb/source/Target/Process.cpp b/lldb/source/Target/Process.cpp index 65731826e81f2..35c683c818374 100644 --- a/lldb/source/Target/Process.cpp +++ b/lldb/source/Target/Process.cpp @@ -1103,7 +1103,7 @@ bool Process::SetExitStatus(int status, llvm::StringRef exit_string) { entry.stats = {start_time, std::chrono::steady_clock::now()}; entry.exit_desc = {status, exit_string.str()}; entry.target_ptr = &GetTarget(); - GetTarget().GetDebugger().GetTelemeter()->LogProcessExit(&entry); + GetTarget().GetDebugger().GetTelemetryManager()->LogProcessExit(&entry); return true; } diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp index 760dd37f89214..3a5a511cb46cc 100644 --- a/lldb/source/Target/Target.cpp +++ b/lldb/source/Target/Target.cpp @@ -1564,14 +1564,14 @@ void Target::SetExecutableModule(ModuleSP &executable_sp, load_start.stats = load_executable_stats; load_start.exec_mod = executable_sp; - m_debugger.GetTelemeter()->LogMainExecutableLoadStart(&load_start); + m_debugger.GetTelemetryManager()->LogMainExecutableLoadStart(&load_start); auto log_on_exit = llvm::make_scope_exit([&]() { load_executable_stats.end = std::chrono::steady_clock::now(); TargetTelemetryInfo load_end; load_end.stats = load_executable_stats; load_end.exec_mod = executable_sp; - m_debugger.GetTelemeter()->LogMainExecutableLoadEnd(&load_end); + m_debugger.GetTelemetryManager()->LogMainExecutableLoadEnd(&load_end); }); } if (executable_sp) { From f7f70049977dad2aaa8d7d3f8636f88b39ac6901 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 11 Dec 2024 15:21:00 -0500 Subject: [PATCH 28/30] remove unused --- lldb/source/Core/Telemetry.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lldb/source/Core/Telemetry.cpp b/lldb/source/Core/Telemetry.cpp index 52b767d663ba6..7eee10d92f06d 100644 --- a/lldb/source/Core/Telemetry.cpp +++ b/lldb/source/Core/Telemetry.cpp @@ -55,13 +55,6 @@ using ::llvm::Error; using ::llvm::telemetry::Destination; using ::llvm::telemetry::TelemetryInfo; -static std::string GetDuration(const EventStats &stats) { - if (stats.end.has_value()) - return std::to_string((stats.end.value() - stats.start).count()) + - "(nanosec)"; - return ""; -} - static size_t ToNanosecOrZero(const std::optional &Point) { if (!Point.has_value()) return 0; From 949b9943d6bf1c8650aeecbe971a44ef84c15e90 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 17 Dec 2024 14:10:55 -0500 Subject: [PATCH 29/30] addressed review comments --- lldb/include/lldb/Core/Telemetry.h | 5 +- lldb/source/Core/Telemetry.cpp | 82 ++++++++++++------------- lldb/test/CMakeLists.txt | 14 ++--- llvm/include/llvm/Telemetry/Telemetry.h | 17 +++-- llvm/lib/Telemetry/Telemetry.cpp | 2 +- 5 files changed, 57 insertions(+), 63 deletions(-) diff --git a/lldb/include/lldb/Core/Telemetry.h b/lldb/include/lldb/Core/Telemetry.h index 241d957672b6c..4d7dfe517ce39 100644 --- a/lldb/include/lldb/Core/Telemetry.h +++ b/lldb/include/lldb/Core/Telemetry.h @@ -45,7 +45,8 @@ struct LldbEntryKind : public ::llvm::telemetry::EntryKind { /// Defines a convenient type for timestamp of various events. /// This is used by the EventStats below. -using SteadyTimePoint = std::chrono::time_point; +using SteadyTimePoint = std::chrono::time_point; /// Various time (and possibly memory) statistics of an event. struct EventStats { @@ -74,8 +75,6 @@ struct LldbBaseTelemetryInfo : public TelemetryInfo { KindType getKind() const override { return LldbEntryKind::BaseInfo; } static bool classof(const TelemetryInfo *t) { - if (t == nullptr) - return false; // Subclasses of this is also acceptable. return (t->getKind() & LldbEntryKind::BaseInfo) == LldbEntryKind::BaseInfo; } diff --git a/lldb/source/Core/Telemetry.cpp b/lldb/source/Core/Telemetry.cpp index 7eee10d92f06d..c771cc6c1ef9d 100644 --- a/lldb/source/Core/Telemetry.cpp +++ b/lldb/source/Core/Telemetry.cpp @@ -55,68 +55,65 @@ using ::llvm::Error; using ::llvm::telemetry::Destination; using ::llvm::telemetry::TelemetryInfo; -static size_t ToNanosecOrZero(const std::optional &Point) { - if (!Point.has_value()) - return 0; - - return Point.value().time_since_epoch().count(); +static unsigned long long ToNanosec(const SteadyTimePoint Point) { + return nanoseconds(Point.value().time_since_epoch()).count(); } void LldbBaseTelemetryInfo::serialize(Serializer &serializer) const { - serializer.writeInt32("EntryKind", getKind()); - serializer.writeString("SessionId", SessionId); + serializer.write("EntryKind", getKind()); + serializer.write("SessionId", SessionId); } void DebuggerTelemetryInfo::serialize(Serializer &serializer) const { LldbBaseTelemetryInfo::serialize(serializer); - serializer.writeString("username", username); - serializer.writeString("lldb_path", lldb_path); - serializer.writeString("cwd", cwd); - serializer.writeSizeT("start", stats.start.time_since_epoch().count()); - serializer.writeSizeT("end", ToNanosecOrZero(stats.end)); + serializer.write("username", username); + serializer.write("lldb_path", lldb_path); + serializer.write("cwd", cwd); + serializer.write("start", ToNanosec(stats.start)); + if (stats.end.has_value()) + serializer.write("end", ToNanosec(stats.end.value())); } void ClientTelemetryInfo::serialize(Serializer &serializer) const { LldbBaseTelemetryInfo::serialize(serializer); - serializer.writeString("request_name", request_name); - serializer.writeString("error_msg", error_msg); - serializer.writeSizeT("start", stats.start.time_since_epoch().count()); - serializer.writeSizeT("end", ToNanosecOrZero(stats.end)); + serializer.write("request_name", request_name); + serializer.write("error_msg", error_msg); + serializer.write("start", ToNanosec(stats.start)); + if (stats.end.has_value()) + serializer.write("end", ToNanosec(stats.end.value())); } void TargetTelemetryInfo::serialize(Serializer &serializer) const { LldbBaseTelemetryInfo::serialize(serializer); - serializer.writeString("target_uuid", target_uuid); - serializer.writeString("binary_path", binary_path); - serializer.writeSizeT("binary_size", binary_size); + serializer.write("target_uuid", target_uuid); + serializer.write("binary_path", binary_path); + serializer.write("binary_size", binary_size); } void CommandTelemetryInfo::serialize(Serializer &serializer) const { LldbBaseTelemetryInfo::serialize(serializer); - serializer.writeString("target_uuid", target_uuid); - serializer.writeString("command_uuid", command_uuid); - serializer.writeString("args", args); - serializer.writeString("original_command", original_command); - serializer.writeSizeT("start", stats.start.time_since_epoch().count()); - serializer.writeSizeT("end", ToNanosecOrZero(stats.end)); - - // If this entry was emitted at the end of the command-execution, - // then calculate the runtime too. - if (stats.end.has_value()) { - serializer.writeSizeT("command_runtime", - (stats.end.value() - stats.start).count()); - if (exit_desc.has_value()) { - serializer.writeInt32("exit_code", exit_desc->exit_code); - serializer.writeString("exit_msg", exit_desc->description); - serializer.writeInt32("return_status", static_cast(ret_status)); - } + serializer.write("target_uuid", target_uuid); + serializer.write("command_uuid", command_uuid); + serializer.write("args", args); + serializer.write("original_command", original_command); + serializer.write("start", ToNanosec(stats.start)); + if (stats.end.has_value()) + serializer.write("end", ToNanosec(stats.end.value())); + + if (exit_desc.has_value()) { + serializer.write("exit_code", exit_desc->exit_code); + serializer.write("exit_msg", exit_desc->description); + serializer.write("return_status", static_cast(ret_status)); } } void MiscTelemetryInfo::serialize(Serializer &serializer) const { LldbBaseTelemetryInfo::serialize(serializer); - serializer.writeString("target_uuid", target_uuid); - serializer.writeKeyValueMap("meta_data", meta_data); + serializer.write("target_uuid", target_uuid); + write.beginObject("meta_data"); + for (const auto &kv : meta_data) + serializer.write(kv.first, kv.second); + serializer.endObject(); } static std::string MakeUUID(lldb_private::Debugger *debugger) { @@ -154,13 +151,12 @@ std::unique_ptr TelemetryManager::CreateInstance( llvm::Error TelemetryManager::dispatch(TelemetryInfo *entry) { entry->SessionId = m_session_uuid; + llvm::Error defferedErrs = llvm::Error::success(); for (auto &destination : m_destinations) { - llvm::Error err = destination->receiveEntry(entry); - if (err) { - return std::move(err); - } + if (auto err = destination->receiveEntry(entry)) + deferredErrs = llvm::joinErrors(std::move(deferredErrs), std::move(err)); } - return Error::success(); + return std::move(deferredErrs); } void TelemetryManager::addDestination( diff --git a/lldb/test/CMakeLists.txt b/lldb/test/CMakeLists.txt index a5a342da7cfaa..6d5a76aaba1b9 100644 --- a/lldb/test/CMakeLists.txt +++ b/lldb/test/CMakeLists.txt @@ -3,7 +3,7 @@ # Lit requires a Python3 interpreter, let's be careful and fail early if it's # not present. if (NOT DEFINED Python3_EXECUTABLE) - message(FATAL_ERROR + message(SEND_ERROR "LLDB test suite requires a Python3 interpreter but none " "was found. Please install Python3 or disable tests with " "`LLDB_INCLUDE_TESTS=OFF`.") @@ -12,7 +12,7 @@ endif() if(LLDB_ENFORCE_STRICT_TEST_REQUIREMENTS) message(STATUS "Enforcing strict test requirements for LLDB") # Lit uses psutil to do per-test timeouts. - set(useful_python_modules psutil) + set(useful_python_modules psutil packaging) if(NOT WIN32) # We no longer vendor pexpect and it is not used on Windows. @@ -22,7 +22,7 @@ if(LLDB_ENFORCE_STRICT_TEST_REQUIREMENTS) foreach(module ${useful_python_modules}) lldb_find_python_module(${module}) if (NOT PY_${module}_FOUND) - message(FATAL_ERROR + message(SEND_ERROR "Python module '${module}' not found. Please install it via pip or via " "your operating system's package manager. Alternatively, disable " "strict testing requirements with " @@ -66,10 +66,10 @@ if (LLDB_TEST_OBJC_GNUSTEP) find_package(GNUstepObjC) if (NOT GNUstepObjC_FOUND) if (LLDB_TEST_OBJC_GNUSTEP_DIR) - message(FATAL_ERROR "Failed to find GNUstep libobjc2 in ${LLDB_TEST_OBJC_GNUSTEP_DIR}. " + message(SEND_ERROR "Failed to find GNUstep libobjc2 in ${LLDB_TEST_OBJC_GNUSTEP_DIR}. " "Please check LLDB_TEST_OBJC_GNUSTEP_DIR or turn off LLDB_TEST_OBJC_GNUSTEP.") else() - message(FATAL_ERROR "Failed to find GNUstep libobjc2. " + message(SEND_ERROR "Failed to find GNUstep libobjc2. " "Please set LLDB_TEST_OBJC_GNUSTEP_DIR or turn off LLDB_TEST_OBJC_GNUSTEP.") endif() endif() @@ -188,7 +188,7 @@ if(TARGET clang) set(LIBCXX_LIBRARY_DIR "${LLDB_TEST_LIBCXX_ROOT_DIR}/lib${LIBCXX_LIBDIR_SUFFIX}") set(LIBCXX_GENERATED_INCLUDE_DIR "${LLDB_TEST_LIBCXX_ROOT_DIR}/include/c++/v1") else() - message(FATAL_ERROR + message(SEND_ERROR "Couldn't find libcxx build in '${LLDB_TEST_LIBCXX_ROOT_DIR}'. To run the " "test-suite for a standalone LLDB build please build libcxx and point " "LLDB_TEST_LIBCXX_ROOT_DIR to it.") @@ -197,7 +197,7 @@ if(TARGET clang) # We require libcxx for the test suite, so if we aren't building it, # provide a helpful error about how to resolve the situation. if(NOT LLDB_HAS_LIBCXX) - message(FATAL_ERROR + message(SEND_ERROR "LLDB test suite requires libc++, but it is currently disabled. " "Please add `libcxx` to `LLVM_ENABLE_RUNTIMES` or disable tests via " "`LLDB_INCLUDE_TESTS=OFF`.") diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index f6198bd4d3401..7e774e856e0d8 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -27,15 +27,14 @@ namespace telemetry { class Serializer { public: - virtual llvm::Error start() = 0; - virtual void writeBool(StringRef KeyName, bool Value) = 0; - virtual void writeInt32(StringRef KeyName, int Value) = 0; - virtual void writeSizeT(StringRef KeyName, size_t Value) = 0; - virtual void writeString(StringRef KeyName, StringRef Value) = 0; - virtual void - writeKeyValueMap(StringRef KeyName, - const std::map &Value) = 0; - virtual llvm::Error finish() = 0; + virtual Error init() = 0; + virtual void write(StringRef KeyName, bool Value) = 0; + virtual void write(StringRef KeyName, int Value) = 0; + virtual void write(StringRef KeyName, unsigned long long Value) = 0; + virtual void write(StringRef KeyName, StringRef Value) = 0; + virtual void beginObject(StringRef KeyName) = 0; + virtual void endObject() = 0; + virtual Error finalize() = 0; }; /// Configuration for the Telemeter class. diff --git a/llvm/lib/Telemetry/Telemetry.cpp b/llvm/lib/Telemetry/Telemetry.cpp index b7ee3c2bb1778..e7640a64195bf 100644 --- a/llvm/lib/Telemetry/Telemetry.cpp +++ b/llvm/lib/Telemetry/Telemetry.cpp @@ -4,7 +4,7 @@ namespace llvm { namespace telemetry { void TelemetryInfo::serialize(Serializer &serializer) const { - serializer.writeString("SessionId", SessionId); + serializer.write("SessionId", SessionId); } } // namespace telemetry From 1eeb6f9eec2fbfb92b4ad807b273d304b90c31cc Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 17 Dec 2024 14:16:08 -0500 Subject: [PATCH 30/30] formatting --- lldb/source/Core/Telemetry.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lldb/source/Core/Telemetry.cpp b/lldb/source/Core/Telemetry.cpp index c771cc6c1ef9d..e37890aecb867 100644 --- a/lldb/source/Core/Telemetry.cpp +++ b/lldb/source/Core/Telemetry.cpp @@ -142,10 +142,8 @@ TelemetryManager::TelemetryManager( std::unique_ptr TelemetryManager::CreateInstance( std::unique_ptr config, lldb_private::Debugger *debugger) { - - TelemetryManager *ins = new TelemetryManager(std::move(config), debugger); - - return std::unique_ptr(ins); + return std::unique_ptr( + new TelemetryManager(std::move(config), debugger)); } llvm::Error TelemetryManager::dispatch(TelemetryInfo *entry) {