Add support for taking traces with Perfetto.

This change adds support for Perfetto, including support for passing
Perfetto arguments on the command line. This provides support for the
same state that we previously supported for atrace.

Atrace is still selected as the trace engine to use.

Bug: 116754732
Test: atest TraceurUiTests passes with Atrace as the trace backend
Test: atest TraceurUiTests passes with Perfetto as the trace backend
Change-Id: I5f370afd3611f65f58d290023f241868b0c73388
diff --git a/src/com/google/android/traceur/AtraceUtils.java b/src/com/google/android/traceur/AtraceUtils.java
index ef46368..d0a218d 100644
--- a/src/com/google/android/traceur/AtraceUtils.java
+++ b/src/com/google/android/traceur/AtraceUtils.java
@@ -16,8 +16,8 @@
 
 package com.android.traceur;
 
-import android.os.Build;
 import android.os.SystemProperties;
+import android.text.TextUtils;
 import android.util.Log;
 
 import java.io.BufferedReader;
@@ -30,23 +30,31 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.text.SimpleDateFormat;
-import java.util.Arrays;
-import java.util.Date;
+import java.util.Collection;
 import java.util.List;
-import java.util.Locale;
 import java.util.TreeMap;
 
 /**
  * Utility functions for calling atrace
  */
-public class AtraceUtils {
+public class AtraceUtils implements TraceUtils.TraceEngine {
 
     static final String TAG = "Traceur";
 
-    public static boolean atraceStart(String tags, int bufferSizeKb, boolean apps) {
+    private static final String DEBUG_TRACING_FILE = "/sys/kernel/debug/tracing/tracing_on";
+    private static final String TRACING_FILE = "/sys/kernel/tracing/tracing_on";
+
+    public static String NAME = "ATRACE";
+    private static String OUTPUT_EXTENSION = "ctrace";
+
+    public String getOutputExtension() {
+        return OUTPUT_EXTENSION;
+    }
+
+    public boolean traceStart(Collection<String> tags, int bufferSizeKb, boolean apps) {
         String appParameter = apps ? "-a '*' " : "";
-        String cmd = "atrace --async_start -c -b " + bufferSizeKb + " " + appParameter + tags;
+        String cmd = "atrace --async_start -c -b " + bufferSizeKb + " "
+            + appParameter + TextUtils.join(" ", tags);
 
         Log.v(TAG, "Starting async atrace: " + cmd);
         try {
@@ -61,7 +69,7 @@
         return true;
     }
 
-    public static void atraceStop() {
+    public void traceStop() {
         String cmd = "atrace --async_stop > /dev/null";
 
         Log.v(TAG, "Stopping async atrace: " + cmd);
@@ -76,7 +84,7 @@
         }
     }
 
-    public static boolean atraceDump(File outFile) {
+    public boolean traceDump(File outFile) {
         String cmd = "atrace --async_stop -z -c -o " + outFile;
 
         Log.v(TAG, "Dumping async atrace: " + cmd);
@@ -106,6 +114,38 @@
         return true;
     }
 
+    public boolean isTracingOn() {
+        boolean userInitiatedTracingFlag =
+            "1".equals(SystemProperties.get("debug.atrace.user_initiated", ""));
+
+        if (!userInitiatedTracingFlag) {
+            return false;
+        }
+
+        boolean tracingOnFlag = false;
+
+        try {
+            List<String> tracingOnContents;
+
+            Path debugTracingOnPath = Paths.get(DEBUG_TRACING_FILE);
+            Path tracingOnPath = Paths.get(TRACING_FILE);
+
+            if (Files.isReadable(debugTracingOnPath)) {
+                tracingOnContents = Files.readAllLines(debugTracingOnPath);
+            } else if (Files.isReadable(tracingOnPath)) {
+                tracingOnContents = Files.readAllLines(tracingOnPath);
+            } else {
+                return false;
+            }
+
+            tracingOnFlag = !tracingOnContents.get(0).equals("0");
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+
+        return userInitiatedTracingFlag && tracingOnFlag;
+    }
+
     public static TreeMap<String,String> atraceListCategories() {
         String cmd = "atrace --list_categories";
 
diff --git a/src/com/google/android/traceur/PerfettoUtils.java b/src/com/google/android/traceur/PerfettoUtils.java
new file mode 100644
index 0000000..da190a6
--- /dev/null
+++ b/src/com/google/android/traceur/PerfettoUtils.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2015 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.traceur;
+
+import android.system.Os;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utility functions for calling Perfetto
+ */
+public class PerfettoUtils implements TraceUtils.TraceEngine {
+
+    static final String TAG = "Traceur";
+    public static final String NAME = "PERFETTO";
+
+    private static final String OUTPUT_EXTENSION = "perfetto-trace";
+    private static final String TEMP_DIR= "/data/local/traces/";
+    private static final String TEMP_TRACE_LOCATION = "/data/local/traces/.trace-in-progress.trace";
+
+    private static final String PERFETTO_TAG = "traceur";
+    private static final String MARKER = "PERFETTO_ARGUMENTS";
+    private static final int STARTUP_TIMEOUT_MS = 300;
+
+    public String getOutputExtension() {
+        return OUTPUT_EXTENSION;
+    }
+
+    public boolean traceStart(Collection<String> tags, int bufferSizeKb, boolean apps) {
+        if (isTracingOn()) {
+            Log.e(TAG, "Attempting to start perfetto trace but trace is already in progress");
+            return false;
+        } else {
+            // Ensure the temporary trace file is cleared.
+            try {
+                Files.deleteIfExists(Paths.get(TEMP_TRACE_LOCATION));
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        // Build the perfetto config that will be passed on the command line.
+        StringBuilder config = new StringBuilder()
+            .append("write_into_file: true\n")
+            // Arbitrarily long flush period, practically meaning "never flush"
+            .append("file_write_period_ms: 1000000000\n")
+            .append("buffers {\n")
+            .append("  size_kb: " + bufferSizeKb + "\n")
+            .append("  fill_policy: RING_BUFFER\n")
+            .append("} \n")
+            .append("data_sources {\n")
+            .append("  config {\n")
+            .append("    name: \"linux.ftrace\"\n")
+            .append("    target_buffer: 0\n")
+            .append("    ftrace_config {\n");
+
+        for (String tag : tags) {
+            // Tags are expected to be all lowercase letters and underscores.
+            String cleanTag = tag.replaceAll("[^a-z_]", "");
+            if (cleanTag.equals(tag)) {
+                Log.w(TAG, "Attempting to use an invalid tag: " + tag);
+            }
+            config.append("      atrace_categories: \"" + cleanTag + "\"\n");
+        }
+
+        if (apps) {
+            config.append("      atrace_apps: \"*\"\n");
+        }
+
+        // These parameters affect only the kernel trace buffer size and how
+        // frequently it gets moved into the userspace buffer defined above.
+        config.append("      buffer_size_kb: 4096\n")
+            .append("      drain_period_ms: 250\n")
+            .append("    }\n")
+            .append("  }\n")
+            .append("}\n")
+            .append(" \n")
+
+            // For process association
+            .append("data_sources {\n")
+            .append("  config {\n")
+            .append("    name: \"linux.process_stats\"\n")
+            .append("    target_buffer: 0\n")
+            .append("  }\n")
+            .append("} \n");
+
+        String configString = config.toString();
+
+        // If the here-doc ends early, within the config string, exit immediately.
+        // This should never happen.
+        if (configString.contains(MARKER)) {
+            throw new RuntimeException("The arguments to the Perfetto command are malformed.");
+        }
+
+        String cmd = "perfetto --detach=" + PERFETTO_TAG
+            + " -o " + TEMP_TRACE_LOCATION
+            + " -c - --txt"
+            + " <<" + MARKER +"\n" + configString + "\n" + MARKER;
+
+        Log.v(TAG, "Starting perfetto trace.");
+        try {
+            Process process = TraceUtils.exec(cmd, TEMP_DIR);
+
+            // If we time out, ensure that the perfetto process is destroyed.
+            if (!process.waitFor(STARTUP_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+                Log.e(TAG, "perfetto traceStart has timed out after "
+                    + STARTUP_TIMEOUT_MS + " ms.");
+                process.destroyForcibly();
+                return false;
+            }
+
+            if (process.exitValue() != 0) {
+                Log.e(TAG, "perfetto traceStart failed with: "
+                    + process.exitValue());
+                return false;
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+        Log.v(TAG, "perfetto traceStart succeeded!");
+        return true;
+    }
+
+    public void traceStop() {
+        Log.v(TAG, "Stopping perfetto trace.");
+
+        if (!isTracingOn()) {
+            Log.w(TAG, "No trace appears to be in progress. Stopping perfetto trace may not work.");
+        }
+
+        String cmd = "perfetto --stop --attach=" + PERFETTO_TAG;
+        try {
+            Process process = TraceUtils.exec(cmd);
+            if (process.waitFor() != 0) {
+                Log.e(TAG, "perfetto traceStop failed with: " + process.exitValue());
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public boolean traceDump(File outFile) {
+        traceStop();
+
+        Log.v(TAG, "Saving perfetto trace to " + outFile);
+
+        try {
+            Os.rename(TEMP_TRACE_LOCATION, outFile.getCanonicalPath());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+        outFile.setReadable(true, false); // (readable, ownerOnly)
+        return true;
+    }
+
+    public boolean isTracingOn() {
+        String cmd = "perfetto --is_detached=" + PERFETTO_TAG;
+
+        try {
+            Process process = TraceUtils.exec(cmd);
+
+            // 0 represents a detached process exists with this name
+            // 2 represents no detached process with this name
+            // 1 (or other error code) represents an error
+            int result = process.waitFor();
+            if (result == 0) {
+                return true;
+            } else if (result == 2) {
+                return false;
+            } else {
+                throw new RuntimeException("Perfetto error: " + result);
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/src/com/google/android/traceur/Receiver.java b/src/com/google/android/traceur/Receiver.java
index 47c03d1..d38f9c4 100644
--- a/src/com/google/android/traceur/Receiver.java
+++ b/src/com/google/android/traceur/Receiver.java
@@ -43,7 +43,6 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
-import java.util.TreeMap;
 
 public class Receiver extends BroadcastReceiver {
 
@@ -97,9 +96,10 @@
         if (prefsTracingOn != TraceUtils.isTracingOn()) {
             if (prefsTracingOn) {
                 // Show notification if the tags in preferences are not all actually available.
-                String activeAvailableTags = getActiveTags(context, prefs, true);
-                String activeTags = getActiveTags(context, prefs, false);
-                if (!TextUtils.equals(activeAvailableTags, activeTags)) {
+                Set<String> activeAvailableTags = getActiveTags(context, prefs, true);
+                Set<String> activeTags = getActiveTags(context, prefs, false);
+
+                if (!activeAvailableTags.equals(activeTags)) {
                     postCategoryNotification(context, prefs);
                 }
 
@@ -203,7 +203,7 @@
         Intent sendIntent = new Intent(context, MainActivity.class);
 
         String title = context.getString(R.string.tracing_categories_unavailable);
-        String msg = getActiveUnavailableTags(context, prefs);
+        String msg = TextUtils.join(", ", getActiveUnavailableTags(context, prefs));
         final Notification.Builder builder =
             new Notification.Builder(context, NOTIFICATION_CHANNEL)
                 .setSmallIcon(R.drawable.stat_sys_adb)
@@ -239,41 +239,28 @@
         notificationManager.createNotificationChannel(channel);
     }
 
-    public static String getActiveTags(Context context, SharedPreferences prefs, boolean onlyAvailable) {
+    public static Set<String> getActiveTags(Context context, SharedPreferences prefs, boolean onlyAvailable) {
         Set<String> tags = prefs.getStringSet(context.getString(R.string.pref_key_tags),
                 getDefaultTagList());
-        StringBuilder sb = new StringBuilder(10 * tags.size());
-        TreeMap<String, String> available =
-                onlyAvailable ? TraceUtils.listCategories() : null;
+        Set<String> available = TraceUtils.listCategories().keySet();
 
-        for (String s : tags) {
-            if (onlyAvailable && !available.containsKey(s)) continue;
-            if (sb.length() > 0) {
-                sb.append(' ');
-            }
-            sb.append(s);
+        if (onlyAvailable) {
+            tags.retainAll(available);
         }
-        String s = sb.toString();
-        Log.v(TAG, "getActiveTags(onlyAvailable=" + onlyAvailable + ") = \"" + s + "\"");
-        return s;
+
+        Log.v(TAG, "getActiveTags(onlyAvailable=" + onlyAvailable + ") = \"" + tags.toString() + "\"");
+        return tags;
     }
 
-    public static String getActiveUnavailableTags(Context context, SharedPreferences prefs) {
+    public static Set<String> getActiveUnavailableTags(Context context, SharedPreferences prefs) {
         Set<String> tags = prefs.getStringSet(context.getString(R.string.pref_key_tags),
                 getDefaultTagList());
-        StringBuilder sb = new StringBuilder(10 * tags.size());
-        TreeMap<String, String> available = TraceUtils.listCategories();
+        Set<String> available = TraceUtils.listCategories().keySet();
 
-        for (String s : tags) {
-            if (available.containsKey(s)) continue;
-            if (sb.length() > 0) {
-                sb.append(' ');
-            }
-            sb.append(s);
-        }
-        String s = sb.toString();
-        Log.v(TAG, "getActiveUnavailableTags() = \"" + s + "\"");
-        return s;
+        tags.removeAll(available);
+
+        Log.v(TAG, "getActiveUnavailableTags() = \"" + tags.toString() + "\"");
+        return tags;
     }
 
     public static Set<String> getDefaultTagList() {
diff --git a/src/com/google/android/traceur/TraceService.java b/src/com/google/android/traceur/TraceService.java
index 745177c..f4c2300 100644
--- a/src/com/google/android/traceur/TraceService.java
+++ b/src/com/google/android/traceur/TraceService.java
@@ -16,7 +16,6 @@
 
 package com.android.traceur;
 
-import com.google.android.collect.Sets;
 
 import android.app.IntentService;
 import android.app.Notification;
@@ -27,8 +26,11 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.preference.PreferenceManager;
+import android.util.Log;
 
 import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
 
 public class TraceService extends IntentService {
 
@@ -44,10 +46,10 @@
     private static int SAVING_TRACE_NOTIFICATION = 2;
 
     public static void startTracing(final Context context,
-            String tags, int bufferSizeKb, boolean apps) {
+            Collection<String> tags, int bufferSizeKb, boolean apps) {
         Intent intent = new Intent(context, TraceService.class);
         intent.setAction(INTENT_ACTION_START_TRACING);
-        intent.putExtra(INTENT_EXTRA_TAGS, tags);
+        intent.putExtra(INTENT_EXTRA_TAGS, new ArrayList(tags));
         intent.putExtra(INTENT_EXTRA_BUFFER, bufferSizeKb);
         intent.putExtra(INTENT_EXTRA_APPS, apps);
         context.startService(intent);
@@ -68,7 +70,7 @@
     @Override
     public void onHandleIntent(Intent intent) {
         if (intent.getAction().equals(INTENT_ACTION_START_TRACING)) {
-            startTracingInternal(intent.getStringExtra(INTENT_EXTRA_TAGS),
+            startTracingInternal(intent.getStringArrayListExtra(INTENT_EXTRA_TAGS),
                 intent.getIntExtra(INTENT_EXTRA_BUFFER,
                     Integer.parseInt(getApplicationContext()
                         .getString(R.string.default_buffer_size))),
@@ -78,7 +80,7 @@
         }
     }
 
-    private void startTracingInternal(String tags, int bufferSizeKb, boolean appTracing) {
+    private void startTracingInternal(Collection<String> tags, int bufferSizeKb, boolean appTracing) {
         Context context = getApplicationContext();
         Intent stopIntent = new Intent(Receiver.STOP_ACTION,
             null, context, Receiver.class);
diff --git a/src/com/google/android/traceur/TraceUtils.java b/src/com/google/android/traceur/TraceUtils.java
index 319787c..3612d5b 100644
--- a/src/com/google/android/traceur/TraceUtils.java
+++ b/src/com/google/android/traceur/TraceUtils.java
@@ -17,24 +17,18 @@
 package com.android.traceur;
 
 import android.os.Build;
-import android.os.SystemProperties;
 import android.util.Log;
 
-import java.io.BufferedReader;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.text.SimpleDateFormat;
 import java.util.Arrays;
 import java.util.Date;
-import java.util.List;
 import java.util.Locale;
+import java.util.Collection;
 import java.util.TreeMap;
 
 /**
@@ -47,21 +41,33 @@
 
     public static final String TRACE_DIRECTORY = "/data/local/traces/";
 
-    private static final String DEBUG_TRACING_FILE = "/sys/kernel/debug/tracing/tracing_on";
-    private static final String TRACING_FILE = "/sys/kernel/tracing/tracing_on";
+    private static TraceEngine mTraceEngine = new AtraceUtils();
 
     private static final Runtime RUNTIME = Runtime.getRuntime();
 
-    public static boolean traceStart(String tags, int bufferSizeKb, boolean apps) {
-        return AtraceUtils.atraceStart(tags, bufferSizeKb, apps);
+    public interface TraceEngine {
+        public String NAME = "DEFAULT";
+        public String getOutputExtension();
+        public boolean traceStart(Collection<String> tags, int bufferSizeKb, boolean apps);
+        public void traceStop();
+        public boolean traceDump(File outFile);
+        public boolean isTracingOn();
+    }
+
+    public static boolean traceStart(Collection<String> tags, int bufferSizeKb, boolean apps) {
+        return mTraceEngine.traceStart(tags, bufferSizeKb, apps);
     }
 
     public static void traceStop() {
-        AtraceUtils.atraceStop();
+        mTraceEngine.traceStop();
     }
 
     public static boolean traceDump(File outFile) {
-        return AtraceUtils.atraceDump(outFile);
+        return mTraceEngine.traceDump(outFile);
+    }
+
+    public static boolean isTracingOn() {
+        return mTraceEngine.isTracingOn();
     }
 
     public static TreeMap<String, String> listCategories() {
@@ -69,7 +75,7 @@
     }
 
     public static void clearSavedTraces() {
-        String cmd = "rm -f " + TRACE_DIRECTORY + "trace-*.ctrace";
+        String cmd = "rm -f " + TRACE_DIRECTORY + "trace-*.*trace";
 
         Log.v(TAG, "Clearing trace directory: " + cmd);
         try {
@@ -84,47 +90,24 @@
     }
 
     public static Process exec(String cmd) throws IOException {
-        String[] cmdarray = {"sh", "-c", cmd};
-        Log.v(TAG, "exec: " + Arrays.toString(cmdarray));
-        return RUNTIME.exec(cmdarray);
+        return exec(cmd, null);
     }
 
-    public static boolean isTracingOn() {
-        boolean userInitiatedTracingFlag =
-            "1".equals(SystemProperties.get("debug.atrace.user_initiated", ""));
+    public static Process exec(String cmd, String tmpdir) throws IOException {
+        String[] cmdarray = {"sh", "-c", cmd};
+        String[] envp = {"TMPDIR=" + tmpdir};
+        envp = tmpdir == null ? null : envp;
 
-        if (!userInitiatedTracingFlag) {
-            return false;
-        }
+        Log.v(TAG, "exec: " + Arrays.toString(envp) + " " + Arrays.toString(cmdarray));
 
-        boolean tracingOnFlag = false;
-
-        try {
-            List<String> tracingOnContents;
-
-            Path debugTracingOnPath = Paths.get(DEBUG_TRACING_FILE);
-            Path tracingOnPath = Paths.get(TRACING_FILE);
-
-            if (Files.isReadable(debugTracingOnPath)) {
-                tracingOnContents = Files.readAllLines(debugTracingOnPath);
-            } else if (Files.isReadable(tracingOnPath)) {
-                tracingOnContents = Files.readAllLines(tracingOnPath);
-            } else {
-                return false;
-            }
-
-            tracingOnFlag = !tracingOnContents.get(0).equals("0");
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
-
-        return userInitiatedTracingFlag && tracingOnFlag;
+        return RUNTIME.exec(cmdarray, envp);
     }
 
     public static String getOutputFilename() {
         String format = "yyyy-MM-dd-HH-mm-ss";
         String now = new SimpleDateFormat(format, Locale.US).format(new Date());
-        return String.format("trace-%s-%s-%s.ctrace", Build.BOARD, Build.ID, now);
+        return String.format("trace-%s-%s-%s.%s", Build.BOARD, Build.ID, now,
+            mTraceEngine.getOutputExtension());
     }
 
     public static File getOutputFile(String filename) {
diff --git a/uitests/src/com/android/settings/ui/TraceurAppTests.java b/uitests/src/com/android/settings/ui/TraceurAppTests.java
index 24c23c9..2393979 100644
--- a/uitests/src/com/android/settings/ui/TraceurAppTests.java
+++ b/uitests/src/com/android/settings/ui/TraceurAppTests.java
@@ -40,7 +40,7 @@
 public class TraceurAppTests {
 
     private static final String TRACEUR_PACKAGE = "com.android.traceur";
-    private static final int TIMEOUT = 5000;   // milliseconds
+    private static final int TIMEOUT = 7000;   // milliseconds
 
     private UiDevice mDevice;
 
@@ -110,7 +110,7 @@
 
     /*
      * In this test:
-     * Take a trace by toggling 'Record trace' and then tap 'Save and share trace'.
+     * Take a trace by toggling 'Record trace' in the UI
      * Tap the notification once the trace is saved, and verify the share dialog appears.
      */
     @Presubmit
@@ -119,6 +119,7 @@
         mDevice.wait(Until.findObject(By.text("Record trace")), TIMEOUT);
 
         mDevice.findObject(By.text("Record trace")).click();
+        mDevice.wait(Until.hasObject(By.text("Trace is being recorded")), TIMEOUT);
         mDevice.findObject(By.text("Record trace")).click();
 
         // Wait for the popover notification to appear and then disappear,