Add thread leaks tracker for Camera

Add options to log the number of threads in Camera process. This
will be posted to the bug inspector.
--dump-thread-count
--dump-thread-count-interval-ms

Change-Id: Ib8bfb5337055016e24beabda11e6546df9f43e14
diff --git a/src/com/android/media/tests/CameraTestBase.java b/src/com/android/media/tests/CameraTestBase.java
index 5a4cc4a..586f604 100644
--- a/src/com/android/media/tests/CameraTestBase.java
+++ b/src/com/android/media/tests/CameraTestBase.java
@@ -67,7 +67,7 @@
     private static final String PROCESS_CAMERA_APP = "com.google.android.GoogleCamera";
     private static final String DUMP_ION_HEAPS_COMMAND = "cat /d/ion/heaps/system";
 
-    
+
     @Option(name = "test-package", description = "Test package to run.")
     private String mTestPackage = "com.google.android.camera";
 
@@ -100,12 +100,19 @@
 
     @Option(name="dump-meminfo-interval-ms",
             description="Interval of calling dumpsys meminfo in milliseconds.")
-    private int mMeminfoIntervalMs = 60 * 1000;
+    private int mMeminfoIntervalMs = 5 * 60 * 1000;     // 5 minutes
 
     @Option(name = "dump-ion-heap", description =
             "dump ION allocations at the end of test.")
     private boolean mDumpIonHeap = false;
 
+    @Option(name = "dump-thread-count", description =
+            "Count the number of threads in Camera process at a given interval time.")
+    private boolean mDumpThreadCount = false;
+
+    @Option(name="dump-thread-count-interval-ms",
+            description="Interval of calling ps to count the number of threads in milliseconds.")
+    private int mThreadCountIntervalMs = 5 * 60 * 1000; // 5 minutes
 
     private ITestDevice mDevice = null;
 
@@ -116,6 +123,7 @@
     private long mStartTimeMs = 0;
 
     private MeminfoTimer mMeminfoTimer = null;
