/* * -----------------------------------------------------------------------\ * PerfCake *   * Copyright (C) 2010 - 2016 the original author or 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 org.perfcake.reporting.destination.c3chart; import org.perfcake.PerfCakeConst; import org.perfcake.PerfCakeException; import org.perfcake.reporting.Measurement; import org.perfcake.reporting.Quantity; import org.perfcake.reporting.ReportingException; import org.perfcake.util.Utils; import io.vertx.core.json.Json; import io.vertx.core.json.JsonArray; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; /** * Representation of all data files needed to write a chart to the disk. Also handles directory creation and copies basic html files and their dependencies. * * @author <a href="mailto:marvenec@gmail.com">Martin Večeřa</a> */ public class C3ChartDataFile { /** * A logger for the class. */ private static final Logger log = LogManager.getLogger(C3ChartDataFile.class); /** * A list of file resources to be copied in the resulting report. */ private static final String[] resourceFiles = new String[] { "c3.min.css", "c3.min.js", "d3.v3.min.js", "report.css", "report.js", "favicon.svg" }; /** * The JavaScript file representing chart data. This is not set for charts created as a combination of existing ones. */ private transient File dataFile = null; /** * A file channel for storing results. */ private transient FileChannel outputChannel; /** * Target path for storing all data files related to the chart. These are the data itself (.js), the description file (.dat), * and the quick view file (.html). */ private final Path target; /** * This is set to false after a first result line is obtained from the getResultLine() method. * In the method, this is used to display a complete warning message for the user to be able to fix * the scenario. But we do not want the warning to show every time as it would slow down the performance. */ private boolean firstResultsLine = true; /** * Chart meta-data. */ private C3Chart chart; /** * Creates a new data files structure and write basic structures to the drive. * * @param chart * Chart meta-data. * @param target * Root path where all the files and directories will be created. Attempts to create the missing directories. * @throws PerfCakeException * In case of an I/O error. */ C3ChartDataFile(final C3Chart chart, final Path target) throws PerfCakeException { this.chart = chart; this.target = target; createOutputFileStructure(); writeDataHeader(); C3ChartHtmlTemplates.writeQuickView(target, chart); writeDescriptor(); } /** * Replaces the data file of the given chart with new data. * * @param chart * Chart meta-data. * @param target * Root path to an existing chart report. * @param newData * New data to be written. * @throws PerfCakeException * In case of an I/O error. */ C3ChartDataFile(final C3Chart chart, final Path target, final C3ChartData newData) throws PerfCakeException { this.chart = chart; this.target = target; writeDataHeader(); try (FileChannel dataOutput = FileChannel.open(getDataFile().toPath(), StandardOpenOption.APPEND);) { for (final JsonArray json : newData.getData()) { StringBuilder sb = new StringBuilder(); sb.append(chart.getBaseName()); sb.append(".push("); sb.append(json.encode()); sb.append(");\n"); dataOutput.write(ByteBuffer.wrap(sb.toString().getBytes(Charset.forName(Utils.getDefaultEncoding())))); } } catch (IOException e) { throw new PerfCakeException("Unable to write new chart data: ", e); } } /** * Reads the chart meta-data from the existing directory structure. All internal structures are initialized. * * @param descriptorFile * The direct pointer to the ${target}/data/${baseName}.json file. * @throws PerfCakeException * In case of an I/O error. */ C3ChartDataFile(final File descriptorFile) throws PerfCakeException { try { final String chartJson = Utils.readFilteredContent(descriptorFile.toURI().toURL()); chart = Json.decodeValue(chartJson, C3Chart.class); target = descriptorFile.getParentFile().getParentFile().toPath(); } catch (IOException e) { throw new PerfCakeException("Unable to read chart descriptor: ", e); } } /** * Reads the chart meta-data from the existing directory structure. All internal structures are initialized. * * @param target * Root path to an existing chart report. * @param baseName * The base name of the chart file data (i. e. ${target}/data/${baseName}.*). * @throws PerfCakeException * In case of an I/O error. */ C3ChartDataFile(final Path target, final String baseName) throws PerfCakeException { this(Paths.get(target.toString(), "data", baseName + ".json").toFile()); } /** * Get chart meta-data. * * @return Chart meta-data. */ public C3Chart getChart() { return chart; } /** * Gets the root path of this chart report. * * @return The root path of this chart report. */ public Path getTarget() { return target; } /** * Gets the specific file with chart data. * * @return The specific file with chart data. */ private File getDataFile() { if (dataFile == null) { final Path dataFilePath = Paths.get(target.toString(), "data", chart.getBaseName() + ".js"); dataFile = dataFilePath.toFile(); } return dataFile; } /** * Opens the data file for output of additional values. * * @throws PerfCakeException * In case of an I/O error. */ public void open() throws PerfCakeException { try { outputChannel = FileChannel.open(getDataFile().toPath(), StandardOpenOption.APPEND); } catch (final IOException e) { throw new PerfCakeException(String.format("Cannot open data file %s for appending data.", dataFile.getAbsolutePath()), e); } } /** * Gets a JavaScript line to be written to the data file that represents the current Measurement. * All attributes required by the attributes list of this chart must be present in the measurement for the line to be returned. * * @param measurement * The current measurement. * @return The line representing the data in measurement specified by the attributes list of this chart, or null when there was some of the attributes missing. */ private String getResultLine(final Measurement measurement) { final StringBuilder sb = new StringBuilder(); boolean missingAttributes = false; boolean isWarmUp = measurement.get(PerfCakeConst.WARM_UP_TAG) != null ? (Boolean) measurement.get(PerfCakeConst.WARM_UP_TAG) : false; sb.append(chart.getBaseName()); sb.append(".push(["); switch (chart.getxAxisType()) { case TIME: sb.append(measurement.getTime()); break; case ITERATION: sb.append(measurement.getIteration()); break; case PERCENTAGE: sb.append(measurement.getPercentage()); break; } int nullFields = 0; for (final String attr : chart.getAttributes()) { if (chart.getAttributes().indexOf(attr) > 0) { boolean warmUpAttr = attr.endsWith(PerfCakeConst.WARM_UP_TAG); // warmUp is handled using separate columns with the same base name and suffix _warmUp String pureAttr = warmUpAttr ? attr.substring(0, attr.length() - PerfCakeConst.WARM_UP_TAG.length() - 1) : attr; sb.append(", "); // we do not have all required attributes, return an empty line if (!warmUpAttr && !measurement.getAll().containsKey(attr)) { missingAttributes = true; if (firstResultsLine) { log.warn(String.format("Missing attribute %s, skipping the record.", attr)); } } else { final Object data = measurement.get(pureAttr); // we put null values in either all the fields with the _warmUp suffix, or the others depending whether we are in the warmUp phase if (isWarmUp ^ warmUpAttr) { nullFields++; sb.append("null"); } else { if (data instanceof String) { sb.append("\""); sb.append(((String) data).replaceAll("\"", "\\\"")); sb.append("\""); } else if (data instanceof Quantity) { sb.append(((Quantity) data).getNumber().toString()); } else { if (data == null) { nullFields++; sb.append("null"); } else { sb.append(data.toString()); } } } } } } firstResultsLine = false; // we must postpone the return for all misses to be shown // we also want to skip any records with just null values if (missingAttributes || chart.getAttributes().size() - nullFields <= 1) { return ""; } sb.append("]);\n"); return sb.toString(); } /** * Appends results to this chart based on the given Measurement. * * @param measurement * The Measurement to be stored. * @throws ReportingException * When it was not possible to write the data. */ void appendResult(final Measurement measurement) throws ReportingException { final String line = getResultLine(measurement); if (!"".equals(line)) { try { outputChannel.write(ByteBuffer.wrap(line.getBytes(Charset.forName(Utils.getDefaultEncoding())))); } catch (final IOException ioe) { throw new ReportingException(String.format("Could not append data to the chart file %s.", getDataFile().getAbsolutePath()), ioe); } } } /** * Closes the output channel. * * @throws PerfCakeException * In case of an I/O error. */ public void close() throws PerfCakeException { try { outputChannel.close(); } catch (final IOException e) { throw new PerfCakeException(String.format("Cannot close output channel to the file %s.", getDataFile().getAbsolutePath()), e); } } /** * Writes the initial header and array definition to the JavaScript data file. * * @throws PerfCakeException * When it was not possible to write the data. */ private void writeDataHeader() throws PerfCakeException { final StringBuilder dataHeader = new StringBuilder("var "); dataHeader.append(chart.getBaseName()); dataHeader.append(" = [ [ "); boolean first = true; for (final String attr : chart.getAttributes()) { if (first) { dataHeader.append("'"); first = false; } else { dataHeader.append(", '"); } dataHeader.append(attr); dataHeader.append("'"); } dataHeader.append(" ] ];\n"); dataHeader.append("\n"); Utils.writeFileContent(getDataFile(), dataHeader.toString()); } /** * Serializes chart meta-data as JSON to the output directory structure. * * @throws PerfCakeException * In case of an I/O error. */ private void writeDescriptor() throws PerfCakeException { final Path descriptorFile = Paths.get(target.toString(), "data", chart.getBaseName() + ".json"); Utils.writeFileContent(descriptorFile, Json.encode(chart)); } /** * Creates output file structure including all needed CSS and JS files. * * @throws PerfCakeException * When it was not possible to create any of the directories or files. */ private void createOutputFileStructure() throws PerfCakeException { if (!target.toFile().exists()) { if (!target.toFile().mkdirs()) { throw new PerfCakeException("Could not create output directory: " + target.toFile().getAbsolutePath()); } } else { if (!target.toFile().isDirectory()) { throw new PerfCakeException("Could not create output directory. It already exists as a file: " + target.toFile().getAbsolutePath()); } } File dir = Paths.get(target.toString(), "data").toFile(); if (!dir.exists() && !dir.mkdirs()) { throw new PerfCakeException("Could not create data directory: " + dir.getAbsolutePath()); } dir = Paths.get(target.toString(), "src").toFile(); if (!dir.exists() && !dir.mkdirs()) { throw new PerfCakeException("Could not create source directory: " + dir.getAbsolutePath()); } try { for (final String resourceFileName : resourceFiles) { copyResourceFile(resourceFileName); } } catch (final IOException e) { throw new PerfCakeException("Cannot copy necessary chart resources to the output path: ", e); } } /** * Copies the given resource file to the target chart report. * * @param resourceFileName * The name of the resource. * @throws IOException * When it was not possible to copy the resource. */ private void copyResourceFile(final String resourceFileName) throws IOException { Files.copy(getClass().getResourceAsStream("/c3chart/" + resourceFileName), Paths.get(target.toString(), "src", resourceFileName), StandardCopyOption.REPLACE_EXISTING); } }