From 071c34fb35693b390640284e6cfe4c15f71a6ddf Mon Sep 17 00:00:00 2001 From: Erwan Viollet Date: Mon, 16 Mar 2026 12:13:59 +0100 Subject: [PATCH 01/18] Integrate dd-win-prof profiling feature into dd-sdk-cpp - CMake: DD_ENABLE_PROFILER option, FetchContent for dd-win-prof - Public API: Profiling::Register() following Rum/Logging patterns - RUM context: RumContextSnapshot + callback wiring to dd-win-prof - Stub UploadThread_PrepareReport (dd-win-prof uploads independently) Inspired from @aforsythe 's branch & co-authored using claude-4.6 --- CMakeLists.txt | 10 +- cmake/DatadogConfig.cmake.in | 4 + cmake/DatadogConvenience.cmake | 53 +++++++-- cmake/profiler.cmake | 38 ++++++ include-cpp/datadog/core.hpp | 1 + include-cpp/datadog/profiling.hpp | 104 +++++++++++++++++ include-cpp/datadog/rum.hpp | 85 ++++++++++++++ src/CMakeLists.txt | 11 ++ src/datadog/cpp/profiling.cpp | 76 ++++++++++++ src/datadog/cpp/rum.cpp | 5 + src/datadog/impl/core/feature_types/rum.hpp | 31 +++-- .../impl/features/profiling/profiling.cpp | 108 ++++++++++++++++++ .../impl/features/profiling/profiling.hpp | 68 +++++++++++ src/datadog/impl/rum/context.cpp | 15 ++- src/datadog/impl/rum/rum.cpp | 33 +++++- src/datadog/impl/rum/rum.hpp | 6 + 16 files changed, 619 insertions(+), 29 deletions(-) create mode 100644 cmake/profiler.cmake create mode 100644 include-cpp/datadog/profiling.hpp create mode 100644 src/datadog/cpp/profiling.cpp create mode 100644 src/datadog/impl/features/profiling/profiling.cpp create mode 100644 src/datadog/impl/features/profiling/profiling.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5faef8e1..2afb5c81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,10 @@ endif() set(DD_ENABLE_SANITIZERS "${DD_ENABLE_SANITIZERS}" CACHE STRING "Comma-separated list of sanitizers to enable, e.g. 'ASan,UBSan', 'MSan,UBSan', 'TSan'") option(DD_ENABLE_ASSERTS "Enable assertions in Datadog SDK implementation" ${DD_DEVELOPMENT}) option(DD_ENABLE_TEST_ALLOCATION_TRACKING "Enable allocation tracking (via instrumented new/delete) in test binaries" ON) +option(DD_ENABLE_PROFILER "Enable integration with Datadog Windows Profiler" OFF) +if(DD_ENABLE_PROFILER AND NOT WIN32) + message(FATAL_ERROR "DD_ENABLE_PROFILER is only supported on Windows") +endif() # C API targets C99; C++ API targets C++17 if(DD_IS_TOP_LEVEL) @@ -57,9 +61,9 @@ if(DD_IS_TOP_LEVEL) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - # Crashpad requires C++20, so building the SDK with crashpad support requires - # targeting C++20 - if(DD_CRASH_MODE STREQUAL "crashpad") + # Crashpad and dd-win-prof both require C++20, so building the SDK with either + # enabled requires targeting C++20 + if(DD_CRASH_MODE STREQUAL "crashpad" OR DD_ENABLE_PROFILER) set(CMAKE_CXX_STANDARD 20) endif() endif() diff --git a/cmake/DatadogConfig.cmake.in b/cmake/DatadogConfig.cmake.in index 4e4a12bf..74a33b5e 100644 --- a/cmake/DatadogConfig.cmake.in +++ b/cmake/DatadogConfig.cmake.in @@ -27,6 +27,10 @@ if(DATADOG_BUILT_WITH_DD_HTTP_USE_SYSTEM_LIBCURL) find_dependency(CURL REQUIRED) endif() +# If the SDK was built with profiler support, expose this to consumers so that +# datadog_enable() can invoke dd_win_prof_copy_runtime_deps() as needed +set(DATADOG_BUILT_WITH_DD_ENABLE_PROFILER @DD_ENABLE_PROFILER@) + # If the SDK was built with Crashpad support, include CrashpadConfig.cmake, ensuring # that crashpad::client and crashpad::handler targets are defined set(DATADOG_BUILT_WITH_DD_CRASH_MODE "@DD_CRASH_MODE@") diff --git a/cmake/DatadogConvenience.cmake b/cmake/DatadogConvenience.cmake index 6ec9fce1..90c9455f 100644 --- a/cmake/DatadogConvenience.cmake +++ b/cmake/DatadogConvenience.cmake @@ -39,18 +39,32 @@ function(datadog_enable target) message(FATAL_ERROR "datadog_enable(): target crashpad::handler does not exist") endif() endif() + + # If the SDK was built with profiler support, copy the required runtime DLLs + # (dd-win-prof.dll and datadog_profiling_ffi.dll) alongside the application + # binary so they can be found at runtime. + if(DD_ENABLE_PROFILER OR DATADOG_BUILT_WITH_DD_ENABLE_PROFILER) + if(COMMAND dd_win_prof_copy_runtime_deps) + dd_win_prof_copy_runtime_deps(${target}) + else() + message(FATAL_ERROR + "datadog_enable(): dd_win_prof_copy_runtime_deps is not available; " + "ensure dd-win-prof was fetched before calling datadog_enable()") + endif() + endif() endfunction() -# datadog_install(bin): +# datadog_install(destination): # -# - (if Crashpad support is enabled) ensures that the crashpad_handler executable will -# be copied to bin/ alongside your application +# Installs runtime dependencies required by the SDK alongside your application: +# - (if Crashpad support is enabled) copies the crashpad_handler executable +# - (if Profiler support is enabled) copies dd-win-prof.dll and datadog_profiling_ffi.dll # # Call this function after defining `install(TARGETS my-app ...)` for your app, -# specifying the destination directory for your application's binaries in lieu of `bin`. -# You may elect not to call this function if a.) your project does not use CMake -# installation rules, b.) you don't need Crashpad support, or c.) you are ensuring that -# the crashpad_handler exectuable makes its way into your builds through other means. +# specifying the destination directory for your application's binaries (typically "bin"). +# You may elect not to call this function if: a.) your project does not use CMake +# installation rules, b.) you don't need Crashpad or Profiler support, or c.) you are +# handling runtime dependencies through other means. # function(datadog_install destination) # If the SDK was built with crashpad support, install the crashpad_handler @@ -63,4 +77,29 @@ function(datadog_install destination) message(FATAL_ERROR "datadog_install(): target crashpad::handler does not exist") endif() endif() + + # If the SDK was built with profiler support, install the required runtime DLLs + # to the destination directory + if(DD_ENABLE_PROFILER OR DATADOG_BUILT_WITH_DD_ENABLE_PROFILER) + if(TARGET dd-win-prof) + # Try to get imported location (for find_package case) + get_target_property(DD_WIN_PROF_DLL dd-win-prof IMPORTED_LOCATION) + + if(DD_WIN_PROF_DLL AND NOT DD_WIN_PROF_DLL MATCHES "-NOTFOUND$") + # Imported target case: install the DLL from its imported location + install(PROGRAMS ${DD_WIN_PROF_DLL} DESTINATION ${destination}) + + get_filename_component(DD_WIN_PROF_DIR ${DD_WIN_PROF_DLL} DIRECTORY) + install(PROGRAMS "${DD_WIN_PROF_DIR}/datadog_profiling_ffi.dll" DESTINATION ${destination}) + else() + # FetchContent case: use generator expressions to get target files + install(FILES "$" DESTINATION ${destination}) + if(TARGET libdatadog_dynamic) + install(FILES "$" DESTINATION ${destination}) + endif() + endif() + else() + message(FATAL_ERROR "datadog_install(): target dd-win-prof does not exist") + endif() + endif() endfunction() diff --git a/cmake/profiler.cmake b/cmake/profiler.cmake new file mode 100644 index 00000000..e0a3cda4 --- /dev/null +++ b/cmake/profiler.cmake @@ -0,0 +1,38 @@ +include(FetchContent) + +# Allow overriding the GitHub fetch with a local clone of dd-win-prof. When +# set, CMake skips the network fetch and uses the local source directory +# directly. +set(DD_WIN_PROF_SOURCE_DIR "" CACHE PATH + "Local path to dd-win-prof source tree (overrides GitHub fetch)") + +if(DD_WIN_PROF_SOURCE_DIR) + FetchContent_Declare( + dd-win-prof + SOURCE_DIR "${DD_WIN_PROF_SOURCE_DIR}" + ) +else() + FetchContent_Declare( + dd-win-prof + GIT_REPOSITORY https://github.com/DataDog/dd-win-prof.git + GIT_TAG 74b9c6f + ) +endif() + +FetchContent_MakeAvailable(dd-win-prof) + +# Link dd-win-prof into the SDK as a public dependency so that consumers can +# include dd-win-prof.h directly if needed. +target_link_libraries(dd_native PUBLIC dd-win-prof) + +if(DD_BUILD_INSTALL) + # Install dd-win-prof.dll to bin/ so it ships alongside the SDK. Include it in + # the DatadogTargets export set since dd_native depends on it. + install(TARGETS dd-win-prof + EXPORT DatadogTargets + RUNTIME DESTINATION bin + ) + + # Install datadog_profiling_ffi.dll, which dd-win-prof.dll loads at runtime. + install(FILES "$" DESTINATION bin) +endif() diff --git a/include-cpp/datadog/core.hpp b/include-cpp/datadog/core.hpp index e9323f1f..8a17ac96 100644 --- a/include-cpp/datadog/core.hpp +++ b/include-cpp/datadog/core.hpp @@ -398,6 +398,7 @@ class Core { friend class Logging; friend class Rum; + friend class Profiling; friend class CrashReporting; friend struct ::CoreTestHarness; }; diff --git a/include-cpp/datadog/profiling.hpp b/include-cpp/datadog/profiling.hpp new file mode 100644 index 00000000..585a4120 --- /dev/null +++ b/include-cpp/datadog/profiling.hpp @@ -0,0 +1,104 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025-Present Datadog, Inc. + +#pragma once + +#include +#include + +#include "datadog/api.hpp" +#include "datadog/core.hpp" + +// Forward declare the RumContextSnapshot type (defined in rum.hpp) +namespace datadog { +struct RumContextSnapshot; +} + +// Forward declare dd-win-prof's config struct (defined in dd-win-prof.h) +struct _ProfilerConfig; +typedef struct _ProfilerConfig ProfilerConfig; + +namespace datadog { + +// Forward declarations +namespace impl { +class Profiling; +} // namespace impl + +/** + * Interface to the Datadog SDK's profiling feature. + * + * Profiling uses the dd-win-prof library to collect CPU and wall-time profiles + * on Windows. Unlike RUM and Logging, the profiler manages its own upload + * pipeline via libdatadog. + * + * Configuration is done via dd-win-prof's ProfilerConfig struct (from + * dd-win-prof.h). The SDK does not wrap or duplicate that struct — callers + * fill it directly and pass a pointer to Register(). + */ +class Profiling { + private: + struct PrivateCtorTag {}; + + public: + // Callers should use Profiling::Register + explicit Profiling(PrivateCtorTag); + explicit Profiling( + std::shared_ptr&& impl, + DiagnosticHandler diagnostic_handler, + DiagnosticLevel diagnostic_threshold, + PrivateCtorTag + ); + DATADOG_API ~Profiling(); + + public: + /** + * Registers the profiling feature with the core of the Datadog SDK. + * + * @param core The SDK core instance. + * @param config Pointer to a ProfilerConfig struct (from dd-win-prof.h). + * The struct is read during Register() and does not need to outlive the + * call. Pass nullptr to use dd-win-prof's defaults (env vars + built-in + * defaults). + * + * On Core::Start(), the profiler will be set up and started. On Core::Stop(), + * the profiler will be stopped. + */ + DATADOG_API static std::shared_ptr Register( + const std::shared_ptr& core, const ProfilerConfig* config = nullptr + ); + + /** + * Creates a callback suitable for use with RumConfig::SetContextChangeCallback(). + * + * When RUM context changes, this callback forwards the new context to + * dd-win-prof so that profiling data can be correlated with RUM sessions and + * views. The callback holds a weak reference to the Profiling instance, so it + * is safe to use even if the Profiling feature is destroyed before RUM. + * + * Usage: + * auto profiling = Profiling::Register(core, &profilerConfig); + * auto rumConfig = RumConfig(appId) + * .SetContextChangeCallback(Profiling::CreateRumContextBridge(profiling)); + * auto rum = Rum::Register(core, rumConfig); + */ + DATADOG_API static std::function CreateRumContextBridge( + const std::shared_ptr& profiling + ); + + private: + // Forbid copying/moving: we use std::shared_ptr at the API boundary + Profiling(const Profiling&) = delete; + Profiling& operator=(const Profiling&) = delete; + Profiling(Profiling&&) = delete; + Profiling& operator=(Profiling&&) = delete; + + std::shared_ptr _impl; + DiagnosticHandler _diagnostic_handler; + DiagnosticLevel _diagnostic_threshold; +}; + +} // namespace datadog diff --git a/include-cpp/datadog/rum.hpp b/include-cpp/datadog/rum.hpp index 4b035877..d18885af 100644 --- a/include-cpp/datadog/rum.hpp +++ b/include-cpp/datadog/rum.hpp @@ -7,6 +7,8 @@ #pragma once #include +#include +#include #include #include #include @@ -24,6 +26,76 @@ class Rum; struct RumScopeDependencies; } // namespace impl +/** + * Snapshot of essential RUM context state, capturing the current application, + * session, view, and action identifiers. + * + * This structure is provided to context change callbacks to allow external + * libraries to correlate their data with RUM state. + */ +struct RumContextSnapshot { + /** + * The RUM application ID. UUID::Zero if RUM is not initialized. + */ + UUID application_id; + + /** + * The current RUM session ID. UUID::Zero if no session is active. + */ + UUID session_id; + + /** + * The current RUM view ID. UUID::Zero if no view is active. + */ + UUID view_id; + + /** + * Name of the current RUM view. Points to an empty string if no view is + * active (i.e., when `view_id` is UUID::Zero). When a view is active, + * contains the explicit name provided to StartView(), or the key if no name + * was provided. + * + * This pointer is valid only for the duration of the synchronous callback. + * If the string value is needed beyond the callback's lifetime, it must be + * copied. + */ + const char* view_name; + + /** + * The current RUM action ID. UUID::Zero if no action is active. + */ + UUID action_id; + + bool operator==(const RumContextSnapshot& other) const { + if (application_id != other.application_id || session_id != other.session_id || + view_id != other.view_id || action_id != other.action_id) { + return false; + } + if (view_name == other.view_name) { + return true; + } + if (!view_name || !other.view_name) { + return false; + } + return std::strcmp(view_name, other.view_name) == 0; + } + + bool operator!=(const RumContextSnapshot& other) const { return !(*this == other); } +}; + +/** + * Callback function invoked when RUM context changes. + * + * The callback receives a snapshot of the new RUM context whenever any of the + * context UUIDs (application_id, session_id, view_id, action_id) changes value, + * including transitions to/from UUID::Zero. + * + * The callback is invoked synchronously on the thread that triggered the + * context change. Callback implementations should be fast and non-blocking. + */ +using RumContextChangeCallback = std::function; + + /** * Configures the details of the RUM feature upon initialization. */ @@ -35,6 +107,7 @@ struct RumConfig { private: UUID application_id; // UUID::Zero if uninitialized or invalid float session_sample_rate{100.0f}; + RumContextChangeCallback context_change_callback{nullptr}; public: /** @@ -68,6 +141,18 @@ struct RumConfig { * intake; at 0.0, no RUM events are generated. Default is 100.0. */ DATADOG_API RumConfig& SetSessionSampleRate(float value); + + /** + * Sets a callback to be invoked whenever RUM context changes. + * + * The callback will be called with a snapshot of the new RUM context whenever + * any of the context UUIDs (application_id, session_id, view_id, action_id) + * changes value. The callback is invoked synchronously on the thread calling + * RUM API methods. + * + * Pass nullptr to clear any previously-set callback. + */ + DATADOG_API RumConfig& SetContextChangeCallback(RumContextChangeCallback value); }; enum class RumActionType : uint8_t { Tap, Click, Scroll, Swipe, Custom }; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fed7c08a..8e167077 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -272,3 +272,14 @@ if(DD_BUILD_INSTALL) RUNTIME DESTINATION bin ) endif() + +# If profiling support is enabled, fetch dd-win-prof and link it into the SDK. +# This must come after dd_native is fully configured so that the profiler module +# can read and modify its compile/link settings. +if(DD_ENABLE_PROFILER) + target_sources(dd_native PRIVATE + datadog/cpp/profiling.cpp + datadog/impl/features/profiling/profiling.cpp + ) + include(${DD_SDK_ROOT_DIR}/cmake/profiler.cmake) +endif() diff --git a/src/datadog/cpp/profiling.cpp b/src/datadog/cpp/profiling.cpp new file mode 100644 index 00000000..a0fda76a --- /dev/null +++ b/src/datadog/cpp/profiling.cpp @@ -0,0 +1,76 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025-Present Datadog, Inc. + +#include "datadog/profiling.hpp" + +#include "datadog/core.hpp" +#include "datadog/rum.hpp" + +#include "datadog/impl/core/core.hpp" +#include "datadog/impl/core/feature.hpp" +#include "datadog/impl/diagnostics.hpp" +#include "datadog/impl/features/profiling/profiling.hpp" + +namespace datadog { + +Profiling::Profiling(Profiling::PrivateCtorTag) + : _impl(nullptr), + _diagnostic_handler(nullptr), + _diagnostic_threshold(DiagnosticLevel::Error) {} + +Profiling::Profiling( + std::shared_ptr&& impl, + DiagnosticHandler diagnostic_handler, + DiagnosticLevel diagnostic_threshold, + Profiling::PrivateCtorTag +) + : _impl(std::move(impl)), + _diagnostic_handler(std::move(diagnostic_handler)), + _diagnostic_threshold(diagnostic_threshold) {} + +Profiling::~Profiling() = default; + +std::shared_ptr Profiling::Register( + const std::shared_ptr& core, const ProfilerConfig* config +) { + // Return a no-op Profiling interface if called without a valid core + if (!core || !core->_impl) { + return std::make_shared(Profiling::PrivateCtorTag{}); + } + + // Initialize our profiling feature implementation (deep-copies config) + auto profiling_impl = std::make_shared(config); + + // Register the feature with the core, returning a no-op interface on failure + if (!core->_impl->RegisterFeature(profiling_impl)) { + return std::make_shared(Profiling::PrivateCtorTag{}); + } + + return std::make_shared( + std::move(profiling_impl), + core->_diagnostic_handler, + core->_diagnostic_threshold, + Profiling::PrivateCtorTag{} + ); +} + +std::function Profiling::CreateRumContextBridge( + const std::shared_ptr& profiling +) { + if (!profiling || !profiling->_impl) { + return nullptr; + } + + // Capture a weak_ptr so the callback doesn't keep profiling alive + std::weak_ptr weak_impl = profiling->_impl; + return [weak_impl](const RumContextSnapshot& context) { + if (auto impl = weak_impl.lock()) { + impl->OnRumContextChanged(context); + } + }; +} + +} // namespace datadog diff --git a/src/datadog/cpp/rum.cpp b/src/datadog/cpp/rum.cpp index b916055a..68ce074d 100644 --- a/src/datadog/cpp/rum.cpp +++ b/src/datadog/cpp/rum.cpp @@ -47,6 +47,11 @@ RumConfig& RumConfig::SetSessionSampleRate(float value) { return *this; } +RumConfig& RumConfig::SetContextChangeCallback(RumContextChangeCallback value) { + context_change_callback = std::move(value); + return *this; +} + Rum::Rum(Rum::PrivateCtorTag) : _impl(nullptr), _diagnostic_handler(nullptr), diff --git a/src/datadog/impl/core/feature_types/rum.hpp b/src/datadog/impl/core/feature_types/rum.hpp index 81e372db..e4647f47 100644 --- a/src/datadog/impl/core/feature_types/rum.hpp +++ b/src/datadog/impl/core/feature_types/rum.hpp @@ -13,10 +13,10 @@ #include "datadog/rum.hpp" #include "datadog/uuid.hpp" -#include "datadog/impl/core/events/enum.hpp" -#include "datadog/impl/core/events/omissible.hpp" -#include "datadog/impl/core/events/struct.hpp" -#include "datadog/impl/core/events/timestamp.hpp" +#include "datadog/impl/events/enum.hpp" +#include "datadog/impl/events/omissible.hpp" +#include "datadog/impl/events/struct.hpp" +#include "datadog/impl/events/timestamp.hpp" /* ===================== INSTRUCTIONS FOR VALIDATING THIS FILE ======================== @@ -122,15 +122,28 @@ namespace datadog::impl { * event payloads with RUM data, which facilitates correlation in the backend. */ struct RumFeatureContext { - UUID application_id; // UUID::Zero if RUM not initialized - UUID session_id; // UUID::Zero if no active session - UUID view_id; // UUID::Zero if no active view - UUID action_id; // UUID::Zero if no active action + UUID application_id; // UUID::Zero if RUM not initialized + UUID session_id; // UUID::Zero if no active session + UUID view_id; // UUID::Zero if no active view + UUID action_id; // UUID::Zero if no active action + std::string view_name; // Empty if no active view + + /** + * Converts this internal RumFeatureContext to the public RumContext type. + */ + datadog::RumContextSnapshot ToPublicContext() const { + return datadog::RumContextSnapshot{ + application_id, session_id, view_id, view_name.c_str(), action_id + }; + } bool operator==(const RumFeatureContext& other) const { return application_id == other.application_id && session_id == other.session_id && - view_id == other.view_id && action_id == other.action_id; + view_id == other.view_id && action_id == other.action_id && + view_name == other.view_name; } + + bool operator!=(const RumFeatureContext& other) const { return !(*this == other); } }; DATADOG_STRING_ENUM( diff --git a/src/datadog/impl/features/profiling/profiling.cpp b/src/datadog/impl/features/profiling/profiling.cpp new file mode 100644 index 00000000..b6537957 --- /dev/null +++ b/src/datadog/impl/features/profiling/profiling.cpp @@ -0,0 +1,108 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025-Present Datadog, Inc. + +#include "datadog/impl/features/profiling/profiling.hpp" + +#include +#include + +#include "datadog/uuid.hpp" + +namespace datadog::impl { + +// Helper: copy a const char* into owned storage, return pointer to owned string +// (or nullptr if source is null/empty) +static const char* CopyString(std::string& storage, const char* src) { + if (src == nullptr || src[0] == '\0') { + storage.clear(); + return nullptr; + } + storage = src; + return storage.c_str(); +} + +Profiling::Profiling(const ProfilerConfig* config) { + if (config != nullptr) { + _has_config = true; + + // Deep-copy the struct, then repoint const char* fields at our owned strings + _config = *config; + + _config.url = CopyString(_url, config->url); + _config.apiKey = CopyString(_api_key, config->apiKey); + _config.serviceEnvironment = CopyString(_service_environment, config->serviceEnvironment); + _config.serviceName = CopyString(_service_name, config->serviceName); + _config.serviceVersion = CopyString(_service_version, config->serviceVersion); + _config.tags = CopyString(_tags, config->tags); + _config.pprofOutputDirectory = CopyString(_pprof_output_directory, config->pprofOutputDirectory); + } +} + +FeatureId Profiling::GetId() const { + return CreateFeatureId("PROF"); +} + +std::string_view Profiling::GetName() const { + return "profiling"; +} + +void Profiling::Start() { + if (_has_config) { + _profiler_setup = SetupProfiler(&_config); + } else { + // No config provided — set up with defaults (size field required) + ProfilerConfig defaults{}; + std::memset(&defaults, 0, sizeof(defaults)); + defaults.size = sizeof(ProfilerConfig); + _profiler_setup = SetupProfiler(&defaults); + } + + if (_profiler_setup) { + StartProfiler(); + } +} + +void Profiling::Stop() { + if (_profiler_setup) { + StopProfiler(); + _profiler_setup = false; + } +} + +std::optional Profiling::UploadThread_PrepareReport( + const HttpContext& /*context*/, BatchReader& /*reader*/ +) { + // dd-win-prof manages its own upload pipeline via libdatadog. + return std::nullopt; +} + +void Profiling::OnRumContextChanged(const datadog::RumContextSnapshot& context) { + if (!_profiler_setup) { + return; + } + + // Stable context: application_id and session_id + const std::string app_id = context.application_id.ToString(); + const std::string session_id = context.session_id.ToString(); + + RumSessionContext session_ctx{}; + session_ctx.application_id = app_id.c_str(); + session_ctx.session_id = session_id.c_str(); + SetRumSession(&session_ctx); + + // Volatile context: view_id and view_name + if (context.view_id == datadog::UUID::Zero) { + SetRumView(nullptr); + } else { + const std::string view_id = context.view_id.ToString(); + RumViewValues view_vals{}; + view_vals.view_id = view_id.c_str(); + view_vals.view_name = context.view_name; + SetRumView(&view_vals); + } +} + +} // namespace datadog::impl diff --git a/src/datadog/impl/features/profiling/profiling.hpp b/src/datadog/impl/features/profiling/profiling.hpp new file mode 100644 index 00000000..b8ea9616 --- /dev/null +++ b/src/datadog/impl/features/profiling/profiling.hpp @@ -0,0 +1,68 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025-Present Datadog, Inc. + +#pragma once + +#include +#include + +#include "datadog/rum.hpp" + +#include "datadog/impl/core/feature.hpp" + +#include "dd-win-prof.h" + +namespace datadog::impl { + +/** + * Internal implementation of the profiling feature. + * + * Wraps the dd-win-prof C API (SetupProfiler, StartProfiler, StopProfiler) and + * integrates it into the SDK's feature lifecycle. Unlike RUM and Logging, the + * profiler manages its own data upload via libdatadog, so + * UploadThread_PrepareReport always returns nullopt. + */ +class Profiling final : public Feature { + public: + // config may be nullptr (use dd-win-prof defaults) + explicit Profiling(const ProfilerConfig* config); + + FeatureId GetId() const override; + std::string_view GetName() const override; + + std::optional UploadThread_PrepareReport( + const HttpContext& context, BatchReader& reader + ) override; + + /** + * Forwards RUM context changes to dd-win-prof's SetRumSession()/SetRumView(). + * Converts UUIDs to string representations as expected by the C API. + */ + void OnRumContextChanged(const datadog::RumContextSnapshot& context); + + protected: + void Start() override; + void Stop() override; + + private: + // We store a deep copy of the config so string pointers remain valid. + // The original ProfilerConfig uses const char* — we own the strings here. + bool _has_config{false}; + ProfilerConfig _config{}; + + // Owned string storage for _config's const char* fields + std::string _url; + std::string _api_key; + std::string _service_environment; + std::string _service_name; + std::string _service_version; + std::string _tags; + std::string _pprof_output_directory; + + bool _profiler_setup{false}; +}; + +} // namespace datadog::impl diff --git a/src/datadog/impl/rum/context.cpp b/src/datadog/impl/rum/context.cpp index 99b32399..f54aaeb3 100644 --- a/src/datadog/impl/rum/context.cpp +++ b/src/datadog/impl/rum/context.cpp @@ -31,24 +31,31 @@ RumFeatureContext RumContext::ToFeatureContext() const { // If we don't have an active session, or if the session isn't sampled, populate the // application ID but leave it at that if (session_id == UUID::Zero || !session_is_active || !session_is_sampled) { - return RumFeatureContext{application_id, UUID::Zero, UUID::Zero, UUID::Zero}; + return RumFeatureContext{application_id, UUID::Zero, UUID::Zero, UUID::Zero, {}}; } // We have a valid session: if there's no active view, just set application and // session ID if (active_view_id == UUID::Zero) { - return RumFeatureContext{application_id, session_id, UUID::Zero, UUID::Zero}; + return RumFeatureContext{application_id, session_id, UUID::Zero, UUID::Zero, {}}; } + // Determine view name: prefer explicit name, fall back to key + std::string view_name_str = + active_view_name.empty() ? active_view_key : active_view_name; + // We have an active view: if there's no active action, set application, session, and // view state if (active_action_id == UUID::Zero) { - return RumFeatureContext{application_id, session_id, active_view_id, UUID::Zero}; + return RumFeatureContext{ + application_id, session_id, active_view_id, UUID::Zero, std::move(view_name_str) + }; } // We have an active action return RumFeatureContext{ - application_id, session_id, active_view_id, active_action_id + application_id, session_id, active_view_id, active_action_id, + std::move(view_name_str) }; } diff --git a/src/datadog/impl/rum/rum.cpp b/src/datadog/impl/rum/rum.cpp index 0d4d7f6e..43fd8e98 100644 --- a/src/datadog/impl/rum/rum.cpp +++ b/src/datadog/impl/rum/rum.cpp @@ -7,7 +7,7 @@ #include "datadog/impl/rum/rum.hpp" #include -#include +#include #include #include @@ -18,7 +18,9 @@ Rum::Rum(const RumConfig& config, const platform::IClock& clock) : _global_attributes(8), _deps(config, clock), _application(_deps), - _application_snapshot() {} + _application_snapshot(), + _context_change_callback(config.context_change_callback), + _previous_context() {} std::optional Rum::UploadThread_PrepareReport( const HttpContext& context, BatchReader& reader @@ -59,10 +61,21 @@ void Rum::Start() { }; void Rum::Stop() { - // Note: _application will be destroyed when Rum is destroyed (after Core joins - // the context thread). In-flight lambdas can safely access _application as long as - // weak_ptr.lock() succeeds. CoreContext is reset by Core::Start() at the top of each - // new run, so no context mutation is needed here. + // On Core shutdown, ensure that any RUM context is purged, so that if the SDK is + // restarted it won't inherit old state + if (_scope) { + _scope->UpdateContext([](CoreContext& ctx) { ctx.rum.reset(); }); + } + + // Fully reinitialize RUM application state to clear all sessions/views/etc. + _application = RumApplicationScope(_deps); + + // Clear the FeatureScope reference in RumScopeDependencies: scopes should no longer + // generate events + _deps.OnStop(); + + // Reset previous context to ensure callback fires on next SDK start + _previous_context = RumFeatureContext{}; } void Rum::AddAttribute(std::string_view name, const Attribute& value) { @@ -219,6 +232,14 @@ void Rum::DispatchAsync(const RumCommand& command) { // command; write the relevant UUIDs to the global RumFeatureContext, so that // other features can enrich their events with RUM data ctx.rum = rum->_application_snapshot.ToFeatureContext(); + + // Invoke the context change callback if the context has changed + // TODO: The Profiling feature should instead listen for ContextChangedMessage + if (_context_change_callback && new_context != _previous_context) { + _previous_context = new_context; + const datadog::RumContextSnapshot callback_context = new_context.ToPublicContext(); + _context_change_callback(callback_context); + } } }); } diff --git a/src/datadog/impl/rum/rum.hpp b/src/datadog/impl/rum/rum.hpp index 0f2f0a1f..9de28adc 100644 --- a/src/datadog/impl/rum/rum.hpp +++ b/src/datadog/impl/rum/rum.hpp @@ -180,6 +180,12 @@ class Rum final : public Feature { // HTTP request details used on upload; owned by the upload thread std::string _request_url; std::string _request_headers; + + // Callback invoked when RUM context changes + RumContextChangeCallback _context_change_callback; + + // Previous RUM context for change detection + RumFeatureContext _previous_context; }; } // namespace datadog::impl From 3fbb5eb266e69d8cba8452e8822a93f8044e735d Mon Sep 17 00:00:00 2001 From: Erwan Viollet Date: Mon, 16 Mar 2026 17:59:32 +0100 Subject: [PATCH 02/18] Simplify RUM x Profiling setup Automatically wire profiling callback if RUM & profiling are active co-authored using claude-4.6 --- include-cpp/datadog/profiling.hpp | 24 -------------- include-cpp/datadog/rum.hpp | 14 --------- src/CMakeLists.txt | 1 + src/datadog/cpp/profiling.cpp | 17 ---------- src/datadog/cpp/rum.cpp | 5 --- src/datadog/impl/core/core.cpp | 52 +++++++++++++++++++++++++++++++ src/datadog/impl/core/core.hpp | 7 +++++ src/datadog/impl/rum/rum.cpp | 5 ++- src/datadog/impl/rum/rum.hpp | 10 +++++- 9 files changed, 73 insertions(+), 62 deletions(-) diff --git a/include-cpp/datadog/profiling.hpp b/include-cpp/datadog/profiling.hpp index 585a4120..73da9350 100644 --- a/include-cpp/datadog/profiling.hpp +++ b/include-cpp/datadog/profiling.hpp @@ -6,17 +6,11 @@ #pragma once -#include #include #include "datadog/api.hpp" #include "datadog/core.hpp" -// Forward declare the RumContextSnapshot type (defined in rum.hpp) -namespace datadog { -struct RumContextSnapshot; -} - // Forward declare dd-win-prof's config struct (defined in dd-win-prof.h) struct _ProfilerConfig; typedef struct _ProfilerConfig ProfilerConfig; @@ -71,24 +65,6 @@ class Profiling { const std::shared_ptr& core, const ProfilerConfig* config = nullptr ); - /** - * Creates a callback suitable for use with RumConfig::SetContextChangeCallback(). - * - * When RUM context changes, this callback forwards the new context to - * dd-win-prof so that profiling data can be correlated with RUM sessions and - * views. The callback holds a weak reference to the Profiling instance, so it - * is safe to use even if the Profiling feature is destroyed before RUM. - * - * Usage: - * auto profiling = Profiling::Register(core, &profilerConfig); - * auto rumConfig = RumConfig(appId) - * .SetContextChangeCallback(Profiling::CreateRumContextBridge(profiling)); - * auto rum = Rum::Register(core, rumConfig); - */ - DATADOG_API static std::function CreateRumContextBridge( - const std::shared_ptr& profiling - ); - private: // Forbid copying/moving: we use std::shared_ptr at the API boundary Profiling(const Profiling&) = delete; diff --git a/include-cpp/datadog/rum.hpp b/include-cpp/datadog/rum.hpp index d18885af..4959b280 100644 --- a/include-cpp/datadog/rum.hpp +++ b/include-cpp/datadog/rum.hpp @@ -107,8 +107,6 @@ struct RumConfig { private: UUID application_id; // UUID::Zero if uninitialized or invalid float session_sample_rate{100.0f}; - RumContextChangeCallback context_change_callback{nullptr}; - public: /** * Initializes a new RUM configuration object with all required values. @@ -141,18 +139,6 @@ struct RumConfig { * intake; at 0.0, no RUM events are generated. Default is 100.0. */ DATADOG_API RumConfig& SetSessionSampleRate(float value); - - /** - * Sets a callback to be invoked whenever RUM context changes. - * - * The callback will be called with a snapshot of the new RUM context whenever - * any of the context UUIDs (application_id, session_id, view_id, action_id) - * changes value. The callback is invoked synchronously on the thread calling - * RUM API methods. - * - * Pass nullptr to clear any previously-set callback. - */ - DATADOG_API RumConfig& SetContextChangeCallback(RumContextChangeCallback value); }; enum class RumActionType : uint8_t { Tap, Click, Scroll, Swipe, Custom }; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8e167077..1be25295 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -277,6 +277,7 @@ endif() # This must come after dd_native is fully configured so that the profiler module # can read and modify its compile/link settings. if(DD_ENABLE_PROFILER) + target_compile_definitions(dd_native PRIVATE DD_ENABLE_PROFILER=1) target_sources(dd_native PRIVATE datadog/cpp/profiling.cpp datadog/impl/features/profiling/profiling.cpp diff --git a/src/datadog/cpp/profiling.cpp b/src/datadog/cpp/profiling.cpp index a0fda76a..1619368c 100644 --- a/src/datadog/cpp/profiling.cpp +++ b/src/datadog/cpp/profiling.cpp @@ -7,7 +7,6 @@ #include "datadog/profiling.hpp" #include "datadog/core.hpp" -#include "datadog/rum.hpp" #include "datadog/impl/core/core.hpp" #include "datadog/impl/core/feature.hpp" @@ -57,20 +56,4 @@ std::shared_ptr Profiling::Register( ); } -std::function Profiling::CreateRumContextBridge( - const std::shared_ptr& profiling -) { - if (!profiling || !profiling->_impl) { - return nullptr; - } - - // Capture a weak_ptr so the callback doesn't keep profiling alive - std::weak_ptr weak_impl = profiling->_impl; - return [weak_impl](const RumContextSnapshot& context) { - if (auto impl = weak_impl.lock()) { - impl->OnRumContextChanged(context); - } - }; -} - } // namespace datadog diff --git a/src/datadog/cpp/rum.cpp b/src/datadog/cpp/rum.cpp index 68ce074d..b916055a 100644 --- a/src/datadog/cpp/rum.cpp +++ b/src/datadog/cpp/rum.cpp @@ -47,11 +47,6 @@ RumConfig& RumConfig::SetSessionSampleRate(float value) { return *this; } -RumConfig& RumConfig::SetContextChangeCallback(RumContextChangeCallback value) { - context_change_callback = std::move(value); - return *this; -} - Rum::Rum(Rum::PrivateCtorTag) : _impl(nullptr), _diagnostic_handler(nullptr), diff --git a/src/datadog/impl/core/core.cpp b/src/datadog/impl/core/core.cpp index cd5477dd..3f3b8cb1 100644 --- a/src/datadog/impl/core/core.cpp +++ b/src/datadog/impl/core/core.cpp @@ -20,6 +20,10 @@ #include "datadog/impl/core/util/assert.hpp" #include "datadog/impl/core/version.hpp" +#ifdef DD_ENABLE_PROFILER +#include "datadog/impl/features/profiling/profiling.hpp" +#endif + namespace datadog::impl { nonstd::expected CoreSubsystems::Init( @@ -400,6 +404,11 @@ bool Core::Start() { ); } + // Wire RUM context changes to the profiling feature, if both are registered. + // This is done after all features are started so both are fully initialized. + // TODO: Use ContextChangedMessage in Profiling impl; remove explicit coupling + WireRumToProfiling(); + // Start the messaging thread only after all features have completed OnCoreStarted(). // This ensures that any ContextChangedMessages dispatched during startup (e.g. from // a feature calling UpdateContext in its Start()) are not delivered to a feature's @@ -587,4 +596,47 @@ std::string_view Core::GetApplicationVersion() const { return _context_provider->GetHttpContext().application_version; } +void Core::WireRumToProfiling() { + // Find the RUM and Profiling features among registered features + constexpr FeatureId kRumId = CreateFeatureId("RUMM"); + [[maybe_unused]] constexpr FeatureId kProfilingId = CreateFeatureId("PROF"); + + std::shared_ptr rum_impl; + for (const auto& feature : _features) { + if (feature.id == kRumId) { + rum_impl = std::dynamic_pointer_cast(feature.impl); + } + } + + if (!rum_impl) { + return; // No RUM feature registered — nothing to wire + } + +#ifdef DD_ENABLE_PROFILER + std::shared_ptr profiling_impl; + for (const auto& feature : _features) { + if (feature.id == kProfilingId) { + profiling_impl = std::dynamic_pointer_cast(feature.impl); + } + } + + if (!profiling_impl) { + return; // No Profiling feature registered — nothing to wire + } + + // Wire RUM context changes → profiling: capture a weak_ptr so the callback + // doesn't extend the profiling feature's lifetime + std::weak_ptr weak_profiling = profiling_impl; + rum_impl->SetContextChangeCallback( + [weak_profiling](const datadog::RumContextSnapshot& context) { + if (auto prof = weak_profiling.lock()) { + prof->OnRumContextChanged(context); + } + } + ); + + _diagnostic_logger.Debug("Wired RUM context changes to profiling feature"); +#endif +} + } // namespace datadog::impl diff --git a/src/datadog/impl/core/core.hpp b/src/datadog/impl/core/core.hpp index 0a3553a7..fd5a297d 100644 --- a/src/datadog/impl/core/core.hpp +++ b/src/datadog/impl/core/core.hpp @@ -367,6 +367,13 @@ class Core { private: bool EnqueueStorageWrite(FeatureId feature_id, Block event, Block event_metadata); + /** + * If both RUM and Profiling features are registered, automatically connects + * RUM context changes to the profiling feature so that profile data is + * correlated with RUM sessions and views. No-op if either feature is absent. + */ + void WireRumToProfiling(); + private: // Initialized in ctor CoreState _state{CoreState::Uninitialized}; diff --git a/src/datadog/impl/rum/rum.cpp b/src/datadog/impl/rum/rum.cpp index 43fd8e98..fc61509b 100644 --- a/src/datadog/impl/rum/rum.cpp +++ b/src/datadog/impl/rum/rum.cpp @@ -19,9 +19,12 @@ Rum::Rum(const RumConfig& config, const platform::IClock& clock) _deps(config, clock), _application(_deps), _application_snapshot(), - _context_change_callback(config.context_change_callback), _previous_context() {} +void Rum::SetContextChangeCallback(RumContextChangeCallback callback) { + _context_change_callback = std::move(callback); +} + std::optional Rum::UploadThread_PrepareReport( const HttpContext& context, BatchReader& reader ) { diff --git a/src/datadog/impl/rum/rum.hpp b/src/datadog/impl/rum/rum.hpp index 9de28adc..54661b58 100644 --- a/src/datadog/impl/rum/rum.hpp +++ b/src/datadog/impl/rum/rum.hpp @@ -181,7 +181,15 @@ class Rum final : public Feature { std::string _request_url; std::string _request_headers; - // Callback invoked when RUM context changes + public: + /** + * Sets a callback invoked whenever RUM context changes. Used internally by + * Core to wire RUM context updates to the profiling feature. + */ + void SetContextChangeCallback(RumContextChangeCallback callback); + + private: + // Callback invoked when RUM context changes (set by Core, not by customer) RumContextChangeCallback _context_change_callback; // Previous RUM context for change detection From abadb0089ea14af0b1492567f4b68223f90e0f4f Mon Sep 17 00:00:00 2001 From: Erwan Viollet Date: Tue, 17 Mar 2026 11:30:57 +0100 Subject: [PATCH 03/18] RUM x Profiling: add an example --- .gitignore | 1 + .gitlab-ci.yml | 23 +++ examples/CMakeLists.txt | 5 + examples/profiling-test/CMakeLists.txt | 11 ++ examples/profiling-test/README.md | 62 ++++++++ examples/profiling-test/load-env.ps1 | 37 +++++ examples/profiling-test/main.cpp | 138 ++++++++++++++++++ include-cpp/datadog/rum.hpp | 2 +- .../impl/features/profiling/profiling.cpp | 14 +- src/datadog/impl/rum/context.cpp | 5 +- 10 files changed, 288 insertions(+), 10 deletions(-) create mode 100644 examples/profiling-test/CMakeLists.txt create mode 100644 examples/profiling-test/README.md create mode 100644 examples/profiling-test/load-env.ps1 create mode 100644 examples/profiling-test/main.cpp diff --git a/.gitignore b/.gitignore index 3604e7ad..80542021 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ Testing/ __pycache__/ *.pyc *.egg-info/ +.env diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2f5c88f5..c239cc82 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -247,6 +247,29 @@ build-windows: cmd /S /C "cmake -DCMAKE_BUILD_TYPE=${env:BUILD_CONFIGURATION} -DBUILD_SHARED_LIBS=${env:BUILD_SHARED_LIBS} -DDD_HTTP_USE_SYSTEM_LIBCURL=${env:USE_SYSTEM_LIBCURL} -DDD_CRASH_MODE=${env:DD_CRASH_MODE} -DDD_RUN_CRASHPAD_TESTS=${env:DD_RUN_CRASHPAD_TESTS} -DMSVC_RUNTIME_LIBRARY=${env:MSVC_RUNTIME_LIBRARY} -S . -B build && cmake --build build --config ${env:BUILD_CONFIGURATION} --parallel ${env:NUM_PARALLEL_BUILD_JOBS}" - If ($LASTEXITCODE -ne "0") { throw "docker run (cmake build) returned $LASTEXITCODE" } +# Build with profiler enabled (dd-win-prof fetched via FetchContent) and verify +# that the profiling-test example compiles and links successfully. +build-windows-profiler: + stage: build + dependencies: [] + tags: [windows-v2:2022] + variables: + OVERRIDE_GIT_STRATEGY: clone # Required for Windows runners + TOOLCHAIN_TARGET: vs2022 + script: + - $ErrorActionPreference = "Stop" + - if (Test-Path build) { remove-item -recurse -force build } + - $env:TOOLCHAIN_IMAGE = "${env:DISTRIBUTION_REGISTRY}/${env:TOOLCHAIN_IMAGE_PATH}:${env:TOOLCHAIN_TARGET}-${env:WINDOWS_TOOLCHAIN_IMAGE_VERSION}" + - > + docker run --rm + -m 8192M + -e NUM_PARALLEL_BUILD_JOBS + -v "$(Get-Location):C:\mnt" + -w C:\mnt + ${env:TOOLCHAIN_IMAGE} + cmd /S /C "cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DDD_HTTP_USE_SYSTEM_LIBCURL=OFF -DDD_CRASH_MODE=inprocess -DMSVC_RUNTIME_LIBRARY=MultiThreadedDLL -DDD_ENABLE_PROFILER=ON -DDD_BUILD_EXAMPLES=ON -S . -B build && cmake --build build --config Release --target dd_profiling_test --parallel %NUM_PARALLEL_BUILD_JOBS%" + - If ($LASTEXITCODE -ne "0") { throw "docker run (cmake build) returned $LASTEXITCODE" } + # Stage 'package' generates release artifacts for the toolchains/configurations that we # support with precompiled binaries diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 694972b2..5d0c9f99 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -38,3 +38,8 @@ target_enable_coverage(dd_native_repl) target_enable_sanitizers(dd_native_repl) target_include_directories(dd_native_repl PRIVATE .) datadog_enable(dd_native_repl) + +# Profiling + RUM integration smoke test (requires dd-win-prof) +if(DD_ENABLE_PROFILER) + add_subdirectory(profiling-test) +endif() diff --git a/examples/profiling-test/CMakeLists.txt b/examples/profiling-test/CMakeLists.txt new file mode 100644 index 00000000..ef02919e --- /dev/null +++ b/examples/profiling-test/CMakeLists.txt @@ -0,0 +1,11 @@ +# Profiling + RUM integration smoke test +# Only built when DD_ENABLE_PROFILER is ON (requires dd-win-prof). + +add_executable(dd_profiling_test + main.cpp +) +target_compile_definitions(dd_profiling_test PRIVATE _CRT_SECURE_NO_WARNINGS) +target_enable_strict_warnings(dd_profiling_test) +target_enable_coverage(dd_profiling_test) +target_enable_sanitizers(dd_profiling_test) +datadog_enable(dd_profiling_test) diff --git a/examples/profiling-test/README.md b/examples/profiling-test/README.md new file mode 100644 index 00000000..e1f4bcb8 --- /dev/null +++ b/examples/profiling-test/README.md @@ -0,0 +1,62 @@ +# profiling-test + +Minimal smoke test that exercises the dd-sdk-cpp **Profiling + RUM** integration end-to-end. + +## What it does + +1. Creates a `datadog::Core` (service=`profiling-test`, env=`dev`, staging endpoint) +2. Registers **Profiling** (agentless, symbolize=true) and **RUM** +3. Calls `core->Start()` which auto-wires RUM context into the profiler +4. Simulates 3 view transitions with ~5s of CPU busy-loop each: + - `HomePage` -> `SettingsPage` -> `ProfilePage` +5. Calls `core->Stop()` and prints a summary + +Total runtime: ~15s. The profiler should collect CPU/wall-time samples tagged with the active RUM view. + +## Environment variables + +| Variable | Read by | Description | +|---|---|---| +| `DD_CLIENT_TOKEN` | Code | Client token (used by Core for RUM) | +| `DD_RUM_APPLICATION_ID` | Code | RUM application ID | +| `DD_API_KEY` | Profiler (env) | Datadog API key for profile upload | +| `DD_SITE` | Profiler (env) | Datadog site, e.g. `datad0g.com` (staging) or `datadoghq.com` (prod) | + +Create a `.env` file in this directory (git-ignored): + +``` +DD_CLIENT_TOKEN=your-client-token-here +DD_RUM_APPLICATION_ID=your-rum-app-id-here +DD_API_KEY=your-api-key-here +DD_SITE=datad0g.com +``` + +Then source it before running: + +```powershell +# Load env vars from .env +. .\load-env.ps1 + +# Or specify a custom path +. .\load-env.ps1 -Path C:\path\to\.env +``` + +## Build + +From the `dd-sdk-cpp` root: + +```powershell +cmake -B build -G "Visual Studio 17 2022" -A x64 ` + -DDD_ENABLE_PROFILER=ON ` + -DDD_BUILD_EXAMPLES=ON ` + -DDD_HTTP_USE_SYSTEM_LIBCURL=OFF ` + -DDD_WIN_PROF_SOURCE_DIR="$PWD\..\dd-win-prof" + +cmake --build build --config Release --target dd_profiling_test +``` + +## Run + +```powershell +.\build\examples\profiling-test\Release\dd_profiling_test.exe +``` diff --git a/examples/profiling-test/load-env.ps1 b/examples/profiling-test/load-env.ps1 new file mode 100644 index 00000000..826ad0a4 --- /dev/null +++ b/examples/profiling-test/load-env.ps1 @@ -0,0 +1,37 @@ +# Load environment variables from a .env file. +# Usage: +# . .\load-env.ps1 # loads .env from current directory +# . .\load-env.ps1 -Path C:\path\to\.env + +param( + [string]$Path = (Join-Path $PSScriptRoot ".env") +) + +if (-not (Test-Path $Path)) { + Write-Error "File not found: $Path" + return +} + +$count = 0 +Get-Content $Path | ForEach-Object { + $line = $_.Trim() + # Skip empty lines and comments + if ($line -eq "" -or $line.StartsWith("#")) { return } + + $eq = $line.IndexOf("=") + if ($eq -lt 1) { return } + + $name = $line.Substring(0, $eq).Trim() + $value = $line.Substring($eq + 1).Trim() + # Strip surrounding quotes if present + if (($value.StartsWith('"') -and $value.EndsWith('"')) -or + ($value.StartsWith("'") -and $value.EndsWith("'"))) { + $value = $value.Substring(1, $value.Length - 2) + } + + [System.Environment]::SetEnvironmentVariable($name, $value, "Process") + Write-Host " $name = $($value.Substring(0, [Math]::Min(8, $value.Length)))..." + $count++ +} + +Write-Host "Loaded $count variable(s) from $Path" diff --git a/examples/profiling-test/main.cpp b/examples/profiling-test/main.cpp new file mode 100644 index 00000000..62bfa991 --- /dev/null +++ b/examples/profiling-test/main.cpp @@ -0,0 +1,138 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025-Present Datadog, Inc. + +// Minimal smoke test: Core + Profiling + RUM end-to-end. +// Spins CPU for ~15s across 3 RUM view transitions so the profiler +// collects wall-time / CPU samples tagged with RUM context. + +#include + +#include +#include +#include +#include + +#include "datadog.hpp" +#include "datadog/profiling.hpp" + +// --------------------------------------------------------------------------- +// Busy-loop for `duration_ms` milliseconds (no Sleep — we want CPU samples). +// --------------------------------------------------------------------------- +static void spin(int duration_ms) { + auto start = std::chrono::steady_clock::now(); + while (true) { + auto now = std::chrono::steady_clock::now(); + auto elapsed = + std::chrono::duration_cast(now - start).count(); + if (elapsed >= duration_ms) break; + } +} + +// --------------------------------------------------------------------------- +static const char* require_env(const char* name) { + const char* value = std::getenv(name); + if (!value || value[0] == '\0') { + std::fprintf(stderr, "ERROR: environment variable %s is not set\n", name); + std::exit(1); + } + return value; +} + +// --------------------------------------------------------------------------- +int main() { + std::printf("=== profiling-test: Core + Profiling + RUM smoke test ===\n\n"); + + // 1. Read required env vars (profiler reads DD_API_KEY, DD_SITE, etc. directly) + const char* client_token = require_env("DD_CLIENT_TOKEN"); + const char* rum_app_id = require_env("DD_RUM_APPLICATION_ID"); + + std::printf("[env] DD_CLIENT_TOKEN = %.8s...\n", client_token); + std::printf("[env] DD_RUM_APPLICATION_ID = %s\n\n", rum_app_id); + + // 2. Create Core + datadog::CoreConfig core_config(client_token, "profiling-test", "dev"); + core_config.SetApplicationVersion("1.0.0"); + core_config.SetEventStorageLocation("."); + core_config.SetInitialTrackingConsent(datadog::TrackingConsent::Granted); + core_config.Internal_UseCustomEndpoint("https://datad0g.com"); + + auto core = datadog::Core::Create(core_config); + if (!core) { + std::fprintf(stderr, "Failed to create Core\n"); + return 1; + } + std::printf("[core] created\n"); + + // 3. Register Profiling (reads DD_API_KEY, DD_SITE, etc. from env) + ProfilerConfig profiler_config = {}; + profiler_config.size = sizeof(ProfilerConfig); + profiler_config.serviceName = "profiling-test"; + profiler_config.serviceVersion = "1.0.0"; + profiler_config.serviceEnvironment = "dev"; + profiler_config.symbolizeCallstacks = true; + + auto profiling = datadog::Profiling::Register(core, &profiler_config); + if (!profiling) { + std::fprintf(stderr, "Failed to register Profiling\n"); + return 1; + } + std::printf("[profiling] registered (agentless, symbolize=true)\n"); + + // 4. Register RUM + auto rum = datadog::Rum::Register(core, datadog::RumConfig(rum_app_id)); + if (!rum) { + std::fprintf(stderr, "Failed to register RUM\n"); + return 1; + } + std::printf("[rum] registered (app_id=%s)\n\n", rum_app_id); + + // 5. Start + if (!core->Start()) { + std::fprintf(stderr, "Failed to start Core\n"); + return 1; + } + std::printf("[core] started — profiler + RUM wired\n\n"); + + // 6. Simulate 3 view transitions with CPU work + struct ViewStep { + const char* key; + const char* name; + int spin_ms; + }; + + ViewStep views[] = { + {"view-1", "HomePage", 5000}, + {"view-2", "SettingsPage", 5000}, + {"view-3", "ProfilePage", 5000}, + }; + + for (const auto& v : views) { + std::printf( + "[rum] StartView(%s, %s) — spinning %dms...\n", v.key, v.name, v.spin_ms + ); + rum->StartView(v.key, v.name); + spin(v.spin_ms); + rum->StopView(v.key); + std::printf("[rum] StopView(%s)\n", v.key); + } + + // 7. Stop + std::printf("\n[core] stopping...\n"); + core->Stop(); + std::printf("[core] stopped\n"); + + // 8. Summary + std::printf("\n=== summary ===\n"); + std::printf(" service : profiling-test\n"); + std::printf(" env : dev\n"); + std::printf(" views : 3 (HomePage, SettingsPage, ProfilePage)\n"); + std::printf(" spin/view : 5s\n"); + std::printf(" total spin : ~15s\n"); + std::printf(" profiler : symbolize=true (reads DD_API_KEY/DD_SITE from env)\n"); + std::printf("=== done ===\n"); + + return 0; +} diff --git a/include-cpp/datadog/rum.hpp b/include-cpp/datadog/rum.hpp index 4959b280..55ee2a02 100644 --- a/include-cpp/datadog/rum.hpp +++ b/include-cpp/datadog/rum.hpp @@ -95,7 +95,6 @@ struct RumContextSnapshot { */ using RumContextChangeCallback = std::function; - /** * Configures the details of the RUM feature upon initialization. */ @@ -107,6 +106,7 @@ struct RumConfig { private: UUID application_id; // UUID::Zero if uninitialized or invalid float session_sample_rate{100.0f}; + public: /** * Initializes a new RUM configuration object with all required values. diff --git a/src/datadog/impl/features/profiling/profiling.cpp b/src/datadog/impl/features/profiling/profiling.cpp index b6537957..b34cfac0 100644 --- a/src/datadog/impl/features/profiling/profiling.cpp +++ b/src/datadog/impl/features/profiling/profiling.cpp @@ -33,21 +33,19 @@ Profiling::Profiling(const ProfilerConfig* config) { _config.url = CopyString(_url, config->url); _config.apiKey = CopyString(_api_key, config->apiKey); - _config.serviceEnvironment = CopyString(_service_environment, config->serviceEnvironment); + _config.serviceEnvironment = + CopyString(_service_environment, config->serviceEnvironment); _config.serviceName = CopyString(_service_name, config->serviceName); _config.serviceVersion = CopyString(_service_version, config->serviceVersion); _config.tags = CopyString(_tags, config->tags); - _config.pprofOutputDirectory = CopyString(_pprof_output_directory, config->pprofOutputDirectory); + _config.pprofOutputDirectory = + CopyString(_pprof_output_directory, config->pprofOutputDirectory); } } -FeatureId Profiling::GetId() const { - return CreateFeatureId("PROF"); -} +FeatureId Profiling::GetId() const { return CreateFeatureId("PROF"); } -std::string_view Profiling::GetName() const { - return "profiling"; -} +std::string_view Profiling::GetName() const { return "profiling"; } void Profiling::Start() { if (_has_config) { diff --git a/src/datadog/impl/rum/context.cpp b/src/datadog/impl/rum/context.cpp index f54aaeb3..b10925f3 100644 --- a/src/datadog/impl/rum/context.cpp +++ b/src/datadog/impl/rum/context.cpp @@ -54,7 +54,10 @@ RumFeatureContext RumContext::ToFeatureContext() const { // We have an active action return RumFeatureContext{ - application_id, session_id, active_view_id, active_action_id, + application_id, + session_id, + active_view_id, + active_action_id, std::move(view_name_str) }; } From 93caa48985d7f42e00b24dccf5416bc500819139 Mon Sep 17 00:00:00 2001 From: Erwan Viollet Date: Tue, 17 Mar 2026 17:38:36 +0100 Subject: [PATCH 04/18] RUM x Profiling: profiling integration - remove interning of profiling config - add more granular change detection on context changes --- cmake/profiler.cmake | 4 +- examples/profiling-test/main.cpp | 9 +- src/datadog/impl/core/feature_types/rum.hpp | 10 +-- .../impl/features/profiling/profiling.cpp | 85 +++++++------------ .../impl/features/profiling/profiling.hpp | 20 ++--- src/datadog/impl/rum/rum.cpp | 14 +-- src/datadog/impl/rum/rum.hpp | 3 - 7 files changed, 53 insertions(+), 92 deletions(-) diff --git a/cmake/profiler.cmake b/cmake/profiler.cmake index e0a3cda4..622ed513 100644 --- a/cmake/profiler.cmake +++ b/cmake/profiler.cmake @@ -12,10 +12,12 @@ if(DD_WIN_PROF_SOURCE_DIR) SOURCE_DIR "${DD_WIN_PROF_SOURCE_DIR}" ) else() + # TODO: Pin to a released tag once the split RUM context API is merged. + # For now, this points to a specific commit on main. FetchContent_Declare( dd-win-prof GIT_REPOSITORY https://github.com/DataDog/dd-win-prof.git - GIT_TAG 74b9c6f + GIT_TAG 13dd010 ) endif() diff --git a/examples/profiling-test/main.cpp b/examples/profiling-test/main.cpp index 62bfa991..40ddba85 100644 --- a/examples/profiling-test/main.cpp +++ b/examples/profiling-test/main.cpp @@ -8,16 +8,19 @@ // Spins CPU for ~15s across 3 RUM view transitions so the profiler // collects wall-time / CPU samples tagged with RUM context. -#include - #include +#include #include #include -#include #include "datadog.hpp" #include "datadog/profiling.hpp" +// dd-win-prof.h uses uint32_t etc. — must come after +// clang-format off +#include +// clang-format on + // --------------------------------------------------------------------------- // Busy-loop for `duration_ms` milliseconds (no Sleep — we want CPU samples). // --------------------------------------------------------------------------- diff --git a/src/datadog/impl/core/feature_types/rum.hpp b/src/datadog/impl/core/feature_types/rum.hpp index e4647f47..d1ae1c49 100644 --- a/src/datadog/impl/core/feature_types/rum.hpp +++ b/src/datadog/impl/core/feature_types/rum.hpp @@ -129,21 +129,13 @@ struct RumFeatureContext { std::string view_name; // Empty if no active view /** - * Converts this internal RumFeatureContext to the public RumContext type. + * Converts this internal RumFeatureContext to the public RumContextSnapshot. */ datadog::RumContextSnapshot ToPublicContext() const { return datadog::RumContextSnapshot{ application_id, session_id, view_id, view_name.c_str(), action_id }; } - - bool operator==(const RumFeatureContext& other) const { - return application_id == other.application_id && session_id == other.session_id && - view_id == other.view_id && action_id == other.action_id && - view_name == other.view_name; - } - - bool operator!=(const RumFeatureContext& other) const { return !(*this == other); } }; DATADOG_STRING_ENUM( diff --git a/src/datadog/impl/features/profiling/profiling.cpp b/src/datadog/impl/features/profiling/profiling.cpp index b34cfac0..b0da0eb7 100644 --- a/src/datadog/impl/features/profiling/profiling.cpp +++ b/src/datadog/impl/features/profiling/profiling.cpp @@ -6,40 +6,21 @@ #include "datadog/impl/features/profiling/profiling.hpp" -#include #include #include "datadog/uuid.hpp" namespace datadog::impl { -// Helper: copy a const char* into owned storage, return pointer to owned string -// (or nullptr if source is null/empty) -static const char* CopyString(std::string& storage, const char* src) { - if (src == nullptr || src[0] == '\0') { - storage.clear(); - return nullptr; - } - storage = src; - return storage.c_str(); -} - Profiling::Profiling(const ProfilerConfig* config) { + // Call SetupProfiler immediately so dd-win-prof interns all string fields. + // This avoids needing to deep-copy the config's const char* pointers. if (config != nullptr) { - _has_config = true; - - // Deep-copy the struct, then repoint const char* fields at our owned strings - _config = *config; - - _config.url = CopyString(_url, config->url); - _config.apiKey = CopyString(_api_key, config->apiKey); - _config.serviceEnvironment = - CopyString(_service_environment, config->serviceEnvironment); - _config.serviceName = CopyString(_service_name, config->serviceName); - _config.serviceVersion = CopyString(_service_version, config->serviceVersion); - _config.tags = CopyString(_tags, config->tags); - _config.pprofOutputDirectory = - CopyString(_pprof_output_directory, config->pprofOutputDirectory); + _profiler_setup = SetupProfiler(const_cast(config)); + } else { + ProfilerConfig defaults{}; + defaults.size = sizeof(ProfilerConfig); + _profiler_setup = SetupProfiler(&defaults); } } @@ -48,16 +29,6 @@ FeatureId Profiling::GetId() const { return CreateFeatureId("PROF"); } std::string_view Profiling::GetName() const { return "profiling"; } void Profiling::Start() { - if (_has_config) { - _profiler_setup = SetupProfiler(&_config); - } else { - // No config provided — set up with defaults (size field required) - ProfilerConfig defaults{}; - std::memset(&defaults, 0, sizeof(defaults)); - defaults.size = sizeof(ProfilerConfig); - _profiler_setup = SetupProfiler(&defaults); - } - if (_profiler_setup) { StartProfiler(); } @@ -82,24 +53,34 @@ void Profiling::OnRumContextChanged(const datadog::RumContextSnapshot& context) return; } - // Stable context: application_id and session_id - const std::string app_id = context.application_id.ToString(); - const std::string session_id = context.session_id.ToString(); + // Stable context: only call SetRumSession when application_id or session_id changes + if (context.application_id != _prev_application_id || + context.session_id != _prev_session_id) { + _prev_application_id = context.application_id; + _prev_session_id = context.session_id; - RumSessionContext session_ctx{}; - session_ctx.application_id = app_id.c_str(); - session_ctx.session_id = session_id.c_str(); - SetRumSession(&session_ctx); + const std::string app_id = context.application_id.ToString(); + const std::string session_id = context.session_id.ToString(); - // Volatile context: view_id and view_name - if (context.view_id == datadog::UUID::Zero) { - SetRumView(nullptr); - } else { - const std::string view_id = context.view_id.ToString(); - RumViewValues view_vals{}; - view_vals.view_id = view_id.c_str(); - view_vals.view_name = context.view_name; - SetRumView(&view_vals); + RumSessionContext session_ctx{}; + session_ctx.application_id = app_id.c_str(); + session_ctx.session_id = session_id.c_str(); + SetRumSession(&session_ctx); + } + + // Volatile context: only call SetRumView when view_id changes + if (context.view_id != _prev_view_id) { + _prev_view_id = context.view_id; + + if (context.view_id == datadog::UUID::Zero) { + SetRumView(nullptr); + } else { + const std::string view_id = context.view_id.ToString(); + RumViewValues view_vals{}; + view_vals.view_id = view_id.c_str(); + view_vals.view_name = context.view_name; + SetRumView(&view_vals); + } } } diff --git a/src/datadog/impl/features/profiling/profiling.hpp b/src/datadog/impl/features/profiling/profiling.hpp index b8ea9616..ee2f8ca8 100644 --- a/src/datadog/impl/features/profiling/profiling.hpp +++ b/src/datadog/impl/features/profiling/profiling.hpp @@ -10,6 +10,7 @@ #include #include "datadog/rum.hpp" +#include "datadog/uuid.hpp" #include "datadog/impl/core/feature.hpp" @@ -48,21 +49,12 @@ class Profiling final : public Feature { void Stop() override; private: - // We store a deep copy of the config so string pointers remain valid. - // The original ProfilerConfig uses const char* — we own the strings here. - bool _has_config{false}; - ProfilerConfig _config{}; - - // Owned string storage for _config's const char* fields - std::string _url; - std::string _api_key; - std::string _service_environment; - std::string _service_name; - std::string _service_version; - std::string _tags; - std::string _pprof_output_directory; - bool _profiler_setup{false}; + + // Change detection for RUM context — avoid redundant calls to dd-win-prof + datadog::UUID _prev_application_id; + datadog::UUID _prev_session_id; + datadog::UUID _prev_view_id; }; } // namespace datadog::impl diff --git a/src/datadog/impl/rum/rum.cpp b/src/datadog/impl/rum/rum.cpp index fc61509b..394128bc 100644 --- a/src/datadog/impl/rum/rum.cpp +++ b/src/datadog/impl/rum/rum.cpp @@ -18,8 +18,7 @@ Rum::Rum(const RumConfig& config, const platform::IClock& clock) : _global_attributes(8), _deps(config, clock), _application(_deps), - _application_snapshot(), - _previous_context() {} + _application_snapshot() {} void Rum::SetContextChangeCallback(RumContextChangeCallback callback) { _context_change_callback = std::move(callback); @@ -76,9 +75,6 @@ void Rum::Stop() { // Clear the FeatureScope reference in RumScopeDependencies: scopes should no longer // generate events _deps.OnStop(); - - // Reset previous context to ensure callback fires on next SDK start - _previous_context = RumFeatureContext{}; } void Rum::AddAttribute(std::string_view name, const Attribute& value) { @@ -236,12 +232,10 @@ void Rum::DispatchAsync(const RumCommand& command) { // other features can enrich their events with RUM data ctx.rum = rum->_application_snapshot.ToFeatureContext(); - // Invoke the context change callback if the context has changed + // Notify the callback (if set). Change detection is handled by the recipient. // TODO: The Profiling feature should instead listen for ContextChangedMessage - if (_context_change_callback && new_context != _previous_context) { - _previous_context = new_context; - const datadog::RumContextSnapshot callback_context = new_context.ToPublicContext(); - _context_change_callback(callback_context); + if (_context_change_callback) { + _context_change_callback(new_context.ToPublicContext()); } } }); diff --git a/src/datadog/impl/rum/rum.hpp b/src/datadog/impl/rum/rum.hpp index 54661b58..4877c5bf 100644 --- a/src/datadog/impl/rum/rum.hpp +++ b/src/datadog/impl/rum/rum.hpp @@ -191,9 +191,6 @@ class Rum final : public Feature { private: // Callback invoked when RUM context changes (set by Core, not by customer) RumContextChangeCallback _context_change_callback; - - // Previous RUM context for change detection - RumFeatureContext _previous_context; }; } // namespace datadog::impl From a8e971f86c0690d5bfbaad383d7def56de5b019f Mon Sep 17 00:00:00 2001 From: Erwan Viollet Date: Tue, 17 Mar 2026 18:07:26 +0100 Subject: [PATCH 05/18] Add a staging endpoint --- examples/profiling-test/main.cpp | 2 +- include-c/datadog/core.h | 1 + include-cpp/datadog/core.hpp | 1 + src/datadog/impl/core/site.hpp | 2 ++ src/datadog/impl/core/types.hpp | 3 +++ 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/profiling-test/main.cpp b/examples/profiling-test/main.cpp index 40ddba85..c8ee71d4 100644 --- a/examples/profiling-test/main.cpp +++ b/examples/profiling-test/main.cpp @@ -58,9 +58,9 @@ int main() { // 2. Create Core datadog::CoreConfig core_config(client_token, "profiling-test", "dev"); core_config.SetApplicationVersion("1.0.0"); + core_config.SetSite(datadog::Site::staging); core_config.SetEventStorageLocation("."); core_config.SetInitialTrackingConsent(datadog::TrackingConsent::Granted); - core_config.Internal_UseCustomEndpoint("https://datad0g.com"); auto core = datadog::Core::Create(core_config); if (!core) { diff --git a/include-c/datadog/core.h b/include-c/datadog/core.h index fe056d5b..01c19561 100644 --- a/include-c/datadog/core.h +++ b/include-c/datadog/core.h @@ -91,6 +91,7 @@ typedef enum { DD_SITE_AP1, DD_SITE_AP2, DD_SITE_US1_FED, + DD_SITE_STAGING, } dd_site_t; /** diff --git a/include-cpp/datadog/core.hpp b/include-cpp/datadog/core.hpp index 8a17ac96..cd667759 100644 --- a/include-cpp/datadog/core.hpp +++ b/include-cpp/datadog/core.hpp @@ -92,6 +92,7 @@ enum class Site : uint8_t { ap1, ap2, us1_fed, + staging, }; /** diff --git a/src/datadog/impl/core/site.hpp b/src/datadog/impl/core/site.hpp index 140e7b0e..fcf8059d 100644 --- a/src/datadog/impl/core/site.hpp +++ b/src/datadog/impl/core/site.hpp @@ -34,6 +34,8 @@ inline std::string GetIntakeHost(Site site) { return "browser-intake-datadoghq.eu"; case Site::us1_fed: return "browser-intake-ddog-gov.com"; + case Site::staging: + return "browser-intake-datad0g.com"; // Fall out to default implementation default: diff --git a/src/datadog/impl/core/types.hpp b/src/datadog/impl/core/types.hpp index 2c6026c0..0fcf3d30 100644 --- a/src/datadog/impl/core/types.hpp +++ b/src/datadog/impl/core/types.hpp @@ -92,6 +92,7 @@ inline Site Site_FromC(dd_site_t value) { static_assert(static_cast(Site::ap1) == DD_SITE_AP1); static_assert(static_cast(Site::ap2) == DD_SITE_AP2); static_assert(static_cast(Site::us1_fed) == DD_SITE_US1_FED); + static_assert(static_cast(Site::staging) == DD_SITE_STAGING); return static_cast(value); } @@ -111,6 +112,8 @@ inline const char* Site_ToString(Site value) { return "ap2"; case Site::us1_fed: return "us1_fed"; + case Site::staging: + return "staging"; default: return ""; } From ae83268bb6327f45f3cd82155eb6d1b68f1e781e Mon Sep 17 00:00:00 2001 From: Erwan Viollet Date: Tue, 17 Mar 2026 18:18:43 +0100 Subject: [PATCH 06/18] =?UTF-8?q?Avoid=20redundant=20calls=20and=20allocat?= =?UTF-8?q?ions=20in=20RUM=E2=86=92profiling=20bridge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Skip SetRumSession when session hasn't changed - Skip SetRumView when view hasn't changed - Call ClearRumContext on session end (application_id == Zero) - Cache UUID string representations to avoid repeated allocations Co-Authored-By: Claude Opus 4.6 (1M context) --- .../impl/features/profiling/profiling.cpp | 25 ++++++++++++++----- .../impl/features/profiling/profiling.hpp | 5 ++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/datadog/impl/features/profiling/profiling.cpp b/src/datadog/impl/features/profiling/profiling.cpp index b0da0eb7..c4dd1705 100644 --- a/src/datadog/impl/features/profiling/profiling.cpp +++ b/src/datadog/impl/features/profiling/profiling.cpp @@ -48,23 +48,35 @@ std::optional Profiling::UploadThread_PrepareReport( return std::nullopt; } +// TODO: RumContextSnapshot passes UUIDs which we convert to strings here. If the +// copy cost matters, RUM could own pre-formatted string representations and pass +// const char* pointers directly, avoiding the UUID→string conversion on this side. void Profiling::OnRumContextChanged(const datadog::RumContextSnapshot& context) { if (!_profiler_setup) { return; } + // Session end: all UUIDs are zero — clear everything and return + if (context.application_id == datadog::UUID::Zero) { + _prev_application_id = datadog::UUID::Zero; + _prev_session_id = datadog::UUID::Zero; + _prev_view_id = datadog::UUID::Zero; + ClearRumContext(); + return; + } + // Stable context: only call SetRumSession when application_id or session_id changes if (context.application_id != _prev_application_id || context.session_id != _prev_session_id) { _prev_application_id = context.application_id; _prev_session_id = context.session_id; - const std::string app_id = context.application_id.ToString(); - const std::string session_id = context.session_id.ToString(); + _app_id_str = context.application_id.ToString(); + _session_id_str = context.session_id.ToString(); RumSessionContext session_ctx{}; - session_ctx.application_id = app_id.c_str(); - session_ctx.session_id = session_id.c_str(); + session_ctx.application_id = _app_id_str.c_str(); + session_ctx.session_id = _session_id_str.c_str(); SetRumSession(&session_ctx); } @@ -75,9 +87,10 @@ void Profiling::OnRumContextChanged(const datadog::RumContextSnapshot& context) if (context.view_id == datadog::UUID::Zero) { SetRumView(nullptr); } else { - const std::string view_id = context.view_id.ToString(); + _view_id_str = context.view_id.ToString(); + RumViewValues view_vals{}; - view_vals.view_id = view_id.c_str(); + view_vals.view_id = _view_id_str.c_str(); view_vals.view_name = context.view_name; SetRumView(&view_vals); } diff --git a/src/datadog/impl/features/profiling/profiling.hpp b/src/datadog/impl/features/profiling/profiling.hpp index ee2f8ca8..9a30e558 100644 --- a/src/datadog/impl/features/profiling/profiling.hpp +++ b/src/datadog/impl/features/profiling/profiling.hpp @@ -55,6 +55,11 @@ class Profiling final : public Feature { datadog::UUID _prev_application_id; datadog::UUID _prev_session_id; datadog::UUID _prev_view_id; + + // Cached string representations — reserved once, reused on each update + std::string _app_id_str; + std::string _session_id_str; + std::string _view_id_str; }; } // namespace datadog::impl From a5c6eadf2d332d35c746fe289ae088bb9ef96dad Mon Sep 17 00:00:00 2001 From: Erwan Viollet Date: Tue, 17 Mar 2026 23:14:31 +0100 Subject: [PATCH 07/18] RUM x Profiling: Handle end of sessions --- cmake/profiler.cmake | 2 +- src/datadog/cpp/profiling.cpp | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cmake/profiler.cmake b/cmake/profiler.cmake index 622ed513..61a9f5b3 100644 --- a/cmake/profiler.cmake +++ b/cmake/profiler.cmake @@ -17,7 +17,7 @@ else() FetchContent_Declare( dd-win-prof GIT_REPOSITORY https://github.com/DataDog/dd-win-prof.git - GIT_TAG 13dd010 + GIT_TAG 13dd010034bef0eda30179f7bafd9a4e6899aa41 ) endif() diff --git a/src/datadog/cpp/profiling.cpp b/src/datadog/cpp/profiling.cpp index 1619368c..71313e72 100644 --- a/src/datadog/cpp/profiling.cpp +++ b/src/datadog/cpp/profiling.cpp @@ -40,7 +40,6 @@ std::shared_ptr Profiling::Register( return std::make_shared(Profiling::PrivateCtorTag{}); } - // Initialize our profiling feature implementation (deep-copies config) auto profiling_impl = std::make_shared(config); // Register the feature with the core, returning a no-op interface on failure From e8bfe64155a5a64fd816166c685ca48f45a1955d Mon Sep 17 00:00:00 2001 From: Erwan Viollet Date: Tue, 17 Mar 2026 23:51:52 +0100 Subject: [PATCH 08/18] RUM x Profiling: minor adjustments - cleanup const cast - allow restart after stop --- cmake/profiler.cmake | 2 +- src/datadog/impl/features/profiling/profiling.cpp | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cmake/profiler.cmake b/cmake/profiler.cmake index 61a9f5b3..b2751e83 100644 --- a/cmake/profiler.cmake +++ b/cmake/profiler.cmake @@ -17,7 +17,7 @@ else() FetchContent_Declare( dd-win-prof GIT_REPOSITORY https://github.com/DataDog/dd-win-prof.git - GIT_TAG 13dd010034bef0eda30179f7bafd9a4e6899aa41 + GIT_TAG 75f644a ) endif() diff --git a/src/datadog/impl/features/profiling/profiling.cpp b/src/datadog/impl/features/profiling/profiling.cpp index c4dd1705..4a0c34e1 100644 --- a/src/datadog/impl/features/profiling/profiling.cpp +++ b/src/datadog/impl/features/profiling/profiling.cpp @@ -16,7 +16,7 @@ Profiling::Profiling(const ProfilerConfig* config) { // Call SetupProfiler immediately so dd-win-prof interns all string fields. // This avoids needing to deep-copy the config's const char* pointers. if (config != nullptr) { - _profiler_setup = SetupProfiler(const_cast(config)); + _profiler_setup = SetupProfiler(config); } else { ProfilerConfig defaults{}; defaults.size = sizeof(ProfilerConfig); @@ -35,9 +35,10 @@ void Profiling::Start() { } void Profiling::Stop() { + // Don't clear _profiler_setup — StopProfiler/StartProfiler can be called + // repeatedly, and Core supports stop/restart cycles. if (_profiler_setup) { StopProfiler(); - _profiler_setup = false; } } From b68748c8126e48840685c4fb12967e21e3fb650b Mon Sep 17 00:00:00 2001 From: Alex Forsythe Date: Wed, 8 Apr 2026 11:45:59 -0400 Subject: [PATCH 09/18] chore: Move feature source to impl/profiling, fix up include paths --- src/CMakeLists.txt | 2 +- src/datadog/cpp/profiling.cpp | 2 +- src/datadog/impl/core/core.cpp | 2 +- src/datadog/impl/{features => }/profiling/profiling.cpp | 2 +- src/datadog/impl/{features => }/profiling/profiling.hpp | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename src/datadog/impl/{features => }/profiling/profiling.cpp (98%) rename src/datadog/impl/{features => }/profiling/profiling.hpp (100%) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1be25295..4817d582 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -280,7 +280,7 @@ if(DD_ENABLE_PROFILER) target_compile_definitions(dd_native PRIVATE DD_ENABLE_PROFILER=1) target_sources(dd_native PRIVATE datadog/cpp/profiling.cpp - datadog/impl/features/profiling/profiling.cpp + datadog/impl/profiling/profiling.cpp ) include(${DD_SDK_ROOT_DIR}/cmake/profiler.cmake) endif() diff --git a/src/datadog/cpp/profiling.cpp b/src/datadog/cpp/profiling.cpp index 71313e72..a86ddc43 100644 --- a/src/datadog/cpp/profiling.cpp +++ b/src/datadog/cpp/profiling.cpp @@ -11,7 +11,7 @@ #include "datadog/impl/core/core.hpp" #include "datadog/impl/core/feature.hpp" #include "datadog/impl/diagnostics.hpp" -#include "datadog/impl/features/profiling/profiling.hpp" +#include "datadog/impl/profiling/profiling.hpp" namespace datadog { diff --git a/src/datadog/impl/core/core.cpp b/src/datadog/impl/core/core.cpp index 3f3b8cb1..6dadbee3 100644 --- a/src/datadog/impl/core/core.cpp +++ b/src/datadog/impl/core/core.cpp @@ -21,7 +21,7 @@ #include "datadog/impl/core/version.hpp" #ifdef DD_ENABLE_PROFILER -#include "datadog/impl/features/profiling/profiling.hpp" +#include "datadog/impl/profiling/profiling.hpp" #endif namespace datadog::impl { diff --git a/src/datadog/impl/features/profiling/profiling.cpp b/src/datadog/impl/profiling/profiling.cpp similarity index 98% rename from src/datadog/impl/features/profiling/profiling.cpp rename to src/datadog/impl/profiling/profiling.cpp index 4a0c34e1..7e7c0155 100644 --- a/src/datadog/impl/features/profiling/profiling.cpp +++ b/src/datadog/impl/profiling/profiling.cpp @@ -4,7 +4,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2025-Present Datadog, Inc. -#include "datadog/impl/features/profiling/profiling.hpp" +#include "datadog/impl/profiling/profiling.hpp" #include diff --git a/src/datadog/impl/features/profiling/profiling.hpp b/src/datadog/impl/profiling/profiling.hpp similarity index 100% rename from src/datadog/impl/features/profiling/profiling.hpp rename to src/datadog/impl/profiling/profiling.hpp From d6efd518d04180a83638552cce3b949dbc3b3ea8 Mon Sep 17 00:00:00 2001 From: Alex Forsythe Date: Wed, 8 Apr 2026 11:59:41 -0400 Subject: [PATCH 10/18] chore: Fix include paths erroneously changed on rebase --- src/datadog/impl/core/feature_types/rum.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/datadog/impl/core/feature_types/rum.hpp b/src/datadog/impl/core/feature_types/rum.hpp index d1ae1c49..e9cce6e9 100644 --- a/src/datadog/impl/core/feature_types/rum.hpp +++ b/src/datadog/impl/core/feature_types/rum.hpp @@ -13,10 +13,10 @@ #include "datadog/rum.hpp" #include "datadog/uuid.hpp" -#include "datadog/impl/events/enum.hpp" -#include "datadog/impl/events/omissible.hpp" -#include "datadog/impl/events/struct.hpp" -#include "datadog/impl/events/timestamp.hpp" +#include "datadog/impl/core/events/enum.hpp" +#include "datadog/impl/core/events/omissible.hpp" +#include "datadog/impl/core/events/struct.hpp" +#include "datadog/impl/core/events/timestamp.hpp" /* ===================== INSTRUCTIONS FOR VALIDATING THIS FILE ======================== From 6365e3f0355f222662c3042601ff4103f4a76353 Mon Sep 17 00:00:00 2001 From: Alex Forsythe Date: Wed, 8 Apr 2026 12:15:18 -0400 Subject: [PATCH 11/18] chore: Fix rum.hpp include erroneously removed in conflict resolution --- src/datadog/impl/core/core.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/datadog/impl/core/core.cpp b/src/datadog/impl/core/core.cpp index 6dadbee3..efef4baf 100644 --- a/src/datadog/impl/core/core.cpp +++ b/src/datadog/impl/core/core.cpp @@ -19,6 +19,7 @@ #include "datadog/impl/core/types.hpp" #include "datadog/impl/core/util/assert.hpp" #include "datadog/impl/core/version.hpp" +#include "datadog/impl/rum/rum.hpp" #ifdef DD_ENABLE_PROFILER #include "datadog/impl/profiling/profiling.hpp" From afc52c3be822ca7afa49902c630d499c10052c6c Mon Sep 17 00:00:00 2001 From: Alex Forsythe Date: Wed, 8 Apr 2026 12:21:22 -0400 Subject: [PATCH 12/18] fix: In Core, don't directly reference features, including RUM ...unless DD_ENABLE_PROFILER is defined. This hardwired interop mechanism is a temporary hack; in theory, `core` should not depend on features like `rum` or `profiling` at all. If the SDK build were modularized into separate DLLs, this approach would constitute a circular dependency and would likely not work. (A proper fix would be for the `Profiling` feature to register a message handler than handles `ContextChangedMessage`.) --- src/datadog/impl/core/core.cpp | 27 ++++++++++----------------- src/datadog/impl/core/core.hpp | 2 ++ 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/datadog/impl/core/core.cpp b/src/datadog/impl/core/core.cpp index efef4baf..2bc783a2 100644 --- a/src/datadog/impl/core/core.cpp +++ b/src/datadog/impl/core/core.cpp @@ -19,10 +19,10 @@ #include "datadog/impl/core/types.hpp" #include "datadog/impl/core/util/assert.hpp" #include "datadog/impl/core/version.hpp" -#include "datadog/impl/rum/rum.hpp" #ifdef DD_ENABLE_PROFILER #include "datadog/impl/profiling/profiling.hpp" +#include "datadog/impl/rum/rum.hpp" #endif namespace datadog::impl { @@ -405,10 +405,12 @@ bool Core::Start() { ); } +#ifdef DD_ENABLE_PROFILER // Wire RUM context changes to the profiling feature, if both are registered. // This is done after all features are started so both are fully initialized. // TODO: Use ContextChangedMessage in Profiling impl; remove explicit coupling WireRumToProfiling(); +#endif // Start the messaging thread only after all features have completed OnCoreStarted(). // This ensures that any ContextChangedMessages dispatched during startup (e.g. from @@ -597,32 +599,23 @@ std::string_view Core::GetApplicationVersion() const { return _context_provider->GetHttpContext().application_version; } +#ifdef DD_ENABLE_PROFILER void Core::WireRumToProfiling() { - // Find the RUM and Profiling features among registered features constexpr FeatureId kRumId = CreateFeatureId("RUMM"); - [[maybe_unused]] constexpr FeatureId kProfilingId = CreateFeatureId("PROF"); + constexpr FeatureId kProfilingId = CreateFeatureId("PROF"); std::shared_ptr rum_impl; + std::shared_ptr profiling_impl; for (const auto& feature : _features) { if (feature.id == kRumId) { rum_impl = std::dynamic_pointer_cast(feature.impl); - } - } - - if (!rum_impl) { - return; // No RUM feature registered — nothing to wire - } - -#ifdef DD_ENABLE_PROFILER - std::shared_ptr profiling_impl; - for (const auto& feature : _features) { - if (feature.id == kProfilingId) { + } else if (feature.id == kProfilingId) { profiling_impl = std::dynamic_pointer_cast(feature.impl); } } - if (!profiling_impl) { - return; // No Profiling feature registered — nothing to wire + if (!rum_impl || !profiling_impl) { + return; // Both features must be registered to wire them together } // Wire RUM context changes → profiling: capture a weak_ptr so the callback @@ -637,7 +630,7 @@ void Core::WireRumToProfiling() { ); _diagnostic_logger.Debug("Wired RUM context changes to profiling feature"); -#endif } +#endif } // namespace datadog::impl diff --git a/src/datadog/impl/core/core.hpp b/src/datadog/impl/core/core.hpp index fd5a297d..4e184c31 100644 --- a/src/datadog/impl/core/core.hpp +++ b/src/datadog/impl/core/core.hpp @@ -367,12 +367,14 @@ class Core { private: bool EnqueueStorageWrite(FeatureId feature_id, Block event, Block event_metadata); +#ifdef DD_ENABLE_PROFILER /** * If both RUM and Profiling features are registered, automatically connects * RUM context changes to the profiling feature so that profile data is * correlated with RUM sessions and views. No-op if either feature is absent. */ void WireRumToProfiling(); +#endif private: // Initialized in ctor From 4d16f9afa5762f6b6e122b7fcfd4f6fe9e954c49 Mon Sep 17 00:00:00 2001 From: Alex Forsythe Date: Wed, 8 Apr 2026 12:36:48 -0400 Subject: [PATCH 13/18] fix: Restore RumFeatureContext operators that are used by CrashReporting --- src/datadog/impl/core/feature_types/rum.hpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/datadog/impl/core/feature_types/rum.hpp b/src/datadog/impl/core/feature_types/rum.hpp index e9cce6e9..ac7fa7b5 100644 --- a/src/datadog/impl/core/feature_types/rum.hpp +++ b/src/datadog/impl/core/feature_types/rum.hpp @@ -136,6 +136,14 @@ struct RumFeatureContext { application_id, session_id, view_id, view_name.c_str(), action_id }; } + + bool operator==(const RumFeatureContext& other) const { + return application_id == other.application_id && session_id == other.session_id && + view_id == other.view_id && action_id == other.action_id && + view_name == other.view_name; + } + + bool operator!=(const RumFeatureContext& other) const { return !(*this == other); } }; DATADOG_STRING_ENUM( From 78955e5879dd2b6ce6928eb6660f74749ba436be Mon Sep 17 00:00:00 2001 From: Alex Forsythe Date: Wed, 8 Apr 2026 12:46:16 -0400 Subject: [PATCH 14/18] fix: Fix botched merge by restoring original `Rum::Stop` The `profiling-integration` changes make no modifications to `Rum::Stop`; we should preserve the intervening changes that were made in `main`. --- src/datadog/impl/rum/rum.cpp | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/datadog/impl/rum/rum.cpp b/src/datadog/impl/rum/rum.cpp index 394128bc..dfbe7c38 100644 --- a/src/datadog/impl/rum/rum.cpp +++ b/src/datadog/impl/rum/rum.cpp @@ -63,18 +63,10 @@ void Rum::Start() { }; void Rum::Stop() { - // On Core shutdown, ensure that any RUM context is purged, so that if the SDK is - // restarted it won't inherit old state - if (_scope) { - _scope->UpdateContext([](CoreContext& ctx) { ctx.rum.reset(); }); - } - - // Fully reinitialize RUM application state to clear all sessions/views/etc. - _application = RumApplicationScope(_deps); - - // Clear the FeatureScope reference in RumScopeDependencies: scopes should no longer - // generate events - _deps.OnStop(); + // Note: _application will be destroyed when Rum is destroyed (after Core joins + // the context thread). In-flight lambdas can safely access _application as long as + // weak_ptr.lock() succeeds. CoreContext is reset by Core::Start() at the top of each + // new run, so no context mutation is needed here. } void Rum::AddAttribute(std::string_view name, const Attribute& value) { From 1c7738e5cfca4f17b595ac6cea5712fdf2032bfb Mon Sep 17 00:00:00 2001 From: Alex Forsythe Date: Wed, 8 Apr 2026 12:48:20 -0400 Subject: [PATCH 15/18] fix: Fix another botched merge in context change callback invocation --- src/datadog/impl/rum/rum.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/datadog/impl/rum/rum.cpp b/src/datadog/impl/rum/rum.cpp index dfbe7c38..965a7121 100644 --- a/src/datadog/impl/rum/rum.cpp +++ b/src/datadog/impl/rum/rum.cpp @@ -222,12 +222,13 @@ void Rum::DispatchAsync(const RumCommand& command) { // _application_snapshot has been updated with the result of processing our // command; write the relevant UUIDs to the global RumFeatureContext, so that // other features can enrich their events with RUM data - ctx.rum = rum->_application_snapshot.ToFeatureContext(); + const RumFeatureContext new_context = rum->_application_snapshot.ToFeatureContext(); + ctx.rum = new_context; // Notify the callback (if set). Change detection is handled by the recipient. // TODO: The Profiling feature should instead listen for ContextChangedMessage - if (_context_change_callback) { - _context_change_callback(new_context.ToPublicContext()); + if (rum->_context_change_callback) { + rum->_context_change_callback(new_context.ToPublicContext()); } } }); From 2e853159422093b7b12d7c262384afb9bb1bd759 Mon Sep 17 00:00:00 2001 From: Alex Forsythe Date: Wed, 8 Apr 2026 12:57:09 -0400 Subject: [PATCH 16/18] chore: Fix missed include path --- src/datadog/cpp/profiling.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datadog/cpp/profiling.cpp b/src/datadog/cpp/profiling.cpp index a86ddc43..0b271291 100644 --- a/src/datadog/cpp/profiling.cpp +++ b/src/datadog/cpp/profiling.cpp @@ -10,7 +10,7 @@ #include "datadog/impl/core/core.hpp" #include "datadog/impl/core/feature.hpp" -#include "datadog/impl/diagnostics.hpp" +#include "datadog/impl/core/util/diagnostics.hpp" #include "datadog/impl/profiling/profiling.hpp" namespace datadog { From 7b1dd44311cdf16a279a75612750e93dc7e3cf5d Mon Sep 17 00:00:00 2001 From: Alex Forsythe Date: Wed, 8 Apr 2026 14:08:47 -0400 Subject: [PATCH 17/18] fix: Ensure that profiling DLLs are copied alongside test binary --- tests/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 704ea093..d1ceeddf 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -149,6 +149,12 @@ target_link_libraries(dd_native_tests Catch2::Catch2WithMain ) +# If profiler support is enabled, copy the required runtime DLLs alongside the +# test binary so they can be found at runtime during test discovery and execution +if(DD_ENABLE_PROFILER AND COMMAND dd_win_prof_copy_runtime_deps) + dd_win_prof_copy_runtime_deps(dd_native_tests) +endif() + # Register tests with CTest include(CTest) include(Catch) From 23ca20f2629b5ab3a7d04c55cccbfb395202f6c1 Mon Sep 17 00:00:00 2001 From: Alex Forsythe Date: Wed, 8 Apr 2026 15:42:00 -0400 Subject: [PATCH 18/18] chore: Reformat --- src/datadog/impl/rum/rum.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/datadog/impl/rum/rum.cpp b/src/datadog/impl/rum/rum.cpp index 965a7121..e9fad9b9 100644 --- a/src/datadog/impl/rum/rum.cpp +++ b/src/datadog/impl/rum/rum.cpp @@ -222,7 +222,8 @@ void Rum::DispatchAsync(const RumCommand& command) { // _application_snapshot has been updated with the result of processing our // command; write the relevant UUIDs to the global RumFeatureContext, so that // other features can enrich their events with RUM data - const RumFeatureContext new_context = rum->_application_snapshot.ToFeatureContext(); + const RumFeatureContext new_context = + rum->_application_snapshot.ToFeatureContext(); ctx.rum = new_context; // Notify the callback (if set). Change detection is handled by the recipient.