diff --git a/extension/android/CMakeLists.txt b/extension/android/CMakeLists.txt index 38b28a1407a..5daeaf49c18 100644 --- a/extension/android/CMakeLists.txt +++ b/extension/android/CMakeLists.txt @@ -91,11 +91,35 @@ list( fbjni ) +# ============================================================================= +# Android ETDump Profiling with Runtime Control +# ============================================================================= if(EXECUTORCH_ANDROID_PROFILING) - list(APPEND link_libraries etdump flatccrt) + # Add ETDump JNI implementation + target_sources(executorch_jni PRIVATE jni/jni_etdump.cpp) + + # Add SDK include directories for ETDump headers + target_include_directories( + executorch_jni + PRIVATE + ${EXECUTORCH_ROOT}/sdk + ${EXECUTORCH_ROOT}/third-party/flatcc/include + ) + + # Add compile definitions target_compile_definitions( - executorch_jni PUBLIC EXECUTORCH_ANDROID_PROFILING=1 + executorch_jni + PUBLIC + ET_EVENT_TRACER_ENABLED + EXECUTORCH_ANDROID_PROFILING=1 ) + + # Link ETDump libraries + list(APPEND link_libraries etdump flatccrt) + + message(STATUS "Android ETDump profiling with runtime control: ENABLED") +else() + message(STATUS "Android ETDump profiling: DISABLED") endif() if(TARGET optimized_native_cpu_ops_lib) diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ETDump.java b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ETDump.java new file mode 100644 index 00000000000..84ee890c960 --- /dev/null +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ETDump.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +package org.pytorch.executorch; + +import android.util.Log; + +/** + * ETDump provides runtime control for ExecuTorch profiling. + * + *

Enable profiling before loading models to capture execution traces. Use + * Module.writeETDumpToPath() to write profiling data to custom locations. + * + *

Example usage: + * + *

