All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.instrumentation.reporter.ReportDecoder Maven / Gradle / Ivy

There is a newer version: 9.0.8
Show newest version
/*
 * 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 static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Comparator.comparing;

import com.google.common.base.Splitter;
import com.google.common.io.CharStreams;
import com.google.debugging.sourcemap.Base64VLQ;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.google.javascript.jscomp.instrumentation.reporter.proto.FileProfile;
import com.google.javascript.jscomp.instrumentation.reporter.proto.InstrumentationPoint;
import com.google.javascript.jscomp.instrumentation.reporter.proto.InstrumentationPointStats;
import com.google.javascript.jscomp.instrumentation.reporter.proto.InstrumentationPointStats.Presence;
import com.google.javascript.jscomp.instrumentation.reporter.proto.ReportProfile;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Class that helps to decode reports received from JS binaries that have been instrumented. Report
 * is usually a map of String => Long. Where key is encoded instrumentation point and value is
 * number of times that code was executed. To decode report beside report itself we need mapping
 * file that was produced when JS binary was compiled.
 *
 * 

Expected usage: {@code * Map mapping = ReportDecoder.parseMappingFromFile("mapping.txt"); * Map encodedReport = readFile("report.txt"); * ReportProfile report = ReportDecoder.decodeReport(mapping, encodedReport); * } * */ public final class ReportDecoder { private ReportDecoder() {} public static Map parseMappingFromFile(String mappingFilePath) throws IOException { String fileContent = CharStreams.toString(Files.newBufferedReader(Paths.get(mappingFilePath), UTF_8)); return ReportDecoder.parseMapping(fileContent); } /** * Parses the file found at location mappingFilePath and populates the properties of this class */ public static Map parseMapping(String mappingFileContent) { Map mapping = new LinkedHashMap<>(); List linesOfFile = Splitter.on('\n').omitEmptyStrings().splitToList(mappingFileContent); // Report should contain at least three lines checkState(linesOfFile.size() >= 3, "Malformed report %s", linesOfFile); 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>() {}.getType(); Gson gson = new GsonBuilder().disableHtmlEscaping().create(); List localFileNames = gson.fromJson(fileNamesAsJsonArray, stringListType); List functionNames = gson.fromJson(functionNamesAsJsonArray, stringListType); List typesAsStringList = gson.fromJson(typesAsJsonArray, stringListType); List types = typesAsStringList.stream() .map(InstrumentationPoint.Type::valueOf) .collect(Collectors.toList()); 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); InstrumentationPoint temp = InstrumentationPoint.newBuilder() .setFileName(localFileNames.get(Base64VLQ.decode(encodedDetailsAsCharIt))) .setFunctionName(functionNames.get(Base64VLQ.decode(encodedDetailsAsCharIt))) .setType(types.get(Base64VLQ.decode(encodedDetailsAsCharIt))) .setLineNumber(Base64VLQ.decode(encodedDetailsAsCharIt)) .setColumnNumber(Base64VLQ.decode(encodedDetailsAsCharIt)) .build(); mapping.putIfAbsent(id, temp); } return mapping; } public static ReportProfile decodeReport( Map mapping, Map frequencies) { // Decode each entry into InstrumentionPoint. Stream instrumentationPoints = mapping.entrySet().stream() .map( (entry) -> InstrumentationPointStats.newBuilder() .setPoint(entry.getValue()) .setTimesExecuted(frequencies.getOrDefault(entry.getKey(), 0L)) .build()); return ReportDecoder.createReportProfile(instrumentationPoints); } /** * This function builds a report for given JS binary analyzing its content. If an instrumentation * point is present in the JS binary - it will be marked as executed once. If the instrumentation * point was removed by the compiler as dead code - it will be marked as executed zero times. This * function doesn't give 100% guarantee for finding unused code but it provides good enough * approximation to be useful. * * @param mapping Parsed instrumentation mapping. Use {@link #parseMapping} function to create it. * @param fileContent Content of a JS binary compiled with production instrumentation enabled. * @param instrumentationArrayName Name of the array that was passed as * --production_instrumentation_array_name flag when compiling JS binary. * @return Report containing all instrumentation points. If a point times_executed is zero - it * means the function or branch was removed as dead code. If it's one - it's present in the JS * binary. */ public static ReportProfile createProfileOfStaticallyUsedCode( Map mapping, String fileContent, String instrumentationArrayName) { Matcher matcher = Pattern.compile(instrumentationArrayName + "\\.push\\(['\"](.*?)['\"]\\)") .matcher(fileContent); final HashSet encodedPoints = new HashSet<>(); while (matcher.find()) { encodedPoints.add(matcher.group(1)); } Stream instrumentationPoints = mapping.entrySet().stream() .map( (entry) -> InstrumentationPointStats.newBuilder() .setPoint(entry.getValue()) .setTimesExecuted(0) .setPointPresence( encodedPoints.contains(entry.getKey()) ? Presence.PRESENT : Presence.STATICALLY_REMOVED) .build()); return ReportDecoder.createReportProfile(instrumentationPoints); } /** * Given list of profiles merges them, summing up execution counts for instrumentation points used * across multiple profiles. Be aware that instrumentation points are compared as equality so * profiles should come from JS binaries compiled with at the same commit, otherwise changes in * source code will cause differences in some instrumentation points. */ public static ReportProfile mergeProfiles(List profiles) { Map frequencies = new HashMap<>(); Map presences = new HashMap<>(); for (ReportProfile profile : profiles) { for (FileProfile fileProfile : profile.getFileProfileList()) { for (InstrumentationPointStats pointStats : fileProfile.getInstrumentationPointsStatsList()) { frequencies.compute( pointStats.getPoint(), (p, existing) -> (existing == null ? 0 : existing) + pointStats.getTimesExecuted()); // Logic of merging presence field. // 1. PRESENT overrides STATICALLY_REMOVED and UNKNOWN. // 2. STATICALLY_REMOVED overrides UNKNOWN. presences.compute( pointStats.getPoint(), (p, existing) -> (pointStats.getPointPresence() == Presence.PRESENT || existing == Presence.PRESENCE_UNKNOWN || existing == null) ? pointStats.getPointPresence() : existing); } } } Stream finalPoints = frequencies.entrySet().stream() .map( (Map.Entry entry) -> InstrumentationPointStats.newBuilder() .setPoint(entry.getKey()) .setTimesExecuted(entry.getValue()) .setPointPresence(presences.get(entry.getKey())) .build()); return ReportDecoder.createReportProfile(finalPoints); } /** * Groups instrumentation points by files and convert them to FileProfile object, one per file and * return as ReportProfile. */ private static ReportProfile createReportProfile( Stream instrumentationPoints) { List fileProfiles = instrumentationPoints .sorted( comparing( (InstrumentationPointStats pointStats) -> pointStats.getPoint().getFileName()) .thenComparingInt( (InstrumentationPointStats pointStats) -> pointStats.getPoint().getLineNumber())) .collect( Collectors.groupingBy( (InstrumentationPointStats pointStats) -> pointStats.getPoint().getFileName())) .entrySet() .stream() .map( (entry) -> FileProfile.newBuilder() .setFileName(entry.getKey()) .addAllInstrumentationPointsStats(entry.getValue()) .build()) .sorted(comparing(FileProfile::getFileName)) .collect(Collectors.toList()); return ReportProfile.newBuilder().addAllFileProfile(fileProfiles).build(); } /** * 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; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy