wire up sampling profiler to dropbox

When system property "persist.sys.profiler_hz" > 0, SamplingProfilerService is
loaded to SystemServer. It creates a FileObserver, watching any new file in the snapshot
directory. When a snapshot is found, it is put in dropbox and deleted after that.

SamplingProfilerIntegration writes snapshots with headers. Headers are <name, value> pairs,
instantiated by caller.

Currently header format is (also in source comment):

Version: <version number of profiler>\n
Process: <process name>\n
Package: <package name, if exists>\n
Package-Version: <version number of the package, if exists>\n
Build: <fingerprint>\n
\n
<the actual snapshot content begins here...>

BUG=2732642

Change-Id: I2c1699f1728e603de13dbd38f9d8443cd3eecc06
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 468b271..112d9da 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -31,6 +31,7 @@
 import android.content.pm.IPackageManager;
 import android.content.pm.InstrumentationInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ProviderInfo;
 import android.content.pm.ServiceInfo;
 import android.content.res.AssetManager;
@@ -2149,8 +2150,27 @@
 
         void maybeSnapshot() {
             if (mBoundApplication != null) {
-                SamplingProfilerIntegration.writeSnapshot(
-                        mBoundApplication.processName);
+                // convert the *private* ActivityThread.PackageInfo to *public* known
+                // android.content.pm.PackageInfo
+                String packageName = mBoundApplication.info.mPackageName;
+                android.content.pm.PackageInfo packageInfo = null;
+                try {
+                    Context context = getSystemContext();
+                    if(context == null) {
+                        Log.e(TAG, "cannot get a valid context");
+                        return;
+                    }
+                    PackageManager pm = context.getPackageManager();
+                    if(pm == null) {
+                        Log.e(TAG, "cannot get a valid PackageManager");
+                        return;
+                    }
+                    packageInfo = pm.getPackageInfo(
+                            packageName, PackageManager.GET_ACTIVITIES);
+                } catch (NameNotFoundException e) {
+                    Log.e(TAG, "cannot get package info for " + packageName, e);
+                }
+                SamplingProfilerIntegration.writeSnapshot(mBoundApplication.processName, packageInfo);
             }
         }
     }
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 37f18de..a66c9ed 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -16,14 +16,11 @@
 
 package android.provider;
 
-import com.google.android.collect.Maps;
 
-import org.apache.commons.codec.binary.Base64;
 
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
 import android.content.ComponentName;
-import android.content.ContentQueryMap;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
@@ -38,19 +35,14 @@
 import android.database.SQLException;
 import android.net.Uri;
 import android.os.*;
