From 824c510752fd6a30cdba5ed7324cb80a5043ce26 Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Fri, 9 Jul 2010 16:26:57 -0700 Subject: [PATCH] Allow "am" to initiate heap dumps. This was mostly cloned from the "am profile" implementation. It's intended to replace the old "kill -10" approach used by "runhat". We could really use a native heap dump, so I pass a "managed" flag through that indicates whether we want to dump the native or managed heap. We don't currently have a native heap dump-to-file function, so it currently just logs a warning. (android.ddm.DdmHandleNativeHeap.getLeakInfo is a good start -- it copies /proc/maps and then calls get_malloc_leak_info to get some goodies. Needs some formatting to make it human-readable. I didn't want to cram all that into this change.) It would be useful if "am" didn't exit until the heap dump operation completed, but I'm not sure how to do that. Bug 2759474. Change-Id: I46bc98067738d8c72ac0fc10002ca67bb4929271 --- cmds/am/src/com/android/commands/am/Am.java | 26 ++++++++ .../android/app/ActivityManagerNative.java | 37 ++++++++++- core/java/android/app/ActivityThread.java | 37 +++++++++++ .../android/app/ApplicationThreadNative.java | 28 ++++++++ core/java/android/app/IActivityManager.java | 7 +- core/java/android/app/IApplicationThread.java | 3 + core/java/android/os/Debug.java | 19 +++++- .../server/am/ActivityManagerService.java | 64 ++++++++++++++++++- 8 files changed, 215 insertions(+), 6 deletions(-) diff --git a/cmds/am/src/com/android/commands/am/Am.java b/cmds/am/src/com/android/commands/am/Am.java index 301883f43c1f6..fb60fdf758088 100644 --- a/cmds/am/src/com/android/commands/am/Am.java +++ b/cmds/am/src/com/android/commands/am/Am.java @@ -98,6 +98,8 @@ public class Am { sendBroadcast(); } else if (op.equals("profile")) { runProfile(); + } else if (op.equals("dumpheap")) { + runDumpHeap(); } else { throw new IllegalArgumentException("Unknown command: " + op); } @@ -424,6 +426,28 @@ public class Am { } } + private void runDumpHeap() throws Exception { + boolean managed = !"-n".equals(nextOption()); + String process = nextArgRequired(); + String heapFile = nextArgRequired(); + ParcelFileDescriptor fd = null; + + try { + fd = ParcelFileDescriptor.open( + new File(heapFile), + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE | + ParcelFileDescriptor.MODE_READ_WRITE); + } catch (FileNotFoundException e) { + System.err.println("Error: Unable to open file: " + heapFile); + return; + } + + if (!mAm.dumpHeap(process, managed, heapFile, fd)) { + throw new AndroidException("HEAP DUMP FAILED on process " + process); + } + } + private class IntentReceiver extends IIntentReceiver.Stub { private boolean mFinished = false; @@ -593,6 +617,8 @@ public class Am { "\n" + " start profiling: am profile start \n" + " stop profiling: am profile stop\n" + + " dump heap: am dumpheap [flags] \n" + + " -n: dump native heap instead of managed heap\n" + "\n" + " specifications include these flags:\n" + " [-a ] [-d ] [-t ]\n" + diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index 1fe85e6c17561..43a08b5f56d51 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -1294,6 +1294,19 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case DUMP_HEAP_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + String process = data.readString(); + boolean managed = data.readInt() != 0; + String path = data.readString(); + ParcelFileDescriptor fd = data.readInt() != 0 + ? data.readFileDescriptor() : null; + boolean res = dumpHeap(process, managed, path, fd); + reply.writeNoException(); + reply.writeInt(res ? 1 : 0); + return true; + } + } return super.onTransact(code, data, reply, flags); @@ -2874,6 +2887,28 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); reply.recycle(); } - + + public boolean dumpHeap(String process, boolean managed, + String path, ParcelFileDescriptor fd) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeString(process); + data.writeInt(managed ? 1 : 0); + data.writeString(path); + if (fd != null) { + data.writeInt(1); + fd.writeToParcel(data, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + data.writeInt(0); + } + mRemote.transact(DUMP_HEAP_TRANSACTION, data, reply, 0); + reply.readException(); + boolean res = reply.readInt() != 0; + reply.recycle(); + data.recycle(); + return res; + } + private IBinder mRemote; } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index d788be8a4c66e..53883b11d1f29 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -356,6 +356,11 @@ public final class ActivityThread { ParcelFileDescriptor fd; } + private static final class DumpHeapData { + String path; + ParcelFileDescriptor fd; + } + private final class ApplicationThread extends ApplicationThreadNative { private static final String HEAP_COLUMN = "%17s %8s %8s %8s %8s"; private static final String ONE_COUNT_COLUMN = "%17s %8d"; @@ -623,6 +628,13 @@ public final class ActivityThread { queueOrSendMessage(H.PROFILER_CONTROL, pcd, start ? 1 : 0); } + public void dumpHeap(boolean managed, String path, ParcelFileDescriptor fd) { + DumpHeapData dhd = new DumpHeapData(); + dhd.path = path; + dhd.fd = fd; + queueOrSendMessage(H.DUMP_HEAP, dhd, managed ? 1 : 0); + } + public void setSchedulingGroup(int group) { // Note: do this immediately, since going into the foreground // should happen regardless of what pending work we have to do @@ -874,6 +886,7 @@ public final class ActivityThread { public static final int ENABLE_JIT = 132; public static final int DISPATCH_PACKAGE_BROADCAST = 133; public static final int SCHEDULE_CRASH = 134; + public static final int DUMP_HEAP = 135; String codeToString(int code) { if (localLOGV) { switch (code) { @@ -912,6 +925,7 @@ public final class ActivityThread { case ENABLE_JIT: return "ENABLE_JIT"; case DISPATCH_PACKAGE_BROADCAST: return "DISPATCH_PACKAGE_BROADCAST"; case SCHEDULE_CRASH: return "SCHEDULE_CRASH"; + case DUMP_HEAP: return "DUMP_HEAP"; } } return "(unknown)"; @@ -1037,6 +1051,9 @@ public final class ActivityThread { break; case SCHEDULE_CRASH: throw new RemoteServiceException((String)msg.obj); + case DUMP_HEAP: + handleDumpHeap(msg.arg1 != 0, (DumpHeapData)msg.obj); + break; } } @@ -3015,6 +3032,26 @@ public final class ActivityThread { } } + final void handleDumpHeap(boolean managed, DumpHeapData dhd) { + if (managed) { + try { + Debug.dumpHprofData(dhd.path, dhd.fd.getFileDescriptor()); + } catch (IOException e) { + Slog.w(TAG, "Managed heap dump failed on path " + dhd.path + + " -- can the process access this path?"); + } finally { + try { + dhd.fd.close(); + } catch (IOException e) { + Slog.w(TAG, "Failure closing profile fd", e); + } + } + } else { + // TODO + Slog.w(TAG, "Native heap dump not yet implemented"); + } + } + final void handleDispatchPackageBroadcast(int cmd, String[] packages) { boolean hasPkgInfo = false; if (packages != null) { diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java index 1c20062f3f84b..dc2145fb4c2cf 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -403,6 +403,17 @@ public abstract class ApplicationThreadNative extends Binder scheduleCrash(msg); return true; } + + case DUMP_HEAP_TRANSACTION: + { + data.enforceInterface(IApplicationThread.descriptor); + boolean managed = data.readInt() != 0; + String path = data.readString(); + ParcelFileDescriptor fd = data.readInt() != 0 + ? data.readFileDescriptor() : null; + dumpHeap(managed, path, fd); + return true; + } } return super.onTransact(code, data, reply, flags); @@ -829,5 +840,22 @@ class ApplicationThreadProxy implements IApplicationThread { data.recycle(); } + + public void dumpHeap(boolean managed, String path, + ParcelFileDescriptor fd) throws RemoteException { + Parcel data = Parcel.obtain(); + data.writeInterfaceToken(IApplicationThread.descriptor); + data.writeInt(managed ? 1 : 0); + data.writeString(path); + if (fd != null) { + data.writeInt(1); + fd.writeToParcel(data, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + data.writeInt(0); + } + mRemote.transact(DUMP_HEAP_TRANSACTION, data, null, + IBinder.FLAG_ONEWAY); + data.recycle(); + } } diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index 20c9a80c430a0..8ea59a700668d 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -316,7 +316,11 @@ public interface IActivityManager extends IInterface { public void crashApplication(int uid, int initialPid, String packageName, String message) throws RemoteException; - + + // Cause the specified process to dump the specified heap. + public boolean dumpHeap(String process, boolean managed, String path, + ParcelFileDescriptor fd) throws RemoteException; + /* * Private non-Binder interfaces */ @@ -533,4 +537,5 @@ public interface IActivityManager extends IInterface { int SET_IMMERSIVE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+111; int IS_TOP_ACTIVITY_IMMERSIVE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+112; int CRASH_APPLICATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+113; + int DUMP_HEAP_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+114; } diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index c8ef17f1b7a94..039bcb9f6b0e1 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -97,6 +97,8 @@ public interface IApplicationThread extends IInterface { void scheduleActivityConfigurationChanged(IBinder token) throws RemoteException; void profilerControl(boolean start, String path, ParcelFileDescriptor fd) throws RemoteException; + void dumpHeap(boolean managed, String path, ParcelFileDescriptor fd) + throws RemoteException; void setSchedulingGroup(int group) throws RemoteException; void getMemoryInfo(Debug.MemoryInfo outInfo) throws RemoteException; static final int PACKAGE_REMOVED = 0; @@ -140,4 +142,5 @@ public interface IApplicationThread extends IInterface { int SCHEDULE_SUICIDE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+32; int DISPATCH_PACKAGE_BROADCAST_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+33; int SCHEDULE_CRASH_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+34; + int DUMP_HEAP_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+35; } diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 2e14667a0fa49..d6b6d723bb153 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -730,7 +730,7 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo } /** - * Dump "hprof" data to the specified file. This will cause a GC. + * Dump "hprof" data to the specified file. This may cause a GC. * * @param fileName Full pathname of output file (e.g. "/sdcard/dump.hprof"). * @throws UnsupportedOperationException if the VM was built without @@ -742,11 +742,24 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo } /** - * Collect "hprof" and send it to DDMS. This will cause a GC. + * Like dumpHprofData(String), but takes an already-opened + * FileDescriptor to which the trace is written. The file name is also + * supplied simply for logging. Makes a dup of the file descriptor. + * + * Primarily for use by the "am" shell command. + * + * @hide + */ + public static void dumpHprofData(String fileName, FileDescriptor fd) + throws IOException { + VMDebug.dumpHprofData(fileName, fd); + } + + /** + * Collect "hprof" and send it to DDMS. This may cause a GC. * * @throws UnsupportedOperationException if the VM was built without * HPROF support. - * * @hide */ public static void dumpHprofDataDdms() { diff --git a/services/java/com/android/server/am/ActivityManagerService.java b/services/java/com/android/server/am/ActivityManagerService.java index 93122c460b75f..58aab087e1782 100644 --- a/services/java/com/android/server/am/ActivityManagerService.java +++ b/services/java/com/android/server/am/ActivityManagerService.java @@ -11562,7 +11562,69 @@ public final class ActivityManagerService extends ActivityManagerNative implemen } } } - + + public boolean dumpHeap(String process, boolean managed, + String path, ParcelFileDescriptor fd) throws RemoteException { + + try { + synchronized (this) { + // note: hijacking SET_ACTIVITY_WATCHER, but should be changed to + // its own permission (same as profileControl). + if (checkCallingPermission(android.Manifest.permission.SET_ACTIVITY_WATCHER) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires permission " + + android.Manifest.permission.SET_ACTIVITY_WATCHER); + } + + if (fd == null) { + throw new IllegalArgumentException("null fd"); + } + + ProcessRecord proc = null; + try { + int pid = Integer.parseInt(process); + synchronized (mPidsSelfLocked) { + proc = mPidsSelfLocked.get(pid); + } + } catch (NumberFormatException e) { + } + + if (proc == null) { + HashMap> all + = mProcessNames.getMap(); + SparseArray procs = all.get(process); + if (procs != null && procs.size() > 0) { + proc = procs.valueAt(0); + } + } + + if (proc == null || proc.thread == null) { + throw new IllegalArgumentException("Unknown process: " + process); + } + + boolean isSecure = "1".equals(SystemProperties.get(SYSTEM_SECURE, "0")); + if (isSecure) { + if ((proc.info.flags&ApplicationInfo.FLAG_DEBUGGABLE) == 0) { + throw new SecurityException("Process not debuggable: " + proc); + } + } + + proc.thread.dumpHeap(managed, path, fd); + fd = null; + return true; + } + } catch (RemoteException e) { + throw new IllegalStateException("Process disappeared"); + } finally { + if (fd != null) { + try { + fd.close(); + } catch (IOException e) { + } + } + } + } + /** In this method we try to acquire our lock to make sure that we have not deadlocked */ public void monitor() { synchronized (this) { }