Repository: google/closure-compiler
Pull request #3688 (2020-09-15 17:22:23)

Adding Production Instrumentation Reporter tool to Closure compiler
This PR contains the code for the production instrumentation reporter tool. The code was reviewed in a separate PR on a different github repo: https://github.com/patrick-sko/instrumentation-reporter/pull/2.

The minor changes between that PR and this one is I made a change to the unit test. I shrunk the size of the golden files by a lot to be more manageable. 

In addition to the unit test changed, I also modified the BUILD.bazel to create a separate Jar file for the reporter. 

This PR is the final step of the following feature proposal: https://docs.google.com/document/d/1uvEazsDdX0w4fRhREESvr_w7YX7JYNe27e5bD-CBeGM/edit?usp=sharing



Patch:
diff --git a/BUILD.bazel b/BUILD.bazel
index c6460cab3..7842b46d5 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -87,6 +87,12 @@ java_binary(
     runtime_deps = [":compiler_lib"],
 )
 
+java_binary(
+    name = "instrumentationReporter",
+    main_class = "com.google.javascript.jscomp.instrumentation.reporter.ProductionInstrumentationReporter",
+    runtime_deps = [":compiler_lib"],
+)
+
 java_library(
     name = "externs",
     resources = [":externs_zip"],
diff --git a/src/com/google/javascript/jscomp/instrumentation/reporter/InstrumentationMapping.java b/src/com/google/javascript/jscomp/instrumentation/reporter/InstrumentationMapping.java
new file mode 100644
index 000000000..7d89db508
--- /dev/null
+++ b/src/com/google/javascript/jscomp/instrumentation/reporter/InstrumentationMapping.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2020 The Closure Compiler Authors.
+ *
+ * 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.javascript.jscomp.instrumentation.reporter;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.gson.GsonBuilder;
+import com.google.javascript.jscomp.instrumentation.reporter.ProductionInstrumentationReporter.InstrumentationType;
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.debugging.sourcemap.Base64VLQ;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+/**
+ * The class contains the data which is provided by the Production Instrumentation pass in the
+ * closure compiler. It will read the data from the file name created with the
+ * --instrument_mapping_report and is required to follow the format used by Closure Compiler class
+ * VariableMap.
+ */
+@GwtIncompatible
+final class InstrumentationMapping {
+
+  final ImmutableList<String> fileNames;
+  final ImmutableMap<String, Location> parameterMapping;
+
+  private InstrumentationMapping(ImmutableList<String> fileNames,  ImmutableMap<String, Location> parameterMapping) {
+    this.fileNames = fileNames;
+    this.parameterMapping = parameterMapping;
+  }
+
+
+  /**
+   * Parses the file found at location mappingFilePath and populates the properties of this class
+   */
+  public static InstrumentationMapping parse(String mappingFilePath) throws IOException {
+
+    Map<String, Location> localParameterMapping = new HashMap<>();
+
+    String instrumentationMappingFile = ProductionInstrumentationReporter.readFile(mappingFilePath);
+    List<String> linesOfFile = Splitter.on('\n').omitEmptyStrings()
+        .splitToList(instrumentationMappingFile);
+
+    // Report should contain at least three lines
+    checkState(linesOfFile.size() >= 3);
+
+    String fileNamesLine = linesOfFile.get(0).trim();
+    String functionNamesLine = linesOfFile.get(1).trim();
+    String typesLine = linesOfFile.get(2).trim();
+
+    checkState(fileNamesLine.startsWith("FileNames:"));
+    checkState(functionNamesLine.startsWith("FunctionNames:"));
+    checkState(typesLine.startsWith("Types:"));
+
+    String fileNamesAsJsonArray = fileNamesLine.substring(fileNamesLine.indexOf(":") + 1);
+    String functionNamesAsJsonArray = functionNamesLine
+        .substring(functionNamesLine.indexOf(":") + 1);
+    String typesAsJsonArray = typesLine.substring(typesLine.indexOf(":") + 1);
+
+    Type stringListType = new TypeToken<List<String>>() {
+    }.getType();
+
+    Gson gson = new GsonBuilder().disableHtmlEscaping().create();
+    List<String> localFileNames = gson.fromJson(fileNamesAsJsonArray, stringListType);
+    List<String> functionNames = gson.fromJson(functionNamesAsJsonArray, stringListType);
+
+    List<String> typesAsStringList = gson.fromJson(typesAsJsonArray, stringListType);
+    List<InstrumentationType> types = InstrumentationType.convertFromStringList(typesAsStringList);
+
+    for (int i = 3; i < linesOfFile.size(); ++i) {
+      String lineItem = linesOfFile.get(i);
+      String id = lineItem.substring(0, lineItem.indexOf(':'));
+      String encodedDetails = lineItem.substring(lineItem.indexOf(':') + 1);
+
+      StringCharIterator encodedDetailsAsCharIt = new StringCharIterator(encodedDetails);
+
+      Location temp = Location.create(
+          localFileNames.get(Base64VLQ.decode(encodedDetailsAsCharIt)),
+          functionNames.get(Base64VLQ.decode(encodedDetailsAsCharIt)),
+          types.get(Base64VLQ.decode(encodedDetailsAsCharIt)),
+          Base64VLQ.decode(encodedDetailsAsCharIt),
+          Base64VLQ.decode(encodedDetailsAsCharIt)
+      );
+      localParameterMapping.putIfAbsent(id, temp);
+    }
+
+    return new InstrumentationMapping(ImmutableList.copyOf(localFileNames),
+        ImmutableMap.copyOf(localParameterMapping));
+
+  }
+
+  public String getFileName(String id) {
+    return parameterMapping.get(id).fileName();
+  }
+
+  public String getFunctionName(String id) {
+    return parameterMapping.get(id).functionName();
+  }
+
+  public InstrumentationType getType(String id) {
+    return parameterMapping.get(id).type();
+  }
+
+  public int getLineNo(String id) {
+    return parameterMapping.get(id).lineNo();
+  }
+
+  public int getColNo(String id) {
+    return parameterMapping.get(id).colNo();
+  }
+
+  /**
+   * This function will return a list of unique parameters which match the criteria specified by the
+   * predicate. As an example, it can return a list of all unique parameters which match a specific
+   * file name.
+   */
+  public List<String> getAllMatchingValues(Predicate<String> comparisonPredicate) {
+    List<String> result = new ArrayList<>();
+    for (String key : parameterMapping.keySet()) {
+      if (comparisonPredicate.test(key)) {
+        result.add(key);
+      }
+    }
+    return result;
+  }
+
+  @AutoValue
+  abstract static class Location {
+
+    static Location create(String a, String b, InstrumentationType c, int d, int e) {
+      return new AutoValue_InstrumentationMapping_Location(a, b, c, d, e);
+    }
+
+    abstract String fileName();
+
+    abstract String functionName();
+
+    abstract InstrumentationType type();
+
+    abstract int lineNo();
+
+    abstract int colNo();
+  }
+
+  /**
+   * A implementation of the Base64VLQ CharIterator used for decoding the mappings encoded in the
+   * JSON string.
+   */
+  private static class StringCharIterator implements Base64VLQ.CharIterator {
+
+    final String content;
+    final int length;
+    int current = 0;
+
+    StringCharIterator(String content) {
+      this.content = content;
+      this.length = content.length();
+    }
+
+    @Override
+    public char next() {
+      return content.charAt(current++);
+    }
+
+    @Override
+    public boolean hasNext() {
+      return current < length;
+    }
+  }
+
+}
diff --git a/src/com/google/javascript/jscomp/instrumentation/reporter/ProductionInstrumentationReporter.java b/src/com/google/javascript/jscomp/instrumentation/reporter/ProductionInstrumentationReporter.java
new file mode 100644
index 000000000..c091ce208
--- /dev/null
+++ b/src/com/google/javascript/jscomp/instrumentation/reporter/ProductionInstrumentationReporter.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2020 The Closure Compiler Authors.
+ *
+ * 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.javascript.jscomp.instrumentation.reporter;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.javascript.jscomp.instrumentation.reporter.ProfilingReport.ProfilingData;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.CharStreams;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.Option;
+
+
+/**
+ * This class will read a file that contains the instrumentation mapping generated by the compiler
+ * production instrumentation pass, and also a list of other files which are the reports sent by the
+ * instrumented production code. It will then take these inputs and generate a single JSON which
+ * provides a detailed breakdown of each instrumentation point.
+ */
+@GwtIncompatible
+final class ProductionInstrumentationReporter {
+
+  @Option(
+      name = "--mapping_file",
+      usage = "The file name of the mapping generated by the production instrumentation pass.",
+      required = true)
+  private String instrumentationMappingLocation = "";
+
+  @Option(
+      name = "--reports_directory",
+      usage = "The folder/directory which contains all the reports created by the instrumented"
+          + " production code.",
+      required = true)
+  private String instrumentationReportsDirectory = "";
+
+  @Option(
+      name = "--result_output",
+      usage = "Use this flag to provide the name of the final report that will be generated by"
+          + "this reporter.",
+      required = true)
+  private String finalResultOutput = "";
+
+
+  public static void main(String[] args) {
+    try {
+      new ProductionInstrumentationReporter().doMain(args);
+    } catch (IOException e) {
+      System.err.println(e.getMessage());
+    }
+  }
+
+  /**
+   * This function reads a file at the given filePath and converts the contents into a string.
+   */
+  public static String readFile(String filePath) throws IOException {
+    return CharStreams.toString(new FileReader(filePath));
+  }
+
+  /**
+   * Reads all files found in folder and converts the contents of each file to a Map<String,
+   * ProfilingData> data structure where it is a mapping of the unique param value to the encoded
+   * values. The folder contains all the reports sent by the instrumented production code.
+   */
+  private ImmutableList<Map<String, ProfilingData>> getAllExecutionResults(File folder)
+      throws IOException {
+    List<Map<String, ProfilingData>> result = new ArrayList<>();
+
+    for (final File fileEntry : folder.listFiles()) {
+      String executionResult = readFile(fileEntry.getAbsolutePath());
+      Type type = new TypeToken<Map<String, ProfilingData>>() {
+      }.getType();
+      Map<String, ProfilingData> executedInstrumentationData = new Gson()
+          .fromJson(executionResult, type);
+
+      result.add(executedInstrumentationData);
+
+    }
+
+    return ImmutableList.copyOf(result);
+  }
+
+  /**
+   * Creates a file with the given fileName (including extension) with the contents of the file
+   * being provided by fileContents.
+   */
+  private void createFile(String fileName, String fileContents) throws IOException {
+
+    File fold = new File(fileName);
+    fold.delete();
+    File myObj = new File(fileName);
+    myObj.createNewFile();
+
+    FileWriter myWriter = new FileWriter(fileName);
+    myWriter.write(fileContents);
+    myWriter.close();
+  }
+
+  private void doMain(String[] args) throws IOException {
+
+    parseCmdLineArguments(args);
+
+    InstrumentationMapping instrumentationMapping = InstrumentationMapping
+        .parse(instrumentationMappingLocation);
+
+    File folder = new File(instrumentationReportsDirectory);
+
+    ImmutableList<Map<String, ProfilingData>> listOfExecutionResults = getAllExecutionResults(
+        folder);
+
+    ProfilingReport profilingReport = ProfilingReport.createProfilingReport(
+        instrumentationMapping, listOfExecutionResults);
+
+    Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
+    createFile(finalResultOutput, gson.toJson(profilingReport));
+
+  }
+
+  private void parseCmdLineArguments(String[] args) {
+    CmdLineParser parser = new CmdLineParser(this);
+    parser.setUsageWidth(80);
+    try {
+      parser.parseArgument(args);
+    } catch (CmdLineException e) {
+      System.err.println(e.getMessage());
+      return;
+    }
+  }
+
+  public enum InstrumentationType {
+    FUNCTION,
+    BRANCH,
+    BRANCH_DEFAULT;
+
+    public static List<InstrumentationType> convertFromStringList(List<String> typesAsString) {
+      return typesAsString.stream()
+          .map(InstrumentationType::valueOf)
+          .collect(Collectors.toList());
+    }
+  }
+
+}
diff --git a/src/com/google/javascript/jscomp/instrumentation/reporter/ProfilingReport.java b/src/com/google/javascript/jscomp/instrumentation/reporter/ProfilingReport.java
new file mode 100644
index 000000000..15e1f6d20
--- /dev/null
+++ b/src/com/google/javascript/jscomp/instrumentation/reporter/ProfilingReport.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2020 The Closure Compiler Authors.
+ *
+ * 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.javascript.jscomp.instrumentation.reporter;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.javascript.jscomp.instrumentation.reporter.ProductionInstrumentationReporter.InstrumentationType;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimaps;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+
+/**
+ * A class that maintains all information about the production instrumentation results which will be
+ * converted to a JSON.
+ */
+@GwtIncompatible
+final class ProfilingReport {
+
+  final int totalReportsParsed;
+  final float percentOfFunctionsExecuted;
+  final float percentOfBranchesExecuted;
+  final List<FileProfilingResult> result;
+
+  private ProfilingReport(int totalReportsParsed, List<FileProfilingResult> result) {
+    this.totalReportsParsed = totalReportsParsed;
+    this.result = result;
+
+    // Calculate the percentages for the profiling report
+    List<Map<String, List<ProfilingResult>>> listOfProfilingResultMaps = new ArrayList<>();
+    for (FileProfilingResult fileProfilingResult : result) {
+      listOfProfilingResultMaps.add(fileProfilingResult.profilingResultPerFunction());
+    }
+
+    this.percentOfFunctionsExecuted = calculatePercentOfInstrumentationExecuted(
+        listOfProfilingResultMaps, InstrumentationType.FUNCTION::equals);
+    this.percentOfBranchesExecuted = calculatePercentOfInstrumentationExecuted(
+        listOfProfilingResultMaps, (t) -> t.equals(InstrumentationType.BRANCH) || t
+            .equals(InstrumentationType.BRANCH_DEFAULT));
+  }
+
+  private static float roundFloat2Decimals(float num) {
+    return (Math.round(num * 10000) / (float) 100);
+  }
+
+  /**
+   * This function takes the instrumentationMapping and reports sent by the instrumented production
+   * code and creates a ProfilingReport with the aggregated information. It will average the
+   * ProfilingData provided by allInstrumentationReports and also calculate a percentage of how
+   * often a parameter is encountered.
+   *
+   * @param instrumentationMapping    The instrumentationMapping generated by the Production
+   *                                  Instrumentation pass
+   * @param allInstrumentationReports A list off all reports sent by the instrumented production
+   *                                  code
+   * @return A ProfilingReport which contains all the aggregated information from the
+   * instrumentationReports with values decoded using the instrumentationMapping.
+   */
+  public static ProfilingReport createProfilingReport(
+      InstrumentationMapping instrumentationMapping,
+      ImmutableList<Map<String, ProfilingData>> allInstrumentationReports) {
+
+    List<FileProfilingResult> result = new ArrayList<>();
+
+    // Iterate over all fileNames since that is what we are grouping by.
+    for (String fileName : instrumentationMapping.fileNames) {
+      FileProfilingResult fileProfilingResult = createFileProfilingResult(fileName,
+          instrumentationMapping, allInstrumentationReports);
+      result.add(fileProfilingResult);
+
+    }
+
+    return new ProfilingReport(allInstrumentationReports.size(), result);
+  }
+
+  private static FileProfilingResult createFileProfilingResult(String fileName,
+      InstrumentationMapping instrumentationMapping,
+      ImmutableList<Map<String, ProfilingData>> allInstrumentationReports) {
+
+    // Get all instrumentation parameters where the source file is fileName.
+    List<String> instrumentationPointsPerFile = instrumentationMapping.getAllMatchingValues((s) ->
+        fileName.equals(instrumentationMapping.getFileName(s))
+    );
+
+    ListMultimap<String, ProfilingResult> profilingResultPerFunction = ArrayListMultimap.create();
+
+    for (String id : instrumentationPointsPerFile) {
+      String functionName = instrumentationMapping.getFunctionName(id);
+
+      ProfilingResult profilingResult = createProfilingResult(id, instrumentationMapping,
+          allInstrumentationReports);
+
+      profilingResultPerFunction.put(functionName, profilingResult);
+    }
+
+    float percentOfFunctionsExecuted = calculatePercentOfInstrumentationExecuted(
+        Arrays.asList(Multimaps.asMap(profilingResultPerFunction)),
+        InstrumentationType.FUNCTION::equals);
+
+    float percentOfBranchesExecuted = calculatePercentOfInstrumentationExecuted(
+        Arrays.asList(Multimaps.asMap(profilingResultPerFunction)),
+        (t) -> t.equals(InstrumentationType.BRANCH) || t
+            .equals(InstrumentationType.BRANCH_DEFAULT));
+
+    return FileProfilingResult
+        .create(fileName, percentOfFunctionsExecuted, percentOfBranchesExecuted,
+            profilingResultPerFunction);
+  }
+
+  private static ProfilingResult createProfilingResult(String id,
+      InstrumentationMapping instrumentationMapping,
+      ImmutableList<Map<String, ProfilingData>> allInstrumentationReports) {
+
+    float totalInstrumentationPointsExecuted = 0;
+    int totalFrequency = 0;
+    int instrumentationPointsCounter = 0;
+
+    // For each id, iterate over allInstrumentationReports and check if the id is present.
+    // If it is, we will average the data, otherwise we will add to the average as if it is 0.
+    for (Map<String, ProfilingData> instrumentationData : allInstrumentationReports) {
+      ProfilingData profilingData = instrumentationData.get(id);
+      instrumentationPointsCounter++;
+      if (profilingData != null) {
+        // if executed, add 1 to executionAverage, and 0 otherwise.
+        totalFrequency += profilingData.frequency;
+        totalInstrumentationPointsExecuted++;
+      }
+    }
+
+    // Round the executedAverage to 2 decimal places for simplicity.
+    float executedAverage = roundFloat2Decimals(
+        totalInstrumentationPointsExecuted / instrumentationPointsCounter);
+
+    ProfilingData data = new ProfilingData(
+        Math.round((float) totalFrequency / instrumentationPointsCounter));
+
+    return ProfilingResult.create(instrumentationMapping, id, executedAverage, data);
+  }
+
+  /**
+   * Iterates over the result property and calculates what percent out of all functions and branches
+   * were executed to get aggregated instrumentation data.
+   */
+  private static float calculatePercentOfInstrumentationExecuted(
+      List<Map<String, List<ProfilingResult>>> result,
+      Predicate<InstrumentationType> instrumentationTypeCheck) {
+    int totalInstrumentationPoints = 0;
+    int totalInstrumentationPointsExecuted = 0;
+
+    List<ProfilingResult> allProfilingResults = result.stream()
+        .flatMap(fileProfilingResult -> fileProfilingResult.values().stream())
+        .flatMap(Collection::stream)
+        .collect(Collectors.toList());
+
+    for (ProfilingResult profilingResult : allProfilingResults) {
+      if (instrumentationTypeCheck.test(profilingResult.type())) {
+        totalInstrumentationPoints++;
+        if (profilingResult.executed() != 0) {
+          totalInstrumentationPointsExecuted++;
+        }
+      }
+    }
+    return totalInstrumentationPoints != 0 ? roundFloat2Decimals(
+        (float) totalInstrumentationPointsExecuted / totalInstrumentationPoints) : 100;
+  }
+
+  /**
+   * A class which groups profiling results by source file and contains a mapping of them to each
+   * function which is present in that source file. Used also for Json conversion.
+   */
+  @AutoValue
+  abstract static class FileProfilingResult {
+
+    static FileProfilingResult create(String fileName, float percentOfFunctionsExecuted,
+        float percentOfBranchesExecuted,
+        ListMultimap<String, ProfilingResult> profilingResultPerFunction) {
+      return new AutoValue_ProfilingReport_FileProfilingResult(fileName, percentOfFunctionsExecuted,
+          percentOfBranchesExecuted, Multimaps.asMap(profilingResultPerFunction));
+    }
+
+    abstract String fileName();
+
+    abstract float percentOfFunctionsExecuted();
+
+    abstract float percentOfBranchesExecuted();
+
+    /**
+     * A map of function names to a list of all associated instrumentation points associated with
+     * that function name. Any given function will typically have a FUNCTION instrumentation point
+     * and several BRANCH and BRANCH_DEFAULT instrumentation points. Note that this property cannot
+     * be a Multimap as it is not supported by GSON deserialization.
+     */
+    abstract Map<String, List<ProfilingResult>> profilingResultPerFunction();
+  }
+
+  /**
+   * This class contains a detailed breakdown of each instrumentation point. For any encoded param,
+   * this class will contain its details. This class is also used for Json serialization.
+   */
+  @AutoValue
+  abstract static class ProfilingResult {
+
+    static ProfilingResult create(InstrumentationMapping instrumentationMapping, String param,
+        float executed, ProfilingData data) {
+      return new AutoValue_ProfilingReport_ProfilingResult(param,
+          instrumentationMapping.getType(param), instrumentationMapping.getLineNo(param),
+          instrumentationMapping.getColNo(param), executed, data);
+    }
+
+    abstract String param();
+
+    abstract InstrumentationType type();
+
+    abstract int lineNo();
+
+    abstract int colNo();
+
+    /**
+     * The `executed` property will be a percent of total times the instrumentation point was called
+     * across all provided instrumentation reports. e.g if this.executed = 100, then every report
+     * would have this point present.
+     */
+    abstract float executed();
+
+    /**
+     * Similar to executed, ProfilingData takes the average of all ProfilingData's for this
+     * instrumentation point in all reports.
+     */
+    abstract ProfilingData data();
+
+  }
+
+
+  /**
+   * The instrumentation will send the report in a JSON format were the JSON is a dictionary of the
+   * encoded params to an object of the data collected. Initially this will just be the frequency.
+   * This class is a great candidate for AutoValue, however GSON deserialization does not seem to
+   * support it.
+   */
+  static class ProfilingData {
+
+    final int frequency;
+
+    public ProfilingData(int frequency) {
+      this.frequency = frequency;
+    }
+
+  }
+
+}


Test patch:
diff --git a/test/com/google/javascript/jscomp/instrumentation/reporter/ProductionInstrumentationReporterTest.java b/test/com/google/javascript/jscomp/instrumentation/reporter/ProductionInstrumentationReporterTest.java
new file mode 100644
index 000000000..f99c9d2f5
--- /dev/null
+++ b/test/com/google/javascript/jscomp/instrumentation/reporter/ProductionInstrumentationReporterTest.java
@@ -0,0 +1,67 @@
+package com.google.javascript.jscomp.instrumentation.reporter;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.google.common.base.Splitter;
+import com.google.common.io.CharStreams;
+import java.io.FileReader;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ProductionInstrumentationReporterTest {
+
+  @Rule
+  public TemporaryFolder folder = new TemporaryFolder();
+
+  private static String readFile(String filePath) throws IOException {
+    return CharStreams.toString(new FileReader(filePath));
+  }
+
+  @Test
+  public void testExpectedReportingResults() throws IOException {
+
+    Path tempFolderPath = folder.getRoot().toPath();
+    String filePath = tempFolderPath + "\\someFile.json";
+
+    String[] args = new String[6];
+    args[0] = "--mapping_file";
+    args[1] = "test/com/google/javascript/jscomp/testdata/ProductionInstrumentationReporterTest/instrumentationMapping.txt";
+    args[2] = "--reports_directory";
+    args[3] = "test/com/google/javascript/jscomp/testdata/ProductionInstrumentationReporterTest/reports";
+    args[4] = "--result_output";
+    args[5] = filePath;
+
+    ProductionInstrumentationReporter.main(args);
+
+    List<String> actualFileContents = Splitter.on('\n').omitEmptyStrings()
+        .splitToList(readFile(filePath));
+
+    List<String> expectedFileContents = Splitter.on('\n').omitEmptyStrings()
+        .splitToList(readFile("test/com/google/javascript/jscomp/testdata/ProductionInstrumentationReporterTest/expectedFinalResult.json"));
+
+    assertWithMessage("Number of lines between expected and actual do not match")
+        .that(actualFileContents.size())
+        .isEqualTo(expectedFileContents.size());
+
+    for (int i = 0; i < actualFileContents.size(); i++) {
+      assertWithMessage(
+          "Expected final JSON does not match expected at line no: "
+              + (i + 1)
+              + "\nExpected:\n"
+              + expectedFileContents.get(i)
+              + "\nFound:\n"
+              + actualFileContents.get(i)
+              + "\n")
+          .that(actualFileContents.get(i))
+          .isEqualTo(expectedFileContents.get(i));
+    }
+
+  }
+}
\ No newline at end of file
diff --git a/test/com/google/javascript/jscomp/testdata/ProductionInstrumentationReporterTest/expectedFinalResult.json b/test/com/google/javascript/jscomp/testdata/ProductionInstrumentationReporterTest/expectedFinalResult.json
new file mode 100644
index 000000000..c2f695b00
--- /dev/null
+++ b/test/com/google/javascript/jscomp/testdata/ProductionInstrumentationReporterTest/expectedFinalResult.json
@@ -0,0 +1,191 @@
+{
+  "totalReportsParsed": 1,
+  "percentOfFunctionsExecuted": 57.14,
+  "percentOfBranchesExecuted": 33.33,
+  "result": [
+    {
+      "fileName": "fib.js",
+      "percentOfFunctionsExecuted": 100.0,
+      "percentOfBranchesExecuted": 100.0,
+      "profilingResultPerFunction": {
+        "main": [
+          {
+            "param": "I",
+            "type": "FUNCTION",
+            "lineNo": 9,
+            "colNo": 0,
+            "executed": 100.0,
+            "data": {
+              "frequency": 1
+            }
+          }
+        ],
+        "fib": [
+          {
+            "param": "C",
+            "type": "BRANCH",
+            "lineNo": 2,
+            "colNo": 1,
+            "executed": 100.0,
+            "data": {
+              "frequency": 5
+            }
+          },
+          {
+            "param": "E",
+            "type": "BRANCH_DEFAULT",
+            "lineNo": 2,
+            "colNo": 1,
+            "executed": 100.0,
+            "data": {
+              "frequency": 4
+            }
+          },
+          {
+            "param": "G",
+            "type": "FUNCTION",
+            "lineNo": 1,
+            "colNo": 0,
+            "executed": 100.0,
+            "data": {
+              "frequency": 9
+            }
+          }
+        ]
+      }
+    },
+    {
+      "fileName": "InstrumentCode.js",
+      "percentOfFunctionsExecuted": 40.0,
+      "percentOfBranchesExecuted": 14.29,
+      "profilingResultPerFunction": {
+        "<Anonymous>": [
+          {
+            "param": "Q",
+            "type": "FUNCTION",
+            "lineNo": 30,
+            "colNo": 17,
+            "executed": 0.0,
+            "data": {
+              "frequency": 0
+            }
+          },
+          {
+            "param": "Y",
+            "type": "FUNCTION",
+            "lineNo": 50,
+            "colNo": 19,
+            "executed": 0.0,
+            "data": {
+              "frequency": 0
+            }
+          },
+          {
+            "param": "gB",
+            "type": "FUNCTION",
+            "lineNo": 57,
+            "colNo": 19,
+            "executed": 100.0,
+            "data": {
+              "frequency": 1
+            }
+          }
+        ],
+        "module$contents$instrument$code_instrumentCode": [
+          {
+            "param": "K",
+            "type": "BRANCH",
+            "lineNo": 20,
+            "colNo": 4,
+            "executed": 0.0,
+            "data": {
+              "frequency": 0
+            }
+          },
+          {
+            "param": "M",
+            "type": "BRANCH_DEFAULT",
+            "lineNo": 20,
+            "colNo": 4,
+            "executed": 0.0,
+            "data": {
+              "frequency": 0
+            }
+          },
+          {
+            "param": "O",
+            "type": "BRANCH",
+            "lineNo": 18,
+            "colNo": 2,
+            "executed": 0.0,
+            "data": {
+              "frequency": 0
+            }
+          },
+          {
+            "param": "S",
+            "type": "BRANCH",
+            "lineNo": 15,
+            "colNo": 1,
+            "executed": 100.0,
+            "data": {
+              "frequency": 1
+            }
+          },
+          {
+            "param": "U",
+            "type": "BRANCH_DEFAULT",
+            "lineNo": 15,
+            "colNo": 1,
+            "executed": 0.0,
+            "data": {
+              "frequency": 0
+            }
+          },
+          {
+            "param": "W",
+            "type": "FUNCTION",
+            "lineNo": 13,
+            "colNo": 2,
+            "executed": 100.0,
+            "data": {
+              "frequency": 1
+            }
+          }
+        ],
+        "module$contents$instrument$code_download": [
+          {
+            "param": "a",
+            "type": "BRANCH",
+            "lineNo": 41,
+            "colNo": 4,
+            "executed": 0.0,
+            "data": {
+              "frequency": 0
+            }
+          },
+          {
+            "param": "c",
+            "type": "BRANCH_DEFAULT",
+            "lineNo": 41,
+            "colNo": 4,
+            "executed": 0.0,
+            "data": {
+              "frequency": 0
+            }
+          },
+          {
+            "param": "e",
+            "type": "FUNCTION",
+            "lineNo": 39,
+            "colNo": 2,
+            "executed": 0.0,
+            "data": {
+              "frequency": 0
+            }
+          }
+        ]
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/test/com/google/javascript/jscomp/testdata/ProductionInstrumentationReporterTest/instrumentationMapping.txt b/test/com/google/javascript/jscomp/testdata/ProductionInstrumentationReporterTest/instrumentationMapping.txt
new file mode 100644
index 000000000..636a72d85
--- /dev/null
+++ b/test/com/google/javascript/jscomp/testdata/ProductionInstrumentationReporterTest/instrumentationMapping.txt
@@ -0,0 +1,19 @@
+ FileNames:["fib.js","InstrumentCode.js"]
+ FunctionNames:["fib","main","module$contents$instrument$code_instrumentCode","<Anonymous>","module$contents$instrument$code_download"]
+ Types:["BRANCH","BRANCH_DEFAULT","FUNCTION"]
+C:AAAEC
+E:AACEC
+G:AAECA
+I:ACESA
+K:CEAoBI
+M:CECoBI
+O:CEAkBE
+Q:CGE8BiB
+S:CEAeC
+U:CECeC
+W:CEEaE
+Y:CGEkDmB
+a:CIAyCI
+c:CICyCI
+e:CIEuCE
+gB:CGEyDmB
diff --git a/test/com/google/javascript/jscomp/testdata/ProductionInstrumentationReporterTest/reports/executionResult.json b/test/com/google/javascript/jscomp/testdata/ProductionInstrumentationReporterTest/reports/executionResult.json
new file mode 100644
index 000000000..bd5ab7d28
--- /dev/null
+++ b/test/com/google/javascript/jscomp/testdata/ProductionInstrumentationReporterTest/reports/executionResult.json
@@ -0,0 +1,23 @@
+{
+   "I":{
+      "frequency":1
+   },
+   "G":{
+      "frequency":9
+   },
+   "E":{
+      "frequency":4
+   },
+   "C":{
+      "frequency":5
+   },
+   "gB":{
+      "frequency":1
+   },
+   "W":{
+      "frequency":1
+   },
+   "S":{
+      "frequency":1
+   }
+}
\ No newline at end of file