Frameworks/base: Add compiler stats to Package Manager

Add a simple class for storing compiler statistics. Capture compile
times for code paths from a package.

Bug: 29223204
Change-Id: I1b066de6a83a739470a42480eee0bfef88423eea
diff --git a/services/core/java/com/android/server/pm/AbstractStatsBase.java b/services/core/java/com/android/server/pm/AbstractStatsBase.java
new file mode 100644
index 0000000..612c476
--- /dev/null
+++ b/services/core/java/com/android/server/pm/AbstractStatsBase.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2016 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.pm;
+
+import android.os.Environment;
+import android.os.SystemClock;
+import android.util.AtomicFile;
+
+import java.io.File;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * A simple base class for statistics that need to be saved/restored from a dedicated file. This
+ * class provides a base implementation that:
+ * <ul>
+ * <li>Provide an AtomicFile to the actual read/write code
+ * <li>A background-thread write and a synchronous write
+ * <li>Write-limiting for the background-thread (by default writes are at least 30 minutes apart)
+ * <li>Can lock on the provided data object before writing
+ * </ul>
+ * For completion, a subclass needs to implement actual {@link #writeInternal(Object) writing} and
+ * {@link #readInternal(Object) reading}.
+ */
+public abstract class AbstractStatsBase<T> {
+
+    private static final int WRITE_INTERVAL_MS =
+            (PackageManagerService.DEBUG_DEXOPT) ? 0 : 30*60*1000;
+    private final Object mFileLock = new Object();
+    private final AtomicLong mLastTimeWritten = new AtomicLong(0);
+    private final AtomicBoolean mBackgroundWriteRunning = new AtomicBoolean(false);
+    private final String mFileName;
+    private final String mBackgroundThreadName;
+    private final boolean mLock;
+
+    protected AbstractStatsBase(String fileName, String threadName, boolean lock) {
+        mFileName = fileName;
+        mBackgroundThreadName = threadName;
+        mLock = lock;
+    }
+
+    protected AtomicFile getFile() {
+        File dataDir = Environment.getDataDirectory();
+        File systemDir = new File(dataDir, "system");
+        File fname = new File(systemDir, mFileName);
+        return new AtomicFile(fname);
+    }
+
+    void writeNow(final T data) {
+        writeImpl(data);
+        mLastTimeWritten.set(SystemClock.elapsedRealtime());
+    }
+
+    boolean maybeWriteAsync(final T data) {
+        if (SystemClock.elapsedRealtime() - mLastTimeWritten.get() < WRITE_INTERVAL_MS
+            && !PackageManagerService.DEBUG_DEXOPT) {
+            return false;
+        }
+
+        if (mBackgroundWriteRunning.compareAndSet(false, true)) {
+            new Thread(mBackgroundThreadName) {
+                @Override
+                public void run() {
+                    try {
+                        writeImpl(data);
+                        mLastTimeWritten.set(SystemClock.elapsedRealtime());
+                    } finally {
+                        mBackgroundWriteRunning.set(false);
+                    }
+                }
+            }.start();
+            return true;
+        }
+
+        return false;
+    }
+
+    private void writeImpl(T data) {
+        if (mLock) {
+            synchronized (data) {
+                synchronized (mFileLock) {
+                    writeInternal(data);
+                }
+            }
+        } else {
+            synchronized (mFileLock) {
+                writeInternal(data);
+            }
+        }
+    }
+
+    protected abstract void writeInternal(T data);
+
+    void read(T data) {
+        if (mLock) {
+            synchronized (data) {
+                synchronized (mFileLock) {
+                    readInternal(data);
+                }
+            }
+        } else {
+            synchronized (mFileLock) {
+                readInternal(data);
+            }
+        }
+        // We use the current time as last-written. read() is called on system server startup
+        // (current situation), and we want to postpone I/O at boot.
+        mLastTimeWritten.set(SystemClock.elapsedRealtime());
+    }
+
+    protected abstract void readInternal(T data);
+}
diff --git a/services/core/java/com/android/server/pm/CompilerStats.java b/services/core/java/com/android/server/pm/CompilerStats.java
new file mode 100644
index 0000000..46efd98
--- /dev/null
+++ b/services/core/java/com/android/server/pm/CompilerStats.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2016 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.pm;
+
+import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.Log;
+
+import com.android.internal.util.FastPrintWriter;
+import com.android.internal.util.IndentingPrintWriter;
+
+import libcore.io.IoUtils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A class that collects, serializes and deserializes compiler-related statistics on a
+ * per-package per-code-path basis.
+ *
+ * Currently used to track compile times.
+ */
+class CompilerStats extends AbstractStatsBase<Void> {
+
+    private final static String COMPILER_STATS_VERSION_HEADER = "PACKAGE_MANAGER__COMPILER_STATS__";
+    private final static int COMPILER_STATS_VERSION = 1;
+
+    /**
+     * Class to collect all stats pertaining to one package.
+     */
+    static class PackageStats {
+
+        private final String packageName;
+
+        /**
+         * This map stores compile-times for all code paths in the package. The value
+         * is in milliseconds.
+         */
+        private final Map<String, Long> compileTimePerCodePath;
+
+        /**
+         * @param packageName
+         */
+        public PackageStats(String packageName) {
+            this.packageName = packageName;
+            // We expect at least one element in here, but let's make it minimal.
+            compileTimePerCodePath = new ArrayMap<>(2);
+        }
+
+        public String getPackageName() {
+            return packageName;
+        }
+
+        /**
+         * Return the recorded compile time for a given code path. Returns
+         * 0 if there is no recorded time.
+         */
+        public long getCompileTime(String codePath) {
+            String storagePath = getStoredPathFromCodePath(codePath);
+            synchronized (compileTimePerCodePath) {
+                Long l = compileTimePerCodePath.get(storagePath);
+                if (l == null) {
+                    return 0;
+                }
+                return l;
+            }
+        }
+
+        public void setCompileTime(String codePath, long compileTimeInMs) {
+            String storagePath = getStoredPathFromCodePath(codePath);
+            synchronized (compileTimePerCodePath) {
+                if (compileTimeInMs <= 0) {
+                    compileTimePerCodePath.remove(storagePath);
+                } else {
+                    compileTimePerCodePath.put(storagePath, compileTimeInMs);
+                }
+            }
+        }
+
+        private static String getStoredPathFromCodePath(String codePath) {
+            int lastSlash = codePath.lastIndexOf(File.separatorChar);
+            return codePath.substring(lastSlash + 1);
+        }
+
+        public void dump(IndentingPrintWriter ipw) {
+            synchronized (compileTimePerCodePath) {
+                if (compileTimePerCodePath.size() == 0) {
+                    ipw.println("(No recorded stats)");
+                } else {
+                    for (Map.Entry<String, Long> e : compileTimePerCodePath.entrySet()) {
+                        ipw.println(" " + e.getKey() + " - " + e.getValue());
+                    }
+                }
+            }
+        }
+    }
+
+    private final Map<String, PackageStats> packageStats;
+
+    public CompilerStats() {
+        super("package-cstats.list", "CompilerStats_DiskWriter", /* lock */ false);
+        packageStats = new HashMap<>();
+    }
+
+    public PackageStats getPackageStats(String packageName) {
+        synchronized (packageStats) {
+            return packageStats.get(packageName);
+        }
+    }
+
+    public void setPackageStats(String packageName, PackageStats stats) {
+        synchronized (packageStats) {
+            packageStats.put(packageName, stats);
+        }
+    }
+
+    public PackageStats createPackageStats(String packageName) {
+        synchronized (packageStats) {
+            PackageStats newStats = new PackageStats(packageName);
+            packageStats.put(packageName, newStats);
+            return newStats;
+        }
+    }
+
+    public PackageStats getOrCreatePackageStats(String packageName) {
+        synchronized (packageStats) {
+            PackageStats existingStats = packageStats.get(packageName);
+            if (existingStats != null) {
+                return existingStats;
+            }
+
+            return createPackageStats(packageName);
+        }
+    }
+
+    public void deletePackageStats(String packageName) {
+        synchronized (packageStats) {
+            packageStats.remove(packageName);
+        }
+    }
+
+    // I/O
+
+    // The encoding is simple:
+    //
+    // 1) The first line is a line consisting of the version header and the version number.
+    //
+    // 2) The rest of the file is package data.
+    // 2.1) A package is started by any line not starting with "-";
+    // 2.2) Any line starting with "-" is code path data. The format is:
+    //      '-'{code-path}':'{compile-time}
+
+    public void write(Writer out) {
+        @SuppressWarnings("resource")
+        FastPrintWriter fpw = new FastPrintWriter(out);
+
+        fpw.print(COMPILER_STATS_VERSION_HEADER);
+        fpw.println(COMPILER_STATS_VERSION);
+
+        synchronized (packageStats) {
+            for (PackageStats pkg : packageStats.values()) {
+                synchronized (pkg.compileTimePerCodePath) {
+                    if (!pkg.compileTimePerCodePath.isEmpty()) {
+                        fpw.println(pkg.getPackageName());
+
+                        for (Map.Entry<String, Long> e : pkg.compileTimePerCodePath.entrySet()) {
+                            fpw.println("-" + e.getKey() + ":" + e.getValue());
+                        }
+                    }
+                }
+            }
+        }
+
+        fpw.flush();
+    }
+
+    public boolean read(Reader r) {
+        synchronized (packageStats) {
+            // TODO: Could make this a final switch, then we wouldn't have to synchronize over
+            //       the whole reading.
+            packageStats.clear();
+
+            try {
+                BufferedReader in = new BufferedReader(r);
+
+                // Read header, do version check.
+                String versionLine = in.readLine();
+                if (versionLine == null) {
+                    throw new IllegalArgumentException("No version line found.");
+                } else {
+                    if (!versionLine.startsWith(COMPILER_STATS_VERSION_HEADER)) {
+                        throw new IllegalArgumentException("Invalid version line: " + versionLine);
+                    }
+                    int version = Integer.parseInt(
+                            versionLine.substring(COMPILER_STATS_VERSION_HEADER.length()));
+                    if (version != COMPILER_STATS_VERSION) {
+                        // TODO: Upgrade older formats? For now, just reject and regenerate.
+                        throw new IllegalArgumentException("Unexpected version: " + version);
+                    }
+                }
+
+                // For simpler code, we ignore any data lines before the first package. We
+                // collect it in a fake package.
+                PackageStats currentPackage = new PackageStats("fake package");
+
+                String s = null;
+                while ((s = in.readLine()) != null) {
+                    if (s.startsWith("-")) {
+                        int colonIndex = s.indexOf(':');
+                        if (colonIndex == -1 || colonIndex == 1) {
+                            throw new IllegalArgumentException("Could not parse data " + s);
+                        }
+                        String codePath = s.substring(1, colonIndex);
+                        long time = Long.parseLong(s.substring(colonIndex + 1));
+                        currentPackage.setCompileTime(codePath, time);
+                    } else {
+                        currentPackage = getOrCreatePackageStats(s);
+                    }
+                }
+            } catch (Exception e) {
+                Log.e(PackageManagerService.TAG, "Error parsing compiler stats", e);
+                return false;
+            }
+
+            return true;
+        }
+    }
+
+    void writeNow() {
+        writeNow(null);
+    }
+
+    boolean maybeWriteAsync() {
+        return maybeWriteAsync(null);
+    }
+
+    @Override
+    protected void writeInternal(Void data) {
+        AtomicFile file = getFile();
+        FileOutputStream f = null;
+
+        try {
+            f = file.startWrite();
+            OutputStreamWriter osw = new OutputStreamWriter(f);
+            osw.flush();
+            file.finishWrite(f);
+        } catch (IOException e) {
+            if (f != null) {
+                file.failWrite(f);
+            }
+            Log.e(PackageManagerService.TAG, "Failed to write compiler stats", e);
+        }
+    }
+
+    void read() {
+        read((Void)null);
+    }
+
+    @Override
+    protected void readInternal(Void data) {
+        AtomicFile file = getFile();
+        BufferedReader in = null;
+        try {
+            in = new BufferedReader(new InputStreamReader(file.openRead()));
+            read(in);
+        } catch (FileNotFoundException expected) {
+        } finally {
+            IoUtils.closeQuietly(in);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/OtaDexoptService.java b/services/core/java/com/android/server/pm/OtaDexoptService.java
index 02c6472..77c69c9 100644
--- a/services/core/java/com/android/server/pm/OtaDexoptService.java
+++ b/services/core/java/com/android/server/pm/OtaDexoptService.java
@@ -225,7 +225,8 @@
 
         optimizer.performDexOpt(nextPackage, nextPackage.usesLibraryFiles,
                 null /* ISAs */, false /* checkProfiles */,
-                getCompilerFilterForReason(compilationReason));
+                getCompilerFilterForReason(compilationReason),
+                null /* CompilerStats.PackageStats */);
 
         mCommandsForCurrentPackage = collectingConnection.commands;
         if (mCommandsForCurrentPackage.isEmpty()) {
@@ -271,7 +272,8 @@
                 mPackageManagerService.mInstaller, mPackageManagerService.mInstallLock, mContext);
         optimizer.performDexOpt(nextPackage, nextPackage.usesLibraryFiles, null /* ISAs */,
                 false /* checkProfiles */,
-                getCompilerFilterForReason(PackageManagerService.REASON_AB_OTA));
+                getCompilerFilterForReason(PackageManagerService.REASON_AB_OTA),
+                mPackageManagerService.getOrCreateCompilerPackageStats(nextPackage));
     }
 
     private void moveAbArtifacts(Installer installer) {
diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
index 26a840d..19b1201 100644
--- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java
+++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
@@ -90,7 +90,8 @@
      * synchronized on {@link #mInstallLock}.
      */
     int performDexOpt(PackageParser.Package pkg, String[] sharedLibraries,
-            String[] instructionSets, boolean checkProfiles, String targetCompilationFilter) {
+            String[] instructionSets, boolean checkProfiles, String targetCompilationFilter,
+            CompilerStats.PackageStats packageStats) {
         synchronized (mInstallLock) {
             final boolean useLock = mSystemReady;
             if (useLock) {
@@ -99,7 +100,7 @@
             }
             try {
                 return performDexOptLI(pkg, sharedLibraries, instructionSets, checkProfiles,
-                        targetCompilationFilter);
+                        targetCompilationFilter, packageStats);
             } finally {
                 if (useLock) {
                     mDexoptWakeLock.release();
@@ -150,7 +151,8 @@
     }
 
     private int performDexOptLI(PackageParser.Package pkg, String[] sharedLibraries,
-            String[] targetInstructionSets, boolean checkProfiles, String targetCompilerFilter) {
+            String[] targetInstructionSets, boolean checkProfiles, String targetCompilerFilter,
+            CompilerStats.PackageStats packageStats) {
         final String[] instructionSets = targetInstructionSets != null ?
                 targetInstructionSets : getAppDexInstructionSets(pkg.applicationInfo);
 
@@ -254,10 +256,17 @@
                         | DEXOPT_BOOTCOMPLETE);
 
                 try {
+                    long startTime = System.currentTimeMillis();
+
                     mInstaller.dexopt(path, sharedGid, pkg.packageName, dexCodeInstructionSet,
                             dexoptNeeded, oatDir, dexFlags, targetCompilerFilter, pkg.volumeUuid,
                             sharedLibrariesPath);
                     performedDexOpt = true;
+
+                    if (packageStats != null) {
+                        long endTime = System.currentTimeMillis();
+                        packageStats.setCompileTime(path, (int)(endTime - startTime));
+                    }
                 } catch (InstallerException e) {
                     Slog.w(TAG, "Failed to dexopt", e);
                     successfulDexOpt = false;
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 78fa3a3..833dca4 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -76,8 +76,6 @@
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.content.pm.PackageParser.PARSE_IS_PRIVILEGED;
 import static android.content.pm.PackageParser.isApkFile;
-import static android.os.Process.PACKAGE_INFO_GID;
-import static android.os.Process.SYSTEM_UID;
 import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
 import static android.system.OsConstants.O_CREAT;
 import static android.system.OsConstants.O_RDWR;
@@ -208,7 +206,6 @@
 import android.text.format.DateUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
-import android.util.AtomicFile;
 import android.util.DisplayMetrics;
 import android.util.EventLog;
 import android.util.ExceptionUtils;
@@ -267,7 +264,6 @@
 import org.xmlpull.v1.XmlPullParserException;
 import org.xmlpull.v1.XmlSerializer;
 
-import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
@@ -280,7 +276,6 @@
 import java.io.FileReader;
 import java.io.FilenameFilter;
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.PrintWriter;
 import java.nio.charset.StandardCharsets;
 import java.security.DigestInputStream;
@@ -307,7 +302,6 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicLong;
 
 /**
  * Keep track of all those APKs everywhere.
@@ -1131,204 +1125,7 @@
     final @NonNull String mSharedSystemSharedLibraryPackageName;
 
     private final PackageUsage mPackageUsage = new PackageUsage();
-
-    private class PackageUsage {
-        private static final int WRITE_INTERVAL
-            = (DEBUG_DEXOPT) ? 0 : 30*60*1000; // 30m in ms
-
-        private final Object mFileLock = new Object();
-        private final AtomicLong mLastWritten = new AtomicLong(0);
-        private final AtomicBoolean mBackgroundWriteRunning = new AtomicBoolean(false);
-
-        private boolean mIsHistoricalPackageUsageAvailable = true;
-
-        boolean isHistoricalPackageUsageAvailable() {
-            return mIsHistoricalPackageUsageAvailable;
-        }
-
-        void write(boolean force) {
-            if (force) {
-                writeInternal();
-                return;
-            }
-            if (SystemClock.elapsedRealtime() - mLastWritten.get() < WRITE_INTERVAL
-                && !DEBUG_DEXOPT) {
-                return;
-            }
-            if (mBackgroundWriteRunning.compareAndSet(false, true)) {
-                new Thread("PackageUsage_DiskWriter") {
-                    @Override
-                    public void run() {
-                        try {
-                            writeInternal();
-                        } finally {
-                            mBackgroundWriteRunning.set(false);
-                        }
-                    }
-                }.start();
-            }
-        }
-
-        private void writeInternal() {
-            synchronized (mPackages) {
-                synchronized (mFileLock) {
-                    AtomicFile file = getFile();
-                    FileOutputStream f = null;
-                    try {
-                        f = file.startWrite();
-                        BufferedOutputStream out = new BufferedOutputStream(f);
-                        FileUtils.setPermissions(file.getBaseFile().getPath(),
-                                0640, SYSTEM_UID, PACKAGE_INFO_GID);
-                        StringBuilder sb = new StringBuilder();
-
-                        sb.append(USAGE_FILE_MAGIC_VERSION_1);
-                        sb.append('\n');
-                        out.write(sb.toString().getBytes(StandardCharsets.US_ASCII));
-
-                        for (PackageParser.Package pkg : mPackages.values()) {
-                            if (pkg.getLatestPackageUseTimeInMills() == 0L) {
-                                continue;
-                            }
-                            sb.setLength(0);
-                            sb.append(pkg.packageName);
-                            for (long usageTimeInMillis : pkg.mLastPackageUsageTimeInMills) {
-                                sb.append(' ');
-                                sb.append(usageTimeInMillis);
-                            }
-                            sb.append('\n');
-                            out.write(sb.toString().getBytes(StandardCharsets.US_ASCII));
-                        }
-                        out.flush();
-                        file.finishWrite(f);
-                    } catch (IOException e) {
-                        if (f != null) {
-                            file.failWrite(f);
-                        }
-                        Log.e(TAG, "Failed to write package usage times", e);
-                    }
-                }
-            }
-            mLastWritten.set(SystemClock.elapsedRealtime());
-        }
-
-        void readLP() {
-            synchronized (mFileLock) {
-                AtomicFile file = getFile();
-                BufferedInputStream in = null;
-                try {
-                    in = new BufferedInputStream(file.openRead());
-                    StringBuffer sb = new StringBuffer();
-
-                    String firstLine = readLine(in, sb);
-                    if (firstLine == null) {
-                        // Empty file. Do nothing.
-                    } else if (USAGE_FILE_MAGIC_VERSION_1.equals(firstLine)) {
-                        readVersion1LP(in, sb);
-                    } else {
-                        readVersion0LP(in, sb, firstLine);
-                    }
-                } catch (FileNotFoundException expected) {
-                    mIsHistoricalPackageUsageAvailable = false;
-                } catch (IOException e) {
-                    Log.w(TAG, "Failed to read package usage times", e);
-                } finally {
-                    IoUtils.closeQuietly(in);
-                }
-            }
-            mLastWritten.set(SystemClock.elapsedRealtime());
-        }
-
-        private void readVersion0LP(InputStream in, StringBuffer sb, String firstLine)
-                throws IOException {
-            // Initial version of the file had no version number and stored one
-            // package-timestamp pair per line.
-            // Note that the first line has already been read from the InputStream.
-            for (String line = firstLine; line != null; line = readLine(in, sb)) {
-                String[] tokens = line.split(" ");
-                if (tokens.length != 2) {
-                    throw new IOException("Failed to parse " + line +
-                            " as package-timestamp pair.");
-                }
-
-                String packageName = tokens[0];
-                PackageParser.Package pkg = mPackages.get(packageName);
-                if (pkg == null) {
-                    continue;
-                }
-
-                long timestamp = parseAsLong(tokens[1]);
-                for (int reason = 0;
-                        reason < PackageManager.NOTIFY_PACKAGE_USE_REASONS_COUNT;
-                        reason++) {
-                    pkg.mLastPackageUsageTimeInMills[reason] = timestamp;
-                }
-            }
-        }
-
-        private void readVersion1LP(InputStream in, StringBuffer sb) throws IOException {
-            // Version 1 of the file started with the corresponding version
-            // number and then stored a package name and eight timestamps per line.
-            String line;
-            while ((line = readLine(in, sb)) != null) {
-                String[] tokens = line.split(" ");
-                if (tokens.length != PackageManager.NOTIFY_PACKAGE_USE_REASONS_COUNT + 1) {
-                    throw new IOException("Failed to parse " + line + " as a timestamp array.");
-                }
-
-                String packageName = tokens[0];
-                PackageParser.Package pkg = mPackages.get(packageName);
-                if (pkg == null) {
-                    continue;
-                }
-
-                for (int reason = 0;
-                        reason < PackageManager.NOTIFY_PACKAGE_USE_REASONS_COUNT;
-                        reason++) {
-                    pkg.mLastPackageUsageTimeInMills[reason] = parseAsLong(tokens[reason + 1]);
-                }
-            }
-        }
-
-        private long parseAsLong(String token) throws IOException {
-            try {
-                return Long.parseLong(token);
-            } catch (NumberFormatException e) {
-                throw new IOException("Failed to parse " + token + " as a long.", e);
-            }
-        }
-
-        private String readLine(InputStream in, StringBuffer sb) throws IOException {
-            return readToken(in, sb, '\n');
-        }
-
-        private String readToken(InputStream in, StringBuffer sb, char endOfToken)
-                throws IOException {
-            sb.setLength(0);
-            while (true) {
-                int ch = in.read();
-                if (ch == -1) {
-                    if (sb.length() == 0) {
-                        return null;
-                    }
-                    throw new IOException("Unexpected EOF");
-                }
-                if (ch == endOfToken) {
-                    return sb.toString();
-                }
-                sb.append((char)ch);
-            }
-        }
-
-        private AtomicFile getFile() {
-            File dataDir = Environment.getDataDirectory();
-            File systemDir = new File(dataDir, "system");
-            File fname = new File(systemDir, "package-usage.list");
-            return new AtomicFile(fname);
-        }
-
-        private static final String USAGE_FILE_MAGIC = "PACKAGE_USAGE__VERSION_";
-        private static final String USAGE_FILE_MAGIC_VERSION_1 = USAGE_FILE_MAGIC + "1";
-    }
+    private final CompilerStats mCompilerStats = new CompilerStats();
 
     class PackageHandler extends Handler {
         private boolean mBound = false;
@@ -2709,7 +2506,8 @@
 
             // Now that we know all the packages we are keeping,
             // read and update their last usage times.
-            mPackageUsage.readLP();
+            mPackageUsage.read(mPackages);
+            mCompilerStats.read();
 
             EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_SCAN_END,
                     SystemClock.uptimeMillis());
@@ -7477,7 +7275,8 @@
                 // Package could not be found. Report failure.
                 return PackageDexOptimizer.DEX_OPT_FAILED;
             }
-            mPackageUsage.write(false);
+            mPackageUsage.maybeWriteAsync(mPackages);
+            mCompilerStats.maybeWriteAsync();
         }
         long callingId = Binder.clearCallingIdentity();
         try {
@@ -7522,11 +7321,12 @@
                 // Currently this will do a full compilation of the library by default.
                 pdo.performDexOpt(depPackage, null /* sharedLibraries */, instructionSets,
                         false /* checkProfiles */,
-                        getCompilerFilterForReason(REASON_NON_SYSTEM_LIBRARY));
+                        getCompilerFilterForReason(REASON_NON_SYSTEM_LIBRARY),
+                        getOrCreateCompilerPackageStats(depPackage));
             }
         }
         return pdo.performDexOpt(p, p.usesLibraryFiles, instructionSets, checkProfiles,
-                targetCompilerFilter);
+                targetCompilerFilter, getOrCreateCompilerPackageStats(p));
     }
 
     Collection<PackageParser.Package> findSharedNonSystemLibraries(PackageParser.Package p) {
@@ -7580,7 +7380,8 @@
     }
 
     public void shutdown() {
-        mPackageUsage.write(true);
+        mPackageUsage.writeNow(mPackages);
+        mCompilerStats.writeNow();
     }
 
     @Override
@@ -15226,7 +15027,8 @@
             // Also, don't fail application installs if the dexopt step fails.
             mPackageDexOptimizer.performDexOpt(pkg, pkg.usesLibraryFiles,
                     null /* instructionSets */, false /* checkProfiles */,
-                    getCompilerFilterForReason(REASON_INSTALL));
+                    getCompilerFilterForReason(REASON_INSTALL),
+                    getOrCreateCompilerPackageStats(pkg));
             Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
 
             // Notify BackgroundDexOptService that the package has been changed.
