Add DiskIo to Statsd

Adds diskio information to statsd. The puller queries the
proc/uid_io/stats file to get cumulative counts since boot.

Bug: b/116331466

Test: unit test
Test: manually verified that the puller and "cat /proc/uid_io/stats"
returned almost identical output when executed at the same time

Change-Id: Iac446f8dd879ab6bf859eed6e779cc16fdee6c5b
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index 30d8bfc..988ffc4 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -178,6 +178,7 @@
         BatteryVoltage battery_voltage = 10030;
         NumFingerprints num_fingerprints = 10031;
         ProcStats proc_stats = 10029;
+        DiskIo disk_io = 10032;
     }
 
     // DO NOT USE field numbers above 100,000 in AOSP.
@@ -2622,6 +2623,30 @@
 }
 
 /**
+ * Pulls per uid I/O stats. The stats are cumulative since boot.
+ *
+ * Read/write bytes are I/O events from a storage device
+ * Read/write chars are data requested by read/write syscalls, and can be
+ *   satisfied by caching.
+ *
+ * Pulled from StatsCompanionService, which reads proc/uid_io/stats.
+ */
+message DiskIo {
+    optional int32 uid = 1 [(is_uid) = true];
+    optional int64 fg_chars_read = 2;
+    optional int64 fg_chars_write = 3;
+    optional int64 fg_bytes_read = 4;
+    optional int64 fg_bytes_write = 5;
+    optional int64 bg_chars_read = 6;
+    optional int64 bg_chars_write = 7;
+    optional int64 bg_bytes_read = 8;
+    optional int64 bg_bytes_write = 9;
+    optional int64 fg_fsync = 10;
+    optional int64 bg_fsync= 11;
+}
+
+
+/**
  * Pulls the number of fingerprints for each user.
  *
  * Pulled from StatsCompanionService, which queries FingerprintManager.
diff --git a/cmds/statsd/src/external/StatsPullerManager.cpp b/cmds/statsd/src/external/StatsPullerManager.cpp
index 5fb196f..fd86714 100644
--- a/cmds/statsd/src/external/StatsPullerManager.cpp
+++ b/cmds/statsd/src/external/StatsPullerManager.cpp
@@ -211,6 +211,12 @@
         // ProcStats.
         {android::util::PROC_STATS,
          {{}, {}, 1 * NS_PER_SEC, new StatsCompanionServicePuller(android::util::PROC_STATS)}},
+        // Disk I/O stats per uid.
+        {android::util::DISK_IO,
+         {{2,3,4,5,6,7,8,9,10,11},
+          {},
+          3 * NS_PER_SEC,
+          new StatsCompanionServicePuller(android::util::DISK_IO)}},
 };
 
 StatsPullerManager::StatsPullerManager() : mNextPullTimeNs(NO_ALARM_UPDATE) {
diff --git a/core/java/com/android/internal/os/StoragedUidIoStatsReader.java b/core/java/com/android/internal/os/StoragedUidIoStatsReader.java
new file mode 100644
index 0000000..9b03469
--- /dev/null
+++ b/core/java/com/android/internal/os/StoragedUidIoStatsReader.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2018 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.internal.os;
+
+import android.os.StrictMode;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+
+/**
+ * Reads /proc/uid_io/stats which has the line format:
+ *
+ * uid: foreground_read_chars foreground_write_chars foreground_read_bytes foreground_write_bytes
+ * background_read_chars background_write_chars background_read_bytes background_write_bytes
+ * foreground_fsync background_fsync
+ *
+ * This provides the number of bytes/chars read/written in foreground/background for each uid.
+ * The file contains a monotonically increasing count of bytes/chars for a single boot.
+ */
+public class StoragedUidIoStatsReader {
+
+    private static final String TAG = StoragedUidIoStatsReader.class.getSimpleName();
+    private static String sUidIoFile = "/proc/uid_io/stats";
+
+    public StoragedUidIoStatsReader() {
+    }
+
+    @VisibleForTesting
+    public StoragedUidIoStatsReader(String file) {
+        sUidIoFile = file;
+    }
+
+    /**
+     * Notifies when new data is available.
+     */
+    public interface Callback {
+
+        /**
+         * Provides data to the client.
+         *
+         * Note: Bytes are I/O events from a storage device. Chars are data requested by syscalls,
+         *   and can be satisfied by caching.
+         */
+        void onUidStorageStats(int uid, long fgCharsRead, long fgCharsWrite, long fgBytesRead,
+                long fgBytesWrite, long bgCharsRead, long bgCharsWrite, long bgBytesRead,
+                long bgBytesWrite, long fgFsync, long bgFsync);
+    }
+
+    /**
+     * Reads the proc file, calling into the callback with raw absolute value of I/O stats
+     * for each UID.
+     *
+     * @param callback The callback to invoke for each line of the proc file.
+     */
+    public void readAbsolute(Callback callback) {
+        final int oldMask = StrictMode.allowThreadDiskReadsMask();
+        File file = new File(sUidIoFile);
+        try (BufferedReader reader = Files.newBufferedReader(file.toPath())) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                String[] fields = TextUtils.split(line, " ");
+                if (fields.length != 11) {
+                    Slog.e(TAG, "Malformed entry in " + sUidIoFile + ": " + line);
+                    continue;
+                }
+                try {
+                    final String uidStr = fields[0];
+                    final int uid = Integer.parseInt(fields[0], 10);
+                    final long fgCharsRead = Long.parseLong(fields[1], 10);
+                    final long fgCharsWrite = Long.parseLong(fields[2], 10);
+                    final long fgBytesRead = Long.parseLong(fields[3], 10);
+                    final long fgBytesWrite = Long.parseLong(fields[4], 10);
+                    final long bgCharsRead = Long.parseLong(fields[5], 10);
+                    final long bgCharsWrite = Long.parseLong(fields[6], 10);
+                    final long bgBytesRead = Long.parseLong(fields[7], 10);
+                    final long bgBytesWrite = Long.parseLong(fields[8], 10);
+                    final long fgFsync = Long.parseLong(fields[9], 10);
+                    final long bgFsync = Long.parseLong(fields[10], 10);
+                    callback.onUidStorageStats(uid, fgCharsRead, fgCharsWrite, fgBytesRead,
+                            fgBytesWrite, bgCharsRead, bgCharsWrite, bgBytesRead, bgBytesWrite,
+                            fgFsync, bgFsync);
+                } catch (NumberFormatException e) {
+                    Slog.e(TAG, "Could not parse entry in " + sUidIoFile + ": " + e.getMessage());
+                }
+            }
+        } catch (IOException e) {
+            Slog.e(TAG, "Failed to read " + sUidIoFile + ": " + e.getMessage());
+        } finally {
+            StrictMode.setThreadPolicyMask(oldMask);
+        }
+    }
+}
diff --git a/core/tests/coretests/src/com/android/internal/os/KernelCpuProcReaderTest.java b/core/tests/coretests/src/com/android/internal/os/KernelCpuProcReaderTest.java
index efdd7e9..8360126 100644
--- a/core/tests/coretests/src/com/android/internal/os/KernelCpuProcReaderTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/KernelCpuProcReaderTest.java
@@ -43,7 +43,7 @@
 /**
  * Test class for {@link KernelCpuProcReader}.
  *
- * $ atest FrameworksCoreTests:com.android.internal.os.KernelCpuProcReader
+ * $ atest FrameworksCoreTests:com.android.internal.os.KernelCpuProcReaderTest
  */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
