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}" \