make it possible to draw the legend outside of the plot area

This commit is contained in:
2017-09-30 17:51:33 +02:00
parent d4fd25dc4c
commit 386f211377
9 changed files with 1460 additions and 1402 deletions

View File

@@ -1,87 +1,92 @@
package org.lucares.recommind.logs;
import java.util.Collection;
import org.lucares.pdb.plot.api.AggreateInternal;
import org.lucares.pdb.plot.api.AxisScale;
public class GnuplotFileGenerator {
public String generate(final GnuplotSettings settings, final Collection<DataSeries> dataSeries) {
final StringBuilder result = new StringBuilder();
appendfln(result, "set terminal %s noenhanced size %d,%d", settings.getTerminal(), settings.getWidth(),
settings.getHeight());
appendfln(result, "set datafile separator \"%s\"", settings.getDatafileSeparator());
int count = 1;
if (settings.getAggregate() != AggreateInternal.NONE)
{
for (final DataSeries dataSerie : dataSeries) {
appendfln(result, "stats '%s' using 2 prefix \"A%d\"", dataSerie.getDataFile(),count);
count++;
}
}
appendfln(result, "set timefmt '%s'", settings.getTimefmt());
appendfln(result, "set xdata time");
appendfln(result, "set format x \"%s\"", settings.getFormatX());
appendfln(result, "set xlabel \"%s\"", settings.getXlabel());
appendfln(result, "set xtics rotate by %d", settings.getRotateXAxisLabel());
appendfln(result, "set xrange [\"%s\":\"%s\"]", settings.getDateFrom(), settings.getDateTo());
final long graphOffset = settings.getYAxisScale() == AxisScale.LINEAR ? 0 : 1;
appendfln(result, "set yrange [\""+graphOffset+"\":]");
appendfln(result, "set ylabel \"%s\"", settings.getYlabel());
switch (settings.getYAxisScale()) {
case LINEAR:
break;
case LOG10:
appendfln(result, "set logscale y");
break;
case LOG2:
appendfln(result, "set logscale y 2");
break;
}
appendfln(result, "set grid");
appendfln(result, "set output \"%s\"", settings.getOutput().toAbsolutePath().toString().replace("\\", "/"));
// marker lines that show which area will be zoomed
final long minDate = Long.parseLong(settings.getDateFrom());
final long maxDate = Long.parseLong(settings.getDateTo());
appendfln(result, "set arrow from "+(minDate + (maxDate-minDate)*0.25)+","+graphOffset+" rto graph 0,1 lt 3 lc rgb \"#EEEEEE\" nohead");
appendfln(result, "set arrow from "+(minDate + (maxDate-minDate)*0.75)+","+graphOffset+" rto graph 0,1 lc rgb \"#EEEEEE\" nohead");
appendf(result, "plot ");
count = 1;
for (final DataSeries dataSerie : dataSeries) {
appendfln(result, "'%s' using 1:2 title '%s' with points, \\", dataSerie.getDataFile(),
dataSerie.getTitle());
if (settings.getAggregate() == AggreateInternal.MEAN) {
appendfln(result, "A%d_mean title '%s Mean', \\", count, dataSerie.getTitle(),
dataSerie.getTitle());
}
count++;
}
return result.toString();
}
private void appendfln(final StringBuilder builder, final String format, final Object... args) {
builder.append(String.format(format + "\n", args));
}
private void appendf(final StringBuilder builder, final String format, final Object... args) {
builder.append(String.format(format, args));
}
}
package org.lucares.recommind.logs;
import java.util.Collection;
import org.lucares.pdb.plot.api.AggreateInternal;
import org.lucares.pdb.plot.api.AxisScale;
public class GnuplotFileGenerator {
public String generate(final GnuplotSettings settings, final Collection<DataSeries> dataSeries) {
final StringBuilder result = new StringBuilder();
appendfln(result, "set terminal %s noenhanced size %d,%d", settings.getTerminal(), settings.getWidth(),
settings.getHeight());
appendfln(result, "set datafile separator \"%s\"", settings.getDatafileSeparator());
int count = 1;
if (settings.getAggregate() != AggreateInternal.NONE)
{
for (final DataSeries dataSerie : dataSeries) {
appendfln(result, "stats '%s' using 2 prefix \"A%d\"", dataSerie.getDataFile(),count);
count++;
}
}
appendfln(result, "set timefmt '%s'", settings.getTimefmt());
appendfln(result, "set xdata time");
appendfln(result, "set format x \"%s\"", settings.getFormatX());
appendfln(result, "set xlabel \"%s\"", settings.getXlabel());
appendfln(result, "set xtics rotate by %d", settings.getRotateXAxisLabel());
appendfln(result, "set xrange [\"%s\":\"%s\"]", settings.getDateFrom(), settings.getDateTo());
final long graphOffset = settings.getYAxisScale() == AxisScale.LINEAR ? 0 : 1;
appendfln(result, "set yrange [\""+graphOffset+"\":]");
appendfln(result, "set ylabel \"%s\"", settings.getYlabel());
switch (settings.getYAxisScale()) {
case LINEAR:
break;
case LOG10:
appendfln(result, "set logscale y");
break;
case LOG2:
appendfln(result, "set logscale y 2");
break;
}
appendfln(result, "set grid");
appendfln(result, "set output \"%s\"", settings.getOutput().toAbsolutePath().toString().replace("\\", "/"));
// marker lines that show which area will be zoomed
final long minDate = Long.parseLong(settings.getDateFrom());
final long maxDate = Long.parseLong(settings.getDateTo());
appendfln(result, "set arrow from "+(minDate + (maxDate-minDate)*0.25)+","+graphOffset+" rto graph 0,1 lt 3 lc rgb \"#EEEEEE\" nohead");
appendfln(result, "set arrow from "+(minDate + (maxDate-minDate)*0.75)+","+graphOffset+" rto graph 0,1 lc rgb \"#EEEEEE\" nohead");
if (settings.isKeyOutside()){
appendfln(result, "set key outside");
}
appendfln(result, "set key font \",10\"");
appendf(result, "plot ");
count = 1;
for (final DataSeries dataSerie : dataSeries) {
appendfln(result, "'%s' using 1:2 title '%s' with points, \\", dataSerie.getDataFile(),
dataSerie.getTitle());
if (settings.getAggregate() == AggreateInternal.MEAN) {
appendfln(result, "A%d_mean title '%s Mean', \\", count, dataSerie.getTitle(),
dataSerie.getTitle());
}
count++;
}
return result.toString();
}
private void appendfln(final StringBuilder builder, final String format, final Object... args) {
builder.append(String.format(format + "\n", args));
}
private void appendf(final StringBuilder builder, final String format, final Object... args) {
builder.append(String.format(format, args));
}
}

