diff --git a/.github/workflows/prof_asan.yml b/.github/workflows/prof_asan.yml index 218dc21e1ff..d46e68a68e2 100644 --- a/.github/workflows/prof_asan.yml +++ b/.github/workflows/prof_asan.yml @@ -37,6 +37,8 @@ jobs: set -eux switch-php nts-asan cd profiling + phpize + ./configure --with-php-config="$(command -v php-config)" export CARGO_TARGET_DIR=/tmp/build-cargo export CC=clang-17 export CFLAGS='-fsanitize=address -fno-omit-frame-pointer' diff --git a/.github/workflows/prof_correctness.yml b/.github/workflows/prof_correctness.yml index da49c2bd496..b2ce1a8bbb1 100644 --- a/.github/workflows/prof_correctness.yml +++ b/.github/workflows/prof_correctness.yml @@ -55,6 +55,8 @@ jobs: sudo apt-get update sudo apt install -y clang-17 cd profiling + phpize + ./configure --with-php-config="$(command -v php-config)" version_number=$(awk -F' = ' '$1 == "channel" { gsub(/"/, "", $2); print $2 }' rust-toolchain.toml) curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal -y --default-toolchain "$version_number" cargo rustc --features="trigger_time_sample" --profile profiler-release --crate-type=cdylib diff --git a/.gitlab/build-profiler.sh b/.gitlab/build-profiler.sh index c4182c48817..d7b18e961ca 100755 --- a/.gitlab/build-profiler.sh +++ b/.gitlab/build-profiler.sh @@ -38,6 +38,8 @@ else fi cd profiling +phpize +./configure --with-php-config="$(command -v php-config)" CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-target}" echo "${CARGO_TARGET_DIR}" if [ "$thread_safety" = "zts" ]; then diff --git a/.gitlab/generate-profiler.php b/.gitlab/generate-profiler.php index a58260cb26a..775cca70b0e 100644 --- a/.gitlab/generate-profiler.php +++ b/.gitlab/generate-profiler.php @@ -46,6 +46,8 @@ - '# NTS' - command -v switch-php && switch-php "${PHP_MAJOR_MINOR}" + - phpize + - ./configure --with-php-config="$(command -v php-config)" - cargo build --profile profiler-release --all-features - (cd ../; TEST_PHP_JUNIT="${CI_PROJECT_DIR}/artifacts/profiler-tests/nts-results.xml" php profiling/tests/run-tests.php -d "extension=/mnt/ramdisk/cargo/profiler-release/libdatadog_php_profiling.so" --show-diff -g "FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP" "profiling/tests/phpt") @@ -53,6 +55,8 @@ - '# ZTS' - command -v switch-php && switch-php "${PHP_MAJOR_MINOR}-zts" + - phpize + - ./configure --with-php-config="$(command -v php-config)" - cargo build --profile profiler-release --all-features - (cd ../; TEST_PHP_JUNIT="${CI_PROJECT_DIR}/artifacts/profiler-tests/zts-results.xml" php profiling/tests/run-tests.php -d "extension=/mnt/ramdisk/cargo/profiler-release/libdatadog_php_profiling.so" --show-diff -g "FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP" "profiling/tests/phpt") after_script: @@ -85,6 +89,8 @@ script: - switch-php nts # not compatible with debug - cd profiling + - phpize + - ./configure --with-php-config="$(command -v php-config)" - sed -i -e "s/crate-type.*$/crate-type = [\"rlib\"]/g" Cargo.toml - cargo clippy --all-targets --all-features -- -D warnings -Aunknown-lints @@ -101,4 +107,6 @@ script: - switch-php nts # not compatible with debug - cd profiling + - phpize + - ./configure --with-php-config="$(command -v php-config)" - cargo test --all-features diff --git a/benchmark/runall.sh b/benchmark/runall.sh index 7f0a5eeafb9..58f18b84e77 100755 --- a/benchmark/runall.sh +++ b/benchmark/runall.sh @@ -6,6 +6,9 @@ if [ "$SCENARIO" = "profiler" ]; then # Run Profiling Benchmarks cd ../profiling/ + phpize + ./configure --with-php-config="$(command -v php-config)" + cargo build --release --features trigger_time_sample sirun benches/timeline.json > "$ARTIFACTS_DIR/sirun_timeline.ndjson" diff --git a/profiling/.gitignore b/profiling/.gitignore new file mode 100644 index 00000000000..0dabaf35589 --- /dev/null +++ b/profiling/.gitignore @@ -0,0 +1,18 @@ +/Makefile +/Makefile.objects +/Makefile.fragments +/aclocal.m4 +/autom4te.cache/ +/build/ +/config.cache +/config.h +/config.h.in +/config.log +/config.nice +/config.status +/configure +/configure.ac +/libtool +/ltmain.sh +/modules/ +/target/ diff --git a/profiling/Makefile.frag b/profiling/Makefile.frag new file mode 100644 index 00000000000..29620d1f62a --- /dev/null +++ b/profiling/Makefile.frag @@ -0,0 +1,59 @@ +DD_PROFILING_NAME = datadog-profiling +DD_PROFILING_MODULE = modules/$(DD_PROFILING_NAME).$(SHLIB_DL_SUFFIX_NAME) +DD_PROFILING_MODULE_LA = modules/$(DD_PROFILING_NAME).la + +DD_PROFILING_CARGO_TARGET_DIR = $(top_builddir)/target + +DD_PROFILING_UNAME_S := $(shell uname -s) +ifeq ($(DD_PROFILING_UNAME_S),Darwin) +DD_PROFILING_RUST_EXT = dylib +else +DD_PROFILING_RUST_EXT = so +endif + +DD_PROFILING_RUST_LIB = $(DD_PROFILING_CARGO_TARGET_DIR)/$(DD_PROFILING_CARGO_PROFILE)/libdatadog_php_profiling.$(DD_PROFILING_RUST_EXT) +DD_PROFILING_CARGO_FEATURES_CLEAN = $(shell echo "$(DD_PROFILING_CARGO_FEATURES)" | tr ',' ' ') + +.PHONY: cargo-test clean distclean clean-profiling distclean-profiling + +PHP_MODULES += $(DD_PROFILING_MODULE_LA) +all: $(DD_PROFILING_MODULE_LA) + +$(DD_PROFILING_RUST_LIB): $(top_builddir)/Makefile $(top_builddir)/config.h + @echo "Building Rust profiler ($(DD_PROFILING_CARGO_PROFILE))" + @CARGO_TARGET_DIR="$(DD_PROFILING_CARGO_TARGET_DIR)" \ + PHP_INCLUDES="$(INCLUDES)" \ + PHP_INCLUDE_DIR="$(phpincludedir)" \ + PHP_PREFIX="$(prefix)" \ + PHP_VERSION_ID="$(DATADOG_PHP_VERSION_ID)" \ + $(DD_PROFILING_CARGO) build --manifest-path "$(top_srcdir)/Cargo.toml" $(DD_PROFILING_CARGO_ARGS) $(if $(DD_PROFILING_CARGO_FEATURES_CLEAN),--features "$(DD_PROFILING_CARGO_FEATURES_CLEAN)",) + +$(DD_PROFILING_MODULE): $(DD_PROFILING_RUST_LIB) + @mkdir -p modules + @cp -f "$(DD_PROFILING_RUST_LIB)" "$@" + +$(DD_PROFILING_MODULE_LA): $(DD_PROFILING_MODULE) + @printf "dlname='%s'\n" "$(DD_PROFILING_NAME).$(SHLIB_DL_SUFFIX_NAME)" > "$@" + +cargo-test: + @CARGO_TARGET_DIR="$(DD_PROFILING_CARGO_TARGET_DIR)" \ + PHP_INCLUDES="$(INCLUDES)" \ + PHP_INCLUDE_DIR="$(phpincludedir)" \ + PHP_PREFIX="$(prefix)" \ + PHP_VERSION_ID="$(DATADOG_PHP_VERSION_ID)" \ + $(DD_PROFILING_CARGO) test --manifest-path "$(top_srcdir)/Cargo.toml" $(if $(DD_PROFILING_CARGO_FEATURES_CLEAN),--features "$(DD_PROFILING_CARGO_FEATURES_CLEAN)",) + +clean: clean-profiling + +clean-profiling: + @rm -f "$(DD_PROFILING_MODULE)" + @# Only cleans the profiler crate; dependencies remain cached. + @$(DD_PROFILING_CARGO) clean -p datadog-php-profiling + +distclean: distclean-profiling + +distclean-profiling: + @rm -rf "$(DD_PROFILING_CARGO_TARGET_DIR)" + @rm -rf build autom4te.cache modules + @rm -f configure configure.ac config.h config.h.in config.nice run-tests.php + @rm -f *~ *.orig diff --git a/profiling/README.md b/profiling/README.md index 400b89752c4..b0a8eeb0870 100644 --- a/profiling/README.md +++ b/profiling/README.md @@ -39,6 +39,70 @@ allows us to gloss over minor differences in const-correctness in the engine definitions across versions, as well as provide more idiomatic types in some cases where they are ABI compatible. +### Phpize build (recommended for installs) + +From the `profiling/` directory, use the normal extension build flow: + +```sh +# Choose the PHP version to build against +export PATH="/opt/php/8.4/bin:$PATH" + +phpize +./configure +make +make install +``` + +This builds `modules/datadog-profiling.so` and installs it into the PHP +extension directory reported by `php-config --extension-dir`. + +To build with the Cargo debug profile: + +```sh +./configure --enable-datadog-profiling-debug +``` + +To enable specific Cargo features: + +```sh +./configure --with-datadog-profiling-cargo-features="stack_walking_tests,trigger_time_sample" +``` + +To run the PHPT tests with the built extension: + +```sh +make test +``` + +To suppress the failure report prompt: + +```sh +NO_INTERACTION=1 make test +``` + +To show diffs on failure, or to run tests in parallel, pass flags via `TESTS`: + +```sh +make test TESTS="--show-diff -j8 tests/phpt" +``` + +To run tests in parallel on newer PHP versions (PHP 8.0+ supports `-j`): + +```sh +make test TESTS="-j8 tests/phpt" +``` + +Note: direct `cargo build` expects you to have run `phpize` and +`./configure` so `build.rs` can read PHP settings from the generated +`Makefile`. + +If you want to run cargo commands and want to reuse artifacts from the phpize +flow, run it with the same target directory: + +```sh +CARGO_TARGET_DIR=target cargo build --release +``` + ## Testing The command `cargo test` will run the tests on the profiler. To also run the diff --git a/profiling/build.rs b/profiling/build.rs index b8566e4a5b1..b306957a260 100644 --- a/profiling/build.rs +++ b/profiling/build.rs @@ -1,25 +1,16 @@ use bindgen::callbacks::IntKind; +use std::collections::HashMap; use std::collections::HashSet; use std::path::Path; use std::path::PathBuf; -use std::process::Command; +use std::sync::{Arc, Mutex, OnceLock}; use std::{env, fs}; fn main() { - let php_config_includes_output = Command::new("php-config") - .arg("--includes") - .output() - .expect("Unable to run `php-config`. Is it in your PATH?"); - - if !php_config_includes_output.status.success() { - match String::from_utf8(php_config_includes_output.stderr) { - Ok(stderr) => panic!("`php-config failed: {stderr}"), - Err(err) => panic!( - "`php-config` failed, not utf8: {}", - String::from_utf8_lossy(err.as_bytes()) - ), - } - } + let php_includes = build_var("PHP_INCLUDES"); + let php_include_dir = build_var("PHP_INCLUDE_DIR"); + let php_prefix = build_var("PHP_PREFIX"); + let php_version_id = php_version_id(&php_includes, &php_include_dir); // Read the version from the VERSION file let version = fs::read_to_string("../VERSION") @@ -29,10 +20,7 @@ fn main() { println!("cargo:rustc-env=PROFILER_VERSION={version}"); println!("cargo:rerun-if-changed=../VERSION"); - let php_config_includes = std::str::from_utf8(php_config_includes_output.stdout.as_slice()) - .expect("`php-config`'s stdout to be valid utf8"); - - let vernum = php_config_vernum(); + let vernum = php_version_id; let post_startup_cb = cfg_post_startup_cb(vernum); let preload = cfg_preload(vernum); let fibers = cfg_fibers(vernum); @@ -40,9 +28,10 @@ fn main() { let trigger_time_sample = cfg_trigger_time_sample(); let zend_error_observer = cfg_zend_error_observer(vernum); - generate_bindings(php_config_includes, fibers, zend_error_observer); + generate_bindings(&php_includes, fibers, zend_error_observer); build_zend_php_ffis( - php_config_includes, + &php_includes, + &php_prefix, post_startup_cb, preload, run_time_cache, @@ -53,32 +42,68 @@ fn main() { cfg_php_major_version(vernum); cfg_php_feature_flags(vernum); - cfg_zts(); + cfg_zts_from_headers(&php_include_dir); } -fn php_config_vernum() -> u64 { - let output = Command::new("php-config") - .arg("--vernum") - .output() - .expect("Unable to run `php-config`. Is it in your PATH?"); - - if !output.status.success() { - match String::from_utf8(output.stderr) { - Ok(stderr) => panic!("`php-config --vernum` failed: {stderr}"), - Err(err) => panic!( - "`php-config --vernum` failed, not utf8: {}", - String::from_utf8_lossy(err.as_bytes()) - ), - } +fn build_var(name: &str) -> String { + if let Some(value) = makefile_var(name) { + return value; } - let vernum = std::str::from_utf8(output.stdout.as_slice()) - .expect("`php-config`'s stdout to be valid utf8"); + panic!( + "Missing required build variable {name}. \ +Run phpize && ./configure to generate Makefile." + ) +} - vernum - .trim() - .parse::() - .expect("output to be a number and fit in u64") +fn php_version_id(php_includes: &str, php_include_dir: &str) -> u64 { + let macros = php_header_macros( + "main/php_version.h", + php_includes, + php_include_dir, + &["PHP_VERSION_ID"], + ); + macros + .get("PHP_VERSION_ID") + .copied() + .unwrap_or_else(|| panic!("PHP_VERSION_ID not found in php_config.h")) as u64 +} + +fn makefile_vars() -> Option<&'static HashMap> { + static MAKEFILE_VARS: OnceLock>> = OnceLock::new(); + MAKEFILE_VARS + .get_or_init(|| { + let path = Path::new("Makefile"); + if !path.exists() { + return None; + } + println!("cargo:rerun-if-changed=Makefile"); + let contents = fs::read_to_string(path).ok()?; + let mut vars = HashMap::new(); + for line in contents.lines() { + if let Some((key, value)) = line.split_once('=') { + let key = key.trim(); + let value = value.trim(); + if !key.is_empty() { + vars.insert(key.to_string(), value.to_string()); + } + } + } + Some(vars) + }) + .as_ref() +} + +fn makefile_var(name: &str) -> Option { + let vars = makefile_vars()?; + let key = match name { + "PHP_INCLUDES" => "INCLUDES", + "PHP_INCLUDE_DIR" => "phpincludedir", + "PHP_PREFIX" => "prefix", + "PHP_VERSION_ID" => "DATADOG_PHP_VERSION_ID", + _ => return None, + }; + vars.get(key).cloned() } const ZAI_H_FILES: &[&str] = &[ @@ -98,6 +123,7 @@ const ZAI_H_FILES: &[&str] = &[ #[allow(clippy::too_many_arguments)] fn build_zend_php_ffis( php_config_includes: &str, + php_prefix: &str, post_startup_cb: bool, preload: bool, run_time_cache: bool, @@ -127,15 +153,9 @@ fn build_zend_php_ffis( println!("cargo:rerun-if-changed={file}"); } - let output = Command::new("php-config") - .arg("--prefix") - .output() - .expect("Unable to run `php-config`. Is it in your PATH?"); - - let prefix = String::from_utf8(output.stdout).expect("only utf8 chars work"); println!( "cargo:rustc-link-search=native={prefix}/lib", - prefix = prefix.trim() + prefix = php_prefix.trim() ); let files = ["src/php_ffi.c", "../ext/handlers_api.c"]; @@ -392,72 +412,66 @@ fn cfg_php_feature_flags(vernum: u64) { } } -fn cfg_zts() { +fn cfg_zts_from_headers(php_include_dir: &str) { println!("cargo::rustc-check-cfg=cfg(php_zts)"); + if php_header_macros("main/php_config.h", "", php_include_dir, &["ZTS"]) + .get("ZTS") + .copied() + .unwrap_or(0) + != 0 + { + println!("cargo:rustc-cfg=php_zts"); + } +} - let output = Command::new("php-config") - .arg("--include-dir") - .output() - .expect("Unable to run `php-config`. Is it in your PATH?"); +fn php_header_macros( + header: &str, + php_includes: &str, + php_include_dir: &str, + interested: &[&str], +) -> HashMap { + let header_path = Path::new(php_include_dir.trim()).join(header); + println!("cargo:rerun-if-changed={}", header_path.display()); + + let macros = Arc::new(Mutex::new(HashMap::::new())); + let callbacks = MacroCollector { + macros: Arc::clone(¯os), + interested: interested.iter().map(|s| s.to_string()).collect(), + }; - if !output.status.success() { - match String::from_utf8(output.stderr) { - Ok(stderr) => panic!("`php-config --include-dir` failed: {stderr}"), - Err(err) => panic!("`php-config --include-dir` failed, not utf8: {err}"), - } - } + let mut builder = bindgen::Builder::default() + .header(header_path.to_string_lossy().to_string()) + .parse_callbacks(Box::new(callbacks)); - let include_dir = std::str::from_utf8(output.stdout.as_slice()) - .expect("`php-config`'s stdout to be valid utf8") - .trim(); - - // Create a temporary C file to probe ZTS - let out_dir = env::var("OUT_DIR").unwrap(); - let probe_path = Path::new(&out_dir).join("zts_probe.c"); - fs::write( - &probe_path, - r#" -#include "main/php_config.h" -#include -int main() { -#ifdef ZTS - printf("1"); -#else - printf("0"); -#endif - return 0; -} -"#, - ) - .expect("Failed to write ZTS probe file"); - - // Get the C compiler from cc crate - let compiler = cc::Build::new().get_compiler(); - - // Compile the probe to an executable - let probe_exe = Path::new(&out_dir).join("zts_probe"); - let compile_status = Command::new(compiler.path()) - .arg(format!("-I{}", include_dir)) - .arg(&probe_path) - .arg("-o") - .arg(&probe_exe) - .status() - .expect("Failed to compile ZTS probe"); - - if !compile_status.success() { - panic!("Failed to compile ZTS probe"); + for arg in php_includes.split_whitespace() { + builder = builder.clang_arg(arg); + } + if !php_include_dir.trim().is_empty() { + builder = builder.clang_arg(format!("-I{}", php_include_dir.trim())); } - // Run the probe - let probe_output = Command::new(&probe_exe) - .output() - .expect("Failed to run ZTS probe"); + builder + .generate() + .expect("bindgen failed to parse PHP headers"); - let zts_value = std::str::from_utf8(&probe_output.stdout) - .expect("ZTS probe output not UTF-8") - .trim(); + let map = macros.lock().unwrap(); + map.clone() +} - if zts_value == "1" { - println!("cargo:rustc-cfg=php_zts"); +#[derive(Debug)] +struct MacroCollector { + macros: Arc>>, + interested: HashSet, +} + +impl bindgen::callbacks::ParseCallbacks for MacroCollector { + fn int_macro(&self, name: &str, value: i64) -> Option { + if self.interested.contains(name) { + self.macros + .lock() + .expect("macro map lock to succeed") + .insert(name.to_string(), value); + } + None } } diff --git a/profiling/config.m4 b/profiling/config.m4 new file mode 100644 index 00000000000..1b8f8a3199d --- /dev/null +++ b/profiling/config.m4 @@ -0,0 +1,70 @@ +AC_ARG_ENABLE([datadog-profiling], + [AS_HELP_STRING([--enable-datadog-profiling], + [Enable Datadog profiling support])], + [PHP_DATADOG_PROFILING=$enableval], + [PHP_DATADOG_PROFILING=yes]) + +AC_MSG_CHECKING([whether to enable Datadog profiling support]) +AC_MSG_RESULT([$PHP_DATADOG_PROFILING]) + +AC_ARG_ENABLE([datadog-profiling-debug], + [AS_HELP_STRING([--enable-datadog-profiling-debug], + [Build Datadog profiling with the Cargo debug profile])], + [PHP_DATADOG_PROFILING_DEBUG=$enableval], + [PHP_DATADOG_PROFILING_DEBUG=no]) + +PHP_ARG_WITH([datadog-profiling-cargo], + [path to cargo binary], + [AS_HELP_STRING([--with-datadog-profiling-cargo=PATH], + [Path to cargo binary for Rust compilation])], + [cargo], + [no]) + +PHP_ARG_WITH([datadog-profiling-cargo-features], + [cargo feature list], + [AS_HELP_STRING([--with-datadog-profiling-cargo-features=LIST], + [Comma or space-separated Cargo feature list for the profiler crate])], + [], + [no]) + +if test "$PHP_DATADOG_PROFILING" != "no"; then + PHP_ALWAYS_SHARED([PHP_DATADOG_PROFILING]) + AC_DEFINE([HAVE_DATADOG_PROFILING], [1], [Have Datadog profiling support]) + + if test "$PHP_DATADOG_PROFILING_CARGO" != "cargo"; then + if test -x "$PHP_DATADOG_PROFILING_CARGO"; then + DD_PROFILING_CARGO="$PHP_DATADOG_PROFILING_CARGO" + else + AC_MSG_ERROR([$PHP_DATADOG_PROFILING_CARGO is not an executable]) + fi + else + AC_CHECK_TOOL([DD_PROFILING_CARGO], [cargo], [:]) + AS_IF([test "$DD_PROFILING_CARGO" = ":"], + [AC_MSG_ERROR([Please install cargo before configuring, or specify it with --with-datadog-profiling-cargo=])]) + fi + PHP_SUBST([DD_PROFILING_CARGO]) + + if test "$PHP_DATADOG_PROFILING_DEBUG" != "no"; then + DD_PROFILING_CARGO_PROFILE="debug" + DD_PROFILING_CARGO_ARGS="" + else + DD_PROFILING_CARGO_PROFILE="release" + DD_PROFILING_CARGO_ARGS="--release" + fi + PHP_SUBST([DD_PROFILING_CARGO_PROFILE]) + PHP_SUBST([DD_PROFILING_CARGO_ARGS]) + + if test "$PHP_DATADOG_PROFILING_CARGO_FEATURES" = "no"; then + DD_PROFILING_CARGO_FEATURES="" + else + DD_PROFILING_CARGO_FEATURES="$PHP_DATADOG_PROFILING_CARGO_FEATURES" + fi + PHP_SUBST([DD_PROFILING_CARGO_FEATURES]) + + DATADOG_PHP_VERSION_ID="$($PHP_CONFIG --vernum 2>/dev/null)" + AS_VAR_IF([DATADOG_PHP_VERSION_ID],, + [AC_MSG_ERROR([Failed to determine PHP version id (php-config --vernum)])]) + PHP_SUBST([DATADOG_PHP_VERSION_ID]) + + PHP_ADD_MAKEFILE_FRAGMENT([Makefile.frag]) +fi diff --git a/profiling/src/profiling/backtrace.rs b/profiling/src/profiling/backtrace.rs new file mode 100644 index 00000000000..9dfcc359d96 --- /dev/null +++ b/profiling/src/profiling/backtrace.rs @@ -0,0 +1,33 @@ +use crate::profiling::stack_walking::ZendFrame; +use core::ops::Deref; + +#[derive(Debug)] +pub struct Backtrace { + frames: Vec, +} + +impl Backtrace { + pub const fn new(frames: Vec) -> Self { + Self { frames } + } + + pub fn len(&self) -> usize { + self.frames.len() + } + + pub fn is_empty(&self) -> bool { + self.frames.is_empty() + } + + pub fn iter(&self) -> impl Iterator { + self.frames.iter() + } +} + +impl Deref for Backtrace { + type Target = [ZendFrame]; + + fn deref(&self) -> &Self::Target { + self.frames.as_slice() + } +} diff --git a/profiling/src/profiling/mod.rs b/profiling/src/profiling/mod.rs index cc9cc845e60..271b192be54 100644 --- a/profiling/src/profiling/mod.rs +++ b/profiling/src/profiling/mod.rs @@ -1,9 +1,11 @@ +mod backtrace; mod interrupts; mod sample_type_filter; pub mod stack_walking; mod thread_utils; mod uploader; +pub use backtrace::Backtrace; pub use interrupts::*; pub use sample_type_filter::*; pub use stack_walking::*; @@ -170,7 +172,7 @@ pub struct ProfileIndex { #[derive(Debug)] pub struct SampleData { - pub frames: Vec, + pub frames: Backtrace, pub labels: Vec