From 0da2b8003ed8e42b3bc407de101d1173be87da6f Mon Sep 17 00:00:00 2001 From: twerth Date: Mon, 13 Aug 2018 11:05:05 -0700 Subject: Add experimental tool to turn Bazel Json profiles into graphs. RELNOTES: None PiperOrigin-RevId: 208509424 --- .../com/google/devtools/build/lib/profiler/BUILD | 13 ++ .../build/lib/profiler/grapher/ProfileGrapher.java | 145 +++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 src/main/java/com/google/devtools/build/lib/profiler/grapher/ProfileGrapher.java diff --git a/src/main/java/com/google/devtools/build/lib/profiler/BUILD b/src/main/java/com/google/devtools/build/lib/profiler/BUILD index 85c506dceb..bf703c4d4d 100644 --- a/src/main/java/com/google/devtools/build/lib/profiler/BUILD +++ b/src/main/java/com/google/devtools/build/lib/profiler/BUILD @@ -43,3 +43,16 @@ java_library( "//third_party:jsr305", ], ) + +java_binary( + name = "profiler-grapher", + srcs = glob([ + "grapher/*.java", + ]), + main_class = "com.google.devtools.build.lib.profiler.grapher.ProfileGrapher", + deps = [ + ":profiler", + "//third_party:gson", + "//third_party:guava", + ], +) diff --git a/src/main/java/com/google/devtools/build/lib/profiler/grapher/ProfileGrapher.java b/src/main/java/com/google/devtools/build/lib/profiler/grapher/ProfileGrapher.java new file mode 100644 index 0000000000..6f77404c21 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/profiler/grapher/ProfileGrapher.java @@ -0,0 +1,145 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// 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.devtools.build.lib.profiler.grapher; + +import com.google.common.collect.ImmutableList; +import com.google.devtools.build.lib.profiler.CpuUsageTimeSeries; +import com.google.gson.stream.JsonReader; +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +/** + * An experimental tool to turn Json profiles into graphs. Do not depend on the continuing existence + * of this tool. + * + *

Run this tool like so: bazel run \ + * //src/main/java/com/google/devtools/build/lib/profiler:profiler-grapher \ + * -- /path/to/command.profile > /tmp/tmp.csv + * + *

Plot the resulting CSV with gnuplot like so: gnuplot -p -e \ + * set datafile sep ','; plot for [col=1:3] '/tmp/tmp.csv' using col lw 2 with lines \ + * title columnheader" + */ +public class ProfileGrapher { + public static final long DEFAULT_BUCKET_SIZE_MILLIS = 1000; + + public static void main(String[] args) throws IOException { + if (args.length != 1) { + System.err.println("Requires filename of a Bazel profile in Chrome event trace format."); + System.exit(1); + } + String filename = args[0]; + + // TODO(twerth): Make it possible to select the set of profiler task descriptions on the command + // line. + ImmutableList profilerTaskDescriptions = + ImmutableList.of( + "Remote execution process wall time", + "action processing", + "Remote execution file fetching"); + Map seriesMap = new LinkedHashMap<>(); + for (String profilerTaskDescription : profilerTaskDescriptions) { + seriesMap.put(profilerTaskDescription, new CpuUsageTimeSeries(0, DEFAULT_BUCKET_SIZE_MILLIS)); + } + + long maxEndTime = 0; + // TODO(twerth): Support gzip compressed profiles. + try (JsonReader reader = + new JsonReader( + new BufferedReader( + new InputStreamReader(new FileInputStream(filename), StandardCharsets.UTF_8)))) { + reader.beginArray(); + while (reader.hasNext()) { + reader.beginObject(); + Map data = new HashMap<>(); + while (reader.hasNext()) { + String name = reader.nextName(); + Object value; + switch (reader.peek()) { + case BOOLEAN: + value = reader.nextBoolean(); + break; + case NUMBER: + value = reader.nextDouble(); + break; + case STRING: + value = reader.nextString(); + break; + default: + reader.skipValue(); + continue; + } + data.put(name, value); + } + Object cat = data.get("cat"); + CpuUsageTimeSeries series = seriesMap.get(cat); + if (series != null) { + Long endTimeMillis = decodeAndAdd(series, data); + if (endTimeMillis != null) { + maxEndTime = Math.max(maxEndTime, endTimeMillis); + } + } + reader.endObject(); + } + reader.endArray(); + } + + // Instead of generating a CSV here, we could generate Json and use the Google Charts API to + // generate interactive web graphs (https://developers.google.com/chart/). + int len = (int) (maxEndTime / DEFAULT_BUCKET_SIZE_MILLIS) + 1; + double[][] numbers = new double[seriesMap.size()][]; + List> allSeries = new ArrayList<>(seriesMap.entrySet()); + // Write the titles in the first line of the CSV + StringJoiner stringJoiner = new StringJoiner(","); + for (int i = 0; i < numbers.length; i++) { + stringJoiner.add(allSeries.get(i).getKey()); + numbers[i] = allSeries.get(i).getValue().toDoubleArray(len); + } + System.out.println(stringJoiner.toString()); + + for (int i = 0; i < numbers[0].length; i++) { + stringJoiner = new StringJoiner(","); + for (int j = 0; j < numbers.length; j++) { + stringJoiner.add(String.valueOf(numbers[j][i])); + } + System.out.println(stringJoiner.toString()); + } + } + + /** + * Decodes the start time and duration from the data, adds it to the given series and returns the + * end time in milliseconds if it was possible to decode the data, otherwise null. + */ + private static Long decodeAndAdd(CpuUsageTimeSeries series, Map data) { + Double ts = (Double) data.get("ts"); + Double dur = (Double) data.get("dur"); + if (ts == null || dur == null) { + return null; + } + long durationMillis = Math.round(dur.doubleValue() / 1000); + long startTimeMillis = Math.round(ts.doubleValue() / 1000); + long endTimeMillis = startTimeMillis + durationMillis; + series.addRange(startTimeMillis, endTimeMillis); + return endTimeMillis; + } +} -- cgit v1.2.3