View File

@@ -1,150 +1,159 @@
package org.lucares.recommind.logs;
import java.nio.file.Path;
import org.lucares.pdb.plot.api.AggreateInternal;
import org.lucares.pdb.plot.api.AxisScale;
public class GnuplotSettings {
private String terminal = "png";
private int height = 1200;
private int width = 1600;
private String timefmt = "%s"; //"%Y-%m-%dT%H:%M:%S"; // TODO @ahr timefmt
// set format for x-axis
private String formatX = "%Y-%m-%d %H:%M:%S";
// set datafile separator
private String datafileSeparator = ",";
// set xlabel
private String xlabel = "Time";
// set ylabel
private String ylabel = "Duration in ms";
// set output "datausage.png"
private final Path output;
// set xtics rotate by 10 degree
private int rotateXAxisLabel = -10;
private AxisScale yAxisScale;
private String dateFrom;
private String dateTo;
private AggreateInternal aggregate;
public GnuplotSettings(final Path output) {
this.output = output;
}
public int getRotateXAxisLabel() {
return rotateXAxisLabel;
}
public void setRotateXAxisLabel(final int rotateXAxisLabel) {
this.rotateXAxisLabel = rotateXAxisLabel;
}
public String getTerminal() {
return terminal;
}
public void setTerminal(final String terminal) {
this.terminal = terminal;
}
public int getHeight() {
return height;
}
public void setHeight(final int height) {
this.height = height;
}
public int getWidth() {
return width;
}
public void setWidth(final int width) {
this.width = width;
}
public String getTimefmt() {
return timefmt;
}
public void setTimefmt(final String timefmt) {
this.timefmt = timefmt;
}
public String getFormatX() {
return formatX;
}
public void setFormatX(final String formatX) {
this.formatX = formatX;
}
public String getDatafileSeparator() {
return datafileSeparator;
}
public void setDatafileSeparator(final String datafileSeparator) {
this.datafileSeparator = datafileSeparator;
}
public String getXlabel() {
return xlabel;
}
public void setXlabel(final String xlabel) {
this.xlabel = xlabel;
}
public String getYlabel() {
return ylabel;
}
public void setYlabel(final String ylabel) {
this.ylabel = ylabel;
}
public Path getOutput() {
return output;
}
public void setYAxisScale(final AxisScale yAxisScale) {
this.yAxisScale = yAxisScale;
}
public AxisScale getYAxisScale() {
return yAxisScale;
}
public void setDateFrom(final String dateFrom) {
this.dateFrom = dateFrom;
}
public String getDateFrom() {
return dateFrom;
}
public void setDateTo(final String dateTo) {
this.dateTo = dateTo;
}
public String getDateTo() {
return dateTo;
}
public void setAggregate(AggreateInternal aggregate) {
this.aggregate = aggregate;
}
public AggreateInternal getAggregate() {
return aggregate;
}
// plot 'sample.txt' using 1:2 title 'Bytes' with linespoints 2
}
package org.lucares.recommind.logs;
import java.nio.file.Path;
import org.lucares.pdb.plot.api.AggreateInternal;
import org.lucares.pdb.plot.api.AxisScale;
public class GnuplotSettings {
private String terminal = "png";
private int height = 1200;
private int width = 1600;
private String timefmt = "%s"; //"%Y-%m-%dT%H:%M:%S"; // TODO @ahr timefmt
// set format for x-axis
private String formatX = "%Y-%m-%d %H:%M:%S";
// set datafile separator
private String datafileSeparator = ",";
// set xlabel
private String xlabel = "Time";
// set ylabel
private String ylabel = "Duration in ms";
// set output "datausage.png"
private final Path output;
// set xtics rotate by 10 degree
private int rotateXAxisLabel = -10;
private AxisScale yAxisScale;
private String dateFrom;
private String dateTo;
private AggreateInternal aggregate;
private boolean keyOutside = false;
public GnuplotSettings(final Path output) {
this.output = output;
}
public int getRotateXAxisLabel() {
return rotateXAxisLabel;
}
public void setRotateXAxisLabel(final int rotateXAxisLabel) {
this.rotateXAxisLabel = rotateXAxisLabel;
}
public String getTerminal() {
return terminal;
}
public void setTerminal(final String terminal) {
this.terminal = terminal;
}
public int getHeight() {
return height;
}
public void setHeight(final int height) {
this.height = height;
}
public int getWidth() {
return width;
}
public void setWidth(final int width) {
this.width = width;
}
public String getTimefmt() {
return timefmt;
}
public void setTimefmt(final String timefmt) {
this.timefmt = timefmt;
}
public String getFormatX() {
return formatX;
}
public void setFormatX(final String formatX) {
this.formatX = formatX;
}
public String getDatafileSeparator() {
return datafileSeparator;
}
public void setDatafileSeparator(final String datafileSeparator) {
this.datafileSeparator = datafileSeparator;
}
public String getXlabel() {
return xlabel;
}
public void setXlabel(final String xlabel) {
this.xlabel = xlabel;
}
public String getYlabel() {
return ylabel;
}
public void setYlabel(final String ylabel) {
this.ylabel = ylabel;
}
public Path getOutput() {
return output;
}
public void setYAxisScale(final AxisScale yAxisScale) {
this.yAxisScale = yAxisScale;
}
public AxisScale getYAxisScale() {
return yAxisScale;
}
public void setDateFrom(final String dateFrom) {
this.dateFrom = dateFrom;
}
public String getDateFrom() {
return dateFrom;
}
public void setDateTo(final String dateTo) {
this.dateTo = dateTo;
}
public String getDateTo() {
return dateTo;
}
public void setAggregate(AggreateInternal aggregate) {
this.aggregate = aggregate;
}
public AggreateInternal getAggregate() {
return aggregate;
}
public void setKeyOutside(boolean keyOutside) {
this.keyOutside = keyOutside;
}
public boolean isKeyOutside() {
return keyOutside;
}
// plot 'sample.txt' using 1:2 title 'Bytes' with linespoints 2
}

