[Lldb-commits] [lldb] [llvm] [lldb]Implement LLDB Telemetry (PR #98528)
Vy Nguyen via lldb-commits
lldb-commits at lists.llvm.org
Wed Dec 11 07:58:51 PST 2024
https://github.com/oontvoo updated https://github.com/llvm/llvm-project/pull/98528
>From dbb8b15edb5e63f37a66dd15e67d46ee1b4f6c1b Mon Sep 17 00:00:00 2001
From: Vy Nguyen <vyng at google.com>
Date: Wed, 7 Aug 2024 11:08:44 -0400
Subject: [PATCH 01/25] [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 00000000000000..e34b228b219c10
--- /dev/null
+++ b/llvm/include/llvm/Telemetry/Telemetry.h
@@ -0,0 +1,99 @@
+#ifndef LVM_TELEMETRY_TELEMETRY_H
+#define LVM_TELEMETRY_TELEMETRY_H
+
+#include <chrono>
+#include <ctime>
+#include <memory>
+#include <optional>
+#include <string>
+
+#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<std::chrono::steady_clock>;
+
+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<std::string> 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<SteadyTimePoint> 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<ExitDescription> 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 638c3bd6f90f53..1d2fb329226484 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 00000000000000..8208bdadb05e94
--- /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 00000000000000..f7100685ee2d2b
--- /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())
+ : "<NONE>");
+ 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()
+ : "<NONE>") +
+ "\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 <vyng at google.com>
Date: Wed, 7 Aug 2024 13:21:32 -0400
Subject: [PATCH 02/25] 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 e34b228b219c10..fab170c61c02ec 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 <vyng at google.com>
Date: Wed, 7 Aug 2024 14:25:22 -0400
Subject: [PATCH 03/25] 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 fab170c61c02ec..dc24f9c3fc3fb8 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 <chrono>
#include <ctime>
@@ -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 <vyng at google.com>
Date: Wed, 28 Aug 2024 22:12:30 -0400
Subject: [PATCH 04/25] 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 dc24f9c3fc3fb8..935b1feddbed6e 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<std::chrono::steady_clock>;
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<std::string> additional_destinations;
+ std::vector<std::string> 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<SteadyTimePoint> m_end;
+ std::optional<SteadyTimePoint> 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<ExitDescription> exit_description;
+ std::optional<ExitDescription> 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 f7100685ee2d2b..b9605c4a38ecd0 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())
- : "<NONE>");
- 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()
- : "<NONE>") +
- "\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 911ede701982f6..9d6b3999c43958 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 00000000000000..a40ae4b2f55607
--- /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 00000000000000..36a4daf55e1018
--- /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 <cstdio>
+#include <vector>
+#include <chrono>
+#include <ctime>
+
+#include <iostream> // 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<llvm::json::Object> 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<std::string> 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<VendorCommonTelemetryInfo>(Entry)) {
+ if (auto *E = dyn_cast<VendorCommonTelemetryInfo>(Entry)) {
+ if (ShouldSanitize) {
+ if (isa<StartupEvent>(E) || isa<ExitEvent>(E)) {
+ // There is nothing to sanitize for this type of data, so keep as-is.
+ E->serializeToStream(OS);
+ } else if (isa<CustomTelemetryEvent>(E)) {
+ auto Sanitized = sanitizeFields(dyn_cast<CustomTelemetryEvent>(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<VendorCommonTelemetryInfo>(Entry)) {
+ if (ShouldSanitize) {
+ if (isa<StartupEvent>(E) || isa<ExitEvent>(E)) {
+ // There is nothing to sanitize for this type of data, so keep as-is.
+ return SendToBlackbox(E->serializeToJson());
+ } else if (isa<CustomTelemetryEvent>(E)) {
+ auto Sanitized = sanitizeFields(dyn_cast<CustomTelemetryEvent>(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<StringError>("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<TestTelemeter> createInstance(TelemetryConfig *config) {
+ if (!config->EnableTelemetry) return std::unique_ptr<TestTelemeter>(nullptr);
+ std::unique_ptr<TestTelemeter> Telemeter = std::make_unique<TestTelemeter>(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<StartupEvent>(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<ExitEvent>(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<CustomTelemetryEvent>(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 <typename T>
+ 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<TelemetryDestination *> 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<llvm::telemetry::TelemetryConfig> GetTelemetryConfig() {
+ // Telemetry is disabled by default.
+ // The vendor can enable in their config.
+ auto Config = std::make_shared<llvm::telemetry::TelemetryConfig>();
+ 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<std::chrono::steady_clock>{};
+// 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<vendor_code::StartupEvent>();
+ Entry.Stats = {StartTime, InitCompleteTime};
+ T->logStartup(ToolName, &Entry);
+}
+
+void AtToolExit(std::string ToolName, vendor_code::TestTelemeter* T) {
+ vendor_code::ExitEvent Entry = T->makeDefaultTelemetryInfo<vendor_code::ExitEvent>();
+ 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<vendor_code::CustomTelemetryEvent>();
+ 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<llvm::telemetry::TelemetryConfig> 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<llvm::telemetry::TelemetryConfig> 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<llvm::telemetry::TelemetryConfig> 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 <vyng at google.com>
Date: Wed, 28 Aug 2024 22:24:43 -0400
Subject: [PATCH 05/25] 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 935b1feddbed6e..7084b81c0304ae 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 b9605c4a38ecd0..f49b88e07f1361 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 36a4daf55e1018..b89bc584717da5 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 <cstdio>
-#include <vector>
#include <chrono>
#include <ctime>
+#include <vector>
-#include <iostream> // 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<VendorCommonTelemetryInfo>(Entry)) {
if (auto *E = dyn_cast<VendorCommonTelemetryInfo>(Entry)) {
if (ShouldSanitize) {
if (isa<StartupEvent>(E) || isa<ExitEvent>(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<CustomTelemetryEvent>(E)) {
auto Sanitized = sanitizeFields(dyn_cast<CustomTelemetryEvent>(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<VendorCommonTelemetryInfo>(Entry)) {
- if (ShouldSanitize) {
- if (isa<StartupEvent>(E) || isa<ExitEvent>(E)) {
- // There is nothing to sanitize for this type of data, so keep as-is.
- return SendToBlackbox(E->serializeToJson());
- } else if (isa<CustomTelemetryEvent>(E)) {
- auto Sanitized = sanitizeFields(dyn_cast<CustomTelemetryEvent>(E));
- return SendToBlackbox(Sanitized.serializeToJson());
- } else {
- llvm_unreachable("unexpected type");
- }
- } else {
+ if (ShouldSanitize) {
+ if (isa<StartupEvent>(E) || isa<ExitEvent>(E)) {
+ // There is nothing to sanitize for this type of data, so keep as-is.
return SendToBlackbox(E->serializeToJson());
+ } else if (isa<CustomTelemetryEvent>(E)) {
+ auto Sanitized = sanitizeFields(dyn_cast<CustomTelemetryEvent>(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<StringError>("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<TestTelemeter> createInstance(TelemetryConfig *config) {
- if (!config->EnableTelemetry) return std::unique_ptr<TestTelemeter>(nullptr);
- std::unique_ptr<TestTelemeter> Telemeter = std::make_unique<TestTelemeter>(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<TestTelemeter>
+ createInstance(TelemetryConfig *config) {
+ if (!config->EnableTelemetry)
+ return std::unique_ptr<TestTelemeter>(nullptr);
+ std::unique_ptr<TestTelemeter> Telemeter =
+ std::make_unique<TestTelemeter>(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<StartupEvent>(Entry)) {
+ if (auto *S = dyn_cast<StartupEvent>(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<ExitEvent>(Entry)) {
+ if (auto *E = dyn_cast<ExitEvent>(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<CustomTelemetryEvent>(Entry)) {
+ if (auto *C = dyn_cast<CustomTelemetryEvent>(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 <typename T>
- T makeDefaultTelemetryInfo() {
+ template <typename T> 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<std::chrono::steady_clock>{};
-// 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<vendor_code::StartupEvent>();
+void AtToolStart(std::string ToolName, vendor_code::TestTelemeter *T) {
+ vendor_code::StartupEvent Entry =
+ T->makeDefaultTelemetryInfo<vendor_code::StartupEvent>();
Entry.Stats = {StartTime, InitCompleteTime};
T->logStartup(ToolName, &Entry);
}
-void AtToolExit(std::string ToolName, vendor_code::TestTelemeter* T) {
- vendor_code::ExitEvent Entry = T->makeDefaultTelemetryInfo<vendor_code::ExitEvent>();
+void AtToolExit(std::string ToolName, vendor_code::TestTelemeter *T) {
+ vendor_code::ExitEvent Entry =
+ T->makeDefaultTelemetryInfo<vendor_code::ExitEvent>();
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<vendor_code::CustomTelemetryEvent>();
+void AtToolMidPoint(vendor_code::TestTelemeter *T) {
+ vendor_code::CustomTelemetryEvent Entry =
+ T->makeDefaultTelemetryInfo<vendor_code::CustomTelemetryEvent>();
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<llvm::telemetry::TelemetryConfig> Config = GetTelemetryConfig();
+ std::shared_ptr<llvm::telemetry::TelemetryConfig> 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<llvm::telemetry::TelemetryConfig> Config = GetTelemetryConfig();
+ std::shared_ptr<llvm::telemetry::TelemetryConfig> 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<llvm::telemetry::TelemetryConfig> Config = GetTelemetryConfig();
+ std::shared_ptr<llvm::telemetry::TelemetryConfig> 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 <vyng at google.com>
Date: Thu, 29 Aug 2024 11:32:13 -0400
Subject: [PATCH 06/25] 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 b89bc584717da5..cd3c61478c0b34 100644
--- a/llvm/unittests/Telemetry/TelemetryTest.cpp
+++ b/llvm/unittests/Telemetry/TelemetryTest.cpp
@@ -24,14 +24,17 @@
#include <ctime>
#include <vector>
-
-// 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<llvm::json::Object> 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<int> 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<TestTelemeter>
createInstance(TelemetryConfig *config) {
+ llvm::errs() << "============================== createInstance is called" << "\n";
if (!config->EnableTelemetry)
- return std::unique_ptr<TestTelemeter>(nullptr);
+ return nullptr;
+ ExpectedUuid = nextUuid();
std::unique_ptr<TestTelemeter> Telemeter =
- std::make_unique<TestTelemeter>(nextUuid());
+ std::make_unique<TestTelemeter>(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<llvm::telemetry::TelemetryConfig> 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<llvm::telemetry::TelemetryConfig> 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<llvm::telemetry::TelemetryConfig> 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 <vyng at google.com>
Date: Thu, 29 Aug 2024 12:47:46 -0400
Subject: [PATCH 07/25] 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 cd3c61478c0b34..fde7e31802f682 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<TestTelemeter>
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 <vyng at google.com>
Date: Thu, 29 Aug 2024 15:00:29 -0400
Subject: [PATCH 08/25] 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 7084b81c0304ae..d4f192ab9159c5 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<std::chrono::steady_clock>;
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<std::string> AdditionalDestinations;
};
+using SteadyTimePoint = std::chrono::time_point<std::chrono::steady_clock>;
+
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<SteadyTimePoint> 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 <vyng at google.com>
Date: Thu, 29 Aug 2024 15:08:18 -0400
Subject: [PATCH 09/25] 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 d4f192ab9159c5..cbc374df632124 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 <vyng at google.com>
Date: Tue, 3 Sep 2024 13:52:20 -0400
Subject: [PATCH 10/25] 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 00000000000000..997436e78735d0
--- /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`.
+
+
+// <TODO insert the flow chart that shows the interaction between the classes here>
+
diff --git a/llvm/docs/UserGuides.rst b/llvm/docs/UserGuides.rst
index 86101ffbd9ca5d..171da2053e7311 100644
--- a/llvm/docs/UserGuides.rst
+++ b/llvm/docs/UserGuides.rst
@@ -293,3 +293,6 @@ Additional Topics
:doc:`Sandbox IR <SandboxIR>`
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 cbc374df632124..55a915a4fb894f 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<std::string> AdditionalDestinations;
};
+// Defines a convenient type for timestamp of various events.
+// This is used by the EventStats below.
using SteadyTimePoint = std::chrono::time_point<std::chrono::steady_clock>;
-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<ExitDescription> 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 <vyng at google.com>
Date: Tue, 3 Sep 2024 21:00:50 -0400
Subject: [PATCH 11/25] 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 55a915a4fb894f..8f26f6ba9ec26e 100644
--- a/llvm/include/llvm/Telemetry/Telemetry.h
+++ b/llvm/include/llvm/Telemetry/Telemetry.h
@@ -63,9 +63,9 @@ struct EventStats {
std::optional<SteadyTimePoint> 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<ExitDescription> 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 f49b88e07f1361..b05a7483c7408c 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 fde7e31802f682..a2ded3cb5a2392 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<StringError>("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<TestTelemeter>
- 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 <typename T> 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<TelemetryDestination *> Destinations;
+ std::vector<Destination *> 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<llvm::telemetry::TelemetryConfig> GetTelemetryConfig() {
+std::shared_ptr<llvm::telemetry::Config> GetTelemetryConfig() {
// Telemetry is disabled by default.
// The vendor can enable in their config.
- auto Config = std::make_shared<llvm::telemetry::TelemetryConfig>();
+ auto Config = std::make_shared<llvm::telemetry::Config>();
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<llvm::telemetry::TelemetryConfig> Config =
+ std::shared_ptr<llvm::telemetry::Config> Config =
GetTelemetryConfig();
auto Tool = vendor_code::TestTelemeter::createInstance(Config.get());
@@ -507,7 +512,7 @@ TEST(TelemetryTest, TelemetryEnabled) {
Buffer.clear();
EmittedJsons.clear();
- std::shared_ptr<llvm::telemetry::TelemetryConfig> Config =
+ std::shared_ptr<llvm::telemetry::Config> 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<llvm::telemetry::TelemetryConfig> Config =
+ std::shared_ptr<llvm::telemetry::Config> 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 <vyng at google.com>
Date: Wed, 4 Sep 2024 12:23:24 -0400
Subject: [PATCH 12/25] 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 997436e78735d0..bca8eda9d13d11 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`.
-
-// <TODO insert the flow chart that shows the interaction between the classes here>
-
+.. 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
zcmeFZc_7s7zdt<Ea(9=6N)aj{WyvmMsca#XeOF0#l4b0pR47Z?BL-vNx3P?63K6o5
zZ5W2EGmIt67-r1O^KsWXzw?~ue9t-0|G&>a^~YS-^}gQw^14Jm(9>l3o9k~72*jdw
zPt5=XIs*Kc`O9BNfq$GQhy6jIP>`0|9V0)RmGmQ at jf@%Lv6kMD>zA+CKMH+^+j!FC
zTUU;w)qzhpo^AZraqg_TnyZ8`_;zb+=oW0<N<0w%+5Fkt^hR*wmLjZw3*C&L97~r%
z&Fn9}mH<xpzu*7m!2e$kJP=<(Vl9ZSQm$dTeO$=Cx8zb~*JdG8D<1-wcC1t}7xRMF
zLL4aU at o<}f?DI=MoWAnUH=0z6t)WS+z-rdV_T)&GNklovB8o(qsqNPI$fyPR<z<(0
z=uW1w$+Xx*ltZf|GYItHu=E9K#|09`3exu8^n&C{t}^XtNFi%?+~DAwRRwR0`q}HM
zD~l5=j%1~6v%v43`Qz0ZRt}nzezDCPE%_^Oag+-zhz3`0*;){f_~%0`R&%IW4y*XD
zB$DH3NjEf<QdT-I(xCWpTCDt6cha==>UGud at jmND@%!6>uJ29U79s0s;>6jb<@D~q
zi%Vp)CnS4Z(+NU8&%t^C>wMl+wFDn4tEN~Gt>qql`x+1SUfy(3R}XvB!)hk6ce#MI
zz1ThQ+NZHJd6L6=+Znclud-$DM;Z*9w#H|tuShj$9I`ovHO81t)H;p?+?f;9r7w@?
zujd2I^tPn#tk&sn=UX)JhybqOZf4W;m$TvJlHOzd&6I6g-kmm$6O?Uk$GYiYmB7C5
z+^eeM_iH@*Q}Dr{-&{Y}DBdE~8`HGcGVfY2GHL~R4DX0$I1Sa(R_v3B<e)U))gA5a
zIxLdsCa^zOmiqg1N~Y#^V{dv$eA}Mz4qf_%irJs at H_0w*LG3YV`|0GYx4$_#zW2q8
zW97Jm^%Kna^XjK}E3Cuyrp0i@{j2u&_Cz71U9LOhBnWiLqsDE*zu~MYk7SWoU9JPd
zdC2!`(!}bM{>Is7)WOD~W>yikf4-8I_`KdqxQ2=ctMZ~z<9_H_hVyX!2A?%jbj!%e
zX~`tg-9oi)8t{xCq(l|G3AdgvjJU5&x-ETj!`9knBCflK{B+9O{-bTL?51|dGM+Df
z^UXC5r;leohO2;m*C;{R#M;8gPcMN$^Y_A(X~%d6;SiXxs70+s#aza*58JB$kRozZ
zhMSnMrGaf>Hra)14dr=RBAg&T*(#X+)t^itkVv+1 at hRqHzWP}X-Beks7YQ+H>GyR6
zzVcf9Spc8pZw8r}#e^)mHTHki6n7lSTjdkAtlO)psqwB;lPhmsB{P9;e;jPEtYe4_
z1vYd=vc7}vsO<Yuhj(L}SUG-kL?lQ^7v>_4A~AveNHS2DvHGsbx6$0Nc*2g4H}xf8
zAQz3cS8(`(Q|ZY4vEu&C6Mgmb78=yczYUI`FJwcsaaAm^-(#Qx&8M4&@VkU&T_O6O
zWCCS|7_{79R~ACJj!=@bTs$g%ErT at z`Pl!E4h>ppD__m(Oy7aT=jlRSCTqswks&jn
zI{0585GicaU56}RYfqHjdRx{!^9sz%O$@$v>7P at d&`2cxkP6L=7k$$JZ!p_G^Vw<b
zcicPKK%nJoJV;-qOkkatiLDFmNS(@NHu-Ys?=!#6z3zurb!PZ}wbf0PVIUs2X+VUK
z(YVQwt02%YTdIr;!h{-=o=@VgADos`*^T1d{>PZuD*brm_P~wgubK=KbmmOCfW19l
z2>G!W7a{-xoe{$|dfP63t6f#t4+acmWkPDv|0VD5l at FD{8&ZuMCKG*P;^n)y(6CAI
z83T7A4iM<?@G8CZ#*FD4r)m{eh2^JU`Ve2!mzdv_ru0!WLU21PLl6^n^)@*c?l-_W
zywO}TT!Tor#`5s8=C9NGE%c9^-+v#&CRKRhsSpf28+(+6yZ45$aKlYB&UIx+1rWoc
z6E at 2W1GcPn-@@#fPR$<i_$~T&tKgaRLf>>2IYwc2_E$_~2wuTm^9+y_L})~Fm5wy2
z_rSu}D<)ngtL%2482e3F#K^|G30_L0Q}r`1ZEbBaU2j$HdK!>aM8sD4 at +jD#t9;T_
z4%A8fHvexkS?sA?hdPCyv|R7p+<X_yK|`ONJaru~4%mVP!X0ei>mx_eQLVy#iTZuQ
z`EZBmdV1ZvcSpTAXoGNE%)xIX1$9$bM-Et}cgrJ(Gvd#!=X`(ccGXfPm7v!|Zv<XN
z>$<J&STcbgbqRZn6ye;`8^x3uFY$tYOy~c|F)zrgTcmNp7tAoTuuzsk+wu^6A2ES0
zL6EW at Xy;8VxBCvhzH2(*CFh?7vWnCy?hs+|@|1K!-P)S(Q{cyeqt1ghOHKTG3NA0X
z9siKxGI0zBtIt&LF_Ge+VZ1oR7JQunN+c4J<t9~$G;0Xi&L&j-kI|j!Y>{i*v8r7s
z=jP_JUPV^3;OL?tP)?Rs^)q~(%k&00+$M!^#N>~l?s~OyO08Cq<o3#HN?h+|TTV=^
z;tzpt3!Lq>s0s3GV7jqSZv`7RhravGLbLB{o?m9L7oTcmLnnGxe;opaN<^p0c+jy5
zzy&+GVKl-KbWzaL74S~Ydkw4->)3c1YD2pgQ|0DXx7O<x_BL16Q}@r|IXI-8)>!K)
z>*6|`Z$8WFLm}9s@`f!o>aymvUvTfT-{7s}KH(cT7|NdgA%N#Rht77YKDX{(iR**j
zPnGRFk at 4`r#gwD)o{gkamgA>*(ga>XR2w at p?04dHqJp;{+NsdT#+-(t(p9F-T>h|=
zIbUDSxBHBpZ4i3Q(p_R_%r4s60jE)`75`$w<ICVqCOoDNqCzUduv)YCx2(MNx3^ya
zE^J at Nd=m5^s>THnP(FCg=jVQ%!;h_3&j>wyLH4Y-b`DQe8lGs}{Mq3<f6p#okHoC-
zTS=dYiCH<W)O&XDLW7tm;0{uauG1Mi2kv<TM*m6D_3~_IfzV7$Oq6e+*R6+E|6&H6
z;YjC_ahJ41wZ$@qSQ4`41fdQ<j_`b<$j7b`GsUv1$+!2F>-TjeY at J{KG-(5mjk;Nk
z0eS;&_!>T8+B*tFyzN-PD#qVly#zWVyb5fnxH>SWN9|$xki}``Q at lFdkce|x*VyS1
zr)B<|fh5C%^VA-hPBgf11}?Ec0)t0gn-|M8 at yW@i`=1HNSbf1AfCv3F8l5Ulb49hq
zq^%#-*y|3ronVoj7(2x^JRHNfe(dod at uqGrBy&&jDosqxfLD?}=PvF%uJ&w?COaxr
zj2>QD_x->GTITQNmUZ9!T1MrgG;*I_!0ER<Y4mmRU{%Skq!Gnce*Na*mC+g$-Fd}^
zx>Jh_+^&gj-ps%x_jY(`OzrBJoA0qkhIo3^!2$8W9LEYg<LhcC++fp_x387A*a7jo
ztT31`!!ADdZ+q2Ztfoh5*9WGi_IAz6_0yvBKg+#vH#h%g8cFxmtZM`I;k)KET(B|6
z?JB#U?A=JS@#}Sqyo7CUp(FVo{ZXS6-!%D%k1Xrl+&rM$0_7fQ(Nvy*$$tBV1rH%g
zS2Qk03UoU?uc_(lS!R>-iuhjP>ZYHN-t^e99iCJytI9U>jX(O4wYM9;>N>IGwn|7A
z^bekNwSo?nhKK-OM+9vx=1J`b^0YYM;|Ke!Dx0PS*5A>0p(C6yQ6HgyY%;&e-pFC1
z!P4}fjHJa~P)Lt9zQa at TYLvO@$EqqbXjq*-RO0{M->yS{=THXwE9*wc(_WR|Vk(K}
z4PKG-?Ip=pEhQnlb13brSjfIG+C%yK>OEko0L4VwQJ!2TWV3T9ee!v=?&{-kxs5~D
z{!nlIYaJDB?RF0m*skRe7(C?~v_C?2QxDVdVyEFRyN7duGQ|d(?u5NH)Ndpd?YW1>
zA1EJ>d_e<kOJ61Q-(+&uA;|ohCBpqNU*LjL`FdJ3zX#GI1dvHxWQccQogEA4!FLhk
zhv=V?fG){)vbesOoL_hC>~k|Cv;Gzc at eQZdWbY~Kz~FL5T4~_L=es3URhy>lv?YVO
zFktzIgoK38*uaL6>Y9Q=?&0~Ca9$@ewz!b<<{yC)vs3?W3Sx-uRHdzvC|}0qohVk|
z=iiJG!r$Mx^KpYRPn*L%<MX^=fkZFy%&vpbRqKpv582D*`}Zx1SfkxtfXwO at hP7|>
zAULJTx+sIu<x}5R9Ra&$7rlO+D at bW`kxGBTY)<&v`S}Oo-^2g`+=MIR4sK<kPWdP4
zO*IJ_$jfLfDcPs0F_6&{Ge<$!{?hL0S?6n+TT^Ld5;0Ou1^YVNt5CkPCj1GYKDdeX
zl_x+v+(dvDb{f~g)iot6Y<2y>?f+UC$udm-#G%aSI$@KA!O>Ezofgj7gsQb8?R!jm
z-DZ_y>&*cEuOQNkTt|?F5asM*Y~+ORX|GBj;@{=gmlt(t)bDOyW}o|D<^3h+4x*zC
zOx|+(vt*C&$|R{%Z35c~Boe1_-90xJ6 at N{>e&hc09g-BoKqOn6EFZb2R$?hQwKYZM
zMjNlTyMc|h(pp`A<mz6|i#H4N)Q)N`pkBN=!24p-$p+l at 4G2;74`?fD6BY`C<fEVh
zrS9w3Gp5#m%{^;m>OrV>CiyNKsmNb>f*Txo*(zcL_MFf&9?ZMR?d4<`v2=Cx(Pwt}
z&1J8CNCmU0#WxqBhJY_0g?@<LxDlv;WdPfW`mT;O5cN6o=_^Ny3*fc^6TZZ$_7hPN
zW at Ksa%$p}h^d at T!6KjNjbXZ}}JW+_0IiKHtEsBDNb;!C*{b<?$YHDNIrB13osFn<~
zgICWig#m^lzuMj1?Jz}Zzj0#n_Cf&mjp;a!e2FQ-jPGA>>TT at -XWAL$UfaQHmrZRD
z#*RdW)WwCC@&|i at LQ~;2HQQ at E<S~Yj-0fU1GL2l9Y&CmKU9BXLOW{Gv8)qT+i9&7c
znP+xlZhuRpw{t}qJ~@Nr^=iGo^98QAiS3T7C!Gr1H#4wMxIV~p3{)WTR>o~~AAb;c
zj^ulnHO`!-j at gAg0CXTfi}{Utf$wZ>dQpz<W7);QywgKkC12$QG_r`Ib*xp~9u1`!
zlcaWxGJQ(cZPe8_JDnwRyNe0*BtNI5X`i-L*0dlechlH at T{(SazUzP7^K~5(3sl1a
zZuVMAbAv9>8|4LlH_)R=@3wn^J?$C1J!0fi?hof2!ee?@>!vAp*w}~}Z%>_hmUW<%
zMRBrk##40C?%o%wJUW+f{!Q7CgLEAa7(xQ>OxcO at uO$vDh1{z_SYHYD0)>t_np>Fc
z3^nVcGCb#E3v|;=#%x12!meByRbt_}_Tw&sqi)qm!0qp9iDcO~DSA&>I)o+uw#NSu
zt^-PdEv&Xr6KMud7rs%vD~q1u(xebTpvvcH4NF&AG<Srgxj|Keb^QSAyZ=^uyp0)k
zIXUevH%5F2Sng{~yU>ry3ZUClOGZH8Cyz(M{O??gf6J=Y9sr-}Ukkj5mPxMH|4%03
z-N6RQ at LN?)BP920Sv8WfGk_g38eS07cYgS9!=!^QT^Z#jh4 at a-MU*wP2mdF<$df9o
zuii+S2uZHrX?P#73<CM*1p51bYf7F<UpoSG{a-AXdba;G`q1aJ`j82AC0|geqUVqp
z^<u(MAL=0sUql2+qZx)H%V)cYdBXmafR>`9)?8b9PjMabo5|({gKqQQi=hVsRa$dq
zd}$sRU&LI;1+cSGE{EW*j}?@mMEVR~`$asHI+T~IycKmJgr`J_58AYe at c(rOWkuhG
zS2}!tymHR+eB&CjW38#W(gEhSl5ERG-I*vXU;liIzUQO<-_oMEwU{4u=x}J0)RVMy
z at _sjqe1l`r#!Ek9>7Ab^t?kdEVv5<N(O$AsW95d9QxMx_saW1s%vY at 8s#uU)l6i+X
zOmTed6pw*z+dw2ov=V7}*p3gnVhb-Tz(Wq$|6iJBp41v3fpZg%nm>n50xURCOIgin
zvHGuqoVqf~r}4c~wGZLl?Og^s7pUJjLz)Y~YbmL&4K!brUY6FOii+L@&}0B{7$aVj
z-8#0nr3HGjQTSik>|=$w7%<#6HRoZd6bISK5d<pgigYPyL`%yJ%oi0d9?MP}0F$jy
zdIfn5SvL#wI!`-0=-#(-Q^z=vh^v$Us1Z^w;xW=aCO36+hgcr=@#S?PrigEO|E29M
zl|<G{0bvrQH|e0l40^zFU0YiR1^qcLNB0c7UzX||a!Gl|%z5+az$KI(p^Y_vbVR|`
zJ^gON;YYYy|E+FJnb`eX<uMeVf2BIW{$8|Z{e5c_8 at ki1jm`y7sKC?YsbH!ug!xCD
zW6|ErU5sr<ty@~^r4yPa4cBKlOUd)`A?cr_!d_48;kg4B5<6lp*sbtEcS&~<4PmlN
zAEu$U{`+C9QO$J9Behv(*7M-NT4-5u9}3TF*RRk6lm*9SBL+6C*h9I{y99WL&GVO)
z_;iPrbO;yN{++FOk?h&y(C`MYFgv@>qtLL%HH#`;Vego@>SSA9*AUN{Hyz5`A1bFq
zh!_^yVp=yoMdpTl?W32K`YLWf<LW}-x3e8BJE>3`1t@)djJnQBKc%2wj?f52k_PuC
zJiZb`MvT8!6Mcp)g58 at R(DY`!pd{mkjisS5JKa>H7GGldI-_(%w$;wS4cn|qz{8Gl
zV?KN7{c030ER`jW7K at U=$aPOOjMc32fAVTW=7yg6s`+JDHa|mVP{q}Rq@<Q0p!KuZ
z at YP;R+JE=9;_lDk!{bi*dp-&6E~6z?dX2svbQ;9IAx^B5m%F6rTyxTJjjlk|q)Yc|
zU3p1em*5?Cm+Ibq7s8bG)dswdK!ct_7YgE3SgFZ-;p$9w=a2vh!8?oA%rA>PZycFV
zo+kIlr~b_|$}Q14)^<uq3DU5z?^U!3gGMQBv=_ltiPCFG7yHc{%U at n5l1|;adJieu
zSOSf{tXgD4d{|ycPh#QP#UmUauLvaX;$JWOJTNG=*I`(Vm%}Q{QEM>MkuyK5BpF-}
zHDgXL+e;lW>)hr-Vm>&_h4b;22oW+&hnD>($$o<C7o_ at p*RV?8+<Ufbicj)9lbd>B
z8MtGQnW)R_+v)46i_hyvKon(t(fvU6l@^r59HWRXPV?}9&VNgQk*Mo6n3p!qcYkeK
z^4+6bPi08--{>*<Cy*?})f57m-x5N&P!=&IXo=<MZy9c(cd<_SdlFh|gQ?|@7}c~g
zo8}<m6a*nNT1j-RX)rCX4O+r2`K~i~?5W$_DM4-~Hf}6|t)Dn2w1yV_e13<fK7mM?
zPZ#S4uiIR-c>HVGx0EDTd5n(=-FXAa3&6K$akI)!1cc95H;^`0gY{AF)4MC3>MHB?
zc&6IGSR0$>Cm+tgS&HIc%h{UW5=%=apj4iKKuMv>XIZ#<Gk^}Sc)duZAS+)4*V#Pc
z>pUGZrOkWJ>8h-yB8G#`L%Ou~cg1lcYd-MfUlL9AU={ButtaR*J5v+ooCuxdVHoak
zrbeiGyn}Hei52GGfr3gi)nlFM{@PJUyiqQ_Y;AApqO8chdiDNj`YB>*vVzYj!Z3dr
z?Q4MHR-P;;27F<nd-YiRZV-{Ou7udTQD7!{Cxa2Kt^!N<C5Enxg;F?`w?9WgP@&uo
zU)FV4*O77d=!8Zzw@}YEL;HL^esL(#gvVKKY(?dLNC*&H;>j^!w*1$_-NogfUX+xS
zOuAQ%_}bvGCg~05L at fmvV%jOlh4G>-9FXkk4CdJcIaq8qC6!L()D+$cGp*K5lhIG|
zu||dm1Ksu}udStnzHc&7<=TSTdy~@DRHEC;NN;}QIy7Z<pirl~(mp^mEr_qd$E6J~
z6#N+Mv*do`I at ee!1lz{zJ>2DskiqD-h-n*$X}!30kZ1fOOjk at +yxTy<L+y)Picdu+
zigYLDX{mLNP9F7St at Wm$fMH?$7*P5p*ir7@?q3}3&Cj<DiWU{UQEfM!R!K-{8NMN2
z{%iSMSo^j64i`Oz(#wPQo2LR%f at _$sKx^y!w~4hXh)o_tJ3gGX{mqH!)p04A3!8o5
zLLcd65ewyzAp1{wOBy}Wtt%be(an3$?7~m!iVkgK2?s(R at WHV3HP0NiqweU6QrDq|
z{m|LS;mys>N5inZHD~EmaI-n`_?KQeeNm4g!Si?W+Xst^^OBs-d<F~1r-Dw7{G}ye
zKw_*-S6!E}Z^+5n<f5+F0gaHaj!Lu)m$FX*th$hx447DCA?EH7v|mxmM~bCz8XQ2+
zP0mZYA(8(uxo(Q<>P5JHstJ#hX*s0E3Wd^$v+;_q1h0Is$io<E&Hb7zVh`nZJzaAo
zdj4b&2}?)``t>z)Y8LR at pRkP(=LU at wn?Sdl#!Z%W!&MXRI<#hI3#BkXi=L){Cg;BJ
zH&p?- at FU2W9pmZVhsVUgg}oZBp=ZL!zob^VknNieKe`++byiM at uA$3`Kp;PW4l45|
z$zXq<-AD&)Zjwd!4x#yTFwpla*K0A_?AHa2zAi3DWF&(`u0pWX%=zhQ-Jq4K{;ypa
zw|P-=ApWW3zf0aIPFG?|Nld&xz)12|fw(}1|CV97qV2&9Cw^Uy>^Hd<@@1wA<s{8y
z_5-M9(mmVLi>y52MN#)B*Zr75ADFTGc8eTy%4US4i2 at _@OU!89Y)39<v%JEOX!KL<
zg4gyIoO)WHD(=H~>HF1HR*Z7H$9onoxl<Tjkb9%s at ->lYgZFx?#+%myTENz3L&5L`
zi~fOhJ#Jl4%kC3ra|XSG{!4XPMp{7DLX#htz2JW6d92d)d`%S9-5K|7$^H9mMzK at 3
zrR(FBhnhzsiwX at 5djJnWuSKI4^i^^N17y<Wt=1ow8+OJd+A|-`DZ>>^Yj(9)SZOs9
zngdBiB8HZpFkQF`ynkn<I?>)(0Hg{B>$@amSgzIL91jho(}M@@6ylw`{kJK_<!iPm
z!4awY^^N;tVoc|Y=3&c|n*-H0<0*2=waHV@>zd~&jd6_iBpuxwO>WyKc1qYV<HhUe
z(=XyUlbixJ;>)2=x#gUmV9X%!dB%;6g2Tyl+aWDDIbbiWJdVKtW<haUm1%r_$nL9y
z#eBhbKud9BKDZpYnw9V{YK8>%U3zw`L|%T60R)3&kwE8t&919Sfl5B?q(++{9M29q
zd79KM<7SRIzFxY7OtymL2M4qY+2E{(r`4ehU^*6vW?)7RKO#UOI3aUQ!yh=wW<AP;
zHPBabV)SK^WBf(;ur?CQcc_z=u>6E2A3}rcbPfgD%&Sl)yS+bQcy46Ke at 6Ot^u1vv
zphNocDYy5!kc`jXXgEw~$~01vjNp+9dGSKpWnzaXp3E{)jE$j28LV1jZZWzoS7`k$
zGl|4o-aO6v(uwQXMqXV(v2$cdS56*i$EFMh+0Wt>6ts<RW>v=C=z022F{#7uBH2)X
zI8$+NvL}u_Qea(of1N~J3;B^Dwb?OmaKYWbVD{}4isJfQQ%2=r4)JxdE&llJ=GeCW
zv)?iuS02QP?VIy^|1->7$qOvL=wB{wuoe{Ly4gaBx<DJuNqAvx2c+Kj>F)31AO9Gl
zk at gmC^gRXlONM=LxuDkmrFjRhY}a7|o&%c99f$EPPb}qP%XL|<z7KT?*0A+nUEPVq
zC~q$>sy;cBClym~m>3E55WglW4tp|o at -XPdBcgqP7`T_^+6Wpjj*WPI0}cVD0HdUa
z$igB0a*?5gNykqbs|}dZYgH9i{F60b)yx(JLDNA)Dz;#b at P$+)7cb^omE)#{%Z~=U
zw!nNgIYJOb#@;}zB%1DTC8`Y}+7vAK^tr8HFQ5-+#yQU{@|W2{(~;)t!?sYForPpW
zm^zWyIbf at qoag?*AQAWxy>IP|t^bg2A$dSURH5wSTl5 at uw@t}ZtCO(4hl70e6qwA$
z^ZI2%%h+FWJuVO))>hN#nY|A}@|zCPbTy*?)`)5$+bkoK5Lp at M;8+LAG+ at uiWeS0`
z_3!GPf<Y2ew}KxSrnRDC=};<;tYa?qfQLhu97X4mA^nPo at 6$_Ck=#j7kSx@@jTHV;
zI#Huz10AEJ<qWS6x-BKdBrBW=FwymSH#*cG4)ZptieVRD3m&;(^a*^#?0Cnj at AyPg
zLQ>watwUR{0egaGMv&fSJ-f8F<6Dw63-QC8o~4nvDZ*s<Lb6rb=hQ-{QsgsheGo82
zTzv}6fu;k{5;Q$3Uq}-fxuo#;g-MargvUU)u+KKu at o5@e!x at T}ywI|?cIM~H8|A5=
zcZx7_7Pf|mN4lbww<_~(KS=&9<v*>KhPgIgp;x^qYS7(AHU=(XF>SE^#xYnoKE`)o
zqygjEOm}9WkpYVYQk5~3PvEb~IqjfjWH>JrX_};!j<4luw=&{>F=6Zjlu|~n7=jnX
zx=BjBxi3(hNaGIq%8^n0kcd_=faz<y7wVNO_v9zIDFjjHJt2Qrw46rc$0YL8Vf#&g
z5nWzqv%8 at khB%K>UVUv|=OVh+6W*WD^@>S)!@oPE%QQ-A%N4jw`cx{u+NwZlz0)a^
zZq8o^_aS{7jw at LTps1S)^_#wuq12gFd3!Q=%?*6q>Uu91S=YM>=uOlTe+AEGxNw6)
zMH<LsL*n~iSp)$s-e8tUjrhnRc(@ZX-MpE?t^x}- at sE*hj>v$&e(xI+y)5`RTT51s
zDaE~W(r2_#1moEDt3K`)+AC4St3T1yHr+xz6=W5%*FA?VQ5(v_Z7pnc=G*WiW2~i_
z3Y==!8kW`;ik6yE%aGfHA?5{r7!3gr)T*te21h8 at 5<aO%Uq!b$FHeD3pSP?LyIv1x
zD6BO&IHx~91M_u?;l*rQkXznM^&S5C_YEUnjQ4Z$<Xyx5rIt>qPr8S0-Y1;24h&Q8
ztex^C&#Me4ju|*;!sA;JF}^tR2X&Hkb8T>E-wxfOLkK#0 at 0yQ>ciCu7vGd_a#>a2_
z*wn-ft;V(_+)r>+C|q_DMO91*<83n(YqzEtmePlaYxy?1z8%{^T)BvPworHAuv5Z?
z1;nMjn#ZpcoGG+cnW5IhnF at wLmtS%(h(Z;^RYNvgw0+-VtPF at chaO!2l*eM)3L8oC
z&Nx?u`OX>r;vwEB{U_ll7q(rMXgK9JZP*2q3D6i?@h;QoBbXWUgMgxDBrnN2B(#}6
zB@|cwNkgEBg<xhTem};o=9_HSLnVbgjA6Ph59Xc`4{(jlG?39<1H4=1s$Lgy3DW$t
z$TA+vfk>|4=Mw~x<m6XgORdb6x4?p?OX|Bn%zG-C`t>ds$d=^1#ujQ|kPDywX_R_t
zJ<$~eiu2s5awH_BeUM-mtSqee8jhA`e}EaJ`nBR!1Y(y?Fs0`|%~I~%&S0 at i867c7
zFVX<z>42^;5>hlS-#Fz|Q{EEspyUL{J7A!i`FQihhv&mEjdR2vd7LRsN5N#|uiJ7=
zehUT1;@AD%4&|_YQOJ`o=ru?<9~?A!tjq54p$GWotp`3nu3K)t_jxf6wtl^?EF7Wl
z`~f at j2qwpuUrlx!*dXLR+WS*$i1abXDs1GpE0xQDK;{GWIW^ukEtt)l{`<FwEj*Dn
z1$n8?&IO~*Ct{uA-xF at RZwhd)qfnbcfF6r6)y=$31l;RSO}^ZWVgzH-D|fW`{pfo^
z5W>XTuc?UbwE?%DT6vc4!&XTM$7%B9!2RR5NBoJUj}qXPYe*bWx^C($lYm_IYZ#5%
zV_t1g4xcZHfqBJ1WA9Ie1U at g*2-b*okxmbs{&vCP&`sppV{aputRlSk^xUT(f=mU%
zDn-=*k|SQFm>;gEio_WKLu<>b_|v}L2`3HXu3BhDa`+@<AD4izd;t<s?K2vhVO{pr
z9eqKoR+x{Q00*3(%|7M)uu=XmGo%a%e*Ct8hzwFB_0dTj{)1f{#!ypNCH(m9I;ou3
zaWSyKJ>4mp7YJpbU}k%x5kg>$_?vJ)w>?paQ}Ahi&Xu8n3czI#4NJ2d at -Gj+JQ<s7
z$$ZoL&;yKb{#t3{#<{M!nNz7;FY_I|RKiHu;;BMTt4hMZ9ZRiQOUdXB=FxBbRWwf;
zHX(VXfQ(*Uvd|ye at Y(-qr>A?XCC3?CUY73z!d3n5%m1Tp)motCo+CK$MgPYAFNtw5
zAvDe_S7WL$gFS?D>mm~$&k?b+<q2jdpn7#jA0ZECmX+p8XOqqhc`Ht;n-P7T+cF3Q
z#X|d&M*6lHuR8}znFxZQvy3zETg9*6O$-`p;PNbowH?k})+8>yKTBgtn2+M^@3_fC
z5WAIm7!cI+QGuyjBOiZPts;5?NS&VUXM$^%&LEIv#avYn2{wB)ZrOQr_ho at _Iy&97
z(U+9ZKpLvbNQzjLk?5_uTt$*dx+UP>dU+=u;<(+6%!N`x!!?71=3;ERNo0{@v*V$g
z3bSI?o5I`sKmxuo>6K^?C>(`I#lL+P4@$5g>O&peC~UYh?IS!J=XF7O6J6%cy^RE!
zooN;MG8V(^PE#v16laByi0t!Wv at GSl6z`Mg2o0tf at vaYiRjC^o_Cf+_E3nrvfq4XN
zWJD`D^&!LruLiHkUJE2tc2#|pn@%n%ZvPZA`WJI>598N+4}VPZxFASIeNf5Nu&h-r
zt~k%hDd+X5!(f87wV`2Z+a{pqy7-Y>OK}jc$XrQUALZ74kaKjsqmmES+p3F)%++84
zV-F2bl4(_El(Ts&Xu?e<WpiUPyQOC}d=NItAtr}(hl~-=5(C{nxgDy|o!FUbNYyI|
ztUs<&h<>4c{`%$2j}=9b+3{qt>-`C!;jQJ*-d*5Q{=%nU1sQuDn|MG(ddx;ucRLeb
z`<8!vPqA)A<=cqMIc1bK)CjkGi)oc~J(WHF<$1Ja^ym;iW(rCf?2cBQF*rZNX*<IS
zrd&~7R~^x+<8>1|ejDN(l5*8cZsqt0 at +F1+F8?WGj|uA?3!@}qrh>!yEt5sp{qs?2
zdDLYFpebcbqeBks7LomrfF at 4cbT7{P+P)P_^6{?ZKP7N{dZRBx&~usv=&&B;RbFW7
zfqIAC^O5nV-=2ILKm1hNdlf8j496lliL-?T4WbrjOY{ysApTP4NJ_oFl5qp4tC)TK
z3u&}ivFz!@k5BYH1OByaOo{}&N+cCD>21iE94Ic^%pZ9h^6RCot#h-QQh6R-1F%x6
zhTI;IA|kk}z;_?S at T3JqKUeAmuiaCk52(@`jQ94p3h}&NQMQ`g?=E_(-Bj9*mNUo?
z===mS%9d0tTOw9FQ1LEmMZQ&|)8;OZkqa_~8AHF5yq{tHs81yvzGqsU>fUcL#Ofu@
zP19L*y at qRYEA9`ymp&q?@ao3I;YZ$FFA6lC6ZU^OL{|)6`$Di#w{Q`)dVe$d at S~KV
z!J2F2mYIV0&qZr*GM(3hoMMT$oSDv17)=JU*Y4$@omJG`O$Xh;6Sl=gCpF(W{+*Hc
zNfTpk#O~1_7ISaBiT}?5W#vQ-&KXRNyOKx!xg3p_YnAzoegUQe^c{u#qSqB6;?}AR
zAgZ+$_GT^bYBB_MvgsxP`a}h;hQ^#Ol|q+#w__5VBX1{tjjaLa;@sV=%(vaZR;G)T
zhi|A#pmZBgz3<+h8VMA(61=?_2lB=Yo<O<251~5_ at dsz+*)z3L-@<$vu7YHc)(ey>
z{($}mqEe?r?r{8b<U?FaYU$%cH?6ua8|j=T_|?0+v<%DOWWx)5XKEAcl?a6Rkb9Hl
z0HF1GUYbV6JZFIiIdAU2B*ZMfE8>CRC~MM_VXQkut02$kR@{2mn9kxHvlNcsZWz=~
zwAT&@-cWaLVAt2at`3t(1u0TjhN}o`ommtfIGF7Q`kZL2AV_X{QbolQ7NX#nkq(jd
z9jh3&_EW`K$HLP-);EG&uvLLA8vE)*LWY8}z4nJoCt}yDW1(ebWEA5FoC6V9H7YXf
zDZoXlW9=WAwghWx_WNLt8>RjAPZ?luLA)7ImiO7E#?GvaC#r3_Sa$fIn)txEd{-Jl
zL*Xhey^^}e^zy;FrpNhKoePAumv;&K44&Zb?9Cv+rQB7;UvJ2=W?2uIU6^+a9};g7
z^j8G*!x(-hXH?U`T;472 at S{hv$8RIv5QyCL5>(bi(p{XBGd4m4Zd)U3mojPW02b+Y
zIP?Hc+OAeu(>}Z$43{nID~{33FSgePz2(0f3!b~X-lE;=<favEGSUxZ3{aS3^D>a=
z1UjwuJm=A`Q#Ac&PHPos7T<N?$vTGuZy%STmB_>K57_&oRkG05?YSCg=eA418E?`!
zmfpX5?luNcfA*NSXsJW-Zq|ZG!cRk2qh_E#Kcitrv4)dRt`2!TdJ%0!gQkF at LY{>q
zC1fmt at yO-#nv-2Q8oz{;@eUXAn3%sj8GEwC+nm?Hbd at hv5qjzRr=c7z=fSHrMF^aU
zZF5yjoF6&>d+%yMvpa4x4NyMOm1&i+USs>|5Eo$Fw2RUNLWywYnpg|=mnwK`*<<5Z
zTZivbTtJKOLbS4K3($AE#n~J?DjgN`YiNr=KGB1dS)Dwb>3d_e3}w;zuuDw<mu>h?
z<{Ow|p(bbfusO}6Ok}8oUpwZ_0uMM(a4(zt+dUf36jWslw_BKRfRQn(x`{M=@fpY*
zWJk(cB at ya08?Kk~!2vhj6a&^msC$@lEz-%gK*jh^?4iqM-Yb(bx3HH!XPj}5$V9|U
zQkFkl%gQ|bM&Ud~Gss)%YT<mwIa$msai%f|$Y5U%{w2EliZD8{UNIMk8UE|tFSESE
zkLu4E$oI+>`HCBv*VDO=fB6E4D7xmM<b57<x4vvU?Brd14l$}IMkB+KQs1XU8TM)x
z9#0L($4Fue`vAqjjC?mHuk#8f#;R;(t9MEYfJo#>y{+gXM6-8uGXr9 at iI)d*rH&hS
zvV246iFJMpI#2$+<?u<E>3qk~b8D4oZA_}4#j%0XVzz0@$>SU*3YwMQOF7pJ_{zf4
z`VVJ5N!H-i)D+Yy4K50>NWQ5E1dD5kJEjMeE0c}&U6z&|*h9^1qxsjK+EHVyTXZJ_
zMS<be5mNy=TB41GcW<Em*US1Iu|h4j!mCknx7 at AwD8xcT&)m_`p;IO3`S(%opew~`
zpnV#7+l8pEnd1bRb~Kk}x>k;wQbAoOV|$=201suFEg}&FTEe^`yP=`EW$k at YFU{(P
z<6rJOdnFL|u%TBK1|K?eutb0GvYmd;{NaJX_BOVjd=Ky0nVZ24Fe at Xdig9vjcv>zs
zrG$la at j#GE?4hY#1s_jw+Cns62pJ`6%GC~B6A)0X`KsxytK#E_^tM#^6lJ)n4__Pp
zG$_b}F8rju<CrjXZep#8VAeD0X2AYxL>GkQ79rdsH6iojuFrV-gwz<i%3G)crJTYP
z0D<Y6RyE9OQ{Xz1-TrXvoG7PH$34)|gDT-pzN`;vl~pLd`Z{CXotpPKU#&zT9epYb
z`(ZR=*oA8?pzvvG9!_0zwxHPwnVMJnrc=6Am)|Vguou_o3<Pro%JG=>&_0q(SZG#B
z06hpi2|1CtCiaDECZBxU+Zo30b>pOs(w^L36BZYbf4MoHd8*#BHs>F_LBrKb!Gqhs
zQ?<yKnfz)31Ec+l`Y!@S)by_u<zw6axpSP>fq|PTaZZ$y2zSLCGdQ_HnX^@x#FfYy
zEl>scY1&0x%{^EtW$;cX3H+wBjpPT1p at GIFqb*i{^f-hyiDLO0P9S-Jc=(wjNF(yI
zda~}QPP(nYT5t>#=$>ZwoD}V)gOJGcFimA?iJ(i)JSx_5%kGVPq-<x7_l{S77<V=6
zh;8&2yB~VsjP>ZxpeyCFJ7c}C2K>+kX_wlMm3)GR6lffEW@&`dH~M=`fuC(Z at wSWe
zehWW?^8yNhQcl0I$MlAoJ?wD1lCAS9%CWWAn4cM8lM$F<bO_BOx>WS>75ZVg<l5m(
zo9Z at JGEATdJ949Z7gOd$5;@S1$A>DeBV-MbA43%M(=R5v6s_j9)#(e4B at F)sz9%{9
zZwmhlu<Qb%5kpA<Y~=CyyUS;s+QI~MLEX>oj{3AqE1B}2;FfiEm75GA&TlwY2`c8j
zHjo#PN(Cio6P%)NmD-)BS^MY<)U*a%1u?e)vEllsx;9>_kmi<pzu^<J<KRS>hyDo8
z=m+Rq=#R3gt);$suVFSPMhqL#E*Ej3>hR+}WmsML-jUUkXKA3}XZ#fN<8}yPgq^^f
zk*rZdhNGmRVVtokdGIK=0?=|p(33`aht`W;Ki&lwtU7hhRM at m|gleZQ^RBtD%OK+&
zYnK)4Rxd4PV?Ruk6}i+;su?dg<JV at 33u%Cy7qm}Zzi_UsGJltQbcmCka&ds*7c5pV
zCD}~5s!33vZlhSC8?Yk$_Xk9s4n5F$NT5&@4Ni_ksC>H@?4R9VK at c>@TYGwKS_SL9
zkpLR_%Q}C%`DF*B9_67~!)^bL1GQ6%ux}<zoLen<gBD?NRaFomvE)i}nao+bFZX<2
zhokLSHoJ7{--T%Rthr7q$x0^IAW}Gb-|()wp+;f^7Bb!K)KF$>YwaKFP3+UswPqn+
z3 at ES_LwJ?R4xPVw32pAmv+}Cg0iDO|h0mjl#S8A?b)vr*zR)4AJ9QUnye6%3-{&Y@
zzF4^V3(K#uyU<YdFq+bM?Nh<)6!xfqj5}ekWcH;?u}YeNai?A{vD5Z-cJ`R4{klzb
ztCQIoF{OFILrBIIp6 at j1ONs&-GTjC!Sl*#V65Kg;Q=Rh&R5hB{Xj2cou~rjb*{?W<
zNe;lZqNXc5DGSGV&CJV$r`;RklF%4aLljQIz|CwSfES^w`;u3(eN3ufdM6*YPeEka
zfXPZ3S_16S&4b2_0nDlTH6wUIDtpk+heG2BxiRVS$OnUJ3_l1FxBpYB>u?wFNJSvt
z2k9%Xbe(cBwJPUSL>{5YE*l?FhJgShudDeuBnD&g8k=%;-yD8ZPr*U$MM9#2h4z at E
z!yO)sh5PdPH$Eh)aAY=hSwSIBVdi_dqY at 8hy@<F5Z9m at S5HtF4Q>%Zaj4hXB<E^M*
z at KkH$*=eMScl(pFMRsy>j;@p1yNQgu2tha%^1cY+p13l!hISUmKKh0;Hox9ggz-5T
z)(M_KoX8)2>Rq18CKlk<CLDq_msAKgOCKN*k_Qfk3qeza at 5Tt$Xy;+CLZxsXQ=eb(
zv{1rDfnJ~KUswzN0bP(wJIq&hbiL?Qjl$%b*f?wRO^LmWGlb3utLIAQQeW^XWpqh+
z1g$g+maQew{zKUvOGj-r?TG3tldI+fdrJ9=#Dx;thdA#9pl*0Vx=0Xz(wQD<QL}a1
z=+>dPT<)JS_imS_75kkQ9H<Ga$Pg{@+AC$f3hSX6%uh*_3&gsVw*E?)jgcK4^jdn)
z?3S4${`w5eoA!J at OPPQB$mca~!L99v#gMFg3T05&*7;h7JBk`&^TRmKsNZvlW at iwi
zBbWEgrM at Oi<0;TCDMG|d1H+g9Q<y)6PeB$I&H_`%>6vM|GU&^d7_-2(US%(4%@zPv
zG>ZZv%M+YIc{-I>)4Nw-ZRkHC_0ka+ASUOVD8ubE2%`?g3aW at W*59;^G<P5Po(y23
z_D$JCeQcY3{}n81^x5Ps`i~P1f@><*32V%teH|^QrPu?k>nn}l1jK<#sLKei={Ykq
zXKj~V<(_#Kvhq?ojMXcWT2t6+YWV8_ at Rx7xmk0?_{)0+1tQ)C1?%q_`O~AIu2uS=r
zWS-`!nHHRwl&miW0<i_iIZw_D$vv7kY&&*^e&hHTWch&=0EryG-LzKOOhPpFuWw2!
ze1O6^8NMkWC3yiDMP>3Rerj*;=5L&tF3yB_7`c&m9Z?t&7&3V<<pVfTqvrsivcw}D
z92p)cKG2tyhCyeS_FDP at zH1icUZqvx6fx;%hq`#e_FqGsN>=Nutx$I?jy+FPSAv4B
z1-L5$O~#fNVd}~<{{u{{x^}KF;~3AL@$3aLo8hfvp|g28IrBI$AJqOOx5#GU3^QBh
zQ~|mAUkZ~4;$FYrh0 at W{u`OSrG2fOjG%+yP{QWfGeUS6+ZM71)W!g$hC!6&F`~Tl4
z%li&)ojj!^D+3FP%j~Triv%W6Xm&1i>s)Dz7qfcH|497lSv}wh+B3lncxN{7e(Q`V
znAO4`VI~Lq_&<T`&fT5dgYbWtc`pgPOse^enrI2X?-Qo>zd5q6ws9r*@Xx~EOBF-9
zd>}vmw at c0W%V6n#F;q at _5GYe#Soj8Y{@_{3oh#F+hjwoV;d$&dql>(ETv2*%lu^sV
zemRc}+hdidhpFSivHZw?v at 0e0pQa1D%T2w=hbet`Bf#XS3l1DKI_vdn;4T2}Ibokp
z5mNcLO%-X|mq6hmyIHpRvH<927*E~zLY;bYxnKuq*Z<c-x!j%(;GMO>OpI*03h0bD
z4}fF-;mNFa at qoyyOc5!3Bg+2 at za^4TZ)jpdcbPS}xB$9l at z$F#=Kq%a;_SegFUPJN
zzumt7-<+G(o-VtJg{J!@C9)g{vsB*iyMz1l2vtK6U&NaXkXp%&W!l`oq8eCDzj2Cw
zWkY1*s1pcuO(SOf$pnuIBuw>Us$$Tk5K?00P~H*xzSr{=0K^`=sbweLJUmuhCh0q?
z#%LP&CIDhkiZ`*6xTBY$`yVYFJkz|R`1ln2)j2Ovcjr3jdk)WoMoYT4P;>>-MVcO(
zNQ9gKuqbFrC7s>-aLr?6G|k(VY;Ki*Ut*_)8l}Vtvg?;`8 at o|1z4OT-ibjK=#GWXS
z=$`)quvz_Jw(l8ZZ#|*C`r?Hg;JMZLThQ{bn9plR!{raKT2=u|-Saptp$pXI;tsLu
z`_2aXWsUA1M7;ZLtq%R7kqvwL6o*t8xyc4ZD`>MVzPdk(N3tE7=Z=fw0gsOH#}p{m
zu2dD`#oYg{psQHqIW%nP*;&@@tjl;8R89xYsZ7*Ob2Tm`h0q3`^+q-CK4B=TnIbp=
zCr2pyuVIHL1lCr7=Rs2J{V&RxLbDg%&gr3kf#u}ev%1=jJpu4QXpIh8=Xs&ZyNT!b
zH#=JY^%Fn3q#4!!#IvOmTQzKLJH3W7|cQ6WYGm*JSvdtXCn`vFUeWGe<lGYUAi
zrSm-wKSi0;+kKJ;X-YBv&)Dy$<;p|b<BPnVMFJX+6##-RZb9)-m|qg{B%na7w%)yN
zUbvs9!w+yyXkZ|(Ks+C~x6nbKW?YykG;|)<1LtP&K at L5S*EQ%lc1n6D)g`KV?v!2K
zv at +tpi4fB@nJ4QBhOdZxUQv|-0)-3lg2ox;4lJ>zMu(x*kZGBn161LvtR2|5M8sok
zKtJP(hTBkHyGv|DnHyGQ0YKLqS(Ag*Ouz9S<g{0Q!<UzgCZ1C}5HowGI0f+y5!Um7
znb@=}(Irky0+dDu`hUG?iaY#MnVOU2f_0HTZf4;Opj}Opfac-;JcnJfMN!p#_-Y7E
zhO`fl)l)(Nm5=~+l0=VDltlCuJWRAd#$)7f1?NXBKHj#Xxm$Yh>_^w+{rr5Za3LaP
zoZCaXw&cfHUpfE}GUR!A=^=wg4zX^(-v{HXfjNq3s{N>=L^)4Q0(jRb9_jC63rQ!G
zdulXlEIHY{OljhE9#-53GqMxO7S4s0M?vr6fh0MWFNm%yF5mum*4O6dj}b?pUo=*e
z_14m%J!1H3ibmZVdrv`7d9y6yek6z-Brw7yT~K0}e)#86?p at 3Q%1D80fio%n`CaEv
zaiPfRt$uFmga;jQ+Ouo;yPbgBw(<>ext&)h_7x(=i_qhgrU^UNCo)$$@B+z)rEa(O
zm(8@%6e8-ME(b5{M7y+6VZm*J6b2vQee|?-dQd@|q}Zkd#!ehC_5jXive~nc=8-3e
ztTDf5W&*JKy?EYeq#y`~4o<ZpW{8_wRaWWwu!l0JFz71AMyC`G{qe>=^;8MlNA%_c
zA2EzY&)5m^r#b9Te2n8X*4=zEr4KzA+UBgYlb at X(4yb!K#PtdO at D^Y2Jm!vCNsNUM
zB}(d*JbYXP*SPhD`HAufzJ5ZkjAdk9TvVJf2)gZQ007q8fVbXTuT(Dzy5o<uSw#zi
zFt*d;$%grPf_0~q>#Ib7#x4Ewyp1J%A=?LNS<`lOMAIa{sn|M)&sH1|TST-8oNXUg
z0FVd60k5^S)rj at ZuV>Xc_b?jmE5>no#>J2D>&-iVYj|2i(kr2iRgAjTXKaFSRKK(4
zD)s^u2MJM9YZusfdF++>WvtcOLUObl`jt4>>8(=bHe8FRTR{kl0z9X97q13NQQB#~
zVDmhpt$nrWQA#l|K`APxENsrj8gf`*FY=4 at 03Bjo^+}gTLsx*`s;s)AfmAm}(5G^?
z(EN;LTd?7w2O%5kH+kwI>%+)0%CW3Ku`AC!q_wvCz+Y=<3(2gr5tf{l*JX;V;WZPr
zEXw3FZ8(L9h=wBFpQD!IW?TBUCT}|TUNK29iaZWvu1cbgFyufFJ|`q3(SZkMkpD<q
z=I9t at OCQw-kIfUTsu6r-Rto@K=$C=;vSY@;T0|&W07Igt?ZM$HZuejPj6HlNKj&y+
z^x;QFK36?jZk^8>ZV6s)m~frLXtpo!4I6gstn+yzvm3qnO>$)qIgJmFiPlN`7MJe7
zP;0juMyZo>5qVVSXWvyAyFRBSQ2!qa6ZT1q3FiNYLZDqzH?@pGpLy$;j<GElv9vn$
zz|a?N4_;rfwT!HCOb5&<eb4ce!rmH1c<tKPRBezmc1Bn3Lg1w5*X+K4Q)H{shv(d_
zF3A{=mxe`*n5K&c-^t|ZK~$=G=f7z8c6Kufo*AhtuZ&e%Oy<Ax%mAi at t<Z}^sh-k8
zedN8;0jHR_PuI&Dp=@bTW{`T%UAdd>_OmvCAsfpvjpamELZ&WUtxs?rLI$H>h^DkW
zf4s2wUI*lypziIRQC3=LdVNSGwXA7yT8$rQDioJzSd=ZbP&!9>Am^ztMov-}K(My1
z(#hE5#Dh3mh(AEJtaDm}+1R$$#Zl)2u})t#E(^h2S0)=mc-jlZn;yBw@^;Yajka$i
z%n~go^cjL9$&D799ip1;e#X<?tK68RNC$IAP^RO?tGiaqHtzqBS}%`yKb|5{h!+=Q
zx&}1nQE$&_m>jcKA)*w{B{c%Q7%HZ|k|C01VP^imK3oO!6ks!}jyB-;S!I3Sivq#&
zs*l%)w=B`Lc at 738A_a=t>lh2(Df}GsP2+OCyo`c1Sf{a*jm=u7H7Gy*6;x#!in5J~
zkbzT70shtQ*P)Jq)ZcNQnKdB42zy)x)aS*e&sL at a^95_}%EYY}74L5W<9r0soH$A+
z<tX4&^)B)V-~~Yo)%`~srIC0S<aBZQQLVaPDH_)_`lF?ou6Z;Fj4fb-YYTeQsBmUB
zi4d9VcJSy|^_htKxEE at Bt94%;PTCG$dSXo92(NTVKO5v(IxZ2#rBPx%()j|2Fbk+)
zWzaQ)rp(jH$Yk!5UNWhmiwe^6zGPfSS;Mmr^@a_jAfqckluToIc}nIuDI`oty3^1r
znavuVGWtvoVqY3Nn-6>SB6|3EKXgfe5p*lix1s~XpgXKE9XlM^dduroRibIxK97mn
zWs>4*6L)Y6Ur2$-5JR(vH{d&}JVVK%lZQz-U!OHDoQsr?js{#PqpVV+VyjYvzX5;g
zN$=kFP{*nBk5&|jMm#+g^*px*$5`aHWBIQvPlsDve?(mkgKjk}M3`-jK81nf89S;>
znfLx(<|E$6zubMOg_`h?+etKyN|$I8XI&ky-h>P|^3`2rdq|9Np34B at 9L~p7!84@@
z8$P;)B-@})vb{I#bvB0v+Z{VqLOJ2AR>I>_)t}x4rDlO1<oMmWEaoFFcYe|NvJgln
z^=6Q4A!$c>_Yxu|52T4XtqAxGz!PPm=v?=w2OXb^vYn2(sxcgQ#=7w+xW3Uo!b}ms
zUsi_mJOi?v4x3<~Uq at bA9%FOCq^@{5Y)*$<10LtvJc!W2^{~8cu=sxb_IRC#Gy4M~
zo#(~90ncgRxd(}y at YhozS+yS>>n&2gDB3H;Jp})rEp*SPV%I25e)Aqf{>mtnQ{=Ct
z6g2)-(&tYyJ>_8$ZyxLJG)V1v*q#BsJ0pIrJ$G&4Xq%Pj_Tisnm0wRGXQagR1diag
z*w{Gp^ZeY9g_XgS^VDB$A-B#YOF0}(XOO41dQMl;ix!@lSO}ZlNFu8|nX!xyUb|19
z;5-s~<=ctBJqrUHfcS|R50@?ZFyT&2=6to@;<EB<#(2H9*gAB*p;LvGO~Miu*f(}3
zEQ&|=$JlX>{TCv<9!(Ns?M^dy0lpKHZG-Js`oP>1&5f0096p)<$zzM(hY0<LLn at Su
z3or0NFN(B_jA%Io9KEur^DPB<x?QW2oT*x1{RO9B#xS1M=b!ax85dV=r_Ai{bz}tl
zR`9MY<#SW1>mfX9B31_?)lzyN&k72?()x$`(`;<Tcn{Qtz)|x%oxkemUh#ARd!d4X
zstfEpuOrw_DM$(=pxee6^kkkHHe!*?IKD3@<6GA;&~3JvBX8=5n<pOuk1zK&sO!;6
zKSLi0egDWmel2HAasSs(5+s)Y32qx68-;tsY<KMB&7E(Yv=?e6;D1y%;G7U22&De{
zk9z?A=*6O`4;SF2#-^I1gMnvG7m4{^c&>?t>PnBMndHk)a1Y`2vGP}TCGkbYQ8YXs
z>eQ9d|A((H4}`jF`yZu6wxTFP%91?_p`o%xcG>qNJIR(ATZAM_)*=+L at 7vghED_3N
zU&fGq8DnG{jNy0YzVGMx^}O%f^H=}m%=w;k?Vrze9icyb)Xc9C<{-1nRX%+?zx<_!
z<nWE?kpu%Ct3X5M!Mb9}@zX0SfL+ at xJn6!wP?++E{j}#u$7d>CKvxx%$*kI~#PfkJ
zEQ{i%#xeubDGuefmaRd9;91g`BP(&W-e2nSkn^85V<4lH<{nk%jAnsv_O=6UusDGY
z8}n;4ld*0h_J9j(S;v-;mD0N2^ZIrg9Qe&bjz4T_=)K&*c{QQ_F`X+)=ZOpHU{Ay}
z51p6?PrzYY-3;(w`}Eq&RSs8aDd~*>C&clGO*xuJGN2s<tl?W?AT`^6T8QR<SNct?
zy>T-ZIGHN!VsHEQiv5Zg`htveGBD~}D6+olpr&V!LO#4)`^U>DD71~~Qe`v;N3ux|
zj0~G^beFB}eZA7YjkR3?3JR2eLnF;;C6tu~-4sx&5@!My`b+WuuZ29P?pnJxVMbOl
zkr;otb<<8uNw8->2qnmo?#xR=_ntK8l{d=A?~B+4r!^gJV=p}<Rr3(X-wjmU+y&X~
z<$sxVFrw$${tUe}N=9VlszU0ZQN_NrA>zHX_%o>Ox3%umqUKf9#7IdNbXjKE33j`c
zZ|}$oLidN0qV7)j7u$}J4@!L`()c#=?sDCjoh7!2H1qO&`b_(=ZUGwB#2xQ#86W~o
zNq^9fkR{&q^D at 3QWl-38I0_0xk~ZNpuD at f1H+>WAr^%mGRaTFC?1XS>pYTI~<Njh#
zwqAL*ZtD~fEroxX;LDdES`pW~+l%Y+%L+<dRtie?<_vda38tmkMSK$x9JgPBog87@
zvWs*`vM6Ol07Dz`r}M6x0^MBo;L2U<i^|gd=98?g>Sv<vJZynRy&T-=RgT0Bc8 at q$
zQw?D}y3d^X!{K0~HWp6QcRBcdZl$x<Cf3bLPEuyZ-J7^P-w`b*;WZN(i5o_ZETj{_
z67qjqBTaQ5P+b&0Fa)X6)OnV!T@<~1&#<QBj3|8Jo9K3KX3&l|IYT8OEKZyeb@;=<
zVDvIOZ*Uvx28%0>2z9jRguent7kTIptSmD==ve!NM_d`33`_mtL37j+Q2REIKWUg>
zmDEcKs@}((V>jBsN^avdmSKH98~b7kYIw(z3-w+6?Zs7zWq=zBf=A(=Kj_8Y43uyl
zof%fv8pcZO<OO~u6pnXZ5$)IkAEsB7T1#AQ_rf`>n1OG5`Et~dZ1tCo-ur@@-I;&+
z!<R1&oAx&}twkDd8ggJ`<lpibyj&Jv`q4ZB+Q at +1ytD>Qc}^)3?P;+5vZjM&kX(Hm
z`qO at v3XR&?eIAu=6iW96>`#Ga&W$~6 at X~8)<~^I2UskN6FQDrYWhSo9WJ at P?`C;*&
z&c_vK<_HcUSq=9F`kMMe_hT+CZKcah5rVu>Zlci0Bao9 at OpGj~ZGdY}8u-JGrnMp#
z>De4sem4JQb)v-aSt`v@*>Z;~Nt{d(?(>w64;@B>-dqXBl#_Fv!LmQ-dQAgfmTbpB
z#_^i80}8{AL-O?E+aH2~(bzJJRB8l$Ju;qRAaXD)IeTWN;t$L03>+Z>n~HAqi}Bt2
zrQAd}^qPe<Kz2WkOG%tV1|89oUW0m(X71lDxe9?Hd#m{ed5$npheG!jEi+@>dqlSh
zlO*Z$P2S^Y)V>eZ1-e{#=(mn`jl@{fTi1JuZ}-RmkJxszuv6BXXG>5+76N&7@}FRZ
zJj)7KSqoG$$9Lc=gLc(a9hL>YHF)6Gw)a7a&Em<OHJjE>URENu1emY3nm?@3rlsE9
zAK#FmFSZ at Ry#MqyDq8kb_M?D9q(!J<lHb0YHJl*fwX^oK`#6XYMP2^SZxXercEMcQ
zzb)T(-U*Za4X{P$g6WU5ZGV at fXV7(#YM-IvJzq~;1v{%-%t>ddU&?OZ#yPH3p!R at h
zi+?&`Bd4U at 0f%P4<pZ1#8w)$DUmLz2@}N1yidchwMWs({Pas$N9qy_}n8NI>JEFF_
zPh($!AOOBn)?V$%>?Dz8I6VXk2rATh=^3^voyQ@!wf`N3ycKaQ7rMffS#_?}7A3J=
zGxOTHzkQ^lM{FB0xi9 at zj>CUs<bb4-Y>9CvJDS=rBO0l%G2Rwl%)pnI#q9CH662kK
z_MQ8;_U)GQpv4}F8ly#h$8K1APmGQTn+0jevWB$fWSd)fya~8irsX$7Yi(fieMEfd
z+IA6wobirT{<pQRiu$%tT8GhI_xf?k8ia--9m at rg43#F!NftnRLT0 at uFVrujH8UUe
z9-ji{g()axK(n_Z>k;s}x&QXM-+-S<JQLA-H)1L|e=J9{^y^_52XF;Y)UEyzT=hou
z6{eto1N)T_=HubZaT;RVNfw|~^fKfj=a%lKAm1o(mi+&AmK!$e)i!MpD^FY34CU3Y
zt6Ix=Tq8%>y|-Q1OvM&Zl3t at Wk!;u7MdS<B(|yJ4QVw^c%Pf5V+FkO0-Cfj_`}}$M
zbT~+6ZQq|+d5j%u9`~ES at K?+cEw?Y$mb~4s8N9_u+!o(n${{Pl-Kzg~cFYfVHkaC0
z>k7(JWmw_;>g@*`(H1A%-HEtJD9PjS=0kNUZ<kWWXP{-*9L6W}07Rb^|82dbhdXWI
z?Zp*QWk-)+)5)zP!^Ax-x<X~yV4lH<c4p9v16ksEVk<pazv?Xi+u;efuPJkgd)3ap
z#@GS;D_~NjW2Ip>*0SOp;pRhA2Lyi5K+<dR$EA<>#4DGFxc{5{dWwf0k`QIXri at N!
zGqe>z9eTL=OF`4r=jZLRJ0C1U-)dI9L7BrZpg%>FAtqOWkPIpQ*Y%*Gj%;OdnsgGU
zq;NO>>T?g)BX$Y2d*OQ~X=k*tduATm*D$86d*4zP$mO{LCLK9NRBS9QK%V}cD2Q4N
zJu*=eRKnh;Ib6SQO&Dn<s}JaolsMJ-1xlcium%CBen(E4ShXgHHDbJP1SLmy`q;Yv
zCL()Gt-R`DUdXN2xJkKtw`=AWi8Imd)vm59K%Q-0*6xkkw!4%mmTD}#6(##5zyDiL
z#geM`!PJZGY+?tvwsb<5DS37te8x|6R8p4MD-hA$VeTX1g9_IT*zQVf>#*>X^FcZd
zOtwQOx4>MYSWgZP!gB+CK at BidYeHi07<}j at Hr5<IoaqIIgCSvVdBc`qY!errJh)~>
zcScUNk!MDZ)Lf7vf_0<`n5O#6+tEtgYm4#Tf2y`M<``)P$61EyBWOff#gT-anc$_y
zF-xWX0J0z3>NMTsb1V4Y=F8o=TcAjKK)7l^r*rnL2~++f!-Zmub4QO^92E!RmasQy
z6`93vkcTdobGs3a($UMUTu)YKz}6yUHHR=8A3!_jUAMl?S}<HoM3V=GDPPXdG66&P
zkWFR<63#dugYlI?6k-pfZ))>3gSc&BRSKQ0>5n~>+xw(qPV&ve*5m8<G9+qiR;5Au
zM<a1*XIY?$?zpg_<w|}53mon2e_?w1LEp21tJX4r+lfa;k`CY?4SVSK;4t**p_!h^
z0THkKTYcrshE1{a?zj6T_tV_hL~~#^A6c|NNfSBTf()2)b~PYiR!nz<Qhq0SY?luy
z!NBCoT+M^0ozTfDVJ?VB{nEtjbqA04Ci-k(_}9p+XA-&Lgxoulc?rw~y#imIljM&3
z!rwpH3l&M9APp3B5}itS*JhCx&usJAe7Z^7a%g<Auk-lcZu$9#HA+0GFo#m at mj~TS
zluLe~NrLvvXaz~E&7*NQyG%Zma`_O{klOZ~ib?S|2P>{pK_Fkk|COCSZ2VRw)C8K?
zt3iLP*E{oW at GYvj09fRT6Lc|pKQBYExVPAi=P8l%4Z}2)I~7s`UT5^+_4(@*&sve1
zJSBO(nM>I(<e*cG=nNYHbc5o38JBg0xliMOhc&cA>Wb at H*L3Auut|sm*9Rp^hescj
zPS$z8!UWUPc|>1P8oVkOx!s>D7kOa4ymMIL&0(qYVCkU4<lEaoPTS5-g|jrdxmV9a
znjt)A8I`V4zq-n=@<lNB<%htNmoHs9dFm9%t_7F#Q%o^~TdNaDV$&1}U-CE~vG=}H
z4dvnOH at UET>!zoMUe0pAUI{PcWoG$APF7sn{X6V|mtLL^9ln|s!QSmm^Zb=gn!C at B
zAMU}VH1p~n0o|a|J$ASus+E}K)D$pum-Sh@)KJxel~>n~DG9%c^7!@0^n+eucF(x)
z7PY<ef at ZeCs9SQm@V#2*(Fzgd6f_FvYW8f9c8It*gL|PHI>tzCa1Ic{r<J4gX7X0E
z!qV60S9GJ>dDGy0S7kPH8zuH*wYSHVo7D4kb&hC7Uw<~@(>z0SljH=q+uq=tQ{;vW
z2kaot+*+T>OphFRjWn^F;9CXo{&70W_~CPs6HMQ%B=_cJiU^{ll at Z3{+>D-%)h-oN
zSCrAsVQe<MbnK5e^j+4{(kCh+lrVN-1S#PWi|F0GjwT|(`ddKnUfMI&wTr~G8#m9h
z7vPdBG*R{#;^gs?64Wo=(YCP1$Iz!(>xkRKa%k?%4z6^Qk}FYEQAQ&R=ldL`>n0&^
zD@;8f+LH!3IR#(l;SrzAObhF|z;;;aE=;q$yx&`0nilI%I+4nLi+&3-AS)p0$KX4^
zhhLP+oOmxyAI0$`pY7>vSu^p&hVG&4{wDQ=yNJp~^po#YxwRDH${*%Wd34XHArA+r
zwU87TvD+Ja_Wp9;H*$6l7S*XQ^>n$X#|p!QckLlTm!{8~XT}J_1QDW?{Hi`ZbEEgo
zX}4Kn6ZcA1w&6J^Y~ncHrH*H}MOIhgY65=9l$v!M4Qdh>yiu4*nOM#D5iz47b8Mr+
zbJgu7uCl+Ux?8wDn~OE!WkG at g??`dh8wRg2Wh8^=YdbMzDo$s|p$xsX5w>mo#r+~K
zLyxl4xXA)S?;Vt4f<Sm)mgZ*Fxg^Lf>mBYpMg`SuK^?st-jkUv41p0Ae!Op)U6ahb
zsTmwq8*o(w*+hf-VklvK{t=X$us!0giG8~EQ9sB0DAxb$nM8ve^Cu13);yHx8x8uM
zC8^#MU%b%BI%LhS%-I9j;dY*pz|D41(uv~_lv`a}(~@U+<j&qYT=QHc{n##WtrZwy
z;#r>^zh8W6z8~#gf^^<UlFM!YVDGM}mhwX2ywbpi{XmwHYHI9NsvOGqQT=sh9Y3Li
zO?xKc4cDF*yl~&~SvIwwT>AVI+E at D#b>n^a+Db$*%>>7wb8ns5nyio^>G4N*2)p-s
zjAe>!>p^f&>6j<>M0C;cM;}hif-wVIH)-Nah(6VjL$k5CVZn*|Ik!(f5OIzV^T#~8
zPg at xG8VC>_LM)0lv57}-k1w)xA&c|*j}3pOywi50L5b6B)6m8G<b(M2v~JbYd=zuS
z>%B+=yruSutq1*+%RZkvw4&?mkWZdRmhxJH-X{j$F`2cGQW3#8RY(Xr{;f`nntNRc
zn*D9nwRa8FjDgEvJ~br-zLrIG7qc=uark`y-s!B$WREV=<)Avy$CoZ{BxEoI>gLEX
zI?4I%eBpyq56Ww!Tg at FqxFKk~x7M{y&)zYiPUivuRWD0AFmR=|%zz}{FJXt8(Ir1e
zA*s4R(xRqmek8(0H at cx+J;elhJTgenwG{@sb{yu65awaLh;tH at H`O4^*DW at B44Pln
zF4N||_*jkgqr<+6jbQ03m`!JRpP$Jd)+^0jyA%ZvCdJo8xX`0m6R`Me6uBt#jopx~
zN}VdE0&Ul(Cn+#|Uhzcwlav$)&7=%+(khiUeV_B)4{i!Wufq*Fsua;HO~dF>Ti%s=
z{JykX=G1%RGll|3K><5mq)BarikGWUJ!L<u)`Sk?_Q~-op)Nmvv6ky(&`alMk$3AB
z%f{PJ=U$zICbee8FnCN!aDR_~8P20pX&DMjQ-q_#=Y!Cm2uv`$>z)i~RXSNnG*pcV
zQQ_xK%g5DNtKPkq<>&btY?|E>M#KFiV=MlFV_$<}%p+<<2=pYzTIpN at WMC$@<AY37
zs(A%$BcxMAlH#WKu4D6Lh*09w?=|6f-oZ?k$)9c2-V0kE%ue&|B^ank59*aDkcvKj
z-6QUyvtiMU!aW}qr8d3Ys6M;tvTKYQ;IXTN1t*Xat<EeGMYAh;wic)Nk(PtmmXrQR
zH8<GnZ>rYtNiC&G?wL?M-nu+E3A!1OiWuv~4ZUd88orMUl3=R8E~aJ~HV~(^bq|s_
z8L+S1;!?{U at G&OEMRmmDaCta9E?QRA5K~7*>`w<Ve{d*a;)(phlgZAwjj+QyPspVn
z5863%1HE(5B+5Id%V<lq?=eFY(dUnx`_!;d9_z%ykjEK2C9##OTT-z2M3q)b at 3JzL
zOcE2L{b;tImWUdJNutoO^+8qOivtsPQr^VYOwO~d8`0 at qpy?IX!1rc$h!N^KNW1V^
z_sClAjK=1&A~?(kn)kT(#)o-Td6sqy(tt?7fAIOttq#xpxDs5R!Yg}v05o;pw}8vd
zl at YMxLW>w?Og7j1WrqWBnrVT1CL3aP(U}B28)K4zCxHHX{HGG)JI;L1%Qa7#g3Hbq
z6NBoe_Tf_t`L&zlei4~m84LxhUcE7{&}@cUt0O;1Y6{S+Xpa$*U2l7az&%0n7bDO_
z*!6sNs?Rt={UrBF!v=$XiFpgV|IG>N5K}m=X)M@%ANSR-!j`vCJ9A&r{b21x(?y8Z
z0Wsd=hr7yY$}{LSmJp9#>)J?8E$!+B(;+R*E#LkaEfU6KGO<%3_3E8W_U8a6?Wt<9
z<D?#>WmH^^(F6!Thp&USmS*R5#6%TB<5XA6qj(tIvgkN0<BQ+}gXn at LAa~<WU68pz
z*_V$o%j#RTus;PY#=ecoti2fCvveikoQy>!_eNA9`I6QiK)Wuw?N&8Us)p8e(1$>&
z!QtdfT5P(_mf0Fm+DK~D at a~vw9C3(3mYTZn@=@jc&|&;C2jCAjl$W7>va4&DtKW&a
z9S>YTlQp=%$RLgKgY12wtZ8<ETS+Pq{rB{;_eMlJW>G%XkHRh3Pb{Ast+a13hkqB5
zcNr_^wt~WW3YDW at I8Ur39fcgI4{NP@>go0V{{za?cMh{`yo9(z$=|a5zI$rl*Bg56
zx*?zbp&uF=ojstv?%EVw2P4+00_OH|bv|=ylStObN`7Dwfx-DA2M#9deJDeiJQTr8
zG4esX-_rjUqOn^YG66_Todi at 7b}Wt3^5**yGoRXPHtG<5(QNjY%U^Q2(iprAFgG|q
zY1Hhwc<7lMjKHs}aV<AUeYj6wl4e&AGhJ>Hkfs=Eut~Wa7F6Q9(f<L+!Ye}GeJbn#
zpA{0mz^gpIZi;wf)v}EinSgT6{0!q*hc`+%y)8AWSYLSVfrWtY#bq5V6u`~&^gN(R
zmrh=;o)SN`RKAc_Pl==)S2~k>674QrkdruX^jCB=|FS#ioK@>YAZL+N!ECaOhv!zJ
zNRVGEm!3Vi({J(u5XWDy7lT;$T%G$xTPS~i__%PKicb=FCCWi}(d4(|;BL05*=@H{
zCT13E>2Kl+f4WLi<Q|jqu+kklav|w8J0sUnnva{`8ey9Y(pr1`M4DR^;bt<g7F4no
z#S0AkWT}Q5)$Lw)J+Z9}ZOP;KOmo{x(@ra-TynG~m5g@)vP9Z at AVTb7EL)o7>O(;o
zs?w}AaLZef7t*SzH2%oOFq+a5X7 at 3sg9D`*1<LTHll$!@aotfe!dALDd+0Z$8zqw%
zZ{9+U)P2rLw{#){L=`)1;e9fj`e{i){;T2z2H-p;$j)<l2;J>}2yfCR%SBqZ at sfUo
zmR%#V*)|QIAcu4^yJnFxzt<n&m=Y at lJY!Z2Q|^!IK7dr${fY~&lkZ`%VwK(XD?J~=
zU^K=9Y^1=C<=Q-DgPOOY)k}t|4T{VIBjB(Y0*oUK!fzDG7C5Ws+D|bI4)ZsUfewJV
zw&vDYG++rvD-^vioXIozsK2I%#Hm1D&HwBt7M+Wc^xx~SY-SKTmC<JAK at qvMVL`J!
zxY4&}?koWMg?PloT`k_eb+<of$0m<|Gf(xt>8W+;dKrw@;mvtybZ7K8jUnI_g|^Sg
z79R{gv3I6t^f;Fa892gk{7Au_!a>mvc|y8l>utXBk!#3<ZNv)<%<<qZr}4!pa at nZX
z4qd$;a;fofLPUOB2S4P6V;Cy2Xpub}7dZhri>t>MB;5>Av*E8n0fEaV3tV5hCzLqL
z_^-3<p8TqLuy}LXem*gR>Fw77Gs*oa8oS~uQE!d=LwsQV01{l2+1vOtTRgXH7T10@
zfiBmpAN887X5u^a>09`sdsKX0lMjR8UERHK*(ipGs!u{ZPXW8qk;OEl^;Ed71a^HH
zNsD!MJ)#B04ipIQozFNXGSo=)EKiFKmNqvmMt#bn{NN!s`|^Xvjqrt2v3}wXFw~uN
zeC0dphCP<hLKMHTLHPVNdqHq4XSqEFWp3>4w;VHx|LVb!!!?E?O?-T2eAcS%<$lOr
z_Gg#a3o at fvlS(<|fkBp4T?FV`i!nlDWPQ$G3i9C*zR^Oa*3gpYYK<`0KL_%(t=G(H
zRE&HecNO1vCv&JHKab<IQ=0Zg4Vw+zDfve;&EdUIX>ocjj-;?jIFWzpobb?;OB1}^
z6mB`x@<BNTc69kXOY+5{9CJ at i*~6!6BQUzDh5&8YL2sLt)c~tzhe&=}Q~`S+>>@Wb
z;blZ3Gl6A`eYwic;=B7|qGS5cDiZd5FHV)e7>3;#=-3^`8elv*0J+w7G?YGgHV1Y=
zmazV~#I$l6_f4_jC`^5dn6D@~UiXnAC!LLpP+4Nabj~kY0eT%XeD>3Z5_0W<fqXco
zPWAYSk}Z-e(og)GX-ku5RIZE1`ErI#MWn>mahkZ&<)U2tE14!Mdw at y7!}@j?Gxw*;
zoX5CUb{9zNrt8rGT!l8>FN%!kk~n4T>JnL9zu~`2+3U0%>}odAK|XlQ%c8rYR^UP2
ze;c%9Iv`;hWC?q^ea!a6Mn_HLWvw&(8>UJwIt=RWJG4;OEQ3Vnfrtl46$}^doaAfz
zMuAU8_r820jpNz6=I4?as}B#TWL8SY&80l&y7tYj54SA*FNmSvOHnxvT(w$wA2%61
zD0Bi6|NX5A-ASd-HN$#(io@=COXQU4EIDPe=AE!zpquP;Jp25W*1H9<rxBq8=1;zO
zHfy2HywOmbuTQH_8B`CQwxlHv?nfpQH09gsw{LyJni;c5rxB6zqa^$xkJJ3OeXi~c
zwp=SIu0X+l#_KXj_%}4My<5T|7>5cBTI#$tM^aq(<{*LVQf{}E>jA+RiOtW?z}60U
zK<=D&I|X8-q!7o6azB%mRq<E16?4D2U#L}VeeZc-w}>aF&0Q4<<`MIB1CESE8n;&V
z?<3C at -@o7Sq{;hgQ_~^T3V}&%(t4?{N7}V*(vSf7^*(2og&(*F_x&Ycc at Otrh}*@a
zzddL=+yH*1OWE0>sKKwn-X&kB*^~3mY0<xewC`?u^*!u`bT2Fa`uUvF-LvlDOp0!Z
zgoH-xRRSFvA041J62i<==z at 8no|0%n9io;7Y*S%9o1W&vD3rhj4D$;!clDNoJp*vz
zmc$06rz9Go$BIwR=U;;Lbe;flO(r?6k+!nrND*yyz4#obH at YBpT5>O{c2efS`)Iiy
z&BQ8)gwhad(z;U?7Z|ox+q9-qZG at Dz!i~*|sBP>nkhSvR_|581sZ!Ho|7+#s?Y0W9
zXRe_XnbUgPwKQ#!e{KDoD{kxsn}=2l7{F+iBC$I+`WRF9LfkR2w21^ljk&%jD{-1V
znqnd*^B~UPj|x4Mu-8Y=7mfck)79fPLrLudiFrtt7!8)k1_CA0t%2H8{6y2^(@K+~
zKo>TD+~urz%w6a<^CH#DcpH`JJ!C{xe*g=R;SY-~k;KHy at y@h`QxO;BNYaqNS5;m(
zm!WETWjriD|3A(}#=ULStCdy;7Rgl#&7TJAEBw9w9_mnS<OS7kcuH?3<91Thj%WGv
zd8vVmVJ!12iJZtZe`&clzT2ZzL=v9#6mlDOu^$m0nfk2B8>%<x0bQ6q3KAP_a$-|7
zvRO7ahwFOLpR1+kCVbQ_1OFDt@!Sv9B4&&C=?eWVw4)=z*pFchUTtR%ys|omigsJS
z)_ZnGew>BGs8=^2GY4G_<*qQl>)(1GM-Mbu321P_RGaP|`h`rsPb9aNxt=Fj_v7E|
z?rrwJNPYIO#kg8+gp41(Q)^AG_fQ#mFXexMzb9Ib$EoclSvX61)4rw$cNHCl+$+h9
zcD9oo_eXdOB~bmod7Ec$Gs^k`u98Fye*ZY*)#!Zbq~|q1yuSg1?$EI3;4fBWi+bTa
zzL;d2kC;$3Yz?I%&M`O9pck4BWvu$Mx&M++KXJS$<Uv9RwRhDJ&KR9oayV^v-{#0~
zm#k$ZT(hsr>ct~)B=g~eIUq&P|B at m-WoZ3ll!jq{HWjNFmle8t*Hi3GsF;P<yC$|d
z!g at EA<D{&WACU(DW&7X~=61G*Ux+(ZMt?W09IKD&I$M|KzD at xml&cL&AIlqljnWTQ
z`an&}ADt-qMcI|lkFvQ$x5?(-<edvU+)-{>ZMY7yu7YCKk at Ulr;Z82&uUUXPF10)^
z79Xh}#(wRRRgp2b(~=H3ZKw@>uivc!#oofR at W(CwrfFR_h#>}rF39CVwJ at Ci;DT5T
z_OGA#dNG-Z>$|@Z7O3$qc9c5g*6yPg*yh)C<{v`xo#dDyk}ajQp<#d4l_y{ba+;Z5
zP6spQ>0}FhTyK477uSD$ikTPYyiCo)#2u022+m`w0wS(qYbR)Lvn<NY`{QGLZcuK{
z#xYjo8>vzxQ6eZxmPX&jk5nnL7!mnjX+AtwVBqCSVFklC at ZQ0N=>&rUBb>CjQ|BI`
z0gh4AePX73T0yPV at hm@DX7a$HgZ{i_aM((#gXe9UahRk3U6sQ+8wh{<-WC;hNT?|X
zgDoM>>EVLu$?29qN)bWR=snS?ne2s2HluEW+%AR_FfRs+E0Um%mY=-SQ^GxHS9vIb
za%ckO&>LyM at nT_5B<mL+#s=Ol?mv~Sdaz`Hpt=18wzu>`+%>KAPA~&tJ^B0S^*L0v
zh7r-fTvcPtpWHcU2nVLC)E at GcOZ8x|peYFhwJ!D!NXhd!#Q*>0N7p(z3f-e!yyi!J
zw&f0IBuhby%*p|_i9y2c;lX}jl0{l+y8k-%!Lg#mkt~%K38%I0=~c4q5->@J?;=?e
zO2M3OV8HXELP_(3J;k%>iwBeHCxKN>2Uaz|a%qF3&;Wm>2=C at uJ8}&2;RxTa954+-
zRrMsl=;BbG6Ezc;(;;bt(cHzzw8$u~l#Y$TYo&~exT#APqAGU(N0QwINX;P{Vq_nw
zDdjZKZdst+dN^E3K=sFk$jW)4^%W$+Yn{au^x^okoGRpOW??DhY;p0i(apdDrW4L>
zV{4d-_2xAzArl^_PKLkM9dIWN^gA3cAU$pk;t1*eZRK~<#U~yc7d5bW^^de~A0DJn
zLXH9_**|^Bm`fod#IE-=qM`gMr%}C^uR#@wILYX>@qHKH%;wFtl76^JK8c~0kb2`*
zB{(|IHuZ1m^#0CNcj(P{1SlFWNCn~wVOV!^el(P??g#gGOB+57egb#h)x2dJBWcB(
z{=O6?yU{HW%wU<5I`4||bYGZ91wWgAM at 7WBl5OcAjEHj>T6hZ^%6Aq>skN)}Zc29k
z{a$(MK&jlZ|6?33z*5(ww3NdA7=_Vmy};++K>nkfJ&U4$w)APY09EJNw<LFX-Ac%q
zYh^BpRBoD;aD`q*(8{c?^yDeXql_v??`rF4N4qW7p$b;sach&tg9z{F<jDW{trTg}
z1?)I7X)w<ORB4|Epx2brKn$`+D-rnZ(`@<}I|-Xek!u`q;NDVP#~*NWIn|qIh2wAy
zo7KHON9EYhg9!WVz?m2!tZGQwqXIxY<?uB~%$@4SicHLn>Tq!v<L)u9MN8||I at HA8
zGHL>9FVQbv^KVfYy&YGa7BDJ4RA;7)cElrBb{GkW6qh9 at _XR{1K`z&R(l<^nCkmZ=
zG#9oBp%j~}cK*v&s;BTcZLho_O&;!g_E~)ne4+mKrz^^frS{1oiK at UvQm%P#1Le;K
zxxKf<u1$Ew;a>wFh94vCn4t-cn|W1(g;fX~IzJ)Rvw2R!b?!L+;JU%@qVH7k7%P#o
z^Fxr;zw at e0IiBj!p$2W-+OF<QSRQOWPDg%K at 3eb8t`YvEBp0rPky&QfiO_7U+`W#M
zz0Q2Df&TBJD|k!nl0#psrqRq}qA at -*8u6vXWvfe|4<HSX0eH}ZPIFFd`c<~I<XtZA
zGB&>I<CJDV2f1z2|C>mIUlRTv?6D=}ZB*1CEwS@$1DD|4^lSM6rF!K=h9`I#>({Qi
zk;KU6F1^>3p5%zExZ~)j;faSS2-55z=!xS0?YoYFN8mCF57&s6QCTci>p_&mDHi*k
z1r<nkxR>HJh5Ma15Q1^)zH2H4lci-_tk05*fUK8Xgaqbi5d$PSZ=Y81ZE*yzR6$hB
zom#_&2yMEB=D|u{pfWNxG`T-mPyvlskiqvAAaU;&N0TN>d!Te>+o^Pw?;`Z!a}da*
zBbdW_Dhd!QGn!{YAWK<kQ0C}5zMWyAqcMJuo7P^jjci#u4*!Cr$Q~+|G&EFxHTd2z
zW}oi<1YQU~L|fkUFvrz-ZM2=>zeRNla$E6tn*xc^P(8 at Q^)|U&2wV~0>dh%q*<kWo
z)b?`*WoC09A5%I1SDyGmG}?Ty+J7Sg^Mr_L_I)XjVzn>v+cYTwH~LO#_@~QVfM at Nz
z7`_U*E!}(c^j9<E&og5tyvM at JbVKP7;&|V=K|X5ku^WZkz|O#I^laMq=YSfYAOj5N
z5*H~UAH at F0apOL+yDgad+LSV^el6&58GguKk<{Sexh!4^%F7c11!@8keoo2+R!i_=
zP({7%x2-M;$XAcw3=WW?50?c4W~^maXF2r|{_^#n%U9;ou4p2uQ0M)_5lAt+gc*QF
zy-oktF#i}0JP!F|MsE`i|NqB8AP=&n?hhL+tRf3Ni5lCB<DblZREGT4Qs)NqY&Q&C
zHoN-l$#P0kQV-&qzn>zvLEZ+>e{SOv+YOIuqX_*W>`QB>sboaM`t<9R^-|pAr+VZD
zuEg9((WKNN&QogVho#L7FH`Y4c at 3riv4&Zt*4HhBdR4=ir}NJX3nR%z=mzN!?P+|V
zFmxqRg&t;eP7z8Q_Fnw*GZ&CdS*&t)1HrlcpMulI{~0p76&+yPsvJc(Kct(o)^5u!
zayBE)N42L2erQqx at V=@kVDlIY&3*UQBAm%tvnK at d)$2cY)2AZ2=K$X<F*$z8zp~Dy
z>3PmE0TOrxQ7h)PJ63^K=CJR>!iRcf!=~I7V+7U?1K$$fD18Khl!q5LV_twn>lamt
zHYCs^<HV)FmRlRsdzKSOeP@{wd3PrG+(&S~&CB9;k2HnMu?Yzw4AC8%9CqYe&g)PA
zMLrtw|JV%VL%te(+6W|VRcyW^+SQg at Ddx3mzIKUZh%Fp>jCy09IpEX*?~73O=muD!
z^TE0@`J!v@{f~?OglIY#b*Y<~uxxCi$Vs&+K=@7#F1anf at xm3wHQ>Iy+yCn1_#-!O
z3u<3l<O at c+0rfV`{!jH5YYx@)cU`dXQvYFsZz*so1u(6sQT5KCcj04mXZS8o1W7Vo
zRv7_M?wyimfWCGLoPb=S`^~W&fk1BaXuBdAH7+<*VTTZ)WFB*IPiA`E5myrz_H0WY
zE4$O9A+yl(fp_YyOiqI1SMWe(?cM+UmPZ-20_F%8Bpo8rc?=z_DRgRnqyI8{|3cGS
z9nJ8 at CntX`Eto&T%I#0cFhL-0nZJL;rBuUX=Ra?gT`9;IO~?vLK7b;hZi($7g8;fN
zc8k(JV+8&z*Qs>4PX$5*Eb!^K1;oD85t?WZ at CVL>Tgf-zodwN at IB#r*L!r-tfXP97
zhe~omXVJ{qUP at E;$`wUtW at sq<^^Z`^gP(&*fKmdeA2 at xdJp$H*xH at d^BcZ*n=^TCF
z*$w$n3FQB6{c1&n^j4uin2oeN%4|5vs96>onKBlyyzp#`0`gMEEcoAMQfn~HZ;6(S
zZR-~qSCu|jEkmykI<J*(1W|shcYP0)_O|aLdH at +mN^_68<)p?ECG96#1iWcEq@?FW
zqX8yt&>?PpupaL>=6UPV$N|8rH)I{{$DJ(j=Q)u|h(@)y+iwjJH|B){gchZyT~L&R
zUgM&>dr{J>3MIEvejGi`DYWv_KY-#AW3Jy*v>?!-W6l@<PVsY9>AxA<)vuHnc%RXK
z3!`NjEvMyxb-XOdzXx%!)7um+z?Uh9LvIYqU#UYarr>F+nG+!K%#!}IY|1zGyNd)s
z0laa2RZ?Q at wR=!j1y8tl_w_r%+>qxnJ+|Jgz+H^-LTYn=`UhQl`4(^t9x!ne2UrF7
zO$4 at ZNX^K2g&yJh)yFs)Hajyo`(t2I*BGE=6q$z2Gn!3V?+qlrnvJ08Gd>+^wp7ul
zRa9CWYYepEOgzAo2VO<5mQ_yD$*lYwOehC1Rol2D-Y=H;*yPBQUj-(?1XKB`alb&d
zoQ+Y;amc&CZK~VC{59$SDq&Xs>X#cMo3VKzlM|6B0F9_7no0UURb^Np(-a&zND?-O
zaP}12`qgT<DwMl%ydyj_UlkI?a-DO|jkqZ at TEN)1HfD!m4`Y8PC?HJnY=tVg!CL*T
zL~8*vKd5;MC at T@ZR|BLV%NY8wq~VuWX#16d#c at vr#ciGsen(;=POb8SEW;jT8Pawq
z7v5bFu7DE8WVlBe)FElU%kTJX0C8a>>dJ!oQ$1DWnIsGBpj)2zM<&f at WaTX{|8W4=
z1Jlm*$h`^IppMb~xoC4E?UoS1S1{x!Jw2+=daizHViuVUm8zR{TeF+)BC0-7#v|N<
z?u>zvDH56o-oP<$_aK_GFJiMeTMx_-qm6t-eWfE>D1-DA^ClC07ULo@(*f)5;ecOV
zX53(SIqXpLg7i33p?|;3 at fdrLo;oz-65|h9im!^t2sD3(OCK-u)jn-2BSo100F7}M
zedl-iY=95IvuT<e5?o3HKhF(MvT(&b7BZh1JX3TJ-nl4h{`6j)w7_W1BNBW<HX}r+
zGU&rajO*=dPI{;c|4g1v>w(+^+az=N{ii!+FrI3cVNs*Tjh=c+fip}=1O2i@@1?Z$
z at f)|cUr!}bhASZaRr2{Mt^z}^;j<#M3Lh`XB=AeWHVNnFmTP?q5Y?ZR6*{3iJtnoB
zbI*)_i5d|$D|EMo$P*&NNIIch5k2&N+brg*Lz5sHyCYIxdcx5z8cHY!SZM_)$E|O>
zsrzl(lUWITRLE4XF{$(O#qKRd9Q at 8luBoPXBcoBaZ*Peu_UQg5{j$&hmVPIZE&u-a
z&b=?*-lvK|<5SbhE!%eLIkuL`-r`PdNsWTB)qKhLd75yKXy#8}luq40OUnXtDev(=
z;Yk}SwA#qYjg5cgfe1 at RI5BcXj(40V(>Yuo;Oe%z$Ab&_83Pd-eq3{9a1Or)gN5*r
z0-fcncUXkH0n|U5M-bUJk&kSxBXcf370W6UTnWFZXL_VABpB4YsG32uCm0dt at e@tJ
z<<5Za_gh+G<CqT5 at lN?uxI4xVj%am4zvqlD?8F7NRlsinn#4OZzizW;_zF9Mzx33#
zyV7zPXGjc{T{>hoBZznul*U-hGqucFPDuei&mDSxwqZRG_{bcVJ35VGIi3+;#XesK
zXe_e>U`||nvH8X}ab(EV|JOFXz1vG;A5WcaJ$eB)nYxkST-#|sP1&UKfEyI&b?8g3
z5YsGAcw(0lLZrR%=e`9j at L08vds=k;5<l^<(iFAy_TXsI!jzv5Of^5uq%zD at C0~pp
zw at t^xMTy%05QqF!*|_g#qFl$m9h(r%m{Wx)!9%u}{XEKxW1XhFQVl?4mt5`&(5Ee9
zmK&;2$%L4GsVdFL<cY=*Xyd%72R2VRE3K*C3`vC{MS#C9@*_C1LbszX8z>L)f8V3y
zd$+V|@-BYAe}f#xPAe`vucw8GQQ#N at O&>h50MR$OZqZe6_ at N*9A~@EHMTz)RH{zB0
zSO`<$*x7Fc(9{2^t&<ipX|@RFr?WE at 6|`Xhw}~;{{?mYCySkc6w%tj)$|Qhh4?IDE
zST;d0x(CY3lFh<a=R<g1%Qu(Lk!bJIVAt0Pt&UAm)eoHm#lXhKj0ZMHwBp-d%QnJ}
zh1w)Hi6k%fop$h^s0SPzMvml!eK-PBfSz}jJLWlv78);pWaI+|Eunt(5Yz9C8pzj+
zi%fP&%Bc^z!<`&{#Xeb;OASV-!LH4 at _nRgqdy9=90+xyH_AvYmTb-=!Qe8af{L{n2
z5I$7Z4XV#q*(_Rfy-UoYb2S-{>P(u%Kv2$^8oeq3`AS)-33;mBOafsTx!ime5FT=O
z2VX7Y3@&c{)Me-~!y&FMyD_UDaX2<$cR3rxt<4XqSFPWJjE}D)oTp%%@@LzLbhT0M
zu1t5Gu0eMZ<6<s7kJY+X;>9Gv{;~|BhX12#!uX9F5`x3<#YE;$`QWP at i*pHcoeLHk
zd@@@r)D0wR(AB%>y@`C#axdMxau`$7NuMA?ag|%>NJd&7e5Ka5+_ba4g-PlH;K%TF
zT=3gS?G#Hr+FRZ-U*tRgWs9#wyzXv!yg0?s^jsat%^)fYcOIPYQyoJ_FD0$-=%qp^
z?@Dw0t{WhE`dc@^fVBa~`_wMCs-6`YG;yh(v=^n?S`=`Rm812cA-t0&y^Ajg)SLx2
zgUXnDm1${k^7MWT*ZihWw@#Nx!}4+bPVWnCkHzKE4H5NG!mncjaRSYuJ at uQ*k7`3H
znI*z3t at 5Qo(~YCY4kGHD at Z;&m)fNUzK;Ef>O25}b$6t*Ab8F*zKs6|;cfCTSu1_X3
zH_86Sb8nDx1>=@?^YM3Tka}pkiAGMLTMR?}*Ou)>I?KT{v-RrGab;$6sy7gY>>s;}
zE=L2^ZWAdlF`pZ!2X&`@P<L84uJpQpZT1OZc5N`wWweFq_eau23PynBxk|w0Jf8bT
z{6!k9-N#_NccC+N9cO4&B6;d7ud4j at g|#d3l at W;+AlviaU%WPHccc`r5)5oKEw=z>
zhB+Ht;&kzB;iKdU?BXjVaAow>wM3=_$i=5aFK4Ss(%uyUE)7Dvh^PYTCBVZulWbQD
zyI9w8QZcLq;WEiLlFd2MOA>!Dh7lPE_;G>HbC6X+P(b~Nmdc0<z1%s$G)8r$&`NdW
znG3K8wT~Npzr_wB|HKaH`_PX!F6s!3F7F6)p9BG#L(DyDg0^Ovz2+^}LiWPi=F0p$
z`1s;4jL(r)g>Z(~y*TOY`hcs_4{F?_<zvy{4UziOVPN(qqms at 8Fq_%i%>KLCuR8Dj
zmPdDwJekiJ<U at UQM70J@r1v^AoBALT>1P3*<*vYZHkV<}(R!{!{T4OPzITuThr!;b
zMfy1QD`(Aoc|ap*h*So#zJNnH_2x}a|IQ?dfJAa&va>JXdZKj<gIBz2xIbgD&>Fji
z_lc%@z at 3`@yB1RX6A at lzY0N#wl+ns!X8cv&6JwPcc8(@pP!$jzRq{$Eo{zDsGljz_
z>xi$I0$vW0GuqB_4<MJ;SkHR-K(mnVR)%lY?m_ZxEj+kwxr at flqZNy8n*av_T|9;T
z&NDS03Fw~<fXk)Zg&|bcP(JoJXPcTiR?PXCrH9o8|Liui>*SB;X7n0E{YRvGQH{Bj
zfD!fZK_<`lAben23eoU3&kvvF-(-&WADQ#e61hg}_nPOe0(AR9ame+`rw%n{opjuu
z<1;TqPb)J&*XrX|dr~;E>DKit!B^1flV#B+OOd44G^tZeQpMm6)o0-P;>t_v`wHy1
zEs+*B>SHzG`5+E{#o*n$aOM-L6?l0NTJ-urG83J5u3eo>qx|`Lw_A66(Dtm*@aeTh
zVn&F~__^1L;jN2qHy%g{%6zyVJ?oWPvRt;X+O%0k(2ez7hF%a7;ip)X4d{*WDpE5W
zsZFN>Sq^7v$?sQj<RAQvm)8EGe`oIO!!v+zFuptEFwjLEYT~jpOSn at Dlg~pW>bFM@
zxifO7))Q3u3@!_f#i<qyDlPC0xKnY+lLbU;&Q2_k<bUE*;nn)}@0z+yr?sQ65k?f<
z0N2H(kn+AtWta1_*Ng7`4hR=ai$Yl5t5_58t)75wHnBkmg6!8lN6$O_$h9x`g);=M
zmd at Q*il(YASTHy2FjF*;_sqnn^mIeF#|4ZU;akghP+v;N)#=C~+CBZSIT$_KYh3PA
zlzvK-EGia|i)^0t#aKxM%(cgp#NNaPz<TzEh2$F+Z$0 at xLSh@O^+(^#2%o<7=Dbkx
zI5*m-rK3Rgez9yp(2SWT{q<0Hfv(8Cv;4oN+2vc~Gl at B1azIX+-<__5$P}C-xu(Ei
z-up#tmRyeL5Pp>c5iDAaF-3$uwpWO&`3MSHo6Ach<oJ3y^woU`*-v=i;+|?*2PA~$
zEoFk<Wp($v^Ml2#c@^R|+!oqF#tRRuO11#OF8fv}!w;8f<LFFE{2dGoM!jJ7k<R;L
zJRnacU(r#y|6LqBfTs4TO%w{@lurNM?o<6^_Y>m3YClv&6>zCuJ(uVkmlrMT3{s4G
zgl0Sgf5`VUZ&xXQf+xmL7e2<yawNgrYdeHAc(@_TlfF3yC_fI<FWiuX at uv$EuZ7#o
zpkN5j*Z{iMuaUv5Y;P*Z;@;AnK|FOyT{E0UTSQKXW_rkb>*nS?{QEetzuOZ1yDPsb
zj^;m#lb at XyXiL+S^>rd}S<3U#BxBIA8{>YNiO7RHKklmud{80`Uv$FIOI<d+)r?W@
zO9j}u#ld;5SEo#>^yE#bWge at D@louoqb;(z3LmvpUNA at IPBR4dWq8)dKNX5$NOaab
zqLqy at nJ+RfdStOFs-SX<R!>CmOm3R5bQFB$>UU3{xkyBgU_3*ggwU%EK&^%osi<zx
zUQR{*t`mSbNDdOda<Rbjtb&Yx+&*;hzs{{a4*Q}_@)3}vBs$itfuwDyKv!>oxKf)W
z$-B at m;1FV(mCoSGBFmm;8zQuMMu*HvKQrJrU7Mr>JC&Hobd5^Ro4Ww>^b(yIN%G`v
z-(GSy<A8qeOO8nZ1`jHCVr?YJdx!F>y85PWtvi<HYTt=_0KNx3BRS?9t4AJiyG3g=
zkfPI{Osc0J{!F;yKRp2!QU9NdY)Rg8Z>9tM1%#zu?(mro%7T`9qFy35cCrFgSeBhj
z_?NkHjmq3^%sTukNx}5$tMyO5o0$7l8}5w}hXQ^qO?btp_8cRp74<=r(tisRxdO{B
zQFw$Ve8x3qUItehU at KxZ*QYwWdMj%wJ8QHy$@Cl-Pt~O)6LUR4-RxO=lNpS1DM1o`
zb!mA?GmnFi9e2tf`*#|81d<gO8}L^(E%X^!Pzw|_RCto(b9?ibB8FVb7A$<gQ*`ev
z>kUfM1m#w88z&ByvTC(jJ9+^1hH0h_m!_<%!06={CD$q79jzT}dJm$65<t+{Fn565
zdsSek=pX+H3cZ^Ug{v-LcwdZaiP3<r<Lawnq%;9#{I;;7l}_ft$mKh#4k7O2=c;~a
z8!-fe`s({z6Zr^PLRUlvd(4<}#8dFPE17MrrJsDP>^Ln{a at SPZ+`YoUe8er?xM2wX
zO-qI9^Wt4?_oSxz1ZJC=oB*2b-XLv|VC2M$ar^0IQ<m0S#BcW#6AmYwwz}k}Ho*Lp
z++Esq!y5jZ`t}Pc<De>Z9FJMyE at 7(G at e_V!0^%F?Q~m<~Iw?p3C)mDiRE^-fEGf6O
zYbCcln$Hc70_wDi$^l;|(<(Z<)qg^kTszRpW`EI(10?hJ;&uZ!YF+K{ehwqzGdw(4
z3?LMmv$-)oS7oo7#9t~gA5IG!4bn@|!O9YJ%J;iiT at h~#qN at OwDpXsfZ_bt?8c++k
z?pMfuC4Oe>Rzq~r;Tjz88LAow at m=3dDpJIH at pWLVQPAK$RR={ZQxc?sG_lm|9?6~q
ziu at 8wQ#m at z_MSVXzHM{E$B{CbQ0c8Sf#w2KIlN?ouY-&s75OX_#Lp~AEkh&N<&CRw
zmK97TDnkH9DU4s&x^bpvE5#ycgMaVfeti at ehVp%JX8Ao;_ZCYAUL9|}_i5m}4Gc+E
zTCZa`slWC0F9pI$+5yTX4<nx{f9>2CK41@^G~VO(IhOD$Ht$MMH#45e{vOxy1`w9%
zmJ at j>ZsNji<2nS?wxHYhRMZq113j^lGzUeXeAXpP9LTlbVio7Bbo#tx3op6;0Cb6<
zt|YHKjrIdQC!Yp at cYW5+M#x at a2CXM{N^%__e#{*%AmqAL_)!bV)J+D)FWi_dJfkRE
zZJq*~%QFY;TwdA>`m<%Al>nydIspE^7iN!-XeQ^c+U5T3PhWHo*}j=P2ZC<o`gA-m
z#ZAD#i6h$WJgzZol}mu+PnNosvXoWl5k at z6MR>6I+Tn>i(5(#J+R$?T3+EHl#XO!}
zd!g5AQ1$RhjakPysNOWof+<#ZZ(bo9_bQN}q{fG2NcaW=O)t%|y6mZO<Bgi%jVG`)
z<jU8POI9d3?j>^KTk1TQtr at Fg=p4<11cbR_KLU1^Sl5vT=gtxO0l&c09{;F6zXt?u
zkS_ZL)AJ3?EZgINfjYYlY8Ru~vU?t-s4FNz6_-hyB(pduW5Q<|gn&IOT|^p4KOQ}-
z at XI+Ghtfp}6_#*W-~47g*S{ciKT+IuRC=^x6W}L21{t!Y*yc{cWiSN^RC(#=^=^Sy
zfo5yPNvvu1 at G9^&P6j`!8UM-(0Cd%-vJqqd%y!&AI9{O-P;ChM_6gwZ03rs??p{%7
zVQ8yERVk>$PR~OwO74DqWK=StoD<Jio6T8hwfn;UdJqmA5<_sW6=E4Yd&l<<(8ys~
zAvP>^%d(n$Ak_U9fsM0KHCODuF)l7BL(C9}LxTiw0zgHb&aEOB5}v-tx~3!xrtE)R
zu_V8lk`~$P!<5<DRCQWy)6n>rlpwWS`l4;l#Jk=-)y7a8)s|3ntMlqb5G}DM@;yH<
zgq2%-{m7_h*#rD7YPu at gKc!sLbaAU5RKn2bD)Ilq3<F<fy-0^~+S8S5obal`6yU8u
ztPz2s^{PDavsJU{X99E1Evnm4r5eo#*zMGnhWjPkOz5kihPa+*%hP?z#D>sLMc at KM
zn!tDqCNSt+rPU6vfMXRRM?FT_?7^$nDuCZluE-1a`-T{MT+K3nY4vV?ikiSDNO2R?
zVq?)a2xgl%d)wYc<?Vq{W&Wx4pKj%lF($$K{XG-iF7T?Rpf3KNbN|7V1T=cl8jGaq
zUTruGpLJ_-28n69w_&!`mtJedh06`__VEw?Hz=NIA=3CdUNIv5|Mo%Ksef0s0M2G_
z$5H_42n$VgxS2Pv;HY(N7_Hrrem4FKPV!!>;8yadn%aT(Ns`?)UtQ!>i!=EOrB`LA
zv^2cWoZfoaPvlr-{p3SYQfGg++y#7BYt(XzAYet_#s3<l78az3*8mfs%;x=2l~f^I
zbIWL+=PY)>F-A}T7?Kh`K>>|TKS4o?T&dl0zf+l%smscYfxWXCreZL)@^7RSg7ct+
z5fcw2Q$94p9pDGC9aIP~zP^R&Ip1rBoO}*-$o7)=)2^~<1;8Xe4jC=};K%zntMfNN
z@@fBtRuQ^+)Bc6V4Pn5wKCFZS`%i{f#LD_WM*;F_ at 00E)g9fy;gBRgsZR58(i&0i7
zF9k!>y8uGV_tr$S)g@&W4c;}0ryMBLE2*Khtn5h9zv{&yuTlh<@{{&J9!GL6EwR1=
z?|g8!e#EImqH5iZ%hix`@s@&WBQxqizm?$7{Dt at R0rv#16!@ziS&O-%lco#)t<K}K
z0hztb%A`~+uFui+7yI?i@$Qtk4k~Ot)n|ac at OH;v^o%}ZyuUm|zRX+{6jpe%7wZi|
z?`i?NNvgjJ(I5g~ghFPNBWUgJoDjMtYPYyHyIn&%E`(mQih0tHWs>~$i9Miq&^Y0w
zu8xL{DoqI5ac)p$)b*e%va{Uo$dhgA5=wUh->>uR%KDAF_NqE|09|eN`rgaz(tpvb
zx8r3`*uu6;zke at hL9(M3x_}=z-E*zw#ZdbDe7dn%5Un3~ry%DyojC9{B^M+`Yrm%+
z)uPU}+cM)ONw^aEM)X0Vws<j|F&mnwV?q7$s!!hTVFps6m8*U}`Sp;T|L43;>yU`(
z7n??yjgf{&>zWx<w^O5g_uMju at qe46#($e5{-9V5)y*3&)g>4?N2?##`Bq50Zu_)4
zmsLt+vaGT}q)BPw!AW~T=GVcjziN31^r#|oywYrA$Me>Q`%mHEzKUlkI&sOt!lqjC
zt>r0IcITW7`s}`B?|<P?5EyVEM#<nFMfaE`UtFzU4aVz)G7tbFFJxHN33!B-08D&4
zK9S`UF+0ty2NHZ8HPjVB4p3lfuFV~MmhhGup}a6H00cYjjqy7J^k!oy{!0X_T>0Ly
z)=|aI+s-9l!FxD*7D68~2uA3HaT;co-cmd{>A$6MlRhi<o3WYUq?B{OTApI3wY}n0
z>F@{r3_6SSZCVbQWXJ=NuZ!-H|67?MrO0 at -AEye#F8Vk<J=goG1evbG8`X42Rbh+6
z118q26Auu{IUFmF?fj=Uhnpy}5%r;LrPMSmv6{)|L!Pe6aM~2jAfJ>(>x(p4G at t0A
ztUo#Cp+uf(wbs)Eg~<4b#_cCjZCVjSxS%igEqmaq6+2rO{d=vu%6Ms1hl;*ZeozRz
z#JoMSij5N9=2Tq`MKXex%$gDFNOrjL`b{p##?t?`xwv1z`UkV?fRc&d5BKjv^j~%*
z at 2QAzf81nO$U|g><|>7Gg$@MczBck^4H)3%xG^mg!DQQh{GypJR~BbuulaLM+7smX
zDQ+wNDh$9Zx=}ekTNx?}+*I^<_xD0Zhvr>nprCyI)<KKjP-Fjdz7i8EwJk$a01@(e
zf_m_)`3_x?hDEnc*>u0&1U^(r24mo!)8^Qwks?Puz+LNI_Gja*zmiGmD2-O6UN+Zi
zhyms_*YnJdzFTpCVIwm4hmTulk(`^djH-ikq~jB6cWC#bE$>x^!e50Z*3E at b5sVJn
zo5EHTACY at isYG!(AM3wxKL&4Ohh6d^Y?gaA2h`6s|B~GizPH(9+`u#1dR^E4YlV3<
zOu+@^K6o(-cDUK70H at DRVR&v#qZD1lW$huWY_K*;!nzM#3}cleWf~OwCHz!NJE3vp
z{Gk%L*q7)0Z{{MHUtO-FncLjjLk+ubZ!V2!xVC?s;_Cx)dH2nj1TP1ud%q^LPRNMv
zO+w+sdI(?~z^{FCZ{-bG>xyV`Jsqf~_`eqIQ|Xkef|6?l+r6b-bz7|2g}KJYzpY&T
zZ>seFqwK5WqT1TFH=u+d7=+TO2$Bj)mmoHwl++;7UD7oQh=LMINQ)>SDV;+~$DlL=
z4Bar~F!Z;^Q+S^9p6B`e&mXY&T6f>qeXX^BPVgTubvz-~x-kO!;N|AKK4?}bDgZz9
zdx)C)gX{`s+I6mLfzthCDHH`@w8-BnOU>uy0~vDR=ajdtV*aGBf?Gn2>XL&q!Lis3
zfY$rm_ at E7=J at oEFyMj1dlN=(oU~z6vL%1fGrt31p5muG|>?Erk>)sS;#<?9|cr^`M
zZEg<yKUSECKT#dDBwU3RPfh<x-|=w&=SRYyKA{;|SOMK=rcn%5$aJOzKN!b07tvK-
zhXiQwQ- at E5ZGUjAk3zRO*blm}Gl0GrWdJ51>fG5uLjtqcvok&(e*VKwJR|i|`Aq`o
z$17%dl-0b=zBwJt(Wl#{>l--XJsqxb;h$v&cX3-xLJ#QQ+9=>7VKKKr?+tKgoBkhC
zTUi4M3QzG70;MyaPKa&peD=>GVTE0k+*Q4dM#pVRkA{Cf{m;9Tk+9sGq7w%(Gt;hf
z3cGO6#$|H0{-13tWC!Mn5$@RwIS<&8Nk7RYli~lz+drL7hcpl+EqZyxZ=wfYwqzey
z-*ar?CjaME{3YXA;r*psK3n at s@mG4e(=88);qQjq-M6}-XW6KEJ_B$Y07oMe=Bn10
zVw|}wOAQGG3~&^+5#sCU`jfxc(K6PqAUG{nWs#slJ(ch at q-t)GTqemt0*Eupi~BEP
z3p5eLk>K5r=dgeYYT!?fk7x+_j3)TNN7FA{4_4tpkx}NBblqO&=)H0cVlc{ZIZVrh
z;0uLYEi8**RGa}OrIz4O3Bn%G$i4+H9UOX}VRm?oR1S8B$*_S+q+7^y-v_Yaz at z@g
z^J?u5mjbN>o<gxOr at ge=j3?vS{6ePr at v2*_b385P;v?WOw=;m}eo!tY{*%P{n{Cp}
zzd?F^HBdTZ7)QDAgx_kvm78P??sOD_zn%Sy2745ear%j(0Gb#6{(?eRloCwz1l>9h
zF*wb$o%!?+W+(v^vbB^@&|yB=os)AKv)K$UP9$N4AogF_eDKx{Fg7QC6|{Yv0kuE}
z!!v;(c+D at YF&SgGy>WE^sAo_fo<WDForM#DP0If&)7CtI@$_WKbBlw+ at mM_c1vqx#
zZscE7^XasMWB3qYmK<5zQ@~R_fO4R)7l-8z0zpCh57c%WKmPiJp42;nF_rd7c+o>)
z5r1boJ)Q+rJnebFC+b;SmMR2=Nx40AaI=i??>6zlyiL2LnqJ7-d|KDdv4gyWmn0-0
zz79O|er5O}YFb+R;-(_i)K1LyOegFtyar78{zCziC}83X;2H!#7}iF8EYJq;grLXn
zze;UVE6*{8sVg&OLn=AJ3e<rxQSe(|5x>ae!zzAa)@{+zcoQbsGrWb>00bxqN+tc9
z{gjmMTeZe at KHu7B_An3qDK*s#e`BGU#~tVcnx!F;qIe6QAl3pqr~*VN2;z$VRcrz7
zR8`?NUgm(EV(F<wZ)xL!1*ZLBVQe at H@Sf>^d36Z0;1qumIQoy8$#Hfuf|`T>L1<jy
zB8a>Dm&j%!2>PS|FBVwhFAq_+yV|Nz>v?TKqt<%}FRQ=t5 at qT-2wAjd<jB2DN2!W_
z3V`RA2YlVbZ at arW-N0Zeb}Ag$C={-6n4KjfHo2{%i}Bb=e*@TPczMPH?Cj_6)1Loo
zUCoo-V!|G)?(?4arNvXVm9&AHep;6*%^{)_KqJG_B_X=Zp(kzHg at v8%B^`wxdvw{1
zGTRg8c=P&|?g`J00tQp<;a!9lAL1qo at hTb2I|Giwd_><pL{$Bp2QVjmhg^u~SCxk|
z0K*CQVTR+a at bGS+yLd~^0JHy$hQwO~fck~!VM6fx9}iK&K>Ei!#x?UFtuOQhxMq&d
zMBu$L>gF#`3J}sCFaYZUu*>*QxkcSQ{Bs#Gqj?=&8}GfmxGTG-z||MwygF+UKgD4v
z at T+}=y-9b>0(7(a!SNH~<w!d$p1bwivQjK;|Jo&MTy7 at G2#6&)Af}KYx8iKP!Rl2J
zmKGYou#f$s*ASb9Z#($30s<9~nrLp0$rZe654)typw%$;-EoO=MvLx3A70NzPoM<j
ze~4u#Oz=TdjH$0KNK!up0S=)2^th1240A&0Teurl_m|$PtWvY*KmEv1 at bEray8$*^
z1UQj7VDj3gNH5TeogZwY7cx85XVj}(%kmI!y)_Fkgqtm*Kh;cS-tLi&_+l*i;`WM)
z<#xGIA)sQp^=8Q2Io8hmJFfjLZpvGw#s-k&GBg7t)RWW|=7$bA#n+kz7wbu~b7r&e
z=gB9ulV$9*1Y+yGvu>W1!2O^W_ioj!uC&%vbD2yra#}FErCH{qg&IR9VDTbPs~KR4
zyqGPT!i#9RiS~>-Ua?Fqo~mJzUNOz=^mvoGdN;)(B5N(n<k>HwoHfgsPs_^ZMZAT3
zZ>s)~`)2<4ciH)(+R5&827OY$6P7iNv~epcJ?53PJqH;Eccus-ct+xU#11Uh)I`_w
z#Wkz$*61R0GYuVYi_7+Pi29?lhf~?h7(CC6iE74jr_jtSa!Be&gx57>S)*)SQy4RR
zlc%KCygSpZ_kP(R(6_F at XI%n#n!tFsnUL+bSL)}plQ$Ty_R(ezjp?ot9=4R1fg}l9
zhM5C}MOYH2+*fPXMQ^<ICUx(#ZO%AU*bbJc7d^0Yf4Yc(iNY7&fop>aK5%7j>A!jt
zd%4EEyDcwid*dhBJ?q&?y~8=O(Gl>@p5Rgb=t5I#Xa3TbGF#cDAMz`s9N64Fcc*+K
zJ>+IHfgy(*L at 2{`{A9(GF?sboCj|ztSv73EiL^`ax0fu3&1WK4`=X at jHS_M=z<QYJ
zytn44E)Sb?oBJl2MNi?Y<E0}v+!wPwuz-laiNM8}EaC)%3bx-c4B$Dsz4b5YetFhM
zd at +Q-N2g%6uYIagxU)<|*1F7MUnE^K<;9yQuQz!64Vh=w5v`lMmIm0zFeAJtJ7>Oj
z={Q8$m&wvB9F at Z}d5<%C9pgsSx4x*%%yr0SG2e*k$%|rO!84<wu_tw#NX_gD5){Eu
z>A)PXo=b0Qqvv5AG_oTr)?ajIqN}nhF<O{&q-sgIQ*&AyJJXpB-*q$RLrIR(m0N=(
zCo^bzB|oqs!Q^<%RpVzNXAT!kGFk7z8I$fbUDT4>ln`c&L at h-?eLfbk^}wRoh6sG<
z?xhtjI1-;w;UZ;|EAu?r)=V*o2{jBoeaXGUjQEnr0H{$<S7dU3yfrj at lqyZV)Eye$
zo;Z#V!pO5A*flE5T5CY8bVGYJOh9SOcdE`Du2^IW-NAXg>aS?==^tzSim(I6&v8!u
ztQLd0iIRI*3J_$L(q4mP7ZSMSr7zoY8W1rtF1_Hxt-lzMvDTsnxtESNOFE*yY1M|V
z at 4Ek@4I*e>KmB02T!WIe2wy-nW2PFi$)}DO>W2)b917uZOQ&79iq**S^<lr`_PyiT
zl8-l{y9(B<#RMO1^n#PUkjx5SxlysBRB?(G=m#*{^d)kK!*~B2fSI)BgBC4^^kD0J
z=HM0+7Z-eUG2utC35<i2Ka+BBJQdvKk;+hRVL3s0*uD&t at bxWyfqDP#rm-8{)kaBR
zulgLvF(x~OHpXk{X+f}nJ{bT0lxDh0WtL=)ZMbvho52siY at lnp`1In(96A>|fkuj6
zX&1H2(vj=pYmeqVa0Jzrqe5pz^?i%<^dl@|ESSm)^3Ibm8(SRYf5T^HnXUV7Sapm3
ztT at Ep26A{qC^zA7rp|2x6zkk%-e}ZzLZmz0l|~8^9lN_O)ek;BJFmEmb&xFIwoV^n
zMcy3ts=zw}93TZ%y+H{Jx(?@eXmp%^N5)uV%E4&>a4N#RY-dZnJdn;XsG(<Qntk_U
zni?wc+bAk2IViu}tze;oj>F9e8cPB9g>yM?5<p>=OX9)WnFFprt4a$S!=mRFHou2-
z!*@TU9Oc}EZZT_~=@YZeu5_W4%Ruj=i>KW-kS!UQvz{4k1ept4$SIdeXza_MPi=oS
zy`C4^8?=#y2pu}+b^dp>GDJa)xugH0WXrLy{U17FHkeiqJRjE?W>@fRKMg7vuKdg-
zz3`s(B*^hEv){VWrVkF0K+0lsM<12Jbn&MwFxw3M=_A5#f5)-=Wx2x^QH4ewWvvL)
z>r-SlJ<h3xa?J{E>3do(jz$AN$}-f4qJ_5O?;!*rWmLB4^`yl+w<UwNtbiOHW>yfE
zgA*hO3tPkN=jc0sY&!6|9I^l)>)*;AT{q at 3KeOC-w%5&(pV>RU+w2jFS<@jH=pj+o
zotERwJ~>3AKetw-J?-P}(mT=!J6ief_kltqG5(Y#xZkQ17{$D92CGAf at Vx7WMgO**
z`h}L>-LKz*M7EUYyOMl(_srvqq at f!nwQ%P?wo3miwcZ4ecV_9W60~;uenzOVM#ZC*
z;2s|Ql~M$a#Nd7>gP~xFzB1+O#tt?;{EW`lx%bl{VVmAA;#jma`z2CH{sD5V^o at B+
zJ~zd|QFwquO at Bjw!8nIo at 6Sht|M at 5%|2UJR<jcZ3g|8 at yhK-7JEhla0h*2E8#ogvT
zU-X3~?F)*iAnJxi&DLc-C{?8N=Dee-4MvNx|I0|xpIZ2!8AvN+(()8@?r0Q$xh9Lq
z*b&rnk($tMz5exwwo at -i5T%gvsUj2n&OVG~;PypP>Gi{f8^YcjSq-6{U`YFidCaOK
zHcGl%)9X%T^L&n3Gw*i#8pXi;HNL?quN^`4l?E+2-Ax4lLFz^;oK*`}bo*U$X3W6z
zZX37WiAJekLe|sh1I+TizdIdABW0;5MzFZ(n)&z3Gskh;+1v;*NSRh2`Q*g`p-kPp
z^VAimmav_23wuMBJ at wK*&q^7V0^*yENsw4=naHJ9sRFFxS$g6RF5B(J=<}?dezowk
zc>QBOYWt%~BK78U-<{u~*1`PV at w0ZuT{TP{T5mMPjD5N+)*!s^a2EdXhjaZNGeMN1
zWFXZ{_p8xLD1^93((xUW;D<_Qg`kC`d5J%vH~4*LncggHW3+pD;Jca`M(5LvP(U)~
z^^Rnk`TE3B<&!g`6k$kwFlE9!DE{U7{(mU;se<;B#Tq(cRyUUV=5%q)2&8<~(_|3y
zK&|E5^byvbjtKBQkC{%<p0Ap6m}tiSTZ at nH3V~BtkNm(Xtf(x-J-5Pz`K{r)JNi$9
z%E%}o`A+=8)e7!=C_=eZR%FUj+!gNaz(i*8FHU8;-$V?kDe24fM_D`7&<<4EJ1Ok{
z_SY8PI_6~>=(e(px3CYy- at iQ$cn0d=kg7~_Iv5#$0fs8NbWsGNRzRQKbXV;bSP>;?
zl*(t9*_tWU?nN(M6U_}Cs0J4UgR80Vln;TpcJ0lYPn>D<_&>_zL1#C=eT5{G!6xz&
zGzag1X&Ca#TXOIY<Dc*7be)HVQOi3H9U*~#44>Rl)BNiNcIfvE>!LU9 at qii+-rb&x
z*$f4~{g|4C7h-;{M<v(2i&7j=zklHoPU$5E2Op`1+k8ND?|+I;aXQoe!9(h9N4&Pw
zBQP39O at 9i!!PCIHWBN?fQ6Q?kpQ4&AcjUht at UhhTIg!lw=GDD4o+w-_WXLjb>ceia
zP3!S(#`7ikI}2#R$vs1{xS}QbI}cQQ6>PzmxHK#D!r4zJoQ5yRX%mLAY`!VEo?vi+
z4&b(}0Jp`Tq3A(yUJ95i#cPkdWivh3aLj=)#MAU1e099yitvoX>j^@@jnH&?Ll`pq
zbhwm`UH*Zta5cdPQ==^}wTM48?KLPoMeG?As)Og(kpCg<kG~09SqNFa2 at WbbWDZF<
z<hKnz%|`48e}=_ at JFg^Hn=dfz;MA~HbK;iW2~(&V{`jvO-r5V@@^zfQuO}epnlw)i
ziC*+WE`0vAIjE7?m^zQ`E8Qtylqtm<>A%YCvqP_gL56GD#Zf=mhkMD6R;z2m?i<_#
zFAlWKC4**TJqCqH3cm(gi*w^JCd1x+>A?XHepd;#aBw9S-b+h88PSlvhUIU4=D%bD
zs*;UDlecAbN?%8XDc1_mi+=}8NWlcV1K(6x)>in94E~F7!z?G_rin4SNwmioO8xl?
zQVIuclqz>Zba$$5x1?)yg7jN$o#2ir(v$yTFel15Sah$0e5GH>xQ|yUsL+QJjf_VN
zb^fw4zYWN8pfqPO7sWS>{egvAUmO`}FIzBdh<6GrV(NeOm*E*+1cul5cf;cxe%+f)
zOjc1yQjj0%tu+$Pk9+!JF~`{vm4Y2EoI)a9CuxiZno&MTB>+?id{X+umejRTMB4q<
z-Od|@k+ftoszQI+;X&Ln;6B!Wcb@>~zE_gZHy3k`JG&btk~bt64Q(@?S#J%r_I6pk
z!C*i8st2<mCFC;ks(KfEmZ@#XdB06=F{d|$SaSP?uIF84=b6-JmulU*J2A!jw}_GB
zFP|Zvd(wJt-LUo8+YCTUzOx^6hcViu=nP7W^rX7;=Q~zPu~5)eIB!`EJKS3N!)KH|
z2$k1%Hl)k49cKoFZm-9=*D7moq5Ll4uO!MkF!Fm->i&tz<G!#(Z^@etso4`Txii=>
zu|R3>f$apx416Wbu=#DHXM85BCr4YH!DFSVaGMH=ugOqHg%b?o+ at 5|Va&yeRhbgWS
z+HJUZso~Mi_>af#n^JK;4We$gHtvt>CJeX8_1cd!%N+=Q8m&0~{Gi9;Sc{33Tk*GE
z=l(dlBf*KV(6r9^VrC(Sak)#5$|Zlf;(cShE9(B`irKsQtteVpMll&BwE|hGt~0uB
zPAF|LJlwFOZtwY~w!;x8 at i`fbjtuMNqC$Vip8fL6eNJ7M{E1%?9lx^Ffv~cDmzn50
z=b7qum$Y!e8~wq04Xf~6zfuXYCj;)gq%7w=uwy8)JRz|Ze+^3-RI-*Oj+)h=T#n}m
z(CFJtPcs^Gko`_pMWz&75a%>Jt7j!6$>7#~LVT;-t8jYQeK*}I)g-Xe<)S9|ICU22
z=&#o(E>?qX^4MGRoy~<a9{b;2abX^vLfu2 at _3=HM>q!|q-*ul`=>|(QmNMFXxMqR6
zaC_jIRi{Xw*C_Aw#Z8U3U)54*s~sfVz=^?jN0Uc*7yz6Tdko4}iaBb)it=nfgR)Cu
z%G6)mZt6{eeRo3)@Ov<_(`i37wRT_1OPTCW-^ea>)QDM`yNcP9ojerzub;SK3C?i*
z?zUeF4GKn#?+ZWK<WC5 at Pnrhey3F?}ZwW~kq#F~ujhnmueM8nUEQ4*e9d-b%M_M?m
zx0ksn+*T|A?jnU8lQPx(YWHeKEACQUEXr&|JIyaxyhGv7HTW6$RE8T;XmF at ws?>L`
zQ5C*}c*<%j^wfAkm_DWirO^#8xi2xVzgFQdv)##KkvMySfNCBsSkeZ#2TdtRx4~A1
zhe4u-6YvjhE|z<H)!<&ouQ!LKE|Cc{X`{N&FK_5+5Tn+$mGczeu%4vrVGtSfWzBKM
z{1AVK6597vyWFMmN>Z}dwoLqp3utm_wzsD$r3G&jGCRp;PYAI?8y+v-(4X2xYzGIv
zGfJh$XVcl8aTv(oGY<DbN&<Hkg6xgIXd$L-_gWb^!^r^nJR|<|pB#dV<bp6K*9THN
zzdGRzBJ#C2h8EgOgh>t-FN!E%-N@~9cUqW5AachRdh-yAY7R>78`*&m3+FMOtJqv8
zT`dxCf3m6|$pN>9R8+B3ZhY890+-vB*3}A|+?AiCmOS|tNasQ!9&v=tlDs#*MDeXT
z4e#Ca#x5OcPhLGB63OFly-aMNM}HN|UbYZ=jpmD2|GBGdC%KMY(xwgwi<6BoyyNt0
zo9- at G^QpUr8d%4QgXx{Yl1 at paAS%us*;=ycRgvAh6D~SE=zv6+lpwdBw!02{!m{``
z3Uzz3+_}$|XG(8<+0{M{ZJ|Ak;<v>pheRNcd=2fbB1ve16=_#$qUWte$x^xCSi78d
zoa=#V#o#KMk>bT9MYzVXG)$f;l4omsNlu*oxC&9#b(QTVvx^>S-ctK4%DY>*V{-d3
zL^80Y0&l+VWWS7O24phzEBi@>->cV_-CAGBCVCX+`w+?)_kW#d<<%*R8=icO$u#Q%
zm%hw|a+E#vN#;$}`D{+D(&03(KD>LBGt0v$t+!aTVBVA*4vQYpi=Sv4*v~5e{Mvs-
z4eo(5ndhUrxFOqA+YKjrSU4B#x+*>%?+lJ2l95q`B9Ey03&zu-%^MC1N#MH~QjVqW
z&2*6(OZ^9mO3)cy)&Q<<?v%DbpIh;i1RvIL?kWs>nr#6sN1DKIGfKhq?7EwkySKwB
zaOzsjCY8r~u0Vq>bAqBl*>fs-4LDr;b!H_CD#-oKjIj%5x?wY-)&%2%yd0ZN+DgN8
z=>Rb?U1A?Capl2)`||3th!^vRy#jNnk<Cym!KOa)-DEx^baSHJ>=A9FmPiC-$cT|P
z*9JE)XE#`KQa7926X%4A&BvGI@@E2>)`!a{uRf{S5$JPYZ?f(hc^08sVSg>)lIo3p
z+>PnLME4f8kEL&-zsm(nvZpWzIUQ4o-XP<Bw88GV;=NPgD(ICtz)5zDGE76S&qunU
zK7ig<rnyj0`%*$;WaVv`RcsP6sQt5Di~si*c?pf$Hd|bg#EWyLojLW3^GzGe;M3xF
z2u9>2nY)t5my!L@<*T=vtIv}VlR|NkgZts#8O8TQJfObh^u at vh5@w4F2{Dne!IcSY
zDUKA728%9_UITJLC8cQoN=9C64>N>iLbWUp-y!($<NFH(sQ7W@;uVq{n!Dy}Mf1X0
z&Oa==p|6gSQ?vUXjL<60Tu=+4CV9gt`0Me#TUPIUrbjp4ANP~pvNR((3J<f~-oMPd
zKV}gvcA!EpYFA3JFmQk|Gw;iwQA at RFq2W|j-A|opib-e=OTX<%FC}9ozN at 4t{Iu23
z&1C9HbfrCa=3b at eKzUp>CQ;~a*9bBv=OZy=NNI9#cO)7Wi at 9hqzA$#nIlOZ{rbBL@
z>16mjuAWz&$p+yVetw&haqlv5i05J237X2HmqPqI;jjND5Z8`zd(XB5>rx>^^A%`f
z*n8`){?a9lI7M)>54Xi~Z{jLr*OxVgAbQBj?aIp9;{Ba9HYlH&@g5`)w8>JSKpF4f
zYam2 at Y%=dN6Lr<5<@Tuaw&9TQRmbXMZhMR09%H^Nb)~vmq$wAO(eT8cS&*Mj`eDSa
z97n8R^tE^1FVbvg=FU3ywfxnCJl^#!;jE+Xvvn;Wh3-DA=}aRa`Lu+1lQg+mU)1B}
zI;&!}8-SixHhk(!vC$K+!|C5`p{o+l>99N{!h6tGWaNM;-z_R5s=}^VO?3OOIVoIe
zO-E}>PCNx$o^IU_*Zj=}L|oe*l~jmfU2nw1CzLf5;Z_Cra5fuf;U0nfn_=^PDwoIG
zrCp?_Z9@;7*dfC~Hd1tS+0r-r+NZmNvP(xkVcy5zsV#Q4U%J${zxk=V5V`51x3^}8
zsomWwK2N>RK=5H><Pl+rorujkWjS+RqB3U!@8Sn&*q|joDc`WRXnntd+vw}&0z9!R
zQH9EU_c24-qs$i+SllNrE+rTjjYhnAugy6hMEsuKM~z_*|BGJ5Ayb)sJU$4P{o+q;
z(&@`i>3_;+@%wP*gfpFX&XMB at o9B4NFTVp{GL^$(W3odsJ8AZdJhwNCUcW0ERL{_k
zh+v9k_h0eevSHpIuD at dc9*BzO)>NZ*gh9Ymk}xgeMxa+bU#+y1+4SrQNDLXZ$DQ}c
zE{b&v3UTk8WDORt9cjN>d`lO58!2QjS-99EV5W5qiY?UtC;<z5$$b-(#iZ2Ytt<0R
z{msc8XRosq_8d$Sap?pjeM&bQXK|ulox+vp(I$gGLhYktOYA$EYY#SfTI2keuh`72
zKeHFx+QLQo&P}`5W4CY*sIk50{%<qeF5Gu{9bvs2l+3Tg?t{S5>3t5;b$j?Og6hHK
zj(O8#BoduqFF%>sCt&o&7;zIsq-6ACH?yuA(U<qmN3YG^JiFlOy`X2cuik(HQRfbs
zQaW%cA+IdG;NE=>#Zs&5(dR3+ZE%Cn1}tDPf^hpAFIvosZ#~}UN7GA~kA{3Q8)SlV
z&x&7sm?Ka~X|T|;x%<szqVhm^`N5(mt7H3uUR2$0A=<L`3(e|T*InuN3p?z3nhm4q
z!oUO$g%o1=YjNi>{h)?e!n)PYFk}O}!Z?V#FOXiZ?5Ep|ToRrb1$4A#4_~{#*_y8V
zwjPBJmS)kTN%fMVJYbY&A(WY~&N;$5?sD(|H8J9P%DYIJGb}j5Ng1B8HaRS9+MgV^
zUj4BfY}~rTV%?<|)|GE0p7sz?NP*oYBIFcsrXEre3o`Gr;`Bi<dy;$)>E-Cq?xgEj
zm}D^gTC~$BZ?!-Cyhyv?!P`^u>c>S`_k`l~?v87e-{kl12qsqY47!-b%;w&wmXbHF
zc>SjLs=)D>em$pC9~E1w&!L}`<05AVEKEGt3Jnk3hrY<$4_x#147<9}S(`U`nG)RS
z$}fF2aB9&@!$~N2Ic0OQ6tj1e&PwcdoZyazan6{)^#PoE-)0X&UZq#*!v=?RcYVqW
zIVrEjavO9ClSYrl_>g|Xq+ at QwV=tB^#v4pF4MjX_-QI&LIbsq?2~@enLF~|3|FxT#
zs`-^lP6z~IEf9!pEBU>*-$mFA%d_yn&if#weEd7dhuk`eGMSUpuFW+*=4WLMIp;8$
z{Py$GXL?8a#>d(233O(SjYX!rJ8Nb=xf=p~D94HCLf(3{e;Lh)q?knS&Ee$omBCZB
zwmUTN(;9C&U_ at UWj1QW5)O9XG%Mw5HW;+9dD7)d{BLpI)1&EZadxd)*u~I|1F;Y6a
zB^20g%Cpy%p1Maw7kK%m^<;4SAgZcZ175buvp;E3-VoaW0b|{P$ZWZB<*N07M0CN^
zI0~%Oz85S;`a<wuda81Ntt)gtYux-ecQZF7dT2Sh+%YPF;8CHQ+4kI=$$-UQwh6_P
z;1gD at Ym)QG22PI}4yv9^Q1$2}XM1f0s$1^G8++uc6T9!h=<g^=F)6j7B<^;bcfYHK
zDJx%|eEss%*k%~`UNM^A2VuQJWZLo4?yA$APISoxIq)rKT&$3{?QxqT^P~&x5}EY2
zaOblEnN*Y9mh<m at R~*Q-CA#1DB)~Eh$j$yiP;yV)&jO{H0;Sm$8XVl&UTBdN=wsMu
zCt2TZNt*e>>^W2IFV=V>TRHRzH8rE`sHpwJoVPjlkLGVBe9A&%Q|SDIKj&cDLbJoG
zhZFM#cG(;bg-n+eHShLFNviQQ>0XgMZzEc6zSEG)&xF?A3)DoDeaoK*Z#0j<`<3zy
z4$%4bs9=FucY`1PT?gU9shxtJU*yXg3}0##o7x>CJD#-6D4J^e%6;Oida%TGT{meZ
zA{diGQcCOFY+b~-$#x}CO2L*-!J#EUpIUzpu29w5&s;HVKRh$<A}MA_0LrR{yW*UW
zntChaKdR|1Do|jJJILkG9~32b9<=z24?IUaVduQc^SY*{_RH;+x38*)$Gb{;*RDS9
zi&>wVh$?(+v!k@$W<!cCKl+#aA(`gb{z`#Bnn*H8f(q{+CL3!vZ?nLHDK(*Eeyce@
zD33)ws}-S~6p0-V391#NTofrEKM`CjLAfC^I35!0Z#UC0%Hj`aBwn=;Wt<36j+1r~
z9=7hjdFQT+*0m7`9wz#$Ecw#g8LAP7w^aPxCSej~DjGYEGT*{#RkTIe6iuE8wyB&%
z*={-MRPaXnuUM6Ol<}bpTZ%BN7F$lRicF0-X=r>=-74&pZ;BcbC3HLL_ZUF{pZ*yE
z2R(;LHGO)M<Y at 60oRRM%YhPSTzJz;TE4oDLBmQk9=t{8<1?5U{Eya?Ek9fyO&{dZ+
z&KcEAmbSMT21ki2Bg98Wf<(Huh(vt;awLe==#L=&yQAF0xSoH+ at TYAwJ+_&8%Wt~S
zRdUtzo5&okG<?7bmq4)+fL4jNzj@;!a1t6Gov^g-v|D)3!Fv$ild>x%b&1`%3%!)|
z_EsaE-~^Y6{!K{@JHZ1NNszVk-`U7z5|v>OuglscRxH2oP&&igda{so$B+Cu1s24M
zdu>|MviSG`;s~1$f{M;tF~PcD0p%r?#Uujdp8B6&a)kj{!uw;b#^l%~Ix$jDKjFOo
z=ZLl%snK%Vdm>yO>^vuvNwZ0Hed}d4*|-=eCq|yRl&LYWUm7(7oelJG>h~H88gaGd
zA!rnd6?P+m1@|j+szzwY2x))SP%`CTKRMh8o-agJNjmL|>bI;OcGCZ{oOStX3~!&s
zxv<l#hWEUI-$CjBs&w1o8Kyr>t8Fr}jdr1%hM6Y*^i89 at qwWlT34VDsahAXn{J-9J
zQ92U=etwOnnf!bIpx(fBf~vXyVbwp~;;xAi5-ir7oQfQI>^xo=8#S=e{E!gvK+p3Y
zT=!4ITENelPjHdC<{9Z8TK5G3u9|&05@@z+=XEKI(KQ&KBjDxa{S?cvcbAZ;OZ+!1
z^D#gB)G=A!#JCRaGoj#_nWw#|6^-&1MQ-^WoK`2rkk{hhIVH=^<}EZI>7)#O6~nt6
z8RWvhum+7#{*_s7$<f1QUF~R2n;)YhL*(`oic?}MPGCvBP4X5`H2B*1A$@0Gbvn{8
zQ*)7D=`T+Kfgawyg0CZ?DtG+^63Dp<UrzicSNb0ibu&?Vs<NCZ_d2kK1!HmxKNjD2
z{gw>dEH%zGcSn7`&6bk!`f|xWv#ikgOQg^U;wzE}E`F%|J#GdaGfLz-r%E?KiZskS
z>Xhe;^uzd_2G3efs8Q4pU)CRcszlD~mnK_nnHmNnAM}U`Z>>k5ayqOlWJ`kYG$^Ov
z3_t5lVmmIA$n%hKY>$0`R-Ih%l)odL7FoBi>>ZUP0j>YG46gZrm$FL$gc$3I6DPMG
zF7+JgSrm`-6PK;YL>1lH6~0F1Bs`h_MIm7U_pR5Wb~3-$X7GdHbo=+vqy|&;zTWIX
z#a>XMXF*&c2Dgo+a>vFF7ug&r-3nsTc32s!3l+u;x-Qf&qNs)|Jj*I at _tmUV)xA2X
zKgRBNPS%?3_DKBGA((_-wH_&Tjh`uuI7dRkX1ILq)1!(MU;l6D<8`;v9p-7($`n;4
z?6jmEy<o8ax+4p&EBVd*66_vaj5Jx~WMO`en at hT4-un`Z!-$xh!ORK at F+!i;zdv&f
zeA-G{T6%SJGv(z=(zJ)4n}>!G?s~ephD}K6z4gv at ZjG#RPS~)Xi at b~4nG5Bt65RQx
zn~#MWlF#loVz-!9zD_i*G;h||5;f9tO`Z7IL_4EK$9!s=3`2U)_f1V?%|-pVug@&e
z!u$rhY;^rKa&jVf4iNrA?X`G-;9vD=<Yo8a!UXjknE|jcZ@)6IhqWn%?ZqOr>GDXm
z>et8DkCBpwB<h*h)YQx*`ZJf7+D|sKw8u-939+&12fpuazoGYz*JbCWWi_k61C8HD
zSt+&vOAV4K1x9sQ&YOkl#EZu+C^lSY(aULgrh7dxw&OM~;j;33Ru9?2O$p9eTE0+T
zRm7#i+q)Rv(SWZSXU<=}h;nxjX?pggYM{X4JTWEXBlXnYGG`3e`J1m_!g38?RhBYK
z=KnVbQk|mi&*B`1UlO at unRfp5Ic?>L$x-wh24U0AWChhSC#z$Wls(TY@~QcBL`L^E
z(Uq>->$BX>m|?{I!jU&}MHV%pxqkMHl<g;A=TT~IlD_eV<RmE<f?~MtpF`1joNmH0
zZP3J at 5WAp=tff7MCaoklX`-F8D~=q-gw|H=)$okE=IVZav^dokGdb)wpGmMe<cvnT
zu1z7-%UxCkGqsDDX;@fTlCo5&@XhIC$MT`HH>aRwSm=Y3SYmES=EX|R4ao{{b?CCx
zr^2S{wF}Ht527>;%f^DUpK$Z5vY|3l<eHoc=>3f}-B1|tJk!O20%|(C$gK4Jhc!jS
zm22(dPp4X==E=y&JT`{Za*tX$H`2BaUDQ_IEV8|g)5P(v*tnZEj*FhyyH$L<kbBEz
z0MkSpv41Z1b}S!4U+-=mW-I-x+9TH2BHsJEt!^80amUEW?tJymiWPN;&wP+{a~!;t
zICZ{<=&+SR9=*L9y(*u at CMUhS?I37G?6)*@ml-Zl_LB2{ae^hXc6 at qlg*W|P6LwI#
z!ko8-i;eA>)8YX5)RsnaRh0~HmxDS86lT at Wj~4OiRqUp`-_suz>9?+%JQ3ofNV$kZ
z at 0N%%8ZuI(v88%y#Rp|yn6M4WzL<~xF>j7*z%*5lGwa7JM_mSA3ar?f9CpVbzIbH$
z9;4xU8W<dmOw7`z;yEO8XjxaVB9_OS0arOE0ztM+KU;-c28?ilf<~u#{>^}vq7uH-
zJ*4S=x`t(r-QBD$pSPzIyk|!JSt5|DDz)XWx%4VLYKqWa?qk?R6#DIA8Y0P%93#?|
zH;%NTB2w9=bP;1l>NDjjrN8hy`-Qy5*>IlI*M?{c71YocFS*n3Q9h8KunPP`U9;%1
zls0LPJ>j_L_V}-S252rAw~y$g)uLwy=|O<+**jYPU3}T?YCQpGXMknVzC8zBoL^hb
zrP0^Rx5g^BnuVNwHP3Q^7J7eoE4P;F+Bb)zo7566%Oe&XO7Vld!!P8j3=E$!2QdXi
z`efhNxn39~^Mc&R?*S1B<scmslP79r4Sd))HeDq}(euhMgBam=%45)P%{m~Bp}!UF
zP4UNS><dlszojZ7AcEdQzh#IklrmB3hV#qgQloOYk}d6C8}#U&=G?0%#)G7_3N7!j
zv9W2roKEZO>+>Bdb52maeR~9DnMu8Gw~}jnqD8~N9%>xb+XX*Lk;L{Hq?|u@?rr=*
z!|F7$GTpL2cW~a;$q_KWei4jKE_&a>9|b3X2C2Xz*j9>889AK{tyB)N)HqlXoDw<W
zkikRst>R$copLUJxt$5eRTV6?%{e;Povyhrs=^6l8B8RV-mxj~*xBBgcQEfxb7bS>
zY+Tk1puUtl0nCRtE6d;)FNKyBuqf|ETZYRJd<U<I3;XIHTlvm4^DkQ^*^uruVkyKM
znpYoN*1tZh`ay<(x7;C;fQYzbw$o!Yi9Im8Y9i=vF^93&J~|2QeXyrp7f2_a<P{V^
z!(GvtYt(WE>=EC5{a%FOl|PE|T%!pPIsz{~eg>-A!`BI?ytyA3gJ54d?JHk%=Wa<-
zAWwRvo>$bzk00|#+I5*-Cr)m!DYf_UXqHPAJCB{N(WrSDC1}zf{OVQ72=46e-X5mV
z^!r%d6@&T-_1+wR0ABoA-#|F#4J>-0Kd<FrOz1HLZPtJ`_4Y2}MwIT2t2f?r?6T^x
zlk2RcsG}=9F9`@dBwrf*tY7=$=|tnn4XaG`a+u2=W2}HtbN}<B<imEm6hQ47F~GLn
z!0%t56T^0ZcIS{eB at O?fLGqs(bjEA&yV$7nC`;~0x3|vT_J-g!$g(Rnl;a3I_tdFV
zOO(tKB5X?W5-*S^fVGP7 at nvzj-XdYuIh3-Y5pA3hdWO(7e>r&c&?e&Wn^;e`z10-O
zqgMOkxZnou2qlwP$I5s^cTs`iP>I8>;ao;~dUG6wd?4-6!!Hj%|Crm}82NR{ABXqT
zYpkjuXvqfv9U(Y|S0K90e4rQ5%mhK#z?_5ChxqtMAnsHfi>6YSm2=%`YWJ|P1>9LK
z)zr&Hnt8_ZT^sY&0EvJyg7AG0I2}^K{`pU<8u at 8eH-S~H!lC5`7Vl3#KHsybjuSsZ
zO#Fi*!9%t;OIHQm^R~bQ$qd3z`KX}n85gwYCgMkl!?eVIu2*0JvESus%JIc~?=C)y
zSg_QOclQD|UOoOzzeN(200~xT0tG`ATBQ$R=O~Su!xBAruq$~RT|h9r+J&$0M(~*7
zC<q7$ispJUcN#$=kp0hH1QJQ_Ta6r~jYCkC<8L7B8#|~u>>L#-1e(wEbztYoL)&x5
zKzRE0^5oO~UDpj>Ud>&}*UXa9z}0PVj3-Z?^o|5COn(1ONw<>uSGAU%fC!=YcuN5#
zDL&vloFE`z?cJE~oy}=rPFh~J<!x*sr98#0mUg3ZcQvp<XRo29AK~syxl5724ssc~
zuD(7(tvA#Ea7hp0_>RCKXbNxXBO9i#iIkJ6U2j8;b^5w*=&x~da^5CikELN^;<ZWH
z2s1gb8!C5w7k$<2T(-{BT^{)WYRxlrbQ<?{i<l3WjnK${F4iy+T3#9|d%A<QDs`NX
zpeVzkC0bz~M+cJ?Vp5;f1)QZM*lMl>A4-iDvZCKxLrPO(mLkUPk-0v at T-fi;bX=c#
zkfu|Ti;V)|Y4t8xTlp8F6MVlWG*XBFYW4exz<obM^5Ooa<}oNlPLgs*<h8i%2$ZQ?
z7BPiPP+FU6dv_q^vdpSm?h<M}Sj?7Ur02bNtvg*K*nXmMv`dh{$H!;V;mCA*+`ULX
z-G_8_Om&H9-cjQ*Z&A{x+E#<bS4xIMFcF*&K6(OsMEoxcsewX#Do=ALc6_??E)^tw
zYFgUl7uo{(>6&>L5qntF(}lh(u8yq!aFD1zRr`>%$OP-94AWZBoj4%_!bljm2H#vq
zA`z&twcBeyjX}#o{e&|02Tmv=;uNI>ML%*nA&-g=A3l(84mv0?<vu21<>PzzOKAH~
zJsUlM8j-`=#kOqW{CZNlMQf|!UUqhNxx$Ytv^u=5vFcqDF>XDRrdKI7*%GPb6cQ?E
zqWDU#5>yn*2~wV>4M9v%E9KG++VI69f--h!0x4!YBICM~JHB~N3L~{r!u?7uN>{f?
zQ};>EUbntt*neBjb;xD1nZ|80TqFN7w<OM7nsT>2R at 7hy5qaPVyk(%$TY7J~mTJ9I
zNxCWhh(h at Qze-?&AFe}=>Vc_g>gW#mly2$6V%yP*0*l_DX5*X_$~=YKElIz`8gc?w
z2ww7g!#NIxU6PLiqcp2Z5D}9kTvts;VQwo{)j&~UNaFSIQrpqz3k0GiCd|Y~kLvQN
zrYd8a!?~yN&+hR1H8T1^C!i{wLw^79J_WSJh4_3g(j1x7LRx`vZi~K_>u^Gk)5}S}
z#C|d#Q_aW8IrFw#R;GrxYQ*3tIsgjC|8$+OR|Kr+kJ>4Uan#}qw(1*m-DaR*e>xqc
zEsg>KN-O_m03CmpCrQxr=g;?fb&8h-LDUUY?R8KOd$e%b@%`yv!|gwa;0y!={qD(=
zC;1~>NjRrIP|tOKPr7v+Nwi*1f6Y8>VdBQ#?i_D5_(vBA{4bwIRwk$K``l%Y!<{qa
zK}RS9K at NWvX(|B%m!OICb<onJI6fKWqO5vBaaI;@u$PWZJS%E?Be`lU at 3=UihP7P4
z^|ypwI&YIQWpYl8lFs`;JL6H#5=fq}m3y6y!yip7$l5s;|JSVmQEdveNKSzv(5~J-
zD$Wu3Rq5uidjW)Gl9iP;1*OT;*&pm1RwaDo4ZfAW-ba^c`;wl^z&Jo4O at YIq+G($;
zC==7k=5W<?b>50SjXgy^YLL8ChDseB(&i-u11t$1YyJ-_xj~YxSE-n;ksa~uAz(93
zcnez%+|bC<xigz4mSO=4{Xa^ia at VyVKtS*2sV7)QZ;djHa*X=g$}hhsAz>DK`jSgM
zBh_;XWa*aSXU<E5cYs9h^Bs}U%QyYbr+Jd_Eic5AP3ipcP-6&HIkQ1Wpv!cBRrH at h
zKjHCJBO6A2@}xd#Fs|@NqMScIWp+t$$#rLH3pa<J_osbQqSBvhWMor98TZ`Qt%+a+
zrl0p!zVG}lo6ZRl-ls}6W=}HpD&y~la~a_>Jm%L{CtF_CYEznt9+^;l&_$_^yITb+
zT*c!ILam8e?t+1~M_F;KzadBekY>6<*yJdun%><a?>yswXK-f$cx~7H<}YI>a%BCE
zY5CI_n+}(YGD&&h)MaIB<a39xWqN&=Ea`+oc}aJ*aMy8aI292vhePEN^3ssn7YjSi
zsRBKPj5<s;hX+>dEQ#0BM58^{j;p1sE07b&$;lz<_+h)RjN799(&y0`rvk at 0R8{=N
zAN*20{ipZ2u1<)iN@=%$uceZ{56V{OhmB#OW7A!BCNy*9ZB=Xrn at d9nq8_+q`<0$?
zzan4J8ZP~Uud2A)I2o5)?^*AkmO5EMO-bs}f1c#IhaYR%odd at UZ@<(bpLpzVO<w{k
zeSy?0FjpHWvPsa_q9jrwBS0D%lXB8F=$U@`?~2{s-K9)pi%tT%`026N-wD<wsvhj`
zP10C^<()cn=6wgybMWRu{~G*}ubV8%sOzZO=(SP##Js9;i(}V(Z`Rb-<kg(f&#Gb@
zJ2on)Pt+gJ?h;|C|2UbN1a38uAIcOb;#Chy#S3LH+ztSl#MQn$l%KzP;p)|ORZB}t
zk}H*$9mQ>(U&)hEm!{UnasHQ_TzQpHW~pSeG4#B7d)U$#%HN_ZHBmRzxG*Fngc}4i
z7vrY4?5=fBCQjZZ?e^7 at rAZ8{8n5wXWqZ_k=>~b7Z>}tRqU<@Zp>e~qC(Gi`${2<A
zob)R^qZ1ZWhNbpaTlhO?J5#!w-p!?FS9}D~nuCrozWw!y5KFX~&3|{>NfUdh*a{`Y
z1uHKWLbskcdv?o2?!mnXp3%zzHg_fuUR>CZJZR7IV!@8t8M+5C9Yn6Ii#F0;kh<V;
zyAb8KMer*5=(F9}%Hm>?+Uc>&w%CRKr(3H=j;o)>xCyq(riXprkr60`bE(aR0+ITr
zC7^WW0k9|fzpYH<$v4t~fPf!UF`BCrO>0&lqqGD}E~`kp-wd!!1PQxQ<Al>GV=^?k
zhiE*?oo0U~DLC~}&XX5NYHI2m+C|om3yUIk1Gz@`a at ig~deqkh+O9(3`MWZ?x)1u5
zn3it*+J4DGq|jr&-++vI5y<H7R at 4~?SU?QDu=RbA3kEy{Rh|(ZrBwx?`s;ST?xkE^
znnsNy^<=?hr~Q~}F6BDXk43QBPBeywfdZp;dX$?B*kO`lwB<@TJVw|?QRoN-71cr6
z{U`w{r<o46+j|~awEbE6jGz7o!w2aVc-}qsdl6kzvy>hTmY at c}T9!||G+P4b7OnsQ
zcJf~m(A at yHdUjIQD3Pw;?evtk&(_&zW_lkZ at 7lTamN=N;<KF1nRjYw8pp8$8 at lRGf
zdNQ?djjg4lHJ(3`Uco0<W$DOt_F*Aa7tQ}gK-7@^%vIM_!^OF4&X;scuPVI1%r=Ci
zqoANDfB5mntCZ+#7PMJjBukvR8*bXMmXef8=e2LKd0cz^!iIc`QWbRPDY_+_;HzUl
zUY+LXYEt2au_Sn`zM^lu_!2WAdYY(;ZO-<;W!+}vr=P`kSC+20Yk0hvlJm^>B$B<2
zUOj;$_iY-_(9&Wxfali6OS%)Mp?$;wl!8#Fb|ZE0WAO{zbrkWqHk;)`Le;G8+>NYG
z*sAd(pPMl2X^#`1RD)Bly(M at Iz{K<2$T9QK#k260zpbZ=0*~d at 3qZwW7>&6KVyLHn
zCksDLdgsD;GfLO%;!(Kw?g~lxC6${o3F6CE87$trS9B3>;rL2C`(@zk*Imi?Zpwm!
z#JQoaF5Foog%x`aB}gC&@$gFAFEk<a;@*`zO96q(``+if;Q}7S74)_sU9UQbYnM4K
z{veb&Z9_1nour-qw^$^&yfQT(lTJ|eW?7sP%!+;vbHLCcOmx!ZAtYimEV1Mv2bT2h
z+qVv+^Z}2J#iW=!mZ!kHn+u`AaPfw`JZRT)4Iiwm<fWti^W+Oh_-{9!ALPDD2cwQ4
zYw*=BwE0$&i<Z>|=yC0;LvXVPd%|9xxJuJexbYBSEBo*&^WSQ*$HY>@siQ}sV&cki
zFz@@VwFA?JC>5`ZN8pT#j`KYV^W`<VAU7yA1hWX2qirhpZh}5#+GNkHGrd?P1ON>|
zZ{ECVOZQ at Frv{*ijx2lb#*y99ud;ngd^%eVbqQuwZ~}p42OX#V*E`rn6%Hj0b~-46
zOD)aDpOR_Ni~Mq=%RLP at x}HPG#APBDo33R-NXG|l2O;r|e%1W<5aJK5m!wY<Ryj<!
zH<|Q)PVpap?SesU&ir6sGTf{jtFi=b6*U7xOxEUcVC!0-K8XkGZ~NlKG0VGBxG#G)
zcVoZo#z>ZH_FMAm1O#sFHIukHY;mS|P`C7HXpx4=r?=z~H$VwKSoCI<bRmtO9WJo{
zeN7<6KL|Z1pYK{CcjJaHMQ-uClLgl33Wd%TrC#e8g*&%zH-34`ey`B-`$ze&+lWtU
zhGpCQ4_#z%G9tJ6)^Ra9D{4Vc`rlexO)+~2TMynAwi#CCP{2zH97A!m1#u>+3u*Gd
zE=ZUJ?lGwaa&RZ=l7A=YXYV!ytt4%{<zHen==g9wOx)Rev7qTT$bb#VF+uhDjzpiz
z?V(QqK%6*tbA~J9WP1oh)~*B3KHt{X3yYqNL;%Qq$5b9D7Fiq3gg(!GO9p+>Dlq2<
zRh})?Z}k=YEY2nA1sya(zZ*p4!u(svesL8tQrs#Lh02{4=|M}tt&1FMc_}>X0&cx3
z{!o#b$OkLG%_R2p1HcXzH=2rL#GRK4a&mGuLl)Dd#3^w&S~SEKV#(xrd1+Qyr;(%U
z#7**D<%;=<`t*Z^I<}EGap#HdH)m&YwK-r(r at 1t;Bp!KafF^tp{u{LOFZOLs2^CqP
zZq^0RbWV*^f)00mGE%SkGEe51QI<wF^U<S6&DQSJ$LW;VFTM^6g1wXW-tYE1MxzC~
z)zJ++K&1+%NW3+L4?o|v;#Wy-?H${}fuviEX^!NtOaWT+2f&+CjuooV8PwLd%tqVq
z!MwJNwpSWh1U(qCxx;lJkQeK&D at kn`T053~l{uXG-aiW#Hg_R_vZ^s6VBY^5G;*6r
zc`7U+hNudR03O6kxaMya6K%rXam%Amb;%k!7y5F1nj?7c0hDN<Ty=kc-`NlKYAN-b
z#KMx;ZBhz~`y(M&5F+1rR8vp1#Rx0SH-1~V%Liqse)%DD1P(1#<GDQq(7%|uFfrW2
zh*>&`Uqs|lOtAQJ-X1Q>WtJBfA1uA!mrr>|Ai(|7)CpVQ<g=yG-*v$T0W{NFsaxhW
z13t!c{P*_Kcx3zyJq&_d{6`F&u7{RW61+b4^*yp;UaHbqvBT61Hu($I6%ADEZ_kGb
z+rX^iHDAe9X~10l+G9s6<OEWH$KDI)>duz)rwpBQ(*K_wgc$ygLn+x~Yt?Mc)*k?-
z9U!hnWTCvf at e_tr0J?+$sKKg2^NT at 4&^kZ?Qv)ru at L`KbQ=kGU|K$JvuFBUWaA!Yp
zLUgSgHyhh%`}trAS$%!|0g1uHTfxjv=ejN#o&2G}NY5<o6$?`H#f0jVcJcJ5pe5yt
z6c3cPxa+hu7-b3d<(VLFZ)Z;%5IMRxef!@6Q|0TUaK`PGTAUZ17j4B3n6 at 2&uuJXy
zKC!J<O#%tP_?KBQgt^W!jsT*sz`f&v1I1=3UT>{WFauLL>v>XNX|yofLk=mm!&6{A
z6b&W`Uq)e}U^+n2!nVK94uD{Wk4Z_r$Mh!JVw^oIJhp^L88pH8NIDAy#M&&~vKOhS
zpr*s!|G#)-I|-a|e~;JlG%fkK at 3ZlSVDw3VG0S-(@CmVTn%sB-tJg3_DXgCH=sNq|
zun>aSZK<eNL?vK$QoI|rP*-$H*s?FArnc6IgQsZ}_Ocrp*VnSb>xU3FbQ0v^GB*L3
z2iL2jrYvjei-$q?e+gZpbkHhqC7mBj6Yc(+lgxDVy_wpZF`!S-o|_-SW3=nNdfxq!
z)t|={Z(O|po>fksvQ|Lb(C~sJm at enrprwki5a_Orw_<N>lsWhVc&r5^Pj7rYe>Rr6
z>lX{Awvkn}t)#>Se-RU))~W`EYvo|rG~&z%cv8RN#GeQ_9h&*~2w3&f>NiLBFL|)H
z(!L;7sN9}QF9jPDo55fbK2!{=F-9vy3Ha^rZR){@i at E@AX5 at e7Nr_;05R>@K@@VbT
zl`;8J2md_PO&UFdM_?YSVr`S9oLJKa0KA}bup^?e%u>>Ezq2(*5+gg5IXRD{p(4ru
zR}CJ8?^cz{)rjfmc4(&pSrthEQ%~vcEtSjCDKVU;S)y!p@$xtdGj{0Ztu$Ym9u2u7
zB^5tVXvLjCG(+i8Fa~Ph)4(R(w94DA=ML?AY8J=i^q4C&&Ty;J)HF(J95FvMp83C{
zq at Ea2m9OuSrHx1;mI^}Kyvtgq{Hhr`C9CB?1>AEMLy2ac$#}$5_OmXyzW!}oz%tDX
zIzAm+@`}~B(=m_l at wqZ6pT55zgE5*4Uyo4MeaVW-tjStGmvIQo`xjH5C53{SC7Y{}
z$8$m9r9rTCa`;3vleklc;&8d^=Jw7G`hng&Wzx+zZ?A`O^Z+;-A9A{q6&SL6!GFY$
z(_a-)1KxB1V4DH|^S6RLaKJtJ(yeWN!7%^Da6g}ic7ea!oIEM*8%!j#zlkv{o6Ku^
zdGh?2!$hOqUC%giTV#QDp=GGX2iGTUv7#zi5yn%e#!i3Rz6)|iIj>G&K!BL2c~|P^
zIqcLgUD>}$ov^CbpUMH(P_)1L*=6@~#kk=-LGgok7Zs*_eSLM*^`<F-`A5bXyO?|8
zM*RVr$+qaQMYwe0qL2`_v=kxw3l#EyXNt(1(4*x0X93y^Z4$1&sX9Ph+GT%T3Q+CS
zpn8xdAtsKt-@%acm&=$S<FEOe$E#K_LIOFu#kr>~{x|y>Ab4C;;ki?I`a5H;AAIm+
zBbojxm=n;}efzeXUQy3PNJxlB=$?)fOS^Rlw)7IoB>#UX=On)Vom&F^%=C%x<Ta;8
z!)%pZOlCR~(bLW0^U2%v&YYleTSQkTxF<Nac1fh2fX+cj&^3osbd5@;bl&@6l!%zF
z)WQ6DP(DTNEnbMuG59Dn7-LnIE9-TOWicx4&An_j at 75&!JlF?)!JW<q_Y|{n1d~?-
zfdtiW=o}zW38d`)AcYJJ3~a1{d2*gCIH%pff+l0|O{9{-jo1JeP9EGxfkDpp4yN+>
zv*?uYY!dk3m6!jlFqgDVp`aY$-Oly;kCFMk92`p(4^{J at vRM^}d*Hf{xQ)ie4=lgd
zTI;>FDsJA96(qzb2`JH#HTEwqdiElfJZ{F+qek(8Z`mvTCjnz0`=ON&fHqZQb%A=C
z<t}z6=eRf|*zeQaBx9;8)*~V$TuRHf^WR#V?(9w<oc%f-!A}AQ_0KxM>C65}<D*~H
zS at c-5MI5NXDI-}2U(?!1rr6I2DN%JQs7Ozbec)dI7sn!!e7m!g=1^Y_T5fd%i*a46
zGa9%$OnInL0 at O=^biC*SX}GaSFjt8_{lGf$MTeRx=sPl_LqkI=fvcBELOjYJpv9rG
z-XxoY$-7EWBVv-v&?SP<ae(K1gziKCyqTF9i#RdIflAPLDEza(jVc77wSCX6O?16T
ziJT;=vf!Q+%YO*<jeV*AmUw|bMyy at qP_}s5NT_Tau18Psc(lfkpMps&pZVm`V-IAH
zCe8pck^B4k?OjN=bB7)kT53j%Idb7qRXh=dgo^nSXs^D50&y11%9u}6By&JKuyR})
zs9~K=0Rr7(67M=?x1!q+%p{KeTr})vF;i?e4r3NHZv7B?{+5)qVPlAiiw9`!1`5GM
z-xb8T=q`{59ss0>Va0n9hXIYlHA4^vY^u^!ztqg7B!geuM12{5Si3g8>A74 at 60?>I
z5c!QzFxb8Zs9JZ7a at x()P%*zC at 4v-fKjAyPkdD*ii;nbz&*t(|72lwGldfaxJ0l}C
z&LrRA1IYgp?7;ML!NQ<#n(&wVOz{?S4$g`~_w&X!MZ5O8In1^aHLu!jkh(2<znS at e
zjJ<h0)a&~P{E<o<(n4h`DwQqCzKoJoin5k%l#nfk3^HaoiXvN!C0mLnW0}O**TfK+
zvK!0TWjA9TGt6^O=RDtU=bZ2Fc|HB9na^DJb>G+azTVgU*-%uND3<FR at pD&R9(w7J
zQLRllmYh<8f?KU$wbtpJej03?c}wBpTUJdzAn(7Z!yq5XcnPi*S%%o*dc`Sj6K?W9
z!zu;7Vld3%v%b#m<sAwcD<7KA-ZdAUlw)EVwtCi#^Q5i*uo|s=u at iD&QPez=EjDiH
z*Nm56x+a=w;APPswP4}x6rE_l at HpM$byw)IC%|=kS8msMK33gWJ6D|&3-4JPjmRIW
z7jnH-#_^jK1Ln4!DLq{nN)|nBqa`i2V>k2Bz!yx&XjA-xGX}%znRK?SB}FvG%BJih
zh4M>(J#w(mlcZssW2u5O$>b-vbU#=1p1leh{_h5BLRw#*G`a`UxO{*-tc at kO{j3V)
ze6Y6YSb|=EXlQef<z^ooGO-;3F-s#4F<e&*(&9ZwAMH at -$gvWn0t(v|HRN=CF)#w5
zsZKoaICUs*Yvchrv7bS_n%e-m0c-JuzU4!kW6S5pFO7<TbrS{hjxA3k=VV9Z?z`&0
z`mIg(SBUFBnhv>t{yMl%=+K4eAAE~i#5LnWfNAp1W6zk_+Bt|VPqaZ%HKpd?G8^Wy
z?<6T7+{7=%r;Xk~FSaoJU%XG)81+%%4si>t4m753(2Uh{o(R<iitv*5?$t4wG<##|
zD?aK@>~vGDe0i!Z;zdoSLe>d9uF>qo>rFwM?k!5lf+-l!@KZ~TjgHd`LvLnZtc!lV
zO%N?3p~a5EYVzEW(JZvzw at WqN)rCK4@(iEi)wz*PRn<l*FKP~HT85ne{6~gixrgyv
zmm4b2V4bnCny=0smen?{uzacEhosLP7oLs9kMrpG9Q38DFrAtAG<=;EE9YJ=xgClj
z3jVU||2V_hRNxHrIjcOEq3={Lp?Q!SOKsn_tth9WP6{}^eFqMl;QS2g>Ch)n-t-oG
z8a;S$c9fKovV&jh66ltv?m2eFef*el1<Lo}0Y$g|#AKhL?V*v8Sav(6$E{L2MXo+@
zu)wwF9$<_-82Ylk<iW%qK+Pb=4HaJpTh1m-eza?hsTHcUa63ue|0uI2 at o#uLz_>1Q
z%a&dC0Ez0;81XZf)){rI0$vQJQTgC?f^W&KmeUH*h-Ck&+HmHUnVQur<rgE{(gUg>
zb7tcMl_n8rtSKpoe~zKW(}S=zG5kOrJOIB&tT9_xfp5L7-mFQ|O>vdhK1*!A;<Ls$
zWb(ZFAZ#uw<e2x)k6=EQM4R1eQB-FpmZPp{5S;ie#pUH$>ETS4&)Q}9v*#ErKSWz;
zFcTq%6i#)Y=Ml|&aIfWb1v#4Y27`jCz1!eXdqQ38+1SkM=Vue~8<N+hQY>MD&FDKf
zj~j&i7l?}1hF&&|#v&n~jamVC*0n28y%$MiW!H{fyVUU&b*EYc5771p3mvGdt-X<F
z{|!WSZKs&nRN(i0SnDhJS>XCk%K0Lf7)y(LQchkM{$JVE;5AU#(mO0Rdfoh&Un^?B
zbNTn}If}nGT-gpq;Xe-#T=(yL4tGtph>aC_f$8C_()BAmbCo#kyZXgMd{<3l#|iP8
z(;F)Vl9G4p&B_sWGyX+HIJ&&tp|5R|R>jxr{u=Q8^$TM?UNbdLXS2`Kcjz|0iD2nV
zLnAEJX1B at X7No`qjddKyz_XbGy8?_-gmsdJR)pVZ%{T#(yz9Rq&{pv!eKa-#I$bOY
z6 at xyl@k!87j!Mba-Zn!>4(8P6*%>PfmvIwC5?b~`imv1b!;P|)mZn!{vFOTV>k;!0
z8UM|v4=!}4KF()$Zpo+3pLZ34R_&p#0>6c{fH1c~uP=ilkqLziE0t_pXw~=5Y1218
z=oznP_^#m|3tw|TsLLDWUE{Zqx9YR{hiJe2u5pV;3jz(2tZ$^*VMm|t_LYI2SH6~0
zIR#a+1Sl%S?dPh<GyI7W1gc$>_04PnadZ4Yb2>kSE1aV3_g7~Y=Nd?W0cx!ZLOk}e
zj5~n~+w1vJwfP0r!Ei+Z`l#6N)()ubB5acXSS<0%u98B@)df#4t+);LW(36O`tr
z0|EjhC#TfA12{NG<TqPoFK!OF_WJBCCBO$%rhSIK*Z#MTm(v5Y%+DW)k{eFvP>TA4
zse=01->u#3ESj at p3ah9qx87YgQE}-noD<Y+1#^W5tQQ7eTzgSRm~H7joio>E9o%_e
zeJOr98A=Xb;Z-P`KMp|O<=x at hN)`K+RF(sp!HBEA_No_(kf>VTXX{V?>No=|)b;0T
z|1w~xXc7k_INq#>s-O$rsA5X7AsR=p*QB9ol}|4QH4eaPqADX_^BTXxcw&WwEXi(0
z_8D6OS`{M|nq)ONY*>YUUmZarYHlG?u^vw(_>G-&O_XSC0wx2{!GCHBv}^Q14<_%_
zvAvK7um!yRy?Z~;VK5kc(T*q+ytNhb=#W#o1myDlP4zfH!BGc2W}H;qeq)a2&HHsL
zJX}TB6ovTZD?Wq!p)!^qO8U)(6+VwCA9uELt?5`-dRDHSfge=R(G$==<ZV#ACm1nP
zQh26pIjl0qac=v7qmPAY%}%%HmQa^wML4rp=x0W2rVyvrtk9#HGd#Q7CyaA82P|g}
z6?xzGsyOAqsa2PflCq^}dF1>ltKzijrZ0#xFh$C at z}4Ug=&PTaHjvMSh#H$Rht&^a
zj(fp5A?M+BQ}#skWf$zbKOHu^f3F9&A at di;d=wZ;;+nZZEoo<O5kj2(f8Q1pao~i2
z?zJT#3sw1TH-m8hExhmWqh;0Hc0zu;@{;c5WlTxb-v&VEv;V9^d2;v&&2t6)E=*Bl
zMc>bWK9mne^XP?ZyR<5F;?$Pir%%Qq=1Ng^Z#1 at f0<KK+c^ZF{ww%twF^w?rR<cPx
zkD~Q>UuLgna>{y)O^hj|JYL%1q3(U}bKra1xtgXvjsa|lLs)?mvinA8i4-c~Or^E|
z at weDvQrfIDWaC;!!MS?Ot3RE2 at UnZ9U7&rmsg^H)xKt8pxD6^E8XmR<;3Na>?mk~R
zIi=D5;l{h4O!|V;`*CCTX8(9w8|zj<x7WEc%2 at jGn_29vj7=)s63{Ma_Nr0%fY2ql
z^-b}R^!;Ecn~HF1&xmhZN|)ds-VObsG~{~gufXH|=>~|j-QC3R2iHekb9+w)mj^}g
z8z|_%Hsyw9NZ*Hh+-C~E(M(*_xnUkQpf>(`8|-#d|Bdb5Be at _m^f<6$*pw9I9$77f
z>&9|#=S>|#=-i`~D2<r-J8lWcGERTaPDGB}t~Tc_o8^ISWy)L|eb5K at H$;Qio$<tX
z=%O87O}^5b4ESO(?_lzq$4LHyRQ)<l(-VhAECT!hqfD9*aOGKJKLGdM?muOGU at t#E
zvP1Ab*I0i5zz>|RE+%Gy69IsOUNzggXLr?ZQK|pXjnbBY9$Ei$sY7XTv89H6w97TE
zJ0PPVXm=T(TceZDuRwAz#0fQ(Ijs3jdC_C~h#5E3UDZJPy1SX`#dkx+aq+p?^Y;h^
z=x(|7+)?c-#Fd7<d(*wXR2Q<yM9&W~$zTe4X4LUDtoxFn!$qiQG-b5y?ThSp`-r60
zfmHIDeB;>aR;q*1-&RQxU>^byB5{5zvnD5AK~Z|oNN5oL#<KyS9s+nysDlR%_*f;y
zN<0k>eai-40UzbvmxmL0uwKxPQ7O6i<L&V~w8KkkOaEB1)kDrIf#Y?JJlZ!&gI12j
zS4(76H<!inW7mlMEM*Ix at d%~qnqYtBOFR~9jg}9cs%$R{yQq^hrKQR8l8)9>`ckoT
zwPs7y)%P=k-&gJM&U21R3z^<8VoIl?Bwhq#_d`5BjN&vqKg1`5r3c4AXpm?ln5Jm#
zqh)a2qrJ{0)1aIC|I}?!tOaBLgN8xLW%jUiFv;)7cfR009Biz?_d^Yxe{j2GNbARV
za0ZKo+*KmRC40dn`l|<$xxy6HrW*RG3pEU;N{Xq_k62GIEPVDp{WM(@^;i_1{XxHG
zwZMAzuI&<a%VK*wX~geC;YqbatcgsIz6JGm1WG}qGTBd~fOnWwUDIaq+}GEqXxQYD
zvjOUABk#ACu|^;BiY<(9wX%&4!non`EttZ;+MwCL+Mr$fx}Xi}$Q6J!^4t2x98fH_
zXM)yzJZM^!cfC0mv at RznSIEi9NuZl2RaU~TC8 at q*<B1BLdfvcRf$tul0FJwm??uQ5
zN_ymf$(&Dbt(H%>2tt}vlwZxfcV|TBSYy$+72|BG%J~-$zl}yUX_h$!U0Ixbb`IE&
zOPs{8MQi^ph1Z(Bs!wSnku7z3Jk*)|6+3 at o*TbZ{V4VK)p=49&XOlb>f}Vei+#hAN
zKoo2(z;rk at Zn04)E5{9P at u_6=w7o3f<=2y4Xj;v9Rf6XTAW;7T4hm{-*t4VaVHoG>
zbc=x6kJEp^-)bGPte1TW16-X*fK|<{f_ at BM_U6X`FlV6y65I5ssNFLfS(D(;SQv{b
zfAYI$0mxnIh(x$$CEDLl&iBLG`kK#wn>BHdDUCAa90Nn;dEA8v#2ak$N}l%4r=n~3
zQ{G?e<a$x3vNb@(bM*98Z0NJlH$r at aWOv<V9!;S>N=XC=8CLf-^UgRHsc}}d#UyCZ
zt_r#qLs`LvCIJ7P`%<`!Jk(~5tgD{#|L#X}Yz^?6U&;Ktw{`#1+s5_EL$#HhkIF!a
zU-12=&rP3;2aZYYtKe_$-R}iBjjF!1qw8x^_(l47N~4Vn=oOv4_x<p}UC8OKXc3<Q
z4xDwlD&T~K0yO_5Xzy4*9=~Lzn73cr{N*yBIDk=>Gk at wq6R<mD74P at 1#>J^OA<Eyl
z9|lvJY*<PTIBH;-TBg~zN7DvTUX-!C%w6~y4Vq((Gd;$Vp-hCLsISl{$@z|jOgs0R
zpIi_%K6Ut8{&(eERSI8|k<N|>h-ODsD^|c1OXENkvDQS=_P}^ENKU?E;l81YiRR~e
zWYGGu27)#|)&h~NRmQ<!U+5;cTPA3f^*aJ|=9AG;v?isd-27{QaJyrbhEL`1$r^CI
z%741vrW6>f0l?v=Vq&mlYi1|>#9{RlWnxzaVs`XAqC9H-Y&RI}9vgI;Sb`Yd3VhL1
z(5%z~Rcdtg0ARsQfUfBSKo$8#23;MFq|yBVk>ou9+FPF;l)pFh<>^_MuG5o!rAivq
zFME6aWZJWCy%-uAQcg<)RT&uS`W{2sf9~A5eD}fgB at dTyYjQ0nps->rPI&qPRzu|8
zO<IKia_O2r&)JP%g_NDDuh%G7gm+R2wO{vLJ|tj~nsz(!5TwaiYYnCbQT+Q)SFD!n
zaFY&NE-}PvR*y$2o-Rw)R$?eC2t9BBd9?G+8lMg~OShV{2Hevhu5sEND@$UVk1SgA
z&6*I*9oCO;^XG=L!dEQVSDQ%Waz~sgWw!7 at tjX>?2^eRT8<jwrY~&c&`*{-}2$})J
z(x|>`)PtwithcY+FP7cw?!?1m0|&uO#RPB>XHJjwc&MBPqc3lE?ZT{0NB{syz_qXB
zl_q0a{<Vf*!LD5)yrTMSz(GGr^#(w{?q2$UuE_sFB({RtjBJ at ZC8t1c393-MOMh8>
zrQYU%x_^W|d0^NSnN#rDG{lMm?)lmKb&3}FD5O*U`>g?!CLV6mqIL&JNkPiM at iJFC
ztQo5ei+06lqG!|LEky`D-9vT7@*pYJ7b50?s}>Y`MXW|QLdj<#)6YE_zZ#VPJU at zm
zou6l+p(P+^8}<g^2J9n%v#4w~6}ykH;lH*>cL&5M*ta;j;ijxxc^gS!6d at d7ZW7yG
zc7smdWk4Ev5qJcC+lNjYO{<EkQLFgu-WVedcng{S75RY26pdzt%ep+M0Dkm-U&#{Y
z4$a-GZ}lGzr#T*2Q at iUjf~l at P%NHB6kT*A7LtnP8(CMIhIYC8(a|YbQ64 at fo2T0{S
zY7q{pgGVk8PZ6^04s>SO4yV}-^#=(P*~_;MXdk`fduL<nW7 at rp&txA+a&X3&TNbKP
zD1&D(4na2s=L($?gwzf02nyLD>FIpX)~#Fnxp{djcY{>r)X}3OcR at zCeZ%eU-I2#}
zadCp6MP6bATI8hwY`8q&FlM)<qGb-?5KbHRCwR62OPPHjaWpkGHQ)Z*p at UMY78AQ5
zJUBe%332KnCHGNf|2X<CP>~Bo-D7%NklW~}B&WE?_|-!lI8V<Rjdp0hai{aHVF|q4
z)&SXSe&-T&oj7bZKKqWkD^%Ie?t<EFT63;9{BgMz%t{#u^8R(=p`zdw$-gJ)LF@$m
z>?SRP at 0Az~UA?+3)J<~iO_bV!8}O7EGq}IPw*a|fP`<A+7f5gl;B3L<d}aQ}o^(S|
z?f1;39hv1OdaaKP!VBlnMJAQkK+ajr$xWm>L91UGoCM#reUJ>x>s-sct{<V at 8hAMO
z30Y49J<sw-^vPz<DkmQ`lbW57JNJLMOMeCJt?Zz!xd-w<V8z&+GwYO?U~a({0N1s5
zr{4lD1nx8VxCg|ng?pbg%;ZW-(5RxnYcn<)a$kJg`MeGs7pJosuNRe8#xnIuO}ij?
zf{L=EvbXWuismGa!<DhBto>Y?XRfw>Orol7`FN;bzn#s-srhGotp at Qmg%>paEUQ4<
zist%_tZw)M3<{9JATX-#jr^-*$Q`hMym>`6%Pw8DzO`-4>- at G&#s8LykuJN7ROb1z
zH;)k)&tcPZU;0HbUh~sUza%wKlTG+6#J>u56q%q?7I?b at 3R7BRII6Dvf9nab;7Ff)
zB~NoboK!a)o|}^eTtv{?Z at cy092~h~v*DyWwC3}-j~H*`W43#Z+Wx)JDt)#&AZtp`
z;Lv;9>eT8_x|Uw%_V!SHst~W_9*awU+ at 20l{pu;=ZBd at Lmow|GXX=03HT+rpb3Tv$
z{~Nra&UtkDzeDx7Hkg<J6Ge8!Ba`YaG%!ri3fO0<%`)xafGLCNg at Gq`#lbM-j>{L$
z8MA}7EK at yDjWa{mc+cC0LT#=8Qd`+ou-#dRxmdVmWPHvcuAr7B^8J0Dyd=bAMjoRn
z-WBP1KNUoFjw=Oh*Lz3dsrPEij2rsIc7e<(8 at T@eVF)3>5MZJ^A%jHab6_Mi#%w8L
z;qo;AZ8(B{HSN^rMN^P^gUQ4`z85L`kf1-r`^fbCeOg~n;D5W<?0^;f16aa+>zdOu
zjD-G<_qjl(2Pl!jO!=*j_}jBPAU(6t^vAHKv at Y5)?giU2mxd+FT{b#p-~P8$k$*`=
zS3!d)@m5I1<KDt$ZkUogs2F}RTVqbf?BzIY(A+R|ZSb|b`Rb*6r$&xac5(bJR3V;>
zP##l`@SFD%!f*A8P~MeipoCi1Bm3wI at u)U?gDN7Jnu&(rJ-})(@XYs}_v<1W&{2s(
zbzt%#xY1O^`a^kewq{A!={X%g`-63nx75`gdy9^V?-~}WjHEVN$JSJ=d1s at p5j7+0
zGkM@~yHlp~xHz&x|Kjm9{E3GAMvDSyk?6j992jMKZhsSm__(MjEx;CwHCqHq*8|nx
z$o at wY(h{6n#G%tsu-)D~Erw6{{-`hf<F_*aL4U%nz+JBjbAcOoe%-tUt{(WBsETH;
zo(s&H1ZezwP-i<=$BR2`(#lz9tQc8ZCMu at n1BgEuOj%%?WokW+KqEuD=d(UqJ`up}
z^nGMgZD18?ee-u(`v2LB)w#gMYD~;BL-n|tn at y<*Lj6UEAkfg)13sQyX1I6Hp7WBX
zS#Ln7%*?x5U<pi5d9%8N%ifDSvKu<iU9vu+Wff~}`v32zG!H-?;Roe|0o5G)O;VaY
z=+}y+^Z7f*3U1Pp;o*>tlHQznLS4M==p07z&A+<{|AU+s9l14tJ$eNq_>}}RbI^*?
z;o=dx?hxxz4CY2Yt?X$!G7xfJBK64tKF2`z)Su>OfbnndlG_S_=7hK5 at 4-@_u(WtR
zo87dWr<yfzUbo*zzX$n%X%Emx=ljJd)4O)z!e&r#Jq58nz1dI_aH=d(6e?m+ at OJ+7
zYuijau>!x^A0;1!2J`7nSF#bCpn>~LwTxfXy1$aNu%5o#DxdS&2&Cr5DPQR8Gci#R
zWWdx^lG3SDR}$b(V90cIrba+!-l{kw7BI#(4<711Uv=#6lyFyq8|N>oWvtS1(QM#i
zof9FbNRXxfyYWp8A-x|2-pgiRau-?2o*;i$ugj^G1FAkFaN`4ko}La$CHB;*7t$Qi
z``D-F`F#5Pqo at QhBY5-T55FtlK|MsM+YSYkQs>fsPp4$=dkch1;|RYi$sMY%wg6K_
zCl4RiN^o#0T8R3E2=tF`s2IwCgcx0e0Ylcd=VbsN+~(l^OtW_ at O6A!K`ViC`j*<91
zHhCdROYeNUVh&OkUQx^|b;$1GjXHqi9t!6?skYHGV+XuAhc(-}JlETN-v at KntWYp;
zP2JN34_m07GG{WFHJ@<W=-%N+8B?uczh;Z95{M#LvwqPJzkW{|TyWBVb1S4YxPGOt
z6RIjH>g!v3ie&$w!4Tqr9g_WXGCb?ky;dF-_F#8C$C^?Bf7yBAYNL^Xh7K`uVR+S0
z2EaB#8%YA_-KIN?9yg!A0n8x9+W40ltd*brBl*yLT!%84_?{!W3Y3EG$V0EXfi%QU
zBwDL at VeGjpjB$3q)2r4o at QDfH%&cb3R_9;fTV?o(yfDk1P at mOugpB@9%(ihb6zfTB
zg60AB$2j%;mhR#ZRZ81SsawxMT_>rw_j1M=K&a=-*%sTdY{it$+G{P4yl?V_G;?80
zB;e0LeQkBX7Dp*b{!^Ysx~Sg0-sgNYN-y&EmN4S?$#)M!t^<_%eTlS_q0RL-E}T5;
z13GJKlMf#*FJT?QeP!fD at Y<9ThDB#;q5Zcu@~>;#w804VEeUAkXt*1qPpoPG`<-yo
zYlW%T at f-u7_nZL`boPAWHzfr?^3V67-YG$b%a at Eua&in at 8w~hpy86t8v}SQ?*YO_d
z$}NxGtKt9gR~>WeTE*`?8~3Ub0>Nl+a-?<{$n}q}mk(9xwk;5HXp|$V)otLW)folc
zk+~)8_xasqaBV30A)2rYni*DPuAk&aVI!9hLD1=;Wn|9JOXh_OX1^?f#A?@q?9N9k
zY|`>uCEhlZNR9t)?y~}#n#yqYu(`752<QbZE%&$?f$4t&{e8L{rDC$(FZin#RPQJj
zg0_9s`RMXzK`^E-Zo18|Gev+6oy>3U-8=t>mFT_jeGoK%b+1`j^dh*yGAp&i>a}z0
zM{reVDz{z(d03Bd+vQuX!OzD}sh2UbJ+Kt(6qs=W4^-8O at DtR4{CG84W}F-^x9i*z
zg*;ZhZ>$>oT&kD7u<gfr;z^_TLQkJQy?i}LUN>iJ0N|*<We+tkTm#felb?l at B7+FF
zY7bL38s(WcSG8^)zPej=$I7<A$+d+ccF~nQ at B>@&edpc{HD68$BAsz&l+v!i7!+R3
zZe#0stbXi4{+W0keDi^qiz~gXgo^;W`SG3;npf!UN2g8Mnp+5|?@eK420~vSz0Kl<
z at H^o<H6CyJ13u(I-%dELpI9V&{P<YTp+prA1*ptpbO(FHtiL0(SxsSZaBvZH+3I_q
zyvW?O8TiWm19G=3KkD8*sxJOgd=4|4V#|hN45~;RSwB`av!lVDcjSYT!5l-Y446yB
zTC}ho(L~ljP4<gjg at -E(XQdC`#I<rjKBb4Q;aXPX<?{=aeUAS}c_4u8iHOQ}=(%{F
z=ZN2s$CjPa64zcosGo+SoVz~Wekoxf;@a<cQ0~rVKt9$_O3K;=Y`pNX;3kq!!fDGQ
z_>;6Rb3`t-<M8=ojwdO-{(~jEpOu{o1iKZPQa;{wnP|kR)&g!mbZqtgV|z-(TSvj;
z$JaH`-BXL~)MEmI%C;BgiN6!OTWaAN7UVFrfKk}36 at UGu7sr6r;G>V=mKRp-_9N$!
zM~qKMsjGQSwpH%fcfw{;y2?^e(IrZQfm_hm1C*<~2A>;V?c{h<p0lHJTV4ZF(0w~C
zDHCF6MOdE^OwtF^Plx=v9T0T4VkWjCs(gvenpJy`oqnR7)GW9wN%Q->)aTCe^5-s*
zUw|Dw>*=0RC3KPp28vgiaPH05kMFayf_xskXl(H8!$r>&V^BSScEc2^Vmva#UQ0l#
z%7ad0C>|8|pe6m`8%zP!J^=EKiDlP{d!(B(TLaX!D>lt at -LCkqbHaef?D9k3UmGds
z-2LzrVvd#PEn at TBc&T4gP|IWY4UWKLC&ivT;oG}o$J-NojQ_ZB?#82?Vs9_)IeNwP
zsa~^+LxakWQ|HcvUf?SV_A5{6Fmp3YiEqU|w0H1+ at 9j`RS at G?6D8w0<VU4g;tOTR>
zFuHRU4kurq2`_px$0`*rG_s9>_rDhy5eROb_Z)}2!kKHMVa_wgCd5!7xa{(*PH4k8
z+){08XRhQDMr7M1t`qMuP<<rYy$|+BHW<>DP0IUOxh%>t;E^u)QpDsl9`aU)m at Abm
zE0?@)inZzsO-4N%Hu`a=Q<bMb)`WIi`$<zq^0x*scVF7I(JZM^wAhm_`KqG)!PsZ$
zx>s9i(~d>~MiN1H=8cYtiEjTCtA+JWSQqYnFSlh<G>7mbGmN_sj+YaE&Z5u?Ged=W
z*R8X|ag+CAhNtZME3eLUSr7}_SiXl33%`j&a~!S~t&BNW<mG+Cs`LKimhRA_-%sJ3
zS=m+(Z6;n3Mf|1(cS?QKl2+CeehTew2EyJMd at PXTin)>l2tKv)B=r|=_nkPk`bBdX
zMBae;uzIyuITps98y2_+$u04r{o7b?6i<AJIL52Gym%*me{7C${s-LTeEDc4ewx7=
zB_$h46?JVQD5 at 4UJ?~`_1(#%D7<4X!s at 6%)z(pLVln<4L2I<FHsx5cqO8>w5HGnP^
z)#7_JEsQHy{q9YSUm2HtEzf#QdfbiK`D)h07>1^{0hE at 7-y&f7kv}m#!Wg>rq>xE#
zRI*<hjg*Uf6wIDPx8=Y6)p&D28O(BSF1MvsukeIF{KopJGce}46J*J1c-k+)=lsUP
z?>tSX+!A_F-A~{YFfmC&ZfA|b=cT2wpWX+)(N7rHKwY7huUE8xLe1;IG6_Z|NjE$!
z%fFn_I~$R=VCyE_W5^m040sT5XAQKHn6~yk>FJDDXu^YrxAb?S4W5IC0uuM2&QI)3
z$G`wC$dWJLRE1feg4b>pbh+LnM-(Cu4_wU=^l7;)u^+<_j8&L+I0f?c6(Ym){)Bz{
zoK#ZzgF8RUWbUlRFKr{7JLi=}4|~xp&2K#<TpNxl&q6})a5!ymdZe^P;zN?0f8)f-
zO7cO2vURg?=dJG>iG2?)WGCS!7o&zJ9>-dKP-eEZIuUzSm(w>$oE&@;!cIYdOx|3S
zch0)uLg+b82s&V5BHTqNw9M;bC2BBYT1A(>9~@4Noi>~Hth?{usB7)#j#%?2wu22s
ze$Gbr<gYHi&TNYy5Y*EC_QcR0(NkmbZplMU6d|};)D4sNEBYFJ^jKDVA*#X1;i385
ziP)(v8<UekYcagVJ}E<=iTZe;UEv%dU6MSdw|3N<S%A#S?mNOh7f&wDT^HPEOvIH6
z`}pLxdFbs^DtqWS6L8UlAYg|S%~Kho)fyz;uw)GB5Rj_K-q`>*0c at Y23x%eMottdB
zW+Gf51d}c99jZ0p>90gS%kC5Z{jr}?SYOGt<`$?_<n?zr^3LQ3#OT`2%0eXNOl7a%
zhB&s3?6^|&g9k1x!x~ocZ}dxE6~{l##2HBwNtyW4i!)v3rFxSr_1(Lp`%~Em%g)XB
zC;iTkV*-*$)jOr+z;K*Xp>M+rhXUr0pm-bRlMDU&e%kuu?_s^biq~RRly8rltC_SL
zM<fVXIYke}gs?2t3OZ(=ufcdXUr6)*z`X-!+o$KYZY9Us<mOuZqy*=r<g*}Xwh!Lr
zLx>3&f3p)~>WmFFV@~wmS$FN6u&AIFFHFng$YR9C1zBYbV#0N$Ga`e)_-&KG6YBOC
z?sG#=C4|+;_EaL at yu+!+^saKj-8QQr;+-peL$Uf{4jMk^b4PO$7Nt{G=1<oC=oB`0
z#^TD45F5Z3%W4mr!OhfSI%Z$~CTZUgbsTQeq{d1}+WrA&dj*}Yw63#IZm at nC(VjvA
zn=G88yS^56hp1iIS=baAmguqagMh at 5{XJ0XbIgT(M7R%P=74#r*>4&}zHqSebsm0T
zg9nd%*B&Ihb_+4h9kL`nhglY>RjE|V?a(K*&U$x<)LLVwTMALV*L2U?Zy%d6_Ht=N
zP?<X`(v^o_3px>-$$h^J_Q at g-uqHxJ*+>jU5q;p%&Ucjg$#g={VUzZWldu-Vmr<@o
zX{Ak7g1kWBq#t|sFo!}p88H~IrE|4*q(m#A1Y7NhAQh1 at E|^U5Oa)mk4_W!k^NN*g
zyI#LObCK>f(B8S>kvrm{m!D{k#gQFlS8E)lVP#d0u}%43dom)BN-XNd+KCn6&O%gI
zI%9ulnfq;EtPz?j8&)30wPEcYR_}`FgX2!buVlgBrB3Kus?rYi9ZSG=ITc<D6NHzR
zA>Gq(s+M^4;2%R(3(J!h$4}GooP{|+*;8o8R#Ks53T=FR&ZKv-gp!SWcR9jaWj4v7
zqBzWoryq}W$tpkgn?02FHM(IwtTqXlO7dINglo?UOk1g4H#A8)<c1y&jX8*B-kEzy
zxs?f2o^Wi{!mIpMm<0=MLh&epf0$g19qwg&8n$29XVy!%nTfmUXYkurE{EzSqL8^?
zN at JyB&@44U>~QT{9TTrB9 at 6<18;q}YEH`?ONiA*){WabI_f!CexR|>f<Gd^_Zxq!^
zSwP}cKY6G(BBY6hdQy2lh*d{yM?@gO`!|2)g!lbE@*>R6^kzX^{-Ri|H>SYu-Mt3h
z_r`?6QS#*pLbEBwrhVuw(Ex01`|>Yiw}+8Gy2U%&CjWUBRb4%pUlCHoyv8z2{Lq`d
z5t+gZxZiicOk;!D7)Gd|67GPPwVu*kjb9PyVKLSCC2$&(qZoB8;*^`E^|%H6)Nu-m
z?Sb_ZLaaZ*8wqlxB8&&3zXrIJjL9mG#IMv-#B9%RY7V&_byNFZ$d-LXKFd7xx7GNK
z599Z7iTI&%myg3G`EPkd*Xg`~6^~u14er64O*1oR!rtN4@^7RKaO17b)eac7=Z>>O
z1K(P$9eU=$oDs$loYo|&1d;h>b)umZm@;boMs2!5iitCptmlz<Fj7TjV?_Do at QnyP
z6-}$V4UfYJN>+Gf8`=->YRE={t$DlUZyV^8Y1X&yju;pj4s}JYpQF|;b)Je at AMmK%
z6ee|D>f_Izuy}`f6?8O?{VyU;$ibtm0Wtnb)Usf8Xe<0wq^`HiX0%z~0)s4<Ir-9M
zS(<O+1_(nq;L{#5-pMYnvp()^K3P%Jyp^DcQ&qLX<YkBL%&9DN^Q9G=?f2H8#sXhy
zB7E~Vk#IO<(26>}vw{WpsadZrjkQSaxM)IrpNZPgtCi+mFLt9B<>61v`mZgqzdIBA
zo4djR*V-fEo9i>xBOo>FKS`*g`IE|SE59M9f2zbx<~g5T_Kb-?=+m;L5}1?8j8_J0
zK}+kx!w~YDH#nRszZEJYJuGu>Lz_ZNiu67Apxt_r)`Nj(70(9yH!7*Gx*%o=-t2g}
z{Q5Wl02Z(lJsajMKuq!WqIf)1eZzjMT3C*(B7GU at -^ffIXtBVUfqf&-{C4h4OuWvP
zj25WvXROn{RE#>$`euQ3dR-AgO&_e~fzfYKXvYef1x3sKM0OHedhKsIirBiMx!84w
z*tareQ8AgLYk_VsGx0jgq>^|XpD7{e%&FOv_czu(fiuS0TK(;-&D$hWJneEvk+5kC
z`{0y3rm@$<(pp6|eqy38_)(S4-dnxYkXR>n=KS|%CW%<1S~+VkQhMLJ(K}aQw=CV)
z%h|sm-=a`KBMiM2#IzQomQ4x;ykT at FcCK>o at 4J&U-w^^oQSoWxxPN0}v)}Zs*k9SB
zDLZ!?3X=5yR5TO4(a4vGsFe<1^0hE&|8e{`Uk0-1uXwlRnW1K)^{j>FK>dp_zZ7(N
zhrWrI)K|hLSbht3EoScFkZic~HV`dLbc6odq#6wE+&Q3c^S;}i{-Kg{s5CcUig?6I
zh?+6MVdTu#OpI at N#i)hInkRxjQ^`(UlKB4Gscm3}jU|A|XyV#xV$z<vy710Gvbk)i
zqhgdZQZ=3GT<!~Ez4c4cz1{9%bIa16*{xHv7#t9Z&}z71#2Swec(S-{SmNp`Lz-6u
zE+}MdV$$RLnclfi!rI-HLOViC>MlBr?$<XlnVR+gnOj=`TXUq)_Is;a`bg#ATD9vA
zfxX+iJ6h!S=Pyq&<%59^PC-F9+$maPtZot-L7_!st=c|i;c%_9ifS{~)1Ip%6{C&!
z8UBr4MnGRzCeeR=KUD~_E?b+0wQc1BMsrO?nOS>zd`UV5-F>ZgmW%lbhzyDpFQwkH
zI8^g1|Hjx}rVV1w2+uz4`q^#kK$XW2i)nm*6mS19rt<6<TZ-jiZ{}e7kQvsxtFX!Y
zb4<_rNA_c|m7<Kr at ld_{bg;r&C5`LOARF1IQkS5N7=w-d(#T$i*Avm6`0`9e<qtR~
z?}rq5w|%-PzK)bL90P~obaQZaa1k<dzBD{=0RC{>-iWCd_v<EJr!yE5`iT?Kf2ss@
zaFv9;m-n-px!8*mp+`sL+gN&q6G8<s4B{`(qGs(!*;`AKL>IZyn~Cs~ImP3-?Egpv
zi)5v4kmT8kb2Dtd{sl`-6zq=I^j8IIw&D0UI_H$$MpQ4YbN~%Ft at w1jD%_u9Jrm~N
zh~iD7Y{8LxMBGOjn49M5phOcZuaSdw4F?D;RD**?j3|Vy-5DmT4bj-PGFpPh;mCd>
z=%M1;PQ`AV8$FmS#T5k~nK33P^*WL`t>6BYZ!h<`OwFV;fYr1Qs;-o|$$XW&NN`Rw
z7+<$E3BgkY$gXStsqcP#tH`U#vt#utJ-I}pt;^=*Rqv$_ya72wBQ?E1-%H9l#dPM`
ztOePXj0HYsOjS_*@;c at D)+d(?ayNE}T24?=K$pTf)j{x?Gc&EYq#yozrPmnw5f?KU
z87zwyh;QPgCaqu#KW(3li7+CFtCDO-#+s<TW5gg6mK}w|EBvdy-FM9En*aJ}zve=d
z+e2x#_ClQJ2h2*ZGV+}q9#C5Dw`yj;`w>q$+c?$6l6C}^uy437Qj|sN5uplH8++a5
z7GXNYI{qeX{^fFmK7&7Hb(2F6ebFVAFXxaCr{YwF{w0R1;T-64wTha*00QgCXspJy
zZY1;W!QpD;eC9K)-us&q<V)Nf3msEBevF;dP1)d`+=$r+y#4ca#eSzA?83S8CSJ;0
zdC`Q%e1lBn#TDrp#hF%Ba_@*mkA`+1<@qL{uumMY?f=(9k1uI at Vf1zYt7!1>Br(Ru
z|F0*e^<!t<E^}N+lZbx`-(0eg8|%#Z7&n=JNPV?k1}KSqu&*$`C*NS|-gDJT`pznO
zLK-C2lJG1=nOf*ziC)0as)4BH|5Df4%Si_1l-JOcOS1BGMk%04SK(Ce{A)$8dj};L
zWm at GF!}YLB26^-l)3r8Rx(!fj_P;;N93YyGacL>KWo+oAVSPJ=Je%nrnm0e-^fUi;
zUXEuhWUN@=FnklPbp~<svFym{T6NtdiT_`lC2dGC8ZR{65EDCMxtdpVyfT^h5O7*|
z{&ikz{dp3YTfNYF?XqHqv~`otn^RQO_y7IOoVCYz^>axS!=<n%FQt at -iJs*z|BuJ&
zO!`eSV{q^bo7s}~K33{wrJx4DW6l31DtC;RMkmWJhcG0-H!&XPitJq?(FcIG2>;U7
zA?7SE(l2?fBAqP2cX{=z;hgzc18V+%KeobiVntoYpJF%@_T*@j!tJ3im6YZBFapN~
z6ECM-|1^b*dKv~+27i7!?U|s`Y-l;J+A;yP`v%C}|0#3Q4-h;Mc%#lj(={<MFGZ`_
zby1X<=mldFY~4UL9vQ#v*xvlJBE1h7g+u<F)u6V4VJB9n;>jhovG~)7A<BJF<C3-B
z;#7_Q_2_6rq7ex^*_bXSHgf=6Y*Fhcw3=zQPz}WX at Gp&suTPL7QQ;3O2%iP`4#siE
zS|fci+&4|I-T#s!ZO(eu-{1zR(0D~m3{bUJx9_zs9ZM!Uafae1um4*im)A9c&N?&3
zEpUE((I?#(?J3=Z2J|;T>V5x``o{g`zB!n#FvYMv?8&Y`-X^(mOIgZFlOQ3GU7-A1
z?zH<@G&aI5V^Zfr at aI+PFlM3ANS3LqSp-gg!@p#IRH?f3783ysG_WJo`oi6xF6Ey?
zG{+(XKTH}6{?n?TMU^D2m8ZWJ;EUfeb_jO+GaO$wJjfd1e1 at B}3FgyW)z~P at 40Cg1
z4WHAX<%c<~2yQnh9|<Pga*r%k`j#MBNc0L<>3#-lGL5|UV=O7M+|oiR+xp?u+ at ZN2
zxeiv=rLHIX8EwwB%cav+D3ImuOR=<D8XDP&&m>HwiT7bm2%q*cI+7wQ4Fyx)`H4Ps
zLG-N~w9m?07WVm)$hL%q0bA0CzQQD8lE2Qob#7jDqf*ziD$gl<i+VOHPgE?dr?hqO
zwztZbX55^b5)=?!{V5Upt&*`$o at uSoUvzA*E*pvnp;IF}KTs at izI@RcBUK8Wee)A2
zfE5Who&L;K*|YHz8}7<8R!1S{AS<H*Z${M8FDp=;RpRHg;x?XOA5XYx7cSe`5y|i7
zzxw#C5F<?3n}P?J)WfFuScU#~*&EK|WLNEI27*N&!&p$0N6n;~i-uy%6ZF5 at j!zkT
zts?JGN=8#*ops}F-Q#h2%8RRc&MSfuF7lW}BpQ*#?X==5apb^^Y}w!j+4WtmwnU0a
z*_6gycD86lWA=RDouM!BPE#Rb)w3>nB>mlnQjCiIVO~n)w;lEm>&Ij8Ml)&fwaJU5
zm+P}thsDlZ<(H{^1h1s8klME%GHDOoC61aG<gDuBWtt$mT`S$D!YkKpXVBt<Anl{j
zlw*5oJ$0U`ml{%P`V1<}OEGG?eh at pQP_Q1pw*xX;9ncLc9*-|98s?R4zn>ry at gT*t
z{j2`8g&c%pg_z>PTVb%Z{5!M0l-_OA_t*MToK})d at kQ&YF~nQmHQxkDfkeGdUa$A7
zH at 88sjrjIZC6W@xb=_S^AM#m!Iv%m=-I?F at R5M#Os%@fes%<8G((1z9xWrHGNHvy<
zsEe@)*O_PY)o!~Wl_HW`LchnUQzX0ENK_fJ=eh!vMbSJ)DW7J9rpKTST-O&p(ir(T
zQ=soqjsaa*@s$}xW<T%B&z<A6o{L|v6>i|`Y!iK|351^Dw;VR>1t)#0q9r0jZ6C9b
zN+7cXsRlDB*Z(0Sc4m(hY|15%W%ohe!q}u{n<!J?74oSGBJof84=02lwC#0URv9}{
zk>4L&+wRRu?TEScc(Y!<pwfNITxO`K4BGeW76S4|^`@%Q<W)U(K_k^)P<1|BF|SG0
zB6lseQ!=M)Avy**v#F|8rabpXmf@;9MMnVsscSs|UXb6t@?<z{3nljwyQtfra?Af)
zoufWT)lX=alLZ9xuIQHxbp9$o5g7d-^3Q~xeJh&l4;vMagX)R8!Hda4z{t`|vvw9a
zy7jDaOwXq99v_xZ8JxnpJA5dN$@(cTbMm-!uJQ0j;@7%e2`l~s;?j|UWd`c8vKX&-
zbsnYAyhRV|!%g6;rJ5_&B8Idj{c#Ajp7nVmtj}9j0soYk<oG^bkj5&y*6UR%9(t^w
zK08 at 4OjJtNlgQyxY4JBVkyOidyLJ+K7K>a3={zBeGCtoB4xNgKM0uk?MXHp8-`^wT
z({?f(aSs&S3LAD!8*BEI9E~~~j>A)$MJ*D7w#u#tYKjzZliyoG^#tz1UL<xX+YSB^
z(UX<;%@l>s#`Bx?E!}JMk42Mc;ZX}jm|7Zsrbhs!Yl|9x?Jszr+E=%-cj_RP1gfGK
zhUdX;6|ik9LyV8hTN4VJtVhtOSgJKu)4lwBhddf%o9QY at deKQ7&bykR9if&tikO#R
z2=^Dw{4q7>Dm4!Jm#|*_BI3%Fx?3`yn%FgqS@)T%KuTg}ch;6Il&2$lP;S1?jf(Nb
ztZrFUI<@?O+qg;}JBp6W??Jvb$SioVRyN&<kMX}x2UcqMQir-<ZMI}ij}8z~ax>c%
z8G)IlQ(x}rRb<Bbjw$U=K>3vpVYx)xw<RghW1$jIZZpUpf$jx*)t?^|Psa2YH+h5~
zcAV3W%y1Rt+5v4R$8lHt7tel2SE}cB4NAbAE%S1yMJpOVIu&^#17+6hx{}8d($>s*
zEX=Lfo1oC?sID)Ci~1Pm<p#euSjT(&<JBAdDZGKgo#*drR3DmZ-Ax4;iQ~Szo0ChY
zJ at 4gGWc5#I-q3U)-`L$6>^wzf-YRUtn!2iM)*gN=IK^-7^W$RfT80S>?ozO{T&q;M
z5>&Uw)Kx)yO+F4^+SevkxDz`0`lQs!pQenKlJN0G!nJ})$rJ2S${!{|iz%!qzmZP`
zM at kc3a5<VZHL@^FpdQ>E^Pc2T^dGMiv?XA(6EpBJH3mlhvh)nr8-hSZriga0&e}-b
zRlltsKC`U;^TWkbZ>cjGeEI&<ttrlrT<OER_CX?EYqjAp`+1GpxLO|rADtdyWFHo9
zB?8fe^IE>9xT>e%@0Z!JysxUr(2}&S^UpP&Y|a{HjH{{5nls%=oPv#;ol%r>?^<33
zYgtNQ)^K^zT)fhul~IkSM}#O6>dovbw+UbrwL^N+eMM4RXI5{#*WtH#%Xj{a42w3h
zAUNe~g)-QEI)_G=-LF^S96$6tLDU5$u^_HbXMCByPyRBh4vp}7Q%fBJAM-mhpYY>D
zU>9;Vw_;Htduh4%!||wWYT{~t`OiAa*?!&%QD0$H4%IK1^y<0-M!$xz7nBuAZR2ed
z=PulRM6UwDf*KpWkuGa8qtEtWbx at Rh2z<G*&lexGMn0n*Q1HlNLNP*fp`~sY3 at sdG
z7rJQ&6k7furgtW?qU(gs3FMvB7pm8x`lcNnZY%G~%{$~d1MeI!Kj13@?NK%SFj~1>
ze^qIdT8?0?$1dn3t_6wMDxP<6Sk>O?`iV1~(RQED{&8tX!GjJ?9nsq&IzsXiw>oF{
z4IF?B6R9F!mINq^TEqV7Z?2^5-O>e8`F8Jb6 at i<yI;ipKD|XArr|4V8igFM;=1XH|
zEXMptmu_6I`@G-Rb>GT9l(<T%?SCEpVOwd;)hB0UJ|^cz-MT&Dzb)>#G_|?zW_*rA
z)|e*q$fMB%5PD>9isL~)kDe_7sLtI5R7={F^ImJd=e02YQI5lyHk<eX>2iv1Wko4f
zBRZnGOur)?c6I+8Nz}b%Kz at VoB$_mxKh6<u_s6G2#H#jwTmLoP3?vp>SZ`Qgi0>-X
z{DZXUH=F1@>ym%0{@K*l9guJ0qIspIXY}|Bv}m1CKx=#wq5!4Y7`;2XK- at PY?4HD0
zzLSiX%93w4MRWIs3m0G0^p-!nBFQt4QS&!UzHx(?t){?n`1T>}K!IdajPD#n at G&2Y
zwQ-+OAZ-{mgRm%tKNUIw-K`S|G?v<9<Qvgs6V~Vm_G()J@#HP~`U#=p at LPdWMCf!%
zU)@QkEYi*^o0NB)-5_P;=ilCXVMAH2*Y-VEBd{jNRQ}PkHult7J)PZ&XBa`B&lDV#
zf|@WI@!d*U4*a(*W4Qtn*K`C65 at 4dqN`<4?6>i;vSiDjDV9!&3NkC6%4rqj3C~ku|
zgbEQmoK?kz7DRvAs38=QiMItfs}^}>W?TfIJ?C>}Sns$zCbr)XtF{0K=nYjj6N%{>
z_{5XAM=+M2tVh(li&X0AnO0NF&!Qm5H3X+f6_eneMT#qvL#h(BdCzQm<(e8eju}OC
z|HykAT3mS|;H)9`Hm9f5XmQw-!VT<~#VL4%3NN%>m0sbhwD;wlBzjx$icF<z5!(Te
z#SlGZ4O6)IqIszOx9sDWOzepxoKZL at rvrmQlnj+Eao?x3MJ<X8L{1*=;^AmtOK|O5
z#i0E)?-m>`l+!+3>G}qWy7L at 5o#YDMWFWlqH19(zbeiAa^P7gj44P{G2c?VV7hV8{
zFjY|#ww~N4MAR at jiL`y3TKX at BDmHu3hj;6H2tGdS$_4!3M;h^Ipw+Ic!)N3#-bnz7
z0%}YgyZT8p*$~W~)Sq<EZ`kSc3 at eI#JmzlaM^`lwu1M-C+^V+a!0^p0A7XMG5{F6y
z8%5niWnJGQ?5#m|^h$|x-w6cnaor^HO<k?n$Qmj+(^BwN0eIX*ozYsvQ+Y<0CU)&T
zDa1LuC3vp&frk8!Tcje=8H(Cwce?FBliTZbs<7Z3TgN_0IocI!zTuV*N>*bY{Enr-
zGWB(v3mY82IR=0OVObhI%B{%KpQ)%{xGP9s8~kcJHf5}9I7wCdrrHB<tXS?pEY@<a
zx%W<j at VX9wYTEvaZ#04Rz)e-MS?f!6$P+xat8I#q`tb8QR}lKnNdDm--Yv*^Ff8vZ
z^ej&-F6nt;WZl$M?b}zPv`eCXCpZrG3|5rQq8LAXLfdS>j)xV=PenURoiPfwv%l+p
z=swy0BXn9LduPZVsIK)9uyr|O1;|)<9pr<r4WTESQBB4Xd9~Z07V!q|u0GuP+E(h8
zL(n%|?(v;aq#7fXtY`$j)AY$+IQCIJk at 9mQwy*780$0E at OjA~454P at B7oZ3woc7n0
zgPLTGq5&P{+>@2#6y(`*%~>su!$+)3WWG*o>-UT<POXL<I{zd88~E#-Lq++?tpSCw
zwwod+prb&bw%nimJ<I_*t!OR_DCu^LROL&!o~%6{_ge7c>VUV?Caut;Idd=Zy7m)t
ztr0AuQ9#ePp at Ekc_nifh+<<w`=N%e@?Bq_H5A8Jjd=?rJ at b9)x9F6^2aN;XF|31`M
zvmLU91RUt;XE-CB<!+qKQzvj7-ea?8lc77}wK*C`y~Gu;FE?LhIjhl`bS!z()uPuF
z#q9rtSncgl at tA05uQ+t{?mG=w=u0?f)YJzL3#M(2`zEc8xSA5>M%h=0sJT#|lOZ5t
znP*#S<NNH6EBmj|dKi_q!FwTE%>k5mTIa)Xn(o_0foQBER5k!ERNy!q>prlDPo1>*
z at R9O!u%GrFV)CjB3{xyb`cd>q2MtDNFQk};8-^G~#;&_GfILZ9dB7+$hx$PCKnCvO
zuSEPBbO7^ed0aQ8<I~3KLeBktB248ijp3pkjtk;@65lt<?(w){va8<^WJL7+<{d`)
z=~E|Tx=bJTr2$>#QgVOZ3GWV~i<)iQ(rvFkgH+}?j1TJ;PpsP?RPj at w!@4s4O?c{E
z^Aq}jp{s&W6SzBGoTeid at c^oSJh!m3N*tzop5yD)@5tNJ`KrI?xs|Je2Cz}yb5aZI
zz{fAz3XK*hzvq^Kjy at R5H_Pdajr2pjzb&*vgHA`Vnyy}*BZ^wZHN+(&)e at m7SpP@n
zi*+7+UkYq?UhfM+IQ1^`o%w^0&~D(A;79D=AE9v2XT%ReLs&D$9a}#XK7FOnj#6{T
zwP*c?%P;44F{7qY6REYEFJ8aNL<B1HSxZ-9xoIBGz}@-f-L^y9XNv08w(tp<V|TxK
zGgzXt&$_dB;U?T7=j0b;{8F{!B})mn?v?DWZ81d=Ru!%wx}<&dqi|`q6wQ?g-nSM)
zepacCT-Gc?oZ2s-Vu0_7W$idkidM`Y)eO<s%tVAr_Pi~6Dqt(B4?QW<ENH-M?YSxP
zplQM%tFxf at G`G)d1=fDAu<Cr+S+qjDjmH%Zn_XudYw)&Xg^>@SCrtMpGbhgbO8W_W
zJQxnK_K|IAZM*n6dbSm3-u)}gJPV{pUaS at 67>tx*zE+;0_YZ`~Zp%aVumr;6Yd2qb
z7Rda*d4|r%P3Dn+E}TDAcS7bxRUCRg*I}Qt|BPUGf#W^kwwmn4m7P&UmwcUJwP4Y#
zJIBQ-q7%GgV3+O&53;kiuH%?TLx?yz(uxGdY63q&Td(prv$D;u5Y>q{gz6cM{-={?
z6IEL{4(IXEUE(w9okt5+hs6{3yMyj&TY;LT+7GNr^jhh3sq8C%mmK!_?Y<LT8^g5K
zJn`pF!0nw+dxndS*Sk4ORCZjt{aNaC@=h%Mx(%V!TB!4>#ogRvhh|P>nS!!gu+#r*
zkIwdC9~tUH?+m8uuu=iJ0B at 64Z(E?fp+|TT<KyryK(CW;lJAt^OLzDV<s6Hb$+NY1
z7Oe|wiPCQLchvQXgNfP;qv at G~gskEGoBGrt0!?}08>`^X!W_ENbTi-UGulDjVNJ;+
zozH!zk(IB7`W}W at 9qKt+)#x^C_M{m0_^u_<(LJjXQyv!;z2q49?P}74vCA)83AXd~
zjB9dTv-z`w|1kr(`(rShz!xz$aa68Q>B7E at W&f&3erm|pWL{dxp3k@!vLzJ=_@;AY
zVRqeT*D)TM at U`g-ISEbwM(8=9OAq3+<&V at +);UqTYply2tHFD(mj(KhorMvdIwxY$
z#E(f#!)=y55shckrnY@<>EyF}FFMr<x2+tkb`Zoo65BnB*fz4dWz<O4aeekk;?a-8
zZFkY($zIIWZF#dT$c#sMi7};ZEpr)t!M(9jiZToS%?r*-Mv-%!t3~yq+Z_`sMaz)(
zjD at 1`xpLaN_;r_+u&?5`L8^Aj5EHxZ90s;C=^zX-^o8NAF(7~+;e3H}I-tW<IWbWl
zq|%1eR%j+C at P~L(G7xE^<5LOfOu5!G(s!`2;_LL$i1z?YzXo1(7?zvXI(&IDKcc4f
zVP%F_nrMho#&#L+F0T}_YirE}wL79fGrMnamTO~3){X-W^YYO%0f0o(O$2yvgYVKB
zMcqp94d566$bC7{G9bA%i)_`JA^2yXAiMv0{Nuzcz2BCC{LOXEmjx#8PtL_SSzfrS
z>RA;6+i=9}zF=Yj0L<lQiT4{%nkAa+KtAnOksag*L4N|V?SdYY3bBa;3uF7HnalTE
zG_%ELbYawZhYzK#V&FL!Pxk;cG+7m3Sd at CMQzkQ30N~URmuD4qsl%N1$Udip|Jjf+
zb*sk<EQm`nw>4Z!F;ypn9>S%ET~r5_(vqT=x88oRrl6a407dbk$Al!~xo_gg3ID=A
z4GrN&4n}T;#uPCz`M7sLj`rjRE@{vu{3om`d=i%Xb&_Q0P-xsICiX6%n+U-4E)oxH
zwjDKK*xoPy&&Er1RLSeL*Q%xIll*)Kk7X&hUX`zWSewKPnr&4;z5fDvi>!T#HLTQ}
z_Z9au3Un at 9JP1q7|6)i&GHU%zyxjjKPDI;}Fll--%+RdRxElx*Mcw=VwD+A|O?6wi
zp;x6y??pjSx^xH#q6i34Km??T(xgf+A#_lXBE7~a2t=AjI)ow;krG~zF1<;ObVvxf
zTRnH&G2ZhF?igo%*q>J0bCo&Q^USrTm{EhnTdDdJQ*chX8;<MWSgiLC0Lam}rFqAu
zkmGlW%=(vb+4)8Qi`c6T(>5KMk^T2CxE>7lIHQgE?`Tfnvq?-ydItXH&mfInizH<;
zxg#fu|0PbTd&<hygrk45kXAPyC)FH;?X?|Xe-nZp5vC*CO8>6EJToM>DHq>Zk_U*$
zvEWx^CU~b0 at LIUb`_rs8^uIVCw_ at Y7IRkiDR#Rii3!p at FpS)*hjP=;`&wN-Pjk@`k
z?Z2f|$TLRlRwn7U8cqMWrw9DO-4$K{gubA at 6+D-C5>bkrcKtUCqnnA(QOgZ!EC~Tx
zSiEX^-cyF(XQA;-zQ&E|zeEbrSSa>A`7)bbmTrx7Vo8&BD0zOtIppAzq1V|hFbJS`
zx&J*185&z3^UrSE7(svBlS)kR<F+dTkS@}*nc6_2378q6TmP#S`=Zc(8<P57ABy=5
z+E`-1!LoL56#gX3(hhz6ZuZm;ECe+4U!}Bd{T98>X0M_K4(SiP4gIf*3 at y9MO>Lsu
z{7$x*NNr*EW+NZG{{8z~#oL7*hpG)Lvm8?99}@IaYPR@%`%a>SU%MUs`(K_)A8Z2$
z>jRMkFGEG3dPnzOBW)9!`&-ig9)~+610^#h+aBdndN)Ecq#DFtYcal=)cp7S;VD7?
z&gfAd4rD8+-rHy|wF%kN+Kn_d#}xgWf)nwVI*{`BG#t4iB61F`;j>^=E#R}DTD?M1
zhD?_FcMmjMDxAxomU~$vZCF at b9ZG=J(sCR<eFT#h)CZXFe at pMEPJIhaf|mnjtdS3a
z61}egZwOde;W=d(9_>6+b`J2F!T)=PhMKOA0=W4C_xgkNiLCYeH*QcrK?5($O at CYf
z_QoXu_ISW+Z|?8+Z<^$&===NcmEWtse~>b-Oh{mSZfRhW6Ns)qU0cW(JhlK2BG-Q^
z{%i4UOO>0JTa;VpAG`-jL{~N at tao6F&YpqifXvtaotd9Wb$j0;PwVi5zu|$Th{)%K
zw*cH5w7rq<NnC0*X|v at G1N!yv0S;{Y{&oZKIw>tPDL1u7-e+OqW|;w=PiWZSwDtQS
zUflptIlVNG%=tA3G;#U7H0{V~5vUq&6{!|RL=Son;}W;C)cof4I9EepW7+Vdsi=em
zzoeZKt;fg%TN*)P7+C22ANML`@+ZJB>tpCQ4Tp_a80rD)q-FtNq67(F#ld^=)&e1D
zC6)hVJ#VpoXWe++WPPwjmYpSSKp}MPE`WMZCDI6n;uBA_EC1H0Rw?l82e-^!uX}+?
z8zw9)xshQ`r^h=cXp*jA=u#6+(ODqa<^NV`zHYE?xNfxG-vR=v<i=E#&=>>!x?T83
zoCC16c$@!8BNC0h=y>AurdRlnmd-B{zFvO(<@e|k4r4jOPrn;_hG`1+{mp9bTP|2G
zS}tCGWlzTxfL{bpMSyqOff+Ra8UEk|@e~I=CGB?D{LP#hlGc=lPgDD7X#!Nr^{8ry
z_*=QG-<&<&7X7n()>!=Qz)U(mRV at w3{=rh~cUGsKA%)*gA(>M3#QC>@;4=e{U;oLv
zogI?elnP}3U at 7w=L7(%(!!;_ekm0K`Ft5^SlJ`-1^(dMVaLEDEZ-3gUmH1GNo0|GI
z;}uMLH;NPIqpGs|uMOwhP6oZl5w}-nwT^J7y?}$q?}R}=X+08L2d`_bw>xQG77@|x
z+AS$f0<=jr;HuWk^e~w4nUoH*Ec-JQ>*MM8WVMu#l%{G+((?p;roH4Lc#5Fb*+`%i
zVTR_s`dP*5t_;)Ps`-icuu{aLBd|8~R^eF15fDxkKW722lWz6OvUDcwjVaBnE04_(
za!}-d3*7nuoHE8q_uB4a?qU@;)L<hPH*UQ9N at I`$1X=lE$7$)_LlZc;7ZL9nx<pmY
z{<lkN^u^$}#&2!kdcF;RI_e`LB4XO_Uc#Gc`!Kw`1-+!6ebz5c5Wo)tD|R1#%Ao(Z
zDfQLgEBG`B{#oFi^jn~Ur=w55djN$+JAahqXW8BjpJ at 3or_K@Vo$loY!%~gS60FX?
z6#t!%pR+>}n-cMfYDpm#mf3gV&w-_op=vlLty#A}q1-RO0SE7Y$iz(or!IjOz^jdv
z1#>O`E5D4AqBm7<&~FCb%(#u(e7F1V0JN!)^u#dmYU}I$%v|U2VR)A9Av6%?J5{)?
zx)x=p1zmNN8AFf%9p6yP^^5D5*2UJv*DtS^JK5+#^$IVEh{(HHHu>!h^^~+p5aE_i
zg<H=r%{1<7KCN!ssSM>0+b25P`b at zAvFTM``YSdYDSxH|CV%DwW`C9g7XN!F+c%zX
z!rvsb0viL$=ql7}4PQc&VQYma48eZShD_DfJObiNcF0&E{DJ?0HM-_-%#%2f+&q1n
z4rOY_)${;<*#m&$TYHl)a4HJWx?Ut7z#r;68uC}-rSacIv#0URux_5%kl3at*~IhB
zaJ9IQxTZLKoLYQHd{e0<2|{z1=`QD8!MoysO!q0hWQh7 at Xv#pzY1Dqq(4IlH4D{1}
zc^533<L^KyXs at T0vX)y^F_{ye>6EQ=uk)<)uJf()1JNV at m(rvc(o4iMqzh+d4OEc(
zp(o#je@#GItG?{=s<rbVLMmcE9-KU at e&)BPbn>lS2r77 at 5nOF?XSDyaSa!I&-9%i!
z&Ow3mH!m(RhK#c+zk7AzH#@@vt=4dcaLLy4{ZfY at LZ@^I&C-?9v_{<n%P_^Z>rPzp
zaq9{_niYPT#nTV$dmx%e*d=A7Tiw~VrcK7-(FwoPzLIqYN68iEjav*Pn~>}1XSD9T
z$gzWt)421$K0`D|KlVSNuSwKpw>6-J$!N;hsZu&g5&hS|Jxxrp>#apukf7p|wjqVx
zOb-<$Gi-3yX`Dl6ct_nWhS1n`fAqbO+iSN=0+q*FZSiSm?@Yf`mY4utWYIA#!s=7K
zkPj<j8bRN%=OY at HfyFgi+SbaPOCXx7m3zGJ6rN_IA^4S3>OXU5&E{q(==k}dV8xwt
z!wR5>7tL_9oBhkmvFjhsfv&t$1SL7#;Q<YBBacE<6u#YjfuN4S(U16waS2GEAK=?c
za9_Q0uGgfTiJL`kxlee&rHRgEPFIq9J$dP=Bi?4m8qO9~7Z}ij=!nmbD~>l$TEIoq
zN><!bWI{izLms2=0o&z`8%=^b$fGz(?b`s(ULbB3OT353d#T9G=5{4x06v at AULN?#
z@>-h%7xT4Wr$MI$8lVC*{Ps|;W3-a2ZlY2_w_?fbM>|9H%UAM^BtQl4{c<5xx^ma}
zXlDX#7c<CZW<tTU^()F3f8m3ET<XlQvr`XhZQ14nPc0j8_IbI;LqPgMN)l$oH`AdL
z(jdO1alxQu(-3Ib%@)q+gIhD9wwySN=Hb9$Q0h?xM!;Rrg@%~6Dpz}broUu$>B at z1
z+h(F>eE65ey>P~OT+BMkN}&zWBZp`z1Z&k617O;!ro%Ud{2A6s91}n<w9xGHZ!3=<
za&xYslFBJMR=+*y+~8l!g}<dUR=AV^Rxq0%`y>ki0dct3_wcvFbuW<XgA+Rr;SDLR
z3XEr#vI264dyRKoVM6=vV<yxLAnuJdCJjzB1$7j<_bHEy235D30Q`gNqX6s2dk`wz
zRySY<0clOvR8mr{!>WWS9(lO~7P}4v<!b!I0HV{Z@~TQXGg*_-efnW%9%8d`#Ut>R
zOW}uGvd=u%+R~NS!t%vDs71A?j?obI{DJ_(8n*gi3zyazJAmZs5-^zbahr&{tv;u?
zWsR>N#PccZ^JYF|L&`pC8=UogI~YR$3-fB~&6Rk1`oZ@)8=)PmK4C6WMs$HHv9HG3
zH&Q%c7uLz>doAMJ4$a*I9bH<bABI8K2nVY}2kA-_9|G5SOL()-+91<`q!kY~q_g<O
zqHmwG%?g<iHr~VA$^{ShnO)BJ$eGWYiTg3S&1O&k6fXw7R=2$O)~j?d#goZ=HdJxH
z{s}1v21Cgr_ERD3*bnF6pNkNR4=#e1{N;M%uyS>pTAw(MPbAva6|?DjZQaiV*QVC?
zxDt*9D)MI`yC*|hF=rmE1I*}U)b2P2M at QTW(n8<GTjkW5O0OBZueP7YiBY~x*CB;!
z2VEx!c2U!5tfF24Jli%8Ud!=N7%$-Pke)Ekj_`-s!(MdwH-l0kY0zJS{Znto;w9~0
zFdxcN7f*#te?Jnfvdr18-xtNN+QY)Q-ZRJWv*Ar9N|;cy=QutsGjf1FrqoPV0x_Qk
zT8`FM4~)vXU$RAg-bVqfoL(6XKFp+r4L>EfLR9%qN^M7DxL$#`V-Y+I2pf<-+|fuM
zLyMPjyfq0E2gW~ltxoXVh`*v}(w$BC+IW}mwB~pC6-VZjo(4eM0%@3=JUUy~{6=`a
z-<QD&i1z`6o#$bEqTn5&*J>(edb(G4_duypF_486MBeoEvcvMB-ux}}g!`??Tl0t7
zW!t?*l%2BVF&G_~oX+v}e`;M(=R71{Uw#wM8URc at k`=xbLIi#OhM?yU^0YWo-T)?J
zX;9^r>?#Yqd)bRO*RB at uA?sqmEw|W$*_<l~Aw$X7m#-=5YK+g1Fn+uSD*pM#-tlAs
zqA87&AY-JG)LR+TIk08s=*FCc|BP>s8vCkPP)9eSM7t1#rPUSbQ+j(2>V|^!e_z_W
z`1Rs`l7rMd_0clQ`H-zTB`O26+{j3sVkq?asaV(RAlpl3aIHxG6g at 6XU+zfW#&KWP
z%D|mnfK5V#xX<Y7XgYPd3nDSWH4{!~hz;gNKxy2WX$8QRP=fb;bqbGSky6mefK6u>
zJPR?s<`D)`JEdwySQaJr^GvOvDOLNZ@!DmbVxu0fJ}ri8NMmzlw}(r_<Id1*+?PR1
z(387mcoD+tFd_s_$4D)D7^`u*;EQOR<wgn$3o%X!n;|AjZmZ+_uOPHq7Kp3CHFm#3
z-T|+SqJ)bV{ij)x#FSJYp?6B4r2~0smT$w at V06o<A6z`f!Dyk at W5ylRwtq{<_Aowx
zOo$le0r-5_4vOdc8#McEs!Ev=3w=CyAg!2~vKHt43&#d!+kq}+-4yu6>b#AgCe!rL
zd}y>5Xvb;-!<8+5$%8ryn}o~1o!inD=4lc)s$KXx4`UBsN`WM4R=kD%vb^w<eAdp#
zE(uQMpt<o=D~;q<wa8?Cx)W6w7GB|Km-q|A0Vb8|)_P=d*Z))>yEe5%Hg^nxI{RpK
zyByx`PG8L1;z}3Y_nk?UiC;f}NthAXs-N#L2WcEqbxU<WDDpZz-$SaQa^E`(EBT{%
zrJI%g8~1wkMD(Od&%8~JKieJN%FEX=Pw3CDEPa3&d at VFUp{=7EMMgit<`gox{c~kk
zn at WR9+2fH)2O~B4bG8?+hjkAax=h~yUN(!n#}X~~$r<UTg(6;0J3yr*FTZ-wcH1}i
zQA(7gmQD5+a{=q*7Z2c>D7zPJ8P`CRIyPCu9;}CVmpt<-D55UVe^q$pPZ{w8{z(4P
zfcpznBQ2ZDQ6rjzeP0~Uc_bb)4`xG4w1{;k?jHa*#d9#@qSn|XzERVlQfBF$+``-n
zLkjJv`6-<Pt7o*(aVN$vjt_LXF7!~ebJeXVU~}emWpB-4LK*i(C#$%56;(?b!2^eC
z;$~E#O(v|~zw(Ae@<FLvm<7HdnF$zew<rZw1kFKhnNXiS(RKpQ_X1zKmx*Xv;UxI;
zWPuQ5Q<q-H5c7IlvL#|fD&SclW|ed4M=FKFnB&iA<D7K)!DF&5a_z~gf(i=q$D^=M
zjYgL{cUK8kW?kMViwJ<k=3qHnA!yaWYOpttNe4ahztm`{wT2#wxj!QK+<dSUBdZqP
z;1p7$Q--D(kEgwrTdB;oKQV6i%#O+2KC4skTF&w}a8YNH=Jo}GqN7%C-w&gCR051^
zZf{kN%XiP`R~O_Uzg{sr_)d#}&H|f2yqxE~GMb2(LCT!q<6g&|cwPa{V`-<h`Nk62
zIyf703q;_=DnqNn1Z&!gVHGc!!FvUJj-dbOLpEx at B~vaIR%XhjFFpD!(Ix47ygD1@
zF2SJjWGY?7(BuGdrLnszEc>#Ku2=|rdoUT5PXM_vg96WrOKb28U<*@)DvHNlmD>b@
z5Tl0f<;Zp5xAJ-^nXj5^-1&C#^?V)ICJX4E(HBet<0X(g##&c#Dtb;H`jRb{Em+m(
zKK)VjhOXkqC2?Yy<9L@*+wI&w at Vt@})%hEYz(#mansFrbdXBopUN at xQK50n-<a^nE
z0$d%Bc|kS`A=i%5%35TsU~}^G;YDpk9ObO?QFPOm(!HA+Z*4S!47gVxhp#e&Y%{AF
zM>b$emcO~yf_~wvUn%zZ at LD)?1IC|p<YG@)!+dx<1Z54=?ZNS{qXUL{KsPz)M~obz
z<ch9L-RMm4=~pu41?{4jWKB~%Hb)<x`IqTr9&@Gn!M;+o2lVkHEwOD(BT3WxjB_Se
zZA8Cg$PyMM;KHO7#50+WKngP64oMspVa%QxByU?Yt9QgmysohVThbTSc&k;&jKmOb
z_!6`FpEzCe6qvLVPt^+gCk|7PaZ<VBA@{&hNdVz(JlVDjs^LHFh`?Ry-fn(%&=*jQ
zyKtE`XLXV77ly13+wfUX^f<cLUUoS5a1(&mfF#d9Z>AB&OuuW43MxB)I&7LV`414B
z;=4%a6=Qyph{XUMzBA1aZI+Jj&RrMVNRv%%Pcy9)iHTRf=t0|D#ZakGzEFYwt^x$+
z1nD~zT`)ntJ&xf5t)L3-yH~A#k`dbcDyXg;mjGNmKmNw;2Kws!>_(ZB8_TbMRs<3B
z?0}M3d#uX(S-Mlep2Mg)>RK$O*Un_y{);~&h_h$3p3wKqaU5XL+4?AJ3edTS at No>S
zy*)tY3t#A!duVuKQ06(+(AXDhl#`kc3rEGdHOIv6G*0s)wdJG^=|ID|$!Qz98+{)D
zw*i%Oh2?jU!Wl4gjP<7O{+A3ccczu{{<`WIH$itAvMz6mcE0ZB*|>Gpt_c({j&-5v
z)14TPE at D#qbGfIolT;D=^)|Y-vn=1d)!~{1vB*b{{~6TcWeF}C%L-R?{VaHWeDyf#
zk$U%)Izh}kgib>dEDrnWlgn}(#7k=s-Fm&Oewl4NGJ)Xac80k^zGWBZ*0(}z25b+|
z at Okp~(%sBEDy_;z0r>-8FJ(9qQsU26FTfYY^F3U!dp%kV<UPlh?wLQ7Qe#|xVIekZ
z4!?VpG!}!4fx=v8jA!SAO8}|xV`ZS4r-)_LZQgFi?gGCoDF=mF=e2RR=fXcFPtN at r
z at gd9IsPgoy97qZhagSGi7nCX#*%_T0VOo^a(~<PzfjfNMofGlZO4UqD{(6lbFa0I@
z(~J at wf619;1c`h_%WbCHJ>`OYw#&?@H0adkBj0Z=d{DeZW?x`_>w4oZsM}P7&gSym
zvt3X*pcOXdb+nwCjLw@!9IQdu{kEJy;LNMHfL7ZVBhT`uSWTiGspq~?<P>%0U7*l*
z at -JadU`ejfNa*W}yau}1{>X`c>LRF^%TC=4<@*Stq)0aRynk8cscbAZV`|h(dQV^&
z{%jX3qZ8C`q(tu-Wj9$I3A}Z|-Scb-FZ&5pGwmzJEHuu~C~)-6-DY#FK;TaYIdd~H
z6=i&2nW%hMfZVpCMN>z4C#+&}$N>kORucqF))(*u at -oO0-K24khH?qen-8jR0nRXI
zw|;hp8jyriPw$l-(&=CTXOmwvtdnvKiCXJ5ttv$~xMbQ`ki*J$wC22r8|9JQx#Ew{
zT1xb;sK<v8T1;+%S=T+dH)x`;QeId_eI4;`4lrEAY;mds0Xr3-2aY`LtDSX=N)zDT
z)17D>|KVO>?0QIfMGBdH4E7avVQEnC*UWf}R5)qJ<`W=kt#D>qyP;E(_7Y&7yd1+o
z>E=$(Uu0VjeKA%*9l>H8EGE06`ca1YuySPR4NnT)(YVQiV7jrZ>)j5O9uMk>KNG at +
zF?K?miUC1g+I#l~P*EgxFXerSzAk%BJvMJQ-VS=<+w`#<TeJtX8kgT`O%A<j!oEj9
zQ?rNHK&fxCpExBhLlW)=eR=H!cten!`AeJL_02wn5B%hD0|mL3IbnVUN%ZlF%$+>@
zDf42FoHfX6XlyJQSdqiJ>>OWKUv;jft at XhHK~l-XEzZx&*7r|66fgiCFRnbZ-KUrj
zoTQ240m(1Mjiu^9y2?>4D=NcI4P^FcyI-nk7Nn(CbVuD at E_^R$y|FeQaf9nilC at 4y
z!&Nh!E|?+>lCY{MMK<Sw;PIlZoMa&hvDEh;cgio`pm(I$01R^Gm)^2Z4j{EpI5=fR
zlh9dM>Psq*ve*bg0-)}lbwdAg#x320c;>m*N3d_03!TC<hU7lS56YyoQ*~+#9(-Li
za%*+%Lm8LZatf~p>JWzCly+s1w{uAkDIYI_2>gzZX9Q^}T#w3E<P}WDw%Qsyj1VLy
z=CFWI%cSO#mwCSmG0wN)ku<vgJl+G}5bJL at vB4nyRE#{%lpY)XL46yu`Br1>G&CD`
z1#qE?r|urX9?kxUI4YRSKZ at 1*T2cesIi{fM_`9qxL2i(|Tnb;_`l_AU;Tp&GE*KP~
z2TbF`M+q#>+Fuq3rGQ0-9(-s5x|Q}hiCI8KQ}cOLY_6gYN4LG78tCHqg65;cM^-KO
zQZ at OJujQ)*-9Y**H~+~R_&NuDdkz(U at 4Q>pDl~;r5%i;?vBkzN at r7>Q#;oAGQ+$wS
zH2W4{E>Xr;I4e2Qr`#6KEBcsA9l=)pBju51Y>nc;yBESk)_J|ILYaUCV%7J`LCK-O
zhc+=zvL*QHKIa$Rb09j@@sHWbu{ZR*K+wOGj?dn}n=a|sx)L^W|GCDs&wluR$76J_
zfEIBtoEy0%Z^}UXae=^7FL&!FX(xA|xB`8s`iUvTRO#FZl&*-*_}<T_r@~QF7M at j!
zk(V5`@dBHi$N*sZHub0bCrSOtM(ZV<=Z!~BpZ{?8f>Zy8B5)Q>gq+Z7iipI{{?53a
z(>Zf{-PD}8t1+v6?JpGMvFAsUY;}$^e02{dy1bjM=bB^N!&Csb;N3zNjL+fi1Ts0;
zRsDHiXjQOv7&yi$j3XF!@uIV)g9!2G1f-wQsG8;L10B+vlx~RU5yS(wHC{$XkaN6>
z6=niC&RUl=@*z?_7C~6V04V9FxI|+7s9^b{ZlOgUN5>dqJSY$Mov*tT7Kayn{s#jp
z?zPVn&|`5RLolPrawN3vS)F5+$>ziWnCwE-C1Xnwt{^otDr%Hm#N1pK#3Ru(|8g>I
z8Er-pBe9LVuaBqhR^-Q9!vtObHmu?l<IPeEZTl5^J&saa`KhGA#4+vb-F at X>Ar#uL
z%ZlgPz-FZoO#5aUq_woA{-IXVu$-J`W?&ha5S*-o0=xGK0;TSYdUFn8veb+Sf%duJ
zEnT2c-iuq4asVz9w*BKKeD1 at VW3{4<v}-nX1m&F=EL#apJd(uEjl|}LcO2e#sarl^
zsT#~W+JORQapyq4|Nc3=3hn|lb2V+Cfe)C44R=UO_(v^vElM-alfNeP;s7h_p(pLg
zVeZ`?=NphLnow0O+7aTgy&$bxjs(U?gPfwS_v+orU9YBT$GnZTJ<51Wp`>#a>Q`Mc
z5|b5%pX&pdb(pD#Ip_-LTxoH~r<U0ROO#-y(c#%6`jOBM!l55vDJMC$R4wWpG>wCC
z7WkGRsD>CZ?<hTB6Qw~7W!wdM-z$sKjIzNG)a&?9N^fvE2Qlh^6L3 at xY*X(v7cV*t
z0jm3FvxbRIB{6 at Cma0+Eq3!#8>m$1m$#lreqnA57n$SvuGi4DMawqyI_skG(PZaA9
zNID4P_ at Ncf5fk;vAt`~GOTN2>4cVR?(AW2tp8$)oe4QkxA#*4=$-+96EJq1wQOnw-
zOZ!i|CAuGx>!Z`T?YK%GfsAKY(3>gl`531HC%#0O3SOtI4kM_vFy1Ao)nKP4cJo+Z
zA at t^-sT2aDpApqLV2EZjr(~qO$3j1LL1&}yQ@@U4di-IGd~>W at 9VLQYj~*o!QL|Nw
zkZf@=>@X_3;JkDLxH;u&@d@!7(Xi at Qj?hj`<4|Ioj=87WcLs#SNsWDzy!-`kWoG17
zh5d}d#vo-ERYgd4svgY9F;b#CzjxE+DI;C^nZ;fmcIq!RnYlJG-0twX2y#y}q_FcV
zkq$G5LDXu$l#Nad2;*P at C-UpE*L<`AE7x%*e)|fswNoFZ8*!}5Z|rh}MC|cQh(R}x
zA#fjGzcME)y at tZMBy_|n{B^Pj9Aj14=DJ8?>(Zb`xg2Y_qTGh4j~QuU3!%NM8L-3s
zw<_skCDW_*&&qTRjZYl&DI-q$1j(ztoHrt5BZxwsXI+Pj;ecTxaP(=A)W~9`u;*fM
zl at -YxAtW2meJ?)F%`R!`N!{JB%?z3Nv!~5FCs$dJtC<4>r}y>_{rb2Voq|}=nrujn
zxcy^QI2qM`pg>|>3sz%5UFYByxz5Q~X+#M2{(1A`&jB$0Nm4syb-I>XF at XYM`$KR%
zFsPtfDLWF9>yvgEvlLbZpvj7AuDlek;}^|R?pPNoR7;UWDNy#7*r%n_2Z(iw6R-sP
zzQxSy!$b4#kA5u_P8qv2f at e9Mgx+IqJF&9m at YzkTNm?>)y9H+-n=+^OD{!iLQV*hy
zAHh9qts1tv#e$TJ3s8<3HAQnz1)Wc-XTeLP7!qayFkuMWDClvbRfF5(ztAT7Gcl>u
zP|+);>qEf at 1t?K9sZ_75+vU$$5ny8nx&SxlORBZrNTJA$PwiJlI)7?Vo&>oRwq6u*
z<UFJ8sJLlt$N4ump!@ZKGk>;a#G27t1gpuDJI)<)q3dxFn|L|6s)UFmgVWfv5T9^O
zqyJmGancPCZGpj8iK-JLv_ywQMuAA5 at 7y)rx{d={I+9AJ?Tv1whmz6Img#cRAI<pt
zkTVzJm>+XEW?sP4Fmq{4r?;9#iOuQ!h~fY(OXC37SlSX#gou1yE;I7!YqWCP^*?Ll
zf1m#+fsj{<24e#j1KLg!)5!grp=rZxiRog$dqYmwK7_9!d*n&Z(=PS^nH0FU0?<2P
zYy8`=Wv==D-+1(={fHrcgKSS%Ibtpw66-hb-umg|-Ljd|fbe&I+h^|-3QXdV at H<}U
z`%M5_^S8~W^eIEZUW at Z-iiRmY7U$DZhKWNY2_g;>n|`%AUx`QWv}FI|<6IKTGeiM^
z;|(lsk8ZIck31V6Pw&U9?mt}P+~e&A*-1gA%y4$wxh1GW<-)28PULDkTAX4J3nBfu
z*9gr=k1Avh8<OnM&(v!4%eu#zmX!YxQVIG;as{AYDJ5e|SK>QjhDZz}zIly}+lCJA
zR1v%}l0o at 1vKy&K&$$EBQexdWmaWtMhtuC0dJR}4lVwQ~E!IR!;I!s#F<%T#7)A(T
z`o7cq?>NcZpP#&NjflWSLi%%zY~;;Q6m;)MrcJCbon<#NwzH}Wj89wkl at V<ziGMnw
zpDmw9-ma?4?kSrtB|l2u&aKxB^1k`erL6Nu?Jn?lT680!jnC=?y0g!!_Hho6qmbn#
zKr1$eGK`-$dMP6USFc>S&on|fet#M#;D~_qgNy9|78OYP*}r0kdVjNa(q?400D#?6
zKE|6V4!MQP at 5Q;y3>=`0FB7fHoQ8hEsvM~nbrc!47a-Hh;8ErLdZsQ51Cw*+ch4YY
zbtfmX-XMJtt6lHsk~YQ>2 at jU4sQ~+rjBL&BfdqGAeE!C4ZY6f&(|#O8Q;dwNbQEAB
zri>~2yfOsXki(nX60I`(cD`w{&ySY7As%~Y5F4444WX5=%VszxT>sk=@l5G|TDCS2
zlhn9ja|CIp5iMGMCET;!2zc%9OP_{7`h-?<ykyuNVJ-(J)>1aqoyi$X{MM&`g!uT7
zNAGO(yH|O%)NPqOW`~B}M2&?RuL<fLprOiUV-J=T{dajhwHQW1b8V;OVomd45H;(%
zBCvtbzy{u<tS(*4r0P<G>)I_{61el}B0g$AKBGQ<-704 at zQqKf6|H<U>H+WdrT|VL
zp7ngf5S5jc88q>%2Yr(l55n!ZL=PT+fwUSIiI=Xg5Pb~*uI;g%Ghx+fW0nIsQJ56m
zlD$71aQz}N?`ftjFEZdL&TR!}<oXuU+EyTM%!}XgDM_sT1h51>{Uim^w6i;;ufm^}
zl)6`G2|+)yg9efm_b$~_I0~n9^xf)C-q7`j4t~un3B1_*cf*s9J25V!w<U*b8k#Vg
zry1#|1IBn<bvD?#30BrAtdj#%p1ON{o&$7GV^1YJZXHdCMgDv|9ni0|m6HeH0Wtn;
z(`@ze@$0H>e0T=ys0+qBhPtUzI{sRnR8ik(9XQ82-8ftH7B-rM&8jz|AU2}OPub50
zoYU at X$TomWL*Jc{jzxuc4xS0t1-q}2vSt8YCX$*Q?6js~+hkH^JQZ5Ip9v`(RryUP
zcBn-e2%R<;Hzp58U^G<C6b^Dmvd$jR;>05_VdGO{#z<ym+sz(BRs1dWEg0t|<&0BV
zvninhgy6^Nwk$9=vi*t<a47F}+|4^lngy2K(wXa2+KMP*9k=h%c~1u+(Sh#V0}k2H
zOA(GFp^V$Q?czGMOuLh`n?b|^w?61vZ(4mEvh7sVY+eaRcJ#920S{=GZ*9X}nR<5r
zToLIFz#h;71RCqje_+ExF4wM;ObCi_P25`Z at Xcu15<p&ATW2bk$`WQcZYUhH;M)Rh
zIv#jOZpzvuuOD#c?0}@St9xECmj0X8NwAoXq}aYZr&*}ikaR1x&~+68UuoXzulS5r
zU04~~_vdW(AuW->O1!v*+GjA1PcE(9!w+d?$x5RNuRUP<qXg6BzCJ|IH*8BnO+SXG
zL)gQCyIbz;^k2TD`!iF>I(^jJHI2x1z3Kf5i=5w)n4_zsjt<bEp0~Nk1hSwtzivEV
zXj}inK4Oe3G2H2fkCv_DQU#>e0m>7`{HZuL^nBatH^(8dbLWrFkJP<m0}aK!eo5FZ
zkeCbBtZ?R)pj59AZ=r}{?&NdQDM7u?vF!2{X`$57+s+=i{|x|ecs~|U-_bt)h8Iou
zkD+vjEkEX;r-X|tj8aEq97ENeHi8A|)M!oiu-iBU{f)?oSZ(H6KHkh0Ewc{psqM!g
zTI)E;%ZndU=aP=%&vN2W+V=YS7d{?@-K5lZg^3j)JPN4jePblY5rQ|zXM;$?*mOa2
zVak;=0Js^8pi3HO8lK4NlNX(H)Oy|}D|J}uca^KZ1)$s?p5EoK-DtM5`YsH**f*t_
zPTY!vc*JX6E1tiK5P%1(DGhRRV}nw1uQLK9DHDstW at 5#vuXD48-I;#c09#?(z6m1l
z4)1LW?YSM=lr>HkuygmQ%KKbs9HD^I$~k%W{aOokcRnq^wI*^Wroy)S)RqG9Er0gg
z1je*$HC$56N{~4p`UeBU2t`owSEBt#zWIH_y6ZI at 07qe|tBAU!b&y&UpDWJxCR#27
z<UPA0f8GAPLP^8RVS);KO0JS?*^SihKEbF=mBg2;y-+20>3Y!tt3|VUHgeUdNCag>
z47tQ={5tx?wOD&R5Vl96)c&DW7|Ax@`h0!{fWg66qNu>7eB^XaOkZRpfn8r$wa=U2
zi<XZ*dW9iAstZq935jaq>$Hqw=&VhNLrx!ATbaqePn}ZZ2aywl;vt1Ax3zl~MTCD~
zUIoOMR=_hV5`PsbY$)geN+9F2#PWXpS5pLZkjMGy7r+4^VL8(=I>u3#o%Mqq82~Fi
z=DC=1X`IC68`Nso>)9&|N`hNR62Q0(PlV#B<KY#uDT$sYea4{0oa^=&!P~n>X=k0Q
zI$tpl)Cc#HU at 8yZG#1cA*wKz`Jp>D}Iulsp$c4eZTiw^gZx=$J%Q15WPT+<kRAO;g
z>%LsI{%b9Ai}XPr-S5Wi at j3}<(Nl0H7R2RHN3<&zD>0266w`_=RER#j<O*EwV4-*F
zEL}A4q8q7 at +a}mgmYoAGG6j1{d>`h1oTl<wirS>$CPpH?uSGOsv5sBWzv&8UFv1ih
zv65?}$czDGzLr!#i%N)N1+9!Ni!hf|Kb*a}Z9>LG3Ht#vAi7>An;S-`Ro3(OdA{{v
z9Hj*fkFhsOn&H;$Pc6#oGUFa=%XWm|Z=B`9J!a}2uQ~OTs_#!%#&X3NrGR8(?RQNP
z_$Q8?JTq`EKa!dmRswcM|G_tp9VQCOh^?ynn|3936bheck(04^HE8NMDYOsevKq6T
z&&K;+FV%51umacPJTDsoAV#-Y(U`Qyr~>cHGa#xj-Y+KL1doedXIl*;b|8#3*mLF@
zY0YOHS8UK)%f+Q26rg-w5hJ0*I7qCYgb&A$VZ<5u8INrJ4h^-vm|`h}iMsUk89Y=M
z8k(ZyMF>NA2!Q14c2qx9>v5rmETj8aAM>`zoE$Eq*(h$3Ydh7Pr$lH$tEVLh|It$*
zHd{7aU&HH!w?7O1L6HYCjqJGvw#vLf1B~_Mmy!H>;;~{1a|?<nz;{F9qT|=kYhIL^
zl=gjb^Kv&uKubIUY&%=PVaP+ZggpoPS)PoxgQ|lEo3^E(o=Tdv3h_q~wPh1ix0Xr=
z$f(XB<n0sb{=dx1CJSW~C^^;R*3V8hKNtL7H+N&Ww-Qj}DkIFa4YcvTz%X{V=L)IB
z4&R+n-W|*RR at -yjLB2gJ4N9smHzgCA@9jpA^0I&;zm7S7uEsQ;JiUE~LL2|k7(gmf
zNm~~*R=b4MsUtpk?RuT%+;!?AClgB1V#|^E8emG-9=v^mH;F$pq%4;AWS&{x$R!%6
zWLBFv-yCA1`^VO0=4K8isUd)1uRa8lebNi at ND<PQ=BSQ5h;bKZ6oT1<y1#UgE6$~e
zyQgS9Sekm?ojV!~qJ1cn>c8)M=KZ;D$g({t+H^8|<{x)M(730PYm84ER{iYmU&A^i
zW?HvZcDP(Q4mcoh4EP{zn=PLXQ#kGIXzZHGM?!?CBML-rcH9w02LT8l*Nw`&ie3tB
zdcR5Ln&r~`5B;-F8_xjo2#h>T{^Iei2+<@J_ryJASTa{Jz~*P0lEV)%2XQ?mtyFog
zollEg#l;$e at 1#H%6BaX;_VG4pLEpsOnUMrvp1|`RlAXYoq>~b9Ji8oxfxHXySOvJ%
z^-CpF?oxPw++(BLb_&&z0ex(}^Plc}ZU7G`bLoK2w8M4DQToB>o0YR0l`ax5L(ip!
zO#{`;`MEoBtW>ZgG08wBD~sg_i4#YYJ0KO&31F3p6Qp6UbM75a1E{lh0<2i5IMEww
zFj|`*`}ILq5fYn&S=F_>E!=SF^c}3r6g6xpcEcrmNE#GoQHItXrwKA{DGE-XN}(G$
zoO&63vI+)N{?^CJ3%k63Mf%wO@)E7(<)4k70$=7J&Xs;IsB{lnE{B<NoEvd(0s?~=
zh{g&<-lUI}o+qoXEUJKsnRQq_CP8X at Oo*r2zQoTH_}`O|(&4JYTbbCSoU!Z2-LBCr
z<X1YCjLTmtxCHTaN|WDyu^F4UL9?)k2<dA at 9l+hH0M1!9F4L}m)Yt7|q>sj(jMZ-6
zWsK#$aRIW0On at R8mMpqY8w_eN@l_=<Mp_0^GAdu(0IpZc8)jfm?$?DEu7nuR8|6xv
zmEOC6{57#-xyu#fy at uA+3Rg6n`e_E7u|s+ty(N9s7u<WWirI at 5AGZMZnwf_s%-6t)
zG4VGj|274hZfapl+Z9y71?sN3))vm&AzzBzZR|E3!x(_{Bl^x*Og<EG0IOG(c(e?8
z`P_#?0FE6-<kc3^xSU1`ZDDt=?T5v=G0snL&Q6_ at ETyC*rI|(=_DECBj_kKzs3b44
zyIrP at dd`q{IVvyr=LtP^cSsJ8Ye3KQ=Lz6Qt^_9gi#c35+$SKhA`^S3Dm%B0y1)Qu
z=YXU0jf;DcaZ?j{GeyYlxU=atxBJZDmY^I-Lm2-apWBamFqbc>lB!{(_B*uhX*p~a
z0%}OE{ly9bMTin-9cICHDX7=4-LiG^-hK3-XxEAEd?CsC{+b%MLd*MSRT9aqZLYP3
zc4l{Lzl%Fk7sguFN at Rq1^c`6s?q@#4&00X~_%49%>rF)aARY+Yn+I3Un%A0bZ5P#9
z(>sK=dRTW at TpoWq<zHKMk84yngsJC_T^Ovd=Ji?gZO}N6ljb<*A-n7+Y*gbchLNUj
zB=4mqp^UXG9O~RRSG(BgJNZr)sYE2Qn{84@#3sJHj<GQR*^>iXO_9Ttez5ht0OC*?
zk(tG{%#4rV_ydLK-a97S408ukBuENw#3MNvJhk{0UY|Jk)yZhMtB#po-JFzs94sq+
z-<p51#7F7i)8bYGm~W4Z9v1?gQpP3)OI5lJBkOsxyUXgPg!aclzcS^jKZC7>?FgGZ
zRd!RW^dN?+yiQv?sDjfa`{5Xi%&F|st=(eEh?GuqGJ75 at zBDzM>dBS%jiN!~2g2i{
zl(W4#tPe+Vovq7JPgV55a5s|3{xwjbf)sJN%L6w1lKW$;;(+9X`r+mystCc9i8>Eu
z_X^3KdY>IDyldZKVSFzFRI`UV^0OlYf#&Ee8Z4-+nNXUW>|FcI_(hdAYI~+MSYe&(
z74PHuwyl0SiWgpjbp at v3V`uc@^rH0T_x1l7jo!WGSmio`rFuXB#_F9HuEO9cAuvRH
zHZI-JTnRdw?D#%@WhT&lq!;CQXwl(_yRvx_)(4swBeTzQvUyzx*0n}FV!vTc<!p|t
zM0;NOI%Uit9b;vl=Mf8SCbOr%a{ob7T_;NCZRHeYgv9qyS!v^R_f0i;o#WVSld^PO
z<)a$M_gDr)Tnb9&O0ef}RSh7e_k(W(ro2L0laV3}PLoG<)kwaL9~0;B8#qa4hR64(
z$Pw?nJ^6V- at 5-yG44%RcoE<_*wq*8d^-a<i`m^_g#(g|Xa~MF_r;=XKk;Zy9 at __2`
z<P(|2N0*$&w&{b$M$hU-x~L+jIW1hKM}5q%H|cb4GJqJWc`?b(>dA)Jm$sobwHARm
z*Ld=FBtvs}6BY!7{>Zre_&v|^$+Vq2TbG%S+0S~U?V&`F8uzN80E11Q)!0UgDbrW}
z#hwN(0ZnOWt}Zcf*nUlZ3$@<d+4!DK2Mu<5GK9ujKY3`%$K-nYUO&X=h%-e$GnR at K
zw|%x?wrWm%Ak=bBcL<wZ>5+v*(l<#n+^AcJw_(22Uao%cxW+ZhtFW)i_R0_HZ>)4P
zeo|m#SuSOM(Ao2w&+4ryPwk!qHc*>p724$8)5_3k^Rb7HcVlOG7f-FCQobEpS&bxN
z*1`9+Prhu+0Dl8IFA?#6y^OwO1No-%MVVxJoz+n_q}rfPH&5wwZ#%JFRZ446{(O8C
z5W@!)VK~k08}ewxd(7PX0hNR4cU($imd)_neAY&Fn-lpvCY)~V;pS}0tqmnW6b%r_
zbMw7V_#KR+V&)D?t}S)GBWJ@*iCABr(Y`g!k1{x{8(sot?EljLB=G;c1cI7Q8T?&8
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 <vyng at google.com>
Date: Wed, 4 Sep 2024 12:43:16 -0400
Subject: [PATCH 13/25] 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 bca8eda9d13d11..be7d7c673c21ef 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 <vyng at google.com>
Date: Wed, 4 Sep 2024 13:52:55 -0400
Subject: [PATCH 14/25] 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 be7d7c673c21ef..afce9af040a6ec 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 <vyng at google.com>
Date: Wed, 4 Sep 2024 14:33:52 -0400
Subject: [PATCH 15/25] 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 a2ded3cb5a2392..fcd1f4c750d313 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<TestTelemeter>
- createInstance(Config *config) {
+ static std::unique_ptr<TestTelemeter> 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<llvm::telemetry::Config> Config =
- GetTelemetryConfig();
+ std::shared_ptr<llvm::telemetry::Config> 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<llvm::telemetry::Config> Config =
- GetTelemetryConfig();
+ std::shared_ptr<llvm::telemetry::Config> 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<llvm::telemetry::Config> Config =
- GetTelemetryConfig();
+ std::shared_ptr<llvm::telemetry::Config> 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 <vyng at google.com>
Date: Wed, 4 Sep 2024 14:36:58 -0400
Subject: [PATCH 16/25] 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 afce9af040a6ec..4dc305362cfed8 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 <vyng at google.com>
Date: Wed, 4 Sep 2024 14:57:38 -0400
Subject: [PATCH 17/25] 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 8f26f6ba9ec26e..9febb08c9caed6 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<std::string> 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<std::chrono::steady_clock>;
-// 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 fcd1f4c750d313..82bdaaa8876c1d 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 <chrono>
#include <ctime>
#include <vector>
>From 4155adde942153cffbbeab94ff4009f2f822ac74 Mon Sep 17 00:00:00 2001
From: Vy Nguyen <vyng at google.com>
Date: Wed, 4 Sep 2024 15:14:35 -0400
Subject: [PATCH 18/25] 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 171da2053e7311..feb07bcf0a7ef6 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 <vyng at google.com>
Date: Thu, 5 Sep 2024 13:53:00 -0400
Subject: [PATCH 19/25] 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 82bdaaa8876c1d..d46543beec87f7 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<llvm::json::Object> 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<llvm::json::Object> 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<TestTelemeter> Telemeter =
- std::make_unique<TestTelemeter>(ExpectedUuid);
+ std::make_unique<TestTelemeter>(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<llvm::telemetry::Config> 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<vendor_code::ExitEvent>();
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<llvm::telemetry::Config> 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<llvm::telemetry::Config> 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<llvm::telemetry::Config> 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 <vyng at google.com>
Date: Thu, 5 Sep 2024 14:14:56 -0400
Subject: [PATCH 20/25] 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 d46543beec87f7..3497cd0b139c0d 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<size_t>(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<size_t>(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 <vyng at google.com>
Date: Thu, 5 Sep 2024 15:33:44 -0400
Subject: [PATCH 21/25] 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 4dc305362cfed8..af976058e66bc3 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<MyTelemeter> createInstatnce(llvm::telemetry::Config* config) {
+ // If Telemetry is not enabled, then just return null;
+ if (!config->EnableTelemetry) return nullptr;
+
+ std::make_unique<MyTelemeter>();
+ }
+ MyTelemeter() = default;
+
+ void logStartup(llvm::StringRef ToolName, TelemetryInfo* Entry) override {
+ if (MyTelemetryInfo* M = dyn_cast<MyTelemetryInfo>(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<MyTelemetryInfo>(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<Destination> 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<std::chrono::steady_clock>::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<std::chrono::steady_clock>::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<std::chrono::steady_clock>::now();
+
+ // ... other events happening here
+
+ auto EndTime = std::chrono::time_point<std::chrono::steady_clock>::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 9febb08c9caed6..f682e1cdf5cafa 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 3497cd0b139c0d..173976bdb1650f 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<TestTelemeter> 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<StartupEvent>(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<ExitEvent>(Entry)) {
- E->MagicExitMsg = llvm::Twine("Three_", ToolPath).str();
+ E->MagicExitMsg = llvm::Twine("Exit_", ToolPath).str();
}
emitToDestinations(Entry);
@@ -549,11 +547,11 @@ TEST(TelemetryTest, TelemetryEnabled) {
{
std::string ExpectedBuffer =
("SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" +
- "MagicStartupMsg:One_" + llvm::Twine(ToolName) + "\n" +
+ "MagicStartupMsg:Startup_" + llvm::Twine(ToolName) + "\n" +
"SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" +
"MSG_0:Two\n" + "MSG_1:Deux\n" + "MSG_2:Zwei\n" +
"SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" +
- "MagicExitMsg:Three_" + llvm::Twine(ToolName) + "\n")
+ "MagicExitMsg:Exit_" + llvm::Twine(ToolName) + "\n")
.str();
EXPECT_STREQ(ExpectedBuffer.c_str(), CurrentContext->Buffer.c_str());
@@ -570,7 +568,7 @@ TEST(TelemetryTest, TelemetryEnabled) {
ASSERT_NE(StartupEntry, nullptr);
EXPECT_STREQ(
("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) +
- "\"],[\"MagicStartupMsg\",\"One_" + llvm::Twine(ToolName) + "\"]]")
+ "\"],[\"MagicStartupMsg\",\"Startup_" + llvm::Twine(ToolName) + "\"]]")
.str()
.c_str(),
ValueToString(StartupEntry).c_str());
@@ -592,7 +590,7 @@ TEST(TelemetryTest, TelemetryEnabled) {
ASSERT_NE(ExitEntry, nullptr);
EXPECT_STREQ(
("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) +
- "\"],[\"MagicExitMsg\",\"Three_" + llvm::Twine(ToolName) + "\"]]")
+ "\"],[\"MagicExitMsg\",\"Exit_" + llvm::Twine(ToolName) + "\"]]")
.str()
.c_str(),
ValueToString(ExitEntry).c_str());
@@ -629,12 +627,12 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) {
// The StringDestination should have removed the odd-positioned msgs.
std::string ExpectedBuffer =
("SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" +
- "MagicStartupMsg:One_" + llvm::Twine(ToolName) + "\n" +
+ "MagicStartupMsg:Startup_" + llvm::Twine(ToolName) + "\n" +
"SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" +
"MSG_0:Two\n" + "MSG_1:\n" + // <<< was sanitized away.
"MSG_2:Zwei\n" +
"SessionId:" + llvm::Twine(CurrentContext->ExpectedUuid) + "\n" +
- "MagicExitMsg:Three_" + llvm::Twine(ToolName) + "\n")
+ "MagicExitMsg:Exit_" + llvm::Twine(ToolName) + "\n")
.str();
EXPECT_STREQ(ExpectedBuffer.c_str(), CurrentContext->Buffer.c_str());
}
@@ -650,7 +648,7 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) {
ASSERT_NE(StartupEntry, nullptr);
EXPECT_STREQ(
("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) +
- "\"],[\"MagicStartupMsg\",\"One_" + llvm::Twine(ToolName) + "\"]]")
+ "\"],[\"MagicStartupMsg\",\"Startup_" + llvm::Twine(ToolName) + "\"]]")
.str()
.c_str(),
ValueToString(StartupEntry).c_str());
@@ -670,7 +668,7 @@ TEST(TelemetryTest, TelemetryEnabledSanitizeData) {
ASSERT_NE(ExitEntry, nullptr);
EXPECT_STREQ(
("[[\"SessionId\",\"" + llvm::Twine(CurrentContext->ExpectedUuid) +
- "\"],[\"MagicExitMsg\",\"Three_" + llvm::Twine(ToolName) + "\"]]")
+ "\"],[\"MagicExitMsg\",\"Exit_" + llvm::Twine(ToolName) + "\"]]")
.str()
.c_str(),
ValueToString(ExitEntry).c_str());
>From 42781f792a953d41d6ae0181f6d4f1a78b0b37fc Mon Sep 17 00:00:00 2001
From: Vy Nguyen <vyng at google.com>
Date: Thu, 12 Sep 2024 14:55:57 -0400
Subject: [PATCH 22/25] [lldb]Implement LLDB Telemetry : Provide the concrete
implementation for telemetry in LLDB.
(This is follow-up patch to PR/102323)
---
lldb/include/lldb/API/SBDebugger.h | 4 +
lldb/include/lldb/Core/Debugger.h | 10 +
lldb/include/lldb/Core/Telemetry.h | 292 +++++++
lldb/include/lldb/lldb-enumerations.h | 4 +-
lldb/source/API/SBDebugger.cpp | 10 +
lldb/source/Core/CMakeLists.txt | 2 +
lldb/source/Core/Debugger.cpp | 22 +-
lldb/source/Core/Telemetry.cpp | 722 ++++++++++++++++++
.../source/Interpreter/CommandInterpreter.cpp | 45 +-
lldb/source/Target/Process.cpp | 11 +
lldb/source/Target/Target.cpp | 15 +
lldb/tools/lldb-dap/DAP.cpp | 15 +
12 files changed, 1139 insertions(+), 13 deletions(-)
create mode 100644 lldb/include/lldb/Core/Telemetry.h
create mode 100644 lldb/source/Core/Telemetry.cpp
diff --git a/lldb/include/lldb/API/SBDebugger.h b/lldb/include/lldb/API/SBDebugger.h
index 84ea9c0f772e16..7ec69dc89b3f37 100644
--- a/lldb/include/lldb/API/SBDebugger.h
+++ b/lldb/include/lldb/API/SBDebugger.h
@@ -13,6 +13,8 @@
#include "lldb/API/SBDefines.h"
#include "lldb/API/SBPlatform.h"
+#include "lldb/API/SBStructuredData.h"
+#include "llvm/Support/JSON.h"
namespace lldb_private {
class CommandPluginInterfaceImplementation;
@@ -245,6 +247,8 @@ class LLDB_API SBDebugger {
lldb::SBTarget GetDummyTarget();
+ void SendTelemetry(const llvm::json::Object &entry);
+
// Return true if target is deleted from the target list of the debugger.
bool DeleteTarget(lldb::SBTarget &target);
diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h
index a72c2596cc2c5e..d0e40e415c0973 100644
--- a/lldb/include/lldb/Core/Debugger.h
+++ b/lldb/include/lldb/Core/Debugger.h
@@ -19,6 +19,7 @@
#include "lldb/Core/FormatEntity.h"
#include "lldb/Core/IOHandler.h"
#include "lldb/Core/SourceManager.h"
+#include "lldb/Core/Telemetry.h"
#include "lldb/Core/UserSettingsController.h"
#include "lldb/Host/HostThread.h"
#include "lldb/Host/StreamFile.h"
@@ -31,6 +32,7 @@
#include "lldb/Utility/Diagnostics.h"
#include "lldb/Utility/FileSpec.h"
#include "lldb/Utility/Status.h"
+#include "lldb/Utility/StructuredData.h"
#include "lldb/Utility/UserID.h"
#include "lldb/lldb-defines.h"
#include "lldb/lldb-enumerations.h"
@@ -45,7 +47,9 @@
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/DynamicLibrary.h"
#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/JSON.h"
#include "llvm/Support/Threading.h"
+#include "llvm/Telemetry/Telemetry.h"
#include <cassert>
#include <cstddef>
@@ -149,6 +153,10 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
repro::DataRecorder *GetInputRecorder();
+ LldbTelemeter *GetTelemeter() { return m_telemeter.get(); }
+
+ void SendClientTelemetry(const llvm::json::Object &entry);
+
Status SetInputString(const char *data);
void SetInputFile(lldb::FileSP file);
@@ -759,6 +767,8 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
eBroadcastBitEventThreadIsListening = (1 << 0),
};
+ std::unique_ptr<LldbTelemeter> m_telemeter;
+
private:
// Use Debugger::CreateInstance() to get a shared pointer to a new debugger
// object
diff --git a/lldb/include/lldb/Core/Telemetry.h b/lldb/include/lldb/Core/Telemetry.h
new file mode 100644
index 00000000000000..bc07369722a127
--- /dev/null
+++ b/lldb/include/lldb/Core/Telemetry.h
@@ -0,0 +1,292 @@
+//===-- Telemetry.h ----------------------------------------------*- C++
+//-*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_CORE_TELEMETRY_H
+#define LLDB_CORE_TELEMETRY_H
+
+#include <chrono>
+#include <ctime>
+#include <memory>
+#include <optional>
+#include <string>
+#include <unordered_map>
+
+#include "lldb/Interpreter/CommandReturnObject.h"
+#include "lldb/Utility/StructuredData.h"
+#include "lldb/lldb-forward.h"
+#include "llvm/ADT/StringExtras.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/JSON.h"
+#include "llvm/Telemetry/Telemetry.h"
+
+namespace lldb_private {
+
+using llvm::telemetry::KindType;
+
+struct LldbEntryKind : public ::llvm::telemetry::EntryKind {
+ static const KindType BaseInfo = 0b11000;
+ static const KindType DebuggerInfo = 0b11001;
+ static const KindType TargetInfo = 0b11010;
+ static const KindType ClientInfo = 0b11100;
+ static const KindType CommandInfo = 0b11101;
+ static const KindType MiscInfo = 0b11110;
+};
+
+struct LldbBaseTelemetryInfo : public ::llvm::telemetry::TelemetryInfo {
+ // For dyn_cast, isa, etc operations.
+ KindType getKind() const override { return LldbEntryKind::BaseInfo; }
+
+ static bool classof(const TelemetryInfo *T) {
+ if (T == nullptr)
+ return false;
+ // Subclasses of this is also acceptable.
+ return (T->getKind() & LldbEntryKind::BaseInfo) == LldbEntryKind::BaseInfo;
+ }
+
+ // Returns a human-readable string description of the struct.
+ // This is for debugging purposes only.
+ // It is NOT meant as a data-serialisation method.
+ virtual std::string ToString() const;
+};
+
+struct DebuggerTelemetryInfo : public LldbBaseTelemetryInfo {
+ std::string username;
+ std::string lldb_git_sha;
+ std::string lldb_path;
+ std::string cwd;
+
+ DebuggerTelemetryInfo() = default;
+
+ // Provide a copy ctor because we may need to make a copy before
+ // sanitizing the data.
+ // (The sanitization might differ between different Destination classes).
+ DebuggerTelemetryInfo(const DebuggerTelemetryInfo &other) {
+ username = other.username;
+ lldb_git_sha = other.lldb_git_sha;
+ lldb_path = other.lldb_path;
+ cwd = other.cwd;
+ };
+
+ KindType getKind() const override { return LldbEntryKind::DebuggerInfo; }
+
+ static bool classof(const TelemetryInfo *T) {
+ if (T == nullptr)
+ return false;
+ return T->getKind() == LldbEntryKind::DebuggerInfo;
+ }
+
+ llvm::json::Object serializeToJson() const override;
+
+ std::string ToString() const override;
+};
+
+struct TargetTelemetryInfo : public LldbBaseTelemetryInfo {
+ // The same as the executable-module's UUID.
+ std::string target_uuid;
+ std::string file_format;
+
+ std::string binary_path;
+ size_t binary_size;
+
+ TargetTelemetryInfo() = default;
+
+ TargetTelemetryInfo(const TargetTelemetryInfo &other) {
+ target_uuid = other.target_uuid;
+ file_format = other.file_format;
+ binary_path = other.binary_path;
+ binary_size = other.binary_size;
+ }
+
+ KindType getKind() const override { return LldbEntryKind::TargetInfo; }
+
+ static bool classof(const TelemetryInfo *T) {
+ if (T == nullptr)
+ return false;
+ return T->getKind() == LldbEntryKind::TargetInfo;
+ }
+
+ llvm::json::Object serializeToJson() const override;
+
+ std::string ToString() const override;
+};
+
+// Entry from client (eg., SB-API)
+struct ClientTelemetryInfo : public LldbBaseTelemetryInfo {
+ std::string request_name;
+ std::string error_msg;
+
+ ClientTelemetryInfo() = default;
+
+ ClientTelemetryInfo(const ClientTelemetryInfo &other) {
+ request_name = other.request_name;
+ error_msg = other.error_msg;
+ }
+
+ KindType getKind() const override { return LldbEntryKind::ClientInfo; }
+
+ static bool classof(const TelemetryInfo *T) {
+ if (T == nullptr)
+ return false;
+ return T->getKind() == LldbEntryKind::ClientInfo;
+ }
+
+ llvm::json::Object serializeToJson() const override;
+
+ std::string ToString() const override;
+};
+
+struct CommandExitDescription : public ::llvm::telemetry::ExitDescription {
+ lldb::ReturnStatus ret_status;
+ CommandExitDescription(int ret_code, std::string ret_desc,
+ lldb::ReturnStatus status) {
+ ExitCode = ret_code;
+ Description = std::move(ret_desc);
+ ret_status = status;
+ }
+};
+
+struct CommandTelemetryInfo : public LldbBaseTelemetryInfo {
+ // If the command is/can be associated with a target entry,
+ // this field contains that target's UUID.
+ // <EMPTY> otherwise.
+ std::string target_uuid;
+ std::string command_uuid;
+
+ // Eg., "breakpoint set"
+ std::string command_name;
+
+ // !!NOTE!!: The following fields may be omitted due to PII risk.
+ // (Configurable via the telemery::Config struct)
+ std::string original_command;
+ std::string args;
+
+ lldb::ReturnStatus ret_status;
+
+ CommandTelemetryInfo() = default;
+
+ CommandTelemetryInfo(const CommandTelemetryInfo &other) {
+ target_uuid = other.target_uuid;
+ command_uuid = other.command_uuid;
+ command_name = other.command_name;
+ original_command = other.original_command;
+ args = other.args;
+ }
+
+ KindType getKind() const override { return LldbEntryKind::CommandInfo; }
+
+ static bool classof(const TelemetryInfo *T) {
+ if (T == nullptr)
+ return false;
+ return T->getKind() == LldbEntryKind::CommandInfo;
+ }
+
+ llvm::json::Object serializeToJson() const override;
+
+ std::string ToString() const override;
+};
+
+// The "catch-all" entry to store a set of custom/non-standard
+// data.
+struct MiscTelemetryInfo : public LldbBaseTelemetryInfo {
+ // If the event is/can be associated with a target entry,
+ // this field contains that target's UUID.
+ // <EMPTY> otherwise.
+ std::string target_uuid;
+
+ // Set of key-value pairs for any optional (or impl-specific) data
+ std::unordered_map<std::string, std::string> meta_data;
+
+ MiscTelemetryInfo() = default;
+
+ MiscTelemetryInfo(const MiscTelemetryInfo &other) {
+ target_uuid = other.target_uuid;
+ meta_data = other.meta_data;
+ }
+
+ KindType getKind() const override { return LldbEntryKind::MiscInfo; }
+
+ static bool classof(const TelemetryInfo *T) {
+ if (T == nullptr)
+ return false;
+ return T->getKind() == LldbEntryKind::MiscInfo;
+ }
+
+ llvm::json::Object serializeToJson() const override;
+
+ std::string ToString() const override;
+};
+
+class LldbTelemeter : public llvm::telemetry::Telemeter {
+public:
+ static std::unique_ptr<LldbTelemeter> CreateInstance(Debugger *);
+
+ virtual ~LldbTelemeter() = default;
+
+ // Invoked upon process exit
+ virtual void LogProcessExit(int status, llvm::StringRef exit_string,
+ llvm::telemetry::EventStats stats,
+ Target *target_ptr) = 0;
+
+ // Invoked upon loading the main executable module
+ // We log in a fire-n-forget fashion so that if the load
+ // crashes, we don't lose the entry.
+ virtual void
+ LogMainExecutableLoadStart(lldb::ModuleSP exec_mod,
+ llvm::telemetry::EventStats stats) = 0;
+ virtual void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod,
+ llvm::telemetry::EventStats stats) = 0;
+
+ // Invoked for each command
+ // We log in a fire-n-forget fashion so that if the command execution
+ // crashes, we don't lose the entry.
+ virtual void LogCommandStart(llvm::StringRef uuid,
+ llvm::StringRef original_command,
+ llvm::telemetry::EventStats stats,
+ Target *target_ptr) = 0;
+ virtual void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name,
+ llvm::StringRef command_args,
+ llvm::telemetry::EventStats stats,
+ Target *target_ptr,
+ CommandReturnObject *result) = 0;
+
+ virtual std::string GetNextUUID() = 0;
+
+ // For client (eg., SB API) to send telemetry entries.
+ virtual void LogClientTelemetry(const llvm::json::Object &entry) = 0;
+};
+
+// Logger configs: LLDB users can also supply their own configs via:
+// $HOME/.lldb_telemetry_config
+//
+// We can propose simple syntax: <field_name><colon><value>
+// Eg.,
+// enable_telemetry:true
+// destination:stdout
+// destination:stderr
+// destination:/path/to/some/file
+//
+// The allowed field_name values are:
+// * enable_telemetry
+// If the fields are specified more than once, the last line will take
+// precedence If enable_logging is set to false, no logging will occur.
+// * destination.
+// This is allowed to be specified multiple times - it will add to the
+// default (ie, specified by vendor) list of destinations.
+// The value can be either:
+// + one of the two magic values "stdout" or "stderr".
+// + a path to a local file
+// !!NOTE!!: We decided to use a separate file instead of the existing settings
+// file because that file is parsed too late in the process and by the
+// there might have been lots of telemetry-entries that need to be
+// sent already.
+// This approach avoid losing log entries if LLDB crashes during init.
+llvm::telemetry::Config *GetTelemetryConfig();
+
+} // namespace lldb_private
+#endif // LLDB_CORE_TELEMETRY_H
diff --git a/lldb/include/lldb/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h
index 7bfde8b9de1271..3ff3aecec783e8 100644
--- a/lldb/include/lldb/lldb-enumerations.h
+++ b/lldb/include/lldb/lldb-enumerations.h
@@ -257,8 +257,8 @@ enum StopReason {
};
/// Command Return Status Types.
-enum ReturnStatus {
- eReturnStatusInvalid,
+enum ReturnStatus : int {
+ eReturnStatusInvalid = 0,
eReturnStatusSuccessFinishNoResult,
eReturnStatusSuccessFinishResult,
eReturnStatusSuccessContinuingNoResult,
diff --git a/lldb/source/API/SBDebugger.cpp b/lldb/source/API/SBDebugger.cpp
index 72501570320d57..950a84cbb53f11 100644
--- a/lldb/source/API/SBDebugger.cpp
+++ b/lldb/source/API/SBDebugger.cpp
@@ -972,6 +972,16 @@ SBTarget SBDebugger::GetDummyTarget() {
return sb_target;
}
+void SBDebugger::SendTelemetry(const llvm::json::Object &entry) {
+ if (lldb_private::Debugger *debugger = this->get()) {
+ debugger->SendClientTelemetry(entry);
+ } else {
+ Log *log = GetLog(LLDBLog::API);
+ LLDB_LOGF(log,
+ "Could not send telemetry from SBDebugger - debugger was null.");
+ }
+}
+
bool SBDebugger::DeleteTarget(lldb::SBTarget &target) {
LLDB_INSTRUMENT_VA(this, target);
diff --git a/lldb/source/Core/CMakeLists.txt b/lldb/source/Core/CMakeLists.txt
index dbc620b91b1ed1..4a02f7f1fc85e5 100644
--- a/lldb/source/Core/CMakeLists.txt
+++ b/lldb/source/Core/CMakeLists.txt
@@ -51,6 +51,7 @@ add_lldb_library(lldbCore
Section.cpp
SourceLocationSpec.cpp
SourceManager.cpp
+ Telemetry.cpp
StreamAsynchronousIO.cpp
ThreadedCommunication.cpp
UserSettingsController.cpp
@@ -94,6 +95,7 @@ add_lldb_library(lldbCore
Support
Demangle
TargetParser
+ Telemetry
)
add_dependencies(lldbCore
diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index 1266355578e321..167a49f9ee8fc3 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -69,6 +69,7 @@
#include "llvm/Support/Threading.h"
#include "llvm/Support/raw_ostream.h"
+#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -733,12 +734,18 @@ void Debugger::InstanceInitialize() {
DebuggerSP Debugger::CreateInstance(lldb::LogOutputCallback log_callback,
void *baton) {
+ llvm::telemetry::SteadyTimePoint start_time =
+ std::chrono::steady_clock::now();
DebuggerSP debugger_sp(new Debugger(log_callback, baton));
if (g_debugger_list_ptr && g_debugger_list_mutex_ptr) {
std::lock_guard<std::recursive_mutex> guard(*g_debugger_list_mutex_ptr);
g_debugger_list_ptr->push_back(debugger_sp);
}
debugger_sp->InstanceInitialize();
+ llvm::telemetry::TelemetryInfo entry;
+ entry.Stats = {start_time, std::chrono::steady_clock::now()};
+ debugger_sp->m_telemeter->logStartup(HostInfo::GetProgramFileSpec().GetPath(),
+ &entry);
return debugger_sp;
}
@@ -860,7 +867,8 @@ Debugger::Debugger(lldb::LogOutputCallback log_callback, void *baton)
m_sync_broadcaster(nullptr, "lldb.debugger.sync"),
m_broadcaster(m_broadcaster_manager_sp,
GetStaticBroadcasterClass().str()),
- m_forward_listener_sp(), m_clear_once() {
+ m_forward_listener_sp(), m_clear_once(),
+ m_telemeter(LldbTelemeter::CreateInstance(this)) {
// Initialize the debugger properties as early as possible as other parts of
// LLDB will start querying them during construction.
m_collection_sp->Initialize(g_debugger_properties);
@@ -952,6 +960,8 @@ void Debugger::Clear() {
// static void Debugger::Destroy(lldb::DebuggerSP &debugger_sp);
// static void Debugger::Terminate();
llvm::call_once(m_clear_once, [this]() {
+ llvm::telemetry::SteadyTimePoint quit_start_time =
+ std::chrono::steady_clock::now();
ClearIOHandlers();
StopIOHandlerThread();
StopEventHandlerThread();
@@ -974,6 +984,12 @@ void Debugger::Clear() {
if (Diagnostics::Enabled())
Diagnostics::Instance().RemoveCallback(m_diagnostics_callback_id);
+
+ // Log the "quit" event (including stats on how long the teardown took)
+ // TBD: We *may* have to send off the log BEFORE the ClearIOHanders()?
+ llvm::telemetry::TelemetryInfo entry;
+ entry.Stats = {quit_start_time, std::chrono::steady_clock::now()};
+ m_telemeter->logExit(HostInfo::GetProgramFileSpec().GetPath(), &entry);
});
}
@@ -2239,3 +2255,7 @@ llvm::ThreadPoolInterface &Debugger::GetThreadPool() {
"Debugger::GetThreadPool called before Debugger::Initialize");
return *g_thread_pool;
}
+
+void Debugger::SendClientTelemetry(const llvm::json::Object &entry) {
+ m_telemeter->LogClientTelemetry(entry);
+}
diff --git a/lldb/source/Core/Telemetry.cpp b/lldb/source/Core/Telemetry.cpp
new file mode 100644
index 00000000000000..d1d0c010395ae8
--- /dev/null
+++ b/lldb/source/Core/Telemetry.cpp
@@ -0,0 +1,722 @@
+
+//===-- Telemetry.cpp -----------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+#include "lldb/Core/Telemetry.h"
+
+#include <stdbool.h>
+#include <sys/auxv.h>
+
+#include <memory>
+
+#include <chrono>
+#include <cstdlib>
+#include <ctime>
+#include <fstream>
+#include <string>
+#include <typeinfo>
+#include <utility>
+#include <vector>
+
+#include "lldb/API/SBDebugger.h"
+#include "lldb/API/SBProcess.h"
+#include "lldb/Core/Debugger.h"
+#include "lldb/Core/Module.h"
+#include "lldb/Host/FileSystem.h"
+#include "lldb/Host/HostInfo.h"
+#include "lldb/Interpreter/CommandInterpreter.h"
+#include "lldb/Target/Process.h"
+#include "lldb/Target/Statistics.h"
+#include "lldb/Utility/ConstString.h"
+#include "lldb/Utility/FileSpec.h"
+#include "lldb/Utility/LLDBLog.h"
+#include "lldb/Utility/UUID.h"
+#include "lldb/Version/Version.h"
+#include "lldb/lldb-enumerations.h"
+#include "lldb/lldb-forward.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallString.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/ADT/Twine.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/JSON.h"
+#include "llvm/Support/LineIterator.h"
+#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/Path.h"
+#include "llvm/Support/RandomNumberGenerator.h"
+#include "llvm/Support/raw_ostream.h"
+#include "llvm/Telemetry/Telemetry.h"
+
+#ifdef HAS_VENDOR_TELEMETRY_PLUGINS
+// TODO: could make this path a build-variable rather than hard-coded header
+// path
+#include "lldb/Core/VendorTelemetryPlugin.h"
+
+namespace vendor_specific {
+
+// Set any additional configurations, as needed.
+extern void ApplyVendorSpecificConfigs(
+ llvm::telemetry::TelemetryConfig *config) /* __attribute__((weak))*/;
+
+// Return a copy of the given entry, but with certain fields that are deemed
+// PII risk removed.
+extern std::shared_ptr<llvm::telemetry::TelemetryInfo> SanitizeSensitiveFields(
+ const llvm::telemetry::TelemetryInfo *entry) /*__attribute__((weak))*/;
+
+extern std::shared_ptr<lldb_private::LldbTelemeter>
+CreateVendorSpecificTelemeter(
+ llvm::telemetry::TelemetryConfig *config) /*__attribute__((weak))*/;
+} // namespace vendor_specific
+#endif
+
+namespace lldb_private {
+
+using ::llvm::telemetry::Destination;
+using ::llvm::telemetry::EventStats;
+using ::llvm::telemetry::ExitDescription;
+using ::llvm::telemetry::SteadyTimePoint;
+using ::llvm::telemetry::TelemetryInfo;
+
+static std::string ExitDescToString(const ExitDescription *desc) {
+ return ("ExitCode:" + desc->ExitCode) +
+ (" ExixitDescription: " + desc->Description);
+}
+
+static std::string GetDuration(const EventStats &stats) {
+ if (stats.End.has_value())
+ return std::to_string((stats.End.value() - stats.Start).count()) +
+ "(nanosec)";
+ return "<NONE>";
+}
+
+std::string LldbBaseTelemetryInfo::ToString() const {
+ return ("[LldbBaseTelemetryInfo]\n") + (" SessionId: " + SessionId + "\n");
+}
+
+std::string DebuggerTelemetryInfo::ToString() const {
+ std::string duration_desc =
+ (ExitDesc.has_value() ? " lldb session duration: "
+ : " lldb startup duration: ") +
+ std::to_string((Stats.End.value() - Stats.Start).count()) + "(nanosec)\n";
+
+ return LldbBaseTelemetryInfo::ToString() + "\n" +
+ ("[DebuggerTelemetryInfo]\n") + (" username: " + username + "\n") +
+ (" lldb_git_sha: " + lldb_git_sha + "\n") +
+ (" lldb_path: " + lldb_path + "\n") + (" cwd: " + cwd + "\n") +
+ duration_desc + "\n";
+}
+
+static size_t ToNanosecOrZero(const std::optional<SteadyTimePoint> &Point) {
+ if (!Point.has_value())
+ return 0;
+
+ return Point.value().time_since_epoch().count();
+}
+
+llvm::json::Object DebuggerTelemetryInfo::serializeToJson() const {
+ return llvm::json::Object{
+ {"DebuggerInfo",
+ {
+ {"SessionId", SessionId},
+ {"username", username},
+ {"lldb_git_sha", lldb_git_sha},
+ {"lldb_path", lldb_path},
+ {"cwd", cwd},
+ {
+ "EventStats",
+ {
+ {"Start", Stats.Start.time_since_epoch().count()},
+ {"End", ToNanosecOrZero(Stats.End)},
+ },
+ },
+ // TODO: fill in more?
+ }}};
+}
+
+std::string ClientTelemetryInfo::ToString() const {
+ return LldbBaseTelemetryInfo::ToString() + "\n" +
+ ("[DapRequestInfoEntry]\n") +
+ (" request_name: " + request_name + "\n") +
+ (" request_duration: " + GetDuration(Stats) + "(nanosec)\n") +
+ (" error_msg: " + error_msg + "\n");
+}
+
+llvm::json::Object ClientTelemetryInfo::serializeToJson() const {
+ return llvm::json::Object{
+ {"ClientInfo",
+ {
+ {"SessionId", SessionId},
+ {"request_name", request_name},
+ {"error_msg", error_msg},
+ {
+ "EventStats",
+ {
+ {"Start", Stats.Start.time_since_epoch().count()},
+ {"End", ToNanosecOrZero(Stats.End)},
+ },
+ },
+ }}};
+}
+
+std::string TargetTelemetryInfo::ToString() const {
+ std::string exit_or_load_desc;
+ if (ExitDesc.has_value()) {
+ // If this entry was emitted for an exit
+ exit_or_load_desc = " process_duration: " + GetDuration(Stats) +
+ ExitDescToString(&(ExitDesc.value())) + "\n";
+ } else {
+ // This was emitted for a load event.
+ // See if it was the start-load or end-load entry
+ if (Stats.End.has_value()) {
+ exit_or_load_desc =
+ " startup_init_duration: " + GetDuration(Stats) + "\n";
+ } else {
+ exit_or_load_desc = " startup_init_start\n";
+ }
+ }
+ return LldbBaseTelemetryInfo::ToString() + "\n" +
+ ("[TargetTelemetryInfo]\n") +
+ (" target_uuid: " + target_uuid + "\n") +
+ (" file_format: " + file_format + "\n") +
+ (" binary_path: " + binary_path + "\n") +
+ (" binary_size: " + std::to_string(binary_size) + "\n") +
+ exit_or_load_desc;
+}
+
+llvm::json::Object TargetTelemetryInfo::serializeToJson() const {
+ return llvm::json::Object{{
+ "TargetInfo",
+ {
+ {"SessionId", SessionId},
+ {"target_uuid", target_uuid},
+ {"binary_path", binary_path},
+ {"binary_size", binary_size},
+ // TODO: fill in more
+ },
+ }};
+}
+
+std::string CommandTelemetryInfo::ToString() const {
+ // Whether this entry was emitted at the start or at the end of the
+ // command-execution.
+ if (Stats.End.has_value()) {
+ return LldbBaseTelemetryInfo::ToString() + "\n" +
+ ("[CommandTelemetryInfo] - END\n") +
+ (" target_uuid: " + target_uuid + "\n") +
+ (" command_uuid: " + command_uuid + "\n") +
+ (" command_name: " + command_name + "\n") +
+ (" args: " + args + "\n") +
+ (" command_runtime: " + GetDuration(Stats) + "\n") +
+ (ExitDesc.has_value() ? ExitDescToString(&(ExitDesc.value()))
+ : "no exit-description") +
+ "\n";
+ } else {
+ return LldbBaseTelemetryInfo::ToString() + "\n" +
+ ("[CommandTelemetryInfo] - START\n") +
+ (" target_uuid: " + target_uuid + "\n") +
+ (" command_uuid: " + command_uuid + "\n") +
+ (" original_command: " + original_command + "\n");
+ }
+}
+
+llvm::json::Object CommandTelemetryInfo::serializeToJson() const {
+ llvm::json::Object inner;
+
+ inner.insert({"SessionId", SessionId});
+ inner.insert({"target_uuid", target_uuid});
+ inner.insert({"command_uuid", command_uuid});
+ inner.insert({"args", args});
+ inner.insert({"original_command", original_command});
+ inner.insert({
+ "EventStats",
+ {
+ {"Start", Stats.Start.time_since_epoch().count()},
+ {"End", ToNanosecOrZero(Stats.End)},
+ },
+ });
+
+ // If this entry was emitted at the end of the command-execution,
+ // then calculate the runtime too.
+ if (Stats.End.has_value()) {
+ inner.insert(
+ {"command_runtime", (Stats.End.value() - Stats.Start).count()});
+ if (ExitDesc.has_value()) {
+ inner.insert({"exit_code", ExitDesc->ExitCode});
+ inner.insert({"exit_msg", ExitDesc->Description});
+ inner.insert({"return_status", static_cast<int>(ret_status)});
+ }
+ }
+
+ return llvm::json::Object{{"CommandInfo", std::move(inner)}};
+}
+
+std::string MiscTelemetryInfo::ToString() const {
+ std::string ret;
+ llvm::raw_string_ostream ret_strm(ret);
+ ret_strm << LldbBaseTelemetryInfo::ToString() << "\n[MiscTelemetryInfo]\n"
+ << " target_uuid: " << target_uuid + "\n"
+ << " meta_data:\n";
+ for (const auto &kv : meta_data) {
+ ret_strm << " " << kv.first << ": " << kv.second << "\n";
+ }
+ return ret;
+}
+
+llvm::json::Object MiscTelemetryInfo::serializeToJson() const {
+ llvm::json::Object meta_data_obj;
+
+ for (const auto &kv : meta_data)
+ meta_data_obj.insert({kv.first, kv.second});
+
+ return llvm::json::Object{{
+ "MiscInfo",
+ {
+ {"SessionId", SessionId},
+ {"target_uuid", target_uuid},
+ {"meta_data", std::move(meta_data_obj)},
+ },
+ }};
+}
+
+class StreamTelemetryDestination : public Destination {
+public:
+ StreamTelemetryDestination(llvm::raw_ostream &os, std::string desc)
+ : os(os), desc(desc) {}
+ llvm::Error emitEntry(const llvm::telemetry::TelemetryInfo *entry) override {
+ // Unless there exists a custom (vendor-defined) data-cleanup
+ // for printing, upstream Telemetry should not leak anything other than the
+ // basic.
+#ifdef HAS_TELEMETRY_FIELDS_PRINTER
+ os << SanitizeSensitiveFields(entry)->ToString() << "\n";
+#else
+ os << "session_uuid: " << entry->SessionId
+ << "<the rest is omitted due to PII risk>\n";
+#endif
+ os.flush();
+ return llvm::ErrorSuccess();
+ }
+
+ std::string name() const override { return desc; }
+
+private:
+ llvm::raw_ostream &os;
+ const std::string desc;
+};
+
+// No-op logger to use when users disable telemetry
+class NoOpTelemeter : public LldbTelemeter {
+public:
+ static std::unique_ptr<LldbTelemeter> CreateInstance(Debugger *debugger) {
+ return std::unique_ptr<LldbTelemeter>(new NoOpTelemeter(debugger));
+ }
+
+ NoOpTelemeter(Debugger *debugger) {}
+ void logStartup(llvm::StringRef tool_path, TelemetryInfo *entry) override {}
+ void logExit(llvm::StringRef tool_path, TelemetryInfo *entry) override {}
+
+ void LogProcessExit(int status, llvm::StringRef exit_string, EventStats stats,
+ Target *target_ptr) override {}
+ void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod,
+ EventStats stats) override {}
+ void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod,
+ EventStats stats) override {}
+
+ void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command,
+ EventStats stats, Target *target_ptr) override {}
+ void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name,
+ llvm::StringRef command_args, EventStats stats,
+ Target *target_ptr, CommandReturnObject *result) override {
+ }
+
+ void LogClientTelemetry(const llvm::json::Object &entry) override {}
+
+ void addDestination(llvm::telemetry::Destination *destination) override {}
+ std::string GetNextUUID() override { return ""; }
+};
+
+class BasicTelemeter : public LldbTelemeter {
+public:
+ static std::unique_ptr<BasicTelemeter> CreateInstance(Debugger *);
+
+ virtual ~BasicTelemeter() = default;
+
+ void logStartup(llvm::StringRef lldb_path, TelemetryInfo *entry) override;
+ void logExit(llvm::StringRef lldb_path, TelemetryInfo *entry) override;
+
+ void LogProcessExit(int status, llvm::StringRef exit_string, EventStats stats,
+ Target *target_ptr) override;
+ void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod,
+ EventStats stats) override;
+ void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod,
+ EventStats stats) override;
+
+ void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command,
+ EventStats stats, Target *target_ptr) override;
+ void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name,
+ llvm::StringRef command_args, EventStats stats,
+ Target *target_ptr, CommandReturnObject *result) override;
+
+ void LogClientTelemetry(const llvm::json::Object &entry) override;
+
+ void addDestination(Destination *destination) override {
+ m_destinations.push_back(destination);
+ }
+
+ std::string GetNextUUID() override {
+ return std::to_string(uuid_seed.fetch_add(1));
+ }
+
+protected:
+ BasicTelemeter(Debugger *debugger);
+
+ void CollectMiscBuildInfo();
+
+private:
+ template <typename EntrySubType> EntrySubType MakeBaseEntry() {
+ EntrySubType entry;
+ entry.SessionId = m_session_uuid;
+ entry.Counter = counter.fetch_add(1);
+ return entry;
+ }
+
+ void EmitToDestinations(const TelemetryInfo *entry);
+
+ Debugger *m_debugger;
+ const std::string m_session_uuid;
+ std::string startup_lldb_path;
+
+ // counting number of entries.
+ std::atomic<size_t> counter = 0;
+
+ std::vector<Destination *> m_destinations;
+
+ std::atomic<size_t> uuid_seed = 0;
+};
+
+static std::string MakeUUID(lldb_private::Debugger *debugger) {
+ std::string ret;
+ uint8_t random_bytes[16];
+ if (auto ec = llvm::getRandomBytes(random_bytes, 16)) {
+ LLDB_LOG(GetLog(LLDBLog::Object),
+ "Failed to generate random bytes for UUID: {0}", ec.message());
+ // fallback to using timestamp + debugger ID.
+ ret = std::to_string(
+ std::chrono::steady_clock::now().time_since_epoch().count()) +
+ "_" + std::to_string(debugger->GetID());
+ } else {
+ ret = lldb_private::UUID(random_bytes).GetAsString();
+ }
+
+ return ret;
+}
+
+BasicTelemeter::BasicTelemeter(lldb_private::Debugger *debugger)
+ : m_debugger(debugger), m_session_uuid(MakeUUID(debugger)) {}
+
+std::unique_ptr<BasicTelemeter>
+BasicTelemeter::CreateInstance(lldb_private::Debugger *debugger) {
+ auto *config = GetTelemetryConfig();
+
+ BasicTelemeter *ins = new BasicTelemeter(debugger);
+ for (const std ::string &dest : config->AdditionalDestinations) {
+ if (dest == "stdout") {
+ ins->addDestination(
+ new StreamTelemetryDestination(llvm::outs(), "stdout"));
+ } else if (dest == "stderr") {
+ ins->addDestination(
+ new StreamTelemetryDestination(llvm::errs(), "stderr"));
+ } else {
+ // TODO: handle custom values as needed?
+ }
+ }
+
+ return std::unique_ptr<BasicTelemeter>(ins);
+}
+
+void BasicTelemeter::EmitToDestinations(const TelemetryInfo *entry) {
+ // TODO: can do this in a separate thread (need to own the ptrs!).
+ for (Destination *destination : m_destinations) {
+ llvm::Error err = destination->emitEntry(entry);
+ if (err) {
+ LLDB_LOG(GetLog(LLDBLog::Object),
+ "Error emitting to destination(name = {0})",
+ destination->name());
+ }
+ }
+}
+
+void BasicTelemeter::logStartup(llvm::StringRef lldb_path,
+ TelemetryInfo *entry) {
+ startup_lldb_path = lldb_path.str();
+ lldb_private::DebuggerTelemetryInfo startup_info =
+ MakeBaseEntry<DebuggerTelemetryInfo>();
+
+ UserIDResolver &resolver = lldb_private::HostInfo::GetUserIDResolver();
+ std::optional<llvm::StringRef> opt_username =
+ resolver.GetUserName(lldb_private::HostInfo::GetUserID());
+ if (opt_username)
+ startup_info.username = *opt_username;
+
+ startup_info.lldb_git_sha =
+ lldb_private::GetVersion(); // TODO: find the real git sha
+ startup_info.lldb_path = startup_lldb_path;
+ startup_info.Stats = entry->Stats;
+
+ llvm::SmallString<64> cwd;
+ if (!llvm::sys::fs::current_path(cwd)) {
+ startup_info.cwd = cwd.c_str();
+ } else {
+ MiscTelemetryInfo misc_info = MakeBaseEntry<MiscTelemetryInfo>();
+ misc_info.meta_data["internal_errors"] = "Cannot determine CWD";
+ EmitToDestinations(&misc_info);
+ }
+
+ EmitToDestinations(&startup_info);
+
+ // Optional part
+ CollectMiscBuildInfo();
+}
+
+void BasicTelemeter::logExit(llvm::StringRef lldb_path, TelemetryInfo *entry) {
+ // we should be shutting down the same instance that we started?!
+ // llvm::Assert(startup_lldb_path == lldb_path.str());
+
+ lldb_private::DebuggerTelemetryInfo exit_info =
+ MakeBaseEntry<lldb_private::DebuggerTelemetryInfo>();
+ exit_info.Stats = entry->Stats;
+ exit_info.lldb_path = startup_lldb_path;
+ if (auto *selected_target =
+ m_debugger->GetSelectedExecutionContext().GetTargetPtr()) {
+ if (!selected_target->IsDummyTarget()) {
+ const lldb::ProcessSP proc = selected_target->GetProcessSP();
+ if (proc == nullptr) {
+ // no process has been launched yet.
+ exit_info.ExitDesc = {-1, "no process launched."};
+ } else {
+ exit_info.ExitDesc = {proc->GetExitStatus(), ""};
+ if (const char *description = proc->GetExitDescription())
+ exit_info.ExitDesc->Description = std::string(description);
+ }
+ }
+ }
+ EmitToDestinations(&exit_info);
+}
+
+void BasicTelemeter::LogProcessExit(int status, llvm::StringRef exit_string,
+ EventStats stats, Target *target_ptr) {
+ lldb_private::TargetTelemetryInfo exit_info =
+ MakeBaseEntry<TargetTelemetryInfo>();
+ exit_info.Stats = std::move(stats);
+ exit_info.target_uuid =
+ target_ptr && !target_ptr->IsDummyTarget()
+ ? target_ptr->GetExecutableModule()->GetUUID().GetAsString()
+ : "";
+ exit_info.ExitDesc = {status, exit_string.str()};
+
+ EmitToDestinations(&exit_info);
+}
+
+void BasicTelemeter::CollectMiscBuildInfo() {
+ // collecting use-case specific data
+}
+
+void BasicTelemeter::LogMainExecutableLoadStart(lldb::ModuleSP exec_mod,
+ EventStats stats) {
+ TargetTelemetryInfo target_info = MakeBaseEntry<TargetTelemetryInfo>();
+ target_info.Stats = std::move(stats);
+ target_info.binary_path =
+ exec_mod->GetFileSpec().GetPathAsConstString().GetCString();
+ target_info.file_format = exec_mod->GetArchitecture().GetArchitectureName();
+ target_info.target_uuid = exec_mod->GetUUID().GetAsString();
+ if (auto err = llvm::sys::fs::file_size(exec_mod->GetFileSpec().GetPath(),
+ target_info.binary_size)) {
+ // If there was error obtaining it, just reset the size to 0.
+ // Maybe log the error too?
+ target_info.binary_size = 0;
+ }
+ EmitToDestinations(&target_info);
+}
+
+void BasicTelemeter::LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod,
+ EventStats stats) {
+ TargetTelemetryInfo target_info = MakeBaseEntry<TargetTelemetryInfo>();
+ target_info.Stats = std::move(stats);
+ target_info.binary_path =
+ exec_mod->GetFileSpec().GetPathAsConstString().GetCString();
+ target_info.file_format = exec_mod->GetArchitecture().GetArchitectureName();
+ target_info.target_uuid = exec_mod->GetUUID().GetAsString();
+ target_info.binary_size = exec_mod->GetObjectFile()->GetByteSize();
+
+ EmitToDestinations(&target_info);
+
+ // Collect some more info, might be useful?
+ MiscTelemetryInfo misc_info = MakeBaseEntry<MiscTelemetryInfo>();
+ misc_info.target_uuid = exec_mod->GetUUID().GetAsString();
+ misc_info.meta_data["symtab_index_time"] =
+ std::to_string(exec_mod->GetSymtabIndexTime().get().count());
+ misc_info.meta_data["symtab_parse_time"] =
+ std::to_string(exec_mod->GetSymtabParseTime().get().count());
+ EmitToDestinations(&misc_info);
+}
+
+void BasicTelemeter::LogClientTelemetry(const llvm::json::Object &entry) {
+ ClientTelemetryInfo client_info = MakeBaseEntry<ClientTelemetryInfo>();
+
+ std::optional<llvm::StringRef> request_name = entry.getString("request_name");
+ if (!request_name.has_value()) {
+ MiscTelemetryInfo misc_info = MakeBaseEntry<MiscTelemetryInfo>();
+ misc_info.meta_data["internal_errors"] =
+ "Cannot determine request name from client entry";
+ // TODO: Dump the errornous entry to stderr too?
+ EmitToDestinations(&misc_info);
+ return;
+ }
+ client_info.request_name = request_name->str();
+
+ std::optional<int64_t> start_time = entry.getInteger("start_time");
+ std::optional<int64_t> end_time = entry.getInteger("end_time");
+
+ if (!start_time.has_value() || !end_time.has_value()) {
+ MiscTelemetryInfo misc_info = MakeBaseEntry<MiscTelemetryInfo>();
+ misc_info.meta_data["internal_errors"] =
+ "Cannot determine start/end time from client entry";
+ EmitToDestinations(&misc_info);
+ return;
+ }
+
+ SteadyTimePoint epoch;
+ client_info.Stats.Start =
+ epoch + std::chrono::nanoseconds(static_cast<size_t>(*start_time));
+ client_info.Stats.End =
+ epoch + std::chrono::nanoseconds(static_cast<size_t>(*end_time));
+
+ std::optional<llvm::StringRef> error_msg = entry.getString("error");
+ if (error_msg.has_value())
+ client_info.error_msg = error_msg->str();
+
+ EmitToDestinations(&client_info);
+}
+
+void BasicTelemeter::LogCommandStart(llvm::StringRef uuid,
+ llvm::StringRef original_command,
+ EventStats stats, Target *target_ptr) {
+
+ lldb_private::CommandTelemetryInfo command_info =
+ MakeBaseEntry<CommandTelemetryInfo>();
+
+ // If we have a target attached to this command, then get the UUID.
+ command_info.target_uuid = "";
+ if (target_ptr && target_ptr->GetExecutableModule() != nullptr) {
+ command_info.target_uuid =
+ target_ptr->GetExecutableModule()->GetUUID().GetAsString();
+ }
+ command_info.command_uuid = uuid.str();
+ command_info.original_command = original_command.str();
+ command_info.Stats = std::move(stats);
+
+ EmitToDestinations(&command_info);
+}
+
+void BasicTelemeter::LogCommandEnd(llvm::StringRef uuid,
+ llvm::StringRef command_name,
+ llvm::StringRef command_args,
+ EventStats stats, Target *target_ptr,
+ CommandReturnObject *result) {
+
+ lldb_private::CommandTelemetryInfo command_info =
+ MakeBaseEntry<CommandTelemetryInfo>();
+
+ // If we have a target attached to this command, then get the UUID.
+ command_info.target_uuid = "";
+ if (target_ptr && target_ptr->GetExecutableModule() != nullptr) {
+ command_info.target_uuid =
+ target_ptr->GetExecutableModule()->GetUUID().GetAsString();
+ }
+ command_info.command_uuid = uuid.str();
+ command_info.command_name = command_name.str();
+ command_info.args = command_args.str();
+ command_info.Stats = std::move(stats);
+ command_info.ExitDesc = {result->Succeeded() ? 0 : -1, ""};
+ if (llvm::StringRef error_data = result->GetErrorData();
+ !error_data.empty()) {
+ command_info.ExitDesc->Description = error_data.str();
+ }
+ command_info.ret_status = result->GetStatus();
+ EmitToDestinations(&command_info);
+}
+
+llvm::StringRef parse_value(llvm::StringRef str, llvm::StringRef label) {
+ return str.substr(label.size()).trim();
+}
+
+bool parse_field(llvm::StringRef str, llvm::StringRef label) {
+ if (parse_value(str, label) == "true")
+ return true;
+ return false;
+}
+
+llvm::telemetry::Config *MakeTelemetryConfig() {
+ bool enable_telemetry = false;
+ std::vector<std::string> additional_destinations;
+
+ // Look in the $HOME/.lldb_telemetry_config file to populate the struct
+ llvm::SmallString<64> init_file;
+ FileSystem::Instance().GetHomeDirectory(init_file);
+ llvm::sys::path::append(init_file, ".lldb_telemetry_config");
+ FileSystem::Instance().Resolve(init_file);
+ if (llvm::sys::fs::exists(init_file)) {
+ auto contents = llvm::MemoryBuffer::getFile(init_file, /*IsText*/ true);
+ if (contents) {
+ llvm::line_iterator iter =
+ llvm::line_iterator(contents->get()->getMemBufferRef());
+ for (; !iter.is_at_eof(); ++iter) {
+ if (iter->starts_with("enable_telemetry:")) {
+ enable_telemetry = parse_field(*iter, "enable_telemetry:");
+ } else if (iter->starts_with("destination:")) {
+ llvm::StringRef dest = parse_value(*iter, "destination:");
+ if (dest == "stdout") {
+ additional_destinations.push_back("stdout");
+ } else if (dest == "stderr") {
+ additional_destinations.push_back("stderr");
+ } else {
+ additional_destinations.push_back(dest.str());
+ }
+ }
+ }
+ } else {
+ LLDB_LOG(GetLog(LLDBLog::Object), "Error reading config file at {0}",
+ init_file.c_str());
+ }
+ }
+
+ auto *ret =
+ new llvm::telemetry::Config{enable_telemetry, additional_destinations};
+#ifdef HAS_VENDOR_TELEMETRY_PLUGINS
+ vendor_specific::ApplyVendorSpecificConfigs(ret);
+#endif
+ return ret;
+}
+
+llvm::telemetry::Config *GetTelemetryConfig() {
+ static llvm::telemetry::Config *config = MakeTelemetryConfig();
+ return config;
+}
+
+std::unique_ptr<LldbTelemeter>
+LldbTelemeter::CreateInstance(lldb_private::Debugger *debugger) {
+ auto *config = GetTelemetryConfig();
+ if (!config->EnableTelemetry) {
+ return NoOpTelemeter::CreateInstance(debugger);
+ }
+
+#ifdef HAS_VENDOR_TELEMETRY_PLUGINS
+ return vendor_specific::CreateVendorSpecificTelemeter(config);
+#else
+ return BasicTelemeter::CreateInstance(debugger);
+#endif
+}
+} // namespace lldb_private
diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp
index df539d5f5bcee9..05b399ceab9a44 100644
--- a/lldb/source/Interpreter/CommandInterpreter.cpp
+++ b/lldb/source/Interpreter/CommandInterpreter.cpp
@@ -1886,8 +1886,34 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
LazyBool lazy_add_to_history,
CommandReturnObject &result,
bool force_repeat_command) {
+ llvm::telemetry::EventStats start_command_stats(
+ std::chrono::steady_clock::now());
+ LldbTelemeter *telemeter = GetDebugger().GetTelemeter();
+ // Generate a UUID for this command so the logger can match
+ // the start/end entries correctly.
+ const std::string command_uuid = telemeter->GetNextUUID();
+
+ telemeter->LogCommandStart(command_uuid, command_line, start_command_stats,
+ GetExecutionContext().GetTargetPtr());
+
std::string command_string(command_line);
std::string original_command_string(command_line);
+ std::string parsed_command_args;
+ CommandObject *cmd_obj = nullptr;
+
+ auto log_on_exit = llvm::make_scope_exit([&]() {
+ llvm::telemetry::EventStats end_command_stats(
+ start_command_stats.Start, std::chrono::steady_clock::now());
+
+ llvm::StringRef command_name =
+ cmd_obj ? cmd_obj->GetCommandName() : "<not found>";
+ // TODO: this is logging the time the command-handler finishes.
+ // But we may want a finer-grain durations too?
+ // (ie., the execute_time recorded below?)
+ telemeter->LogCommandEnd(command_uuid, command_name, parsed_command_args,
+ end_command_stats,
+ GetExecutionContext().GetTargetPtr(), &result);
+ });
Log *log = GetLog(LLDBLog::Commands);
llvm::PrettyStackTraceFormat stack_trace("HandleCommand(command = \"%s\")",
@@ -1925,9 +1951,9 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
bool empty_command = false;
bool comment_command = false;
- if (command_string.empty())
+ if (command_string.empty()) {
empty_command = true;
- else {
+ } else {
const char *k_space_characters = "\t\n\v\f\r ";
size_t non_space = command_string.find_first_not_of(k_space_characters);
@@ -1992,7 +2018,7 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
// From 1 above, we can determine whether the Execute function wants raw
// input or not.
- CommandObject *cmd_obj = ResolveCommandImpl(command_string, result);
+ cmd_obj = ResolveCommandImpl(command_string, result);
// We have to preprocess the whole command string for Raw commands, since we
// don't know the structure of the command. For parsed commands, we only
@@ -2053,30 +2079,29 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
if (add_to_history)
m_command_history.AppendString(original_command_string);
- std::string remainder;
const std::size_t actual_cmd_name_len = cmd_obj->GetCommandName().size();
if (actual_cmd_name_len < command_string.length())
- remainder = command_string.substr(actual_cmd_name_len);
+ parsed_command_args = command_string.substr(actual_cmd_name_len);
// Remove any initial spaces
- size_t pos = remainder.find_first_not_of(k_white_space);
+ size_t pos = parsed_command_args.find_first_not_of(k_white_space);
if (pos != 0 && pos != std::string::npos)
- remainder.erase(0, pos);
+ parsed_command_args.erase(0, pos);
LLDB_LOGF(
log, "HandleCommand, command line after removing command name(s): '%s'",
- remainder.c_str());
+ parsed_command_args.c_str());
// To test whether or not transcript should be saved, `transcript_item` is
// used instead of `GetSaveTrasncript()`. This is because the latter will
// fail when the command is "settings set interpreter.save-transcript true".
if (transcript_item) {
transcript_item->AddStringItem("commandName", cmd_obj->GetCommandName());
- transcript_item->AddStringItem("commandArguments", remainder);
+ transcript_item->AddStringItem("commandArguments", parsed_command_args);
}
ElapsedTime elapsed(execute_time);
- cmd_obj->Execute(remainder.c_str(), result);
+ cmd_obj->Execute(parsed_command_args.c_str(), result);
}
LLDB_LOGF(log, "HandleCommand, command %s",
diff --git a/lldb/source/Target/Process.cpp b/lldb/source/Target/Process.cpp
index 97ce2c14458e93..a21c5adf9b2f25 100644
--- a/lldb/source/Target/Process.cpp
+++ b/lldb/source/Target/Process.cpp
@@ -22,6 +22,7 @@
#include "lldb/Core/ModuleSpec.h"
#include "lldb/Core/PluginManager.h"
#include "lldb/Core/Progress.h"
+#include "lldb/Core/Telemetry.h"
#include "lldb/Expression/DiagnosticManager.h"
#include "lldb/Expression/DynamicCheckerFunctions.h"
#include "lldb/Expression/UserExpression.h"
@@ -74,6 +75,8 @@
#include "lldb/Utility/SelectHelper.h"
#include "lldb/Utility/State.h"
#include "lldb/Utility/Timer.h"
+#include "llvm/Telemetry/Telemetry.h"
+#include <chrono>
using namespace lldb;
using namespace lldb_private;
@@ -1065,6 +1068,9 @@ bool Process::SetExitStatus(int status, llvm::StringRef exit_string) {
// Use a mutex to protect setting the exit status.
std::lock_guard<std::mutex> guard(m_exit_status_mutex);
+ llvm::telemetry::SteadyTimePoint start_time =
+ std::chrono::steady_clock::now();
+
Log *log(GetLog(LLDBLog::State | LLDBLog::Process));
LLDB_LOG(log, "(plugin = {0} status = {1} ({1:x8}), description=\"{2}\")",
GetPluginName(), status, exit_string);
@@ -1094,6 +1100,11 @@ bool Process::SetExitStatus(int status, llvm::StringRef exit_string) {
// Allow subclasses to do some cleanup
DidExit();
+ llvm::telemetry::EventStats stats = {start_time,
+ std::chrono::steady_clock::now()};
+ GetTarget().GetDebugger().GetTelemeter()->LogProcessExit(status, exit_string,
+ stats, &GetTarget());
+
return true;
}
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index fcac0a48f46e6d..6ff01d142a7cf8 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -24,6 +24,7 @@
#include "lldb/Core/Section.h"
#include "lldb/Core/SourceManager.h"
#include "lldb/Core/StructuredDataImpl.h"
+#include "lldb/Core/Telemetry.h"
#include "lldb/Core/ValueObject.h"
#include "lldb/Core/ValueObjectConstResult.h"
#include "lldb/Expression/DiagnosticManager.h"
@@ -67,7 +68,9 @@
#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/SetVector.h"
+#include "llvm/Telemetry/Telemetry.h"
+#include <chrono>
#include <memory>
#include <mutex>
#include <optional>
@@ -1477,9 +1480,21 @@ void Target::DidExec() {
void Target::SetExecutableModule(ModuleSP &executable_sp,
LoadDependentFiles load_dependent_files) {
+ llvm::telemetry::EventStats load_executable_stats(
+ std::chrono::steady_clock::now());
Log *log = GetLog(LLDBLog::Target);
ClearModules(false);
+ if (executable_sp) {
+ m_debugger.GetTelemeter()->LogMainExecutableLoadStart(
+ executable_sp, load_executable_stats);
+
+ auto log_on_exit = llvm::make_scope_exit([&]() {
+ load_executable_stats.End = std::chrono::steady_clock::now();
+ m_debugger.GetTelemeter()->LogMainExecutableLoadEnd(
+ executable_sp, load_executable_stats);
+ });
+ }
if (executable_sp) {
ElapsedTime elapsed(m_stats.GetCreateTime());
LLDB_SCOPED_TIMERF("Target::SetExecutableModule (executable = '%s')",
diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp
index 6012ee52110b73..eb4be3ac64aa1a 100644
--- a/lldb/tools/lldb-dap/DAP.cpp
+++ b/lldb/tools/lldb-dap/DAP.cpp
@@ -682,17 +682,32 @@ PacketStatus DAP::GetNextObject(llvm::json::Object &object) {
}
bool DAP::HandleObject(const llvm::json::Object &object) {
+ auto start_time = std::chrono::steady_clock::now();
const auto packet_type = GetString(object, "type");
if (packet_type == "request") {
const auto command = GetString(object, "command");
auto handler_pos = request_handlers.find(std::string(command));
+ llvm::json::Object telemetry_entry;
+ telemetry_entry.insert({"request_name", std::string(command)});
+ telemetry_entry.insert(
+ {"start_time", start_time.time_since_epoch().count()});
+
if (handler_pos != request_handlers.end()) {
handler_pos->second(object);
+ auto end_time = std::chrono::steady_clock::now();
+ telemetry_entry.insert({"end_time", end_time.time_since_epoch().count()});
+ debugger.SendTelemetry(telemetry_entry);
return true; // Success
} else {
if (log)
*log << "error: unhandled command \"" << command.data() << "\""
<< std::endl;
+ auto end_time = std::chrono::steady_clock::now();
+ telemetry_entry.insert({"end_time", end_time.time_since_epoch().count()});
+ telemetry_entry.insert(
+ {"error", "unhandled-command:" + std::string(command)});
+ debugger.SendTelemetry(telemetry_entry);
+ debugger.SendTelemetry(telemetry_entry);
return false; // Fail
}
}
>From 04b7da899d6544fac977321f6e9c00f14d42446e Mon Sep 17 00:00:00 2001
From: Vy Nguyen <vyng at google.com>
Date: Tue, 17 Sep 2024 15:21:44 -0400
Subject: [PATCH 23/25] change the data type from json to structureddata
---
lldb/include/lldb/API/SBDebugger.h | 3 +--
lldb/include/lldb/Core/Debugger.h | 4 ++--
lldb/include/lldb/Core/Telemetry.h | 4 +++-
lldb/source/API/SBDebugger.cpp | 4 ++--
lldb/source/Core/Debugger.cpp | 3 ++-
lldb/source/Core/Telemetry.cpp | 13 ++++++----
lldb/tools/lldb-dap/DAP.cpp | 38 +++++++++++++++++++++++-------
7 files changed, 48 insertions(+), 21 deletions(-)
diff --git a/lldb/include/lldb/API/SBDebugger.h b/lldb/include/lldb/API/SBDebugger.h
index 7ec69dc89b3f37..b12c4623de2858 100644
--- a/lldb/include/lldb/API/SBDebugger.h
+++ b/lldb/include/lldb/API/SBDebugger.h
@@ -14,7 +14,6 @@
#include "lldb/API/SBDefines.h"
#include "lldb/API/SBPlatform.h"
#include "lldb/API/SBStructuredData.h"
-#include "llvm/Support/JSON.h"
namespace lldb_private {
class CommandPluginInterfaceImplementation;
@@ -247,7 +246,7 @@ class LLDB_API SBDebugger {
lldb::SBTarget GetDummyTarget();
- void SendTelemetry(const llvm::json::Object &entry);
+ void SendTelemetry(const lldb::SBStructuredData &entry);
// Return true if target is deleted from the target list of the debugger.
bool DeleteTarget(lldb::SBTarget &target);
diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h
index d0e40e415c0973..7a500aa9cd787b 100644
--- a/lldb/include/lldb/Core/Debugger.h
+++ b/lldb/include/lldb/Core/Debugger.h
@@ -19,6 +19,7 @@
#include "lldb/Core/FormatEntity.h"
#include "lldb/Core/IOHandler.h"
#include "lldb/Core/SourceManager.h"
+#include "lldb/Core/StructuredDataImpl.h"
#include "lldb/Core/Telemetry.h"
#include "lldb/Core/UserSettingsController.h"
#include "lldb/Host/HostThread.h"
@@ -47,7 +48,6 @@
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/DynamicLibrary.h"
#include "llvm/Support/FormatVariadic.h"
-#include "llvm/Support/JSON.h"
#include "llvm/Support/Threading.h"
#include "llvm/Telemetry/Telemetry.h"
@@ -155,7 +155,7 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
LldbTelemeter *GetTelemeter() { return m_telemeter.get(); }
- void SendClientTelemetry(const llvm::json::Object &entry);
+ void SendClientTelemetry(const lldb_private::StructuredDataImpl &entry);
Status SetInputString(const char *data);
diff --git a/lldb/include/lldb/Core/Telemetry.h b/lldb/include/lldb/Core/Telemetry.h
index bc07369722a127..61cdca59f31fc6 100644
--- a/lldb/include/lldb/Core/Telemetry.h
+++ b/lldb/include/lldb/Core/Telemetry.h
@@ -17,6 +17,7 @@
#include <string>
#include <unordered_map>
+#include "lldb/Core/StructuredDataImpl.h"
#include "lldb/Interpreter/CommandReturnObject.h"
#include "lldb/Utility/StructuredData.h"
#include "lldb/lldb-forward.h"
@@ -258,7 +259,8 @@ class LldbTelemeter : public llvm::telemetry::Telemeter {
virtual std::string GetNextUUID() = 0;
// For client (eg., SB API) to send telemetry entries.
- virtual void LogClientTelemetry(const llvm::json::Object &entry) = 0;
+ virtual void
+ LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) = 0;
};
// Logger configs: LLDB users can also supply their own configs via:
diff --git a/lldb/source/API/SBDebugger.cpp b/lldb/source/API/SBDebugger.cpp
index 950a84cbb53f11..d9cfaf3c51470e 100644
--- a/lldb/source/API/SBDebugger.cpp
+++ b/lldb/source/API/SBDebugger.cpp
@@ -972,9 +972,9 @@ SBTarget SBDebugger::GetDummyTarget() {
return sb_target;
}
-void SBDebugger::SendTelemetry(const llvm::json::Object &entry) {
+void SBDebugger::SendTelemetry(const lldb::SBStructuredData &entry) {
if (lldb_private::Debugger *debugger = this->get()) {
- debugger->SendClientTelemetry(entry);
+ debugger->SendClientTelemetry(*(entry.m_impl_up.get()));
} else {
Log *log = GetLog(LLDBLog::API);
LLDB_LOGF(log,
diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index 167a49f9ee8fc3..b1cfca06539382 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -2256,6 +2256,7 @@ llvm::ThreadPoolInterface &Debugger::GetThreadPool() {
return *g_thread_pool;
}
-void Debugger::SendClientTelemetry(const llvm::json::Object &entry) {
+void Debugger::SendClientTelemetry(
+ const lldb_private::StructuredDataImpl &entry) {
m_telemeter->LogClientTelemetry(entry);
}
diff --git a/lldb/source/Core/Telemetry.cpp b/lldb/source/Core/Telemetry.cpp
index d1d0c010395ae8..795065a0e25e0f 100644
--- a/lldb/source/Core/Telemetry.cpp
+++ b/lldb/source/Core/Telemetry.cpp
@@ -332,7 +332,8 @@ class NoOpTelemeter : public LldbTelemeter {
Target *target_ptr, CommandReturnObject *result) override {
}
- void LogClientTelemetry(const llvm::json::Object &entry) override {}
+ void
+ LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override {}
void addDestination(llvm::telemetry::Destination *destination) override {}
std::string GetNextUUID() override { return ""; }
@@ -360,7 +361,8 @@ class BasicTelemeter : public LldbTelemeter {
llvm::StringRef command_args, EventStats stats,
Target *target_ptr, CommandReturnObject *result) override;
- void LogClientTelemetry(const llvm::json::Object &entry) override;
+ void
+ LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override;
void addDestination(Destination *destination) override {
m_destinations.push_back(destination);
@@ -563,9 +565,11 @@ void BasicTelemeter::LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod,
EmitToDestinations(&misc_info);
}
-void BasicTelemeter::LogClientTelemetry(const llvm::json::Object &entry) {
+void BasicTelemeter::LogClientTelemetry(
+ const lldb_private::StructuredDataImpl &entry) {
+ // TODO: pull the dictionary out of entry
ClientTelemetryInfo client_info = MakeBaseEntry<ClientTelemetryInfo>();
-
+ /*
std::optional<llvm::StringRef> request_name = entry.getString("request_name");
if (!request_name.has_value()) {
MiscTelemetryInfo misc_info = MakeBaseEntry<MiscTelemetryInfo>();
@@ -597,6 +601,7 @@ void BasicTelemeter::LogClientTelemetry(const llvm::json::Object &entry) {
std::optional<llvm::StringRef> error_msg = entry.getString("error");
if (error_msg.has_value())
client_info.error_msg = error_msg->str();
+ */
EmitToDestinations(&client_info);
}
diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp
index eb4be3ac64aa1a..584cac90b2143a 100644
--- a/lldb/tools/lldb-dap/DAP.cpp
+++ b/lldb/tools/lldb-dap/DAP.cpp
@@ -13,6 +13,7 @@
#include <sstream>
#include "DAP.h"
+#include "JSONUtils.h"
#include "LLDBUtils.h"
#include "lldb/API/SBCommandInterpreter.h"
#include "llvm/ADT/StringExtras.h"
@@ -687,15 +688,30 @@ bool DAP::HandleObject(const llvm::json::Object &object) {
if (packet_type == "request") {
const auto command = GetString(object, "command");
auto handler_pos = request_handlers.find(std::string(command));
- llvm::json::Object telemetry_entry;
- telemetry_entry.insert({"request_name", std::string(command)});
- telemetry_entry.insert(
- {"start_time", start_time.time_since_epoch().count()});
+ lldb::SBStructuredData telemetry_entry;
+
+ // There does not seem to be a direct way to construct an SBStructuredData.
+ // So we first create a json::Array object,
+ // then we serialize it to a string,
+ // and finally call SBStructuredData::SetFromJSON(string).
+ //
+ // TODO: This seems unnecessarily complex. Ideally, we should be able to
+ // just send a json::Object directly? Does the SB API allow json?
+ //
+ llvm::json::Array telemetry_array({
+ {"request_name", std::string(command)},
+ {"start_time", start_time.time_since_epoch().count()},
+ });
if (handler_pos != request_handlers.end()) {
handler_pos->second(object);
auto end_time = std::chrono::steady_clock::now();
- telemetry_entry.insert({"end_time", end_time.time_since_epoch().count()});
+ telemetry_array.push_back(
+ llvm::json::Value{"end_time", end_time.time_since_epoch().count()});
+
+ llvm::json::Value val(std::move(telemetry_array));
+ std::string string_rep = lldb_dap::JSONToString(val);
+ telemetry_entry.SetFromJSON(string_rep.c_str());
debugger.SendTelemetry(telemetry_entry);
return true; // Success
} else {
@@ -703,10 +719,14 @@ bool DAP::HandleObject(const llvm::json::Object &object) {
*log << "error: unhandled command \"" << command.data() << "\""
<< std::endl;
auto end_time = std::chrono::steady_clock::now();
- telemetry_entry.insert({"end_time", end_time.time_since_epoch().count()});
- telemetry_entry.insert(
- {"error", "unhandled-command:" + std::string(command)});
- debugger.SendTelemetry(telemetry_entry);
+ telemetry_array.push_back(
+ llvm::json::Value{"end_time", end_time.time_since_epoch().count()});
+ telemetry_array.push_back(llvm::json::Value{
+ "error", llvm::Twine("unhandled-command:" + command).str()});
+
+ llvm::json::Value val(std::move(telemetry_array));
+ std::string string_rep = lldb_dap::JSONToString(val);
+ telemetry_entry.SetFromJSON(string_rep.c_str());
debugger.SendTelemetry(telemetry_entry);
return false; // Fail
}
>From 0e49b932f3b92b3ae6744a3b86499c95b70f97cf Mon Sep 17 00:00:00 2001
From: Vy Nguyen <vyng at google.com>
Date: Fri, 20 Sep 2024 10:55:55 -0400
Subject: [PATCH 24/25] Rework the default impl of Telemetry in LLDB as
"Plugin" to allow for testing
---
lldb/include/lldb/Core/PluginManager.h | 9 +
lldb/include/lldb/Core/Telemetry.h | 90 ++--
lldb/include/lldb/Core/TelemetryVendor.h | 46 ++
lldb/include/lldb/lldb-forward.h | 1 +
lldb/include/lldb/lldb-private-interfaces.h | 1 +
lldb/source/Core/CMakeLists.txt | 1 +
lldb/source/Core/PluginManager.cpp | 27 +
lldb/source/Core/Telemetry.cpp | 476 +----------------
lldb/source/Core/TelemetryVendor.cpp | 546 ++++++++++++++++++++
lldb/test/CMakeLists.txt | 3 +
10 files changed, 695 insertions(+), 505 deletions(-)
create mode 100644 lldb/include/lldb/Core/TelemetryVendor.h
create mode 100644 lldb/source/Core/TelemetryVendor.cpp
diff --git a/lldb/include/lldb/Core/PluginManager.h b/lldb/include/lldb/Core/PluginManager.h
index e4e0c3eea67f8c..f17104cfdc796d 100644
--- a/lldb/include/lldb/Core/PluginManager.h
+++ b/lldb/include/lldb/Core/PluginManager.h
@@ -379,6 +379,15 @@ class PluginManager {
const UUID *uuid,
const ArchSpec *arch);
+ // TelemetryVendor
+ static bool RegisterPlugin(llvm::StringRef name, llvm::StringRef description,
+ TelemetryVendorCreateInstance create_callback);
+
+ static bool UnregisterPlugin(TelemetryVendorCreateInstance create_callback);
+
+ static TelemetryVendorCreateInstance
+ GetTelemetryVendorCreateCallbackAtIndex(uint32_t idx);
+
// Trace
static bool RegisterPlugin(
llvm::StringRef name, llvm::StringRef description,
diff --git a/lldb/include/lldb/Core/Telemetry.h b/lldb/include/lldb/Core/Telemetry.h
index 61cdca59f31fc6..3c6118414bb28d 100644
--- a/lldb/include/lldb/Core/Telemetry.h
+++ b/lldb/include/lldb/Core/Telemetry.h
@@ -192,15 +192,15 @@ struct CommandTelemetryInfo : public LldbBaseTelemetryInfo {
std::string ToString() const override;
};
-// The "catch-all" entry to store a set of custom/non-standard
-// data.
+/// The "catch-all" entry to store a set of custom/non-standard
+/// data.
struct MiscTelemetryInfo : public LldbBaseTelemetryInfo {
- // If the event is/can be associated with a target entry,
- // this field contains that target's UUID.
- // <EMPTY> otherwise.
+ /// If the event is/can be associated with a target entry,
+ /// this field contains that target's UUID.
+ /// <EMPTY> otherwise.
std::string target_uuid;
- // Set of key-value pairs for any optional (or impl-specific) data
+ /// Set of key-value pairs for any optional (or impl-specific) data
std::unordered_map<std::string, std::string> meta_data;
MiscTelemetryInfo() = default;
@@ -223,29 +223,39 @@ struct MiscTelemetryInfo : public LldbBaseTelemetryInfo {
std::string ToString() const override;
};
+/// The base Telemeter instance in LLDB.
+/// This class declares additional instrumentation points
+/// applicable to LLDB.
class LldbTelemeter : public llvm::telemetry::Telemeter {
public:
- static std::unique_ptr<LldbTelemeter> CreateInstance(Debugger *);
+ /// Creates an instance of LldbTelemeter.
+ /// This uses the plugin registry to find an instance:
+ /// - If a vendor supplies a implementation, it will use it.
+ /// - If not, it will either return a no-op instance or a basic
+ /// implementation for testing.
+ ///
+ /// See also lldb_private::TelemetryVendor.
+ static std::unique_ptr<LldbTelemeter> CreateInstance(Debugger *debugger);
virtual ~LldbTelemeter() = default;
- // Invoked upon process exit
+ /// Invoked upon process exit
virtual void LogProcessExit(int status, llvm::StringRef exit_string,
llvm::telemetry::EventStats stats,
Target *target_ptr) = 0;
- // Invoked upon loading the main executable module
- // We log in a fire-n-forget fashion so that if the load
- // crashes, we don't lose the entry.
+ /// Invoked upon loading the main executable module
+ /// We log in a fire-n-forget fashion so that if the load
+ /// crashes, we don't lose the entry.
virtual void
LogMainExecutableLoadStart(lldb::ModuleSP exec_mod,
llvm::telemetry::EventStats stats) = 0;
virtual void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod,
llvm::telemetry::EventStats stats) = 0;
- // Invoked for each command
- // We log in a fire-n-forget fashion so that if the command execution
- // crashes, we don't lose the entry.
+ /// Invoked for each command
+ /// We log in a fire-n-forget fashion so that if the command execution
+ /// crashes, we don't lose the entry.
virtual void LogCommandStart(llvm::StringRef uuid,
llvm::StringRef original_command,
llvm::telemetry::EventStats stats,
@@ -258,36 +268,36 @@ class LldbTelemeter : public llvm::telemetry::Telemeter {
virtual std::string GetNextUUID() = 0;
- // For client (eg., SB API) to send telemetry entries.
+ /// For client (eg., SB API) to send telemetry entries.
virtual void
LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) = 0;
};
-// Logger configs: LLDB users can also supply their own configs via:
-// $HOME/.lldb_telemetry_config
-//
-// We can propose simple syntax: <field_name><colon><value>
-// Eg.,
-// enable_telemetry:true
-// destination:stdout
-// destination:stderr
-// destination:/path/to/some/file
-//
-// The allowed field_name values are:
-// * enable_telemetry
-// If the fields are specified more than once, the last line will take
-// precedence If enable_logging is set to false, no logging will occur.
-// * destination.
-// This is allowed to be specified multiple times - it will add to the
-// default (ie, specified by vendor) list of destinations.
-// The value can be either:
-// + one of the two magic values "stdout" or "stderr".
-// + a path to a local file
-// !!NOTE!!: We decided to use a separate file instead of the existing settings
-// file because that file is parsed too late in the process and by the
-// there might have been lots of telemetry-entries that need to be
-// sent already.
-// This approach avoid losing log entries if LLDB crashes during init.
+/// Logger configs: LLDB users can also supply their own configs via:
+/// $HOME/.lldb_telemetry_config
+///
+/// We can propose simple syntax: <field_name><colon><value>
+/// Eg.,
+/// enable_telemetry:true
+/// destination:stdout
+/// destination:stderr
+/// destination:/path/to/some/file
+///
+/// The allowed field_name values are:
+/// * enable_telemetry
+/// If the fields are specified more than once, the last line will take
+/// precedence If enable_logging is set to false, no logging will occur.
+/// * destination.
+/// This is allowed to be specified multiple times - it will add to the
+/// default (ie, specified by vendor) list of destinations.
+/// The value can be either:
+/// + one of the two magic values "stdout" or "stderr".
+/// + a path to a local file
+/// !!NOTE!!: We decided to use a separate file instead of the existing settings
+/// file because that file is parsed too late in the process and by the
+/// there might have been lots of telemetry-entries that need to be
+/// sent already.
+/// This approach avoid losing log entries if LLDB crashes during init.
llvm::telemetry::Config *GetTelemetryConfig();
} // namespace lldb_private
diff --git a/lldb/include/lldb/Core/TelemetryVendor.h b/lldb/include/lldb/Core/TelemetryVendor.h
new file mode 100644
index 00000000000000..f8a57980db7ad5
--- /dev/null
+++ b/lldb/include/lldb/Core/TelemetryVendor.h
@@ -0,0 +1,46 @@
+//===-- TelemetryVendor.h ---------------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_CORE_TELEMETRYVENDOR_H
+#define LLDB_CORE_TELEMETRYVENDOR_H
+
+#include "lldb/Core/PluginInterface.h"
+#include "lldb/Core/Telemetry.h"
+#include "llvm/Telemetry/Telemetry.h"
+
+#include <memory>
+
+namespace lldb_private {
+
+class TelemetryVendor : public PluginInterface {
+public:
+ static TelemetryVendor *FindPlugin();
+
+ TelemetryVendor() = default;
+
+ llvm::StringRef GetPluginName() override;
+
+ std::unique_ptr<llvm::telemetry::Config> GetTelemetryConfig();
+
+ // Creates an LldbTelemeter instance.
+ // Vendor plugins can override this to create customized instance as needed.
+ virtual std::unique_ptr<LldbTelemeter>
+ CreateTelemeter(lldb_private::Debugger *debugger);
+
+ // TODO: move most of the basictelemeter concrete impl here to the plug in (to
+ // its .ccpp file that is)
+protected:
+ // Returns a vendor-specific config which may or may not be the same as the
+ // given "default_config". Downstream implementation can define their
+ // configugrations in addition to OR overriding the default option.
+ virtual std::unique_ptr<llvm::telemetry::Config> GetVendorSpecificConfig(
+ std::unique_ptr<llvm::telemetry::Config> default_config);
+};
+
+} // namespace lldb_private
+#endif // LLDB_CORE_TELEMETRYVENDOR_H
diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h
index 337eff696fcf3f..6efc265870f683 100644
--- a/lldb/include/lldb/lldb-forward.h
+++ b/lldb/include/lldb/lldb-forward.h
@@ -234,6 +234,7 @@ class SystemRuntime;
class Target;
class TargetList;
class TargetProperties;
+class TelemetryVendor;
class Thread;
class ThreadCollection;
class ThreadList;
diff --git a/lldb/include/lldb/lldb-private-interfaces.h b/lldb/include/lldb/lldb-private-interfaces.h
index b3c8cda899b95e..dcf27a3e160fcf 100644
--- a/lldb/include/lldb/lldb-private-interfaces.h
+++ b/lldb/include/lldb/lldb-private-interfaces.h
@@ -129,6 +129,7 @@ typedef bool (*ScriptedInterfaceCreateInstance)(lldb::ScriptLanguage language,
ScriptedInterfaceUsages usages);
typedef int (*ComparisonFunction)(const void *, const void *);
typedef void (*DebuggerInitializeCallback)(Debugger &debugger);
+typedef TelemetryVendor *(*TelemetryVendorCreateInstance)();
/// Trace
/// \{
typedef llvm::Expected<lldb::TraceSP> (*TraceCreateInstanceFromBundle)(
diff --git a/lldb/source/Core/CMakeLists.txt b/lldb/source/Core/CMakeLists.txt
index 4a02f7f1fc85e5..0632c42e78c65f 100644
--- a/lldb/source/Core/CMakeLists.txt
+++ b/lldb/source/Core/CMakeLists.txt
@@ -52,6 +52,7 @@ add_lldb_library(lldbCore
SourceLocationSpec.cpp
SourceManager.cpp
Telemetry.cpp
+ TelemetryVendor.cpp
StreamAsynchronousIO.cpp
ThreadedCommunication.cpp
UserSettingsController.cpp
diff --git a/lldb/source/Core/PluginManager.cpp b/lldb/source/Core/PluginManager.cpp
index fd5cb792c101a4..7b3abb6868f804 100644
--- a/lldb/source/Core/PluginManager.cpp
+++ b/lldb/source/Core/PluginManager.cpp
@@ -1224,6 +1224,33 @@ FileSpec PluginManager::FindSymbolFileInBundle(const FileSpec &symfile_bundle,
return {};
}
+#pragma mark TelemetryVendor
+
+typedef PluginInstance<TelemetryVendorCreateInstance> TelemetryVendorInstance;
+typedef PluginInstances<TelemetryVendorInstance> TelemetryVendorInstances;
+
+static TelemetryVendorInstances &GetTelemetryVendorInstances() {
+ static TelemetryVendorInstances g_instances;
+ return g_instances;
+}
+
+bool PluginManager::RegisterPlugin(
+ llvm::StringRef name, llvm::StringRef description,
+ TelemetryVendorCreateInstance create_callback) {
+ return GetTelemetryVendorInstances().RegisterPlugin(name, description,
+ create_callback);
+}
+
+bool PluginManager::UnregisterPlugin(
+ TelemetryVendorCreateInstance create_callback) {
+ return GetTelemetryVendorInstances().UnregisterPlugin(create_callback);
+}
+
+TelemetryVendorCreateInstance
+PluginManager::GetTelemetryVendorCreateCallbackAtIndex(uint32_t idx) {
+ return GetTelemetryVendorInstances().GetCallbackAtIndex(idx);
+}
+
#pragma mark Trace
struct TraceInstance
diff --git a/lldb/source/Core/Telemetry.cpp b/lldb/source/Core/Telemetry.cpp
index 795065a0e25e0f..9c4ab9dadc7454 100644
--- a/lldb/source/Core/Telemetry.cpp
+++ b/lldb/source/Core/Telemetry.cpp
@@ -8,15 +8,11 @@
//===----------------------------------------------------------------------===//
#include "lldb/Core/Telemetry.h"
-#include <stdbool.h>
-#include <sys/auxv.h>
-
-#include <memory>
-
#include <chrono>
#include <cstdlib>
#include <ctime>
#include <fstream>
+#include <memory>
#include <string>
#include <typeinfo>
#include <utility>
@@ -26,6 +22,7 @@
#include "lldb/API/SBProcess.h"
#include "lldb/Core/Debugger.h"
#include "lldb/Core/Module.h"
+#include "lldb/Core/TelemetryVendor.h"
#include "lldb/Host/FileSystem.h"
#include "lldb/Host/HostInfo.h"
#include "lldb/Interpreter/CommandInterpreter.h"
@@ -51,28 +48,6 @@
#include "llvm/Support/raw_ostream.h"
#include "llvm/Telemetry/Telemetry.h"
-#ifdef HAS_VENDOR_TELEMETRY_PLUGINS
-// TODO: could make this path a build-variable rather than hard-coded header
-// path
-#include "lldb/Core/VendorTelemetryPlugin.h"
-
-namespace vendor_specific {
-
-// Set any additional configurations, as needed.
-extern void ApplyVendorSpecificConfigs(
- llvm::telemetry::TelemetryConfig *config) /* __attribute__((weak))*/;
-
-// Return a copy of the given entry, but with certain fields that are deemed
-// PII risk removed.
-extern std::shared_ptr<llvm::telemetry::TelemetryInfo> SanitizeSensitiveFields(
- const llvm::telemetry::TelemetryInfo *entry) /*__attribute__((weak))*/;
-
-extern std::shared_ptr<lldb_private::LldbTelemeter>
-CreateVendorSpecificTelemeter(
- llvm::telemetry::TelemetryConfig *config) /*__attribute__((weak))*/;
-} // namespace vendor_specific
-#endif
-
namespace lldb_private {
using ::llvm::telemetry::Destination;
@@ -81,7 +56,8 @@ using ::llvm::telemetry::ExitDescription;
using ::llvm::telemetry::SteadyTimePoint;
using ::llvm::telemetry::TelemetryInfo;
-static std::string ExitDescToString(const ExitDescription *desc) {
+static std::string
+ExitDescToString(const llvm::telemetry::ExitDescription *desc) {
return ("ExitCode:" + desc->ExitCode) +
(" ExixitDescription: " + desc->Description);
}
@@ -282,446 +258,16 @@ llvm::json::Object MiscTelemetryInfo::serializeToJson() const {
}};
}
-class StreamTelemetryDestination : public Destination {
-public:
- StreamTelemetryDestination(llvm::raw_ostream &os, std::string desc)
- : os(os), desc(desc) {}
- llvm::Error emitEntry(const llvm::telemetry::TelemetryInfo *entry) override {
- // Unless there exists a custom (vendor-defined) data-cleanup
- // for printing, upstream Telemetry should not leak anything other than the
- // basic.
-#ifdef HAS_TELEMETRY_FIELDS_PRINTER
- os << SanitizeSensitiveFields(entry)->ToString() << "\n";
-#else
- os << "session_uuid: " << entry->SessionId
- << "<the rest is omitted due to PII risk>\n";
-#endif
- os.flush();
- return llvm::ErrorSuccess();
- }
-
- std::string name() const override { return desc; }
-
-private:
- llvm::raw_ostream &os;
- const std::string desc;
-};
-
-// No-op logger to use when users disable telemetry
-class NoOpTelemeter : public LldbTelemeter {
-public:
- static std::unique_ptr<LldbTelemeter> CreateInstance(Debugger *debugger) {
- return std::unique_ptr<LldbTelemeter>(new NoOpTelemeter(debugger));
- }
-
- NoOpTelemeter(Debugger *debugger) {}
- void logStartup(llvm::StringRef tool_path, TelemetryInfo *entry) override {}
- void logExit(llvm::StringRef tool_path, TelemetryInfo *entry) override {}
-
- void LogProcessExit(int status, llvm::StringRef exit_string, EventStats stats,
- Target *target_ptr) override {}
- void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod,
- EventStats stats) override {}
- void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod,
- EventStats stats) override {}
-
- void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command,
- EventStats stats, Target *target_ptr) override {}
- void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name,
- llvm::StringRef command_args, EventStats stats,
- Target *target_ptr, CommandReturnObject *result) override {
- }
-
- void
- LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override {}
-
- void addDestination(llvm::telemetry::Destination *destination) override {}
- std::string GetNextUUID() override { return ""; }
-};
-
-class BasicTelemeter : public LldbTelemeter {
-public:
- static std::unique_ptr<BasicTelemeter> CreateInstance(Debugger *);
-
- virtual ~BasicTelemeter() = default;
-
- void logStartup(llvm::StringRef lldb_path, TelemetryInfo *entry) override;
- void logExit(llvm::StringRef lldb_path, TelemetryInfo *entry) override;
-
- void LogProcessExit(int status, llvm::StringRef exit_string, EventStats stats,
- Target *target_ptr) override;
- void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod,
- EventStats stats) override;
- void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod,
- EventStats stats) override;
-
- void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command,
- EventStats stats, Target *target_ptr) override;
- void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name,
- llvm::StringRef command_args, EventStats stats,
- Target *target_ptr, CommandReturnObject *result) override;
-
- void
- LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override;
-
- void addDestination(Destination *destination) override {
- m_destinations.push_back(destination);
- }
-
- std::string GetNextUUID() override {
- return std::to_string(uuid_seed.fetch_add(1));
- }
-
-protected:
- BasicTelemeter(Debugger *debugger);
-
- void CollectMiscBuildInfo();
-
-private:
- template <typename EntrySubType> EntrySubType MakeBaseEntry() {
- EntrySubType entry;
- entry.SessionId = m_session_uuid;
- entry.Counter = counter.fetch_add(1);
- return entry;
- }
-
- void EmitToDestinations(const TelemetryInfo *entry);
-
- Debugger *m_debugger;
- const std::string m_session_uuid;
- std::string startup_lldb_path;
-
- // counting number of entries.
- std::atomic<size_t> counter = 0;
-
- std::vector<Destination *> m_destinations;
-
- std::atomic<size_t> uuid_seed = 0;
-};
-
-static std::string MakeUUID(lldb_private::Debugger *debugger) {
- std::string ret;
- uint8_t random_bytes[16];
- if (auto ec = llvm::getRandomBytes(random_bytes, 16)) {
- LLDB_LOG(GetLog(LLDBLog::Object),
- "Failed to generate random bytes for UUID: {0}", ec.message());
- // fallback to using timestamp + debugger ID.
- ret = std::to_string(
- std::chrono::steady_clock::now().time_since_epoch().count()) +
- "_" + std::to_string(debugger->GetID());
- } else {
- ret = lldb_private::UUID(random_bytes).GetAsString();
- }
-
- return ret;
-}
-
-BasicTelemeter::BasicTelemeter(lldb_private::Debugger *debugger)
- : m_debugger(debugger), m_session_uuid(MakeUUID(debugger)) {}
-
-std::unique_ptr<BasicTelemeter>
-BasicTelemeter::CreateInstance(lldb_private::Debugger *debugger) {
- auto *config = GetTelemetryConfig();
-
- BasicTelemeter *ins = new BasicTelemeter(debugger);
- for (const std ::string &dest : config->AdditionalDestinations) {
- if (dest == "stdout") {
- ins->addDestination(
- new StreamTelemetryDestination(llvm::outs(), "stdout"));
- } else if (dest == "stderr") {
- ins->addDestination(
- new StreamTelemetryDestination(llvm::errs(), "stderr"));
- } else {
- // TODO: handle custom values as needed?
- }
- }
-
- return std::unique_ptr<BasicTelemeter>(ins);
-}
-
-void BasicTelemeter::EmitToDestinations(const TelemetryInfo *entry) {
- // TODO: can do this in a separate thread (need to own the ptrs!).
- for (Destination *destination : m_destinations) {
- llvm::Error err = destination->emitEntry(entry);
- if (err) {
- LLDB_LOG(GetLog(LLDBLog::Object),
- "Error emitting to destination(name = {0})",
- destination->name());
- }
- }
-}
-
-void BasicTelemeter::logStartup(llvm::StringRef lldb_path,
- TelemetryInfo *entry) {
- startup_lldb_path = lldb_path.str();
- lldb_private::DebuggerTelemetryInfo startup_info =
- MakeBaseEntry<DebuggerTelemetryInfo>();
-
- UserIDResolver &resolver = lldb_private::HostInfo::GetUserIDResolver();
- std::optional<llvm::StringRef> opt_username =
- resolver.GetUserName(lldb_private::HostInfo::GetUserID());
- if (opt_username)
- startup_info.username = *opt_username;
-
- startup_info.lldb_git_sha =
- lldb_private::GetVersion(); // TODO: find the real git sha
- startup_info.lldb_path = startup_lldb_path;
- startup_info.Stats = entry->Stats;
-
- llvm::SmallString<64> cwd;
- if (!llvm::sys::fs::current_path(cwd)) {
- startup_info.cwd = cwd.c_str();
- } else {
- MiscTelemetryInfo misc_info = MakeBaseEntry<MiscTelemetryInfo>();
- misc_info.meta_data["internal_errors"] = "Cannot determine CWD";
- EmitToDestinations(&misc_info);
- }
-
- EmitToDestinations(&startup_info);
-
- // Optional part
- CollectMiscBuildInfo();
-}
-
-void BasicTelemeter::logExit(llvm::StringRef lldb_path, TelemetryInfo *entry) {
- // we should be shutting down the same instance that we started?!
- // llvm::Assert(startup_lldb_path == lldb_path.str());
-
- lldb_private::DebuggerTelemetryInfo exit_info =
- MakeBaseEntry<lldb_private::DebuggerTelemetryInfo>();
- exit_info.Stats = entry->Stats;
- exit_info.lldb_path = startup_lldb_path;
- if (auto *selected_target =
- m_debugger->GetSelectedExecutionContext().GetTargetPtr()) {
- if (!selected_target->IsDummyTarget()) {
- const lldb::ProcessSP proc = selected_target->GetProcessSP();
- if (proc == nullptr) {
- // no process has been launched yet.
- exit_info.ExitDesc = {-1, "no process launched."};
- } else {
- exit_info.ExitDesc = {proc->GetExitStatus(), ""};
- if (const char *description = proc->GetExitDescription())
- exit_info.ExitDesc->Description = std::string(description);
- }
- }
- }
- EmitToDestinations(&exit_info);
-}
-
-void BasicTelemeter::LogProcessExit(int status, llvm::StringRef exit_string,
- EventStats stats, Target *target_ptr) {
- lldb_private::TargetTelemetryInfo exit_info =
- MakeBaseEntry<TargetTelemetryInfo>();
- exit_info.Stats = std::move(stats);
- exit_info.target_uuid =
- target_ptr && !target_ptr->IsDummyTarget()
- ? target_ptr->GetExecutableModule()->GetUUID().GetAsString()
- : "";
- exit_info.ExitDesc = {status, exit_string.str()};
-
- EmitToDestinations(&exit_info);
-}
-
-void BasicTelemeter::CollectMiscBuildInfo() {
- // collecting use-case specific data
-}
-
-void BasicTelemeter::LogMainExecutableLoadStart(lldb::ModuleSP exec_mod,
- EventStats stats) {
- TargetTelemetryInfo target_info = MakeBaseEntry<TargetTelemetryInfo>();
- target_info.Stats = std::move(stats);
- target_info.binary_path =
- exec_mod->GetFileSpec().GetPathAsConstString().GetCString();
- target_info.file_format = exec_mod->GetArchitecture().GetArchitectureName();
- target_info.target_uuid = exec_mod->GetUUID().GetAsString();
- if (auto err = llvm::sys::fs::file_size(exec_mod->GetFileSpec().GetPath(),
- target_info.binary_size)) {
- // If there was error obtaining it, just reset the size to 0.
- // Maybe log the error too?
- target_info.binary_size = 0;
- }
- EmitToDestinations(&target_info);
-}
-
-void BasicTelemeter::LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod,
- EventStats stats) {
- TargetTelemetryInfo target_info = MakeBaseEntry<TargetTelemetryInfo>();
- target_info.Stats = std::move(stats);
- target_info.binary_path =
- exec_mod->GetFileSpec().GetPathAsConstString().GetCString();
- target_info.file_format = exec_mod->GetArchitecture().GetArchitectureName();
- target_info.target_uuid = exec_mod->GetUUID().GetAsString();
- target_info.binary_size = exec_mod->GetObjectFile()->GetByteSize();
-
- EmitToDestinations(&target_info);
-
- // Collect some more info, might be useful?
- MiscTelemetryInfo misc_info = MakeBaseEntry<MiscTelemetryInfo>();
- misc_info.target_uuid = exec_mod->GetUUID().GetAsString();
- misc_info.meta_data["symtab_index_time"] =
- std::to_string(exec_mod->GetSymtabIndexTime().get().count());
- misc_info.meta_data["symtab_parse_time"] =
- std::to_string(exec_mod->GetSymtabParseTime().get().count());
- EmitToDestinations(&misc_info);
-}
-
-void BasicTelemeter::LogClientTelemetry(
- const lldb_private::StructuredDataImpl &entry) {
- // TODO: pull the dictionary out of entry
- ClientTelemetryInfo client_info = MakeBaseEntry<ClientTelemetryInfo>();
- /*
- std::optional<llvm::StringRef> request_name = entry.getString("request_name");
- if (!request_name.has_value()) {
- MiscTelemetryInfo misc_info = MakeBaseEntry<MiscTelemetryInfo>();
- misc_info.meta_data["internal_errors"] =
- "Cannot determine request name from client entry";
- // TODO: Dump the errornous entry to stderr too?
- EmitToDestinations(&misc_info);
- return;
- }
- client_info.request_name = request_name->str();
-
- std::optional<int64_t> start_time = entry.getInteger("start_time");
- std::optional<int64_t> end_time = entry.getInteger("end_time");
-
- if (!start_time.has_value() || !end_time.has_value()) {
- MiscTelemetryInfo misc_info = MakeBaseEntry<MiscTelemetryInfo>();
- misc_info.meta_data["internal_errors"] =
- "Cannot determine start/end time from client entry";
- EmitToDestinations(&misc_info);
- return;
- }
-
- SteadyTimePoint epoch;
- client_info.Stats.Start =
- epoch + std::chrono::nanoseconds(static_cast<size_t>(*start_time));
- client_info.Stats.End =
- epoch + std::chrono::nanoseconds(static_cast<size_t>(*end_time));
-
- std::optional<llvm::StringRef> error_msg = entry.getString("error");
- if (error_msg.has_value())
- client_info.error_msg = error_msg->str();
- */
-
- EmitToDestinations(&client_info);
-}
-
-void BasicTelemeter::LogCommandStart(llvm::StringRef uuid,
- llvm::StringRef original_command,
- EventStats stats, Target *target_ptr) {
-
- lldb_private::CommandTelemetryInfo command_info =
- MakeBaseEntry<CommandTelemetryInfo>();
-
- // If we have a target attached to this command, then get the UUID.
- command_info.target_uuid = "";
- if (target_ptr && target_ptr->GetExecutableModule() != nullptr) {
- command_info.target_uuid =
- target_ptr->GetExecutableModule()->GetUUID().GetAsString();
- }
- command_info.command_uuid = uuid.str();
- command_info.original_command = original_command.str();
- command_info.Stats = std::move(stats);
-
- EmitToDestinations(&command_info);
-}
-
-void BasicTelemeter::LogCommandEnd(llvm::StringRef uuid,
- llvm::StringRef command_name,
- llvm::StringRef command_args,
- EventStats stats, Target *target_ptr,
- CommandReturnObject *result) {
-
- lldb_private::CommandTelemetryInfo command_info =
- MakeBaseEntry<CommandTelemetryInfo>();
-
- // If we have a target attached to this command, then get the UUID.
- command_info.target_uuid = "";
- if (target_ptr && target_ptr->GetExecutableModule() != nullptr) {
- command_info.target_uuid =
- target_ptr->GetExecutableModule()->GetUUID().GetAsString();
- }
- command_info.command_uuid = uuid.str();
- command_info.command_name = command_name.str();
- command_info.args = command_args.str();
- command_info.Stats = std::move(stats);
- command_info.ExitDesc = {result->Succeeded() ? 0 : -1, ""};
- if (llvm::StringRef error_data = result->GetErrorData();
- !error_data.empty()) {
- command_info.ExitDesc->Description = error_data.str();
- }
- command_info.ret_status = result->GetStatus();
- EmitToDestinations(&command_info);
-}
-
-llvm::StringRef parse_value(llvm::StringRef str, llvm::StringRef label) {
- return str.substr(label.size()).trim();
-}
-
-bool parse_field(llvm::StringRef str, llvm::StringRef label) {
- if (parse_value(str, label) == "true")
- return true;
- return false;
-}
-
-llvm::telemetry::Config *MakeTelemetryConfig() {
- bool enable_telemetry = false;
- std::vector<std::string> additional_destinations;
-
- // Look in the $HOME/.lldb_telemetry_config file to populate the struct
- llvm::SmallString<64> init_file;
- FileSystem::Instance().GetHomeDirectory(init_file);
- llvm::sys::path::append(init_file, ".lldb_telemetry_config");
- FileSystem::Instance().Resolve(init_file);
- if (llvm::sys::fs::exists(init_file)) {
- auto contents = llvm::MemoryBuffer::getFile(init_file, /*IsText*/ true);
- if (contents) {
- llvm::line_iterator iter =
- llvm::line_iterator(contents->get()->getMemBufferRef());
- for (; !iter.is_at_eof(); ++iter) {
- if (iter->starts_with("enable_telemetry:")) {
- enable_telemetry = parse_field(*iter, "enable_telemetry:");
- } else if (iter->starts_with("destination:")) {
- llvm::StringRef dest = parse_value(*iter, "destination:");
- if (dest == "stdout") {
- additional_destinations.push_back("stdout");
- } else if (dest == "stderr") {
- additional_destinations.push_back("stderr");
- } else {
- additional_destinations.push_back(dest.str());
- }
- }
- }
- } else {
- LLDB_LOG(GetLog(LLDBLog::Object), "Error reading config file at {0}",
- init_file.c_str());
- }
- }
-
- auto *ret =
- new llvm::telemetry::Config{enable_telemetry, additional_destinations};
-#ifdef HAS_VENDOR_TELEMETRY_PLUGINS
- vendor_specific::ApplyVendorSpecificConfigs(ret);
-#endif
- return ret;
-}
-
-llvm::telemetry::Config *GetTelemetryConfig() {
- static llvm::telemetry::Config *config = MakeTelemetryConfig();
- return config;
-}
-
std::unique_ptr<LldbTelemeter>
LldbTelemeter::CreateInstance(lldb_private::Debugger *debugger) {
- auto *config = GetTelemetryConfig();
- if (!config->EnableTelemetry) {
- return NoOpTelemeter::CreateInstance(debugger);
+ // TODO: do we cache the plugin?
+ TelemetryVendor *vendor = TelemetryVendor::FindPlugin();
+ if (vendor == nullptr) {
+ LLDB_LOG(GetLog(LLDBLog::Object),
+ "Failed to find a TelemetryVendor plugin instance");
+ return nullptr;
}
-#ifdef HAS_VENDOR_TELEMETRY_PLUGINS
- return vendor_specific::CreateVendorSpecificTelemeter(config);
-#else
- return BasicTelemeter::CreateInstance(debugger);
-#endif
+ return vendor->CreateTelemeter(debugger);
}
} // namespace lldb_private
diff --git a/lldb/source/Core/TelemetryVendor.cpp b/lldb/source/Core/TelemetryVendor.cpp
new file mode 100644
index 00000000000000..fa40ee2ad045e1
--- /dev/null
+++ b/lldb/source/Core/TelemetryVendor.cpp
@@ -0,0 +1,546 @@
+//===-- TelemetryVendor.cpp -------------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "lldb/Core/TelemetryVendor.h"
+
+#include <chrono>
+#include <cstdlib>
+#include <ctime>
+#include <fstream>
+#include <memory>
+#include <string>
+#include <typeinfo>
+#include <utility>
+#include <vector>
+
+#include "lldb/API/SBDebugger.h"
+#include "lldb/API/SBProcess.h"
+#include "lldb/Core/Debugger.h"
+#include "lldb/Core/Module.h"
+#include "lldb/Core/PluginManager.h"
+#include "lldb/Core/Telemetry.h"
+#include "lldb/Host/FileSystem.h"
+#include "lldb/Host/HostInfo.h"
+#include "lldb/Interpreter/CommandInterpreter.h"
+#include "lldb/Target/Process.h"
+#include "lldb/Target/Statistics.h"
+#include "lldb/Utility/ConstString.h"
+#include "lldb/Utility/FileSpec.h"
+#include "lldb/Utility/LLDBLog.h"
+#include "lldb/Utility/UUID.h"
+#include "lldb/Version/Version.h"
+#include "lldb/lldb-enumerations.h"
+#include "lldb/lldb-forward.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallString.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/ADT/Twine.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/JSON.h"
+#include "llvm/Support/LineIterator.h"
+#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/Path.h"
+#include "llvm/Support/RandomNumberGenerator.h"
+#include "llvm/Support/raw_ostream.h"
+#include "llvm/Telemetry/Telemetry.h"
+
+using namespace lldb;
+using namespace lldb_private;
+
+namespace {
+
+using ::llvm::telemetry::Destination;
+using ::llvm::telemetry::EventStats;
+using ::llvm::telemetry::ExitDescription;
+using ::llvm::telemetry::SteadyTimePoint;
+using ::llvm::telemetry::TelemetryInfo;
+
+// No-op logger to use when users disable telemetry
+class NoOpTelemeter : public LldbTelemeter {
+public:
+ static std::unique_ptr<LldbTelemeter> CreateInstance(Debugger *debugger) {
+ return std::unique_ptr<LldbTelemeter>(new NoOpTelemeter(debugger));
+ }
+
+ NoOpTelemeter(Debugger *debugger) {}
+ void logStartup(llvm::StringRef tool_path, TelemetryInfo *entry) override {}
+ void logExit(llvm::StringRef tool_path, TelemetryInfo *entry) override {}
+
+ void LogProcessExit(int status, llvm::StringRef exit_string, EventStats stats,
+ Target *target_ptr) override {}
+ void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod,
+ EventStats stats) override {}
+ void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod,
+ EventStats stats) override {}
+
+ void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command,
+ EventStats stats, Target *target_ptr) override {}
+ void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name,
+ llvm::StringRef command_args, EventStats stats,
+ Target *target_ptr, CommandReturnObject *result) override {
+ }
+
+ void
+ LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override {}
+
+ void addDestination(llvm::telemetry::Destination *destination) override {}
+ std::string GetNextUUID() override { return ""; }
+};
+
+class BasicTelemeter : public LldbTelemeter {
+public:
+ static std::unique_ptr<BasicTelemeter>
+ CreateInstance(std::unique_ptr<llvm::telemetry::Config> config,
+ Debugger *debugger);
+
+ virtual ~BasicTelemeter() = default;
+
+ void logStartup(llvm::StringRef lldb_path, TelemetryInfo *entry) override;
+ void logExit(llvm::StringRef lldb_path, TelemetryInfo *entry) override;
+
+ void LogProcessExit(int status, llvm::StringRef exit_string, EventStats stats,
+ Target *target_ptr) override;
+ void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod,
+ EventStats stats) override;
+ void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod,
+ EventStats stats) override;
+
+ void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command,
+ EventStats stats, Target *target_ptr) override;
+ void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name,
+ llvm::StringRef command_args, EventStats stats,
+ Target *target_ptr, CommandReturnObject *result) override;
+
+ void
+ LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override;
+
+ void addDestination(Destination *destination) override {
+ m_destinations.push_back(destination);
+ }
+
+ std::string GetNextUUID() override {
+ return std::to_string(uuid_seed.fetch_add(1));
+ }
+
+protected:
+ BasicTelemeter(std::unique_ptr<llvm::telemetry::Config> config,
+ Debugger *debugger);
+
+ void CollectMiscBuildInfo();
+
+private:
+ template <typename EntrySubType> EntrySubType MakeBaseEntry() {
+ EntrySubType entry;
+ entry.SessionId = m_session_uuid;
+ entry.Counter = counter.fetch_add(1);
+ return entry;
+ }
+
+ void EmitToDestinations(const TelemetryInfo *entry);
+
+ std::unique_ptr<llvm::telemetry::Config> m_config;
+ Debugger *m_debugger;
+ const std::string m_session_uuid;
+ std::string startup_lldb_path;
+
+ // counting number of entries.
+ std::atomic<size_t> counter = 0;
+
+ std::vector<Destination *> m_destinations;
+
+ std::atomic<size_t> uuid_seed = 0;
+};
+
+class StreamTelemetryDestination : public Destination {
+public:
+ StreamTelemetryDestination(llvm::raw_ostream &os, std::string desc)
+ : os(os), desc(desc) {}
+ llvm::Error emitEntry(const llvm::telemetry::TelemetryInfo *entry) override {
+ // Upstream Telemetry should not leak anything other than the
+ // basic data, unless running in test mode.
+#ifdef TEST_TELEMETRY
+ os << entry->ToString() << "\n";
+#else
+ os << "session_uuid: " << entry->SessionId
+ << "<the rest is omitted due to PII risk>\n";
+#endif
+ os.flush();
+ return llvm::ErrorSuccess();
+ }
+
+ std::string name() const override { return desc; }
+
+private:
+ llvm::raw_ostream &os;
+ const std::string desc;
+};
+
+static std::string MakeUUID(lldb_private::Debugger *debugger) {
+ std::string ret;
+ uint8_t random_bytes[16];
+ if (auto ec = llvm::getRandomBytes(random_bytes, 16)) {
+ LLDB_LOG(GetLog(LLDBLog::Object),
+ "Failed to generate random bytes for UUID: {0}", ec.message());
+ // fallback to using timestamp + debugger ID.
+ ret = std::to_string(
+ std::chrono::steady_clock::now().time_since_epoch().count()) +
+ "_" + std::to_string(debugger->GetID());
+ } else {
+ ret = lldb_private::UUID(random_bytes).GetAsString();
+ }
+
+ return ret;
+}
+
+BasicTelemeter::BasicTelemeter(std::unique_ptr<llvm::telemetry::Config> config,
+ lldb_private::Debugger *debugger)
+ : m_config(std::move(config)), m_debugger(debugger),
+ m_session_uuid(MakeUUID(debugger)) {}
+
+std::unique_ptr<BasicTelemeter>
+BasicTelemeter::CreateInstance(std::unique_ptr<llvm::telemetry::Config> config,
+ lldb_private::Debugger *debugger) {
+
+ BasicTelemeter *ins = new BasicTelemeter(std::move(config), debugger);
+ for (const std ::string &dest : config->AdditionalDestinations) {
+ if (dest == "stdout") {
+ ins->addDestination(
+ new StreamTelemetryDestination(llvm::outs(), "stdout"));
+ } else if (dest == "stderr") {
+ ins->addDestination(
+ new StreamTelemetryDestination(llvm::errs(), "stderr"));
+ } else {
+ // TODO: handle custom values as needed?
+ }
+ }
+
+ return std::unique_ptr<BasicTelemeter>(ins);
+}
+
+void BasicTelemeter::EmitToDestinations(const TelemetryInfo *entry) {
+ // TODO: can do this in a separate thread (need to own the ptrs!).
+ for (Destination *destination : m_destinations) {
+ llvm::Error err = destination->emitEntry(entry);
+ if (err) {
+ LLDB_LOG(GetLog(LLDBLog::Object),
+ "Error emitting to destination(name = {0})",
+ destination->name());
+ }
+ }
+}
+
+void BasicTelemeter::logStartup(llvm::StringRef lldb_path,
+ TelemetryInfo *entry) {
+ startup_lldb_path = lldb_path.str();
+ lldb_private::DebuggerTelemetryInfo startup_info =
+ MakeBaseEntry<DebuggerTelemetryInfo>();
+
+ UserIDResolver &resolver = lldb_private::HostInfo::GetUserIDResolver();
+ std::optional<llvm::StringRef> opt_username =
+ resolver.GetUserName(lldb_private::HostInfo::GetUserID());
+ if (opt_username)
+ startup_info.username = *opt_username;
+
+ startup_info.lldb_git_sha =
+ lldb_private::GetVersion(); // TODO: find the real git sha
+ startup_info.lldb_path = startup_lldb_path;
+ startup_info.Stats = entry->Stats;
+
+ llvm::SmallString<64> cwd;
+ if (!llvm::sys::fs::current_path(cwd)) {
+ startup_info.cwd = cwd.c_str();
+ } else {
+ MiscTelemetryInfo misc_info = MakeBaseEntry<MiscTelemetryInfo>();
+ misc_info.meta_data["internal_errors"] = "Cannot determine CWD";
+ EmitToDestinations(&misc_info);
+ }
+
+ EmitToDestinations(&startup_info);
+
+ // Optional part
+ CollectMiscBuildInfo();
+}
+
+void BasicTelemeter::logExit(llvm::StringRef lldb_path, TelemetryInfo *entry) {
+ // we should be shutting down the same instance that we started?!
+ // llvm::Assert(startup_lldb_path == lldb_path.str());
+
+ lldb_private::DebuggerTelemetryInfo exit_info =
+ MakeBaseEntry<lldb_private::DebuggerTelemetryInfo>();
+ exit_info.Stats = entry->Stats;
+ exit_info.lldb_path = startup_lldb_path;
+ if (auto *selected_target =
+ m_debugger->GetSelectedExecutionContext().GetTargetPtr()) {
+ if (!selected_target->IsDummyTarget()) {
+ const lldb::ProcessSP proc = selected_target->GetProcessSP();
+ if (proc == nullptr) {
+ // no process has been launched yet.
+ exit_info.ExitDesc = {-1, "no process launched."};
+ } else {
+ exit_info.ExitDesc = {proc->GetExitStatus(), ""};
+ if (const char *description = proc->GetExitDescription())
+ exit_info.ExitDesc->Description = std::string(description);
+ }
+ }
+ }
+ EmitToDestinations(&exit_info);
+}
+
+void BasicTelemeter::LogProcessExit(int status, llvm::StringRef exit_string,
+ EventStats stats, Target *target_ptr) {
+ lldb_private::TargetTelemetryInfo exit_info =
+ MakeBaseEntry<TargetTelemetryInfo>();
+ exit_info.Stats = std::move(stats);
+ exit_info.target_uuid =
+ target_ptr && !target_ptr->IsDummyTarget()
+ ? target_ptr->GetExecutableModule()->GetUUID().GetAsString()
+ : "";
+ exit_info.ExitDesc = {status, exit_string.str()};
+
+ EmitToDestinations(&exit_info);
+}
+
+void BasicTelemeter::CollectMiscBuildInfo() {
+ // collecting use-case specific data
+}
+
+void BasicTelemeter::LogMainExecutableLoadStart(lldb::ModuleSP exec_mod,
+ EventStats stats) {
+ TargetTelemetryInfo target_info = MakeBaseEntry<TargetTelemetryInfo>();
+ target_info.Stats = std::move(stats);
+ target_info.binary_path =
+ exec_mod->GetFileSpec().GetPathAsConstString().GetCString();
+ target_info.file_format = exec_mod->GetArchitecture().GetArchitectureName();
+ target_info.target_uuid = exec_mod->GetUUID().GetAsString();
+ if (auto err = llvm::sys::fs::file_size(exec_mod->GetFileSpec().GetPath(),
+ target_info.binary_size)) {
+ // If there was error obtaining it, just reset the size to 0.
+ // Maybe log the error too?
+ target_info.binary_size = 0;
+ }
+ EmitToDestinations(&target_info);
+}
+
+void BasicTelemeter::LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod,
+ EventStats stats) {
+ TargetTelemetryInfo target_info = MakeBaseEntry<TargetTelemetryInfo>();
+ target_info.Stats = std::move(stats);
+ target_info.binary_path =
+ exec_mod->GetFileSpec().GetPathAsConstString().GetCString();
+ target_info.file_format = exec_mod->GetArchitecture().GetArchitectureName();
+ target_info.target_uuid = exec_mod->GetUUID().GetAsString();
+ target_info.binary_size = exec_mod->GetObjectFile()->GetByteSize();
+
+ EmitToDestinations(&target_info);
+
+ // Collect some more info, might be useful?
+ MiscTelemetryInfo misc_info = MakeBaseEntry<MiscTelemetryInfo>();
+ misc_info.target_uuid = exec_mod->GetUUID().GetAsString();
+ misc_info.meta_data["symtab_index_time"] =
+ std::to_string(exec_mod->GetSymtabIndexTime().get().count());
+ misc_info.meta_data["symtab_parse_time"] =
+ std::to_string(exec_mod->GetSymtabParseTime().get().count());
+ EmitToDestinations(&misc_info);
+}
+
+void BasicTelemeter::LogClientTelemetry(
+ const lldb_private::StructuredDataImpl &entry) {
+ // TODO: pull the dictionary out of entry
+ ClientTelemetryInfo client_info = MakeBaseEntry<ClientTelemetryInfo>();
+ /*
+ std::optional<llvm::StringRef> request_name = entry.getString("request_name");
+ if (!request_name.has_value()) {
+ MiscTelemetryInfo misc_info = MakeBaseEntry<MiscTelemetryInfo>();
+ misc_info.meta_data["internal_errors"] =
+ "Cannot determine request name from client entry";
+ // TODO: Dump the errornous entry to stderr too?
+ EmitToDestinations(&misc_info);
+ return;
+ }
+ client_info.request_name = request_name->str();
+
+ std::optional<int64_t> start_time = entry.getInteger("start_time");
+ std::optional<int64_t> end_time = entry.getInteger("end_time");
+
+ if (!start_time.has_value() || !end_time.has_value()) {
+ MiscTelemetryInfo misc_info = MakeBaseEntry<MiscTelemetryInfo>();
+ misc_info.meta_data["internal_errors"] =
+ "Cannot determine start/end time from client entry";
+ EmitToDestinations(&misc_info);
+ return;
+ }
+
+ SteadyTimePoint epoch;
+ client_info.Stats.Start =
+ epoch + std::chrono::nanoseconds(static_cast<size_t>(*start_time));
+ client_info.Stats.End =
+ epoch + std::chrono::nanoseconds(static_cast<size_t>(*end_time));
+
+ std::optional<llvm::StringRef> error_msg = entry.getString("error");
+ if (error_msg.has_value())
+ client_info.error_msg = error_msg->str();
+ */
+
+ EmitToDestinations(&client_info);
+}
+
+void BasicTelemeter::LogCommandStart(llvm::StringRef uuid,
+ llvm::StringRef original_command,
+ EventStats stats, Target *target_ptr) {
+
+ lldb_private::CommandTelemetryInfo command_info =
+ MakeBaseEntry<CommandTelemetryInfo>();
+
+ // If we have a target attached to this command, then get the UUID.
+ command_info.target_uuid = "";
+ if (target_ptr && target_ptr->GetExecutableModule() != nullptr) {
+ command_info.target_uuid =
+ target_ptr->GetExecutableModule()->GetUUID().GetAsString();
+ }
+ command_info.command_uuid = uuid.str();
+ command_info.original_command = original_command.str();
+ command_info.Stats = std::move(stats);
+
+ EmitToDestinations(&command_info);
+}
+
+void BasicTelemeter::LogCommandEnd(llvm::StringRef uuid,
+ llvm::StringRef command_name,
+ llvm::StringRef command_args,
+ EventStats stats, Target *target_ptr,
+ CommandReturnObject *result) {
+
+ lldb_private::CommandTelemetryInfo command_info =
+ MakeBaseEntry<CommandTelemetryInfo>();
+
+ // If we have a target attached to this command, then get the UUID.
+ command_info.target_uuid = "";
+ if (target_ptr && target_ptr->GetExecutableModule() != nullptr) {
+ command_info.target_uuid =
+ target_ptr->GetExecutableModule()->GetUUID().GetAsString();
+ }
+ command_info.command_uuid = uuid.str();
+ command_info.command_name = command_name.str();
+ command_info.args = command_args.str();
+ command_info.Stats = std::move(stats);
+ command_info.ExitDesc = {result->Succeeded() ? 0 : -1, ""};
+ if (llvm::StringRef error_data = result->GetErrorData();
+ !error_data.empty()) {
+ command_info.ExitDesc->Description = error_data.str();
+ }
+ command_info.ret_status = result->GetStatus();
+ EmitToDestinations(&command_info);
+}
+
+} // namespace
+
+TelemetryVendor *TelemetryVendor::FindPlugin() {
+ // The default implementation (ie., upstream impl) returns
+ // the basic instance.
+ //
+ // Vendors can provide their plugins as needed.
+
+ std::unique_ptr<TelemetryVendor> instance_up;
+ TelemetryVendorCreateInstance create_callback;
+
+ for (size_t idx = 0;
+ (create_callback =
+ PluginManager::GetTelemetryVendorCreateCallbackAtIndex(idx)) !=
+ nullptr;
+ ++idx) {
+ instance_up.reset(create_callback());
+
+ if (instance_up) {
+ return instance_up.release();
+ }
+ }
+
+ return new TelemetryVendor();
+}
+
+llvm::StringRef TelemetryVendor::GetPluginName() {
+ return "DefaultTelemetryVendor";
+}
+
+static llvm::StringRef ParseValue(llvm::StringRef str, llvm::StringRef label) {
+ return str.substr(label.size()).trim();
+}
+
+static bool ParseBoolValue(llvm::StringRef str, llvm::StringRef label) {
+ if (ParseValue(str, label) == "true")
+ return true;
+ return false;
+}
+
+std::unique_ptr<llvm::telemetry::Config> TelemetryVendor::GetTelemetryConfig() {
+ // Telemetry is disabled by default.
+ bool enable_telemetry = false;
+ std::vector<std::string> additional_destinations;
+
+ // Look in the $HOME/.lldb_telemetry_config file to populate the struct
+ llvm::SmallString<64> init_file;
+ FileSystem::Instance().GetHomeDirectory(init_file);
+ llvm::sys::path::append(init_file, ".lldb_telemetry_config");
+ FileSystem::Instance().Resolve(init_file);
+ if (llvm::sys::fs::exists(init_file)) {
+ auto contents = llvm::MemoryBuffer::getFile(init_file, /*IsText*/ true);
+ if (contents) {
+ llvm::line_iterator iter =
+ llvm::line_iterator(contents->get()->getMemBufferRef());
+ for (; !iter.is_at_eof(); ++iter) {
+ if (iter->starts_with("enable_telemetry:")) {
+ enable_telemetry = ParseBoolValue(*iter, "enable_telemetry:");
+ } else if (iter->starts_with("destination:")) {
+ llvm::StringRef dest = ParseValue(*iter, "destination:");
+ if (dest == "stdout") {
+ additional_destinations.push_back("stdout");
+ } else if (dest == "stderr") {
+ additional_destinations.push_back("stderr");
+ } else {
+ additional_destinations.push_back(dest.str());
+ }
+ }
+ }
+ } else {
+ LLDB_LOG(GetLog(LLDBLog::Object), "Error reading config file at {0}",
+ init_file.c_str());
+ }
+ }
+
+// Enable Telemetry in upstream config only if we are running tests.
+#ifdef TEST_TELEMETRY
+ enable_telemetry = true;
+#endif
+
+ auto config = std::make_unique<llvm::telemetry::Config>();
+ config->EnableTelemetry = enable_telemetry;
+ config->AdditionalDestinations = std::move(additional_destinations);
+
+ // Now apply any additional vendor config, if available.
+ // TODO: cache the Config? (given it's not going to change after LLDB starts
+ // up) However, it's possible we want to supporting restarting the Telemeter
+ // with new config?
+ return GetVendorSpecificConfig(std::move(config));
+}
+
+std::unique_ptr<llvm::telemetry::Config>
+TelemetryVendor::GetVendorSpecificConfig(
+ std::unique_ptr<llvm::telemetry::Config> default_config) {
+ return std::move(default_config);
+}
+
+std::unique_ptr<LldbTelemeter>
+TelemetryVendor::CreateTelemeter(Debugger *debugger) {
+ auto config = GetTelemetryConfig();
+
+ if (!config->EnableTelemetry) {
+ return NoOpTelemeter::CreateInstance(debugger);
+ }
+
+ return BasicTelemeter::CreateInstance(std::move(config), debugger);
+}
diff --git a/lldb/test/CMakeLists.txt b/lldb/test/CMakeLists.txt
index 5ac474736eb63d..a5a342da7cfaa4 100644
--- a/lldb/test/CMakeLists.txt
+++ b/lldb/test/CMakeLists.txt
@@ -108,6 +108,9 @@ endfunction(add_lldb_test_dependency)
add_lldb_test_dependency(lldb)
add_lldb_test_dependency(lldb-test)
+# Enable Telemetry for testing.
+target_compile_definitions(lldb PRIVATE -DTEST_TELEMETRY)
+
# On Darwin, darwin-debug is an hard dependency for the testsuites.
if (CMAKE_SYSTEM_NAME MATCHES "Darwin")
add_lldb_test_dependency(darwin-debug)
>From dbef2be2d3a41f9172a748a453215b9d134abf96 Mon Sep 17 00:00:00 2001
From: Vy Nguyen <vyng at google.com>
Date: Wed, 11 Dec 2024 10:58:25 -0500
Subject: [PATCH 25/25] UPdate LLDB implementation to match changes in LLVM's
telemetry
---
lldb/include/lldb/Core/Telemetry.h | 149 ++++----
lldb/include/lldb/Core/TelemetryVendor.h | 2 -
lldb/source/Core/Debugger.cpp | 7 +-
lldb/source/Core/Telemetry.cpp | 214 +++--------
lldb/source/Core/TelemetryVendor.cpp | 344 +++++++++---------
.../source/Interpreter/CommandInterpreter.cpp | 25 +-
lldb/source/Target/Process.cpp | 9 +-
lldb/source/Target/Target.cpp | 13 +-
llvm/include/llvm/Telemetry/Telemetry.h | 117 ++----
llvm/lib/Telemetry/Telemetry.cpp | 8 +-
10 files changed, 357 insertions(+), 531 deletions(-)
diff --git a/lldb/include/lldb/Core/Telemetry.h b/lldb/include/lldb/Core/Telemetry.h
index 3c6118414bb28d..5a9357d3ea51b4 100644
--- a/lldb/include/lldb/Core/Telemetry.h
+++ b/lldb/include/lldb/Core/Telemetry.h
@@ -28,7 +28,10 @@
namespace lldb_private {
+using llvm::telemetry::Destination;
using llvm::telemetry::KindType;
+using llvm::telemetry::Serializer;
+using llvm::telemetry::TelemetryInfo;
struct LldbEntryKind : public ::llvm::telemetry::EntryKind {
static const KindType BaseInfo = 0b11000;
@@ -39,21 +42,38 @@ struct LldbEntryKind : public ::llvm::telemetry::EntryKind {
static const KindType MiscInfo = 0b11110;
};
-struct LldbBaseTelemetryInfo : public ::llvm::telemetry::TelemetryInfo {
+/// Defines a convenient type for timestamp of various events.
+/// This is used by the EventStats below.
+using SteadyTimePoint = std::chrono::time_point<std::chrono::steady_clock>;
+
+/// 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<SteadyTimePoint> 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) {}
+};
+
+struct LldbBaseTelemetryInfo : public TelemetryInfo {
+ EventStats stats;
+
// For dyn_cast, isa, etc operations.
KindType getKind() const override { return LldbEntryKind::BaseInfo; }
- static bool classof(const TelemetryInfo *T) {
- if (T == nullptr)
+ static bool classof(const TelemetryInfo *t) {
+ if (t == nullptr)
return false;
// Subclasses of this is also acceptable.
- return (T->getKind() & LldbEntryKind::BaseInfo) == LldbEntryKind::BaseInfo;
+ return (t->getKind() & LldbEntryKind::BaseInfo) == LldbEntryKind::BaseInfo;
}
- // Returns a human-readable string description of the struct.
- // This is for debugging purposes only.
- // It is NOT meant as a data-serialisation method.
- virtual std::string ToString() const;
+ void serialize(Serializer& serializer) const override;
};
struct DebuggerTelemetryInfo : public LldbBaseTelemetryInfo {
@@ -82,12 +102,19 @@ struct DebuggerTelemetryInfo : public LldbBaseTelemetryInfo {
return T->getKind() == LldbEntryKind::DebuggerInfo;
}
- llvm::json::Object serializeToJson() const override;
+ void serialize(Serializer& serializer) const override;
+};
- std::string ToString() const override;
+/// Describes the exit signal of an event.
+struct ExitDescription {
+ int ExitCode;
+ std::string Description;
};
struct TargetTelemetryInfo : public LldbBaseTelemetryInfo {
+ lldb::ModuleSP exec_mod;
+ Target* target_ptr;
+
// The same as the executable-module's UUID.
std::string target_uuid;
std::string file_format;
@@ -95,13 +122,16 @@ struct TargetTelemetryInfo : public LldbBaseTelemetryInfo {
std::string binary_path;
size_t binary_size;
+ ExitDescription exit_desc;
TargetTelemetryInfo() = default;
TargetTelemetryInfo(const TargetTelemetryInfo &other) {
+ exec_mod = other.exec_mod;
target_uuid = other.target_uuid;
file_format = other.file_format;
binary_path = other.binary_path;
binary_size = other.binary_size;
+ exit_desc = other.exit_desc;
}
KindType getKind() const override { return LldbEntryKind::TargetInfo; }
@@ -112,9 +142,7 @@ struct TargetTelemetryInfo : public LldbBaseTelemetryInfo {
return T->getKind() == LldbEntryKind::TargetInfo;
}
- llvm::json::Object serializeToJson() const override;
-
- std::string ToString() const override;
+ void serialize(Serializer& serializer) const override;
};
// Entry from client (eg., SB-API)
@@ -137,22 +165,14 @@ struct ClientTelemetryInfo : public LldbBaseTelemetryInfo {
return T->getKind() == LldbEntryKind::ClientInfo;
}
- llvm::json::Object serializeToJson() const override;
-
- std::string ToString() const override;
+ void serialize(Serializer& serializer) const override;
};
-struct CommandExitDescription : public ::llvm::telemetry::ExitDescription {
- lldb::ReturnStatus ret_status;
- CommandExitDescription(int ret_code, std::string ret_desc,
- lldb::ReturnStatus status) {
- ExitCode = ret_code;
- Description = std::move(ret_desc);
- ret_status = status;
- }
-};
struct CommandTelemetryInfo : public LldbBaseTelemetryInfo {
+ Target* target_ptr;
+ CommandReturnObject* result;
+
// If the command is/can be associated with a target entry,
// this field contains that target's UUID.
// <EMPTY> otherwise.
@@ -167,6 +187,7 @@ struct CommandTelemetryInfo : public LldbBaseTelemetryInfo {
std::string original_command;
std::string args;
+ std::optional<ExitDescription> exit_desc;
lldb::ReturnStatus ret_status;
CommandTelemetryInfo() = default;
@@ -177,6 +198,8 @@ struct CommandTelemetryInfo : public LldbBaseTelemetryInfo {
command_name = other.command_name;
original_command = other.original_command;
args = other.args;
+ exit_desc = other.exit_desc;
+ ret_status = other.ret_status;
}
KindType getKind() const override { return LldbEntryKind::CommandInfo; }
@@ -187,9 +210,7 @@ struct CommandTelemetryInfo : public LldbBaseTelemetryInfo {
return T->getKind() == LldbEntryKind::CommandInfo;
}
- llvm::json::Object serializeToJson() const override;
-
- std::string ToString() const override;
+ void serialize(Serializer& serializer) const override;
};
/// The "catch-all" entry to store a set of custom/non-standard
@@ -218,15 +239,13 @@ struct MiscTelemetryInfo : public LldbBaseTelemetryInfo {
return T->getKind() == LldbEntryKind::MiscInfo;
}
- llvm::json::Object serializeToJson() const override;
-
- std::string ToString() const override;
+ void serialize(Serializer& serializer) const override;
};
-/// The base Telemeter instance in LLDB.
+/// The base Telemetry manager instance in LLDB
/// This class declares additional instrumentation points
/// applicable to LLDB.
-class LldbTelemeter : public llvm::telemetry::Telemeter {
+class LldbTelemeter : public llvm::telemetry::Manager {
public:
/// Creates an instance of LldbTelemeter.
/// This uses the plugin registry to find an instance:
@@ -239,65 +258,41 @@ class LldbTelemeter : public llvm::telemetry::Telemeter {
virtual ~LldbTelemeter() = default;
- /// Invoked upon process exit
- virtual void LogProcessExit(int status, llvm::StringRef exit_string,
- llvm::telemetry::EventStats stats,
- Target *target_ptr) = 0;
+ /// To be invoked upon LLDB startup.
+ virtual void LogStartup(DebuggerTelemetryInfo* entry) = 0;
- /// Invoked upon loading the main executable module
+ /// To be invoked upon LLDB exit.
+ virtual void LogExit(DebuggerTelemetryInfo* entry) = 0;
+
+ /// To be invoked upon loading the main executable module.
/// We log in a fire-n-forget fashion so that if the load
/// crashes, we don't lose the entry.
- virtual void
- LogMainExecutableLoadStart(lldb::ModuleSP exec_mod,
- llvm::telemetry::EventStats stats) = 0;
- virtual void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod,
- llvm::telemetry::EventStats stats) = 0;
+ virtual void LogMainExecutableLoadStart(TargetTelemetryInfo* entry) = 0;
+ virtual void LogMainExecutableLoadEnd(TargetTelemetryInfo* entry) = 0;
+
+ /// To be invoked upon process exit.
+ virtual void LogProcessExit(TargetTelemetryInfo* entry);
+
/// Invoked for each command
/// We log in a fire-n-forget fashion so that if the command execution
/// crashes, we don't lose the entry.
- virtual void LogCommandStart(llvm::StringRef uuid,
- llvm::StringRef original_command,
- llvm::telemetry::EventStats stats,
- Target *target_ptr) = 0;
- virtual void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name,
- llvm::StringRef command_args,
- llvm::telemetry::EventStats stats,
- Target *target_ptr,
- CommandReturnObject *result) = 0;
+ virtual void LogCommandStart(CommandTelemetryInfo* entry) = 0;
+ virtual void LogCommandEnd(CommandTelemetryInfo* entry) = 0;
virtual std::string GetNextUUID() = 0;
/// For client (eg., SB API) to send telemetry entries.
virtual void
LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) = 0;
+
+ private:
+ const std::string SessionId;
+ std::vector<std::unique_ptr<Destination>> destinations;
};
-/// Logger configs: LLDB users can also supply their own configs via:
-/// $HOME/.lldb_telemetry_config
-///
-/// We can propose simple syntax: <field_name><colon><value>
-/// Eg.,
-/// enable_telemetry:true
-/// destination:stdout
-/// destination:stderr
-/// destination:/path/to/some/file
-///
-/// The allowed field_name values are:
-/// * enable_telemetry
-/// If the fields are specified more than once, the last line will take
-/// precedence If enable_logging is set to false, no logging will occur.
-/// * destination.
-/// This is allowed to be specified multiple times - it will add to the
-/// default (ie, specified by vendor) list of destinations.
-/// The value can be either:
-/// + one of the two magic values "stdout" or "stderr".
-/// + a path to a local file
-/// !!NOTE!!: We decided to use a separate file instead of the existing settings
-/// file because that file is parsed too late in the process and by the
-/// there might have been lots of telemetry-entries that need to be
-/// sent already.
-/// This approach avoid losing log entries if LLDB crashes during init.
+/// Logger configs. This should be overriden by vendor's specific config.
+/// The default (upstream) config will have telemetry disabled.
llvm::telemetry::Config *GetTelemetryConfig();
} // namespace lldb_private
diff --git a/lldb/include/lldb/Core/TelemetryVendor.h b/lldb/include/lldb/Core/TelemetryVendor.h
index f8a57980db7ad5..3adc77af07e4fb 100644
--- a/lldb/include/lldb/Core/TelemetryVendor.h
+++ b/lldb/include/lldb/Core/TelemetryVendor.h
@@ -32,8 +32,6 @@ class TelemetryVendor : public PluginInterface {
virtual std::unique_ptr<LldbTelemeter>
CreateTelemeter(lldb_private::Debugger *debugger);
- // TODO: move most of the basictelemeter concrete impl here to the plug in (to
- // its .ccpp file that is)
protected:
// Returns a vendor-specific config which may or may not be the same as the
// given "default_config". Downstream implementation can define their
diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index 83f74cbd4935a4..75a0396fd683cf 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -987,9 +987,10 @@ void Debugger::Clear() {
// Log the "quit" event (including stats on how long the teardown took)
// TBD: We *may* have to send off the log BEFORE the ClearIOHanders()?
- llvm::telemetry::TelemetryInfo entry;
- entry.Stats = {quit_start_time, std::chrono::steady_clock::now()};
- m_telemeter->logExit(HostInfo::GetProgramFileSpec().GetPath(), &entry);
+ DebuggerTelemetryInfo entry;
+ entry.stats = {quit_start_time, std::chrono::steady_clock::now()};
+ entry.lldb_path = HostInfo::GetProgramFileSpec().GetPath().str();
+ m_telemeter->logExit(&entry);
});
}
diff --git a/lldb/source/Core/Telemetry.cpp b/lldb/source/Core/Telemetry.cpp
index 9c4ab9dadc7454..55118f16e5e311 100644
--- a/lldb/source/Core/Telemetry.cpp
+++ b/lldb/source/Core/Telemetry.cpp
@@ -51,41 +51,16 @@
namespace lldb_private {
using ::llvm::telemetry::Destination;
-using ::llvm::telemetry::EventStats;
-using ::llvm::telemetry::ExitDescription;
-using ::llvm::telemetry::SteadyTimePoint;
using ::llvm::telemetry::TelemetryInfo;
-static std::string
-ExitDescToString(const llvm::telemetry::ExitDescription *desc) {
- return ("ExitCode:" + desc->ExitCode) +
- (" ExixitDescription: " + desc->Description);
-}
static std::string GetDuration(const EventStats &stats) {
- if (stats.End.has_value())
- return std::to_string((stats.End.value() - stats.Start).count()) +
+ if (stats.end.has_value())
+ return std::to_string((stats.end.value() - stats.start).count()) +
"(nanosec)";
return "<NONE>";
}
-std::string LldbBaseTelemetryInfo::ToString() const {
- return ("[LldbBaseTelemetryInfo]\n") + (" SessionId: " + SessionId + "\n");
-}
-
-std::string DebuggerTelemetryInfo::ToString() const {
- std::string duration_desc =
- (ExitDesc.has_value() ? " lldb session duration: "
- : " lldb startup duration: ") +
- std::to_string((Stats.End.value() - Stats.Start).count()) + "(nanosec)\n";
-
- return LldbBaseTelemetryInfo::ToString() + "\n" +
- ("[DebuggerTelemetryInfo]\n") + (" username: " + username + "\n") +
- (" lldb_git_sha: " + lldb_git_sha + "\n") +
- (" lldb_path: " + lldb_path + "\n") + (" cwd: " + cwd + "\n") +
- duration_desc + "\n";
-}
-
static size_t ToNanosecOrZero(const std::optional<SteadyTimePoint> &Point) {
if (!Point.has_value())
return 0;
@@ -93,169 +68,63 @@ static size_t ToNanosecOrZero(const std::optional<SteadyTimePoint> &Point) {
return Point.value().time_since_epoch().count();
}
-llvm::json::Object DebuggerTelemetryInfo::serializeToJson() const {
- return llvm::json::Object{
- {"DebuggerInfo",
- {
- {"SessionId", SessionId},
- {"username", username},
- {"lldb_git_sha", lldb_git_sha},
- {"lldb_path", lldb_path},
- {"cwd", cwd},
- {
- "EventStats",
- {
- {"Start", Stats.Start.time_since_epoch().count()},
- {"End", ToNanosecOrZero(Stats.End)},
- },
- },
- // TODO: fill in more?
- }}};
-}
-
-std::string ClientTelemetryInfo::ToString() const {
- return LldbBaseTelemetryInfo::ToString() + "\n" +
- ("[DapRequestInfoEntry]\n") +
- (" request_name: " + request_name + "\n") +
- (" request_duration: " + GetDuration(Stats) + "(nanosec)\n") +
- (" error_msg: " + error_msg + "\n");
+void LldbBaseTelemetryInfo::serialize(Serializer& serializer) const {
+ serializer.writeInt32("EntryKind", getKind());
+ serializer.writeString("SessionId", SessionId);
}
-llvm::json::Object ClientTelemetryInfo::serializeToJson() const {
- return llvm::json::Object{
- {"ClientInfo",
- {
- {"SessionId", SessionId},
- {"request_name", request_name},
- {"error_msg", error_msg},
- {
- "EventStats",
- {
- {"Start", Stats.Start.time_since_epoch().count()},
- {"End", ToNanosecOrZero(Stats.End)},
- },
- },
- }}};
+void DebuggerTelemetryInfo::serialize(Serializer& serializer) const {
+ LldbBaseTelemetryInfo::serialize(serializer);
+ serializer.writeString("username", username);
+ serializer.writeString("lldb_path", lldb_path);
+ serializer.writeString("cwd", cwd);
+ serializer.writeSizeT("start", stats.start.time_since_epoch().count());
+ serializer.writeSizeT("end", ToNanosecOrZero(stats.end));
}
-std::string TargetTelemetryInfo::ToString() const {
- std::string exit_or_load_desc;
- if (ExitDesc.has_value()) {
- // If this entry was emitted for an exit
- exit_or_load_desc = " process_duration: " + GetDuration(Stats) +
- ExitDescToString(&(ExitDesc.value())) + "\n";
- } else {
- // This was emitted for a load event.
- // See if it was the start-load or end-load entry
- if (Stats.End.has_value()) {
- exit_or_load_desc =
- " startup_init_duration: " + GetDuration(Stats) + "\n";
- } else {
- exit_or_load_desc = " startup_init_start\n";
- }
- }
- return LldbBaseTelemetryInfo::ToString() + "\n" +
- ("[TargetTelemetryInfo]\n") +
- (" target_uuid: " + target_uuid + "\n") +
- (" file_format: " + file_format + "\n") +
- (" binary_path: " + binary_path + "\n") +
- (" binary_size: " + std::to_string(binary_size) + "\n") +
- exit_or_load_desc;
+void ClientTelemetryInfo::serialize(Serializer& serializer) const {
+ LldbBaseTelemetryInfo::serialize(serializer);
+ serializer.writeString("request_name", request_name);
+ serializer.writeString("error_msg", error_msg);
+ serializer.writeSizeT("start", stats.start.time_since_epoch().count());
+ serializer.writeSizeT("end", ToNanosecOrZero(stats.end));
}
-llvm::json::Object TargetTelemetryInfo::serializeToJson() const {
- return llvm::json::Object{{
- "TargetInfo",
- {
- {"SessionId", SessionId},
- {"target_uuid", target_uuid},
- {"binary_path", binary_path},
- {"binary_size", binary_size},
- // TODO: fill in more
- },
- }};
-}
-std::string CommandTelemetryInfo::ToString() const {
- // Whether this entry was emitted at the start or at the end of the
- // command-execution.
- if (Stats.End.has_value()) {
- return LldbBaseTelemetryInfo::ToString() + "\n" +
- ("[CommandTelemetryInfo] - END\n") +
- (" target_uuid: " + target_uuid + "\n") +
- (" command_uuid: " + command_uuid + "\n") +
- (" command_name: " + command_name + "\n") +
- (" args: " + args + "\n") +
- (" command_runtime: " + GetDuration(Stats) + "\n") +
- (ExitDesc.has_value() ? ExitDescToString(&(ExitDesc.value()))
- : "no exit-description") +
- "\n";
- } else {
- return LldbBaseTelemetryInfo::ToString() + "\n" +
- ("[CommandTelemetryInfo] - START\n") +
- (" target_uuid: " + target_uuid + "\n") +
- (" command_uuid: " + command_uuid + "\n") +
- (" original_command: " + original_command + "\n");
- }
+void TargetTelemetryInfo::serialize(Serializer& serializer) const {
+ LldbBaseTelemetryInfo::serialize(serializer);
+ serializer.writeString("target_uuid", target_uuid);
+ serializer.writeString("binary_path", binary_path);
+ serializer.writeSizeT("binary_size", binary_size);
}
-llvm::json::Object CommandTelemetryInfo::serializeToJson() const {
- llvm::json::Object inner;
- inner.insert({"SessionId", SessionId});
- inner.insert({"target_uuid", target_uuid});
- inner.insert({"command_uuid", command_uuid});
- inner.insert({"args", args});
- inner.insert({"original_command", original_command});
- inner.insert({
- "EventStats",
- {
- {"Start", Stats.Start.time_since_epoch().count()},
- {"End", ToNanosecOrZero(Stats.End)},
- },
- });
+void CommandTelemetryInfo::serialize(Serializer& serializer) const {
+ LldbBaseTelemetryInfo::serialize(serializer);
+ serializer.writeString("target_uuid", target_uuid);
+ serializer.writeString("command_uuid", command_uuid);
+ serializer.writeString("args", args);
+ serializer.writeString("original_command", original_command);
+ serializer.writeSizeT("start", stats.start.time_since_epoch().count());
+ serializer.writeSizeT("end", ToNanosecOrZero(stats.end));
// If this entry was emitted at the end of the command-execution,
// then calculate the runtime too.
- if (Stats.End.has_value()) {
- inner.insert(
- {"command_runtime", (Stats.End.value() - Stats.Start).count()});
- if (ExitDesc.has_value()) {
- inner.insert({"exit_code", ExitDesc->ExitCode});
- inner.insert({"exit_msg", ExitDesc->Description});
- inner.insert({"return_status", static_cast<int>(ret_status)});
+ if (stats.end.has_value()) {
+ serializer.writeSizeT("command_runtime", (stats.end.value() - stats.start).count());
+ if (exit_desc.has_value()) {
+ serializer.writeInt32("exit_code", exit_desc->ExitCode);
+ serializer.writeString("exit_msg", exit_desc->Description);
+ serializer.writeInt32("return_status", static_cast<int>(ret_status));
}
}
-
- return llvm::json::Object{{"CommandInfo", std::move(inner)}};
}
-std::string MiscTelemetryInfo::ToString() const {
- std::string ret;
- llvm::raw_string_ostream ret_strm(ret);
- ret_strm << LldbBaseTelemetryInfo::ToString() << "\n[MiscTelemetryInfo]\n"
- << " target_uuid: " << target_uuid + "\n"
- << " meta_data:\n";
- for (const auto &kv : meta_data) {
- ret_strm << " " << kv.first << ": " << kv.second << "\n";
- }
- return ret;
-}
-
-llvm::json::Object MiscTelemetryInfo::serializeToJson() const {
- llvm::json::Object meta_data_obj;
- for (const auto &kv : meta_data)
- meta_data_obj.insert({kv.first, kv.second});
-
- return llvm::json::Object{{
- "MiscInfo",
- {
- {"SessionId", SessionId},
- {"target_uuid", target_uuid},
- {"meta_data", std::move(meta_data_obj)},
- },
- }};
+void MiscTelemetryInfo::serialize(Serializer& serializer) const {
+ LldbBaseTelemetryInfo::serialize(serializer);
+ serializer.writeString("target_uuid", target_uuid);
+ serializer.writeKeyValueMap("meta_data", meta_data);
}
std::unique_ptr<LldbTelemeter>
@@ -270,4 +139,5 @@ LldbTelemeter::CreateInstance(lldb_private::Debugger *debugger) {
return vendor->CreateTelemeter(debugger);
}
+
} // namespace lldb_private
diff --git a/lldb/source/Core/TelemetryVendor.cpp b/lldb/source/Core/TelemetryVendor.cpp
index fa40ee2ad045e1..88cdb9fc9f8cbe 100644
--- a/lldb/source/Core/TelemetryVendor.cpp
+++ b/lldb/source/Core/TelemetryVendor.cpp
@@ -40,6 +40,7 @@
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/Twine.h"
+#include "llvm/Support/Error.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/JSON.h"
#include "llvm/Support/LineIterator.h"
@@ -55,10 +56,9 @@ using namespace lldb_private;
namespace {
using ::llvm::telemetry::Destination;
-using ::llvm::telemetry::EventStats;
-using ::llvm::telemetry::ExitDescription;
-using ::llvm::telemetry::SteadyTimePoint;
using ::llvm::telemetry::TelemetryInfo;
+using ::llvm::StringRef;
+using ::llvm::Error;
// No-op logger to use when users disable telemetry
class NoOpTelemeter : public LldbTelemeter {
@@ -68,27 +68,19 @@ class NoOpTelemeter : public LldbTelemeter {
}
NoOpTelemeter(Debugger *debugger) {}
- void logStartup(llvm::StringRef tool_path, TelemetryInfo *entry) override {}
- void logExit(llvm::StringRef tool_path, TelemetryInfo *entry) override {}
-
- void LogProcessExit(int status, llvm::StringRef exit_string, EventStats stats,
- Target *target_ptr) override {}
- void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod,
- EventStats stats) override {}
- void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod,
- EventStats stats) override {}
-
- void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command,
- EventStats stats, Target *target_ptr) override {}
- void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name,
- llvm::StringRef command_args, EventStats stats,
- Target *target_ptr, CommandReturnObject *result) override {
- }
+ void addDestination(std::unique_ptr<Destination> destination) override {}
+ llvm::Error dispatch(TelemetryInfo* entry) override { return Error::success(); }
+ void LogStartup(DebuggerTelemetryInfo* entry) override {}
+ void LogExit(DebuggerTelemetryInfo* entry) override {}
+ void LogMainExecutableLoadStart(TargetTelemetryInfo* entry) override {}
+ void LogMainExecutableLoadEnd(TargetTelemetryInfo* entry) override {}
+ void LogProcessExit(TargetTelemetryInfo* entry) override {}
+ void LogCommandStart(CommandTelemetryInfo* entry) override {}
+ void LogCommandEnd(CommandTelemetryInfo* entry) override {}
void
LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override {}
- void addDestination(llvm::telemetry::Destination *destination) override {}
std::string GetNextUUID() override { return ""; }
};
@@ -100,33 +92,26 @@ class BasicTelemeter : public LldbTelemeter {
virtual ~BasicTelemeter() = default;
- void logStartup(llvm::StringRef lldb_path, TelemetryInfo *entry) override;
- void logExit(llvm::StringRef lldb_path, TelemetryInfo *entry) override;
-
- void LogProcessExit(int status, llvm::StringRef exit_string, EventStats stats,
- Target *target_ptr) override;
- void LogMainExecutableLoadStart(lldb::ModuleSP exec_mod,
- EventStats stats) override;
- void LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod,
- EventStats stats) override;
-
- void LogCommandStart(llvm::StringRef uuid, llvm::StringRef original_command,
- EventStats stats, Target *target_ptr) override;
- void LogCommandEnd(llvm::StringRef uuid, llvm::StringRef command_name,
- llvm::StringRef command_args, EventStats stats,
- Target *target_ptr, CommandReturnObject *result) override;
-
+ void LogStartup(DebuggerTelemetryInfo* entry) override;
+ void LogExit(DebuggerTelemetryInfo* entry) override;
+ void LogMainExecutableLoadStart(TargetTelemetryInfo* entry) override;
+ void LogMainExecutableLoadEnd(TargetTelemetryInfo* entry) override;
+ void LogProcessExit(TargetTelemetryInfo* entry) override;
+ void LogCommandStart(CommandTelemetryInfo* entry) override;
+ void LogCommandEnd(CommandTelemetryInfo* entry) override;
void
- LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override;
+ LogClientTelemetry(const lldb_private::StructuredDataImpl &entry) override;
- void addDestination(Destination *destination) override {
- m_destinations.push_back(destination);
+ void addDestination(std::unique_ptr<Destination>destination) override {
+ m_destinations.push_back(std::move(destination));
}
std::string GetNextUUID() override {
return std::to_string(uuid_seed.fetch_add(1));
}
+ llvm::Error dispatch(TelemetryInfo* entry) override;
+
protected:
BasicTelemeter(std::unique_ptr<llvm::telemetry::Config> config,
Debugger *debugger);
@@ -137,34 +122,96 @@ class BasicTelemeter : public LldbTelemeter {
template <typename EntrySubType> EntrySubType MakeBaseEntry() {
EntrySubType entry;
entry.SessionId = m_session_uuid;
- entry.Counter = counter.fetch_add(1);
return entry;
}
- void EmitToDestinations(const TelemetryInfo *entry);
-
std::unique_ptr<llvm::telemetry::Config> m_config;
Debugger *m_debugger;
const std::string m_session_uuid;
std::string startup_lldb_path;
// counting number of entries.
- std::atomic<size_t> counter = 0;
+ std::atomic<size_t> counter= 0;
- std::vector<Destination *> m_destinations;
+ std::vector<std::unique_ptr<Destination>> m_destinations;
std::atomic<size_t> uuid_seed = 0;
};
+class BasicSerializer : public Serializer {
+ public:
+ const std::string &getString() { return Buffer; }
+
+ llvm::Error start() override {
+ if (started)
+ return llvm::createStringError("Serializer already in use");
+ started = true;
+ Buffer.clear();
+ return Error::success();
+ }
+
+ void writeBool(StringRef KeyName, bool Value) override {
+ writeHelper(KeyName, Value);
+ }
+
+ void writeInt32(StringRef KeyName, int Value) override {
+ writeHelper(KeyName, Value);
+ }
+
+ void writeSizeT(StringRef KeyName, size_t Value) override {
+ writeHelper(KeyName, Value);
+ }
+ void writeString(StringRef KeyName, StringRef Value) override {
+ assert(started && "serializer not started");
+ }
+
+ void writeKeyValueMap(StringRef KeyName,
+ const std::map<std::string, std::string>& Value) override {
+ std::string Inner;
+ for (auto kv : Value) {
+ writeHelper(StringRef(kv.first), StringRef(kv.second), &Inner);
+ }
+ writeHelper(KeyName, StringRef(Inner));
+ }
+
+ llvm::Error finish() override {
+ if (!started)
+ return llvm::createStringError("Serializer not currently in use");
+ started = false;
+ return Error::success();
+ }
+
+private:
+ template <typename T>
+ void writeHelper(StringRef Name, T Value, std::string *Buff) {
+ assert(started && "serializer not started");
+ Buff->append((Name + ":" + llvm::Twine(Value) + "\n").str());
+ }
+
+ template <typename T> void writeHelper(StringRef Name, T Value) {
+ writeHelper(Name, Value, &Buffer);
+ }
+
+ bool started = false;
+ std::string Buffer;
+};
+
class StreamTelemetryDestination : public Destination {
public:
- StreamTelemetryDestination(llvm::raw_ostream &os, std::string desc)
- : os(os), desc(desc) {}
- llvm::Error emitEntry(const llvm::telemetry::TelemetryInfo *entry) override {
+ StreamTelemetryDestination(llvm::raw_ostream &os)
+ : os(os) {}
+ llvm::Error receiveEntry(const llvm::telemetry::TelemetryInfo *entry) override {
// Upstream Telemetry should not leak anything other than the
// basic data, unless running in test mode.
#ifdef TEST_TELEMETRY
- os << entry->ToString() << "\n";
+ if (Error err = serializer.start()) {
+ return err;
+ }
+ entry->serialize(serializer);
+ if (Error err = serializer.finish()) {
+ return err;
+ }
+ os << serializer.getString() << "\n";
#else
os << "session_uuid: " << entry->SessionId
<< "<the rest is omitted due to PII risk>\n";
@@ -173,11 +220,11 @@ class StreamTelemetryDestination : public Destination {
return llvm::ErrorSuccess();
}
- std::string name() const override { return desc; }
+ llvm::StringLiteral name() const override { return "StreamDestination"; }
private:
llvm::raw_ostream &os;
- const std::string desc;
+ BasicSerializer serializer;
};
static std::string MakeUUID(lldb_private::Debugger *debugger) {
@@ -207,145 +254,116 @@ BasicTelemeter::CreateInstance(std::unique_ptr<llvm::telemetry::Config> config,
lldb_private::Debugger *debugger) {
BasicTelemeter *ins = new BasicTelemeter(std::move(config), debugger);
- for (const std ::string &dest : config->AdditionalDestinations) {
- if (dest == "stdout") {
- ins->addDestination(
- new StreamTelemetryDestination(llvm::outs(), "stdout"));
- } else if (dest == "stderr") {
- ins->addDestination(
- new StreamTelemetryDestination(llvm::errs(), "stderr"));
- } else {
- // TODO: handle custom values as needed?
- }
- }
return std::unique_ptr<BasicTelemeter>(ins);
}
-void BasicTelemeter::EmitToDestinations(const TelemetryInfo *entry) {
- // TODO: can do this in a separate thread (need to own the ptrs!).
- for (Destination *destination : m_destinations) {
- llvm::Error err = destination->emitEntry(entry);
+llvm::Error BasicTelemeter::dispatch(TelemetryInfo *entry) {
+ entry->SessionId = m_session_uuid;
+
+ for (auto& destination : m_destinations) {
+ llvm::Error err = destination->receiveEntry(entry);
if (err) {
- LLDB_LOG(GetLog(LLDBLog::Object),
- "Error emitting to destination(name = {0})",
- destination->name());
+ return std::move(err);
}
}
+ return Error::success();
}
-void BasicTelemeter::logStartup(llvm::StringRef lldb_path,
- TelemetryInfo *entry) {
- startup_lldb_path = lldb_path.str();
- lldb_private::DebuggerTelemetryInfo startup_info =
- MakeBaseEntry<DebuggerTelemetryInfo>();
-
+void BasicTelemeter::LogStartup(DebuggerTelemetryInfo* entry) {
UserIDResolver &resolver = lldb_private::HostInfo::GetUserIDResolver();
std::optional<llvm::StringRef> opt_username =
resolver.GetUserName(lldb_private::HostInfo::GetUserID());
if (opt_username)
- startup_info.username = *opt_username;
+ entry->username = *opt_username;
- startup_info.lldb_git_sha =
- lldb_private::GetVersion(); // TODO: find the real git sha
- startup_info.lldb_path = startup_lldb_path;
- startup_info.Stats = entry->Stats;
+ entry->lldb_git_sha =
+ lldb_private::GetVersion(); // TODO: find the real git sha?
llvm::SmallString<64> cwd;
if (!llvm::sys::fs::current_path(cwd)) {
- startup_info.cwd = cwd.c_str();
+ entry->cwd = cwd.c_str();
} else {
MiscTelemetryInfo misc_info = MakeBaseEntry<MiscTelemetryInfo>();
misc_info.meta_data["internal_errors"] = "Cannot determine CWD";
- EmitToDestinations(&misc_info);
+ if(auto er = dispatch(&misc_info)){
+ LLDB_LOG(GetLog(LLDBLog::Object,
+ "Failed to dispatch misc-info from startup: {0}", er.message()));
+ }
}
- EmitToDestinations(&startup_info);
+ if(auto er = dispatch(entry)) {
+ LLDB_LOG(GetLog(LLDBLog::Object,
+ "Failed to dispatch entry from startup: {0}", er.message()));
+ }
// Optional part
CollectMiscBuildInfo();
}
-void BasicTelemeter::logExit(llvm::StringRef lldb_path, TelemetryInfo *entry) {
- // we should be shutting down the same instance that we started?!
- // llvm::Assert(startup_lldb_path == lldb_path.str());
-
- lldb_private::DebuggerTelemetryInfo exit_info =
- MakeBaseEntry<lldb_private::DebuggerTelemetryInfo>();
- exit_info.Stats = entry->Stats;
- exit_info.lldb_path = startup_lldb_path;
+void BasicTelemeter::LogExit(DebuggerTelemetryInfo* entry) {
if (auto *selected_target =
m_debugger->GetSelectedExecutionContext().GetTargetPtr()) {
if (!selected_target->IsDummyTarget()) {
const lldb::ProcessSP proc = selected_target->GetProcessSP();
if (proc == nullptr) {
// no process has been launched yet.
- exit_info.ExitDesc = {-1, "no process launched."};
+ entry->ExitDesc = {-1, "no process launched."};
} else {
- exit_info.ExitDesc = {proc->GetExitStatus(), ""};
+ entry->ExitDesc = {proc->GetExitStatus(), ""};
if (const char *description = proc->GetExitDescription())
- exit_info.ExitDesc->Description = std::string(description);
+ entry->ExitDesc->Description = std::string(description);
}
}
}
- EmitToDestinations(&exit_info);
+ dispatch(entry);
}
-void BasicTelemeter::LogProcessExit(int status, llvm::StringRef exit_string,
- EventStats stats, Target *target_ptr) {
- lldb_private::TargetTelemetryInfo exit_info =
- MakeBaseEntry<TargetTelemetryInfo>();
- exit_info.Stats = std::move(stats);
- exit_info.target_uuid =
- target_ptr && !target_ptr->IsDummyTarget()
- ? target_ptr->GetExecutableModule()->GetUUID().GetAsString()
+void BasicTelemeter::LogProcessExit(TargetTelemetryInfo* entry) {
+ entry->target_uuid =
+ entry->target_ptr && !entry->target_ptr->IsDummyTarget()
+ ? entry->target_ptr->GetExecutableModule()->GetUUID().GetAsString()
: "";
- exit_info.ExitDesc = {status, exit_string.str()};
- EmitToDestinations(&exit_info);
+ dispatch(entry);
}
void BasicTelemeter::CollectMiscBuildInfo() {
// collecting use-case specific data
}
-void BasicTelemeter::LogMainExecutableLoadStart(lldb::ModuleSP exec_mod,
- EventStats stats) {
- TargetTelemetryInfo target_info = MakeBaseEntry<TargetTelemetryInfo>();
- target_info.Stats = std::move(stats);
- target_info.binary_path =
- exec_mod->GetFileSpec().GetPathAsConstString().GetCString();
- target_info.file_format = exec_mod->GetArchitecture().GetArchitectureName();
- target_info.target_uuid = exec_mod->GetUUID().GetAsString();
- if (auto err = llvm::sys::fs::file_size(exec_mod->GetFileSpec().GetPath(),
- target_info.binary_size)) {
+void BasicTelemeter::LogMainExecutableLoadStart(TargetTelemetryInfo* entry) {
+ entry->binary_path =
+ entry->exec_mod->GetFileSpec().GetPathAsConstString().GetCString();
+ entry->file_format = entry->exec_mod->GetArchitecture().GetArchitectureName();
+ entry->target_uuid = entry->exec_mod->GetUUID().GetAsString();
+ if (auto err = llvm::sys::fs::file_size(entry->exec_mod->GetFileSpec().GetPath(),
+ entry->binary_size)) {
// If there was error obtaining it, just reset the size to 0.
// Maybe log the error too?
- target_info.binary_size = 0;
+ entry->binary_size = 0;
}
- EmitToDestinations(&target_info);
+ dispatch(entry);
}
-void BasicTelemeter::LogMainExecutableLoadEnd(lldb::ModuleSP exec_mod,
- EventStats stats) {
- TargetTelemetryInfo target_info = MakeBaseEntry<TargetTelemetryInfo>();
- target_info.Stats = std::move(stats);
- target_info.binary_path =
+void BasicTelemeter::LogMainExecutableLoadEnd(TargetTelemetryInfo* entry) {
+ lldb::ModuleSP exec_mod = entry->exec_mod;
+ entry->binary_path =
exec_mod->GetFileSpec().GetPathAsConstString().GetCString();
- target_info.file_format = exec_mod->GetArchitecture().GetArchitectureName();
- target_info.target_uuid = exec_mod->GetUUID().GetAsString();
- target_info.binary_size = exec_mod->GetObjectFile()->GetByteSize();
+ entry->file_format = exec_mod->GetArchitecture().GetArchitectureName();
+ entry->target_uuid = exec_mod->GetUUID().GetAsString();
+ entry->binary_size = exec_mod->GetObjectFile()->GetByteSize();
- EmitToDestinations(&target_info);
+ dispatch(entry);
- // Collect some more info, might be useful?
+ // Collect some more info, might be useful?
MiscTelemetryInfo misc_info = MakeBaseEntry<MiscTelemetryInfo>();
misc_info.target_uuid = exec_mod->GetUUID().GetAsString();
misc_info.meta_data["symtab_index_time"] =
std::to_string(exec_mod->GetSymtabIndexTime().get().count());
misc_info.meta_data["symtab_parse_time"] =
std::to_string(exec_mod->GetSymtabParseTime().get().count());
- EmitToDestinations(&misc_info);
+ dispatch(&misc_info);
}
void BasicTelemeter::LogClientTelemetry(
@@ -386,55 +404,37 @@ void BasicTelemeter::LogClientTelemetry(
client_info.error_msg = error_msg->str();
*/
- EmitToDestinations(&client_info);
+ dispatch(&client_info);
}
-void BasicTelemeter::LogCommandStart(llvm::StringRef uuid,
- llvm::StringRef original_command,
- EventStats stats, Target *target_ptr) {
-
- lldb_private::CommandTelemetryInfo command_info =
- MakeBaseEntry<CommandTelemetryInfo>();
-
+void BasicTelemeter::LogCommandStart(CommandTelemetryInfo* entry) {
// If we have a target attached to this command, then get the UUID.
- command_info.target_uuid = "";
- if (target_ptr && target_ptr->GetExecutableModule() != nullptr) {
- command_info.target_uuid =
- target_ptr->GetExecutableModule()->GetUUID().GetAsString();
+ if (entry->target_ptr && entry->target_ptr->GetExecutableModule() != nullptr) {
+ entry->target_uuid =
+ entry->target_ptr->GetExecutableModule()->GetUUID().GetAsString();
+ } else {
+ entry->target_uuid = "";
}
- command_info.command_uuid = uuid.str();
- command_info.original_command = original_command.str();
- command_info.Stats = std::move(stats);
- EmitToDestinations(&command_info);
+ dispatch(entry);
}
-void BasicTelemeter::LogCommandEnd(llvm::StringRef uuid,
- llvm::StringRef command_name,
- llvm::StringRef command_args,
- EventStats stats, Target *target_ptr,
- CommandReturnObject *result) {
-
- lldb_private::CommandTelemetryInfo command_info =
- MakeBaseEntry<CommandTelemetryInfo>();
-
+void BasicTelemeter::LogCommandEnd(CommandTelemetryInfo* entry) {
// If we have a target attached to this command, then get the UUID.
- command_info.target_uuid = "";
- if (target_ptr && target_ptr->GetExecutableModule() != nullptr) {
- command_info.target_uuid =
- target_ptr->GetExecutableModule()->GetUUID().GetAsString();
+ if (entry->target_ptr && target_ptr->GetExecutableModule() != nullptr) {
+ entry->target_uuid =
+ entry->target_ptr->GetExecutableModule()->GetUUID().GetAsString();
+ } else {
+ entry->target_uuid = "";
}
- command_info.command_uuid = uuid.str();
- command_info.command_name = command_name.str();
- command_info.args = command_args.str();
- command_info.Stats = std::move(stats);
- command_info.ExitDesc = {result->Succeeded() ? 0 : -1, ""};
- if (llvm::StringRef error_data = result->GetErrorData();
+
+ entry->exit_desc = {entry->result->Succeeded() ? 0 : -1, ""};
+ if (llvm::StringRef error_data = entry->result->GetErrorData();
!error_data.empty()) {
- command_info.ExitDesc->Description = error_data.str();
+ entry->exit_desc->Description = error_data.str();
}
- command_info.ret_status = result->GetStatus();
- EmitToDestinations(&command_info);
+ entry->ret_status = entry->result->GetStatus();
+ dispatch(entry);
}
} // namespace
@@ -480,7 +480,6 @@ static bool ParseBoolValue(llvm::StringRef str, llvm::StringRef label) {
std::unique_ptr<llvm::telemetry::Config> TelemetryVendor::GetTelemetryConfig() {
// Telemetry is disabled by default.
bool enable_telemetry = false;
- std::vector<std::string> additional_destinations;
// Look in the $HOME/.lldb_telemetry_config file to populate the struct
llvm::SmallString<64> init_file;
@@ -495,15 +494,6 @@ std::unique_ptr<llvm::telemetry::Config> TelemetryVendor::GetTelemetryConfig() {
for (; !iter.is_at_eof(); ++iter) {
if (iter->starts_with("enable_telemetry:")) {
enable_telemetry = ParseBoolValue(*iter, "enable_telemetry:");
- } else if (iter->starts_with("destination:")) {
- llvm::StringRef dest = ParseValue(*iter, "destination:");
- if (dest == "stdout") {
- additional_destinations.push_back("stdout");
- } else if (dest == "stderr") {
- additional_destinations.push_back("stderr");
- } else {
- additional_destinations.push_back(dest.str());
- }
}
}
} else {
@@ -517,9 +507,7 @@ std::unique_ptr<llvm::telemetry::Config> TelemetryVendor::GetTelemetryConfig() {
enable_telemetry = true;
#endif
- auto config = std::make_unique<llvm::telemetry::Config>();
- config->EnableTelemetry = enable_telemetry;
- config->AdditionalDestinations = std::move(additional_destinations);
+ auto config = std::make_unique<llvm::telemetry::Config>(enable_telemetry);
// Now apply any additional vendor config, if available.
// TODO: cache the Config? (given it's not going to change after LLDB starts
diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp
index 3df40629792ce9..9bad200bc53906 100644
--- a/lldb/source/Interpreter/CommandInterpreter.cpp
+++ b/lldb/source/Interpreter/CommandInterpreter.cpp
@@ -1,4 +1,4 @@
-//===-- CommandInterpreter.cpp --------------------------------------------===//
+ //===-- CommandInterpreter.cpp --------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
@@ -1886,15 +1886,19 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
LazyBool lazy_add_to_history,
CommandReturnObject &result,
bool force_repeat_command) {
- llvm::telemetry::EventStats start_command_stats(
+ EventStats start_command_stats(
std::chrono::steady_clock::now());
LldbTelemeter *telemeter = GetDebugger().GetTelemeter();
// Generate a UUID for this command so the logger can match
// the start/end entries correctly.
const std::string command_uuid = telemeter->GetNextUUID();
- telemeter->LogCommandStart(command_uuid, command_line, start_command_stats,
- GetExecutionContext().GetTargetPtr());
+ CommandTelemetryInfo start_entry;
+ start_entry.stats = start_command_stats;
+ start_entry.command_uuid = command_uuid;
+ start_entry.target_ptr = GetExecutionContext().GetTargetptr();
+
+ telemeter->LogCommandStart(&start_entry);
std::string command_string(command_line);
std::string original_command_string(command_line);
@@ -1902,7 +1906,7 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
CommandObject *cmd_obj = nullptr;
auto log_on_exit = llvm::make_scope_exit([&]() {
- llvm::telemetry::EventStats end_command_stats(
+ EventStats end_command_stats(
start_command_stats.Start, std::chrono::steady_clock::now());
llvm::StringRef command_name =
@@ -1910,9 +1914,14 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
// TODO: this is logging the time the command-handler finishes.
// But we may want a finer-grain durations too?
// (ie., the execute_time recorded below?)
- telemeter->LogCommandEnd(command_uuid, command_name, parsed_command_args,
- end_command_stats,
- GetExecutionContext().GetTargetPtr(), &result);
+ CommandTelemetryInfo end_entry;
+ end_entry.stats = end_command_stats;
+ end_entry.command_uuid = command_uuid;
+ end_entry.command_name = command_name.str();
+ end_entry.args = parsed_command_args;
+ end_entry.target_ptr = GetExecutionContext().GetTargetPtr();
+ end_entry.result = &result;
+ telemeter->LogCommandEnd(&end_entry);
});
Log *log = GetLog(LLDBLog::Commands);
diff --git a/lldb/source/Target/Process.cpp b/lldb/source/Target/Process.cpp
index 7e88851430b2bd..fa8cd17f789047 100644
--- a/lldb/source/Target/Process.cpp
+++ b/lldb/source/Target/Process.cpp
@@ -1100,10 +1100,11 @@ bool Process::SetExitStatus(int status, llvm::StringRef exit_string) {
// Allow subclasses to do some cleanup
DidExit();
- llvm::telemetry::EventStats stats = {start_time,
- std::chrono::steady_clock::now()};
- GetTarget().GetDebugger().GetTelemeter()->LogProcessExit(status, exit_string,
- stats, &GetTarget());
+ TargetTelemetryInfo entry;
+ entry.stats = {start_time, std::chrono::steady_clock::now()};
+ entry.exit_desc = {status, exit_string.str()};
+ entry.target_ptr = &GetTarget();
+ GetTarget().GetDebugger().GetTelemeter()->LogProcessExit(&entry);
return true;
}
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index 3e36dd8046fc52..ccb6f9bd3d245d 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -1561,13 +1561,18 @@ void Target::SetExecutableModule(ModuleSP &executable_sp,
ClearModules(false);
if (executable_sp) {
- m_debugger.GetTelemeter()->LogMainExecutableLoadStart(
- executable_sp, load_executable_stats);
+ TargetTelemetryInfo load_start;
+ load_start.stats = load_executable_stats;
+ load_start.exec_mod = executable_sp;
+
+ m_debugger.GetTelemeter()->LogMainExecutableLoadStart(&load_start);
auto log_on_exit = llvm::make_scope_exit([&]() {
load_executable_stats.End = std::chrono::steady_clock::now();
- m_debugger.GetTelemeter()->LogMainExecutableLoadEnd(
- executable_sp, load_executable_stats);
+ TargetTelemetryInfo load_end;
+ load_end.stats = load_executable_stats;
+ load_end.executable_sp = executable_sp;
+ m_debugger.GetTelemeter()->LogMainExecutableLoadEnd(&load_end);
});
}
if (executable_sp) {
diff --git a/llvm/include/llvm/Telemetry/Telemetry.h b/llvm/include/llvm/Telemetry/Telemetry.h
index f682e1cdf5cafa..cc7075183e43fa 100644
--- a/llvm/include/llvm/Telemetry/Telemetry.h
+++ b/llvm/include/llvm/Telemetry/Telemetry.h
@@ -8,125 +8,86 @@
///
/// \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 <chrono>
-#include <ctime>
-#include <memory>
-#include <optional>
-#include <string>
-
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/JSON.h"
+#include <memory>
+#include <optional>
+#include <string>
namespace llvm {
namespace telemetry {
+class Serializer {
+public:
+ virtual llvm::Error start() = 0;
+ virtual void writeBool(StringRef KeyName, bool Value) = 0;
+ virtual void writeInt32(StringRef KeyName, int Value) = 0;
+ virtual void writeSizeT(StringRef KeyName, size_t Value) = 0;
+ virtual void writeString(StringRef KeyName, StringRef Value) = 0;
+ virtual void writeKeyValueMap(StringRef KeyName,
+ const std::map<std::string, std::string>& Value) = 0;
+ virtual llvm::Error finish() = 0;
+};
+
/// Configuration for the Telemeter class.
/// This stores configurations from both users and vendors and is passed
-/// to the Telemeter upon contruction. (Any changes to the config after
-/// the Telemeter's construction will not have effect on it).
+/// to the Telemeter upon construction. (Any changes to the config after
+/// the Telemeter's construction will not have any effect on it).
///
/// This struct can be extended as needed to add additional configuration
/// points specific to a vendor's implementation.
struct Config {
// If true, telemetry will be enabled.
- bool EnableTelemetry;
-
- // Implementation-defined names of additional destinations to send
- // telemetry data (Could be stdout, stderr, or some local paths, etc).
- //
- // These strings will be interpreted by the vendor's code.
- // So the users must pick the from their vendor's pre-defined
- // set of Destinations.
- std::vector<std::string> AdditionalDestinations;
-};
+ const bool EnableTelemetry;
+ Config(bool E) : EnableTelemetry(E) {}
-/// Defines a convenient type for timestamp of various events.
-/// This is used by the EventStats below.
-using SteadyTimePoint = std::chrono::time_point<std::chrono::steady_clock>;
-
-/// 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<SteadyTimePoint> End;
- // TBD: could add some memory stats here too?
-
- EventStats() = default;
- EventStats(SteadyTimePoint Start) : Start(Start) {}
- EventStats(SteadyTimePoint Start, SteadyTimePoint End)
- : Start(Start), End(End) {}
-};
-
-/// Describes the exit signal of an event.
-/// This is used by TelemetryInfo below.
-struct ExitDescription {
- int ExitCode;
- std::string Description;
+ virtual std::string makeSessionId() { return "0"; }
};
/// For isa, dyn_cast, etc operations on TelemetryInfo.
typedef unsigned KindType;
/// This struct is used by TelemetryInfo to support isa<>, dyn_cast<>
/// operations.
-/// It is defined as a struct(rather than an enum) because it is
-/// expectend to be extended by subclasses which may have
+/// It is defined as a struct (rather than an enum) because it is
+/// expected to be extended by subclasses which may have
/// additional TelemetryInfo types defined to describe different events.
struct EntryKind {
static const KindType Base = 0;
};
/// TelemetryInfo is the data courier, used to move instrumented data
-/// from the tool being monitored to the Telemery framework.
+/// from the tool being monitored to the Telemetry framework.
///
/// This base class contains only the basic set of telemetry data.
-/// Downstream implementations can add more fields as needed to describe
-/// additional events.
+/// Downstream implementations can define more subclasses with
+/// additional fields to describe different events and concepts.
///
-/// For eg., The LLDB debugger can define a DebugCommandInfo subclass
+/// For example, The LLDB debugger can define a DebugCommandInfo subclass
/// which has additional fields about the debug-command being instrumented,
/// such as `CommandArguments` or `CommandName`.
struct TelemetryInfo {
// This represents a unique-id, conventionally corresponding to
- // a tools' session - ie., every time the tool starts until it exits.
+ // a tool's session - i.e., every time the tool starts until it exits.
//
- // Note: a tool could have mutliple sessions running at once, in which
+ // Note: a tool could have multiple sessions running at once, in which
// case, these shall be multiple sets of TelemetryInfo with multiple unique
// ids.
//
// Different usages can assign different types of IDs to this field.
std::string SessionId;
- // Time/memory statistics of this event.
- EventStats Stats;
-
- std::optional<ExitDescription> ExitDesc;
-
- // Counting number of entries.
- // (For each set of entries with the same SessionId, this value should
- // be unique for each entry)
- size_t Counter;
-
TelemetryInfo() = default;
- ~TelemetryInfo() = default;
+ virtual ~TelemetryInfo() = default;
- virtual json::Object serializeToJson() const;
+ virtual void serialize(Serializer &serializer) const;
// For isa, dyn_cast, etc, operations.
virtual KindType getKind() const { return EntryKind::Base; }
@@ -146,23 +107,23 @@ struct TelemetryInfo {
class Destination {
public:
virtual ~Destination() = default;
- virtual Error emitEntry(const TelemetryInfo *Entry) = 0;
- virtual std::string name() const = 0;
+ virtual Error receiveEntry(const TelemetryInfo *Entry) = 0;
+ virtual llvm::StringLiteral name() const = 0;
};
/// This class is the main interaction point between any LLVM tool
/// and this framework.
/// It is responsible for collecting telemetry data from the tool being
/// monitored and transmitting the data elsewhere.
-class Telemeter {
+class Manager {
public:
- // Invoked upon tool startup
- virtual void logStartup(llvm::StringRef ToolPath, TelemetryInfo *Entry) = 0;
-
- // Invoked upon tool exit.
- virtual void logExit(llvm::StringRef ToolPath, TelemetryInfo *Entry) = 0;
+ // Dispatch Telemetry data to the Destination(s).
+ // The argument is non-const because the Manager may add or remove
+ // data from the entry.
+ virtual Error dispatch(TelemetryInfo *Entry) = 0;
- virtual void addDestination(Destination *Destination) = 0;
+ // Register a Destination.
+ virtual void addDestination(std::unique_ptr<Destination> Destination) = 0;
};
} // namespace telemetry
diff --git a/llvm/lib/Telemetry/Telemetry.cpp b/llvm/lib/Telemetry/Telemetry.cpp
index b05a7483c7408c..b7ee3c2bb1778b 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
More information about the lldb-commits
mailing list