{@code
+ * // Enable profiling
+ * ETDump.enableProfiling();
+ *
+ * // Load and run model
+ * Module module = Module.load("model.pte");
+ * module.forward(inputs);
+ *
+ * // Write profiling data to custom path (no root access needed!)
+ * module.writeETDumpToPath(getCacheDir() + "/profile.etdump");
+ *
+ * // Disable profiling
+ * ETDump.disableProfiling();
+ * }
+ */ +public class ETDump { + private static final String TAG = "ExecuTorch-ETDump"; + + static { + try { + System.loadLibrary("executorch"); + nativeInit(); + } catch (UnsatisfiedLinkError e) { + Log.e(TAG, "Failed to load executorch library", e); + throw e; + } + } + + /** Initialize the ETDump subsystem. Called automatically when the class is loaded. */ + private static native void nativeInit(); + + /** + * Enable profiling for subsequently loaded models. + * + *

Modules loaded after calling this method will capture profiling data. This has zero runtime + * overhead until a model is actually loaded. + * + * @return true if profiling was successfully enabled + */ + public static boolean enableProfiling() { + boolean result = nativeEnableProfiling(); + if (result) { + Log.i(TAG, "Profiling enabled"); + } else { + Log.e(TAG, "Failed to enable profiling"); + } + return result; + } + + /** + * Disable profiling. + * + *

Modules loaded after calling this method will not capture profiling data. + */ + public static void disableProfiling() { + nativeDisableProfiling(); + Log.i(TAG, "Profiling disabled"); + } + + /** + * Check if profiling is currently enabled. + * + * @return true if profiling is enabled + */ + public static boolean isProfilingEnabled() { + return nativeIsProfilingEnabled(); + } + + // Native methods + private static native boolean nativeEnableProfiling(); + + private static native void nativeDisableProfiling(); + + private static native boolean nativeIsProfilingEnabled(); +} diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.java b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.java index 6da76bf4b74..66037be2d0b 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.java +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.java @@ -257,4 +257,71 @@ public void destroy() { + " released."); } } + + /** + * Write ETDump profiling data to a specific path. + * + *

This allows writing profiling data to custom locations such as app cache directory or + * external storage. No root access required. + * + *

Example: + * + *

{@code
+   * Module module = Module.load("model.pte");
+   * module.forward(inputs);
+   * module.writeETDumpToPath(getCacheDir() + "/profile.etdump");
+   * }
+ * + * @param outputPath The file path where ETDump data will be written. Must be a writable location. + * @return true if the data was successfully written, false otherwise + */ + @DoNotStrip + public boolean writeETDumpToPath(String outputPath) { + if (outputPath == null || outputPath.isEmpty()) { + android.util.Log.e("ExecuTorch-Module", "Output path cannot be null or empty"); + return false; + } + + // Validate that parent directory exists + java.io.File file = new java.io.File(outputPath); + java.io.File parentDir = file.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + if (!parentDir.mkdirs()) { + android.util.Log.e("ExecuTorch-Module", "Failed to create parent directory: " + parentDir); + return false; + } + } + + return nativeWriteETDumpToPath(outputPath); + } + + /** + * Get ETDump profiling data as a byte array. + * + *

This is useful for custom handling of profiling data, such as uploading to a server or + * processing in-memory without writing to disk. + * + *

Example: + * + *

{@code
+   * Module module = Module.load("model.pte");
+   * module.forward(inputs);
+   * byte[] profileData = module.getETDumpData();
+   * if (profileData != null) {
+   *   uploadToServer(profileData);
+   * }
+   * }
+ * + * @return byte array containing the ETDump data, or null if no data is available + */ + @DoNotStrip + public byte[] getETDumpData() { + return nativeGetETDumpData(); + } + + @DoNotStrip + private native boolean nativeWriteETDumpToPath(String outputPath); + + @DoNotStrip + private native byte[] nativeGetETDumpData(); } diff --git a/extension/android/jni/jni_etdump.cpp b/extension/android/jni/jni_etdump.cpp new file mode 100644 index 00000000000..ed13423e146 --- /dev/null +++ b/extension/android/jni/jni_etdump.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include + +#ifdef EXECUTORCH_ANDROID_PROFILING + +#include "jni_etdump.h" + +namespace executorch { +namespace extension { +namespace jni { + +// ============================================================================ +// Global ETDump Manager +// ============================================================================ + +// Global ETDump manager (one per process) +static std::unique_ptr g_etdump_manager = nullptr; + +extern "C" ETDumpManager* getGlobalETDumpManager() { + return g_etdump_manager.get(); +} + +// ============================================================================ +// JNI Methods for ETDump Java class +// ============================================================================ + +// Initialize ETDump manager +extern "C" JNIEXPORT void JNICALL +Java_org_pytorch_executorch_ETDump_nativeInit(JNIEnv* env, jclass clazz) { + if (!g_etdump_manager) { + g_etdump_manager = std::make_unique(); + } +} + +// Enable profiling +extern "C" JNIEXPORT jboolean JNICALL +Java_org_pytorch_executorch_ETDump_nativeEnableProfiling( + JNIEnv* env, + jclass clazz) { + if (!g_etdump_manager) { + g_etdump_manager = std::make_unique(); + } + g_etdump_manager->enableProfiling(); + return JNI_TRUE; +} + +// Disable profiling +extern "C" JNIEXPORT void JNICALL +Java_org_pytorch_executorch_ETDump_nativeDisableProfiling( + JNIEnv* env, + jclass clazz) { + if (g_etdump_manager) { + g_etdump_manager->disableProfiling(); + } +} + +// Check if profiling is enabled +extern "C" JNIEXPORT jboolean JNICALL +Java_org_pytorch_executorch_ETDump_nativeIsProfilingEnabled( + JNIEnv* env, + jclass clazz) { + if (!g_etdump_manager) { + return JNI_FALSE; + } + return g_etdump_manager->isProfilingEnabled() ? JNI_TRUE : JNI_FALSE; +} + +} // namespace jni +} // namespace extension +} // namespace executorch + +#endif // EXECUTORCH_ANDROID_PROFILING diff --git a/extension/android/jni/jni_etdump.h b/extension/android/jni/jni_etdump.h new file mode 100644 index 00000000000..5b495655770 --- /dev/null +++ b/extension/android/jni/jni_etdump.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#ifdef EXECUTORCH_ANDROID_PROFILING +#include + +namespace executorch { +namespace extension { +namespace jni { + +/** + * ETDumpManager manages the global profiling enabled state. + * This is a singleton that controls whether newly loaded modules + * should capture profiling data. + */ +class ETDumpManager { + public: + ETDumpManager() : profiling_enabled_(false) {} + + /** + * Enable profiling for subsequently loaded modules. + */ + void enableProfiling() { + profiling_enabled_ = true; + } + + /** + * Disable profiling. + */ + void disableProfiling() { + profiling_enabled_ = false; + } + + /** + * Check if profiling is currently enabled. + * @return true if profiling is enabled + */ + bool isProfilingEnabled() const { + return profiling_enabled_; + } + + private: + bool profiling_enabled_; +}; + +/** + * Get the global ETDump manager instance. + * @return Pointer to the global ETDumpManager, or nullptr if not initialized + */ +extern "C" ETDumpManager* getGlobalETDumpManager(); + +} // namespace jni +} // namespace extension +} // namespace executorch + +#endif // EXECUTORCH_ANDROID_PROFILING diff --git a/extension/android/jni/jni_layer.cpp b/extension/android/jni/jni_layer.cpp index 1f8457e00c5..d4bd7e71db3 100644 --- a/extension/android/jni/jni_layer.cpp +++ b/extension/android/jni/jni_layer.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #include #include @@ -34,9 +36,8 @@ #endif #ifdef EXECUTORCH_ANDROID_PROFILING -#include -#include -#include +#include +#include "jni_etdump.h" #endif #include @@ -44,6 +45,7 @@ using namespace executorch::extension; using namespace torch::executor; +using executorch::etdump::ETDumpGen; namespace executorch::extension { class TensorHybrid : public facebook::jni::HybridClass { @@ -268,11 +270,21 @@ class ExecuTorchJni : public facebook::jni::HybridClass { } else if (loadMode == 3) { load_mode = Module::LoadMode::MmapUseMlockIgnoreErrors; } + + // Module loading section #ifdef EXECUTORCH_ANDROID_PROFILING - auto etdump_gen = std::make_unique(); + auto* manager = executorch::extension::jni::getGlobalETDumpManager(); + + // Create ETDumpGen only if profiling is enabled at runtime + std::unique_ptr etdump_gen = nullptr; + + if (manager && manager->isProfilingEnabled()) { + etdump_gen = std::make_unique(); + } #else - auto etdump_gen = nullptr; + std::unique_ptr etdump_gen = nullptr; #endif + module_ = std::make_unique( modelPath->toStdString(), load_mode, std::move(etdump_gen)); @@ -460,7 +472,7 @@ class ExecuTorchJni : public facebook::jni::HybridClass { ET_LOG(Error, "Cannot write result.etdump error: %d", errno); return false; } else { - ET_LOG(Info, "ETDump written %d bytes to file.", bytes_written); + ET_LOG(Info, "ETDump written %zd bytes to file.", bytes_written); } close(etdump_file); free(etdump_data.buf); @@ -518,6 +530,54 @@ class ExecuTorchJni : public facebook::jni::HybridClass { return ret; } + // Write ETDump to custom path + jboolean writeETDumpToPath(facebook::jni::alias_ref output_path) { +#ifdef EXECUTORCH_ANDROID_PROFILING + executorch::etdump::ETDumpGen* etdumpgen = + (executorch::etdump::ETDumpGen*)module_->event_tracer(); + + if (etdumpgen) { + executorch::etdump::ETDumpResult result = etdumpgen->get_etdump_data(); + + if (result.buf != nullptr && result.size > 0) { + std::string path_str = output_path->toStdString(); + + std::ofstream file(path_str, std::ios::binary); + if (!file.is_open()) { + return JNI_FALSE; + } + + file.write(reinterpret_cast(result.buf), result.size); + file.close(); + + return JNI_TRUE; + } + } +#endif + return JNI_FALSE; + } + + // Get ETDump data as byte array + facebook::jni::local_ref getETDumpData() { +#ifdef EXECUTORCH_ANDROID_PROFILING + executorch::etdump::ETDumpGen* etdumpgen = + (executorch::etdump::ETDumpGen*)module_->event_tracer(); + + if (etdumpgen) { + executorch::etdump::ETDumpResult result = etdumpgen->get_etdump_data(); + + if (result.buf != nullptr && result.size > 0) { + const uint8_t* buf_ptr = static_cast(result.buf); + auto data = facebook::jni::JArrayByte::newArray(result.size); + data->setRegion( + 0, result.size, reinterpret_cast(buf_ptr)); + return data; + } + } +#endif + return nullptr; + } + static void registerNatives() { registerHybrid({ makeNativeMethod("initHybrid", ExecuTorchJni::initHybrid), @@ -529,6 +589,9 @@ class ExecuTorchJni : public facebook::jni::HybridClass { makeNativeMethod("etdump", ExecuTorchJni::etdump), makeNativeMethod("getMethods", ExecuTorchJni::getMethods), makeNativeMethod("getUsedBackends", ExecuTorchJni::getUsedBackends), + makeNativeMethod( + "nativeWriteETDumpToPath", ExecuTorchJni::writeETDumpToPath), + makeNativeMethod("nativeGetETDumpData", ExecuTorchJni::getETDumpData), }); } }; diff --git a/scripts/build_android_library.sh b/scripts/build_android_library.sh index a9934bbc508..4a2aefb87bc 100755 --- a/scripts/build_android_library.sh +++ b/scripts/build_android_library.sh @@ -38,6 +38,7 @@ build_android_native_library() { -DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK}/build/cmake/android.toolchain.cmake" \ --preset "android-${ANDROID_ABI}" \ -DANDROID_PLATFORM=android-26 \ + -DEXECUTORCH_ANDROID_PROFILING="${EXECUTORCH_ANDROID_PROFILING:-OFF}" \ -DEXECUTORCH_ENABLE_EVENT_TRACER="${EXECUTORCH_ANDROID_PROFILING:-OFF}" \ -DEXECUTORCH_BUILD_EXTENSION_LLM="${EXECUTORCH_BUILD_EXTENSION_LLM:-ON}" \ -DEXECUTORCH_BUILD_EXTENSION_LLM_RUNNER="${EXECUTORCH_BUILD_EXTENSION_LLM:-ON}" \