-import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.AndroidException;
 import android.util.Config;
 import android.util.Log;
 
 import java.net.URISyntaxException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Map;
 
 
 /**
@@ -2416,6 +2408,14 @@
         public static final String PARENTAL_CONTROL_REDIRECT_URL = "parental_control_redirect_url";
 
         /**
+         * A positive value indicates the frequency of SamplingProfiler
+         * taking snapshots in hertz. Zero value means SamplingProfiler is disabled.
+         *
+         * @hide
+         */
+        public static final String SAMPLING_PROFILER_HZ = "sampling_profiler_hz";
+
+        /**
          * Settings classname to launch when Settings is clicked from All
          * Applications.  Needed because of user testing between the old
          * and new Settings apps.
diff --git a/core/java/com/android/internal/os/SamplingProfilerIntegration.java b/core/java/com/android/internal/os/SamplingProfilerIntegration.java
index 5f5c7a4..38362c1 100644
--- a/core/java/com/android/internal/os/SamplingProfilerIntegration.java
+++ b/core/java/com/android/internal/os/SamplingProfilerIntegration.java
@@ -16,14 +16,15 @@
 
 package com.android.internal.os;
 
+import android.content.pm.PackageInfo;
 import dalvik.system.SamplingProfiler;
 
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.FileNotFoundException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import android.util.Log;
 import android.os.*;
@@ -35,15 +36,27 @@
 
     private static final String TAG = "SamplingProfilerIntegration";
 
+    public static final String SNAPSHOT_DIR = "/data/snapshots";
+
     private static final boolean enabled;
     private static final Executor snapshotWriter;
+    private static final int samplingProfilerHz;
+    
+    /** Whether or not we've created the snapshots dir. */
+    private static boolean dirMade = false;
+
+    /** Whether or not a snapshot is being persisted. */
+    private static final AtomicBoolean pending = new AtomicBoolean(false);
+
     static {
-        enabled = "1".equals(SystemProperties.get("persist.sampling_profiler"));
-        if (enabled) {
+        samplingProfilerHz = SystemProperties.getInt("persist.sys.profiler_hz", 0);
+        if (samplingProfilerHz > 0) {
             snapshotWriter = Executors.newSingleThreadExecutor();
-            Log.i(TAG, "Profiler is enabled.");
+            enabled = true;
+            Log.i(TAG, "Profiler is enabled. Sampling Profiler Hz: " + samplingProfilerHz);
         } else {
             snapshotWriter = null;
+            enabled = false;
             Log.i(TAG, "Profiler is disabled.");
         }
     }
@@ -60,45 +73,45 @@
      */
     public static void start() {
         if (!enabled) return;
-        SamplingProfiler.getInstance().start(10);
+        SamplingProfiler.getInstance().start(samplingProfilerHz);
     }
 
-    /** Whether or not we've created the snapshots dir. */
-    static boolean dirMade = false;
-
-    /** Whether or not a snapshot is being persisted. */
-    static volatile boolean pending;
-
     /**
-     * Writes a snapshot to the SD card if profiling is enabled.
+     * Writes a snapshot if profiling is enabled.
      */
-    public static void writeSnapshot(final String name) {
+    public static void writeSnapshot(final String processName, final PackageInfo packageInfo) {
         if (!enabled) return;
 
         /*
-         * If we're already writing a snapshot, don't bother enqueing another
+         * If we're already writing a snapshot, don't bother enqueueing another
          * request right now. This will reduce the number of individual
          * snapshots and in turn the total amount of memory consumed (one big
          * snapshot is smaller than N subset snapshots).
          */
-        if (!pending) {
-            pending = true;
+        if (pending.compareAndSet(false, true)) {
             snapshotWriter.execute(new Runnable() {
                 public void run() {
-                    String dir = "/sdcard/snapshots";
                     if (!dirMade) {
-                        new File(dir).mkdirs();
-                        if (new File(dir).isDirectory()) {
+                        File dir = new File(SNAPSHOT_DIR);
+                        dir.mkdirs();
+                        // the directory needs to be writable to anybody
+                        dir.setWritable(true, false);
+                        // the directory needs to be executable to anybody
+                        // don't know why yet, but mode 723 would work, while
+                        // mode 722 throws FileNotFoundExecption at line 151
+                        dir.setExecutable(true, false);
+                        if (new File(SNAPSHOT_DIR).isDirectory()) {
                             dirMade = true;
                         } else {
-                            Log.w(TAG, "Creation of " + dir + " failed.");
+                            Log.w(TAG, "Creation of " + SNAPSHOT_DIR + " failed.");
+                            pending.set(false);
                             return;
                         }
                     }
                     try {
-                        writeSnapshot(dir, name);
+                        writeSnapshot(SNAPSHOT_DIR, processName, packageInfo);
                     } finally {
-                        pending = false;
+                        pending.set(false);
                     }
                 }
             });
@@ -110,13 +123,13 @@
      */
     public static void writeZygoteSnapshot() {
         if (!enabled) return;
-
-        String dir = "/data/zygote/snapshots";
-        new File(dir).mkdirs();
-        writeSnapshot(dir, "zygote");
+        writeSnapshot("zygote", null);
     }
 
-    private static void writeSnapshot(String dir, String name) {
+    /**
+     * pass in PackageInfo to retrieve various values for snapshot header
+     */
+    private static void writeSnapshot(String dir, String processName, PackageInfo packageInfo) {
         byte[] snapshot = SamplingProfiler.getInstance().snapshot();
         if (snapshot == null) {
             return;
@@ -128,39 +141,54 @@
          * we capture two snapshots in rapid succession.
          */
         long start = System.currentTimeMillis();
-        String path = dir + "/" + name.replace(':', '.') + "-" +
-                + System.currentTimeMillis() + ".snapshot";
+        String name = processName.replaceAll(":", ".");
+        String path = dir + "/" + name + "-" +System.currentTimeMillis() + ".snapshot";
+        FileOutputStream out = null;
         try {
-            // Try to open the file a few times. The SD card may not be mounted.
-            FileOutputStream out;
-            int count = 0;
-            while (true) {
-                try {
-                    out = new FileOutputStream(path);
-                    break;
-                } catch (FileNotFoundException e) {
-                    if (++count > 3) {
-                        Log.e(TAG, "Could not open " + path + ".");
-                        return;
-                    }
-
-                    // Sleep for a bit and then try again.
-                    try {
-                        Thread.sleep(2500);
-                    } catch (InterruptedException e1) { /* ignore */ }
-                }
-            }
-
-            try {
-                out.write(snapshot);
-            } finally {
-                out.close();
-            }
-            long elapsed = System.currentTimeMillis() - start;
-            Log.i(TAG, "Wrote snapshot for " + name
-                    + " in " + elapsed + "ms.");
+            out = new FileOutputStream(path);
+            generateSnapshotHeader(name, packageInfo, out);
+            out.write(snapshot);
         } catch (IOException e) {
             Log.e(TAG, "Error writing snapshot.", e);
+        } finally {
+            try {
+                if(out != null) {
+                    out.close();
+                }
+            } catch (IOException ex) {
+                // let it go.
+            }
         }
+        // set file readable to the world so that SamplingProfilerService
+        // can put it to dropbox
+        new File(path).setReadable(true, false);
+
+        long elapsed = System.currentTimeMillis() - start;
+        Log.i(TAG, "Wrote snapshot for " + name + " in " + elapsed + "ms.");
+    }
+
+    /**
+     * generate header for snapshots, with the following format (like http header):
+     *
+     * Version: <version number of profiler>\n
+     * Process: <process name>\n
+     * Package: <package name, if exists>\n
+     * Package-Version: <version number of the package, if exists>\n
+     * Build: <fingerprint>\n
+     * \n
+     * <the actual snapshot content begins here...>
+     */
+    private static void generateSnapshotHeader(String processName, PackageInfo packageInfo,
+            FileOutputStream out) throws IOException {
+        // profiler version
+        out.write("Version: 1\n".getBytes());
+        out.write(("Process: " + processName + "\n").getBytes());
+        if(packageInfo != null) {
+            out.write(("Package: " + packageInfo.packageName + "\n").getBytes());
+            out.write(("Package-Version: " + packageInfo.versionCode + "\n").getBytes());
+        }
+        out.write(("Build: " + Build.FINGERPRINT + "\n").getBytes());
+        // single blank line means the end of snapshot header.
+        out.write("\n".getBytes());
     }
 }
diff --git a/services/java/com/android/server/SamplingProfilerService.java b/services/java/com/android/server/SamplingProfilerService.java
new file mode 100644
index 0000000..26af7f7
--- /dev/null
+++ b/services/java/com/android/server/SamplingProfilerService.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.
+ */
+
+package com.android.server;
+
+import android.content.ContentResolver;
+import android.os.DropBoxManager;
+import android.os.FileObserver;
+import android.os.Binder;
+
+import android.util.Slog;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import com.android.internal.os.SamplingProfilerIntegration;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+public class SamplingProfilerService extends Binder {
+
+    private static final String TAG = "SamplingProfilerService";
+    private static final boolean LOCAL_LOGV = false;
+    public static final String SNAPSHOT_DIR = SamplingProfilerIntegration.SNAPSHOT_DIR;
+
+    private FileObserver snapshotObserver;
+
+    public SamplingProfilerService(Context context) {
+        registerSettingObserver(context);
+        startWorking(context);
+    }
+
+    private void startWorking(Context context) {
+        if (LOCAL_LOGV) Slog.v(TAG, "starting SamplingProfilerService!");
+
+        final DropBoxManager dropbox =
+                (DropBoxManager) context.getSystemService(Context.DROPBOX_SERVICE);
+
+        // before FileObserver is ready, there could have already been some snapshots
+        // in the directory, we don't want to miss them
+        File[] snapshotFiles = new File(SNAPSHOT_DIR).listFiles();
+        for (int i = 0; snapshotFiles != null && i < snapshotFiles.length; i++) {
+            handleSnapshotFile(snapshotFiles[i], dropbox);
+        }
+
+        // detect new snapshot and put it in dropbox
+        // delete it afterwards no matter what happened before
+        // Note: needs listening at event ATTRIB rather than CLOSE_WRITE, because we set the
+        // readability of snapshot files after writing them!
+        snapshotObserver = new FileObserver(SNAPSHOT_DIR, FileObserver.ATTRIB) {
+            @Override
+            public void onEvent(int event, String path) {
+                handleSnapshotFile(new File(SNAPSHOT_DIR, path), dropbox);
+            }
+        };
+        snapshotObserver.startWatching();
+
+        if (LOCAL_LOGV) Slog.v(TAG, "SamplingProfilerService activated");
+    }
+
+    private void handleSnapshotFile(File file, DropBoxManager dropbox) {
+        try {
+            dropbox.addFile(TAG, file, 0);
+            if (LOCAL_LOGV) Slog.v(TAG, file.getPath() + " added to dropbox");
+        } catch (IOException e) {
+            Slog.e(TAG, "Can't add " + file.getPath() + " to dropbox", e);
+        } finally {
+            file.delete();
+        }
+    }
+
+    private void registerSettingObserver(Context context) {
+        ContentResolver contentResolver = context.getContentResolver();
+        contentResolver.registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.SAMPLING_PROFILER_HZ),
+                false, new SamplingProfilerSettingsObserver(contentResolver));
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("SamplingProfilerService:");
+        pw.println("Watching directory: " + SNAPSHOT_DIR);
+    }
+
+    private class SamplingProfilerSettingsObserver extends ContentObserver {
+        private ContentResolver mContentResolver;
+        public SamplingProfilerSettingsObserver(ContentResolver contentResolver) {
+            super(null);
+            mContentResolver = contentResolver;
+            onChange(false);
+        }
+        @Override
+        public void onChange(boolean selfChange) {
+            Integer samplingProfilerHz = Settings.Secure.getInt(
+                    mContentResolver, Settings.Secure.SAMPLING_PROFILER_HZ, 0);
+            // setting this secure property will start or stop sampling profiler,
+            // as well as adjust the frequency of taking snapshots.
+            SystemProperties.set("persist.sys.profiler_hz", samplingProfilerHz.toString());
+        }
+    }
+}
+
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index e7b8c02..e511d1f 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -77,7 +77,7 @@
                 android.os.Process.THREAD_PRIORITY_FOREGROUND);
 
         BinderInternal.disableBackgroundScheduling(true);
-        
+
         String factoryTestStr = SystemProperties.get("ro.factorytest");
         int factoryTest = "".equals(factoryTestStr) ? SystemServer.FACTORY_TEST_OFF
                 : Integer.parseInt(factoryTestStr);
@@ -401,6 +401,18 @@
             } catch (Throwable e) {
                 Slog.e(TAG, "Failure starting DiskStats Service", e);
             }
+
+            try {
+                // need to add this service even if SamplingProfilerIntegration.isEnabled()
+                // is false, because it is this service that detects system property change and
+                // turns on SamplingProfilerIntegration. Plus, when sampling profiler doesn't work,
+                // there is little overhead for running this service.
+                Slog.i(TAG, "SamplingProfiler Service");
+                ServiceManager.addService("samplingprofiler",
+                            new SamplingProfilerService(context));
+            } catch (Throwable e) {
+                Slog.e(TAG, "Failure starting SamplingProfiler Service", e);
+            }
         }
 
         // make sure the ADB_ENABLED setting value matches the secure property value
@@ -519,7 +531,7 @@
             timer.schedule(new TimerTask() {
                 @Override
                 public void run() {
-                    SamplingProfilerIntegration.writeSnapshot("system_server");
+                    SamplingProfilerIntegration.writeSnapshot("system_server", null);
                 }
             }, SNAPSHOT_INTERVAL, SNAPSHOT_INTERVAL);
         }
@@ -527,7 +539,7 @@
         // The system server has to run all of the time, so it needs to be
         // as efficient as possible with its memory usage.
         VMRuntime.getRuntime().setTargetHeapUtilization(0.8f);
-        
+
         System.loadLibrary("android_servers");
         init1(args);
     }