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
diff --git a/cmds/am/src/com/android/commands/am/Am.java b/cmds/am/src/com/android/commands/am/Am.java
index 301883f..fb60fdf 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 @@
             sendBroadcast();
         } else if (op.equals("profile")) {
             runProfile();
+        } else if (op.equals("dumpheap")) {
+            runDumpHeap();
         } else {
             throw new IllegalArgumentException("Unknown command: " + op);
         }
@@ -424,6 +426,28 @@
         }
     }
 
+    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 @@
                 "\n" +
                 "    start profiling: am profile <PROCESS> start <FILE>\n" +
                 "    stop profiling: am profile <PROCESS> stop\n" +
+                "    dump heap: am dumpheap [flags] <PROCESS> <FILE>\n" +
+                "        -n: dump native heap instead of managed heap\n" +
                 "\n" +
                 "    <INTENT> specifications include these flags:\n" +
                 "        [-a <ACTION>] [-d <DATA_URI>] [-t <MIME_TYPE>]\n" +
diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java
index 1fe85e6..43a08b5 100644
--- a/core/java/android/app/ActivityManagerNative.java
+++ b/core/java/android/app/ActivityManagerNative.java
@@ -1294,6 +1294,19 @@
             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 @@
         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 d788be8..53883b1 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -356,6 +356,11 @@
         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 @@
             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 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 @@
                     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 @@
                     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 @@
         }
     }
 
+    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 1c20062..dc2145f 100644
--- a/core/java/android/app/ApplicationThreadNative.java
+++ b/core/java/android/app/ApplicationThreadNative.java
@@ -403,6 +403,17 @@
             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 @@
         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 20c9a80..8ea59a7 100644
--- a/core/java/android/app/IActivityManager.java
+++ b/core/java/android/app/IActivityManager.java
@@ -316,7 +316,11 @@
     
     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 @@
     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 c8ef17f..039bcb9 100644
--- a/core/java/android/app/IApplicationThread.java
+++ b/core/java/android/app/IApplicationThread.java
@@ -97,6 +97,8 @@
     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 @@
     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 2e14667..d6b6d72 100644
--- a/core/java/android/os/Debug.java
+++ b/core/java/android/os/Debug.java
@@ -730,7 +730,7 @@
     }
 
     /**
-     * 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 @@
     }
 
     /**
-     * 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 93122c4..58aab08 100644
--- a/services/java/com/android/server/am/ActivityManagerService.java
+++ b/services/java/com/android/server/am/ActivityManagerService.java
@@ -11562,7 +11562,69 @@
             }
         }
     }
-    
+
+    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<String, SparseArray<ProcessRecord>> all
+                            = mProcessNames.getMap();
+                    SparseArray<ProcessRecord> 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) { }