package com.ikokoon.serenity.process;
import java.awt.Font;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.title.TextTitle;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import com.ikokoon.serenity.Configuration;
import com.ikokoon.serenity.IConstants;
import com.ikokoon.serenity.Profiler;
import com.ikokoon.serenity.model.Class;
import com.ikokoon.serenity.model.Method;
import com.ikokoon.serenity.model.Snapshot;
import com.ikokoon.serenity.persistence.IDataBase;
import com.ikokoon.toolkit.Toolkit;
/**
* This class takes a database and produces reports based on the snapshots for each method, for the profiler.
*
* @author Michael Couck
* @since 19.06.10
* @version 01.00
*/
public class Reporter extends AProcess {
private IDataBase dataBase;
public Reporter(IProcess parent, IDataBase dataBase) {
super(parent);
this.dataBase = dataBase;
}
public void execute() {
try {
// Only execute the reports for the profiler if the snapshot interval is set
long snapshptInterval = Configuration.getConfiguration().getSnapshotInterval();
if (snapshptInterval < 0) {
return;
}
try {
// Write the style sheet first
File file = new File("./" + IConstants.STYLE_SHEET_FILE);
if (!file.exists()) {
InputStream inputStream = Reporter.class.getResourceAsStream(IConstants.REPORT_STYLE_SHEET);
Toolkit.setContents(file, Toolkit.getContents(inputStream).toByteArray());
}
} catch (Exception e) {
logger.error("Exception writing the style sheet : ", e);
}
String html = methodSeries(dataBase);
writeReport(IConstants.METHOD_SERIES_FILE, html);
html = methodNetSeries(dataBase);
writeReport(IConstants.METHOD_NET_SERIES_FILE, html);
html = methodChangeSeries(dataBase);
writeReport(IConstants.METHOD_CHANGE_SERIES_FILE, html);
html = methodNetChangeSeries(dataBase);
writeReport(IConstants.METHOD_NET_CHANGE_SERIES_FILE, html);
} catch (Exception e) {
logger.error("Exception writing the reports", e);
}
super.execute();
}
/**
* Writes the report data to the file system.
*
* @param name
* the name of the report
* @param html
* the html to write in the file
*/
private void writeReport(String name, String html) {
try {
File file = new File(name);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
if (!file.exists()) {
file.createNewFile();
}
if (logger.isInfoEnabled()) {
logger.info("Writing report : " + file.getAbsolutePath());
}
Toolkit.setContents(file, html.getBytes());
} catch (Exception e) {
logger.error("Exception writing report : " + name, e);
}
}
/**
* This method generates the time series for the methods and puts it in an HTML string. The methods are sorted according to the greatest average
* time for each method.
*/
@SuppressWarnings("unchecked")
protected String methodSeries(final IDataBase dataBase) {
Comparator<Method> comparator = new Comparator<Method>() {
public int compare(Method o1, Method o2) {
Long o1Average = new Long(Profiler.averageMethodTime(o1));
Long o2Average = new Long(Profiler.averageMethodTime(o2));
// We want a descending table, i.e. the most expensive at the top
return o2Average.compareTo(o1Average);
}
};
Set<Method> sortedMethods = new TreeSet<Method>(comparator);
List<Method> methods = dataBase.find(Method.class);
sortedMethods.addAll(methods);
List<Snapshot<?, ?>> snapshots = methods.size() > 0 ? methods.get(0).getSnapshots() : new ArrayList<Snapshot<?, ?>>();
Element tableElement = tableElement(snapshots);
for (Method method : sortedMethods) {
Class<?, ?> klass = (Class<?, ?>) method.getParent();
String className = klass.getName();
String methodName = method.getName();
List<Long> methodSeries = Profiler.methodSeries(method);
String url = buildGraph(IConstants.METHOD_SERIES, method, methodSeries);
Element rowElement = addElement(tableElement, "tr", null);
addElement(rowElement, "td", className);
addElement(rowElement, "td", methodName);
addElement(rowElement, "td", Long.toString(Profiler.totalMethodTime(method)));
addElement(rowElement, "td", Long.toString(Profiler.totalNetMethodTime(method)));
Element dataElement = addElement(rowElement, "td", null);
Element imageElement = addElement(dataElement, "img", null);
addAttributes(imageElement, new String[] { "src" }, new String[] { url });
}
Document document = tableElement.getDocument();
return prettyPrint(document);
}
@SuppressWarnings("unchecked")
protected String methodNetSeries(final IDataBase dataBase) {
Comparator<Method> comparator = new Comparator<Method>() {
public int compare(Method o1, Method o2) {
Long o1Average = new Long(Profiler.averageMethodNetTime(o1));
Long o2Average = new Long(Profiler.averageMethodNetTime(o2));
// We want a descending table, i.e. the most expensive at the top
return o2Average.compareTo(o1Average);
}
};
Set<Method> sortedMethods = new TreeSet<Method>(comparator);
List<Method> methods = dataBase.find(Method.class);
sortedMethods.addAll(methods);
List<Snapshot<?, ?>> snapshots = methods.size() > 0 ? methods.get(0).getSnapshots() : new ArrayList<Snapshot<?, ?>>();
Element tableElement = tableElement(snapshots);
for (Method method : sortedMethods) {
Class<?, ?> klass = (Class<?, ?>) method.getParent();
String className = klass.getName();
String methodName = method.getName();
List<Long> methodSeries = Profiler.methodNetSeries(method);
String url = buildGraph(IConstants.METHOD_NET_SERIES, method, methodSeries);
Element rowElement = addElement(tableElement, "tr", null);
addElement(rowElement, "td", className);
addElement(rowElement, "td", methodName);
addElement(rowElement, "td", Long.toString(Profiler.totalMethodTime(method)));
addElement(rowElement, "td", Long.toString(Profiler.totalNetMethodTime(method)));
Element dataElement = addElement(rowElement, "td", null);
Element imageElement = addElement(dataElement, "img", null);
addAttributes(imageElement, new String[] { "src" }, new String[] { url });
}
Document document = tableElement.getDocument();
return prettyPrint(document);
}
@SuppressWarnings("unchecked")
protected String methodChangeSeries(final IDataBase dataBase) {
Comparator<Method> comparator = new Comparator<Method>() {
public int compare(Method o1, Method o2) {
Long o1Average = new Long(Profiler.averageMethodTime(o1));
Long o2Average = new Long(Profiler.averageMethodTime(o2));
return o2Average.compareTo(o1Average);
}
};
Set<Method> sortedMethods = new TreeSet<Method>(comparator);
List<Method> methods = dataBase.find(Method.class);
sortedMethods.addAll(methods);
List<Snapshot<?, ?>> snapshots = methods.size() > 0 ? methods.get(0).getSnapshots() : new ArrayList<Snapshot<?, ?>>();
Element tableElement = tableElement(snapshots);
for (Method method : sortedMethods) {
Class<?, ?> klass = (Class<?, ?>) method.getParent();
String className = klass.getName();
String methodName = method.getName();
List<Long> methodSeries = Profiler.methodChangeSeries(method);
String url = buildGraph(IConstants.METHOD_CHANGE_SERIES, method, methodSeries);
Element rowElement = addElement(tableElement, "tr", null);
addElement(rowElement, "td", className);
addElement(rowElement, "td", methodName);
addElement(rowElement, "td", Long.toString(Profiler.averageMethodTime(method)));
addElement(rowElement, "td", Long.toString(Profiler.averageMethodNetTime(method)));
Element dataElement = addElement(rowElement, "td", null);
Element imageElement = addElement(dataElement, "img", null);
addAttributes(imageElement, new String[] { "src" }, new String[] { url });
}
Document document = tableElement.getDocument();
return prettyPrint(document);
}
@SuppressWarnings("unchecked")
protected String methodNetChangeSeries(final IDataBase dataBase) {
Comparator<Method> comparator = new Comparator<Method>() {
public int compare(Method o1, Method o2) {
Long o1Average = new Long(Profiler.averageMethodTime(o1));
Long o2Average = new Long(Profiler.averageMethodTime(o2));
return o2Average.compareTo(o1Average);
}
};
Set<Method> sortedMethods = new TreeSet<Method>(comparator);
List<Method> methods = dataBase.find(Method.class);
sortedMethods.addAll(methods);
List<Snapshot<?, ?>> snapshots = methods.size() > 0 ? methods.get(0).getSnapshots() : new ArrayList<Snapshot<?, ?>>();
Element tableElement = tableElement(snapshots);
for (Method method : sortedMethods) {
Class<?, ?> klass = (Class<?, ?>) method.getParent();
String className = klass.getName();
String methodName = method.getName();
List<Long> methodSeries = Profiler.methodNetChangeSeries(method);
String url = buildGraph(IConstants.METHOD_NET_CHANGE_SERIES, method, methodSeries);
Element rowElement = addElement(tableElement, "tr", null);
addElement(rowElement, "td", className);
addElement(rowElement, "td", methodName);
addElement(rowElement, "td", Long.toString(Profiler.averageMethodTime(method)));
addElement(rowElement, "td", Long.toString(Profiler.averageMethodNetTime(method)));
Element dataElement = addElement(rowElement, "td", null);
Element imageElement = addElement(dataElement, "img", null);
addAttributes(imageElement, new String[] { "src" }, new String[] { url });
}
Document document = tableElement.getDocument();
return prettyPrint(document);
}
private Element tableElement(List<Snapshot<?, ?>> snapshots) {
Document document = DocumentHelper.createDocument();
Element htmlElement = document.addElement("html");
Element headElement = addElement(htmlElement, "head", null);
Element linkElement = addElement(headElement, "link", null);
addAttributes(linkElement, new String[] { "href", "rel", "type", "media" }, new String[] { IConstants.STYLE_SHEET, "stylesheet", "text/css",
"screen" });
Element bodyElement = addElement(linkElement, "body", null);
Element tableElement = addElement(bodyElement, "table", null);
Element headerRowElement = addElement(tableElement, "tr", null);
String periods = "no periods";
if (snapshots.size() > 0) {
SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy hh:mm:ss");
Snapshot<?, ?> firstSnapshot = snapshots.get(0);
Snapshot<?, ?> lastSnapshot = snapshots.get(snapshots.size() - 1);
String start = dateFormat.format(firstSnapshot.getStart());
String end = dateFormat.format(lastSnapshot.getStart());
StringBuilder builder = new StringBuilder(start);
builder.append(" to ");
builder.append(end);
long intervals = firstSnapshot.getEnd().getTime() - firstSnapshot.getStart().getTime();
builder.append(", at intervals of : ");
builder.append(intervals);
builder.append(" ms.");
periods = builder.toString();
}
Element headerElement = addElement(headerRowElement, "th", "Period from : " + periods);
addAttributes(headerElement, new String[] { "colspan" }, new String[] { "5" });
Element rowElement = addElement(tableElement, "tr", null);
addElement(rowElement, "th", "Class");
addElement(rowElement, "th", "Method");
addElement(rowElement, "th", "Time");
addElement(rowElement, "th", "Net time");
addElement(rowElement, "th", "Graph");
return tableElement;
}
private Element addElement(Element parent, String name, String text) {
Element element = parent.addElement(name);
if (text != null) {
element.addText(text);
}
return element;
}
private void addAttributes(Element element, String[] names, String[] values) {
for (int i = 0; i < names.length; i++) {
element.addAttribute(names[i], values[i]);
}
}
private String prettyPrint(Document document) {
try {
OutputFormat format = OutputFormat.createPrettyPrint();
// XMLWriter writer = new XMLWriter(System.out, format);
// writer.write(document);
// format = OutputFormat.createCompactFormat();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
XMLWriter writer = new XMLWriter(byteArrayOutputStream, format);
writer.write(document);
return byteArrayOutputStream.toString();
} catch (Exception e) {
logger.error("Exception pretty printing the output", e);
}
return document.asXML();
}
@SuppressWarnings("unchecked")
protected String buildGraph(String seriesDirectory, Method method, List<Long> datas) {
XYSeries series = new XYSeries("XYGraph", false, false);
double snapshot = 0;
for (Long data : datas) {
double seconds = nanosToSeconds(data);
series.add(snapshot++, seconds);
}
XYSeriesCollection seriesCollection = new XYSeriesCollection();
seriesCollection.addSeries(series);
JFreeChart chart = ChartFactory.createXYLineChart(null, "Snapshots", "Time", seriesCollection, PlotOrientation.VERTICAL, false, false, false);
chart.setTitle(new TextTitle(method.getName(), new Font("Arial", Font.BOLD, 11)));
XYPlot xyPlot = chart.getXYPlot();
NumberAxis yAxis = (NumberAxis) xyPlot.getRangeAxis();
yAxis.setAutoRange(true);
yAxis.setAutoRangeIncludesZero(true);
NumberAxis xAxis = (NumberAxis) xyPlot.getDomainAxis();
xAxis.setAutoRange(true);
xAxis.setAutoRangeIncludesZero(true);
// xAxis.setTickUnit(new NumberTickUnit(1));
StringBuilder builder = new StringBuilder(method.getClassName());
builder.append(method.getName());
builder.append(method.getDescription());
String fileName = Long.toString(Toolkit.hash(builder.toString()));
fileName += ".jpeg";
File chartSeriesDirectory = new File(IConstants.chartDirectory, seriesDirectory);
File chartFile = new File(chartSeriesDirectory, fileName);
try {
if (!IConstants.chartDirectory.exists()) {
IConstants.chartDirectory.mkdirs();
}
if (!chartSeriesDirectory.exists()) {
chartSeriesDirectory.mkdirs();
}
if (!chartFile.exists()) {
chartFile.createNewFile();
}
ChartUtilities.saveChartAsJPEG(chartFile, chart, 450, 150);
builder = new StringBuilder(IConstants.CHARTS);
builder.append(File.separatorChar);
builder.append(seriesDirectory);
builder.append(File.separatorChar);
builder.append(fileName);
return builder.toString();
} catch (Exception e) {
logger.error("Exception generating the graph", e);
}
return null;
}
@SuppressWarnings("unused")
private double nanosToMillis(Long nanos) {
double millis = nanos / 1000000d;
return millis;
}
private double nanosToSeconds(Long nanos) {
double seconds = nanos / 1000000000d;
return seconds;
}
@SuppressWarnings("unused")
private long min(List<Long> datas) {
long min = 0;
for (Long data : datas) {
if (data < min) {
min = data;
}
}
return min;
}
@SuppressWarnings("unused")
private long max(List<Long> datas) {
long max = 0;
for (Long data : datas) {
if (data > max) {
max = data;
}
}
return max;
}
}