diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..2943a03c8 --- /dev/null +++ b/.clang-format @@ -0,0 +1,17 @@ +--- +Language: Cpp +BasedOnStyle: LLVM +IndentWidth: 4 +ColumnLimit: 100 +BreakBeforeBraces: Attach +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortNamespacesOnASingleLine: false +AllowShortStructsOnASingleLine: false +AllowShortEnumsOnASingleLine: false +AllowShortLambdasOnASingleLine: false +AllowShortCompoundStatementsOnASingleLine: false +AllowShortAlwaysBreakType: None diff --git a/.gitignore b/.gitignore index 39e6fe42e..f27422d6e 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ datadog/maven/resources /gradle.properties **/hs_err* +# cursor AI history +.history + diff --git a/ddprof-lib/benchmarks/build.gradle b/ddprof-lib/benchmarks/build.gradle new file mode 100644 index 000000000..c6bd1db5c --- /dev/null +++ b/ddprof-lib/benchmarks/build.gradle @@ -0,0 +1,60 @@ +plugins { + id 'cpp-application' +} + +// this feels weird but it is the only way invoking `./gradlew :ddprof-lib:*` tasks will work +if (rootDir.toString().endsWith("ddprof-lib/gradle")) { + apply from: rootProject.file('../../common.gradle') +} + +application { + baseName = "unwind_failures_benchmark" + source.from file('src') + privateHeaders.from file('src') + + targetMachines = [machines.macOS, machines.linux.x86_64] +} + +// Include the main library headers +tasks.withType(CppCompile).configureEach { + includes file('../src/main/cpp').toString() +} + +// Add a task to run the benchmark +tasks.register('runBenchmark', Exec) { + dependsOn 'assemble' + workingDir = buildDir + + doFirst { + // Find the executable by looking for it in the build directory + def executableName = "unwind_failures_benchmark" + def executable = null + + // Search for the executable in the build directory + buildDir.eachFileRecurse { file -> + if (file.isFile() && file.name == executableName && file.canExecute()) { + executable = file + return true // Stop searching once found + } + } + + if (executable == null) { + throw new GradleException("Executable '${executableName}' not found in ${buildDir.absolutePath}. Make sure the build was successful.") + } + + // Build command line with the executable path and any additional arguments + def cmd = [executable.absolutePath] + + // Add any additional arguments passed to the Gradle task + if (project.hasProperty('args')) { + cmd.addAll(project.args.split(' ')) + } + + println "Running benchmark using executable at: ${executable.absolutePath}" + commandLine = cmd + } + + doLast { + println "Benchmark completed." + } +} diff --git a/ddprof-lib/benchmarks/build_run.sh b/ddprof-lib/benchmarks/build_run.sh new file mode 100755 index 000000000..868d844ad --- /dev/null +++ b/ddprof-lib/benchmarks/build_run.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${HERE}/.." + +# Build and run the benchmark using Gradle +./gradlew :ddprof-lib:benchmarks:runBenchmark diff --git a/ddprof-lib/benchmarks/src/benchmarkConfig.h b/ddprof-lib/benchmarks/src/benchmarkConfig.h new file mode 100644 index 000000000..de47b8655 --- /dev/null +++ b/ddprof-lib/benchmarks/src/benchmarkConfig.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +struct BenchmarkResult { + std::string name; + long long total_time_ns; + int iterations; + double avg_time_ns; +}; + +struct BenchmarkConfig { + int warmup_iterations; + int measurement_iterations; + std::string csv_file; + std::string json_file; + bool debug; + + BenchmarkConfig() : warmup_iterations(100000), measurement_iterations(1000000), debug(false) { + } +}; diff --git a/ddprof-lib/benchmarks/src/unwindFailuresBenchmark.cpp b/ddprof-lib/benchmarks/src/unwindFailuresBenchmark.cpp new file mode 100644 index 000000000..833da9a21 --- /dev/null +++ b/ddprof-lib/benchmarks/src/unwindFailuresBenchmark.cpp @@ -0,0 +1,268 @@ +#include "benchmarkConfig.h" +#include "unwindStats.h" +#include +#include +#include +#include +#include +#include +#include + +// Test data - using JVM stub function names +const char *TEST_NAMES[] = {"Java_java_lang_String_toString", "Java_java_lang_Object_hashCode", + "Java_java_lang_System_arraycopy", + "Java_java_lang_Thread_currentThread", "Java_java_lang_Class_getName"}; +const int NUM_NAMES = sizeof(TEST_NAMES) / sizeof(TEST_NAMES[0]); + +// Global variables +std::vector results; +BenchmarkConfig config; + +// Pre-generated random values for benchmarking +struct RandomValues { + std::vector names; + std::vector kinds; + std::vector name_indices; + + void generate(int count) { + std::mt19937 rng(42); // Fixed seed for reproducibility + names.resize(count); + kinds.resize(count); + name_indices.resize(count); + + for (int i = 0; i < count; i++) { + name_indices[i] = rng() % NUM_NAMES; + names[i] = TEST_NAMES[name_indices[i]]; + kinds[i] = static_cast(rng() % 3); + } + } +}; + +RandomValues random_values; + +void exportResultsToCSV(const std::string &filename) { + std::ofstream file(filename); + if (!file.is_open()) { + std::cerr << "Failed to open file: " << filename << std::endl; + return; + } + + // Write header + file << "Benchmark,Total Time (ns),Iterations,Average Time (ns)\n"; + + // Write data + for (const auto &result : results) { + file << result.name << "," << result.total_time_ns << "," << result.iterations << "," + << result.avg_time_ns << "\n"; + } + + file.close(); + std::cout << "Results exported to CSV: " << filename << std::endl; +} + +void exportResultsToJSON(const std::string &filename) { + std::ofstream file(filename); + if (!file.is_open()) { + std::cerr << "Failed to open file: " << filename << std::endl; + return; + } + + file << "{\n \"benchmarks\": [\n"; + for (size_t i = 0; i < results.size(); ++i) { + const auto &result = results[i]; + file << " {\n" + << " \"name\": \"" << result.name << "\",\n" + << " \"total_time_ns\": " << result.total_time_ns << ",\n" + << " \"iterations\": " << result.iterations << ",\n" + << " \"avg_time_ns\": " << result.avg_time_ns << "\n" + << " }" << (i < results.size() - 1 ? "," : "") << "\n"; + } + file << " ]\n}\n"; + + file.close(); + std::cout << "Results exported to JSON: " << filename << std::endl; +} + +// Helper function to run a benchmark with warmup +template +BenchmarkResult runBenchmark(const std::string &name, F &&func, double rng_overhead = 0.0) { + std::cout << "\n--- Benchmark: " << name << " ---" << std::endl; + + // Warmup phase + if (config.warmup_iterations > 0) { + std::cout << "Warming up with " << config.warmup_iterations << " iterations..." + << std::endl; + for (int i = 0; i < config.warmup_iterations; i++) { + func(i); + } + } + + // Measurement phase + std::cout << "Running " << config.measurement_iterations << " iterations..." << std::endl; + auto start = std::chrono::high_resolution_clock::now(); + + for (int i = 0; i < config.measurement_iterations; i++) { + func(i); + if (config.debug && i % 100000 == 0) { + std::cout << "Progress: " << (i * 100 / config.measurement_iterations) << "%" + << std::endl; + } + } + + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + + double avg_time = (double)duration.count() / config.measurement_iterations; + if (rng_overhead > 0) { + avg_time -= rng_overhead; + } + + std::cout << "Total time: " << duration.count() << " ns" << std::endl; + std::cout << "Average time per operation: " << avg_time << " ns" << std::endl; + if (rng_overhead > 0) { + std::cout << " (RNG overhead of " << rng_overhead << " ns has been subtracted)" + << std::endl; + } + + return {name, static_cast(avg_time * config.measurement_iterations), + config.measurement_iterations, avg_time}; +} + +// Benchmark just the RNG overhead +BenchmarkResult measureRNGOverhead() { + std::mt19937 rng(42); + std::vector names(config.measurement_iterations); + std::vector kinds(config.measurement_iterations); + std::vector indices(config.measurement_iterations); + + return runBenchmark("RNG Overhead", [&](int i) { + indices[i] = rng() % NUM_NAMES; + names[i] = TEST_NAMES[indices[i]]; + kinds[i] = static_cast(rng() % 3); + }); +} + +// Main benchmark function +void benchmarkUnwindFailures() { + UnwindFailures failures; + results.clear(); // Clear any previous results + + std::cout << "=== Benchmarking UnwindFailures ===" << std::endl; + std::cout << "Configuration:" << std::endl; + std::cout << " Warmup iterations: " << config.warmup_iterations << std::endl; + std::cout << " Measurement iterations: " << config.measurement_iterations << std::endl; + std::cout << " Number of test names: " << NUM_NAMES << std::endl; + std::cout << " Debug mode: " << (config.debug ? "enabled" : "disabled") << std::endl; + + // First measure RNG overhead + std::cout << "\nMeasuring RNG overhead..." << std::endl; + auto rng_overhead = measureRNGOverhead(); + double overhead_per_op = rng_overhead.avg_time_ns; + std::cout << "RNG overhead per operation: " << overhead_per_op << " ns" << std::endl; + + // Create RNG for actual benchmarks + std::mt19937 rng(42); + + // Run actual benchmarks with RNG inline and overhead subtracted internally + results.push_back(runBenchmark( + "Record Single Failure Kind", + [&](int) { + int idx = rng() % NUM_NAMES; + auto kind = static_cast(rng() % 3); + failures.record(UNWIND_FAILURE_STUB, TEST_NAMES[idx]); + }, + overhead_per_op)); + + results.push_back(runBenchmark( + "Record Mixed Failures", + [&](int) { + int idx = rng() % NUM_NAMES; + auto kind = static_cast(rng() % 3); + failures.record(kind, TEST_NAMES[idx]); + }, + overhead_per_op)); + + results.push_back(runBenchmark( + "Find Name", + [&](int) { + int idx = rng() % NUM_NAMES; + failures.findName(TEST_NAMES[idx]); + }, + overhead_per_op)); + + results.push_back(runBenchmark( + "Count Failures with Mixed Kinds", + [&](int) { + int idx = rng() % NUM_NAMES; + auto kind = static_cast(rng() % 3); + failures.count(TEST_NAMES[idx], kind); + }, + overhead_per_op)); + + // For merge benchmark, we'll pre-populate the collections since that's not part of what we're + // measuring + UnwindFailures failures1; + UnwindFailures failures2; + // Use a smaller number of items for pre-population to avoid overflow + const int prePopulateCount = std::min(1000, config.measurement_iterations / 2); + for (int i = 0; i < prePopulateCount; i++) { + int idx = rng() % NUM_NAMES; + auto kind = static_cast(rng() % 3); + failures1.record(kind, TEST_NAMES[idx]); + failures2.record(kind, TEST_NAMES[idx]); + } + + results.push_back(runBenchmark("Merge Failures", [&](int) { + failures1.merge(failures2); + })); + + std::cout << "\n=== Benchmark Complete ===" << std::endl; +} + +void printUsage(const char *programName) { + std::cout << "Usage: " << programName << " [options]\n" + << "Options:\n" + << " --csv Export results to CSV file\n" + << " --json Export results to JSON file\n" + << " --warmup Number of warmup iterations (default: 100000)\n" + << " --iterations Number of measurement iterations (default: 1000000)\n" + << " --debug Enable debug output\n" + << " -h, --help Show this help message\n"; +} + +int main(int argc, char *argv[]) { + // Parse command line arguments + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--csv") == 0 && i + 1 < argc) { + config.csv_file = argv[++i]; + } else if (strcmp(argv[i], "--json") == 0 && i + 1 < argc) { + config.json_file = argv[++i]; + } else if (strcmp(argv[i], "--warmup") == 0 && i + 1 < argc) { + config.warmup_iterations = std::atoi(argv[++i]); + } else if (strcmp(argv[i], "--iterations") == 0 && i + 1 < argc) { + config.measurement_iterations = std::atoi(argv[++i]); + } else if (strcmp(argv[i], "--debug") == 0) { + config.debug = true; + } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + printUsage(argv[0]); + return 0; + } else { + std::cerr << "Unknown option: " << argv[i] << std::endl; + printUsage(argv[0]); + return 1; + } + } + + std::cout << "Running UnwindFailures benchmark..." << std::endl; + benchmarkUnwindFailures(); + + // Export results if requested + if (!config.csv_file.empty()) { + exportResultsToCSV(config.csv_file); + } + if (!config.json_file.empty()) { + exportResultsToJSON(config.json_file); + } + + return 0; +} diff --git a/ddprof-lib/build.gradle b/ddprof-lib/build.gradle index 9fd8e44fa..d6e5389a9 100644 --- a/ddprof-lib/build.gradle +++ b/ddprof-lib/build.gradle @@ -28,6 +28,13 @@ dependencies { project(':ddprof-lib:gtest') } +// Add a task to run all benchmarks +tasks.register('runBenchmarks') { + dependsOn ':ddprof-lib:benchmarks:runBenchmark' + group = 'verification' + description = 'Run all benchmarks' +} + test { onlyIf { !project.hasProperty('skip-tests') diff --git a/ddprof-lib/settings.gradle b/ddprof-lib/settings.gradle index b3141d229..1cee1b9e1 100644 --- a/ddprof-lib/settings.gradle +++ b/ddprof-lib/settings.gradle @@ -1 +1,2 @@ -rootProject.name = "JavaProfiler" \ No newline at end of file +rootProject.name = "JavaProfiler" +include ':ddprof-lib:benchmarks' diff --git a/ddprof-lib/src/main/cpp/codeCache.cpp b/ddprof-lib/src/main/cpp/codeCache.cpp index 37582e76e..985bc5c21 100644 --- a/ddprof-lib/src/main/cpp/codeCache.cpp +++ b/ddprof-lib/src/main/cpp/codeCache.cpp @@ -190,7 +190,7 @@ CodeBlob *CodeCache::findBlobByAddress(const void *address) { return NULL; } -const char *CodeCache::binarySearch(const void *address) { +const void *CodeCache::binarySearch(const void *address, const char **name) { int low = 0; int high = _count - 1; @@ -201,7 +201,10 @@ const char *CodeCache::binarySearch(const void *address) { } else if (_blobs[mid]._start > address) { high = mid - 1; } else { - return _blobs[mid]._name; + if (name != NULL) { + *name = _blobs[mid]._name; + } + return _blobs[mid]._start; } } @@ -210,7 +213,12 @@ const char *CodeCache::binarySearch(const void *address) { // point beyond the function. if (low > 0 && (_blobs[low - 1]._start == _blobs[low - 1]._end || _blobs[low - 1]._end == address)) { - return _blobs[low - 1]._name; + + if (name != NULL) { + *name = _blobs[low - 1]._name; + } + + return _blobs[low - 1]._start; } return _name; } diff --git a/ddprof-lib/src/main/cpp/codeCache.h b/ddprof-lib/src/main/cpp/codeCache.h index 2c35b00b0..677ff23d4 100644 --- a/ddprof-lib/src/main/cpp/codeCache.h +++ b/ddprof-lib/src/main/cpp/codeCache.h @@ -162,7 +162,7 @@ class CodeCache { CodeBlob *findBlob(const char *name); CodeBlob *findBlobByAddress(const void *address); - const char *binarySearch(const void *address); + const void *binarySearch(const void *address, const char **name); const void *findSymbol(const char *name); const void *findSymbolByPrefix(const char *prefix); const void *findSymbolByPrefix(const char *prefix, int prefix_len); diff --git a/ddprof-lib/src/main/cpp/dictionary.cpp b/ddprof-lib/src/main/cpp/dictionary.cpp index 8b3a17aaa..c1ca2860d 100644 --- a/ddprof-lib/src/main/cpp/dictionary.cpp +++ b/ddprof-lib/src/main/cpp/dictionary.cpp @@ -130,6 +130,10 @@ unsigned int Dictionary::lookup(const char *key, size_t length, bool for_insert, } } +bool Dictionary::check(const char* key) { + return lookup(key, strlen(key), false, 0) != 0; +} + unsigned int Dictionary::bounded_lookup(const char *key, size_t length, int size_limit) { // bounded lookup will find the encoding if the key is already mapped, diff --git a/ddprof-lib/src/main/cpp/dictionary.h b/ddprof-lib/src/main/cpp/dictionary.h index 434f67412..c5300991a 100644 --- a/ddprof-lib/src/main/cpp/dictionary.h +++ b/ddprof-lib/src/main/cpp/dictionary.h @@ -74,6 +74,7 @@ class Dictionary { void clear(); + bool check(const char* key); unsigned int lookup(const char *key); unsigned int lookup(const char *key, size_t length); unsigned int bounded_lookup(const char *key, size_t length, int size_limit); diff --git a/ddprof-lib/src/main/cpp/flightRecorder.cpp b/ddprof-lib/src/main/cpp/flightRecorder.cpp index 9b6fa3bd6..a4cdcebc7 100644 --- a/ddprof-lib/src/main/cpp/flightRecorder.cpp +++ b/ddprof-lib/src/main/cpp/flightRecorder.cpp @@ -19,6 +19,7 @@ #include "profiler.h" #include "rustDemangler.h" #include "spinLock.h" +#include "unwindStats.h" #include "symbols.h" #include "threadFilter.h" #include "threadState.h" @@ -35,6 +36,7 @@ #include #include #include +#include #include static SpinLock _rec_lock(1); @@ -423,6 +425,11 @@ off_t Recording::finishChunk(bool end_recording) { // information for now. writeCounters(_buf); + // Keep a simple stats for where we failed to unwind + // For the sakes of simplicity we are not keeping the count of failed unwinds which would also be + // just 'eventually consistent' because we do not want to block the unwinding while writing out the stats. + writeUnwindFailures(_buf); + for (int i = 0; i < CONCURRENCY_LEVEL; i++) { flush(&_buf[i]); } @@ -1249,6 +1256,22 @@ void Recording::writeCounters(Buffer *buf) { } } +void Recording::writeUnwindFailures(Buffer *buf) { + static UnwindFailures failures; + UnwindStats::collectAndReset(failures); + + failures.forEach([&](UnwindFailureKind kind, const char *name, u64 count) { + int start = buf->skip(1); + buf->putVar64(T_UNWIND_FAILURE); + buf->putVar64(_start_ticks); + buf->putUtf8((kind & UNWIND_FAILURE_STUB) ? "stub" : "other"); + buf->putUtf8(name); + buf->putVar64(count); + writeEventSizePrefix(buf, start); + flushIfNeeded(buf); + }); +} + void Recording::writeContext(Buffer *buf, Context &context) { buf->putVar64(context.spanId); buf->putVar64(context.rootSpanId); diff --git a/ddprof-lib/src/main/cpp/flightRecorder.h b/ddprof-lib/src/main/cpp/flightRecorder.h index b80e0e20e..978fa686a 100644 --- a/ddprof-lib/src/main/cpp/flightRecorder.h +++ b/ddprof-lib/src/main/cpp/flightRecorder.h @@ -237,6 +237,8 @@ class Recording { void writeCounters(Buffer *buf); + void writeUnwindFailures(Buffer *buf); + void writeContext(Buffer *buf, Context &context); void recordExecutionSample(Buffer *buf, int tid, u32 call_trace_id, diff --git a/ddprof-lib/src/main/cpp/jfrMetadata.cpp b/ddprof-lib/src/main/cpp/jfrMetadata.cpp index 60a7c3628..cf81ae4d4 100644 --- a/ddprof-lib/src/main/cpp/jfrMetadata.cpp +++ b/ddprof-lib/src/main/cpp/jfrMetadata.cpp @@ -279,6 +279,13 @@ void JfrMetadata::initialize( << field("name", T_COUNTER_NAME, "Name") << field("count", T_LONG, "Count")) + << (type("datadog.UnwindFailure", T_UNWIND_FAILURE, "Unwind Failure") + << category("Datadog") + << field("startTime", T_LONG, "Start Time", F_TIME_TICKS) + << field("kind", T_STRING, "Kind") + << field("name", T_STRING, "Name") + << field("count", T_LONG, "Count")) + << (type("jdk.OSInformation", T_OS_INFORMATION, "OS Information") << category("Operating System") << field("startTime", T_LONG, "Start Time", F_TIME_TICKS) diff --git a/ddprof-lib/src/main/cpp/jfrMetadata.h b/ddprof-lib/src/main/cpp/jfrMetadata.h index fecd908a9..77da96d3f 100644 --- a/ddprof-lib/src/main/cpp/jfrMetadata.h +++ b/ddprof-lib/src/main/cpp/jfrMetadata.h @@ -77,6 +77,7 @@ enum JfrType { T_QUEUE_TIME = 123, T_DATADOG_CLASSREF_CACHE = 124, T_DATADOG_COUNTER = 125, + T_UNWIND_FAILURE = 126, T_ANNOTATION = 200, T_LABEL = 201, T_CATEGORY = 202, diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 4d83c73f4..005e2f160 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -245,7 +245,11 @@ const char *Profiler::getLibraryName(const char *native_symbol) { const char *Profiler::findNativeMethod(const void *address) { CodeCache *lib = _libs->findLibraryByAddress(address); - return lib == NULL ? NULL : lib->binarySearch(address); + const char *name = NULL; + if (lib != NULL) { + lib->binarySearch(address, &name); + } + return name; } CodeBlob *Profiler::findRuntimeStub(const void *address) { diff --git a/ddprof-lib/src/main/cpp/stackFrame_aarch64.cpp b/ddprof-lib/src/main/cpp/stackFrame_aarch64.cpp index e169123e6..58d1ae475 100644 --- a/ddprof-lib/src/main/cpp/stackFrame_aarch64.cpp +++ b/ddprof-lib/src/main/cpp/stackFrame_aarch64.cpp @@ -105,7 +105,7 @@ bool StackFrame::unwindStub(instruction_t *entry, const char *name, sp += 32; pc = link(); return true; - } + } return false; } diff --git a/ddprof-lib/src/main/cpp/stackWalker.cpp b/ddprof-lib/src/main/cpp/stackWalker.cpp index 0d93d1cbf..c39394809 100644 --- a/ddprof-lib/src/main/cpp/stackWalker.cpp +++ b/ddprof-lib/src/main/cpp/stackWalker.cpp @@ -22,6 +22,7 @@ #include "safeAccess.h" #include "stackFrame.h" #include "symbols.h" +#include "thread.h" #include "vmStructs.h" #include @@ -264,10 +265,13 @@ int StackWalker::walkVM(void* ucontext, ASGCT_CallFrame* frames, int max_depth, VMThread *vm_thread = VMThread::current(); void *saved_exception = vm_thread != NULL ? vm_thread->exception() : NULL; - // Should be preserved across setjmp/longjmp - volatile int depth = 0; JavaFrameAnchor* anchor = nullptr; bool recovered_from_anchor = false; + ProfiledThread *pThread = ProfiledThread::current(); + + // Should be preserved across setjmp/longjmp + volatile int depth = 0; + UnwindFailures *unwindFailures = pThread != nullptr ? pThread->unwindFailures() : nullptr; if (vm_thread != NULL) { vm_thread->exception() = &crash_protection_ctx; @@ -277,6 +281,9 @@ int StackWalker::walkVM(void* ucontext, ASGCT_CallFrame* frames, int max_depth, TEST_LOG("Crash protection triggered"); fillErrorFrame(frames[depth++], "break_not_walkable", truncated); } + if (unwindFailures) { + UnwindStats::recordFailures(unwindFailures); + } return depth; } anchor = vm_thread->anchor(); @@ -473,13 +480,20 @@ int StackWalker::walkVM(void* ucontext, ASGCT_CallFrame* frames, int max_depth, pc = stripPointer(*(const void **)(fp +sizeof(void*))); fp = *(uintptr_t *)fp; sp = fp; - continue; + if (profiler->isAddressInCode(pc, true)) { + continue; + } } if (depth > 1 && nm->frameSize() > 0) { sp += nm->frameSize() * sizeof(void *); fp = ((uintptr_t *)sp)[-FRAME_PC_SLOT - 1]; pc = ((const void **)sp)[-FRAME_PC_SLOT]; - continue; + if (profiler->isAddressInCode(pc, true)) { + continue; + } + } + if (unwindFailures) { + unwindFailures->record(UNWIND_FAILURE_STUB, name); } } } @@ -490,11 +504,12 @@ int StackWalker::walkVM(void* ucontext, ASGCT_CallFrame* frames, int max_depth, if (cc == NULL || !cc->contains(pc)) { cc = libraries->findLibraryByAddress(pc); } - const char *name = cc == NULL ? NULL : cc->binarySearch(pc); + const char *name = NULL; + const void* symbolPc = cc == NULL ? NULL : cc->binarySearch(pc, &name); fillFrame(frames[depth++], BCI_NATIVE_FRAME, name); - if (Symbols::isRootSymbol(pc)) { + if (symbolPc && Symbols::isRootSymbol(symbolPc)) { break; } prev_pc = pc; @@ -599,6 +614,11 @@ int StackWalker::walkVM(void* ucontext, ASGCT_CallFrame* frames, int max_depth, if (vm_thread != NULL) vm_thread->exception() = saved_exception; + if (unwindFailures && !unwindFailures->empty()) { + TEST_LOG("Recording unwind failures"); + UnwindStats::recordFailures(unwindFailures); + } + return depth; } diff --git a/ddprof-lib/src/main/cpp/symbols_linux.cpp b/ddprof-lib/src/main/cpp/symbols_linux.cpp index 11626fb94..090f16e93 100644 --- a/ddprof-lib/src/main/cpp/symbols_linux.cpp +++ b/ddprof-lib/src/main/cpp/symbols_linux.cpp @@ -353,9 +353,9 @@ void ElfParser::loadSymbols(bool use_debug) { void ElfParser::addSymbol(const void *start, int length, const char *name, bool update_bounds) { _cc->add(start, length, name, update_bounds); - for (int i = 0; i < sizeof(root_symbol_table)/sizeof(root_symbol_table[0]); i++) { + for (int i = 0; i < LAST_ROOT_SYMBOL_KIND; i++) { if (!strcmp(root_symbol_table[i].name, name)) { - TEST_LOG("===> found %s", name); + TEST_LOG("Adding root symbol %s: %p", name, start); _root_symbols[root_symbol_table[i].kind] = (uintptr_t)start; break; } diff --git a/ddprof-lib/src/main/cpp/symbols_linux.h b/ddprof-lib/src/main/cpp/symbols_linux.h index 615348ab7..8a818b88c 100644 --- a/ddprof-lib/src/main/cpp/symbols_linux.h +++ b/ddprof-lib/src/main/cpp/symbols_linux.h @@ -127,7 +127,8 @@ static const bool MUSL = false; X(start_thread, "start_thread") \ X(_ZL19thread_native_entryP6Thread, "_ZL19thread_native_entryP6Thread") \ X(_thread_start, "_thread_start") \ - X(thread_start, "thread_start") + X(thread_start, "thread_start") \ + X(thread_native_entry, "thread_native_entry") #define X_ENUM(a, b) a, typedef enum RootSymbolKind : int { diff --git a/ddprof-lib/src/main/cpp/thread.h b/ddprof-lib/src/main/cpp/thread.h index 085b217d0..264ad6b8b 100644 --- a/ddprof-lib/src/main/cpp/thread.h +++ b/ddprof-lib/src/main/cpp/thread.h @@ -3,6 +3,7 @@ #include "os.h" #include "threadLocalData.h" +#include "unwindStats.h" #include #include #include @@ -42,6 +43,7 @@ class ProfiledThread : public ThreadLocalData { u32 _wall_epoch; u32 _call_trace_id; u32 _recording_epoch; + UnwindFailures _unwind_failures; ProfiledThread(int buffer_pos, int tid) : ThreadLocalData(), _pc(0), _span_id(0), _crash_depth(0), _buffer_pos(buffer_pos), _tid(tid), _cpu_epoch(0), @@ -110,6 +112,13 @@ class ProfiledThread : public ThreadLocalData { return _crash_depth > CRASH_HANDLER_NESTING_LIMIT; } + UnwindFailures* unwindFailures(bool reset = true) { + if (reset) { + _unwind_failures.clear(); + } + return &_unwind_failures; + } + static void signalHandler(int signo, siginfo_t *siginfo, void *ucontext); }; diff --git a/ddprof-lib/src/main/cpp/unwindStats.cpp b/ddprof-lib/src/main/cpp/unwindStats.cpp new file mode 100644 index 000000000..82a38cf17 --- /dev/null +++ b/ddprof-lib/src/main/cpp/unwindStats.cpp @@ -0,0 +1,5 @@ +#include "unwindStats.h" + +// initialize static members +SpinLock UnwindStats::_lock; +UnwindFailures UnwindStats::_unwind_failures; diff --git a/ddprof-lib/src/main/cpp/unwindStats.h b/ddprof-lib/src/main/cpp/unwindStats.h new file mode 100644 index 000000000..1eb4eab29 --- /dev/null +++ b/ddprof-lib/src/main/cpp/unwindStats.h @@ -0,0 +1,224 @@ +#ifndef STUB_UNWIND_STATS_H +#define STUB_UNWIND_STATS_H + +#include "common.h" +#include "spinLock.h" + +#include +#include + +enum UnwindFailureKind { + UNWIND_FAILURE_STUB = 1, + UNWIND_FAILURE_COMPILED = 2, + UNWIND_FAILURE_ANY = 3, // UNWIND_FAILURE_STUB | UNWIND_FAILURE_COMPILED +}; + +// Maximum number of unique names that can be tracked +#define MAX_UNWIND_FAILURE_NAMES 1024 +// Maximum length of a name string +#define MAX_NAME_LENGTH 256 + +class UnwindFailures { + private: + char (*_names)[MAX_NAME_LENGTH]; + volatile int _nameCount; + volatile u64 (*_counters)[UNWIND_FAILURE_ANY + 1]; + + public: + UnwindFailures() : _nameCount(0) { + _names = new char[MAX_UNWIND_FAILURE_NAMES][MAX_NAME_LENGTH]; + _counters = new u64[MAX_UNWIND_FAILURE_NAMES][UNWIND_FAILURE_ANY + 1]; + memset((void*)_names, 0, MAX_UNWIND_FAILURE_NAMES * MAX_NAME_LENGTH); + memset((void*)_counters, 0, MAX_UNWIND_FAILURE_NAMES * (UNWIND_FAILURE_ANY + 1) * sizeof(u64)); + } + + ~UnwindFailures() { + delete[] _names; + delete[] _counters; + } + + // Disable copy constructor and assignment operator + UnwindFailures(const UnwindFailures&) = delete; + UnwindFailures& operator=(const UnwindFailures&) = delete; + + void record(UnwindFailureKind kind, const char *name) { + if (!name) return; + + // Fast path: try to find existing name first + int index = findName(name); + if (index >= 0) { + _counters[index][kind - 1]++; + return; + } + + // Slow path: create new entry if needed + index = findOrCreateName(name); + if (index >= 0) { + _counters[index][kind - 1]++; + } + } + + int findName(const char *name) const { + for (int i = 0; i < _nameCount; i++) { + if (strcmp(_names[i], name) == 0) { + return i; + } + } + return -1; + } + + int findOrCreateName(const char *name) { + int index = findName(name); + if (index >= 0) { + return index; + } + + int newIndex = _nameCount++; + if (newIndex < MAX_UNWIND_FAILURE_NAMES) { + size_t len = strlen(name); + if (len >= MAX_NAME_LENGTH) { + len = MAX_NAME_LENGTH - 1; + } + memcpy(_names[newIndex], name, len); + _names[newIndex][len] = '\0'; + return newIndex; + } + + return -1; + } + + u64 count(const char *name, UnwindFailureKind kind = UNWIND_FAILURE_ANY) const { + int index = findName(name); + if (index < 0) { + return 0; + } + + if (kind == UNWIND_FAILURE_ANY) { + return _counters[index][UNWIND_FAILURE_STUB - 1] + + _counters[index][UNWIND_FAILURE_COMPILED - 1]; + } + return _counters[index][kind - 1]; + } + + void clear() { + _nameCount = 0; + memset((void*)_names, 0, MAX_UNWIND_FAILURE_NAMES * MAX_NAME_LENGTH); + memset((void*)_counters, 0, MAX_UNWIND_FAILURE_NAMES * (UNWIND_FAILURE_ANY + 1) * sizeof(u64)); + } + + bool empty() const { + return _nameCount == 0; + } + + void merge(const UnwindFailures &other) { + for (int i = 0; i < other._nameCount; i++) { + int index = findOrCreateName(other._names[i]); + if (index >= 0) { + for (int j = 0; j < UNWIND_FAILURE_ANY; j++) { + _counters[index][j] += other._counters[i][j]; + } + } + } + } + + void swap(UnwindFailures &other) { + // Swap pointers + char (*temp_names)[MAX_NAME_LENGTH] = const_cast(_names); + _names = other._names; + other._names = temp_names; + + u64 (*temp_counters)[UNWIND_FAILURE_ANY + 1] = const_cast(_counters); + _counters = other._counters; + other._counters = temp_counters; + + // Swap name count + int temp_count = _nameCount; + _nameCount = other._nameCount; + other._nameCount = temp_count; + } + + template + void forEach(Func fn) const + { + for (int i = 0; i < _nameCount; i++) + { + const char *name = _names[i]; + for (int j = 0; j < 2; j++) + { + UnwindFailureKind kind = static_cast(j + 1); + u64 count = _counters[i][j]; + if (count > 0) + { + fn(kind, name, count); + } + } + } + } +}; + +class ExclusiveLock { + private: + SpinLock &_lock; + public: + ExclusiveLock(SpinLock &lock) : _lock(lock) { + _lock.lock(); + } + ~ExclusiveLock() { + _lock.unlock(); + } + + ExclusiveLock(const ExclusiveLock &) = delete; + ExclusiveLock &operator=(const ExclusiveLock &) = delete; +}; + +class SharedLock { + private: + SpinLock &_lock; + public: + SharedLock(SpinLock &lock) : _lock(lock) { + _lock.lockShared(); + } + ~SharedLock() { + _lock.unlockShared(); + } + + SharedLock(const SharedLock &) = delete; + SharedLock &operator=(const SharedLock &) = delete; +}; + +class UnwindStats +{ + private: + static SpinLock _lock; + static UnwindFailures _unwind_failures; + public: + static void recordFailures(UnwindFailures &failures) { + if (_lock.tryLock()) { + _unwind_failures.merge(failures); + _lock.unlock(); + } + } + + static void recordFailures(UnwindFailures *failures) { + if (failures) { + recordFailures(*failures); + } + } + + static u64 countFailures(const char* name, UnwindFailureKind kind = UNWIND_FAILURE_ANY) { + SharedLock l(_lock); + return _unwind_failures.count(name, kind); + } + + static void collectAndReset(UnwindFailures& result) { + ExclusiveLock l(_lock); + result.swap(_unwind_failures); + } + + static void reset() { + ExclusiveLock l(_lock); + _unwind_failures.clear(); + } +}; + +#endif // STUB_UNWIND_STATS_H diff --git a/ddprof-lib/src/test/cpp/ddprof_ut.cpp b/ddprof-lib/src/test/cpp/ddprof_ut.cpp index 23565f0cf..6c5c8b93e 100644 --- a/ddprof-lib/src/test/cpp/ddprof_ut.cpp +++ b/ddprof-lib/src/test/cpp/ddprof_ut.cpp @@ -6,10 +6,13 @@ #include "counters.h" #include "mutex.h" #include "os.h" + #include "unwindStats.h" #include "threadFilter.h" #include "threadInfo.h" #include "threadLocalData.h" #include "vmEntry.h" + #include + #include #include ssize_t callback(char* ptr, int len) { @@ -246,6 +249,149 @@ EXPECT_EQ(24, hs_version1); } + TEST(UnwindFailures, BasicFunctionality) { + UnwindFailures failures; + + // Test recording failures + EXPECT_EQ(0, failures.count("test_stub1")); + failures.record(UNWIND_FAILURE_STUB, "test_stub1"); + EXPECT_EQ(1, failures.count("test_stub1")); + + failures.record(UNWIND_FAILURE_COMPILED, "test_stub1"); + EXPECT_EQ(1, failures.count("test_stub1", UNWIND_FAILURE_STUB)); + EXPECT_EQ(1, failures.count("test_stub1", UNWIND_FAILURE_COMPILED)); + EXPECT_EQ(2, failures.count("test_stub1")); + + // Test different stubs + EXPECT_EQ(0, failures.count("test_stub2")); + failures.record(UNWIND_FAILURE_STUB, "test_stub2"); + EXPECT_EQ(2, failures.count("test_stub1")); + EXPECT_EQ(1, failures.count("test_stub2")); + + // Test reset + failures.clear(); + EXPECT_TRUE(failures.empty()); + EXPECT_EQ(0, failures.count("test_stub1")); + EXPECT_EQ(0, failures.count("test_stub2")); + } + + TEST(UnwindFailures, HashCollisions) { + UnwindFailures failures; + + // Test multiple entries that might collide + for (int i = 0; i < 100; i++) { + char name[32]; + snprintf(name, sizeof(name), "stub_%d", i); + EXPECT_EQ(0, failures.count(name)); + failures.record(UNWIND_FAILURE_STUB, name); + EXPECT_EQ(1, failures.count(name)); + } + + // Verify counts are correct + for (int i = 0; i < 100; i++) { + char name[32]; + snprintf(name, sizeof(name), "stub_%d", i); + EXPECT_EQ(1, failures.count(name)); + } + } + + TEST(UnwindFailures, SwapEmptyInstances) { + UnwindFailures failures1; + UnwindFailures failures2; + + // Verify both instances are empty + EXPECT_TRUE(failures1.empty()); + EXPECT_TRUE(failures2.empty()); + + // Swap the empty instances + failures1.swap(failures2); + + // Verify both instances are still empty after swap + EXPECT_TRUE(failures1.empty()); + EXPECT_TRUE(failures2.empty()); + + // Add some data to failures1 + failures1.record(UNWIND_FAILURE_STUB, "test_stub1"); + EXPECT_FALSE(failures1.empty()); + EXPECT_TRUE(failures2.empty()); + + // Swap again + failures1.swap(failures2); + + // Verify data moved correctly + EXPECT_TRUE(failures1.empty()); + EXPECT_FALSE(failures2.empty()); + EXPECT_EQ(1, failures2.count("test_stub1")); + } + + TEST(UnwindStats, CollectAndReset) { + // Record some failures + UnwindFailures failures; + failures.record(UNWIND_FAILURE_STUB, "test_stub1"); + failures.record(UNWIND_FAILURE_STUB, "test_stub2"); + UnwindStats::recordFailures(failures); + + // Collect and reset + UnwindFailures result; + UnwindStats::collectAndReset(result); + + // Verify the result contains the recorded failures + EXPECT_EQ(1, result.count("test_stub1")); + EXPECT_EQ(1, result.count("test_stub2")); + + // Verify the stats are reset + UnwindFailures empty_result; + UnwindStats::collectAndReset(empty_result); + EXPECT_TRUE(empty_result.empty()); + } + + TEST(UnwindStatsTest, ThreadSafety) { + UnwindStats::reset(); + + const int numThreads = 4; + const int iterations = 10000; + std::thread threads[numThreads]; + + fprintf(stderr, "Starting %d threads with %d iterations each\n", numThreads, iterations); + // Each thread records failures for different stubs + for (int i = 0; i < numThreads; i++) { + threads[i] = std::thread([i, iterations]() { + UnwindFailures failures; // per-thread instance + char name[32]; + snprintf(name, sizeof(name), "thread_%d_stub", i); + for (int j = 0; j < iterations; j++) { + failures.record(UNWIND_FAILURE_STUB, name); + } + // record the thread-stats + UnwindStats::recordFailures(failures); + }); + } + + fprintf(stderr, "Waiting for threads to finish\n"); + // Wait for all threads + for (int i = 0; i < numThreads; i++) { + threads[i].join(); + } + + fprintf(stderr, "All threads finished\n"); + fprintf(stderr, "Verifying counts\n"); + + UnwindFailures result; + UnwindStats::collectAndReset(result); + // Verify counts + u64 globalCount = 0; + for (int i = 0; i < numThreads; i++) { + char name[32]; + snprintf(name, sizeof(name), "thread_%d_stub", i); + // due to expected concurrency issues some failures may not be counted + // failure recording prefers dropping the failure over blocking on the lock + u64 count = result.count(name); + EXPECT_TRUE(count <= iterations); + globalCount += count; + } + EXPECT_TRUE(globalCount > 0); + } + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/settings.gradle b/settings.gradle index 7a5e35647..e6a9219f5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,6 @@ include ':ddprof-lib' include ':ddprof-lib:gtest' +include ':ddprof-lib:benchmarks' include ':ddprof-test-tracer' include ':ddprof-test' include ':malloc-shim'