@@ -18187,6 +17989,7 @@
         public static final int DUMP_DOMAIN_PREFERRED = 1 << 18;
         public static final int DUMP_FROZEN = 1 << 19;
         public static final int DUMP_DEXOPT = 1 << 20;
+        public static final int DUMP_COMPILER_STATS = 1 << 21;
 
         public static final int OPTION_SHOW_FILTERS = 1 << 0;
 
@@ -18304,6 +18107,7 @@
                 pw.println("    installs: details about install sessions");
                 pw.println("    check-permission <permission> <package> [<user>]: does pkg hold perm?");
                 pw.println("    dexopt: dump dexopt state");
+                pw.println("    compiler-stats: dump compiler statistics");
                 pw.println("    <package.name>: info about given package");
                 return;
             } else if ("--checkin".equals(opt)) {
@@ -18425,6 +18229,8 @@
                 dumpState.setDump(DumpState.DUMP_FROZEN);
             } else if ("dexopt".equals(cmd)) {
                 dumpState.setDump(DumpState.DUMP_DEXOPT);
+            } else if ("compiler-stats".equals(cmd)) {
+                dumpState.setDump(DumpState.DUMP_COMPILER_STATS);
             } else if ("write".equals(cmd)) {
                 synchronized (mPackages) {
                     mSettings.writeLPr();
@@ -18787,6 +18593,11 @@
                 dumpDexoptStateLPr(pw, packageName);
             }
 
+            if (!checkin && dumpState.isDumping(DumpState.DUMP_COMPILER_STATS)) {
+                if (dumpState.onTitlePrinted()) pw.println();
+                dumpCompilerStatsLPr(pw, packageName);
+            }
+
             if (!checkin && dumpState.isDumping(DumpState.DUMP_MESSAGES) && packageName == null) {
                 if (dumpState.onTitlePrinted()) pw.println();
                 mSettings.dumpReadMessagesLPr(pw, dumpState);
@@ -18851,6 +18662,38 @@
         }
     }
 
