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/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..b2751e83 --- /dev/null +++ b/cmake/profiler.cmake @@ -0,0 +1,40 @@ +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() + # 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 75f644a + ) +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/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..c8ee71d4 --- /dev/null +++ b/examples/profiling-test/main.cpp @@ -0,0 +1,141 @@ +// 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 "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). +// --------------------------------------------------------------------------- +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.SetSite(datadog::Site::staging); + core_config.SetEventStorageLocation("."); + core_config.SetInitialTrackingConsent(datadog::TrackingConsent::Granted); + + 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-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 e9323f1f..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, }; /** @@ -398,6 +399,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..73da9350 --- /dev/null +++ b/include-cpp/datadog/profiling.hpp @@ -0,0 +1,80 @@ +// 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 "datadog/api.hpp" +#include "datadog/core.hpp" + +// 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 + ); + + 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..55ee2a02 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,75 @@ 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. */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fed7c08a..4817d582 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -272,3 +272,15 @@ 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_compile_definitions(dd_native PRIVATE DD_ENABLE_PROFILER=1) + target_sources(dd_native PRIVATE + datadog/cpp/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 new file mode 100644 index 00000000..0b271291 --- /dev/null +++ b/src/datadog/cpp/profiling.cpp @@ -0,0 +1,58 @@ +// 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/impl/core/core.hpp" +#include "datadog/impl/core/feature.hpp" +#include "datadog/impl/core/util/diagnostics.hpp" +#include "datadog/impl/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{}); + } + + 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{} + ); +} + +} // namespace datadog diff --git a/src/datadog/impl/core/core.cpp b/src/datadog/impl/core/core.cpp index cd5477dd..2bc783a2 100644 --- a/src/datadog/impl/core/core.cpp +++ b/src/datadog/impl/core/core.cpp @@ -20,6 +20,11 @@ #include "datadog/impl/core/util/assert.hpp" #include "datadog/impl/core/version.hpp" +#ifdef DD_ENABLE_PROFILER +#include "datadog/impl/profiling/profiling.hpp" +#include "datadog/impl/rum/rum.hpp" +#endif + namespace datadog::impl { nonstd::expected CoreSubsystems::Init( @@ -400,6 +405,13 @@ 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 // a feature calling UpdateContext in its Start()) are not delivered to a feature's @@ -587,4 +599,38 @@ std::string_view Core::GetApplicationVersion() const { return _context_provider->GetHttpContext().application_version; } +#ifdef DD_ENABLE_PROFILER +void Core::WireRumToProfiling() { + constexpr FeatureId kRumId = CreateFeatureId("RUMM"); + 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); + } else if (feature.id == kProfilingId) { + profiling_impl = std::dynamic_pointer_cast(feature.impl); + } + } + + 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 + // 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..4e184c31 100644 --- a/src/datadog/impl/core/core.hpp +++ b/src/datadog/impl/core/core.hpp @@ -367,6 +367,15 @@ 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 CoreState _state{CoreState::Uninitialized}; diff --git a/src/datadog/impl/core/feature_types/rum.hpp b/src/datadog/impl/core/feature_types/rum.hpp index 81e372db..ac7fa7b5 100644 --- a/src/datadog/impl/core/feature_types/rum.hpp +++ b/src/datadog/impl/core/feature_types/rum.hpp @@ -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 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_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/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 ""; } diff --git a/src/datadog/impl/profiling/profiling.cpp b/src/datadog/impl/profiling/profiling.cpp new file mode 100644 index 00000000..7e7c0155 --- /dev/null +++ b/src/datadog/impl/profiling/profiling.cpp @@ -0,0 +1,101 @@ +// 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/profiling/profiling.hpp" + +#include + +#include "datadog/uuid.hpp" + +namespace datadog::impl { + +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(config); + } else { + ProfilerConfig defaults{}; + defaults.size = sizeof(ProfilerConfig); + _profiler_setup = SetupProfiler(&defaults); + } +} + +FeatureId Profiling::GetId() const { return CreateFeatureId("PROF"); } + +std::string_view Profiling::GetName() const { return "profiling"; } + +void Profiling::Start() { + if (_profiler_setup) { + StartProfiler(); + } +} + +void Profiling::Stop() { + // Don't clear _profiler_setup — StopProfiler/StartProfiler can be called + // repeatedly, and Core supports stop/restart cycles. + if (_profiler_setup) { + StopProfiler(); + } +} + +std::optional Profiling::UploadThread_PrepareReport( + const HttpContext& /*context*/, BatchReader& /*reader*/ +) { + // dd-win-prof manages its own upload pipeline via libdatadog. + 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; + + _app_id_str = context.application_id.ToString(); + _session_id_str = context.session_id.ToString(); + + RumSessionContext session_ctx{}; + session_ctx.application_id = _app_id_str.c_str(); + session_ctx.session_id = _session_id_str.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 { + _view_id_str = context.view_id.ToString(); + + RumViewValues view_vals{}; + view_vals.view_id = _view_id_str.c_str(); + view_vals.view_name = context.view_name; + SetRumView(&view_vals); + } + } +} + +} // namespace datadog::impl diff --git a/src/datadog/impl/profiling/profiling.hpp b/src/datadog/impl/profiling/profiling.hpp new file mode 100644 index 00000000..9a30e558 --- /dev/null +++ b/src/datadog/impl/profiling/profiling.hpp @@ -0,0 +1,65 @@ +// 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/uuid.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: + 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; + + // 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 diff --git a/src/datadog/impl/rum/context.cpp b/src/datadog/impl/rum/context.cpp index 99b32399..b10925f3 100644 --- a/src/datadog/impl/rum/context.cpp +++ b/src/datadog/impl/rum/context.cpp @@ -31,24 +31,34 @@ 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..e9fad9b9 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 @@ -20,6 +20,10 @@ Rum::Rum(const RumConfig& config, const platform::IClock& clock) _application(_deps), _application_snapshot() {} +void Rum::SetContextChangeCallback(RumContextChangeCallback callback) { + _context_change_callback = std::move(callback); +} + std::optional Rum::UploadThread_PrepareReport( const HttpContext& context, BatchReader& reader ) { @@ -218,7 +222,15 @@ 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 (rum->_context_change_callback) { + rum->_context_change_callback(new_context.ToPublicContext()); + } } }); } diff --git a/src/datadog/impl/rum/rum.hpp b/src/datadog/impl/rum/rum.hpp index 0f2f0a1f..4877c5bf 100644 --- a/src/datadog/impl/rum/rum.hpp +++ b/src/datadog/impl/rum/rum.hpp @@ -180,6 +180,17 @@ class Rum final : public Feature { // HTTP request details used on upload; owned by the upload thread std::string _request_url; std::string _request_headers; + + 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; }; } // namespace datadog::impl 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)