diff --git a/.azure-pipelines/steps/update-github-pipeline-status.yml b/.azure-pipelines/steps/update-github-pipeline-status.yml index aa830726f312..e85a5e540fa3 100644 --- a/.azure-pipelines/steps/update-github-pipeline-status.yml +++ b/.azure-pipelines/steps/update-github-pipeline-status.yml @@ -39,7 +39,10 @@ stages: - integration_tests_linux_debugger - profiler_integration_tests_windows - profiler_integration_tests_linux + - profiler_integration_tests_arm64 - asan_profiler_tests + - asan_arm64_profiler_tests + - ubsan_arm64_profiler_tests - ubsan_profiler_tests - tsan_profiler_tests - integration_tests_arm64 @@ -118,7 +121,10 @@ stages: in(dependencies.integration_tests_linux_debugger.result, 'Succeeded','SucceededWithIssues','Skipped'), in(dependencies.profiler_integration_tests_windows.result, 'Succeeded','SucceededWithIssues','Skipped'), in(dependencies.profiler_integration_tests_linux.result, 'Succeeded','SucceededWithIssues','Skipped'), + in(dependencies.profiler_integration_tests_arm64.result, 'Succeeded','SucceededWithIssues','Skipped'), in(dependencies.asan_profiler_tests.result, 'Succeeded','SucceededWithIssues','Skipped'), + in(dependencies.asan_arm64_profiler_tests.result, 'Succeeded','SucceededWithIssues','Skipped'), + in(dependencies.ubsan_arm64_profiler_tests.result, 'Succeeded','SucceededWithIssues','Skipped'), in(dependencies.ubsan_profiler_tests.result, 'Succeeded','SucceededWithIssues','Skipped'), in(dependencies.tsan_profiler_tests.result, 'Succeeded','SucceededWithIssues','Skipped'), in(dependencies.integration_tests_arm64.result, 'Succeeded','SucceededWithIssues','Skipped'), @@ -209,7 +215,10 @@ stages: - integration_tests_linux_debugger - profiler_integration_tests_windows - profiler_integration_tests_linux + - profiler_integration_tests_arm64 - asan_profiler_tests + - asan_arm64_profiler_tests + - ubsan_arm64_profiler_tests - ubsan_profiler_tests - tsan_profiler_tests - integration_tests_arm64 @@ -288,7 +297,10 @@ stages: in(dependencies.integration_tests_linux_debugger.result, 'Succeeded','SucceededWithIssues','Skipped'), in(dependencies.profiler_integration_tests_windows.result, 'Succeeded','SucceededWithIssues','Skipped'), in(dependencies.profiler_integration_tests_linux.result, 'Succeeded','SucceededWithIssues','Skipped'), + in(dependencies.profiler_integration_tests_arm64.result, 'Succeeded','SucceededWithIssues','Skipped'), in(dependencies.asan_profiler_tests.result, 'Succeeded','SucceededWithIssues','Skipped'), + in(dependencies.asan_arm64_profiler_tests.result, 'Succeeded','SucceededWithIssues','Skipped'), + in(dependencies.ubsan_arm64_profiler_tests.result, 'Succeeded','SucceededWithIssues','Skipped'), in(dependencies.ubsan_profiler_tests.result, 'Succeeded','SucceededWithIssues','Skipped'), in(dependencies.tsan_profiler_tests.result, 'Succeeded','SucceededWithIssues','Skipped'), in(dependencies.integration_tests_arm64.result, 'Succeeded','SucceededWithIssues','Skipped'), diff --git a/.azure-pipelines/ultimate-pipeline.yml b/.azure-pipelines/ultimate-pipeline.yml index be14376a2801..b0ad693d7b1c 100644 --- a/.azure-pipelines/ultimate-pipeline.yml +++ b/.azure-pipelines/ultimate-pipeline.yml @@ -2907,6 +2907,107 @@ stages: testResultsFiles: profiler/build_data/results/**/*.trx condition: succeededOrFailed() +- stage: profiler_integration_tests_arm64 + condition: > + and( + succeeded(), + or( + eq(variables.isMainOrReleaseBranch, true), + eq(dependencies.generate_variables.outputs['generate_variables_job.generate_variables_step.IsProfilerChanged'], 'True') + ) + ) + dependsOn: [package_arm64, generate_variables, merge_commit_id] + variables: + targetShaId: $[ stageDependencies.merge_commit_id.fetch.outputs['set_sha.sha']] + targetBranch: $[ stageDependencies.merge_commit_id.fetch.outputs['set_sha.branch']] + jobs: + - template: steps/update-github-status-jobs.yml + parameters: + jobs: [Test] + + - job: Test + timeoutInMinutes: 60 #default value + strategy: + matrix: + arm64: + baseImage: debian + artifactSuffix: linux-arm64 + alpine: + baseImage: alpine + artifactSuffix: linux-musl-arm64 + + variables: + IncludeMinorPackageVersions: $[eq(variables.perform_comprehensive_testing, 'true')] + + pool: + name: $(linuxArm64Pool) + + steps: + - template: steps/clone-repo.yml + parameters: + targetShaId: $(targetShaId) + targetBranch: $(targetBranch) + + - template: steps/restore-working-directory.yml + parameters: + artifact: build-$(artifactSuffix)-working-directory + + - template: steps/download-artifact.yml + parameters: + artifact: linux-monitoring-home-$(artifactSuffix) + path: $(monitoringHome) + + - template: steps/download-artifact.yml + parameters: + artifact: linux-profiler-symbols-$(artifactSuffix) + path: $(monitoringHome) + + - template: steps/run-in-docker.yml + parameters: + build: true + baseImage: $(baseImage) + command: "BuildProfilerSamples" + apiKey: $(DD_LOGGER_DD_API_KEY) + retryCountForRunCommand: 3 + + - template: steps/run-in-docker.yml + parameters: + baseImage: $(baseImage) + command: "BuildAndRunProfilerCpuLimitTests" + extraArgs: "--cpus 2 --env CONTAINER_CPUS=1" + apiKey: $(DD_LOGGER_DD_API_KEY) + retryCountForRunCommand: 3 + + - template: steps/run-in-docker.yml + parameters: + baseImage: $(baseImage) + command: "BuildAndRunProfilerCpuLimitTests" + extraArgs: "--cpus 0.5 --env CONTAINER_CPUS=0.5" + apiKey: $(DD_LOGGER_DD_API_KEY) + retryCountForRunCommand: 3 + + - script: | + docker-compose -f docker-compose.yml -p $(DockerComposeProjectName) \ + run --rm \ + -e baseImage=$(baseImage) \ + ProfilerIntegrationTests + displayName: docker-compose run --no-deps ProfilerIntegrationTests + env: + DD_LOGGER_DD_API_KEY: $(ddApiKey) + baseImage: $(baseImage) # for interpolation in the docker-compose file + + - publish: profiler/build_data + artifact: _$(System.StageName)_$(Agent.JobName)_logs_$(System.JobAttempt) + condition: always() + continueOnError: true + + - task: PublishTestResults@2 + displayName: publish test results + inputs: + testResultsFormat: VSTest + testResultsFiles: profiler/build_data/results/**/*.trx + condition: succeededOrFailed() + - stage: asan_profiler_tests #address sanitizer tests condition: > @@ -2992,6 +3093,114 @@ stages: condition: always() continueOnError: false +- stage: asan_arm64_profiler_tests + #address sanitizer tests on arm64 + condition: > + and( + succeeded(), + eq(dependencies.generate_variables.outputs['generate_variables_job.generate_variables_step.IsProfilerChanged'], 'True') + ) + dependsOn: [merge_commit_id, generate_variables] + variables: + targetShaId: $[ stageDependencies.merge_commit_id.fetch.outputs['set_sha.sha']] + targetBranch: $[ stageDependencies.merge_commit_id.fetch.outputs['set_sha.branch']] + jobs: + - template: steps/update-github-status-jobs.yml + parameters: + jobs: [Linux] + + - job: Linux + timeoutInMinutes: 60 + + pool: + name: $(linuxArm64Pool) + + steps: + - template: steps/clone-repo.yml + parameters: + targetShaId: $(targetShaId) + targetBranch: $(targetBranch) + + - template: steps/run-in-docker.yml + parameters: + build: true + baseImage: debian + command: "BuildProfilerAsanTest -Framework net7.0" + apiKey: $(DD_LOGGER_DD_API_KEY) + + - template: steps/run-in-docker.yml + parameters: + build: true + baseImage: debian + command: "BuildProfilerSampleForSanitiserTests -Framework net7.0" + apiKey: $(DD_LOGGER_DD_API_KEY) + + - template: steps/run-in-docker.yml + parameters: + baseImage: debian + command: "RunSampleWithProfilerAsan -Framework net7.0" + apiKey: $(DD_LOGGER_DD_API_KEY) + + - publish: profiler/build_data + displayName: Uploading Address sanitizer test results + artifact: _$(System.StageName)_$(Agent.JobName)_test_results_$(System.JobAttempt) + condition: always() + continueOnError: false + +- stage: ubsan_arm64_profiler_tests + #undefined behavior sanitizer tests on arm64 + condition: > + and( + succeeded(), + eq(dependencies.generate_variables.outputs['generate_variables_job.generate_variables_step.IsProfilerChanged'], 'True') + ) + dependsOn: [merge_commit_id, generate_variables] + variables: + targetShaId: $[ stageDependencies.merge_commit_id.fetch.outputs['set_sha.sha']] + targetBranch: $[ stageDependencies.merge_commit_id.fetch.outputs['set_sha.branch']] + jobs: + - template: steps/update-github-status-jobs.yml + parameters: + jobs: [Linux] + + - job: Linux + timeoutInMinutes: 60 + + pool: + name: $(linuxArm64Pool) + + steps: + - template: steps/clone-repo.yml + parameters: + targetShaId: $(targetShaId) + targetBranch: $(targetBranch) + + - template: steps/run-in-docker.yml + parameters: + build: true + baseImage: debian + command: "BuildProfilerUbsanTest -Framework net7.0" + apiKey: $(DD_LOGGER_DD_API_KEY) + + - template: steps/run-in-docker.yml + parameters: + build: true + baseImage: debian + command: "BuildProfilerSampleForSanitiserTests -Framework net7.0" + apiKey: $(DD_LOGGER_DD_API_KEY) + + - template: steps/run-in-docker.yml + parameters: + baseImage: debian + command: "RunSampleWithProfilerUbsan -Framework net7.0" + apiKey: $(DD_LOGGER_DD_API_KEY) + + - publish: profiler/build_data + displayName: Uploading test results + artifact: _$(System.StageName)_$(Agent.JobName)_logs_$(System.JobAttempt) + condition: always() + continueOnError: false + - stage: ubsan_profiler_tests #undefined behavior sanitizer tests condition: > diff --git a/build/cmake/FindLibunwind.cmake b/build/cmake/FindLibunwind.cmake index 3bdd4ffff9c0..b1bb8e1112cf 100644 --- a/build/cmake/FindLibunwind.cmake +++ b/build/cmake/FindLibunwind.cmake @@ -1,10 +1,29 @@ -SET(LIBUNWIND_VERSION "v1.8.3") +# We intentionally pin to the DataDog/libunwind fork on tag +# gleocadie/v1.8.1-custom-3 rather than upstream libunwind v1.8.1 or v1.8.3. +# The fork carries the following patches that the Continuous Profiler's +# HybridUnwinder depends on (arm64 Linux): +# - unw_cursor_snapshot_t / unw_cursor_snapshot(): a signal-safe public API +# to inspect the dwarf cursor (CFA, loc_fp/loc_lr/loc_sp, frame_type, +# cfa_reg_sp, cfa_reg_offset, dwarf_step_ret, step_method, loc_info). +# Used by UnwinderTracer.h / HybridUnwinder.cpp without having to mirror +# libunwind's internal layouts. +# - unw_init_local2() + UNW_INIT_SIGNAL_FRAME flag: lets us initialize the +# cursor directly from a signal-delivered ucontext_t and flag the first +# frame as a signal frame so libunwind returns the interrupted PC instead +# of the signal-trampoline PC. +# - Additional accessors on the tdep_frame (fp_cfa_offset / lr_cfa_offset / +# sp_cfa_offset, cfa_is_unreliable, next_to_signal_frame) consumed by the +# tracer for diagnostics. +# Upstream libunwind 1.8.3 does not expose these APIs. When they land upstream +# (or when the fork is rebased on top of a newer upstream tag), update this +# file accordingly. +SET(LIBUNWIND_VERSION "v1.8.1-custom-3") SET(LIBUNWIND_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/libunwind-prefix/src/libunwind-build) ExternalProject_Add(libunwind GIT_REPOSITORY https://github.com/DataDog/libunwind.git - GIT_TAG gleocadie/v1.8.3 + GIT_TAG gleocadie/v1.8.1-custom-3 GIT_PROGRESS true INSTALL_COMMAND "" UPDATE_COMMAND "" diff --git a/profiler/src/Demos/Directory.Build.props b/profiler/src/Demos/Directory.Build.props index 06bc6d1f79af..dc34e7dedf00 100644 --- a/profiler/src/Demos/Directory.Build.props +++ b/profiler/src/Demos/Directory.Build.props @@ -1,4 +1,4 @@ - + @@ -7,7 +7,13 @@ net48;netcoreapp3.1;net6.0;net7.0;net8.0;net9.0;net10.0 - netcoreapp3.1;net6.0;net7.0;net8.0;net9.0;net10.0 + netcoreapp3.1;net6.0;net7.0;net8.0;net9.0;net10.0 + + net6.0;net7.0;net8.0;net9.0;net10.0 AnyCPU;x64;x86 diff --git a/profiler/src/Demos/Samples.BuggyBits/Samples.BuggyBits.csproj b/profiler/src/Demos/Samples.BuggyBits/Samples.BuggyBits.csproj index bb0ef4a35992..d9bf5660cab5 100644 --- a/profiler/src/Demos/Samples.BuggyBits/Samples.BuggyBits.csproj +++ b/profiler/src/Demos/Samples.BuggyBits/Samples.BuggyBits.csproj @@ -1,7 +1,9 @@ - + - netcoreapp3.1;net6.0;net7.0;net8.0;net9.0;net10.0 + netcoreapp3.1;net6.0;net7.0;net8.0;net9.0;net10.0 + netcoreapp3.1;net6.0;net7.0;net8.0;net9.0;net10.0 + net6.0;net7.0;net8.0;net9.0;net10.0 AnyCPU;x64;x86 diff --git a/profiler/src/Demos/Samples.Website-AspNetCore01/Samples.Website-AspNetCore01.csproj b/profiler/src/Demos/Samples.Website-AspNetCore01/Samples.Website-AspNetCore01.csproj index 62de37bebb41..4980e5aa7393 100644 --- a/profiler/src/Demos/Samples.Website-AspNetCore01/Samples.Website-AspNetCore01.csproj +++ b/profiler/src/Demos/Samples.Website-AspNetCore01/Samples.Website-AspNetCore01.csproj @@ -1,7 +1,9 @@ - + - netcoreapp3.1;net6.0;net7.0;net8.0;net9.0;net10.0 + netcoreapp3.1;net6.0;net7.0;net8.0;net9.0;net10.0 + netcoreapp3.1;net6.0;net7.0;net8.0;net9.0;net10.0 + net6.0;net7.0;net8.0;net9.0;net10.0 Samples.Website_AspNetCore01 AnyCPU;x64;x86 diff --git a/profiler/src/ProfilerEngine/Datadog.Linux.ApiWrapper/CMakeLists.txt b/profiler/src/ProfilerEngine/Datadog.Linux.ApiWrapper/CMakeLists.txt index b115d6c96037..8253991051c3 100644 --- a/profiler/src/ProfilerEngine/Datadog.Linux.ApiWrapper/CMakeLists.txt +++ b/profiler/src/ProfilerEngine/Datadog.Linux.ApiWrapper/CMakeLists.txt @@ -37,6 +37,10 @@ endif() # ****************************************************** SET(API_WRAPPER_BASENAME Datadog.Linux.ApiWrapper) +# For arm64, we cannot replace the x64 suffix with arm64. +# This library is already shipped and used by customers. Their scripts expect the x64 suffix. +# SSI relies on this file. +# Changing this would bring little value but lots of headaches. SET(API_WRAPPER_SHARED_LIB_NAME ${API_WRAPPER_BASENAME}.x64) SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${DEPLOY_DIR}) diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/Backtrace2Unwinder.cpp b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/Backtrace2Unwinder.cpp index 7f2f13b86ae9..5985ecd41514 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/Backtrace2Unwinder.cpp +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/Backtrace2Unwinder.cpp @@ -8,7 +8,9 @@ Backtrace2Unwinder::Backtrace2Unwinder() = default; -std::int32_t Backtrace2Unwinder::Unwind(void* ctx, std::uintptr_t* buffer, std::size_t bufferSize) const +std::int32_t Backtrace2Unwinder::Unwind(void* ctx, std::uintptr_t* buffer, std::size_t bufferSize, + std::uintptr_t stackBase, std::uintptr_t stackEnd, + UnwinderTracer* tracer) const { // unw_backtrace2 handles the case ctx == nullptr auto* context = reinterpret_cast(ctx); diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/Backtrace2Unwinder.h b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/Backtrace2Unwinder.h index 9ce4b6578449..acdb4ed228a6 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/Backtrace2Unwinder.h +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/Backtrace2Unwinder.h @@ -13,6 +13,8 @@ class Backtrace2Unwinder : public IUnwinder ~Backtrace2Unwinder() override = default; // Returns the number of frames unwound - std::int32_t Unwind(void* ctx, std::uintptr_t* buffer, std::size_t bufferSize) const override; + std::int32_t Unwind(void* ctx, std::uintptr_t* buffer, std::size_t bufferSize, + std::uintptr_t stackBase = 0, std::uintptr_t stackEnd = 0, + UnwinderTracer* tracer = nullptr) const override; -}; \ No newline at end of file +}; diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/CMakeLists.txt b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/CMakeLists.txt index 145d4edb23a3..80f14adb73e3 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/CMakeLists.txt +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/CMakeLists.txt @@ -59,15 +59,15 @@ endif() if (ISLINUX) # ------------------------------------------------------ # Hardening: make sure no target in this project ever - # requests an executable stack. Without this, glibc ≥2.41 - # (Debian 13 "trixie", Fedora 40, etc.) rejects the shared + # requests an executable stack. Without this, glibc >=2.41 + # (Debian 13 "trixie", Fedora 40, etc.) rejects the shared # library with: # "cannot enable executable stack as shared object requires" # ------------------------------------------------------ # 1. Tell the assembler to emit a .note.GNU-stack note that - # marks the object as **non‑exec‑stack**. + # marks the object as **non-exec-stack**. add_compile_options("$<$:-Wa,--noexecstack>") - # 2. Instruct the linker to *clear* any stray exec‑stack flag + # 2. Instruct the linker to *clear* any stray exec-stack flag # that might still be present when it produces the final ELF. add_link_options(-Wl,-z,noexecstack) endif() @@ -90,6 +90,14 @@ SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${DEPLOY_DIR}) FILE(GLOB LINUX_PROFILER_SRC CONFIGURE_DEPENDS "*.cpp") +if (ISARM64) + list(REMOVE_ITEM LINUX_PROFILER_SRC "${CMAKE_CURRENT_SOURCE_DIR}/Backtrace2Unwinder.cpp") +else() + list(REMOVE_ITEM LINUX_PROFILER_SRC "${CMAKE_CURRENT_SOURCE_DIR}/HybridUnwinder.cpp") + list(REMOVE_ITEM LINUX_PROFILER_SRC "${CMAKE_CURRENT_SOURCE_DIR}/UnwinderTracer.cpp") + list(REMOVE_ITEM LINUX_PROFILER_SRC "${CMAKE_CURRENT_SOURCE_DIR}/UnwindTracersProvider.cpp") +endif() + FILE(GLOB COMMON_PROFILER_SRC LIST_DIRECTORIES false "../Datadog.Profiler.Native/*.cpp") FILE(GLOB EXCLUDE_DLLMAIN "../Datadog.Profiler.Native/DllMain.cpp") diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/HybridUnwinder.cpp b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/HybridUnwinder.cpp new file mode 100644 index 000000000000..170aad97edbb --- /dev/null +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/HybridUnwinder.cpp @@ -0,0 +1,295 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc. + +#include "HybridUnwinder.h" +#include "ManagedCodeCache.h" + +#include "UnwinderTracer.h" + +#include "FrameStore.h" + +#define UNW_LOCAL_ONLY +#include + +#ifndef ARM64 +#error "HybridUnwinder is only supported on aarch64" +#endif + +#define UNW_REG_FP UNW_AARCH64_X29 + +static inline bool IsValidFp(uintptr_t fp, uintptr_t prevFp, + uintptr_t stackBase, uintptr_t stackEnd) +{ + if (fp == 0) + return false; + + if (fp % sizeof(void*) != 0) + return false; + + // Ensure the full frame record [fp, fp+16) lies within the stack. + if (fp < stackBase || fp + 2 * sizeof(void*) > stackEnd) + return false; + + // Stack grows down on arm64: FP chain must grow toward higher addresses. + if (prevFp != 0 && fp <= prevFp) + return false; + + return true; +} + +HybridUnwinder::HybridUnwinder(ManagedCodeCache* managedCodeCache) : + _codeCache(managedCodeCache) +{ +} + +// This is temporary workaround to try at best identifying an instruction pointer. +// Once ManagedCodeCache has a better concurrent data structure, we can remove this function. +std::optional HybridUnwinder::IsManaged(uintptr_t ip) const +{ + // best effort to get the managed code address range + // If IsManaged returns nullopt (which means that we failed at acquiring the lock), + // we try 3 times. + const std::size_t MaxRetries = 3; + for (auto i = 0; i < MaxRetries; i++) + { + auto isManaged = _codeCache->IsManaged(ip); + if (isManaged.has_value()) + { + return isManaged; + } + } + return std::nullopt; +} + +struct UnwindCursor +{ + unw_cursor_t cursor; +}; + +bool HybridUnwinder::UnwindNativeFrames(UnwindCursor* cursor, std::uintptr_t* buffer, std::size_t bufferSize, + UnwinderTracer* tracer, std::size_t& i) const +{ + unw_word_t ip = 0; + while (true) + { + if (i >= bufferSize) + { + if (tracer) tracer->RecordFinish(static_cast(i), FinishReason::BufferFull); + return false; + } + + if (auto getResult = unw_get_reg(&cursor->cursor, UNW_REG_IP, &ip); getResult != 0 || ip == 0) + { + if (tracer) tracer->RecordFinish(getResult, FinishReason::FailedGetReg); + return false; + } + + auto isManaged = IsManaged(ip); + if (isManaged.has_value()) + { + if (isManaged.value()) + { + if (tracer) + { + unw_word_t managedFp = 0; + unw_get_reg(&cursor->cursor, UNW_REG_FP, &managedFp); + tracer->Record(EventType::ManagedTransition, ip, managedFp); + } + break; + } + } + else + { + buffer[i++] = FrameStore::FakeUnknownIP; + if (tracer) + { + tracer->RecordFinish(static_cast(i), FinishReason::FailedIsManaged); + } + return false; + } + + if (tracer) + { + unw_word_t sp = 0; + unw_word_t nativeFp = 0; + unw_get_reg(&cursor->cursor, UNW_AARCH64_SP, &sp); + unw_get_reg(&cursor->cursor, UNW_REG_FP, &nativeFp); + tracer->Record(EventType::NativeFrame, ip, nativeFp, sp); + } + + buffer[i++] = ip; + + auto stepResult = unw_step(&cursor->cursor); + unw_cursor_snapshot_t snapshot; + unw_get_cursor_snapshot(&cursor->cursor, &snapshot); + if (tracer) tracer->Record(EventType::LibunwindStep, stepResult, snapshot); + if (stepResult <= 0) + { + if (tracer) tracer->RecordFinish(static_cast(i), FinishReason::FailedLibunwindStep); + return false; + } + } + + return true; +} + +bool HybridUnwinder::UnwindManagedFrames(UnwindCursor* cursor, std::uintptr_t* buffer, std::size_t bufferSize, + UnwinderTracer* tracer, std::size_t& i, + std::uintptr_t stackBase, std::uintptr_t stackEnd) const +{ + if (i >= bufferSize) + { + if (tracer) tracer->RecordFinish(static_cast(i), FinishReason::BufferFull); + return false; + } + + unw_word_t ip = 0; + if (auto result = unw_get_reg(&cursor->cursor, UNW_REG_IP, &ip); result != 0 || ip == 0) + { + if (tracer) tracer->RecordFinish(static_cast(UNW_REG_IP), FinishReason::FailedGetReg); + return false; + } + + buffer[i++] = ip; + + unw_word_t fp = 0; + if (auto result = unw_get_reg(&cursor->cursor, UNW_REG_FP, &fp); result != 0 || !IsValidFp(fp, 0, stackBase, stackEnd)) + { + if (tracer) tracer->RecordFinish(static_cast(i), FinishReason::InvalidFp); + return false; + } + + // For now we do not handle leaf function case. + // This is a TODO: + // The reason is that we may duplicate top frame in some cases. + // Instead, in a follow up PR, we will give unwinding info to the unwinder + // to make the callstack collection more accurate. + + // Walk the FP chain. + // In .NET 10+, user managed code calls throw via 3 native frames before reaching + // the managed RhThrowEx: + // IL_Throw (asm stub) -> IL_Throw_Impl (C++) -> DispatchManagedException (C++) -> RhThrowEx (managed) + // In .NET 9, SoftwareExceptionFrame::Init() additionally calls PAL_VirtualUnwind(), + // which adds 1-3 extra native frames, bringing the total to 5-6. + // We must skip these native frames rather than stopping, or we lose the caller frame. + // The limit of 8 consecutive non-managed frames (6 + 2 margin) stops useless walking + // once we leave the managed portion of the stack entirely (e.g., thread startup code). + uintptr_t prevFp = 0; + int consecutiveNativeFrames = 0; + FinishReason finishReason = FinishReason::Success; + while (true) + { + if (i >= bufferSize) + { + finishReason = FinishReason::BufferFull; + break; + } + + auto ip = *reinterpret_cast(fp + sizeof(void*)); + if (ip == 0) + { + break; + } + + if (tracer) tracer->Record(EventType::FrameChainStep, ip, fp); + + auto isManaged = IsManaged(ip); + if (isManaged.has_value() && isManaged.value()) + { + buffer[i++] = ip; + consecutiveNativeFrames = 0; + } + else + { + static constexpr std::size_t MaxConsecutiveNativeFrames = 8; + // In case we were unable to identify, we assume it's a managed frame + if (!isManaged.has_value()) + { + buffer[i++] = FrameStore::FakeUnknownIP; + } + if (++consecutiveNativeFrames > MaxConsecutiveNativeFrames) + { + finishReason = FinishReason::TooManyNativeFrames; + break; + } + } + + prevFp = fp; + fp = *reinterpret_cast(fp); + if (!IsValidFp(fp, prevFp, stackBase, stackEnd)) + { + finishReason = FinishReason::InvalidFp; + break; + } + } + if (tracer) tracer->RecordFinish(static_cast(i), finishReason); + return true; +} + +std::int32_t HybridUnwinder::Unwind(void* ctx, std::uintptr_t* buffer, std::size_t bufferSize, + uintptr_t stackBase, uintptr_t stackEnd, + UnwinderTracer* tracer) const +{ + if (bufferSize == 0) [[unlikely]] + { + return 0; + } + + if (tracer) tracer->RecordStart(reinterpret_cast(ctx)); + + if (stackBase == 0 || stackEnd == 0) + { + if (tracer) tracer->RecordFinish(0, FinishReason::NoStackBounds); + return 0; + } + + auto* context = reinterpret_cast(ctx); + auto flag = static_cast(UNW_INIT_SIGNAL_FRAME); + + unw_context_t localContext; + if (ctx == nullptr) + { + flag = static_cast(0); + if (auto getResult = unw_getcontext(&localContext) != 0) + { + if (tracer) tracer->RecordFinish(getResult, FinishReason::FailedGetContext); + return -1; + } + context = &localContext; + } + + UnwindCursor unwindCursor{0}; + auto initResult = unw_init_local2(&unwindCursor.cursor, context, flag); + unw_cursor_snapshot_t snapshot= {0}; + unw_get_cursor_snapshot(&unwindCursor.cursor, &snapshot); + if (tracer) tracer->Record(EventType::InitCursor, initResult, snapshot); + if (initResult != 0) + { + if (tracer) tracer->RecordFinish(initResult, FinishReason::FailedInitLocal2); + return -1; + } + + // === Phase 1: Walk native frames with libunwind until managed code is reached === + std::size_t i = 0; + auto keepOnUnwinding = UnwindNativeFrames(&unwindCursor, buffer, bufferSize, tracer, i); + if (!keepOnUnwinding) + { + // already recorded state + return i; + } + + if (i >= bufferSize) + { + if (tracer) tracer->RecordFinish(static_cast(i), FinishReason::BufferFull); + return i; + } + + // === Phase 2: Walk managed frames using the FP chain === + // The .NET JIT on arm64 always emits a frame record [prev_fp, saved_lr] for + // every managed method, so FP chaining is reliable once we enter managed code. + + auto _ = UnwindManagedFrames(&unwindCursor, buffer, bufferSize, tracer, i, stackBase, stackEnd); + + // Already recorded state in tracer + return i; +} diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/HybridUnwinder.h b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/HybridUnwinder.h new file mode 100644 index 000000000000..c795918dba97 --- /dev/null +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/HybridUnwinder.h @@ -0,0 +1,32 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc. + +#pragma once + +#include "IUnwinder.h" + +#include + +class ManagedCodeCache; +class UnwindCursor; + +class HybridUnwinder: public IUnwinder +{ +public: + HybridUnwinder(ManagedCodeCache* managedCodeCache); + ~HybridUnwinder() override = default; + + std::int32_t Unwind(void* ctx, std::uintptr_t* buffer, std::size_t bufferSize, + std::uintptr_t stackBase = 0, std::uintptr_t stackEnd = 0, + UnwinderTracer* tracer = nullptr) const override; + +private: + std::optional IsManaged(std::uintptr_t ip) const; + bool UnwindNativeFrames(UnwindCursor* cursor, std::uintptr_t* buffer, std::size_t bufferSize, + UnwinderTracer* tracer, std::size_t& i) const; + bool UnwindManagedFrames(UnwindCursor* cursor, std::uintptr_t* buffer, std::size_t bufferSize, + UnwinderTracer* tracer, std::size_t& i, + std::uintptr_t stackBase, std::uintptr_t stackEnd) const; + + ManagedCodeCache* _codeCache; +}; diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/IUnwinder.h b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/IUnwinder.h index f339e9be7e75..3ee2fc30ff56 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/IUnwinder.h +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/IUnwinder.h @@ -2,11 +2,15 @@ #include +class UnwinderTracer; + class IUnwinder { public: virtual ~IUnwinder() = default; // Returns the number of frames unwound - virtual std::int32_t Unwind(void* ctx, std::uintptr_t* buffer, std::size_t bufferSize) const = 0; -}; \ No newline at end of file + virtual std::int32_t Unwind(void* ctx, std::uintptr_t* buffer, std::size_t bufferSize, + std::uintptr_t stackBase = 0, std::uintptr_t stackEnd = 0, + UnwinderTracer* tracer = nullptr) const = 0; +}; diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/LinuxStackFramesCollector.cpp b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/LinuxStackFramesCollector.cpp index 2a6f5f001ab3..e87575a1772f 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/LinuxStackFramesCollector.cpp +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/LinuxStackFramesCollector.cpp @@ -27,7 +27,7 @@ using namespace std::chrono_literals; std::mutex LinuxStackFramesCollector::s_stackWalkInProgressMutex; -LinuxStackFramesCollector* LinuxStackFramesCollector::s_pInstanceCurrentlyStackWalking = nullptr; +std::atomic LinuxStackFramesCollector::s_pInstanceCurrentlyStackWalking = nullptr; LinuxStackFramesCollector::LinuxStackFramesCollector( ProfilerSignalManager* signalManager, @@ -101,10 +101,6 @@ StackSnapshotResultBuffer* LinuxStackFramesCollector::CollectStackSampleImplemen { long errorCode; - // If there a timer associated to the managed thread, we have to disarm it. - // Otherwise, the CPU consumption to collect the callstack, will be accounted as "user app CPU time" - auto timerId = pThreadInfo->GetTimerId(); - if (selfCollect) { // In case we are self-unwinding, we do not want to be interrupted by the signal-based profilers (walltime and cpu) @@ -221,7 +217,14 @@ std::int32_t LinuxStackFramesCollector::CollectCallStackCurrentThread(void* ctx) inline std::int32_t LinuxStackFramesCollector::CollectStack(void* ctx) { auto buffer = Data(); - auto count = _pUnwinder->Unwind(ctx, reinterpret_cast(buffer.data()), buffer.size()); + std::uintptr_t stackBase = 0; + std::uintptr_t stackEnd = 0; + auto* threadInfo = _pCurrentCollectionThreadInfo; + if (threadInfo) + { + std::tie(stackBase, stackEnd) = threadInfo->GetStackBounds(); + } + auto count = _pUnwinder->Unwind(ctx, reinterpret_cast(buffer.data()), buffer.size(), stackBase, stackEnd, _tracer); if (count == 0) { diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/LinuxStackFramesCollector.h b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/LinuxStackFramesCollector.h index a6b9bb902fe0..0263b173bbb7 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/LinuxStackFramesCollector.h +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/LinuxStackFramesCollector.h @@ -88,7 +88,7 @@ class LinuxStackFramesCollector : public StackFramesCollectorBase static std::mutex s_stackWalkInProgressMutex; - static LinuxStackFramesCollector* s_pInstanceCurrentlyStackWalking; + static std::atomic s_pInstanceCurrentlyStackWalking; std::int32_t CollectCallStackCurrentThread(void* ctx); @@ -97,4 +97,4 @@ class LinuxStackFramesCollector : public StackFramesCollectorBase std::shared_ptr _discardMetrics; IUnwinder* _pUnwinder; -}; \ No newline at end of file +}; diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/OsSpecificApi.cpp b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/OsSpecificApi.cpp index 8a4756024274..de93c77cd3f3 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/OsSpecificApi.cpp +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/OsSpecificApi.cpp @@ -25,7 +25,12 @@ #include "OpSysTools.h" #include "ScopeFinalizer.h" +#include "IUnwinder.h" +#ifdef ARM64 +#include "HybridUnwinder.h" +#else #include "Backtrace2Unwinder.h" +#endif #include "IConfiguration.h" #include "IThreadInfo.h" #include "LinuxStackFramesCollector.h" @@ -46,6 +51,18 @@ using namespace std::chrono_literals; // not change during the lifetime of the process. static auto ticks_per_second = sysconf(_SC_CLK_TCK); +static IUnwinder* s_pUnwinder = nullptr; + +void InitializeUnwinder(ManagedCodeCache* managedCodeCache) +{ +#ifdef ARM64 + static auto unwinder = std::make_unique(managedCodeCache); +#else + static auto unwinder = std::make_unique(); +#endif + s_pUnwinder = unwinder.get(); +} + std::pair GetLastErrorMessage() { DWORD errorCode = errno; @@ -64,9 +81,8 @@ std::unique_ptr CreateNewStackFramesCollectorInstance( CallstackProvider* callstackProvider, MetricsRegistry& metricsRegistry) { - static auto pUnwinder = std::make_unique(); return std::make_unique( - ProfilerSignalManager::Get(SIGUSR1), pConfiguration, callstackProvider, metricsRegistry, pUnwinder.get()); + ProfilerSignalManager::Get(SIGUSR1), pConfiguration, callstackProvider, metricsRegistry, s_pUnwinder); } // https://linux.die.net/man/5/proc diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/TimerCreateCpuProfiler.cpp b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/TimerCreateCpuProfiler.cpp index 1a7189481b37..ea1fd37a9215 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/TimerCreateCpuProfiler.cpp +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/TimerCreateCpuProfiler.cpp @@ -11,6 +11,9 @@ #include "OpSysTools.h" #include "ProfilerSignalManager.h" #include "IConfiguration.h" +#ifdef ARM64 +#include "UnwindTracersProvider.h" +#endif #include /* Definition of SYS_* constants */ #include @@ -32,7 +35,8 @@ TimerCreateCpuProfiler::TimerCreateCpuProfiler( _pProvider{pProvider}, _samplingInterval{pConfiguration->GetCpuProfilingInterval()}, _nbThreadsInSignalHandler{0}, - _pUnwinder{pUnwinder} + _pUnwinder{pUnwinder}, + _useUnwinderTracer{Log::IsDebugEnabled()} { Log::Info("Cpu profiling interval: ", _samplingInterval.count(), "ms"); Log::Info("timer_create Cpu profiler is enabled"); @@ -254,8 +258,25 @@ bool TimerCreateCpuProfiler::Collect(void* ctx) return false; } + std::uintptr_t stackBase = 0; + std::uintptr_t stackEnd = 0; + if (threadInfo) + { + std::tie(stackBase, stackEnd) = threadInfo->GetStackBounds(); + } + + UnwinderTracer* tracer = nullptr; +#ifdef ARM64 + auto scopedTracer = UnwindTracersProvider::ScopedTracer(nullptr); + if (_useUnwinderTracer) + { + scopedTracer = UnwindTracersProvider::GetInstance().GetTracer(); + tracer = scopedTracer.get(); + } +#endif auto buffer = rawCpuSample->Stack.AsSpan(); - auto count = _pUnwinder->Unwind(ctx, buffer.data(), buffer.size()); + + auto count = _pUnwinder->Unwind(ctx, buffer.data(), buffer.size(), stackBase, stackEnd, tracer); rawCpuSample->Stack.SetCount(count); if (count == 0) diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/TimerCreateCpuProfiler.h b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/TimerCreateCpuProfiler.h index 05150b57a37b..2ea90b233b1b 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/TimerCreateCpuProfiler.h +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/TimerCreateCpuProfiler.h @@ -23,6 +23,7 @@ class IManagedThreadList; class ProfilerSignalManager; class CpuSampleProvider; class IUnwinder; +class UnwinderTracer; class TimerCreateCpuProfiler : public ServiceBase { @@ -63,4 +64,12 @@ class TimerCreateCpuProfiler : public ServiceBase std::shared_ptr _discardMetrics; std::atomic _nbThreadsInSignalHandler; std::unique_ptr _pUnwinder; -}; \ No newline at end of file + // Snapshot of Log::IsDebugEnabled() taken at construction time. This gates + // allocation / use of the UnwinderTracer pool inside the signal handler, + // so we deliberately avoid re-reading it on every sample. This relies on + // CorProfilerCallback::Initialize() calling Log::EnableDebug() BEFORE + // InitializeServices() constructs this profiler; changing that order, or + // toggling the log level at runtime (e.g. through RCM), will not enable + // the tracer path dynamically. + bool _useUnwinderTracer; +}; diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/UnwindTracersProvider.cpp b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/UnwindTracersProvider.cpp new file mode 100644 index 000000000000..a642742927e4 --- /dev/null +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/UnwindTracersProvider.cpp @@ -0,0 +1,113 @@ +#include "UnwindTracersProvider.h" + +#define NB_TRACER 50 + +UnwindTracersProvider::UnwindTracersProvider(std::size_t nbTracers) + : _headTracer(nullptr) +{ + for (std::size_t i = 0; i < nbTracers; ++i) + { + auto* node = new TracerNode(); + node->tracer = new UnwinderTracer(); + node->next.store(_headTracer.load(std::memory_order_relaxed), std::memory_order_relaxed); + _headTracer.store(node, std::memory_order_relaxed); + } +} + +UnwindTracersProvider::~UnwindTracersProvider() +{ + auto* current = _headTracer.load(std::memory_order_relaxed); + while (current != nullptr) + { + auto* next = current->next.load(std::memory_order_relaxed); + delete current->tracer; + delete current; + current = next; + } +} + +UnwindTracersProvider& UnwindTracersProvider::GetInstance() +{ + static UnwindTracersProvider instance(NB_TRACER); + return instance; +} + +UnwindTracersProvider::ScopedTracer UnwindTracersProvider::GetTracer() +{ + return ScopedTracer(this); +} + +UnwindTracersProvider::ScopedTracer::ScopedTracer(UnwindTracersProvider* provider) + : _provider(provider) +{ + if (_provider != nullptr) + { + _node = _provider->AcquireTracer(); + } +} + +UnwindTracersProvider::ScopedTracer::ScopedTracer(ScopedTracer&& other) noexcept + : _provider(other._provider), _node(other._node) +{ + other._provider = nullptr; + other._node = nullptr; +} + +UnwindTracersProvider::ScopedTracer& UnwindTracersProvider::ScopedTracer::operator=(ScopedTracer&& other) noexcept +{ + if (this != &other) + { + if (_provider != nullptr && _node != nullptr) + { + _provider->ReleaseTracer(_node); + } + _provider = other._provider; + _node = other._node; + other._provider = nullptr; + other._node = nullptr; + } + return *this; +} + +UnwindTracersProvider::ScopedTracer::~ScopedTracer() +{ + if (_provider != nullptr && _node != nullptr) + { + _provider->ReleaseTracer(_node); + } +} + +UnwinderTracer* UnwindTracersProvider::ScopedTracer::get() +{ + if (_node == nullptr) + { + return nullptr; + } + return _node->tracer; +} + +UnwindTracersProvider::TracerNode* UnwindTracersProvider::AcquireTracer() +{ + TracerNode* head = _headTracer.load(std::memory_order_acquire); + while (head != nullptr) + { + if (_headTracer.compare_exchange_weak(head, head->next.load(std::memory_order_relaxed), + std::memory_order_acq_rel, std::memory_order_relaxed)) + { + head->next.store(nullptr, std::memory_order_relaxed); + return head; + } + } + return nullptr; +} + +void UnwindTracersProvider::ReleaseTracer(TracerNode* node) +{ + TracerNode* head = _headTracer.load(std::memory_order_relaxed); + node->tracer->Reset(); + do + { + node->next.store(head, std::memory_order_relaxed); + } while (!_headTracer.compare_exchange_weak(head, node, + std::memory_order_release, std::memory_order_relaxed)); +} diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/UnwindTracersProvider.h b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/UnwindTracersProvider.h new file mode 100644 index 000000000000..6b34efda09fa --- /dev/null +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/UnwindTracersProvider.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include + +#include "UnwinderTracer.h" + +class UnwindTracersProvider +{ +public: + ~UnwindTracersProvider(); + + UnwindTracersProvider(UnwindTracersProvider&& other) noexcept = delete; + UnwindTracersProvider(const UnwindTracersProvider& other) = delete; + UnwindTracersProvider& operator=(UnwindTracersProvider&& other) noexcept = delete; + UnwindTracersProvider& operator=(const UnwindTracersProvider& other) = delete; + + static UnwindTracersProvider& GetInstance(); + + struct TracerNode + { + UnwinderTracer* tracer; + std::atomic next; + }; + + struct ScopedTracer + { + ScopedTracer(UnwindTracersProvider* provider); + ~ScopedTracer(); + + // Ownership of _node is unique: a copied ScopedTracer would release the + // same node twice through its destructor, corrupting the free-list. + ScopedTracer(const ScopedTracer&) = delete; + ScopedTracer& operator=(const ScopedTracer&) = delete; + + ScopedTracer(ScopedTracer&& other) noexcept; + ScopedTracer& operator=(ScopedTracer&& other) noexcept; + + UnwinderTracer* get(); + + private: + UnwindTracersProvider* _provider = nullptr; + TracerNode* _node = nullptr; + }; + + ScopedTracer GetTracer(); + +private: + UnwindTracersProvider(std::size_t nbTracers); + TracerNode* AcquireTracer(); + void ReleaseTracer(TracerNode* node); + + friend class ScopedTracer; + std::atomic _headTracer; +}; diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/UnwinderTracer.cpp b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/UnwinderTracer.cpp new file mode 100644 index 000000000000..ee14a87593a5 --- /dev/null +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/UnwinderTracer.cpp @@ -0,0 +1,103 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc. + +#include "UnwinderTracer.h" + +#include +#include + +static const char* EventTypeName(EventType t) +{ + switch (t) + { + case EventType::Start: return "Start"; + case EventType::InitCursor: return "InitCursor"; + case EventType::NativeFrame: return "NativeFrame"; + case EventType::ManagedTransition: return "ManagedTransition"; + case EventType::LibunwindStep: return "LibunwindStep"; + case EventType::FrameChainStep: return "FrameChainStep"; + case EventType::Finish: return "Finish"; + default: return "Unknown"; + } +} + +static const char* FinishReasonName(FinishReason r) +{ + switch (r) + { + case FinishReason::Success: return "Success"; + case FinishReason::BufferFull: return "BufferFull"; + case FinishReason::FailedGetContext: return "FailedGetContext"; + case FinishReason::FailedInitLocal2: return "FailedInitLocal2"; + case FinishReason::FailedGetReg: return "FailedGetReg"; + case FinishReason::FailedLibunwindStep: return "FailedLibunwindStep"; + case FinishReason::NoStackBounds: return "NoStackBounds"; + case FinishReason::InvalidFp: return "InvalidFp"; + case FinishReason::TooManyNativeFrames: return "TooManyNativeFrames"; + case FinishReason::InvalidIp: return "InvalidIp"; + case FinishReason::FailedIsManaged: return "FailedIsManaged"; + default: return "Unknown"; + } +} + +void UnwinderTracer::WriteTo(std::ostream& os) const +{ + auto recorded = RecordedEvents(); + os << "# UnwinderTrace: " << recorded << " events recorded, " + << _totalEvents << " total"; + if (Overflowed()) + os << " (" << (_totalEvents - Capacity) << " discarded)"; + os << "\n"; + + for (std::size_t i = 0; i < recorded; ++i) + { + const auto& e = _entries[i]; + os << "[" << std::setw(3) << i << "] " + << std::left << std::setw(20) << EventTypeName(e.eventType); + + switch (e.eventType) + { + case EventType::Start: + os << " result=" << e.result; + break; + + case EventType::Finish: + os << " result=" << e.result + << " reason=" << FinishReasonName(e.finishReason); + break; + + case EventType::InitCursor: + case EventType::LibunwindStep: + { + const auto& cs = e.cursorSnapshot; + os << " result=" << e.result + << " cursor={ ip=0x" << std::hex << cs.ip + << " cfa=0x" << cs.cfa + << " locFp=0x" << cs.locFp + << " locLr=0x" << cs.locLr + << " locSp=0x" << cs.locSp + << std::dec + << " nextToSignalFrame=" << cs.nextToSignalFrame + << " cfaIsUnreliable=" << cs.cfaIsUnreliable + << " frameType=" << cs.frameType + << " cfaRegSp=" << cs.cfaRegSp + << " cfaRegOffset=" << cs.cfaRegOffset + << " }"; + break; + } + + case EventType::NativeFrame: + os << " ip=0x" << std::hex << e.ip + << " fp=0x" << e.fp + << " sp=0x" << e.sp << std::dec; + break; + + case EventType::ManagedTransition: + case EventType::FrameChainStep: + os << " ip=0x" << std::hex << e.ip + << " fp=0x" << e.fp << std::dec; + break; + } + os << "\n"; + } +} diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/UnwinderTracer.h b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/UnwinderTracer.h new file mode 100644 index 000000000000..c5579e55f972 --- /dev/null +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/UnwinderTracer.h @@ -0,0 +1,196 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#define UNW_LOCAL_ONLY +#include + +// --------------------------------------------------------------------------- +// EventType +// --------------------------------------------------------------------------- +enum class EventType : std::uint8_t +{ + Start = 0, + InitCursor = 1, + NativeFrame = 2, + ManagedTransition = 3, + LibunwindStep = 4, + FrameChainStep = 5, + Finish = 6, +}; + +// --------------------------------------------------------------------------- +// FinishReason -- why Unwind stopped +// --------------------------------------------------------------------------- +enum class FinishReason : std::uint8_t +{ + Success = 0, + BufferFull = 1, + FailedGetContext = 2, + FailedInitLocal2 = 3, + FailedGetReg = 4, + FailedLibunwindStep = 5, + NoStackBounds = 6, + InvalidFp = 7, + TooManyNativeFrames = 8, + InvalidIp = 9, + FailedIsManaged = 10, +}; + +// --------------------------------------------------------------------------- +// CursorSnapshot -- 10 diagnostic fields from libunwind cursor internals +// --------------------------------------------------------------------------- +struct CursorSnapshot +{ + std::uintptr_t ip; + std::uintptr_t cfa; + std::uintptr_t locFp; + std::uintptr_t locLr; + std::uintptr_t locSp; + std::uint32_t nextToSignalFrame; + std::uint32_t cfaIsUnreliable; + + std::int64_t frameType; + std::int64_t cfaRegSp; + std::int64_t cfaRegOffset; + std::int32_t dwarfStepResult; + std::int32_t stepMethod; + std::int32_t locInfo; +}; + +// --------------------------------------------------------------------------- +// TraceEvent +// --------------------------------------------------------------------------- +struct TraceEvent +{ + EventType eventType; + FinishReason finishReason; + std::int32_t result; + std::uintptr_t ip; + std::uintptr_t fp; + std::uintptr_t sp; + CursorSnapshot cursorSnapshot; + ucontext_t* context; +}; + +// --------------------------------------------------------------------------- +// SnapshotCursor -- extract CursorSnapshot from opaque unw_cursor_t +// --------------------------------------------------------------------------- +inline CursorSnapshot SnapshotCursor(const unw_cursor_snapshot_t& snapshot) +{ + CursorSnapshot s; + s.ip = snapshot.ip; + s.cfa = snapshot.cfa; + s.locFp = snapshot.loc_fp; + s.locLr = snapshot.loc_ip; + s.locSp = snapshot.loc_sp; + s.nextToSignalFrame = snapshot.next_to_signal_frame; + s.cfaIsUnreliable = snapshot.cfa_is_unreliable; + s.frameType = snapshot.frame_type; + s.cfaRegSp = snapshot.cfa_reg_sp; + s.cfaRegOffset = snapshot.cfa_reg_offset; + s.dwarfStepResult = snapshot.dwarf_step_ret; + s.stepMethod = snapshot.step_method; + s.locInfo = snapshot.loc_info; + return s; +} + +// --------------------------------------------------------------------------- +// UnwinderTracer +// --------------------------------------------------------------------------- +class UnwinderTracer +{ +public: + void Reset() + { + _totalEvents = 0; + } + + void RecordStart(ucontext_t* context) + { + if (_totalEvents < Capacity) + { + _entries[_totalEvents].eventType = EventType::Start; + _entries[_totalEvents].result = 0; + _entries[_totalEvents].context = context; + } + _totalEvents++; + } + + void Record(EventType eventType, std::int32_t result = 0) + { + if (_totalEvents < Capacity) + { + _entries[_totalEvents].eventType = eventType; + _entries[_totalEvents].result = result; + } + _totalEvents++; + } + + void RecordFinish(std::int32_t result, FinishReason reason) + { + if (_totalEvents < Capacity) + { + _entries[_totalEvents].eventType = EventType::Finish; + _entries[_totalEvents].result = result; + _entries[_totalEvents].finishReason = reason; + } + _totalEvents++; + } + + void Record(EventType eventType, std::int32_t result, const unw_cursor_snapshot_t& cursor) + { + if (_totalEvents < Capacity) + { + _entries[_totalEvents].eventType = eventType; + _entries[_totalEvents].result = result; + _entries[_totalEvents].cursorSnapshot = SnapshotCursor(cursor); + } + _totalEvents++; + } + + void Record(EventType eventType, std::uintptr_t ip, std::uintptr_t fp, std::uintptr_t sp) + { + if (_totalEvents < Capacity) + { + _entries[_totalEvents].eventType = eventType; + _entries[_totalEvents].ip = ip; + _entries[_totalEvents].fp = fp; + _entries[_totalEvents].sp = sp; + } + _totalEvents++; + } + + void Record(EventType eventType, std::uintptr_t ip, std::uintptr_t fp) + { + if (_totalEvents < Capacity) + { + _entries[_totalEvents].eventType = eventType; + _entries[_totalEvents].ip = ip; + _entries[_totalEvents].fp = fp; + } + _totalEvents++; + } + + std::size_t TotalEvents() const { return _totalEvents; } + std::size_t RecordedEvents() const { return std::min(_totalEvents, Capacity); } + bool Overflowed() const { return _totalEvents > Capacity; } + + const TraceEvent* begin() const { return _entries.data(); } + const TraceEvent* end() const { return _entries.data() + RecordedEvents(); } + + void WriteTo(std::ostream& os) const; + +private: + static constexpr std::size_t Capacity = 256; + std::array _entries; + std::size_t _totalEvents = 0; +}; diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Windows/OsSpecificApi.cpp b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Windows/OsSpecificApi.cpp index 8edb240ada9c..040c6b6971db 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Windows/OsSpecificApi.cpp +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native.Windows/OsSpecificApi.cpp @@ -32,6 +32,8 @@ class CallstackProvider; namespace OsSpecificApi { +void InitializeUnwinder(ManagedCodeCache*) {} + // if a system message was not found for the last error code the message will contain GetLastError between () std::pair GetLastErrorMessage() { diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/CorProfilerCallback.cpp b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/CorProfilerCallback.cpp index 95a0e48fb326..acd5c8fe4b1a 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/CorProfilerCallback.cpp +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/CorProfilerCallback.cpp @@ -56,12 +56,18 @@ #include "ThreadsCpuManager.h" #include "WallTimeProvider.h" #ifdef LINUX +#ifdef ARM64 +#include "HybridUnwinder.h" +#include "UnwindTracersProvider.h" +#else #include "Backtrace2Unwinder.h" +#endif #include "ProfilerSignalManager.h" #include "SystemCallsShield.h" #include "TimerCreateCpuProfiler.h" #include "LibrariesInfoCache.h" #include "CpuSampleProvider.h" +#include #endif #include "shared/src/native-src/pal.h" @@ -620,7 +626,16 @@ void CorProfilerCallback::InitializeServices() #ifdef LINUX if (_pConfiguration->IsCpuProfilingEnabled() && _pConfiguration->GetCpuProfilerType() == CpuProfilerType::TimerCreate) { +#ifdef ARM64 + // Initialize the UnwindTracersProvider + if (Log::IsDebugEnabled()) + { + UnwindTracersProvider::GetInstance(); + } + _pUnwinder = std::make_unique(_managedCodeCache.get()); +#else _pUnwinder = std::make_unique(); +#endif // Other alternative in case of crash-at-shutdown, do not register it as a service // we will have to start it by hand (already stopped by hand) _pCpuProfiler = std::make_unique( @@ -1109,6 +1124,16 @@ ULONG STDMETHODCALLTYPE CorProfilerCallback::GetRefCount() const return refCount; } +// Rationale for no_sanitize("vptr") on the three CorProfilerCallback entry +// points below (InspectRuntimeCompatibility, InspectRuntimeVersion, Initialize): +// on arm64 Linux the CLR exports typeinfo for ProfToEEInterfaceImpl, so UBSAN's +// vptr check fires on calls through COM interface pointers (e.g. QueryInterface). +// On x86_64 the CLR does not export that typeinfo, so the check is silently +// skipped there. We deliberately scope the suppression to clang + arm64 so +// that x86_64 builds still benefit from the full UBSAN coverage. +#if defined(__clang__) && defined(ARM64) +__attribute__((no_sanitize("vptr"))) +#endif void CorProfilerCallback::InspectRuntimeCompatibility(IUnknown* corProfilerInfoUnk, uint16_t& runtimeMajor, uint16_t& runtimeMinor) { runtimeMajor = 0; @@ -1293,6 +1318,9 @@ void CorProfilerCallback::InspectProcessorInfo() #endif } +#if defined(__clang__) && defined(ARM64) +__attribute__((no_sanitize("vptr"))) +#endif void CorProfilerCallback::InspectRuntimeVersion(ICorProfilerInfo5* pCorProfilerInfo, USHORT& major, USHORT& minor, COR_PRF_RUNTIME_TYPE& runtimeType) { USHORT clrInstanceId; @@ -1387,6 +1415,9 @@ void CorProfilerCallback::PrintEnvironmentVariables() const uint32_t InformationalVerbosity = 4; const uint32_t VerboseVerbosity = 5; +#if defined(__clang__) && defined(ARM64) +__attribute__((no_sanitize("vptr"))) +#endif HRESULT STDMETHODCALLTYPE CorProfilerCallback::Initialize(IUnknown* corProfilerInfoUnk) { Log::Info("CorProfilerCallback is initializing."); @@ -1449,6 +1480,13 @@ HRESULT STDMETHODCALLTYPE CorProfilerCallback::Initialize(IUnknown* corProfilerI COR_PRF_RUNTIME_TYPE runtimeType; CorProfilerCallback::InspectRuntimeVersion(_pCorProfilerInfo, major, minor, runtimeType); +#if !defined(_WINDOWS) && defined(ARM64) + if (major < 5) + { + Log::Warn("The Continuous Profiler has been disabled on arm64 Linux: .NET 5.0 or greater is required."); + return E_FAIL; + } +#endif // We only need to get the complete version for .NET Framework // For the other runtimes, no need to wait for mscorlib to be loaded if (runtimeType != COR_PRF_DESKTOP_CLR) @@ -1546,6 +1584,8 @@ HRESULT STDMETHODCALLTYPE CorProfilerCallback::Initialize(IUnknown* corProfilerI } } + OsSpecificApi::InitializeUnwinder(_managedCodeCache.get()); + // create services without starting them InitializeServices(); @@ -1790,6 +1830,18 @@ HRESULT STDMETHODCALLTYPE CorProfilerCallback::Shutdown() // The aggregator must be stopped before the provider, since it will call them to get the last samples _pStackSamplerLoopManager->Stop(); + +#ifdef LINUX +if (_pCpuProfiler != nullptr) +{ + // if we failed at stopping the time_create-based CPU profiler, + // it's safer to not release the memory. + // Otherwise, we might crash the application. + // Reason: one thread could be executing the signal handler and accessing some field + auto success = _pCpuProfiler->Stop(); + LogServiceStop(success, _pCpuProfiler->GetName()); +} +#endif // TODO: maybe move the following 2 lines AFTER stopping the providers // --> to ensure that the last samples are collected _pSamplesCollector->Stop(); @@ -2257,7 +2309,23 @@ HRESULT STDMETHODCALLTYPE CorProfilerCallback::ThreadAssignedToOSThread(ThreadID _pManagedThreadList->SetThreadOsInfo(managedThreadId, osThreadId, dupOsThreadHandle); #ifdef LINUX - // This call must be made *after* we assigne the SetThreadOsInfo function call. + { + pthread_attr_t attr; + if (pthread_getattr_np(pthread_self(), &attr) == 0) + { + void* stackAddr; + size_t stackSize; + if (pthread_attr_getstack(&attr, &stackAddr, &stackSize) == 0) + { + auto stackBase = reinterpret_cast(stackAddr); + auto stackEnd = stackBase + stackSize; + threadInfo->SetStackBounds(stackBase, stackEnd); + } + pthread_attr_destroy(&attr); + } + } + + // This call must be made *after* we assign the SetThreadOsInfo function call. // Otherwise the threadInfo won't have it's OsThread field set and timer_create // will have random behavior. if (_pCpuProfiler != nullptr) @@ -2277,16 +2345,20 @@ HRESULT STDMETHODCALLTYPE CorProfilerCallback::ThreadAssignedToOSThread(ThreadID } // TL;DR prevent the profiler from deadlocking application thread on malloc - // Backtrace2Unwinder relies on libunwind. We need to call it to make sure + // The unwinder relies on libunwind. We need to call it to make sure // libunwind allocates and initializes TLS (Thread Local Storage) data structures for the current // thread. // Initialization of TLS object does call malloc. Unfortunately, if those calls to malloc // occurs in our profiler signal handler, we end up deadlocking the application. - // To prevent that, we call unw_backtrace here for the current thread, to force libunwind + // To prevent that, we call the unwinder here for the current thread, to force libunwind // initializing the TLS'd data structures for the current thread. - Backtrace2Unwinder bt2; +#ifdef ARM64 + HybridUnwinder warmup(_managedCodeCache.get()); +#else + Backtrace2Unwinder warmup; +#endif uintptr_t tab[1]; - bt2.Unwind(nullptr, tab, 1); + warmup.Unwind(nullptr, tab, 1); // check if SIGUSR1 signal is blocked for current thread sigset_t currentMask; diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/DebugInfoStore.h b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/DebugInfoStore.h index bdf63b294c3d..0df24a30014a 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/DebugInfoStore.h +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/DebugInfoStore.h @@ -71,7 +71,7 @@ class DebugInfoStore : public IDebugInfoStore public: DebugInfoStore(ICorProfilerInfo4* profilerInfo, IConfiguration* configuration) noexcept; - SymbolDebugInfo Get(ModuleID moduleId, mdMethodDef methodDef); + SymbolDebugInfo Get(ModuleID moduleId, mdMethodDef methodDef) override; // Memory measurement (IMemoryFootprintProvider) size_t GetMemorySize() const override; diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/DllMain.cpp b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/DllMain.cpp index f6b671d78767..655ea293fb81 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/DllMain.cpp +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/DllMain.cpp @@ -140,11 +140,6 @@ extern "C" HRESULT STDMETHODCALLTYPE DllGetClassObject(REFCLSID rclsid, REFIID r return CORPROF_E_PROFILER_CANCEL_ACTIVATION; } -#ifdef ARM64 - Log::Warn("Profiler is deactivated because it runs on an unsupported architecture."); - return CORPROF_E_PROFILER_CANCEL_ACTIVATION; -#endif - CorProfilerCallbackFactory* factory = new CorProfilerCallbackFactory(std::move(configuration)); if (factory == nullptr) { diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/FrameStore.cpp b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/FrameStore.cpp index 72e4370272fb..daf8a049955c 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/FrameStore.cpp +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/FrameStore.cpp @@ -77,7 +77,7 @@ std::optional> FrameStore::GetFunctionFromIP(uint std::pair FrameStore::GetFrame(uintptr_t instructionPointer) { static const std::string NotResolvedModuleName("NotResolvedModule"); - static const std::string NotResolvedFrame("NotResolvedFrame"); + static const std::string NotResolvedFrame("|lm:Unknown-Assembly |ns: |ct:Unknown-Type |cg: |fn:NotResolvedFrame |fg: |sg:(?)"); static const std::string UnloadedModuleName("UnloadedModule"); static const std::string FakeModuleName("FakeModule"); @@ -111,12 +111,17 @@ std::pair FrameStore::GetFrame(uintptr_t instructionPointer std::optional> result = GetFunctionFromIP(instructionPointer); if (!result.has_value()) { + // Windows-only: GetFunctionFromIP was wrapped in __try/__except and caught an + // SEH exception coming out of the CLR. Surface the frame as resolved + // (isResolved=true) so the existing Windows pipeline keeps its placeholder + // frame rather than silently dropping it. return {true, {NotResolvedModuleName, NotResolvedFrame, "", 0}}; } std::tie(hr, functionId) = result.value(); - // if native frame if (FAILED(hr)) { + // IP is not in managed ranges (native frame). Return isResolved=false so + // RawSampleTransformer drops it from the final callstack. return {false, {NotResolvedModuleName, NotResolvedFrame, "", 0}}; } } @@ -126,16 +131,18 @@ std::pair FrameStore::GetFrame(uintptr_t instructionPointer if (!functionId.has_value()) { - return {false, {NotResolvedModuleName, NotResolvedFrame, "", 0}}; + // Windows-only: the ICorProfilerInfo::GetFunctionFromIP call inside + // ManagedCodeCache was wrapped in __try/__except and caught an SEH + // exception from the CLR. Keep isResolved=true so the Windows pipeline + // preserves the placeholder frame (legacy semantic). + return {true, {NotResolvedModuleName, NotResolvedFrame, "", 0}}; } if (functionId.value() == ManagedCodeCache::InvalidFunctionId) { - // We have a value but not a valid one. This is fake function ID. - // This can occur when the calling into the CLR from managed code cache - // resulted in a crash(lucky us on windows, we can catch on linux ....:grimacing:) - // This is to preserve the current semantic - return {true, {NotResolvedModuleName, NotResolvedFrame, "", 0}}; + // IP is not in managed ranges (native frame). Return isResolved=false so + // RawSampleTransformer drops it from the final callstack. + return {false, {NotResolvedModuleName, NotResolvedFrame, "", 0}}; } } diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/ManagedCodeCache.cpp b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/ManagedCodeCache.cpp index 41454fc275b3..5f6535f8be53 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/ManagedCodeCache.cpp +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/ManagedCodeCache.cpp @@ -69,23 +69,38 @@ bool ManagedCodeCache::Initialize() return false; } -bool ManagedCodeCache::IsCodeInR2RModule(std::uintptr_t ip) const noexcept +std::optional ManagedCodeCache::IsCodeInR2RModule(std::uintptr_t ip, bool signalSafe) const noexcept { - std::shared_lock moduleLock(_modulesMutex); + // IsCodeInR2RModule can be called in a signal handler or not. + // If it's called in a signal handler, we need to use a shared lock with try_to_lock. + // If it's called not in a signal handler, we need to use a shared lock with defer_lock. + auto moduleLock = [](std::shared_mutex& mutex, bool signalSafe) { + if (signalSafe) + { + return std::shared_lock(mutex, std::try_to_lock); + } + return std::shared_lock(mutex); + }(_modulesMutex, signalSafe); + + if (!moduleLock.owns_lock()) + { + return std::nullopt; + } auto moduleCodeRange = FindRange(_modulesCodeRanges, ip); if (!moduleCodeRange.has_value()) { - return false; + return {false}; } if (moduleCodeRange->isRemoved) { - LogOnce(Debug, "ManagedCodeCache::IsCodeInR2RModule: Module code range was removed for ip: 0x", std::hex, ip); - return false; + // No print, can be called in a signal handler + // LogOnce(Debug, "ManagedCodeCache::IsCodeInR2RModule: Module code range was removed for ip: 0x", std::hex, ip); + return {false}; } - return moduleCodeRange->contains(ip); + return {moduleCodeRange->contains(ip)}; } // must not be called in a signal handler (GetFunctionFromIP is not signal-safe) @@ -100,26 +115,32 @@ std::optional ManagedCodeCache::GetFunctionId(std::uintptr_t ip) noe // Level 2: Check if the IP is within a module code range - if (IsCodeInR2RModule(ip)) - { - auto functionId = GetFunctionFromIP_Original(ip); - if (functionId.has_value()) { - // We found a function id and we can add it synchronously to our cache. - // It's safe to do this synchronously because this function is called - // by a native thread belonging to profiler. - // This thread won't be interrupted by the profiler. - AddFunctionImpl(functionId.value(), false); - return std::optional(functionId.value()); - } - // If we arrive here, it means that the call to GetFunctionFromIP_Original possibly crashed. - // Possible reason: race against the CLR unloading the module containing the function. - // On Windows, we catch the exception and return nullopt. - // On Linux, we cannot do anything. - // Maybe a better value ? + auto isR2r = IsCodeInR2RModule(ip, false); + // GetFunctionId is NOT signal-safe: we pass signalSafe=false, so + // IsCodeInR2RModule takes the shared lock unconditionally and always + // returns an engaged optional. The !has_value() guard below is defence + // in depth in case IsCodeInR2RModule ever grows a new failure path. + if (!isR2r.has_value() || !isR2r.value()) + { + // if it has value `false`, just return InvalidFunctionId return std::optional(InvalidFunctionId); } - return std::nullopt; + auto functionId = GetFunctionFromIP_Original(ip); + if (functionId.has_value() && functionId.value() != InvalidFunctionId) { + // We found a function id and we can add it synchronously to our cache. + // It's safe to do this synchronously because this function is called + // by a native thread belonging to profiler. + // This thread won't be interrupted by the profiler. + AddFunctionImpl(functionId.value(), false); + return std::optional(functionId.value()); + } + // If we arrive here, it means that the call to GetFunctionFromIP_Original possibly crashed. + // Possible reason: race against the CLR unloading the module containing the function. + // On Windows, we catch the exception and return nullopt. + // On Linux, we cannot do anything, we'll never get there. + + return functionId; } std::optional ManagedCodeCache::GetFunctionFromIP_Original(std::uintptr_t ip) noexcept @@ -128,7 +149,11 @@ std::optional ManagedCodeCache::GetFunctionFromIP_Original(std::uint // On Windows, the call to GetFunctionFromIP can crash: // We may end up in a situation where the module containing that symbol was just unloaded. - // For linux, we use the custom GetFunctionFromIP based on the code cache. + // For linux, we use the custom GetFunctionFromIP based on the code cache + + // Cannot return while in __try/__except (compilation error) + // We need a flag to know if an access violation exception was raised. + bool wasAccessViolationRaised = false; #ifdef _WINDOWS __try { @@ -143,10 +168,15 @@ std::optional ManagedCodeCache::GetFunctionFromIP_Original(std::uint { // we could return a fake function id to display a good'ish callstack shape // add a metric ? + wasAccessViolationRaised = true; } #endif - return std::nullopt; + if (wasAccessViolationRaised) + { + return std::nullopt; + } + return std::optional(InvalidFunctionId); } std::optional ManagedCodeCache::GetFunctionIdImpl(std::uintptr_t ip) const noexcept @@ -173,28 +203,36 @@ std::optional ManagedCodeCache::GetFunctionIdImpl(std::uintptr_t ip) } // can be called in a signal handler -bool ManagedCodeCache::IsManaged(std::uintptr_t ip) const noexcept +std::optional ManagedCodeCache::IsManaged(std::uintptr_t ip) const noexcept { uint64_t page = GetPageNumber(static_cast(ip)); { // Level 1: Find the page (shared lock on map structure) - std::shared_lock mapLock(_pagesMutex); + std::shared_lock mapLock(_pagesMutex, std::try_to_lock); + if (!mapLock.owns_lock()) + { + return std::nullopt; + } auto pageIt = _pagesMap.find(page); if (pageIt != _pagesMap.end()) - { + { // Level 2: Binary search within the page's ranges (shared lock on page) - std::shared_lock pageLock(pageIt->second.lock); + std::shared_lock pageLock(pageIt->second.lock, std::try_to_lock); + if (!pageLock.owns_lock()) + { + return std::nullopt; + } auto range = FindRange(pageIt->second.ranges, static_cast(ip)); if (range.has_value()) { - return true; + return std::optional{true}; } } } - // Check if the IP is within a module code range - return IsCodeInR2RModule(ip); + // Page not found or IP not in any JIT-compiled range: check R2R modules + return IsCodeInR2RModule(ip, true); } void ManagedCodeCache::AddFunction(FunctionID functionId) @@ -521,4 +559,4 @@ std::vector ManagedCodeCache::GetModuleCodeRanges(ModuleID modu } } return result; -} \ No newline at end of file +} diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/ManagedCodeCache.h b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/ManagedCodeCache.h index a7810620013d..5d36ed1097c7 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/ManagedCodeCache.h +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/ManagedCodeCache.h @@ -84,7 +84,7 @@ class ManagedCodeCache { ~ManagedCodeCache(); // Signal-safe lookup methods (no allocation) - [[nodiscard]] bool IsManaged(std::uintptr_t ip) const noexcept; + [[nodiscard]] std::optional IsManaged(std::uintptr_t ip) const noexcept; // Not signal-safe [[nodiscard]] std::optional GetFunctionId(std::uintptr_t ip) noexcept; @@ -134,11 +134,25 @@ class ManagedCodeCache { // Append new ranges to the cache (accumulative - never removes old ranges) // This preserves old tier code that might still be on the stack void AddFunctionRangesToCache(std::vector newRanges); + +// Expose the helpers below to tests without duplicating the declarations. #ifdef DD_TEST public: #endif void AddModuleRangesToCache(std::vector moduleCodeRanges); #ifdef DD_TEST + // Test-only hooks to simulate signal-handler contention. IsManaged uses + // try_to_lock semantics on these two mutexes and returns std::nullopt when + // ownership cannot be acquired. Holding an exclusive lock on either mutex + // from another thread deterministically reproduces that "contended" state. + std::unique_lock LockPagesMutexExclusiveForTest() + { + return std::unique_lock(_pagesMutex); + } + std::unique_lock LockModulesMutexExclusiveForTest() + { + return std::unique_lock(_modulesMutex); + } private: #endif void AddModuleCodeRangesAsync(std::vector moduleCodeRanges); @@ -173,7 +187,7 @@ class ManagedCodeCache { template void EnqueueWork(WorkType work); std::optional GetFunctionIdImpl(std::uintptr_t ip) const noexcept; - bool IsCodeInR2RModule(std::uintptr_t ip) const noexcept; + std::optional IsCodeInR2RModule(std::uintptr_t ip, bool signalSafe) const noexcept; std::optional GetFunctionFromIP_Original(std::uintptr_t ip) noexcept; void AddFunctionImpl(FunctionID functionId, bool isAsync); diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/ManagedThreadInfo.h b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/ManagedThreadInfo.h index 34802e7f806c..c95c229a179e 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/ManagedThreadInfo.h +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/ManagedThreadInfo.h @@ -102,6 +102,8 @@ struct ManagedThreadInfo : public IThreadInfo inline int32_t SetTimerId(int32_t timerId); inline int32_t GetTimerId() const; inline bool CanBeInterrupted() const; + inline void SetStackBounds(std::uintptr_t stackBase, std::uintptr_t stackEnd); + inline std::pair GetStackBounds() const; #endif #ifdef DD_TEST @@ -170,6 +172,8 @@ struct ManagedThreadInfo : public IThreadInfo // doing a syscalls. volatile int* _sharedMemoryArea; std::int32_t _timerId; + std::uintptr_t _stackBase = 0; + std::uintptr_t _stackEnd = 0; #endif uint64_t _blockingThreadId; shared::WSTRING _blockingThreadName; @@ -430,6 +434,17 @@ inline std::int32_t ManagedThreadInfo::GetTimerId() const { return _timerId; } + +inline void ManagedThreadInfo::SetStackBounds(std::uintptr_t stackBase, std::uintptr_t stackEnd) +{ + _stackBase = stackBase; + _stackEnd = stackEnd; +} + +inline std::pair ManagedThreadInfo::GetStackBounds() const +{ + return {_stackBase, _stackEnd}; +} #endif inline AppDomainID ManagedThreadInfo::GetAppDomainId() diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/OsSpecificApi.h b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/OsSpecificApi.h index 59ddfb58d70e..ab14de35d833 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/OsSpecificApi.h +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/OsSpecificApi.h @@ -22,11 +22,14 @@ class IAllocationsListener; class IContentionListener; class IGCSuspensionsListener; class CallstackProvider; +class ManagedCodeCache; // Those functions must be defined in the main projects (Linux and Windows) // Here are forward declarations to avoid hard coupling namespace OsSpecificApi { + void InitializeUnwinder(ManagedCodeCache* managedCodeCache); + std::unique_ptr CreateNewStackFramesCollectorInstance( ICorProfilerInfo4* pCorProfilerInfo, IConfiguration const* pConfiguration, diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/StackFramesCollectorBase.cpp b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/StackFramesCollectorBase.cpp index 9b98d79cde86..09bad3dcf681 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/StackFramesCollectorBase.cpp +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/StackFramesCollectorBase.cpp @@ -174,18 +174,21 @@ void StackFramesCollectorBase::ResumeTargetThreadIfRequired(ManagedThreadInfo* p ResumeTargetThreadIfRequiredImplementation(pThreadInfo, isTargetThreadSuspended, pErrorCodeHR); } -StackSnapshotResultBuffer* StackFramesCollectorBase::CollectStackSample(ManagedThreadInfo* pThreadInfo, uint32_t* pHR) +StackSnapshotResultBuffer* StackFramesCollectorBase::CollectStackSample(ManagedThreadInfo* pThreadInfo, uint32_t* pHR, UnwinderTracer* tracer) { // Update state with the info for the thread that we are collecting: _pCurrentCollectionThreadInfo = pThreadInfo; const auto currentThreadId = OpSysTools::GetThreadId(); + _tracer.store(tracer); + // Execute the actual collection: StackSnapshotResultBuffer* result = CollectStackSampleImplementation(pThreadInfo, pHR, pThreadInfo->GetOsThreadId() == currentThreadId); // No longer collecting the specified thread: _pCurrentCollectionThreadInfo = nullptr; + _tracer.store(nullptr); // If someone has requested an abort, notify them now: diff --git a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/StackFramesCollectorBase.h b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/StackFramesCollectorBase.h index 4eb89776993c..37ebf1a14de8 100644 --- a/profiler/src/ProfilerEngine/Datadog.Profiler.Native/StackFramesCollectorBase.h +++ b/profiler/src/ProfilerEngine/Datadog.Profiler.Native/StackFramesCollectorBase.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include @@ -13,6 +14,7 @@ #include "StackSnapshotResultBuffer.h" class IConfiguration; +class UnwinderTracer; class StackFramesCollectorBase { @@ -49,11 +51,12 @@ class StackFramesCollectorBase void PrepareForNextCollection(); bool SuspendTargetThread(ManagedThreadInfo* pThreadInfo, bool* pIsTargetThreadSuspended); void ResumeTargetThreadIfRequired(ManagedThreadInfo* pThreadInfo, bool isTargetThreadSuspended, uint32_t* pErrorCodeHR); - StackSnapshotResultBuffer* CollectStackSample(ManagedThreadInfo* pThreadInfo, uint32_t* pHR); + StackSnapshotResultBuffer* CollectStackSample(ManagedThreadInfo* pThreadInfo, uint32_t* pHR, UnwinderTracer* tracer = nullptr); protected: ManagedThreadInfo* _pCurrentCollectionThreadInfo; CallstackProvider* _callstackProvider; + std::atomic _tracer{nullptr}; private: std::unique_ptr _pStackSnapshotResult; diff --git a/profiler/test/Datadog.Profiler.IntegrationTests/Exceptions/ExceptionsTest.cs b/profiler/test/Datadog.Profiler.IntegrationTests/Exceptions/ExceptionsTest.cs index 37a92838a13a..af01c9bb0a65 100644 --- a/profiler/test/Datadog.Profiler.IntegrationTests/Exceptions/ExceptionsTest.cs +++ b/profiler/test/Datadog.Profiler.IntegrationTests/Exceptions/ExceptionsTest.cs @@ -113,7 +113,9 @@ public void ThrowExceptionsInParallel(string appName, string framework, string a total += sample.Count; sample.Type.Should().Be("System.Exception"); sample.Message.Should().BeEmpty(); - Assert.True(sample.Stacktrace.EndWith(expectedStack)); + Assert.True( + sample.Stacktrace.EndWith(expectedStack), + $"Stacktrace does not end with expected frames.\nExpected ({expectedStack.FramesCount} frames):\n{expectedStack}\nActual ({sample.Stacktrace.FramesCount} frames):\n{sample.Stacktrace}"); } foreach (var file in Directory.GetFiles(runner.Environment.LogDir)) @@ -221,7 +223,9 @@ public void ThrowExceptionsInParallelWithCustomGetFunctionFromIp(string appName, total += sample.Count; sample.Type.Should().Be("System.Exception"); sample.Message.Should().BeEmpty(); - Assert.True(sample.Stacktrace.EndWith(expectedStack)); + Assert.True( + sample.Stacktrace.EndWith(expectedStack), + $"Stacktrace does not end with expected frames.\nExpected ({expectedStack.FramesCount} frames):\n{expectedStack}\nActual ({sample.Stacktrace.FramesCount} frames):\n{sample.Stacktrace}"); } foreach (var file in Directory.GetFiles(runner.Environment.LogDir)) @@ -295,7 +299,9 @@ public void ThrowExceptionsInParallelWithNewCpuProfiler(string appName, string f { labels.Should().ContainSingle(x => x.Name == "exception type" && x.Value == "System.Exception"); labels.Should().ContainSingle(x => x.Name == "exception message" && string.IsNullOrWhiteSpace(x.Value)); - Assert.True(stackTrace.EndWith(expectedStack)); + Assert.True( + stackTrace.EndWith(expectedStack), + $"Stacktrace does not end with expected frames.\nExpected ({expectedStack.FramesCount} frames):\n{expectedStack}\nActual ({stackTrace.FramesCount} frames):\n{stackTrace}"); } foreach (var file in Directory.GetFiles(runner.Environment.LogDir)) diff --git a/profiler/test/Datadog.Profiler.IntegrationTests/Helpers/EnvironmentHelper.cs b/profiler/test/Datadog.Profiler.IntegrationTests/Helpers/EnvironmentHelper.cs index a46ee4b2cee4..96f709ecffd4 100644 --- a/profiler/test/Datadog.Profiler.IntegrationTests/Helpers/EnvironmentHelper.cs +++ b/profiler/test/Datadog.Profiler.IntegrationTests/Helpers/EnvironmentHelper.cs @@ -81,7 +81,12 @@ public static bool IsRunningOnWindows() public static string GetPlatform() { - return Environment.Is64BitProcess ? "x64" : "x86"; + return RuntimeInformation.ProcessArchitecture switch + { + Architecture.Arm64 => "ARM64", + Architecture.X86 => "x86", + _ => "x64", + }; } public static bool IsRunningInCi() => @@ -352,8 +357,8 @@ private static string GetArchitectureSubfolder(bool isAlpine) ("win", "x86", _) => "win-x86", ("linux", "x64", false) => "linux-x64", ("linux", "x64", true) => "linux-musl-x64", - ("linux", "Arm64", false) => "linux-arm64", - ("linux", "Arm64", true) => "linux-musl-arm64", + ("linux", "ARM64", false) => "linux-arm64", + ("linux", "ARM64", true) => "linux-musl-arm64", ("osx", _, _) => "osx-x64", _ => throw new PlatformNotSupportedException() }; diff --git a/profiler/test/Datadog.Profiler.Native.Tests/FrameStoreTest.cpp b/profiler/test/Datadog.Profiler.Native.Tests/FrameStoreTest.cpp new file mode 100644 index 000000000000..17e6dab945b9 --- /dev/null +++ b/profiler/test/Datadog.Profiler.Native.Tests/FrameStoreTest.cpp @@ -0,0 +1,107 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc. + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "FrameStore.h" +#include "ManagedCodeCache.h" +#include "MockProfilerInfo.h" + +#include +#include + +using namespace testing; + +namespace { + +// The string produced by FrameStore::GetFrame when an IP cannot be resolved to a managed +// function (see FrameStore.cpp). Kept in sync with the private constant in the .cpp file. +// If the production string changes, update this constant so the test intent stays explicit. +constexpr const char* NotResolvedFrameText = + "|lm:Unknown-Assembly |ns: |ct:Unknown-Type |cg: |fn:NotResolvedFrame |fg: |sg:(?)"; + +} // namespace + +// These tests guard the contract that FrameStore::GetFrame uses to tell +// RawSampleTransformer whether a frame is resolved (kept) or not (dropped): +// - pair => keep the frame in the sample +// - pair => drop the frame from the sample +// +// Native instruction pointers (addresses that don't belong to any managed method) +// MUST be reported as "not resolved" (false) so that they are filtered out by the +// transformer. Regressing this behavior causes long runs of "NotResolvedFrame" +// entries at the top of exception/walltime profiles on platforms that unwind +// native frames before reaching managed frames (notably Linux ARM64, where the +// HybridUnwinder walks native frames via libunwind before transitioning to +// managed frames via frame-pointer unwinding). +// +// Two parallel code paths exist in FrameStore::GetFrame: +// * no ManagedCodeCache: asks ICorProfilerInfo::GetFunctionFromIP directly +// * with ManagedCodeCache: asks the cache for the FunctionID +// Both must behave the same way for native IPs. + +// Test 2: No-cache path - a native IP (FAILED hr from GetFunctionFromIP) must be +// reported as not resolved so RawSampleTransformer drops it from the sample. +TEST(FrameStoreTest, GetFrame_NoCache_NativeIp_ReturnsNotResolvedAndDropped) +{ + auto mockProfiler = MockProfilerInfo{}; + + EXPECT_CALL(mockProfiler, GetFunctionFromIP(_, _)) + .WillRepeatedly(Return(E_FAIL)); + + FrameStore frameStore( + /*pCorProfilerInfo*/ &mockProfiler, + /*pConfiguration */ nullptr, + /*pDebugInfoStore */ nullptr, + /*pManagedCodeCache*/ nullptr); + + // IP must be greater than FrameStore::MaxFakeIP so we don't hit the fake-IP + // short-circuit at the top of GetFrame. + const uintptr_t nativeIp = 0x12345; + + auto [isResolved, frameInfo] = frameStore.GetFrame(nativeIp); + + EXPECT_FALSE(isResolved) << "Native IPs must be reported as unresolved so " + "RawSampleTransformer drops them from the sample."; + EXPECT_EQ(std::string(frameInfo.Frame), std::string(NotResolvedFrameText)); +} + +// Test 5: Cached path - a native IP that is not in any managed code range must be +// reported as not resolved. The ManagedCodeCache returns InvalidFunctionId for +// such IPs; FrameStore must translate that to isResolved == false. +// +// This is the regression guard for the ARM64 exception-profiling issue where a +// long sequence of "NotResolvedFrame" entries leaked into the sample because +// FrameStore was returning isResolved == true for InvalidFunctionId. +TEST(FrameStoreTest, GetFrame_WithCache_NativeIp_ReturnsNotResolvedAndDropped) +{ + auto mockProfiler = MockProfilerInfo{}; + + // Empty cache => any IP resolves to InvalidFunctionId (there are no registered + // JIT ranges and no R2R modules), which is exactly the "native IP" case. + auto cache = std::make_unique(&mockProfiler); + cache->Initialize(); + + FrameStore frameStore( + /*pCorProfilerInfo*/ &mockProfiler, + /*pConfiguration */ nullptr, + /*pDebugInfoStore */ nullptr, + /*pManagedCodeCache*/ cache.get()); + + // Sanity-check the upstream contract we rely on: the cache must report the IP + // as "definitely native" (a value equal to InvalidFunctionId), not nullopt. + const uintptr_t nativeIp = 0xDEAD; + auto cacheResult = cache->GetFunctionId(nativeIp); + ASSERT_TRUE(cacheResult.has_value()); + ASSERT_EQ(ManagedCodeCache::InvalidFunctionId, cacheResult.value()); + + auto [isResolved, frameInfo] = frameStore.GetFrame(nativeIp); + + EXPECT_FALSE(isResolved) << "Native IPs (InvalidFunctionId from the cache) must " + "be reported as unresolved so RawSampleTransformer " + "drops them from the sample."; + EXPECT_EQ(std::string(frameInfo.Frame), std::string(NotResolvedFrameText)); + + cache.reset(); +} diff --git a/profiler/test/Datadog.Profiler.Native.Tests/LibrariesInfoCacheTest.cpp b/profiler/test/Datadog.Profiler.Native.Tests/LibrariesInfoCacheTest.cpp index 0dce96bd9881..73fd948a3191 100644 --- a/profiler/test/Datadog.Profiler.Native.Tests/LibrariesInfoCacheTest.cpp +++ b/profiler/test/Datadog.Profiler.Native.Tests/LibrariesInfoCacheTest.cpp @@ -6,7 +6,9 @@ #include "MemoryResourceManager.h" #include "LibrariesInfoCache.h" +#ifndef ARM64 #include "Backtrace2Unwinder.h" +#endif struct ServiceWrapper { diff --git a/profiler/test/Datadog.Profiler.Native.Tests/LinuxStackFramesCollectorTest.cpp b/profiler/test/Datadog.Profiler.Native.Tests/LinuxStackFramesCollectorTest.cpp index 7569e1b19f88..9096fbd02362 100644 --- a/profiler/test/Datadog.Profiler.Native.Tests/LinuxStackFramesCollectorTest.cpp +++ b/profiler/test/Datadog.Profiler.Native.Tests/LinuxStackFramesCollectorTest.cpp @@ -7,7 +7,12 @@ #include "profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/LinuxStackFramesCollector.h" #include "profiler/src/ProfilerEngine/Datadog.Profiler.Native.Linux/ProfilerSignalManager.h" +#ifdef ARM64 +#include "HybridUnwinder.h" +#include "ManagedCodeCache.h" +#else #include "Backtrace2Unwinder.h" +#endif #include "CallstackProvider.h" #include "ManagedThreadInfo.h" #include "MemoryResourceManager.h" @@ -159,7 +164,13 @@ class LinuxStackFramesCollectorFixture : public ::testing::Test _stopWorker = false; _workerThread = std::make_unique(_stopWorker); +#ifdef ARM64 + // TODO maybe a mock of ICorProfilerInfo to avoid crashing + _pManagedCodeCache = std::make_unique(nullptr); + _pUnwinder = std::make_unique(_pManagedCodeCache.get()); +#else _pUnwinder = std::make_unique(); +#endif ResetCallbackState(); @@ -199,11 +210,6 @@ class LinuxStackFramesCollectorFixture : public ::testing::Test inside_wrapped_functions = 1; // do not profile } - pid_t GetWorkerThreadId() - { - return _workerThread->GetThreadId(); - } - void SendSignal() { ResetCallbackState(); @@ -227,6 +233,11 @@ class LinuxStackFramesCollectorFixture : public ::testing::Test _callbackCalledFuture = _callbackCalledPromise.get_future(); } + ManagedThreadInfo* GetWorkerThreadInfo() + { + return _workerThread->GetThreadInfo(); + } + void ValidateCallstack(const Callstack& callstack) { // Disable this check due to flackyness @@ -273,10 +284,12 @@ class LinuxStackFramesCollectorFixture : public ::testing::Test _stopWorker(stopWorker), _workerThreadIdPromise(), _workerThreadIdFuture{_workerThreadIdPromise.get_future()}, - _callstack{shared::span(_framesBuffer.data(), _framesBuffer.size())} + _callstack{shared::span(_framesBuffer.data(), _framesBuffer.size())}, + _threadInfo(std::make_shared((ThreadID)0, nullptr)) { _worker = std::thread(&WorkerThread::Work, this); + InitializeThreadInfo(); } ~WorkerThread() @@ -294,9 +307,15 @@ class LinuxStackFramesCollectorFixture : public ::testing::Test return _callstack; } + ManagedThreadInfo* GetThreadInfo() + { + return _threadInfo.get(); + } + private: void Work() { + ManagedThreadInfo::CurrentThreadInfo = _threadInfo; // Get the callstack auto buffer = _callstack.AsSpan(); auto nb = unw_backtrace((void**)buffer.data(), buffer.size()); @@ -309,6 +328,25 @@ class LinuxStackFramesCollectorFixture : public ::testing::Test } } + void InitializeThreadInfo() + { + _threadInfo->SetOsInfo((DWORD)GetThreadId(), (HANDLE)0); + + pthread_attr_t attr; + if (pthread_getattr_np(pthread_self(), &attr) == 0) + { + void* stackAddr; + size_t stackSize; + if (pthread_attr_getstack(&attr, &stackAddr, &stackSize) == 0) + { + auto stackBase = reinterpret_cast(stackAddr); + auto stackEnd = stackBase + stackSize; + _threadInfo->SetStackBounds(stackBase, stackEnd); + } + pthread_attr_destroy(&attr); + } + } + const std::atomic& _stopWorker; std::promise _workerThreadIdPromise; std::shared_future _workerThreadIdFuture; @@ -316,6 +354,7 @@ class LinuxStackFramesCollectorFixture : public ::testing::Test static constexpr std::uint8_t MaxFrames = 20; std::array _framesBuffer; Callstack _callstack; + std::shared_ptr _threadInfo; }; bool _isStopped; @@ -326,9 +365,13 @@ class LinuxStackFramesCollectorFixture : public ::testing::Test std::future _callbackCalledFuture; std::unique_ptr _workerThread; std::unique_ptr _librariesInfoCache; +#ifdef ARM64 + std::unique_ptr _pManagedCodeCache; +#endif std::unique_ptr _pUnwinder; }; + TEST_F(LinuxStackFramesCollectorFixture, CheckSamplingThreadCollectCallStack) { auto* signalManager = GetSignalManager(); @@ -339,14 +382,13 @@ TEST_F(LinuxStackFramesCollectorFixture, CheckSamplingThreadCollectCallStack) MetricsRegistry metricsRegistry; auto collector = CreateStackFramesCollector(signalManager, configuration.get(), &p, metricsRegistry, GetUnwinder()); - auto threadInfo = ManagedThreadInfo((ThreadID)0, nullptr); - threadInfo.SetOsInfo((DWORD)GetWorkerThreadId(), (HANDLE)0); + auto* threadInfo = GetWorkerThreadInfo(); std::uint32_t hr; StackSnapshotResultBuffer* buffer; collector.PrepareForNextCollection(); - ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(&threadInfo, &hr)); + ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(threadInfo, &hr)); EXPECT_EQ(hr, S_OK); auto callstack = buffer->GetCallstack(); @@ -364,14 +406,13 @@ TEST_F(LinuxStackFramesCollectorFixture, CheckSamplingThreadCollectCallStackWith MetricsRegistry metricsRegistry; auto collector = CreateStackFramesCollector(signalManager, configuration.get(), &p, metricsRegistry, GetUnwinder()); - auto threadInfo = ManagedThreadInfo((ThreadID)0, nullptr); - threadInfo.SetOsInfo((DWORD)GetWorkerThreadId(), (HANDLE)0); + auto* threadInfo = GetWorkerThreadInfo(); std::uint32_t hr; StackSnapshotResultBuffer* buffer; collector.PrepareForNextCollection(); - ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(&threadInfo, &hr)); + ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(threadInfo, &hr)); EXPECT_EQ(hr, S_OK); auto callstack = buffer->GetCallstack(); @@ -391,14 +432,13 @@ TEST_F(LinuxStackFramesCollectorFixture, CheckCollectionAbortIfInPthreadCreateCa MetricsRegistry metricsRegistry; auto collector = CreateStackFramesCollector(signalManager, configuration.get(), &p, metricsRegistry, GetUnwinder()); - auto threadInfo = ManagedThreadInfo((ThreadID)0, nullptr); - threadInfo.SetOsInfo((DWORD)GetWorkerThreadId(), (HANDLE)0); + auto* threadInfo = GetWorkerThreadInfo(); std::uint32_t hr; StackSnapshotResultBuffer* buffer; collector.PrepareForNextCollection(); - ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(&threadInfo, &hr)); + ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(threadInfo, &hr)); EXPECT_EQ(hr, E_FAIL); EXPECT_EQ(buffer->GetFramesCount(), 0); } @@ -413,6 +453,7 @@ TEST_F(LinuxStackFramesCollectorFixture, MustNotCollectIfUnknownThreadId) MetricsRegistry metricsRegistry; auto collector = CreateStackFramesCollector(signalManager, configuration.get(), &p, metricsRegistry, GetUnwinder()); + // Unknown thread auto threadInfo = ManagedThreadInfo((ThreadID)0, nullptr); threadInfo.SetOsInfo(0, (HANDLE)0); @@ -437,16 +478,14 @@ TEST_F(LinuxStackFramesCollectorFixture, CheckProfilerSignalHandlerIsRestoredIfA auto collector = CreateStackFramesCollector(signalManager, configuration.get(), &p, metricsRegistry, GetUnwinder()); // Validate the profiler is working correctly - auto threadId = (DWORD)GetWorkerThreadId(); - auto threadInfo = ManagedThreadInfo((ThreadID)0, nullptr); - threadInfo.SetOsInfo(threadId, (HANDLE)0); + auto* threadInfo = GetWorkerThreadInfo(); std::uint32_t hr; StackSnapshotResultBuffer* buffer; collector.PrepareForNextCollection(); - ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(&threadInfo, &hr)); + ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(threadInfo, &hr)); EXPECT_EQ(hr, S_OK); auto callstack = buffer->GetCallstack(); @@ -458,7 +497,7 @@ TEST_F(LinuxStackFramesCollectorFixture, CheckProfilerSignalHandlerIsRestoredIfA // The profiler must not work collector.PrepareForNextCollection(); - ASSERT_DURATION_LE(3s, buffer = collector.CollectStackSample(&threadInfo, &hr)); + ASSERT_DURATION_LE(3s, buffer = collector.CollectStackSample(threadInfo, &hr)); EXPECT_EQ(hr, E_FAIL); // .. but the other handler yes @@ -467,7 +506,7 @@ TEST_F(LinuxStackFramesCollectorFixture, CheckProfilerSignalHandlerIsRestoredIfA // Reset to validate that the profiler will not call the test handler ResetCallbackState(); collector.PrepareForNextCollection(); - ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(&threadInfo, &hr)); + ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(threadInfo, &hr)); EXPECT_EQ(hr, S_OK); callstack = buffer->GetCallstack(); @@ -504,13 +543,11 @@ TEST_F(LinuxStackFramesCollectorFixture, CheckProfilerHandlerIsInstalledCorrectl std::uint32_t hr; StackSnapshotResultBuffer* buffer; - auto threadId = GetWorkerThreadId(); - auto threadInfo = ManagedThreadInfo((ThreadID)0, nullptr); - threadInfo.SetOsInfo((DWORD)threadId, (HANDLE)0); + auto* threadInfo = GetWorkerThreadInfo(); // validate it's working collector.PrepareForNextCollection(); - ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(&threadInfo, &hr)); + ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(threadInfo, &hr)); EXPECT_EQ(hr, S_OK); @@ -548,13 +585,11 @@ TEST_F(LinuxStackFramesCollectorFixture, CheckProfilerHandlerIsInstalledCorrectl std::uint32_t hr; StackSnapshotResultBuffer* buffer; - auto threadId = GetWorkerThreadId(); - auto threadInfo = ManagedThreadInfo((ThreadID)0, nullptr); - threadInfo.SetOsInfo((DWORD)threadId, (HANDLE)0); + auto* threadInfo = GetWorkerThreadInfo(); // validate it's working collector.PrepareForNextCollection(); - ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(&threadInfo, &hr)); + ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(threadInfo, &hr)); EXPECT_EQ(hr, S_OK); @@ -593,13 +628,11 @@ TEST_F(LinuxStackFramesCollectorFixture, CheckProfilerHandlerIsInstalledCorrectl std::uint32_t hr; StackSnapshotResultBuffer* buffer; - auto threadId = GetWorkerThreadId(); - auto threadInfo = ManagedThreadInfo((ThreadID)0, nullptr); - threadInfo.SetOsInfo((DWORD)threadId, (HANDLE)0); + auto* threadInfo = GetWorkerThreadInfo(); // validate it's working collector.PrepareForNextCollection(); - ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(&threadInfo, &hr)); + ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(threadInfo, &hr)); EXPECT_EQ(hr, S_OK); @@ -660,14 +693,12 @@ TEST_F(LinuxStackFramesCollectorFixture, CheckThatProfilerHandlerAndOtherHandler InstallHandler(SA_SIGINFO, true); // Validate the profiler is still working correctly - auto threadId = (DWORD)GetWorkerThreadId(); - auto threadInfo = ManagedThreadInfo((ThreadID)0, nullptr); - threadInfo.SetOsInfo(threadId, (HANDLE)0); + auto* threadInfo = GetWorkerThreadInfo(); std::uint32_t hr; StackSnapshotResultBuffer* buffer; collector.PrepareForNextCollection(); - ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(&threadInfo, &hr)); + ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(threadInfo, &hr)); EXPECT_EQ(hr, S_OK); auto callstack = buffer->GetCallstack(); @@ -715,16 +746,14 @@ TEST_F(LinuxStackFramesCollectorFixture, CheckTheProfilerStopWorkingIfSignalHand MetricsRegistry metricsRegistry; auto collector = CreateStackFramesCollector(signalManager, configuration.get(), &p, metricsRegistry, GetUnwinder()); - const auto threadId = GetWorkerThreadId(); - auto threadInfo = ManagedThreadInfo((ThreadID)0, nullptr); - threadInfo.SetOsInfo((DWORD)threadId, (HANDLE)0); + auto* threadInfo = GetWorkerThreadInfo(); std::uint32_t hr; StackSnapshotResultBuffer* buffer; { collector.PrepareForNextCollection(); // validate it's working - ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(&threadInfo, &hr)); + ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(threadInfo, &hr)); EXPECT_EQ(hr, S_OK); auto callstack = buffer->GetCallstack(); @@ -736,12 +765,12 @@ TEST_F(LinuxStackFramesCollectorFixture, CheckTheProfilerStopWorkingIfSignalHand { // profiler handler was replaced, so the signal will be lost and we will return after 2s collector.PrepareForNextCollection(); - ASSERT_DURATION_LE(3s, buffer = collector.CollectStackSample(&threadInfo, &hr)); + ASSERT_DURATION_LE(3s, buffer = collector.CollectStackSample(threadInfo, &hr)); EXPECT_EQ(hr, E_FAIL); // At this point, the profiler restored its handler, ensure it's working as expected collector.PrepareForNextCollection(); - ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(&threadInfo, &hr)); + ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(threadInfo, &hr)); EXPECT_EQ(hr, S_OK); } @@ -752,14 +781,14 @@ TEST_F(LinuxStackFramesCollectorFixture, CheckTheProfilerStopWorkingIfSignalHand { // profiler handler was replaced, so the signal will be lost and we will return after 2s collector.PrepareForNextCollection(); - ASSERT_DURATION_LE(3s, buffer = collector.CollectStackSample(&threadInfo, &hr)); + ASSERT_DURATION_LE(3s, buffer = collector.CollectStackSample(threadInfo, &hr)); EXPECT_EQ(hr, E_FAIL); ResetCallbackState(); // At this point, we stop restoring the profiler signal handler and stop profiling collector.PrepareForNextCollection(); - ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(&threadInfo, &hr)); + ASSERT_DURATION_LE(100ms, buffer = collector.CollectStackSample(threadInfo, &hr)); EXPECT_EQ(hr, E_FAIL); } } diff --git a/profiler/test/Datadog.Profiler.Native.Tests/ManagedCodeCacheTest.cpp b/profiler/test/Datadog.Profiler.Native.Tests/ManagedCodeCacheTest.cpp index 3e5edd2bc180..936d2a536b71 100644 --- a/profiler/test/Datadog.Profiler.Native.Tests/ManagedCodeCacheTest.cpp +++ b/profiler/test/Datadog.Profiler.Native.Tests/ManagedCodeCacheTest.cpp @@ -68,8 +68,12 @@ TEST_F(ManagedCodeCacheTest, AddFunction_SingleRange_GetFunctionIdReturnsCorrect EXPECT_EQ(testFuncId, cache->GetFunctionId(codeStart + codeSize - 1).value_or(0)); // IPs outside range should return nullopt - EXPECT_FALSE(cache->GetFunctionId(codeStart - 1).has_value()); - EXPECT_FALSE(cache->GetFunctionId(codeStart + codeSize).has_value()); + auto beforeStartIp = cache->GetFunctionId(codeStart - 1); + EXPECT_TRUE(beforeStartIp.has_value()); + EXPECT_EQ(ManagedCodeCache::InvalidFunctionId, *beforeStartIp); + auto borderIp = cache->GetFunctionId(codeStart + codeSize); + EXPECT_TRUE(borderIp.has_value()); + EXPECT_EQ(ManagedCodeCache::InvalidFunctionId, *borderIp); } // Test: Multiple ranges (tiered JIT simulation) @@ -113,8 +117,12 @@ TEST_F(ManagedCodeCacheTest, IsManaged_ValidManagedIP_ReturnsTrue) { // Test: IsManaged for invalid IP TEST_F(ManagedCodeCacheTest, IsManaged_InvalidIP_ReturnsFalse) { - EXPECT_FALSE(cache->IsManaged(0xDEADBEEF)); - EXPECT_FALSE(cache->IsManaged(0)); + auto deadbeef = cache->IsManaged(0xDEADBEEF); + EXPECT_TRUE(deadbeef.has_value()); + EXPECT_FALSE(deadbeef.value()); + auto zero = cache->IsManaged(0); + EXPECT_TRUE(zero.has_value()); + EXPECT_FALSE(zero.value()); } // Test: Multiple functions don't interfere @@ -136,7 +144,9 @@ TEST_F(ManagedCodeCacheTest, AddFunction_MultipleFunctions_NoInterference) { EXPECT_EQ(func2, cache->GetFunctionId(code2Start + 0x50).value_or(0)); // No cross-contamination - EXPECT_FALSE(cache->GetFunctionId(code1Start + codeSize + 10).has_value()); + auto outside = cache->GetFunctionId(code1Start + codeSize + 10); + EXPECT_TRUE(outside.has_value()); + EXPECT_EQ(ManagedCodeCache::InvalidFunctionId, outside.value()); } // Test: Thread safety (concurrent AddFunction calls) @@ -190,8 +200,12 @@ TEST_F(ManagedCodeCacheTest, AddFunction_ConcurrentCalls_ThreadSafe) { } // Verify an IP outside all registered ranges returns empty - EXPECT_FALSE(cache->GetFunctionId(0xDEAD).has_value()); - EXPECT_FALSE(cache->IsManaged(0xDEAD)); + auto outside = cache->GetFunctionId(0xDEAD); + EXPECT_TRUE(outside.has_value()); + EXPECT_EQ(ManagedCodeCache::InvalidFunctionId, outside.value()); + auto outsideIsManaged = cache->IsManaged(0xDEAD); + EXPECT_TRUE(outsideIsManaged.has_value()); + EXPECT_FALSE(outsideIsManaged.value()); } // Test: IsManaged (no blocking) @@ -242,8 +256,12 @@ TEST_F(ManagedCodeCacheTest, GetFunctionId_BoundaryIPs_CorrectBehavior) { EXPECT_EQ(testFuncId, cache->GetFunctionId(codeStart + codeSize - 1).value_or(0)); // Last byte (inclusive) // Just outside boundaries - EXPECT_FALSE(cache->GetFunctionId(codeStart - 1).has_value()); - EXPECT_FALSE(cache->GetFunctionId(codeStart + codeSize).has_value()); + auto beforeStartIp = cache->GetFunctionId(codeStart - 1); + EXPECT_TRUE(beforeStartIp.has_value()); + EXPECT_EQ(ManagedCodeCache::InvalidFunctionId, *beforeStartIp); + auto borderIp = cache->GetFunctionId(codeStart + codeSize); + EXPECT_TRUE(borderIp.has_value()); + EXPECT_EQ(ManagedCodeCache::InvalidFunctionId, *borderIp); } // Test: Zero-sized code range @@ -280,7 +298,9 @@ TEST_F(ManagedCodeCacheTest, AddFunction_LargeCodeRange_WorksCorrectly) { // Test: Null IP TEST_F(ManagedCodeCacheTest, GetFunctionId_NullIP_ReturnsEmpty) { - EXPECT_FALSE(cache->GetFunctionId(0).has_value()); + auto nullIp = cache->GetFunctionId(0); + EXPECT_TRUE(nullIp.has_value()); + EXPECT_EQ(ManagedCodeCache::InvalidFunctionId, nullIp.value()); } // Test: GetCodeInfo2 failure handling @@ -294,7 +314,124 @@ TEST_F(ManagedCodeCacheTest, AddFunction_GetCodeInfo2Fails_HandledGracefully) { WaitForWorkerThread(); // Should not crash - EXPECT_FALSE(cache->GetFunctionId(0x1000).has_value()); + auto nullIp = cache->GetFunctionId(0x1000); + EXPECT_TRUE(nullIp.has_value()); + EXPECT_EQ(ManagedCodeCache::InvalidFunctionId, nullIp.value()); +} + +#ifdef _WINDOWS +// Test: On Windows, GetFunctionFromIP can crash (e.g. module unloaded concurrently). +// The SEH __try/__except in GetFunctionFromIP_Original must catch the access violation +// and GetFunctionId must return std::nullopt to signal the failure to the caller. +TEST_F(ManagedCodeCacheTest, GetFunctionId_GetFunctionFromIPRaisesAccessViolation_ReturnsNullopt) { + // Register an R2R module range so that GetFunctionId falls through to + // GetFunctionFromIP_Original (which wraps the ICorProfilerInfo call in __try/__except). + uintptr_t r2rCodeStart = 0xB0000000; + uintptr_t r2rCodeEnd = 0xB000FFFF; + uintptr_t ipInR2R = r2rCodeStart + 0x500; + + std::vector moduleRanges; + moduleRanges.emplace_back(r2rCodeStart, r2rCodeEnd); + cache->AddModuleRangesToCache(std::move(moduleRanges)); + + // Simulate a crash during GetFunctionFromIP by raising an access violation. + // This mirrors the real-world scenario where the CLR unloads the module containing + // the target symbol while we are resolving the IP. + EXPECT_CALL(*mockProfiler, GetFunctionFromIP(reinterpret_cast(ipInR2R), _)) + .WillOnce([](LPCBYTE, FunctionID*) -> HRESULT { + ::RaiseException(EXCEPTION_ACCESS_VIOLATION, 0, 0, nullptr); + return S_OK; // unreachable + }); + + auto result = cache->GetFunctionId(ipInR2R); + EXPECT_FALSE(result.has_value()) + << "GetFunctionId should return std::nullopt when GetFunctionFromIP raises an access violation"; +} +#endif + +// Test: IsManaged is signal-safe - it must return std::nullopt rather than block +// when another thread holds the writer lock on the pages mutex. This guards the +// contract relied on by the HybridUnwinder in a signal handler on ARM64. +TEST_F(ManagedCodeCacheTest, IsManaged_WriterHoldsPagesMutex_ReturnsNullopt) { + FunctionID testFuncId = 321; + uintptr_t codeStart = 0xC000; + ULONG32 codeSize = 0x100; + + SetupMockCodeInfo(testFuncId, codeStart, codeSize); + cache->AddFunction(testFuncId); + WaitForWorkerThread(); + + // Sanity: with no contention, IsManaged returns a concrete value. + auto baseline = cache->IsManaged(codeStart + 0x50); + ASSERT_TRUE(baseline.has_value()); + EXPECT_TRUE(baseline.value()); + + // Simulate signal-handler contention: another thread holds the pages mutex + // exclusively. IsManaged must fail the try_to_lock and return std::nullopt + // instead of blocking (which would deadlock if called from a signal handler). + std::atomic writerHoldsLock{false}; + std::atomic readerDone{false}; + std::thread writer([&]() { + auto lock = cache->LockPagesMutexExclusiveForTest(); + writerHoldsLock.store(true); + // Hold the lock until the reader side of the test has observed the nullopt. + while (!readerDone.load()) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + }); + + // Wait for the writer to actually hold the lock before probing. + while (!writerHoldsLock.load()) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + auto contended = cache->IsManaged(codeStart + 0x50); + EXPECT_FALSE(contended.has_value()) + << "IsManaged must return nullopt under writer-lock contention so the " + "signal handler can back off instead of deadlocking."; + + readerDone.store(true); + writer.join(); + + // Sanity: once contention clears, IsManaged resumes normal behavior. + auto afterRelease = cache->IsManaged(codeStart + 0x50); + ASSERT_TRUE(afterRelease.has_value()); + EXPECT_TRUE(afterRelease.value()); +} + +// Test: IsManaged must also return nullopt when the writer holds the modules +// mutex exclusively and the probed IP is not in any JIT page (i.e. the code path +// falls through to IsCodeInR2RModule(ip, /*signalSafe*/true)). +TEST_F(ManagedCodeCacheTest, IsManaged_WriterHoldsModulesMutex_ReturnsNullopt) { + // Probe an IP that is not covered by any registered JIT range, so IsManaged + // falls through to IsCodeInR2RModule which try_to_locks _modulesMutex. + const uintptr_t ipOutsideJit = 0xDEADBEEF; + + std::atomic writerHoldsLock{false}; + std::atomic readerDone{false}; + std::thread writer([&]() { + auto lock = cache->LockModulesMutexExclusiveForTest(); + writerHoldsLock.store(true); + while (!readerDone.load()) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + }); + + while (!writerHoldsLock.load()) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + auto contended = cache->IsManaged(ipOutsideJit); + EXPECT_FALSE(contended.has_value()) + << "IsManaged must return nullopt when the modules-mutex writer is active " + "and the IP is not in any JIT page (R2R fallback path)."; + + readerDone.store(true); + writer.join(); } // Test: IsManaged falls back to R2R module check when IP is not in the JIT page map @@ -315,5 +452,7 @@ TEST_F(ManagedCodeCacheTest, IsManaged_IPInR2RModule_NotInPageMap_ReturnsTrue) { << "IsManaged should return true for an IP in an R2R module even when the JIT page map has no entry for that page"; // An IP outside both the JIT page map and any R2R module should still be false - EXPECT_FALSE(cache->IsManaged(0xDEADBEEF)); + auto outsideIsManaged = cache->IsManaged(0xDEADBEEF); + EXPECT_TRUE(outsideIsManaged.has_value()); + EXPECT_FALSE(outsideIsManaged.value()); } diff --git a/shared/src/Datadog.Trace.ClrProfiler.Native/loader.conf b/shared/src/Datadog.Trace.ClrProfiler.Native/loader.conf index 76571ddd520d..f4f53bc047fe 100644 --- a/shared/src/Datadog.Trace.ClrProfiler.Native/loader.conf +++ b/shared/src/Datadog.Trace.ClrProfiler.Native/loader.conf @@ -3,8 +3,8 @@ PROFILER;{BD1A650D-AC5D-4896-B64F-D6FA25D6B26A};win-x64;.\Datadog.Profiler.Nativ PROFILER;{BD1A650D-AC5D-4896-B64F-D6FA25D6B26A};win-x86;.\Datadog.Profiler.Native.dll PROFILER;{BD1A650D-AC5D-4896-B64F-D6FA25D6B26A};linux-x64;./Datadog.Profiler.Native.so PROFILER;{BD1A650D-AC5D-4896-B64F-D6FA25D6B26A};linux-musl-x64;./Datadog.Profiler.Native.so -# PROFILER;{BD1A650D-AC5D-4896-B64F-D6FA25D6B26A};linux-arm64;./Datadog.Profiler.Native.so -# PROFILER;{BD1A650D-AC5D-4896-B64F-D6FA25D6B26A};linux-musl-arm64;./Datadog.Profiler.Native.so +PROFILER;{BD1A650D-AC5D-4896-B64F-D6FA25D6B26A};linux-arm64;./Datadog.Profiler.Native.so +PROFILER;{BD1A650D-AC5D-4896-B64F-D6FA25D6B26A};linux-musl-arm64;./Datadog.Profiler.Native.so #Tracer TRACER;{50DA5EED-F1ED-B00B-1055-5AFE55A1ADE5};win-arm64;.\Datadog.Tracer.Native.dll diff --git a/tracer/build/_build/Build.Profiler.Steps.cs b/tracer/build/_build/Build.Profiler.Steps.cs index 61a6c9b7917a..04b879b2a543 100644 --- a/tracer/build/_build/Build.Profiler.Steps.cs +++ b/tracer/build/_build/Build.Profiler.Steps.cs @@ -253,6 +253,11 @@ partial class Build .After(CompileProfilerNativeSrc) .Executes(() => { + // arch resolves via GetUnixArchitectureAndExtension() to one of: + // linux-x64, linux-arm64, linux-musl-x64, linux-musl-arm64. + // On arm64 specifically, this target runs on both the glibc and the + // Alpine/musl CI legs, which together populate both linux-arm64 and + // linux-musl-arm64 RID folders in the aggregated monitoring home. var (arch, _) = GetUnixArchitectureAndExtension(); var sourceDir = ProfilerDeployDirectory / arch; EnsureExistingDirectory(MonitoringHomeDirectory / arch); @@ -329,7 +334,7 @@ partial class Build Target BuildAndRunProfilerCpuLimitTests => _ => _ .After(BuildProfilerSamples) .Description("Run the profiler container tests") - .Requires(() => IsLinux && !IsArm64) + .Requires(() => IsLinux) .Executes(() => { BuildAndRunProfilerIntegrationTestsInternal("(Category=CpuLimitTest)"); @@ -338,7 +343,6 @@ partial class Build Target BuildAndRunProfilerIntegrationTests => _ => _ .After(BuildProfilerSamples) .Description("Builds and runs the profiler integration tests") - .Requires(() => !IsArm64) .Executes(() => { // Exclude CpuLimitTest from this path: They are already launched in a specific step + specific setup @@ -681,7 +685,7 @@ void RunCppCheck(string projectName, MSBuildTargetPlatform platform) var platforms = IsWin ? new[] { MSBuildTargetPlatform.x64, MSBuildTargetPlatform.x86 } - : new[] { MSBuildTargetPlatform.x64 }; + : new[] { IsArm64 ? ARM64TargetPlatform : MSBuildTargetPlatform.x64 }; foreach (var platform in platforms) { @@ -696,7 +700,7 @@ void RunCppCheck(string projectName, MSBuildTargetPlatform platform) var platforms = IsWin ? new[] { MSBuildTargetPlatform.x64, MSBuildTargetPlatform.x86 } - : new[] { MSBuildTargetPlatform.x64 }; + : new[] { IsArm64 ? ARM64TargetPlatform : MSBuildTargetPlatform.x64 }; foreach (var platform in platforms) { @@ -784,7 +788,12 @@ void RunCppCheck(string projectName, MSBuildTargetPlatform platform) Target CompileProfilerWithTsanLinux => _ => _ .Unlisted() - .OnlyWhenStatic(() => IsLinux) + // TODO: re-enable on arm64 once we have CI hosts whose kernel/hardware + // exposes the 48-bit VMA that TSAN's shadow memory mapping requires. + // Many AWS Graviton / Ampere arm64 instances still default to 39- or + // 42-bit VMA, which trips TSAN's initializer. + // Tracking: https://github.com/DataDog/dd-trace-dotnet/issues/TBD (replace with real issue id). + .OnlyWhenStatic(() => IsLinux && !IsArm64) // TSAN requires 48-bit VMA, unavailable on arm64 CI .Before(PublishProfiler) .Executes(() => { @@ -799,7 +808,9 @@ void RunCppCheck(string projectName, MSBuildTargetPlatform platform) Target RunUnitTestsWithTsanLinux => _ => _ .Unlisted() - .OnlyWhenStatic(() => IsLinux) + // See CompileProfilerWithTsanLinux above for the arm64 VMA limitation + // and the tracking-issue link. + .OnlyWhenStatic(() => IsLinux && !IsArm64) // TSAN requires 48-bit VMA, unavailable on arm64 CI .Executes(() => { // Filtering tests is temporary. @@ -823,7 +834,7 @@ void RunCppCheck(string projectName, MSBuildTargetPlatform platform) var platforms = IsWin ? new[] { MSBuildTargetPlatform.x64, MSBuildTargetPlatform.x86 } - : new[] { MSBuildTargetPlatform.x64 }; + : new[] { IsArm64 ? ARM64TargetPlatform : MSBuildTargetPlatform.x64 }; var sampleApp = ProfilerSamplesSolution.GetProject("Samples.Computer01"); @@ -848,7 +859,7 @@ void RunCppCheck(string projectName, MSBuildTargetPlatform platform) .Triggers(CheckTestResultForProfilerWithSanitizer) .Executes(() => { - RunSampleWithSanitizer(MSBuildTargetPlatform.x64, SanitizerKind.Ubsan); + RunSampleWithSanitizer(IsArm64 ? ARM64TargetPlatform : MSBuildTargetPlatform.x64, SanitizerKind.Ubsan); }); Target ValidateNativeProfilerGlibcCompatibility => _ => _ @@ -915,7 +926,12 @@ void RunSampleWithSanitizer(MSBuildTargetPlatform platform, SanitizerKind saniti { if (sanitizer is SanitizerKind.Asan) { - envVars["LD_PRELOAD"] = "libasan.so.6"; + // libasan SONAME differs between the two ASAN CI images: + // - arm64: older Ubuntu/Debian base shipping gcc 9 -> libasan.so.5. + // - x64: newer image shipping gcc 10+ -> libasan.so.6. + // If/when the arm64 image is upgraded to gcc 10+, this can be + // collapsed to libasan.so.6 unconditionally. + envVars["LD_PRELOAD"] = IsArm64 ? "libasan.so.5" : "libasan.so.6"; // detect_leaks set to 0 to avoid false positive since not all libs are compiled against ASAN (ex. CLR binaries) envVars["ASAN_OPTIONS"] = "detect_leaks=0"; } diff --git a/tracer/build/_build/Build.Steps.cs b/tracer/build/_build/Build.Steps.cs index 7da2eb9e5180..4ea77a0c5ada 100644 --- a/tracer/build/_build/Build.Steps.cs +++ b/tracer/build/_build/Build.Steps.cs @@ -2578,12 +2578,6 @@ string NormalizedPath(AbsolutePath ap) knownPatterns.Add(new(@".*'dddlopen' dddlerror returned: Library linux-vdso\.so\.1 is not already loaded", RegexOptions.Compiled)); } - if (IsArm64) - { - // Profiler is not yet supported on Arm64 - knownPatterns.Add(new(@".*Profiler is deactivated because it runs on an unsupported architecture", RegexOptions.Compiled)); - } - var isAzureFunctionsScenario = SmokeTestCategory is SmokeTests.SmokeTestCategory.LinuxAzureFunctionsNuGet or SmokeTests.SmokeTestCategory.WindowsAzureFunctionsNuGet; if (isAzureFunctionsScenario) { diff --git a/tracer/build/_build/LogParsing/LogParser.cs b/tracer/build/_build/LogParsing/LogParser.cs index db094d5a6c03..f0b3a2e44e80 100644 --- a/tracer/build/_build/LogParsing/LogParser.cs +++ b/tracer/build/_build/LogParsing/LogParser.cs @@ -76,7 +76,7 @@ public static async Task DoLogsContainErrors( || (managedFiles.Count > 0 // && libdatadogFiles.Count > 0 Libdatadog exporter is off by default, so we don't require it to be there && nativeTracerFiles.Count > 0 - && (nativeProfilerFiles.Count > 0 || EnvironmentInfo.IsOsx || EnvironmentInfo.IsArm64) // profiler doesn't support mac or ARM64 + && (nativeProfilerFiles.Count > 0 || EnvironmentInfo.IsOsx) // profiler doesn't support mac && nativeLoaderFiles.Count > 0); var hasErrors = managedErrors.Count != 0 || libdatadogErrors.Count != 0 diff --git a/tracer/src/Datadog.Trace/ContinuousProfiler/ProfilerAvailabilityHelper.cs b/tracer/src/Datadog.Trace/ContinuousProfiler/ProfilerAvailabilityHelper.cs index c21dc8df68a4..743486b25ede 100644 --- a/tracer/src/Datadog.Trace/ContinuousProfiler/ProfilerAvailabilityHelper.cs +++ b/tracer/src/Datadog.Trace/ContinuousProfiler/ProfilerAvailabilityHelper.cs @@ -37,7 +37,7 @@ internal static bool IsContinuousProfilerAvailable_TestingOnly(Func isClrP private static bool GetIsContinuousProfilerAvailable(Func isClrProfilerAttached, bool isAwsLambda, bool isAzureFunction) { - // Profiler is not available on ARM(64) + // Profiler is not available on ARM (32-bit) var fd = FrameworkDescription.Instance; if (!IsSupportedArch(fd)) { @@ -66,7 +66,7 @@ static bool IsSupportedArch(FrameworkDescription fd) return fd.OSPlatform switch { OSPlatformName.Windows when fd.ProcessArchitecture is ProcessArchitecture.X64 or ProcessArchitecture.X86 => true, - OSPlatformName.Linux when fd.ProcessArchitecture is ProcessArchitecture.X64 => true, + OSPlatformName.Linux when fd.ProcessArchitecture is ProcessArchitecture.X64 or ProcessArchitecture.Arm64 => true, _ => false, }; } diff --git a/tracer/src/Datadog.Trace/ContinuousProfiler/ProfilerSettings.cs b/tracer/src/Datadog.Trace/ContinuousProfiler/ProfilerSettings.cs index d12d964af9f3..49740ca40e36 100644 --- a/tracer/src/Datadog.Trace/ContinuousProfiler/ProfilerSettings.cs +++ b/tracer/src/Datadog.Trace/ContinuousProfiler/ProfilerSettings.cs @@ -86,7 +86,7 @@ public static bool IsProfilingSupported var fd = FrameworkDescription.Instance; return (fd.OSPlatform == OSPlatformName.Windows && fd.ProcessArchitecture is ProcessArchitecture.X64 or ProcessArchitecture.X86) || - (fd.OSPlatform == OSPlatformName.Linux && fd.ProcessArchitecture is ProcessArchitecture.X64); + (fd.OSPlatform == OSPlatformName.Linux && fd.ProcessArchitecture is ProcessArchitecture.X64 or ProcessArchitecture.Arm64); } } diff --git a/tracer/src/Datadog.Trace/NativeLoader.cs b/tracer/src/Datadog.Trace/NativeLoader.cs index 39bdb8056212..9d14a3aa9335 100644 --- a/tracer/src/Datadog.Trace/NativeLoader.cs +++ b/tracer/src/Datadog.Trace/NativeLoader.cs @@ -19,7 +19,7 @@ private static bool IsAvailable get { var fd = FrameworkDescription.Instance; - return fd.ProcessArchitecture != ProcessArchitecture.Arm && fd.ProcessArchitecture != ProcessArchitecture.Arm64; + return fd.ProcessArchitecture != ProcessArchitecture.Arm; } } diff --git a/tracer/test/Datadog.Trace.Tests/ContinuousProfiler/ProfilerAvailabilityHelperTests.cs b/tracer/test/Datadog.Trace.Tests/ContinuousProfiler/ProfilerAvailabilityHelperTests.cs index 925c99e58d51..fbcef47af7e9 100644 --- a/tracer/test/Datadog.Trace.Tests/ContinuousProfiler/ProfilerAvailabilityHelperTests.cs +++ b/tracer/test/Datadog.Trace.Tests/ContinuousProfiler/ProfilerAvailabilityHelperTests.cs @@ -33,6 +33,7 @@ public void IsContinuousProfilerAvailable_OnUnsupportedPlatforms_ReturnsFalse() SkipOn.PlatformAndArchitecture(SkipOn.PlatformValue.Windows, SkipOn.ArchitectureValue.X64); SkipOn.PlatformAndArchitecture(SkipOn.PlatformValue.Windows, SkipOn.ArchitectureValue.X86); SkipOn.PlatformAndArchitecture(SkipOn.PlatformValue.Linux, SkipOn.ArchitectureValue.X64); + SkipOn.PlatformAndArchitecture(SkipOn.PlatformValue.Linux, SkipOn.ArchitectureValue.ARM64); ProfilerAvailabilityHelper.IsContinuousProfilerAvailable_TestingOnly( ClrProfilerIsAttached, @@ -105,7 +106,6 @@ private static void SkipUnsupported() { SkipOn.Platform(SkipOn.PlatformValue.MacOs); SkipOn.PlatformAndArchitecture(SkipOn.PlatformValue.Linux, SkipOn.ArchitectureValue.X86); - SkipOn.PlatformAndArchitecture(SkipOn.PlatformValue.Linux, SkipOn.ArchitectureValue.ARM64); SkipOn.PlatformAndArchitecture(SkipOn.PlatformValue.Windows, SkipOn.ArchitectureValue.ARM64); SkipOn.Platform(SkipOn.PlatformValue.Windows); // Windows is controlled by env var only, so doesn't apply to most tests } diff --git a/tracer/test/Datadog.Trace.Tests/ContinuousProfiler/ProfilerSettingsTests.cs b/tracer/test/Datadog.Trace.Tests/ContinuousProfiler/ProfilerSettingsTests.cs index b8dac79c257b..abf9332bc72e 100644 --- a/tracer/test/Datadog.Trace.Tests/ContinuousProfiler/ProfilerSettingsTests.cs +++ b/tracer/test/Datadog.Trace.Tests/ContinuousProfiler/ProfilerSettingsTests.cs @@ -147,6 +147,7 @@ public void ProfilerState_IsProfilingSupported_OnlySupportedOnExpectedPlatforms( (Architecture.X64, true, _) => true, // Windows x64 (Architecture.X86, true, _) => true, // Windows x86 (Architecture.X64, _, true) => true, // Linux x64 + (Architecture.Arm64, _, true) => true, // Linux arm64 _ => false // Unsupported platforms };