diff --git a/core/tests/coretests/src/com/android/internal/os/StoragedUidIoStatsReaderTest.java b/core/tests/coretests/src/com/android/internal/os/StoragedUidIoStatsReaderTest.java
new file mode 100644
index 0000000..c051a1c
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/os/StoragedUidIoStatsReaderTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2018 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.internal.os;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.content.Context;
+import android.os.FileUtils;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.nio.file.Files;
+
+
+/**
+ * Test class for {@link StoragedUidIoStatsReader}.
+ *
+ * To run it:
+ * atest FrameworksCoreTests:com.android.internal.os.StoragedUidIoStatsReaderTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class StoragedUidIoStatsReaderTest {
+
+    private File mRoot;
+    private File mTestDir;
+    private File mTestFile;
+    // private Random mRand = new Random();
+
+    private StoragedUidIoStatsReader mStoragedUidIoStatsReader;
+    @Mock
+    private StoragedUidIoStatsReader.Callback mCallback;
+
+    private Context getContext() {
+        return InstrumentationRegistry.getContext();
+    }
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mTestDir = getContext().getDir("test", Context.MODE_PRIVATE);
+        mRoot = getContext().getFilesDir();
+        mTestFile = new File(mTestDir, "test.file");
+        mStoragedUidIoStatsReader = new StoragedUidIoStatsReader(mTestFile.getAbsolutePath());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        FileUtils.deleteContents(mTestDir);
+        FileUtils.deleteContents(mRoot);
+    }
+
+
+    /**
+     * Tests that reading will never call the callback.
+     */
+    @Test
+    public void testReadNonexistentFile() throws Exception {
+        mStoragedUidIoStatsReader.readAbsolute(mCallback);
+        verifyZeroInteractions(mCallback);
+
+    }
+
+    /**
+     * Tests that reading a file with 3 uids works as expected.
+     */
+    @Test
+    public void testReadExpected() throws Exception {
+        BufferedWriter bufferedWriter = Files.newBufferedWriter(mTestFile.toPath());
+        int[] uids = {0, 100, 200};
+        long[] fg_chars_read = {1L, 101L, 201L};
+        long[] fg_chars_write = {2L, 102L, 202L};
+        long[] fg_bytes_read = {3L, 103L, 203L};
+        long[] fg_bytes_write = {4L, 104L, 204L};
+        long[] bg_chars_read = {5L, 105L, 205L};
+        long[] bg_chars_write = {6L, 106L, 206L};
+        long[] bg_bytes_read = {7L, 107L, 207L};
+        long[] bg_bytes_write = {8L, 108L, 208L};
+        long[] fg_fsync = {9L, 109L, 209L};
+        long[] bg_fsync = {10L, 110L, 210L};
+
+        for (int i = 0; i < uids.length; i++) {
+            bufferedWriter.write(String
+                    .format("%d %d %d %d %d %d %d %d %d %d %d\n", uids[i], fg_chars_read[i],
+                            fg_chars_write[i], fg_bytes_read[i], fg_bytes_write[i],
+                            bg_chars_read[i], bg_chars_write[i], bg_bytes_read[i],
+                            bg_bytes_write[i], fg_fsync[i], bg_fsync[i]));
+        }
+        bufferedWriter.close();
+
+        mStoragedUidIoStatsReader.readAbsolute(mCallback);
+        for (int i = 0; i < uids.length; i++) {
+            verify(mCallback).onUidStorageStats(uids[i], fg_chars_read[i], fg_chars_write[i],
+                    fg_bytes_read[i], fg_bytes_write[i], bg_chars_read[i], bg_chars_write[i],
+                    bg_bytes_read[i], bg_bytes_write[i], fg_fsync[i], bg_fsync[i]);
+        }
+        verifyNoMoreInteractions(mCallback);
+
+    }
+
+    /**
+     * Tests that a line with less than 11 items is passed over.
+     */
+    @Test
+    public void testLineDoesNotElevenEntries() throws Exception {
+        BufferedWriter bufferedWriter = Files.newBufferedWriter(mTestFile.toPath());
+
+        // Only has 10 numbers.
+        bufferedWriter.write(String
+                .format("%d %d %d %d %d %d %d %d %d %d\n", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9));
+
+        bufferedWriter.write(String
+                .format("%d %d %d %d %d %d %d %d %d %d %d\n", 10, 11, 12, 13, 14, 15, 16, 17, 18,
+                        19, 20));
+        bufferedWriter.close();
+
+        // Make sure we get the second line, but the first is skipped.
+        mStoragedUidIoStatsReader.readAbsolute(mCallback);
+        verify(mCallback).onUidStorageStats(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
+        verifyNoMoreInteractions(mCallback);
+    }
+
+
+    /**
+     * Tests that a line that is malformed is passed over.
+     */
+    @Test
+    public void testLineIsMalformed() throws Exception {
+        BufferedWriter bufferedWriter = Files.newBufferedWriter(mTestFile.toPath());
+
+        // Line is not formatted properly. It has a string.
+        bufferedWriter.write(String
+                .format("%d %d %d %d %d %s %d %d %d %d %d\n", 0, 1, 2, 3, 4, "NotANumber", 5, 6, 7,
+                        8, 9));
+
+        bufferedWriter.write(String
+                .format("%d %d %d %d %d %d %d %d %d %d %d\n", 10, 11, 12, 13, 14, 15, 16, 17, 18,
+                        19, 20));
+        bufferedWriter.close();
+
+        // Make sure we get the second line, but the first is skipped.
+        mStoragedUidIoStatsReader.readAbsolute(mCallback);
+        verify(mCallback).onUidStorageStats(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
+        verifyNoMoreInteractions(mCallback);
+    }
+}
diff --git a/services/core/java/com/android/server/stats/StatsCompanionService.java b/services/core/java/com/android/server/stats/StatsCompanionService.java
index 3c64dd2..d0de940 100644
--- a/services/core/java/com/android/server/stats/StatsCompanionService.java
+++ b/services/core/java/com/android/server/stats/StatsCompanionService.java
@@ -82,6 +82,7 @@
 import com.android.internal.os.KernelWakelockStats;
 import com.android.internal.os.LooperStats;
 import com.android.internal.os.PowerProfile;
+import com.android.internal.os.StoragedUidIoStatsReader;
 import com.android.internal.util.DumpUtils;
 import com.android.server.BinderCallsStatsService;
 import com.android.server.LocalServices;
@@ -178,6 +179,8 @@
             new KernelUidCpuActiveTimeReader();
     private KernelUidCpuClusterTimeReader mKernelUidCpuClusterTimeReader =
             new KernelUidCpuClusterTimeReader();
+    private StoragedUidIoStatsReader mStoragedUidIoStatsReader =
+            new StoragedUidIoStatsReader();
 
     private static IThermalService sThermalService;
     private File mBaseDir =
@@ -1333,6 +1336,27 @@
         }
     }
 
