/* ****************************************************************************** * * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at * https://www.apache.org/licenses/LICENSE-2.0. * * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. * * SPDX-License-Identifier: Apache-2.0 ******************************************************************************/ // // Implementation of lifecycle tracking native API functions // Author: Adam Gibson // #include // Forward declare the ComprehensiveLeakAnalyzer class before including tracker headers // This ensures the friend declarations in the tracker classes can see the class namespace sd { namespace analysis { class ComprehensiveLeakAnalyzer; } } // Always include lifecycle trackers - they work without SD_GCC_FUNCTRACE // but stack trace capture is only enabled when SD_GCC_FUNCTRACE is defined #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef _WIN32 #include #include #include #include #endif #ifdef __linux__ #include #endif // Only include filesystem and AllocationLogger when functrace is enabled #if defined(SD_GCC_FUNCTRACE) #include #include #include #include #endif using namespace sd::array; #if defined(SD_GCC_FUNCTRACE) namespace { #ifndef _WIN32 struct CrashEvent { int signal; void* faultAddress; long crashingThreadId; }; constexpr int kCrashSignals[] = { SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGABRT }; class LifecycleCrashHandler { public: static LifecycleCrashHandler& instance() { static LifecycleCrashHandler handler; return handler; } void ensureInitialized() { bool expected = false; if (!_initialized.compare_exchange_strong(expected, true)) { return; } if (::pipe(_signalPipe) != 0) { std::cerr << "[sd-crash] Failed to create crash notification pipe" << std::endl; _initialized.store(false); return; } _worker = std::thread(&LifecycleCrashHandler::workerLoop, this); _worker.detach(); setupAltStack(); installHandlers(); _ready.store(true, std::memory_order_release); } private: LifecycleCrashHandler() { _signalPipe[0] = -1; _signalPipe[1] = -1; _dumpComplete.store(true); } void setupAltStack() { const size_t altStackSize = determineAltStackSize(); _altStackStorage.assign(altStackSize, 0); stack_t ss; ss.ss_sp = _altStackStorage.data(); ss.ss_size = _altStackStorage.size(); ss.ss_flags = 0; if (sigaltstack(&ss, &_previousAltStack) == 0) { _altStackInstalled = true; } } void installHandlers() { struct sigaction sa; std::memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = &LifecycleCrashHandler::signalHandler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER; for (size_t i = 0; i < kSignalCount; ++i) { if (sigaction(kCrashSignals[i], &sa, &_oldHandlers[i]) != 0) { std::cerr << "[sd-crash] Failed to install handler for signal " << kCrashSignals[i] << std::endl; } } _handlersInstalled = true; } static void signalHandler(int signo, siginfo_t* info, void* ucontext) { LifecycleCrashHandler::instance().handleSignal(signo, info, ucontext); } void handleSignal(int signo, siginfo_t* info, void* ucontext) { if (!_ready.load(std::memory_order_acquire)) { restoreAndReraise(signo, info, ucontext); return; } if (_handling.exchange(true, std::memory_order_acq_rel)) { restoreAndReraise(signo, info, ucontext); return; } CrashEvent event{}; event.signal = signo; event.faultAddress = info ? info->si_addr : nullptr; event.crashingThreadId = currentThreadId(); ssize_t wrote = ::write(_signalPipe[1], &event, sizeof(event)); if (wrote != sizeof(event)) { // If write fails, just continue - better to let JVM handle it than hang std::cerr << "[sd-crash] Failed to notify dump worker thread\n"; } // Reset handling flag and immediately chain to JVM handler // The dump will happen asynchronously in the worker thread _handling.store(false, std::memory_order_release); restoreAndReraise(signo, info, ucontext); } void restoreAndReraise(int signo, siginfo_t* info, void* ucontext) { if (!_handlersInstalled) { // No old handler to call, just raise the signal ::raise(signo); return; } // Find and call the old handler for this signal for (size_t i = 0; i < kSignalCount; ++i) { if (kCrashSignals[i] == signo) { struct sigaction& oldHandler = _oldHandlers[i]; #ifdef __cpp_exceptions try { if (oldHandler.sa_flags & SA_SIGINFO) { // Old handler is a sigaction-style handler (takes siginfo_t and ucontext) if (oldHandler.sa_sigaction != nullptr && oldHandler.sa_sigaction != (void (*)(int, siginfo_t*, void*))SIG_DFL && oldHandler.sa_sigaction != (void (*)(int, siginfo_t*, void*))SIG_IGN) { // Call the original handler with the ORIGINAL siginfo and ucontext // This preserves si_code, si_addr, and all other signal information oldHandler.sa_sigaction(signo, info, ucontext); return; } } else { // Old handler is a simple signal handler (only takes signal number) if (oldHandler.sa_handler != SIG_DFL && oldHandler.sa_handler != SIG_IGN) { oldHandler.sa_handler(signo); return; } } } catch (const std::exception& e) { // Old handler threw an exception - log it and convert to signal std::cerr << "[sd-crash] Exception from old signal handler for signal " << signo << ": " << e.what() << std::endl; std::cerr << "[sd-crash] Converting exception to signal termination" << std::endl; // Fall through to restore and raise } catch (...) { // Unknown exception from old handler std::cerr << "[sd-crash] Unknown exception from old signal handler for signal " << signo << std::endl; std::cerr << "[sd-crash] Converting exception to signal termination" << std::endl; // Fall through to restore and raise } #else if (oldHandler.sa_flags & SA_SIGINFO) { // Old handler is a sigaction-style handler (takes siginfo_t and ucontext) if (oldHandler.sa_sigaction != nullptr && oldHandler.sa_sigaction != (void (*)(int, siginfo_t*, void*))SIG_DFL && oldHandler.sa_sigaction != (void (*)(int, siginfo_t*, void*))SIG_IGN) { // Call the original handler with the ORIGINAL siginfo and ucontext // This preserves si_code, si_addr, and all other signal information oldHandler.sa_sigaction(signo, info, ucontext); return; } } else { // Old handler is a simple signal handler (only takes signal number) if (oldHandler.sa_handler != SIG_DFL && oldHandler.sa_handler != SIG_IGN) { oldHandler.sa_handler(signo); return; } } #endif // If we get here, the old handler was SIG_DFL or SIG_IGN, or threw an exception // Restore it and raise the signal (this is safe since it's the default handler) sigaction(signo, &oldHandler, nullptr); ::raise(signo); return; } } // Signal not found in our list - just raise it ::raise(signo); } void restoreOriginal(int signo) { if (!_handlersInstalled) return; for (size_t i = 0; i < kSignalCount; ++i) { if (kCrashSignals[i] == signo) { sigaction(signo, &_oldHandlers[i], nullptr); break; } } } void workerLoop() { while (true) { CrashEvent event{}; ssize_t rd = ::read(_signalPipe[0], &event, sizeof(event)); if (rd == sizeof(event)) { dumpCrash(event); _dumpComplete.store(true, std::memory_order_release); } } } void dumpCrash(const CrashEvent &event) { std::string path = buildCrashFilePath(); std::ofstream out(path, std::ios::out | std::ios::trunc); if (!out.is_open()) { std::cerr << "[sd-crash] Failed to open crash log at " << path << std::endl; return; } auto now = std::chrono::system_clock::now(); std::time_t now_time = std::chrono::system_clock::to_time_t(now); std::tm time_buf; localtime_r(&now_time, &time_buf); out << "============================================\n"; out << " ND4J Native Crash Report\n"; out << "============================================\n"; out << "Timestamp: " << std::put_time(&time_buf, "%Y-%m-%d %H:%M:%S") << "\n"; out << "PID: " << getpid() << "\n"; out << "Thread: " << event.crashingThreadId << "\n"; out << "Signal: " << event.signal << " (" << signalName(event.signal) << ")\n"; out << "Address: " << event.faultAddress << "\n\n"; bool matched = false; // NDArray allocation lookup #ifdef __cpp_exceptions try { matched |= NDArrayLifecycleTracker::getInstance().logAllocationForPointer(event.faultAddress, out); } catch (const std::exception& e) { out << "[sd-crash] NDArray logAllocationForPointer failed: " << e.what() << "\n"; } catch (...) { out << "[sd-crash] NDArray logAllocationForPointer failed: unknown exception\n"; } #else matched |= NDArrayLifecycleTracker::getInstance().logAllocationForPointer(event.faultAddress, out); #endif // DataBuffer allocation lookup #ifdef __cpp_exceptions try { matched |= DataBufferLifecycleTracker::getInstance().logAllocationForAddress(event.faultAddress, out); } catch (const std::exception& e) { out << "[sd-crash] DataBuffer logAllocationForAddress failed: " << e.what() << "\n"; } catch (...) { out << "[sd-crash] DataBuffer logAllocationForAddress failed: unknown exception\n"; } #else matched |= DataBufferLifecycleTracker::getInstance().logAllocationForAddress(event.faultAddress, out); #endif // ShapeCache allocation lookup #ifdef __cpp_exceptions try { matched |= ShapeCacheLifecycleTracker::getInstance().logShapeForAddress(event.faultAddress, out); } catch (const std::exception& e) { out << "[sd-crash] ShapeCache logShapeForAddress failed: " << e.what() << "\n"; } catch (...) { out << "[sd-crash] ShapeCache logShapeForAddress failed: unknown exception\n"; } #else matched |= ShapeCacheLifecycleTracker::getInstance().logShapeForAddress(event.faultAddress, out); #endif // TADCache allocation lookup #ifdef __cpp_exceptions try { matched |= TADCacheLifecycleTracker::getInstance().logTADForAddress(event.faultAddress, out); } catch (const std::exception& e) { out << "[sd-crash] TADCache logTADForAddress failed: " << e.what() << "\n"; } catch (...) { out << "[sd-crash] TADCache logTADForAddress failed: unknown exception\n"; } #else matched |= TADCacheLifecycleTracker::getInstance().logTADForAddress(event.faultAddress, out); #endif if (!matched) { out << "No tracked allocation matched the faulting address.\n"; } // NDArray statistics and leaks out << "\n=== NDArray Snapshot ===\n"; #ifdef __cpp_exceptions try { NDArrayLifecycleTracker::getInstance().printStatistics(out); } catch (const std::exception& e) { out << "[sd-crash] NDArray printStatistics failed: " << e.what() << "\n"; } catch (...) { out << "[sd-crash] NDArray printStatistics failed: unknown exception\n"; } #else NDArrayLifecycleTracker::getInstance().printStatistics(out); #endif #ifdef __cpp_exceptions try { NDArrayLifecycleTracker::getInstance().printCurrentLeaks(out); } catch (const std::exception& e) { out << "[sd-crash] NDArray printCurrentLeaks failed: " << e.what() << "\n"; } catch (...) { out << "[sd-crash] NDArray printCurrentLeaks failed: unknown exception\n"; } #else NDArrayLifecycleTracker::getInstance().printCurrentLeaks(out); #endif // DataBuffer statistics and leaks out << "\n=== DataBuffer Snapshot ===\n"; #ifdef __cpp_exceptions try { DataBufferLifecycleTracker::getInstance().printStatistics(out); } catch (const std::exception& e) { out << "[sd-crash] DataBuffer printStatistics failed: " << e.what() << "\n"; } catch (...) { out << "[sd-crash] DataBuffer printStatistics failed: unknown exception\n"; } #else DataBufferLifecycleTracker::getInstance().printStatistics(out); #endif #ifdef __cpp_exceptions try { DataBufferLifecycleTracker::getInstance().printCurrentLeaks(out); } catch (const std::exception& e) { out << "[sd-crash] DataBuffer printCurrentLeaks failed: " << e.what() << "\n"; } catch (...) { out << "[sd-crash] DataBuffer printCurrentLeaks failed: unknown exception\n"; } #else DataBufferLifecycleTracker::getInstance().printCurrentLeaks(out); #endif // ShapeCache statistics and leaks out << "\n=== Shape Cache Snapshot ===\n"; #ifdef __cpp_exceptions try { ShapeCacheLifecycleTracker::getInstance().printStatistics(out); } catch (const std::exception& e) { out << "[sd-crash] ShapeCache printStatistics failed: " << e.what() << "\n"; } catch (...) { out << "[sd-crash] ShapeCache printStatistics failed: unknown exception\n"; } #else ShapeCacheLifecycleTracker::getInstance().printStatistics(out); #endif #ifdef __cpp_exceptions try { ShapeCacheLifecycleTracker::getInstance().printCurrentLeaks(out); } catch (const std::exception& e) { out << "[sd-crash] ShapeCache printCurrentLeaks failed: " << e.what() << "\n"; } catch (...) { out << "[sd-crash] ShapeCache printCurrentLeaks failed: unknown exception\n"; } #else ShapeCacheLifecycleTracker::getInstance().printCurrentLeaks(out); #endif // TADCache statistics and leaks out << "\n=== TAD Cache Snapshot ===\n"; #ifdef __cpp_exceptions try { TADCacheLifecycleTracker::getInstance().printStatistics(out); } catch (const std::exception& e) { out << "[sd-crash] TADCache printStatistics failed: " << e.what() << "\n"; } catch (...) { out << "[sd-crash] TADCache printStatistics failed: unknown exception\n"; } #else TADCacheLifecycleTracker::getInstance().printStatistics(out); #endif #ifdef __cpp_exceptions try { TADCacheLifecycleTracker::getInstance().printCurrentLeaks(out); } catch (const std::exception& e) { out << "[sd-crash] TADCache printCurrentLeaks failed: " << e.what() << "\n"; } catch (...) { out << "[sd-crash] TADCache printCurrentLeaks failed: unknown exception\n"; } #else TADCacheLifecycleTracker::getInstance().printCurrentLeaks(out); #endif out.close(); std::cerr << "[sd-crash] Crash dump written to " << path << std::endl; } std::string buildCrashFilePath() { namespace fs = std::filesystem; std::error_code ec; fs::path cwd = fs::current_path(ec); if (ec) { cwd = "."; } std::string base = "sd_crash_pid" + std::to_string(getpid()); fs::path candidate = cwd / (base + ".log"); int suffix = 1; while (fs::exists(candidate, ec)) { candidate = cwd / (base + "_" + std::to_string(suffix++) + ".log"); } return candidate.string(); } static const char* signalName(int signo) { switch (signo) { case SIGSEGV: return "SIGSEGV"; case SIGBUS: return "SIGBUS"; case SIGILL: return "SIGILL"; case SIGFPE: return "SIGFPE"; case SIGABRT: return "SIGABRT"; default: return "UNKNOWN"; } } static long currentThreadId() { #if defined(__linux__) return static_cast(::syscall(SYS_gettid)); #else return static_cast(reinterpret_cast(pthread_self())); #endif } static constexpr size_t kSignalCount = sizeof(kCrashSignals) / sizeof(int); std::atomic _initialized{false}; std::atomic _ready{false}; std::atomic _handling{false}; std::atomic _dumpComplete{true}; int _signalPipe[2]; std::thread _worker; std::array _oldHandlers{}; bool _handlersInstalled{false}; stack_t _previousAltStack{}; bool _altStackInstalled{false}; std::vector _altStackStorage; static size_t determineAltStackSize() { long baseSize = 0; #if defined(SIGSTKSZ) baseSize = SIGSTKSZ; #endif #if defined(MINSIGSTKSZ) long minSize = MINSIGSTKSZ; #else long minSize = 64 * 1024; // 64KB fallback when platform doesn't define MINSIGSTKSZ #endif if (baseSize < minSize) { baseSize = minSize; } if (baseSize <= 0) { baseSize = minSize; } return static_cast(baseSize) * 4; } }; #else class LifecycleCrashHandler { public: static LifecycleCrashHandler& instance() { static LifecycleCrashHandler handler; return handler; } void ensureInitialized() {} }; #endif // _WIN32 } // namespace #endif // SD_GCC_FUNCTRACE (crash handlers section) // Forward declarations for cache clearing functions SD_LIB_EXPORT void clearTADCache(); SD_LIB_EXPORT void clearShapeCache(); SD_LIB_EXPORT void checkAndCleanupCaches(); // Note: ComprehensiveLeakAnalyzer is forward declared but not yet implemented // The friend declarations in lifecycle tracker classes allow for future extension // initializeLifecycleCrashHandlers moved to end of file // (single definition with #if SD_GCC_FUNCTRACE guard inside) // ═══════════════════════════════════════════════════════════════════════════ // LIFECYCLE STATS AND REPORT FUNCTIONS - Always available // These functions use the trackers which work without SD_GCC_FUNCTRACE. // Stack trace output will be limited without functrace, but stats work. // ═══════════════════════════════════════════════════════════════════════════ /** * Converts NDArray lifecycle statistics to JSON format. */ const char* getNDArrayLifecycleStats() { auto stats = NDArrayLifecycleTracker::getInstance().getStats(); std::ostringstream json; json << "{\n"; json << " \"total_allocations\": " << stats.totalAllocations << ",\n"; json << " \"total_deallocations\": " << stats.totalDeallocations << ",\n"; json << " \"current_live\": " << stats.currentLive << ",\n"; json << " \"total_bytes_allocated\": " << stats.totalBytesAllocated << ",\n"; json << " \"total_bytes_deallocated\": " << stats.totalBytesDeallocated << ",\n"; json << " \"peak_live\": " << stats.peakLive << "\n"; json << "}"; std::string result = json.str(); char* cstr = new char[result.length() + 1]; std::strcpy(cstr, result.c_str()); return cstr; } /** * Converts DataBuffer lifecycle statistics to JSON format. * Note: DataBufferStats is a unified structure, not separated by buffer type. */ const char* getDataBufferLifecycleStats() { auto stats = DataBufferLifecycleTracker::getInstance().getStats(); std::ostringstream json; json << "{\n"; json << " \"total_allocations\": " << stats.totalAllocations << ",\n"; json << " \"total_deallocations\": " << stats.totalDeallocations << ",\n"; json << " \"current_live\": " << stats.currentLive << ",\n"; json << " \"peak_live\": " << stats.peakLive << ",\n"; json << " \"total_bytes_allocated\": " << stats.totalBytesAllocated << ",\n"; json << " \"total_bytes_deallocated\": " << stats.totalBytesDeallocated << "\n"; json << "}"; std::string result = json.str(); char* cstr = new char[result.length() + 1]; std::strcpy(cstr, result.c_str()); return cstr; } /** * Generates a flamegraph SVG for NDArray allocations. */ void generateNDArrayAllocationFlamegraph(const char* outputPath) { if (outputPath == nullptr) { return; } std::string path(outputPath); NDArrayLifecycleTracker::getInstance().generateFlamegraph(path); } /** * Generates a flamegraph SVG for NDArray deallocations. */ void generateNDArrayDeallocationFlamegraph(const char* outputPath) { if (outputPath == nullptr) { return; } std::string path(outputPath); NDArrayLifecycleTracker::getInstance().generateDeletionFlamegraph(path); } /** * Generates a flamegraph SVG for DataBuffer allocations. */ void generateDataBufferAllocationFlamegraph(const char* outputPath, int bufferType) { if (outputPath == nullptr) { return; } std::string path(outputPath); BufferType type = (bufferType == 0) ? BufferType::PRIMARY : BufferType::SPECIAL; DataBufferLifecycleTracker::getInstance().generateFlamegraph(path, static_cast(type)); } /** * Generates a flamegraph SVG for DataBuffer deallocations. */ void generateDataBufferDeallocationFlamegraph(const char* outputPath, int bufferType) { if (outputPath == nullptr) { return; } std::string path(outputPath); BufferType type = (bufferType == 0) ? BufferType::PRIMARY : BufferType::SPECIAL; DataBufferLifecycleTracker::getInstance().generateDeletionFlamegraph(path, static_cast(type)); } /** * Generates a comprehensive leak report combining all lifecycle trackers. * This report now includes sample stack traces for each leaked allocation. */ void generateLifecycleLeakReport(const char* outputPath) { if (outputPath == nullptr) { return; } std::string path(outputPath); // Create combined report with all tracker statistics std::ofstream combined(path); if (combined.is_open()) { combined << "============================================\n"; combined << " COMPREHENSIVE LIFECYCLE LEAK REPORT\n"; combined << "============================================\n\n"; // NDArray statistics auto ndarray_stats = NDArrayLifecycleTracker::getInstance().getStats(); combined << "=== NDArray Statistics ===\n"; combined << " Tracking Enabled: " << (NDArrayLifecycleTracker::getInstance().isEnabled() ? "YES" : "NO") << "\n"; combined << " Total Allocations: " << ndarray_stats.totalAllocations << "\n"; combined << " Total Deallocations: " << ndarray_stats.totalDeallocations << "\n"; combined << " Current Live: " << ndarray_stats.currentLive << "\n"; combined << " Peak Live: " << ndarray_stats.peakLive << "\n"; combined << " Total Bytes Allocated: " << ndarray_stats.totalBytesAllocated << "\n"; combined << " Total Bytes Deallocated: " << ndarray_stats.totalBytesDeallocated << "\n"; combined << "\n"; // DataBuffer statistics auto databuffer_stats = DataBufferLifecycleTracker::getInstance().getStats(); combined << "=== DataBuffer Statistics ===\n"; combined << " Tracking Enabled: " << (DataBufferLifecycleTracker::getInstance().isEnabled() ? "YES" : "NO") << "\n"; combined << " Total Allocations: " << databuffer_stats.totalAllocations << "\n"; combined << " Total Deallocations: " << databuffer_stats.totalDeallocations << "\n"; combined << " Current Live: " << databuffer_stats.currentLive << "\n"; combined << " Peak Live: " << databuffer_stats.peakLive << "\n"; combined << " Total Bytes Allocated: " << databuffer_stats.totalBytesAllocated << "\n"; combined << " Total Bytes Deallocated: " << databuffer_stats.totalBytesDeallocated << "\n"; combined << "\n"; // TADCache statistics auto tad_stats = TADCacheLifecycleTracker::getInstance().getStats(); combined << "=== TADCache Statistics ===\n"; combined << " Tracking Enabled: " << (TADCacheLifecycleTracker::getInstance().isEnabled() ? "YES" : "NO") << "\n"; combined << " Total Allocations: " << tad_stats.totalAllocations << "\n"; combined << " Total Deallocations: " << tad_stats.totalDeallocations << "\n"; combined << " Current Live: " << tad_stats.currentLive << "\n"; combined << " Peak Live: " << tad_stats.peakLive << "\n"; combined << " Total Bytes Allocated: " << tad_stats.totalBytesAllocated << "\n"; combined << " Total Bytes Deallocated: " << tad_stats.totalBytesDeallocated << "\n"; combined << "\n"; // ShapeCache statistics auto shape_stats = ShapeCacheLifecycleTracker::getInstance().getStats(); combined << "=== ShapeCache Statistics ===\n"; combined << " Tracking Enabled: " << (ShapeCacheLifecycleTracker::getInstance().isEnabled() ? "YES" : "NO") << "\n"; combined << " Total Allocations: " << shape_stats.totalAllocations << "\n"; combined << " Total Deallocations: " << shape_stats.totalDeallocations << "\n"; combined << " Current Live: " << shape_stats.currentLive << "\n"; combined << " Peak Live: " << shape_stats.peakLive << "\n"; combined << "\n"; // OpContext statistics auto opctx_stats = sd::graph::OpContextLifecycleTracker::getInstance().getStats(); combined << "=== OpContext Statistics ===\n"; combined << " Tracking Enabled: " << (sd::graph::OpContextLifecycleTracker::getInstance().isEnabled() ? "YES" : "NO") << "\n"; combined << " Total Allocations: " << opctx_stats.totalAllocations << "\n"; combined << " Total Deallocations: " << opctx_stats.totalDeallocations << "\n"; combined << " Current Live: " << opctx_stats.currentLive << "\n"; combined << " Peak Live: " << opctx_stats.peakLive << "\n"; combined << "\n"; // DeallocatorService statistics combined << "=== DeallocatorService Statistics ===\n"; combined << " Tracking Enabled: " << (DeallocatorServiceLifecycleTracker::getInstance().isEnabled() ? "YES" : "NO") << "\n"; combined << " Total Allocations: " << DeallocatorServiceLifecycleTracker::getInstance().getTotalAllocations() << "\n"; combined << " Total Deallocations: " << DeallocatorServiceLifecycleTracker::getInstance().getTotalDeallocations() << "\n"; combined << " Current Live Count: " << DeallocatorServiceLifecycleTracker::getInstance().getCurrentLiveCount() << "\n"; combined << " Current Bytes In Use: " << DeallocatorServiceLifecycleTracker::getInstance().getCurrentBytesInUse() << "\n"; combined << " Peak Live Count: " << DeallocatorServiceLifecycleTracker::getInstance().getPeakLiveCount() << "\n"; combined << " Peak Bytes: " << DeallocatorServiceLifecycleTracker::getInstance().getPeakBytes() << "\n"; combined << "\n"; // Summary combined << "============================================\n"; combined << " SUMMARY\n"; combined << "============================================\n"; size_t total_leaks = ndarray_stats.currentLive + databuffer_stats.currentLive + opctx_stats.currentLive; if (total_leaks > 0) { combined << " TOTAL POTENTIAL LEAKS: " << total_leaks << "\n"; combined << " - NDArrays: " << ndarray_stats.currentLive << "\n"; combined << " - DataBuffers: " << databuffer_stats.currentLive << "\n"; combined << " - OpContexts: " << opctx_stats.currentLive << "\n"; } else { combined << " No leaks detected.\n"; } combined << "\n"; // Now output sample stack traces for each type of leak combined << "============================================\n"; combined << " SAMPLE LEAK STACK TRACES\n"; combined << "============================================\n\n"; // NDArray leaks with stack traces NDArrayLifecycleTracker::getInstance().printCurrentLeaks(combined, 5); combined << "\n"; // DataBuffer leaks with stack traces DataBufferLifecycleTracker::getInstance().printCurrentLeaks(combined, 5); combined << "\n"; // OpContext leaks with stack traces sd::graph::OpContextLifecycleTracker::getInstance().printCurrentLeaks(combined, 5); combined << "\n"; // Per-operation analysis - groups allocations by operation with stack traces combined << "============================================\n"; combined << " PER-OPERATION ALLOCATION BREAKDOWN\n"; combined << "============================================\n"; combined << "This section groups leaked allocations by the operation\n"; combined << "that created them, with sample stack traces for each.\n\n"; // NDArray per-op analysis with stack traces NDArrayLifecycleTracker::getInstance().printPerOpAnalysis(combined, 3); combined << "\n"; // DataBuffer per-op analysis with stack traces DataBufferLifecycleTracker::getInstance().printPerOpAnalysis(combined, 3); combined << "\n"; // OpContext per-op analysis with stack traces sd::graph::OpContextLifecycleTracker::getInstance().printPerOpAnalysis(combined, 3); combined << "\n"; // Actionable analysis section combined << "============================================\n"; combined << " ACTIONABLE RECOMMENDATIONS\n"; combined << "============================================\n"; combined << "This section provides specific actions to address memory issues.\n\n"; // Top leaking operations combined << "--- TOP OPERATIONS BY LIVE ALLOCATIONS ---\n\n"; auto ndTopOps = NDArrayLifecycleTracker::getInstance().getTopOpsByLiveCount(5); if (!ndTopOps.empty()) { combined << " NDArray Top 5:\n"; for (const auto& op : ndTopOps) { double javaPct = op.liveCount > 0 ? (100.0 * op.javaCount / op.liveCount) : 0; combined << " " << op.opName << ": " << op.liveCount << " live (" << (op.liveBytes / (1024*1024)) << " MB) - " << javaPct << "% Java\n"; } combined << "\n"; } auto dbTopOps = DataBufferLifecycleTracker::getInstance().getTopOpsByLiveCount(5); if (!dbTopOps.empty()) { combined << " DataBuffer Top 5:\n"; for (const auto& op : dbTopOps) { double javaPct = op.liveCount > 0 ? (100.0 * op.javaCount / op.liveCount) : 0; combined << " " << op.opName << ": " << op.liveCount << " live (" << (op.liveBytes / (1024*1024)) << " MB) - " << javaPct << "% Java\n"; } combined << "\n"; } // Detailed actionable analysis per tracker NDArrayLifecycleTracker::getInstance().printActionableAnalysis(combined); combined << "\n"; DataBufferLifecycleTracker::getInstance().printActionableAnalysis(combined); combined << "\n"; // DeallocatorService status combined << "--- DeallocatorService Status ---\n"; auto deallocAllocs = DeallocatorServiceLifecycleTracker::getInstance().getTotalAllocations(); auto deallocDeallocs = DeallocatorServiceLifecycleTracker::getInstance().getTotalDeallocations(); auto backlog = deallocAllocs - deallocDeallocs; double backlogPct = deallocAllocs > 0 ? (100.0 * backlog / deallocAllocs) : 0; combined << " Allocations: " << deallocAllocs << "\n"; combined << " Deallocations: " << deallocDeallocs << "\n"; combined << " Backlog: " << backlog << " (" << backlogPct << "%)\n"; if (backlogPct > 10) { combined << " [WARNING] Deallocator falling behind - consider System.gc()\n"; } else if (backlogPct > 5) { combined << " [INFO] Mild deallocation lag - normal during high throughput\n"; } else { combined << " [OK] Deallocator keeping up\n"; } combined << "\n"; // Cache status and actions combined << "--- Cache Actions ---\n"; auto tadStats = TADCacheLifecycleTracker::getInstance().getStats(); auto shapeStats = ShapeCacheLifecycleTracker::getInstance().getStats(); combined << " TAD Cache: " << tadStats.currentLive << " entries\n"; combined << " Shape Cache: " << shapeStats.currentLive << " entries\n"; if (tadStats.currentLive > 5000) { combined << " [ACTION] TAD cache large - call clearTADCache() to free memory\n"; } combined << "\n"; combined.close(); } } /** * Generates a comprehensive leak source analysis combining ALL lifecycle trackers. * This is a stub implementation that currently just calls generateLifecycleLeakReport. */ void generateComprehensiveLeakAnalysis(const char* outputDir) { if (outputDir == nullptr) { return; } std::string dir(outputDir); std::string reportPath = dir + "/comprehensive_leak_report.txt"; generateLifecycleLeakReport(reportPath.c_str()); } // ═══════════════════════════════════════════════════════════════════════════ // LIFECYCLE TRACKING FUNCTIONS - Always available (not dependent on SD_GCC_FUNCTRACE) // Stack trace capture is only enabled when SD_GCC_FUNCTRACE is defined, // but basic tracking (counts, pointers, timestamps) always works. // ═══════════════════════════════════════════════════════════════════════════ /** * Enables NDArray lifecycle tracking. * When enabled, all NDArray allocations and deallocations are tracked * with stack traces for leak detection. */ SD_LIB_EXPORT void enableNDArrayTracking() { NDArrayLifecycleTracker::getInstance().setEnabled(true); } /** * Disables NDArray lifecycle tracking. */ SD_LIB_EXPORT void disableNDArrayTracking() { NDArrayLifecycleTracker::getInstance().setEnabled(false); } /** * Enables DataBuffer lifecycle tracking. * When enabled, all DataBuffer allocations and deallocations are tracked * with stack traces for leak detection. */ SD_LIB_EXPORT void enableDataBufferTracking() { DataBufferLifecycleTracker::getInstance().setEnabled(true); } /** * Disables DataBuffer lifecycle tracking. */ SD_LIB_EXPORT void disableDataBufferTracking() { DataBufferLifecycleTracker::getInstance().setEnabled(false); } /** * Enables TADCache lifecycle tracking. * When enabled, all TAD (Tensor Along Dimension) cache allocations and deallocations * are tracked with stack traces for leak detection. */ SD_LIB_EXPORT void enableTADCacheTracking() { TADCacheLifecycleTracker::getInstance().setEnabled(true); } /** * Disables TADCache lifecycle tracking. */ SD_LIB_EXPORT void disableTADCacheTracking() { TADCacheLifecycleTracker::getInstance().setEnabled(false); } /** * Enables ShapeCache lifecycle tracking. * When enabled, all shape cache allocations and deallocations are tracked * with stack traces for leak detection. */ SD_LIB_EXPORT void enableShapeCacheTracking() { ShapeCacheLifecycleTracker::getInstance().setEnabled(true); } /** * Disables ShapeCache lifecycle tracking. */ SD_LIB_EXPORT void disableShapeCacheTracking() { ShapeCacheLifecycleTracker::getInstance().setEnabled(false); } /** * Enables OpContext lifecycle tracking. * When enabled, all operation context allocations and deallocations are tracked * with stack traces for leak detection. */ SD_LIB_EXPORT void enableOpContextTracking() { sd::graph::OpContextLifecycleTracker::getInstance().setEnabled(true); } /** * Disables OpContext lifecycle tracking. */ SD_LIB_EXPORT void disableOpContextTracking() { sd::graph::OpContextLifecycleTracker::getInstance().setEnabled(false); } /** * Sets the current operation context for allocation tracking. * All allocations (NDArray, DataBuffer, OpContext) made while an op context is set * will be tagged with the operation name for per-op analysis. * @param opName The name of the operation (e.g., "matmul", "add", "conv2d") */ SD_LIB_EXPORT void setLifecycleOpContext(const char* opName) { if (opName == nullptr) { NDArrayLifecycleTracker::clearCurrentOpContext(); DataBufferLifecycleTracker::clearCurrentOpContext(); sd::graph::OpContextLifecycleTracker::clearCurrentOpContext(); } else { std::string op(opName); NDArrayLifecycleTracker::setCurrentOpContext(op); DataBufferLifecycleTracker::setCurrentOpContext(op); sd::graph::OpContextLifecycleTracker::setCurrentOpContext(op); } } /** * Clears the current operation context for allocation tracking. * Subsequent allocations will be tagged as "(unknown)". */ SD_LIB_EXPORT void clearLifecycleOpContext() { NDArrayLifecycleTracker::clearCurrentOpContext(); DataBufferLifecycleTracker::clearCurrentOpContext(); sd::graph::OpContextLifecycleTracker::clearCurrentOpContext(); } /** * Gets the current operation context for allocation tracking. * @return The current operation name, or empty string if none is set */ SD_LIB_EXPORT const char* getLifecycleOpContext() { // Thread-local static to avoid dangling pointer thread_local static std::string g_opContext; g_opContext = NDArrayLifecycleTracker::getCurrentOpContext(); return g_opContext.c_str(); } // OpExecutionLogger and AllocationLogger functions moved to end of file // (single definitions with #if SD_GCC_FUNCTRACE guards inside) /** * Generates a temporal leak report for NDArray allocations over time. */ SD_LIB_EXPORT void generateNDArrayTemporalLeakReport(const char* outputPath, int windowCount, double windowDurationSec) { if (outputPath == nullptr) { return; } std::string path(outputPath); NDArrayLifecycleTracker::getInstance().generateTemporalLeakReport(path, windowCount, windowDurationSec); } /** * Generates a temporal leak report for TAD cache allocations over time. */ SD_LIB_EXPORT void generateTADCacheTemporalLeakReport(const char* outputPath, int windowCount, double windowDurationSec) { if (outputPath == nullptr) { return; } std::string path(outputPath); TADCacheLifecycleTracker::getInstance().generateTemporalLeakReport(path, windowCount, windowDurationSec); } /** * Captures a snapshot of current NDArray allocations. * Returns a snapshot ID that can be used with generateNDArraySnapshotDiff. */ SD_LIB_EXPORT sd::LongType captureNDArrayLeakSnapshot() { return NDArrayLifecycleTracker::getInstance().captureLeakSnapshot(); } /** * Captures a snapshot of current TAD cache allocations. * Returns a snapshot ID that can be used with generateTADCacheSnapshotDiff. */ SD_LIB_EXPORT sd::LongType captureTADCacheLeakSnapshot() { return TADCacheLifecycleTracker::getInstance().captureLeakSnapshot(); } /** * Generates a diff report between two NDArray allocation snapshots. */ SD_LIB_EXPORT void generateNDArraySnapshotDiff(sd::LongType snapshot1, sd::LongType snapshot2, const char* outputPath) { if (outputPath == nullptr) { return; } std::string path(outputPath); NDArrayLifecycleTracker::getInstance().generateSnapshotDiff(snapshot1, snapshot2, path); } /** * Generates a diff report between two TAD cache allocation snapshots. */ SD_LIB_EXPORT void generateTADCacheSnapshotDiff(sd::LongType snapshot1, sd::LongType snapshot2, const char* outputPath) { if (outputPath == nullptr) { return; } std::string path(outputPath); TADCacheLifecycleTracker::getInstance().generateSnapshotDiff(snapshot1, snapshot2, path); } /** * Clears all stored NDArray allocation snapshots to free memory. */ SD_LIB_EXPORT void clearNDArraySnapshots() { NDArrayLifecycleTracker::getInstance().clearSnapshots(); } /** * Clears all stored TAD cache allocation snapshots to free memory. */ SD_LIB_EXPORT void clearTADCacheSnapshots() { TADCacheLifecycleTracker::getInstance().clearSnapshots(); } /** * Set the current allocation context (operation name) for lifecycle tracking. * This allows Java code to tag allocations with the operation that triggered them, * providing much better granularity in leak reports than stack trace analysis alone. * * This function updates BOTH the OpExecutionLogger AND all lifecycle trackers * (NDArray, DataBuffer, OpContext) so that any allocations made during this * operation are properly tagged. */ SD_LIB_EXPORT void setAllocationContext(const char* opName) { if (opName != nullptr) { std::string op(opName); // Set the op name in OpExecutionLogger for logging sd::ops::OpExecutionLogger::setCurrentOpName(op); // Also set the op context in all lifecycle trackers so allocations are tagged NDArrayLifecycleTracker::setCurrentOpContext(op); DataBufferLifecycleTracker::setCurrentOpContext(op); sd::graph::OpContextLifecycleTracker::setCurrentOpContext(op); } } /** * Clear the current allocation context for this thread. * Clears the op context from both OpExecutionLogger and all lifecycle trackers. */ SD_LIB_EXPORT void clearAllocationContext() { sd::ops::OpExecutionLogger::clearCurrentOpName(); // Also clear the op context in all lifecycle trackers NDArrayLifecycleTracker::clearCurrentOpContext(); DataBufferLifecycleTracker::clearCurrentOpContext(); sd::graph::OpContextLifecycleTracker::clearCurrentOpContext(); } // updateAllocationJavaStackTrace moved to end of file // (single definition with #if SD_GCC_FUNCTRACE guard inside) // =============================== // DeallocatorService Lifecycle Tracking // These functions receive data from Java DeallocatorService // =============================== /** * Record a snapshot of DeallocatorService statistics from Java. */ SD_LIB_EXPORT void recordDeallocatorServiceSnapshot( sd::LongType totalAllocations, sd::LongType totalDeallocations, sd::LongType totalBytesAllocated, sd::LongType totalBytesDeallocated, sd::LongType peakLiveCount, sd::LongType peakBytes) { DeallocatorServiceLifecycleTracker::getInstance().recordSnapshot( static_cast(totalAllocations), static_cast(totalDeallocations), static_cast(totalBytesAllocated), static_cast(totalBytesDeallocated), static_cast(peakLiveCount), static_cast(peakBytes) ); } /** * Enable DeallocatorService lifecycle tracking. */ SD_LIB_EXPORT void enableDeallocatorServiceTracking() { DeallocatorServiceLifecycleTracker::getInstance().enable(); } /** * Disable DeallocatorService lifecycle tracking. */ SD_LIB_EXPORT void disableDeallocatorServiceTracking() { DeallocatorServiceLifecycleTracker::getInstance().disable(); } /** * Check if DeallocatorService tracking is enabled. */ SD_LIB_EXPORT bool isDeallocatorServiceTrackingEnabled() { return DeallocatorServiceLifecycleTracker::getInstance().isEnabled(); } /** * Get current live count from DeallocatorService tracker. */ SD_LIB_EXPORT sd::LongType getDeallocatorServiceLiveCount() { return static_cast( DeallocatorServiceLifecycleTracker::getInstance().getCurrentLiveCount()); } /** * Get current bytes in use from DeallocatorService tracker. */ SD_LIB_EXPORT sd::LongType getDeallocatorServiceBytesInUse() { return static_cast( DeallocatorServiceLifecycleTracker::getInstance().getCurrentBytesInUse()); } // AUTO CACHE CLEANUP - MOVED OUTSIDE SD_GCC_FUNCTRACE GUARD // Critical fix: Cache cleanup must work even without functrace! // The caches accumulate regardless of tracking, so cleanup must always be available. #include #include #include #include #include #include #include namespace { // Operation counter for automatic cache cleanup std::atomic g_operation_counter_nocache{0}; // Get cleanup interval from environment or use default uint64_t getCleanupIntervalNoCache() { static uint64_t interval = 0; if (interval == 0) { const char* env_val = std::getenv("SD_CACHE_CLEANUP_INTERVAL"); if (env_val != nullptr) { interval = std::atoll(env_val); } if (interval == 0) { // Default: cleanup every 100 operations // Too aggressive cleanup (interval=1) causes use-after-free: // - Operation gets TadPack from cache // - Operation completes and calls checkAndCleanupCaches() // - Cache is cleared immediately (interval=1) // - TadPack is deleted while operation still using it // - SIGSEGV when accessing deleted PointerWrapper // Interval of 100 provides good balance between memory and safety interval = 100; } } return interval; } // Check if auto-cleanup is enabled bool isAutoCleanupEnabledNoCache() { static int enabled = -1; if (enabled == -1) { const char* env_val = std::getenv("SD_AUTO_CACHE_CLEANUP"); if (env_val != nullptr) { enabled = (strcmp(env_val, "0") != 0 && strcasecmp(env_val, "false") != 0) ? 1 : 0; } else { // Enabled by default enabled = 1; } } return enabled == 1; } } /** * Automatic cache cleanup called after operations. * Clears TAD cache at configurable intervals to prevent accumulation. * Available in all builds (not just with SD_GCC_FUNCTRACE). */ SD_LIB_EXPORT void checkAndCleanupCaches() { uint64_t count = g_operation_counter_nocache.fetch_add(1, std::memory_order_relaxed) + 1; uint64_t interval = getCleanupIntervalNoCache(); if ((count % interval) == 0) { clearTADCache(); } } /** * Clears all cached TAD packs to prevent memory leaks during testing. * This is particularly useful when running memory leak tests that * track allocations, as it allows clearing accumulated cache between tests. */ SD_LIB_EXPORT void clearTADCache() { sd::ConstantTadHelper::getInstance().clearCache(); } /** * Marks that shutdown is in progress. * CRITICAL: Call this early in JVM shutdown (e.g., from a shutdown hook) * to prevent SIGSEGV crashes during cache cleanup. * * During JVM/static destruction, memory allocators may have been destroyed, * leaving corrupted pointers in cached data structures. Setting this flag * causes clearTADCache() and similar functions to skip tree traversal, * letting the OS safely reclaim memory at process exit instead. * * @param inProgress true to mark shutdown in progress, false otherwise */ SD_LIB_EXPORT void setTADCacheShutdownInProgress(bool inProgress) { sd::ConstantTadHelper::getInstance().setShutdownInProgress(inProgress); } /** * Check if TAD cache shutdown is in progress. * @return true if shutdown is marked as in progress */ SD_LIB_EXPORT bool isTADCacheShutdownInProgress() { return sd::ConstantTadHelper::getInstance().isShutdownInProgress(); } // NOTE: DO NOT register atexit handler to clear TAD cache at shutdown! // During JVM/static destruction, the order of destruction is undefined. // Memory allocators and other infrastructure may have already been destroyed, // causing corrupted pointers in the trie. Traversing the tree in this state // causes SIGSEGV crashes (e.g., in deleteTadPacksRecursive). // // The OS will reclaim all memory when the process exits anyway, so explicit // cleanup during shutdown is unnecessary and dangerous. // // For explicit cleanup during runtime (e.g., testing), call clearTADCache() directly. // // Previous code that caused SIGSEGV crashes during shutdown (REMOVED): // namespace { // void clearTADCacheAtShutdown() { clearTADCache(); } // struct ShutdownCleanupRegistrar { // ShutdownCleanupRegistrar() { std::atexit(clearTADCacheAtShutdown); } // }; // static ShutdownCleanupRegistrar g_shutdown_cleanup_registrar; // } /** * Clears all cached shape buffers to prevent memory leaks. * This is called during application shutdown to free accumulated cache memory. */ SD_LIB_EXPORT void clearShapeCache() { sd::ConstantShapeHelper::getInstance().clearCache(); } /** * Get the total number of cached shape buffer entries. */ SD_LIB_EXPORT sd::LongType getShapeCachedEntries() { return sd::ConstantShapeHelper::getInstance().getCachedEntries(); } /** * Get the total memory used by cached shape buffers in bytes. */ SD_LIB_EXPORT sd::LongType getShapeCachedBytes() { return sd::ConstantShapeHelper::getInstance().getCachedBytes(); } /** * Get the peak number of shape entries that were cached simultaneously. */ SD_LIB_EXPORT sd::LongType getShapePeakCachedEntries() { return sd::ConstantShapeHelper::getInstance().getPeakCachedEntries(); } /** * Get the peak memory usage by cached shape buffers in bytes. */ SD_LIB_EXPORT sd::LongType getShapePeakCachedBytes() { return sd::ConstantShapeHelper::getInstance().getPeakCachedBytes(); } /** * Get count of LEAKED TAD packs for leak detection. * * DESIGN DECISION (Session #1065): * The TAD cache is a PERMANENT CACHE by design - it holds TAD packs indefinitely * for performance optimization. Entries in the cache are NOT leaks. * * Previous sessions tried: * - Session #1062: Pointer comparison - failed due to pointer mismatch * - Session #1063: Added diagnostics * - Session #1064: Count comparison (live - cached) - gave false positives * * The count comparison approach fails because: * 1. Cache and lifecycle tracker may count the same TAD packs differently due to timing * 2. Auto-cleanup (checkAndCleanupCaches) can clear the cache between creation and check * 3. The comparison assumes 1:1 correspondence which may not hold due to threading * * NEW APPROACH: Return 0 to indicate no TAD cache leaks. * * RATIONALE: * - TAD packs in the cache are working as designed (intentional caching) * - TAD packs are created via ConstantTadHelper and stored in DirectTadTrie * - When the cache is cleared, TadPack destructors are called which removes them from tracker * - There is no mechanism for TAD packs to "escape" the cache in normal operation * - If there were actual leaks, they would be from code bugs, not from cache behavior * * To get the actual cache size, use ConstantTadHelper::getCachedEntries() directly. */ SD_LIB_EXPORT sd::LongType getTADCachedEntries() { // TAD cache entries are NOT leaks - they are intentionally cached for performance. // Return 0 to indicate no TAD cache leaks. // // The actual cache size can be obtained via: // sd::ConstantTadHelper::getInstance().getCachedEntries() return 0; } /** * Get total memory used by LEAKED TAD packs for leak detection. * * DESIGN DECISION (Session #1065): * The TAD cache is a PERMANENT CACHE by design - it holds TAD packs indefinitely * for performance optimization. Memory used by cached entries is NOT leaked memory. * * Previous sessions tried: * - Session #1062-#1063: Pointer comparison - failed * - Session #1064: Byte count comparison (live - cached) - gave false positives * * NEW APPROACH: Return 0 to indicate no TAD cache memory leaks. * * RATIONALE: Same as getTADCachedEntries() above. * TAD packs in the cache are working as designed. The cache memory is intentional. * * To get the actual cache memory usage, use ConstantTadHelper::getCachedBytes() directly. */ SD_LIB_EXPORT sd::LongType getTADCachedBytes() { // TAD cache memory is NOT leaked - it is intentionally cached for performance. // Return 0 to indicate no TAD cache memory leaks. // // The actual cache memory usage can be obtained via: // sd::ConstantTadHelper::getInstance().getCachedBytes() return 0; } /** * Get the peak number of TAD pack entries that were cached simultaneously. */ SD_LIB_EXPORT sd::LongType getTADPeakCachedEntries() { return sd::ConstantTadHelper::getInstance().getPeakCachedEntries(); } /** * Get the peak memory usage by cached TAD packs in bytes. */ SD_LIB_EXPORT sd::LongType getTADPeakCachedBytes() { return sd::ConstantTadHelper::getInstance().getPeakCachedBytes(); } /** * Get a string representation of the shape cache for debugging. */ SD_LIB_EXPORT const char* getShapeCacheString(int maxDepth, int maxEntries) { std::string result = sd::ConstantShapeHelper::getInstance().toString(maxDepth, maxEntries); // Allocate C-style string that Java can read char* cstr = new char[result.length() + 1]; std::strcpy(cstr, result.c_str()); return cstr; } /** * Get a string representation of the TAD cache for debugging. */ SD_LIB_EXPORT const char* getTADCacheString(int maxDepth, int maxEntries) { std::string result = sd::ConstantTadHelper::getInstance().toString(maxDepth, maxEntries); // Allocate C-style string that Java can read char* cstr = new char[result.length() + 1]; std::strcpy(cstr, result.c_str()); return cstr; } /** * Free a string returned by native code. */ SD_LIB_EXPORT void freeString(const char* ptr) { if (ptr != nullptr) { delete[] ptr; } } // ═══════════════════════════════════════════════════════════════════════════ // Functions that require SD_GCC_FUNCTRACE for full functionality // These are no-ops when functrace is not available // ═══════════════════════════════════════════════════════════════════════════ SD_LIB_EXPORT void initializeLifecycleCrashHandlers() { #if defined(SD_GCC_FUNCTRACE) && !defined(_WIN32) LifecycleCrashHandler::instance().ensureInitialized(); #endif } SD_LIB_EXPORT void enableOpExecutionLogging() { #if defined(SD_GCC_FUNCTRACE) sd::ops::OpExecutionLogger::getInstance().enable(); #endif } SD_LIB_EXPORT void disableOpExecutionLogging() { #if defined(SD_GCC_FUNCTRACE) sd::ops::OpExecutionLogger::getInstance().disable(); #endif } SD_LIB_EXPORT bool isOpExecutionLoggingEnabled() { #if defined(SD_GCC_FUNCTRACE) return sd::ops::OpExecutionLogger::getInstance().isEnabled(); #else return false; #endif } SD_LIB_EXPORT const char* getOpExecutionLogPath() { #if defined(SD_GCC_FUNCTRACE) static thread_local std::string g_opLogPath; g_opLogPath = sd::ops::OpExecutionLogger::getInstance().getLogPath(); return g_opLogPath.c_str(); #else static const char* empty = ""; return empty; #endif } SD_LIB_EXPORT const char* getOpExecutionLogContents(size_t maxBytes, bool fromEnd) { #if defined(SD_GCC_FUNCTRACE) static thread_local std::string g_opLogContents; g_opLogContents = sd::ops::OpExecutionLogger::getInstance().getLogContents(maxBytes, fromEnd); return g_opLogContents.c_str(); #else static const char* empty = ""; return empty; #endif } SD_LIB_EXPORT void dumpOpExecutionLog() { #if defined(SD_GCC_FUNCTRACE) sd::ops::OpExecutionLogger::getInstance().flush(); #endif } SD_LIB_EXPORT void dumpOpExecutionState(const char* message) { #if defined(SD_GCC_FUNCTRACE) std::string msg = message ? message : ""; sd::ops::OpExecutionLogger::getInstance().dumpCurrentState(msg); #endif } SD_LIB_EXPORT const char* getAllocationLogPath() { #if defined(SD_GCC_FUNCTRACE) static thread_local std::string g_allocLogPath; g_allocLogPath = sd::array::AllocationLogger::getInstance().getLogPath(); return g_allocLogPath.c_str(); #else static const char* empty = ""; return empty; #endif } SD_LIB_EXPORT void updateAllocationJavaStackTrace(OpaqueNDArray array, const char* javaStackTrace) { #if defined(SD_GCC_FUNCTRACE) if (array != nullptr && javaStackTrace != nullptr) { NDArrayLifecycleTracker::getInstance().updateJavaStackTrace(array, std::string(javaStackTrace)); } #endif }