Support command cache statistics collection

This will add per process files to a specified directory with one byte per cache hit/miss/put

tools/test.py will clean out the stats on each run with the flag, then sum them up on completion

Change-Id: Ib182851002a0a45371e5d89f6a6a92496d2a7f1b
diff --git a/build.gradle b/build.gradle
index 7f2f61d..f7b3a44 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1709,6 +1709,9 @@
     if (project.hasProperty('command_cache_dir')) {
         systemProperty 'command_cache_dir', project.property('command_cache_dir')
     }
+    if (project.hasProperty('command_cache_stats_dir')) {
+        systemProperty 'command_cache_stats_dir', project.property('command_cache_stats_dir')
+    }
     if (project.hasProperty('r8lib')) {
         dependsOn configureTestForR8Lib
         // R8lib should be used instead of the main output and all the tests in
diff --git a/d8_r8/commonBuildSrc/src/main/kotlin/TestConfigurationHelper.kt b/d8_r8/commonBuildSrc/src/main/kotlin/TestConfigurationHelper.kt
index 9a8c17f..943feea 100644
--- a/d8_r8/commonBuildSrc/src/main/kotlin/TestConfigurationHelper.kt
+++ b/d8_r8/commonBuildSrc/src/main/kotlin/TestConfigurationHelper.kt
@@ -90,7 +90,8 @@
         "desugar_jdk_json_dir",
         "desugar_jdk_libs",
         "test_dir",
-        "command_cache_dir").forEach {
+        "command_cache_dir",
+        "command_cache_stats_dir").forEach {
         val propertyName = it
         if (project.hasProperty(propertyName)) {
           project.property(propertyName)?.let { v -> test.systemProperty(propertyName, v) }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index d209cb8..8cacb48 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -70,6 +70,7 @@
 import java.nio.file.Paths;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -753,6 +754,61 @@
     }
   }
 
+  public static class CommandCacheStatistics {
+
+    public static CommandCacheStatistics INSTANCE = new CommandCacheStatistics();
+    private final Path cachePutCounter;
+    private final Path cacheMissCounter;
+    private final Path cacheHitCounter;
+
+    private CommandCacheStatistics() {
+      String commandCacheStatsDir = System.getProperty("command_cache_stats_dir");
+      if (commandCacheStatsDir != null) {
+        String processSpecificUUID = UUID.randomUUID().toString();
+        cachePutCounter = Paths.get(commandCacheStatsDir, processSpecificUUID + "CACHEPUT");
+        cacheMissCounter = Paths.get(commandCacheStatsDir, processSpecificUUID + "CACHEFAIL");
+        cacheHitCounter = Paths.get(commandCacheStatsDir, processSpecificUUID + "CACHEHIT");
+        try {
+          Files.createFile(cachePutCounter);
+          Files.createFile(cacheMissCounter);
+          Files.createFile(cacheHitCounter);
+        } catch (IOException e) {
+          throw new RuntimeException(e);
+        }
+      } else {
+        cachePutCounter = null;
+        cacheMissCounter = null;
+        cacheHitCounter = null;
+      }
+    }
+
+    private static void increaseCount(Path path) {
+      // Not enabled
+      if (path == null) {
+        return;
+      }
+      synchronized (path) {
+        try {
+          Files.write(path, "X".getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);
+        } catch (IOException e) {
+          throw new RuntimeException(e);
+        }
+      }
+    }
+
+    public void addCachePut() {
+      increaseCount(cachePutCounter);
+    }
+
+    public void addCacheHit() {
+      increaseCount(cacheHitCounter);
+    }
+
+    public void addCacheMiss() {
+      increaseCount(cacheMissCounter);
+    }
+  }
+
   public static class CommandResultCache {
     private static CommandResultCache INSTANCE =
         System.getProperty("command_cache_dir") != null
@@ -816,6 +872,7 @@
         // but this should have no impact.
 
         Path outputFile = getOutputFile(cacheLookupKey);
+        CommandCacheStatistics.INSTANCE.addCacheHit();
         return new Pair(
             new ProcessResult(
                 exitCode,
@@ -823,6 +880,7 @@
                 getStringContent(getStderrFile(cacheLookupKey))),
             outputFile.toFile().exists() ? outputFile : null);
       }
+      CommandCacheStatistics.INSTANCE.addCacheMiss();
       return null;
     }
 
@@ -870,6 +928,7 @@
             exitCodeFile,
             StandardCopyOption.ATOMIC_MOVE,
             StandardCopyOption.REPLACE_EXISTING);
+        CommandCacheStatistics.INSTANCE.addCachePut();
       } catch (IOException e) {
         StringBuilder exceptionMessage = new StringBuilder();
         exceptionMessage.append(
diff --git a/tools/test.py b/tools/test.py
index 0dbc827..55c25b7 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -118,6 +118,9 @@
   result.add_argument('--command-cache-dir', '--command_cache_dir',
       help='Cache command invocations to this directory, speeds up test runs',
       default=os.environ.get('R8_COMMAND_CACHE_DIR'))
+  result.add_argument('--command-cache-stats', '--command_cache_stats',
+      help='Collect and print statistics about the command cache.',
+      default=False, action='store_true')
   result.add_argument('--java-home', '--java_home',
       help='Use a custom java version to run tests.')
   result.add_argument('--java-max-memory-size', '--java_max_memory_size',
@@ -324,6 +327,15 @@
     gradle_args.append('-Pcommand_cache_dir=' + options.command_cache_dir)
     if not os.path.exists(options.command_cache_dir):
       os.makedirs(options.command_cache_dir)
+    if options.command_cache_stats:
+      stats_dir = os.path.join(options.command_cache_dir, 'stats')
+      gradle_args.append('-Pcommand_cache_stats_dir=' + stats_dir)
+      if not os.path.exists(stats_dir):
+        os.makedirs(stats_dir)
+      # Clean out old stats files
+      for (_, _, file_names) in os.walk(stats_dir):
+        for f in file_names:
+          os.remove(os.path.join(stats_dir, f))
   if options.java_home:
     gradle_args.append('-Dorg.gradle.java.home=' + options.java_home)
   if options.java_max_memory_size:
@@ -490,8 +502,7 @@
         utils.upload_file_to_cloud_storage(archive,
                                            'gs://r8-test-results/golden-files/' + archive)
 
-    if return_code != 0:
-      return archive_and_return(return_code, options)
+    return archive_and_return(return_code, options)
 
   return 0
 
@@ -499,6 +510,23 @@
   if return_code != 0:
     if options.archive_failures:
       archive_failures(options)
+  if options.command_cache_stats:
+    stats_dir = os.path.join(options.command_cache_dir, 'stats')
+    cache_hit = 0
+    cache_miss = 0
+    cache_put = 0
+    for (_, _, file_names) in os.walk(stats_dir):
+      for f in file_names:
+        if f.endswith('CACHEHIT'):
+          cache_hit += os.stat(os.path.join(stats_dir, f)).st_size
+        if f.endswith('CACHEMISS'):
+          cache_miss += os.stat(os.path.join(stats_dir, f)).st_size
+        if f.endswith('CACHEPUT'):
+          cache_put += os.stat(os.path.join(stats_dir, f)).st_size
+    print('Command cache stats')
+    print('  Cache hits: ' + str(cache_hit))
+    print('  Cache miss: ' + str(cache_miss))
+    print('  Cache puts: ' + str(cache_put))
   return return_code
 
 def print_jstacks():