dolphin/Source/Core/Common/Analytics.h

193 lines
5.8 KiB
C++

// Copyright 2016 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <chrono>
#include <memory>
#include <shared_mutex>
#include <string>
#include <string_view>
#include <thread>
#include <utility>
#include <vector>
#include "Common/CommonTypes.h"
#include "Common/Event.h"
#include "Common/Flag.h"
#include "Common/HttpRequest.h"
#include "Common/SPSCQueue.h"
// Utilities for analytics reporting in Dolphin. This reporting is designed to
// provide anonymous data about how well Dolphin performs in the wild. It also
// allows developers to declare trace points in Dolphin's source code and get
// information about what games trigger these conditions.
//
// This unfortunately implements Yet Another Serialization Framework within
// Dolphin. We cannot really use ChunkFile because there is precedents for
// backwards incompatible changes in the ChunkFile format. We could use
// something like protobuf but setting up external dependencies is Hard™.
//
// Example usage:
//
// static auto s_reporter = std::make_unique<AnalyticsReporter>();
// if (user_gave_consent)
// {
// s_reporter->SetBackend(std::make_unique<MyReportingBackend>());
// }
// s_reporter->Send(s_reporter->Builder()
// .AddData("my_key", 42)
// .AddData("other_key", false));
namespace Common
{
// Generic interface for an analytics reporting backends. The main
// implementation used in Dolphin can be found in Core/DolphinAnalytics.h.
class AnalyticsReportingBackend
{
public:
virtual ~AnalyticsReportingBackend() = default;
// Called from the AnalyticsReporter backend thread.
virtual void Send(std::string report) = 0;
};
// Builder object for an analytics report.
class AnalyticsReportBuilder
{
public:
AnalyticsReportBuilder();
~AnalyticsReportBuilder() = default;
AnalyticsReportBuilder(const AnalyticsReportBuilder& other) : m_report{other.Get()} {}
AnalyticsReportBuilder(AnalyticsReportBuilder&& other) : m_report{other.Consume()} {}
const AnalyticsReportBuilder& operator=(const AnalyticsReportBuilder& other)
{
if (this != &other)
{
std::string other_report = other.Get();
std::lock_guard lk{m_lock};
m_report = std::move(other_report);
}
return *this;
}
// Append another builder to this one.
AnalyticsReportBuilder& AddBuilder(const AnalyticsReportBuilder& other)
{
// Get before locking the object to avoid deadlocks with this += this.
std::string other_report = other.Get();
std::lock_guard lk{m_lock};
m_report += other_report;
return *this;
}
template <typename T>
AnalyticsReportBuilder& AddData(std::string_view key, const T& value)
{
std::lock_guard lk{m_lock};
AppendSerializedValue(&m_report, key);
AppendSerializedValue(&m_report, value);
return *this;
}
template <typename T>
AnalyticsReportBuilder& AddData(std::string_view key, const std::vector<T>& value)
{
std::lock_guard lk{m_lock};
AppendSerializedValue(&m_report, key);
AppendSerializedValueVector(&m_report, value);
return *this;
}
std::string Get() const
{
std::shared_lock lk{m_lock};
return m_report;
}
// More efficient version of Get().
std::string Consume()
{
std::lock_guard lk{m_lock};
return std::move(m_report);
}
protected:
static void AppendSerializedValue(std::string* report, std::string_view v);
static void AppendSerializedValue(std::string* report, const char* v);
static void AppendSerializedValue(std::string* report, bool v);
static void AppendSerializedValue(std::string* report, u64 v);
static void AppendSerializedValue(std::string* report, s64 v);
static void AppendSerializedValue(std::string* report, u32 v);
static void AppendSerializedValue(std::string* report, s32 v);
static void AppendSerializedValue(std::string* report, float v);
static void AppendSerializedValueVector(std::string* report, const std::vector<u32>& v);
mutable std::shared_mutex m_lock;
std::string m_report;
};
class AnalyticsReporter
{
public:
AnalyticsReporter();
~AnalyticsReporter();
// Sets a reporting backend and enables sending reports. Do not set a remote
// backend without user consent.
void SetBackend(std::unique_ptr<AnalyticsReportingBackend> backend)
{
m_backend = std::move(backend);
m_reporter_event.Set(); // In case reports are waiting queued.
}
// Gets the base report builder which is closed for each subsequent report
// being sent. DO NOT use this builder to send a report. Only use it to add
// new fields that should be globally available.
AnalyticsReportBuilder& BaseBuilder() { return m_base_builder; }
// Gets a cloned builder that can be used to send a report.
AnalyticsReportBuilder Builder() const { return m_base_builder; }
// Enqueues a report for sending. Consumes the report builder.
void Send(AnalyticsReportBuilder&& report);
// For convenience.
void Send(AnalyticsReportBuilder& report) { Send(std::move(report)); }
protected:
void ThreadProc();
std::shared_ptr<AnalyticsReportingBackend> m_backend;
AnalyticsReportBuilder m_base_builder;
std::thread m_reporter_thread;
Common::Event m_reporter_event;
Common::Flag m_reporter_stop_request;
SPSCQueue<std::string> m_reports_queue;
};
// Analytics backend to be used for debugging purpose, which dumps reports to
// stdout.
class StdoutAnalyticsBackend : public AnalyticsReportingBackend
{
public:
void Send(std::string report) override;
};
// Analytics backend that POSTs data to a remote HTTP(s) endpoint. WARNING:
// remember to get explicit user consent before using.
class HttpAnalyticsBackend : public AnalyticsReportingBackend
{
public:
explicit HttpAnalyticsBackend(std::string endpoint);
~HttpAnalyticsBackend() override;
void Send(std::string report) override;
protected:
std::string m_endpoint;
HttpRequest m_http{std::chrono::seconds{5}};
};
} // namespace Common