// Copyright 2016 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.pubsub.flic.output; import com.google.common.base.Ascii; import com.google.pubsub.flic.common.LatencyDistribution; import com.google.pubsub.flic.controllers.Client.ClientType; import com.google.pubsub.flic.controllers.Controller.LoadtestStats; import java.io.BufferedWriter; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.file.Files; import java.nio.file.Paths; import java.text.DecimalFormat; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** Outputs results of the load test as a GnuPlot. */ public class GnuPlot { private static final Logger log = LoggerFactory.getLogger(SheetsService.class); private static final String STYLE_OPTS = "set style line 1 lc rgb '#8b1a0e' pt 1 ps 1 lt 1 lw 2 # --- red\n" + "set style line 2 lc rgb '#5e9c36' pt 6 ps 1 lt 1 lw 2 # --- green\n" + "set style line 11 lc rgb '#808080' lt 1\n" + "set border 3 back ls 11\n" + "set tics nomirror\n" + "set style line 12 lc rgb '#808080' lt 0 lw 1\n" + "set grid back ls 12\n"; private static final String PLOT_TEMPLATE = "set term png\n" + "set autoscale\n" + "set logscale y 2\n" + "unset label\n" + "set xtic auto\n" + "set ytic auto\n" + "set yr [:]\n"; private static final String THROUGHPUT_TEMPLATE = "set output '%1$s-throughput-%3$d.png'\n" + "set title '%2$s Throughput'\n" + "set xlabel 'Cores'\n" + "set ylabel 'Throughput (MB/s)'\n" + "set xr [0.0:16.0]\n" + "plot "; private static final String LATENCIES_TEMPLATE = "set output '%1$s-latencies-%3$d.png'\n" + "set title '%2$s Latencies'\n" + "set xlabel 'Percentile'\n" + "set ylabel 'Latency (ms)'\n" + "set xr [0.0:100.0]\n" + "plot "; private static final String LATENCIES_PLOT_COMMAND = "'%1$s_latency.dat' using 1:2 title '%1$s' with linespoints"; private static final String THROUGHPUT_PLOT_COMMAND = "'%1$s_throughput.dat' using 1:2 title '%1$s' with linespoints"; private static String buildLatenciesPlot(String prefix, Stream<ClientType> types) { return STYLE_OPTS + PLOT_TEMPLATE + String.format( LATENCIES_TEMPLATE, Ascii.toLowerCase(prefix), prefix, System.currentTimeMillis()) + String.join( ", ", types .map(type -> String.format(LATENCIES_PLOT_COMMAND, type.toString())) .collect(Collectors.toList())); } private static String buildThroughputPlot(String prefix, Stream<ClientType> types) { return STYLE_OPTS + PLOT_TEMPLATE + String.format( THROUGHPUT_TEMPLATE, Ascii.toLowerCase(prefix), prefix, System.currentTimeMillis()) + String.join( ", ", types .map(type -> String.format(THROUGHPUT_PLOT_COMMAND, type.toString())) .collect(Collectors.toList())); } private static String buildLatenciesDat(LoadtestStats stats) { StringBuilder dat = new StringBuilder(); for (int percentile = 5; percentile < 100; percentile += 5) { dat.append(percentile) .append(" ") .append(LatencyDistribution.getNthPercentileMidpoint(stats.bucketValues, percentile)) .append("\n"); } dat.append("99 ") .append(LatencyDistribution.getNthPercentileMidpoint(stats.bucketValues, 99)) .append("\n" + "99.9 ") .append(LatencyDistribution.getNthPercentileMidpoint(stats.bucketValues, 99.9)) .append("\n"); return dat.toString(); } public static void plot(Map<ClientType, LoadtestStats> statsMap) { statsMap.forEach( (type, stats) -> { try (Writer writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(type + "_latency.dat"), "utf-8"))) { writer.write(buildLatenciesDat(stats)); } catch (IOException e) { log.error("Error writing latencies.plot.", e); } }); try (Writer writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream("publish_latencies.plot"), "utf-8"))) { writer.write( buildLatenciesPlot( "Publish", statsMap.keySet().stream().filter(ClientType::isPublisher))); } catch (IOException e) { log.error("Error writing publish_latencies.plot.", e); } try (Writer writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream("end_to_end_latencies.plot"), "utf-8"))) { writer.write( buildLatenciesPlot( "End-to-End", statsMap.keySet().stream().filter(c -> !c.isPublisher()))); } catch (IOException e) { log.error("Error writing end_to_end_latencies.plot.", e); } Runtime runtime = Runtime.getRuntime(); try { runtime.exec(new String[] {"gnuplot", "publish_latencies.plot"}).waitFor(); runtime.exec(new String[] {"gnuplot", "end_to_end_latencies.plot"}).waitFor(); statsMap.keySet().forEach(type -> { try { Files.deleteIfExists(Paths.get(type + "_latency.dat")); } catch (IOException e) { // File must not have been created successfully } }); Files.deleteIfExists(Paths.get("publish_latencies.plot")); Files.deleteIfExists(Paths.get("end_to_end_latencies.plot")); } catch (Exception e) { log.error("Error running gnuplot.", e); } } private final Map<ClientType, Map<Integer, Double>> coreStats; public GnuPlot() { coreStats = new HashMap<>(); } public void addCoresResult(Integer numCores, Map<ClientType, LoadtestStats> statsMap) { statsMap.forEach((type, stats) -> { coreStats.putIfAbsent(type, new HashMap<>()); coreStats.get(type).put(numCores, stats.getThroughput()); }); } private String buildThroughputDat(Map<Integer, Double> throughputMap) { StringBuilder dat = new StringBuilder(); DecimalFormat decimalFormat = new DecimalFormat("#.##"); for (int cores = 1; cores <= 16; cores *= 2) { try { dat.append(cores) .append(" ") .append(decimalFormat.format(throughputMap.get(cores))) .append("\n"); } catch (Exception e) { log.error("There was a problem getting the results from one of the tests.", e); } } return dat.toString(); } public void plotStatsPerCore() { coreStats.forEach( (type, throughputMap) -> { try (Writer writer = new BufferedWriter( new OutputStreamWriter( new FileOutputStream(type + "_throughput.dat"), "utf-8"))) { writer.write(buildThroughputDat(throughputMap)); } catch (IOException e) { log.error("Error writing throughput data.", e); } }); try (Writer writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream("publish_throughput.plot"), "utf-8"))) { writer.write( buildThroughputPlot( "Publish", coreStats.keySet().stream().filter(ClientType::isPublisher))); } catch (IOException e) { log.error("Error writing publish_throughput.plot.", e); } try (Writer writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream("subscribe_throughput.plot"), "utf-8"))) { writer.write( buildThroughputPlot( "Subscribe", coreStats.keySet().stream().filter(c -> !c.isPublisher()))); } catch (IOException e) { log.error("Error writing subscribe_throughput.plot.", e); } Runtime runtime = Runtime.getRuntime(); try { runtime.exec(new String[] {"gnuplot", "publish_throughput.plot"}).waitFor(); runtime.exec(new String[] {"gnuplot", "subscribe_throughput.plot"}).waitFor(); coreStats.keySet().forEach(type -> { try { Files.deleteIfExists(Paths.get(type + "_throughput.dat")); } catch (IOException e) { // File must not have been created successfully } }); Files.deleteIfExists(Paths.get("publish_throughput.plot")); Files.deleteIfExists(Paths.get("subscribe_throughput.plot")); } catch (Exception e) { log.error("Error running gnuplot.", e); } } }