+    private void pullDiskIo(int tagId, long elapsedNanos, final long wallClockNanos,
+            List<StatsLogEventWrapper> pulledData) {
+        mStoragedUidIoStatsReader.readAbsolute((uid, fgCharsRead, fgCharsWrite, fgBytesRead,
+                fgBytesWrite, bgCharsRead, bgCharsWrite, bgBytesRead, bgBytesWrite,
+                fgFsync, bgFsync) -> {
+            StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos);
+            e.writeInt(uid);
+            e.writeLong(fgCharsRead);
+            e.writeLong(fgCharsWrite);
+            e.writeLong(fgBytesRead);
+            e.writeLong(fgBytesWrite);
+            e.writeLong(bgCharsRead);
+            e.writeLong(bgCharsWrite);
+            e.writeLong(bgBytesRead);
+            e.writeLong(bgBytesWrite);
+            e.writeLong(fgFsync);
+            e.writeLong(bgFsync);
+            pulledData.add(e);
+        });
+    }
+
     /**
      * Pulls various data.
      */
@@ -1450,6 +1474,10 @@
                 pullProcessStats(tagId, elapsedNanos, wallClockNanos, ret);
                 break;
             }
+            case StatsLog.DISK_IO: {
+                pullDiskIo(tagId, elapsedNanos, wallClockNanos, ret);
+                break;
+            }
             default:
                 Slog.w(TAG, "No such tagId data as " + tagId);
                 return null;