Added classes to process the output of a compile.

Change-Id: Ie165d75366482bd7eaee500d035b91a36718a30c
diff --git a/pom.xml b/pom.xml
index 881a047..8488bf5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,6 +38,11 @@
       <artifactId>zip4j</artifactId>
       <version>1.3.2</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+      <version>3.3.2</version>
+    </dependency>
   </dependencies>
   <build>
     <pluginManagement>
diff --git a/src/main/java/com/google/gwt/benchmark/artifacts/PermutationInfo.java b/src/main/java/com/google/gwt/benchmark/artifacts/PermutationInfo.java
new file mode 100644
index 0000000..7ba085a
--- /dev/null
+++ b/src/main/java/com/google/gwt/benchmark/artifacts/PermutationInfo.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * 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.google.gwt.benchmark.artifacts;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gwt.benchmark.util.Util;
+
+import org.apache.commons.lang3.tuple.Pair;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.List;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.logging.Logger;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * Represents information about the compiler output for a single permutation.
+ */
+public class PermutationInfo {
+
+  /**
+   * Stats about a single permutation.
+   * <p>
+   * All sizes is are in bytes.
+   */
+  public class Stats {
+    private long initialCompressedSize;
+    private long initialUncompressedSize;
+    private long leftoverCompressedSize;
+    private long leftoverUncompressedSize;
+    private int numberOfFragments;
+    private long symbolMapItems;
+    private long symbolMapSize;
+    private long totalCompressedSize;
+    private long totalUncompressedSize;
+
+    /**
+     * Returns the compressed size of the initial fragment.
+     */
+    public final long getInitialCompressedSize() {
+      return initialCompressedSize;
+    }
+
+    /**
+     * Returns the uncompressed size of the initial fragment.
+     */
+    public final long getInitialUncompressedSize() {
+      return initialUncompressedSize;
+    }
+
+    /**
+     * Returns the compressed size of the leftover fragment; 0 if there is none.
+     */
+    public final long getLeftoverCompressedSize() {
+      return leftoverCompressedSize;
+    }
+
+    /**
+     * Returns the uncompressed size of the leftover fragment; 0 if there is none.
+     */
+    public final long getLeftoverUncompressedSize() {
+      return leftoverUncompressedSize;
+    }
+
+    /**
+     * Returns the number of fragments.
+     */
+    public final int getNumberofFragments() {
+      return numberOfFragments;
+    }
+
+    /**
+     * Returns the number of symbolMap items.
+     */
+    public final long getSymbolMapItems() {
+      return symbolMapItems;
+    }
+
+    /**
+     * Returns the size of the symbolMap.
+     */
+    public final long getSymbolMapSize() {
+      return symbolMapSize;
+    }
+
+    /**
+     * Returns the compressed size of all fragments.
+     */
+    public final long getTotalCompressedSize() {
+      return totalCompressedSize;
+    }
+
+    /**
+     * Returns the uncompressed size of all fragments.
+     */
+    public final long getTotalUncompressedSize() {
+      return totalUncompressedSize;
+    }
+
+    private final void setInitialCompressedSize(long initialCompressedSize) {
+      this.initialCompressedSize = initialCompressedSize;
+    }
+
+    private final void setInitialUncompressedSize(long initialUncompressedSize) {
+      this.initialUncompressedSize = initialUncompressedSize;
+    }
+
+    private final void setLeftoverCompressedSize(long leftoverCompressedSize) {
+      this.leftoverCompressedSize = leftoverCompressedSize;
+    }
+
+    private final void setLeftoverUncompressedSize(long leftoverUncompressedSize) {
+      this.leftoverUncompressedSize = leftoverUncompressedSize;
+    }
+
+    private final void setNumberofFragments(int numberofFragments) {
+      this.numberOfFragments = numberofFragments;
+    }
+
+    private final void setSymbolMapItems(long symbolMapItems) {
+      this.symbolMapItems = symbolMapItems;
+    }
+
+    private final void setSymbolMapSize(long symbolMapSize) {
+      this.symbolMapSize = symbolMapSize;
+    }
+
+    private final void setTotalCompressedSize(long totalCompressedSize) {
+      this.totalCompressedSize = totalCompressedSize;
+    }
+
+    private final void setTotalUncompressedSize(long totalUncompressedSize) {
+      this.totalUncompressedSize = totalUncompressedSize;
+    }
+  }
+
+  private static final Logger log = Logger.getLogger(PermutationInfo.class.getName());
+
+  private static final String MAIN_FILE_SUFFIX = ".cache.js";
+
+  /**
+   * Retrieves the PermutationInfo for all permutations in a compilation result.
+   */
+  public static List<PermutationInfo> getAllPemutationInfo(String outputJar, String auxJar,
+      String basePath) throws IOException {
+
+    int count = 0;
+    try (
+        ZipFile jarFile = new ZipFile(outputJar);
+        ZipFile auxJarFile = new ZipFile(auxJar)) {
+
+      // Read compile mappings.
+      List <PermutationInfo> permutationInfos =
+          processCompilationMappingsFile(basePath, jarFile);
+
+      // Compute the stats for each permutation defined in compilation-mappings.txt.
+      for (PermutationInfo permInfo : permutationInfos) {
+        PermutationInfo.Stats stats = permInfo.new Stats();
+        computeJsSizes(basePath, jarFile, permInfo, stats);
+        computeSymbolInformation(basePath, auxJarFile, permInfo, stats);
+        permInfo.setStats(stats);
+        log.info("Loaded " + permInfo);
+      }
+      log.info("Found " + permutationInfos.size() + " permutations");
+      return permutationInfos;
+    }
+  }
+
+  /**
+   * Reads the permutation info from the compilation-mappings.txt file.
+   * <p>
+   * The compilation-mappipng.txt file consists of a series of multiline records, one for each
+   * permutation. Each record is separated from the previous by a blank line.
+   * <p>
+   * Each record is as follows:
+   * <p>
+   * <16-byte-HASH>.cache.js
+   * <property-name> <property-value>
+   * <property-name> <property-value>
+   * ...
+   */
+  public static PermutationInfo readPermutationInfo(BufferedReader in) {
+    String line = null;
+
+    try {
+      // Process the HASH line.
+      line = in.readLine();
+      if (line == null || !line.endsWith(MAIN_FILE_SUFFIX)) {
+        return null;
+      }
+      PermutationInfo permInfo =
+          new PermutationInfo(line.substring(0, line.length() -  MAIN_FILE_SUFFIX.length()));
+
+      // Read the properties.
+      while((line = in.readLine()) != null) {
+        String[] property = line.split(" ");
+        if (property.length != 2) {
+          break;
+        }
+        permInfo.addProperty(property[0], property[1]);
+      }
+      return permInfo;
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  /**
+   * Computes the compressed and uncompressed sizes for a JavaScript artifact.
+   */
+  private static Pair<Long, Long> computeJsFileSizes(ZipFile jarFile, String baseFileName) {
+    // Check if there is an uncompressed version first.
+    ZipEntry entry = jarFile.getEntry(baseFileName);
+    if (entry != null) {
+      return Pair.of(entry.getSize(), entry.getCompressedSize());
+    }
+    entry = jarFile.getEntry(baseFileName + ".gz");
+    if (entry == null) {
+      // No entry found.
+      return null;
+    }
+    // The artifact was gzipped and then zipped so get the uncompressed size by
+    // looking into the file and use the compressed size the gzipped size.
+    return Pair.of(getUncompressedFromGZippedEntry(jarFile, entry),
+        entry.getSize());
+  }
+
+  /**
+   * Computes the sizes of JavaScript artifacts.
+   */
+  private static void computeJsSizes(String basePath, ZipFile jarFile, PermutationInfo permInfo,
+      PermutationInfo.Stats stats) {
+    Pair<Long, Long> sizes =
+        computeJsFileSizes(jarFile, basePath + "/" + permInfo.getHash() + MAIN_FILE_SUFFIX);
+    long uncompressedSize = sizes.getLeft();
+    long compressedSize = sizes.getRight();
+    stats.setInitialUncompressedSize(uncompressedSize);
+    stats.setInitialCompressedSize(compressedSize);
+    long totalUncompressedSize = uncompressedSize;
+    long totalCompressedSize = compressedSize;
+    for (int i = 1; ; i++) {
+      sizes = computeJsFileSizes(jarFile,
+          basePath + "/deferredjs/" + permInfo.getHash() + "/" + i+ MAIN_FILE_SUFFIX);
+      if (sizes == null) {
+        break;
+      }
+      uncompressedSize = sizes.getLeft();
+      compressedSize = sizes.getRight();
+
+      totalUncompressedSize += uncompressedSize;
+      totalCompressedSize += compressedSize;
+      // These values get overwritten ending with the values for the last fragment which
+      // is the leftovers fragment.
+      stats.setLeftoverUncompressedSize(uncompressedSize);
+      stats.setLeftoverCompressedSize(compressedSize);
+      stats.setNumberofFragments(i);
+    }
+    stats.setTotalCompressedSize(totalCompressedSize);
+    stats.setTotalUncompressedSize(totalUncompressedSize);
+  }
+
+  /**
+   * Computes stats related to symbolmaps.
+   */
+  private static void computeSymbolInformation(String basePath, ZipFile auxJarFile,
+      PermutationInfo permInfo, Stats stats) throws IOException {
+    ZipEntry entry = auxJarFile.getEntry(basePath + "/symbolMaps/" + permInfo.getHash() +
+        ".symbolMap");
+    assert entry != null;
+
+    stats.setSymbolMapSize(entry.getSize());
+    try (
+        InputStream entryStream = auxJarFile.getInputStream(entry);
+        Scanner scanner = new Scanner(entryStream)) {
+      scanner.useDelimiter("\n");
+      int lines = 0;
+      for (; scanner.hasNext(); scanner.next(), lines++) {
+      }
+      stats.setSymbolMapItems(lines);
+    }
+  }
+
+  /**
+   * Retrieves the uncompressed size of a zip file entry that was compressed with gzip
+   * before being zipped.
+   */
+  private static long getUncompressedFromGZippedEntry(ZipFile jarFile, ZipEntry gzippedEntry) {
+    try (InputStream entryStream = jarFile.getInputStream(gzippedEntry)){
+      return Util.getGzUncompressedSizeFromInputStream(entryStream, gzippedEntry.getSize());
+    } catch (IOException e) {
+      return -1;
+    }
+  }
+
+  /**
+   * Processes the compilation-mappings.txt file and creates an empty permutation info
+   * for each permutation in it.
+   */
+  private static List<PermutationInfo> processCompilationMappingsFile(
+      String basePath, ZipFile jarFile) throws IOException {
+    List<PermutationInfo> permutationInfos = Lists.newArrayList();
+    ZipEntry compilationMappings = jarFile.getEntry(basePath + "/" + "compilation-mappings.txt");
+    try (BufferedReader compilationMappingsReader = new BufferedReader(
+        new InputStreamReader(jarFile.getInputStream(compilationMappings)))) {
+      PermutationInfo permInfo;
+      while ((permInfo = readPermutationInfo(compilationMappingsReader)) != null){
+        permutationInfos.add(permInfo);
+      }
+    }
+    return permutationInfos;
+  }
+
+  private final String hash;
+  private final Map<String, String> properties = Maps.newTreeMap();
+  private Stats stats;
+
+  private PermutationInfo(String hash) {
+    this.hash = hash;
+  }
+
+  /**
+   * Returns the base hash for the permutation.
+   */
+  public final String getHash() {
+    return hash;
+  }
+
+  /**
+   * Returns a permutation identifier based on the values of its properties.
+   */
+  public String getPermutationKey() {
+    return properties.toString();
+  }
+
+  /**
+   * Returns the value of property {@code propertyKey} in the permutation.
+   */
+  public Object getProperty(String propertyKey) {
+    return properties.get(propertyKey);
+  }
+
+  /**
+   * Returns the stats for the permutation.
+   */
+  public Stats getStats() {
+    return stats;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder result = new StringBuilder();
+    result.append(hash);
+    result.append(" ");
+    result.append(getPermutationKey());
+    result.append(" fragments ");
+    result.append(stats.getNumberofFragments());
+    result.append(" ");
+    result.append(" symbols ");
+    result.append(stats.getSymbolMapItems());
+    result.append(" ");
+    result.append(" symbolMap-size ");
+    result.append(stats.getSymbolMapSize());
+    result.append(" ");
+    result.append(String.format("total size= %s, initial size = %s, leftoversize = %s",
+        Util.formatSize(stats.getTotalCompressedSize()), Util.formatSize(stats.getInitialCompressedSize()),
+        Util.formatSize(stats.getLeftoverCompressedSize())));
+
+    result.append(String.format(" - total ucsize= %s, initial ucsize = %s, ucleftoversize = %s",
+        Util.formatSize(stats.getTotalUncompressedSize()), Util.formatSize(stats.getInitialUncompressedSize()),
+        Util.formatSize(stats.getLeftoverUncompressedSize())));
+    return result.toString();
+  }
+
+  private void addProperty(String name, String value) {
+    properties.put(name, value);
+  }
+
+  private void setStats(Stats stats) {
+    this.stats = stats;
+  }
+}
\ No newline at end of file
diff --git a/src/test/java/com/google/gwt/benchmark/artifacts/PermutationInfoTest.java b/src/test/java/com/google/gwt/benchmark/artifacts/PermutationInfoTest.java
new file mode 100644
index 0000000..92a2d02
--- /dev/null
+++ b/src/test/java/com/google/gwt/benchmark/artifacts/PermutationInfoTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * 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.google.gwt.benchmark.artifacts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Tests for {@link PermutationInfo}.
+ */
+public class PermutationInfoTest {
+
+  @Test
+  public void testReadCompileOutput() throws IOException {
+    List<PermutationInfo> permutationInfos = PermutationInfo.getAllPemutationInfo(
+        "src/test/resources/artifacts/showcase.jar",
+        "src/test/resources/artifacts/showcase-aux.jar",
+        "showcase");
+    assertEquals(25, permutationInfos.size());
+    PermutationInfo permutationInfo =
+        getByHash(permutationInfos, "0F002976225CC32E85BE1F39AEEE6C96");
+    assertNotNull(permutationInfo);
+
+    // This permutation has a gz initial fragment.
+    assertEquals(196974, permutationInfo.getStats().getInitialCompressedSize());
+    assertEquals(921215, permutationInfo.getStats().getInitialUncompressedSize());
+
+    // and a non .gz leftovers.
+    assertEquals(196527, permutationInfo.getStats().getLeftoverCompressedSize());
+    assertEquals(1016833, permutationInfo.getStats().getLeftoverUncompressedSize());
+
+    // and finally check the symbol maps.
+    assertEquals(15626, permutationInfo.getStats().getSymbolMapItems());
+    assertEquals(3866905, permutationInfo.getStats().getSymbolMapSize());
+  }
+
+  private static PermutationInfo getByHash(List<PermutationInfo> permutationInfos, String hash) {
+    for (PermutationInfo permutationInfo : permutationInfos) {
+      if (permutationInfo.getHash().equals(hash)) {
+        return permutationInfo;
+      }
+    }
+    return null;
+  }
+}
diff --git a/src/test/resources/artifacts/showcase-aux.jar b/src/test/resources/artifacts/showcase-aux.jar
new file mode 100644
index 0000000..367196a
--- /dev/null
+++ b/src/test/resources/artifacts/showcase-aux.jar
Binary files differ
diff --git a/src/test/resources/artifacts/showcase.jar b/src/test/resources/artifacts/showcase.jar
new file mode 100644
index 0000000..324c86d
--- /dev/null
+++ b/src/test/resources/artifacts/showcase.jar
Binary files differ