diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ee60bdf4..e50999f52 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -234,22 +234,35 @@ jobs: cmake -S .. -DDatadog_ROOT=$LIBDD_OUTPUT_FOLDER cmake --build . fi - - name: "Test building CXX bindings (Unix)" + - name: "Test building CXX bindings - Crashinfo (Unix)" shell: bash if: matrix.platform != 'windows-latest' run: | set -e cd examples/cxx ./build-and-run-crashinfo.sh + - name: "Test building CXX bindings - Profiling (Unix)" + shell: bash + if: matrix.platform != 'windows-latest' + run: | + set -e + cd examples/cxx + ./build-profiling.sh - name: "Setup MSVC (Windows)" if: matrix.platform == 'windows-latest' uses: ilammy/msvc-dev-cmd@v1 - - name: "Test building CXX bindings (Windows)" + - name: "Test building CXX bindings - Crashinfo (Windows)" shell: pwsh if: matrix.platform == 'windows-latest' run: | cd examples/cxx .\build-and-run-crashinfo.ps1 + - name: "Test building CXX bindings - Profiling (Windows)" + shell: pwsh + if: matrix.platform == 'windows-latest' + run: | + cd examples/cxx + .\build-profiling.ps1 cross-centos7: name: build and test using cross - on centos7 diff --git a/.gitignore b/.gitignore index 07fa526c8..57165b9df 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,10 @@ docker-sync.yml libtest.so libtest_cpp.so examples/cxx/crashinfo +examples/cxx/crashinfo.exe +examples/cxx/profiling +examples/cxx/profiling.exe +examples/cxx/owned_sample +examples/cxx/owned_sample.exe +profile.pprof +profile_owned_sample.pprof diff --git a/Cargo.lock b/Cargo.lock index af1ddb3ad..217d9a046 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,6 +49,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + [[package]] name = "allocator-api2" version = "0.2.21" @@ -1620,6 +1626,26 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "enum-ordinalize" version = "3.1.15" @@ -2862,11 +2888,16 @@ dependencies = [ "anyhow", "bitmaps", "bolero", + "bumpalo", "byteorder", "bytes", "chrono", "criterion", + "crossbeam-queue", "crossbeam-utils", + "cxx", + "cxx-build", + "enum-map", "futures", "hashbrown 0.16.0", "http", @@ -2879,6 +2910,8 @@ dependencies = [ "libdd-profiling-protobuf", "lz4_flex", "mime", + "nix 0.29.0", + "ouroboros", "parking_lot", "proptest", "prost", @@ -3680,6 +3713,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.110", +] + [[package]] name = "page_size" version = "0.6.0" @@ -3966,6 +4023,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", + "version_check", + "yansi", +] + [[package]] name = "proptest" version = "1.5.0" diff --git a/examples/cxx/README.md b/examples/cxx/README.md index 68ea54112..8cbddc2e8 100644 --- a/examples/cxx/README.md +++ b/examples/cxx/README.md @@ -1,12 +1,35 @@ -# CXX Bindings Example for libdd-crashtracker +# CXX Bindings Examples -This example demonstrates how to use the CXX bindings for the libdd-crashtracker crate, providing a safer and more idiomatic C++ API compared to the traditional C FFI. +This directory contains C++ examples demonstrating the CXX bindings for libdatadog components. -## Features +CXX bindings provide a safer and more idiomatic C++ API compared to the traditional C FFI bindings, with automatic memory management and exception handling. -The CXX bindings provide access to: +## Examples -### Core Types +### Crashtracker (`crashinfo.cpp`) + +Demonstrates building crash reports using the CXX bindings for `libdd-crashtracker`. + +**Build and run:** + +Unix (Linux/macOS): +```bash +./build-and-run-crashinfo.sh +``` + +Windows: +```powershell +.\build-and-run-crashinfo.ps1 +``` + +**Key features:** +- Type-safe crash report builder API +- Support for stack traces, frames, and metadata +- Process and OS information +- Automatic memory management +- Exception-based error handling + +**Core Types:** - `CrashInfoBuilder` - Builder for constructing crash information - `StackFrame` - Individual stack frame with debug info and addresses - `StackTrace` - Collection of stack frames @@ -15,115 +38,51 @@ The CXX bindings provide access to: - `ProcInfo` - Process information - `OsInfo` - Operating system information -### Enums -- `ErrorKind` - Type of error (Panic, UnhandledException, UnixSignal) -- `BuildIdType` - Build ID format (GNU, GO, PDB, SHA1) -- `FileType` - Binary file format (APK, ELF, PE) +**Enums:** +- `CxxErrorKind` - Type of error (Panic, UnhandledException, UnixSignal) +- `CxxBuildIdType` - Build ID format (GNU, GO, PDB, SHA1) +- `CxxFileType` - Binary file format (APK, ELF, PE) -### Key API +### Profiling (`profiling.cpp`) -**Object Creation:** -```cpp -auto builder = CrashInfoBuilder::create(); -auto frame = StackFrame::create(); -auto stacktrace = StackTrace::create(); -``` +Demonstrates building profiling data using the CXX bindings for `libdd-profiling`. -**CrashInfoBuilder Methods:** -- `set_kind(CxxErrorKind)` - Set error type (Panic, UnhandledException, UnixSignal) -- `with_message(String)` - Set error message -- `with_counter(String, i64)` - Add a named counter -- `with_log_message(String, bool)` - Add a log message -- `with_fingerprint(String)` - Set crash fingerprint -- `with_incomplete(bool)` - Mark as incomplete -- `set_metadata(Metadata)` - Set library metadata -- `set_proc_info(ProcInfo)` - Set process information -- `set_os_info(OsInfo)` - Set OS information -- `add_stack(Box)` - Add a stack trace -- `with_timestamp_now()` - Set current timestamp -- `with_file(String)` - Add a file to the report - -**StackFrame Methods:** -- `with_function(String)`, `with_file(String)`, `with_line(u32)`, `with_column(u32)` - Set debug info -- `with_ip(usize)`, `with_sp(usize)` - Set instruction/stack pointers -- `with_module_base_address(usize)`, `with_symbol_address(usize)` - Set base addresses -- `with_build_id(String)` - Set build ID -- `build_id_type(CxxBuildIdType)` - Set build ID format (GNU, GO, PDB, SHA1) -- `file_type(CxxFileType)` - Set binary format (APK, ELF, PE) -- `with_path(String)` - Set module path -- `with_relative_address(usize)` - Set relative address - -**StackTrace Methods:** -- `add_frame(Box, bool)` - Add a frame (bool = incomplete) -- `mark_complete()` - Mark trace as complete - -**Building & Output:** -```cpp -auto crash_info = crashinfo_build(std::move(builder)); -auto json = crash_info->to_json(); -``` - -## Building and Running - -### Unix (Linux/macOS) - -The `build-and-run-crashinfo.sh` script handles the entire build process: +**Build and run:** +Unix (Linux/macOS): ```bash -./examples/cxx/build-and-run-crashinfo.sh +./build-profiling.sh ``` -### Windows - -The `build-and-run-crashinfo.ps1` PowerShell script handles the build process on Windows: - +Windows: ```powershell -.\examples\cxx\build-and-run-crashinfo.ps1 +.\build-profiling.ps1 ``` -**Prerequisites for Windows:** -- Either MSVC (via Visual Studio) or MinGW/LLVM with C++ compiler -- PowerShell 5.0 or later (comes with Windows 10+) -- Rust toolchain - -The build script will: -1. Build libdd-crashtracker with the `cxx` feature enabled -2. Find the CXX bridge headers and libraries -3. Compile the C++ example (automatically detects MSVC or MinGW/Clang) -4. Run the example and display the output - -## Example Output - -The example creates a crash report with: -- Error kind and message -- Library metadata with tags -- Process and OS information -- A stack trace with multiple frames (debug info + binary addresses) -- Counters and log messages -- Timestamp - -The output is a JSON object that can be sent to Datadog's crash tracking service. - -## Notes +**Key features:** +- Type-safe API for building profiles +- Support for samples, locations, mappings, and labels +- String interning for efficient memory usage +- Pprof format serialization with zstd compression +- Automatic memory management +- Exception-based error handling +- Modern C++20 syntax with designated initializers and `std::format` -- The CXX bindings use `rust::String` types which need to be converted to `std::string` for use with standard C++ streams -- All functions that can fail will use exceptions (standard C++ exception handling) -- The bindings are type-safe and prevent many common C FFI errors -- Memory is managed automatically through RAII and smart pointers +## Build Scripts -## Comparison to C FFI +The examples use a consolidated build system: -The CXX bindings provide several advantages over the traditional C FFI: +- **Unix (Linux/macOS)**: `build-and-run.sh ` +- **Windows**: `build-and-run.ps1 -CrateName -ExampleName ` -1. **Type Safety**: No void pointers, proper type checking at compile time -2. **Memory Safety**: Automatic memory management through smart pointers -3. **Ergonomics**: More natural C++ idioms, no need for manual handle management -4. **Error Handling**: Exceptions instead of error codes -5. **String Handling**: Seamless `rust::String` ↔ C++ string interop +Convenience wrappers are provided for each example: +- `build-and-run-crashinfo.sh` / `build-and-run-crashinfo.ps1` +- `build-profiling.sh` / `build-profiling.ps1` ## Requirements - C++20 or later - Rust toolchain +- C++ compiler (clang++ or g++) - Platform: macOS, Linux, or Windows - Windows: Requires MSVC (via Visual Studio) or MinGW/LLVM diff --git a/examples/cxx/build-and-run-crashinfo.ps1 b/examples/cxx/build-and-run-crashinfo.ps1 index bd920608c..285c6754c 100644 --- a/examples/cxx/build-and-run-crashinfo.ps1 +++ b/examples/cxx/build-and-run-crashinfo.ps1 @@ -1,159 +1,7 @@ # Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ # SPDX-License-Identifier: Apache-2.0 -# Build and run the CXX crashinfo example on Windows -$ErrorActionPreference = "Stop" - +# Build and run the CXX crashinfo example $SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path -$PROJECT_ROOT = (Get-Item (Join-Path $SCRIPT_DIR ".." "..")).FullName -Set-Location $PROJECT_ROOT - -Write-Host "šŸ”Ø Building libdd-crashtracker with cxx feature..." -ForegroundColor Cyan -cargo build -p libdd-crashtracker --features cxx --release - -Write-Host "šŸ” Finding CXX bridge headers..." -ForegroundColor Cyan -$CXX_BRIDGE_INCLUDE = Get-ChildItem -Path "target\release\build\libdd-crashtracker-*\out\cxxbridge\include" -Directory -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName -$CXX_BRIDGE_CRATE = Get-ChildItem -Path "target\release\build\libdd-crashtracker-*\out\cxxbridge\crate" -Directory -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName -$RUST_CXX_INCLUDE = Get-ChildItem -Path "target\release\build\cxx-*\out" -Directory -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName - -if (-not $CXX_BRIDGE_INCLUDE -or -not $CXX_BRIDGE_CRATE -or -not $RUST_CXX_INCLUDE) { - Write-Host "āŒ Error: Could not find CXX bridge directories" -ForegroundColor Red - exit 1 -} - -Write-Host "šŸ“ CXX include: $CXX_BRIDGE_INCLUDE" -ForegroundColor Green -Write-Host "šŸ“ CXX crate: $CXX_BRIDGE_CRATE" -ForegroundColor Green -Write-Host "šŸ“ Rust CXX: $RUST_CXX_INCLUDE" -ForegroundColor Green - -# Check if we have MSVC (cl.exe) or MinGW (g++/clang++) -# Note: Prefer MSVC on Windows as it's the default Rust toolchain -$MSVC = Get-Command cl.exe -ErrorAction SilentlyContinue -$GPP = Get-Command g++.exe -ErrorAction SilentlyContinue -$CLANGPP = Get-Command clang++.exe -ErrorAction SilentlyContinue - -# Auto-detect which toolchain Rust used by checking which library exists -# Note: On Windows, Rust still uses 'lib' prefix even for MSVC .lib files -$HAS_MSVC_LIB = Test-Path (Join-Path $PROJECT_ROOT "target\release\libdd_crashtracker.lib") -$HAS_GNU_LIB = (Test-Path (Join-Path $PROJECT_ROOT "target\release\libdd_crashtracker.a")) -or ` - (Test-Path (Join-Path $PROJECT_ROOT "target\release\liblibdd_crashtracker.a")) - -if ($HAS_MSVC_LIB -and $MSVC) { - $USE_MSVC = $true - Write-Host "Detected MSVC Rust toolchain" -ForegroundColor Cyan -} elseif ($HAS_GNU_LIB -and ($GPP -or $CLANGPP)) { - $USE_MSVC = $false - Write-Host "Detected GNU Rust toolchain" -ForegroundColor Cyan -} elseif ($MSVC) { - $USE_MSVC = $true - Write-Host "Defaulting to MSVC (library not found yet, will check after)" -ForegroundColor Yellow -} elseif ($GPP -or $CLANGPP) { - $USE_MSVC = $false - Write-Host "Defaulting to GNU toolchain (library not found yet, will check after)" -ForegroundColor Yellow -} else { - Write-Host "āŒ Error: No C++ compiler found. Please install MSVC (via Visual Studio) or MinGW/LLVM" -ForegroundColor Red - exit 1 -} - -Write-Host "šŸ”Ø Finding libraries..." -ForegroundColor Cyan -# Note: Rust library naming varies by platform and toolchain -if ($USE_MSVC) { - # MSVC: libdd_crashtracker.lib (Rust keeps the lib prefix even on Windows) - $CRASHTRACKER_LIB = Join-Path $PROJECT_ROOT "target\release\libdd_crashtracker.lib" - $CXX_BRIDGE_LIB = Get-ChildItem -Path "target\release\build\libdd-crashtracker-*\out" -Filter "libdd-crashtracker-cxx.lib" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName -} else { - # MinGW: Try both possible naming patterns - $CRASHTRACKER_LIB_1 = Join-Path $PROJECT_ROOT "target\release\libdd_crashtracker.a" - $CRASHTRACKER_LIB_2 = Join-Path $PROJECT_ROOT "target\release\liblibdd_crashtracker.a" - - if (Test-Path $CRASHTRACKER_LIB_1) { - $CRASHTRACKER_LIB = $CRASHTRACKER_LIB_1 - } elseif (Test-Path $CRASHTRACKER_LIB_2) { - $CRASHTRACKER_LIB = $CRASHTRACKER_LIB_2 - } else { - $CRASHTRACKER_LIB = $CRASHTRACKER_LIB_1 # Use this for error message - } - - # Try both naming patterns for CXX bridge - $CXX_BRIDGE_LIB = Get-ChildItem -Path "target\release\build\libdd-crashtracker-*\out" -Filter "libdd-crashtracker-cxx.a" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName - if (-not $CXX_BRIDGE_LIB) { - $CXX_BRIDGE_LIB = Get-ChildItem -Path "target\release\build\libdd-crashtracker-*\out" -Filter "liblibdd-crashtracker-cxx.a" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName - } -} - -if (-not (Test-Path $CRASHTRACKER_LIB)) { - Write-Host "āŒ Error: Could not find libdd-crashtracker library at $CRASHTRACKER_LIB" -ForegroundColor Red - if (-not $MSVC) { - Write-Host "Searched for: libdd_crashtracker.a and liblibdd_crashtracker.a" -ForegroundColor Yellow - Write-Host "Files in target/release/:" -ForegroundColor Yellow - Get-ChildItem -Path "target\release" -Filter "*crashtracker*" | Select-Object -First 10 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } - } - exit 1 -} - -if (-not $CXX_BRIDGE_LIB) { - if ($USE_MSVC) { - Write-Host "āŒ Error: Could not find CXX bridge library (looking for libdd-crashtracker-cxx.lib)" -ForegroundColor Red - } else { - Write-Host "āŒ Error: Could not find CXX bridge library" -ForegroundColor Red - Write-Host "Searched for: libdd-crashtracker-cxx.a and liblibdd-crashtracker-cxx.a" -ForegroundColor Yellow - } - exit 1 -} - -Write-Host "šŸ“š Crashtracker library: $CRASHTRACKER_LIB" -ForegroundColor Green -Write-Host "šŸ“š CXX bridge library: $CXX_BRIDGE_LIB" -ForegroundColor Green - -Write-Host "šŸ”Ø Compiling C++ example..." -ForegroundColor Cyan - -if ($USE_MSVC) { - Write-Host "Using MSVC compiler" -ForegroundColor Yellow - - # MSVC compilation - cl.exe /std:c++20 /EHsc ` - /I"$CXX_BRIDGE_INCLUDE" ` - /I"$CXX_BRIDGE_CRATE" ` - /I"$RUST_CXX_INCLUDE" ` - /I"$PROJECT_ROOT" ` - examples\cxx\crashinfo.cpp ` - "$CRASHTRACKER_LIB" ` - "$CXX_BRIDGE_LIB" ` - ws2_32.lib advapi32.lib userenv.lib ntdll.lib bcrypt.lib ` - /Fe:examples\cxx\crashinfo.exe - - if ($LASTEXITCODE -ne 0) { - Write-Host "āŒ Compilation failed" -ForegroundColor Red - exit 1 - } -} elseif ($GPP -or $CLANGPP) { - $COMPILER = if ($GPP) { "g++" } else { "clang++" } - Write-Host "Using $COMPILER compiler" -ForegroundColor Yellow - - # MinGW/Clang compilation - needs proper library ordering and Rust std lib - & $COMPILER -std=c++20 ` - -I"$CXX_BRIDGE_INCLUDE" ` - -I"$CXX_BRIDGE_CRATE" ` - -I"$RUST_CXX_INCLUDE" ` - -I"$PROJECT_ROOT" ` - examples/cxx/crashinfo.cpp ` - "$CXX_BRIDGE_LIB" ` - "$CRASHTRACKER_LIB" ` - -lws2_32 -ladvapi32 -luserenv -lntdll -lbcrypt -lgcc_eh -lpthread ` - -o examples/cxx/crashinfo.exe - - if ($LASTEXITCODE -ne 0) { - Write-Host "āŒ Compilation failed" -ForegroundColor Red - exit 1 - } -} - -Write-Host "šŸš€ Running example..." -ForegroundColor Cyan -& ".\examples\cxx\crashinfo.exe" - -if ($LASTEXITCODE -ne 0) { - Write-Host "āŒ Example failed with exit code $LASTEXITCODE" -ForegroundColor Red - exit 1 -} - -Write-Host "" -Write-Host "āœ… Success!" -ForegroundColor Green - +& "$SCRIPT_DIR\build-and-run.ps1" -CrateName "libdd-crashtracker" -ExampleName "crashinfo" +exit $LASTEXITCODE diff --git a/examples/cxx/build-and-run-crashinfo.sh b/examples/cxx/build-and-run-crashinfo.sh index acc154888..055c158ed 100755 --- a/examples/cxx/build-and-run-crashinfo.sh +++ b/examples/cxx/build-and-run-crashinfo.sh @@ -3,67 +3,5 @@ # SPDX-License-Identifier: Apache-2.0 # Build and run the CXX crashinfo example -set -e - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -cd "$PROJECT_ROOT" - -echo "šŸ”Ø Building libdd-crashtracker with cxx feature..." -cargo build -p libdd-crashtracker --features cxx --release - -echo "šŸ” Finding CXX bridge headers..." -CXX_BRIDGE_INCLUDE=$(find target/release/build/libdd-crashtracker-*/out/cxxbridge/include -type d 2>/dev/null | head -n 1) -CXX_BRIDGE_CRATE=$(find target/release/build/libdd-crashtracker-*/out/cxxbridge/crate -type d 2>/dev/null | head -n 1) -RUST_CXX_INCLUDE=$(find target/release/build/cxx-*/out -type d 2>/dev/null | head -n 1) - -if [ -z "$CXX_BRIDGE_INCLUDE" ] || [ -z "$CXX_BRIDGE_CRATE" ] || [ -z "$RUST_CXX_INCLUDE" ]; then - echo "āŒ Error: Could not find CXX bridge directories" - exit 1 -fi - -echo "šŸ“ CXX include: $CXX_BRIDGE_INCLUDE" -echo "šŸ“ CXX crate: $CXX_BRIDGE_CRATE" -echo "šŸ“ Rust CXX: $RUST_CXX_INCLUDE" - -echo "šŸ”Ø Finding libraries..." -CRASHTRACKER_LIB="$PROJECT_ROOT/target/release/liblibdd_crashtracker.a" -CXX_BRIDGE_LIB=$(find target/release/build/libdd-crashtracker-*/out -name "liblibdd-crashtracker-cxx.a" | head -n 1) - -if [ ! -f "$CRASHTRACKER_LIB" ]; then - echo "āŒ Error: Could not find libdd-crashtracker library at $CRASHTRACKER_LIB" - exit 1 -fi - -if [ ! -f "$CXX_BRIDGE_LIB" ]; then - echo "āŒ Error: Could not find CXX bridge library" - exit 1 -fi - -echo "šŸ“š Crashtracker library: $CRASHTRACKER_LIB" -echo "šŸ“š CXX bridge library: $CXX_BRIDGE_LIB" - -echo "šŸ”Ø Compiling C++ example..." -# Platform-specific linker flags -if [[ "$OSTYPE" == "darwin"* ]]; then - PLATFORM_LIBS="-framework Security -framework CoreFoundation" -else - PLATFORM_LIBS="" -fi - -c++ -std=c++20 \ - -I"$CXX_BRIDGE_INCLUDE" \ - -I"$CXX_BRIDGE_CRATE" \ - -I"$RUST_CXX_INCLUDE" \ - -I"$PROJECT_ROOT" \ - examples/cxx/crashinfo.cpp \ - "$CRASHTRACKER_LIB" \ - "$CXX_BRIDGE_LIB" \ - -lpthread -ldl $PLATFORM_LIBS \ - -o examples/cxx/crashinfo - -echo "šŸš€ Running example..." -./examples/cxx/crashinfo - -echo "" -echo "āœ… Success!" +exec "$SCRIPT_DIR/build-and-run.sh" libdd-crashtracker crashinfo diff --git a/examples/cxx/build-and-run.ps1 b/examples/cxx/build-and-run.ps1 new file mode 100644 index 000000000..cf7fbd76b --- /dev/null +++ b/examples/cxx/build-and-run.ps1 @@ -0,0 +1,165 @@ +# Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +# Generic script to build and run CXX examples +# Usage: .\build-and-run.ps1 +# Example: .\build-and-run.ps1 libdd-profiling profiling + +param( + [Parameter(Mandatory=$true)] + [string]$CrateName, + + [Parameter(Mandatory=$true)] + [string]$ExampleName +) + +$ErrorActionPreference = "Stop" + +$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path +$PROJECT_ROOT = (Get-Item (Join-Path $SCRIPT_DIR ".." "..")).FullName +Set-Location $PROJECT_ROOT + +Write-Host "šŸ”Ø Building $CrateName with cxx feature..." -ForegroundColor Cyan +cargo build -p $CrateName --features cxx --release + +Write-Host "šŸ” Finding CXX bridge headers..." -ForegroundColor Cyan +$CXX_BRIDGE_INCLUDE = Get-ChildItem -Path "target\release\build\$CrateName-*\out\cxxbridge\include" -Directory -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName +$CXX_BRIDGE_CRATE = Get-ChildItem -Path "target\release\build\$CrateName-*\out\cxxbridge\crate" -Directory -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName +$RUST_CXX_INCLUDE = Get-ChildItem -Path "target\release\build\cxx-*\out" -Directory -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName + +if (-not $CXX_BRIDGE_INCLUDE -or -not $CXX_BRIDGE_CRATE -or -not $RUST_CXX_INCLUDE) { + Write-Host "āŒ Error: Could not find CXX bridge directories" -ForegroundColor Red + exit 1 +} + +Write-Host "šŸ“ CXX include: $CXX_BRIDGE_INCLUDE" -ForegroundColor Green +Write-Host "šŸ“ CXX crate: $CXX_BRIDGE_CRATE" -ForegroundColor Green +Write-Host "šŸ“ Rust CXX: $RUST_CXX_INCLUDE" -ForegroundColor Green + +# Check if we have MSVC (cl.exe) or MinGW (g++/clang++) +$MSVC = Get-Command cl.exe -ErrorAction SilentlyContinue +$GPP = Get-Command g++.exe -ErrorAction SilentlyContinue +$CLANGPP = Get-Command clang++.exe -ErrorAction SilentlyContinue + +# Convert crate name with dashes to underscores for library name +$LibName = $CrateName -replace '-', '_' + +# Auto-detect which toolchain Rust used by checking which library exists +$HAS_MSVC_LIB = Test-Path (Join-Path $PROJECT_ROOT "target\release\${LibName}.lib") +$HAS_GNU_LIB = (Test-Path (Join-Path $PROJECT_ROOT "target\release\${LibName}.a")) -or ` + (Test-Path (Join-Path $PROJECT_ROOT "target\release\lib${LibName}.a")) + +if ($HAS_MSVC_LIB -and $MSVC) { + $USE_MSVC = $true + Write-Host "Detected MSVC Rust toolchain" -ForegroundColor Cyan +} elseif ($HAS_GNU_LIB -and ($GPP -or $CLANGPP)) { + $USE_MSVC = $false + Write-Host "Detected GNU Rust toolchain" -ForegroundColor Cyan +} elseif ($MSVC) { + $USE_MSVC = $true + Write-Host "Defaulting to MSVC (library not found yet, will check after)" -ForegroundColor Yellow +} elseif ($GPP -or $CLANGPP) { + $USE_MSVC = $false + Write-Host "Defaulting to GNU toolchain (library not found yet, will check after)" -ForegroundColor Yellow +} else { + Write-Host "āŒ Error: No C++ compiler found. Please install MSVC (via Visual Studio) or MinGW/LLVM" -ForegroundColor Red + exit 1 +} + +Write-Host "šŸ”Ø Finding libraries..." -ForegroundColor Cyan +if ($USE_MSVC) { + # MSVC naming + $CRATE_LIB = Join-Path $PROJECT_ROOT "target\release\${LibName}.lib" + $CXX_BRIDGE_LIB = Get-ChildItem -Path "target\release\build\$CrateName-*\out" -Filter "$CrateName-cxx.lib" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName +} else { + # MinGW naming - try both patterns + $CRATE_LIB_1 = Join-Path $PROJECT_ROOT "target\release\${LibName}.a" + $CRATE_LIB_2 = Join-Path $PROJECT_ROOT "target\release\lib${LibName}.a" + + if (Test-Path $CRATE_LIB_1) { + $CRATE_LIB = $CRATE_LIB_1 + } elseif (Test-Path $CRATE_LIB_2) { + $CRATE_LIB = $CRATE_LIB_2 + } else { + $CRATE_LIB = $CRATE_LIB_1 + } + + # Try both naming patterns for CXX bridge + $CXX_BRIDGE_LIB = Get-ChildItem -Path "target\release\build\$CrateName-*\out" -Filter "$CrateName-cxx.a" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName + if (-not $CXX_BRIDGE_LIB) { + $CXX_BRIDGE_LIB = Get-ChildItem -Path "target\release\build\$CrateName-*\out" -Filter "lib$CrateName-cxx.a" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName + } +} + +if (-not (Test-Path $CRATE_LIB)) { + Write-Host "āŒ Error: Could not find $CrateName library at $CRATE_LIB" -ForegroundColor Red + if (-not $USE_MSVC) { + Write-Host "Searched for: ${LibName}.a and lib${LibName}.a" -ForegroundColor Yellow + } + exit 1 +} + +if (-not $CXX_BRIDGE_LIB) { + Write-Host "āŒ Error: Could not find CXX bridge library for $CrateName" -ForegroundColor Red + exit 1 +} + +Write-Host "šŸ“š Crate library: $CRATE_LIB" -ForegroundColor Green +Write-Host "šŸ“š CXX bridge library: $CXX_BRIDGE_LIB" -ForegroundColor Green + +Write-Host "šŸ”Ø Compiling C++ example..." -ForegroundColor Cyan + +$ExampleCpp = "examples\cxx\$ExampleName.cpp" +$ExampleExe = "examples\cxx\$ExampleName.exe" + +if ($USE_MSVC) { + Write-Host "Using MSVC compiler" -ForegroundColor Yellow + + cl.exe /std:c++20 /EHsc ` + /I"$CXX_BRIDGE_INCLUDE" ` + /I"$CXX_BRIDGE_CRATE" ` + /I"$RUST_CXX_INCLUDE" ` + /I"$PROJECT_ROOT" ` + $ExampleCpp ` + "$CRATE_LIB" ` + "$CXX_BRIDGE_LIB" ` + ws2_32.lib advapi32.lib userenv.lib ntdll.lib bcrypt.lib ` + /Fe:$ExampleExe + + if ($LASTEXITCODE -ne 0) { + Write-Host "āŒ Compilation failed" -ForegroundColor Red + exit 1 + } +} elseif ($GPP -or $CLANGPP) { + $COMPILER = if ($GPP) { "g++" } else { "clang++" } + Write-Host "Using $COMPILER compiler" -ForegroundColor Yellow + + & $COMPILER -std=c++20 ` + -I"$CXX_BRIDGE_INCLUDE" ` + -I"$CXX_BRIDGE_CRATE" ` + -I"$RUST_CXX_INCLUDE" ` + -I"$PROJECT_ROOT" ` + $ExampleCpp ` + "$CXX_BRIDGE_LIB" ` + "$CRATE_LIB" ` + -lws2_32 -ladvapi32 -luserenv -lntdll -lbcrypt -lgcc_eh -lpthread ` + -o $ExampleExe + + if ($LASTEXITCODE -ne 0) { + Write-Host "āŒ Compilation failed" -ForegroundColor Red + exit 1 + } +} + +Write-Host "šŸš€ Running example..." -ForegroundColor Cyan +& ".\$ExampleExe" + +if ($LASTEXITCODE -ne 0) { + Write-Host "āŒ Example failed with exit code $LASTEXITCODE" -ForegroundColor Red + exit 1 +} + +Write-Host "" +Write-Host "āœ… Success!" -ForegroundColor Green + + diff --git a/examples/cxx/build-and-run.sh b/examples/cxx/build-and-run.sh new file mode 100755 index 000000000..0a1409d8c --- /dev/null +++ b/examples/cxx/build-and-run.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +# Generic script to build and run CXX examples +# Usage: ./build-and-run.sh +# Example: ./build-and-run.sh libdd-profiling profiling + +set -e + +if [ $# -ne 2 ]; then + echo "Usage: $0 " + echo "Example: $0 libdd-profiling profiling" + exit 1 +fi + +CRATE_NAME="$1" +EXAMPLE_NAME="$2" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$PROJECT_ROOT" + +echo "šŸ”Ø Building $CRATE_NAME with cxx feature..." +cargo build -p "$CRATE_NAME" --features cxx --release + +echo "šŸ” Finding CXX bridge headers..." +CXX_BRIDGE_INCLUDE=$(find target/release/build/${CRATE_NAME}-*/out/cxxbridge/include -type d 2>/dev/null | head -n 1) +CXX_BRIDGE_CRATE=$(find target/release/build/${CRATE_NAME}-*/out/cxxbridge/crate -type d 2>/dev/null | head -n 1) +RUST_CXX_INCLUDE=$(find target/release/build/cxx-*/out -type d 2>/dev/null | head -n 1) + +if [ -z "$CXX_BRIDGE_INCLUDE" ] || [ -z "$CXX_BRIDGE_CRATE" ] || [ -z "$RUST_CXX_INCLUDE" ]; then + echo "āŒ Error: Could not find CXX bridge directories" + exit 1 +fi + +echo "šŸ“ CXX include: $CXX_BRIDGE_INCLUDE" +echo "šŸ“ CXX crate: $CXX_BRIDGE_CRATE" +echo "šŸ“ Rust CXX: $RUST_CXX_INCLUDE" + +echo "šŸ”Ø Finding libraries..." +# Convert crate name with dashes to underscores for library name +LIB_NAME=$(echo "$CRATE_NAME" | tr '-' '_') +CRATE_LIB="$PROJECT_ROOT/target/release/lib${LIB_NAME}.a" +CXX_BRIDGE_LIB=$(find target/release/build/${CRATE_NAME}-*/out -name "lib${CRATE_NAME}-cxx.a" | head -n 1) + +if [ ! -f "$CRATE_LIB" ]; then + echo "āŒ Error: Could not find $CRATE_NAME library at $CRATE_LIB" + exit 1 +fi + +if [ ! -f "$CXX_BRIDGE_LIB" ]; then + echo "āŒ Error: Could not find CXX bridge library" + exit 1 +fi + +echo "šŸ“š Crate library: $CRATE_LIB" +echo "šŸ“š CXX bridge library: $CXX_BRIDGE_LIB" + +echo "šŸ”Ø Compiling C++ example..." +# Platform-specific linker flags +if [[ "$OSTYPE" == "darwin"* ]]; then + PLATFORM_LIBS="-framework Security -framework CoreFoundation" +else + PLATFORM_LIBS="" +fi + +c++ -std=c++20 \ + -I"$CXX_BRIDGE_INCLUDE" \ + -I"$CXX_BRIDGE_CRATE" \ + -I"$RUST_CXX_INCLUDE" \ + -I"$PROJECT_ROOT" \ + "examples/cxx/${EXAMPLE_NAME}.cpp" \ + "$CRATE_LIB" \ + "$CXX_BRIDGE_LIB" \ + -lpthread -ldl $PLATFORM_LIBS \ + -o "examples/cxx/${EXAMPLE_NAME}" + +echo "šŸš€ Running example..." +"./examples/cxx/${EXAMPLE_NAME}" + +echo "" +echo "āœ… Success!" + + diff --git a/examples/cxx/build-owned-sample.ps1 b/examples/cxx/build-owned-sample.ps1 new file mode 100644 index 000000000..8c504d7f2 --- /dev/null +++ b/examples/cxx/build-owned-sample.ps1 @@ -0,0 +1,8 @@ +# Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +# Build and run the CXX owned_sample example +$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path +& "$SCRIPT_DIR\build-and-run.ps1" -CrateName "libdd-profiling" -ExampleName "owned_sample" +exit $LASTEXITCODE + diff --git a/examples/cxx/build-owned-sample.sh b/examples/cxx/build-owned-sample.sh new file mode 100755 index 000000000..e62c28fc2 --- /dev/null +++ b/examples/cxx/build-owned-sample.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +# Build and run the CXX owned_sample example +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/build-and-run.sh" libdd-profiling owned_sample + diff --git a/examples/cxx/build-profiling.ps1 b/examples/cxx/build-profiling.ps1 new file mode 100644 index 000000000..b58d23858 --- /dev/null +++ b/examples/cxx/build-profiling.ps1 @@ -0,0 +1,7 @@ +# Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +# Build and run the CXX profiling example +$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path +& "$SCRIPT_DIR\build-and-run.ps1" -CrateName "libdd-profiling" -ExampleName "profiling" +exit $LASTEXITCODE diff --git a/examples/cxx/build-profiling.sh b/examples/cxx/build-profiling.sh new file mode 100755 index 000000000..defd8a08c --- /dev/null +++ b/examples/cxx/build-profiling.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +# Build and run the CXX profiling example +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/build-and-run.sh" libdd-profiling profiling diff --git a/examples/cxx/owned_sample.cpp b/examples/cxx/owned_sample.cpp new file mode 100644 index 000000000..4c9dd6caa --- /dev/null +++ b/examples/cxx/owned_sample.cpp @@ -0,0 +1,217 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#ifdef __unix__ +#include +#endif +#include "libdd-profiling/src/cxx.rs.h" + +using namespace datadog::profiling; + +int main() { + try { + std::cout << "Creating Profile using CXX bindings with OwnedSample..." << std::endl; + + ValueType wall_time{ + .type_ = "wall-time", + .unit = "nanoseconds" + }; + + Period period{ + .value_type = wall_time, + .value = 60 + }; + + auto profile = Profile::create({wall_time}, period); + std::cout << "āœ… Profile created" << std::endl; + + std::cout << "Adding upscaling rules..." << std::endl; + + // Poisson upscaling for sampled data + constexpr auto value_offsets = std::array{size_t{0}}; + profile->add_upscaling_rule_poisson( + rust::Slice(value_offsets.data(), value_offsets.size()), + "thread_id", + "0", + 0, + 0, + 1000000 + ); + + // Proportional upscaling (scale by factor) + profile->add_upscaling_rule_proportional( + rust::Slice(value_offsets.data(), value_offsets.size()), + "thread_id", + "1", + 100.0 + ); + + std::cout << "āœ… Added upscaling rules" << std::endl; + + std::cout << "Creating Metadata and SamplePool for efficient sample reuse..." << std::endl; + + // Create metadata (configuration shared by all samples in the pool) + // Parameters: sample_types, max_frames, arena_allocation_limit (no_allocation_limit() or bytes), timeline_enabled + auto metadata = Metadata::create({SampleType::WallTime}, 64, no_allocation_limit(), true); + + // Create a pool of reusable samples using the metadata + // Parameters: metadata, capacity + auto pool = SamplePool::create(*metadata, 10); + std::cout << "āœ… Created SamplePool with capacity " << pool->pool_capacity() << std::endl; + + std::cout << "Adding samples..." << std::endl; + for (int i = 0; i < 100; i++) { + // Get a sample from the pool (reused if available, freshly allocated if not) + auto owned_sample = pool->get_sample(); + + // Set the wall time value + auto wall_time_value = 1000000 + (i % 1000) * 1000; + owned_sample->set_value(SampleType::WallTime, wall_time_value); + + // Set the end time to the current time + // This is the simplest way to set the endtime + try { + owned_sample->set_endtime_ns_now(); + } catch (const std::exception& e) { + std::cerr << "Failed to set endtime to now: " << e.what() << std::endl; + } + + // Alternative: set endtime using monotonic time (Unix only) + // This is useful if you already have a monotonic timestamp + #ifdef __unix__ + // timespec ts; + // clock_gettime(CLOCK_MONOTONIC, &ts); + // auto monotonic_ns = static_cast(ts.tv_sec) * 1'000'000'000LL + ts.tv_nsec; + // owned_sample->set_endtime_from_monotonic_ns(monotonic_ns); + #endif + + Mapping mapping{ + .memory_start = 0x10000000, + .memory_limit = 0x20000000, + .file_offset = 0, + .filename = "/usr/lib/libexample.so", + .build_id = "abc123" + }; + + // Add locations - OwnedSample copies strings into its arena + // No need for string storage since OwnedSample owns the data! + owned_sample->add_location(Location{ + .mapping = mapping, + .function = Function{ + .name = std::format("hot_function_{}", i % 3), + .system_name = std::format("_Z12hot_function{}v", i % 3), + .filename = "/src/hot_path.cpp" + }, + .address = uint64_t(0x10003000 + (i % 3) * 0x100), + .line = 100 + (i % 3) * 10 + }); + + owned_sample->add_location(Location{ + .mapping = mapping, + .function = Function{ + .name = std::format("process_request_{}", i % 5), + .system_name = std::format("_Z15process_request{}v", i % 5), + .filename = "/src/handler.cpp" + }, + .address = uint64_t(0x10002000 + (i % 5) * 0x80), + .line = 50 + (i % 5) * 5 + }); + + owned_sample->add_location(Location{ + .mapping = mapping, + .function = Function{ + .name = "main", + .system_name = "main", + .filename = "/src/main.cpp" + }, + .address = 0x10001000, + .line = 42 + }); + + // Add an extra location for some samples + if (i % 7 == 0) { + owned_sample->add_location(Location{ + .mapping = mapping, + .function = Function{ + .name = "worker_loop", + .system_name = "_Z11worker_loopv", + .filename = "/src/worker.cpp" + }, + .address = 0x10000500, + .line = 25 + }); + } + + // Demonstrate reverse_locations feature - reverse the stack trace for some samples + // In profiling, you might want leaf-first (normal) or root-first (reversed) order + if (i % 13 == 0) { + owned_sample->set_reverse_locations(true); + } + + // Add labels using convenience methods with well-known label keys + // This is simpler and more type-safe than using raw strings + owned_sample->add_num_label(LabelKey::ThreadId, i % 4); + owned_sample->add_string_label(LabelKey::ThreadName, i % 2 == 0 ? "worker-even" : "worker-odd"); + + // Can also add labels the traditional way + owned_sample->add_label(Label{ + .key = "sample_id", + .str = "", + .num = int64_t(i), + .num_unit = "" + }); + + // Track memory usage (print for first sample only) + if (i == 0) { + auto bytes = owned_sample->allocated_bytes(); + std::cout << " First sample allocated " << bytes << " bytes for strings" << std::endl; + } + + // Add OwnedSample directly to profile + owned_sample->add_to_profile(*profile); + + // Return sample to pool for reuse (automatically resets it) + pool->return_sample(std::move(owned_sample)); + } + + std::cout << "āœ… Added 100 samples using SamplePool" << std::endl; + std::cout << " Pool now contains " << pool->pool_len() << " reusable samples" << std::endl; + + std::cout << "Adding endpoint mappings..." << std::endl; + profile->add_endpoint(12345, "/api/users"); + profile->add_endpoint(67890, "/api/orders"); + profile->add_endpoint(11111, "/api/products"); + + profile->add_endpoint_count("/api/users", 150); + profile->add_endpoint_count("/api/orders", 75); + profile->add_endpoint_count("/api/products", 200); + std::cout << "āœ… Added endpoint mappings and counts" << std::endl; + + std::cout << "Serializing profile..." << std::endl; + auto serialized = profile->serialize_to_vec(); + std::cout << "āœ… Profile serialized to " << serialized.size() << " bytes" << std::endl; + + std::ofstream out("profile_owned_sample.pprof", std::ios::binary); + out.write(reinterpret_cast(serialized.data()), serialized.size()); + out.close(); + std::cout << "āœ… Profile written to profile_owned_sample.pprof" << std::endl; + + std::cout << "Resetting profile..." << std::endl; + profile->reset(); + std::cout << "āœ… Profile reset" << std::endl; + + std::cout << "\nāœ… Success! OwnedSample demonstrates efficient sample reuse with arena allocation." << std::endl; + return 0; + + } catch (const std::exception& e) { + std::cerr << "āŒ Exception: " << e.what() << std::endl; + return 1; + } +} + diff --git a/examples/cxx/profiling.cpp b/examples/cxx/profiling.cpp new file mode 100644 index 000000000..dc5471fac --- /dev/null +++ b/examples/cxx/profiling.cpp @@ -0,0 +1,197 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include "libdd-profiling/src/cxx.rs.h" + +using namespace datadog::profiling; + +int main() { + try { + std::cout << "Creating Profile using CXX bindings..." << std::endl; + + ValueType wall_time{ + .type_ = "wall-time", + .unit = "nanoseconds" + }; + + Period period{ + .value_type = wall_time, + .value = 60 + }; + + auto profile = Profile::create({wall_time}, period); + std::cout << "āœ… Profile created" << std::endl; + + std::cout << "Adding upscaling rules..." << std::endl; + + // Poisson upscaling for sampled data + std::vector value_offsets = {0}; + profile->add_upscaling_rule_poisson( + rust::Slice(value_offsets.data(), value_offsets.size()), + "thread_id", + "0", + 0, + 0, + 1000000 + ); + + // Proportional upscaling (scale by factor) + profile->add_upscaling_rule_proportional( + rust::Slice(value_offsets.data(), value_offsets.size()), + "thread_id", + "1", + 100.0 + ); + + std::cout << "āœ… Added upscaling rules" << std::endl; + + std::cout << "Adding samples..." << std::endl; + for (int i = 0; i < 100; i++) { + // String storage must outlive add_sample() call for the profile to intern them + std::vector string_storage; + string_storage.push_back(std::format("hot_function_{}", i % 3)); + string_storage.push_back(std::format("_Z12hot_function{}v", i % 3)); + string_storage.push_back(std::format("process_request_{}", i % 5)); + string_storage.push_back(std::format("_Z15process_request{}v", i % 5)); + + Mapping mapping{ + .memory_start = 0x10000000, + .memory_limit = 0x20000000, + .file_offset = 0, + .filename = "/usr/lib/libexample.so", + .build_id = "abc123" + }; + + auto wall_time_value = 1000000 + (i % 1000) * 1000; + + if (i % 7 == 0) { + profile->add_sample(Sample{ + .locations = { + Location{ + .mapping = mapping, + .function = Function{ + .name = string_storage[0], + .system_name = string_storage[1], + .filename = "/src/hot_path.cpp" + }, + .address = uint64_t(0x10003000 + (i % 3) * 0x100), + .line = 100 + (i % 3) * 10 + }, + Location{ + .mapping = mapping, + .function = Function{ + .name = string_storage[2], + .system_name = string_storage[3], + .filename = "/src/handler.cpp" + }, + .address = uint64_t(0x10002000 + (i % 5) * 0x80), + .line = 50 + (i % 5) * 5 + }, + Location{ + .mapping = mapping, + .function = Function{ + .name = "main", + .system_name = "main", + .filename = "/src/main.cpp" + }, + .address = 0x10001000, + .line = 42 + }, + Location{ + .mapping = mapping, + .function = Function{ + .name = "worker_loop", + .system_name = "_Z11worker_loopv", + .filename = "/src/worker.cpp" + }, + .address = 0x10000500, + .line = 25 + } + }, + .values = {wall_time_value}, + .labels = { + Label{.key = "thread_id", .str = "", .num = int64_t(i % 4), .num_unit = ""}, + Label{.key = "sample_id", .str = "", .num = int64_t(i), .num_unit = ""} + } + }); + } else { + profile->add_sample(Sample{ + .locations = { + Location{ + .mapping = mapping, + .function = Function{ + .name = string_storage[0], + .system_name = string_storage[1], + .filename = "/src/hot_path.cpp" + }, + .address = uint64_t(0x10003000 + (i % 3) * 0x100), + .line = 100 + (i % 3) * 10 + }, + Location{ + .mapping = mapping, + .function = Function{ + .name = string_storage[2], + .system_name = string_storage[3], + .filename = "/src/handler.cpp" + }, + .address = uint64_t(0x10002000 + (i % 5) * 0x80), + .line = 50 + (i % 5) * 5 + }, + Location{ + .mapping = mapping, + .function = Function{ + .name = "main", + .system_name = "main", + .filename = "/src/main.cpp" + }, + .address = 0x10001000, + .line = 42 + } + }, + .values = {wall_time_value}, + .labels = { + Label{.key = "thread_id", .str = "", .num = int64_t(i % 4), .num_unit = ""}, + Label{.key = "sample_id", .str = "", .num = int64_t(i), .num_unit = ""} + } + }); + } + } + + std::cout << "āœ… Added 100 samples" << std::endl; + + std::cout << "Adding endpoint mappings..." << std::endl; + profile->add_endpoint(12345, "/api/users"); + profile->add_endpoint(67890, "/api/orders"); + profile->add_endpoint(11111, "/api/products"); + + profile->add_endpoint_count("/api/users", 150); + profile->add_endpoint_count("/api/orders", 75); + profile->add_endpoint_count("/api/products", 200); + std::cout << "āœ… Added endpoint mappings and counts" << std::endl; + + std::cout << "Serializing profile..." << std::endl; + auto serialized = profile->serialize_to_vec(); + std::cout << "āœ… Profile serialized to " << serialized.size() << " bytes" << std::endl; + + std::ofstream out("profile.pprof", std::ios::binary); + out.write(reinterpret_cast(serialized.data()), serialized.size()); + out.close(); + std::cout << "āœ… Profile written to profile.pprof" << std::endl; + + std::cout << "Resetting profile..." << std::endl; + profile->reset(); + std::cout << "āœ… Profile reset" << std::endl; + + std::cout << "\nāœ… Success!" << std::endl; + return 0; + + } catch (const std::exception& e) { + std::cerr << "āŒ Exception: " << e.what() << std::endl; + return 1; + } +} diff --git a/libdd-profiling/Cargo.toml b/libdd-profiling/Cargo.toml index bc8eb7092..18f09f847 100644 --- a/libdd-profiling/Cargo.toml +++ b/libdd-profiling/Cargo.toml @@ -13,9 +13,13 @@ license.workspace = true autobenches = false [lib] -crate-type = ["lib"] +crate-type = ["lib", "staticlib"] bench = false +[features] +default = [] +cxx = ["dep:cxx", "dep:cxx-build"] + [[bench]] name = "main" harness = false @@ -24,9 +28,13 @@ harness = false allocator-api2 = { version = "0.2", default-features = false, features = ["alloc"] } anyhow = "1.0" bitmaps = "3.2.0" +bumpalo = { version = "3.16", features = ["collections"] } +enum-map = "2.7" +ouroboros = "0.18" byteorder = { version = "1.5", features = ["std"] } bytes = "1.1" chrono = {version = "0.4", default-features = false, features = ["std", "clock"]} +crossbeam-queue = "0.3.11" crossbeam-utils = { version = "0.8.21" } libdd-alloc = { version = "1.0.0", path = "../libdd-alloc" } libdd-profiling-protobuf = { version = "1.0.0", path = "../libdd-profiling-protobuf", features = ["prost_impls"] } @@ -50,9 +58,16 @@ thiserror = "2" tokio = {version = "1.23", features = ["rt", "macros"]} tokio-util = "0.7.1" zstd = { version = "0.13", default-features = false } +cxx = { version = "1.0", optional = true } + +[target.'cfg(unix)'.dependencies] +nix = { version = "0.29", features = ["time"], default-features = false } [dev-dependencies] bolero = "0.13" criterion = "0.5.1" lz4_flex = { version = "0.9", default-features = false, features = ["std", "frame"] } proptest = "1" + +[build-dependencies] +cxx-build = { version = "1.0", optional = true } diff --git a/libdd-profiling/build.rs b/libdd-profiling/build.rs new file mode 100644 index 000000000..b32dff0f9 --- /dev/null +++ b/libdd-profiling/build.rs @@ -0,0 +1,14 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +fn main() { + // Build CXX bridge if feature is enabled + #[cfg(feature = "cxx")] + { + cxx_build::bridge("src/cxx.rs") + .flag_if_supported("-std=c++20") + .compile("libdd-profiling-cxx"); + + println!("cargo:rerun-if-changed=src/cxx.rs"); + } +} diff --git a/libdd-profiling/src/cxx.rs b/libdd-profiling/src/cxx.rs new file mode 100644 index 000000000..0f0fbb35a --- /dev/null +++ b/libdd-profiling/src/cxx.rs @@ -0,0 +1,594 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! CXX bindings for profiling module - provides a safe and idiomatic C++ API + +#![allow(clippy::needless_lifetimes)] + +use crate::api; +use crate::internal; + +// ============================================================================ +// CXX Bridge - C++ Bindings +// ============================================================================ + +#[cxx::bridge(namespace = "datadog::profiling")] +pub mod ffi { + // Shared structs - CXX-friendly types + struct ValueType<'a> { + type_: &'a str, + unit: &'a str, + } + + struct Period<'a> { + value_type: ValueType<'a>, + value: i64, + } + + struct Mapping<'a> { + memory_start: u64, + memory_limit: u64, + file_offset: u64, + filename: &'a str, + build_id: &'a str, + } + + struct Function<'a> { + name: &'a str, + system_name: &'a str, + filename: &'a str, + } + + struct Location<'a> { + mapping: Mapping<'a>, + function: Function<'a>, + address: u64, + line: i64, + } + + struct Label<'a> { + key: &'a str, + str: &'a str, + num: i64, + num_unit: &'a str, + } + + struct Sample<'a> { + locations: Vec>, + values: Vec, + labels: Vec>, + } + + // Enums + #[derive(Debug)] + #[repr(u32)] + enum SampleType { + CpuTime = 0, + CpuCount = 1, + WallTime = 2, + WallCount = 3, + ExceptionCount = 4, + LockAcquireTime = 5, + LockAcquireCount = 6, + LockReleaseTime = 7, + LockReleaseCount = 8, + AllocSpace = 9, + AllocCount = 10, + HeapSpace = 11, + GpuTime = 12, + GpuCount = 13, + GpuAllocSpace = 14, + GpuAllocCount = 15, + GpuFlops = 16, + GpuFlopsSamples = 17, + } + + #[derive(Debug)] + enum LabelKey { + ExceptionType, + ThreadId, + ThreadNativeId, + ThreadName, + TaskId, + TaskName, + SpanId, + LocalRootSpanId, + TraceType, + ClassName, + LockName, + GpuDeviceName, + } + + // Opaque Rust types + extern "Rust" { + type Profile; + type Metadata; + type OwnedSample; + type SamplePool; + + // Helper function to get the sentinel value for no arena allocation limit + fn no_allocation_limit() -> i64; + + // Metadata static factory + // arena_allocation_limit: maximum bytes for arena allocator, or use no_allocation_limit() + #[Self = "Metadata"] + fn create(sample_types: Vec, max_frames: usize, arena_allocation_limit: i64, timeline_enabled: bool) -> Result>; + + // Profile static factory + #[Self = "Profile"] + fn create(sample_types: Vec, period: &Period) -> Result>; + + // Profile methods + fn add_sample(self: &mut Profile, sample: &Sample) -> Result<()>; + fn add_endpoint(self: &mut Profile, local_root_span_id: u64, endpoint: &str) -> Result<()>; + fn add_endpoint_count(self: &mut Profile, endpoint: &str, value: i64) -> Result<()>; + + // Upscaling rule methods + fn add_upscaling_rule_poisson( + self: &mut Profile, + offset_values: &[usize], + label_name: &str, + label_value: &str, + sum_value_offset: usize, + count_value_offset: usize, + sampling_distance: u64, + ) -> Result<()>; + + fn add_upscaling_rule_poisson_non_sample_type_count( + self: &mut Profile, + offset_values: &[usize], + label_name: &str, + label_value: &str, + sum_value_offset: usize, + count_value: u64, + sampling_distance: u64, + ) -> Result<()>; + + fn add_upscaling_rule_proportional( + self: &mut Profile, + offset_values: &[usize], + label_name: &str, + label_value: &str, + scale: f64, + ) -> Result<()>; + + fn reset(self: &mut Profile) -> Result<()>; + fn serialize_to_vec(self: &mut Profile) -> Result>; + + // OwnedSample methods + #[Self = "OwnedSample"] + fn create(metadata: &Metadata) -> Result>; + + fn set_value(self: &mut OwnedSample, sample_type: SampleType, value: i64) -> Result<()>; + fn get_value(self: &OwnedSample, sample_type: SampleType) -> Result; + + fn is_reverse_locations(self: &OwnedSample) -> bool; + fn set_reverse_locations(self: &mut OwnedSample, reverse: bool); + + fn set_endtime_ns(self: &mut OwnedSample, endtime_ns: i64) -> i64; + fn set_endtime_ns_now(self: &mut OwnedSample) -> Result; + fn endtime_ns(self: &OwnedSample) -> i64; + + #[cfg(unix)] + fn set_endtime_from_monotonic_ns(self: &mut OwnedSample, monotonic_ns: i64) -> Result; + + fn add_location(self: &mut OwnedSample, location: &Location); + fn add_label(self: &mut OwnedSample, label: &Label) -> Result<()>; + fn add_string_label(self: &mut OwnedSample, key: LabelKey, value: &str) -> Result<()>; + fn add_num_label(self: &mut OwnedSample, key: LabelKey, value: i64) -> Result<()>; + fn allocated_bytes(self: &OwnedSample) -> usize; + fn reset_sample(self: &mut OwnedSample); + fn add_to_profile(self: &OwnedSample, profile: &mut Profile) -> Result<()>; + + // SamplePool methods + #[Self = "SamplePool"] + fn create(metadata: &Metadata, capacity: usize) -> Result>; + + fn get_sample(self: &mut SamplePool) -> Box; + fn return_sample(self: &mut SamplePool, sample: Box); + fn pool_len(self: &SamplePool) -> usize; + fn pool_capacity(self: &SamplePool) -> usize; + } +} + +// ============================================================================ +// From Implementations - Convert CXX types to API types +// ============================================================================ + +impl<'a> From<&ffi::ValueType<'a>> for api::ValueType<'a> { + fn from(vt: &ffi::ValueType<'a>) -> Self { + api::ValueType::new(vt.type_, vt.unit) + } +} + +impl<'a> From<&ffi::Period<'a>> for api::Period<'a> { + fn from(period: &ffi::Period<'a>) -> Self { + api::Period { + r#type: (&period.value_type).into(), + value: period.value, + } + } +} + +impl<'a> From<&ffi::Mapping<'a>> for api::Mapping<'a> { + fn from(mapping: &ffi::Mapping<'a>) -> Self { + api::Mapping { + memory_start: mapping.memory_start, + memory_limit: mapping.memory_limit, + file_offset: mapping.file_offset, + filename: mapping.filename, + build_id: mapping.build_id, + } + } +} + +impl<'a> From<&ffi::Function<'a>> for api::Function<'a> { + fn from(func: &ffi::Function<'a>) -> Self { + api::Function { + name: func.name, + system_name: func.system_name, + filename: func.filename, + } + } +} + +impl<'a> From<&ffi::Location<'a>> for api::Location<'a> { + fn from(loc: &ffi::Location<'a>) -> Self { + api::Location { + mapping: (&loc.mapping).into(), + function: (&loc.function).into(), + address: loc.address, + line: loc.line, + } + } +} + +impl<'a> From<&ffi::Label<'a>> for api::Label<'a> { + fn from(label: &ffi::Label<'a>) -> Self { + api::Label { + key: label.key, + str: label.str, + num: label.num, + num_unit: label.num_unit, + } + } +} + +// ============================================================================ +// Profile - Wrapper around internal::Profile +// ============================================================================ + +pub struct Profile { + inner: internal::Profile, +} + +impl Profile { + pub fn create( + sample_types: Vec, + period: &ffi::Period, + ) -> anyhow::Result> { + // Convert using From trait + let types: Vec = sample_types.iter().map(Into::into).collect(); + let period_value: api::Period = period.into(); + + // Profile::try_new interns the strings + let inner = internal::Profile::try_new(&types, Some(period_value))?; + + Ok(Box::new(Profile { inner })) + } + + pub fn add_sample(&mut self, sample: &ffi::Sample) -> anyhow::Result<()> { + let api_sample = api::Sample { + locations: sample.locations.iter().map(Into::into).collect(), + values: &sample.values, + labels: sample.labels.iter().map(Into::into).collect(), + }; + + // Profile interns the strings + self.inner.try_add_sample(api_sample, None)?; + Ok(()) + } + + + pub fn add_endpoint(&mut self, local_root_span_id: u64, endpoint: &str) -> anyhow::Result<()> { + self.inner + .add_endpoint(local_root_span_id, std::borrow::Cow::Borrowed(endpoint)) + } + + pub fn add_endpoint_count(&mut self, endpoint: &str, value: i64) -> anyhow::Result<()> { + self.inner + .add_endpoint_count(std::borrow::Cow::Borrowed(endpoint), value) + } + + pub fn add_upscaling_rule_poisson( + &mut self, + offset_values: &[usize], + label_name: &str, + label_value: &str, + sum_value_offset: usize, + count_value_offset: usize, + sampling_distance: u64, + ) -> anyhow::Result<()> { + let upscaling_info = api::UpscalingInfo::Poisson { + sum_value_offset, + count_value_offset, + sampling_distance, + }; + self.inner + .add_upscaling_rule(offset_values, label_name, label_value, upscaling_info) + } + + pub fn add_upscaling_rule_poisson_non_sample_type_count( + &mut self, + offset_values: &[usize], + label_name: &str, + label_value: &str, + sum_value_offset: usize, + count_value: u64, + sampling_distance: u64, + ) -> anyhow::Result<()> { + let upscaling_info = api::UpscalingInfo::PoissonNonSampleTypeCount { + sum_value_offset, + count_value, + sampling_distance, + }; + self.inner + .add_upscaling_rule(offset_values, label_name, label_value, upscaling_info) + } + + pub fn add_upscaling_rule_proportional( + &mut self, + offset_values: &[usize], + label_name: &str, + label_value: &str, + scale: f64, + ) -> anyhow::Result<()> { + let upscaling_info = api::UpscalingInfo::Proportional { scale }; + self.inner + .add_upscaling_rule(offset_values, label_name, label_value, upscaling_info) + } + + pub fn reset(&mut self) -> anyhow::Result<()> { + // Reset and discard the old profile + self.inner.reset_and_return_previous()?; + Ok(()) + } + + pub fn serialize_to_vec(&mut self) -> anyhow::Result> { + // Reset the profile and get the old one to serialize + let old_profile = self.inner.reset_and_return_previous()?; + let end_time = Some(std::time::SystemTime::now()); + let encoded = old_profile.serialize_into_compressed_pprof(end_time, None)?; + Ok(encoded.buffer) + } +} + +// ============================================================================ +// OwnedSample - Wrapper around owned_sample::OwnedSample +// ============================================================================ + +use crate::owned_sample; +use std::sync::Arc; + +/// Returns the sentinel value indicating no arena allocation limit. +/// Use this for the arena_allocation_limit parameter in Metadata::create(). +pub fn no_allocation_limit() -> i64 { + -1 +} + +pub struct Metadata { + inner: Arc, +} + +impl Metadata { + pub fn create(sample_types: Vec, max_frames: usize, arena_allocation_limit: i64, timeline_enabled: bool) -> anyhow::Result> { + // Convert CXX SampleType to owned_sample::SampleType + let types: Vec = sample_types + .into_iter() + .map(ffi_sample_type_to_owned) + .collect::>>()?; + + // Convert arena_allocation_limit (use NO_ALLOCATION_LIMIT or any value <= 0 for no limit) + let arena_allocation_limit = if arena_allocation_limit > 0 { + Some(arena_allocation_limit as usize) + } else { + None + }; + + // Create metadata with specified configuration + let inner = Arc::new(owned_sample::Metadata::new(types, max_frames, arena_allocation_limit, timeline_enabled)?); + Ok(Box::new(Metadata { inner })) + } +} + +pub struct OwnedSample { + inner: owned_sample::OwnedSample, +} + +impl OwnedSample { + pub fn create(metadata: &Metadata) -> anyhow::Result> { + // Use the provided metadata (clone the Arc for shared ownership) + let inner = owned_sample::OwnedSample::new(Arc::clone(&metadata.inner)); + Ok(Box::new(OwnedSample { inner })) + } + + pub fn set_value(&mut self, sample_type: ffi::SampleType, value: i64) -> anyhow::Result<()> { + let st = ffi_sample_type_to_owned(sample_type)?; + self.inner.set_value(st, value) + } + + pub fn get_value(&self, sample_type: ffi::SampleType) -> anyhow::Result { + let st = ffi_sample_type_to_owned(sample_type)?; + self.inner.get_value(st) + } + + pub fn is_reverse_locations(&self) -> bool { + self.inner.is_reverse_locations() + } + + pub fn set_reverse_locations(&mut self, reverse: bool) { + self.inner.set_reverse_locations(reverse); + } + + pub fn set_endtime_ns(&mut self, endtime_ns: i64) -> i64 { + self.inner.set_endtime_ns(endtime_ns) + } + + pub fn set_endtime_ns_now(&mut self) -> anyhow::Result { + self.inner.set_endtime_ns_now() + } + + pub fn endtime_ns(&self) -> i64 { + self.inner.endtime_ns() + .map(|nz| nz.get()) + .unwrap_or(0) + } + + #[cfg(unix)] + pub fn set_endtime_from_monotonic_ns(&mut self, monotonic_ns: i64) -> anyhow::Result { + self.inner.set_endtime_from_monotonic_ns(monotonic_ns) + } + + pub fn add_location(&mut self, location: &ffi::Location) { + let api_location: api::Location = location.into(); + self.inner.add_location(api_location); + } + + pub fn add_label(&mut self, label: &ffi::Label) -> anyhow::Result<()> { + let api_label: api::Label = label.into(); + self.inner.add_label(api_label)?; + Ok(()) + } + + pub fn add_string_label(&mut self, key: ffi::LabelKey, value: &str) -> anyhow::Result<()> { + let rust_key = ffi_label_key_to_owned(key); + self.inner.add_string_label(rust_key, value)?; + Ok(()) + } + + pub fn add_num_label(&mut self, key: ffi::LabelKey, value: i64) -> anyhow::Result<()> { + let rust_key = ffi_label_key_to_owned(key); + self.inner.add_num_label(rust_key, value)?; + Ok(()) + } + + + pub fn allocated_bytes(&self) -> usize { + self.inner.allocated_bytes() + } + + pub fn reset_sample(&mut self) { + self.inner.reset(); + } + + pub fn add_to_profile(&self, profile: &mut Profile) -> anyhow::Result<()> { + self.inner.add_to_profile(&mut profile.inner) + } +} + +// ============================================================================ +// SamplePool - Wrapper around owned_sample::SamplePool +// ============================================================================ + +pub struct SamplePool { + inner: owned_sample::SamplePool, +} + +impl SamplePool { + pub fn create(metadata: &Metadata, capacity: usize) -> anyhow::Result> { + // Use the provided metadata (clone the Arc for shared ownership) + let inner = owned_sample::SamplePool::new(Arc::clone(&metadata.inner), capacity); + Ok(Box::new(SamplePool { inner })) + } + + pub fn get_sample(&mut self) -> Box { + let inner = self.inner.get(); + Box::new(OwnedSample { inner: *inner }) + } + + #[allow(clippy::boxed_local)] + pub fn return_sample(&mut self, sample: Box) { + self.inner.put(Box::new(sample.inner)); + } + + pub fn pool_len(&self) -> usize { + self.inner.len() + } + + pub fn pool_capacity(&self) -> usize { + self.inner.capacity() + } +} + +// Note: We must redeclare SampleType in the CXX bridge because CXX doesn't support +// using external Rust enums. This conversion function maps between the two. +fn ffi_sample_type_to_owned(st: ffi::SampleType) -> anyhow::Result { + match st { + ffi::SampleType::CpuTime => Ok(owned_sample::SampleType::CpuTime), + ffi::SampleType::CpuCount => Ok(owned_sample::SampleType::CpuCount), + ffi::SampleType::WallTime => Ok(owned_sample::SampleType::WallTime), + ffi::SampleType::WallCount => Ok(owned_sample::SampleType::WallCount), + ffi::SampleType::ExceptionCount => Ok(owned_sample::SampleType::ExceptionCount), + ffi::SampleType::LockAcquireTime => Ok(owned_sample::SampleType::LockAcquireTime), + ffi::SampleType::LockAcquireCount => Ok(owned_sample::SampleType::LockAcquireCount), + ffi::SampleType::LockReleaseTime => Ok(owned_sample::SampleType::LockReleaseTime), + ffi::SampleType::LockReleaseCount => Ok(owned_sample::SampleType::LockReleaseCount), + ffi::SampleType::AllocSpace => Ok(owned_sample::SampleType::AllocSpace), + ffi::SampleType::AllocCount => Ok(owned_sample::SampleType::AllocCount), + ffi::SampleType::HeapSpace => Ok(owned_sample::SampleType::HeapSpace), + ffi::SampleType::GpuTime => Ok(owned_sample::SampleType::GpuTime), + ffi::SampleType::GpuCount => Ok(owned_sample::SampleType::GpuCount), + ffi::SampleType::GpuAllocSpace => Ok(owned_sample::SampleType::GpuAllocSpace), + ffi::SampleType::GpuAllocCount => Ok(owned_sample::SampleType::GpuAllocCount), + ffi::SampleType::GpuFlops => Ok(owned_sample::SampleType::GpuFlops), + ffi::SampleType::GpuFlopsSamples => Ok(owned_sample::SampleType::GpuFlopsSamples), + _ => anyhow::bail!("Unknown SampleType variant: {:?}", st), + } +} + +fn ffi_label_key_to_owned(key: ffi::LabelKey) -> owned_sample::LabelKey { + match key { + ffi::LabelKey::ExceptionType => owned_sample::LabelKey::ExceptionType, + ffi::LabelKey::ThreadId => owned_sample::LabelKey::ThreadId, + ffi::LabelKey::ThreadNativeId => owned_sample::LabelKey::ThreadNativeId, + ffi::LabelKey::ThreadName => owned_sample::LabelKey::ThreadName, + ffi::LabelKey::TaskId => owned_sample::LabelKey::TaskId, + ffi::LabelKey::TaskName => owned_sample::LabelKey::TaskName, + ffi::LabelKey::SpanId => owned_sample::LabelKey::SpanId, + ffi::LabelKey::LocalRootSpanId => owned_sample::LabelKey::LocalRootSpanId, + ffi::LabelKey::TraceType => owned_sample::LabelKey::TraceType, + ffi::LabelKey::ClassName => owned_sample::LabelKey::ClassName, + ffi::LabelKey::LockName => owned_sample::LabelKey::LockName, + ffi::LabelKey::GpuDeviceName => owned_sample::LabelKey::GpuDeviceName, + _ => owned_sample::LabelKey::ThreadId, // Default case, should not happen + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sample_type_enum_sync() { + // Ensure ffi::SampleType and owned_sample::SampleType stay in sync + // This will fail to compile if variants don't match + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::CpuTime).unwrap() as usize, owned_sample::SampleType::CpuTime as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::CpuCount).unwrap() as usize, owned_sample::SampleType::CpuCount as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::WallTime).unwrap() as usize, owned_sample::SampleType::WallTime as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::WallCount).unwrap() as usize, owned_sample::SampleType::WallCount as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::ExceptionCount).unwrap() as usize, owned_sample::SampleType::ExceptionCount as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::LockAcquireTime).unwrap() as usize, owned_sample::SampleType::LockAcquireTime as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::LockAcquireCount).unwrap() as usize, owned_sample::SampleType::LockAcquireCount as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::LockReleaseTime).unwrap() as usize, owned_sample::SampleType::LockReleaseTime as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::LockReleaseCount).unwrap() as usize, owned_sample::SampleType::LockReleaseCount as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::AllocSpace).unwrap() as usize, owned_sample::SampleType::AllocSpace as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::AllocCount).unwrap() as usize, owned_sample::SampleType::AllocCount as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::HeapSpace).unwrap() as usize, owned_sample::SampleType::HeapSpace as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::GpuTime).unwrap() as usize, owned_sample::SampleType::GpuTime as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::GpuCount).unwrap() as usize, owned_sample::SampleType::GpuCount as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::GpuAllocSpace).unwrap() as usize, owned_sample::SampleType::GpuAllocSpace as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::GpuAllocCount).unwrap() as usize, owned_sample::SampleType::GpuAllocCount as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::GpuFlops).unwrap() as usize, owned_sample::SampleType::GpuFlops as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::GpuFlopsSamples).unwrap() as usize, owned_sample::SampleType::GpuFlopsSamples as usize); + } +} diff --git a/libdd-profiling/src/lib.rs b/libdd-profiling/src/lib.rs index 486104e79..8c0cffc2c 100644 --- a/libdd-profiling/src/lib.rs +++ b/libdd-profiling/src/lib.rs @@ -8,8 +8,11 @@ pub mod api; pub mod collections; +#[cfg(feature = "cxx")] +pub mod cxx; pub mod exporter; pub mod internal; pub mod iter; +pub mod owned_sample; pub mod pprof; pub mod profiles; diff --git a/libdd-profiling/src/owned_sample/label_key.rs b/libdd-profiling/src/owned_sample/label_key.rs new file mode 100644 index 000000000..1c5b404b1 --- /dev/null +++ b/libdd-profiling/src/owned_sample/label_key.rs @@ -0,0 +1,63 @@ +// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +/// Well-known label keys used in profiling. +/// +/// These correspond to standard labels that profilers commonly attach to samples, +/// such as thread information, exception details, and tracing context. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum LabelKey { + ExceptionType, + ThreadId, + ThreadNativeId, + ThreadName, + TaskId, + TaskName, + SpanId, + LocalRootSpanId, + TraceType, + ClassName, + LockName, + GpuDeviceName, +} + +impl LabelKey { + /// Returns the string representation of this label key. + /// + /// # Example + /// ``` + /// # use libdd_profiling::owned_sample::LabelKey; + /// assert_eq!(LabelKey::ThreadId.as_str(), "thread id"); + /// assert_eq!(LabelKey::ExceptionType.as_str(), "exception type"); + /// ``` + pub const fn as_str(self) -> &'static str { + match self { + LabelKey::ExceptionType => "exception type", + LabelKey::ThreadId => "thread id", + LabelKey::ThreadNativeId => "thread native id", + LabelKey::ThreadName => "thread name", + LabelKey::TaskId => "task id", + LabelKey::TaskName => "task name", + LabelKey::SpanId => "span id", + LabelKey::LocalRootSpanId => "local root span id", + LabelKey::TraceType => "trace type", + LabelKey::ClassName => "class name", + LabelKey::LockName => "lock name", + LabelKey::GpuDeviceName => "gpu device name", + } + } +} + +impl AsRef for LabelKey { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl std::fmt::Display for LabelKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + diff --git a/libdd-profiling/src/owned_sample/metadata.rs b/libdd-profiling/src/owned_sample/metadata.rs new file mode 100644 index 000000000..98c008471 --- /dev/null +++ b/libdd-profiling/src/owned_sample/metadata.rs @@ -0,0 +1,175 @@ +// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Metadata for profiling samples, including sample type configuration. + +use super::SampleType; +use anyhow::Context; +use enum_map::EnumMap; + +/// Maps sample types to their indices in a values array. +/// +/// Each sample has a values array, and this struct tracks which index corresponds to +/// which sample type. This allows efficient O(1) indexing into the values array using +/// an `EnumMap` for lookups. +/// +/// # Example +/// ```no_run +/// # use libdd_profiling::owned_sample::{Metadata, SampleType}; +/// let metadata = Metadata::new(vec![ +/// SampleType::CpuTime, +/// SampleType::WallTime, +/// SampleType::AllocSpace, +/// ], 256, None, true).unwrap(); +/// +/// assert_eq!(metadata.get_index(&SampleType::CpuTime), Some(0)); +/// assert_eq!(metadata.get_index(&SampleType::WallTime), Some(1)); +/// assert_eq!(metadata.get_index(&SampleType::AllocSpace), Some(2)); +/// assert_eq!(metadata.get_index(&SampleType::HeapSpace), None); +/// assert_eq!(metadata.len(), 3); +/// assert_eq!(metadata.max_frames(), 256); +/// ``` +#[derive(Clone, Debug)] +pub struct Metadata { + /// Ordered list of sample types + sample_types: Vec, + /// O(1) lookup map: sample type -> values array index + /// None means the sample type is not configured + type_to_index: EnumMap>, + /// Maximum number of stack frames to collect per sample + max_frames: usize, + /// Optional allocation limit in bytes for the arena allocator + arena_allocation_limit: Option, + /// Whether timeline is enabled for samples using this metadata. + /// When disabled, time-setting methods become no-ops. + timeline_enabled: bool, + /// Offset between monotonic time and epoch time (Unix only). + /// Allows converting CLOCK_MONOTONIC timestamps to epoch timestamps. + #[cfg(unix)] + monotonic_to_epoch_offset: i64, +} + +impl Metadata { + /// Creates a new Metadata with the given sample types, max frames, and timeline setting. + /// + /// The order of sample types in the vector determines their index in the values array. + /// + /// On Unix platforms, this also computes and caches the offset between monotonic time + /// (CLOCK_MONOTONIC) and epoch time for efficient timestamp conversion. + /// + /// # Arguments + /// + /// * `sample_types` - The sample types to configure + /// * `max_frames` - Maximum number of stack frames to collect per sample + /// * `arena_allocation_limit` - Optional allocation limit in bytes for the arena allocator + /// * `timeline_enabled` - Whether timeline should be enabled for samples using this metadata + /// + /// # Errors + /// + /// Returns an error if: + /// - The sample types vector is empty + /// - The same sample type appears more than once + /// - (Unix only) System time is before UNIX_EPOCH + /// - (Unix only) `clock_gettime(CLOCK_MONOTONIC)` fails + pub fn new(sample_types: Vec, max_frames: usize, arena_allocation_limit: Option, timeline_enabled: bool) -> anyhow::Result { + anyhow::ensure!(!sample_types.is_empty(), "sample types cannot be empty"); + + let mut type_to_index: EnumMap> = EnumMap::default(); + + for (index, &sample_type) in sample_types.iter().enumerate() { + anyhow::ensure!( + type_to_index[sample_type].is_none(), + "duplicate sample type: {:?}", + sample_type + ); + + type_to_index[sample_type] = Some(index); + } + + // Compute monotonic to epoch offset (Unix only) + #[cfg(unix)] + let monotonic_to_epoch_offset = { + use std::time::SystemTime; + + // Get the current epoch time in nanoseconds + let epoch_ns = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .context("system time is before UNIX_EPOCH")? + .as_nanos() as i64; + + // Get the current monotonic time using clock_gettime (safe wrapper from nix crate) + let ts = nix::time::clock_gettime(nix::time::ClockId::CLOCK_MONOTONIC) + .context("failed to get monotonic time from CLOCK_MONOTONIC")?; + + let monotonic_ns = ts.tv_sec() * 1_000_000_000 + ts.tv_nsec(); + + // Compute the offset (epoch_ns will be larger since we're after 1970) + epoch_ns - monotonic_ns + }; + + Ok(Self { + sample_types, + type_to_index, + max_frames, + arena_allocation_limit, + timeline_enabled, + #[cfg(unix)] + monotonic_to_epoch_offset, + }) + } + + /// Returns the index for the given sample type, or None if not configured. + pub fn get_index(&self, sample_type: &SampleType) -> Option { + self.type_to_index[*sample_type] + } + + /// Returns the sample type at the given index, or None if out of bounds. + pub fn get_type(&self, index: usize) -> Option { + self.sample_types.get(index).copied() + } + + /// Returns the number of configured sample types. + pub fn len(&self) -> usize { + self.sample_types.len() + } + + /// Returns true if no sample types are configured. + pub fn is_empty(&self) -> bool { + self.sample_types.is_empty() + } + + /// Returns an iterator over the sample types in order. + pub fn iter(&self) -> impl Iterator { + self.sample_types.iter() + } + + /// Returns a slice of all configured sample types in order. + pub fn types(&self) -> &[SampleType] { + &self.sample_types + } + + /// Returns the maximum number of stack frames to collect per sample. + pub fn max_frames(&self) -> usize { + self.max_frames + } + + /// Returns the optional arena allocation limit in bytes. + pub fn arena_allocation_limit(&self) -> Option { + self.arena_allocation_limit + } + + /// Returns whether timeline is enabled for samples using this metadata. + pub fn is_timeline_enabled(&self) -> bool { + self.timeline_enabled + } + + /// Returns the offset between monotonic time and epoch time (Unix only). + /// + /// This offset is computed once during construction and allows converting + /// CLOCK_MONOTONIC timestamps to epoch timestamps. + #[cfg(unix)] + pub fn monotonic_to_epoch_offset(&self) -> i64 { + self.monotonic_to_epoch_offset + } +} + diff --git a/libdd-profiling/src/owned_sample/mod.rs b/libdd-profiling/src/owned_sample/mod.rs new file mode 100644 index 000000000..9f61d6ee4 --- /dev/null +++ b/libdd-profiling/src/owned_sample/mod.rs @@ -0,0 +1,596 @@ +// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Owned versions of profiling types that can be stored without lifetime constraints. +//! +//! These types use bumpalo arena allocation for strings - all strings within a sample are stored +//! in a bump allocator arena, and locations/labels reference them via the arena's lifetime. +//! +//! # Example +//! +//! ```no_run +//! use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType}; +//! use libdd_profiling::api::{Location, Mapping, Function, Label}; +//! use std::sync::Arc; +//! +//! let metadata = Arc::new(Metadata::new(vec![ +//! SampleType::CpuTime, +//! SampleType::WallTime, +//! ], 64, None, true).unwrap()); +//! +//! let mut sample = OwnedSample::new(metadata); +//! +//! // Set values by type +//! sample.set_value(SampleType::CpuTime, 1000).unwrap(); +//! sample.set_value(SampleType::WallTime, 2000).unwrap(); +//! +//! // Add a location +//! sample.add_location(Location { +//! mapping: Mapping { +//! memory_start: 0x1000, +//! memory_limit: 0x2000, +//! file_offset: 0, +//! filename: "libfoo.so", +//! build_id: "abc123", +//! }, +//! function: Function { +//! name: "my_function", +//! system_name: "_Z11my_functionv", +//! filename: "foo.cpp", +//! }, +//! address: 0x1234, +//! line: 42, +//! }); +//! +//! // Add labels +//! sample.add_label(Label { key: "thread_name", str: "worker-1", num: 0, num_unit: "" }).unwrap(); +//! sample.add_label(Label { key: "thread_id", str: "", num: 123, num_unit: "" }).unwrap(); +//! ``` + +use bumpalo::Bump; +use std::num::NonZeroI64; +use std::sync::Arc; +use anyhow::{self, Context}; +use crate::api::{Function, Label, Location, Mapping, Sample}; + +mod label_key; +mod metadata; +mod pool; +mod sample_type; + +#[cfg(test)] +mod tests; + +pub use label_key::LabelKey; +pub use metadata::Metadata; +pub use sample_type::SampleType; +pub use pool::SamplePool; + +/// Wrapper around bumpalo::AllocErr that implements std::error::Error +#[derive(Debug)] +pub struct AllocError(bumpalo::AllocErr); + +impl std::fmt::Display for AllocError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "arena allocation failed") + } +} + +impl std::error::Error for AllocError {} + +impl From for AllocError { + fn from(err: bumpalo::AllocErr) -> Self { + AllocError(err) + } +} + +/// Errors that can occur during owned sample operations. +#[derive(Debug, thiserror::Error)] +pub enum OwnedSampleError { + /// Arena allocation failed (out of memory) + #[error(transparent)] + AllocationFailed(#[from] AllocError), + + /// Invalid sample type index + #[error("invalid sample type index: {0}")] + InvalidIndex(usize), +} + +impl From for OwnedSampleError { + fn from(err: bumpalo::AllocErr) -> Self { + OwnedSampleError::AllocationFailed(AllocError(err)) + } +} + +/// Internal data structure that holds the arena and references into it. +/// This is a self-referential structure created using the ouroboros crate. +#[ouroboros::self_referencing] +#[derive(Debug)] +struct SampleInner { + /// Bump arena where all strings are allocated + arena: Bump, + + /// Locations with string references into the arena + #[borrows(arena)] + #[covariant] + locations: Vec>, + + /// Labels with string references into the arena + #[borrows(arena)] + #[covariant] + labels: Vec>, +} + +/// An owned sample with arena-allocated strings. +/// +/// All strings (in mappings, functions, labels) are stored in an internal bumpalo arena, +/// providing efficient memory usage and cache locality. The sample can be passed around +/// freely without lifetime constraints. +#[derive(Debug)] +pub struct OwnedSample { + inner: SampleInner, + values: Vec, + metadata: Arc, + endtime_ns: Option, + reverse_locations: bool, + dropped_frames: usize, +} + +impl OwnedSample { + /// Creates a new empty sample with the given sample type indices. + /// + /// The values vector will be initialized with zeros, one for each sample type + /// configured in the indices. + /// + /// # Example + /// ```no_run + /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType}; + /// # use std::sync::Arc; + /// let metadata = Arc::new(Metadata::new(vec![ + /// SampleType::CpuTime, + /// SampleType::WallTime, + /// ], 64, None, true).unwrap()); + /// let sample = OwnedSample::new(metadata); + /// ``` + pub fn new(metadata: Arc) -> Self { + let num_values = metadata.len(); + let arena = Bump::new(); + arena.set_allocation_limit(metadata.arena_allocation_limit()); + Self { + inner: SampleInnerBuilder { + arena, + locations_builder: |_| Vec::new(), + labels_builder: |_| Vec::new(), + }.build(), + values: vec![0; num_values], + metadata, + endtime_ns: None, + reverse_locations: false, + dropped_frames: 0, + } + } + + /// Sets the value for the given sample type. + /// + /// # Errors + /// + /// Returns an error if the sample type is not configured. + /// + /// # Example + /// ```no_run + /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType}; + /// # use std::sync::Arc; + /// # let indices = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + /// let mut sample = OwnedSample::new(indices); + /// sample.set_value(SampleType::CpuTime, 1000).unwrap(); + /// ``` + pub fn set_value(&mut self, sample_type: SampleType, value: i64) -> anyhow::Result<()> { + let index = self.metadata.get_index(&sample_type) + .with_context(|| format!("sample type {:?} not configured", sample_type))?; + + self.values[index] = value; + + Ok(()) + } + + /// Gets the value for the given sample type. + /// + /// # Errors + /// + /// Returns an error if the sample type is not configured. + pub fn get_value(&self, sample_type: SampleType) -> anyhow::Result { + let index = self.metadata.get_index(&sample_type) + .with_context(|| format!("sample type {:?} not configured", sample_type))?; + Ok(self.values[index]) + } + + /// Returns a reference to the sample metadata. + pub fn metadata(&self) -> &Arc { + &self.metadata + } + + /// Returns whether locations should be reversed when converting to a Sample. + pub fn is_reverse_locations(&self) -> bool { + self.reverse_locations + } + + /// Sets whether locations should be reversed when converting to a Sample. + /// + /// When enabled, `as_sample()` will return locations in reverse order. + pub fn set_reverse_locations(&mut self, reverse: bool) { + self.reverse_locations = reverse; + } + + /// Sets the end time of the sample in nanoseconds. + /// + /// If `endtime_ns` is 0, the end time will be cleared (set to None). + /// + /// Returns the timestamp that was passed in. If timeline is disabled, + /// the value is not stored but is still returned. + pub fn set_endtime_ns(&mut self, endtime_ns: i64) -> i64 { + if self.metadata.is_timeline_enabled() { + self.endtime_ns = NonZeroI64::new(endtime_ns); + } + endtime_ns + } + + /// Sets the end time of the sample to the current time (now). + /// + /// On Unix platforms, this uses `CLOCK_MONOTONIC` for accurate timing and converts + /// to epoch time. On other platforms, it uses the system clock directly. + /// + /// Returns the calculated timestamp. If timeline is disabled, the timestamp + /// is calculated and returned but not stored in the sample. + /// + /// # Errors + /// + /// Returns an error if: + /// - On Unix: system time is before UNIX_EPOCH or `clock_gettime(CLOCK_MONOTONIC)` fails + /// - On non-Unix: system time is before UNIX_EPOCH + #[cfg(unix)] + pub fn set_endtime_ns_now(&mut self) -> anyhow::Result { + // Get current monotonic time + let ts = nix::time::clock_gettime(nix::time::ClockId::CLOCK_MONOTONIC) + .context("failed to get current monotonic time")?; + + let monotonic_ns = ts.tv_sec() * 1_000_000_000 + ts.tv_nsec(); + + // Convert to epoch time and set (set_endtime_from_monotonic_ns handles timeline check) + self.set_endtime_from_monotonic_ns(monotonic_ns) + } + + #[cfg(not(unix))] + pub fn set_endtime_ns_now(&mut self) -> anyhow::Result { + use std::time::SystemTime; + + let now_ns = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .context("system time is before UNIX_EPOCH")? + .as_nanos() as i64; + + // set_endtime_ns returns the timestamp and handles timeline check + Ok(self.set_endtime_ns(now_ns)) + } + + /// Returns the end time of the sample in nanoseconds, or None if not set. + pub fn endtime_ns(&self) -> Option { + self.endtime_ns + } + + /// Converts a monotonic timestamp (CLOCK_MONOTONIC) to epoch time and sets it as endtime_ns. + /// + /// Monotonic times have their epoch at system start, so they need an adjustment + /// to the standard epoch. This function computes the offset once (on first call) + /// and reuses it for all subsequent conversions. + /// + /// This uses `clock_gettime(CLOCK_MONOTONIC)` to determine the offset. + /// + /// Returns the converted epoch timestamp. If timeline is disabled, the timestamp + /// is calculated and returned but not stored. + /// + /// # Arguments + /// * `monotonic_ns` - Monotonic timestamp in nanoseconds since system boot + /// + /// # Errors + /// + /// Returns an error if: + /// - System time is before UNIX_EPOCH + /// - `clock_gettime(CLOCK_MONOTONIC)` fails + #[cfg(unix)] + pub fn set_endtime_from_monotonic_ns(&mut self, monotonic_ns: i64) -> anyhow::Result { + let offset = self.metadata.monotonic_to_epoch_offset(); + let endtime = monotonic_ns + offset; + Ok(self.set_endtime_ns(endtime)) + } + + /// Add a location to the sample. + /// + /// The location's strings will be copied into the internal arena. + /// If the number of locations has reached `max_frames` or arena allocation fails + /// (e.g., allocation limit reached), the frame will be dropped and the dropped + /// frame count will be incremented instead. + pub fn add_location(&mut self, location: Location<'_>) { + // Check if we've reached the max_frames limit + let current_count = self.inner.borrow_locations().len(); + if current_count >= self.metadata.max_frames() { + // Drop this frame and increment the counter + self.dropped_frames += 1; + return; + } + + // Try to add the location, but if allocation fails, just drop the frame + let result: Result<(), OwnedSampleError> = self.inner.with_mut(|fields| { + // Allocate strings in the arena + let filename_ref = fields.arena.try_alloc_str(location.mapping.filename)?; + let build_id_ref = fields.arena.try_alloc_str(location.mapping.build_id)?; + let name_ref = fields.arena.try_alloc_str(location.function.name)?; + let system_name_ref = fields.arena.try_alloc_str(location.function.system_name)?; + let func_filename_ref = fields.arena.try_alloc_str(location.function.filename)?; + + // Create location with references to arena strings + let owned_location = Location { + mapping: Mapping { + memory_start: location.mapping.memory_start, + memory_limit: location.mapping.memory_limit, + file_offset: location.mapping.file_offset, + filename: filename_ref, + build_id: build_id_ref, + }, + function: Function { + name: name_ref, + system_name: system_name_ref, + filename: func_filename_ref, + }, + address: location.address, + line: location.line, + }; + + fields.locations.push(owned_location); + Ok(()) + }); + + // If allocation failed, drop the frame + if result.is_err() { + self.dropped_frames += 1; + } + } + + /// Add multiple locations to the sample. + /// + /// The locations' strings will be copied into the internal arena. + /// Frames that exceed `max_frames` or cause allocation failures will be dropped. + pub fn add_locations(&mut self, locations: &[Location<'_>]) { + for location in locations { + self.add_location(*location); + } + } + + /// Add a label to the sample. + /// + /// The label's strings will be copied into the internal arena. + /// + /// # Errors + /// + /// Returns an error if arena allocation fails (out of memory). + pub fn add_label(&mut self, label: Label<'_>) -> Result<(), OwnedSampleError> { + self.inner.with_mut(|fields| { + let key_ref = fields.arena.try_alloc_str(label.key)?; + let str_ref = fields.arena.try_alloc_str(label.str)?; + let num_unit_ref = fields.arena.try_alloc_str(label.num_unit)?; + + let owned_label = Label { + key: key_ref, + str: str_ref, + num: label.num, + num_unit: num_unit_ref, + }; + + fields.labels.push(owned_label); + Ok(()) + }) + } + + /// Add multiple labels to the sample. + /// + /// The labels' strings will be copied into the internal arena. + /// + /// # Errors + /// + /// Returns an error if arena allocation fails (out of memory). + pub fn add_labels(&mut self, labels: &[Label<'_>]) -> Result<(), OwnedSampleError> { + for label in labels { + self.add_label(*label)?; + } + Ok(()) + } + + /// Add a string label to the sample using a well-known label key. + /// + /// This is a convenience method for adding labels with string values. + /// The string will be copied into the internal arena. + /// + /// # Example + /// ```no_run + /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType, LabelKey}; + /// # use std::sync::Arc; + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + /// # let mut sample = OwnedSample::new(metadata); + /// sample.add_string_label(LabelKey::ThreadName, "worker-1")?; + /// sample.add_string_label(LabelKey::ExceptionType, "ValueError")?; + /// # Ok::<(), libdd_profiling::owned_sample::OwnedSampleError>(()) + /// ``` + pub fn add_string_label(&mut self, key: LabelKey, value: &str) -> Result<(), OwnedSampleError> { + self.add_label(Label { + key: key.as_str(), + str: value, + num: 0, + num_unit: "", + }) + } + + /// Add a numeric label to the sample using a well-known label key. + /// + /// This is a convenience method for adding labels with numeric values. + /// + /// # Example + /// ```no_run + /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType, LabelKey}; + /// # use std::sync::Arc; + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + /// # let mut sample = OwnedSample::new(metadata); + /// sample.add_num_label(LabelKey::ThreadId, 42)?; + /// sample.add_num_label(LabelKey::SpanId, 12345)?; + /// # Ok::<(), libdd_profiling::owned_sample::OwnedSampleError>(()) + /// ``` + pub fn add_num_label(&mut self, key: LabelKey, value: i64) -> Result<(), OwnedSampleError> { + self.add_label(Label { + key: key.as_str(), + str: "", + num: value, + num_unit: "", + }) + } + + /// Get the sample values. + pub fn values(&self) -> &[i64] { + &self.values + } + + /// Get a mutable reference to the sample values. + pub fn values_mut(&mut self) -> &mut [i64] { + &mut self.values + } + + /// Reset the sample, clearing all locations and labels, and zeroing all values. + /// Reuses the arena and values vector allocations, avoiding reallocation overhead. + /// + /// # Example + /// ```no_run + /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType}; + /// # use libdd_profiling::api::{Location, Mapping, Function, Label}; + /// # use std::sync::Arc; + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime, SampleType::WallTime], 64, None, true).unwrap()); + /// let mut sample = OwnedSample::new(metadata); + /// sample.add_location(Location { + /// mapping: Mapping { memory_start: 0, memory_limit: 0, file_offset: 0, filename: "foo", build_id: "" }, + /// function: Function { name: "bar", system_name: "", filename: "" }, + /// address: 0, + /// line: 0, + /// }); + /// sample.add_label(Label { key: "thread", str: "main", num: 0, num_unit: "" }); + /// + /// sample.reset(); + /// assert_eq!(sample.locations().len(), 0); + /// assert_eq!(sample.labels().len(), 0); + /// assert_eq!(sample.values(), &[0, 0]); // Values are zeroed, not cleared + /// ``` + pub fn reset(&mut self) { + // Create a temporary empty inner to swap with + let temp_inner = SampleInnerBuilder { + arena: Bump::new(), + locations_builder: |_| Vec::new(), + labels_builder: |_| Vec::new(), + }.build(); + + // Replace self.inner with temp and extract the heads from the old one + let old_inner = std::mem::replace(&mut self.inner, temp_inner); + let mut heads = old_inner.into_heads(); + + // Reset the arena - this reuses the allocation! + heads.arena.reset(); + + // Re-apply the allocation limit after reset + heads.arena.set_allocation_limit(self.metadata.arena_allocation_limit()); + + // Zero out all values but keep the vector length and capacity + self.values.fill(0); + + self.endtime_ns = None; + self.reverse_locations = false; + self.dropped_frames = 0; + + // Rebuild with the reset arena + self.inner = SampleInnerBuilder { + arena: heads.arena, + locations_builder: |_| Vec::new(), + labels_builder: |_| Vec::new(), + }.build(); + } + + /// Get the number of frames that were dropped due to exceeding max_frames. + pub fn dropped_frames(&self) -> usize { + self.dropped_frames + } + + /// Get the number of bytes allocated in the internal arena. + /// + /// This includes all memory allocated for strings (location names, label keys, etc.) + /// stored in this sample. Useful for tracking memory usage and pool optimization. + pub fn allocated_bytes(&self) -> usize { + self.inner.with(|fields| { + fields.arena.allocated_bytes() + }) + } + + /// Add this sample to a profile. + /// + /// If frames were dropped (exceeding `max_frames`), a pseudo-frame will be appended + /// indicating how many frames were omitted. The profile will intern all strings, so + /// no memory is leaked. + /// + /// # Example + /// ```no_run + /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType}; + /// # use libdd_profiling::internal::Profile; + /// # use std::sync::Arc; + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + /// # let mut profile = Profile::try_new(&[], None).unwrap(); + /// let sample = OwnedSample::new(metadata); + /// sample.add_to_profile(&mut profile).unwrap(); + /// ``` + pub fn add_to_profile(&self, profile: &mut crate::internal::Profile) -> anyhow::Result<()> { + let mut locations = self.inner.borrow_locations().clone(); + + // Reverse locations if the flag is set + if self.reverse_locations { + locations.reverse(); + } + + // If frames were dropped, add a pseudo-frame indicating how many + let temp_name; + if self.dropped_frames > 0 { + let frame_word = if self.dropped_frames == 1 { "frame" } else { "frames" }; + temp_name = format!("<{} {} omitted>", self.dropped_frames, frame_word); + + // Create a pseudo-location for the dropped frames indicator + let pseudo_location = Location { + function: Function { + name: &temp_name, + ..Default::default() + }, + ..Default::default() + }; + + locations.push(pseudo_location); + } + + let sample = Sample { + locations, + values: &self.values, + labels: self.inner.borrow_labels().clone(), + }; + + // Profile will intern the strings, including the temp_name if it was created + profile.try_add_sample(sample, None) + } + + /// Get a slice of all locations. + pub fn locations(&self) -> &[Location<'_>] { + self.inner.borrow_locations() + } + + /// Get a slice of all labels. + pub fn labels(&self) -> &[Label<'_>] { + self.inner.borrow_labels() + } +} diff --git a/libdd-profiling/src/owned_sample/pool.rs b/libdd-profiling/src/owned_sample/pool.rs new file mode 100644 index 000000000..0c0bcb991 --- /dev/null +++ b/libdd-profiling/src/owned_sample/pool.rs @@ -0,0 +1,250 @@ +// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Pool for reusing `OwnedSample` instances to reduce allocation overhead. + +use super::{Metadata, OwnedSample}; +use crossbeam_queue::ArrayQueue; +use std::sync::Arc; + +/// A bounded pool of `OwnedSample` instances for efficient reuse. +/// +/// The pool maintains a limited number of samples that can be reused +/// across multiple profiling operations. When a sample is requested, +/// it's either taken from the pool or freshly allocated. When returned, +/// it's reset and added back to the pool if there's space, otherwise dropped. +/// +/// This pool is **thread-safe** and uses a lock-free `ArrayQueue` internally, +/// allowing concurrent access from multiple threads without locks. +/// +/// # Example +/// ```no_run +/// # use libdd_profiling::owned_sample::{SamplePool, Metadata, SampleType}; +/// # use std::sync::Arc; +/// let metadata = Arc::new(Metadata::new(vec![ +/// SampleType::CpuTime, +/// SampleType::WallTime, +/// ], 64, None, true).unwrap()); +/// +/// let pool = SamplePool::new(metadata, 10); +/// +/// // Get a sample from the pool (thread-safe) +/// let mut sample = pool.get(); +/// sample.set_value(SampleType::CpuTime, 100).unwrap(); +/// // ... use sample ... +/// +/// // Return it to the pool for reuse (thread-safe) +/// pool.put(sample); +/// ``` +pub struct SamplePool { + /// The sample type metadata configuration shared by all samples + metadata: Arc, + /// Lock-free bounded queue of available samples. + /// Uses `ArrayQueue` for lock-free concurrent access via atomic operations, + /// enabling efficient multi-threaded usage without mutex contention. + samples: ArrayQueue>, +} + +impl SamplePool { + /// Creates a new sample pool with the given capacity. + /// + /// # Arguments + /// * `indices` - The sample type indices configuration to use for all samples + /// * `capacity` - Maximum number of samples to keep in the pool + /// + /// # Example + /// ```no_run + /// # use libdd_profiling::owned_sample::{SamplePool, Metadata, SampleType}; + /// # use std::sync::Arc; + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + /// let pool = SamplePool::new(metadata, 100); + /// ``` + pub fn new(metadata: Arc, capacity: usize) -> Self { + Self { + metadata, + samples: ArrayQueue::new(capacity), + } + } + + /// Gets a sample from the pool, or allocates a new one if the pool is empty. + /// + /// The returned sample is guaranteed to be reset and ready to use. + /// + /// This method is **thread-safe** and can be called concurrently from multiple threads. + /// + /// # Example + /// ```no_run + /// # use libdd_profiling::owned_sample::{SamplePool, Metadata, SampleType}; + /// # use std::sync::Arc; + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + /// # let pool = SamplePool::new(metadata, 10); + /// let sample = pool.get(); + /// assert_eq!(sample.locations().len(), 0); + /// ``` + pub fn get(&self) -> Box { + self.samples.pop().unwrap_or_else(|| { + Box::new(OwnedSample::new(self.metadata.clone())) + }) + } + + /// Returns a sample to the pool for reuse. + /// + /// The sample is reset before being added to the pool. If the pool is at capacity, + /// the sample is dropped instead. + /// + /// This method is **thread-safe** and can be called concurrently from multiple threads. + /// + /// # Example + /// ```no_run + /// # use libdd_profiling::owned_sample::{SamplePool, Metadata, SampleType}; + /// # use std::sync::Arc; + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + /// # let pool = SamplePool::new(metadata, 10); + /// let mut sample = pool.get(); + /// sample.set_value(SampleType::CpuTime, 100).unwrap(); + /// pool.put(sample); // Resets and returns to pool + /// ``` + pub fn put(&self, mut sample: Box) { + // Reset the sample to clean state + sample.reset(); + + // Try to add back to pool (lock-free operation) + // If full, push() returns Err(sample), which we just drop + let _ = self.samples.push(sample); + } + + /// Returns the current number of samples in the pool. + pub fn len(&self) -> usize { + self.samples.len() + } + + /// Returns true if the pool is empty. + pub fn is_empty(&self) -> bool { + self.samples.is_empty() + } + + /// Returns the maximum capacity of the pool. + pub fn capacity(&self) -> usize { + self.samples.capacity() + } +} + +// SAFETY: SamplePool uses ArrayQueue which is Send + Sync, and Arc which is also Send + Sync +unsafe impl Send for SamplePool {} +unsafe impl Sync for SamplePool {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::owned_sample::SampleType; + + #[test] + fn test_pool_basic() { + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + let pool = SamplePool::new(metadata, 5); + + assert_eq!(pool.len(), 0); + assert!(pool.is_empty()); + assert_eq!(pool.capacity(), 5); + + // Get a sample - should allocate new + let sample = pool.get(); + assert_eq!(pool.len(), 0); + + // Return it + pool.put(sample); + assert_eq!(pool.len(), 1); + assert!(!pool.is_empty()); + + // Get it back - should reuse + let sample = pool.get(); + assert_eq!(pool.len(), 0); + + pool.put(sample); + assert_eq!(pool.len(), 1); + } + + #[test] + fn test_pool_capacity_limit() { + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + let pool = SamplePool::new(metadata, 2); + + // Fill the pool + let sample1 = pool.get(); + let sample2 = pool.get(); + let sample3 = pool.get(); + + pool.put(sample1); + pool.put(sample2); + assert_eq!(pool.len(), 2); + + // This one should be dropped since pool is at capacity + pool.put(sample3); + assert_eq!(pool.len(), 2); + } + + #[test] + fn test_pool_reset() { + let metadata = Arc::new(Metadata::new(vec![ + SampleType::CpuTime, + SampleType::WallTime, + ], 64, None, true).unwrap()); + let pool = SamplePool::new(metadata, 5); + + // Get a sample and modify it + let mut sample = pool.get(); + sample.set_value(SampleType::CpuTime, 100).unwrap(); + sample.set_value(SampleType::WallTime, 200).unwrap(); + + // Return it to pool + pool.put(sample); + + // Get it back - should be reset + let sample = pool.get(); + assert_eq!(sample.get_value(SampleType::CpuTime).unwrap(), 0); + assert_eq!(sample.get_value(SampleType::WallTime).unwrap(), 0); + assert_eq!(sample.locations().len(), 0); + assert_eq!(sample.labels().len(), 0); + } + + #[test] + fn test_pool_thread_safety() { + use std::thread; + + let metadata = Arc::new(Metadata::new(vec![ + SampleType::CpuTime, + SampleType::WallTime, + ], 64, None, true).unwrap()); + let pool = Arc::new(SamplePool::new(metadata, 20)); + + // Spawn multiple threads that all use the pool concurrently + let handles: Vec<_> = (0..4) + .map(|thread_id| { + let pool = Arc::clone(&pool); + thread::spawn(move || { + for i in 0..100 { + // Get a sample from the pool + let mut sample = pool.get(); + + // Use it + sample.set_value(SampleType::CpuTime, (thread_id * 1000 + i) as i64).unwrap(); + sample.set_value(SampleType::WallTime, (thread_id * 2000 + i) as i64).unwrap(); + + // Return it to the pool + pool.put(sample); + } + }) + }) + .collect(); + + // Wait for all threads to complete + for handle in handles { + handle.join().unwrap(); + } + + // Pool should have accumulated samples (up to its capacity) + assert!(pool.len() <= pool.capacity()); + assert!(!pool.is_empty()); // Should have at least some samples + } +} + diff --git a/libdd-profiling/src/owned_sample/sample_type.rs b/libdd-profiling/src/owned_sample/sample_type.rs new file mode 100644 index 000000000..c038fac8c --- /dev/null +++ b/libdd-profiling/src/owned_sample/sample_type.rs @@ -0,0 +1,66 @@ +// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use enum_map::Enum; + +/// Types of profiling values that can be collected. +/// +/// Each variant corresponds to a specific metric value in a profiling sample. +/// Some high-level sample types (like CPU, Wall, GPU) have multiple associated values. +/// +/// Based on the sample types and ValueIndex from [dd-trace-py](https://github.com/DataDog/dd-trace-py/blob/main/ddtrace/internal/datadog/profiling/dd_wrapper/include/types.hpp). +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Enum)] +pub enum SampleType { + // CPU profiling - 2 values + /// CPU time spent + CpuTime, + /// Number of CPU samples + CpuCount, + + // Wall clock profiling - 2 values + /// Wall clock time spent + WallTime, + /// Number of wall clock samples + WallCount, + + // Exception tracking - 1 value + /// Number of exceptions + ExceptionCount, + + // Lock acquisition profiling - 2 values + /// Time spent acquiring locks + LockAcquireTime, + /// Number of lock acquisitions + LockAcquireCount, + + // Lock release profiling - 2 values + /// Time spent releasing locks + LockReleaseTime, + /// Number of lock releases + LockReleaseCount, + + // Memory allocation profiling - 2 values + /// Allocated space in bytes + AllocSpace, + /// Number of allocations + AllocCount, + + // Heap profiling - 1 value + /// Heap space in bytes + HeapSpace, + + // GPU profiling - 6 values + /// GPU time spent + GpuTime, + /// Number of GPU samples + GpuCount, + /// GPU allocated space in bytes + GpuAllocSpace, + /// Number of GPU allocations + GpuAllocCount, + /// GPU floating point operations + GpuFlops, + /// Number of GPU FLOPS samples + GpuFlopsSamples, +} + diff --git a/libdd-profiling/src/owned_sample/tests.rs b/libdd-profiling/src/owned_sample/tests.rs new file mode 100644 index 000000000..fcffff2f2 --- /dev/null +++ b/libdd-profiling/src/owned_sample/tests.rs @@ -0,0 +1,835 @@ +// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use crate::api::{Function, Label, Location, Mapping, Sample}; + +/// Test helper to build a `Sample` from an `OwnedSample` for inspection. +/// +/// Note: The pseudo-frame message string (if any) is leaked via `Box::leak` +/// to obtain a `'static` lifetime, but this is only used in tests so it's acceptable. +fn as_sample(owned: &OwnedSample) -> Sample<'_> { + let mut locations = owned.inner.borrow_locations().clone(); + + // Reverse locations if the flag is set + if owned.reverse_locations { + locations.reverse(); + } + + // If frames were dropped, add a pseudo-frame indicating how many + if owned.dropped_frames > 0 { + let frame_word = if owned.dropped_frames == 1 { "frame" } else { "frames" }; + let name = format!("<{} {} omitted>", owned.dropped_frames, frame_word); + + // Leak the string only in tests - this is acceptable for test code + let name_static: &'static str = Box::leak(name.into_boxed_str()); + + // Create a pseudo-location for the dropped frames indicator + let pseudo_location = Location { + function: Function { + name: name_static, + ..Default::default() + }, + ..Default::default() + }; + + locations.push(pseudo_location); + } + + Sample { + locations, + values: &owned.values, + labels: owned.inner.borrow_labels().clone(), + } +} + +#[test] +fn test_owned_sample_basic() { + let metadata = Arc::new(Metadata::new(vec![ + SampleType::CpuTime, + SampleType::WallTime, + ], 64, None, true).unwrap()); + let mut sample = OwnedSample::new(metadata.clone()); + + sample.set_value(SampleType::CpuTime, 100).unwrap(); + sample.set_value(SampleType::WallTime, 200).unwrap(); + + sample.add_location(Location { + mapping: Mapping { + memory_start: 0x1000, + memory_limit: 0x2000, + file_offset: 0, + filename: "libfoo.so", + build_id: "abc123", + }, + function: Function { + name: "my_function", + system_name: "_Z11my_functionv", + filename: "foo.cpp", + }, + address: 0x1234, + line: 42, + }); + + sample.add_label(Label { key: "thread_name", str: "worker-1", num: 0, num_unit: "" }).unwrap(); + sample.add_label(Label { key: "thread_id", str: "", num: 123, num_unit: "" }).unwrap(); + + assert_eq!(sample.locations().len(), 1); + assert_eq!(sample.labels().len(), 2); + assert_eq!(sample.get_value(SampleType::CpuTime).unwrap(), 100); + assert_eq!(sample.get_value(SampleType::WallTime).unwrap(), 200); + + let location = sample.locations()[0]; + assert_eq!(location.mapping.filename, "libfoo.so"); + assert_eq!(location.function.name, "my_function"); + assert_eq!(location.address, 0x1234); + + let label = sample.labels()[0]; + assert_eq!(label.key, "thread_name"); + assert_eq!(label.str, "worker-1"); +} + + +#[test] +fn test_as_sample() { + let metadata = Arc::new(Metadata::new(vec![ + SampleType::CpuTime, + SampleType::WallTime, + ], 64, None, true).unwrap()); + let mut owned = OwnedSample::new(metadata.clone()); + owned.set_value(SampleType::CpuTime, 100).unwrap(); + owned.set_value(SampleType::WallTime, 200).unwrap(); + owned.add_location(Location { + mapping: Mapping { + memory_start: 0x1000, + memory_limit: 0x2000, + file_offset: 0, + filename: "libfoo.so", + build_id: "abc123", + }, + function: Function { + name: "my_function", + system_name: "_Z11my_functionv", + filename: "foo.cpp", + }, + address: 0x1234, + line: 42, + }); + owned.add_label(Label { key: "key", str: "value", num: 0, num_unit: "" }).unwrap(); + + let borrowed = as_sample(&owned); + assert_eq!(borrowed.values, &[100, 200]); + assert_eq!(borrowed.locations.len(), 1); + assert_eq!(borrowed.labels.len(), 1); + assert_eq!(borrowed.locations[0].function.name, "my_function"); + assert_eq!(borrowed.labels[0].key, "key"); +} + +#[test] +fn test_set_value_error() { + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + let mut sample = OwnedSample::new(metadata); + + // Should work for configured type + assert!(sample.set_value(SampleType::CpuTime, 100).is_ok()); + assert_eq!(sample.get_value(SampleType::CpuTime).unwrap(), 100); + + // Should fail for unconfigured type + assert!(sample.set_value(SampleType::WallTime, 200).is_err()); + assert!(sample.get_value(SampleType::WallTime).is_err()); +} + +#[test] +fn test_sample_type_indices_basic() { + let metadata = Metadata::new(vec![ + SampleType::CpuTime, + SampleType::WallTime, + SampleType::AllocSpace, + ], 64, None, true).unwrap(); + + assert_eq!(metadata.len(), 3); + assert!(!metadata.is_empty()); + + assert_eq!(metadata.get_index(&SampleType::CpuTime), Some(0)); + assert_eq!(metadata.get_index(&SampleType::WallTime), Some(1)); + assert_eq!(metadata.get_index(&SampleType::AllocSpace), Some(2)); + assert_eq!(metadata.get_index(&SampleType::HeapSpace), None); + + assert_eq!(metadata.get_type(0), Some(SampleType::CpuTime)); + assert_eq!(metadata.get_type(1), Some(SampleType::WallTime)); + assert_eq!(metadata.get_type(2), Some(SampleType::AllocSpace)); + assert_eq!(metadata.get_type(3), None); +} + +#[test] +fn test_sample_type_indices_duplicate_error() { + let result = Metadata::new(vec![ + SampleType::CpuTime, + SampleType::WallTime, + SampleType::CpuTime, // Duplicate + SampleType::AllocSpace, + ], 64, None, true); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().contains("duplicate")); +} + +#[test] +fn test_sample_type_indices_empty_error() { + let result = Metadata::new(vec![], 64, None, true); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().contains("empty")); +} + +#[test] +fn test_sample_type_indices_iter() { + let metadata = Metadata::new(vec![ + SampleType::CpuTime, + SampleType::WallTime, + SampleType::AllocSpace, + ], 64, None, true).unwrap(); + + let types: Vec<_> = metadata.iter().copied().collect(); + assert_eq!(types, vec![ + SampleType::CpuTime, + SampleType::WallTime, + SampleType::AllocSpace, + ]); +} + +#[test] +fn test_reset() { + let metadata = Arc::new(Metadata::new(vec![ + SampleType::CpuTime, + SampleType::WallTime, + SampleType::AllocSpace, + ], 64, None, true).unwrap()); + let mut sample = OwnedSample::new(metadata); + sample.set_value(SampleType::CpuTime, 100).unwrap(); + sample.set_value(SampleType::WallTime, 200).unwrap(); + sample.set_value(SampleType::AllocSpace, 300).unwrap(); + + sample.add_location(Location { + mapping: Mapping { + memory_start: 0x1000, + memory_limit: 0x2000, + file_offset: 0, + filename: "libfoo.so", + build_id: "abc123", + }, + function: Function { + name: "my_function", + system_name: "_Z11my_functionv", + filename: "foo.cpp", + }, + address: 0x1234, + line: 42, + }); + sample.add_label(Label { key: "key", str: "value", num: 0, num_unit: "" }).unwrap(); + + assert_eq!(sample.locations().len(), 1); + assert_eq!(sample.labels().len(), 1); + assert_eq!(sample.get_value(SampleType::CpuTime).unwrap(), 100); + assert_eq!(sample.get_value(SampleType::WallTime).unwrap(), 200); + assert_eq!(sample.get_value(SampleType::AllocSpace).unwrap(), 300); + + // Reset clears locations/labels and zeros values + sample.reset(); + + assert_eq!(sample.locations().len(), 0); + assert_eq!(sample.labels().len(), 0); + assert_eq!(sample.get_value(SampleType::CpuTime).unwrap(), 0); + assert_eq!(sample.get_value(SampleType::WallTime).unwrap(), 0); + assert_eq!(sample.get_value(SampleType::AllocSpace).unwrap(), 0); + + // Can add new data after reset + sample.add_location(Location { + mapping: Mapping { + memory_start: 0, + memory_limit: 0, + file_offset: 0, + filename: "new.so", + build_id: "", + }, + function: Function { + name: "new_func", + system_name: "", + filename: "", + }, + address: 0, + line: 1, + }); + assert_eq!(sample.locations().len(), 1); + let loc = sample.locations()[0]; + assert_eq!(loc.mapping.filename, "new.so"); +} + +#[test] +fn test_add_multiple() { + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + let mut sample = OwnedSample::new(metadata); + + // Add multiple locations at once + let locations = &[ + Location { + mapping: Mapping { memory_start: 0x1000, memory_limit: 0x2000, file_offset: 0, filename: "lib1.so", build_id: "" }, + function: Function { name: "func1", system_name: "", filename: "file1.c" }, + address: 0x1234, + line: 10, + }, + Location { + mapping: Mapping { memory_start: 0x3000, memory_limit: 0x4000, file_offset: 0, filename: "lib2.so", build_id: "" }, + function: Function { name: "func2", system_name: "", filename: "file2.c" }, + address: 0x5678, + line: 20, + }, + ]; + sample.add_locations(locations); + + // Add multiple labels at once + let labels = &[ + Label { key: "thread", str: "main", num: 0, num_unit: "" }, + Label { key: "thread_id", str: "", num: 123, num_unit: "" }, + ]; + sample.add_labels(labels).unwrap(); + + assert_eq!(sample.locations().len(), 2); + assert_eq!(sample.labels().len(), 2); + + let loc0 = sample.locations()[0]; + assert_eq!(loc0.mapping.filename, "lib1.so"); + assert_eq!(loc0.function.name, "func1"); + + let loc1 = sample.locations()[1]; + assert_eq!(loc1.mapping.filename, "lib2.so"); + assert_eq!(loc1.function.name, "func2"); + + let label0 = sample.labels()[0]; + assert_eq!(label0.key, "thread"); + assert_eq!(label0.str, "main"); + + let label1 = sample.labels()[1]; + assert_eq!(label1.key, "thread_id"); + assert_eq!(label1.num, 123); +} + +#[test] +fn test_endtime_ns() { + use std::num::NonZeroI64; + + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + let mut sample = OwnedSample::new(metadata); + + // Initially, endtime_ns should be None + assert_eq!(sample.endtime_ns(), None); + + // Set a non-zero endtime + sample.set_endtime_ns(123456789); + assert_eq!(sample.endtime_ns(), NonZeroI64::new(123456789)); + + // Setting to 0 should clear it + sample.set_endtime_ns(0); + assert_eq!(sample.endtime_ns(), None); + + // Set another value + sample.set_endtime_ns(987654321); + assert_eq!(sample.endtime_ns(), NonZeroI64::new(987654321)); + + // Reset should clear endtime_ns + sample.reset(); + assert_eq!(sample.endtime_ns(), None); +} + +#[test] +fn test_set_endtime_ns_now() { + use std::time::SystemTime; + + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + let mut sample = OwnedSample::new(metadata); + + // Initially, endtime_ns should be None + assert_eq!(sample.endtime_ns(), None); + + // Get approximate current time + let approx_now_ns = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_nanos() as i64; + + // Set endtime to now and get the returned timestamp + let returned_time = sample.set_endtime_ns_now().unwrap(); + + // The endtime should be set to a reasonable value + let endtime = sample.endtime_ns().unwrap().get(); + + // The returned time should match what was set + assert_eq!(returned_time, endtime); + + // Allow for a 1 second difference due to monotonic vs realtime clock differences + // and the time taken to compute the offset + let second_ns = 1_000_000_000i64; + assert!( + (endtime - approx_now_ns).abs() < second_ns, + "endtime {} should be within 1 second of approx_now {}", + endtime, + approx_now_ns + ); + + // Test that calling it twice gives increasing values + let first_endtime = sample.endtime_ns().unwrap().get(); + std::thread::sleep(std::time::Duration::from_millis(1)); + sample.set_endtime_ns_now().unwrap(); + let second_endtime = sample.endtime_ns().unwrap().get(); + assert!( + second_endtime >= first_endtime, + "second endtime {} should be >= first endtime {}", + second_endtime, + first_endtime + ); + + // Reset should clear it + sample.reset(); + assert_eq!(sample.endtime_ns(), None); +} + +#[test] +fn test_timeline_enabled() { + // Test with timeline enabled + let metadata_enabled = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + assert!(metadata_enabled.is_timeline_enabled()); + let mut sample_enabled = OwnedSample::new(metadata_enabled); + + // Set endtime should work when timeline is enabled + sample_enabled.set_endtime_ns(123456789); + assert_eq!(sample_enabled.endtime_ns().unwrap().get(), 123456789); + + // set_endtime_ns_now should return the timestamp it sets when enabled + let returned_time = sample_enabled.set_endtime_ns_now().unwrap(); + assert_ne!(returned_time, 0); // should not be 0 when timeline is enabled + assert_eq!(sample_enabled.endtime_ns().unwrap().get(), returned_time); // should match + + // Test with timeline disabled + let metadata_disabled = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, false).unwrap()); + assert!(!metadata_disabled.is_timeline_enabled()); + let mut sample_disabled = OwnedSample::new(metadata_disabled); + + // Set endtime should be a no-op when timeline is disabled + sample_disabled.set_endtime_ns(987654321); + assert_eq!(sample_disabled.endtime_ns(), None); // not set + + // set_endtime_ns_now should still calculate and return time when disabled, but not set it + let returned_time = sample_disabled.set_endtime_ns_now().unwrap(); + assert_ne!(returned_time, 0); // still returns the calculated timestamp + assert_eq!(sample_disabled.endtime_ns(), None); // but doesn't set it +} + +#[test] +#[cfg(unix)] +fn test_set_endtime_from_monotonic_ns() { + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + let mut sample = OwnedSample::new(metadata); + + // Set endtime from a monotonic time + let monotonic_ns = 123456789000; // Some monotonic time + sample.set_endtime_from_monotonic_ns(monotonic_ns).unwrap(); + + // The endtime should be set (monotonic + offset) + let endtime = sample.endtime_ns(); + assert!(endtime.is_some()); + + // The endtime should be much larger than the monotonic time + // (because it includes the offset from system boot to epoch) + let endtime_val = endtime.unwrap().get(); + + // Get current epoch time to verify the conversion is reasonable + use std::time::SystemTime; + let now_epoch_ns = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_nanos() as i64; + + // The converted time should be somewhere near the current time + // (within a reasonable range, e.g., the last year and next minute) + let year_ns = 365 * 24 * 60 * 60 * 1_000_000_000i64; + let minute_ns = 60 * 1_000_000_000i64; + assert!(endtime_val > now_epoch_ns - year_ns, "endtime too far in the past"); + assert!(endtime_val < now_epoch_ns + minute_ns, "endtime too far in the future"); + + // Set endtime from another monotonic time + let monotonic_ns2 = monotonic_ns + 1_000_000; // 1ms later + sample.set_endtime_from_monotonic_ns(monotonic_ns2).unwrap(); + + let endtime2 = sample.endtime_ns().unwrap().get(); + // The difference should match (1ms) + assert_eq!(endtime2 - endtime_val, 1_000_000); +} + +#[test] +fn test_reverse_locations() { + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + let mut sample = OwnedSample::new(metadata); + + // Initially, reverse_locations should be false + assert!(!sample.is_reverse_locations()); + + // Add three locations + sample.add_location(Location { + mapping: Mapping { memory_start: 0x1000, memory_limit: 0x2000, file_offset: 0, filename: "lib1.so", build_id: "" }, + function: Function { name: "func1", system_name: "", filename: "" }, + address: 0x1001, + line: 10, + }); + sample.add_location(Location { + mapping: Mapping { memory_start: 0x2000, memory_limit: 0x3000, file_offset: 0, filename: "lib2.so", build_id: "" }, + function: Function { name: "func2", system_name: "", filename: "" }, + address: 0x2002, + line: 20, + }); + sample.add_location(Location { + mapping: Mapping { memory_start: 0x3000, memory_limit: 0x4000, file_offset: 0, filename: "lib3.so", build_id: "" }, + function: Function { name: "func3", system_name: "", filename: "" }, + address: 0x3003, + line: 30, + }); + + // Get sample with normal order + let normal_sample = as_sample(&sample); + assert_eq!(normal_sample.locations.len(), 3); + assert_eq!(normal_sample.locations[0].function.name, "func1"); + assert_eq!(normal_sample.locations[1].function.name, "func2"); + assert_eq!(normal_sample.locations[2].function.name, "func3"); + + // Enable reverse locations + sample.set_reverse_locations(true); + assert!(sample.is_reverse_locations()); + + // Get sample with reversed order + let reversed_sample = as_sample(&sample); + assert_eq!(reversed_sample.locations.len(), 3); + assert_eq!(reversed_sample.locations[0].function.name, "func3"); + assert_eq!(reversed_sample.locations[1].function.name, "func2"); + assert_eq!(reversed_sample.locations[2].function.name, "func1"); + + // Disable reverse locations + sample.set_reverse_locations(false); + assert!(!sample.is_reverse_locations()); + + // Should be back to normal order + let normal_again = as_sample(&sample); + assert_eq!(normal_again.locations[0].function.name, "func1"); + assert_eq!(normal_again.locations[1].function.name, "func2"); + assert_eq!(normal_again.locations[2].function.name, "func3"); + + // Reset should clear the flag + sample.set_reverse_locations(true); + sample.reset(); + assert!(!sample.is_reverse_locations()); +} + +#[test] +fn test_label_key() { + // Test as_str() + assert_eq!(LabelKey::ExceptionType.as_str(), "exception type"); + assert_eq!(LabelKey::ThreadId.as_str(), "thread id"); + assert_eq!(LabelKey::ThreadNativeId.as_str(), "thread native id"); + assert_eq!(LabelKey::ThreadName.as_str(), "thread name"); + assert_eq!(LabelKey::TaskId.as_str(), "task id"); + assert_eq!(LabelKey::TaskName.as_str(), "task name"); + assert_eq!(LabelKey::SpanId.as_str(), "span id"); + assert_eq!(LabelKey::LocalRootSpanId.as_str(), "local root span id"); + assert_eq!(LabelKey::TraceType.as_str(), "trace type"); + assert_eq!(LabelKey::ClassName.as_str(), "class name"); + assert_eq!(LabelKey::LockName.as_str(), "lock name"); + assert_eq!(LabelKey::GpuDeviceName.as_str(), "gpu device name"); + + // Test AsRef + let key: &str = LabelKey::ThreadId.as_ref(); + assert_eq!(key, "thread id"); + + // Test Display + assert_eq!(format!("{}", LabelKey::ThreadName), "thread name"); + + // Test that it can be used as a label key + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + let mut sample = OwnedSample::new(metadata); + + sample.add_label(Label { + key: LabelKey::ThreadId.as_str(), + str: "", + num: 42, + num_unit: "", + }).unwrap(); + + sample.add_label(Label { + key: LabelKey::ThreadName.as_str(), + str: "worker-1", + num: 0, + num_unit: "", + }).unwrap(); + + assert_eq!(sample.labels().len(), 2); +} + +#[test] +fn test_add_string_label() { + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + let mut sample = OwnedSample::new(metadata); + + // Add string labels using the convenience method + sample.add_string_label(LabelKey::ThreadName, "worker-1").unwrap(); + sample.add_string_label(LabelKey::ExceptionType, "ValueError").unwrap(); + sample.add_string_label(LabelKey::ClassName, "MyClass").unwrap(); + + assert_eq!(sample.labels().len(), 3); + + // Verify the labels were added correctly + let api_sample = as_sample(&sample); + assert_eq!(api_sample.labels.len(), 3); + + // Check first label + assert_eq!(api_sample.labels[0].key, "thread name"); + assert_eq!(api_sample.labels[0].str, "worker-1"); + assert_eq!(api_sample.labels[0].num, 0); + + // Check second label + assert_eq!(api_sample.labels[1].key, "exception type"); + assert_eq!(api_sample.labels[1].str, "ValueError"); + assert_eq!(api_sample.labels[1].num, 0); +} + +#[test] +fn test_add_num_label() { + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + let mut sample = OwnedSample::new(metadata); + + // Add numeric labels using the convenience method + sample.add_num_label(LabelKey::ThreadId, 42).unwrap(); + sample.add_num_label(LabelKey::ThreadNativeId, 12345).unwrap(); + sample.add_num_label(LabelKey::SpanId, 98765).unwrap(); + + assert_eq!(sample.labels().len(), 3); + + // Verify the labels were added correctly + let api_sample = as_sample(&sample); + assert_eq!(api_sample.labels.len(), 3); + + // Check first label + assert_eq!(api_sample.labels[0].key, "thread id"); + assert_eq!(api_sample.labels[0].str, ""); + assert_eq!(api_sample.labels[0].num, 42); + + // Check second label + assert_eq!(api_sample.labels[1].key, "thread native id"); + assert_eq!(api_sample.labels[1].num, 12345); + + // Check third label + assert_eq!(api_sample.labels[2].key, "span id"); + assert_eq!(api_sample.labels[2].num, 98765); +} + +#[test] +fn test_mixed_label_types() { + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + let mut sample = OwnedSample::new(metadata); + + // Mix string and numeric labels + sample.add_string_label(LabelKey::ThreadName, "worker-1").unwrap(); + sample.add_num_label(LabelKey::ThreadId, 42).unwrap(); + sample.add_string_label(LabelKey::ExceptionType, "RuntimeError").unwrap(); + sample.add_num_label(LabelKey::SpanId, 12345).unwrap(); + + assert_eq!(sample.labels().len(), 4); + + let api_sample = as_sample(&sample); + assert_eq!(api_sample.labels[0].key, "thread name"); + assert_eq!(api_sample.labels[0].str, "worker-1"); + assert_eq!(api_sample.labels[1].key, "thread id"); + assert_eq!(api_sample.labels[1].num, 42); + assert_eq!(api_sample.labels[2].key, "exception type"); + assert_eq!(api_sample.labels[2].str, "RuntimeError"); + assert_eq!(api_sample.labels[3].key, "span id"); + assert_eq!(api_sample.labels[3].num, 12345); +} + +#[test] +fn test_dropped_frames() { + // Create metadata with a small max_frames limit + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 3, None, true).unwrap()); + let mut sample = OwnedSample::new(metadata); + + // Add 3 locations (at the limit) + for i in 0..3 { + sample.add_location(Location { + mapping: Mapping { + memory_start: 0x1000 + i * 0x100, + memory_limit: 0x2000 + i * 0x100, + file_offset: 0, + filename: "test.so", + build_id: "abc", + }, + function: Function { + name: "function", + system_name: "function", + filename: "test.c", + }, + address: 0x1234 + i, + line: 10 + i as i64, + }); + } + + assert_eq!(sample.locations().len(), 3); + assert_eq!(sample.dropped_frames(), 0); + + // Try to add 2 more locations (should be dropped) + for i in 3..5 { + sample.add_location(Location { + mapping: Mapping { + memory_start: 0x1000 + i * 0x100, + memory_limit: 0x2000 + i * 0x100, + file_offset: 0, + filename: "test.so", + build_id: "abc", + }, + function: Function { + name: "dropped_function", + system_name: "dropped_function", + filename: "test.c", + }, + address: 0x1234 + i, + line: 10 + i as i64, + }); + } + + // Should still have 3 locations, but 2 dropped + assert_eq!(sample.locations().len(), 3); + assert_eq!(sample.dropped_frames(), 2); + + // Convert to API sample and verify pseudo-frame was added + let api_sample = as_sample(&sample); + + // Should have 4 locations now: 3 real + 1 pseudo-frame + assert_eq!(api_sample.locations.len(), 4); + + // The last location should be the pseudo-frame + let pseudo_frame = &api_sample.locations[3]; + assert_eq!(pseudo_frame.function.name, "<2 frames omitted>"); + assert_eq!(pseudo_frame.address, 0); + assert_eq!(pseudo_frame.line, 0); + + // Test with a single dropped frame (singular "frame") + sample.reset(); + for i in 0..3 { + sample.add_location(Location { + mapping: Mapping { + memory_start: 0x1000 + i * 0x100, + memory_limit: 0x2000 + i * 0x100, + file_offset: 0, + filename: "test.so", + build_id: "abc", + }, + function: Function { + name: "function", + system_name: "function", + filename: "test.c", + }, + address: 0x1234 + i, + line: 10 + i as i64, + }); + } + sample.add_location(Location { + mapping: Mapping { + memory_start: 0x4000, + memory_limit: 0x5000, + file_offset: 0, + filename: "test.so", + build_id: "abc", + }, + function: Function { + name: "dropped", + system_name: "dropped", + filename: "test.c", + }, + address: 0x4444, + line: 100, + }); + + assert_eq!(sample.dropped_frames(), 1); + let api_sample = as_sample(&sample); + assert_eq!(api_sample.locations.len(), 4); + assert_eq!(api_sample.locations[3].function.name, "<1 frame omitted>"); +} + +#[test] +fn test_allocated_bytes() { + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); + let mut sample = OwnedSample::new(metadata); + + // Initially, should have some small allocation (or zero) + let initial_bytes = sample.allocated_bytes(); + + // Add a location with some strings + sample.add_location(Location { + mapping: Mapping { + memory_start: 0x1000, + memory_limit: 0x2000, + file_offset: 0, + filename: "this_is_a_filename.so", + build_id: "abc123def456", + }, + function: Function { + name: "my_function_name", + system_name: "_Z16my_function_namev", + filename: "source_file.cpp", + }, + address: 0x1234, + line: 42, + }); + + // Should have allocated more bytes for the strings + let after_location = sample.allocated_bytes(); + assert!(after_location > initial_bytes); + + // Add a label with more strings + sample.add_label(Label { + key: "thread_name", + str: "worker-thread-12345", + num: 0, + num_unit: "", + }).unwrap(); + + // Should have allocated at least as many bytes (may be more if arena grew) + let after_label = sample.allocated_bytes(); + assert!(after_label >= after_location); + + // Reset should clear the arena but may keep capacity + sample.reset(); + let after_reset = sample.allocated_bytes(); + + // After reset, the arena is cleared but bumpalo reuses the allocation, + // so allocated_bytes() still reports the chunk capacity + assert!(after_reset <= after_label); + + // Add data again to verify the arena still works after reset + sample.add_location(Location { + mapping: Mapping { + memory_start: 0x3000, + memory_limit: 0x4000, + file_offset: 0, + filename: "new_file.so", + build_id: "xyz", + }, + function: Function { + name: "new_function", + system_name: "new_function", + filename: "new.cpp", + }, + address: 0x5678, + line: 100, + }); + + // Verify we can still track allocations after reset + let after_second_add = sample.allocated_bytes(); + assert!(after_second_add > 0); +}