+    private void dumpCompilerStatsLPr(PrintWriter pw, String packageName) {
+        final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ", 120);
+        ipw.println();
+        ipw.println("Compiler stats:");
+        ipw.increaseIndent();
+        Collection<PackageParser.Package> packages = null;
+        if (packageName != null) {
+            PackageParser.Package targetPackage = mPackages.get(packageName);
+            if (targetPackage != null) {
+                packages = Collections.singletonList(targetPackage);
+            } else {
+                ipw.println("Unable to find package: " + packageName);
+                return;
+            }
+        } else {
+            packages = mPackages.values();
+        }
+
+        for (PackageParser.Package pkg : packages) {
+            ipw.println("[" + pkg.packageName + "]");
+            ipw.increaseIndent();
+
+            CompilerStats.PackageStats stats = getCompilerPackageStats(pkg.packageName);
+            if (stats == null) {
+                ipw.println("(No recorded stats)");
+            } else {
+                stats.dump(ipw);
+            }
+            ipw.decreaseIndent();
+        }
+    }
+
     private String dumpDomainString(String packageName) {
         List<IntentFilterVerificationInfo> iviList = getIntentFilterVerifications(packageName)
                 .getList();
@@ -21012,4 +20855,20 @@
         msg.setData(data);
         mProcessLoggingHandler.sendMessage(msg);
     }