View File

@@ -1,284 +1,285 @@
package org.lucares.recommind.logs;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Formatter;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.lucares.pdb.api.Entry;
import org.lucares.pdb.api.GroupResult;
import org.lucares.pdb.api.Result;
import org.lucares.pdb.api.Tags;
import org.lucares.pdb.plot.api.Limit;
import org.lucares.pdb.plot.api.PlotSettings;
import org.lucares.performance.db.PerformanceDb;
import org.lucares.utils.file.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Plotter {
private static final Logger LOGGER = LoggerFactory.getLogger(Plotter.class);
private static final Logger METRICS_LOGGER = LoggerFactory.getLogger("org.lucares.metrics.plotter");
private static final String DEFAULT_GROUP = "<none>";
private static final int INT_TO_STRING_CACHE_SIZE= 1000;
private static final String[] INT_TO_STRING;
static {
INT_TO_STRING = new String[INT_TO_STRING_CACHE_SIZE];
for (int i = 0; i < INT_TO_STRING_CACHE_SIZE; i++){
INT_TO_STRING[i] = String.valueOf(i);
}
}
private final PerformanceDb db;
private final Path tmpBaseDir;
private final Path outputDir;
public Plotter(final PerformanceDb db, final Path tmpBaseDir, final Path outputDir) {
this.db = db;
this.tmpBaseDir = tmpBaseDir;
this.outputDir = outputDir;
if (!Files.isDirectory(tmpBaseDir, LinkOption.NOFOLLOW_LINKS)) {
throw new IllegalArgumentException(tmpBaseDir + " is not a directory");
}
if (!Files.isDirectory(outputDir)) {
throw new IllegalArgumentException(outputDir + " is not a directory");
}
}
public Path getOutputDir() {
return outputDir;
}
public PlotResult plot(final PlotSettings plotSettings) throws InternalPlottingException {
LOGGER.trace("start plot: {}", plotSettings);
final String tmpSubDir = uniqueDirectoryName();
final Path tmpDir = tmpBaseDir.resolve(tmpSubDir);
try {
Files.createDirectories(tmpDir);
final List<DataSeries> dataSeries = new ArrayList<>();
final String query = plotSettings.getQuery();
final List<String> groupBy = plotSettings.getGroupBy();
final int height = plotSettings.getHeight();
final int width = plotSettings.getWidth();
final OffsetDateTime dateFrom = plotSettings.dateFrom();
final OffsetDateTime dateTo = plotSettings.dateTo();
final Result result = db.get(query, groupBy);
for (final GroupResult groupResult : result.getGroups()) {
final Stream<Entry> entries = groupResult.asStream();
final File dataFile = File.createTempFile("data", ".dat", tmpDir.toFile());
final CsvSummary csvSummary = toCsv(entries, dataFile, dateFrom, dateTo);
final String title = title(groupResult.getGroupedBy(), csvSummary.getValues());
final DataSeries dataSerie = new DataSeries(dataFile, title, csvSummary.getValues(), csvSummary.getMaxValue());
if (dataSerie.getValues() > 0) {
dataSeries.add(dataSerie);
}
}
if (dataSeries.isEmpty()) {
throw new NoDataPointsException();
}
sortAndLimit(dataSeries, plotSettings);
final Path outputFile = Files.createTempFile(outputDir, "out", ".png");
final Gnuplot gnuplot = new Gnuplot(tmpBaseDir);
final GnuplotSettings gnuplotSettings = new GnuplotSettings(outputFile);
gnuplotSettings.setHeight(height);
gnuplotSettings.setWidth(width);
defineXAxis(gnuplotSettings, plotSettings.dateFrom(), plotSettings.dateTo());
gnuplotSettings.setYAxisScale(plotSettings.getYAxisScale());
gnuplotSettings.setAggregate(plotSettings.getAggregate());
gnuplot.plot(gnuplotSettings, dataSeries);
return new PlotResult(outputFile.getFileName(), dataSeries);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Plotting was interrupted.");
} catch (final IOException e) {
throw new InternalPlottingException("Plotting failed: " + e.getMessage(), e);
} finally {
FileUtils.delete(tmpDir);
LOGGER.trace("done plot");
}
}
private void defineXAxis(final GnuplotSettings gnuplotSettings, final OffsetDateTime minDate,
final OffsetDateTime maxDate) {
String formatX;
int rotateX;
String formattedMinDate;
String formattedMaxDate;
if (minDate.until(maxDate, ChronoUnit.WEEKS) > 1) {
formatX = "%Y-%m-%d";
rotateX = 0;
} else if (minDate.until(maxDate, ChronoUnit.SECONDS) > 30) {
formatX = "%Y-%m-%d %H:%M:%S";
rotateX = gnuplotSettings.getRotateXAxisLabel();
} else {
formatX = "%Y-%m-%d %H:%M:%.3S";
rotateX = gnuplotSettings.getRotateXAxisLabel();
}
formattedMinDate = String.valueOf(minDate.toEpochSecond());
formattedMaxDate = String.valueOf(maxDate.toEpochSecond());
gnuplotSettings.setFormatX(formatX);
gnuplotSettings.setRotateXAxisLabel(rotateX);
gnuplotSettings.setDateFrom(formattedMinDate);
gnuplotSettings.setDateTo(formattedMaxDate);
}
private String uniqueDirectoryName() {
return OffsetDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm_ss")) + "_"
+ UUID.randomUUID().toString();
}
private void sortAndLimit(final List<DataSeries> dataSeries, final PlotSettings plotSettings) {
final Limit limitBy = plotSettings.getLimitBy();
dataSeries.sort(getDataSeriesComparator(limitBy));
switch (limitBy) {
case FEWEST_VALUES:
case MOST_VALUES:
case MAX_VALUE:
case MIN_VALUE:
while (dataSeries.size() > plotSettings.getLimit()) {
dataSeries.remove(plotSettings.getLimit());
}
break;
case NO_LIMIT:
}
}
private Comparator<? super DataSeries> getDataSeriesComparator(final Limit limitBy) {
switch (limitBy) {
case MOST_VALUES:
return DataSeries.BY_NUMBER_OF_VALUES.reversed();
case FEWEST_VALUES:
return DataSeries.BY_NUMBER_OF_VALUES;
case MAX_VALUE:
return DataSeries.BY_MAX_VALUE.reversed();
case MIN_VALUE:
return DataSeries.BY_MAX_VALUE;
case NO_LIMIT:
return DataSeries.BY_NUMBER_OF_VALUES;
}
throw new IllegalStateException("unhandled enum: "+ limitBy);
}
private String title(final Tags tags, final int values) {
final StringBuilder result = new StringBuilder();
if (tags.isEmpty()) {
result.append(DEFAULT_GROUP);
} else {
tags.forEach((k, v) -> {
if (result.length() > 0) {
result.append(" / ");
}
result.append(v);
});
}
result.append(" (");
result.append(values);
result.append(")");
return result.toString();
}
private static CsvSummary toCsv(final Stream<Entry> entries, final File dataFile, final OffsetDateTime dateFrom,
final OffsetDateTime dateTo) throws IOException {
final long start = System.nanoTime();
int count = 0;
final long fromEpochMilli = dateFrom.toInstant().toEpochMilli();
final long toEpochMilli = dateTo.toInstant().toEpochMilli();
final boolean useMillis = (toEpochMilli - fromEpochMilli) < TimeUnit.MINUTES.toMillis(5);
long maxValue = 0;
long ignoredValues = 0;
final int separator = ',';
final int newline = '\n';
final StringBuilder formattedDateBuilder = new StringBuilder();
try (final Writer output = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(dataFile), StandardCharsets.US_ASCII));
final Formatter formatter = new Formatter(formattedDateBuilder);) {
final Iterator<Entry> it = entries.iterator();
while (it.hasNext()) {
final Entry entry = it.next();
if (fromEpochMilli <= entry.getEpochMilli() && entry.getEpochMilli() <= toEpochMilli) {
final String value = longToString(entry.getValue());
final String formattedDate;
if (useMillis){
formattedDateBuilder.delete(0, formattedDateBuilder.length());
formatter.format("%.3f", entry.getEpochMilli() / 1000.0);
formattedDate = formattedDateBuilder.toString();
}else {
formattedDate = String.valueOf(entry.getEpochMilli() / 1000);
}
output.write(formattedDate);
output.write(separator);
output.write(value);
output.write(newline);
count++;
maxValue = Math.max(maxValue, entry.getValue());
}else {
ignoredValues++;
}
}
}
METRICS_LOGGER.debug("wrote {} values to csv in: {}ms (ignored {} values) use millis: {}", count, (System.nanoTime() - start) / 1_000_000.0, ignoredValues, Boolean.toString(useMillis));
return new CsvSummary(count, maxValue);
}
private static String longToString(final long value){
// using pre-generated strings reduces memory allocation by up to 25%
if (value < INT_TO_STRING_CACHE_SIZE){
return INT_TO_STRING[(int) value];
}
return String.valueOf(value);
}
}
package org.lucares.recommind.logs;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Formatter;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.lucares.pdb.api.Entry;
import org.lucares.pdb.api.GroupResult;
import org.lucares.pdb.api.Result;
import org.lucares.pdb.api.Tags;
import org.lucares.pdb.plot.api.Limit;
import org.lucares.pdb.plot.api.PlotSettings;
import org.lucares.performance.db.PerformanceDb;
import org.lucares.utils.file.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Plotter {
private static final Logger LOGGER = LoggerFactory.getLogger(Plotter.class);
private static final Logger METRICS_LOGGER = LoggerFactory.getLogger("org.lucares.metrics.plotter");
private static final String DEFAULT_GROUP = "<none>";
private static final int INT_TO_STRING_CACHE_SIZE= 1000;
private static final String[] INT_TO_STRING;
static {
INT_TO_STRING = new String[INT_TO_STRING_CACHE_SIZE];
for (int i = 0; i < INT_TO_STRING_CACHE_SIZE; i++){
INT_TO_STRING[i] = String.valueOf(i);
}
}
private final PerformanceDb db;
private final Path tmpBaseDir;
private final Path outputDir;
public Plotter(final PerformanceDb db, final Path tmpBaseDir, final Path outputDir) {
this.db = db;
this.tmpBaseDir = tmpBaseDir;
this.outputDir = outputDir;
if (!Files.isDirectory(tmpBaseDir, LinkOption.NOFOLLOW_LINKS)) {
throw new IllegalArgumentException(tmpBaseDir + " is not a directory");
}
if (!Files.isDirectory(outputDir)) {
throw new IllegalArgumentException(outputDir + " is not a directory");
}
}
public Path getOutputDir() {
return outputDir;
}
public PlotResult plot(final PlotSettings plotSettings) throws InternalPlottingException {
LOGGER.trace("start plot: {}", plotSettings);
final String tmpSubDir = uniqueDirectoryName();
final Path tmpDir = tmpBaseDir.resolve(tmpSubDir);
try {
Files.createDirectories(tmpDir);
final List<DataSeries> dataSeries = new ArrayList<>();
final String query = plotSettings.getQuery();
final List<String> groupBy = plotSettings.getGroupBy();
final int height = plotSettings.getHeight();
final int width = plotSettings.getWidth();
final OffsetDateTime dateFrom = plotSettings.dateFrom();
final OffsetDateTime dateTo = plotSettings.dateTo();
final Result result = db.get(query, groupBy);
for (final GroupResult groupResult : result.getGroups()) {
final Stream<Entry> entries = groupResult.asStream();
final File dataFile = File.createTempFile("data", ".dat", tmpDir.toFile());
final CsvSummary csvSummary = toCsv(entries, dataFile, dateFrom, dateTo);
final String title = title(groupResult.getGroupedBy(), csvSummary.getValues());
final DataSeries dataSerie = new DataSeries(dataFile, title, csvSummary.getValues(), csvSummary.getMaxValue());
if (dataSerie.getValues() > 0) {
dataSeries.add(dataSerie);
}
}
if (dataSeries.isEmpty()) {
throw new NoDataPointsException();
}
sortAndLimit(dataSeries, plotSettings);
final Path outputFile = Files.createTempFile(outputDir, "out", ".png");
final Gnuplot gnuplot = new Gnuplot(tmpBaseDir);
final GnuplotSettings gnuplotSettings = new GnuplotSettings(outputFile);
gnuplotSettings.setHeight(height);
gnuplotSettings.setWidth(width);
defineXAxis(gnuplotSettings, plotSettings.dateFrom(), plotSettings.dateTo());
gnuplotSettings.setYAxisScale(plotSettings.getYAxisScale());
gnuplotSettings.setAggregate(plotSettings.getAggregate());
gnuplotSettings.setKeyOutside(plotSettings.isKeyOutside());
gnuplot.plot(gnuplotSettings, dataSeries);
return new PlotResult(outputFile.getFileName(), dataSeries);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Plotting was interrupted.");
} catch (final IOException e) {
throw new InternalPlottingException("Plotting failed: " + e.getMessage(), e);
} finally {
FileUtils.delete(tmpDir);
LOGGER.trace("done plot");
}
}
private void defineXAxis(final GnuplotSettings gnuplotSettings, final OffsetDateTime minDate,
final OffsetDateTime maxDate) {
String formatX;
int rotateX;
String formattedMinDate;
String formattedMaxDate;
if (minDate.until(maxDate, ChronoUnit.WEEKS) > 1) {
formatX = "%Y-%m-%d";
rotateX = 0;
} else if (minDate.until(maxDate, ChronoUnit.SECONDS) > 30) {
formatX = "%Y-%m-%d %H:%M:%S";
rotateX = gnuplotSettings.getRotateXAxisLabel();
} else {
formatX = "%Y-%m-%d %H:%M:%.3S";
rotateX = gnuplotSettings.getRotateXAxisLabel();
}
formattedMinDate = String.valueOf(minDate.toEpochSecond());
formattedMaxDate = String.valueOf(maxDate.toEpochSecond());
gnuplotSettings.setFormatX(formatX);
gnuplotSettings.setRotateXAxisLabel(rotateX);
gnuplotSettings.setDateFrom(formattedMinDate);
gnuplotSettings.setDateTo(formattedMaxDate);
}
private String uniqueDirectoryName() {
return OffsetDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm_ss")) + "_"
+ UUID.randomUUID().toString();
}
private void sortAndLimit(final List<DataSeries> dataSeries, final PlotSettings plotSettings) {
final Limit limitBy = plotSettings.getLimitBy();
dataSeries.sort(getDataSeriesComparator(limitBy));
switch (limitBy) {
case FEWEST_VALUES:
case MOST_VALUES:
case MAX_VALUE:
case MIN_VALUE:
while (dataSeries.size() > plotSettings.getLimit()) {
dataSeries.remove(plotSettings.getLimit());
}
break;
case NO_LIMIT:
}
}
private Comparator<? super DataSeries> getDataSeriesComparator(final Limit limitBy) {
switch (limitBy) {
case MOST_VALUES:
return DataSeries.BY_NUMBER_OF_VALUES.reversed();
case FEWEST_VALUES:
return DataSeries.BY_NUMBER_OF_VALUES;
case MAX_VALUE:
return DataSeries.BY_MAX_VALUE.reversed();
case MIN_VALUE:
return DataSeries.BY_MAX_VALUE;
case NO_LIMIT:
return DataSeries.BY_NUMBER_OF_VALUES;
}
throw new IllegalStateException("unhandled enum: "+ limitBy);
}
private String title(final Tags tags, final int values) {
final StringBuilder result = new StringBuilder();
if (tags.isEmpty()) {
result.append(DEFAULT_GROUP);
} else {
tags.forEach((k, v) -> {
if (result.length() > 0) {
result.append(" / ");
}
result.append(v);
});
}
result.append(" (");
result.append(values);
result.append(")");
return result.toString();
}
private static CsvSummary toCsv(final Stream<Entry> entries, final File dataFile, final OffsetDateTime dateFrom,
final OffsetDateTime dateTo) throws IOException {
final long start = System.nanoTime();
int count = 0;
final long fromEpochMilli = dateFrom.toInstant().toEpochMilli();
final long toEpochMilli = dateTo.toInstant().toEpochMilli();
final boolean useMillis = (toEpochMilli - fromEpochMilli) < TimeUnit.MINUTES.toMillis(5);
long maxValue = 0;
long ignoredValues = 0;
final int separator = ',';
final int newline = '\n';
final StringBuilder formattedDateBuilder = new StringBuilder();
try (final Writer output = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(dataFile), StandardCharsets.US_ASCII));
final Formatter formatter = new Formatter(formattedDateBuilder);) {
final Iterator<Entry> it = entries.iterator();
while (it.hasNext()) {
final Entry entry = it.next();
if (fromEpochMilli <= entry.getEpochMilli() && entry.getEpochMilli() <= toEpochMilli) {
final String value = longToString(entry.getValue());
final String formattedDate;
if (useMillis){
formattedDateBuilder.delete(0, formattedDateBuilder.length());
formatter.format("%.3f", entry.getEpochMilli() / 1000.0);
formattedDate = formattedDateBuilder.toString();
}else {
formattedDate = String.valueOf(entry.getEpochMilli() / 1000);
}
output.write(formattedDate);
output.write(separator);
output.write(value);
output.write(newline);
count++;
maxValue = Math.max(maxValue, entry.getValue());
}else {
ignoredValues++;
}
}
}
METRICS_LOGGER.debug("wrote {} values to csv in: {}ms (ignored {} values) use millis: {}", count, (System.nanoTime() - start) / 1_000_000.0, ignoredValues, Boolean.toString(useMillis));
return new CsvSummary(count, maxValue);
}
private static String longToString(final long value){
// using pre-generated strings reduces memory allocation by up to 25%
if (value < INT_TO_STRING_CACHE_SIZE){
return INT_TO_STRING[(int) value];
}
return String.valueOf(value);
}
}