+    private ThreadTrackerTimer mThreadTrackerTimer = null;
 
     /**
      * {@inheritDoc}
@@ -170,6 +178,10 @@
         if (shouldDumpMeminfo()) {
             mMeminfoTimer = new MeminfoTimer(0, mMeminfoIntervalMs);
         }
+        if (shouldDumpThreadCount()) {
+            long delayMs = mThreadCountIntervalMs / 2;  // Not to run all dump at the same interval.
+            mThreadTrackerTimer = new ThreadTrackerTimer(delayMs, mThreadCountIntervalMs);
+        }
 
         mStartTimeMs = System.currentTimeMillis();
         listener.testRunStarted(getRuKey(), 1);
@@ -242,7 +254,7 @@
         @Override
         public void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
             handleCollectedMetrics(test, testMetrics);
-            stopDumpingMeminfo(test);
+            stopDumping(test);
             dumpIonHeaps(test);
             mListener.testEnded(test, testMetrics);
         }
@@ -250,7 +262,7 @@
         @Override
         public void testStarted(TestIdentifier test) {
             ++mTestCount;
-            startDumpingMeminfo(test);
+            startDumping(test);
             mListener.testStarted(test);
         }
 
@@ -270,19 +282,21 @@
             mListener.invocationFailed(cause);
         }
 
-        protected void startDumpingMeminfo(TestIdentifier test) {
+        protected void startDumping(TestIdentifier test) {
             if (shouldDumpMeminfo()) {
                 mMeminfoTimer.start(test);
             }
+            if (shouldDumpThreadCount()) {
+                mThreadTrackerTimer.start(test);
+            }
         }
 
-        protected void stopDumpingMeminfo(TestIdentifier test) {
+        protected void stopDumping(TestIdentifier test) {
+            InputStreamSource outputSource = null;
+            File outputFile = null;
             if (shouldDumpMeminfo()) {
                 mMeminfoTimer.stop();
-
                 // Grab a snapshot of meminfo file and post it to dashboard.
-                InputStreamSource outputSource = null;
-                File outputFile = null;
                 try {
                     outputFile = mMeminfoTimer.getOutputFile();
                     outputSource = new SnapshotInputStreamSource(new FileInputStream(outputFile));
@@ -297,6 +311,22 @@
                     }
                 }
             }
+            if (shouldDumpThreadCount()) {
+                mThreadTrackerTimer.stop();
+                try {
+                    outputFile = mThreadTrackerTimer.getOutputFile();
+                    outputSource = new SnapshotInputStreamSource(new FileInputStream(outputFile));
+                    String logName = String.format("ps_%s", test.getTestName());
+                    mListener.testLog(logName, LogDataType.TEXT, outputSource);
+                    outputFile.delete();
+                } catch (FileNotFoundException e) {
+                    CLog.w("Failed to read thread count log %s: %s", outputFile, e);
+                } finally {
+                    if (outputSource != null) {
+                        outputSource.cancel();
+                    }
+                }
+            }
         }
 
         protected void dumpIonHeaps(TestIdentifier test) {
@@ -499,6 +529,129 @@
         }
     }
 
+    private class ThreadTrackerTimer {
+
+        // list all threads in a given process, remove the first header line, squeeze whitespaces,
+        // select thread name (in 14th column), then sort and group by its name.
+        // Examples:
+        //    3 SoundPoolThread
+        //    3 SoundPool
+        //    2 Camera Job Disp
+        //    1 pool-7-thread-1
+        //    1 pool-6-thread-1
+        // FIXME: Resolve the error "sh: syntax error: '|' unexpected" using the command below
+        // $ /system/bin/ps -t -p %s | tr -s ' ' | cut -d' ' -f13- | sort | uniq -c | sort -nr"
+        private final String PS_COMMAND_FORMAT = "/system/bin/ps -t -p %s";
+        private final String PGREP_COMMAND_FORMAT = "pgrep %s";
+        private final int STATE_STOPPED = 0;
+        private final int STATE_SCHEDULED = 1;
+        private final int STATE_RUNNING = 2;
+
+        private int mState = STATE_STOPPED;
+        private Timer mTimer = new Timer(true); // run as a daemon thread
+        private long mDelayMs = 0;
+        private long mPeriodMs = 60 * 1000;  // 60 sec
+        private File mOutputFile = null;
+
+        public ThreadTrackerTimer(long delayMs, long periodMs) {
+            mDelayMs = delayMs;
+            mPeriodMs = periodMs;
+        }
+
+        synchronized void start(TestIdentifier test) {
+            if (isRunning()) {
+                stop();
+            }
+            // Create an output file.
+            if (createOutputFile(test) == null) {
+                CLog.w("Stop collecting thread counts since log file not found.");
+                mState = STATE_STOPPED;
+                return; // No-op
+            }
+            mTimer.scheduleAtFixedRate(new TimerTask() {
+                @Override
+                public void run() {
+                    mState = STATE_RUNNING;
+                    dumpThreadCount(PS_COMMAND_FORMAT, getPid(PROCESS_CAMERA_APP), mOutputFile);
+                }
+            }, mDelayMs, mPeriodMs);
+            mState = STATE_SCHEDULED;
+        }
+
+        synchronized void stop() {
+            mState = STATE_STOPPED;
+            mTimer.cancel();
+        }
+
+        synchronized boolean isRunning() {
+            return (mState == STATE_RUNNING);
+        }
+
+        public File getOutputFile() {
+            return mOutputFile;
+        }
+
+        File createOutputFile(TestIdentifier test) {
+            try {
+                mOutputFile = FileUtil.createTempFile(
+                        String.format("ps_%s", test.getTestName()), "txt");
+                new BufferedWriter(new FileWriter(mOutputFile, false)).close();
+            } catch (IOException e) {
+                CLog.w("Failed to create processes and threads file %s: %s",
+                        mOutputFile.getAbsolutePath(), e);
+                return null;
+            }
+            return mOutputFile;
+        }
+
+        String getPid(String processName) {
+            String result = null;
+            try {
+                result = getDevice().executeShellCommand(String.format(PGREP_COMMAND_FORMAT,
+                        processName));
+            } catch (DeviceNotAvailableException e) {
+                CLog.w("Failed to get pid %s: %s", processName, e);
+            }
+            return result;
+        }
+
+        String getUptime() {
+            String uptime = null;
+            try {
+                // uptime will typically have a format like "5278.73 1866.80".  Use the first one
+                // (which is wall-time)
+                uptime = getDevice().executeShellCommand("cat /proc/uptime").split(" ")[0];
+                Float.parseFloat(uptime);
+            } catch (NumberFormatException e) {
+                CLog.w("Failed to get valid uptime %s: %s", uptime, e);
+            } catch (DeviceNotAvailableException e) {
+                CLog.w("Failed to get valid uptime: %s", e);
+            }
+            return uptime;
+        }
+
+        void dumpThreadCount(String commandFormat, String pid, File outputFile) {
+            try {
+                if ("".equals(pid)) {
+                    return;
+                }
+                String result = getDevice().executeShellCommand(String.format(commandFormat, pid));
+                String header = String.format("UPTIME: %s", getUptime());
+                BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile, true));
+                writer.write(header);
+                writer.newLine();
+                writer.write(result);
+                writer.newLine();
+                writer.flush();
+                writer.close();
+            } catch (DeviceNotAvailableException e) {
+                CLog.w("Failed to dump thread count: %s", e);
+            } catch (IOException e) {
+                CLog.w("Failed to dump thread count: %s", e);
+            }
+        }
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -572,6 +725,10 @@
         return mDumpIonHeap;
     }
 
+    public boolean shouldDumpThreadCount() {
+        return mDumpThreadCount;
+    }
+
     public AbstractCollectingListener getCollectingListener() {
         return mCollectingListener;
     }