+
+    public CompilerStats.PackageStats getCompilerPackageStats(String pkgName) {
+        return mCompilerStats.getPackageStats(pkgName);
+    }
+
+    public CompilerStats.PackageStats getOrCreateCompilerPackageStats(PackageParser.Package pkg) {
+        return getOrCreateCompilerPackageStats(pkg.packageName);
+    }
+
+    public CompilerStats.PackageStats getOrCreateCompilerPackageStats(String pkgName) {
+        return mCompilerStats.getOrCreatePackageStats(pkgName);
+    }
+
+    public void deleteCompilerPackageStats(String pkgName) {
+        mCompilerStats.deletePackageStats(pkgName);
+    }
 }
diff --git a/services/core/java/com/android/server/pm/PackageUsage.java b/services/core/java/com/android/server/pm/PackageUsage.java
new file mode 100644
index 0000000..ac1f739
--- /dev/null
+++ b/services/core/java/com/android/server/pm/PackageUsage.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2016 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.pm;
+
+import static android.os.Process.PACKAGE_INFO_GID;
+import static android.os.Process.SYSTEM_UID;
+
+import android.content.pm.PackageManager;
+import android.content.pm.PackageParser;
+import android.os.FileUtils;
+import android.util.AtomicFile;
+import android.util.Log;
+
+import libcore.io.IoUtils;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+class PackageUsage extends AbstractStatsBase<Map<String, PackageParser.Package>> {
+
+    private static final String USAGE_FILE_MAGIC = "PACKAGE_USAGE__VERSION_";
+    private static final String USAGE_FILE_MAGIC_VERSION_1 = USAGE_FILE_MAGIC + "1";
+
+    private boolean mIsHistoricalPackageUsageAvailable = true;
+
+    PackageUsage() {
+        super("package-usage.list", "PackageUsage_DiskWriter", /* lock */ true);
+    }
+
+    boolean isHistoricalPackageUsageAvailable() {
+        return mIsHistoricalPackageUsageAvailable;
+    }
+
+    @Override
+    protected void writeInternal(Map<String, PackageParser.Package> packages) {
+        AtomicFile file = getFile();
+        FileOutputStream f = null;
+        try {
+            f = file.startWrite();
+            BufferedOutputStream out = new BufferedOutputStream(f);
+            FileUtils.setPermissions(file.getBaseFile().getPath(),
+                    0640, SYSTEM_UID, PACKAGE_INFO_GID);
+            StringBuilder sb = new StringBuilder();
+
+            sb.append(USAGE_FILE_MAGIC_VERSION_1);
+            sb.append('\n');
+            out.write(sb.toString().getBytes(StandardCharsets.US_ASCII));
+
+            for (PackageParser.Package pkg : packages.values()) {
+                if (pkg.getLatestPackageUseTimeInMills() == 0L) {
+                    continue;
+                }
+                sb.setLength(0);
+                sb.append(pkg.packageName);
+                for (long usageTimeInMillis : pkg.mLastPackageUsageTimeInMills) {
+                    sb.append(' ');
+                    sb.append(usageTimeInMillis);
+                }
+                sb.append('\n');
+                out.write(sb.toString().getBytes(StandardCharsets.US_ASCII));
+            }
+            out.flush();
+            file.finishWrite(f);
+        } catch (IOException e) {
+            if (f != null) {
+                file.failWrite(f);
+            }
+            Log.e(PackageManagerService.TAG, "Failed to write package usage times", e);
+        }
+    }
+
+    @Override
+    protected void readInternal(Map<String, PackageParser.Package> packages) {
+        AtomicFile file = getFile();
+        BufferedInputStream in = null;
+        try {
+            in = new BufferedInputStream(file.openRead());
+            StringBuffer sb = new StringBuffer();
+
+            String firstLine = readLine(in, sb);
+            if (firstLine == null) {
+                // Empty file. Do nothing.
+            } else if (USAGE_FILE_MAGIC_VERSION_1.equals(firstLine)) {
+                readVersion1LP(packages, in, sb);
+            } else {
+                readVersion0LP(packages, in, sb, firstLine);
+            }
+        } catch (FileNotFoundException expected) {
+            mIsHistoricalPackageUsageAvailable = false;
+        } catch (IOException e) {
+            Log.w(PackageManagerService.TAG, "Failed to read package usage times", e);
+        } finally {
+            IoUtils.closeQuietly(in);
+        }
+    }
+
+    private void readVersion0LP(Map<String, PackageParser.Package> packages, InputStream in,
+            StringBuffer sb, String firstLine)
+            throws IOException {
+        // Initial version of the file had no version number and stored one
+        // package-timestamp pair per line.
+        // Note that the first line has already been read from the InputStream.
+        for (String line = firstLine; line != null; line = readLine(in, sb)) {
+            String[] tokens = line.split(" ");
+            if (tokens.length != 2) {
+                throw new IOException("Failed to parse " + line +
+                        " as package-timestamp pair.");
+            }
+
+            String packageName = tokens[0];
+            PackageParser.Package pkg = packages.get(packageName);
+            if (pkg == null) {
+                continue;
+            }
+
+            long timestamp = parseAsLong(tokens[1]);
+            for (int reason = 0;
+                    reason < PackageManager.NOTIFY_PACKAGE_USE_REASONS_COUNT;
+                    reason++) {
+                pkg.mLastPackageUsageTimeInMills[reason] = timestamp;
+            }
+        }
+    }
+
+    private void readVersion1LP(Map<String, PackageParser.Package> packages, InputStream in,
+            StringBuffer sb) throws IOException {
+        // Version 1 of the file started with the corresponding version
+        // number and then stored a package name and eight timestamps per line.
+        String line;
+        while ((line = readLine(in, sb)) != null) {
+            String[] tokens = line.split(" ");
+            if (tokens.length != PackageManager.NOTIFY_PACKAGE_USE_REASONS_COUNT + 1) {
+                throw new IOException("Failed to parse " + line + " as a timestamp array.");
+            }
+
+            String packageName = tokens[0];
+            PackageParser.Package pkg = packages.get(packageName);
+            if (pkg == null) {
+                continue;
+            }
+
+            for (int reason = 0;
+                    reason < PackageManager.NOTIFY_PACKAGE_USE_REASONS_COUNT;
+                    reason++) {
+                pkg.mLastPackageUsageTimeInMills[reason] = parseAsLong(tokens[reason + 1]);
+            }
+        }
+    }
+
+    private long parseAsLong(String token) throws IOException {
+        try {
+            return Long.parseLong(token);
+        } catch (NumberFormatException e) {
+            throw new IOException("Failed to parse " + token + " as a long.", e);
+        }
+    }
+
+    private String readLine(InputStream in, StringBuffer sb) throws IOException {
+        return readToken(in, sb, '\n');
+    }
+
+    private String readToken(InputStream in, StringBuffer sb, char endOfToken)
+            throws IOException {
+        sb.setLength(0);
+        while (true) {
+            int ch = in.read();
+            if (ch == -1) {
+                if (sb.length() == 0) {
+                    return null;
+                }
+                throw new IOException("Unexpected EOF");
+            }
+            if (ch == endOfToken) {
+                return sb.toString();
+            }
+            sb.append((char)ch);
+        }
+    }
+}
\ No newline at end of file