From dbb8b15edb5e63f37a66dd15e67d46ee1b4f6c1b Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 7 Aug 2024 11:08:44 -0400 Subject: [PATCH 01/94] [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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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/94] 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 479cd134064d92144e3e22e5179fb6ff60c839d7 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Fri, 20 Sep 2024 12:35:45 -0400 Subject: [PATCH 22/94] reformat docs --- llvm/docs/Telemetry.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index af976058e66bc..e6459faf7a50b 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -91,6 +91,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of 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. From 3a17dbb36263dd01b09de194e374fab101aea314 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 24 Sep 2024 10:44:57 -0400 Subject: [PATCH 23/94] Update llvm/docs/Telemetry.rst Co-authored-by: Alina Sbirlea --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index e6459faf7a50b..0fcdc96f43591 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -68,7 +68,7 @@ High-level design Key components -------------- -The framework is consisted of four important classes: +The framework consists of four important classes: * `llvm::telemetry::Telemeter`: The class responsible for collecting and transmitting telemetry data. This is the main point of interaction between the From a6a6c7b5858a833ff93ebd2bce879bb3fa678314 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 24 Sep 2024 10:45:13 -0400 Subject: [PATCH 24/94] Update llvm/docs/Telemetry.rst Co-authored-by: Alina Sbirlea --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 0fcdc96f43591..eb40039504eea 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -27,7 +27,7 @@ Characteristics * 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 + * End users of such tool can also configure Telemetry (as allowed by their vendor). From f30dec6fffb4068d9780de0e5b3f0236e9654648 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 24 Sep 2024 10:45:24 -0400 Subject: [PATCH 25/94] Update llvm/docs/Telemetry.rst Co-authored-by: Alina Sbirlea --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index eb40039504eea..d32e035166a79 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -92,7 +92,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of .. code-block:: c++ - // This destiantion just prints the given entry to a stdout. + // This destination 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 { From fd3da20966da123504faab66374424fa08220b17 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 24 Sep 2024 10:45:37 -0400 Subject: [PATCH 26/94] Update llvm/include/llvm/Telemetry/Telemetry.h Co-authored-by: Alina Sbirlea --- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index f682e1cdf5caf..f851a4687a0eb 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -104,7 +104,7 @@ struct EntryKind { /// 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 // case, these shall be multiple sets of TelemetryInfo with multiple unique From 60616ef017da2d1f9eef7e1b57997e198e1e7a19 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 24 Sep 2024 10:45:54 -0400 Subject: [PATCH 27/94] Update llvm/unittests/Telemetry/TelemetryTest.cpp Co-authored-by: Alina Sbirlea --- llvm/unittests/Telemetry/TelemetryTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 173976bdb1650..cca9285accef9 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -33,7 +33,7 @@ struct TestContext { bool HasExitError = false; std::string ExitMsg = ""; - // Controllilng whether there is a vendor-provided config for + // Controlling whether there is a vendor-provided config for // Telemetry. bool HasVendorConfig = false; From 8c0ac5a3057b1d8aa139b2d36d41ce41fee8079d Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 24 Sep 2024 10:46:03 -0400 Subject: [PATCH 28/94] Update llvm/include/llvm/Telemetry/Telemetry.h Co-authored-by: Alina Sbirlea --- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index f851a4687a0eb..a0a6579aa59b7 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -106,7 +106,7 @@ struct TelemetryInfo { // This represents a unique-id, conventionally corresponding to // 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. // From c8be8296d9ef2d0e6ebb91e9b0be716e46abd3c9 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 24 Sep 2024 10:46:22 -0400 Subject: [PATCH 29/94] Update llvm/include/llvm/Telemetry/Telemetry.h Co-authored-by: Alina Sbirlea --- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index a0a6579aa59b7..d997e611e3479 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -85,7 +85,7 @@ struct ExitDescription { 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 +/// 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 { From 1393c1f9990b7c634d2ac990f9ca69ba72440746 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 24 Sep 2024 10:46:31 -0400 Subject: [PATCH 30/94] Update llvm/docs/Telemetry.rst Co-authored-by: Alina Sbirlea --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index d32e035166a79..3fc5fc7b24f29 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -39,7 +39,7 @@ Important notes 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 + 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. From eb07577d3cb8b9347c93c958dd01f59721bc0bdd Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 24 Sep 2024 10:46:37 -0400 Subject: [PATCH 31/94] Update llvm/docs/Telemetry.rst Co-authored-by: Alina Sbirlea --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 3fc5fc7b24f29..e69eb9c3407e0 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -38,7 +38,7 @@ Important notes 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 + 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 From 0376abc07667b6c8c962d5288992086e40eec1ca Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 24 Sep 2024 10:55:24 -0400 Subject: [PATCH 32/94] fix comment --- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index d997e611e3479..c74a74877131a 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -99,7 +99,7 @@ struct EntryKind { /// Downstream implementations can add more fields as needed to describe /// additional events. /// -/// 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 { From e2f7c234089d0f4ba123b6908afdc7f36661db97 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 24 Sep 2024 13:54:57 -0400 Subject: [PATCH 33/94] Update llvm/include/llvm/Telemetry/Telemetry.h Co-authored-by: Alina Sbirlea --- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index c74a74877131a..f62dd8c06f777 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -51,7 +51,7 @@ struct Config { // 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 + // So the users must pick the from their vendor's pre-defined // set of Destinations. std::vector AdditionalDestinations; }; From 17dfac7331e8993a621c7fd97112ff009b0496cb Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 24 Sep 2024 14:18:51 -0400 Subject: [PATCH 34/94] Update llvm/docs/Telemetry.rst Co-authored-by: Alina Sbirlea --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index e69eb9c3407e0..f81addac441d8 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -166,7 +166,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of } private: void emitToAllDestinations(const TelemetryInfo* Entry) { - // Note: could do this in paralle, if needed. + // Note: could do this in parallel, if needed. for (Destination* Dest : Destinations) Dest->emitEntry(Entry); } From 1ea0906d8b7d24fef6ff77a80894a7982e1da998 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 24 Sep 2024 14:18:59 -0400 Subject: [PATCH 35/94] Update llvm/docs/Telemetry.rst Co-authored-by: Alina Sbirlea --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index f81addac441d8..4a3df3d5b36bd 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -95,7 +95,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of // This destination 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 { + class MyStdoutDestination : public llvm::telemetry::Destination { public: Error emitEntry(const TelemetryInfo* Entry) override { return sendToBlackBox(Entry); From 39fd0e7517cb55ca1736bca2549dceb6815055e9 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 24 Sep 2024 14:32:13 -0400 Subject: [PATCH 36/94] Update llvm/include/llvm/Telemetry/Telemetry.h Co-authored-by: Alina Sbirlea --- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index f62dd8c06f777..c1622aa4199ef 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -38,7 +38,7 @@ namespace telemetry { /// 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 +/// to the Telemeter upon construction. (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 From 67b768800322a2835e0289c67d39f153159e0814 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 24 Sep 2024 15:07:09 -0400 Subject: [PATCH 37/94] Addressed review comments: remove most of the member fields from TelemetryInfo, leaving only the ID. Also renamed method to avoid 'log' --- llvm/include/llvm/Telemetry/Telemetry.h | 39 ++------------------ llvm/unittests/Telemetry/TelemetryTest.cpp | 41 ++++++++++++++++++---- 2 files changed, 37 insertions(+), 43 deletions(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index c1622aa4199ef..6c75582148b56 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -56,31 +56,6 @@ struct Config { 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; - -/// 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; -}; - /// For isa, dyn_cast, etc operations on TelemetryInfo. typedef unsigned KindType; /// This struct is used by TelemetryInfo to support isa<>, dyn_cast<> @@ -113,16 +88,6 @@ struct TelemetryInfo { // 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; @@ -157,10 +122,10 @@ class Destination { class Telemeter { public: // Invoked upon tool startup - virtual void logStartup(llvm::StringRef ToolPath, TelemetryInfo *Entry) = 0; + virtual void atStartup(llvm::StringRef ToolPath, TelemetryInfo *Entry) = 0; // Invoked upon tool exit. - virtual void logExit(llvm::StringRef ToolPath, TelemetryInfo *Entry) = 0; + virtual void atExit(llvm::StringRef ToolPath, TelemetryInfo *Entry) = 0; virtual void addDestination(Destination *Destination) = 0; }; diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index cca9285accef9..8d13a43a46fac 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -68,6 +68,31 @@ struct VendorEntryKind { static const KindType Exit = 170; // 0b010101010 }; +// Describes the exit signal of an event. +// This is used by TelemetryInfo below. +struct ExitDescription { + int ExitCode; + std::string Description; +}; + +// 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) {} +}; + // 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. @@ -83,6 +108,10 @@ struct VendorCommonTelemetryInfo : public TelemetryInfo { KindType getKind() const override { return VendorEntryKind::VendorCommon; } virtual void serializeToStream(llvm::raw_ostream &OS) const = 0; + + std::optional ExitDesc; + EventStats Stats; + size_t Counter; }; struct StartupEvent : public VendorCommonTelemetryInfo { @@ -341,7 +370,7 @@ class TestTelemeter : public Telemeter { return Telemeter; } - void logStartup(llvm::StringRef ToolPath, TelemetryInfo *Entry) override { + void atStartup(llvm::StringRef ToolPath, TelemetryInfo *Entry) override { ToolName = ToolPath.str(); // The vendor can add additional stuff to the entry before logging. @@ -351,7 +380,7 @@ class TestTelemeter : public Telemeter { emitToDestinations(Entry); } - void logExit(llvm::StringRef ToolPath, TelemetryInfo *Entry) override { + void atExit(llvm::StringRef ToolPath, TelemetryInfo *Entry) override { // Ensure we're shutting down the same tool we started with. if (ToolPath != ToolName) { std::string Str; @@ -372,7 +401,7 @@ class TestTelemeter : public Telemeter { Destinations.push_back(Dest); } - void logMidpoint(TelemetryInfo *Entry) { + void atMidpoint(TelemetryInfo *Entry) { // The custom Telemeter can record and send additional data. if (auto *C = dyn_cast(Entry)) { C->Msgs.push_back("Two"); @@ -475,7 +504,7 @@ void AtToolStart(std::string ToolName, vendor_code::TestTelemeter *T) { vendor_code::StartupEvent Entry = T->makeDefaultTelemetryInfo(); Entry.Stats = {StartTime, InitCompleteTime}; - T->logStartup(ToolName, &Entry); + T->atStartup(ToolName, &Entry); } void AtToolExit(std::string ToolName, vendor_code::TestTelemeter *T) { @@ -486,14 +515,14 @@ void AtToolExit(std::string ToolName, vendor_code::TestTelemeter *T) { if (CurrentContext->HasExitError) { Entry.ExitDesc = {1, CurrentContext->ExitMsg}; } - T->logExit(ToolName, &Entry); + T->atExit(ToolName, &Entry); } void AtToolMidPoint(vendor_code::TestTelemeter *T) { vendor_code::CustomTelemetryEvent Entry = T->makeDefaultTelemetryInfo(); Entry.Stats = {MidPointTime, MidPointCompleteTime}; - T->logMidpoint(&Entry); + T->atMidpoint(&Entry); } // Helper function to print the given object content to string. From efd25d8753fbf26e3fa92453f10c254f988193b4 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 24 Sep 2024 15:17:30 -0400 Subject: [PATCH 38/94] use llvm::StringLiteral --- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- llvm/unittests/Telemetry/TelemetryTest.cpp | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 6c75582148b56..1390ba4dfce46 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -112,7 +112,7 @@ class Destination { public: virtual ~Destination() = default; virtual Error emitEntry(const TelemetryInfo *Entry) = 0; - virtual std::string name() const = 0; + virtual llvm::StringLiteral name() const = 0; }; /// This class is the main interaction point between any LLVM tool diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 8d13a43a46fac..7b1c04eb2dddf 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -233,8 +233,8 @@ struct CustomTelemetryEvent : public VendorCommonTelemetryInfo { // + where to send the data // + in what form -const std::string STRING_DEST("STRING"); -const std::string JSON_DEST("JSON"); +static constexpr llvm::StringLiteral STRING_DEST("STRING"); +static constexpr llvm::StringLiteral JSON_DEST("JSON"); // This Destination sends data to a std::string given at ctor. class StringDestination : public Destination { @@ -269,7 +269,7 @@ class StringDestination : public Destination { return Error::success(); } - std::string name() const override { return STRING_DEST; } + llvm::StringLiteral name() const override { return STRING_DEST; } private: // Returns a copy of the given entry, but with some fields sanitized. @@ -315,7 +315,7 @@ class JsonStreamDestination : public Destination { inconvertibleErrorCode()); } - std::string name() const override { return JSON_DEST; } + llvm::StringLiteral name() const override { return JSON_DEST; } private: // Returns a copy of the given entry, but with some fields sanitized. @@ -356,10 +356,10 @@ class TestTelemeter : public Telemeter { 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) { + if (Dest == JSON_DEST.str()) { Telemeter->addDestination(new vendor_code::JsonStreamDestination( CurrentContext->SanitizeData)); - } else if (Dest == STRING_DEST) { + } else if (Dest == STRING_DEST.str()) { Telemeter->addDestination(new vendor_code::StringDestination( CurrentContext->SanitizeData, CurrentContext->Buffer)); } else { @@ -560,8 +560,8 @@ TEST(TelemetryTest, TelemetryEnabled) { std::shared_ptr Config = GetTelemetryConfig(); // Add some destinations - Config->AdditionalDestinations.push_back(vendor_code::STRING_DEST); - Config->AdditionalDestinations.push_back(vendor_code::JSON_DEST); + Config->AdditionalDestinations.push_back(vendor_code::STRING_DEST.str()); + Config->AdditionalDestinations.push_back(vendor_code::JSON_DEST.str()); auto Tool = vendor_code::TestTelemeter::createInstance(Config.get()); @@ -642,8 +642,8 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { std::shared_ptr Config = GetTelemetryConfig(); // Add some destinations - Config->AdditionalDestinations.push_back(vendor_code::STRING_DEST); - Config->AdditionalDestinations.push_back(vendor_code::JSON_DEST); + Config->AdditionalDestinations.push_back(vendor_code::STRING_DEST.str()); + Config->AdditionalDestinations.push_back(vendor_code::JSON_DEST.str()); auto Tool = vendor_code::TestTelemeter::createInstance(Config.get()); From 4a8276f9af1e3ce9c93fe0c6632ac7e2237cd24a Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 25 Sep 2024 21:14:02 -0400 Subject: [PATCH 39/94] Addressed review comment: - typo in doc - removed cpy ctor (replaced it wih = default) - used unique_ptr<> for storing Destination (rather than raw ptrs) - constructed expected json objects and compare the objects (rather than their string-reps) - used StringRef() comparisons rather than .str() == - removed thread_local usages (and passed the TestContext obj down from the test body) --- llvm/docs/Telemetry.rst | 2 +- llvm/include/llvm/Telemetry/Telemetry.h | 4 +- llvm/unittests/Telemetry/TelemetryTest.cpp | 178 +++++++++------------ 3 files changed, 82 insertions(+), 102 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 4a3df3d5b36bd..29af0d57811f4 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -198,7 +198,7 @@ Logging the tool init-process: 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: +Additionally, at any other point in the tool's lifetime, it can also log telemetry: .. code-block:: c++ diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 1390ba4dfce46..d9c5e545c0009 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -89,7 +89,7 @@ struct TelemetryInfo { std::string SessionId; TelemetryInfo() = default; - ~TelemetryInfo() = default; + virtual ~TelemetryInfo() = default; virtual json::Object serializeToJson() const; @@ -127,7 +127,7 @@ class Telemeter { // Invoked upon tool exit. virtual void atExit(llvm::StringRef ToolPath, TelemetryInfo *Entry) = 0; - virtual void addDestination(Destination *Destination) = 0; + virtual void addDestination(std::unique_ptr Destination) = 0; }; } // namespace telemetry diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 7b1c04eb2dddf..9bc57da3273d3 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -49,9 +49,6 @@ struct TestContext { std::string ExpectedUuid = ""; }; -// This is set by the test body. -static thread_local TestContext *CurrentContext = nullptr; - namespace llvm { namespace telemetry { namespace vendor_code { @@ -118,14 +115,7 @@ struct StartupEvent : public VendorCommonTelemetryInfo { std::string MagicStartupMsg; StartupEvent() = default; - StartupEvent(const StartupEvent &E) { - SessionId = E.SessionId; - Stats = E.Stats; - ExitDesc = E.ExitDesc; - Counter = E.Counter; - - MagicStartupMsg = E.MagicStartupMsg; - } + StartupEvent(const StartupEvent &E) = default; static bool classof(const TelemetryInfo *T) { if (T == nullptr) @@ -154,14 +144,7 @@ struct ExitEvent : public VendorCommonTelemetryInfo { ExitEvent() = default; // Provide a copy ctor because we may need to make a copy // before sanitizing the Entry. - ExitEvent(const ExitEvent &E) { - SessionId = E.SessionId; - Stats = E.Stats; - ExitDesc = E.ExitDesc; - Counter = E.Counter; - - MagicExitMsg = E.MagicExitMsg; - } + ExitEvent(const ExitEvent &E) = default; static bool classof(const TelemetryInfo *T) { if (T == nullptr) @@ -195,14 +178,7 @@ struct CustomTelemetryEvent : public VendorCommonTelemetryInfo { std::vector Msgs; CustomTelemetryEvent() = default; - CustomTelemetryEvent(const CustomTelemetryEvent &E) { - SessionId = E.SessionId; - Stats = E.Stats; - ExitDesc = E.ExitDesc; - Counter = E.Counter; - - Msgs = E.Msgs; - } + CustomTelemetryEvent(const CustomTelemetryEvent &E) = default; void serializeToStream(llvm::raw_ostream &OS) const override { OS << "SessionId:" << SessionId << "\n"; @@ -290,7 +266,8 @@ class StringDestination : public Destination { // This Destination sends data to some "blackbox" in form of JSON. class JsonStreamDestination : public Destination { public: - JsonStreamDestination(bool ShouldSanitize) : ShouldSanitize(ShouldSanitize) {} + JsonStreamDestination(bool ShouldSanitize, TestContext *Ctxt) + : ShouldSanitize(ShouldSanitize), CurrentContext(Ctxt) {} Error emitEntry(const TelemetryInfo *Entry) override { if (auto *E = dyn_cast(Entry)) { @@ -298,21 +275,17 @@ class JsonStreamDestination : public Destination { 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)) { + } + if (isa(E)) { auto Sanitized = sanitizeFields(dyn_cast(E)); return SendToBlackbox(Sanitized.serializeToJson()); - } else { - llvm_unreachable("unexpected type"); } - } else { - return SendToBlackbox(E->serializeToJson()); + llvm_unreachable("unexpected type"); } - } else { - // Unfamiliar entries, just send the entry's ID - return SendToBlackbox(json::Object{{"SessionId", Entry->SessionId}}); + return SendToBlackbox(E->serializeToJson()); } - return make_error("unhandled codepath in emitEntry", - inconvertibleErrorCode()); + // Unfamiliar entries, just send the entry's ID + return SendToBlackbox(json::Object{{"SessionId", Entry->SessionId}}); } llvm::StringLiteral name() const override { return JSON_DEST; } @@ -339,6 +312,7 @@ class JsonStreamDestination : public Destination { return Error::success(); } bool ShouldSanitize; + TestContext *CurrentContext; }; // Custom vendor-defined Telemeter that has additional data-collection point. @@ -346,7 +320,8 @@ 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, TestContext *CurrentContext) { if (!config->EnableTelemetry) return nullptr; CurrentContext->ExpectedUuid = nextUuid(); @@ -356,17 +331,20 @@ class TestTelemeter : public Telemeter { 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.str()) { - Telemeter->addDestination(new vendor_code::JsonStreamDestination( - CurrentContext->SanitizeData)); - } else if (Dest == STRING_DEST.str()) { - Telemeter->addDestination(new vendor_code::StringDestination( - CurrentContext->SanitizeData, CurrentContext->Buffer)); + if (llvm::StringRef(Dest) == JSON_DEST) { + Telemeter->addDestination( + std::make_unique( + CurrentContext->SanitizeData, CurrentContext)); + } else if (llvm::StringRef(Dest) == STRING_DEST) { + Telemeter->addDestination( + std::make_unique( + CurrentContext->SanitizeData, CurrentContext->Buffer)); } else { llvm_unreachable( llvm::Twine("unknown destination: ", Dest).str().c_str()); } } + Telemeter->CurrentContext = CurrentContext; return Telemeter; } @@ -397,8 +375,8 @@ class TestTelemeter : public Telemeter { emitToDestinations(Entry); } - void addDestination(Destination *Dest) override { - Destinations.push_back(Dest); + void addDestination(std::unique_ptr Dest) override { + Destinations.push_back(std::move(Dest)); } void atMidpoint(TelemetryInfo *Entry) { @@ -414,10 +392,7 @@ class TestTelemeter : public Telemeter { const std::string &getUuid() const { return Uuid; } - ~TestTelemeter() { - for (auto *Dest : Destinations) - delete Dest; - } + ~TestTelemeter() = default; template T makeDefaultTelemetryInfo() { T Ret; @@ -426,9 +401,11 @@ class TestTelemeter : public Telemeter { return Ret; } + TestContext *CurrentContext = nullptr; + private: void emitToDestinations(TelemetryInfo *Entry) { - for (Destination *Dest : Destinations) { + for (const auto &Dest : Destinations) { llvm::Error err = Dest->emitEntry(Entry); if (err) { // Log it and move on. @@ -439,7 +416,7 @@ 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. @@ -458,7 +435,8 @@ void ApplyCommonConfig(llvm::telemetry::Config *config) { // ..... } -std::shared_ptr GetTelemetryConfig() { +std::shared_ptr +GetTelemetryConfig(TestContext *CurrentContext) { // Telemetry is disabled by default. // The vendor can enable in their config. auto Config = std::make_shared(); @@ -512,8 +490,8 @@ void AtToolExit(std::string ToolName, vendor_code::TestTelemeter *T) { T->makeDefaultTelemetryInfo(); Entry.Stats = {ExitTime, ExitCompleteTime}; - if (CurrentContext->HasExitError) { - Entry.ExitDesc = {1, CurrentContext->ExitMsg}; + if (T->CurrentContext->HasExitError) { + Entry.ExitDesc = {1, T->CurrentContext->ExitMsg}; } T->atExit(ToolName, &Entry); } @@ -525,23 +503,17 @@ void AtToolMidPoint(vendor_code::TestTelemeter *T) { T->atMidpoint(&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) { // Preset some test params. TestContext Context; Context.HasVendorConfig = false; - CurrentContext = &Context; + TestContext *CurrentContext = &Context; - std::shared_ptr Config = GetTelemetryConfig(); - auto Tool = vendor_code::TestTelemeter::createInstance(Config.get()); + std::shared_ptr Config = + GetTelemetryConfig(CurrentContext); + auto Tool = + vendor_code::TestTelemeter::createInstance(Config.get(), CurrentContext); EXPECT_EQ(nullptr, Tool.get()); } @@ -555,15 +527,17 @@ TEST(TelemetryTest, TelemetryEnabled) { Context.SanitizeData = false; Context.Buffer.clear(); Context.EmittedJsons.clear(); - CurrentContext = &Context; + TestContext *CurrentContext = &Context; - std::shared_ptr Config = GetTelemetryConfig(); + std::shared_ptr Config = + GetTelemetryConfig(CurrentContext); // Add some destinations Config->AdditionalDestinations.push_back(vendor_code::STRING_DEST.str()); Config->AdditionalDestinations.push_back(vendor_code::JSON_DEST.str()); - auto Tool = vendor_code::TestTelemeter::createInstance(Config.get()); + auto Tool = + vendor_code::TestTelemeter::createInstance(Config.get(), CurrentContext); AtToolStart(ToolName, Tool.get()); AtToolMidPoint(Tool.get()); @@ -595,34 +569,32 @@ TEST(TelemetryTest, TelemetryEnabled) { const json::Value *StartupEntry = CurrentContext->EmittedJsons[0].get("Startup"); ASSERT_NE(StartupEntry, nullptr); - EXPECT_STREQ( + llvm::Expected ExpectedStartup = json::parse( ("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) + "\"],[\"MagicStartupMsg\",\"Startup_" + llvm::Twine(ToolName) + "\"]]") - .str() - .c_str(), - ValueToString(StartupEntry).c_str()); + .str()); + ASSERT_TRUE((bool)ExpectedStartup); + EXPECT_EQ(ExpectedStartup.get(), *StartupEntry); 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(CurrentContext->ExpectedUuid) + "\"}") - .str() - .c_str(), - ValueToString(MidpointEntry).c_str()); + llvm::Expected ExpectedMidpoint = + json::parse(("{\"MSG_0\":\"Two\",\"MSG_1\":\"Deux\",\"MSG_2\":\"Zwei\"," + "\"SessionId\":\"" + + llvm::Twine(CurrentContext->ExpectedUuid) + "\"}") + .str()); + ASSERT_TRUE((bool)ExpectedMidpoint); + EXPECT_EQ(ExpectedMidpoint.get(), *MidpointEntry); const json::Value *ExitEntry = CurrentContext->EmittedJsons[2].get("Exit"); ASSERT_NE(ExitEntry, nullptr); - EXPECT_STREQ( + llvm::Expected ExpectedExit = json::parse( ("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) + "\"],[\"MagicExitMsg\",\"Exit_" + llvm::Twine(ToolName) + "\"]]") - .str() - .c_str(), - ValueToString(ExitEntry).c_str()); + .str()); + ASSERT_TRUE((bool)ExpectedExit); + EXPECT_EQ(ExpectedExit.get(), *ExitEntry); } } @@ -637,15 +609,17 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { Context.SanitizeData = true; Context.Buffer.clear(); Context.EmittedJsons.clear(); - CurrentContext = &Context; - std::shared_ptr Config = GetTelemetryConfig(); + TestContext *CurrentContext = &Context; + std::shared_ptr Config = + GetTelemetryConfig(CurrentContext); // Add some destinations Config->AdditionalDestinations.push_back(vendor_code::STRING_DEST.str()); Config->AdditionalDestinations.push_back(vendor_code::JSON_DEST.str()); - auto Tool = vendor_code::TestTelemeter::createInstance(Config.get()); + auto Tool = + vendor_code::TestTelemeter::createInstance(Config.get(), CurrentContext); AtToolStart(ToolName, Tool.get()); AtToolMidPoint(Tool.get()); @@ -675,32 +649,38 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { const json::Value *StartupEntry = CurrentContext->EmittedJsons[0].get("Startup"); ASSERT_NE(StartupEntry, nullptr); - EXPECT_STREQ( + llvm::Expected ExpectedStartup = json::parse( ("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) + "\"],[\"MagicStartupMsg\",\"Startup_" + llvm::Twine(ToolName) + "\"]]") .str() - .c_str(), - ValueToString(StartupEntry).c_str()); + + ); + ASSERT_TRUE((bool)ExpectedStartup); + EXPECT_EQ(ExpectedStartup.get(), *StartupEntry); const json::Value *MidpointEntry = CurrentContext->EmittedJsons[1].get("Midpoint"); ASSERT_NE(MidpointEntry, nullptr); // The JsonDestination should have removed the even-positioned msgs. - EXPECT_STREQ( + llvm::Expected ExpectedMidpoint = json::parse( ("{\"MSG_0\":\"\",\"MSG_1\":\"Deux\",\"MSG_2\":\"\",\"SessionId\":\"" + llvm::Twine(CurrentContext->ExpectedUuid) + "\"}") .str() - .c_str(), - ValueToString(MidpointEntry).c_str()); + + ); + ASSERT_TRUE((bool)ExpectedMidpoint); + EXPECT_EQ(ExpectedMidpoint.get(), *MidpointEntry); const json::Value *ExitEntry = CurrentContext->EmittedJsons[2].get("Exit"); ASSERT_NE(ExitEntry, nullptr); - EXPECT_STREQ( + llvm::Expected ExpectedExit = json::parse( ("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) + "\"],[\"MagicExitMsg\",\"Exit_" + llvm::Twine(ToolName) + "\"]]") .str() - .c_str(), - ValueToString(ExitEntry).c_str()); + + ); + ASSERT_TRUE((bool)ExpectedExit); + EXPECT_EQ(ExpectedExit.get(), *ExitEntry); } } From a16344b9f3ead51b9cb6af14198bc574eeba4dcc Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Fri, 27 Sep 2024 09:35:09 -0400 Subject: [PATCH 40/94] pass test context as arg to ctor --- llvm/unittests/Telemetry/TelemetryTest.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 9bc57da3273d3..30f72c0df78bc 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -318,15 +318,16 @@ class JsonStreamDestination : public Destination { // Custom vendor-defined Telemeter that has additional data-collection point. class TestTelemeter : public Telemeter { public: - TestTelemeter(std::string SessionId) : Uuid(SessionId), Counter(0) {} + TestTelemeter(std::string SessionId, TestContext *Ctxt) + : Uuid(SessionId), Counter(0), CurrentContext(Ctxt) {} static std::unique_ptr createInstance(Config *config, TestContext *CurrentContext) { if (!config->EnableTelemetry) return nullptr; CurrentContext->ExpectedUuid = nextUuid(); - std::unique_ptr Telemeter = - std::make_unique(CurrentContext->ExpectedUuid); + std::unique_ptr Telemeter = std::make_unique( + CurrentContext->ExpectedUuid, CurrentContext); // 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 @@ -344,7 +345,6 @@ class TestTelemeter : public Telemeter { llvm::Twine("unknown destination: ", Dest).str().c_str()); } } - Telemeter->CurrentContext = CurrentContext; return Telemeter; } From 26ee5eb9cb934d96164bfa98931d8ecfcc962342 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Mon, 30 Sep 2024 14:02:25 -0400 Subject: [PATCH 41/94] construct expected json directly rather than parsing from strings --- llvm/unittests/Telemetry/TelemetryTest.cpp | 80 +++++++++------------- 1 file changed, 33 insertions(+), 47 deletions(-) diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 30f72c0df78bc..772cc93f3d4d5 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -190,11 +190,10 @@ struct CustomTelemetryEvent : public VendorCommonTelemetryInfo { } json::Object serializeToJson() const override { - json::Object Inner; - Inner.try_emplace("SessionId", SessionId); + json::Array Inner{{"SessionId", SessionId}}; int I = 0; for (const std::string &M : Msgs) { - Inner.try_emplace(("MSG_" + llvm::Twine(I)).str(), M); + Inner.push_back(json::Value({("MSG_" + llvm::Twine(I)).str(), M})); ++I; } @@ -319,7 +318,7 @@ class JsonStreamDestination : public Destination { class TestTelemeter : public Telemeter { public: TestTelemeter(std::string SessionId, TestContext *Ctxt) - : Uuid(SessionId), Counter(0), CurrentContext(Ctxt) {} + : CurrentContext(Ctxt), Uuid(SessionId), Counter(0) {} static std::unique_ptr createInstance(Config *config, TestContext *CurrentContext) { @@ -569,32 +568,28 @@ TEST(TelemetryTest, TelemetryEnabled) { const json::Value *StartupEntry = CurrentContext->EmittedJsons[0].get("Startup"); ASSERT_NE(StartupEntry, nullptr); - llvm::Expected ExpectedStartup = json::parse( - ("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) + - "\"],[\"MagicStartupMsg\",\"Startup_" + llvm::Twine(ToolName) + "\"]]") - .str()); - ASSERT_TRUE((bool)ExpectedStartup); - EXPECT_EQ(ExpectedStartup.get(), *StartupEntry); + json::Value ExpectedStartup({ + {"SessionId", CurrentContext->ExpectedUuid}, + {"MagicStartupMsg", ("Startup_" + llvm::Twine(ToolName)).str()}, + }); + EXPECT_EQ(ExpectedStartup, *StartupEntry); const json::Value *MidpointEntry = CurrentContext->EmittedJsons[1].get("Midpoint"); ASSERT_NE(MidpointEntry, nullptr); - llvm::Expected ExpectedMidpoint = - json::parse(("{\"MSG_0\":\"Two\",\"MSG_1\":\"Deux\",\"MSG_2\":\"Zwei\"," - "\"SessionId\":\"" + - llvm::Twine(CurrentContext->ExpectedUuid) + "\"}") - .str()); - ASSERT_TRUE((bool)ExpectedMidpoint); - EXPECT_EQ(ExpectedMidpoint.get(), *MidpointEntry); + json::Value ExpectedMidpoint{{"SessionId", CurrentContext->ExpectedUuid}, + {"MSG_0", "Two"}, + {"MSG_1", "Deux"}, + {"MSG_2", "Zwei"}}; + EXPECT_EQ(ExpectedMidpoint, *MidpointEntry); const json::Value *ExitEntry = CurrentContext->EmittedJsons[2].get("Exit"); ASSERT_NE(ExitEntry, nullptr); - llvm::Expected ExpectedExit = json::parse( - ("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) + - "\"],[\"MagicExitMsg\",\"Exit_" + llvm::Twine(ToolName) + "\"]]") - .str()); - ASSERT_TRUE((bool)ExpectedExit); - EXPECT_EQ(ExpectedExit.get(), *ExitEntry); + json::Value ExpectedExit({ + {"SessionId", CurrentContext->ExpectedUuid}, + {"MagicExitMsg", ("Exit_" + llvm::Twine(ToolName)).str()}, + }); + EXPECT_EQ(ExpectedExit, *ExitEntry); } } @@ -649,38 +644,29 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { const json::Value *StartupEntry = CurrentContext->EmittedJsons[0].get("Startup"); ASSERT_NE(StartupEntry, nullptr); - llvm::Expected ExpectedStartup = json::parse( - ("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) + - "\"],[\"MagicStartupMsg\",\"Startup_" + llvm::Twine(ToolName) + "\"]]") - .str() - - ); - ASSERT_TRUE((bool)ExpectedStartup); - EXPECT_EQ(ExpectedStartup.get(), *StartupEntry); + json::Value ExpectedStartup({ + {"SessionId", CurrentContext->ExpectedUuid}, + {"MagicStartupMsg", ("Startup_" + llvm::Twine(ToolName)).str()}, + }); + EXPECT_EQ(ExpectedStartup, *StartupEntry); const json::Value *MidpointEntry = CurrentContext->EmittedJsons[1].get("Midpoint"); ASSERT_NE(MidpointEntry, nullptr); // The JsonDestination should have removed the even-positioned msgs. - llvm::Expected ExpectedMidpoint = json::parse( - ("{\"MSG_0\":\"\",\"MSG_1\":\"Deux\",\"MSG_2\":\"\",\"SessionId\":\"" + - llvm::Twine(CurrentContext->ExpectedUuid) + "\"}") - .str() - - ); - ASSERT_TRUE((bool)ExpectedMidpoint); - EXPECT_EQ(ExpectedMidpoint.get(), *MidpointEntry); + json::Value ExpectedMidpoint{{"SessionId", CurrentContext->ExpectedUuid}, + {"MSG_0", ""}, + {"MSG_1", "Deux"}, + {"MSG_2", ""}}; + EXPECT_EQ(ExpectedMidpoint, *MidpointEntry); const json::Value *ExitEntry = CurrentContext->EmittedJsons[2].get("Exit"); ASSERT_NE(ExitEntry, nullptr); - llvm::Expected ExpectedExit = json::parse( - ("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) + - "\"],[\"MagicExitMsg\",\"Exit_" + llvm::Twine(ToolName) + "\"]]") - .str() - - ); - ASSERT_TRUE((bool)ExpectedExit); - EXPECT_EQ(ExpectedExit.get(), *ExitEntry); + json::Value ExpectedExit({ + {"SessionId", CurrentContext->ExpectedUuid}, + {"MagicExitMsg", ("Exit_" + llvm::Twine(ToolName)).str()}, + }); + EXPECT_EQ(ExpectedExit, *ExitEntry); } } From b766e3c94afc8af8a922e27270dc68f0aa8f44d1 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Mon, 30 Sep 2024 14:06:00 -0400 Subject: [PATCH 42/94] remove unnecessary use of atomic seed --- 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 772cc93f3d4d5..afe4c5c1062fc 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -55,8 +55,8 @@ namespace vendor_code { // Generate unique (but deterministic "uuid" for testing purposes). static std::string nextUuid() { - static std::atomic seed = 1111; - return std::to_string(seed.fetch_add(1, std::memory_order_acquire)); + static int seed = 1111; + return std::to_string(seed++); } struct VendorEntryKind { From c8cddabba1f3a4888f59fa848b57f79b5e5813d7 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 21 Nov 2024 15:20:47 -0500 Subject: [PATCH 43/94] addressed review comments --- llvm/docs/Telemetry.rst | 62 +++++++++++----------- llvm/include/llvm/Telemetry/Telemetry.h | 37 ++++--------- llvm/unittests/Telemetry/TelemetryTest.cpp | 36 ++++++------- 3 files changed, 58 insertions(+), 77 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 29af0d57811f4..b7610b0400333 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -17,7 +17,7 @@ Objective Provides a common framework in LLVM for collecting various usage and performance metrics. -It is located at `llvm/Telemetry/Telemetry.h` +It is located at ``llvm/Telemetry/Telemetry.h`` Characteristics --------------- @@ -30,7 +30,6 @@ Characteristics * End users of such tool can also configure Telemetry (as allowed by their vendor). - Important notes ---------------- @@ -38,7 +37,7 @@ Important notes 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 + 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 @@ -56,7 +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 + * E.g., data collected by Telemetry is not necessarily 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. @@ -70,42 +69,41 @@ Key components The framework consists of four important classes: -* `llvm::telemetry::Telemeter`: The class responsible for collecting and +* ``llvm::telemetry::Manager``: The class responsible for collecting and 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 + framework and any tool that wants to enable telemetry. +* ``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`. + to forward them to for their final storage. +* ``llvm::telemetry::Config``: Configurations for the ``Manager``. .. image:: llvm_telemetry_design.png How to implement and interact with the API ------------------------------------------ -To use Telemetry in your tool, you need to provide a concrete implementation of the `Telemeter` class and `Destination`. +To use Telemetry in your tool, you need to provide a concrete implementation of the ``Manager`` class and ``Destination``. -1) Define a custom `Telemeter` and `Destination` +1) Define a custom ``Manager`` and ``Destination`` .. code-block:: c++ - // This destination just prints the given entry to a stdout. + // This destination just prints the given entry to stdout. // In "real life", this would be where you forward the data to your // custom data storage. class MyStdoutDestination : public llvm::telemetry::Destination { 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. + // 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(); } @@ -116,11 +114,11 @@ To use Telemetry in your tool, you need to provide a concrete implementation of }; // This defines a custom TelemetryInfo that has an addition Msg field. - struct MyTelemetryInfo : public llvm::telemetry::TelemetryInfo { + struct MyTelemetryInfo : public telemetry::TelemetryInfo { std::string Msg; - json::Object serializeToJson() const { - json::Object Ret = TelemeteryInfo::serializeToJson(); + json::Object serializeToJson() oconst { + json::Object Ret = ManageryInfo::serializeToJson(); Ret.emplace_back("MyMsg", Msg); return std::move(Ret); } @@ -128,17 +126,17 @@ To use Telemetry in your tool, you need to provide a concrete implementation of // TODO: implement getKind() and classof() to support dyn_cast operations. }; - class MyTelemeter : public llvm::telemery::Telemeter { + class MyManager : public telemery::Manager { public: - static std::unique_ptr createInstatnce(llvm::telemetry::Config* config) { + static std::unique_ptr createInstatnce(telemetry::Config* config) { // If Telemetry is not enabled, then just return null; if (!config->EnableTelemetry) return nullptr; - std::make_unique(); + std::make_unique(); } - MyTelemeter() = default; + MyManager() = default; - void logStartup(llvm::StringRef ToolName, TelemetryInfo* Entry) override { + void logStartup(StringRef ToolName, TelemetryInfo* Entry) override { if (MyTelemetryInfo* M = dyn_cast(Entry)) { M->Msg = "Starting up tool with name: " + ToolName; emitToAllDestinations(M); @@ -147,9 +145,9 @@ To use Telemetry in your tool, you need to provide a concrete implementation of } } - void logExit(llvm::StringRef ToolName, TelemetryInfo* Entry) override { + void logExit(StringRef ToolName, TelemetryInfo* Entry) override { if (MyTelemetryInfo* M = dyn_cast(Entry)) { - M->Msg = "Exitting tool with name: " + ToolName; + M->Msg = "Exiting tool with name: " + ToolName; emitToAllDestinations(M); } else { emitToAllDestinations(Entry); @@ -160,7 +158,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of destinations.push_back(dest); } - // You can also define additional instrumentation points.) + // You can also define additional instrumentation points. void logAdditionalPoint(TelemetryInfo* Entry) { // .... code here } @@ -179,10 +177,10 @@ Logging the tool init-process: .. code-block:: c++ - // At tool's init code + // In tool's initialization 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); + telemetry::Config MyConfig = makeConfig(); // Build up the appropriate Config struct here. + auto Manager = MyManager::createInstance(&MyConfig); std::string CurrentSessionId = ...; // Make some unique ID corresponding to the current session here. // Any other tool's init code can go here @@ -194,7 +192,7 @@ Logging the tool init-process: MyTelemetryInfo Entry; Entry.SessionId = CurrentSessionId ; // Assign some unique ID here. Entry.Stats = {StartTime, EndTime}; - Telemeter->logStartup("MyTool", &Entry); + Manager->logStartup("MyTool", &Entry); Similar code can be used for logging the tool's exit. @@ -211,4 +209,4 @@ Additionally, at any other point in the tool's lifetime, it can also log telemet MyTelemetryInfo Entry; Entry.SessionId = CurrentSessionId ; // Assign some unique ID here. Entry.Stats = {StartTime, EndTime}; - Telemeter->logAdditionalPoint(&Entry); + Manager->logAdditionalPoint(&Entry); diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index d9c5e545c0009..b22af683e4db4 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -8,52 +8,35 @@ /// /// \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 "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/JSON.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 { /// Configuration for the Telemeter class. /// This stores configurations from both users and vendors and is passed /// to the Telemeter upon construction. (Any changes to the config after -/// the Telemeter's construction will not have effect on it). +/// 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; }; /// For isa, dyn_cast, etc operations on TelemetryInfo. @@ -61,18 +44,18 @@ 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 +/// 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 example, The LLDB debugger can define a DebugCommandInfo subclass /// which has additional fields about the debug-command being instrumented, @@ -119,7 +102,7 @@ class Destination { /// 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 atStartup(llvm::StringRef ToolPath, TelemetryInfo *Entry) = 0; diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index afe4c5c1062fc..257b7454bf0eb 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -314,29 +314,29 @@ class JsonStreamDestination : public Destination { TestContext *CurrentContext; }; -// Custom vendor-defined Telemeter that has additional data-collection point. -class TestTelemeter : public Telemeter { +// Custom vendor-defined Manager that has additional data-collection point. +class TestManager : public Manager { public: - TestTelemeter(std::string SessionId, TestContext *Ctxt) + TestManager(std::string SessionId, TestContext *Ctxt) : CurrentContext(Ctxt), Uuid(SessionId), Counter(0) {} - static std::unique_ptr + static std::unique_ptr createInstance(Config *config, TestContext *CurrentContext) { if (!config->EnableTelemetry) return nullptr; CurrentContext->ExpectedUuid = nextUuid(); - std::unique_ptr Telemeter = std::make_unique( + std::unique_ptr Manager = std::make_unique( CurrentContext->ExpectedUuid, CurrentContext); // 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 (llvm::StringRef(Dest) == JSON_DEST) { - Telemeter->addDestination( + Manager->addDestination( std::make_unique( CurrentContext->SanitizeData, CurrentContext)); } else if (llvm::StringRef(Dest) == STRING_DEST) { - Telemeter->addDestination( + Manager->addDestination( std::make_unique( CurrentContext->SanitizeData, CurrentContext->Buffer)); } else { @@ -344,7 +344,7 @@ class TestTelemeter : public Telemeter { llvm::Twine("unknown destination: ", Dest).str().c_str()); } } - return Telemeter; + return Manager; } void atStartup(llvm::StringRef ToolPath, TelemetryInfo *Entry) override { @@ -379,7 +379,7 @@ class TestTelemeter : public Telemeter { } void atMidpoint(TelemetryInfo *Entry) { - // The custom Telemeter can record and send additional data. + // The custom Manager can record and send additional data. if (auto *C = dyn_cast(Entry)) { C->Msgs.push_back("Two"); C->Msgs.push_back("Deux"); @@ -391,7 +391,7 @@ class TestTelemeter : public Telemeter { const std::string &getUuid() const { return Uuid; } - ~TestTelemeter() = default; + ~TestManager() = default; template T makeDefaultTelemetryInfo() { T Ret; @@ -477,14 +477,14 @@ auto ExitTime = StartTime + std::chrono::milliseconds(20); // milliseconds. auto ExitCompleteTime = ExitTime + std::chrono::milliseconds(10); -void AtToolStart(std::string ToolName, vendor_code::TestTelemeter *T) { +void AtToolStart(std::string ToolName, vendor_code::TestManager *T) { vendor_code::StartupEvent Entry = T->makeDefaultTelemetryInfo(); Entry.Stats = {StartTime, InitCompleteTime}; T->atStartup(ToolName, &Entry); } -void AtToolExit(std::string ToolName, vendor_code::TestTelemeter *T) { +void AtToolExit(std::string ToolName, vendor_code::TestManager *T) { vendor_code::ExitEvent Entry = T->makeDefaultTelemetryInfo(); Entry.Stats = {ExitTime, ExitCompleteTime}; @@ -495,7 +495,7 @@ void AtToolExit(std::string ToolName, vendor_code::TestTelemeter *T) { T->atExit(ToolName, &Entry); } -void AtToolMidPoint(vendor_code::TestTelemeter *T) { +void AtToolMidPoint(vendor_code::TestManager *T) { vendor_code::CustomTelemetryEvent Entry = T->makeDefaultTelemetryInfo(); Entry.Stats = {MidPointTime, MidPointCompleteTime}; @@ -512,7 +512,7 @@ TEST(TelemetryTest, TelemetryDefault) { std::shared_ptr Config = GetTelemetryConfig(CurrentContext); auto Tool = - vendor_code::TestTelemeter::createInstance(Config.get(), CurrentContext); + vendor_code::TestManager::createInstance(Config.get(), CurrentContext); EXPECT_EQ(nullptr, Tool.get()); } @@ -536,7 +536,7 @@ TEST(TelemetryTest, TelemetryEnabled) { Config->AdditionalDestinations.push_back(vendor_code::JSON_DEST.str()); auto Tool = - vendor_code::TestTelemeter::createInstance(Config.get(), CurrentContext); + vendor_code::TestManager::createInstance(Config.get(), CurrentContext); AtToolStart(ToolName, Tool.get()); AtToolMidPoint(Tool.get()); @@ -562,7 +562,7 @@ TEST(TelemetryTest, TelemetryEnabled) { // Check that the JsonDestination emitted properly { - // There should be 3 events emitted by the Telemeter (start, midpoint, exit) + // There should be 3 events emitted by the Manager (start, midpoint, exit) EXPECT_EQ(static_cast(3), CurrentContext->EmittedJsons.size()); const json::Value *StartupEntry = @@ -614,7 +614,7 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { Config->AdditionalDestinations.push_back(vendor_code::JSON_DEST.str()); auto Tool = - vendor_code::TestTelemeter::createInstance(Config.get(), CurrentContext); + vendor_code::TestManager::createInstance(Config.get(), CurrentContext); AtToolStart(ToolName, Tool.get()); AtToolMidPoint(Tool.get()); @@ -638,7 +638,7 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) { // Check that the JsonDestination emitted properly { - // There should be 3 events emitted by the Telemeter (start, midpoint, exit) + // There should be 3 events emitted by the Manager (start, midpoint, exit) EXPECT_EQ(static_cast(3), CurrentContext->EmittedJsons.size()); const json::Value *StartupEntry = From 5f8d65f318ff2028afa2a7129deadf81c08ac81c Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 21 Nov 2024 15:25:55 -0500 Subject: [PATCH 44/94] s/emitEntry/receiveEntry --- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- llvm/unittests/Telemetry/TelemetryTest.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index b22af683e4db4..ad12cb635f85a 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -94,7 +94,7 @@ struct TelemetryInfo { class Destination { public: virtual ~Destination() = default; - virtual Error emitEntry(const TelemetryInfo *Entry) = 0; + virtual Error receiveEntry(const TelemetryInfo *Entry) = 0; virtual llvm::StringLiteral name() const = 0; }; diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 257b7454bf0eb..48062874940df 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -219,7 +219,7 @@ class StringDestination : public Destination { StringDestination(bool ShouldSanitize, std::string &Buf) : ShouldSanitize(ShouldSanitize), OS(Buf) {} - Error emitEntry(const TelemetryInfo *Entry) override { + Error receiveEntry(const TelemetryInfo *Entry) override { if (isa(Entry)) { if (auto *E = dyn_cast(Entry)) { if (ShouldSanitize) { @@ -268,7 +268,7 @@ class JsonStreamDestination : public Destination { JsonStreamDestination(bool ShouldSanitize, TestContext *Ctxt) : ShouldSanitize(ShouldSanitize), CurrentContext(Ctxt) {} - Error emitEntry(const TelemetryInfo *Entry) override { + Error receiveEntry(const TelemetryInfo *Entry) override { if (auto *E = dyn_cast(Entry)) { if (ShouldSanitize) { if (isa(E) || isa(E)) { @@ -405,7 +405,7 @@ class TestManager : public Manager { private: void emitToDestinations(TelemetryInfo *Entry) { for (const auto &Dest : Destinations) { - llvm::Error err = Dest->emitEntry(Entry); + llvm::Error err = Dest->receiveEntry(Entry); if (err) { // Log it and move on. } From dffeacd19cb42e2b77a4cbc2bfdc9670f2b9a5af Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 26 Nov 2024 20:54:00 -0500 Subject: [PATCH 45/94] Addressed review comments: - reworked serialization interface (ie., use a Serializer) - updated tests - fix styling issues --- llvm/include/llvm/Telemetry/Telemetry.h | 30 +- llvm/lib/Telemetry/Telemetry.cpp | 8 +- llvm/unittests/Telemetry/TelemetryTest.cpp | 737 ++++++--------------- 3 files changed, 211 insertions(+), 564 deletions(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index ad12cb635f85a..fb4a204ff5f68 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -18,8 +18,6 @@ #include "llvm/ADT/StringRef.h" #include "llvm/Support/Error.h" #include "llvm/Support/JSON.h" -#include -#include #include #include #include @@ -27,6 +25,18 @@ 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, + 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 construction. (Any changes to the config after @@ -36,7 +46,10 @@ namespace telemetry { /// points specific to a vendor's implementation. struct Config { // If true, telemetry will be enabled. - bool EnableTelemetry; + const bool EnableTelemetry; + Config(bool E) : EnableTelemetry(E) {} + + virtual std::string makeSessionId() { return "0"; } }; /// For isa, dyn_cast, etc operations on TelemetryInfo. @@ -74,7 +87,7 @@ struct TelemetryInfo { 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; } @@ -104,11 +117,10 @@ class Destination { /// monitored and transmitting the data elsewhere. class Manager { public: - // Invoked upon tool startup - virtual void atStartup(llvm::StringRef ToolPath, TelemetryInfo *Entry) = 0; - - // Invoked upon tool exit. - virtual void atExit(llvm::StringRef ToolPath, TelemetryInfo *Entry) = 0; + // Dispatch Telemetry data to the Destination(s). + // This is non-const because the Manager may add or remove + // data from the entry. + virtual Error dispatch(TelemetryInfo *Entry) = 0; virtual void addDestination(std::unique_ptr Destination) = 0; }; 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 diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 48062874940df..b509456e287ff 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -23,651 +23,288 @@ #include #include +#include + +namespace llvm { +namespace telemetry { // Testing parameters. // These are set by each test to force certain outcomes. // 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 = ""; - - // Controlling whether there is a vendor-provided config for - // Telemetry. - bool HasVendorConfig = false; - - // Controlling whether the data should be sanitized. - bool SanitizeData = false; + // Controlling whether there is vendor plugin. + // In "real" implementation, the plugin-registration + // framework will handle the overrides but for tests, + // we just use a bool flag to decide which function to call. + bool HasVendorPlugin = false; - // These two fields data emitted by the framework for later + // These two fields contain data emitted by the framework for later // verifications by the tests. std::string Buffer = ""; - std::vector EmittedJsons; + std::vector EmittedJsons; // The expected Uuid generated by the fake tool. std::string ExpectedUuid = ""; }; -namespace llvm { -namespace telemetry { -namespace vendor_code { - -// Generate unique (but deterministic "uuid" for testing purposes). -static std::string nextUuid() { - static int seed = 1111; - return std::to_string(seed++); -} - -struct VendorEntryKind { - static const KindType VendorCommon = 168; // 0b010101000 - static const KindType Startup = 169; // 0b010101001 - static const KindType Exit = 170; // 0b010101010 -}; - -// Describes the exit signal of an event. -// This is used by TelemetryInfo below. -struct ExitDescription { - int ExitCode; - std::string Description; -}; - -// 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) {} -}; +class JsonSerializer : public Serializer { +public: + json::Object *getOutputObject() { return object.get(); } -// 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) { - if (T == nullptr) - return false; - // Subclasses of this is also acceptable. - return (T->getKind() & VendorEntryKind::VendorCommon) == - VendorEntryKind::VendorCommon; + llvm::Error start() override { + if (started) + return createStringError("Serializer already in use"); + started = true; + object = std::make_unique(); + return Error::success(); } - KindType getKind() const override { return VendorEntryKind::VendorCommon; } - - virtual void serializeToStream(llvm::raw_ostream &OS) const = 0; - - std::optional ExitDesc; - EventStats Stats; - size_t Counter; -}; - -struct StartupEvent : public VendorCommonTelemetryInfo { - std::string MagicStartupMsg; + void writeBool(StringRef KeyName, bool Value) override { + writeHelper(KeyName, Value); + } - StartupEvent() = default; - StartupEvent(const StartupEvent &E) = default; + void writeInt32(StringRef KeyName, int Value) override { + writeHelper(KeyName, Value); + } - static bool classof(const TelemetryInfo *T) { - if (T == nullptr) - return false; - return T->getKind() == VendorEntryKind::Startup; + void writeSizeT(StringRef KeyName, size_t Value) override { + writeHelper(KeyName, Value); + } + void writeString(StringRef KeyName, StringRef Value) override { + writeHelper(KeyName, Value); } - KindType getKind() const override { return VendorEntryKind::Startup; } + void writeKeyValueMap(StringRef KeyName, + std::map Value) override { + json::Object Inner; + for (auto kv : Value) { + Inner.try_emplace(kv.first, kv.second); + } + writeHelper(KeyName, json::Value(std::move(Inner))); + } - void serializeToStream(llvm::raw_ostream &OS) const override { - OS << "SessionId:" << SessionId << "\n"; - OS << "MagicStartupMsg:" << MagicStartupMsg << "\n"; + llvm::Error finish() override { + if (!started) + return createStringError("Serializer not currently in use"); + started = false; + return Error::success(); } - json::Object serializeToJson() const override { - return json::Object{ - {"Startup", - {{"SessionId", SessionId}, {"MagicStartupMsg", MagicStartupMsg}}}, - }; +private: + template void writeHelper(StringRef Name, T Value) { + assert(started && "serializer not started"); + object->try_emplace(Name, Value); } + bool started = false; + std::unique_ptr object; }; -struct ExitEvent : public VendorCommonTelemetryInfo { - std::string MagicExitMsg; +class StringSerializer : public Serializer { +public: + const std::string &getString() { return Buffer; } - ExitEvent() = default; - // Provide a copy ctor because we may need to make a copy - // before sanitizing the Entry. - ExitEvent(const ExitEvent &E) = default; + llvm::Error start() override { + if (started) + return createStringError("Serializer already in use"); + started = true; + Buffer.clear(); + return Error::success(); + } - static bool classof(const TelemetryInfo *T) { - if (T == nullptr) - return false; - return T->getKind() == VendorEntryKind::Exit; + void writeBool(StringRef KeyName, bool Value) override { + writeHelper(KeyName, Value); } - unsigned getKind() const override { return VendorEntryKind::Exit; } + void writeInt32(StringRef KeyName, int Value) override { + writeHelper(KeyName, Value); + } - void serializeToStream(llvm::raw_ostream &OS) const override { - OS << "SessionId:" << SessionId << "\n"; - if (ExitDesc.has_value()) - OS << "ExitCode:" << ExitDesc->ExitCode << "\n"; - OS << "MagicExitMsg:" << MagicExitMsg << "\n"; + void writeSizeT(StringRef KeyName, size_t Value) override { + writeHelper(KeyName, Value); + } + void writeString(StringRef KeyName, StringRef Value) override { + assert(started && "serializer not started"); } - json::Object serializeToJson() const override { - json::Array I = json::Array{ - {"SessionId", SessionId}, - {"MagicExitMsg", MagicExitMsg}, - }; - if (ExitDesc.has_value()) - I.push_back(json::Value({"ExitCode", ExitDesc->ExitCode})); - return json::Object{ - {"Exit", std::move(I)}, - }; + void writeKeyValueMap(StringRef KeyName, + std::map Value) override { + std::string Inner; + for (auto kv : Value) { + writeHelper(StringRef(kv.first), StringRef(kv.second), &Inner); + } + writeHelper(KeyName, StringRef(Inner)); } -}; -struct CustomTelemetryEvent : public VendorCommonTelemetryInfo { - std::vector Msgs; + llvm::Error finish() override { + if (!started) + return createStringError("Serializer not currently in use"); + started = false; + return Error::success(); + } - CustomTelemetryEvent() = default; - CustomTelemetryEvent(const CustomTelemetryEvent &E) = default; +private: + template + void writeHelper(StringRef Name, T Value, std::string *Buff) { + assert(started && "serializer not started"); + Buff->append((Name + ":" + llvm::Twine(Value) + "\n").str()); + } - void serializeToStream(llvm::raw_ostream &OS) const override { - OS << "SessionId:" << SessionId << "\n"; - int I = 0; - for (const std::string &M : Msgs) { - OS << "MSG_" << I << ":" << M << "\n"; - ++I; - } + template void writeHelper(StringRef Name, T Value) { + writeHelper(Name, Value, &Buffer); } - json::Object serializeToJson() const override { - json::Array Inner{{"SessionId", SessionId}}; - int I = 0; - for (const std::string &M : Msgs) { - Inner.push_back(json::Value({("MSG_" + llvm::Twine(I)).str(), M})); - ++I; - } + bool started = false; + std::string Buffer; +}; - return json::Object{{"Midpoint", std::move(Inner)}}; +namespace vendor { +struct VendorConfig : public Config { + VendorConfig(bool Enable) : Config(Enable) {} + std::string makeSessionId() override { + static int seed = 0; + return std::to_string(seed++); } }; -// The following classes demonstrate how downstream code can -// 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 -// + in what form - -static constexpr llvm::StringLiteral STRING_DEST("STRING"); -static constexpr llvm::StringLiteral JSON_DEST("JSON"); +std::shared_ptr GetTelemetryConfig(const TestContext &Ctxt) { + return std::make_shared(/*EnableTelemetry*/ true); +} -// This Destination sends data to a std::string given at ctor. -class StringDestination : public Destination { +class JsonStorageDestination : public Destination { public: - // ShouldSanitize: if true, sanitize the data before emitting, otherwise, emit - // the full set. - StringDestination(bool ShouldSanitize, std::string &Buf) - : ShouldSanitize(ShouldSanitize), OS(Buf) {} + JsonStorageDestination(TestContext *Ctxt) : CurrentContext(Ctxt) {} Error receiveEntry(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 << "SessionId:" << Entry->SessionId << "\n"; + if (Error err = serializer.start()) { + return err; } + Entry->serialize(serializer); + if (Error err = serializer.finish()) { + return err; + } + + json::Object copied = *serializer.getOutputObject(); + CurrentContext->EmittedJsons.push_back(std::move(copied)); + return Error::success(); } - llvm::StringLiteral name() const override { return STRING_DEST; } + llvm::StringLiteral name() const override { return "JsonDestination"; } 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; + TestContext *CurrentContext; + JsonSerializer serializer; }; -// This Destination sends data to some "blackbox" in form of JSON. -class JsonStreamDestination : public Destination { -public: - JsonStreamDestination(bool ShouldSanitize, TestContext *Ctxt) - : ShouldSanitize(ShouldSanitize), CurrentContext(Ctxt) {} - - Error receiveEntry(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()); - } - if (isa(E)) { - auto Sanitized = sanitizeFields(dyn_cast(E)); - return SendToBlackbox(Sanitized.serializeToJson()); - } - llvm_unreachable("unexpected type"); - } - return SendToBlackbox(E->serializeToJson()); - } - // Unfamiliar entries, just send the entry's ID - return SendToBlackbox(json::Object{{"SessionId", Entry->SessionId}}); - } - - llvm::StringLiteral name() const override { return JSON_DEST; } +struct StartupInfo : public TelemetryInfo { + std::string ToolName; -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; + void serialize(Serializer &serializer) const override { + TelemetryInfo::serialize(serializer); + serializer.writeString("ToolName", ToolName); } +}; - 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. - CurrentContext->EmittedJsons.push_back(std::move(O)); - return Error::success(); +struct ExitInfo : public TelemetryInfo { + int ExitCode; + std::string ExitDesc; + void serialize(Serializer &serializer) const override { + TelemetryInfo::serialize(serializer); + serializer.writeInt32("ExitCode", ExitCode); + serializer.writeString("ExitDesc", ExitDesc); } - bool ShouldSanitize; - TestContext *CurrentContext; }; -// Custom vendor-defined Manager that has additional data-collection point. class TestManager : public Manager { public: - TestManager(std::string SessionId, TestContext *Ctxt) - : CurrentContext(Ctxt), Uuid(SessionId), Counter(0) {} - static std::unique_ptr createInstance(Config *config, TestContext *CurrentContext) { if (!config->EnableTelemetry) return nullptr; - CurrentContext->ExpectedUuid = nextUuid(); - std::unique_ptr Manager = std::make_unique( - CurrentContext->ExpectedUuid, CurrentContext); - // 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 (llvm::StringRef(Dest) == JSON_DEST) { - Manager->addDestination( - std::make_unique( - CurrentContext->SanitizeData, CurrentContext)); - } else if (llvm::StringRef(Dest) == STRING_DEST) { - Manager->addDestination( - std::make_unique( - CurrentContext->SanitizeData, CurrentContext->Buffer)); - } else { - llvm_unreachable( - llvm::Twine("unknown destination: ", Dest).str().c_str()); - } - } - return Manager; - } + CurrentContext->ExpectedUuid = config->makeSessionId(); + std::unique_ptr Ret = std::make_unique( + CurrentContext, CurrentContext->ExpectedUuid); - void atStartup(llvm::StringRef ToolPath, TelemetryInfo *Entry) override { - ToolName = ToolPath.str(); + // Add a few destinations. + Ret->addDestination( + std::make_unique(CurrentContext)); - // The vendor can add additional stuff to the entry before logging. - if (auto *S = dyn_cast(Entry)) { - S->MagicStartupMsg = llvm::Twine("Startup_", ToolPath).str(); - } - emitToDestinations(Entry); + return Ret; } - void atExit(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()); - } + TestManager(TestContext *Ctxt, std::string Id) + : CurrentContext(Ctxt), SessionId(Id) {} - // The vendor can add additional stuff to the entry before logging. - if (auto *E = dyn_cast(Entry)) { - E->MagicExitMsg = llvm::Twine("Exit_", ToolPath).str(); + Error dispatch(TelemetryInfo *Entry) override { + Entry->SessionId = SessionId; + for (auto &Dest : Destinations) { + if (Error err = Dest->receiveEntry(Entry)) { + return err; + } } - - emitToDestinations(Entry); + return Error::success(); } void addDestination(std::unique_ptr Dest) override { Destinations.push_back(std::move(Dest)); } - void atMidpoint(TelemetryInfo *Entry) { - // The custom Manager 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); - } - - const std::string &getUuid() const { return Uuid; } + std::string getSessionId() { return SessionId; } - ~TestManager() = default; + Error atStartup(StartupInfo *Info) { return dispatch(Info); } - template T makeDefaultTelemetryInfo() { - T Ret; - Ret.SessionId = Uuid; - Ret.Counter = Counter++; - return Ret; - } - - TestContext *CurrentContext = nullptr; + Error atExit(ExitInfo *Info) { return dispatch(Info); } private: - void emitToDestinations(TelemetryInfo *Entry) { - for (const auto &Dest : Destinations) { - llvm::Error err = Dest->receiveEntry(Entry); - if (err) { - // Log it and move on. - } - } - } - - const std::string Uuid; - size_t Counter; - std::string ToolName; + TestContext *CurrentContext; + const std::string SessionId; std::vector> Destinations; }; -// Pretend to be a "weakly" defined vendor-specific function. -void ApplyVendorSpecificConfigs(Config *config) { - config->EnableTelemetry = true; -} - -} // namespace vendor_code -} // namespace telemetry -} // namespace llvm - -namespace { - -void ApplyCommonConfig(llvm::telemetry::Config *config) { - // Any shareable configs for the upstream tool can go here. - // ..... -} - -std::shared_ptr -GetTelemetryConfig(TestContext *CurrentContext) { - // 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 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 (CurrentContext->HasVendorConfig) { - llvm::telemetry::vendor_code::ApplyVendorSpecificConfigs(Config.get()); - } - return Config; -} +} // namespace vendor -using namespace llvm; -using namespace llvm::telemetry; +std::shared_ptr GetTelemetryConfig(const TestContext &Ctxt) { + if (Ctxt.HasVendorPlugin) + return vendor::GetTelemetryConfig(Ctxt); -// 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::TestManager *T) { - vendor_code::StartupEvent Entry = - T->makeDefaultTelemetryInfo(); - Entry.Stats = {StartTime, InitCompleteTime}; - T->atStartup(ToolName, &Entry); -} - -void AtToolExit(std::string ToolName, vendor_code::TestManager *T) { - vendor_code::ExitEvent Entry = - T->makeDefaultTelemetryInfo(); - Entry.Stats = {ExitTime, ExitCompleteTime}; - - if (T->CurrentContext->HasExitError) { - Entry.ExitDesc = {1, T->CurrentContext->ExitMsg}; - } - T->atExit(ToolName, &Entry); -} - -void AtToolMidPoint(vendor_code::TestManager *T) { - vendor_code::CustomTelemetryEvent Entry = - T->makeDefaultTelemetryInfo(); - Entry.Stats = {MidPointTime, MidPointCompleteTime}; - T->atMidpoint(&Entry); -} - -// Without vendor's implementation, telemetry is not enabled by default. -TEST(TelemetryTest, TelemetryDefault) { - // Preset some test params. - TestContext Context; - Context.HasVendorConfig = false; - TestContext *CurrentContext = &Context; - - std::shared_ptr Config = - GetTelemetryConfig(CurrentContext); - auto Tool = - vendor_code::TestManager::createInstance(Config.get(), CurrentContext); - - EXPECT_EQ(nullptr, Tool.get()); + return std::make_shared(false); } TEST(TelemetryTest, TelemetryEnabled) { - const std::string ToolName = "TelemetryTest"; - - // Preset some test params. - TestContext Context; - Context.HasVendorConfig = true; - Context.SanitizeData = false; - Context.Buffer.clear(); - Context.EmittedJsons.clear(); - TestContext *CurrentContext = &Context; - - std::shared_ptr Config = - GetTelemetryConfig(CurrentContext); + const std::string ToolName = "TelemetryTestTool"; - // Add some destinations - Config->AdditionalDestinations.push_back(vendor_code::STRING_DEST.str()); - Config->AdditionalDestinations.push_back(vendor_code::JSON_DEST.str()); - - auto Tool = - vendor_code::TestManager::createInstance(Config.get(), CurrentContext); - - AtToolStart(ToolName, Tool.get()); - AtToolMidPoint(Tool.get()); - AtToolExit(ToolName, Tool.get()); - - // Check that the Tool uses the expected UUID. - EXPECT_STREQ(Tool->getUuid().c_str(), CurrentContext->ExpectedUuid.c_str()); - - // Check that the StringDestination emitted properly - { - std::string ExpectedBuffer = - ("SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\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:Exit_" + llvm::Twine(ToolName) + "\n") - .str(); - - EXPECT_STREQ(ExpectedBuffer.c_str(), CurrentContext->Buffer.c_str()); - } - - // Check that the JsonDestination emitted properly - { - - // There should be 3 events emitted by the Manager (start, midpoint, exit) - EXPECT_EQ(static_cast(3), CurrentContext->EmittedJsons.size()); - - const json::Value *StartupEntry = - CurrentContext->EmittedJsons[0].get("Startup"); - ASSERT_NE(StartupEntry, nullptr); - json::Value ExpectedStartup({ - {"SessionId", CurrentContext->ExpectedUuid}, - {"MagicStartupMsg", ("Startup_" + llvm::Twine(ToolName)).str()}, - }); - EXPECT_EQ(ExpectedStartup, *StartupEntry); - - const json::Value *MidpointEntry = - CurrentContext->EmittedJsons[1].get("Midpoint"); - ASSERT_NE(MidpointEntry, nullptr); - json::Value ExpectedMidpoint{{"SessionId", CurrentContext->ExpectedUuid}, - {"MSG_0", "Two"}, - {"MSG_1", "Deux"}, - {"MSG_2", "Zwei"}}; - EXPECT_EQ(ExpectedMidpoint, *MidpointEntry); - - const json::Value *ExitEntry = CurrentContext->EmittedJsons[2].get("Exit"); - ASSERT_NE(ExitEntry, nullptr); - json::Value ExpectedExit({ - {"SessionId", CurrentContext->ExpectedUuid}, - {"MagicExitMsg", ("Exit_" + llvm::Twine(ToolName)).str()}, - }); - EXPECT_EQ(ExpectedExit, *ExitEntry); - } -} - -// 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 = "TelemetryTest_SanitizedData"; - - // Preset some test params. + // Preset some params TestContext Context; - Context.HasVendorConfig = true; - Context.SanitizeData = true; + Context.HasVendorPlugin = true; Context.Buffer.clear(); Context.EmittedJsons.clear(); - TestContext *CurrentContext = &Context; - std::shared_ptr Config = - GetTelemetryConfig(CurrentContext); - - // Add some destinations - Config->AdditionalDestinations.push_back(vendor_code::STRING_DEST.str()); - Config->AdditionalDestinations.push_back(vendor_code::JSON_DEST.str()); - - auto Tool = - vendor_code::TestManager::createInstance(Config.get(), CurrentContext); - - 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 ExpectedBuffer = - ("SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\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:Exit_" + llvm::Twine(ToolName) + "\n") - .str(); - EXPECT_STREQ(ExpectedBuffer.c_str(), CurrentContext->Buffer.c_str()); - } - - // Check that the JsonDestination emitted properly - { - - // There should be 3 events emitted by the Manager (start, midpoint, exit) - EXPECT_EQ(static_cast(3), CurrentContext->EmittedJsons.size()); - - const json::Value *StartupEntry = - CurrentContext->EmittedJsons[0].get("Startup"); - ASSERT_NE(StartupEntry, nullptr); - json::Value ExpectedStartup({ - {"SessionId", CurrentContext->ExpectedUuid}, - {"MagicStartupMsg", ("Startup_" + llvm::Twine(ToolName)).str()}, - }); - EXPECT_EQ(ExpectedStartup, *StartupEntry); - - const json::Value *MidpointEntry = - CurrentContext->EmittedJsons[1].get("Midpoint"); - ASSERT_NE(MidpointEntry, nullptr); - // The JsonDestination should have removed the even-positioned msgs. - json::Value ExpectedMidpoint{{"SessionId", CurrentContext->ExpectedUuid}, - {"MSG_0", ""}, - {"MSG_1", "Deux"}, - {"MSG_2", ""}}; - EXPECT_EQ(ExpectedMidpoint, *MidpointEntry); - - const json::Value *ExitEntry = CurrentContext->EmittedJsons[2].get("Exit"); - ASSERT_NE(ExitEntry, nullptr); - json::Value ExpectedExit({ - {"SessionId", CurrentContext->ExpectedUuid}, - {"MagicExitMsg", ("Exit_" + llvm::Twine(ToolName)).str()}, - }); - EXPECT_EQ(ExpectedExit, *ExitEntry); - } + std::shared_ptr Config = GetTelemetryConfig(Context); + auto Manager = vendor::TestManager::createInstance(Config.get(), &Context); + + EXPECT_STREQ(Manager->getSessionId().c_str(), Context.ExpectedUuid.c_str()); + + vendor::StartupInfo S; + S.ToolName = ToolName; + + Error startupEmitStatus = Manager->atStartup(&S); + EXPECT_FALSE(startupEmitStatus); + const json::Object &StartupEntry = Context.EmittedJsons[0]; + json::Object ExpectedStartup( + {{"SessionId", Context.ExpectedUuid}, {"ToolName", ToolName}}); + EXPECT_EQ(ExpectedStartup, StartupEntry); + + vendor::ExitInfo E; + E.ExitCode = 0; + E.ExitDesc = "success"; + Error exitEmitStatus = Manager->atExit(&E); + EXPECT_FALSE(exitEmitStatus); + const json::Object &ExitEntry = Context.EmittedJsons[1]; + json::Object ExpectedExit({{"SessionId", Context.ExpectedUuid}, + {"ExitCode", 0}, + {"ExitDesc", "success"}}); + EXPECT_EQ(ExpectedExit, ExitEntry); } -} // namespace +} // namespace telemetry +} // namespace llvm From 398ed3e37d656bc998684fba6c69f643ed10dfd9 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 26 Nov 2024 21:11:35 -0500 Subject: [PATCH 46/94] updated doc --- llvm/docs/Telemetry.rst | 192 +++++++++++++++++++++------------------- 1 file changed, 101 insertions(+), 91 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index b7610b0400333..dce1444ce273d 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -87,89 +87,113 @@ How to implement and interact with the API To use Telemetry in your tool, you need to provide a concrete implementation of the ``Manager`` class and ``Destination``. -1) Define a custom ``Manager`` and ``Destination`` +1) Define a custom ``Serializer``, ``Manager`` and ``Destination`` .. code-block:: c++ - // This destination just prints the given entry to stdout. - // In "real life", this would be where you forward the data to your - // custom data storage. - class MyStdoutDestination : public llvm::telemetry::Destination { - 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. - } - }; +class JsonSerializer : public Serializer { +public: + json::Object *getOutputObject() { return object.get(); } + + llvm::Error start() override { + if (started) + return createStringError("Serializer already in use"); + started = true; + object = std::make_unique(); + 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 { + writeHelper(KeyName, Value); + } + + void writeKeyValueMap(StringRef KeyName, + std::map Value) override { + json::Object Inner; + for (auto kv : Value) { + Inner.try_emplace(kv.first, kv.second); + } + writeHelper(KeyName, json::Value(std::move(Inner))); + } + + llvm::Error finish() override { + if (!started) + return createStringError("Serializer not currently in use"); + started = false; + return Error::success(); + } + +private: + template void writeHelper(StringRef Name, T Value) { + assert(started && "serializer not started"); + object->try_emplace(Name, Value); + } + bool started = false; + std::unique_ptr object; +}; - // This defines a custom TelemetryInfo that has an addition Msg field. - struct MyTelemetryInfo : public telemetry::TelemetryInfo { - std::string Msg; - - json::Object serializeToJson() oconst { - json::Object Ret = ManageryInfo::serializeToJson(); - Ret.emplace_back("MyMsg", Msg); - return std::move(Ret); - } +// This defines a custom TelemetryInfo that has an addition Msg field. +struct MyTelemetryInfo : public telemetry::TelemetryInfo { + std::string Msg; + + Error serialize(Serializer& serializer) const override { + TelemetryInfo::serialize(serializer); + serializer.writeString("MyMsg", Msg); + } - // TODO: implement getKind() and classof() to support dyn_cast operations. - }; + // Note: implement getKind() and classof() to support dyn_cast operations. +}; - class MyManager : public telemery::Manager { - public: - static std::unique_ptr createInstatnce(telemetry::Config* config) { - // If Telemetry is not enabled, then just return null; - if (!config->EnableTelemetry) return nullptr; - - std::make_unique(); - } - MyManager() = default; +class MyManager : public telemery::Manager { +public: +static std::unique_ptr createInstatnce(telemetry::Config* config) { + // If Telemetry is not enabled, then just return null; + if (!config->EnableTelemetry) return nullptr; + + return std::make_unique(); +} +MyManager() = default; + +Error dispatch(TelemetryInfo* Entry) const override { + Entry->SessionId = SessionId; + emitToAllDestinations(Entry); +} + +Error logStartup(MyTelemetryInfo* Entry) { + // Optionally add additional field + Entry->Msg = "CustomMsg"; + return dispatch(Entry); + +} - void logStartup(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 addDestination(std::unique_ptr dest) override { + destinations.push_back(std::move(dest)); +} - void logExit(StringRef ToolName, TelemetryInfo* Entry) override { - if (MyTelemetryInfo* M = dyn_cast(Entry)) { - M->Msg = "Exiting 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 parallel, if needed. - for (Destination* Dest : Destinations) - Dest->emitEntry(Entry); - } - std::vector Destinations; - }; +// You can also define additional instrumentation points. +void logAdditionalPoint(TelemetryInfo* Entry) { +// .... code here +} +private: +void emitToAllDestinations(const TelemetryInfo* Entry) { +// Note: could do this in parallel, if needed. +for (Destination* Dest : Destinations) +Dest->receiveEntry(Entry); +} +std::vector Destinations; +const std::string SessionId; +}; 2) Use the library in your tool. @@ -181,7 +205,7 @@ Logging the tool init-process: auto StartTime = std::chrono::time_point::now(); telemetry::Config MyConfig = makeConfig(); // Build up the appropriate Config struct here. auto Manager = MyManager::createInstance(&MyConfig); - std::string CurrentSessionId = ...; // Make some unique ID corresponding to the current session here. + // Any other tool's init code can go here // ... @@ -190,23 +214,9 @@ Logging the tool init-process: // 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}; Manager->logStartup("MyTool", &Entry); Similar code can be used for logging the tool's exit. -Additionally, 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}; - Manager->logAdditionalPoint(&Entry); From e462908c8407ff23af5ed37c99a3e708bed00908 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 4 Dec 2024 14:52:44 -0500 Subject: [PATCH 47/94] fix build warning --- llvm/docs/Telemetry.rst | 177 +++++++++++++++++++--------------------- 1 file changed, 86 insertions(+), 91 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index dce1444ce273d..56473ffba0ea2 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -91,109 +91,104 @@ To use Telemetry in your tool, you need to provide a concrete implementation of .. code-block:: c++ -class JsonSerializer : public Serializer { -public: - json::Object *getOutputObject() { return object.get(); } - - llvm::Error start() override { - if (started) - return createStringError("Serializer already in use"); - started = true; - object = std::make_unique(); - return Error::success(); - } + class JsonSerializer : public Serializer { + public: + json::Object *getOutputObject() { return object.get(); } + + llvm::Error start() override { + if (started) + return createStringError("Serializer already in use"); + started = true; + object = std::make_unique(); + return Error::success(); + } - void writeBool(StringRef KeyName, bool Value) override { - writeHelper(KeyName, Value); - } + void writeBool(StringRef KeyName, bool Value) override { + writeHelper(KeyName, Value); + } - void writeInt32(StringRef KeyName, int 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 { - writeHelper(KeyName, Value); - } + void writeSizeT(StringRef KeyName, size_t Value) override { + writeHelper(KeyName, Value); + } + void writeString(StringRef KeyName, StringRef Value) override { + writeHelper(KeyName, Value); + } - void writeKeyValueMap(StringRef KeyName, - std::map Value) override { - json::Object Inner; - for (auto kv : Value) { - Inner.try_emplace(kv.first, kv.second); + void writeKeyValueMap(StringRef KeyName, + std::map Value) override { + json::Object Inner; + for (auto kv : Value) { + Inner.try_emplace(kv.first, kv.second); + } + writeHelper(KeyName, json::Value(std::move(Inner))); } - writeHelper(KeyName, json::Value(std::move(Inner))); - } - llvm::Error finish() override { - if (!started) - return createStringError("Serializer not currently in use"); - started = false; - return Error::success(); - } + llvm::Error finish() override { + if (!started) + return createStringError("Serializer not currently in use"); + started = false; + return Error::success(); + } -private: - template void writeHelper(StringRef Name, T Value) { - assert(started && "serializer not started"); - object->try_emplace(Name, Value); - } - bool started = false; - std::unique_ptr object; -}; + private: + template void writeHelper(StringRef Name, T Value) { + assert(started && "serializer not started"); + object->try_emplace(Name, Value); + } + bool started = false; + std::unique_ptr object; + }; + + // This defines a custom TelemetryInfo that has an addition Msg field. + struct MyTelemetryInfo : public telemetry::TelemetryInfo { + std::string Msg; + + Error serialize(Serializer& serializer) const override { + TelemetryInfo::serialize(serializer); + serializer.writeString("MyMsg", Msg); + } + + // Note: implement getKind() and classof() to support dyn_cast operations. + }; -// This defines a custom TelemetryInfo that has an addition Msg field. -struct MyTelemetryInfo : public telemetry::TelemetryInfo { - std::string Msg; + class MyManager : public telemery::Manager { + public: + static std::unique_ptr createInstatnce(telemetry::Config* config) { + // If Telemetry is not enabled, then just return null; + if (!config->EnableTelemetry) return nullptr; - Error serialize(Serializer& serializer) const override { - TelemetryInfo::serialize(serializer); - serializer.writeString("MyMsg", Msg); + return std::make_unique(); + } + MyManager() = default; + + Error dispatch(TelemetryInfo* Entry) const override { + Entry->SessionId = SessionId; + emitToAllDestinations(Entry); } - // Note: implement getKind() and classof() to support dyn_cast operations. -}; - -class MyManager : public telemery::Manager { -public: -static std::unique_ptr createInstatnce(telemetry::Config* config) { - // If Telemetry is not enabled, then just return null; - if (!config->EnableTelemetry) return nullptr; - - return std::make_unique(); -} -MyManager() = default; - -Error dispatch(TelemetryInfo* Entry) const override { - Entry->SessionId = SessionId; - emitToAllDestinations(Entry); -} - -Error logStartup(MyTelemetryInfo* Entry) { - // Optionally add additional field - Entry->Msg = "CustomMsg"; - return dispatch(Entry); - -} - -void addDestination(std::unique_ptr dest) override { - destinations.push_back(std::move(dest)); -} + void addDestination(std::unique_ptr dest) override { + destinations.push_back(std::move(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 parallel, if needed. -for (Destination* Dest : Destinations) -Dest->receiveEntry(Entry); -} -std::vector Destinations; -const std::string SessionId; -}; + // You can also define additional instrumentation points. + void logAdditionalPoint(TelemetryInfo* Entry) { + // .... code here + } + + private: + void emitToAllDestinations(const TelemetryInfo* Entry) { + for (Destination* Dest : Destinations) + Dest->receiveEntry(Entry); + } + } + + std::vector Destinations; + const std::string SessionId; + }; 2) Use the library in your tool. From df8a9af01f208cd80463dce5e4f01ddda44a6ce0 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 4 Dec 2024 15:13:31 -0500 Subject: [PATCH 48/94] more cleanup --- llvm/docs/Telemetry.rst | 61 ++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 56473ffba0ea2..b25c1bb1d9fed 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -87,7 +87,7 @@ How to implement and interact with the API To use Telemetry in your tool, you need to provide a concrete implementation of the ``Manager`` class and ``Destination``. -1) Define a custom ``Serializer``, ``Manager`` and ``Destination`` +1) Define a custom ``Serializer``, ``Manager``, ``Destination`` and optionally a subclass of ``TelemetryInfo`` .. code-block:: c++ @@ -142,19 +142,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of bool started = false; std::unique_ptr object; }; - - // This defines a custom TelemetryInfo that has an addition Msg field. - struct MyTelemetryInfo : public telemetry::TelemetryInfo { - std::string Msg; - - Error serialize(Serializer& serializer) const override { - TelemetryInfo::serialize(serializer); - serializer.writeString("MyMsg", Msg); - } - - // Note: implement getKind() and classof() to support dyn_cast operations. - }; - + class MyManager : public telemery::Manager { public: static std::unique_ptr createInstatnce(telemetry::Config* config) { @@ -173,8 +161,14 @@ To use Telemetry in your tool, you need to provide a concrete implementation of void addDestination(std::unique_ptr dest) override { destinations.push_back(std::move(dest)); } - + // You can also define additional instrumentation points. + void logStartup(TelemetryInfo* Entry) { + // Add some additional data to entry. + Entry->Msg = "Some message"; + dispatch(Entry); + } + void logAdditionalPoint(TelemetryInfo* Entry) { // .... code here } @@ -189,6 +183,40 @@ To use Telemetry in your tool, you need to provide a concrete implementation of std::vector Destinations; const std::string SessionId; }; + + class MyDestination : public telemetry::Destination { + public: + Error receiveEntry(const TelemetryInfo* Entry) override { + if (Error err = serializer.start()) { + return err; + } + Entry->serialize(serializer); + if (Error err = serializer.finish()) { + return err; + } + + json::Object copied = *serializer.getOutputObject(); + // Send the `copied` object to wherever. + return Error::success(); + } + + }; + + // This defines a custom TelemetryInfo that has an addition Msg field. + struct MyTelemetryInfo : public telemetry::TelemetryInfo { + std::chrono::time_point Start; + std::chrono::time_point End; + + std::string Msg; + + Error serialize(Serializer& serializer) const override { + TelemetryInfo::serialize(serializer); + serializer.writeString("MyMsg", Msg); + } + + // Note: implement getKind() and classof() to support dyn_cast operations. + }; + 2) Use the library in your tool. @@ -210,7 +238,8 @@ Logging the tool init-process: auto EndTime = std::chrono::time_point::now(); MyTelemetryInfo Entry; - Entry.Stats = {StartTime, EndTime}; + Entry.Start = StartTime; + Entry.End = EndTime; Manager->logStartup("MyTool", &Entry); Similar code can be used for logging the tool's exit. From 70f47428d1696d1f8c2538962f9e76e89f381c01 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 4 Dec 2024 15:16:23 -0500 Subject: [PATCH 49/94] spacing --- llvm/docs/Telemetry.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index b25c1bb1d9fed..77b862947dae7 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -93,7 +93,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of class JsonSerializer : public Serializer { public: - json::Object *getOutputObject() { return object.get(); } + json::Object *getOutputObject() { return object.get(); } llvm::Error start() override { if (started) @@ -199,7 +199,9 @@ To use Telemetry in your tool, you need to provide a concrete implementation of // Send the `copied` object to wherever. return Error::success(); } - + + private: + JsonSerializer serializer; }; // This defines a custom TelemetryInfo that has an addition Msg field. From 8eab77a12f8c0ffbba9b1f5ce864145681db2a0c Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 4 Dec 2024 15:20:54 -0500 Subject: [PATCH 50/94] more docs --- llvm/docs/Telemetry.rst | 2 +- llvm/include/llvm/Telemetry/Telemetry.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 77b862947dae7..d83b5b68caa91 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -242,7 +242,7 @@ Logging the tool init-process: Entry.Start = StartTime; Entry.End = EndTime; - Manager->logStartup("MyTool", &Entry); + Manager->logStartup(&Entry); Similar code can be used for logging the tool's exit. diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index fb4a204ff5f68..9da67f4699d9b 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -118,10 +118,11 @@ class Destination { class Manager { public: // Dispatch Telemetry data to the Destination(s). - // This is non-const because the Manager may add or remove + // The argument is non-const because the Manager may add or remove // data from the entry. virtual Error dispatch(TelemetryInfo *Entry) = 0; + // Register a Destination. virtual void addDestination(std::unique_ptr Destination) = 0; }; From f9e1a6583137ce698c3e995f3845271936165221 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Mon, 9 Dec 2024 15:15:13 -0500 Subject: [PATCH 51/94] pass map arg as const ref --- llvm/include/llvm/Telemetry/Telemetry.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 9da67f4699d9b..f6198bd4d3401 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -32,8 +32,9 @@ class Serializer { 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, - std::map Value) = 0; + virtual void + writeKeyValueMap(StringRef KeyName, + const std::map &Value) = 0; virtual llvm::Error finish() = 0; }; From 2a1fbe59b59d3682eb2766940b762556e853abd5 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 11 Dec 2024 10:08:36 -0500 Subject: [PATCH 52/94] Update llvm/docs/Telemetry.rst Co-authored-by: James Henderson --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index d83b5b68caa91..98ddf35fa27da 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -127,7 +127,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of writeHelper(KeyName, json::Value(std::move(Inner))); } - llvm::Error finish() override { + Error finish() override { if (!started) return createStringError("Serializer not currently in use"); started = false; From 6e3a85dad0a2e511bf56ff09967c2052171c9bcf Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 11 Dec 2024 10:08:59 -0500 Subject: [PATCH 53/94] Update llvm/docs/Telemetry.rst Co-authored-by: James Henderson --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 98ddf35fa27da..8cce9405f3ec5 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -204,7 +204,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of JsonSerializer serializer; }; - // This defines a custom TelemetryInfo that has an addition Msg field. + // This defines a custom TelemetryInfo that has an additional Msg field. struct MyTelemetryInfo : public telemetry::TelemetryInfo { std::chrono::time_point Start; std::chrono::time_point End; From f9b1cce143c0aeb517bca9ad0cba2fc83cfdf53d Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 11 Dec 2024 10:48:35 -0500 Subject: [PATCH 54/94] addressed review comments --- llvm/docs/Telemetry.rst | 26 +++++++------- llvm/include/llvm/Telemetry/Telemetry.h | 23 ++++++------ llvm/lib/Telemetry/Telemetry.cpp | 2 +- llvm/unittests/Telemetry/TelemetryTest.cpp | 42 +++++++++++----------- 4 files changed, 45 insertions(+), 48 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 8cce9405f3ec5..88220fe869bd6 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -95,7 +95,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of public: json::Object *getOutputObject() { return object.get(); } - llvm::Error start() override { + llvm::Error init() override { if (started) return createStringError("Serializer already in use"); started = true; @@ -103,23 +103,24 @@ To use Telemetry in your tool, you need to provide a concrete implementation of return Error::success(); } - void writeBool(StringRef KeyName, bool Value) override { + // Serialize the given value. + void write(StringRef KeyName, bool Value) override { writeHelper(KeyName, Value); } - void writeInt32(StringRef KeyName, int Value) override { + void write(StringRef KeyName, int Value) override { writeHelper(KeyName, Value); } - void writeSizeT(StringRef KeyName, size_t Value) override { + void write(StringRef KeyName, size_t Value) override { writeHelper(KeyName, Value); } - void writeString(StringRef KeyName, StringRef Value) override { + void write(StringRef KeyName, StringRef Value) override { writeHelper(KeyName, Value); } - void writeKeyValueMap(StringRef KeyName, - std::map Value) override { + void write(StringRef KeyName, + const std::map& Value) override { json::Object Inner; for (auto kv : Value) { Inner.try_emplace(kv.first, kv.second); @@ -127,7 +128,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of writeHelper(KeyName, json::Value(std::move(Inner))); } - Error finish() override { + Error finalize() override { if (!started) return createStringError("Serializer not currently in use"); started = false; @@ -175,7 +176,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of private: void emitToAllDestinations(const TelemetryInfo* Entry) { - for (Destination* Dest : Destinations) + for (Destination* Dest : Destinations) { Dest->receiveEntry(Entry); } } @@ -187,11 +188,11 @@ To use Telemetry in your tool, you need to provide a concrete implementation of class MyDestination : public telemetry::Destination { public: Error receiveEntry(const TelemetryInfo* Entry) override { - if (Error err = serializer.start()) { + if (Error err = serializer.init()) { return err; } Entry->serialize(serializer); - if (Error err = serializer.finish()) { + if (Error err = serializer.finalize()) { return err; } @@ -206,9 +207,6 @@ To use Telemetry in your tool, you need to provide a concrete implementation of // This defines a custom TelemetryInfo that has an additional Msg field. struct MyTelemetryInfo : public telemetry::TelemetryInfo { - std::chrono::time_point Start; - std::chrono::time_point End; - std::string Msg; Error serialize(Serializer& serializer) const override { diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index f6198bd4d3401..f2a70195c3c00 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -27,21 +27,20 @@ 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 llvm::Error init() = 0; + virtual void write(StringRef KeyName, bool Value) = 0; + virtual void write(StringRef KeyName, int Value) = 0; + virtual void write(StringRef KeyName, size_t Value) = 0; + virtual void write(StringRef KeyName, StringRef Value) = 0; + virtual void write(StringRef KeyName, + const std::map &Value) = 0; + virtual llvm::Error finalize() = 0; }; -/// Configuration for the Telemeter class. +/// Configuration for the Manager class. /// This stores configurations from both users and vendors and is passed -/// to the Telemeter upon construction. (Any changes to the config after -/// the Telemeter's construction will not have any effect on it). +/// to the Manager upon construction. (Any changes to the config after +/// the Manager'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. 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 diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index b509456e287ff..c860ef761ab8f 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -51,7 +51,7 @@ class JsonSerializer : public Serializer { public: json::Object *getOutputObject() { return object.get(); } - llvm::Error start() override { + llvm::Error init() override { if (started) return createStringError("Serializer already in use"); started = true; @@ -59,23 +59,23 @@ class JsonSerializer : public Serializer { return Error::success(); } - void writeBool(StringRef KeyName, bool Value) override { + void write(StringRef KeyName, bool Value) override { writeHelper(KeyName, Value); } - void writeInt32(StringRef KeyName, int Value) override { + void write(StringRef KeyName, int Value) override { writeHelper(KeyName, Value); } - void writeSizeT(StringRef KeyName, size_t Value) override { + void write(StringRef KeyName, size_t Value) override { writeHelper(KeyName, Value); } - void writeString(StringRef KeyName, StringRef Value) override { + void write(StringRef KeyName, StringRef Value) override { writeHelper(KeyName, Value); } - void writeKeyValueMap(StringRef KeyName, - std::map Value) override { + void write(StringRef KeyName, + const std::map &Value) override { json::Object Inner; for (auto kv : Value) { Inner.try_emplace(kv.first, kv.second); @@ -83,7 +83,7 @@ class JsonSerializer : public Serializer { writeHelper(KeyName, json::Value(std::move(Inner))); } - llvm::Error finish() override { + llvm::Error finalize() override { if (!started) return createStringError("Serializer not currently in use"); started = false; @@ -103,7 +103,7 @@ class StringSerializer : public Serializer { public: const std::string &getString() { return Buffer; } - llvm::Error start() override { + llvm::Error init() override { if (started) return createStringError("Serializer already in use"); started = true; @@ -111,23 +111,23 @@ class StringSerializer : public Serializer { return Error::success(); } - void writeBool(StringRef KeyName, bool Value) override { + void write(StringRef KeyName, bool Value) override { writeHelper(KeyName, Value); } - void writeInt32(StringRef KeyName, int Value) override { + void write(StringRef KeyName, int Value) override { writeHelper(KeyName, Value); } - void writeSizeT(StringRef KeyName, size_t Value) override { + void write(StringRef KeyName, size_t Value) override { writeHelper(KeyName, Value); } - void writeString(StringRef KeyName, StringRef Value) override { + void write(StringRef KeyName, StringRef Value) override { assert(started && "serializer not started"); } - void writeKeyValueMap(StringRef KeyName, - std::map Value) override { + void write(StringRef KeyName, + const std::map &Value) override { std::string Inner; for (auto kv : Value) { writeHelper(StringRef(kv.first), StringRef(kv.second), &Inner); @@ -135,7 +135,7 @@ class StringSerializer : public Serializer { writeHelper(KeyName, StringRef(Inner)); } - llvm::Error finish() override { + llvm::Error finalize() override { if (!started) return createStringError("Serializer not currently in use"); started = false; @@ -175,11 +175,11 @@ class JsonStorageDestination : public Destination { JsonStorageDestination(TestContext *Ctxt) : CurrentContext(Ctxt) {} Error receiveEntry(const TelemetryInfo *Entry) override { - if (Error err = serializer.start()) { + if (Error err = serializer.init()) { return err; } Entry->serialize(serializer); - if (Error err = serializer.finish()) { + if (Error err = serializer.finalize()) { return err; } @@ -201,7 +201,7 @@ struct StartupInfo : public TelemetryInfo { void serialize(Serializer &serializer) const override { TelemetryInfo::serialize(serializer); - serializer.writeString("ToolName", ToolName); + serializer.write("ToolName", ToolName); } }; @@ -210,8 +210,8 @@ struct ExitInfo : public TelemetryInfo { std::string ExitDesc; void serialize(Serializer &serializer) const override { TelemetryInfo::serialize(serializer); - serializer.writeInt32("ExitCode", ExitCode); - serializer.writeString("ExitDesc", ExitDesc); + serializer.write("ExitCode", ExitCode); + serializer.write("ExitDesc", ExitDesc); } }; From 0f38a74d2dc73450919e3999fa9f6396ccc2c706 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Mon, 16 Dec 2024 11:08:24 -0500 Subject: [PATCH 55/94] Update llvm/docs/Telemetry.rst Co-authored-by: James Henderson --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 88220fe869bd6..c9e58552be566 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -120,7 +120,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of } void write(StringRef KeyName, - const std::map& Value) override { + const std::map &Value) override { json::Object Inner; for (auto kv : Value) { Inner.try_emplace(kv.first, kv.second); From ad57099fa43032fcc0ba09c1eda4033499f8dc7b Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Mon, 16 Dec 2024 11:08:51 -0500 Subject: [PATCH 56/94] Update llvm/docs/Telemetry.rst Co-authored-by: James Henderson --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index c9e58552be566..bb55071917e45 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -230,7 +230,7 @@ Logging the tool init-process: auto Manager = MyManager::createInstance(&MyConfig); - // Any other tool's init code can go 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 From a2fcd4d53cb3f4f553815420843ce13f26438f13 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Mon, 16 Dec 2024 11:09:06 -0500 Subject: [PATCH 57/94] Update llvm/include/llvm/Telemetry/Telemetry.h Co-authored-by: James Henderson --- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index f2a70195c3c00..91c86504bedb5 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -7,7 +7,7 @@ //===----------------------------------------------------------------------===// /// /// \file -/// This file provides the basic framework for Telemetry +/// This file provides the basic framework for Telemetry. /// Refer to its documentation at llvm/docs/Telemetry.rst for more details. //===---------------------------------------------------------------------===// From 628e7e9674bacc3fc9f3569ae160a5fca1f2482f Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Mon, 16 Dec 2024 11:09:20 -0500 Subject: [PATCH 58/94] Update llvm/include/llvm/Telemetry/Telemetry.h Co-authored-by: James Henderson --- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 91c86504bedb5..b432a7c163f68 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -79,7 +79,7 @@ struct TelemetryInfo { // // Note: a tool could have multiple sessions running at once, in which // case, these shall be multiple sets of TelemetryInfo with multiple unique - // ids. + // IDs. // // Different usages can assign different types of IDs to this field. std::string SessionId; From 6ea4e927ada8ed82c83461997ae5723fa3b1fa7a Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Mon, 16 Dec 2024 11:10:07 -0500 Subject: [PATCH 59/94] Update llvm/docs/Telemetry.rst Co-authored-by: James Henderson --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index bb55071917e45..e84256cec5e37 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -234,7 +234,7 @@ Logging the tool init-process: // ... // Finally, take a snapshot of the time now so we know how long it took the - // init process to finish + // init process to finish. auto EndTime = std::chrono::time_point::now(); MyTelemetryInfo Entry; From 08cf32f1e2cc9f83fad120014d278cfc1ed98259 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Mon, 16 Dec 2024 11:10:19 -0500 Subject: [PATCH 60/94] Update llvm/docs/Telemetry.rst Co-authored-by: James Henderson --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index e84256cec5e37..4cf024337a92f 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -17,7 +17,7 @@ Objective Provides a common framework in LLVM for collecting various usage and performance metrics. -It is located at ``llvm/Telemetry/Telemetry.h`` +It is located at ``llvm/Telemetry/Telemetry.h``. Characteristics --------------- From 3c524010385e803a67b2d386a3d7236883b92018 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Mon, 16 Dec 2024 11:10:34 -0500 Subject: [PATCH 61/94] Update llvm/include/llvm/Telemetry/Telemetry.h Co-authored-by: James Henderson --- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index b432a7c163f68..b9a6540e463a6 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -27,7 +27,7 @@ namespace telemetry { class Serializer { public: - virtual llvm::Error init() = 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, size_t Value) = 0; From 07f06c077fa1778d7b0c95b17d28783594e2b52b Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Mon, 16 Dec 2024 11:10:48 -0500 Subject: [PATCH 62/94] Update llvm/include/llvm/Telemetry/Telemetry.h Co-authored-by: James Henderson --- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index b9a6540e463a6..a5c7a286f41e4 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -34,7 +34,7 @@ class Serializer { virtual void write(StringRef KeyName, StringRef Value) = 0; virtual void write(StringRef KeyName, const std::map &Value) = 0; - virtual llvm::Error finalize() = 0; + virtual Error finalize() = 0; }; /// Configuration for the Manager class. From 06e746eb6ea01390732e7499d852b550414a0d90 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Mon, 16 Dec 2024 11:15:43 -0500 Subject: [PATCH 63/94] addressed review comments: - use unsigned long long instead of size_t - updated header comment - various styling fixes --- llvm/docs/Telemetry.rst | 6 +----- llvm/include/llvm/Telemetry/Telemetry.h | 4 ++-- llvm/unittests/Telemetry/TelemetryTest.cpp | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 4cf024337a92f..114c2e8db7c6f 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -8,10 +8,6 @@ Telemetry framework in LLVM .. toctree:: :hidden: -=========================== -Telemetry framework in LLVM -=========================== - Objective ========= @@ -112,7 +108,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of writeHelper(KeyName, Value); } - void write(StringRef KeyName, size_t Value) override { + void write(StringRef KeyName, unsigned long long Value) override { writeHelper(KeyName, Value); } void write(StringRef KeyName, StringRef Value) override { diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index a5c7a286f41e4..47df315d72fec 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -1,4 +1,4 @@ -//===- 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. @@ -30,7 +30,7 @@ class Serializer { 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, size_t Value) = 0; + virtual void write(StringRef KeyName, unsigned long long Value) = 0; virtual void write(StringRef KeyName, StringRef Value) = 0; virtual void write(StringRef KeyName, const std::map &Value) = 0; diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index c860ef761ab8f..c0858f5a1d9ac 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -67,7 +67,7 @@ class JsonSerializer : public Serializer { writeHelper(KeyName, Value); } - void write(StringRef KeyName, size_t Value) override { + void write(StringRef KeyName, unsigned long long Value) override { writeHelper(KeyName, Value); } void write(StringRef KeyName, StringRef Value) override { From 247629447fe7b10d7618df3ad9347ed71108be01 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Mon, 16 Dec 2024 11:23:35 -0500 Subject: [PATCH 64/94] more styling fixe --- llvm/docs/Telemetry.rst | 54 +++++++++++----------- llvm/unittests/Telemetry/TelemetryTest.cpp | 40 ++++++++-------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 114c2e8db7c6f..96e29322d5b0d 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -89,13 +89,13 @@ To use Telemetry in your tool, you need to provide a concrete implementation of class JsonSerializer : public Serializer { public: - json::Object *getOutputObject() { return object.get(); } + json::Object *getOutputObject() { return Out.get(); } llvm::Error init() override { - if (started) + if (Started) return createStringError("Serializer already in use"); started = true; - object = std::make_unique(); + Out = std::make_unique(); return Error::success(); } @@ -118,31 +118,31 @@ To use Telemetry in your tool, you need to provide a concrete implementation of void write(StringRef KeyName, const std::map &Value) override { json::Object Inner; - for (auto kv : Value) { - Inner.try_emplace(kv.first, kv.second); + for (auto KV : Value) { + Inner.try_emplace(KV.first, KV.second); } writeHelper(KeyName, json::Value(std::move(Inner))); } Error finalize() override { - if (!started) + if (!Started) return createStringError("Serializer not currently in use"); - started = false; + Started = false; return Error::success(); } private: template void writeHelper(StringRef Name, T Value) { assert(started && "serializer not started"); - object->try_emplace(Name, Value); + Out->try_emplace(Name, Value); } - bool started = false; - std::unique_ptr object; + bool Started = false; + std::unique_ptr Out; }; class MyManager : public telemery::Manager { public: - static std::unique_ptr createInstatnce(telemetry::Config* config) { + static std::unique_ptr createInstatnce(telemetry::Config *config) { // If Telemetry is not enabled, then just return null; if (!config->EnableTelemetry) return nullptr; @@ -150,7 +150,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of } MyManager() = default; - Error dispatch(TelemetryInfo* Entry) const override { + Error dispatch(TelemetryInfo *Entry) const override { Entry->SessionId = SessionId; emitToAllDestinations(Entry); } @@ -160,19 +160,19 @@ To use Telemetry in your tool, you need to provide a concrete implementation of } // You can also define additional instrumentation points. - void logStartup(TelemetryInfo* Entry) { + void logStartup(TelemetryInfo *Entry) { // Add some additional data to entry. Entry->Msg = "Some message"; dispatch(Entry); } - void logAdditionalPoint(TelemetryInfo* Entry) { + void logAdditionalPoint(TelemetryInfo Entry) { // .... code here } private: - void emitToAllDestinations(const TelemetryInfo* Entry) { - for (Destination* Dest : Destinations) { + void emitToAllDestinations(const TelemetryInfo *Entry) { + for (Destination *Dest : Destinations) { Dest->receiveEntry(Entry); } } @@ -183,31 +183,31 @@ To use Telemetry in your tool, you need to provide a concrete implementation of class MyDestination : public telemetry::Destination { public: - Error receiveEntry(const TelemetryInfo* Entry) override { - if (Error err = serializer.init()) { - return err; + Error receiveEntry(const TelemetryInfo *Entry) override { + if (Error Err = Serializer.init()) { + return Err; } - Entry->serialize(serializer); - if (Error err = serializer.finalize()) { - return err; + Entry->serialize(Serializer); + if (Error Err = Serializer.finalize()) { + return Err; } - json::Object copied = *serializer.getOutputObject(); - // Send the `copied` object to wherever. + json::Object Copied = *Serializer.getOutputObject(); + // Send the `Copied` object to wherever. return Error::success(); } private: - JsonSerializer serializer; + JsonSerializer Serializer; }; // This defines a custom TelemetryInfo that has an additional Msg field. struct MyTelemetryInfo : public telemetry::TelemetryInfo { std::string Msg; - Error serialize(Serializer& serializer) const override { + Error serialize(Serializer& Serializer) const override { TelemetryInfo::serialize(serializer); - serializer.writeString("MyMsg", Msg); + Serializer.writeString("MyMsg", Msg); } // Note: implement getKind() and classof() to support dyn_cast operations. diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index c0858f5a1d9ac..0ca05a57dca63 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -49,13 +49,13 @@ struct TestContext { class JsonSerializer : public Serializer { public: - json::Object *getOutputObject() { return object.get(); } + json::Object *getOutputObject() { return Out.get(); } llvm::Error init() override { - if (started) + if (Started) return createStringError("Serializer already in use"); - started = true; - object = std::make_unique(); + Started = true; + Out = std::make_unique(); return Error::success(); } @@ -77,26 +77,26 @@ class JsonSerializer : public Serializer { void write(StringRef KeyName, const std::map &Value) override { json::Object Inner; - for (auto kv : Value) { - Inner.try_emplace(kv.first, kv.second); + for (auto KV : Value) { + Inner.try_emplace(KV.first, KV.second); } writeHelper(KeyName, json::Value(std::move(Inner))); } llvm::Error finalize() override { - if (!started) + if (!Started) return createStringError("Serializer not currently in use"); - started = false; + Started = false; return Error::success(); } private: template void writeHelper(StringRef Name, T Value) { assert(started && "serializer not started"); - object->try_emplace(Name, Value); + Out->try_emplace(Name, Value); } - bool started = false; - std::unique_ptr object; + bool Started = false; + std::unique_ptr Out; }; class StringSerializer : public Serializer { @@ -104,9 +104,9 @@ class StringSerializer : public Serializer { const std::string &getString() { return Buffer; } llvm::Error init() override { - if (started) + if (Started) return createStringError("Serializer already in use"); - started = true; + Started = true; Buffer.clear(); return Error::success(); } @@ -119,7 +119,7 @@ class StringSerializer : public Serializer { writeHelper(KeyName, Value); } - void write(StringRef KeyName, size_t Value) override { + void write(StringRef KeyName, unsigned long long Value) override { writeHelper(KeyName, Value); } void write(StringRef KeyName, StringRef Value) override { @@ -129,23 +129,23 @@ class StringSerializer : public Serializer { void write(StringRef KeyName, const std::map &Value) override { std::string Inner; - for (auto kv : Value) { - writeHelper(StringRef(kv.first), StringRef(kv.second), &Inner); + for (auto KV : Value) { + writeHelper(StringRef(KV.first), StringRef(KV.second), &Inner); } writeHelper(KeyName, StringRef(Inner)); } llvm::Error finalize() override { - if (!started) + if (!Started) return createStringError("Serializer not currently in use"); - started = false; + Started = false; return Error::success(); } private: template void writeHelper(StringRef Name, T Value, std::string *Buff) { - assert(started && "serializer not started"); + assert(Started && "serializer not started"); Buff->append((Name + ":" + llvm::Twine(Value) + "\n").str()); } @@ -153,7 +153,7 @@ class StringSerializer : public Serializer { writeHelper(Name, Value, &Buffer); } - bool started = false; + bool Started = false; std::string Buffer; }; From 5ce406c70a69eb3f38bafa74fcd6a43b5545d41c Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Mon, 16 Dec 2024 11:26:14 -0500 Subject: [PATCH 65/94] dest -> Dest --- llvm/docs/Telemetry.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 96e29322d5b0d..e73d0cc86a81b 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -155,8 +155,8 @@ To use Telemetry in your tool, you need to provide a concrete implementation of emitToAllDestinations(Entry); } - void addDestination(std::unique_ptr dest) override { - destinations.push_back(std::move(dest)); + void addDestination(std::unique_ptr Dest) override { + destinations.push_back(std::move(Dest)); } // You can also define additional instrumentation points. From fd57d0621e69dc4cb4e7b035188d58e6a93fcf6e Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Mon, 16 Dec 2024 13:28:18 -0500 Subject: [PATCH 66/94] fix caps --- 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 0ca05a57dca63..581a50d67b35e 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -92,7 +92,7 @@ class JsonSerializer : public Serializer { private: template void writeHelper(StringRef Name, T Value) { - assert(started && "serializer not started"); + assert(Started && "serializer not started"); Out->try_emplace(Name, Value); } bool Started = false; @@ -123,7 +123,7 @@ class StringSerializer : public Serializer { writeHelper(KeyName, Value); } void write(StringRef KeyName, StringRef Value) override { - assert(started && "serializer not started"); + writeHelper(KeyName, Value); } void write(StringRef KeyName, From a8a878b489bbe65d3650af43d109b0dc6b9aa4d2 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 17 Dec 2024 10:16:06 -0500 Subject: [PATCH 67/94] Update llvm/docs/Telemetry.rst Co-authored-by: James Henderson --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index e73d0cc86a81b..cc5a23f56de4d 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -166,7 +166,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of dispatch(Entry); } - void logAdditionalPoint(TelemetryInfo Entry) { + void logAdditionalPoint(TelemetryInfo *Entry) { // .... code here } From 54eeaba28a0c9d31f4d5cde8ff5192c0ee38ff83 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 17 Dec 2024 10:28:49 -0500 Subject: [PATCH 68/94] Update llvm/docs/Telemetry.rst Co-authored-by: James Henderson --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index cc5a23f56de4d..765da62f4f9d9 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -205,7 +205,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of struct MyTelemetryInfo : public telemetry::TelemetryInfo { std::string Msg; - Error serialize(Serializer& Serializer) const override { + Error serialize(Serializer &Serializer) const override { TelemetryInfo::serialize(serializer); Serializer.writeString("MyMsg", Msg); } From 32dfc6a35ba162cb6c0ba9d0990782c4fe73c842 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 17 Dec 2024 10:30:03 -0500 Subject: [PATCH 69/94] Update llvm/docs/Telemetry.rst Co-authored-by: James Henderson --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 765da62f4f9d9..b0fc0df2643fc 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -27,7 +27,7 @@ Characteristics 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 From 0893c5b31ff4936619f374720cb3d6ec22259538 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 17 Dec 2024 10:30:13 -0500 Subject: [PATCH 70/94] Update llvm/docs/Telemetry.rst Co-authored-by: James Henderson --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index b0fc0df2643fc..698fce46b0b1d 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -37,7 +37,7 @@ Important notes 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. + into a shared place. This is TBD - contributions are welcome. * No implementation of Telemetry in upstream LLVM shall store any of the collected data due to privacy and security reasons: From 155f243b109f7188b92887c0bb67b6ee8c5fa5db Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 17 Dec 2024 10:30:23 -0500 Subject: [PATCH 71/94] Update llvm/docs/Telemetry.rst Co-authored-by: James Henderson --- llvm/docs/Telemetry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 698fce46b0b1d..d9eea9cb6e73f 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -91,7 +91,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of public: json::Object *getOutputObject() { return Out.get(); } - llvm::Error init() override { + Error init() override { if (Started) return createStringError("Serializer already in use"); started = true; From b255e3adfe0fbfa11c700ffb9475f84c68d061a2 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 17 Dec 2024 10:30:42 -0500 Subject: [PATCH 72/94] Update llvm/docs/Telemetry.rst Co-authored-by: James Henderson --- llvm/docs/Telemetry.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index d9eea9cb6e73f..88cdff797317b 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -144,7 +144,8 @@ To use Telemetry in your tool, you need to provide a concrete implementation of public: static std::unique_ptr createInstatnce(telemetry::Config *config) { // If Telemetry is not enabled, then just return null; - if (!config->EnableTelemetry) return nullptr; + if (!config->EnableTelemetry) + return nullptr; return std::make_unique(); } From 14d1c9248fc550979d94582d1b6b726309d41ce8 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 17 Dec 2024 10:47:38 -0500 Subject: [PATCH 73/94] formatting --- llvm/docs/Telemetry.rst | 26 ++++++++++------------ llvm/unittests/Telemetry/TelemetryTest.cpp | 25 ++++++++++----------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 88cdff797317b..bfb2d5779d1e3 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -142,7 +142,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of class MyManager : public telemery::Manager { public: - static std::unique_ptr createInstatnce(telemetry::Config *config) { + static std::unique_ptr createInstatnce(telemetry::Config *Config) { // If Telemetry is not enabled, then just return null; if (!config->EnableTelemetry) return nullptr; @@ -153,7 +153,12 @@ To use Telemetry in your tool, you need to provide a concrete implementation of Error dispatch(TelemetryInfo *Entry) const override { Entry->SessionId = SessionId; - emitToAllDestinations(Entry); + Error AllErrs = Error::success(); + for (auto &Dest : Destinations) { + if (Error Err = Dest->receiveEntry(Entry)) + AllErrs = joinErrors(std::move(AllErrs), std::move(Err)); + } + return AllErrs; } void addDestination(std::unique_ptr Dest) override { @@ -171,13 +176,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of // .... code here } - private: - void emitToAllDestinations(const TelemetryInfo *Entry) { - for (Destination *Dest : Destinations) { - Dest->receiveEntry(Entry); - } - } - + private: std::vector Destinations; const std::string SessionId; }; @@ -185,14 +184,13 @@ To use Telemetry in your tool, you need to provide a concrete implementation of class MyDestination : public telemetry::Destination { public: Error receiveEntry(const TelemetryInfo *Entry) override { - if (Error Err = Serializer.init()) { + if (Error Err = Serializer.init()) return Err; - } + Entry->serialize(Serializer); - if (Error Err = Serializer.finalize()) { + if (Error Err = Serializer.finalize()) return Err; - } - + json::Object Copied = *Serializer.getOutputObject(); // Send the `Copied` object to wherever. return Error::success(); diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 581a50d67b35e..16de142b537a7 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -175,13 +175,12 @@ class JsonStorageDestination : public Destination { JsonStorageDestination(TestContext *Ctxt) : CurrentContext(Ctxt) {} Error receiveEntry(const TelemetryInfo *Entry) override { - if (Error err = serializer.init()) { - return err; - } + if (Error Err = serializer.init()) + return Err; + Entry->serialize(serializer); - if (Error err = serializer.finalize()) { - return err; - } + if (Error Err = serializer.finalize()) + return Err; json::Object copied = *serializer.getOutputObject(); CurrentContext->EmittedJsons.push_back(std::move(copied)); @@ -218,10 +217,10 @@ struct ExitInfo : public TelemetryInfo { class TestManager : public Manager { public: static std::unique_ptr - createInstance(Config *config, TestContext *CurrentContext) { - if (!config->EnableTelemetry) + createInstance(Config *Config, TestContext *CurrentContext) { + if (!Config->EnableTelemetry) return nullptr; - CurrentContext->ExpectedUuid = config->makeSessionId(); + CurrentContext->ExpectedUuid = Config->makeSessionId(); std::unique_ptr Ret = std::make_unique( CurrentContext, CurrentContext->ExpectedUuid); @@ -237,12 +236,12 @@ class TestManager : public Manager { Error dispatch(TelemetryInfo *Entry) override { Entry->SessionId = SessionId; + Error AllErrs = Error::success(); for (auto &Dest : Destinations) { - if (Error err = Dest->receiveEntry(Entry)) { - return err; - } + if (Error Err = Dest->receiveEntry(Entry)) + AllErrs = joinErrors(std::move(AllErrs), std::move(Err)); } - return Error::success(); + return AllErrs; } void addDestination(std::unique_ptr Dest) override { From bf2172d15b5c0eb227dbd0cce417f2be5b144ee0 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 17 Dec 2024 10:50:41 -0500 Subject: [PATCH 74/94] update design chart --- llvm/docs/llvm_telemetry_design.png | Bin 94299 -> 94604 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/llvm/docs/llvm_telemetry_design.png b/llvm/docs/llvm_telemetry_design.png index 0ccb75cf8e30b662c3dcac0faf009bd0753df7c8..48fbc003da18e668b0a4bd2478a26bd47242f243 100644 GIT binary patch delta 70619 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
  • &&If1)Pw| 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

    ik7qa9>)2<8*R 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�KWcAQ!7QwSviz>Y=Kvq!&`e)6xoWE+xBIeO=H<$Aw>wM}{Q zhu2#Rq5Hw~)*nU(zP0)J)tMKzWO=!#&f{x-{taI(9Ul6vm!S9EAlT-Qk`?q6bu+4t zqN%)lS@|}GCIhe3fIeLfZe7<-TURTnncF%S$NH_UeuNVqu{tA?-j}qa38IKoTJ7#F zZ|+fVpD!!OE6@EAcSJ5v;3RAuT*ffaRu959|f5| z1$ql)WPVVWKFK!%RpfRvyk>x#giA$#xLnF-tp3E4X zq^PBBFbhFV{t>gUYHuiE&>YOWg2-sK_}=J1LLHE$W7FeX#e>&x;`7&!9v-To7LW8j zRnL8GF5If7j}q3@RQ^(7o8(oDi5Ggoc>#yZy8DO|oiV|E=Tbfe^&qy-KP~8v%1+~1 z=W_TMePeawS??bMb!-o*#SeE}2)nxDlV|bOm%U!lfOyMWJ?2lhR+_gr{TwLa-3O?B zJVg6KAa30wx*`dV7Z!UqJSJXdfL(gcO*x*~C=%-Wy#M4*dzD)-orkd+dTc-M-QmZ? zpFPme#siy_6FFatSzUZ7+r)f9?_uf<#3yUjZ6(Qbp%VR#-C3js@)x{(y`>kTgz zd8&;;+(dQ@T98qo{lQ4N<$JJ(tjY~K2Z%3W3;nv+)KE{&lFhjiIAKjUvo;euFuY^F6*qu+F+!Pobs3ua4u& z6*ID!0-b&!pzg}<_#1kTdd!tTUG+hcXYvWXB@Ps=WbyunZcbBSIjaoi=Z4V)m*QT! z*;LLrx%mpkm)#E6WE3+E+w;o1B!Ite zb3Jlw8amu?D>0qvPU4~JpD!y>=MYxXJh{|o+M7L>Sz)~#lnJk?{^&X*C~fyXSij|$ zq}$OJ)-mz2XXM0!+g;lgf_DYY*jp;SgvlhI7?6TRq~Ms|kfuuxZgqhsn_V_B`6NyR1vl`fJ}T(kxOQ0F6OV(ODedCk4A7CqG+yOjDh9MZaqN@s)m6*nO(C zt>uuqe9Pw(VCj!$mY*^~chK=+4847+QgNd=b!OF#WiO0%vCYfCR>ZweD#NN?qho*A zJLlbdVSXCRonV^^G&jKkr2~L0DWdULYQt` z8$ClXF4i*eu9?TWoZ%FQMKD<;PGG*Vspi#c{%Be%+#RG7bajtTb*;U_Mpm7e5X|sP z+vK#IRIVDvcdyKd1G<0Zzfl-Fz^=T8EIguN&z?oDQBNA8*)Qa!esf-oX0Ks|ZCG~u=S*}r?-9b(Xx5JyJo94}vcBtBo9pKq;t|>Z*GLye1q^E{hD>(kg zlLods*y!Bi&4U|v(s2CjAQn}TgqZG7;eA%Ze%Bf|ty{}J8u%`Zai38{?@Vis)^U9; zMbK$D1wZSnD+Cm>>4V$)KjQK-ZrwAV@p{n)dsdH_b`Dpt<(z~pj&k2mSS;)Z!AGT( zZL}-DDcj~-thr-V+#d>$pWCM>fg>iSzK|`RB~3l&d8*U~K&ALrX;?HB^sDYVDR;3y z4z6Ggi(>U*@?%l^?<3W>mSK5a*oA*|HrYU3Zf z4S=I@lxT8tXRAaY&$}nA{rkwSJ1)?CYnEC9L@X*apn(z0lQHE>U)QyZrtdHvao;OL zxYTt0GM;&#WsR*(8c&HKC0`uM>X zs9C(kb!a0f4Hx!H&mD&yD;yJ@=hn1%kaQ{NkH@n-2ELYFQ#B~(vUmIYZMs-8j=CjP z+7PSmiQO7i_TDIV1MK#BZg774!We$m>HTfD?{0R7H2NTGdR>;W#kgvfixo}cld5*# z_qZ)VrHP2TQ+ZC|qbCp%C?%#NOPf6>2s^IcDI*$WtR?X@OCPWEW(4)AgFBFxX^Yuk zcfK&%2eW>5RRAp{M?j@9Ul)Mlyw3R_0_ff2*+#$c*0UjvuQNa$E zfbsM`?@)$ccG?iapeP|9qnm{^HEoY1NPnC@0DWUQ4u>-`DveI;wt0(Vf*a1{@oxT8 z*I(_b^B6fNvNy-OJA6#pjp0P53Wap;l|a3B8_cM*d?^z(bs*md^DSQv4LLVDZ|=Pk zrfRD)PB2#STP#uBTk0v4BAgBm-81F}+8_RdQMtBL8 zcN>-U!Y8<;5fagIL@H8BVQY=zsI@=kl-#*jRSiF{q9x<&T`1XKf8-YTXMMP_qmrtC z4tcd@q*LdWxVyfb+8xfy2i?Z<^fYx`wM*7KZ>h37x{}BpOxBwf&ihZm%`;|8W9Cs} zgp{DRdM`o9fyhFAzy&=R-Z%oCJes4t_=lqa<@o-zLA-Wi$1PhFtu(9YOzL13X6RN$ zq}xl4!)I+5Z#^=4WY}~k%GtXIg{|9`#&1wiv%Z#(+-D#kw*IfuA)kuKrDraeM;$qX z5;Cye{Gh%qzs3m47qw6BFc1#C5M^~!edBG{yzu9%J5=;;Om*&bnaB9r6;*G6pN4sK;=>GK~38Kc1T5m$8>H%+% z7!Y0c>4gXioT8#~uNFZCrqT|BLa!RC%Vgmm8+a}xBoC>)y6miIJeE^66d7xlh!z71Yo`J?vg#xdyUG zVu@T>@`Ks-J|U2<$)_3e3{!4~mKaZf4?|B5*~Qy*Ve+@h2aA}l!2|k-tC6i=cTJyf z#tq6Rm0$dw+$KrJ-)_kIGH9XEli`(S1EtKpqq$_&X=&QZY*o^0L?|p5jf-j-uQtpS zC%V^W!NCnCAb!rhq4?l;g`?_d2EyYjt2$Dzx&=9Z0;KiZ&!t90KXtemU9Y=AihH>9{KUe`b-zj<3plDXegbh(mpo}Wc_(V|;0aaga&WZYd681%BFew=vB%Q#-r?W1 zxMY=UXXq*$IN*FrlNfqg%4G7<^(Z^aZcg zQkDy$G#|&|^3S=~#3=j!I6g!ZCz0Y#n`_?ZmIdh>)^6w0+AjU6;CS3x)IT%09Q7cP zvntoo-qC3v1J-NixBRO(F#M_=viiz$4o+tOXEh}W!I|j3nceZDqg}GI2+V0J*rH_k zv%uUowDIX3ZV>s;S+$FKc`TPhB2HO2EzP<;=FN%#vl2LaxwU(@5}fSvuklT?Fj)y% zSjZe_&lPjF&TZauLri~Yd)%?;p&q>AU-hmGEz)?b32oY_qgfJO_@v?R{VunICk858 z9$V3k)O)fVDF3Q?6_&PiD zcYN(=lFvo<=`G$0L}Do z*?+TW>!06{$N2lSlue?OH%rp_!T4v3@1A&*!JO@l`6vIqGn_J4CBSBr|OeVep6 z_+4bT0a=*A6VP7^rICh2#*@Xy1n^vo>4w+v@4ft#1)w|s-2RyIxXn1_l62A`u9)3c ziJ*qr7%B*LHEzHFdg1Rv28hKeHG7+6M(U4qFxAvFyX8lH#M(-~Q(~p0`hqhJr|kXD z#Rc>Wd2iKXv+S}Q&2bi7T=tCzK%E4-Pi9vL7zN;pY>e=ECVpXs0qRxKEmChB$wX6g zw+$ZDj0Ouww=}3dxtwZN7?bl%;*f510!ZPhqedbrGzpOhaIckrvMMF6Bda63Be!`I zuBrLB+QdTBi=L{FIxFG|_wxML!%-}P9wgmnk*ywFx<6R_-NkB#Mq2K?QSARSPaKiU zj>jpN|93&qpXKEWE7Ffuh3(0)j*N~hNqF{?ClPy)!l2q;w2$EYqh4wYRB2jR?!krB z@aQ4Ma~whN3aw4gp;oxp`X(x6@EwtNP||DqGtxX_cH7b%J%r>C6>U3PhoL>o>eA?^ zH`pyR-TsAaf_tTH1FuW1TXljG+FTA91D6FR5d{j=Bb-efoK?@kupdkz#D_1cqiL_jgc_Cd+>kO1J{rzlnIyx;XEjcZ( zY2OVlEZ-?pN*a8ztBM%5$oP<#Ld1%3{ctJ@{3oY0aCc;)WZ z0(HjokN1@$WbNB#5j_tY=c%+h4b^2VwGqH77{7YFO%=t8q)yTxeX>;pGg14plrA0# z>~VfJdM%uSK+iJM6EGK5nfRcBn={h&VC$Du;!xdH!0^_|ZD2xwPcjOfH5KPSRWX;2 zPi=AR3J?_)MJ;6Oo5v2R0h-b`Bm|r{`m^W#du~7c{X%LFz>cibGl`jFnNzR5k_aRT zQBk9bs&wrL0mZp|;JSW6sr!oHsEBU;z19d6EKn znQLSez71l?O%JbWp9pkz^z;4=lFSgY4)wST@`gYge958n(|#@te4VuQ-Ab)Qx% zU&-ojR_oU|xC`Hr;GHQ&2fKlO6|7GPfeyhN>`dbov8p3*kl53cud(M8qjS^?zFabn~flm_P3J zuc5%1LlI_B-08s_&ucSCUAhlL#d|K6NPA70%{qH7x2|a{CAUjgOw54tz>p*BueCdv z>JDDszAU7vrfI2((oE{e!DJ?6Vlz`UH8tzb9MmPk%Mml`>AVsqTADg`n$2%)~el$%qTCG}(R!;iOUk9a|wFf_oh-qOF< z4siCX4aXJfH0*~fDTox8UifpRtoYYqF&xSsQy8 zMpB3=RI;arVhC9?Zp)y>nlwZ-73MpULC7|i$|zfrB_pGVH1^#vGtb@k_j_La-~3-b zZ{{;EX71~{ujRbX>p0HiJn<9w#T&!$ua;k}zFL2^`Fa*yDQ4|%ws29RcDuZ7q_)}X zbui_QouWF%bOm0L{O?=*vCoQ`UODwAH$c?NrS+gRF(#!kg^^;C8e>zh>#8Vox|%27K8|5hleD6zCP-7_kkPjH_?cV}I#okuzh9nnzw8 zp`)anY+Lgy1K*Y6WBPJdV5AOE?1I&pH)JNrB&Rv^olkSq z8+e8Wxsav=OXsxO$e#wajW-8?APv$fLeZ}3svJsb%C1t~<7BHPIK-z;#T4|~Js5DQ z-guGBK4zxvWz1E_KSaNBtQF6>r{kPi8yJ?)G2ekq#))^(?D{`m5>EJL3wSj8RWD-j zF_mSi5(+n19x+$Q)j~F@e~kN|&~)rawMRGofB4~Jo@(cMI>#X9ee%ExifTIcja z!scFpoy2~AGz+_s@LaIi&#G2iOvZ*SPVDCwSqwJ$<7({OAN^`CMYiWkNq;U``G5O~ z`=S7eJG{;tFxZhM2$AL=+BLKZ+X=_V1ah|BpA&-kUe4i(4@>^jqR^LCoqd%6oji%y zDCAGEx!;dt0bX;69U&P1<`;H~I_ard)525op7$H;z>lH zlLvOzRQ0_l3Sg&x4B{Eh5q-h_ z2*^i2-BH&5t*ZLecKE(dmCiWx`hONg0Bq3$<0FXk_?j^jW;-I=EIvm@pW{p~Yr9OD zf*G9a33?DHMVxH;KB^Axw?l{IQ8%rvRWH}9Cmr7gB>sVl`(j2kZ=?n6kPo|k7((K1&Yp)6#m+wEMWB{as`AEUi( znjut?eC&sC8FKYN6tij}*vibpXJ-}JOTNGGTBKqm`9Y;Nrj_O1=sAE=+El|-+F_>`U^L!p)F5gCi)wEVvwd`uV$v?{2?sx-7M*jzmOC^B-*2=`bStAx?iY*J1$OO zS-AIlNA+w0+lCRrFy%Q!=A};?K8med&%^@-SQs{Vr`&XN<)jq=oSew4%msnerAKxQ zaq2h2Ry@Du)bwYzf~eURHDjLmCM>Hz4|A7nBZKQOk8t(>t~gqJ_11SS z$YJMeihDL|b-!!z0(pn9W@*s5x?=tf=G_3P(p^pFU6%2vqGG2ym(Ol8%PC^+-F##c zH`*B7Ci5zRk=;O`SpsYc!-zJMC5EUZPwFr=w$lK~KMCfv{n}VoL|QalcmsZzw4|Hz zGV@Lxp%dXR4H=k~**~D0$m$-=-lToRH=reWysz| zCFkACb0xhIUcq%Q+FsmhGIZX$ew4#U2HNos-zuZL_w4uB%Wo-l5=e5%kMzSR!c#EjH%w7hEMaMBux_SxyvAHDixth?vGAvOOFR~684Ap|+rs5W|;4BFN1JGKz31l#PLTx5$hHhCq4tjJI%P3ngVl~q{P3MCp% zz&f({YI^;xipTVvVfs6!{3rO$d@tL*ZO81)=bL??zfk%W60McgJ|*6@{Dfaut>}`_ z(5$03#aFCTH*99GH#`<)7czS^8nWGuGy@Ell<^9%t7K^+rJ?d0ij4H2+>cOAWjU3D zc7)4(Jq+%4V|Z-p??6D@}^1sEkk&q-r_i zzkN7+lF@62hd2LZ&PJ2GmSaj)A=5R=FQ>x4OAwivSs_ZL`k)m$p(XZdus@pIJbb%Y z73*XFvlcy!Q@4R01dcc}pN0fgAzLU|LbKoc{_P8d#Yk^LCnsadr#F1PEGU`WOKw+; zkE)pWP3z9dG2m?X>)h&{wx94aucsgao;e_UeMye1Hfo&;!(ZUr<@~o3mMY(BclIZe zio(~6yCMtd+g6f5^E1t2MfxbHiLFZRXWv7j3$Ax+GWr0QUV~_ek-0O?bv#q|3FhI- zbROzt=QJO)aO>v-d<@fzZ^-;Zz*SQ&g-lFB$OGNSvvRFgtMEW5wOQ5c*k9Au(j9cp zV{tDu`%HFsD=2$rE#sKkGbu>Pt7|Cwv5RVYYIpzA&{^K`2!rH&g*PXw8%&W<$M$mt zG7dUGQi8>y&i$v+b5^+4eM;o;zM_tMc9&ptVh zTtCTGhK&d*0P{`aerg{Pf+Qeuzit;`F%k_0hw1T?SdP^<*W{r}TV;9n_h1D-1GPA~ z$1uc7Yi{N})(0lP&s&k?V1dee%ZD(Sg=Y|8MmO$Zh2Lpm`mtswKHPSXgX3@sRH=Pg z6Ca~#j5QC0rTo+_S5CV(4fPguTg&(BSys+dS)MZEVThqe?xsH}{OYdgcju`jG12V1 zD!)#W&7K?}i_E48a3X z>XB#ImV)kP+%fE-^5P3qIp2#PYH!o-JnncZ#?!h zS6=jXx9>`-oPzYW7OM3}Ly1mZu_#X57OJDQe@zZS|HnTXGMd3{rGFpT4HN1e>P803Qe2g+T+ zBalE!0YaaeG@1*e>12<+1)t0Deut)XfI3dz4k``!iN}V>wJYk_<#?N? zK1+%il!Kg}I6%zr;PN%SRCf<6HuVJkcavg1`jjeAM{67Q=*VRC72QK~tSsL6g(wUG zWyV%^hDE68#>2_CH;{_FN*QIbDSPmgPc2+DA+A8KSIyG%UP+dauBi4n>F;LiyeoO^ zY?b8YG_VED_SC!{z6`b@d0HTCgUqHM0$7g4}i!zwRQ^30z_S7Cw8Do_>pIzc z6>qZa=;(9PrU{be^nkC@DEuxYgUTPWy|v9k0qOY$T>0fT9Tprm4z>416+Cfol;<0d zzUg~;zlL#Ps=NqvLz1H%OnWna?!OAj>kqwB)lf7@jwh)A^W!py#33i9j{6ayO2r?Q z@lXD-ZnG%>u?sfUpMVF$Bqg zst|iWlhdz-Dqs}Q8=JE4dnAVlkJ+^l4XP6PBJFgQWR|CDNF_@UF(i2|(RV_SFqx$| zY2vJ?U%x{DWOQ+7m9oE1YzxMr+6k1m|2=wX5&T1OHfrw{VmIdtxbA1yC$qlY?OJPY zT<<(?Mg~HX9UFVF*yl0C9XcHA2GGhTQ&M0<_%}_+w;m!I1RkjA-;VHkW z>!DN&@#pPutMEDO-!7f-+)v)W$t;OJiAkN!<%!4MHf>$sSWbQHcoZ!IlL;Get;6=l z7MJ)s9Zq@nYR2&2SwG|VabKxJwLd|456GrvW%I^!@k=ORxe8a`+Y6(d-(TI`Mm6zS z53A!D+5Cx`V@NgN{A8JiEYWF2ONc40!#=-efVy)K*ATT<7Xh7I%K6m=65N%lM?@e~ ztD;!V&nFXkTahV%le{ND^qx<#9gS;5=MZTln+MwMOjrJ z^nnB3a`W$Q0ffZP_AX9~O*le&!im9~FfEO!brdxzg?eM*S)VgpFYldCUHGtaxFc$( z7WtIC<4zbOc{%Ru=`z(?!)a<>`KJ4oI*Tz`O+W^BUxtgu21VaNS%{nH)-CTy;1AF? zMVM1tNEr$Vs{-Q2vLcB94n=o!iwKw%%E3xGnbx& zufkeMvek+stt z%9q-Jo|suha{4q3zI6p6$`4o`pSQ}PYKO}oZS>|d!x_A+Dz z>z?-S6{p?1RtZgDti};Fs+>9hXnHvMOpa|Wo+}_dyDRrek`kc zK}PO!F?dS(W0Q^*^SijALZGmY-vK*;$V3&?*{aN0sQ1B2G7WHdh*on3_@EQVH_Kv8 zl@I=GIV_qglKdx>{}|URY_e(J^l(9^X+Y|qaIxoc$KSv;V{Y1OLehR5!X&|kNi^Nl zGt2~?tO&&bE!f)ar>p`wdjTd8KUwoalTFh{f5k4Dn%gl1$EAsb@nIMFT@PxCAntyz1|QY)GHfTF0zC-rwG zv$YZqG1v%tpeceUQP2VQQl}S8{q>|fnj~$NX2hv3MT0K{oL^~}iQzE1e-MRTj+&c0 zE1}U-ey>J{=fxoCxJWID)kqKi%uSrXhvd!;VIl!ei~BnF>u#UsS&~ENnLVoZ3Nu@} za}9(ON_&5yK|kW)XLojy&c+CZE?e%es0KM~bj_?r)+x$f;LG}>UOmXms9$js^kdkH z5Ca#%?1@o#2hM-Mkgcy*9}Cv-Po-H!xe+=FQo)sB3eK_4gMQ{Yj3$xvV|a!kV=8C} z0jAB)TBW@$l0BDX>E+XUfQB;bzbC5x>oXFK%fDigB}p53n{I5y&g_4=WVke?j>mj1 zs6IwQ$kC6>eFJ}x*EJEhN7bAc>K>#& z(Fw@dSg$e#V`z*4iL>|0GAXPRZUhcFAo0E4HDyyjo{NRq(I^6HUA&{LP@dR-G==(| zgQm$f^Uep1msm@*&B#cLP zL`F^p^ael2md)sxGhtuZXAb{$oez2N)U2!B=iA4ZoF~*~59P=)Qqd5B7zSxM<%w#9 zd=hVO+mBb*$xDZF@fs%IbSoS?6>`eL`q}#B-)u>5)G*0R;8C$&i>mY1u5(SV_iYmh z7wR?gTF9RxHNAW-Q9=^edJ2L2g8C4y`?dnC0IP6;aKRo0!J#{!?sRXwGm_sw=oFm_ zVk@$urpu;|{6l_i8fs&rj8kNJ;s_kOo0w5>7eVMO8c-bTIH0A6J-A02ddTYgr`az=k~_?*4N zk15YGEfpXR!Y(8m zk{xa5G{J#-U4BY1)3S?qr9~<2D)o(D;bD7*e~l0ZH+z<_@c?xaVYz5CsvRCxIf_&0 zKkPh(@r{l3^P;n46DX-S_a-r=>;cuwtsyn?zGlVTTVR<~ZoCp&M&%G!N}8@VR-N=h z9=7IBVJZ4Zsrd@_KVYUtV6ks}1rxxiwTqg*{LCjI_p96%w_r%Dl+eLqkhgh~keBxFXlG&kTEeYjJaK?6Q zy!{NAz|e!p-f6rUzUMXmiKeB$b#_NNzf$1t4T6C7*ae;af8NU)XKx_F$0X5e{%VWH z9yirQuf0qbvNps!3)cWq^19dX1mEZ{WGTs<0{YtW3b-bFqB6SJm(I#z|7@_;-FOSN zJ{b&VrEP6LL?&D623|)=`|wazjh@99Xamk=I-x*^?ska3CQ_wm^0|DmHMSqTjB}R< z;`-AUK0H5h;?c-RMy=IG-U3Q%MMh$>`E=b`#ghhK{l*W<67w&L1*san&{krr5?*x8 zR+)dNy;%VSt|}zC>ctklan-!R8Owu(ueAIM`vkwkIA3@+XJB#7?T9n}NZB433Invo z&WI$87}kVa$h5_Y`T*dysP!IHDf5o)Z}vD}MSirNz7)R;pM_}NJHdZALG-qB;RIF9 zJmQnI6}$HPcC3`h(3-8EUmQOAjtNST2WHAoTND|Z+OJbx*dNcqIGJhfHsPn>hetBA z^xZ3U6Ry(^(5Ewn=U5g5=`G$(d4<{T-s?)vDEF6U%Era?lZfzi zhp|qJ0|vrFzJrfvdjh)IU2Phdhu6k#M4|}{3$mjvH#2w37>CKN(vV+@mo3?APlcB* zvrd?Q#_`{5ppEPn7$7VqpdX{9 zp-p)^seH^wloF;QVk2g5c099gPjPf>C*vMXY~8wZ@JK(gq7HLd zEOu+O-7fut1n!kpm)1Q8af=AmKcdW zO8Yt~Ve23^V|?q0GIcTrivuj77)Y90r<#vtc{O1d8fxtkc@fIYcfdAs&nKD#DJj}%ClXo_9~iI!pw=6P~x z+H%T3x-D>}(Qz)K#IE*D-ip2OkYD4iI?pTLbexx*PQuSK7_3s*)65iNaWCLczR>7G zzVI(ObE+}N%}%lJl5n=ZZRV#-!Z|RIt6^(0i;>AW8kIQ)2dwu>)*t@c>}i0AmO1~2w8S4x%BIJYCCoS04bi{0r#zBojQ)T4J*m! zUjMxCuHE-(OBe^bHZ}y%>dYpR>^GY zkB#dbZ6{ZA`HMlqDC%;YRp!btve*2j*S~=A-7~o5h5GWPI?@zlD)l{NdE8Lm77bs%{ zISjd1`72?-$|~H}4NI?EDA3^iphabR*0Gqq8^Qg0EO zXZE%`Eqg=~Zs=VqGQzxyQpo%`U;G^*NiG#l(}CS{y8rDcI`}vfHRdn62_ip(T8F2? z(u?IQQ|p2QZ=7!rFnlPG9l)6-hj2+73bZrk7n5U*=N|Lz_jRvU3q22zYW7f49pNamltn12TgWDV2}ST^J9L$yQh$ z_Kmt1%kmnauRar^B|L=L5_b9Wu@PN>9@EtX^<;c>wZ>yXBEmyqak}f$bzG{2KXEBa z1*w1Hu=}`j#X7WJCl6^wJ$OF?+Gx*_Xnf=8gCpt(=MThVL$n{6pJ!ZKV*@KTh@bRd z=7DR!2iV73l-G+ngt%gR2BKfy?ngeY+X|ht!<$t64L!C<4Cl(V*p56OCXa_^MqeeM z{^-8aWLAkzIC9t(i74||vR9DbBMV-7oQw*N&UEY!R@?C*)L9x?!LK!ejD&`B!X@nI z;Rl%>6eD{C#t zsJPD1_-g))*2}Q@l4SSg>-Um(U%&K-5wb|UwQ202-~U78Sbqu&iJ^RM)n*=EU$R@z zdDB0jZ6t3}G~7qGV_)ug+Y>S1ef~d&u~R85ReiFtWj*{OBi+hn8w8}@G@~bCIZPvcZC86^q@bE(?uMI#l%TWmxtll{jnK6m*oT% zG8fRr6|Z05nQXH1CR)Nl|MM!SHz@{9iQlJ>|SsDeA2%Au3_#Wuv zNKi&+bPun|ia|=4`L|N)LZsAsWQpFXaWX!@6w?vz80VZUnzsuo(7ZU0>Rg4}1AcBN zy3^t_nTN&Sy&0_^sPfiZ>qByfuGqLnRV?}6IG+k$K7;G+H|yz$T6xyztdtsTOzh0u zsD;-x(vnaPB&m;_ZQl_3HAmnZS)|l_Rf*?Gx{n~YDsiPgP$flwt$LNd zD^t!tBsh!{|HVZ)j?Je0A@AIHX)zk6DG$)cxvD$s6 z-#2_8#Ky$BU%ltyw@Fn$e6^K>!-C_Z-Wdfu5?gm>Ii~gD_s3Qc8F;1hCT3#9!zy5# z0?c3HsLOd6o-MQ7shiZ60e?8Xq{2+@PtMA$ig-3F%f3a53Ij_|z@>@QhFFWkeVZzR z+=$r6)bymihbFt?zDhca)wD8bhycws@PTXHf6dvcJpB9eG?7`uo>OrWJX%u5Lb@Q2 z6L1DCMWmktRiX#$NO?3NL3H&U-y**`FP9C%onP5CFro$!YUH4$3u%TC>_jeh6SGX7k0k#*ceZia0y87~>^@0WbdN z4{)Bm?;vmv)%GC(B_sv8aHcaWb&$pv!`GH4^v*Z!6qvPoUhOBfDugf=YI|H~H|08q zXyU#^{UQ~rUxX67wPF1eYB#}KN}2l6@hPXk9!Ls=9i~sNAZt$siSldJi3~B6&Tdr=)cT?xw#$XQ9^9&h!4$ap!Gf$TLd z9l_Suq>B0dSkeC6&mXon^6&{y{CY#79^@KImZ}=nch53H5aLUlrtRsz=()?yVW9x1 z={RLAl#0i%jjwGeiZF3}XRePcHFu8yu-)8S>R!ctX)hJUpPP*|r|V?OhAeEhM}T1G zCn@}xf@M^rrY9$z|67Kf1*;8`_8^J-W}Bkpr~*Lhst0v`6>AXDx1LJ$-?Mu^QqUze z@v^*Sb=sO=1MMKAK?XOlAu0VQS^qr(VzA#4EjoiY3{&Fwis^kJL{IJ}*8ms?P7DRz z?|Rwm!F`!i6_3k7{7c~Wm?-I2SwU4V?K(Xa+!G34+%rC)KP}=LHB+t!W)7<7>qngX z-D==7e$+?R+5nz)LT2U7l+4SQrgEj)ki&PU9!;+^JhG!c9W7VHVNg&L^SIzRD~_5o z(ZWsoxCVp6Of`&9cIxh#>A0aBndQ~%(P?f5#{f&S{Z6V6MjyIR1r zyeqg36!e6G4S{Sm${Xd-Toj!{%@i2g1nt1itT`yiTBxp=!dd%4m4OK<7I0^4>FU{w zogv$Y6H^kE{3qj&e)=h*ql2WJv9}&~mgd~QUmxZwc3z6P@4ZEjecCA@K?B$csr`QG z$-O-r?;89Vw>K)O-(g)E{hZvGS72G<*G?`-S{$vh=1CBDe63!D%H?NO^wagr&uAEQ z0lQK+2`rtgi!-i^ivw7C?4{Hln^~@SZl9Ld&x1$I7njsxU!iXuBdG_zDv5CUyl$NA z!Ts?2HDFzjebnpV8VrcZV}sw7p_2l~z2H5S`qgbwwfE}K=m4+2>HrM)UBJA>Gxy@F z3LOe$*{^J5SEiKM#F0{8ADzh$c_qFOM!u8BP9NO6VidUq$kpUFB1@_QG1u$ti^kA%y` zwH$4dj}?k(7fJ4~Zca5`8S%=6kRT0fzO|R(@6k~2g;uu0+LO1$gHxz}W8XAc})Yph9JrgOA8yD~QOD99=D?o1wi$u+*IujF;j zQ#)p16GvNZMKTToffUZM&v6ZmJRzR0{03vcdQ}> zTGGTzE=_a|ONHJ4Z$rP+agC6F-Ne}i{ zcP>x#zjW*3#>CjjIqv8?ZnpsCuf%~s_4$)L@yE(9`^-HN=od(j0QZR#VJpxbof9~3 zQnXn`uZh!pt>yN?n>uW+t$_)1k9GaXTmMVojzE57vVvwkFi6KZOSHZD|E*ZNi)jZY|B_@YO20`+!cQ zSrNeM{wfqo-M$963uSsfCimc(Tc5q>Oe$*rG<7skB@ko-)b)XLKyYqItJtyy}16Tv=Cw-usk? z@AptXJ9~+8&G+_5^(L%ywK|lBRRnxT&$|1bPoD7W z{N=|_R@((vv26cPczOJ)D#Z(YA;@?MfaVGP?w7{ zULpV#C2hkoqO_r4F$?lc;Hd+Wn((bS}H z>~KcL`IAr`#CMEE=_38B^MWd$g)?3z`5ZiZ=FGuEht8gz5}wP7Gs5+(EsS8vwPV{v z%;T)G&38>|Z7%L!qthGLuDagQNuTS|!SF*bQwm4YQ^ON(U+45W^YWyB-}%%a&KA4F z&t7RIID53WJNs@YAmP$hA?@e+o1EzTZ4Km9+giBy2K&=5QoSY9W^T>xV-j8?g%Qia zi8~z^I9G zJXKvhO-{IS^(4%hO^7YhXtTnRM-N~yZA<(Ep8_85A3DTdNnY=h8{$oAX4$aXgtQs3Rw>Xv4d*TdVXV&Y~)(haA7tWmHg! zD<@4Ia%mY?!|ry|YhtNzV)84SR^D>oSJF3jX4L7=v^F~@zPW(9uwex~KlAe>G9~N+ zO1SJ2ji9PW>*T;~B*Qv8YUCDXejS}oYaY%O6F;2xDY&$RP!6}tV8_qu*|DaZ51jurRO zlzL{LAQLbCx2z7)#_sgYHmcQx5|V6`UR54~v^)hrLhzE-`<*rLPZrBM#TDJ)k?>_R`Z3tV5EniBR*g@^CXX*=G{c>rW_P{;m28V`(*l|9& zW7l>UU1zr2*Rvfeg!)hLEl&*J&O0>KHRO!J+O0&(rB$<%5F19C%F{kmO5H0~-KqMj z@o&z7wXQP| zzn@K8HR<%h0}|&-n%eKua>T@-Lr;io?Prp^3ge^p$q&!4HDmL#1o!uSq`Cg-O0^QV z@#=m1hX?OPEGM+69uc6Lys+GX)gzkTrCqz%H9F_Z+~*Iqkv^|w+gkG2S#7yIzJG7BQ$1ld zl&cMM(e3CG^Ji8flbLZY!0m z!jKVi`e1XORGF1EQ^9la`AfT}!8a;r3WmQN$v8h5ejIrYmQjPO59W~XtA_ch2=XRm z5y~H`-nf|R<@y@D7pD#CFt20sTEsn@&T7T5NAzWWh=aI?O2;Fi?(vMqB$?WHWD#n` zuSryr>Z<#OP2H%UNYs<><$>2@P=<4qotb^*|b=R20J#1+==;23zH*TSks;~(^2nz8Y`St zS5?aeq-GN&Hx20?ubu4~HKW$VD&ii_tmuTcEfPCXy zZ(qR+zYmMESM4|1o$Ct>50zEDkE^63cO`C#!JmBnNh6LocN|BX!dC)zD_x;8tu73c zNIjk7h-jMYM+z7)cW<#GqJ~aVAOBE2UHHb9jX9J1&6vbmhBhX9#!;0)O6ROHolNuz z7OR0YFHKQ3=!8Vh{f2ubJ}-6*owqYb*2dGHu3ZUDY_6wCLRAa=HPOq0aOh!}{-{m8 zm;rp$>YO5w=RJ*Eyqm*){HlDj8`%lq;L+^@9+L}w3>f+^v{>HigAs^dq8B zm$NwOJ`zY(qrCIgE6lnWWId<%)e(9>BQ&9Q(ARm3@YSoxfwCYX&9CKS@@n_Xm%~ow&5rC*bHFCbNg9kb{o@dBpR;K&Q7FgYtbr@?mH&gTm6?BC)zwp9|f9l(#{zFTxk|-e648e`(i=h?tm}(EC)oB`p%y@;3lGspQ=CUZkMU|wX&KP zb!fzCbi9ewmWv-upfSDruHV?B;-a}aBzP%}^X1&<44wpLcRkz{?hkj$H|- z%W6FET(wlvZ6-{DWfjV3u2x8zs=wMi?nc0KjOR*M5>S9wq|V!P_-+9+EP9$>d3f2V z%+aiVeMoc!!9D&XfNL3DBVqeCU%zO1`k4zJ0y`$8?o4N+jdgTf5YcB2o-G{{KQvo7 zolr%Cr5RQ_nsIQ*`9gMj)Rg}33}^~|+4B6jS;L4Acdl*rM68U9>splPbFT&-9V>9I z-);&)HcPK(p0jT{*mYg1r}>fee)<5NfFy`IG(FCjV&4vf<5{QYv{;K98K0?Ox=P6T z;UGzsv0uu?OuL`@RP<|37M|V&4=1lh(mD^+P;r1Z!6}4vHI`o!A=%KzuLeVNqxpB7 z=;S+`b;Q<^?@AsZrXVefjhf7`a6=4;ZC}BR;@tUj?!<3# zk2)oinITntpNadyH2No)Y&h#x-ZV3ajuMpUIM+MW;=PNmy-QJ!gcgHY-5^cN8f=|K z0x?n!YioSbByMW63$}@SBLYHSYS@X4_R>y1XtHuC+_&;(E1Ma+?1oqVNk3-&K|bIf zh1%yTGL`kp-};x%-dC+wWbN(;;$nskg7pHgHV3Z7jtIlSJp(z%GV z6B3|0$tNc6VDk2@v(<4+j+?~4rs&?)K6#kGT!7-Y{_7+nv?=u4on8?2MAnbW=I!)2 zTG=tNxEzP$p>AqHgGUO^RMv`?SX$}EwYOQy)EBWk>t!!+D4U%d+yD=ddf zxBSl3?2d@uT-PG@x)GRrF(&o0yThWi&@n<4F+2LIpPIQ~c^lBWG>X=B7P=zH6DJPK_}jR%{G?Y8aoVI=9OG>zjM1+;5G;+;HZq>MjTz^yJkMK_@lOg zCh7cpom#W;h(=wedV$bZ6_3{xaWKbJ-U^frVSb?M=;wL}IGD!N6XTjsE>vPrm*=ny zcsmDbO$5F>`@-hQzhLBXebQtA4*>#8(%tbe_Ss?UDCS4UU<8db#Cy zgY)Sb&g_&B_UL@>o4^^%s?H?1Xw@5(-RWPR^p=8>9uaFyWGe2P@|`cg!1Iqto_y*s zwjB_oh~$RiyOvlkSRMOum~c0)cI(ppIz49^!GRPcy5k80L8snkJ75$|7MwkPp?A!_ z7X8HtZQ5I(q#TFZH+PaP_H=GK(p5ut_sL>EigBzOzKh!F_+)0*&92cXnw1ce&FO!E3q{B?mQD^llY@#8-KEcpemS$!mo70jT?PZ+VexFd5A)V_6x+?(mKHUCK(UerH3YmJv#FnUWo9 z4w*+kcmLb9#WDglyF*d4C+qtyXjdA`gU@On6lpfM6?LA`3K;F*te0Cx5xLYrB_ySkPmW|mOK(wnVUU+J#VALp<+3HdBbIms(mjn_0{ z@mX2P_=;{9==u?-Lm*9x_Olr&^fH`Z5Pv1FnDNc|RE1*0JJ%h{86u@lZZ3*M22C+` z+py%`xi-Ib8+$zO_U%T;T6g$-ZS5|?tPB@btNBt_XM4k<_KpO3p=yge)x--Nh4a=d zaKgJ=FT^dw65sCB?yi6!)2wV~om=fyZDpUORp-im{m7-?iQ0E3ovK#O9LAT$zn`C; zzj4et@PwjMS!86*!vzxK5HS>1>-S^j%dyA3P>+i;r1w*U6M&b>sp8Plm>xiocz~0B zVeG74zq6?a4&9VojdhhAD$`*n#%2l5qFS41L28piNo}OfOu6DNH8`ApM$j3U7CR!j z8B#GSbMIZKT&qS@iFIVYAN}^SZ7Pp;V@O5qqDrYgR@#g(I4P>OvN26D&IiL+OFzU* z{e;E-&(g8$pNia`I`~ti{M1>mIQ%L2_sbu5APm0XaM7%1l;Saqlh~LXc#NxjA#M|u zU%L%*kI7EQ`EZR^4KY!}DUf&whWPJFSm_Q;j`0` z8+`7L36f!>13#5UF9DNSZNI@?^q4otY5YaJo56ZpihHjmJtkigOS8V$nlSV z3Zx@X%WX?TKCepL2==|DHrs@{mr&rwf+}$ z8-Z~hDJ<*JwZB{opH0D1-KPWCrsku#u?gPNa({WQ573dfiQ7*gdDQ8yvcTZ@r?u_~ zogNp&^u#_;#N4G6F}eM#1(TD6<`-QVnmSGw_BkaJ-vVu(Y*)=?HF=k)&{fYc(C>?X zAHeMP>FADIYGZHiM#I-C*OuV-6Hu6XX*D*b$Kk%*Io6mic0@V9AkVpR_lJ zUdUv51oLVlbzH&1@BdzSbG7S5{L=^Z2Bm8KMQxjtF=`F5RW2(1@1(p>3ATpH@!5WQ zNzu@J{)DfIzRkJ&pj()fP&-S>Vec|`kx06|Z*Tt8X$H{)%y+pbmU3a+QUQBwYxHAU1_|(mDEd|cnP`} z=|}Hg(n~NHio)#vG`?+4z2~4c^)Hlfsj|EA2~np!xHkzj>Dg~h(osfKJ#M>mzblQ7 zT@2`D?B3g}7~T=y#*y!K#qv zW0eWc%Typ`IbW^xzOZ^}fT_3G17TSBh!(R%)0`#PW`2fomBYE#Jp;A-OSyib`ZB1% zvh!5?6L;tPns^Mprh0F0NzDz5MFi^xz?>J)nB^Mq+RkcG3Wy5-ce*Wi5cq3EHo@?j zJqFi&h3u!7V~?H7Q41jG_DqX~;eS8l>*-iBq4V}ifX|(nHv{y3*ESy2qgKBrvbKxd zY@*~4!5ne<)zw=Q4t#JgbcB^jP762aYHI-kUPqOGGY|a7W{ll%CuPc~N3M$=DC6F* zDkSO`w&o(17Sv^0wmv~x+_q_xRE@@|X*(J#A5l-tdtzLm3&R2*4KNW6U?MvG)dYS#%2Bdv=D`~u`Mj<}X{tN3CS`}^ zKchBhUx+&-S&tqYd+cKLG z1ZjG+9m?UZV`IzVuDACv*G|SKptlH4D{Fmp>{l8Uo~r&BR> zWP$_5tM;JC1nb`Ex2WDMJE6D=*s{dEHr8(M+45C_Ub#)z`)~qms%@B`PvyLyO8>Qv z&$Dl!GSW_)Xk&Uh`(Y!ytR-IHkudbCgAp+%*HZQ|}GgM`nw z4-<)loZI*)stf~o$GJYQQV_m67w^tD&$YXVPir8H37-k@dBZN~KBLS@^?=3Ltd!b+k`y!_wl01wEA=Ow@c;=Pr| z%0o6p?EEhlQdyk+tgZ8OSqVul!)nwsTrNFiC}Up+Y89dp8!fd1Zj;AMJ&wyel``RQb{0mSM##Bges1>TTk;>Z!*=ApB5vGayge{m*V7 z=w7PS?LPhK1#FHh`Of;x@CO*_q0;NM7(dyQX|bYbzuMesMbXx6CH3b#Q@7*fRw(bm z_vd?@;ke>bsvo4=i+}mhb}Y86dim$;xaEAp=!Yuo3IT)ES2WDG4tJIM)oyA&&VM3r znX-n`f30C$toe1VOdT$%9(1+)aca6hHg~96+VAd9qR$1ye3S|k%KW$@f+66Y_}QoS zpw2Q`;BEohbM_DInf)aA5t>*Fg)JJCLl~!ewc{4QSn!G-OHS}mZOujR8e+iYuVf5( zKb9P%=jLLN9i^srg`lr)|GW-N@#<7ir;6mJHF_0ec;Jk8aix9jv-JH!+aYmVnqjY< z@d^wwyPZkXNmbbDRk?EpWq3;*dhNnW{v!w~49u&;y#O`X5*_9)93JtK1sVU?fjxuQ)mbEC9B z&0MU`U&GsB+Qd!0-H=$HZJ!T@wwLrvfAzTMJVx4~HwdEGZKFArelXa*j;RB)*d`%b z=nUhYC_@cF@_ct4uq<~@S(o5@%Romx?DVG0&t)61o$!FCYd7(-zgq9eOxGvzkG+*i zkF@!E5$xv1pSyXKTp@%kv&S-)McEAw`D%z9np){P%h@$u`&LUMu*UJ=((E+y5ngU< zMCJqpIi>8~#hLi~`%ivcrvz3|O&U_{lp=y$j}};By;l!rZ00n{?=&bQ9^*Nbi1sKd z69p@L*t3Gz=Sos7!wzxBj&v>d3c4w~ZkOPWPqxnSm=-eJ`O>Hoo86QK$= z(T)}2kAB_mud(x^Ct6aLT$de3(6!wp;SI?PeMk?9yUQAOp*yZEP*O1r%;0U+LQ%E! zT8?+a7q__q*88I@8U_Braf1=c(@)o^pqDbnzW?@^nmq|#vka7Bw{EUMN&Yj{f}w>T zyT$yK=QQuD9NrG5yKl2#-2LhcM)Qqq%`LcxewuTlKr&T@p; zpzF0W{W~xiUa!d7SPk2n5KnSrmC9<=-mbsZE4wmv0T!8&`f1-r)8jULX=5`)dO-nm zEv|jv5pI$BAKoz48I}yMCqu%(l=j=5{<{T`q002lz_Mz-Fx&m8?pqWC+dv@$7pn1P zcGfM(+EQorQZ}(bIS_HB=jOS}wwXBM;Zjz%NGO6;(tCnK`N~#Ht`O*#*DNncN-Vy1 zeja|o<1Bpd1TVRpU3kk-XpMl?2#!!_y->CAm>|PpX^iH43>@epGlDWVKFr}7%Au_D z7Zg#-LDKLT{^@T#ThytUo1gc_0I2=Z2rxnJeigbgGykI}vJlLxJ`LLtz21-? z8&W$iWgGgEjBbt%d-HS2Nd76SH*l=x2nD3%F<1DHcO3|ta-kRaYz?3XiD1_}Y_oNefRc0WYrE_u662}?9 z86zxof$Hfzop1F+^|ulY*{p(~@hPONMEYI|rUt17hzFSJB{%;s=dx#pUCXHC`I2BS z>ad_g3;dw8umYupwB^C+m~$735TrpF-T`KHD8X|sM!+0^3M(P!rjbu{RIx{+P4GQV zneOjTXgsCZXJKLc4uA!I+A(1_M;x9v4s1<^BK7aq`w727VL#~T(0$Z#@JbLfNntKl zU9DW3SalXv^$0QO=M{El@d<|GBs6y1K@47REvrpCjZfun*fA~}s1YFRDp6~;=_SS? zYYlbT406Scx*xXm!As4?yu8p&)UL)U>ERPO-d!>W!mM0cD~F&nEbX#XUlsS04*#7T zy+12Z@#Is%c92o~xmf75=hTLPIdJtv4h`gel6}6<$H~4?HXb2-eRk&MTY%NrimrKgy|aTmUc9|c7dAwUPbf6~^s9<%ZH^KZ zuD`*QD;StfVTMm04s;1+{qR}o(CuRkY$*G}R&QEYhBqP$HQc=Th(|7E0kLJaEIh%( zjVCDphy6W5JDpIe?Uey?pHo{Vdj2Ni}(RCaVGILE+Y#7(*I^1;g5aGJP zJ!;J+#P{@p>0jZ`KNX>`0WKoO*sI034n5CVCYXQd;tgpzoE;QKXPF31BtIwb1I;lu zWQrz|?T{3pc&;-Ea`fqdEJtRy5Vo-$z;it%zk2g*4oqcPtpO@=@x|(27E%4Z4&F=a zOjb@W4Ln@6)^(VKIAkjOeKRb6dTGc;7s6MlPHHh>&XE&LX%UHOX9Q-ijCT4-|pXBWE6;*T`-J^*y>uLsAG&S6sH>@@Sst`3js|n>ZpSu|yhaL#wj%i1(%3jEJxH%B z1SYe%qO9zzEjybKrElU?b^RwnKd`1I`JOqoX2hr0vTzM*zrT5Gt!V5_=Mii7kt#sm zuyDmjZlTL?*+Wxya{FC-hD3(*9_f3q_{6p?wUQHW4d~sh5*Hn9mXuTPbw=A-FTHVG zkc;;%Q$t;xbnY^Wj&&Cs-UVQZ_Ie-sC`YBN#cX8+_tcZ!96y1!k}Gr{&vTeGWVmCH zh{^I~_)(={tr*C|x#L3@B%rS}#Tw9)yLH!S3X&jbD;W25KQ96|5&+7E5!R%1fh z#W^Qdb!zMV-Iiz5K%!drfPUfOJ5cBeGzRiz57YekM4PBK;NG0#tkvA(ENYO)qvNtr z*+4!WE!_6Tpa9-YAR^r|SJVSqVxqNJZiLy_21)gV@m7=;dEJ@es`oX|CTE0{>$opl15p zMC_?GyKQdM+g~ernyXjKqk7Ql65ZO2kiD!NgU4Y~jP!O)L?HljraAPA!)_HPB%tqu z;UbP_YSp7ft68*q?f~&u$BVlzGjsVXqUr;rrTs$TLUsPFRjYH4D*S0!B?3&%vZSdp z6l`36m4zI?06+d&vrx}$3Xq)=g!DU|p@{QAygEz5`;$c8D-ov;<1OF& zTE_T~S2iAHPzmO%Dn76Bdyf_K5ye4KCZNi5-RPhDwnt@jcH_n;{&hcm7 zM9!nw5&X<6Y;c?eQ)vbHL?m?jN$<;v(rsz?A|d-~k&UvDZ0T8m<{XW+tU#Tvgc}bk z24KqUM+N%RxJNp+#qSN`M7!bkQv$9%>=MF4>eQMl{VGgy$AuGE)Rp{;)v-rUp@P)j zuXO$^i?|?IF8a?nV)r;>XF+TPo=H9Q|l?66}s^1zlQ9s_{O4WZ#Qp8M(hs(5yF>(yL4W1w#9o{lHE zx&+R1$BaGs=4aFWq>{;1*<8x;SFgHyH$_XpQIt&O*On0MK-(~gBPyQ1zcp?$-SB3O z=n+bHk2gMXuQ5{nlL4M*tI61ZBDsOh*x>6;SDaaV!12S0!m0ptRLR7QJR9UxKM{zc zsU*KT0l(~}l(J2QEc| z13;+<_fq)2KPUPm#F6jI^R;`-?NL1be-zI}Q}iN(*K5AF3W)W4d489RpIDb0H!vRddYgUv+9;iu4q={a~8A%9bnX_H(;QOj1SfFS?H)5QZ0GsyeeR zc}=k<=IVpxfmtB)eZ}nFF@5wyTLXytAlK;dfKsCDeqB62nn3g8^+t>KdXL31k-j5~ zGvs7H^Wo#K6$2Y)oUYuH5|&B29XjC_4`b#ErWa~gOGw&pJm(OnL_`ItW@O=xFt=Pa z2+`7A&-rvyv*97T$1?pyIS(NUV z;9h4I-uNn#s({{E;lXg#>>j0Qfo^9yuA3Ak(VVmtgEP0iX+PwguUh`Dp?%`W-O!7q zenlrh!`SE0+*T=WwtM<|#<|P7%pO;-5Rm!mjUm{rE8x&ZRv1Cj{knDEN$d6ut3ppS zv(G~QUs*BpOm%6}%x=9*2)Ly)D_=qPLC-F8$=Z*$|OJPQOfbK0U$fdhhFJsfUD`E&=63s^2B9f5#OpW| zihkPiJ)`^k2!L3{p3_pf{at*frc~|2r%MQkTRIU3?@5#B{)qvR5nt~DORMrmE9#v%;5zK;q-;cgsp!va`xP2t&F#ACc$0e~y4f)c<>tHOX7fb&P;vC^OZ`=sVg-lU-j$)`^07@uNkc zk1}V65uD>C)F|^hv1$pbMEf$SFH|{xH$-}r-rpP~_rmS7BW@A#t@|luMb-0B>A&>` zf@V`WC32__2aY?0P00}QVGB{S$xhXYg=?v^X{iGh(MHF4_)5-18zOZ8!P&a_SBh_& zJqAl!YSwgVV`*`Qwgf@u?v zA*J9L(<(hog0S3tM$O31Y-)vOLt`s|a88U@5)zcP6;syq@2-txmC2HtgOWMJ29<-JR0N?YbhV~#?EZoS3<${$BW0v>ksh+& zDbb+17yV6Bh3@0bjl0g#wNsI-=Huz`;q{K^cR_uT9wEl-rISXJUu6=p4tJ3wcSmg3 zns1J+h+Y7BCqv@>a>1**R?~6Ap!suvh@0odoT$+9y70;nBsQFfyoLTHD=3m1Y``L< zTsS6G%4zW;=9)|0@xFmvZ7vm+9f7733qxrQ-XpS<7J_CP=Zg*kz{=l=TXtWmaIhqL zS@(;N^YP(f9Dodw98N6HR5?pViDxj#zJ!2*=Q?p(cv+HG;Z`%dL)l+?p(S9DDpHZF zi{yyA1XlpO{TwBhM2xRps}9ZGT|^UI{Z+#u&*hEiTt&Q_KqKy98^U)})mjmMA{wgR z9+|Ck4(3b;ZGDN^v2?9DE7xE7o}VWB4q#!K6om9zf>2#{TOk@VBG5<~9p!pVXGe_B zO3fjHIBy!15KN0sU{rd+>?9{*S@X)#^0hdV=PQDnJGZMsXlVk!&y3H#qib1XOD3ul zA8vl={%(%KQf$|4KSAnmv;A#G1W8N4Ve+vEs0vn0eii_J0dd$J9=1w1MVD~67lQ_w z!FbyD@W#IYz05cl#Yc6O5NHwJD5SQASihyBrpV&sikCdRlMDJ}&9da~468MEae-pn zkFyqN%;kIFdPv*3*!d@+Uf{OLhi(v3pLTJO(&kygr4vgfxkivWX7pu~GMtLM=tX2I zM|~ouuT10|g%xE>k#VTWOe6qx`58{>P85I(1SnO9Ztw?gxJ?1Fv7EPRv-7wA-=1z^ zzpTbif|MJ({4;`|>MCGy!pWAu9+w+8$VEa~qxnwxYz3v6q=D59Q9eAeqHp9nVl7#_ z!oN`P)XAtsSj^?=#TPmadL<7W%Z(d{K_jP5wu2_g@~>BAHJe3P&^r^rGDm(xf%`6X zvf7;S;lni=-wj6ahbjFp>N93-a=f#Y>NnqRGEL)+%6+>~J}e;U^1Mj+336p)0-85n zq#b>V?@Pp^uB=v&u0hRK(?%z&S=rWy0hkI;tb-=TK$`5POMctAHenU}QS)fya0E-_ z$HQepR_u&H384IV^q|NqeP4K+cAH2JhUekcZw8ZH(;~N{#2p5t2Z~m~(1cGfSvDVE z*F-uS3q^pQF4L6GHE=(m?xEr+-YBha0VHU)dhMkwzl#Devg%Y>3A1`?F${bAMksUw z=!T?g6$!)#MqxmFw{rb+{2Q!G@fn9`>?&zm+`Yt?F0mwh42QS>^ z>1W__J{9@WEM{tqo+Kblbrm#T-wa(L8LwXL_!JY8x!FPU!#lp}!?kqEXi2yV-r6+O zZtfNm71k!+vg_IzlR$*dJj7!Uw=7ie22MED+kxUV(fxj!+2@XjiqmJS(O-s>ysuC_ z)htU8Xne(j_5M2`t;he};sOIUD@(R)s4>7l%KB>Nl!A@s#lE4tjfux1J`*HwH3+Z8 zekiZ#{xZ64dC^lFI|kR=QI1i1L3T{@j{DIgYY)1}Tnp?UJg6|0RyPXm!Oj{&=Hi3_ zDtUvr)I)zkm>ygku!3U+>^bc|-~|6X`nm+h@ey%=^*D?uClEX@=!2lc(o{HRm~q0`1;iSzR6a7$cxBOShq?jsnu z@V7*qb`3oW8*I(>QVuku4S~wJ+JScmiFc`x%by|^Ymn>%% zC)Ufi4~PscnuR%b;ZH~|ec*&UdJG~DYHQswS3T@EWHZ^LGT^Y&9NbiBN$t2!kD+lU z2`ruDRMxH8uu|2s1f$hDx9=US_FbplIvU}ZtpU^P_Fdi`+}W`G0;h=p_JcVkd8y)i{60$1m&s+*Z-PYs~8N#E` zR^!;y3snogM9_<7CV`o}$o-Ngz!sW1;I4fqYV#rbU6aNLSb4Kv;y51R}453Ba(ex*rL2sp#K-oJnkxv;bvWfyP9TU;{ zorwM}vbg{>nNMEZsn_YP>3SxBF{D#lyKe-hg95IgMf_^2QCqZn#%WbH(W@g;pbeAE z;B{9@P)HN92rkACGJRXOQLN7CQtMKQvTjNC*M(cFtp?S60}Yq8t-cl^Ls1I$ZO%QXLr}Y`H412^v^eHx z28WeGb9o-R$SUhC4s7F{drt?j%WbFV<#|Q^P)ne_Lw#~riPHPawEH*BNCo*6YH4KD zHMG*BE?Xg`%c?JK9iaMpkJ9k{YfyrpOFrECEj@gdlrysI2XF)Q#c$3H{P1FPP`$$u zA2r4Qvs|Z2lUxac(j!>!$ZxJ(V^7P;FfiD&X6e5H>fghA(}ng22MiQ`fGT*gS=D#+ z3>5PUlp+_jhNk{SzfuEa?z6{AklX-W`r#5VY!vOfaqNP(FZ4)|GQGKF7x!$>h`v9#j3Zg3T8?FfC^og;)yrD(Rh;F zSmXzm|Ghu?Yqr}{tTMoD<~nsvtL=5V2383%h%keuRmPlPy>$b3AHfy4oY*wG*Bf!*y4B zZGO&hVY!(C)H(dY!E^@pNOz5A#qr$-3V$mHa3o2Zl?VO}V*+dkXx4FjaGQ7jLyeP< z;u+^3h}!LZ3+IewgFzDn5LuK@4wQNfX&|MJu&xRp0GDz$b6x%gb5cQJ8w-gg?bee$ z1qCcbQnOF_d)hM)#QO((=ksk1%)Ji;hHNsh56!b8DZCEkQ~w9xCI1+;Jw7@x<`q&z z!QH@+4W5kL2R5Ymr_vaLy19xS!sY zMcGo2MpQq6c9p8AA^E01o}6U;-LRsLO~T%K+8E{9PTwv<8}=&UGgwruHgXR}meooi z-C>#{$8FAZAy4_#gV`M9^Jexx`0#!RmY*B~x;PI$T>wB|!=dC7L-0@tGDZI>xJiQ| z*Eo*u-1Ifsv_w0Qw;`p#Z{=hEe6MB7sH?}O{i_lZ(L1uSt_MIR2+BD4U)oYpxnuV& zzU%qM4kzqs{nu~7-Q)Q$V$3>z*9?G>kM`Y>X`!%OEy&%EfLMYc!Pq}pu2*QLkC6`E z)&MWD_0nLob?{vG)niPW|2dKecxARfo*l+{c}lbl+|?hNDR6&k4mHD}|3hwq;PVZt zI|6hwQB?i10A>a1_{S=mPFKF^HF$~7>oveQinRWVBb5K#PR(U7bCcfNa*V0$w+Mv) zSRj9!6YRUEIr|~}aN*rlB+yhSQhhH+B`-a>X=F_FBxeT$a*QI<1knEaz5lG|f2vsX zWRJ9@=Q0}ar6wnnVW?pUPU^Rc-8;SqNCGXzh-?JJnu~kz-LSN@tF@xN)N`8^#+;Vl zoUo?o*q?-^PhkwunmSLoY*Yp(v@guNVz}rexX+S2s=5a*{ZIs&MwHsNlmbFkN3sLu ziS*_4p{U~C?Lev&b|&V{yZBi;c9?|%<# zYoiC$`_La}8|Ev^%e0Vc!6_x z{6D1C`va)R~?VrJukD;e3|ft7E>^b zQ7ckRY^BQ~w*;+!f%BA3UHjE}Oa}g@Usr3duk&;=-P~>7N?9MS@--Ox*_OCYk$P7B zAXhXWMk-wd+oCk;6~j=ocuXck&my~Kn$yjc#96nM?i`i3l4tSk54V0M@9_l#J750< z8+2cW84gV%dD;Rj9{CKPd;P6?7y)$guRTTaAO=yGq&VXE30UnY-g0Wcw5!i8=spZ zIDJUM6%E!sxmJB@Rm0%Sjm16~Q~6|xjrUkdIDQJ$J%>qKW53dWA{Y15c!heew!DOc zXcG+{K3`FvwP3#tbehPd)oKuCD&dZnkY2nLs*Ji_Y?+w zL0aNr{V`)=@S=;yJCF>t+ zB~$Vm%6D)|;MxAxsanads;f8bt88|zX6vW>2gi5^Q?xh?&uJv}c}_eFP{T-bibcEU zzJA)i2e_Bd(=Q#nN#So9PJ15`!hZSYUz=O#jN!6gi|Z|lVdtVC&Ct&WpGj@{m>Sw4 zDqQ8@oQTQOzHeW>{yqly_jM7fue@zFtNuN?qaX8Xi3Y^82X#^kr{)aS$kVxv9mcu% zzi9WfR@;M+CqHa?sW_-H(SjlF%HwBYC-+kA@_BD7EK7Q_jE5FbQxe3Vv^wcxy7+h4 zjk`AG4*S?8tY6yw3XP_OSD&j@lS_DEj6DY&hn>(Nvawa%-ixzWn1V)&ZuT7xwCM(J zWIt1uZY3HT*_>dY_~W)y!0mlipS9P6&amRP>p8%=ajJYgxi^K8FGYjfb9MQnGSi(a zqIZzHt{>vv2F}U18e{F0D=QH1aFqTyI8Hqc&3b!Q8W{$8Zq=!G>^=}hST?Wb01r`Egx;W zAB)#V{1~3}jbjj|Yq8xWEB4Zt@2~ZN`?`?)(gnEMyxdi`JlziD^2ANHiko{~`M@0L z6B*z`2(~yA}PYxs1c(8Q>co8SK?IwiAqdmF!UpC5dG# zu^!kWJw~x!`HBEaYsSw&v>=x{JYE@{z6{==A4ZDqOzCH9*XAJ#93$OxgNKv;*h2So zISV1Vfb}V>*w@2-a!++G%0;irtlYPx19!j@x)pxW$S@Fsz+9==?L)G)EW~y);Ms0q=%kKk<%YVM@1{CC}uE zX=ZnZaLQ$%`|;&7E?dafFK9aVFYS1lOFPIJr)g*+?XSmnyq?|63+)Tq%Blz*A@yMU zgK`W}6ldxjxG2?1`gPz#XUrDk`jPwNde|_#l4sX0s9?0}Go#GXJC;)*=D*5*>q3(; zJVFGih%X#}Sf;0oJ12qOr60%`6M6dwBRwF;9lktNXw+HOR$+2&h6K^;m|7^`qUe%w z@W{#DXz)kb7mbl<;obPV6#|gTP`22$q~+VUq=I%Vfh29`)+#K9r-&+Gb1TE_m#Eu+ zZad(bJhA|gO2cvXkPMKkj)J6;v`M|Yr!5O;18TvOEr3}aFE3yW=u$fEO>`M|Ee#)a0t{jcAGM0Zpc zyOX?N-UGAvA{ppy z>FG$YWPh2;wP&`7UVaA0=ehSXAQ41gH(@MFhW!#Tq;MZOQToO#C7+x8=(wl9ZS6qg zK*1!3OW)5&Mf~+BFTXh0I4SwEh)&^a3W8CiB3+AV1TEnZ2XAqYS#O$wh?Gr15hX;` zxUAK-st=`#mfl#jS3{s5F$|=Q75$}%4_W~z%9*3{*>6yO9H#+2XR8W4UMoG=~5P1F|cA*i;ssl^96_%VCGx)Lx;nFwNB>fNU zvWq@KuO9lj(sDFWl!~FR78ePbeZM?+0=t{dT_Fys&=??}_#WNDm#trPoVnuA8opO< zZez%Npi%nQsVBoyFv*564ic{`6TQ?XU4T)dJ4g7zX}6;o#m3U*Q|C#CvjOx+ZNH(C zu(4DsSkN$aWV6WxJ@M(Xc%#UnEuH6sAC3(NOnC2=qyi}CdR~uLK_P@iQubktS3gua zDh4ejElU2yE_vQ_l0#kvFvq5LeqJ3U1Ggo zwdB#!3aoYELL(BbY1I6eFdN;UKJT18+k6~|A@8Rc=F9E*@A!W#eg2X__Iu0vK|0TSY%FBR zJb3!Ueleo$#4ZC{3GQ|Q8gQb{P&}?^g-PM|eYHMCD~R)vR;7Lf`I22@?IU;GB-#%=YU2z!vSuYOUJ|(r@ za)EvitB$E&kg(`Um>H_&kN>&_YcKUE)N}svdjA+98J-*>{pbf=xQK~3s+HWDVZ-#7 z?v*dgmg0=-zsln?Y};r1^ow)Z#c?0G2M5Xamg^fLu3OxLzDJMDq=M#S-G+sU3cm(C zm*B>tjYr}Bj9~x!f5@9kB)AIW*+)Y)9od+@f#Gj^;kRN8Qi-iXRo`mO(Ch*cv*vk%YekX1!jGp5VhqDYcRR_}29%%m^C|}wdQqT|@hIW0e=N;s z3$hq2%~{SJ;v2>Mz(8%j$HqF!mJA!?9m0zk2VVbaRUNm|%x>UGV&`*)l65y!)J`Q@Cx6hf(8UtRY*DvoohFD})& z^mL(%4Q>)5Cttm&cJynCq;ez-UJO`_;SQO8#|b9+qpP3BD^I*U z>NO{AHMVpq{?_L>5J!6~I8iS&y=$?UN!WH${*t{)$>08W?-|bjbpQ6h?EU<K*2*b|$gO}Udw(v0ri3M5n&M(hbiwgbhdk@Pm_d9f7@*{jr zaN^2JXN9FzSZ1R4f_tjV9pb`481>=#21fCvL6vgFfh@RclA@gR$eO;$;-utCyby*s zsAMBcVrX8IVl|$_U$cKZBi(4iR_;4VHHmU?L7c4wAk#+uL3`>vs6v85{1_CEPQT(GcIey_xi7@4N0x zOWk0}rcws$4?^Zc7j6v-S$2u`dyMnWUfkAv`&B)Krp8v%1su}rus42)tK`9%t;ZnW zBwwHc^GWUJxw$*u zHD-}w=&fq3F@q?5;=+0TgUn@-_F@5W6DVY#l&S6m*B#W2SKcANSd`g>a#&n44;#WA zFz|WmRTW`KuF0W+uGZKSqAU!9c*<%k_0@R+7LzJN>9oTut}9HNfvWsvR(pBOl5`jF zC>K#zOWHy6zc~fzGTcW0AV|z`%JW0Jlf}V74axxj>y1(AOC%zU+Cx2Tt6Tb-ghQL! zDsZ0C8X*AUUrR~#S(Ql~a{^^5_3rLWmCV&!_)HFR z*;B&o(3YF;n{%i4D|Uke!i-YS;o{-^-lQG~!E?6vK;!`z2!a5NyX_#RY+tAhoSdWw zV>M%bi=S+Ri{yjQr#1&uyS_SL;ir-L+FK(_9VH?}N6QyQRRp(kyIdWX<|`_4Czks1 zDwfr4m0h>810EDEqTSaqxemIIh+sdG>L95>m&VkgVu#%L@QZj(x2m4kC~k9CeUe^r z=T{_N2!(hgDiA9QFfPsTu07+~cK~Bnj&&rj9}$S=@wZ(je5!v=5W^0aErkkEr+EyV z7i2xfMS4k_%0E0#F4FL}!|PqzJ6tWN?-*)g>?@CE_l8Tlq>xvK;#`q!CEFgA*?oHv zVza}x$O_{Ur!KV_z?wj)e>x9x0-%}>%Lm$@H&ct+;^HhLX$Kdoo9mN+1Xu@mta4kN>F`G zb+_5{qFXvFeaNh`zk?-}Ka3%e)mtfm`FfIlzPxx!BHOTbm{j<^W@FW*%~vkbtuWsU zU-sF+*F_dyowB&m>Bs0y({6AD%Umc&*#oa+-c+5>W>l)34vVnH=>BoeEH|U{zGAV0 zMH4blz34&x_^I~6!>sbpfqrZ1o^C_Ni+q$9x8$1ZdOQgp6fOiiuS-nEJAxyEBqWrf z$YW}LSL10>W{pRMM4tO!r0q*xTWF&+R|bxhl%cb_EdE?Q+$rq=UN_?@@IGu{T~+B1 zwA%e!k2Ql&Gr-b%`qn)yDm}Xq^&7F+j%!TH=9G}@ zo4IFB=-I~Y$U0-R6Y_FwHgOv@l zsofLkcin7$-aYmrQmxWP$p4bs^+W9S*?>gXR`rjiZ=%1;2TQT1&i2ueG&cC3vyyEo)Yrb0kQh~UOV2Vk2^rM!*}B#5yKi1X zlQv?9D+(rDUNGs(X;@xt-ddfWroN3gCNIU*ojkdU9Dpth-fXF1BO)Y*;-ZERBYM6R z-wSbr`ja!33y+AH%r7LwM8yVIC9tO0lS7)!x;*-g$R*X3qQxs;@?v|LAPnPB>+0xj zybnLV`#yz=A4e@;Ar(Qi!6US(S^pPm^;8>XYECt^ z!_=wfn1q(_j9d2Sq-8B7_LY@H?Ai=njAx!iSJ`l99#pvxmdDkg6NT?|k0En%J`ys7 zlqSQ$JyEEkSoB5n$)$;#juBm(F`e>PxO!iAB|nWo^YbH0CgCy(i0482N$RSi zSHk>z5rO|A66elI8~663=cU3GEmxqaQF!N$!OA7gI3;l24!gsAcS?|4y=wN*tl0(bv93pD5G0x!aqVKn200Jl@S6k*wpc z^YyJCh3`D5?MlZZ`m|Eu~z@nCq&>bK0)X zi1Hq_7a7^2%lC`Q2&yq_mQy``8xD$B+A>huQd4$d)3a@d5n8|OK-9VYVM(Pp#`$_o zd_q}c5qACR0T!`E=jj%}za75VuX=g1L&iyF)++SKfgLg&W+g^Nm#uuWsk7T3mRmXY z3H>hqb{$;oXtQ#u|8VH2SnPJ}KX@u4wbHk=y9&_L65}ccLn#+5Tht^oN!zXy%k7t#;0_6A9brc_l7~ zMIH*vV=yts&hehtY+eLw4MZ+3jv?C)KW7+-I;2i|h;b_AZn|DB1)Hi3Ev?HJT z&m@V^R9p}6i07-5kv5&3KM9E=-yd-2J+yw$vIB*C`zYe=s1(sI-bC9mfgRn@-`_p@7&5w~tRDzAdbYj1NQQEVL8vvS; zQIB0ryRS!IK46R9n7=`{(=o?zZp+e9f_>o zUv_7)?^x1*U;hiPE$Y&!*Xf-1W!^3AvFmF!j-v_#5;zo72t5NO948Ed8e{S6*So@z zjqHk(0D^sy9-<(0$u5gbJ2n)~8POkfwl-7-X37WfyC5 zj#pHsk*YD85$$gJCv)2o8Xo#=Z=A4jKl7j5U#G`->1qP->s%fr!^McM`T-=2=w zI3db%ARMQEXHv8L20sjU1`{f~2VKl!Vs-6TPsy89x^@Fz7dR0!pzm<{qf%?ldDN3~ zY}EXqxv~33q2ZD1NScDppb+dHF1XZHmp6Tx0^GmKFC!Q*v+SYiAe_6JvOQgjKDa?^ zDSj*N>YnDaoC$$zgIJCJ?cNFn)js79TO2Yy4Jp3z(jLp@2viEpsM%{iIbzT_ZJ*ot z*n@e6;W{Itv504*2Od_<5tmF#pv)xr;8$MlUwMXrFM)OUl9zmMzv!fOs4V{b7 zs^rfw+Cg7IkllFn5dxTL4ltADpzy#gR(d2iMp|dTgdDR=L3d5r&NVW+z{5Me_Y1dI zMRhfc|Eo3y_9v|>TjE;)I5uqwOySk0HNoeDlFLl3#Eu{P*?a~ z)}+}9?iOwe)W~Xbx&8YDyoZG@rn?IZ#)Ia68z>Y{go|Ev&Pk4ATUdQ6Pml*?f;>nk zIoo3=K*Qo7{+U~@2BGVL-nrY#(u~UOLy~uTV6&dE8a)-2%hQ3cK22407XQHV<;%=GZ)3yqWMR3yDdg^$Y%-gKiJaj;I+;%p2TiwFT(;CwMxgsKXsm zQk0YFGJT?Xun`S6-x&JEtH|U~O$(+O4&>uWLppyG!~u1RwXuY|c!*FMN#HQ{HSx z5M#=Z|E&T@wk39;N+5tbiUg9R#CeJF*2axn%zD8TS`ewvdd?3D()TawL@B04V<$s` z>clCQMaw5o2G>baY>5s}h6MXr!*h+}%zmB>gzM&F3{xR0aWYOKqtAP8+`i-VNN5c5 z3>W*SIQcR<=xZu|-&FBSM8fotiCFC9kl7CAQDu81qGy+a`C^|w1gEJlCg`7d{Y1Mi}p*&fpWWgyLu5i@W3O_sV#1Wmq)F3`w$ z4mw~H$k+T)s?jz#u0H^-Ld~NSp5Bvw6X!rUN5VN&c9oPap(}TxhqC^`dX$4F-erRS zrl-ce;K7ST$ht-NJ1d!NqKe*wYjW0!m8VruM#Q<*wF`=xPHQ1d_!X8uUKpfP7F9=s;eSP>T@ zz2E^APPIr)Sz+z3n#v~po2N#bzzc*)s)%R3hXyQaMjZ^&R|4 z?gwT3RQw-$hyP9SR-3tG>@@nHOfIs+sz2~HS zHWK_@h`NRB`@pdN;5EGJh5upMKb_-_u@Mri7B(ZJBtw$2;pDMZt2dq>5(1v+&gRB- z?~Gn6_yE&ME@J0ABi-LtenEh%_E4S(ny=mqEM+zl(&MuSBRF|K1v3hF3yV3$f5R}5 z`q-pS$nhq|b!wjt1<%Yp<3Xill(#H;)92`n1~Hnf4);!YMvk2o7G8{UP=UUR<2;W9 za^hdwfW|2P4l_69&w0u@TT`DgBc&v%$Q{6!pukj~#1O;AdCMmoy%9c0@A=nV_S8&N zTx3@U%2NR7d-km1;1Q~JHAo&+$i!lF|1Z>d6K6sx7=;jB!Nf&*E+Zs{a?wg0G48+H%@}= z`Dm&8Snsk#l#hg5ZRSwX?R^m;5(km#{4~XcCG598^SbH$KE&_``08xO_t2z96V##p z{88mWP@#K4Tp=2}i=lMI#Euprj+Ac(F>2ecP1J{qpog888kUDBM=RaSDzW#}pP#OO zebhk8?sH!5IqR*lc)JljN&OmqVyapn6FtHlNkznH`NpS*l_}nS-%uy&Z)Vso(x{gy zsYzNtlCk&D(*s+A|FGEqEL$#ft}FR1{F3Z$TnyA%WF+A}_S-AE;_#v5@@PfOjbJ84 z#Tenw@7|py1>Zs?BO|lEy`A#v6><85&n+XP6|VZaxrWV1nS;%)O>WJsa!$QbeJ2Gc z^|KesStPmh&$N(+8p34r`%Rb~#_k8;wi{uY`;SQ@@iUbTu#!?*U)yyN~~HP5mMUL10@6>Wa;q#7P9FlQsAV0fsJ+E?a? z=3=`M_)0I=@O4!wlT`ly+$$V=BVS}*D+cHK*f3CZoYZ3*Qg@N^jyEJD zO1Tge!*%cc5VhNxW(?yNb^J;33rfg38d4N-6`64}&4P7t)F?W%uKJ*sXWThg_w&Q$ znf93JQJ2L`yzLQ36w-NPrb46KY3*vJb`cXbGc$8imMSH#a(!%HK9c_CG_riHje6vHrIX@#UmmxHG zpvm%J0TnH6R942}gW4j(s*Mf_yP3B4izFl@Zd)VjxyLOXn`qibE^4c67g^oHYGHZT z5UwUolVT?iZWiAvEf_YX}+>{}q$&FnJ1;z2dxA zoM3^ho1C3k}aE+g1Yxh+$gu@^}%Se&v43 zy90x9(Shgn(9JJ{q7Y=oh$|uB z?3+&+JOv7x92WVv{acGl_|EhaXZYwEmf838u(W>OolSt}#{HNhk?X2;<$+xKm2R~~ zC=b^O%<>TG?Q(iWk|7ydv^#GSX-P?-x=Z0C&V)2z%u~+r^`T26t93M7z~k)lwH4X|KIB9=LjQqH3o_*wI^yjtf-Shx#hKNjj~L znR6(|5A%-t%2z)%e8CjN=pW^keNX3FVUVmZnU~Lf0wRiGT1J@BeQ0gt`t|Fv8LBBt z?pH?X#qqyWkV3zTc`zl0`c`x>!yl`8C_Kggma+(s0D1@giWXNWWZ!ETF0M{WkIUyu zwRU)HokR7uJGa)%eK9PGiQ*FQvq$VDBR`we;GLBo__ z6|8GTr;Qv=g;uGASZE%tU7Zm}K6p)18g{%Qv!ma95gdY5y2p(jJ@P)wCm zk9jzeSY{7V-nqBCwP#G94%^dH^|ttw)M;AJcN%aG?D zP8k;tH9ofSop0e^wM;?~?=@k_B^q1SA6qm8(y4ur#p5lvjlv@!?40j%8&6^n$gZ9W zx>L;Y%;ON1go2L_wCe+CWs*FC{HeJsyK-Tp*0W%jxWep@#0+Nu<3sKn)c(T9Jd019 zg{lv5iNk4_`~EWk{8h8w3bnWIlq3c4WJKwEy#M&|WByo&E|c@rsof3bj(#4ka_M5n zi8HmDwXfb^HSP$0{kmifOSgY;fG#xoK2d+=X~UF8UydJOH*g6L{4?W$uuNMR)Y3p+ z>(PYpV+h)=1x4(w{fg`Fb*~Fvf5)-UqQg$6vzDTPs&v03An<@}W%#o}ov+NR74g}Z>yUt18zbb>DOdBhVn!|U_Ukx7rZ5MI4U2JAp-E4l(qh9BG;_4R77zLwv=h|drPZ3;TI8tJJ zb2OLX+&MEWxk3QV$b+;8pMNauZjJqO(H}<-GioiXA!x-5P$3~$`qu#G=02Pg(8>h( z`_zns#f$L7MQ$ju8_6;7D+j>&wzrMfJWdFh()~94a5b zYIW8L<-T3Zd7RBhQw2p@V!_BZl8n8L$j>DyZDo6l)vN?KjD;%#ar zrZ~;5o_@V*e?6d4=b*85pu*LWVxK&L9RxO1eM3W}dS9mB@0mV?h53$oLeLD(-p95~ z0tr--shn>?P4x!4Z_WvEa&q1x+l-}VWaLGpY=s*iS&x)EheZpTp3l~?+vid6r_wr0 zORITLw}=UsOa7TkLSz5FTEj$Wb!DW?ZVzKwYQGpsUWP?Uw&}SYhli6DV^W{g`_ob2 z?X*;ZFFK7Dwmf&Rfs~;@uS8DVC2@X%zHr!=X}>vlKV7FJ7xNzA)cPH;xbifDlYIXS zZ^RHD)aLV(sQZ4F=)=8BEfY|PycETrXrP4E7?i177CD1VP~Mnn4?B`}T4m8KcY;Ho z4;Qnh80o_YLOmIp!8TJ(KYaK=wmodC%$WO_h=q?Y?6;~8!v4Fejqbxx6LM6$*orlRUte0c zXk$IX!`j+9SLAW!qfQSY7JVVnXKiQG^{b?(Tcea6LPD<^E4`Ml0x5<{g0#CyV-Vx} zwQ`w8ZO`Q*yfSuZ3MpMNmhO|BgB}B=@PAtm{7>66 zx&}E-w@|xGM`-3>=9a>m$x!Tf#ELzgtB5*s2Tn6s1vcSAAsO4GCNfn+#?$)G#fDRcR3*=eys~UyRO+w+9 z6yAdloMjaN$0SMTb(3*Dmo>{8psa8t;bugs)%eRLJh2jECc@*#b@|j%RnRRF+_Sic zcl!JjBYmKgP_@o){{QhFIkdx7@%e6)88WApxU#~rJz6N&_M|?ihl4?h&2&DDuHob4 zoO|0NCtJ%~J@)h`eE_n_fA^s9*LW3paa0mZRvKFiJ*FU$x0?;pmKXv^^(g<9 zKP`WjJ5kWfmoE=_b&6N70sszB>$BAef4Fqn{@s~>BJUqUfPbHofPndA4u8giq> zmxUnPzw%Xtx_|fakQ_0 zI230OysLD3)U^OYGRn!xnSflyZvF@R7HnC0rUMnKhL=Z~Oo<3R|R_VjjOaPCw2u?ro*8`i_W*N!;!g zm&TV=_ZbkiTSs3wt_GP%chOj1AJl8rp0CY zzY_h=pw4jns+p}vb?TG>aWGb{@JFJ&A1;D+OLEEgWNM4FgtGb3JSkBf$Tc!Tlu*RI zv~p?28`Cq$`>N2-w$G|_Qk2(Dxz_YarhZlYod_-?Y?wzkEex_;r4#hRZHZZ~R|Bk$ zvtU_%aY_FHb-F`XxQM$5$*rQNU^ za&ol_xg(e|{r*c9w8Ejh#QTr1*RbkXRZ-BZ8_L7yr6#s17O`JYgMnf~#%*U>A_6M+ zRwU}Eqfu@fC)6`E6v^=9<>isI{CfMZpS4H(WGte-oDP`eP*e4jxc`5|Ea&wpu~cd8 zj_-ApGWS5{>iD23Jal5V+uE3Vp}f7C_38G?$dQ;EcGYGL?w#~0@)oP*GAQ_}hP{QA zb-MYE<=z?TQReOzcqJgRE-S3DWVa1_T4~>`svu z29J}eh&(L^^FtZqL_HcnvUs6P54#6wCkgem2MUXV7X$@2)hsM5h^|y!wwJJSd<`p* zP?e_E#c}>00l7*LUv{NryD5~-tRsA7V#v?DJ2g=^^jTp@NC-E;FsEnDZ`qyepG=*) zL)_#2NRB!&yn3?Mn}zjZ)1~WV_1?L1?1^&cJw_%C%bu)CyeMN3K5#IobdQD;mQzNh z57t}xyXL!6dYZ!)GO{Z_0+{BY#gFd@JQ-qvGDZB~zB+Ae0~K2i33KU{7Yn1>PSVls z7|Y+k8_6?%*&lIx`pEafVboDamIpIt!rIU^i18?DZBwj?=7KbQ!R=Pzkk1a@>*V7v z_G7Dxi$&{ZCoWrImImy0){X4fKTUAs?Uc=qdWDhTDMxUrFN6ZAdZ#B0=_&xWPwanN zn&^{n#Qy&NKW1XI)~A{`EI}-3^`Bl;EW_j= zV(2m7FJ7ZT6f(NA^ZqOZtRUu`$n$$32n@PiRegbfoJQ>n{4nS1?tt#4TwUrW&0`HD z!6avV7;7)(+Mk<*@Eqq2MN1t=Zn zI$3WWxMk4{WaTq_`X9_6gjV2yciHbocF(}`6mIkuAOpZylux}fT>*?2wg9kr3TX+b z9zbQiI3;J4NIT$iW=7jP|0)=qsTw#GQP8{ZJL91I5v`J@#8WX-KNQ~Tz` zMg~grkKtP58E5^>>w3%M=GPd_v6?krsi=ZJW5WyiVi zNkj)*efk2&?jf4a($HWufd|*cOSuxJqr4;lwNjx$gIn$O*I1WjfC__+M63O^##u8U&zX613(* zB_Y(?u$P7FIE6Ve+<33+aq+k(yuU^iaY^+?OoGI!mwXBQ&=$Phpys@K|H*Y_X>^iJ-GMf>Bu3Fec%0jCqe*rBdk1U1!DEO zt%P=&!}1S&*)s^d8SNzPjDG>8tE+1>i!m8^)o)fMC_p#qSI+PQ3KyF;egKIgMkQC= zlBCgFyAbiMBo`&83wY(ab=%+;H}-^sJPFn2fi>bR%VS4OuI6Q zRYP9F$H{`;ym`}};lbQN1xOMdIriMGWBa9F<@%NRbaoo+6HKc;@dQ?FbsP=@_b|(< z9Lk#Pv`{3MdOE_7f^pb`>~fUTT}>C--rtCd%LL4b?p1t9#|vc*A#qiI_2Tys!Vhhi zWX|AM+s<}08~1%q@f!_vLcZXK3gAIoyieoI!9P=VbDWR%d*Tb?T()-AOQEz&gp^p*_b2AshM^S-Q- zZsfBUzbD!Mc~S76s{$eae&~6HeCHDR>({->bBn`H6+A~(Dt4tP_dSnMynX9dQ`%eh zyM-3tKPr6Pt@xyFShmamz)2P>D|(A>6C0zmrXKWU;O%qE874Q8=fk%|5Tj}wia1$; z!zlJv0C1AJk;ebFBKZF!ClMk~x9LY9BzFy6^6LVn?VZM;wWO`L{3{GkJ3m|tmvDT( zT+n~sg4l(tbJRaL%y9IU-RBCiGZN_j;=aV zDtc}-7y2^yEeVwNsKAUL26>(p<*$^>lMd^YUo!bv0aANue-@by7kk5W=TGD z(*zayu;(wb=f7C_a|)=)a_C0AKXup4Bn2pTH>acYTQ2itP8elrW-}c>e%y59c0-&_ ziOq6gP>^1j41Cz*LrVP!)UD%NILb;528|}(+QJ8)?^yDyCb#uX>|sF+E=IRR@mHk) z&G`W~&LPKgsL&DQ*EdbaJMQXv>=^B?H8NjyqtE7!(1AddtiPr#y<_;?zU-^)?{Vd*0N ziR8Ttc%#8`wZp?hN1vhBE2-ZkmsZ4Y5tEbO8wENe9#xQ zv>&p^JfW2u+&~Z>^q-CWr>%<+db$}g$t3ZMiav}9h9y?>4zTZ?=6UtvgJll;^C@l% z_`6=3IcWu)eZDmMyDr!w;A;A+bjuv(z?bQq_@mH#Ha7X@oF3$9{%>eF+W@VmBzS!6 z?|*2?v{J3PW{s{LZuYxcUo=>GxVsoGg3z;!*Lp2qt*Phi19!xZSIP^d0MES}!PS#3 z??(~3;9&4?e)9iw6GG204&`LGopsX7waQ%l=K*EFr;=!^~EA44x z&?cY;Qv=K&;qZ$`OP~@Mz$y5De_2%^k*A}N1U{6@n$fS z-9q;zqf*TH$>sIRuy>MlVNwQ4(mbdK~uR0T5?AKfvFtdCAQb8OCkYN|0+wb!c2#h!=Ku|Higk|oS+cVfWJJv#(pO}1TS`1`u%Hy63|>J-haPT zUvyf;qCcdzuFi;qr+HlORSz<*zjck*r$WTg;VKuGnK9r%xLy}EXFZp>_y;5ZlaT)} z7$!ywt@BpV`Y<=s9KJclM0>6;Q+peZ0R@Ei!r};yyxr{6hXC^RsI;H$#9rSjv-Ja%Su2R8Fb>;aOl0o+#Dey2RCQe&F=4^q za|}>hbtAn{IcP_XIWmCxs$UH9Cp=Eu7XAY~7Fa*MVSDWGk{f#)jW4lc)$T$@DcG9$ z92%YQp>kB6AzJahfY0HzAjtO|6gj z5HxO8pChtV5^LK5s~1!bwnQ+ISxP+VLsxq&F{)FAlk-^mP!!p3N`#^Rrp4o)`_-lL zwc-Z3o!Y5DTt(8r;8S|~O69Y3N(^VIS18(?Jlu}!J+tlOtukAi9S^x8Ege5tXvv*G zFh}85Faa{+Gr%xi9+kIW%N;p%*D8+3>N8bpp5<1fu5FUiJO-Qn*1!4x1E#(>LAAH{ zv6ZbT0_IA5tGvsPO!(El=#;FN11)ebn2#iyb|vG8P`S^#;6nPhasI2+zO;NgR%Dgy zZ)am3-{o_rS2=U`?3TBdOsX?B|1-Y0e z-pZ-blhKS44quc;%bmA(_x4ct^%p6UZoGMWEu5nlFx5Eh=}uOp&+Y@iNSrt)D5?&8 z>Im>SgMMr`gWRydOZhUM!-#)yoBzdpKeIvm!0!>KPRV!&6UZKJqYcZZ^O|3sVwfrOcf)_aQgJbnQyyyK-?(j)d}$T7Z)?@PW`-qnfbF| zgTW{p1pPO`<5$=DQQBe~iw@U6JMCYtoHSg-E509gQE|rG+gnFNf0hC`K~&r`Co^~K zxE~-nS(j~h@K;V=6c)ymmR5-UgDm;Cr~!kKDKOXrlIW-#(8J_=bbtqjw(-~BR3BAb z+Gh_e1&sF@kWR=D5fVn*?4il{%Vmv`@j~8a@oJR}kU)-Zaqb!O|ILO5@gCP!y6+X9 z`Oc8*<2ih)iNs(X23-W2`fuO%Gb$Sx@$vC-w4L1~p^QuC)z&@&+2rT{yM9jLvf_mm zQ0dH`{7zPTdOX}p#mRWCGZ8i060w-Pd(M#))O5?Jssz^r`?hY$^pnte2tJ0Y{XJyY zs%FaMy&D}871xzMT4Vzm)%%0x3(+}GKMD`WSeE6=dE8`PexLs4ZniqymSm8p_rc)m zURR@Qis@tpqemm2B;_y44h&uir0oA7hMqosig*rel=D=0a~fnVO-+?;uwxcy)is7Mrl zbGyI14mNWdSM*0|d!OH!>h?{EnkJ|==bjeNe*pDQq&0j?yucqL-l6$h=&)l6F`Xe#=74zg;Bss_$ZQ==06E=i9Pd11y{6k3%qW5RTr}!pK38l#smFBn zS=)zDwwuy2hD{;HPHv#=8!R;INp}Wl7u^kl!hJv|(XYYFSTv|8gbV>3Aga^V(rOn{ zlEKHehteh=)NRaeyRX&|#cbpPwtg!V^uUF{jMr@=IH$ubeJJK1wEQm^_7S3HDce+U0J;r!*d0R&%;d^-# z+hW}ZJshSxiCTizTf{D_@SC|qVWIhwDP>|n=dPrqBN<7bpU%!WeDBd*Q`RMED;@cyKrv$<78uTa`{ zYKNfVBj$xG`*1U%ZQU%zC+v6K>8da?$oL2)VH2-?sHIdlTX9Q~IYZfAyC-Br+tDmE z_SuHdXPfALfA&`Z#bL)P5~;T{HLdmNnuI9*p{?nl{D~$8ORaLe-@F;jfE#J-d}f5= znJ+(<sX}r#_EBunG3CQg70W|^d&cq<*s$US;?QWWudK|bscx-Znc;@Rj zPXd7mCY5JB*-#I%<js}(fm_ao?9wK{MO6!gn!kXqZd9yR<$kT zR1D{?hkie`DciWKRs`5j%GDGB^`}P0=P~o$P)J?bqxOtubmo&dQ967Yo>TH3haQ_h z_T0mgV-BaC#7_7jUevr$+c{iUlgxU>(C~tZ5_#r~g8G8y2OVd&X{*LQgm_grS9efY zU53yX&g}G7O-?u(-wWSllY<`|t+9Q>wbj~cy0kIzVI}Nxg6O8? z$iub$R-I!_(cJpgJ*N^ZdJ4a~Ml~Uxrk??^w;Ut5v!=)14JYjK;=0O_2Bp2dCiLHA9Fh-h?YnFUAWkhfKhXi5)!gjil6W z{xq27XnZ$Oq5{;8o4EXH)EZgOP^FEyv_|cnQ5X9UdLA%gH%{=E-?qVH(SIGuCZMC2 z)^Jov3B#?TbSeeTrCL8O3qe!dqgaW_jN{97*gF+Bg1bL^Rzr{WTkXW!n4p=qI4>IX zo-DO)Zf<7$)H30*d58f zfCexr_(c*v{1#_OA z$&bsGgP{cX_fT&2hRrthoRa4QXO^Si3dUw1a>FV-2|6+k11YX?zY-YhaI5ySwe zql4}26##;lt~fhv?QLzIPPnAP_*cU97xLqP|1B8|?euCN4kO$#(ZW*tIW$zKFJzlc zR`oz3*|n2nD()k1^(_;%qoSC?;30zQo32Fy_ZlaivES6$$m?2rIoawiA8r?Na~n2k zR#+P?yB9a=dsI{yk96b9R<@|POX*WLZOjbWFV~p~X3Fi5-faC4bfm-!O&y7MAQmoa zBU|Ahse^R9vB2d*qe56DNH0!GHpq3lZnnTWp8W5C_*6K2XF4hfx?I8ym4Jq>+9l9> z4oS$ACtfay_a%_0AdBMH=R+_%6#O^?30n@@O}6q^>1f`Ug+tK_rV-W#;d+(+%ml}S^lfu8NLp$KQ4#jL_={sJf@ z0WpVV3e4u5vUclXK5Ez8uzomuw&ODmlwP#BPVFwaUjpYSbAXfDvXuqzQ*QHF6%eHi z+kO@A2wO9Luq_0A6&^+OfU$Tc+>wEqC|uM9xQVQ9Y}Dj94{sF!TszSbBZ6M?^z`Ij zSd<*_#3PuxdQ3NS^O&b%w4jy0SbMjESn-OHr|yF-rpv%lE$6VK7%c;ww|uT1$e^jGY4eR`JrYOkmhc5Hp^yWh~I`!^HyC4gj}#5 zb;W+J`NDzvu5M`<7j9Q{K+)UCidl1A%evkJk-!SkwCr&qg}by$=Ny$wPWxIfmlg?> zV*;uoP0=J{rcT-#b!3RQI#9oK<(vIshPkw^3-?m6e`i0%NX8&EFiHx6w^abE9vAd6y>yc!`#nx=NzqN11rUNU1LGN1! z%otU>l4wHde;4LYXgv5O5M+=md0w1JUfLo@lL4Sa1OM8I=)xrHy6v_{Qh>13wvwyz z&O|;q_qS8DODt(y}fuFV8*T*$iD_pipgC7t>B@>gAPXUX$=V4{FTD7n! z)suQZX>NaK!z_7L4X5w#AO9b3?-|#`*7XesQLzDHL!~Kr#6nR~Is^-%qM#J%L_|P3 zp@bGPC<+SFEEH*?U?h}?5IO-wh)7d9p@fcv-U&&_yMyPtZja}FpWpN8$w!#kvu3Tm z`hV@IigqT_tkt1w!}P@;ziG$zvgm~TP5!L{2Cr&`_6uG?m>Y@(rS1je-?K=&*0k7` z#Z+M?d&9dKfoMz%)YJ0$;R)4T`rUP z^>g|T%G_|*J0NrZW&s3V*L!QLCp44|!bR8p%Q|%l5ia2sg1AmB_g3EI0i^o`}U#Q1e~}=di_Cx!S4D( zd}o|4h*>>u2b+vMLR?RP$TASEWcSf^+1YGv~_m~H@UX9=Hk?Juta zrcUk9yh(+lQz`Fm63LZ2KDCc}2kHGOS=YX}IO>i_CX>NR7>X$>cM+deIflvo^Z_ zcuxvHdm{jciSu2a(m(bJ7NPm95d)mzXeOw?2LX5#`oRAEUSD3|`-N|Kg^sgJF zeJptsp3yM5iON7f`71motF@*+cIB2=g&dU0>5`IUnUl8W<608Y(^UqGvn%f>1-`FX z;q9kw<>u2pUqnI0;}PO70M_+J&NG%x8sO_0c)G^u6vi{U?{$Q`v!yzCXV61@_?JBQ@S1Drx)rx1J1Y{um23VxgdmLfGiZ zZZP!z%Kc=v5Jj=Bl6K-!C7q#=bX)L8v>O;nhR!mguXJ$2-_qQQ2bh+}qo$U{L>&y*B!KEpL6( zNUfJKaTC41O1joY2sc=|3CjPwfttDAK<&`d01Z@IE`O}9@8&ng0Hv`t-2}>aiUn&M$PDFlNBz!@Tnh;s8cPm3%IsGeRdvbkwc95phE-HK14xuNi;>KvZg$=qctKT?NJ!aXp%fAwJEZP?E`8CVcz<}GS(gxNevpg9j} zv#Z^7=UhoCu78tP8NI9JW$6yzuFU+~74%m{cn*KU?BA7zysBH)Y-fE4&Uq@;#J}p} z#6!eewZo=grC-7U<|iB!w5C@;3kNT42z~SigB$7~+)a&$*g2_^K?Q%u!dVQNHUp% zt{2q`oBb8shEH9`20sgaBgofJcF|bkQ5Ec=6omozVdkWobH)}bP~t3aiHcL1yDs2Z z2+Ih}*9XPo=`Z<9sDmx$sOpMwzwf?{w$1*&Gt24!G`udq3~x-g46Le*^HB*X0pxu* z^fL6iy#FtWJ;Z$t-FrO%BvR3P?g(>r9KS#tO{%@)0QyU3?|(mZUf^wf zmns5aNieAX9|t`l^T(rC%w%)+N*cdh0w@VE9&_dwCsYAGG+NfFdnG1Dxei(S#^n&0 z17+8)P6L|_6jbeYrp0hdKiY#bl9RpzKdCZjTWL>=GNsFtU=g#gFzAyrZS(0?E{2~R zkd7TURKiUAomG4-Dcd%@{@%b(( zjFz;vz#T7h{&3a`{Xn2M%#hnT9rV|FZ9xI&lkO3WDy6d2_*-vat8KZ8SK06J8?eIi zzpQXw5}Z{7is!dQMd69&jCKfd?2z)Y64C4YQQNv6Q64paw(1Xbi4HhLDnbr!21e;A zXmhH895%9|4o9q{ikp@4#L)IxLe;lDR}b==B&S%!AB3sWSDOQ811NsIr-&=1 z>fDV7Oc&{*l`BWXWlxnPUX-KDpA&Sq0nxSn_DZidXH)0Ov)bGfA8v5o`Ku&>twhoo zNcGLCAlwDsi*NDchGm2`r+*9ufVZ9}H=X0aJ12)KHhJ!YMFL^Jd2mXmv~+6q)!P zdx~DE(kEnItN7;a*b&4lqQ!(jNkS_@@fIkO-MjJuL(Kg`A~l0)k4&k%MJGVO3361t zLvKlJndV0S>OUNz3{Y^2)Ct5?3Uc`z?)h1#YFQI}1j_E~`_2ATeOG5m5v%=;2?6rJ z1~XRL%;_t1lUCVhB4<+(O$A6zjf2&NGB9XJOG}WH0}gjM*DY)|ycSG83%mWfD{Z~C z`R68*{d*HV3l1&f9o&h) zq&oE!EkbwaTs(SK{;)g^u|TO>9hZ;{W#uWp=%D$W>4{3(k~vYmZPvpMR?t7K%{?fd zDQv&Lv6M$C%qF@2@YTU_LWb4;_6*Cx6sv*W03lML3wR}^{A0@fw9jNO z*vbA$J#J}O`MDBA8vP*5P+&UWE>2Kc`>ueX6^fS12iv@Pb1ye9Z`n=|yPP<3Wau7< z^0uxy-@7;TI3^}W0Cd=kbU}x`7*r#!_SuYBZ6Y>JgF=Pfn)NZBEkIwU+{KS1Cnx7x z*B^uqNGO_&?SSFI4l0d{Q4T6{LYMrZ01mJM2a2-m#FhZ(;o*~gt54Pdl+$L38 zVKcSc?RN}{<436rT!PObiyK9?;A_nGD`I|n;&V1=1*e^^viC5K(vsPn>1?&Tlq%kBzULn zfl0AE&No3b8oq>9b71RS#-!Z{v>ekPk;fZ2%k8|B^eeZ+T)6*>g8JKuZ)Q7j)m<=G zBvzD-U^7pO3gi}O0%g6{&Qv2{N)TTCkGp^ao4@}_#o%;t5e8lGPku&6!dw>Kwm+{1 zTgDDLoA!D^Zh0hKtFdke3=yXwZ!7Pq_mYl?ZbIC?Zc@5!I3_*UgQ# zjCfqwvZ0Vo7W8Y}n{DXWbkS3#cQlr`z3k4R$Ha2n>2i{M5r|j)Wayiq1oE{puld+l zfwsb9G|D`0r+h1xTf~O_m&mO6`wRrJ&dibVPBI4c~9;#x#G+1 zW&``Wa)M+b!t?fOdiBk8t@<5YyL#!o9@~jaGn5}Fai&S@?Rs02e^Kp!0JH2I z+3GBev5>!MXmr{pCa;Pm{QZ57%t;u75&qYl>^M0ZKq|40 zmyJ3ewVUIRc0%O!(x#!~1*^d%MPTi1Ve=2AftjjB9jB(% zeXS2vhZ`v?+jbZHCAMQwuq=GG);zkBxaygSzCltA|C-K&h}oGmk;BE25&SnEsODF_ zgblEQ%){3aR-mLLvga!fj7UAVHUus|CL%%&K+mEL769KhL1s9#_tBW7IHwwE;8X;B zrzcO7&J(_eU-KXSwhv&iUzJ&4xmSd^z;Qq8$A5v74!*@JVi+suA7oHL@%}wX!|f|# z#cVdHWijc{vaYFVylhe~=m`XZu?=j4RF&&t*wDbvnT(I7Pxx`$y&v7F&^8MrYCu#|WFTG(JDu zXn_rCQal{e5t19@k130_9GQk>-~8To_%G58`V;CF8NS({J<|m|`n5O%W6;1-=i(8( zX%p>G2u4vpE$^y3+!u5~Jo!lhWuJPPH%>R_PwXh@2-Sf z%o@~i-n8CByASgM^CO@Q&&P(RLGzk7E?wFPlCh`2*QYk5K$gdLb;9iFV@mzpsvOp6BavyJ@+J1>;~1>GAL>WFjZT!(K)%N-TT1lHC* z9tJBAU}^oXgX04zPxlA+Gua33g_p61)!%*9;8e>38KEvXV}d|SO$A95d$835Wenhg z>>c!cCUxdfL>!nvG`#%7_u6-mDG{o-!u*S8r&In8u%PuYUCPOmKU5M&_?=HKu&)&+ zU|8w+p+jnMHg*N`5$h#`-r+R`9VrkdV=5sq&24!>3V_Y;9N3#~@NQYIH1nJm4AvbA ziU01XJdnl3ck8bigBXTa1bU?mv$}k%8kBqwhH@TTePqcjjwFmV=oR^}{y<4^uu8AtW|&+* z^IA_kth}h8r)Tvkiv2|G0pxxw6#IEnJj{u1GgqQD5U|@wT{1vM+s{MSYjw3%)Jfs< zgDW~xptK{nmcWnMd3&4g#ZxpYU$aBi3ew5+T_7(`83IqzZf)Bd_1cA zQjptG7`4Lrk>`$Z`q{m9ubM}|6=S5yDb>o&_Ukpeve08WA*S15J!VT`Qd--gEu&!Y z){RsLo0)<7EA^5uZ0alwQlzxJlrVY@a=w#FyRW950f2qJtS!+sOJ)qojNRt^iF>Fo zBpLG~!v5=yyYo`fl|5f4sf&%%O4B z&e4wP%O%oDQaD2J0P zTEH2wljm@Urx&r`XLgdoX{KZ!3}FXsa!{7RJkE{AhA$n2!A=bO$nX4QjqCBnt!acJH2fXeM$$bPo(RbA7i#N#p`J zRWl>G&Fr;(^G9%iXfn5E4S7(LVBt`ot^M=y)7h(#bQe6yJPEEB#{(;GNBRn=z&3#8+a?t8(fPoG}B86cyPwb>sa;NP-`r{`|~ zG^YN~d`f|K7@NC?C~LJc48s*Q!$a41DsEfe@_@QJKfun%68nB&i@tB)y{6>N34=AB zv8NZ$U4zppysDjg=CN3<=mYyEW7Y8u`(G|BceCOygX+$Y_nfeqbKSj|lyOVQ*hEly zcM>c80qonOw;8-J{C324mB$+%A_iP(TM0+C;tQmY9v#U#7_Z=Z4p!niyp27**4vie zpmeUkzkdNV=e~A5d6B+jBe0r#`%YVwebg{KqAd1OY#N$MvSe3lw96YgGJdS6W<~^Pf(}nlueSz$A@j^i^iQ*pYvzEPqP!fFZ7#1LjqH@CL4FB~~UkPu}b3 zfB6U8*{-mNOq;ID7kCc)4!CaGE-8NFwfolzScHAYM~j!@+QN>#wg*n%-3U<4S_ugm zJAkg|KNi@~=oNR$v;grW<;ygQi)}!>9!~Qlq<;MXnX=npnaO}Oqwu8C(T=MmT~4JY za1^6$v-iWTNnvko1&$tNs$e?D7ud1L7z|djH9tq}o#?${6UUGMn}K=g+)lOFn=d^$ z`po(teFW#ru&TBmzJNNccT7TA$%EQbwr$U`J5pE? z27w(3s^4cMKDUpSK6eQJ0@UeQSLdW6p`F~4<_i)2w2ld|k^ZMDKh4A^yPT8Mje{BZ9Fy z9AZDIr%wtNL`W}9sR!4LB21Mww`ZSRgoL+T;X3vn=yo{9r3d~n6HJ3kC*=GrTN2^u zb4?X^DXjk)5A#%pF_xh+mYG9Ny=;~8e4TFB+8wQ!Q_1oZA1i~~&3z>)L%Ey%89T4+ zSZg?`Qn1jKdh!*q)P3YL?50Ocaox6BetH5yWAcr^8WF&K_R6(VI( z=LR=#O`)f-oehB6yZw*(vmBvo zSpX|Adi%#P8p9%S@^eW3luYJt~=bqRU8{E5v^+xvChp@kR6_*z7 z#_o;I63YF6qs~C3!)5phI%~KwQCFg%V*^1}v7qjGH-jXwC=G`&+4NbZc5)gn>?oyl zpg1@{E5=l5sV!UbKP=XUT`8!-cd44_m96;R9~-?kdh)dl>viMfPH6k9DSbUSX0`OJ|`j9oFare|P`8tDyhHu(a-iQd8-# zFa-CSx3qU6HE#U}{o{9`FO2O@h2Wro$dW1DP!2K25mlQ79B$T~CJ7Qq`>$sScr{%W z-wV+NqR&m(oB(nBGKucyG-jPTEs;>_e)mU-)ZNwC#Vv&M=RGoLAuk#v_n8j}RfR&O z87SC09PXkgEnLzh-ly@jUoA9tyo`K6yM$>FYQOn?Evd)-Qf2~!|c8BO=}M9s z)A7ZZCp(ObHK{D+ozTw6-emUfvU2wRNxS>wFaDD##kVPT~1($>b?1UoH65ZA(hfyWvDmraEejgD_jWYjS9$E zd!lzRRx89t#p^=$a8}%cWYY4?@v0x~LdN!3TVN^lKq;zi>Lr_0=xRb*>YBM`JumIiY*;x6f8S$lq6cu2hK@(%(E zNA`0?D^D}#_mB`?$jSZ2#RhDq{>@X=3mY?U`$6|Rc(CNV)&S{MBjf~k(4yotv?N@m zP^OgKrUen2r##z)tIV+zP5J2V8yaV=w~kEec{tP}XBpdxsq%xb1?)%-=#EtS-u4m~3!t-MstRjnu7y=0 zt!-xS3TXYKcBF4)f#1oF>sc(n80O%BjH{0Z}4a6RUZrYv67(-v}-^95TtPzh!Wq&D-mWaZzlmxJYRd3bx zJ+8S*=9;Y0zg4rGXemL@S_EMDnlBA4oL^EX`g;V_Gs$7edB;{*L z1<@)`D9`HM{Tkl)dW8I8^3^dy!)?l) z)`7PqZ6My(rFEsZhLb-!$J$#a{@Rd=j;@SvNC{F-Z3z@R@Md>dy3joCcOe+4tTAds z2*g>!UGSyY6Oigk>@t5Bi=nhn9H&AZhLo|$6V9gQqb7(GM=5BwG1iI;GXI3AC7f<7 zfLxKimB6;7QOjN9zs;T?WxCZX*<`m>kMDUQUGfq2EdAg=UdMNI5Wj~@*cY9>bQE!N zpOI^1wfYNq;mEbBz%IPO1S5SC3VDZD%Dt7+$Bj2PR@$%Inmx+)6MU;xR+vfmXJNK8x5?czL4R6Xo7(4#o9+* z;S~2lX6Urkko4)biz1JT@WIpa4OKRR!(`v0XaqRWV_F$*T2~b@Cw^8 zQz7>Jq$E!dimQ*}8}=vNLb9Y~X-g=-T1IkTlL=%1L`RvA&Qsb;XU=^g z4%<(8XF%XhJ+29(Ck|Bcz-dO5xxexmc?Ch5s?gqT)IA#%ZO0C%|M{W`!sh5^&Z^6l?)T2L_GP${sSC4|{Ro++ zbHq8qz*_;RIUl{GpU>|Jr`fTCm;3(|P{K@G5aJl|)7nwL+V}?F38U!si1Ic&oEv}w zPWpW+n2cPj<%>sFNrox&TIBur_WmcEC? zH^K&ZZWDGjYT9Q&I@Ep(@ErOY0e{P>5}G@I989SIs;QL!zTfoR1iyum>%^OvVkY5xNoyMO>3?!;Vev_=93Ntuhpnzek&z~P#wWR)h(C)`$sh{Lr`biY~; zUBIzxRLtM^Ckw*NOID}gEt|Q3>fBI}XH;DsU6f41bl#|%;$lFb0MQ_Q@sR5-i9t8K z@~e&RX52we>*CpaUMsVO3AlOmkf_S%N3qsEQDtXG*kmjPdNTS`2Mn<09r<;hpQE~% zAKA-b%S7l4qrsX^H1Lj9aw<3NL3px9p*l_;IRYP9=gDrH*JF`x_|kM)`42cdPalXP zB$=jB4hNJHwf@Tah5H3t=+ItQU41piQvO8?N$>pZL$Eh^BxK{jFUA@vOqpZ1z9m!CSJD z*BWKm;dBE8pZWTNr6d9*oH<*6{A%s9iY<^8i_2|*2X@O|ZLjk8CYeu$_|>9$ zQz)BoCp&G`98QOZ_PB|KB6zx%_ zseTZi!cTTw^-F&Dqn?;knPbK3mV0ugagHgSl~b{s*7pX)5Vf-=&o@80qMg0AO~iC;77e&5lvNRcm^?GtjDr$>_-Pj3py!5NPNS!> zOqyVPW5-qJ@;2?8+jB=>2pPatMz$hj^_AYyqfl{6wnCvqzY0&6Z8I9i>zDSb&eu8n zNV54CWIxkqP<)-9YiHw5X>w{-&3yMGmU6asyoDuc3-n>nV0E|%tFcRXmcK&J;~uv# z!!Fu3+DeFHr?U1h5}i+D2EJ%CmM&$H4<+Li1^@MMN1bWV5Nj4OegVi-mr_}YXLY9Q?f9Lnd7+7kenrHvlGl;)s?K;yAk5 zt&u)5x_-d{2s`C`mE%&1c z7(#8Xb~@_vvgD-fWV7PyZm3_JUZPe?(P4&M(axa--Ck|6q}>7UpZV{L8GWSNBV1~- z&S`7vDOm4zLAM5)tE%A#oL2gOB>G>09!;B1TQ)+2(N9-C-;NHFRFY6SJDmUD*)Xe{>2k9=2&AmI@E_q_{`6DAci{{GDmqs+=lwt&VLb- zGHpKPr+uq2UvF7d6kxJew(d4B9!VtGaR%e4H~-C&t4vkEwf3}86Pzz!)e5NBuB;CYp~+z`8E zfvwu5LxE&V?xDpp?;;cng<0k*-b-guQ^>17MiRnHO-19 z^LxA-$(DrqKFh`rJ^2Zw1V8n6Om1Ff-D1bH3ePFK3%b_IjuGdXNiA)>t$v{{TZ8p691H}aA%@>(ARDQ0#XJ!mSgKCdZ-Ko!9 zWnF7Ov7wGUBh?gg7OE@~U}Ho~eKQ^?vxD_md??b9QlP6D93(TAk0Aqibr9P3EXzej^c;+PfC~cuaOYQn-A)@KG-3Ko)O?I@8M4sB zGz`rOx{V8MhQn8L?@oDBy0=U?t@b3@EhpT@7ci5fNJgHO^#Y9#NSf`u9`9ESx4__Q zv8};!jgTDV$aE3Zf_bex9SvLYY|rg@s+y@7(K6OD-ZB|VHM?{#CjL_^N{OW);-II` zb>`Vjh4T)WLP4W*T(9ei6OC3?;ph@n*UfXV42tSsl+p=$aB37r+mX58nnKUT-3BZV z=IGOa7hao`W%Tkc|J*)0*LC^Zjr=uywPm~)R6!tg1-|9D!^}JGT^=bO9&GuTyahxl>v?W#{CB4LJwGWJ1;4W93$rT z23ECtvXa}Pj2>^)%oUJxGR?`bSY;Tg0UwyT$ z7KpEpsg{!Y1#+%w74@~RCyq!+D}eknu4~V-D$}P{_9)1DX4iP33?!T^xj1EIlBMyL zH41fY2<`G>d6gh2tb2pd!4L-Pr;OC`qmtQrgKP2Ms&~XK`}K)QhCe9LR*sg2Jl<8i z7Q<#tx>z3!f!&s9Ag+cDTs-NALn?JKXGriKPsMZir=$ei_pt(Vtb!Ze9%W*|fA!L) zsKh~%T%x9U7MDVkpRxW)rEKRL$6?Q+Q7a$@CuC4YXKF%WC!nx!v?m%QlX6-3y5TY@ z?8?Zgzm3LZ;`bT!EZ(p6i^eq0g+|Pi;7Td@$u54hh9!FRwV!~~Y)|#_?(qZIMv%-z z(cKPgA;O_8%LDX}OPk~J>dc2Q=;&GVSyh+P3vDtO$THngpz%dJX)xz{+{G}ZoMGgQ zI9;f>VDjPkw4=l*XiLJowF*efbz8+-K#WJG_6Skl}&O3 z+a~FP*`*U-?rIX#W4uS?_Qs)oiwCe=BCT5zMg8u z4LxK#eK9=EQGjb3Y%4j2yTY$<>N}=PIlH4@9Byx#lQmngtn#B>mKUZi&w3427d;l2 zG-u3U;m+Njgmdk-%32b*h>sy2&WQW{)w~uTuix62#QQ*~{lYz!ii6|LJ7+idlZ|!Ujv>%^k809x%Z#ychRvmgQFuu>&>&NBn)iixL!Xa;Q zsY%@*rcdv|u_`?%*;stn&1*AVplcft_*Ifs#ShfTm=?+hD<^?or*7;lz$(caEX%CcjX;3Hw^Nt<8D) zU8!-K4CjNpM@#p63&VCP>U%Q-NpXl-}%*cr}9 ztIua2UfGuC-o~jeVj-+9C?jsvKDDQBKTIcnR`|;zKV?B}&@c7PwWQsfIzY(Y>RC^G zutBYDHa2zHYU$`WZPQ3W7INE6arC6gh~Mzyt((=K_j)_-S>A&dQz*9l+lWKJiXXC+ zgswk1BlR&cJHp6f%x_D~QOVhcYQxwpn~V`v#^Fc9`(c=&-AT3wd|kUX`J>x+=FOVU zjoa@w=X+iS-!}{nylJ@;+b3B{@h&44&#FX*Rg`G8rNXc8oo*CyY3h?%<2#ONoX8#J z2(^0nX#u%%aj&J{szw?L3!8_SHO%?gjuO>}jSIe0@%B>=xqp3qHokcq%sYO;xXjco za`Xj8q*~X%IW_^AhgPkP+!>iC=A9ODUwk#!PRc`J(YuqPy7SVd%dh7&mp(h9$dixJ zGq?2Xxq(ksI>&Ly;vlv!?_^z+_cUGLF&~Sy=0wkv)QOlxniM0R3Lb-C_o{^-=t=C- z^$x4M6H;pnJSJEeYV<&Y1Ur?~Q+?bnqjCGS4f5O0t~I9R=2~pNv?hPL z+wwhEZNMgf+vI)2XYW|gR%xp5j6XvU_49r+y~^oS?zK7JjL)1;!zmkKAUjC?+^B^7D=oQWh0&!OvN~ zz$-QBzz^GXAzO;|j>~myt5ZmY3D`w%W;N1LP)FYn?(X>QOlIA^>TvykGl2UF4 z1vRQ7Fy2U{f`b<+jtmZoGW5n>Q>kUEN?=c>70~>`@2{zGK{>);!y$J!q$X9Waoj0J zXj?CdQSe9wUf5Pe8qra1_si*%m@R?JQe}<>Y~wl-MRJqYN#f#*_9kVp5nVeEd)c4@c{2oMX=lgz;0omv<=t^u`oL_*^}#bTm+!`bNB}(|hF$rjny3RtNxmL;$*tM$ z^$aV5eLUh~+J~qtn|NrT_(83ROR%)#Tcou)2!LM6QJn06 zV;3l=?-8dnpI;fqr{O!Hw_?Ztj zluKtY7pqancq~_1WMRsK&#N7QV>@&5p-X6|0Ox^_oU^cJIifKM&-25p$H%MQz7mZlIO*J(q({Wb2QxQ!1I;gLc8Hn;!w4 zKdpBTrYFP%LEmc~*prRuI=!%*s;y58cpvPnIMn{yQo_h4pdOcfbUQ3ui5^Ur)dly| zeXh=tv2caz@E5MeyXQoP@S}HB!G~df3kd(f;wBO zI;F;O=&*T_)VGO?T3y2n<10Z2FZ{@@2Y;QmA(kHB?4J*BF%&)qI|3+b$^N;oi_u4$ zE11pzY_f=wD0_*}l(xp>UJG1a>GQPPpcZ^2Yx*T#!+Pv=a~O-H>)%yB(D%~B$(|p@ z4M604o=|WUs+~J!Cb-?;^I6yszh9?${BZQQykpaCbo}S-cE(V7bFK~Rj{vBm8k*oN7I9s+Npba19JuflO1onQxQgAp={yn!i z?1=k7u0d9Nbht0_y@lZN9PCsWtM2;sX_AOpOifH8N+})|0q_0Dc){es_awomW;DMb zg_7?v-Wj`pgzW^@331r^{b33RZBpz2YyfKj(eGONqVcKAJyw*;gN|LyTMp}H?s8gX zttv`wCts{qoxU)v#A`KGj^(U+C=GXa-Og==ZJjLmsgqaZ?1&_f}KKfF)RGSK>iv*m^1yP?Bs=}8b)dJ*+ zz5EK=_^xQywo{Ffvbn>mL0YQm$l#M*ZwsFCTZ(AGo|LHO)!@~384A1Cjrn2K=QW>Z z_joMBTkq$WUkEviITw4!^%}>W9cOGS@s=a`;qI^}43}+_$1ZqF`U<(a4+fcgNjEjO zT>cz6)r>RlTz5Io9?-%sSBXFz{ozuK*YcCJ-o7B|Ejg$z7Jq1L)y7ND9x%RdoTTw_ zlX=8pmoA*BJ|^{|JO(q9ZL`PTZ&DyM&-OkrV0G4F^7d$wL$3OuQlLo2-J@a@kuhFT zAgFtR{p>)l<0$lK02y<-aao*VHijRYW0w0Fm|12LMbu+;U|;D_t>3A{sd&XEjzc*- zG>6!}DEIvz|O5Bd6-Mm96HWzGvYH~08uY;4vGH!z`TcF+VTbKIQK`*IU zAI~&~;-DOqN6y3FN%?A-cX3UV@FL2~=AFN0J0F$rgw9JB#5VaXlb4B8Re0812i6pQ zvDVL4!z%_ZVl9NBr3(-;26GLyW(Np!^7Hkqyu0($n6ld&`Cgy77|4N)tAi#%bBnA8>c&_ za;N(Ls|=@|M&NfIyofTy&2j}x=J%v6`IU$7n+@8W$U7Ib>oe|!bP*AW*l?aK#Ln@o z+D60UzSW%}Ctzsxgs%M>v;aQKec|fz>c^^fjC5FIRe0}rvp^fMJwL2n{a7>xB7IC? z=xj0V3adSnGQQ<=Q#+s4dy(;Ggk@QOg^d98NOb2ga?8-lreR%aTjtc^_#+<&Tkc^( z6FnF!TXLqFP-%~H;-iXNnx@lw0=uIlWToc)8s_cgbi=3HR|>w0Y_*Lm6DdJi)8`99 zr%UIUVmBR@L%xYwfavXn4iwFV>_dP^6Ar+U17GN#Dt-L;A#yZ!21Wn*Kd0SYas zi{~21ar=VYC~3$Pk|)(PB*6aM*iLOTPg=lNgen(mZ&Tnj2Qx>{FKJ zks=bLo3>TTv%@2a?ATm6HrpANr<&Q*KgG2+AZ^8gfqQsq>VpbI0%V8;-*xa_T%{O`H6vNOGOPtmPB z2)akD5`=<=y9PzX&_Tv*AWeC0Z!KWPSPn<`>5tbz_@xZN)4=akT29h2M6 zU-A6&ViJrkHCK{uE0aMJ;fhbktPM+UQP#s#b1!(S07ENKIPs!I1tsFS4RPeSe^oto zbP&2Wy3YA}5Jgl}CgvTWq&2aIOA<8m{w%2q9fxOsqc-Z;f_ioWyjB!OCbM zsukU*)7mTJHIr@z3d{tVB4^ut+oZP0h@#i4uN8|^sr&d2{FNc!d|jr@rz(LLG{njQ zga29Jg+NECz}%Zu$x6<8PjpJlQ@?ci06aeTi%uhoUgf9n;qotf!diZWNYWbMItKZA zoq(c<+1*F47g)TL%%|z9#-!Oz@Bgh1yIzKM=?29cWet@ZW(oV)cf86EO*++I2#OE$ z5CN^G+uD2T&zG=W{9a>2)DpU((xYP*^5mJT>Nk(=QpB+btY}^b9GUqqj!Npt$>HhC zKcaBPm+n3LxQL!FTWn;v?Hv7eTc>3ITtDrVPi#@_Y9W;R8gR)ne&)d|)!90LGPou1 zQ;*>Luf~sEiq=}+wIp;@QDJH@xFewMjML{wCha{xP_W=6gOHGCHlh)6Pz_)q?ASK)7>^ci@BQ4YZ|IB(1< z|Li+Wrc5O`E-4g@Q1(gcXFs_T{&7;!4XUF(Jp9IP`A^#Ir52|^mc1yt=&X4TtZ(U1 zdtFVN>-MD7pNzke+K}3tI^~%5Q2mn6OUZ(xZ{R!Ll2!iPF$YtL> z1N%WcP1gi>n~(1RUoSZOpkSQe+&x)-S&E2 z4AhaK38@1=@o@n3mPF`Z#Z~8nOoV-ivhsgl%VR*jLDiyaQ@@v-+RGQ&AmclB3ly-s zLbybHS3_uOnZK4)%oKL*J|KO|S;td$T$_(C_5s?Ou`;VoWL*^T9V+5VT=P`D|DPrG zsn@8Q)a%sx5*Sz#IzY-u<#31I=qUWohjBJ9Rd_8ur ziY-6-(&K#?MvJBsT1BMp*QNSyX(DN2Y2s-o=P&4jYKsU%6r9cu_DFwak|8bGe%g=0 znyWDTb@3~o$fC&ANX1V^+F;TB(A}&iCfjRJFH;y(V(Uinqroovb83VFU!G>v z0(b@cMq!waPJ3QI#)qsn}=0P2d4ytd`f*n_kI!5pA!+aR8bRYf1_i zfv>fCO456|c$C&MM(3n?f2||L)M=DtCN8V0`DrAyXDowniLUk;B;`q!I!77V(C6D_ zd#Q5mz4Vxz4Zm*C$OdUc2TP~y7U@>wxp76P)!@>l_g}fLg@ezkbR+nU9BQ~c$@Bg} zPQF8%@OuHS%@J$}#ce67@3h{TzjJzrv2R0)ii+yiJERJ{GB?Gfl@N!NqSxxB=)$Yt zfetPtF}7d%^(>`PwoWpfH77SWSh3WN2xDU6W?aN zCBAKV+iTZm)`DoU0Bg%Uv(ogGD1Ec=D%J+moD^lgYs9+Ab&K~F|1FVQ;!mGES=i_-&Ajq~xY6@OPtL-lrul1lX}{csv@cNfzV(;D z$*55yvAfa?vLIf4rt5_d)C1ImQ~|0WRS0|?^8e)z`hWSb|K(Z#f6tGU8hDW^{!%J| za_X8qZo!xK?eI+J&C;x@IRVA;gULP_k3TKaT=A~%W3sewX@~qoXv*GrgBxx2$Bss0 zln|YP^%oZ7ZN540Kf3+pT9&&`9_I3AjHmI~v}V$Y(zFGvg%|y^R4UgHbP^j`cyZAP zeXi`h_5Pqhs!WYahWo3O9>Y5|H&vD!hvcqnR7acZ7HMGup0IxlvL-M0rJl4I*VxW7 z^Epp+-QpmCYhSEj1oF&O-c)J(RByHIW5~t5=GV4B(kjvjg^kvdOph_unj+rE)Y8PL zn_eljvL>0iSB~W4rHTss~ZW=jzziR8P6~QuEcwwfDMJnW@@C z|6fgC9tc(c{eQ>SSjy7a_Y|RI$yT;Tix5*t$U0@qn(W3MjHSpB*`ujMlTgD*cF8Eq z6puYywq%*?3}e3cc|O12{rk?Hd(ZoQw%6;N_fan4M_Lt9P1ql)M-}q+H(qp+V3?H} zDCpHUEtGl`qx9$Bt&6|bxR>3CqR;Jf{3qrG$IFqx&cpB0W!Db(X#ls6Thk_%x@WcH z|GYm8D8JSOQe3Zz0X?87aXUgs&pU zGwfTW!tf|{S5EEp6X)WgWM0VGm}q{C9JA}~>x z>xJVsuZ%}JfxJ(?$x zhWkUZ*JvVqRU>?QW?)$T3WCJu^0FC_F>fOAWD6Mo2b#Q9Tg44<>X-YM;}0ws&KTc&zdD6-TvYZAz2f=o{T0>wehW<* zS|B1SU*4BP&X9el5ha*k5Mow|s0p|AY#4Wjn5#A^(~&^$;e@N_CpDKH=r!Nz651x> zPj4fLs%}l+C;VQ1kKp=Cc{2J!IgyL&`&;B%4&QP8BCxgw4cwg}79~f7s zZG!ifjZY}rvq9x{6lP&>bG~;oLyPr&D7YYAES`PPgc=KF&iNvU9tw-oL0uk>b1)fd zqHln`C*2@$Vm9A5XDVwv;pe~#!iDP|T^@6mJiGqVzvMgKkJn}*Qgfr`E;9@SNved6 zG?XCq{o$g|g#^u8$AOt(wax^p8d=}4N@!P9rz0L@HNho)`loFPR4SWVdHhWeGb`5Wx>p6zIi`Ep6zi4k{M|fc41Gtje}Ef zp7aD_&sN0fhUhI>x|Hi}uDu~aLNMq8K57ZB1JY3Gn7`89qb~*%m0TY2ZK-m+8jV)@ zXIrk^E@!1?LykW00!E3v1^IB22)gBPF)wN2uu#=3j}Y(yUpe*;n(EY3zr8TOXHkxtjON3C{RlZ<=;{*%S>m#dZ7xRFJoxf`7!|OI z5)`|gm@Iuw=J`1t>r2L`*4BZvSRCpXo{%?op>*p_Z)g5;i-G41IkCLg}l}6k$B< zN|)vak(*9@LhY2@=--LWv4b6&1!T^CE%>i6D%|*3m)6U}7;h4)`=6Qh;~mF0Qe00@ zacs|$Jhl)O_}EO!Y%LE5-c086J^8lhVNNHkiwwaSN&F=dN>^@w-r`PI)?ClEHHaZ1 zMEHywBlX?ee58q#@XBHLMwBDvQAkO`fmIpAf3O*B1R!XG@)(<==Qoy;I|El&K_n|axTtPZ*C5FIc7_q)k znTFGMh1_7%x(7?Tr>z;!0{vD|4HOVpTJN!ObjP%GCDgODos~Xak+=BUa*PX;kBKt` zR_%u=BH0Qjd^uvNDYX2nx%Ev^ewGOXM$ew-QCx~<@Te61vX|gryQ9BZCY%GEQ;OJK z^%s8|rZc_Er+IDmS7XUh(Pe1o^|!UQN{oHVU;@V z;M(=hOUEv$Fz*Lg?tFIMx{{ZVv`OmDe^+vg@cp?=533QgPfcnHD#U!*pQA{eZB%)*Ju3l%2;EMa&>4t31-9oEOJvn(c|-o zR0~IVZr3&n32rFRg|`)OQwE4e#d`VHfj8(5pQX+Xa4 zM}D#t2>6@Vdn_MFTc-Sy2s<+j!rSFo*|CQmNkQk!4TPcW-wJSV_h7%_{K9F7)oU~ z){IOIQP3K^uEW2fiuR(S1e?QQ?13RHvfLVCbDj<1k08sUi_ug_IHk4(jsGR}-pEmF zp)cokvhkg=(!2KU`12{U$Pzzr!~6>+iRT31Lvb+H9F3b)$2>;FBf@n9Z*pzNEgEYs zo={*!p@-Vcn?$eXb{R}*xw9X+$O9Ru8!9~gkr#5#E3UU^vzf!rfDN_gs2-n+o#=T! z!0BlPPyZsQW%n;rS{ibb+aJmLPCmQYrhc#9H^eQ(f(4^VfioVc{j!?>U~2hNzt>b*34 z#z&;=T?}Gmb>DSA;U_iXtdM3H_9KB(kh%MA&R6XgT1$!$U@_9P22@J!w_s@}+E?nH zY<7jbq8&XckTXAx_)CG2sUJT}%k9KBBZ=&8nhsXVkrS9JR ze}BwL6F@FO$S5z|(G~ct(kkU5WL_70HlEVyY`Nm{C71^g{yI>z*L5E~1o6;_F1E^o z#@wx(bHk9MlvHrq8V^&9KSlM=k^aPh0PW0FK8wbNUiKwoZZ z`l9h-*L#S0*U8YBeT{lHZor3gSTf%Dk|_fE@Xo#ICRgCRrF{;;+5%XcCEDvI68`A6 z4U?d?Bu6&xui|{$7%9yU)$XZ|uwyrB9F40d_`e05FVf$Wk8!*6%Xs*GKiz$Fre4C#l%#2F(emB>KTmwK4F%0RlJ?2z2HICX;+O2kA)$` zL)qU-yNCac4vzKOt;N2{I0Xt3S(%J=arHGIO(v!_E)8o{nDezI<SlHo4e%bap0AR^LcAhkQ+)%6E^v{u=cRGOXKNi_0(8dE`|Lbi{+`*ZVVc)heKJEJ&VDW=N7pYWtgy zXv+Ww8wElwl_D%$vaNMJ;bJK6A0WrI%PMUgX2{KP(O zXSj{{*5aEO(OiW)2ld65l+Pzd?A2RdF%bCUE4p|HjHRCTr}CH}721UiijG-NkG2rN zeJL&|;b@+3B~NP&8+7ik#?d6V)5@SNL2mcOJk zEI==>>n%E8eL1lI?FvVp_+cY3#8u`j&u6%kEnM(q3fWbx)o}C0!5u^4uLNDPVd?mVA2MHF3MB=eL+Q z5aw0tPWc4q2^U4aq&OhMKAy6s85^*sqmt$|Ps1jC31a^6cO(2v8UC8?9k5k>`XU#a zbrFh%`5p(T-rYo1-xU&8l}l;NqH>(jamzyNg-HSJtAF;oXEU!DZzl3hHn@S`C`Vgm z$IVy*cW#xcWTzo3O>cEfn|n8Sb&)JeorGoogd+F)UX-+DvNVgR^lI-+1ACI_JLA%D zRu0D}J2!_Zh|GYEjG~<6yCXo^OOdFPEs{fJquf zoo2;FjeCE7g|hTgZ*V^{o2CpEBqegR_fxkgeqy%^CiA!B4Lgb}!POmB_Ll#t_X*TR zroZR2$5%Rx(ptO{5U<05uuITh-f~Og_b~eMYp(=~gBrhsw=IDy>7P^hq|WH;Ka7pf z)eIDBcX@CQI6m}C-)+myzWzp(K-2%ao*yD^y__kn0}n#G8Ck+C6;+;p}Fu^W%CL@$q7pJ;ioo)zZf3|I7vG{Cq@QeaEU`>^zbd81E2#< zGS0RoE$03>E3zTD^>)>FV7&lNxDhQ%TvoT@hJX0ACsw0&7Ut+my(W%ClbewyuW_b zz59KkLDnIxn#?@@nBwZj>X5VhVH+3Yw48)fxcRQ;M;hwXBmF5($eqj}X$`@Q7D&^c$L!Y9`@&f1yhvHfZ0{%lGieDbWbbc!+CMOO^zj14qnHEE% zag0PDkM@s5dkL68mwxyk7W1mpB}?iOzblNuM}p-j8o3B~;AUZy@(^HkH1>pr-7%4{ zbJpw}BsJ{hWERRd*((2JD!de)SqSnlnpnEJ#K>1-K3wVBGY)Q|YL4<;E@3rtnd7=7 zRN|yQn({q)(C9*Y*P9L$tI_k)SCdT!)+H!P^HMshp`^a%wqZ)2nwtL8&{CL85v+xk zx?Y9CaCF7KI83mdsUt*Sx;*K2o*0by@#PUU2p)-A`FXi$^8Et3Leo*j3(V%gvR%Vb z5ygiRiA+gRA~iRTC&XF6=>@0l?AvAc%&-~o4)jfI zbM;5I>}@?Lg^Jur zSM@RP_B_}MMTo0$+qnm3MJ^2a?vRwtD50N*(L%V`D%X@GJ`wfydIYgKHKeQSrQ{xn zvwNPRrgvsbdW2t~H&u-da^-f_DJM4WwJWx}v6wVwh&qduxB(UubB#-QpL~jYfxAR9 zs6$7Vk}06H)~}&9X~U1s=j1Q%Nd1bu{NH|x0Jz5Gkel3b2pcvYwj2ChgGZ5zT~*yk z&5Xn?oO)fnH<^tfc!`T7kF8uTAt=>*nzfjh9`%^H2=yI#*;Wyr6F$yActbEsOBd2Y zjNxwRb{#+yWzynbq|Sb!+gqD^ms3Aes5m01I<0$*>8$I^M;v4(`$z_SIyq`?tUf;7 zi4^4cQu#F35hvK*LYeYS3?O+Cdcs20bWsx4-o9*aik<{ktZwX(oth z)8im{?&x`uyn8uzc;Mi#$jN~TR-(F+g)AW=8kq;}QnkafBF~Qq8W%Mrjdel&fJBOd%N^Y!82d)3R5H1qs@HQKxp{>Jx!a*ah@jp) zVzcu(1e6?MHnUY^7@m8z$4YSeDLoV#R#2gp9fQgZOy9!IM3qD6uI4!}aaOOPUoWqzVsLWnkQz^YMnH0(%94L@BG zZ?-o9CG@@2T4~>1!%Et^k67Y zYfNt0gmW@=o;qD@*Sf6~hp8WX!~D-rYW$o2Oh%ydaa)5n1!5n^PYfbEEBLh#?nnAH zd*J-FJtBkYGh*0d>f~HL;=SjM+D#X-WsP9WY*ize02uK^({!-Mw#Ud_ag4Z8**j*I ztvL4T!Hr(`v+tu9h+oy29%G&^5V;Y$J_oT41`hO>QL`db-T&gERU6n|Nz-gUFEvIk zf=GQZ<fh(*`@8*hGsxP$-H^5UnAQ8VdI+au|kX85nR zN5Bk{S#mE^kwHVnXPm0Y*Alz5U%c_*PA-$=7oh*DQz^J&a!Ynt;)ZZT;~`&oIJFmb-QQGs=xiemn}QH#lM9MH(>Gh;e8-al7qQrbSm zJEQ#{L6T&$tyTd1#TO6GC?~eydO;?4e^6d+?TVSJ4?9*IrxcbyuDY1E{ZKSC9Ut#4 zH0zKN+?Vmv%)iGr6_%yMuw9TVE`r-M;l7wza`*2=nFNh(ycT9@ez^O{3yY=2pt^I+ z9o4N#temfzR-XhOd#EmEu4GjdSnSttXcL<7$^X5DNw#_(OS7)ApdYB3aaw(VrCIb+ z9T0H&gJ)^$&+0Yk`wThzBWv%IrP{L($~S1PcVdZeilM)8G&{rdd9jlX8#=Fi^d@ir z-pJo# zq13(iFIbLd2d9ym+$+COW7-A-+W9rSZMLSChi$GOpi0TRyQ%?z2_WFy`MJ1>r(e;R zzkD>rX2Urs zUG?GiY&*(#{Q%{72EQmX7j@E_#!KsdS*-9>qOQq`xVj6JAV}9n#_j0KRX=1TB#7Iu9ZlNj#SG6o@%m2m_P&T6 zjIvmeMs7A@w5oDd=oKT8R(&)(<@xp173ckii_I5A{9 z!F!Ho?)4JY&{UvqAx>WnEKaVjf&lBUU^i1kF8$ydvqQFOMyq`uz8Cg0JFF)~bNxgO zD_R!c(siXhb{R|5W~%dcv+nDVGyVk-nKFk4?I{Nd^>~CRAh_ID7(2 zTV;_sp!VqXog+fP4ZU@p?1Vp!d-25Icg8}xwU%@8pfnj5j2J`IBqsjR1)C)3+zzov zEmqBp(@rCU4O`h`zrkIF2V1>`>o5IU{3L5#u^5YTluNxQcqHVoQR~NS(;^kj>s{uK zob2l6eQ9#I&jK@R9AXJ#I9Lqa7xbJyS(aLiMpkb;MLiqP`Iq8qITvdxvtReBHuXD} zqNi)Ev6<7Kb#Mz#Q;0bMQWMi~gG}qvl{(+va>@FddWy%4cILjS^{7k%LHfg3QgN&fY@&=L*u*I&KrVKCUQXtUg3%pai3#4j~^wt}kCQ zg85ig-9KDfv%=PI8ht;zIN|rW}v+0=96Q3l>w)oN6AqUI9|5I#j(buk7TZ^SntA^x=9(7Hu*bma~JcSuk=IsHCu5=_}n zJjh8P8M&C`AN{Zyb(zh`3zRP)_!h8p1>uy22-25_Cc>C~)C_4GS+=VHoYr??QEC`I_W}>Z-0|{*ic865^FbbUdd>$X={<;NO&gYkyldiDS&b%F;rIn% zeglv80iCzGn1sCoVSA6%wYLlP9PRmVhyhF%O-=(>y3Wmn(Chy*y-7UjXDdZc^J$?b z1G#SX^bs_HR~-zO4-!)wX5RC z)X4SNPg_qRSaf4!2y9p!i{gCZpT=FUS4KmjU^X8b0(;%a(fGND*m{XpyIAhlYJ385 zY}>)!TJ>$(=s8J%g%OsBdNy~}=e_btLF+uSgL3Qq_w^J|=hlnbfnTk0~rC2 zJ^ln1S%c2y_6yNhpJ5)V@ri^E)B3PF@w8LqFQ**-7xjo9>q5ERY^SV?yUQ4Iu?i;_ zB;;^ln>Al)@%C@Ng3JFZle>Mw3o85P2f5r^OQuczIs41hR`y&*N)JPwF5&))|MZF8 zN!R&&n!~c-GDR_?t6mN}O%^l`{-jL$jqa_gJ#DlaP= zcDcOfvU9OBt40-^I1+zd?+{s-)o4pCt2WEyV5sZ)3|Zu(0MtX}chVfn%-gLC2UUCr zG{hetK%-vi4{#y7bXxo$1UMhXXdzhW3Fb`J!fhtdbB>LMv#&}3668gmFk&!_1XTP3 z#XzB-eS`yp`(mm!tA{vT%G6=r!CKv^uX8;P%O>E8Nq+8OGlomCb0>hPxy1N zyY#t$zy@M**KwlDNu(j2em?j|cw!K=l9PEa)eHcZ7e><7#*BUA@L?M89nT!EmIM<)eD@Z8GOMurvr#icPeXQ>5t9olz>M4dvJ_&%jv z%@D`z7!R1Bfb{Fr!11K%%$W_kV|v&(c^|&)QV-ZUg9D>zV-}K0_>ns8>{_kkjFXtd z=}}`S7Nf}rKJG$uWr9RXD%9sQH~og7xQn*D2I$BQLu#S0rvg6Dxsfg72~(G{Ep0R& zIuvG6Ul^V-is$U#8hsqMJ8uB_&gBp9j%EjV!el|A!~H&=pg9Ysa`7@Tm$~9qreHPi z3d=xw>lXVvOjPw(OUAxY5aaVO9a?KFo9ii!muKR)a|SQ$w0p(zvnaP}S-g3y;Ta~; zs={*h(NcWg;-O#Dgov&N(q@r&c~KUin()-QdmzaBcub()waoeUpjnFDjUg%6ucr{w z-Ulib-h_t(A1#A)89Xs|p=>~U`7ubD?$7ER9 zTEW}G%0z(n%Ck+;;w|bW#I@RXt3e8A3YcKK4)`tK7YadYqRZHxMLm8PC?r)x?IYw> zKRe{PA0x9&T|K)ImEg@YH7qaEc=eG^>a?KuNqFo-?!1$+ zdAYxLxj5P*a>TqszCL`IWDp~g#OrcAMkkQU39436V{0cUJcr2rd5`V(N(D-z zF&Bd=A7S`3Du^%4g*KKbzlimlD>uQ}j6UX4=n-pTJMAO!04pP8W5j}P_J zBtvB<-=b3;`$R)ola!9H_Nf zHNE`NKw@2lixz3SND|a59=K{FP z^IDypF&geCT>5afPr=dblI68mY*>7&4a`N1N-s$ZXTPi5yjb|1@qX{lHvV9Jk{T%V zitOaM{k1%~vJ5bOw*Dq~_#w)uh+k3~N;X!a=Gs_CoSX2(k7hsnO$K4nU)HG!-4 zbg!^4%yKR`H)mRTYa2pQUp}Wc1*~Cv6q-JGV;oTPn91 z4Np3w9PZawUJ1$R?e~`o*nbruV>257$Vh6U^mY%_kLGlZl&QLxFxJU=BAxy{F`l>94Ipu!K|$rS6`Yc01ud zxWW}SIB-DjZ)1-c6Snmn8wj+y@Cn(v#0_v)h*MHM&ZnAPm|4M8R@;Wsyb@WOiQIM3 zpQr#dNFnp&ows7MyJOBih&Jm$>)$m*r!B>>bE5OoQrwPt_JfOfE8Y&t>8~F}r1Vul zOs+8_w9jQheVO#9uC?|p0@-Nbez&)g>TvhAl?1QX{#%oXz-?i?$viEzYXp zr@njmeG8nwGU{jabrS(J>6bTJzP|Skj2yEWyp6sdKQ2DKZy$^QwqZveJ}F`YR^0Fs$a7d_HK^0L1;I8EQTf$0Dzy|(%Zo3 zYZSER(^ZmMQ`%HZ&Z4yzqvlO!^YWM^$#jd{Hv_AA?+5$LA`aai_GzCx*z3KTdGKA= Qe}8`UlEuYxq)Y7o0o|;`_y7O^ From c2dcb7d5f72ed9db2b28c53e4d648aafc23d5024 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 17 Dec 2024 11:21:22 -0500 Subject: [PATCH 75/94] replace write(std::map) with beginObject/endObject interface --- llvm/docs/Telemetry.rst | 26 ++++++++-- llvm/include/llvm/Telemetry/Telemetry.h | 4 +- llvm/unittests/Telemetry/TelemetryTest.cpp | 60 ++++++++++++++-------- 3 files changed, 64 insertions(+), 26 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index bfb2d5779d1e3..a00dc714ad053 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -123,8 +123,23 @@ To use Telemetry in your tool, you need to provide a concrete implementation of } writeHelper(KeyName, json::Value(std::move(Inner))); } + + void beginObject(StringRef KeyName) override { + Children.push_back(json::Object()); + ChildrenNames.push_back(KeyName.str()); + } + + void endObject() override { + assert(!Children.empty() && !ChildrenNames.empty()); + json::Value Val = json::Value(std::move(Children.back())); + std::string Name = ChildrenNames.back(); + + Children.pop_back(); + ChildrenNames.pop_back(); + writeHelper(Name, std::move(Val)); + } - Error finalize() override { + llvm::Error finalize() override { if (!Started) return createStringError("Serializer not currently in use"); Started = false; @@ -133,11 +148,16 @@ To use Telemetry in your tool, you need to provide a concrete implementation of private: template void writeHelper(StringRef Name, T Value) { - assert(started && "serializer not started"); - Out->try_emplace(Name, Value); + assert(Started && "serializer not started"); + if (Children.empty()) + Out->try_emplace(Name, Value); + else + Children.back().try_emplace(Name, Value); } bool Started = false; std::unique_ptr Out; + std::vector Children; + std::vector ChildrenNames; }; class MyManager : public telemery::Manager { diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 47df315d72fec..269ed94b80f5c 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -32,8 +32,8 @@ class Serializer { 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 write(StringRef KeyName, - const std::map &Value) = 0; + virtual void beginObject(StringRef KeyName) = 0; + virtual void endObject() = 0; virtual Error finalize() = 0; }; diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 16de142b537a7..450f6cbbef1c1 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -74,13 +74,19 @@ class JsonSerializer : public Serializer { writeHelper(KeyName, Value); } - void write(StringRef KeyName, - const std::map &Value) override { - json::Object Inner; - for (auto KV : Value) { - Inner.try_emplace(KV.first, KV.second); - } - writeHelper(KeyName, json::Value(std::move(Inner))); + void beginObject(StringRef KeyName) override { + Children.push_back(json::Object()); + ChildrenNames.push_back(KeyName.str()); + } + + void endObject() override { + assert(!Children.empty() && !ChildrenNames.empty()); + json::Value Val = json::Value(std::move(Children.back())); + std::string Name = ChildrenNames.back(); + + Children.pop_back(); + ChildrenNames.pop_back(); + writeHelper(Name, std::move(Val)); } llvm::Error finalize() override { @@ -93,10 +99,15 @@ class JsonSerializer : public Serializer { private: template void writeHelper(StringRef Name, T Value) { assert(Started && "serializer not started"); - Out->try_emplace(Name, Value); + if (Children.empty()) + Out->try_emplace(Name, Value); + else + Children.back().try_emplace(Name, Value); } bool Started = false; std::unique_ptr Out; + std::vector Children; + std::vector ChildrenNames; }; class StringSerializer : public Serializer { @@ -126,16 +137,22 @@ class StringSerializer : public Serializer { writeHelper(KeyName, Value); } - void write(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)); + void beginObject(StringRef KeyName) override { + Children.push_back(std::string()); + ChildrenNames.push_back(KeyName.str()); + } + + void endObject() override { + assert(!Children.empty() && !ChildrenNames.empty()); + std::string ChildBuff = Children.back(); + std::string Name = ChildrenNames.back(); + Children.pop_back(); + ChildrenNames.pop_back(); + writeHelper(Name, ChildBuff); } llvm::Error finalize() override { + assert(Children.empty() && ChildrenNames.empty()); if (!Started) return createStringError("Serializer not currently in use"); Started = false; @@ -144,17 +161,18 @@ class StringSerializer : public Serializer { private: template - void writeHelper(StringRef Name, T Value, std::string *Buff) { + void writeHelper(StringRef Name, T Value) { assert(Started && "serializer not started"); - Buff->append((Name + ":" + llvm::Twine(Value) + "\n").str()); - } - - template void writeHelper(StringRef Name, T Value) { - writeHelper(Name, Value, &Buffer); + if (Children.empty()) + Buffer.append((Name + ":" + llvm::Twine(Value) + "\n").str()); + else + Children.back().append((Name + ":" + llvm::Twine(Value) + "\n").str()); } bool Started = false; std::string Buffer; + std::vector Children; + std::vector ChildrenNames; }; namespace vendor { From bbe9009a5313d6ac937babb9e39a6db420e63946 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Tue, 17 Dec 2024 12:08:46 -0500 Subject: [PATCH 76/94] formatting --- llvm/unittests/Telemetry/TelemetryTest.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 450f6cbbef1c1..deebbfbc681b0 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -160,8 +160,7 @@ class StringSerializer : public Serializer { } private: - template - void writeHelper(StringRef Name, T Value) { + template void writeHelper(StringRef Name, T Value) { assert(Started && "serializer not started"); if (Children.empty()) Buffer.append((Name + ":" + llvm::Twine(Value) + "\n").str()); From e2036c6d396e208e59874f272822b76ad04719a0 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 18 Dec 2024 10:12:27 -0500 Subject: [PATCH 77/94] Update llvm/include/llvm/Telemetry/Telemetry.h Co-authored-by: James Henderson --- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 269ed94b80f5c..718e69a4b7b18 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -108,7 +108,7 @@ class Destination { public: virtual ~Destination() = default; virtual Error receiveEntry(const TelemetryInfo *Entry) = 0; - virtual llvm::StringLiteral name() const = 0; + virtual StringLiteral name() const = 0; }; /// This class is the main interaction point between any LLVM tool From 05947d52909e2622d85befc5860cf66a4f8e82f0 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 18 Dec 2024 10:12:48 -0500 Subject: [PATCH 78/94] Update llvm/docs/Telemetry.rst Co-authored-by: James Henderson --- llvm/docs/Telemetry.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index a00dc714ad053..09d6acfb8dfce 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -111,6 +111,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of void write(StringRef KeyName, unsigned long long Value) override { writeHelper(KeyName, Value); } + void write(StringRef KeyName, StringRef Value) override { writeHelper(KeyName, Value); } From f1100bb359fa5cb44a6d932427e22102ced918a0 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 18 Dec 2024 11:25:50 -0500 Subject: [PATCH 79/94] add helper write() that takes a map. also updated docs --- llvm/docs/Telemetry.rst | 21 +++++---------------- llvm/include/llvm/Telemetry/Telemetry.h | 23 +++++++++++++++++++++-- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 09d6acfb8dfce..52f5cbd69ce8e 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -115,21 +115,12 @@ To use Telemetry in your tool, you need to provide a concrete implementation of void write(StringRef KeyName, StringRef Value) override { writeHelper(KeyName, Value); } - - void write(StringRef KeyName, - const std::map &Value) override { - json::Object Inner; - for (auto KV : Value) { - Inner.try_emplace(KV.first, KV.second); - } - writeHelper(KeyName, json::Value(std::move(Inner))); - } - + void beginObject(StringRef KeyName) override { Children.push_back(json::Object()); ChildrenNames.push_back(KeyName.str()); } - + void endObject() override { assert(!Children.empty() && !ChildrenNames.empty()); json::Value Val = json::Value(std::move(Children.back())); @@ -139,8 +130,8 @@ To use Telemetry in your tool, you need to provide a concrete implementation of ChildrenNames.pop_back(); writeHelper(Name, std::move(Val)); } - - llvm::Error finalize() override { + + Error finalize() override { if (!Started) return createStringError("Serializer not currently in use"); Started = false; @@ -165,9 +156,8 @@ To use Telemetry in your tool, you need to provide a concrete implementation of public: static std::unique_ptr createInstatnce(telemetry::Config *Config) { // If Telemetry is not enabled, then just return null; - if (!config->EnableTelemetry) + if (!Config->EnableTelemetry) return nullptr; - return std::make_unique(); } MyManager() = default; @@ -259,4 +249,3 @@ Logging the tool init-process: Manager->logStartup(&Entry); Similar code can be used for logging the tool's exit. - diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 718e69a4b7b18..c7648c2a0c7ba 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -14,13 +14,14 @@ #ifndef LLVM_TELEMETRY_TELEMETRY_H #define LLVM_TELEMETRY_TELEMETRY_H +#include "llvm/ADT/DenseMap.h" #include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Error.h" -#include "llvm/Support/JSON.h" +#include #include -#include #include +#include namespace llvm { namespace telemetry { @@ -34,6 +35,24 @@ class Serializer { virtual void write(StringRef KeyName, StringRef Value) = 0; virtual void beginObject(StringRef KeyName) = 0; virtual void endObject() = 0; + + + template + using Is_DenseMap = std::is_same>; + template + using Is_StdMap = std::is_same>; + template + using Enable_If_Map = std::enable_if_t::value || Is_StdMap::value>; + + + template > + void write(StringRef KeyName, const T &Map) { + beginObject(KeyName); + for (const auto &KeyVal : Map) + write(KeyVal.first, KeyVal.second); + endObject(); + } virtual Error finalize() = 0; }; From 2f64b29957030a1532df687ee89073a1d6e6f90f Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 18 Dec 2024 13:04:29 -0500 Subject: [PATCH 80/94] added additional overloads for int types --- llvm/docs/Telemetry.rst | 18 +++++++++++++++++- llvm/include/llvm/Telemetry/Telemetry.h | 21 +++++++++++++-------- llvm/unittests/Telemetry/TelemetryTest.cpp | 18 +++++++++++++++++- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 52f5cbd69ce8e..0ea22b5076582 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -108,6 +108,22 @@ To use Telemetry in your tool, you need to provide a concrete implementation of writeHelper(KeyName, Value); } + void write(StringRef KeyName, unsigned int Value) override { + writeHelper(KeyName, Value); + } + + void write(StringRef KeyName, unsigned long Value) override { + writeHelper(KeyName, Value); + } + + void write(StringRef KeyName, long Value) override { + writeHelper(KeyName, Value); + } + + void write(StringRef KeyName, long long Value ) override { + writeHelper(KeyName, Value); + } + void write(StringRef KeyName, unsigned long long Value) override { writeHelper(KeyName, Value); } @@ -172,7 +188,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of return AllErrs; } - void addDestination(std::unique_ptr Dest) override { + void addDestination(std::unique_ptr&& Dest) override { destinations.push_back(std::move(Dest)); } diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index c7648c2a0c7ba..28780bd1bc10f 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -31,28 +31,33 @@ class Serializer { 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 int Value) = 0; + virtual void write(StringRef KeyName, unsigned long Value) = 0; + virtual void write(StringRef KeyName, long Value) = 0; + virtual void write(StringRef KeyName, long long 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; - template - using Is_DenseMap = std::is_same>; + using Is_DenseMap = + std::is_same>; template - using Is_StdMap = std::is_same>; + using Is_StdMap = + std::is_same>; template - using Enable_If_Map = std::enable_if_t::value || Is_StdMap::value>; - + using Enable_If_Map = + std::enable_if_t::value || Is_StdMap::value>; - template > + template > void write(StringRef KeyName, const T &Map) { beginObject(KeyName); for (const auto &KeyVal : Map) write(KeyVal.first, KeyVal.second); endObject(); } + virtual Error finalize() = 0; }; @@ -142,7 +147,7 @@ class Manager { virtual Error dispatch(TelemetryInfo *Entry) = 0; // Register a Destination. - virtual void addDestination(std::unique_ptr Destination) = 0; + virtual void addDestination(std::unique_ptr &&Destination) = 0; }; } // namespace telemetry diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index deebbfbc681b0..57274fdaef6ce 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -67,6 +67,22 @@ class JsonSerializer : public Serializer { writeHelper(KeyName, Value); } + void write(StringRef KeyName, unsigned int Value) override { + writeHelper(KeyName, Value); + } + + void write(StringRef KeyName, unsigned long Value) override { + writeHelper(KeyName, Value); + } + + void write(StringRef KeyName, long Value) override { + writeHelper(KeyName, Value); + } + + void write(StringRef KeyName, long long Value) override { + writeHelper(KeyName, Value); + } + void write(StringRef KeyName, unsigned long long Value) override { writeHelper(KeyName, Value); } @@ -261,7 +277,7 @@ class TestManager : public Manager { return AllErrs; } - void addDestination(std::unique_ptr Dest) override { + void addDestination(std::unique_ptr &&Dest) override { Destinations.push_back(std::move(Dest)); } From a5df7a039beac187114999de1dc3a145e8deb927 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 18 Dec 2024 13:11:11 -0500 Subject: [PATCH 81/94] formatting --- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- llvm/unittests/Telemetry/TelemetryTest.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 28780bd1bc10f..c317042a27e0a 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -147,7 +147,7 @@ class Manager { virtual Error dispatch(TelemetryInfo *Entry) = 0; // Register a Destination. - virtual void addDestination(std::unique_ptr &&Destination) = 0; + virtual void addDestination(std::unique_ptr Destination) = 0; }; } // namespace telemetry diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 57274fdaef6ce..366399d7f8fb9 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -277,7 +277,7 @@ class TestManager : public Manager { return AllErrs; } - void addDestination(std::unique_ptr &&Dest) override { + void addDestination(std::unique_ptr Dest) override { Destinations.push_back(std::move(Dest)); } From 585fee84c7655705b756d6bb643de68e59a9b4f8 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Wed, 18 Dec 2024 13:59:00 -0500 Subject: [PATCH 82/94] remove more unused headers --- llvm/docs/Telemetry.rst | 2 +- llvm/unittests/Telemetry/TelemetryTest.cpp | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 0ea22b5076582..84e9cb2001673 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -188,7 +188,7 @@ To use Telemetry in your tool, you need to provide a concrete implementation of return AllErrs; } - void addDestination(std::unique_ptr&& Dest) override { + void addDestination(std::unique_ptr Dest) override { destinations.push_back(std::move(Dest)); } diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 366399d7f8fb9..67a85b3cb5688 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -8,15 +8,9 @@ #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 From 3812cdacc27ac6608beaf2de2e54a92b8df3de60 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 19 Dec 2024 10:24:06 -0500 Subject: [PATCH 83/94] Update llvm/unittests/Telemetry/TelemetryTest.cpp Co-authored-by: James Henderson --- llvm/unittests/Telemetry/TelemetryTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 67a85b3cb5688..f0832f4eadd56 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -33,7 +33,7 @@ struct TestContext { bool HasVendorPlugin = false; // These two fields contain data emitted by the framework for later - // verifications by the tests. + // verification by the tests. std::string Buffer = ""; std::vector EmittedJsons; From 0cdc240628198792e4f0a0afddaa91a606177592 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 19 Dec 2024 10:24:42 -0500 Subject: [PATCH 84/94] Update llvm/unittests/Telemetry/TelemetryTest.cpp Co-authored-by: James Henderson --- llvm/unittests/Telemetry/TelemetryTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index f0832f4eadd56..d7eb7b1e660e7 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -193,7 +193,7 @@ struct VendorConfig : public Config { } }; -std::shared_ptr GetTelemetryConfig(const TestContext &Ctxt) { +std::shared_ptr getTelemetryConfig(const TestContext &Ctxt) { return std::make_shared(/*EnableTelemetry*/ true); } From da8056f749fb55b6f56234541f0067160c1dcffb Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 19 Dec 2024 10:29:03 -0500 Subject: [PATCH 85/94] Update llvm/unittests/Telemetry/TelemetryTest.cpp Co-authored-by: James Henderson --- llvm/unittests/Telemetry/TelemetryTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index d7eb7b1e660e7..3b9f186e31e04 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -194,7 +194,7 @@ struct VendorConfig : public Config { }; std::shared_ptr getTelemetryConfig(const TestContext &Ctxt) { - return std::make_shared(/*EnableTelemetry*/ true); + return std::make_shared(/*EnableTelemetry=*/true); } class JsonStorageDestination : public Destination { From c116074673675522f5612e2bc5dad334298a9a79 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 19 Dec 2024 10:50:22 -0500 Subject: [PATCH 86/94] Update llvm/unittests/Telemetry/TelemetryTest.cpp Co-authored-by: James Henderson --- llvm/unittests/Telemetry/TelemetryTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 3b9f186e31e04..c4562b9f89fd7 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -289,7 +289,7 @@ class TestManager : public Manager { } // namespace vendor -std::shared_ptr GetTelemetryConfig(const TestContext &Ctxt) { +std::shared_ptr getTelemetryConfig(const TestContext &Ctxt) { if (Ctxt.HasVendorPlugin) return vendor::GetTelemetryConfig(Ctxt); From a4e279929c4124f5cc0882e2971c8b61ac089a33 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 19 Dec 2024 10:53:23 -0500 Subject: [PATCH 87/94] Update llvm/unittests/Telemetry/TelemetryTest.cpp Co-authored-by: James Henderson --- llvm/unittests/Telemetry/TelemetryTest.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index c4562b9f89fd7..ca006ca9ddadc 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -286,7 +286,6 @@ class TestManager : public Manager { const std::string SessionId; std::vector> Destinations; }; - } // namespace vendor std::shared_ptr getTelemetryConfig(const TestContext &Ctxt) { From 2763d46de8ed3b4b299578660c47388296951ccb Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 19 Dec 2024 11:00:10 -0500 Subject: [PATCH 88/94] Update llvm/unittests/Telemetry/TelemetryTest.cpp Co-authored-by: James Henderson --- llvm/unittests/Telemetry/TelemetryTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index ca006ca9ddadc..23cbdcc617fbf 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -298,7 +298,7 @@ std::shared_ptr getTelemetryConfig(const TestContext &Ctxt) { TEST(TelemetryTest, TelemetryEnabled) { const std::string ToolName = "TelemetryTestTool"; - // Preset some params + // Preset some params. TestContext Context; Context.HasVendorPlugin = true; Context.Buffer.clear(); From 75c1b4ba6ea92ccf127e6d7de68fcbd2026eb023 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 19 Dec 2024 11:06:52 -0500 Subject: [PATCH 89/94] addressed review comments --- llvm/include/llvm/Telemetry/Telemetry.h | 3 +- llvm/unittests/Telemetry/TelemetryTest.cpp | 32 ++++++++-------------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index c317042a27e0a..1b71aa6258a5c 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -14,6 +14,7 @@ #ifndef LLVM_TELEMETRY_TELEMETRY_H #define LLVM_TELEMETRY_TELEMETRY_H +#include " #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringRef.h" @@ -73,7 +74,7 @@ struct Config { const bool EnableTelemetry; Config(bool E) : EnableTelemetry(E) {} - virtual std::string makeSessionId() { return "0"; } + virtual std::optional makeSessionId() { return std::nullopt; } }; /// For isa, dyn_cast, etc operations on TelemetryInfo. diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 23cbdcc617fbf..1356513943234 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -11,14 +11,10 @@ #include "llvm/Support/Casting.h" #include "llvm/Support/Error.h" #include "llvm/Support/JSON.h" -#include "llvm/Support/raw_ostream.h" #include "gtest/gtest.h" -#include -#include +#include #include -#include - namespace llvm { namespace telemetry { // Testing parameters. @@ -45,7 +41,7 @@ class JsonSerializer : public Serializer { public: json::Object *getOutputObject() { return Out.get(); } - llvm::Error init() override { + Error init() override { if (Started) return createStringError("Serializer already in use"); Started = true; @@ -99,7 +95,7 @@ class JsonSerializer : public Serializer { writeHelper(Name, std::move(Val)); } - llvm::Error finalize() override { + Error finalize() override { if (!Started) return createStringError("Serializer not currently in use"); Started = false; @@ -124,7 +120,7 @@ class StringSerializer : public Serializer { public: const std::string &getString() { return Buffer; } - llvm::Error init() override { + Error init() override { if (Started) return createStringError("Serializer already in use"); Started = true; @@ -161,7 +157,7 @@ class StringSerializer : public Serializer { writeHelper(Name, ChildBuff); } - llvm::Error finalize() override { + Error finalize() override { assert(Children.empty() && ChildrenNames.empty()); if (!Started) return createStringError("Serializer not currently in use"); @@ -173,9 +169,9 @@ class StringSerializer : public Serializer { template void writeHelper(StringRef Name, T Value) { assert(Started && "serializer not started"); if (Children.empty()) - Buffer.append((Name + ":" + llvm::Twine(Value) + "\n").str()); + Buffer.append((Name + ":" + Twine(Value) + "\n").str()); else - Children.back().append((Name + ":" + llvm::Twine(Value) + "\n").str()); + Children.back().append((Name + ":" + Twine(Value) + "\n").str()); } bool Started = false; @@ -187,7 +183,7 @@ class StringSerializer : public Serializer { namespace vendor { struct VendorConfig : public Config { VendorConfig(bool Enable) : Config(Enable) {} - std::string makeSessionId() override { + std::optional makeSessionId() override { static int seed = 0; return std::to_string(seed++); } @@ -215,7 +211,7 @@ class JsonStorageDestination : public Destination { return Error::success(); } - llvm::StringLiteral name() const override { return "JsonDestination"; } + StringLiteral name() const override { return "JsonDestination"; } private: TestContext *CurrentContext; @@ -247,7 +243,7 @@ class TestManager : public Manager { createInstance(Config *Config, TestContext *CurrentContext) { if (!Config->EnableTelemetry) return nullptr; - CurrentContext->ExpectedUuid = Config->makeSessionId(); + CurrentContext->ExpectedUuid = *(Config->makeSessionId()); std::unique_ptr Ret = std::make_unique( CurrentContext, CurrentContext->ExpectedUuid); @@ -277,10 +273,6 @@ class TestManager : public Manager { std::string getSessionId() { return SessionId; } - Error atStartup(StartupInfo *Info) { return dispatch(Info); } - - Error atExit(ExitInfo *Info) { return dispatch(Info); } - private: TestContext *CurrentContext; const std::string SessionId; @@ -312,7 +304,7 @@ TEST(TelemetryTest, TelemetryEnabled) { vendor::StartupInfo S; S.ToolName = ToolName; - Error startupEmitStatus = Manager->atStartup(&S); + Error startupEmitStatus = Manager->dispatch(&S); EXPECT_FALSE(startupEmitStatus); const json::Object &StartupEntry = Context.EmittedJsons[0]; json::Object ExpectedStartup( @@ -322,7 +314,7 @@ TEST(TelemetryTest, TelemetryEnabled) { vendor::ExitInfo E; E.ExitCode = 0; E.ExitDesc = "success"; - Error exitEmitStatus = Manager->atExit(&E); + Error exitEmitStatus = Manager->dispatch(&E); EXPECT_FALSE(exitEmitStatus); const json::Object &ExitEntry = Context.EmittedJsons[1]; json::Object ExpectedExit({{"SessionId", Context.ExpectedUuid}, From 9780ec7faba0d4cb2b4c568905e93572a94545aa Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Thu, 19 Dec 2024 14:02:20 -0500 Subject: [PATCH 90/94] Addressed review comments: - simplified tests + add more test to cover write(Key, Map) - make dispatch virtual with default impl, add an additional preDispatch - move Destination list management to base Manager --- llvm/docs/Telemetry.rst | 14 +- llvm/include/llvm/Telemetry/Telemetry.h | 14 +- llvm/lib/Telemetry/Telemetry.cpp | 16 +++ llvm/unittests/Telemetry/TelemetryTest.cpp | 148 ++++++--------------- 4 files changed, 68 insertions(+), 124 deletions(-) diff --git a/llvm/docs/Telemetry.rst b/llvm/docs/Telemetry.rst index 84e9cb2001673..e9d0d2cf95220 100644 --- a/llvm/docs/Telemetry.rst +++ b/llvm/docs/Telemetry.rst @@ -178,18 +178,9 @@ To use Telemetry in your tool, you need to provide a concrete implementation of } MyManager() = default; - Error dispatch(TelemetryInfo *Entry) const override { + Error preDispatch(TelemetryInfo *Entry) override { Entry->SessionId = SessionId; - Error AllErrs = Error::success(); - for (auto &Dest : Destinations) { - if (Error Err = Dest->receiveEntry(Entry)) - AllErrs = joinErrors(std::move(AllErrs), std::move(Err)); - } - return AllErrs; - } - - void addDestination(std::unique_ptr Dest) override { - destinations.push_back(std::move(Dest)); + return Error::success(); } // You can also define additional instrumentation points. @@ -204,7 +195,6 @@ To use Telemetry in your tool, you need to provide a concrete implementation of } private: - std::vector Destinations; const std::string SessionId; }; diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 1b71aa6258a5c..916134223d521 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -14,15 +14,16 @@ #ifndef LLVM_TELEMETRY_TELEMETRY_H #define LLVM_TELEMETRY_TELEMETRY_H -#include " #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Error.h" #include #include +#include #include #include +#include namespace llvm { namespace telemetry { @@ -142,13 +143,20 @@ class Destination { /// monitored and transmitting the data elsewhere. class Manager { public: + // Optional callback for subclasses to perform additional tasks before + // dispatching to Destinations. + virtual Error preDispatch(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 Error dispatch(TelemetryInfo *Entry); // Register a Destination. - virtual void addDestination(std::unique_ptr Destination) = 0; + void addDestination(std::unique_ptr Destination); + +private: + std::vector> Destinations; }; } // namespace telemetry diff --git a/llvm/lib/Telemetry/Telemetry.cpp b/llvm/lib/Telemetry/Telemetry.cpp index e7640a64195bf..d48390d0758b0 100644 --- a/llvm/lib/Telemetry/Telemetry.cpp +++ b/llvm/lib/Telemetry/Telemetry.cpp @@ -7,5 +7,21 @@ void TelemetryInfo::serialize(Serializer &serializer) const { serializer.write("SessionId", SessionId); } +Error Manager::dispatch(TelemetryInfo *Entry) { + if (Error Err = preDispatch(Entry)) + return std::move(Err); + + Error AllErrs = Error::success(); + for (auto &Dest : Destinations) { + if (Error Err = Dest->receiveEntry(Entry)) + AllErrs = joinErrors(std::move(AllErrs), std::move(Err)); + } + return AllErrs; +} + +void Manager::addDestination(std::unique_ptr Dest) { + Destinations.push_back(std::move(Dest)); +} + } // namespace telemetry } // namespace llvm diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 1356513943234..338798d87a515 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -10,7 +10,6 @@ #include "llvm/ADT/StringRef.h" #include "llvm/Support/Casting.h" #include "llvm/Support/Error.h" -#include "llvm/Support/JSON.h" #include "gtest/gtest.h" #include #include @@ -18,34 +17,31 @@ namespace llvm { namespace telemetry { // Testing parameters. +// // These are set by each test to force certain outcomes. -// Since the tests may run in parallel, each test will have -// its own TestContext populated. struct TestContext { - // Controlling whether there is vendor plugin. - // In "real" implementation, the plugin-registration - // framework will handle the overrides but for tests, - // we just use a bool flag to decide which function to call. + // Controlling whether there is vendor plugin. In "real" implementation, the + // plugin-registration framework will handle the overrides but for tests, we + // just use a bool flag to decide which function to call. bool HasVendorPlugin = false; - // These two fields contain data emitted by the framework for later + // This field contains data emitted by the framework for later // verification by the tests. std::string Buffer = ""; - std::vector EmittedJsons; // The expected Uuid generated by the fake tool. std::string ExpectedUuid = ""; }; -class JsonSerializer : public Serializer { +class StringSerializer : public Serializer { public: - json::Object *getOutputObject() { return Out.get(); } + const std::string &getString() { return Buffer; } Error init() override { if (Started) return createStringError("Serializer already in use"); Started = true; - Out = std::make_unique(); + Buffer.clear(); return Error::success(); } @@ -81,70 +77,7 @@ class JsonSerializer : public Serializer { } void beginObject(StringRef KeyName) override { - Children.push_back(json::Object()); - ChildrenNames.push_back(KeyName.str()); - } - - void endObject() override { - assert(!Children.empty() && !ChildrenNames.empty()); - json::Value Val = json::Value(std::move(Children.back())); - std::string Name = ChildrenNames.back(); - - Children.pop_back(); - ChildrenNames.pop_back(); - writeHelper(Name, std::move(Val)); - } - - Error finalize() override { - if (!Started) - return createStringError("Serializer not currently in use"); - Started = false; - return Error::success(); - } - -private: - template void writeHelper(StringRef Name, T Value) { - assert(Started && "serializer not started"); - if (Children.empty()) - Out->try_emplace(Name, Value); - else - Children.back().try_emplace(Name, Value); - } - bool Started = false; - std::unique_ptr Out; - std::vector Children; - std::vector ChildrenNames; -}; - -class StringSerializer : public Serializer { -public: - const std::string &getString() { return Buffer; } - - Error init() override { - if (Started) - return createStringError("Serializer already in use"); - Started = true; - Buffer.clear(); - return Error::success(); - } - - void write(StringRef KeyName, bool Value) override { - writeHelper(KeyName, Value); - } - - void write(StringRef KeyName, int Value) override { - writeHelper(KeyName, Value); - } - - void write(StringRef KeyName, unsigned long long Value) override { - writeHelper(KeyName, Value); - } - void write(StringRef KeyName, StringRef Value) override { - writeHelper(KeyName, Value); - } - - void beginObject(StringRef KeyName) override { - Children.push_back(std::string()); + Children.push_back(std::string("\n")); ChildrenNames.push_back(KeyName.str()); } @@ -193,9 +126,9 @@ std::shared_ptr getTelemetryConfig(const TestContext &Ctxt) { return std::make_shared(/*EnableTelemetry=*/true); } -class JsonStorageDestination : public Destination { +class TestStorageDestination : public Destination { public: - JsonStorageDestination(TestContext *Ctxt) : CurrentContext(Ctxt) {} + TestStorageDestination(TestContext *Ctxt) : CurrentContext(Ctxt) {} Error receiveEntry(const TelemetryInfo *Entry) override { if (Error Err = serializer.init()) @@ -205,25 +138,25 @@ class JsonStorageDestination : public Destination { if (Error Err = serializer.finalize()) return Err; - json::Object copied = *serializer.getOutputObject(); - CurrentContext->EmittedJsons.push_back(std::move(copied)); - + CurrentContext->Buffer.append(serializer.getString()); return Error::success(); } - StringLiteral name() const override { return "JsonDestination"; } + StringLiteral name() const override { return "TestDestination"; } private: TestContext *CurrentContext; - JsonSerializer serializer; + StringSerializer serializer; }; struct StartupInfo : public TelemetryInfo { std::string ToolName; + std::map MetaData; void serialize(Serializer &serializer) const override { TelemetryInfo::serialize(serializer); serializer.write("ToolName", ToolName); + serializer.write("MetaData", MetaData); } }; @@ -247,9 +180,9 @@ class TestManager : public Manager { std::unique_ptr Ret = std::make_unique( CurrentContext, CurrentContext->ExpectedUuid); - // Add a few destinations. + // Add a destination. Ret->addDestination( - std::make_unique(CurrentContext)); + std::make_unique(CurrentContext)); return Ret; } @@ -257,18 +190,9 @@ class TestManager : public Manager { TestManager(TestContext *Ctxt, std::string Id) : CurrentContext(Ctxt), SessionId(Id) {} - Error dispatch(TelemetryInfo *Entry) override { + Error preDispatch(TelemetryInfo *Entry) override { Entry->SessionId = SessionId; - Error AllErrs = Error::success(); - for (auto &Dest : Destinations) { - if (Error Err = Dest->receiveEntry(Entry)) - AllErrs = joinErrors(std::move(AllErrs), std::move(Err)); - } - return AllErrs; - } - - void addDestination(std::unique_ptr Dest) override { - Destinations.push_back(std::move(Dest)); + return Error::success(); } std::string getSessionId() { return SessionId; } @@ -276,17 +200,25 @@ class TestManager : public Manager { private: TestContext *CurrentContext; const std::string SessionId; - std::vector> Destinations; }; } // namespace vendor std::shared_ptr getTelemetryConfig(const TestContext &Ctxt) { if (Ctxt.HasVendorPlugin) - return vendor::GetTelemetryConfig(Ctxt); + return vendor::getTelemetryConfig(Ctxt); return std::make_shared(false); } +TEST(TelemetryTest, TelemetryDisabled) { + TestContext Context; + Context.HasVendorPlugin = false; + + std::shared_ptr Config = getTelemetryConfig(Context); + auto Manager = vendor::TestManager::createInstance(Config.get(), &Context); + EXPECT_EQ(nullptr, Manager); +} + TEST(TelemetryTest, TelemetryEnabled) { const std::string ToolName = "TelemetryTestTool"; @@ -294,33 +226,31 @@ TEST(TelemetryTest, TelemetryEnabled) { TestContext Context; Context.HasVendorPlugin = true; Context.Buffer.clear(); - Context.EmittedJsons.clear(); - std::shared_ptr Config = GetTelemetryConfig(Context); + std::shared_ptr Config = getTelemetryConfig(Context); auto Manager = vendor::TestManager::createInstance(Config.get(), &Context); EXPECT_STREQ(Manager->getSessionId().c_str(), Context.ExpectedUuid.c_str()); vendor::StartupInfo S; S.ToolName = ToolName; + S.MetaData["a"] = "A"; + S.MetaData["b"] = "B"; Error startupEmitStatus = Manager->dispatch(&S); EXPECT_FALSE(startupEmitStatus); - const json::Object &StartupEntry = Context.EmittedJsons[0]; - json::Object ExpectedStartup( - {{"SessionId", Context.ExpectedUuid}, {"ToolName", ToolName}}); - EXPECT_EQ(ExpectedStartup, StartupEntry); + std::string ExpectedBuffer = + "SessionId:0\nToolName:TelemetryTestTool\nMetaData:\na:A\nb:B\n\n"; + EXPECT_EQ(ExpectedBuffer, Context.Buffer); + Context.Buffer.clear(); vendor::ExitInfo E; E.ExitCode = 0; E.ExitDesc = "success"; Error exitEmitStatus = Manager->dispatch(&E); EXPECT_FALSE(exitEmitStatus); - const json::Object &ExitEntry = Context.EmittedJsons[1]; - json::Object ExpectedExit({{"SessionId", Context.ExpectedUuid}, - {"ExitCode", 0}, - {"ExitDesc", "success"}}); - EXPECT_EQ(ExpectedExit, ExitEntry); + ExpectedBuffer = "SessionId:0\nExitCode:0\nExitDesc:success\n"; + EXPECT_EQ(ExpectedBuffer, Context.Buffer); } } // namespace telemetry From 4715743c0aee6a0cb22f33452a24750356a5d571 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Fri, 20 Dec 2024 09:08:48 -0500 Subject: [PATCH 91/94] Update llvm/lib/Telemetry/Telemetry.cpp Co-authored-by: Pavel Labath --- llvm/lib/Telemetry/Telemetry.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/llvm/lib/Telemetry/Telemetry.cpp b/llvm/lib/Telemetry/Telemetry.cpp index d48390d0758b0..de8e77d52623c 100644 --- a/llvm/lib/Telemetry/Telemetry.cpp +++ b/llvm/lib/Telemetry/Telemetry.cpp @@ -13,8 +13,7 @@ Error Manager::dispatch(TelemetryInfo *Entry) { Error AllErrs = Error::success(); for (auto &Dest : Destinations) { - if (Error Err = Dest->receiveEntry(Entry)) - AllErrs = joinErrors(std::move(AllErrs), std::move(Err)); + AllErrs = joinErrors(std::move(AllErrs), Dest->receiveEntry(Entry)); } return AllErrs; } From d231bf2c8af321dd397ad8d20a5a35b6c1fd5b5d Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Fri, 20 Dec 2024 09:09:16 -0500 Subject: [PATCH 92/94] Update llvm/include/llvm/Telemetry/Telemetry.h Co-authored-by: Pavel Labath --- llvm/include/llvm/Telemetry/Telemetry.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 916134223d521..1c75c4cc865c9 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -34,8 +34,8 @@ class Serializer { virtual void write(StringRef KeyName, bool Value) = 0; virtual void write(StringRef KeyName, int Value) = 0; virtual void write(StringRef KeyName, unsigned int Value) = 0; - virtual void write(StringRef KeyName, unsigned long Value) = 0; virtual void write(StringRef KeyName, long Value) = 0; + virtual void write(StringRef KeyName, unsigned long Value) = 0; virtual void write(StringRef KeyName, long long Value) = 0; virtual void write(StringRef KeyName, unsigned long long Value) = 0; virtual void write(StringRef KeyName, StringRef Value) = 0; From 1941b1fc07bc721e9b00f5769aea527ff460a1c3 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Fri, 20 Dec 2024 09:46:36 -0500 Subject: [PATCH 93/94] addressed review comments: - combine all the write(int-variants) into one (pavel's suggestion) - replace the complex enable_if logic with simply mapped_type and let users deal with compile errors if mis-use --- llvm/include/llvm/Telemetry/Telemetry.h | 33 +++++++++------------- llvm/unittests/Telemetry/TelemetryTest.cpp | 31 ++++++-------------- 2 files changed, 22 insertions(+), 42 deletions(-) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 1c75c4cc865c9..816178daf2a54 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -32,27 +32,17 @@ class Serializer { public: 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 int Value) = 0; - virtual void write(StringRef KeyName, long Value) = 0; - virtual void write(StringRef KeyName, unsigned long Value) = 0; - virtual void write(StringRef KeyName, long long 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; template - using Is_DenseMap = - std::is_same>; - template - using Is_StdMap = - std::is_same>; - template - using Enable_If_Map = - std::enable_if_t::value || Is_StdMap::value>; + std::enable_if_t> write(StringRef KeyName, T Value) { + if constexpr (std::is_signed_v) + writeSigned(KeyName, Value); + else + writeUnsigned(KeyName, Value); + } - template > + template void write(StringRef KeyName, const T &Map) { beginObject(KeyName); for (const auto &KeyVal : Map) @@ -60,7 +50,14 @@ class Serializer { endObject(); } + virtual void beginObject(StringRef KeyName) = 0; + virtual void endObject() = 0; + virtual Error finalize() = 0; + +private: + virtual void writeUnsigned(StringRef KeyName, unsigned long long) = 0; + virtual void writeSigned(StringRef KeyName, long long) = 0; }; /// Configuration for the Manager class. @@ -118,8 +115,6 @@ 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; } }; diff --git a/llvm/unittests/Telemetry/TelemetryTest.cpp b/llvm/unittests/Telemetry/TelemetryTest.cpp index 338798d87a515..05523f1bcfaa2 100644 --- a/llvm/unittests/Telemetry/TelemetryTest.cpp +++ b/llvm/unittests/Telemetry/TelemetryTest.cpp @@ -49,29 +49,6 @@ class StringSerializer : public Serializer { writeHelper(KeyName, Value); } - void write(StringRef KeyName, int Value) override { - writeHelper(KeyName, Value); - } - - void write(StringRef KeyName, unsigned int Value) override { - writeHelper(KeyName, Value); - } - - void write(StringRef KeyName, unsigned long Value) override { - writeHelper(KeyName, Value); - } - - void write(StringRef KeyName, long Value) override { - writeHelper(KeyName, Value); - } - - void write(StringRef KeyName, long long Value) override { - writeHelper(KeyName, Value); - } - - void write(StringRef KeyName, unsigned long long Value) override { - writeHelper(KeyName, Value); - } void write(StringRef KeyName, StringRef Value) override { writeHelper(KeyName, Value); } @@ -107,6 +84,14 @@ class StringSerializer : public Serializer { Children.back().append((Name + ":" + Twine(Value) + "\n").str()); } + void writeUnsigned(StringRef KeyName, unsigned long long Value) override { + writeHelper(KeyName, Value); + } + + void writeSigned(StringRef KeyName, long long Value) override { + writeHelper(KeyName, Value); + } + bool Started = false; std::string Buffer; std::vector Children; From bd3df5efeaf32ba83fa37b22c55b35b77fede090 Mon Sep 17 00:00:00 2001 From: Vy Nguyen Date: Fri, 20 Dec 2024 09:54:20 -0500 Subject: [PATCH 94/94] Add simple assert to make sure the map's key is string compat --- llvm/include/llvm/Telemetry/Telemetry.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h index 816178daf2a54..ed519448315c2 100644 --- a/llvm/include/llvm/Telemetry/Telemetry.h +++ b/llvm/include/llvm/Telemetry/Telemetry.h @@ -44,6 +44,8 @@ class Serializer { template void write(StringRef KeyName, const T &Map) { + static_assert(std::is_convertible_v, + "KeyType must be convertible to string"); beginObject(KeyName); for (const auto &KeyVal : Map) write(KeyVal.first, KeyVal.second);