/* * Copyright 2016-present Facebook, 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.facebook.buck.util.trace; import com.facebook.buck.io.ProjectFilesystem; import com.google.common.base.Preconditions; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.file.Path; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Optional; import java.util.Set; /** * Event-driven parser for <a * href="https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview"> * Chrome traces</a>. */ public class ChromeTraceParser { /** Extracts data of interest if it finds a Chrome trace event of the type it is looking for. */ public interface ChromeTraceEventMatcher<T> { /** * Tests the specified event to see if it is a match. Its name has already been extracted, for * convenience. If it is a match, it should return the data of interest in the return value. If * not, then it should return {@link Optional#empty()}. */ Optional<T> test(JsonObject event, String name); } /** * Tries to extract the command that was used to trigger the invocation of Buck that generated the * trace. If found, it returns the command as an opaque string. */ public static final ChromeTraceEventMatcher<String> COMMAND = (json, name) -> { JsonElement argsEl = json.get("args"); if (argsEl == null || !argsEl.isJsonObject() || argsEl.getAsJsonObject().get("command_args") == null || !argsEl.getAsJsonObject().get("command_args").isJsonPrimitive()) { return Optional.empty(); } String commandArgs = argsEl.getAsJsonObject().get("command_args").getAsString(); String command = "buck " + name + (commandArgs.isEmpty() ? "" : " " + commandArgs); return Optional.of(command); }; private final ProjectFilesystem projectFilesystem; public ChromeTraceParser(ProjectFilesystem projectFilesystem) { this.projectFilesystem = projectFilesystem; } /** * Parses a Chrome trace and stops parsing once all of the specified matchers have been satisfied. * This method parses only one Chrome trace event at a time, which avoids loading the entire trace * into memory. * * @param pathToTrace is a relative path [to the ProjectFilesystem] to a Chrome trace in the "JSON * Array Format." * @param chromeTraceEventMatchers set of matchers this invocation of {@code parse()} is trying to * satisfy. Once a matcher finds a match, it will not consider any other events in the trace. * @return a {@code Map} where every matcher that found a match will have an entry whose key is * the matcher and whose value is the one returned by {@link * ChromeTraceEventMatcher#test(JsonObject, String)} without the {@link Optional} wrapper. */ public Map<ChromeTraceEventMatcher<?>, Object> parse( Path pathToTrace, Set<ChromeTraceEventMatcher<?>> chromeTraceEventMatchers) throws IOException { Set<ChromeTraceEventMatcher<?>> unmatchedMatchers = new HashSet<>(chromeTraceEventMatchers); Preconditions.checkArgument(!unmatchedMatchers.isEmpty(), "Must specify at least one matcher"); Map<ChromeTraceEventMatcher<?>, Object> results = new HashMap<>(); try (InputStream input = projectFilesystem.newFileInputStream(pathToTrace); JsonReader jsonReader = new JsonReader(new InputStreamReader(input))) { jsonReader.beginArray(); Gson gson = new Gson(); featureSearch: while (true) { // If END_ARRAY is the next token, then there are no more elements in the array. if (jsonReader.peek().equals(JsonToken.END_ARRAY)) { break; } // Verify and extract the name property before invoking any of the matchers. JsonElement eventEl = gson.fromJson(jsonReader, JsonElement.class); JsonObject event = eventEl.getAsJsonObject(); JsonElement nameEl = event.get("name"); if (nameEl == null || !nameEl.isJsonPrimitive()) { continue; } String name = nameEl.getAsString(); // Prefer Iterator to Iterable+foreach so we can use remove(). for (Iterator<ChromeTraceEventMatcher<?>> iter = unmatchedMatchers.iterator(); iter.hasNext(); ) { ChromeTraceEventMatcher<?> chromeTraceEventMatcher = iter.next(); Optional<?> result = chromeTraceEventMatcher.test(event, name); if (result.isPresent()) { iter.remove(); results.put(chromeTraceEventMatcher, result.get()); if (unmatchedMatchers.isEmpty()) { break featureSearch; } } } } } // We could throw if !unmatchedMatchers.isEmpty(), but that might be overbearing. return results; } /** * Designed for use with the result of {@link ChromeTraceParser#parse(Path, Set)}. Helper function * to avoid some distasteful casting logic. */ @SuppressWarnings("unchecked") public static <T> Optional<T> getResultForMatcher( ChromeTraceEventMatcher<T> matcher, Map<ChromeTraceEventMatcher<?>, Object> results) { T result = (T) results.get(matcher); return Optional.ofNullable(result); } }