From f3d75a30022bd33f53088c4060dc3c9bdfbc508a Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbonne Date: Wed, 3 Dec 2025 14:53:53 -0500 Subject: [PATCH 01/12] profiling: Initial C++ API --- .github/workflows/test.yml | 17 +- .gitignore | 4 + Cargo.lock | 2 + examples/cxx/README.md | 151 +++++------- examples/cxx/build-and-run-crashinfo.ps1 | 158 +------------ examples/cxx/build-and-run-crashinfo.sh | 64 +----- examples/cxx/build-and-run.ps1 | 165 ++++++++++++++ examples/cxx/build-and-run.sh | 85 +++++++ examples/cxx/build-profiling.ps1 | 7 + examples/cxx/build-profiling.sh | 7 + examples/cxx/profiling.cpp | 197 ++++++++++++++++ libdd-profiling/Cargo.toml | 10 +- libdd-profiling/build.rs | 14 ++ libdd-profiling/src/cxx.rs | 278 +++++++++++++++++++++++ libdd-profiling/src/lib.rs | 2 + 15 files changed, 844 insertions(+), 317 deletions(-) create mode 100644 examples/cxx/build-and-run.ps1 create mode 100755 examples/cxx/build-and-run.sh create mode 100644 examples/cxx/build-profiling.ps1 create mode 100755 examples/cxx/build-profiling.sh create mode 100644 examples/cxx/profiling.cpp create mode 100644 libdd-profiling/build.rs create mode 100644 libdd-profiling/src/cxx.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ee60bdf40..e50999f521 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 07fa526c83..9629b6552e 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,7 @@ docker-sync.yml libtest.so libtest_cpp.so examples/cxx/crashinfo +examples/cxx/crashinfo.exe +examples/cxx/profiling +examples/cxx/profiling.exe +profile.pprof diff --git a/Cargo.lock b/Cargo.lock index af1ddb3ad3..45f3b14cfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2867,6 +2867,8 @@ dependencies = [ "chrono", "criterion", "crossbeam-utils", + "cxx", + "cxx-build", "futures", "hashbrown 0.16.0", "http", diff --git a/examples/cxx/README.md b/examples/cxx/README.md index 68ea54112f..8cbddc2e85 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 bd920608c5..285c6754c9 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 acc1548889..055c158ed2 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 0000000000..cf7fbd76be --- /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 0000000000..0a1409d8cd --- /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-profiling.ps1 b/examples/cxx/build-profiling.ps1 new file mode 100644 index 0000000000..b58d23858b --- /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 0000000000..defd8a08c6 --- /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/profiling.cpp b/examples/cxx/profiling.cpp new file mode 100644 index 0000000000..dc5471fac4 --- /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 bc8eb70928..5c8b9c113b 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 @@ -50,9 +54,13 @@ 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 } [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 0000000000..b32dff0f98 --- /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 0000000000..ee2aede613 --- /dev/null +++ b/libdd-profiling/src/cxx.rs @@ -0,0 +1,278 @@ +// 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>, + } + + // Opaque Rust types + extern "Rust" { + type Profile; + + // Static factory methods + #[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 (one for each variant) + 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>; + } +} + +// ============================================================================ +// 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) + } +} diff --git a/libdd-profiling/src/lib.rs b/libdd-profiling/src/lib.rs index 486104e792..513a67b689 100644 --- a/libdd-profiling/src/lib.rs +++ b/libdd-profiling/src/lib.rs @@ -8,6 +8,8 @@ pub mod api; pub mod collections; +#[cfg(feature = "cxx")] +pub mod cxx; pub mod exporter; pub mod internal; pub mod iter; From 7cb8b661dcbfb7b80bb2f5266b4e894736c8ed6b Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbonne Date: Thu, 4 Dec 2025 22:49:37 -0500 Subject: [PATCH 02/12] profiling: Add OwnedSample class --- Cargo.lock | 66 +++ libdd-profiling/Cargo.toml | 3 + libdd-profiling/src/lib.rs | 1 + libdd-profiling/src/owned_sample/mod.rs | 483 ++++++++++++++++++++++ libdd-profiling/src/owned_sample/pool.rs | 198 +++++++++ libdd-profiling/src/owned_sample/tests.rs | 278 +++++++++++++ 6 files changed, 1029 insertions(+) create mode 100644 libdd-profiling/src/owned_sample/mod.rs create mode 100644 libdd-profiling/src/owned_sample/pool.rs create mode 100644 libdd-profiling/src/owned_sample/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 45f3b14cfc..b1174e6177 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,6 +2888,7 @@ dependencies = [ "anyhow", "bitmaps", "bolero", + "bumpalo", "byteorder", "bytes", "chrono", @@ -2869,6 +2896,7 @@ dependencies = [ "crossbeam-utils", "cxx", "cxx-build", + "enum-map", "futures", "hashbrown 0.16.0", "http", @@ -2881,6 +2909,7 @@ dependencies = [ "libdd-profiling-protobuf", "lz4_flex", "mime", + "ouroboros", "parking_lot", "proptest", "prost", @@ -3682,6 +3711,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" @@ -3968,6 +4021,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/libdd-profiling/Cargo.toml b/libdd-profiling/Cargo.toml index 5c8b9c113b..1059c74024 100644 --- a/libdd-profiling/Cargo.toml +++ b/libdd-profiling/Cargo.toml @@ -28,6 +28,9 @@ 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"]} diff --git a/libdd-profiling/src/lib.rs b/libdd-profiling/src/lib.rs index 513a67b689..8c0cffc2c8 100644 --- a/libdd-profiling/src/lib.rs +++ b/libdd-profiling/src/lib.rs @@ -13,5 +13,6 @@ 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/mod.rs b/libdd-profiling/src/owned_sample/mod.rs new file mode 100644 index 0000000000..bb5f93f480 --- /dev/null +++ b/libdd-profiling/src/owned_sample/mod.rs @@ -0,0 +1,483 @@ +// 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, SampleTypeIndices, SampleType}; +//! use std::sync::Arc; +//! +//! let indices = Arc::new(SampleTypeIndices::new(vec![ +//! SampleType::Cpu, +//! SampleType::Wall, +//! ]).unwrap()); +//! +//! let mut sample = OwnedSample::new(indices); +//! +//! // Set values by type +//! sample.set_value(SampleType::Cpu, 1000).unwrap(); +//! sample.set_value(SampleType::Wall, 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: "" }); +//! sample.add_label(Label { key: "thread_id", str: "", num: 123, num_unit: "" }); +//! ``` + +use bumpalo::Bump; +use enum_map::{Enum, EnumMap}; +use std::sync::Arc; +use anyhow::{self, Context}; +use crate::api::{Function, Label, Location, Mapping, Sample}; + +mod pool; + +#[cfg(test)] +mod tests; + +pub use pool::SamplePool; + +/// Types of profiling samples that can be collected. +/// +/// Based on the sample types from [dd-trace-py](https://github.com/DataDog/dd-trace-py/blob/d239f91be2c4ca1ec2ded88263ed132e28fe031b/ddtrace/internal/datadog/profiling/dd_wrapper/include/types.hpp#L4). +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Enum)] +pub enum SampleType { + /// CPU time profiling + Cpu, + /// Wall clock time profiling + Wall, + /// Exception tracking + Exception, + /// Lock acquisition profiling + LockAcquire, + /// Lock release profiling + LockRelease, + /// Memory allocation profiling + Allocation, + /// Heap profiling + Heap, + /// GPU time profiling + GpuTime, + /// GPU memory profiling + GpuMemory, + /// GPU floating point operations profiling + GpuFlops, +} + +/// 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::{SampleTypeIndices, SampleType}; +/// let indices = SampleTypeIndices::new(vec![ +/// SampleType::Cpu, +/// SampleType::Wall, +/// SampleType::Allocation, +/// ]).unwrap(); +/// +/// assert_eq!(indices.get_index(&SampleType::Cpu), Some(0)); +/// assert_eq!(indices.get_index(&SampleType::Wall), Some(1)); +/// assert_eq!(indices.get_index(&SampleType::Allocation), Some(2)); +/// assert_eq!(indices.get_index(&SampleType::Heap), None); +/// assert_eq!(indices.len(), 3); +/// ``` +#[derive(Clone, Debug)] +pub struct SampleTypeIndices { + /// 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>, +} + +impl SampleTypeIndices { + /// Creates a new SampleTypeIndices with the given sample types. + /// + /// The order of sample types in the vector determines their index in the values array. + /// + /// # Errors + /// + /// Returns an error if: + /// - The sample types vector is empty + /// - The same sample type appears more than once + pub fn new(sample_types: Vec) -> 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); + } + + Ok(Self { + sample_types, + type_to_index, + }) + } + + /// 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 + } +} + +/// 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] +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. +pub struct OwnedSample { + inner: SampleInner, + values: Vec, + indices: Arc, +} + +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, SampleTypeIndices, SampleType}; + /// # use std::sync::Arc; + /// let indices = Arc::new(SampleTypeIndices::new(vec![ + /// SampleType::Cpu, + /// SampleType::Wall, + /// ]).unwrap()); + /// let sample = OwnedSample::new(indices); + /// ``` + pub fn new(indices: Arc) -> Self { + let num_values = indices.len(); + Self { + inner: SampleInnerBuilder { + arena: Bump::new(), + locations_builder: |_| Vec::new(), + labels_builder: |_| Vec::new(), + }.build(), + values: vec![0; num_values], + indices, + } + } + + /// 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, SampleTypeIndices, SampleType}; + /// # use std::sync::Arc; + /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + /// let mut sample = OwnedSample::new(indices); + /// sample.set_value(SampleType::Cpu, 1000).unwrap(); + /// ``` + pub fn set_value(&mut self, sample_type: SampleType, value: i64) -> anyhow::Result<()> { + let index = self.indices.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.indices.get_index(&sample_type) + .with_context(|| format!("sample type {:?} not configured", sample_type))?; + Ok(self.values[index]) + } + + /// Returns a reference to the sample type indices. + pub fn indices(&self) -> &Arc { + &self.indices + } + + /// Add a location to the sample. + /// + /// The location's strings will be copied into the internal arena. + pub fn add_location(&mut self, location: Location<'_>) { + self.inner.with_mut(|fields| { + // Allocate strings in the arena + let filename_ref = fields.arena.alloc_str(location.mapping.filename); + let build_id_ref = fields.arena.alloc_str(location.mapping.build_id); + let name_ref = fields.arena.alloc_str(location.function.name); + let system_name_ref = fields.arena.alloc_str(location.function.system_name); + let func_filename_ref = fields.arena.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); + }); + } + + /// Add multiple locations to the sample. + /// + /// The locations' strings will be copied into the internal arena. + 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. + pub fn add_label(&mut self, label: Label<'_>) { + self.inner.with_mut(|fields| { + let key_ref = fields.arena.alloc_str(label.key); + let str_ref = fields.arena.alloc_str(label.str); + let num_unit_ref = fields.arena.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); + }); + } + + /// Add multiple labels to the sample. + /// + /// The labels' strings will be copied into the internal arena. + pub fn add_labels(&mut self, labels: &[Label<'_>]) { + for label in labels { + self.add_label(*label); + } + } + + /// 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, SampleTypeIndices, SampleType}; + /// # use libdd_profiling::api::{Location, Mapping, Function, Label}; + /// # use std::sync::Arc; + /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu, SampleType::Wall]).unwrap()); + /// let mut sample = OwnedSample::new(indices); + /// 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.num_locations(), 0); + /// assert_eq!(sample.num_labels(), 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(); + + // Zero out all values but keep the vector length and capacity + self.values.fill(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 locations in this sample. + pub fn num_locations(&self) -> usize { + self.inner.borrow_locations().len() + } + + /// Get the number of labels in this sample. + pub fn num_labels(&self) -> usize { + self.inner.borrow_labels().len() + } + + /// Get a location by index. + pub fn get_location(&self, index: usize) -> Option> { + self.inner.borrow_locations().get(index).copied() + } + + /// Get a label by index. + pub fn get_label(&self, index: usize) -> Option> { + self.inner.borrow_labels().get(index).copied() + } + + /// Get a borrowed `Sample` view of this owned sample. + /// The returned sample borrows from this OwnedSample. + /// + /// # Example + /// ```no_run + /// # use libdd_profiling::owned_sample::{OwnedSample, SampleTypeIndices, SampleType}; + /// # use std::sync::Arc; + /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + /// let sample = OwnedSample::new(indices); + /// let borrowed = sample.as_sample(); + /// ``` + pub fn as_sample(&self) -> Sample<'_> { + Sample { + locations: self.inner.borrow_locations().clone(), + values: &self.values, + labels: self.inner.borrow_labels().clone(), + } + } + + /// Iterate over all locations. + pub fn locations(&self) -> impl Iterator> + '_ { + self.inner.borrow_locations().iter().copied() + } + + /// Iterate over all labels. + pub fn labels(&self) -> impl Iterator> + '_ { + self.inner.borrow_labels().iter().copied() + } +} + +impl std::fmt::Debug for OwnedSample { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OwnedSample") + .field("sample_types", &self.indices.types()) + .field("num_locations", &self.num_locations()) + .field("num_labels", &self.num_labels()) + .field("values", &self.values()) + .finish() + } +} + +impl PartialEq for OwnedSample { + fn eq(&self, other: &Self) -> bool { + // Compare indices configuration (pointer equality is fine since they're Arc) + Arc::ptr_eq(&self.indices, &other.indices) + && self.values() == other.values() + && self.num_locations() == other.num_locations() + && self.num_labels() == other.num_labels() + && self.locations().zip(other.locations()).all(|(a, b)| a == b) + && self.labels().zip(other.labels()).all(|(a, b)| a == b) + } +} + +impl Eq for OwnedSample {} + diff --git a/libdd-profiling/src/owned_sample/pool.rs b/libdd-profiling/src/owned_sample/pool.rs new file mode 100644 index 0000000000..1470725f2f --- /dev/null +++ b/libdd-profiling/src/owned_sample/pool.rs @@ -0,0 +1,198 @@ +// 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::{OwnedSample, SampleTypeIndices}; +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. +/// +/// # Example +/// ```no_run +/// # use libdd_profiling::owned_sample::{SamplePool, SampleTypeIndices, SampleType}; +/// # use std::sync::Arc; +/// let indices = Arc::new(SampleTypeIndices::new(vec![ +/// SampleType::Cpu, +/// SampleType::Wall, +/// ]).unwrap()); +/// +/// let mut pool = SamplePool::new(indices, 10); +/// +/// // Get a sample from the pool +/// let mut sample = pool.get(); +/// sample.set_value(SampleType::Cpu, 100).unwrap(); +/// // ... use sample ... +/// +/// // Return it to the pool for reuse +/// pool.put(sample); +/// ``` +pub struct SamplePool { + /// The sample type indices configuration shared by all samples + indices: Arc, + /// Maximum number of samples to keep in the pool + capacity: usize, + /// Stack of available samples + samples: Vec>, +} + +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, SampleTypeIndices, SampleType}; + /// # use std::sync::Arc; + /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + /// let pool = SamplePool::new(indices, 100); + /// ``` + pub fn new(indices: Arc, capacity: usize) -> Self { + Self { + indices, + capacity, + samples: Vec::with_capacity(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. + /// + /// # Example + /// ```no_run + /// # use libdd_profiling::owned_sample::{SamplePool, SampleTypeIndices, SampleType}; + /// # use std::sync::Arc; + /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + /// # let mut pool = SamplePool::new(indices, 10); + /// let sample = pool.get(); + /// assert_eq!(sample.num_locations(), 0); + /// ``` + pub fn get(&mut self) -> Box { + self.samples.pop().unwrap_or_else(|| { + Box::new(OwnedSample::new(self.indices.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. + /// + /// # Example + /// ```no_run + /// # use libdd_profiling::owned_sample::{SamplePool, SampleTypeIndices, SampleType}; + /// # use std::sync::Arc; + /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + /// # let mut pool = SamplePool::new(indices, 10); + /// let mut sample = pool.get(); + /// sample.set_value(SampleType::Cpu, 100).unwrap(); + /// pool.put(sample); // Resets and returns to pool + /// ``` + pub fn put(&mut self, mut sample: Box) { + if self.samples.len() < self.capacity { + sample.reset(); + self.samples.push(sample); + } + // Otherwise, sample is dropped when it goes out of scope + } + + /// 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.capacity + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::owned_sample::SampleType; + + #[test] + fn test_pool_basic() { + let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + let mut pool = SamplePool::new(indices, 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 indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + let mut pool = SamplePool::new(indices, 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 indices = Arc::new(SampleTypeIndices::new(vec![ + SampleType::Cpu, + SampleType::Wall, + ]).unwrap()); + let mut pool = SamplePool::new(indices, 5); + + // Get a sample and modify it + let mut sample = pool.get(); + sample.set_value(SampleType::Cpu, 100).unwrap(); + sample.set_value(SampleType::Wall, 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::Cpu).unwrap(), 0); + assert_eq!(sample.get_value(SampleType::Wall).unwrap(), 0); + assert_eq!(sample.num_locations(), 0); + assert_eq!(sample.num_labels(), 0); + } +} + diff --git a/libdd-profiling/src/owned_sample/tests.rs b/libdd-profiling/src/owned_sample/tests.rs new file mode 100644 index 0000000000..b0aecd6103 --- /dev/null +++ b/libdd-profiling/src/owned_sample/tests.rs @@ -0,0 +1,278 @@ +// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use crate::api::{Function, Label, Location, Mapping}; + +#[test] +fn test_owned_sample_basic() { + let indices = Arc::new(SampleTypeIndices::new(vec![ + SampleType::Cpu, + SampleType::Wall, + ]).unwrap()); + let mut sample = OwnedSample::new(indices.clone()); + + sample.set_value(SampleType::Cpu, 100).unwrap(); + sample.set_value(SampleType::Wall, 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: "" }); + sample.add_label(Label { key: "thread_id", str: "", num: 123, num_unit: "" }); + + assert_eq!(sample.num_locations(), 1); + assert_eq!(sample.num_labels(), 2); + assert_eq!(sample.get_value(SampleType::Cpu).unwrap(), 100); + assert_eq!(sample.get_value(SampleType::Wall).unwrap(), 200); + + let location = sample.get_location(0).unwrap(); + assert_eq!(location.mapping.filename, "libfoo.so"); + assert_eq!(location.function.name, "my_function"); + assert_eq!(location.address, 0x1234); + + let label = sample.get_label(0).unwrap(); + assert_eq!(label.key, "thread_name"); + assert_eq!(label.str, "worker-1"); +} + + +#[test] +fn test_as_sample() { + let indices = Arc::new(SampleTypeIndices::new(vec![ + SampleType::Cpu, + SampleType::Wall, + ]).unwrap()); + let mut owned = OwnedSample::new(indices.clone()); + owned.set_value(SampleType::Cpu, 100).unwrap(); + owned.set_value(SampleType::Wall, 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: "" }); + + let borrowed = owned.as_sample(); + 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 indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + let mut sample = OwnedSample::new(indices); + + // Should work for configured type + assert!(sample.set_value(SampleType::Cpu, 100).is_ok()); + assert_eq!(sample.get_value(SampleType::Cpu).unwrap(), 100); + + // Should fail for unconfigured type + assert!(sample.set_value(SampleType::Wall, 200).is_err()); + assert!(sample.get_value(SampleType::Wall).is_err()); +} + +#[test] +fn test_sample_type_indices_basic() { + let indices = SampleTypeIndices::new(vec![ + SampleType::Cpu, + SampleType::Wall, + SampleType::Allocation, + ]).unwrap(); + + assert_eq!(indices.len(), 3); + assert!(!indices.is_empty()); + + assert_eq!(indices.get_index(&SampleType::Cpu), Some(0)); + assert_eq!(indices.get_index(&SampleType::Wall), Some(1)); + assert_eq!(indices.get_index(&SampleType::Allocation), Some(2)); + assert_eq!(indices.get_index(&SampleType::Heap), None); + + assert_eq!(indices.get_type(0), Some(SampleType::Cpu)); + assert_eq!(indices.get_type(1), Some(SampleType::Wall)); + assert_eq!(indices.get_type(2), Some(SampleType::Allocation)); + assert_eq!(indices.get_type(3), None); +} + +#[test] +fn test_sample_type_indices_duplicate_error() { + let result = SampleTypeIndices::new(vec![ + SampleType::Cpu, + SampleType::Wall, + SampleType::Cpu, // Duplicate + SampleType::Allocation, + ]); + + 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 = SampleTypeIndices::new(vec![]); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().contains("empty")); +} + +#[test] +fn test_sample_type_indices_iter() { + let indices = SampleTypeIndices::new(vec![ + SampleType::Cpu, + SampleType::Wall, + SampleType::Allocation, + ]).unwrap(); + + let types: Vec<_> = indices.iter().copied().collect(); + assert_eq!(types, vec![ + SampleType::Cpu, + SampleType::Wall, + SampleType::Allocation, + ]); +} + +#[test] +fn test_reset() { + let indices = Arc::new(SampleTypeIndices::new(vec![ + SampleType::Cpu, + SampleType::Wall, + SampleType::Allocation, + ]).unwrap()); + let mut sample = OwnedSample::new(indices); + sample.set_value(SampleType::Cpu, 100).unwrap(); + sample.set_value(SampleType::Wall, 200).unwrap(); + sample.set_value(SampleType::Allocation, 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: "" }); + + assert_eq!(sample.num_locations(), 1); + assert_eq!(sample.num_labels(), 1); + assert_eq!(sample.get_value(SampleType::Cpu).unwrap(), 100); + assert_eq!(sample.get_value(SampleType::Wall).unwrap(), 200); + assert_eq!(sample.get_value(SampleType::Allocation).unwrap(), 300); + + // Reset clears locations/labels and zeros values + sample.reset(); + + assert_eq!(sample.num_locations(), 0); + assert_eq!(sample.num_labels(), 0); + assert_eq!(sample.get_value(SampleType::Cpu).unwrap(), 0); + assert_eq!(sample.get_value(SampleType::Wall).unwrap(), 0); + assert_eq!(sample.get_value(SampleType::Allocation).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.num_locations(), 1); + let loc = sample.get_location(0).unwrap(); + assert_eq!(loc.mapping.filename, "new.so"); +} + +#[test] +fn test_add_multiple() { + let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + let mut sample = OwnedSample::new(indices); + + // 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); + + assert_eq!(sample.num_locations(), 2); + assert_eq!(sample.num_labels(), 2); + + let loc0 = sample.get_location(0).unwrap(); + assert_eq!(loc0.mapping.filename, "lib1.so"); + assert_eq!(loc0.function.name, "func1"); + + let loc1 = sample.get_location(1).unwrap(); + assert_eq!(loc1.mapping.filename, "lib2.so"); + assert_eq!(loc1.function.name, "func2"); + + let label0 = sample.get_label(0).unwrap(); + assert_eq!(label0.key, "thread"); + assert_eq!(label0.str, "main"); + + let label1 = sample.get_label(1).unwrap(); + assert_eq!(label1.key, "thread_id"); + assert_eq!(label1.num, 123); +} + From 25e4ed249f15b24817215e64c2f09e548fc5107f Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbone Date: Fri, 5 Dec 2025 14:42:44 -0500 Subject: [PATCH 03/12] cxx bindings for owned_sample --- examples/cxx/build-owned-sample.ps1 | 8 + examples/cxx/build-owned-sample.sh | 8 + examples/cxx/owned_sample.cpp | 182 +++++++++++++++++++++ libdd-profiling/src/cxx.rs | 191 ++++++++++++++++++++++- libdd-profiling/src/owned_sample/pool.rs | 1 + 5 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 examples/cxx/build-owned-sample.ps1 create mode 100755 examples/cxx/build-owned-sample.sh create mode 100644 examples/cxx/owned_sample.cpp diff --git a/examples/cxx/build-owned-sample.ps1 b/examples/cxx/build-owned-sample.ps1 new file mode 100644 index 0000000000..8c504d7f28 --- /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 0000000000..e62c28fc2c --- /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/owned_sample.cpp b/examples/cxx/owned_sample.cpp new file mode 100644 index 0000000000..1420d1c0df --- /dev/null +++ b/examples/cxx/owned_sample.cpp @@ -0,0 +1,182 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#include +#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 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 SamplePool for efficient sample reuse..." << std::endl; + + // Create a pool of reusable samples for Wall time + auto pool = SamplePool::create({SampleType::Wall}, 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::Wall, wall_time_value); + + 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 + }); + } + + // Add labels + owned_sample->add_label(Label{ + .key = "thread_id", + .str = "", + .num = int64_t(i % 4), + .num_unit = "" + }); + + owned_sample->add_label(Label{ + .key = "sample_id", + .str = "", + .num = int64_t(i), + .num_unit = "" + }); + + // Add OwnedSample directly to profile + profile->add_owned_sample(*owned_sample); + + // 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/libdd-profiling/src/cxx.rs b/libdd-profiling/src/cxx.rs index ee2aede613..95bdabaee9 100644 --- a/libdd-profiling/src/cxx.rs +++ b/libdd-profiling/src/cxx.rs @@ -59,20 +59,39 @@ pub mod ffi { labels: Vec>, } + // Enums + #[derive(Debug)] + #[repr(u32)] + enum SampleType { + Cpu = 0, + Wall = 1, + Exception = 2, + LockAcquire = 3, + LockRelease = 4, + Allocation = 5, + Heap = 6, + GpuTime = 7, + GpuMemory = 8, + GpuFlops = 9, + } + // Opaque Rust types extern "Rust" { type Profile; + type OwnedSample; + type SamplePool; - // Static factory methods + // 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_owned_sample(self: &mut Profile, sample: &OwnedSample) -> 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 (one for each variant) + // Upscaling rule methods fn add_upscaling_rule_poisson( self: &mut Profile, offset_values: &[usize], @@ -103,6 +122,27 @@ pub mod ffi { fn reset(self: &mut Profile) -> Result<()>; fn serialize_to_vec(self: &mut Profile) -> Result>; + + // OwnedSample methods + #[Self = "OwnedSample"] + fn create(sample_types: Vec) -> Result>; + + fn set_value(self: &mut OwnedSample, sample_type: SampleType, value: i64) -> Result<()>; + fn get_value(self: &OwnedSample, sample_type: SampleType) -> Result; + fn add_location(self: &mut OwnedSample, location: &Location); + fn add_label(self: &mut OwnedSample, label: &Label); + fn num_locations(self: &OwnedSample) -> usize; + fn num_labels(self: &OwnedSample) -> usize; + fn reset_sample(self: &mut OwnedSample); + + // SamplePool methods + #[Self = "SamplePool"] + fn create(sample_types: Vec, 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; } } @@ -204,6 +244,15 @@ impl Profile { Ok(()) } + pub fn add_owned_sample(&mut self, sample: &OwnedSample) -> anyhow::Result<()> { + // Convert OwnedSample to API Sample + let api_sample = sample.inner.as_sample(); + + // 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)) @@ -276,3 +325,141 @@ impl Profile { Ok(encoded.buffer) } } + +// ============================================================================ +// OwnedSample - Wrapper around owned_sample::OwnedSample +// ============================================================================ + +use crate::owned_sample; +use std::sync::Arc; + +pub struct OwnedSample { + inner: owned_sample::OwnedSample, +} + +impl OwnedSample { + pub fn create(sample_types: Vec) -> anyhow::Result> { + // Convert CXX SampleType to owned_sample::SampleType + let types: Vec = sample_types + .into_iter() + .map(ffi_sample_type_to_owned) + .collect::>>()?; + + // Create indices internally + let indices = Arc::new(owned_sample::SampleTypeIndices::new(types)?); + let inner = owned_sample::OwnedSample::new(indices); + 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 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) { + let api_label: api::Label = label.into(); + self.inner.add_label(api_label); + } + + pub fn num_locations(&self) -> usize { + self.inner.num_locations() + } + + pub fn num_labels(&self) -> usize { + self.inner.num_labels() + } + + pub fn reset_sample(&mut self) { + self.inner.reset(); + } +} + +// ============================================================================ +// SamplePool - Wrapper around owned_sample::SamplePool +// ============================================================================ + +pub struct SamplePool { + inner: owned_sample::SamplePool, +} + +impl SamplePool { + pub fn create(sample_types: Vec, capacity: usize) -> anyhow::Result> { + // Convert CXX SampleType to owned_sample::SampleType + let types: Vec = sample_types + .into_iter() + .map(ffi_sample_type_to_owned) + .collect::>>()?; + + // Create indices internally + let indices = Arc::new(owned_sample::SampleTypeIndices::new(types)?); + let inner = owned_sample::SamplePool::new(indices, 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::Cpu => Ok(owned_sample::SampleType::Cpu), + ffi::SampleType::Wall => Ok(owned_sample::SampleType::Wall), + ffi::SampleType::Exception => Ok(owned_sample::SampleType::Exception), + ffi::SampleType::LockAcquire => Ok(owned_sample::SampleType::LockAcquire), + ffi::SampleType::LockRelease => Ok(owned_sample::SampleType::LockRelease), + ffi::SampleType::Allocation => Ok(owned_sample::SampleType::Allocation), + ffi::SampleType::Heap => Ok(owned_sample::SampleType::Heap), + ffi::SampleType::GpuTime => Ok(owned_sample::SampleType::GpuTime), + ffi::SampleType::GpuMemory => Ok(owned_sample::SampleType::GpuMemory), + ffi::SampleType::GpuFlops => Ok(owned_sample::SampleType::GpuFlops), + _ => anyhow::bail!("Unknown SampleType variant: {:?}", st), + } +} + +#[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::Cpu).unwrap() as usize, owned_sample::SampleType::Cpu as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::Wall).unwrap() as usize, owned_sample::SampleType::Wall as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::Exception).unwrap() as usize, owned_sample::SampleType::Exception as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::LockAcquire).unwrap() as usize, owned_sample::SampleType::LockAcquire as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::LockRelease).unwrap() as usize, owned_sample::SampleType::LockRelease as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::Allocation).unwrap() as usize, owned_sample::SampleType::Allocation as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::Heap).unwrap() as usize, owned_sample::SampleType::Heap 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::GpuMemory).unwrap() as usize, owned_sample::SampleType::GpuMemory as usize); + assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::GpuFlops).unwrap() as usize, owned_sample::SampleType::GpuFlops as usize); + } +} diff --git a/libdd-profiling/src/owned_sample/pool.rs b/libdd-profiling/src/owned_sample/pool.rs index 1470725f2f..6c2f3f6527 100644 --- a/libdd-profiling/src/owned_sample/pool.rs +++ b/libdd-profiling/src/owned_sample/pool.rs @@ -38,6 +38,7 @@ pub struct SamplePool { /// Maximum number of samples to keep in the pool capacity: usize, /// Stack of available samples + #[allow(clippy::vec_box)] samples: Vec>, } From 3df794defde8fe1908a7e656ee0d2f008c684e2e Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbone Date: Fri, 5 Dec 2025 15:38:55 -0500 Subject: [PATCH 04/12] handle time in the owned sample the way dd-trace-py does --- Cargo.lock | 1 + examples/cxx/owned_sample.cpp | 20 +++ libdd-profiling/Cargo.toml | 3 + libdd-profiling/src/cxx.rs | 40 ++++++ libdd-profiling/src/owned_sample/mod.rs | 151 +++++++++++++++++++++ libdd-profiling/src/owned_sample/tests.rs | 158 ++++++++++++++++++++++ 6 files changed, 373 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index b1174e6177..6ff1c10aca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2909,6 +2909,7 @@ dependencies = [ "libdd-profiling-protobuf", "lz4_flex", "mime", + "nix 0.29.0", "ouroboros", "parking_lot", "proptest", diff --git a/examples/cxx/owned_sample.cpp b/examples/cxx/owned_sample.cpp index 1420d1c0df..c0383ec29d 100644 --- a/examples/cxx/owned_sample.cpp +++ b/examples/cxx/owned_sample.cpp @@ -7,6 +7,9 @@ #include #include #include +#ifdef __unix__ +#include +#endif #include "libdd-profiling/src/cxx.rs.h" using namespace datadog::profiling; @@ -66,6 +69,23 @@ int main() { auto wall_time_value = 1000000 + (i % 1000) * 1000; owned_sample->set_value(SampleType::Wall, 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 rust::Error& 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, diff --git a/libdd-profiling/Cargo.toml b/libdd-profiling/Cargo.toml index 1059c74024..30309303e2 100644 --- a/libdd-profiling/Cargo.toml +++ b/libdd-profiling/Cargo.toml @@ -59,6 +59,9 @@ 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" diff --git a/libdd-profiling/src/cxx.rs b/libdd-profiling/src/cxx.rs index 95bdabaee9..22db10a5df 100644 --- a/libdd-profiling/src/cxx.rs +++ b/libdd-profiling/src/cxx.rs @@ -129,6 +129,19 @@ pub mod ffi { fn set_value(self: &mut OwnedSample, sample_type: SampleType, value: i64) -> Result<()>; fn get_value(self: &OwnedSample, sample_type: SampleType) -> Result; + + #[Self = "OwnedSample"] + fn is_timeline_enabled() -> bool; + #[Self = "OwnedSample"] + fn set_timeline_enabled(enabled: 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); fn num_locations(self: &OwnedSample) -> usize; @@ -361,6 +374,33 @@ impl OwnedSample { self.inner.get_value(st) } + pub fn is_timeline_enabled() -> bool { + owned_sample::OwnedSample::is_timeline_enabled() + } + + pub fn set_timeline_enabled(enabled: bool) { + owned_sample::OwnedSample::set_timeline_enabled(enabled); + } + + 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); diff --git a/libdd-profiling/src/owned_sample/mod.rs b/libdd-profiling/src/owned_sample/mod.rs index bb5f93f480..2fa957c684 100644 --- a/libdd-profiling/src/owned_sample/mod.rs +++ b/libdd-profiling/src/owned_sample/mod.rs @@ -48,7 +48,9 @@ use bumpalo::Bump; use enum_map::{Enum, EnumMap}; +use std::num::NonZeroI64; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; use anyhow::{self, Context}; use crate::api::{Function, Label, Location, Mapping, Sample}; @@ -59,6 +61,56 @@ mod tests; pub use pool::SamplePool; +/// Global flag to enable/disable timeline for all samples. +/// When disabled, time-setting methods become no-ops. +static TIMELINE_ENABLED: AtomicBool = AtomicBool::new(true); + +/// Computes the offset between monotonic time (CLOCK_MONOTONIC) and epoch time. +/// This is computed once and cached in an atomic. +/// +/// The offset allows converting monotonic timestamps (which start at system boot) +/// to epoch timestamps (which start at 1970-01-01). +/// +/// # Errors +/// +/// Returns an error if: +/// - System time is before UNIX_EPOCH +/// - `clock_gettime(CLOCK_MONOTONIC)` fails +#[cfg(unix)] +fn monotonic_to_epoch_offset() -> anyhow::Result { + static OFFSET: AtomicI64 = AtomicI64::new(0); + + // Fast path: offset already computed + let offset = OFFSET.load(Ordering::Relaxed); + if offset != 0 { + return Ok(offset); + } + + // Slow path: compute the 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 difference (epoch_ns will be larger since we're after 1970) + let computed_offset = epoch_ns - monotonic_ns; + + // Store it atomically (if another thread raced and stored it, that's fine) + OFFSET.store(computed_offset, Ordering::Relaxed); + + Ok(computed_offset) +} + + /// Types of profiling samples that can be collected. /// /// Based on the sample types from [dd-trace-py](https://github.com/DataDog/dd-trace-py/blob/d239f91be2c4ca1ec2ded88263ed132e28fe031b/ddtrace/internal/datadog/profiling/dd_wrapper/include/types.hpp#L4). @@ -205,6 +257,7 @@ pub struct OwnedSample { inner: SampleInner, values: Vec, indices: Arc, + endtime_ns: Option, } impl OwnedSample { @@ -233,6 +286,7 @@ impl OwnedSample { }.build(), values: vec![0; num_values], indices, + endtime_ns: None, } } @@ -275,6 +329,100 @@ impl OwnedSample { &self.indices } + /// Returns whether timeline is enabled globally for all samples. + pub fn is_timeline_enabled() -> bool { + TIMELINE_ENABLED.load(Ordering::Relaxed) + } + + /// Sets whether timeline is enabled globally for all samples. + /// + /// When timeline is disabled, time-setting methods become no-ops. + pub fn set_timeline_enabled(enabled: bool) { + TIMELINE_ENABLED.store(enabled, Ordering::Relaxed); + } + + /// 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::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 = 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. @@ -398,6 +546,9 @@ impl OwnedSample { // Zero out all values but keep the vector length and capacity self.values.fill(0); + // Reset endtime_ns + self.endtime_ns = None; + // Rebuild with the reset arena self.inner = SampleInnerBuilder { arena: heads.arena, diff --git a/libdd-profiling/src/owned_sample/tests.rs b/libdd-profiling/src/owned_sample/tests.rs index b0aecd6103..a1ad770b6b 100644 --- a/libdd-profiling/src/owned_sample/tests.rs +++ b/libdd-profiling/src/owned_sample/tests.rs @@ -276,3 +276,161 @@ fn test_add_multiple() { assert_eq!(label1.num, 123); } +#[test] +fn test_endtime_ns() { + use std::num::NonZeroI64; + + let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + let mut sample = OwnedSample::new(indices); + + // 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 indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + let mut sample = OwnedSample::new(indices); + + // 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() { + let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + let mut sample = OwnedSample::new(indices); + + // Timeline should be enabled by default + assert!(OwnedSample::is_timeline_enabled()); + + // Set endtime should work when timeline is enabled + sample.set_endtime_ns(123456789); + assert_eq!(sample.endtime_ns().unwrap().get(), 123456789); + + // Disable timeline + OwnedSample::set_timeline_enabled(false); + assert!(!OwnedSample::is_timeline_enabled()); + + // Set endtime should be a no-op when timeline is disabled + sample.set_endtime_ns(987654321); + assert_eq!(sample.endtime_ns().unwrap().get(), 123456789); // unchanged + + // set_endtime_ns_now should still calculate and return time when disabled, but not set it + let returned_time = sample.set_endtime_ns_now().unwrap(); + assert_ne!(returned_time, 0); // still returns the calculated timestamp + assert_eq!(sample.endtime_ns().unwrap().get(), 123456789); // but doesn't set it (unchanged) + + // Re-enable timeline + OwnedSample::set_timeline_enabled(true); + assert!(OwnedSample::is_timeline_enabled()); + + // Now set_endtime_ns should work again + sample.set_endtime_ns(999888777); + assert_eq!(sample.endtime_ns().unwrap().get(), 999888777); + + // set_endtime_ns_now should return the timestamp it sets when enabled + let returned_time = sample.set_endtime_ns_now().unwrap(); + assert_ne!(returned_time, 0); // should not be 0 when timeline is enabled + assert_eq!(sample.endtime_ns().unwrap().get(), returned_time); // should match +} + +#[test] +#[cfg(unix)] +fn test_set_endtime_from_monotonic_ns() { + let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + let mut sample = OwnedSample::new(indices); + + // 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); +} From 17bb9f08dcaabc5e312b828b897b19c3a3cf272d Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbone Date: Sat, 6 Dec 2025 00:23:41 -0500 Subject: [PATCH 05/12] reverse output --- examples/cxx/owned_sample.cpp | 6 +++ libdd-profiling/src/cxx.rs | 11 ++++ libdd-profiling/src/owned_sample/mod.rs | 25 ++++++++- libdd-profiling/src/owned_sample/tests.rs | 62 +++++++++++++++++++++++ 4 files changed, 102 insertions(+), 2 deletions(-) diff --git a/examples/cxx/owned_sample.cpp b/examples/cxx/owned_sample.cpp index c0383ec29d..2ca66e6d81 100644 --- a/examples/cxx/owned_sample.cpp +++ b/examples/cxx/owned_sample.cpp @@ -143,6 +143,12 @@ int main() { }); } + // 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 owned_sample->add_label(Label{ .key = "thread_id", diff --git a/libdd-profiling/src/cxx.rs b/libdd-profiling/src/cxx.rs index 22db10a5df..bb0ce1f6db 100644 --- a/libdd-profiling/src/cxx.rs +++ b/libdd-profiling/src/cxx.rs @@ -135,6 +135,9 @@ pub mod ffi { #[Self = "OwnedSample"] fn set_timeline_enabled(enabled: bool); + 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; @@ -382,6 +385,14 @@ impl OwnedSample { owned_sample::OwnedSample::set_timeline_enabled(enabled); } + 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) } diff --git a/libdd-profiling/src/owned_sample/mod.rs b/libdd-profiling/src/owned_sample/mod.rs index 2fa957c684..5f04427e15 100644 --- a/libdd-profiling/src/owned_sample/mod.rs +++ b/libdd-profiling/src/owned_sample/mod.rs @@ -258,6 +258,7 @@ pub struct OwnedSample { values: Vec, indices: Arc, endtime_ns: Option, + reverse_locations: bool, } impl OwnedSample { @@ -287,6 +288,7 @@ impl OwnedSample { values: vec![0; num_values], indices, endtime_ns: None, + reverse_locations: false, } } @@ -341,6 +343,18 @@ impl OwnedSample { TIMELINE_ENABLED.store(enabled, Ordering::Relaxed); } + /// 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). @@ -546,8 +560,8 @@ impl OwnedSample { // Zero out all values but keep the vector length and capacity self.values.fill(0); - // Reset endtime_ns self.endtime_ns = None; + self.reverse_locations = false; // Rebuild with the reset arena self.inner = SampleInnerBuilder { @@ -589,8 +603,15 @@ impl OwnedSample { /// let borrowed = sample.as_sample(); /// ``` pub fn as_sample(&self) -> Sample<'_> { + let mut locations = self.inner.borrow_locations().clone(); + + // Reverse locations if the flag is set + if self.reverse_locations { + locations.reverse(); + } + Sample { - locations: self.inner.borrow_locations().clone(), + locations, values: &self.values, labels: self.inner.borrow_labels().clone(), } diff --git a/libdd-profiling/src/owned_sample/tests.rs b/libdd-profiling/src/owned_sample/tests.rs index a1ad770b6b..c771479aee 100644 --- a/libdd-profiling/src/owned_sample/tests.rs +++ b/libdd-profiling/src/owned_sample/tests.rs @@ -434,3 +434,65 @@ fn test_set_endtime_from_monotonic_ns() { // The difference should match (1ms) assert_eq!(endtime2 - endtime_val, 1_000_000); } + +#[test] +fn test_reverse_locations() { + let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + let mut sample = OwnedSample::new(indices); + + // 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 = sample.as_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 = sample.as_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 = sample.as_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()); +} From dfb3d8f95657cfaf8a0c008e46cfd34039c44813 Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbone Date: Sat, 6 Dec 2025 00:25:28 -0500 Subject: [PATCH 06/12] lock free list for samplepool --- Cargo.lock | 1 + libdd-profiling/Cargo.toml | 1 + libdd-profiling/src/owned_sample/pool.rs | 97 ++++++++++++++++++------ 3 files changed, 76 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ff1c10aca..217d9a046f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2893,6 +2893,7 @@ dependencies = [ "bytes", "chrono", "criterion", + "crossbeam-queue", "crossbeam-utils", "cxx", "cxx-build", diff --git a/libdd-profiling/Cargo.toml b/libdd-profiling/Cargo.toml index 30309303e2..18f09f8473 100644 --- a/libdd-profiling/Cargo.toml +++ b/libdd-profiling/Cargo.toml @@ -34,6 +34,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"] } diff --git a/libdd-profiling/src/owned_sample/pool.rs b/libdd-profiling/src/owned_sample/pool.rs index 6c2f3f6527..bc3d1a5c33 100644 --- a/libdd-profiling/src/owned_sample/pool.rs +++ b/libdd-profiling/src/owned_sample/pool.rs @@ -4,6 +4,7 @@ //! Pool for reusing `OwnedSample` instances to reduce allocation overhead. use super::{OwnedSample, SampleTypeIndices}; +use crossbeam_queue::ArrayQueue; use std::sync::Arc; /// A bounded pool of `OwnedSample` instances for efficient reuse. @@ -13,6 +14,9 @@ use std::sync::Arc; /// 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, SampleTypeIndices, SampleType}; @@ -22,24 +26,23 @@ use std::sync::Arc; /// SampleType::Wall, /// ]).unwrap()); /// -/// let mut pool = SamplePool::new(indices, 10); +/// let pool = SamplePool::new(indices, 10); /// -/// // Get a sample from the pool +/// // Get a sample from the pool (thread-safe) /// let mut sample = pool.get(); /// sample.set_value(SampleType::Cpu, 100).unwrap(); /// // ... use sample ... /// -/// // Return it to the pool for reuse +/// // Return it to the pool for reuse (thread-safe) /// pool.put(sample); /// ``` pub struct SamplePool { /// The sample type indices configuration shared by all samples indices: Arc, - /// Maximum number of samples to keep in the pool - capacity: usize, - /// Stack of available samples - #[allow(clippy::vec_box)] - samples: Vec>, + /// 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 { @@ -59,8 +62,7 @@ impl SamplePool { pub fn new(indices: Arc, capacity: usize) -> Self { Self { indices, - capacity, - samples: Vec::with_capacity(capacity), + samples: ArrayQueue::new(capacity), } } @@ -68,16 +70,18 @@ impl SamplePool { /// /// 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, SampleTypeIndices, SampleType}; /// # use std::sync::Arc; /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - /// # let mut pool = SamplePool::new(indices, 10); + /// # let pool = SamplePool::new(indices, 10); /// let sample = pool.get(); /// assert_eq!(sample.num_locations(), 0); /// ``` - pub fn get(&mut self) -> Box { + pub fn get(&self) -> Box { self.samples.pop().unwrap_or_else(|| { Box::new(OwnedSample::new(self.indices.clone())) }) @@ -88,22 +92,25 @@ impl SamplePool { /// 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, SampleTypeIndices, SampleType}; /// # use std::sync::Arc; /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - /// # let mut pool = SamplePool::new(indices, 10); + /// # let pool = SamplePool::new(indices, 10); /// let mut sample = pool.get(); /// sample.set_value(SampleType::Cpu, 100).unwrap(); /// pool.put(sample); // Resets and returns to pool /// ``` - pub fn put(&mut self, mut sample: Box) { - if self.samples.len() < self.capacity { - sample.reset(); - self.samples.push(sample); - } - // Otherwise, sample is dropped when it goes out of scope + 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. @@ -118,10 +125,14 @@ impl SamplePool { /// Returns the maximum capacity of the pool. pub fn capacity(&self) -> usize { - self.capacity + 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::*; @@ -130,7 +141,7 @@ mod tests { #[test] fn test_pool_basic() { let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - let mut pool = SamplePool::new(indices, 5); + let pool = SamplePool::new(indices, 5); assert_eq!(pool.len(), 0); assert!(pool.is_empty()); @@ -156,7 +167,7 @@ mod tests { #[test] fn test_pool_capacity_limit() { let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - let mut pool = SamplePool::new(indices, 2); + let pool = SamplePool::new(indices, 2); // Fill the pool let sample1 = pool.get(); @@ -178,7 +189,7 @@ mod tests { SampleType::Cpu, SampleType::Wall, ]).unwrap()); - let mut pool = SamplePool::new(indices, 5); + let pool = SamplePool::new(indices, 5); // Get a sample and modify it let mut sample = pool.get(); @@ -195,5 +206,45 @@ mod tests { assert_eq!(sample.num_locations(), 0); assert_eq!(sample.num_labels(), 0); } + + #[test] + fn test_pool_thread_safety() { + use std::thread; + + let indices = Arc::new(SampleTypeIndices::new(vec![ + SampleType::Cpu, + SampleType::Wall, + ]).unwrap()); + let pool = Arc::new(SamplePool::new(indices, 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::Cpu, (thread_id * 1000 + i) as i64).unwrap(); + sample.set_value(SampleType::Wall, (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.len() > 0); // Should have at least some samples + } } From 51e0b63aeffbe91e897c9e7963d32db4658517ff Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbone Date: Sat, 6 Dec 2025 15:41:31 -0500 Subject: [PATCH 07/12] label enum api --- .gitignore | 3 + examples/cxx/owned_sample.cpp | 14 ++- libdd-profiling/src/cxx.rs | 46 ++++++++ libdd-profiling/src/owned_sample/mod.rs | 105 ++++++++++++++++++ libdd-profiling/src/owned_sample/tests.rs | 125 ++++++++++++++++++++++ profile_owned_sample.pprof | Bin 0 -> 1725 bytes 6 files changed, 285 insertions(+), 8 deletions(-) create mode 100644 profile_owned_sample.pprof diff --git a/.gitignore b/.gitignore index 9629b6552e..57165b9df1 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,7 @@ 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/examples/cxx/owned_sample.cpp b/examples/cxx/owned_sample.cpp index 2ca66e6d81..999db01c8c 100644 --- a/examples/cxx/owned_sample.cpp +++ b/examples/cxx/owned_sample.cpp @@ -73,7 +73,7 @@ int main() { // This is the simplest way to set the endtime try { owned_sample->set_endtime_ns_now(); - } catch (const rust::Error& e) { + } catch (const std::exception& e) { std::cerr << "Failed to set endtime to now: " << e.what() << std::endl; } @@ -149,14 +149,12 @@ int main() { owned_sample->set_reverse_locations(true); } - // Add labels - owned_sample->add_label(Label{ - .key = "thread_id", - .str = "", - .num = int64_t(i % 4), - .num_unit = "" - }); + // 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 = "", diff --git a/libdd-profiling/src/cxx.rs b/libdd-profiling/src/cxx.rs index bb0ce1f6db..d9daf3d8ec 100644 --- a/libdd-profiling/src/cxx.rs +++ b/libdd-profiling/src/cxx.rs @@ -75,6 +75,22 @@ pub mod ffi { GpuFlops = 9, } + #[derive(Debug)] + enum LabelKey { + ExceptionType, + ThreadId, + ThreadNativeId, + ThreadName, + TaskId, + TaskName, + SpanId, + LocalRootSpanId, + TraceType, + ClassName, + LockName, + GpuDeviceName, + } + // Opaque Rust types extern "Rust" { type Profile; @@ -147,6 +163,8 @@ pub mod ffi { fn add_location(self: &mut OwnedSample, location: &Location); fn add_label(self: &mut OwnedSample, label: &Label); + fn add_string_label(self: &mut OwnedSample, key: LabelKey, value: &str); + fn add_num_label(self: &mut OwnedSample, key: LabelKey, value: i64); fn num_locations(self: &OwnedSample) -> usize; fn num_labels(self: &OwnedSample) -> usize; fn reset_sample(self: &mut OwnedSample); @@ -422,6 +440,16 @@ impl OwnedSample { self.inner.add_label(api_label); } + pub fn add_string_label(&mut self, key: ffi::LabelKey, value: &str) { + let rust_key = ffi_label_key_to_owned(key); + self.inner.add_string_label(rust_key, value); + } + + pub fn add_num_label(&mut self, key: ffi::LabelKey, value: i64) { + let rust_key = ffi_label_key_to_owned(key); + self.inner.add_num_label(rust_key, value); + } + pub fn num_locations(&self) -> usize { self.inner.num_locations() } @@ -494,6 +522,24 @@ fn ffi_sample_type_to_owned(st: ffi::SampleType) -> anyhow::Result 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::*; diff --git a/libdd-profiling/src/owned_sample/mod.rs b/libdd-profiling/src/owned_sample/mod.rs index 5f04427e15..1f08922864 100644 --- a/libdd-profiling/src/owned_sample/mod.rs +++ b/libdd-profiling/src/owned_sample/mod.rs @@ -61,6 +61,66 @@ mod tests; pub use pool::SamplePool; +/// 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()) + } +} + /// Global flag to enable/disable timeline for all samples. /// When disabled, time-setting methods become no-ops. static TIMELINE_ENABLED: AtomicBool = AtomicBool::new(true); @@ -509,6 +569,51 @@ impl OwnedSample { } } + /// 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, SampleTypeIndices, SampleType, LabelKey}; + /// # use std::sync::Arc; + /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + /// # let mut sample = OwnedSample::new(indices); + /// sample.add_string_label(LabelKey::ThreadName, "worker-1"); + /// sample.add_string_label(LabelKey::ExceptionType, "ValueError"); + /// ``` + pub fn add_string_label(&mut self, key: LabelKey, value: &str) { + 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, SampleTypeIndices, SampleType, LabelKey}; + /// # use std::sync::Arc; + /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + /// # let mut sample = OwnedSample::new(indices); + /// sample.add_num_label(LabelKey::ThreadId, 42); + /// sample.add_num_label(LabelKey::SpanId, 12345); + /// ``` + pub fn add_num_label(&mut self, key: LabelKey, value: i64) { + self.add_label(Label { + key: key.as_str(), + str: "", + num: value, + num_unit: "", + }); + } + /// Get the sample values. pub fn values(&self) -> &[i64] { &self.values diff --git a/libdd-profiling/src/owned_sample/tests.rs b/libdd-profiling/src/owned_sample/tests.rs index c771479aee..c582488136 100644 --- a/libdd-profiling/src/owned_sample/tests.rs +++ b/libdd-profiling/src/owned_sample/tests.rs @@ -496,3 +496,128 @@ fn test_reverse_locations() { 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 indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + let mut sample = OwnedSample::new(indices); + + sample.add_label(Label { + key: LabelKey::ThreadId.as_str(), + str: "", + num: 42, + num_unit: "", + }); + + sample.add_label(Label { + key: LabelKey::ThreadName.as_str(), + str: "worker-1", + num: 0, + num_unit: "", + }); + + assert_eq!(sample.num_labels(), 2); +} + +#[test] +fn test_add_string_label() { + let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + let mut sample = OwnedSample::new(indices); + + // Add string labels using the convenience method + sample.add_string_label(LabelKey::ThreadName, "worker-1"); + sample.add_string_label(LabelKey::ExceptionType, "ValueError"); + sample.add_string_label(LabelKey::ClassName, "MyClass"); + + assert_eq!(sample.num_labels(), 3); + + // Verify the labels were added correctly + let api_sample = sample.as_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 indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + let mut sample = OwnedSample::new(indices); + + // Add numeric labels using the convenience method + sample.add_num_label(LabelKey::ThreadId, 42); + sample.add_num_label(LabelKey::ThreadNativeId, 12345); + sample.add_num_label(LabelKey::SpanId, 98765); + + assert_eq!(sample.num_labels(), 3); + + // Verify the labels were added correctly + let api_sample = sample.as_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 indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + let mut sample = OwnedSample::new(indices); + + // Mix string and numeric labels + sample.add_string_label(LabelKey::ThreadName, "worker-1"); + sample.add_num_label(LabelKey::ThreadId, 42); + sample.add_string_label(LabelKey::ExceptionType, "RuntimeError"); + sample.add_num_label(LabelKey::SpanId, 12345); + + assert_eq!(sample.num_labels(), 4); + + let api_sample = sample.as_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); +} diff --git a/profile_owned_sample.pprof b/profile_owned_sample.pprof new file mode 100644 index 0000000000000000000000000000000000000000..d589b8501be1b7b32d860f462ded86d13a376662 GIT binary patch literal 1725 zcmV;u215BLwJ-euNToFZnqpiPK~O4H2eOQmU4rHYL}CmnkZpp*I5{eV#CRBs16adh zL?W>F^S`UI`R%c;s)`p@NgV@?)FMJbeUd26koT$2JUCzHo1YcxEg==C#ame}g!uEB}m!ujaWgkbzlc z`7u|8NwD(cT+1ByzdPtEKc>yNVPY1p6*oLIM3!IMaOSvoaD!UD^5Z?r9QPX1EDV#C zALrZ8Pgj0St7VS+r$l74%yADP2gzjR*Bob#dtQ(ZSpJK*);#wEev!iRCDqNaffNf@?+a9bKLX5sBP;Mgb=L!7u#x@=iUj(r%Pv!`(DsdSbn@~ndAP5E^VEr zoo}6H)V6gB;z(J3%ys6t4}mUio!&a%I<;+_4x@+0%Mz-=}n~0&6F9uM`@?V^5n&*BAUD}$ER{F(EL=*-i z%|s<-IWFYSYt5ZrSIH~={F_piAKTI! z2HFA3U!&p7aql9%l(td~_h=1=?Nq&prMox|EI;1F;T4vI^akCGofy6W z+hC>*Dl(~C8k5Kd=-Goqbg1-0WHR*Jy zf?nI3$D~Vu<~nU}UXu=oI`!G!ye1urBItT=9+U0>YM|%6c?EQje#d+B2)mbJHZ6L)ZTH=WzPH}pwq?_zx9{C|KzeQK-EG@KEqZ%x z_nn4L+j@7~c1?@kPTPIA?6a+Rw`~cv=>f z{1@A_=D9C1EYt3SOR)B@O*>)>SN@ChO!M4#xM5)&um?85(myO>B=QhUmNC7Ows3;9 z0v~qFy52gf!_Q z$)>T#)v5~@ELc!gwaVwK^7)WRCRdxy=96_pj2N}@l~kel9_xS zmsCB9Po|Urwb?vAv8xifG)nc3th3kEDku!TsNw+?s`zxSR6ka>=ya|}Cfg*SHk-$% zWzsm+JF?DR0Ckx(PHDB-JU+3j50+`wJF?E+0JW;gWGaj7>Vsuk`Fv5ut~QxWWszNd zuuQ9)YC-iKc^GLb)A_V2n^Q(0E03hI$0Moiu}-QUlZs3hS7md`=(MTJwQ8-vu0B|% z)lFTej7uHTxUy2un~F^8kj9mjdfHUqRiUnmOcs&Gm6dwhR3~!DJg!dW(zv{->s0bt zL8&xa*;MDU*qkyxo$FNcSwt?^u0EGZW)rzwb?ec{J_Q7T_oy*!1x7M4Vv>TChBZ*5 zNLw8dAYg(7SYRe9GU`=-RoSLe^%e3+`Rh$QfH1T)=P^+__P-DXm2e%y$ZiNt30VS> zDc?sNsys(3xCO^p?**hnCIbzm$oIV^RHo(9u+s{wKX(u%2cBzZJqtfro4rC(_qET~ z-Q^^RbRCvdukV|X9mZ3bX=M zy2A#lpZ!G;v@hT15-#RJQ00QVnqECY{!1Ty_|CO0(~Gnzr#%X$fW)(7YrI|z1|wTw9Ha1nvD=Zr#$>q*CBFYbNU>8V TsLN}kY$_fPd)@`55zwIqTjF@0 literal 0 HcmV?d00001 From 5ce52bb310d29e6ffb0fe7006d300e6473726832 Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbone Date: Sat, 6 Dec 2025 15:46:18 -0500 Subject: [PATCH 08/12] refactor to a metadata struct --- examples/cxx/owned_sample.cpp | 11 +- libdd-profiling/src/cxx.rs | 62 +++--- libdd-profiling/src/owned_sample/metadata.rs | 166 ++++++++++++++ libdd-profiling/src/owned_sample/mod.rs | 223 +++---------------- libdd-profiling/src/owned_sample/pool.rs | 62 +++--- libdd-profiling/src/owned_sample/tests.rs | 140 ++++++------ profile_owned_sample.pprof | Bin 1725 -> 0 bytes 7 files changed, 335 insertions(+), 329 deletions(-) create mode 100644 libdd-profiling/src/owned_sample/metadata.rs delete mode 100644 profile_owned_sample.pprof diff --git a/examples/cxx/owned_sample.cpp b/examples/cxx/owned_sample.cpp index 999db01c8c..c991fd34d7 100644 --- a/examples/cxx/owned_sample.cpp +++ b/examples/cxx/owned_sample.cpp @@ -54,10 +54,15 @@ int main() { std::cout << "āœ… Added upscaling rules" << std::endl; - std::cout << "Creating SamplePool for efficient sample reuse..." << std::endl; + std::cout << "Creating Metadata and SamplePool for efficient sample reuse..." << std::endl; - // Create a pool of reusable samples for Wall time - auto pool = SamplePool::create({SampleType::Wall}, 10); + // Create metadata (configuration shared by all samples in the pool) + // Parameters: sample_types, max_frames, timeline_enabled + auto metadata = Metadata::create({SampleType::Wall}, 64, 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; diff --git a/libdd-profiling/src/cxx.rs b/libdd-profiling/src/cxx.rs index d9daf3d8ec..74bf6261e2 100644 --- a/libdd-profiling/src/cxx.rs +++ b/libdd-profiling/src/cxx.rs @@ -94,9 +94,14 @@ pub mod ffi { // Opaque Rust types extern "Rust" { type Profile; + type Metadata; type OwnedSample; type SamplePool; + // Metadata static factory + #[Self = "Metadata"] + fn create(sample_types: Vec, max_frames: usize, timeline_enabled: bool) -> Result>; + // Profile static factory #[Self = "Profile"] fn create(sample_types: Vec, period: &Period) -> Result>; @@ -141,16 +146,11 @@ pub mod ffi { // OwnedSample methods #[Self = "OwnedSample"] - fn create(sample_types: Vec) -> Result>; + 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; - #[Self = "OwnedSample"] - fn is_timeline_enabled() -> bool; - #[Self = "OwnedSample"] - fn set_timeline_enabled(enabled: bool); - fn is_reverse_locations(self: &OwnedSample) -> bool; fn set_reverse_locations(self: &mut OwnedSample, reverse: bool); @@ -171,7 +171,7 @@ pub mod ffi { // SamplePool methods #[Self = "SamplePool"] - fn create(sample_types: Vec, capacity: usize) -> Result>; + fn create(metadata: &Metadata, capacity: usize) -> Result>; fn get_sample(self: &mut SamplePool) -> Box; fn return_sample(self: &mut SamplePool, sample: Box); @@ -281,7 +281,7 @@ impl Profile { pub fn add_owned_sample(&mut self, sample: &OwnedSample) -> anyhow::Result<()> { // Convert OwnedSample to API Sample let api_sample = sample.inner.as_sample(); - + // Profile interns the strings self.inner.try_add_sample(api_sample, None)?; Ok(()) @@ -367,21 +367,32 @@ impl Profile { use crate::owned_sample; use std::sync::Arc; -pub struct OwnedSample { - inner: owned_sample::OwnedSample, +pub struct Metadata { + inner: Arc, } -impl OwnedSample { - pub fn create(sample_types: Vec) -> anyhow::Result> { +impl Metadata { + pub fn create(sample_types: Vec, max_frames: usize, 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::>>()?; - // Create indices internally - let indices = Arc::new(owned_sample::SampleTypeIndices::new(types)?); - let inner = owned_sample::OwnedSample::new(indices); + // Create metadata with specified configuration + let inner = Arc::new(owned_sample::Metadata::new(types, max_frames, 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 })) } @@ -395,14 +406,6 @@ impl OwnedSample { self.inner.get_value(st) } - pub fn is_timeline_enabled() -> bool { - owned_sample::OwnedSample::is_timeline_enabled() - } - - pub fn set_timeline_enabled(enabled: bool) { - owned_sample::OwnedSample::set_timeline_enabled(enabled); - } - pub fn is_reverse_locations(&self) -> bool { self.inner.is_reverse_locations() } @@ -472,16 +475,9 @@ pub struct SamplePool { } impl SamplePool { - pub fn create(sample_types: Vec, capacity: usize) -> anyhow::Result> { - // Convert CXX SampleType to owned_sample::SampleType - let types: Vec = sample_types - .into_iter() - .map(ffi_sample_type_to_owned) - .collect::>>()?; - - // Create indices internally - let indices = Arc::new(owned_sample::SampleTypeIndices::new(types)?); - let inner = owned_sample::SamplePool::new(indices, capacity); + 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 })) } diff --git a/libdd-profiling/src/owned_sample/metadata.rs b/libdd-profiling/src/owned_sample/metadata.rs new file mode 100644 index 0000000000..69220e45d4 --- /dev/null +++ b/libdd-profiling/src/owned_sample/metadata.rs @@ -0,0 +1,166 @@ +// 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::Cpu, +/// SampleType::Wall, +/// SampleType::Allocation, +/// ], 256, true).unwrap(); +/// +/// assert_eq!(metadata.get_index(&SampleType::Cpu), Some(0)); +/// assert_eq!(metadata.get_index(&SampleType::Wall), Some(1)); +/// assert_eq!(metadata.get_index(&SampleType::Allocation), Some(2)); +/// assert_eq!(metadata.get_index(&SampleType::Heap), 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, + /// 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 + /// * `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, 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, + 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 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 index 1f08922864..4dba522f65 100644 --- a/libdd-profiling/src/owned_sample/mod.rs +++ b/libdd-profiling/src/owned_sample/mod.rs @@ -9,15 +9,15 @@ //! # Example //! //! ```no_run -//! use libdd_profiling::owned_sample::{OwnedSample, SampleTypeIndices, SampleType}; +//! use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType}; //! use std::sync::Arc; //! -//! let indices = Arc::new(SampleTypeIndices::new(vec![ +//! let metadata = Arc::new(Metadata::new(vec![ //! SampleType::Cpu, //! SampleType::Wall, -//! ]).unwrap()); +//! ], 64, true).unwrap()); //! -//! let mut sample = OwnedSample::new(indices); +//! let mut sample = OwnedSample::new(metadata); //! //! // Set values by type //! sample.set_value(SampleType::Cpu, 1000).unwrap(); @@ -47,18 +47,19 @@ //! ``` use bumpalo::Bump; -use enum_map::{Enum, EnumMap}; +use enum_map::Enum; use std::num::NonZeroI64; use std::sync::Arc; -use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; use anyhow::{self, Context}; use crate::api::{Function, Label, Location, Mapping, Sample}; +mod metadata; mod pool; #[cfg(test)] mod tests; +pub use metadata::Metadata; pub use pool::SamplePool; /// Well-known label keys used in profiling. @@ -121,56 +122,6 @@ impl std::fmt::Display for LabelKey { } } -/// Global flag to enable/disable timeline for all samples. -/// When disabled, time-setting methods become no-ops. -static TIMELINE_ENABLED: AtomicBool = AtomicBool::new(true); - -/// Computes the offset between monotonic time (CLOCK_MONOTONIC) and epoch time. -/// This is computed once and cached in an atomic. -/// -/// The offset allows converting monotonic timestamps (which start at system boot) -/// to epoch timestamps (which start at 1970-01-01). -/// -/// # Errors -/// -/// Returns an error if: -/// - System time is before UNIX_EPOCH -/// - `clock_gettime(CLOCK_MONOTONIC)` fails -#[cfg(unix)] -fn monotonic_to_epoch_offset() -> anyhow::Result { - static OFFSET: AtomicI64 = AtomicI64::new(0); - - // Fast path: offset already computed - let offset = OFFSET.load(Ordering::Relaxed); - if offset != 0 { - return Ok(offset); - } - - // Slow path: compute the 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 difference (epoch_ns will be larger since we're after 1970) - let computed_offset = epoch_ns - monotonic_ns; - - // Store it atomically (if another thread raced and stored it, that's fine) - OFFSET.store(computed_offset, Ordering::Relaxed); - - Ok(computed_offset) -} - - /// Types of profiling samples that can be collected. /// /// Based on the sample types from [dd-trace-py](https://github.com/DataDog/dd-trace-py/blob/d239f91be2c4ca1ec2ded88263ed132e28fe031b/ddtrace/internal/datadog/profiling/dd_wrapper/include/types.hpp#L4). @@ -198,98 +149,6 @@ pub enum SampleType { GpuFlops, } -/// 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::{SampleTypeIndices, SampleType}; -/// let indices = SampleTypeIndices::new(vec![ -/// SampleType::Cpu, -/// SampleType::Wall, -/// SampleType::Allocation, -/// ]).unwrap(); -/// -/// assert_eq!(indices.get_index(&SampleType::Cpu), Some(0)); -/// assert_eq!(indices.get_index(&SampleType::Wall), Some(1)); -/// assert_eq!(indices.get_index(&SampleType::Allocation), Some(2)); -/// assert_eq!(indices.get_index(&SampleType::Heap), None); -/// assert_eq!(indices.len(), 3); -/// ``` -#[derive(Clone, Debug)] -pub struct SampleTypeIndices { - /// 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>, -} - -impl SampleTypeIndices { - /// Creates a new SampleTypeIndices with the given sample types. - /// - /// The order of sample types in the vector determines their index in the values array. - /// - /// # Errors - /// - /// Returns an error if: - /// - The sample types vector is empty - /// - The same sample type appears more than once - pub fn new(sample_types: Vec) -> 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); - } - - Ok(Self { - sample_types, - type_to_index, - }) - } - - /// 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 - } -} - /// 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] @@ -316,7 +175,7 @@ struct SampleInner { pub struct OwnedSample { inner: SampleInner, values: Vec, - indices: Arc, + metadata: Arc, endtime_ns: Option, reverse_locations: bool, } @@ -329,7 +188,7 @@ impl OwnedSample { /// /// # Example /// ```no_run - /// # use libdd_profiling::owned_sample::{OwnedSample, SampleTypeIndices, SampleType}; + /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType}; /// # use std::sync::Arc; /// let indices = Arc::new(SampleTypeIndices::new(vec![ /// SampleType::Cpu, @@ -337,8 +196,8 @@ impl OwnedSample { /// ]).unwrap()); /// let sample = OwnedSample::new(indices); /// ``` - pub fn new(indices: Arc) -> Self { - let num_values = indices.len(); + pub fn new(metadata: Arc) -> Self { + let num_values = metadata.len(); Self { inner: SampleInnerBuilder { arena: Bump::new(), @@ -346,7 +205,7 @@ impl OwnedSample { labels_builder: |_| Vec::new(), }.build(), values: vec![0; num_values], - indices, + metadata, endtime_ns: None, reverse_locations: false, } @@ -360,14 +219,14 @@ impl OwnedSample { /// /// # Example /// ```no_run - /// # use libdd_profiling::owned_sample::{OwnedSample, SampleTypeIndices, SampleType}; + /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType}; /// # use std::sync::Arc; - /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + /// # let indices = Arc::new(Metadata::new(vec![SampleType::Cpu], 64).unwrap()); /// let mut sample = OwnedSample::new(indices); /// sample.set_value(SampleType::Cpu, 1000).unwrap(); /// ``` pub fn set_value(&mut self, sample_type: SampleType, value: i64) -> anyhow::Result<()> { - let index = self.indices.get_index(&sample_type) + let index = self.metadata.get_index(&sample_type) .with_context(|| format!("sample type {:?} not configured", sample_type))?; self.values[index] = value; @@ -381,26 +240,14 @@ impl OwnedSample { /// /// Returns an error if the sample type is not configured. pub fn get_value(&self, sample_type: SampleType) -> anyhow::Result { - let index = self.indices.get_index(&sample_type) + 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 type indices. - pub fn indices(&self) -> &Arc { - &self.indices - } - - /// Returns whether timeline is enabled globally for all samples. - pub fn is_timeline_enabled() -> bool { - TIMELINE_ENABLED.load(Ordering::Relaxed) - } - - /// Sets whether timeline is enabled globally for all samples. - /// - /// When timeline is disabled, time-setting methods become no-ops. - pub fn set_timeline_enabled(enabled: bool) { - TIMELINE_ENABLED.store(enabled, Ordering::Relaxed); + /// 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. @@ -422,7 +269,7 @@ impl OwnedSample { /// 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::is_timeline_enabled() { + if self.metadata.is_timeline_enabled() { self.endtime_ns = NonZeroI64::new(endtime_ns); } endtime_ns @@ -492,7 +339,7 @@ impl OwnedSample { /// - `clock_gettime(CLOCK_MONOTONIC)` fails #[cfg(unix)] pub fn set_endtime_from_monotonic_ns(&mut self, monotonic_ns: i64) -> anyhow::Result { - let offset = monotonic_to_epoch_offset()?; + let offset = self.metadata.monotonic_to_epoch_offset(); let endtime = monotonic_ns + offset; Ok(self.set_endtime_ns(endtime)) } @@ -576,10 +423,10 @@ impl OwnedSample { /// /// # Example /// ```no_run - /// # use libdd_profiling::owned_sample::{OwnedSample, SampleTypeIndices, SampleType, LabelKey}; + /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType, LabelKey}; /// # use std::sync::Arc; - /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - /// # let mut sample = OwnedSample::new(indices); + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + /// # let mut sample = OwnedSample::new(metadata); /// sample.add_string_label(LabelKey::ThreadName, "worker-1"); /// sample.add_string_label(LabelKey::ExceptionType, "ValueError"); /// ``` @@ -598,10 +445,10 @@ impl OwnedSample { /// /// # Example /// ```no_run - /// # use libdd_profiling::owned_sample::{OwnedSample, SampleTypeIndices, SampleType, LabelKey}; + /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType, LabelKey}; /// # use std::sync::Arc; - /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - /// # let mut sample = OwnedSample::new(indices); + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + /// # let mut sample = OwnedSample::new(metadata); /// sample.add_num_label(LabelKey::ThreadId, 42); /// sample.add_num_label(LabelKey::SpanId, 12345); /// ``` @@ -629,11 +476,11 @@ impl OwnedSample { /// /// # Example /// ```no_run - /// # use libdd_profiling::owned_sample::{OwnedSample, SampleTypeIndices, SampleType}; + /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType}; /// # use libdd_profiling::api::{Location, Mapping, Function, Label}; /// # use std::sync::Arc; - /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu, SampleType::Wall]).unwrap()); - /// let mut sample = OwnedSample::new(indices); + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu, SampleType::Wall], 64, 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: "" }, @@ -701,9 +548,9 @@ impl OwnedSample { /// /// # Example /// ```no_run - /// # use libdd_profiling::owned_sample::{OwnedSample, SampleTypeIndices, SampleType}; + /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType}; /// # use std::sync::Arc; - /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); + /// # let indices = Arc::new(Metadata::new(vec![SampleType::Cpu], 64).unwrap()); /// let sample = OwnedSample::new(indices); /// let borrowed = sample.as_sample(); /// ``` @@ -736,7 +583,7 @@ impl OwnedSample { impl std::fmt::Debug for OwnedSample { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("OwnedSample") - .field("sample_types", &self.indices.types()) + .field("sample_types", &self.metadata.types()) .field("num_locations", &self.num_locations()) .field("num_labels", &self.num_labels()) .field("values", &self.values()) @@ -746,8 +593,8 @@ impl std::fmt::Debug for OwnedSample { impl PartialEq for OwnedSample { fn eq(&self, other: &Self) -> bool { - // Compare indices configuration (pointer equality is fine since they're Arc) - Arc::ptr_eq(&self.indices, &other.indices) + // Compare metadata configuration (pointer equality is fine since they're Arc) + Arc::ptr_eq(&self.metadata, &other.metadata) && self.values() == other.values() && self.num_locations() == other.num_locations() && self.num_labels() == other.num_labels() diff --git a/libdd-profiling/src/owned_sample/pool.rs b/libdd-profiling/src/owned_sample/pool.rs index bc3d1a5c33..fcf6540197 100644 --- a/libdd-profiling/src/owned_sample/pool.rs +++ b/libdd-profiling/src/owned_sample/pool.rs @@ -3,7 +3,7 @@ //! Pool for reusing `OwnedSample` instances to reduce allocation overhead. -use super::{OwnedSample, SampleTypeIndices}; +use super::{Metadata, OwnedSample}; use crossbeam_queue::ArrayQueue; use std::sync::Arc; @@ -19,14 +19,14 @@ use std::sync::Arc; /// /// # Example /// ```no_run -/// # use libdd_profiling::owned_sample::{SamplePool, SampleTypeIndices, SampleType}; +/// # use libdd_profiling::owned_sample::{SamplePool, Metadata, SampleType}; /// # use std::sync::Arc; -/// let indices = Arc::new(SampleTypeIndices::new(vec![ +/// let metadata = Arc::new(Metadata::new(vec![ /// SampleType::Cpu, /// SampleType::Wall, -/// ]).unwrap()); +/// ], 64, true).unwrap()); /// -/// let pool = SamplePool::new(indices, 10); +/// let pool = SamplePool::new(metadata, 10); /// /// // Get a sample from the pool (thread-safe) /// let mut sample = pool.get(); @@ -37,8 +37,8 @@ use std::sync::Arc; /// pool.put(sample); /// ``` pub struct SamplePool { - /// The sample type indices configuration shared by all samples - indices: Arc, + /// 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. @@ -54,14 +54,14 @@ impl SamplePool { /// /// # Example /// ```no_run - /// # use libdd_profiling::owned_sample::{SamplePool, SampleTypeIndices, SampleType}; + /// # use libdd_profiling::owned_sample::{SamplePool, Metadata, SampleType}; /// # use std::sync::Arc; - /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - /// let pool = SamplePool::new(indices, 100); + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + /// let pool = SamplePool::new(metadata, 100); /// ``` - pub fn new(indices: Arc, capacity: usize) -> Self { + pub fn new(metadata: Arc, capacity: usize) -> Self { Self { - indices, + metadata, samples: ArrayQueue::new(capacity), } } @@ -74,16 +74,16 @@ impl SamplePool { /// /// # Example /// ```no_run - /// # use libdd_profiling::owned_sample::{SamplePool, SampleTypeIndices, SampleType}; + /// # use libdd_profiling::owned_sample::{SamplePool, Metadata, SampleType}; /// # use std::sync::Arc; - /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - /// # let pool = SamplePool::new(indices, 10); + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + /// # let pool = SamplePool::new(metadata, 10); /// let sample = pool.get(); /// assert_eq!(sample.num_locations(), 0); /// ``` pub fn get(&self) -> Box { self.samples.pop().unwrap_or_else(|| { - Box::new(OwnedSample::new(self.indices.clone())) + Box::new(OwnedSample::new(self.metadata.clone())) }) } @@ -96,17 +96,17 @@ impl SamplePool { /// /// # Example /// ```no_run - /// # use libdd_profiling::owned_sample::{SamplePool, SampleTypeIndices, SampleType}; + /// # use libdd_profiling::owned_sample::{SamplePool, Metadata, SampleType}; /// # use std::sync::Arc; - /// # let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - /// # let pool = SamplePool::new(indices, 10); + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + /// # let pool = SamplePool::new(metadata, 10); /// let mut sample = pool.get(); /// sample.set_value(SampleType::Cpu, 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(); + sample.reset(); // Try to add back to pool (lock-free operation) // If full, push() returns Err(sample), which we just drop @@ -129,7 +129,7 @@ impl SamplePool { } } -// SAFETY: SamplePool uses ArrayQueue which is Send + Sync, and Arc which is also Send + Sync +// 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 {} @@ -140,8 +140,8 @@ mod tests { #[test] fn test_pool_basic() { - let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - let pool = SamplePool::new(indices, 5); + let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let pool = SamplePool::new(metadata, 5); assert_eq!(pool.len(), 0); assert!(pool.is_empty()); @@ -166,8 +166,8 @@ mod tests { #[test] fn test_pool_capacity_limit() { - let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - let pool = SamplePool::new(indices, 2); + let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let pool = SamplePool::new(metadata, 2); // Fill the pool let sample1 = pool.get(); @@ -185,11 +185,11 @@ mod tests { #[test] fn test_pool_reset() { - let indices = Arc::new(SampleTypeIndices::new(vec![ + let metadata = Arc::new(Metadata::new(vec![ SampleType::Cpu, SampleType::Wall, - ]).unwrap()); - let pool = SamplePool::new(indices, 5); + ], 64, true).unwrap()); + let pool = SamplePool::new(metadata, 5); // Get a sample and modify it let mut sample = pool.get(); @@ -211,11 +211,11 @@ mod tests { fn test_pool_thread_safety() { use std::thread; - let indices = Arc::new(SampleTypeIndices::new(vec![ + let metadata = Arc::new(Metadata::new(vec![ SampleType::Cpu, SampleType::Wall, - ]).unwrap()); - let pool = Arc::new(SamplePool::new(indices, 20)); + ], 64, true).unwrap()); + let pool = Arc::new(SamplePool::new(metadata, 20)); // Spawn multiple threads that all use the pool concurrently let handles: Vec<_> = (0..4) diff --git a/libdd-profiling/src/owned_sample/tests.rs b/libdd-profiling/src/owned_sample/tests.rs index c582488136..2621eaf514 100644 --- a/libdd-profiling/src/owned_sample/tests.rs +++ b/libdd-profiling/src/owned_sample/tests.rs @@ -6,11 +6,11 @@ use crate::api::{Function, Label, Location, Mapping}; #[test] fn test_owned_sample_basic() { - let indices = Arc::new(SampleTypeIndices::new(vec![ + let metadata = Arc::new(Metadata::new(vec![ SampleType::Cpu, SampleType::Wall, - ]).unwrap()); - let mut sample = OwnedSample::new(indices.clone()); + ], 64, true).unwrap()); + let mut sample = OwnedSample::new(metadata.clone()); sample.set_value(SampleType::Cpu, 100).unwrap(); sample.set_value(SampleType::Wall, 200).unwrap(); @@ -53,11 +53,11 @@ fn test_owned_sample_basic() { #[test] fn test_as_sample() { - let indices = Arc::new(SampleTypeIndices::new(vec![ + let metadata = Arc::new(Metadata::new(vec![ SampleType::Cpu, SampleType::Wall, - ]).unwrap()); - let mut owned = OwnedSample::new(indices.clone()); + ], 64, true).unwrap()); + let mut owned = OwnedSample::new(metadata.clone()); owned.set_value(SampleType::Cpu, 100).unwrap(); owned.set_value(SampleType::Wall, 200).unwrap(); owned.add_location(Location { @@ -88,8 +88,8 @@ fn test_as_sample() { #[test] fn test_set_value_error() { - let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - let mut sample = OwnedSample::new(indices); + let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let mut sample = OwnedSample::new(metadata); // Should work for configured type assert!(sample.set_value(SampleType::Cpu, 100).is_ok()); @@ -102,34 +102,34 @@ fn test_set_value_error() { #[test] fn test_sample_type_indices_basic() { - let indices = SampleTypeIndices::new(vec![ + let metadata = Metadata::new(vec![ SampleType::Cpu, SampleType::Wall, SampleType::Allocation, - ]).unwrap(); + ], 64, true).unwrap(); - assert_eq!(indices.len(), 3); - assert!(!indices.is_empty()); + assert_eq!(metadata.len(), 3); + assert!(!metadata.is_empty()); - assert_eq!(indices.get_index(&SampleType::Cpu), Some(0)); - assert_eq!(indices.get_index(&SampleType::Wall), Some(1)); - assert_eq!(indices.get_index(&SampleType::Allocation), Some(2)); - assert_eq!(indices.get_index(&SampleType::Heap), None); + assert_eq!(metadata.get_index(&SampleType::Cpu), Some(0)); + assert_eq!(metadata.get_index(&SampleType::Wall), Some(1)); + assert_eq!(metadata.get_index(&SampleType::Allocation), Some(2)); + assert_eq!(metadata.get_index(&SampleType::Heap), None); - assert_eq!(indices.get_type(0), Some(SampleType::Cpu)); - assert_eq!(indices.get_type(1), Some(SampleType::Wall)); - assert_eq!(indices.get_type(2), Some(SampleType::Allocation)); - assert_eq!(indices.get_type(3), None); + assert_eq!(metadata.get_type(0), Some(SampleType::Cpu)); + assert_eq!(metadata.get_type(1), Some(SampleType::Wall)); + assert_eq!(metadata.get_type(2), Some(SampleType::Allocation)); + assert_eq!(metadata.get_type(3), None); } #[test] fn test_sample_type_indices_duplicate_error() { - let result = SampleTypeIndices::new(vec![ + let result = Metadata::new(vec![ SampleType::Cpu, SampleType::Wall, SampleType::Cpu, // Duplicate SampleType::Allocation, - ]); + ], 64, true); assert!(result.is_err()); let err = result.unwrap_err(); @@ -138,7 +138,7 @@ fn test_sample_type_indices_duplicate_error() { #[test] fn test_sample_type_indices_empty_error() { - let result = SampleTypeIndices::new(vec![]); + let result = Metadata::new(vec![], 64, true); assert!(result.is_err()); let err = result.unwrap_err(); assert!(err.to_string().contains("empty")); @@ -146,13 +146,13 @@ fn test_sample_type_indices_empty_error() { #[test] fn test_sample_type_indices_iter() { - let indices = SampleTypeIndices::new(vec![ + let metadata = Metadata::new(vec![ SampleType::Cpu, SampleType::Wall, SampleType::Allocation, - ]).unwrap(); + ], 64, true).unwrap(); - let types: Vec<_> = indices.iter().copied().collect(); + let types: Vec<_> = metadata.iter().copied().collect(); assert_eq!(types, vec![ SampleType::Cpu, SampleType::Wall, @@ -162,12 +162,12 @@ fn test_sample_type_indices_iter() { #[test] fn test_reset() { - let indices = Arc::new(SampleTypeIndices::new(vec![ + let metadata = Arc::new(Metadata::new(vec![ SampleType::Cpu, SampleType::Wall, SampleType::Allocation, - ]).unwrap()); - let mut sample = OwnedSample::new(indices); + ], 64, true).unwrap()); + let mut sample = OwnedSample::new(metadata); sample.set_value(SampleType::Cpu, 100).unwrap(); sample.set_value(SampleType::Wall, 200).unwrap(); sample.set_value(SampleType::Allocation, 300).unwrap(); @@ -229,8 +229,8 @@ fn test_reset() { #[test] fn test_add_multiple() { - let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - let mut sample = OwnedSample::new(indices); + let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let mut sample = OwnedSample::new(metadata); // Add multiple locations at once let locations = &[ @@ -280,8 +280,8 @@ fn test_add_multiple() { fn test_endtime_ns() { use std::num::NonZeroI64; - let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - let mut sample = OwnedSample::new(indices); + let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let mut sample = OwnedSample::new(metadata); // Initially, endtime_ns should be None assert_eq!(sample.endtime_ns(), None); @@ -307,8 +307,8 @@ fn test_endtime_ns() { fn test_set_endtime_ns_now() { use std::time::SystemTime; - let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - let mut sample = OwnedSample::new(indices); + let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let mut sample = OwnedSample::new(metadata); // Initially, endtime_ns should be None assert_eq!(sample.endtime_ns(), None); @@ -357,48 +357,40 @@ fn test_set_endtime_ns_now() { #[test] fn test_timeline_enabled() { - let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - let mut sample = OwnedSample::new(indices); - - // Timeline should be enabled by default - assert!(OwnedSample::is_timeline_enabled()); + // Test with timeline enabled + let metadata_enabled = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, 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.set_endtime_ns(123456789); - assert_eq!(sample.endtime_ns().unwrap().get(), 123456789); + sample_enabled.set_endtime_ns(123456789); + assert_eq!(sample_enabled.endtime_ns().unwrap().get(), 123456789); - // Disable timeline - OwnedSample::set_timeline_enabled(false); - assert!(!OwnedSample::is_timeline_enabled()); + // 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::Cpu], 64, 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.set_endtime_ns(987654321); - assert_eq!(sample.endtime_ns().unwrap().get(), 123456789); // unchanged + 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.set_endtime_ns_now().unwrap(); + let returned_time = sample_disabled.set_endtime_ns_now().unwrap(); assert_ne!(returned_time, 0); // still returns the calculated timestamp - assert_eq!(sample.endtime_ns().unwrap().get(), 123456789); // but doesn't set it (unchanged) - - // Re-enable timeline - OwnedSample::set_timeline_enabled(true); - assert!(OwnedSample::is_timeline_enabled()); - - // Now set_endtime_ns should work again - sample.set_endtime_ns(999888777); - assert_eq!(sample.endtime_ns().unwrap().get(), 999888777); - - // set_endtime_ns_now should return the timestamp it sets when enabled - let returned_time = sample.set_endtime_ns_now().unwrap(); - assert_ne!(returned_time, 0); // should not be 0 when timeline is enabled - assert_eq!(sample.endtime_ns().unwrap().get(), returned_time); // should match + assert_eq!(sample_disabled.endtime_ns(), None); // but doesn't set it } #[test] #[cfg(unix)] fn test_set_endtime_from_monotonic_ns() { - let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - let mut sample = OwnedSample::new(indices); + let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let mut sample = OwnedSample::new(metadata); // Set endtime from a monotonic time let monotonic_ns = 123456789000; // Some monotonic time @@ -437,8 +429,8 @@ fn test_set_endtime_from_monotonic_ns() { #[test] fn test_reverse_locations() { - let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - let mut sample = OwnedSample::new(indices); + let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let mut sample = OwnedSample::new(metadata); // Initially, reverse_locations should be false assert!(!sample.is_reverse_locations()); @@ -521,8 +513,8 @@ fn test_label_key() { assert_eq!(format!("{}", LabelKey::ThreadName), "thread name"); // Test that it can be used as a label key - let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - let mut sample = OwnedSample::new(indices); + let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let mut sample = OwnedSample::new(metadata); sample.add_label(Label { key: LabelKey::ThreadId.as_str(), @@ -543,8 +535,8 @@ fn test_label_key() { #[test] fn test_add_string_label() { - let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - let mut sample = OwnedSample::new(indices); + let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let mut sample = OwnedSample::new(metadata); // Add string labels using the convenience method sample.add_string_label(LabelKey::ThreadName, "worker-1"); @@ -570,8 +562,8 @@ fn test_add_string_label() { #[test] fn test_add_num_label() { - let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - let mut sample = OwnedSample::new(indices); + let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let mut sample = OwnedSample::new(metadata); // Add numeric labels using the convenience method sample.add_num_label(LabelKey::ThreadId, 42); @@ -600,8 +592,8 @@ fn test_add_num_label() { #[test] fn test_mixed_label_types() { - let indices = Arc::new(SampleTypeIndices::new(vec![SampleType::Cpu]).unwrap()); - let mut sample = OwnedSample::new(indices); + let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let mut sample = OwnedSample::new(metadata); // Mix string and numeric labels sample.add_string_label(LabelKey::ThreadName, "worker-1"); diff --git a/profile_owned_sample.pprof b/profile_owned_sample.pprof deleted file mode 100644 index d589b8501be1b7b32d860f462ded86d13a376662..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1725 zcmV;u215BLwJ-euNToFZnqpiPK~O4H2eOQmU4rHYL}CmnkZpp*I5{eV#CRBs16adh zL?W>F^S`UI`R%c;s)`p@NgV@?)FMJbeUd26koT$2JUCzHo1YcxEg==C#ame}g!uEB}m!ujaWgkbzlc z`7u|8NwD(cT+1ByzdPtEKc>yNVPY1p6*oLIM3!IMaOSvoaD!UD^5Z?r9QPX1EDV#C zALrZ8Pgj0St7VS+r$l74%yADP2gzjR*Bob#dtQ(ZSpJK*);#wEev!iRCDqNaffNf@?+a9bKLX5sBP;Mgb=L!7u#x@=iUj(r%Pv!`(DsdSbn@~ndAP5E^VEr zoo}6H)V6gB;z(J3%ys6t4}mUio!&a%I<;+_4x@+0%Mz-=}n~0&6F9uM`@?V^5n&*BAUD}$ER{F(EL=*-i z%|s<-IWFYSYt5ZrSIH~={F_piAKTI! z2HFA3U!&p7aql9%l(td~_h=1=?Nq&prMox|EI;1F;T4vI^akCGofy6W z+hC>*Dl(~C8k5Kd=-Goqbg1-0WHR*Jy zf?nI3$D~Vu<~nU}UXu=oI`!G!ye1urBItT=9+U0>YM|%6c?EQje#d+B2)mbJHZ6L)ZTH=WzPH}pwq?_zx9{C|KzeQK-EG@KEqZ%x z_nn4L+j@7~c1?@kPTPIA?6a+Rw`~cv=>f z{1@A_=D9C1EYt3SOR)B@O*>)>SN@ChO!M4#xM5)&um?85(myO>B=QhUmNC7Ows3;9 z0v~qFy52gf!_Q z$)>T#)v5~@ELc!gwaVwK^7)WRCRdxy=96_pj2N}@l~kel9_xS zmsCB9Po|Urwb?vAv8xifG)nc3th3kEDku!TsNw+?s`zxSR6ka>=ya|}Cfg*SHk-$% zWzsm+JF?DR0Ckx(PHDB-JU+3j50+`wJF?E+0JW;gWGaj7>Vsuk`Fv5ut~QxWWszNd zuuQ9)YC-iKc^GLb)A_V2n^Q(0E03hI$0Moiu}-QUlZs3hS7md`=(MTJwQ8-vu0B|% z)lFTej7uHTxUy2un~F^8kj9mjdfHUqRiUnmOcs&Gm6dwhR3~!DJg!dW(zv{->s0bt zL8&xa*;MDU*qkyxo$FNcSwt?^u0EGZW)rzwb?ec{J_Q7T_oy*!1x7M4Vv>TChBZ*5 zNLw8dAYg(7SYRe9GU`=-RoSLe^%e3+`Rh$QfH1T)=P^+__P-DXm2e%y$ZiNt30VS> zDc?sNsys(3xCO^p?**hnCIbzm$oIV^RHo(9u+s{wKX(u%2cBzZJqtfro4rC(_qET~ z-Q^^RbRCvdukV|X9mZ3bX=M zy2A#lpZ!G;v@hT15-#RJQ00QVnqECY{!1Ty_|CO0(~Gnzr#%X$fW)(7YrI|z1|wTw9Ha1nvD=Zr#$>q*CBFYbNU>8V TsLN}kY$_fPd)@`55zwIqTjF@0 From 6fbaee48d1ea85f9edab858ff3da3c9797e87c22 Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbone Date: Sat, 6 Dec 2025 22:54:32 -0500 Subject: [PATCH 09/12] max frames --- examples/cxx/owned_sample.cpp | 2 +- libdd-profiling/src/cxx.rs | 14 +- libdd-profiling/src/owned_sample/mod.rs | 60 ++++++-- libdd-profiling/src/owned_sample/tests.rs | 163 ++++++++++++++++++++-- 4 files changed, 213 insertions(+), 26 deletions(-) diff --git a/examples/cxx/owned_sample.cpp b/examples/cxx/owned_sample.cpp index c991fd34d7..efbeb88dcf 100644 --- a/examples/cxx/owned_sample.cpp +++ b/examples/cxx/owned_sample.cpp @@ -168,7 +168,7 @@ int main() { }); // Add OwnedSample directly to profile - profile->add_owned_sample(*owned_sample); + owned_sample->add_to_profile(*profile); // Return sample to pool for reuse (automatically resets it) pool->return_sample(std::move(owned_sample)); diff --git a/libdd-profiling/src/cxx.rs b/libdd-profiling/src/cxx.rs index 74bf6261e2..8b616563c0 100644 --- a/libdd-profiling/src/cxx.rs +++ b/libdd-profiling/src/cxx.rs @@ -108,7 +108,6 @@ pub mod ffi { // Profile methods fn add_sample(self: &mut Profile, sample: &Sample) -> Result<()>; - fn add_owned_sample(self: &mut Profile, sample: &OwnedSample) -> 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<()>; @@ -168,6 +167,7 @@ pub mod ffi { fn num_locations(self: &OwnedSample) -> usize; fn num_labels(self: &OwnedSample) -> usize; fn reset_sample(self: &mut OwnedSample); + fn add_to_profile(self: &OwnedSample, profile: &mut Profile) -> Result<()>; // SamplePool methods #[Self = "SamplePool"] @@ -278,14 +278,6 @@ impl Profile { Ok(()) } - pub fn add_owned_sample(&mut self, sample: &OwnedSample) -> anyhow::Result<()> { - // Convert OwnedSample to API Sample - let api_sample = sample.inner.as_sample(); - - // 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 @@ -464,6 +456,10 @@ impl OwnedSample { 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) + } } // ============================================================================ diff --git a/libdd-profiling/src/owned_sample/mod.rs b/libdd-profiling/src/owned_sample/mod.rs index 4dba522f65..22f9b6f303 100644 --- a/libdd-profiling/src/owned_sample/mod.rs +++ b/libdd-profiling/src/owned_sample/mod.rs @@ -178,6 +178,7 @@ pub struct OwnedSample { metadata: Arc, endtime_ns: Option, reverse_locations: bool, + dropped_frames: usize, } impl OwnedSample { @@ -208,6 +209,7 @@ impl OwnedSample { metadata, endtime_ns: None, reverse_locations: false, + dropped_frames: 0, } } @@ -347,7 +349,17 @@ impl OwnedSample { /// 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`, 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; + } + self.inner.with_mut(|fields| { // Allocate strings in the arena let filename_ref = fields.arena.alloc_str(location.mapping.filename); @@ -514,6 +526,7 @@ impl OwnedSample { self.endtime_ns = None; self.reverse_locations = false; + self.dropped_frames = 0; // Rebuild with the reset arena self.inner = SampleInnerBuilder { @@ -533,6 +546,11 @@ impl OwnedSample { self.inner.borrow_labels().len() } + /// Get the number of frames that were dropped due to exceeding max_frames. + pub fn dropped_frames(&self) -> usize { + self.dropped_frames + } + /// Get a location by index. pub fn get_location(&self, index: usize) -> Option> { self.inner.borrow_locations().get(index).copied() @@ -543,18 +561,23 @@ impl OwnedSample { self.inner.borrow_labels().get(index).copied() } - /// Get a borrowed `Sample` view of this owned sample. - /// The returned sample borrows from this OwnedSample. + /// 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 indices = Arc::new(Metadata::new(vec![SampleType::Cpu], 64).unwrap()); - /// let sample = OwnedSample::new(indices); - /// let borrowed = sample.as_sample(); + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + /// # let mut profile = Profile::try_new(vec![], None).unwrap(); + /// let sample = OwnedSample::new(metadata); + /// sample.add_to_profile(&mut profile).unwrap(); /// ``` - pub fn as_sample(&self) -> Sample<'_> { + 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 @@ -562,11 +585,32 @@ impl OwnedSample { locations.reverse(); } - Sample { + // 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) } /// Iterate over all locations. diff --git a/libdd-profiling/src/owned_sample/tests.rs b/libdd-profiling/src/owned_sample/tests.rs index 2621eaf514..fd25cc8044 100644 --- a/libdd-profiling/src/owned_sample/tests.rs +++ b/libdd-profiling/src/owned_sample/tests.rs @@ -2,7 +2,46 @@ // SPDX-License-Identifier: Apache-2.0 use super::*; -use crate::api::{Function, Label, Location, Mapping}; +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() { @@ -78,7 +117,7 @@ fn test_as_sample() { }); owned.add_label(Label { key: "key", str: "value", num: 0, num_unit: "" }); - let borrowed = owned.as_sample(); + let borrowed = as_sample(&owned); assert_eq!(borrowed.values, &[100, 200]); assert_eq!(borrowed.locations.len(), 1); assert_eq!(borrowed.labels.len(), 1); @@ -456,7 +495,7 @@ fn test_reverse_locations() { }); // Get sample with normal order - let normal_sample = sample.as_sample(); + 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"); @@ -467,7 +506,7 @@ fn test_reverse_locations() { assert!(sample.is_reverse_locations()); // Get sample with reversed order - let reversed_sample = sample.as_sample(); + 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"); @@ -478,7 +517,7 @@ fn test_reverse_locations() { assert!(!sample.is_reverse_locations()); // Should be back to normal order - let normal_again = sample.as_sample(); + 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"); @@ -546,7 +585,7 @@ fn test_add_string_label() { assert_eq!(sample.num_labels(), 3); // Verify the labels were added correctly - let api_sample = sample.as_sample(); + let api_sample = as_sample(&sample); assert_eq!(api_sample.labels.len(), 3); // Check first label @@ -573,7 +612,7 @@ fn test_add_num_label() { assert_eq!(sample.num_labels(), 3); // Verify the labels were added correctly - let api_sample = sample.as_sample(); + let api_sample = as_sample(&sample); assert_eq!(api_sample.labels.len(), 3); // Check first label @@ -603,7 +642,7 @@ fn test_mixed_label_types() { assert_eq!(sample.num_labels(), 4); - let api_sample = sample.as_sample(); + 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"); @@ -613,3 +652,111 @@ fn test_mixed_label_types() { 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::Cpu], 3, 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.num_locations(), 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.num_locations(), 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>"); +} From 4160a5380e08f8260760f6126fec432ab4114f55 Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbone Date: Sat, 6 Dec 2025 23:20:20 -0500 Subject: [PATCH 10/12] sampleTypes --- examples/cxx/owned_sample.cpp | 4 +- libdd-profiling/src/cxx.rs | 76 +++++++---- libdd-profiling/src/owned_sample/label_key.rs | 63 ++++++++++ libdd-profiling/src/owned_sample/metadata.rs | 8 +- libdd-profiling/src/owned_sample/mod.rs | 112 +++-------------- libdd-profiling/src/owned_sample/pool.rs | 38 +++--- .../src/owned_sample/sample_type.rs | 66 ++++++++++ libdd-profiling/src/owned_sample/tests.rs | 118 +++++++++--------- 8 files changed, 277 insertions(+), 208 deletions(-) create mode 100644 libdd-profiling/src/owned_sample/label_key.rs create mode 100644 libdd-profiling/src/owned_sample/sample_type.rs diff --git a/examples/cxx/owned_sample.cpp b/examples/cxx/owned_sample.cpp index efbeb88dcf..8086884dd7 100644 --- a/examples/cxx/owned_sample.cpp +++ b/examples/cxx/owned_sample.cpp @@ -58,7 +58,7 @@ int main() { // Create metadata (configuration shared by all samples in the pool) // Parameters: sample_types, max_frames, timeline_enabled - auto metadata = Metadata::create({SampleType::Wall}, 64, true); + auto metadata = Metadata::create({SampleType::WallTime}, 64, true); // Create a pool of reusable samples using the metadata // Parameters: metadata, capacity @@ -72,7 +72,7 @@ int main() { // Set the wall time value auto wall_time_value = 1000000 + (i % 1000) * 1000; - owned_sample->set_value(SampleType::Wall, wall_time_value); + 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 diff --git a/libdd-profiling/src/cxx.rs b/libdd-profiling/src/cxx.rs index 8b616563c0..e1739b0dac 100644 --- a/libdd-profiling/src/cxx.rs +++ b/libdd-profiling/src/cxx.rs @@ -63,16 +63,24 @@ pub mod ffi { #[derive(Debug)] #[repr(u32)] enum SampleType { - Cpu = 0, - Wall = 1, - Exception = 2, - LockAcquire = 3, - LockRelease = 4, - Allocation = 5, - Heap = 6, - GpuTime = 7, - GpuMemory = 8, - GpuFlops = 9, + 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)] @@ -500,16 +508,24 @@ impl SamplePool { // 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::Cpu => Ok(owned_sample::SampleType::Cpu), - ffi::SampleType::Wall => Ok(owned_sample::SampleType::Wall), - ffi::SampleType::Exception => Ok(owned_sample::SampleType::Exception), - ffi::SampleType::LockAcquire => Ok(owned_sample::SampleType::LockAcquire), - ffi::SampleType::LockRelease => Ok(owned_sample::SampleType::LockRelease), - ffi::SampleType::Allocation => Ok(owned_sample::SampleType::Allocation), - ffi::SampleType::Heap => Ok(owned_sample::SampleType::Heap), + 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::GpuMemory => Ok(owned_sample::SampleType::GpuMemory), + 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), } } @@ -540,15 +556,23 @@ mod tests { 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::Cpu).unwrap() as usize, owned_sample::SampleType::Cpu as usize); - assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::Wall).unwrap() as usize, owned_sample::SampleType::Wall as usize); - assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::Exception).unwrap() as usize, owned_sample::SampleType::Exception as usize); - assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::LockAcquire).unwrap() as usize, owned_sample::SampleType::LockAcquire as usize); - assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::LockRelease).unwrap() as usize, owned_sample::SampleType::LockRelease as usize); - assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::Allocation).unwrap() as usize, owned_sample::SampleType::Allocation as usize); - assert_eq!(ffi_sample_type_to_owned(ffi::SampleType::Heap).unwrap() as usize, owned_sample::SampleType::Heap as usize); + 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::GpuMemory).unwrap() as usize, owned_sample::SampleType::GpuMemory 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/owned_sample/label_key.rs b/libdd-profiling/src/owned_sample/label_key.rs new file mode 100644 index 0000000000..1c5b404b16 --- /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 index 69220e45d4..81d7a966aa 100644 --- a/libdd-profiling/src/owned_sample/metadata.rs +++ b/libdd-profiling/src/owned_sample/metadata.rs @@ -17,12 +17,12 @@ use enum_map::EnumMap; /// ```no_run /// # use libdd_profiling::owned_sample::{Metadata, SampleType}; /// let metadata = Metadata::new(vec![ -/// SampleType::Cpu, -/// SampleType::Wall, -/// SampleType::Allocation, +/// SampleType::CpuTime, +/// SampleType::WallTime, +/// SampleType::AllocSpace, /// ], 256, true).unwrap(); /// -/// assert_eq!(metadata.get_index(&SampleType::Cpu), Some(0)); +/// assert_eq!(metadata.get_index(&SampleType::CpuTime), Some(0)); /// assert_eq!(metadata.get_index(&SampleType::Wall), Some(1)); /// assert_eq!(metadata.get_index(&SampleType::Allocation), Some(2)); /// assert_eq!(metadata.get_index(&SampleType::Heap), None); diff --git a/libdd-profiling/src/owned_sample/mod.rs b/libdd-profiling/src/owned_sample/mod.rs index 22f9b6f303..3efeaf2b4a 100644 --- a/libdd-profiling/src/owned_sample/mod.rs +++ b/libdd-profiling/src/owned_sample/mod.rs @@ -13,15 +13,15 @@ //! use std::sync::Arc; //! //! let metadata = Arc::new(Metadata::new(vec![ -//! SampleType::Cpu, -//! SampleType::Wall, +//! SampleType::CpuTime, +//! SampleType::WallTime, //! ], 64, true).unwrap()); //! //! let mut sample = OwnedSample::new(metadata); //! //! // Set values by type -//! sample.set_value(SampleType::Cpu, 1000).unwrap(); -//! sample.set_value(SampleType::Wall, 2000).unwrap(); +//! sample.set_value(SampleType::CpuTime, 1000).unwrap(); +//! sample.set_value(SampleType::WallTime, 2000).unwrap(); //! //! // Add a location //! sample.add_location(Location { @@ -47,108 +47,24 @@ //! ``` use bumpalo::Bump; -use enum_map::Enum; 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; -/// 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()) - } -} - -/// Types of profiling samples that can be collected. -/// -/// Based on the sample types from [dd-trace-py](https://github.com/DataDog/dd-trace-py/blob/d239f91be2c4ca1ec2ded88263ed132e28fe031b/ddtrace/internal/datadog/profiling/dd_wrapper/include/types.hpp#L4). -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Enum)] -pub enum SampleType { - /// CPU time profiling - Cpu, - /// Wall clock time profiling - Wall, - /// Exception tracking - Exception, - /// Lock acquisition profiling - LockAcquire, - /// Lock release profiling - LockRelease, - /// Memory allocation profiling - Allocation, - /// Heap profiling - Heap, - /// GPU time profiling - GpuTime, - /// GPU memory profiling - GpuMemory, - /// GPU floating point operations profiling - GpuFlops, -} - /// 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] @@ -223,9 +139,9 @@ impl OwnedSample { /// ```no_run /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType}; /// # use std::sync::Arc; - /// # let indices = Arc::new(Metadata::new(vec![SampleType::Cpu], 64).unwrap()); + /// # let indices = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); /// let mut sample = OwnedSample::new(indices); - /// sample.set_value(SampleType::Cpu, 1000).unwrap(); + /// 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) @@ -437,7 +353,7 @@ impl OwnedSample { /// ```no_run /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType, LabelKey}; /// # use std::sync::Arc; - /// # let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); /// # let mut sample = OwnedSample::new(metadata); /// sample.add_string_label(LabelKey::ThreadName, "worker-1"); /// sample.add_string_label(LabelKey::ExceptionType, "ValueError"); @@ -459,7 +375,7 @@ impl OwnedSample { /// ```no_run /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType, LabelKey}; /// # use std::sync::Arc; - /// # let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); /// # let mut sample = OwnedSample::new(metadata); /// sample.add_num_label(LabelKey::ThreadId, 42); /// sample.add_num_label(LabelKey::SpanId, 12345); @@ -491,7 +407,7 @@ impl OwnedSample { /// # 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::Cpu, SampleType::Wall], 64, true).unwrap()); + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime, SampleType::WallTime], 64, 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: "" }, @@ -572,7 +488,7 @@ impl OwnedSample { /// # 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::Cpu], 64, true).unwrap()); + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); /// # let mut profile = Profile::try_new(vec![], None).unwrap(); /// let sample = OwnedSample::new(metadata); /// sample.add_to_profile(&mut profile).unwrap(); diff --git a/libdd-profiling/src/owned_sample/pool.rs b/libdd-profiling/src/owned_sample/pool.rs index fcf6540197..14629b1d7c 100644 --- a/libdd-profiling/src/owned_sample/pool.rs +++ b/libdd-profiling/src/owned_sample/pool.rs @@ -22,15 +22,15 @@ use std::sync::Arc; /// # use libdd_profiling::owned_sample::{SamplePool, Metadata, SampleType}; /// # use std::sync::Arc; /// let metadata = Arc::new(Metadata::new(vec![ -/// SampleType::Cpu, -/// SampleType::Wall, +/// SampleType::CpuTime, +/// SampleType::WallTime, /// ], 64, 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::Cpu, 100).unwrap(); +/// sample.set_value(SampleType::CpuTime, 100).unwrap(); /// // ... use sample ... /// /// // Return it to the pool for reuse (thread-safe) @@ -56,7 +56,7 @@ impl SamplePool { /// ```no_run /// # use libdd_profiling::owned_sample::{SamplePool, Metadata, SampleType}; /// # use std::sync::Arc; - /// # let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); /// let pool = SamplePool::new(metadata, 100); /// ``` pub fn new(metadata: Arc, capacity: usize) -> Self { @@ -76,7 +76,7 @@ impl SamplePool { /// ```no_run /// # use libdd_profiling::owned_sample::{SamplePool, Metadata, SampleType}; /// # use std::sync::Arc; - /// # let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); /// # let pool = SamplePool::new(metadata, 10); /// let sample = pool.get(); /// assert_eq!(sample.num_locations(), 0); @@ -98,10 +98,10 @@ impl SamplePool { /// ```no_run /// # use libdd_profiling::owned_sample::{SamplePool, Metadata, SampleType}; /// # use std::sync::Arc; - /// # let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); /// # let pool = SamplePool::new(metadata, 10); /// let mut sample = pool.get(); - /// sample.set_value(SampleType::Cpu, 100).unwrap(); + /// sample.set_value(SampleType::CpuTime, 100).unwrap(); /// pool.put(sample); // Resets and returns to pool /// ``` pub fn put(&self, mut sample: Box) { @@ -140,7 +140,7 @@ mod tests { #[test] fn test_pool_basic() { - let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); let pool = SamplePool::new(metadata, 5); assert_eq!(pool.len(), 0); @@ -166,7 +166,7 @@ mod tests { #[test] fn test_pool_capacity_limit() { - let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); let pool = SamplePool::new(metadata, 2); // Fill the pool @@ -186,23 +186,23 @@ mod tests { #[test] fn test_pool_reset() { let metadata = Arc::new(Metadata::new(vec![ - SampleType::Cpu, - SampleType::Wall, + SampleType::CpuTime, + SampleType::WallTime, ], 64, true).unwrap()); let pool = SamplePool::new(metadata, 5); // Get a sample and modify it let mut sample = pool.get(); - sample.set_value(SampleType::Cpu, 100).unwrap(); - sample.set_value(SampleType::Wall, 200).unwrap(); + 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::Cpu).unwrap(), 0); - assert_eq!(sample.get_value(SampleType::Wall).unwrap(), 0); + assert_eq!(sample.get_value(SampleType::CpuTime).unwrap(), 0); + assert_eq!(sample.get_value(SampleType::WallTime).unwrap(), 0); assert_eq!(sample.num_locations(), 0); assert_eq!(sample.num_labels(), 0); } @@ -212,8 +212,8 @@ mod tests { use std::thread; let metadata = Arc::new(Metadata::new(vec![ - SampleType::Cpu, - SampleType::Wall, + SampleType::CpuTime, + SampleType::WallTime, ], 64, true).unwrap()); let pool = Arc::new(SamplePool::new(metadata, 20)); @@ -227,8 +227,8 @@ mod tests { let mut sample = pool.get(); // Use it - sample.set_value(SampleType::Cpu, (thread_id * 1000 + i) as i64).unwrap(); - sample.set_value(SampleType::Wall, (thread_id * 2000 + i) as i64).unwrap(); + 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); 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 0000000000..c038fac8c3 --- /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 index fd25cc8044..f904aced72 100644 --- a/libdd-profiling/src/owned_sample/tests.rs +++ b/libdd-profiling/src/owned_sample/tests.rs @@ -46,13 +46,13 @@ fn as_sample(owned: &OwnedSample) -> Sample<'_> { #[test] fn test_owned_sample_basic() { let metadata = Arc::new(Metadata::new(vec![ - SampleType::Cpu, - SampleType::Wall, + SampleType::CpuTime, + SampleType::WallTime, ], 64, true).unwrap()); let mut sample = OwnedSample::new(metadata.clone()); - sample.set_value(SampleType::Cpu, 100).unwrap(); - sample.set_value(SampleType::Wall, 200).unwrap(); + sample.set_value(SampleType::CpuTime, 100).unwrap(); + sample.set_value(SampleType::WallTime, 200).unwrap(); sample.add_location(Location { mapping: Mapping { @@ -76,8 +76,8 @@ fn test_owned_sample_basic() { assert_eq!(sample.num_locations(), 1); assert_eq!(sample.num_labels(), 2); - assert_eq!(sample.get_value(SampleType::Cpu).unwrap(), 100); - assert_eq!(sample.get_value(SampleType::Wall).unwrap(), 200); + assert_eq!(sample.get_value(SampleType::CpuTime).unwrap(), 100); + assert_eq!(sample.get_value(SampleType::WallTime).unwrap(), 200); let location = sample.get_location(0).unwrap(); assert_eq!(location.mapping.filename, "libfoo.so"); @@ -93,12 +93,12 @@ fn test_owned_sample_basic() { #[test] fn test_as_sample() { let metadata = Arc::new(Metadata::new(vec![ - SampleType::Cpu, - SampleType::Wall, + SampleType::CpuTime, + SampleType::WallTime, ], 64, true).unwrap()); let mut owned = OwnedSample::new(metadata.clone()); - owned.set_value(SampleType::Cpu, 100).unwrap(); - owned.set_value(SampleType::Wall, 200).unwrap(); + owned.set_value(SampleType::CpuTime, 100).unwrap(); + owned.set_value(SampleType::WallTime, 200).unwrap(); owned.add_location(Location { mapping: Mapping { memory_start: 0x1000, @@ -127,47 +127,47 @@ fn test_as_sample() { #[test] fn test_set_value_error() { - let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); let mut sample = OwnedSample::new(metadata); // Should work for configured type - assert!(sample.set_value(SampleType::Cpu, 100).is_ok()); - assert_eq!(sample.get_value(SampleType::Cpu).unwrap(), 100); + 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::Wall, 200).is_err()); - assert!(sample.get_value(SampleType::Wall).is_err()); + 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::Cpu, - SampleType::Wall, - SampleType::Allocation, + SampleType::CpuTime, + SampleType::WallTime, + SampleType::AllocSpace, ], 64, true).unwrap(); assert_eq!(metadata.len(), 3); assert!(!metadata.is_empty()); - assert_eq!(metadata.get_index(&SampleType::Cpu), Some(0)); - assert_eq!(metadata.get_index(&SampleType::Wall), Some(1)); - assert_eq!(metadata.get_index(&SampleType::Allocation), Some(2)); - assert_eq!(metadata.get_index(&SampleType::Heap), None); + 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::Cpu)); - assert_eq!(metadata.get_type(1), Some(SampleType::Wall)); - assert_eq!(metadata.get_type(2), Some(SampleType::Allocation)); + 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::Cpu, - SampleType::Wall, - SampleType::Cpu, // Duplicate - SampleType::Allocation, + SampleType::CpuTime, + SampleType::WallTime, + SampleType::CpuTime, // Duplicate + SampleType::AllocSpace, ], 64, true); assert!(result.is_err()); @@ -186,30 +186,30 @@ fn test_sample_type_indices_empty_error() { #[test] fn test_sample_type_indices_iter() { let metadata = Metadata::new(vec![ - SampleType::Cpu, - SampleType::Wall, - SampleType::Allocation, + SampleType::CpuTime, + SampleType::WallTime, + SampleType::AllocSpace, ], 64, true).unwrap(); let types: Vec<_> = metadata.iter().copied().collect(); assert_eq!(types, vec![ - SampleType::Cpu, - SampleType::Wall, - SampleType::Allocation, + SampleType::CpuTime, + SampleType::WallTime, + SampleType::AllocSpace, ]); } #[test] fn test_reset() { let metadata = Arc::new(Metadata::new(vec![ - SampleType::Cpu, - SampleType::Wall, - SampleType::Allocation, + SampleType::CpuTime, + SampleType::WallTime, + SampleType::AllocSpace, ], 64, true).unwrap()); let mut sample = OwnedSample::new(metadata); - sample.set_value(SampleType::Cpu, 100).unwrap(); - sample.set_value(SampleType::Wall, 200).unwrap(); - sample.set_value(SampleType::Allocation, 300).unwrap(); + 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 { @@ -231,18 +231,18 @@ fn test_reset() { assert_eq!(sample.num_locations(), 1); assert_eq!(sample.num_labels(), 1); - assert_eq!(sample.get_value(SampleType::Cpu).unwrap(), 100); - assert_eq!(sample.get_value(SampleType::Wall).unwrap(), 200); - assert_eq!(sample.get_value(SampleType::Allocation).unwrap(), 300); + 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.num_locations(), 0); assert_eq!(sample.num_labels(), 0); - assert_eq!(sample.get_value(SampleType::Cpu).unwrap(), 0); - assert_eq!(sample.get_value(SampleType::Wall).unwrap(), 0); - assert_eq!(sample.get_value(SampleType::Allocation).unwrap(), 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 { @@ -268,7 +268,7 @@ fn test_reset() { #[test] fn test_add_multiple() { - let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); let mut sample = OwnedSample::new(metadata); // Add multiple locations at once @@ -319,7 +319,7 @@ fn test_add_multiple() { fn test_endtime_ns() { use std::num::NonZeroI64; - let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); let mut sample = OwnedSample::new(metadata); // Initially, endtime_ns should be None @@ -346,7 +346,7 @@ fn test_endtime_ns() { fn test_set_endtime_ns_now() { use std::time::SystemTime; - let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); let mut sample = OwnedSample::new(metadata); // Initially, endtime_ns should be None @@ -397,7 +397,7 @@ fn test_set_endtime_ns_now() { #[test] fn test_timeline_enabled() { // Test with timeline enabled - let metadata_enabled = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let metadata_enabled = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); assert!(metadata_enabled.is_timeline_enabled()); let mut sample_enabled = OwnedSample::new(metadata_enabled); @@ -411,7 +411,7 @@ fn test_timeline_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::Cpu], 64, false).unwrap()); + let metadata_disabled = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, false).unwrap()); assert!(!metadata_disabled.is_timeline_enabled()); let mut sample_disabled = OwnedSample::new(metadata_disabled); @@ -428,7 +428,7 @@ fn test_timeline_enabled() { #[test] #[cfg(unix)] fn test_set_endtime_from_monotonic_ns() { - let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); let mut sample = OwnedSample::new(metadata); // Set endtime from a monotonic time @@ -468,7 +468,7 @@ fn test_set_endtime_from_monotonic_ns() { #[test] fn test_reverse_locations() { - let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); let mut sample = OwnedSample::new(metadata); // Initially, reverse_locations should be false @@ -552,7 +552,7 @@ fn test_label_key() { 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::Cpu], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); let mut sample = OwnedSample::new(metadata); sample.add_label(Label { @@ -574,7 +574,7 @@ fn test_label_key() { #[test] fn test_add_string_label() { - let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); let mut sample = OwnedSample::new(metadata); // Add string labels using the convenience method @@ -601,7 +601,7 @@ fn test_add_string_label() { #[test] fn test_add_num_label() { - let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); let mut sample = OwnedSample::new(metadata); // Add numeric labels using the convenience method @@ -631,7 +631,7 @@ fn test_add_num_label() { #[test] fn test_mixed_label_types() { - let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); let mut sample = OwnedSample::new(metadata); // Mix string and numeric labels @@ -656,7 +656,7 @@ fn test_mixed_label_types() { #[test] fn test_dropped_frames() { // Create metadata with a small max_frames limit - let metadata = Arc::new(Metadata::new(vec![SampleType::Cpu], 3, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 3, true).unwrap()); let mut sample = OwnedSample::new(metadata); // Add 3 locations (at the limit) From 1069ea77ebc9f220d89d5b738ce2c9708919a74e Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbone Date: Sun, 7 Dec 2025 00:40:15 -0500 Subject: [PATCH 11/12] better bumpalo usage, with max allocation size --- examples/cxx/owned_sample.cpp | 10 +- libdd-profiling/src/cxx.rs | 49 ++++-- libdd-profiling/src/owned_sample/metadata.rs | 19 ++- libdd-profiling/src/owned_sample/mod.rs | 149 ++++++++++++++----- libdd-profiling/src/owned_sample/pool.rs | 16 +- libdd-profiling/src/owned_sample/tests.rs | 113 +++++++++++--- 6 files changed, 271 insertions(+), 85 deletions(-) diff --git a/examples/cxx/owned_sample.cpp b/examples/cxx/owned_sample.cpp index 8086884dd7..4c9dd6caac 100644 --- a/examples/cxx/owned_sample.cpp +++ b/examples/cxx/owned_sample.cpp @@ -57,8 +57,8 @@ int main() { 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, timeline_enabled - auto metadata = Metadata::create({SampleType::WallTime}, 64, true); + // 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 @@ -167,6 +167,12 @@ int main() { .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); diff --git a/libdd-profiling/src/cxx.rs b/libdd-profiling/src/cxx.rs index e1739b0dac..e737d44540 100644 --- a/libdd-profiling/src/cxx.rs +++ b/libdd-profiling/src/cxx.rs @@ -106,9 +106,13 @@ pub mod ffi { 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, timeline_enabled: bool) -> Result>; + fn create(sample_types: Vec, max_frames: usize, arena_allocation_limit: i64, timeline_enabled: bool) -> Result>; // Profile static factory #[Self = "Profile"] @@ -169,11 +173,12 @@ pub mod ffi { 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); - fn add_string_label(self: &mut OwnedSample, key: LabelKey, value: &str); - fn add_num_label(self: &mut OwnedSample, key: LabelKey, value: i64); + 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 num_locations(self: &OwnedSample) -> usize; fn num_labels(self: &OwnedSample) -> usize; + fn allocated_bytes(self: &OwnedSample) -> usize; fn reset_sample(self: &mut OwnedSample); fn add_to_profile(self: &OwnedSample, profile: &mut Profile) -> Result<()>; @@ -367,20 +372,33 @@ impl Profile { 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, timeline_enabled: bool) -> anyhow::Result> { + 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, timeline_enabled)?); + let inner = Arc::new(owned_sample::Metadata::new(types, max_frames, arena_allocation_limit, timeline_enabled)?); Ok(Box::new(Metadata { inner })) } } @@ -438,19 +456,22 @@ impl OwnedSample { self.inner.add_location(api_location); } - pub fn add_label(&mut self, label: &ffi::Label) { + pub fn add_label(&mut self, label: &ffi::Label) -> anyhow::Result<()> { let api_label: api::Label = label.into(); - self.inner.add_label(api_label); + self.inner.add_label(api_label)?; + Ok(()) } - pub fn add_string_label(&mut self, key: ffi::LabelKey, value: &str) { + 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); + self.inner.add_string_label(rust_key, value)?; + Ok(()) } - pub fn add_num_label(&mut self, key: ffi::LabelKey, value: i64) { + 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); + self.inner.add_num_label(rust_key, value)?; + Ok(()) } pub fn num_locations(&self) -> usize { @@ -461,6 +482,10 @@ impl OwnedSample { self.inner.num_labels() } + pub fn allocated_bytes(&self) -> usize { + self.inner.allocated_bytes() + } + pub fn reset_sample(&mut self) { self.inner.reset(); } diff --git a/libdd-profiling/src/owned_sample/metadata.rs b/libdd-profiling/src/owned_sample/metadata.rs index 81d7a966aa..98c0084718 100644 --- a/libdd-profiling/src/owned_sample/metadata.rs +++ b/libdd-profiling/src/owned_sample/metadata.rs @@ -20,12 +20,12 @@ use enum_map::EnumMap; /// SampleType::CpuTime, /// SampleType::WallTime, /// SampleType::AllocSpace, -/// ], 256, true).unwrap(); +/// ], 256, None, true).unwrap(); /// /// assert_eq!(metadata.get_index(&SampleType::CpuTime), Some(0)); -/// assert_eq!(metadata.get_index(&SampleType::Wall), Some(1)); -/// assert_eq!(metadata.get_index(&SampleType::Allocation), Some(2)); -/// assert_eq!(metadata.get_index(&SampleType::Heap), None); +/// 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); /// ``` @@ -38,6 +38,8 @@ pub struct Metadata { 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, @@ -59,6 +61,7 @@ impl Metadata { /// /// * `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 @@ -68,7 +71,7 @@ impl Metadata { /// - 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, timeline_enabled: bool) -> anyhow::Result { + 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(); @@ -108,6 +111,7 @@ impl Metadata { sample_types, type_to_index, max_frames, + arena_allocation_limit, timeline_enabled, #[cfg(unix)] monotonic_to_epoch_offset, @@ -149,6 +153,11 @@ impl Metadata { 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 diff --git a/libdd-profiling/src/owned_sample/mod.rs b/libdd-profiling/src/owned_sample/mod.rs index 3efeaf2b4a..b178f3702d 100644 --- a/libdd-profiling/src/owned_sample/mod.rs +++ b/libdd-profiling/src/owned_sample/mod.rs @@ -10,12 +10,13 @@ //! //! ```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, true).unwrap()); +//! ], 64, None, true).unwrap()); //! //! let mut sample = OwnedSample::new(metadata); //! @@ -42,8 +43,8 @@ //! }); //! //! // Add labels -//! sample.add_label(Label { key: "thread_name", str: "worker-1", num: 0, num_unit: "" }); -//! sample.add_label(Label { key: "thread_id", str: "", num: 123, num_unit: "" }); +//! 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; @@ -65,6 +66,42 @@ 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] @@ -107,17 +144,19 @@ impl OwnedSample { /// ```no_run /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType}; /// # use std::sync::Arc; - /// let indices = Arc::new(SampleTypeIndices::new(vec![ - /// SampleType::Cpu, - /// SampleType::Wall, - /// ]).unwrap()); - /// let sample = OwnedSample::new(indices); + /// 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: Bump::new(), + arena, locations_builder: |_| Vec::new(), labels_builder: |_| Vec::new(), }.build(), @@ -139,7 +178,7 @@ impl OwnedSample { /// ```no_run /// # use libdd_profiling::owned_sample::{OwnedSample, Metadata, SampleType}; /// # use std::sync::Arc; - /// # let indices = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); + /// # 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(); /// ``` @@ -265,8 +304,9 @@ impl OwnedSample { /// 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`, the frame will be dropped - /// and the dropped frame count will be incremented instead. + /// 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(); @@ -276,13 +316,14 @@ impl OwnedSample { return; } - self.inner.with_mut(|fields| { + // 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.alloc_str(location.mapping.filename); - let build_id_ref = fields.arena.alloc_str(location.mapping.build_id); - let name_ref = fields.arena.alloc_str(location.function.name); - let system_name_ref = fields.arena.alloc_str(location.function.system_name); - let func_filename_ref = fields.arena.alloc_str(location.function.filename); + 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 { @@ -303,12 +344,19 @@ impl OwnedSample { }; 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); @@ -318,11 +366,15 @@ impl OwnedSample { /// Add a label to the sample. /// /// The label's strings will be copied into the internal arena. - pub fn add_label(&mut self, label: Label<'_>) { + /// + /// # 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.alloc_str(label.key); - let str_ref = fields.arena.alloc_str(label.str); - let num_unit_ref = fields.arena.alloc_str(label.num_unit); + 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, @@ -332,16 +384,22 @@ impl OwnedSample { }; fields.labels.push(owned_label); - }); + Ok(()) + }) } /// Add multiple labels to the sample. /// /// The labels' strings will be copied into the internal arena. - pub fn add_labels(&mut self, labels: &[Label<'_>]) { + /// + /// # 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); + self.add_label(*label)?; } + Ok(()) } /// Add a string label to the sample using a well-known label key. @@ -353,18 +411,19 @@ impl OwnedSample { /// ```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, true).unwrap()); + /// # 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"); + /// 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) { + 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. @@ -375,18 +434,19 @@ impl OwnedSample { /// ```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, true).unwrap()); + /// # 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); + /// 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) { + 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. @@ -407,7 +467,7 @@ impl OwnedSample { /// # 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, true).unwrap()); + /// # 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: "" }, @@ -437,6 +497,9 @@ impl OwnedSample { // 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); @@ -467,6 +530,16 @@ impl OwnedSample { 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() + }) + } + /// Get a location by index. pub fn get_location(&self, index: usize) -> Option> { self.inner.borrow_locations().get(index).copied() @@ -488,8 +561,8 @@ impl OwnedSample { /// # 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, true).unwrap()); - /// # let mut profile = Profile::try_new(vec![], None).unwrap(); + /// # 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(); /// ``` diff --git a/libdd-profiling/src/owned_sample/pool.rs b/libdd-profiling/src/owned_sample/pool.rs index 14629b1d7c..d8ce25ca73 100644 --- a/libdd-profiling/src/owned_sample/pool.rs +++ b/libdd-profiling/src/owned_sample/pool.rs @@ -24,7 +24,7 @@ use std::sync::Arc; /// let metadata = Arc::new(Metadata::new(vec![ /// SampleType::CpuTime, /// SampleType::WallTime, -/// ], 64, true).unwrap()); +/// ], 64, None, true).unwrap()); /// /// let pool = SamplePool::new(metadata, 10); /// @@ -56,7 +56,7 @@ impl SamplePool { /// ```no_run /// # use libdd_profiling::owned_sample::{SamplePool, Metadata, SampleType}; /// # use std::sync::Arc; - /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); + /// # 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 { @@ -76,7 +76,7 @@ impl SamplePool { /// ```no_run /// # use libdd_profiling::owned_sample::{SamplePool, Metadata, SampleType}; /// # use std::sync::Arc; - /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); + /// # 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.num_locations(), 0); @@ -98,7 +98,7 @@ impl SamplePool { /// ```no_run /// # use libdd_profiling::owned_sample::{SamplePool, Metadata, SampleType}; /// # use std::sync::Arc; - /// # let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); + /// # 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(); @@ -140,7 +140,7 @@ mod tests { #[test] fn test_pool_basic() { - let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); let pool = SamplePool::new(metadata, 5); assert_eq!(pool.len(), 0); @@ -166,7 +166,7 @@ mod tests { #[test] fn test_pool_capacity_limit() { - let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); let pool = SamplePool::new(metadata, 2); // Fill the pool @@ -188,7 +188,7 @@ mod tests { let metadata = Arc::new(Metadata::new(vec![ SampleType::CpuTime, SampleType::WallTime, - ], 64, true).unwrap()); + ], 64, None, true).unwrap()); let pool = SamplePool::new(metadata, 5); // Get a sample and modify it @@ -214,7 +214,7 @@ mod tests { let metadata = Arc::new(Metadata::new(vec![ SampleType::CpuTime, SampleType::WallTime, - ], 64, true).unwrap()); + ], 64, None, true).unwrap()); let pool = Arc::new(SamplePool::new(metadata, 20)); // Spawn multiple threads that all use the pool concurrently diff --git a/libdd-profiling/src/owned_sample/tests.rs b/libdd-profiling/src/owned_sample/tests.rs index f904aced72..633aa7eafd 100644 --- a/libdd-profiling/src/owned_sample/tests.rs +++ b/libdd-profiling/src/owned_sample/tests.rs @@ -48,7 +48,7 @@ fn test_owned_sample_basic() { let metadata = Arc::new(Metadata::new(vec![ SampleType::CpuTime, SampleType::WallTime, - ], 64, true).unwrap()); + ], 64, None, true).unwrap()); let mut sample = OwnedSample::new(metadata.clone()); sample.set_value(SampleType::CpuTime, 100).unwrap(); @@ -95,7 +95,7 @@ fn test_as_sample() { let metadata = Arc::new(Metadata::new(vec![ SampleType::CpuTime, SampleType::WallTime, - ], 64, true).unwrap()); + ], 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(); @@ -127,7 +127,7 @@ fn test_as_sample() { #[test] fn test_set_value_error() { - let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); let mut sample = OwnedSample::new(metadata); // Should work for configured type @@ -145,7 +145,7 @@ fn test_sample_type_indices_basic() { SampleType::CpuTime, SampleType::WallTime, SampleType::AllocSpace, - ], 64, true).unwrap(); + ], 64, None, true).unwrap(); assert_eq!(metadata.len(), 3); assert!(!metadata.is_empty()); @@ -168,7 +168,7 @@ fn test_sample_type_indices_duplicate_error() { SampleType::WallTime, SampleType::CpuTime, // Duplicate SampleType::AllocSpace, - ], 64, true); + ], 64, None, true); assert!(result.is_err()); let err = result.unwrap_err(); @@ -177,7 +177,7 @@ fn test_sample_type_indices_duplicate_error() { #[test] fn test_sample_type_indices_empty_error() { - let result = Metadata::new(vec![], 64, true); + let result = Metadata::new(vec![], 64, None, true); assert!(result.is_err()); let err = result.unwrap_err(); assert!(err.to_string().contains("empty")); @@ -189,7 +189,7 @@ fn test_sample_type_indices_iter() { SampleType::CpuTime, SampleType::WallTime, SampleType::AllocSpace, - ], 64, true).unwrap(); + ], 64, None, true).unwrap(); let types: Vec<_> = metadata.iter().copied().collect(); assert_eq!(types, vec![ @@ -205,7 +205,7 @@ fn test_reset() { SampleType::CpuTime, SampleType::WallTime, SampleType::AllocSpace, - ], 64, true).unwrap()); + ], 64, None, true).unwrap()); let mut sample = OwnedSample::new(metadata); sample.set_value(SampleType::CpuTime, 100).unwrap(); sample.set_value(SampleType::WallTime, 200).unwrap(); @@ -268,7 +268,7 @@ fn test_reset() { #[test] fn test_add_multiple() { - let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); let mut sample = OwnedSample::new(metadata); // Add multiple locations at once @@ -319,7 +319,7 @@ fn test_add_multiple() { fn test_endtime_ns() { use std::num::NonZeroI64; - let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); + 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 @@ -346,7 +346,7 @@ fn test_endtime_ns() { fn test_set_endtime_ns_now() { use std::time::SystemTime; - let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); + 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 @@ -397,7 +397,7 @@ fn test_set_endtime_ns_now() { #[test] fn test_timeline_enabled() { // Test with timeline enabled - let metadata_enabled = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); + 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); @@ -411,7 +411,7 @@ fn test_timeline_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, false).unwrap()); + 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); @@ -428,7 +428,7 @@ fn test_timeline_enabled() { #[test] #[cfg(unix)] fn test_set_endtime_from_monotonic_ns() { - let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); + 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 @@ -468,7 +468,7 @@ fn test_set_endtime_from_monotonic_ns() { #[test] fn test_reverse_locations() { - let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); + 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 @@ -552,7 +552,7 @@ fn test_label_key() { 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, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); let mut sample = OwnedSample::new(metadata); sample.add_label(Label { @@ -574,7 +574,7 @@ fn test_label_key() { #[test] fn test_add_string_label() { - let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); + 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 @@ -601,7 +601,7 @@ fn test_add_string_label() { #[test] fn test_add_num_label() { - let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); + 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 @@ -631,7 +631,7 @@ fn test_add_num_label() { #[test] fn test_mixed_label_types() { - let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, true).unwrap()); + let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 64, None, true).unwrap()); let mut sample = OwnedSample::new(metadata); // Mix string and numeric labels @@ -656,7 +656,7 @@ fn test_mixed_label_types() { #[test] fn test_dropped_frames() { // Create metadata with a small max_frames limit - let metadata = Arc::new(Metadata::new(vec![SampleType::CpuTime], 3, true).unwrap()); + 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) @@ -760,3 +760,76 @@ fn test_dropped_frames() { 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: "", + }); + + // 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); +} From 5b5862f09cbb0d73d52ffd19eee2040ec63aa128 Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbone Date: Sun, 7 Dec 2025 01:00:26 -0500 Subject: [PATCH 12/12] cleanup owned_sample api --- libdd-profiling/src/cxx.rs | 9 --- libdd-profiling/src/owned_sample/mod.rs | 64 +++--------------- libdd-profiling/src/owned_sample/pool.rs | 8 +-- libdd-profiling/src/owned_sample/tests.rs | 80 +++++++++++------------ 4 files changed, 54 insertions(+), 107 deletions(-) diff --git a/libdd-profiling/src/cxx.rs b/libdd-profiling/src/cxx.rs index e737d44540..0f0fbb35a2 100644 --- a/libdd-profiling/src/cxx.rs +++ b/libdd-profiling/src/cxx.rs @@ -176,8 +176,6 @@ pub mod ffi { 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 num_locations(self: &OwnedSample) -> usize; - fn num_labels(self: &OwnedSample) -> usize; fn allocated_bytes(self: &OwnedSample) -> usize; fn reset_sample(self: &mut OwnedSample); fn add_to_profile(self: &OwnedSample, profile: &mut Profile) -> Result<()>; @@ -474,13 +472,6 @@ impl OwnedSample { Ok(()) } - pub fn num_locations(&self) -> usize { - self.inner.num_locations() - } - - pub fn num_labels(&self) -> usize { - self.inner.num_labels() - } pub fn allocated_bytes(&self) -> usize { self.inner.allocated_bytes() diff --git a/libdd-profiling/src/owned_sample/mod.rs b/libdd-profiling/src/owned_sample/mod.rs index b178f3702d..9f61d6ee41 100644 --- a/libdd-profiling/src/owned_sample/mod.rs +++ b/libdd-profiling/src/owned_sample/mod.rs @@ -105,6 +105,7 @@ impl From for OwnedSampleError { /// 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, @@ -125,6 +126,7 @@ struct SampleInner { /// 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, @@ -478,8 +480,8 @@ impl OwnedSample { /// sample.add_label(Label { key: "thread", str: "main", num: 0, num_unit: "" }); /// /// sample.reset(); - /// assert_eq!(sample.num_locations(), 0); - /// assert_eq!(sample.num_labels(), 0); + /// 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) { @@ -515,16 +517,6 @@ impl OwnedSample { }.build(); } - /// Get the number of locations in this sample. - pub fn num_locations(&self) -> usize { - self.inner.borrow_locations().len() - } - - /// Get the number of labels in this sample. - pub fn num_labels(&self) -> usize { - self.inner.borrow_labels().len() - } - /// Get the number of frames that were dropped due to exceeding max_frames. pub fn dropped_frames(&self) -> usize { self.dropped_frames @@ -540,16 +532,6 @@ impl OwnedSample { }) } - /// Get a location by index. - pub fn get_location(&self, index: usize) -> Option> { - self.inner.borrow_locations().get(index).copied() - } - - /// Get a label by index. - pub fn get_label(&self, index: usize) -> Option> { - self.inner.borrow_labels().get(index).copied() - } - /// Add this sample to a profile. /// /// If frames were dropped (exceeding `max_frames`), a pseudo-frame will be appended @@ -602,39 +584,13 @@ impl OwnedSample { profile.try_add_sample(sample, None) } - /// Iterate over all locations. - pub fn locations(&self) -> impl Iterator> + '_ { - self.inner.borrow_locations().iter().copied() + /// Get a slice of all locations. + pub fn locations(&self) -> &[Location<'_>] { + self.inner.borrow_locations() } - /// Iterate over all labels. - pub fn labels(&self) -> impl Iterator> + '_ { - self.inner.borrow_labels().iter().copied() + /// Get a slice of all labels. + pub fn labels(&self) -> &[Label<'_>] { + self.inner.borrow_labels() } } - -impl std::fmt::Debug for OwnedSample { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("OwnedSample") - .field("sample_types", &self.metadata.types()) - .field("num_locations", &self.num_locations()) - .field("num_labels", &self.num_labels()) - .field("values", &self.values()) - .finish() - } -} - -impl PartialEq for OwnedSample { - fn eq(&self, other: &Self) -> bool { - // Compare metadata configuration (pointer equality is fine since they're Arc) - Arc::ptr_eq(&self.metadata, &other.metadata) - && self.values() == other.values() - && self.num_locations() == other.num_locations() - && self.num_labels() == other.num_labels() - && self.locations().zip(other.locations()).all(|(a, b)| a == b) - && self.labels().zip(other.labels()).all(|(a, b)| a == b) - } -} - -impl Eq for OwnedSample {} - diff --git a/libdd-profiling/src/owned_sample/pool.rs b/libdd-profiling/src/owned_sample/pool.rs index d8ce25ca73..0c0bcb9919 100644 --- a/libdd-profiling/src/owned_sample/pool.rs +++ b/libdd-profiling/src/owned_sample/pool.rs @@ -79,7 +79,7 @@ impl SamplePool { /// # 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.num_locations(), 0); + /// assert_eq!(sample.locations().len(), 0); /// ``` pub fn get(&self) -> Box { self.samples.pop().unwrap_or_else(|| { @@ -203,8 +203,8 @@ mod tests { 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.num_locations(), 0); - assert_eq!(sample.num_labels(), 0); + assert_eq!(sample.locations().len(), 0); + assert_eq!(sample.labels().len(), 0); } #[test] @@ -244,7 +244,7 @@ mod tests { // Pool should have accumulated samples (up to its capacity) assert!(pool.len() <= pool.capacity()); - assert!(pool.len() > 0); // Should have at least some samples + assert!(!pool.is_empty()); // Should have at least some samples } } diff --git a/libdd-profiling/src/owned_sample/tests.rs b/libdd-profiling/src/owned_sample/tests.rs index 633aa7eafd..fcffff2f29 100644 --- a/libdd-profiling/src/owned_sample/tests.rs +++ b/libdd-profiling/src/owned_sample/tests.rs @@ -71,20 +71,20 @@ fn test_owned_sample_basic() { line: 42, }); - sample.add_label(Label { key: "thread_name", str: "worker-1", num: 0, num_unit: "" }); - sample.add_label(Label { key: "thread_id", str: "", num: 123, num_unit: "" }); + 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.num_locations(), 1); - assert_eq!(sample.num_labels(), 2); + 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.get_location(0).unwrap(); + 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.get_label(0).unwrap(); + let label = sample.labels()[0]; assert_eq!(label.key, "thread_name"); assert_eq!(label.str, "worker-1"); } @@ -115,7 +115,7 @@ fn test_as_sample() { address: 0x1234, line: 42, }); - owned.add_label(Label { key: "key", str: "value", num: 0, num_unit: "" }); + owned.add_label(Label { key: "key", str: "value", num: 0, num_unit: "" }).unwrap(); let borrowed = as_sample(&owned); assert_eq!(borrowed.values, &[100, 200]); @@ -227,10 +227,10 @@ fn test_reset() { address: 0x1234, line: 42, }); - sample.add_label(Label { key: "key", str: "value", num: 0, num_unit: "" }); + sample.add_label(Label { key: "key", str: "value", num: 0, num_unit: "" }).unwrap(); - assert_eq!(sample.num_locations(), 1); - assert_eq!(sample.num_labels(), 1); + 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); @@ -238,8 +238,8 @@ fn test_reset() { // Reset clears locations/labels and zeros values sample.reset(); - assert_eq!(sample.num_locations(), 0); - assert_eq!(sample.num_labels(), 0); + 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); @@ -261,8 +261,8 @@ fn test_reset() { address: 0, line: 1, }); - assert_eq!(sample.num_locations(), 1); - let loc = sample.get_location(0).unwrap(); + assert_eq!(sample.locations().len(), 1); + let loc = sample.locations()[0]; assert_eq!(loc.mapping.filename, "new.so"); } @@ -293,24 +293,24 @@ fn test_add_multiple() { Label { key: "thread", str: "main", num: 0, num_unit: "" }, Label { key: "thread_id", str: "", num: 123, num_unit: "" }, ]; - sample.add_labels(labels); + sample.add_labels(labels).unwrap(); - assert_eq!(sample.num_locations(), 2); - assert_eq!(sample.num_labels(), 2); + assert_eq!(sample.locations().len(), 2); + assert_eq!(sample.labels().len(), 2); - let loc0 = sample.get_location(0).unwrap(); + let loc0 = sample.locations()[0]; assert_eq!(loc0.mapping.filename, "lib1.so"); assert_eq!(loc0.function.name, "func1"); - let loc1 = sample.get_location(1).unwrap(); + let loc1 = sample.locations()[1]; assert_eq!(loc1.mapping.filename, "lib2.so"); assert_eq!(loc1.function.name, "func2"); - let label0 = sample.get_label(0).unwrap(); + let label0 = sample.labels()[0]; assert_eq!(label0.key, "thread"); assert_eq!(label0.str, "main"); - let label1 = sample.get_label(1).unwrap(); + let label1 = sample.labels()[1]; assert_eq!(label1.key, "thread_id"); assert_eq!(label1.num, 123); } @@ -560,16 +560,16 @@ fn test_label_key() { 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.num_labels(), 2); + assert_eq!(sample.labels().len(), 2); } #[test] @@ -578,11 +578,11 @@ fn test_add_string_label() { let mut sample = OwnedSample::new(metadata); // Add string labels using the convenience method - sample.add_string_label(LabelKey::ThreadName, "worker-1"); - sample.add_string_label(LabelKey::ExceptionType, "ValueError"); - sample.add_string_label(LabelKey::ClassName, "MyClass"); + 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.num_labels(), 3); + assert_eq!(sample.labels().len(), 3); // Verify the labels were added correctly let api_sample = as_sample(&sample); @@ -605,11 +605,11 @@ fn test_add_num_label() { let mut sample = OwnedSample::new(metadata); // Add numeric labels using the convenience method - sample.add_num_label(LabelKey::ThreadId, 42); - sample.add_num_label(LabelKey::ThreadNativeId, 12345); - sample.add_num_label(LabelKey::SpanId, 98765); + 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.num_labels(), 3); + assert_eq!(sample.labels().len(), 3); // Verify the labels were added correctly let api_sample = as_sample(&sample); @@ -635,12 +635,12 @@ fn test_mixed_label_types() { let mut sample = OwnedSample::new(metadata); // Mix string and numeric labels - sample.add_string_label(LabelKey::ThreadName, "worker-1"); - sample.add_num_label(LabelKey::ThreadId, 42); - sample.add_string_label(LabelKey::ExceptionType, "RuntimeError"); - sample.add_num_label(LabelKey::SpanId, 12345); + 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.num_labels(), 4); + assert_eq!(sample.labels().len(), 4); let api_sample = as_sample(&sample); assert_eq!(api_sample.labels[0].key, "thread name"); @@ -679,7 +679,7 @@ fn test_dropped_frames() { }); } - assert_eq!(sample.num_locations(), 3); + assert_eq!(sample.locations().len(), 3); assert_eq!(sample.dropped_frames(), 0); // Try to add 2 more locations (should be dropped) @@ -703,7 +703,7 @@ fn test_dropped_frames() { } // Should still have 3 locations, but 2 dropped - assert_eq!(sample.num_locations(), 3); + assert_eq!(sample.locations().len(), 3); assert_eq!(sample.dropped_frames(), 2); // Convert to API sample and verify pseudo-frame was added @@ -797,7 +797,7 @@ fn test_allocated_bytes() { 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();