/*******************************************************************************
*
* Copyright (c) 2004-2011, Oracle Corporation
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi, Winston Prakash
*
*******************************************************************************/
package org.eclipse.hudson.graph;
import hudson.model.Api;
import hudson.model.Messages;
import hudson.model.TimeSeries;
import hudson.util.TimeUnit2;
import java.awt.Color;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import javax.servlet.ServletException;
import org.jvnet.localizer.Localizable;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
/**
* Maintains several {@link TimeSeries} with different update frequencies to
* satisfy three goals; (1) retain data over long timespan, (2) save memory, and
* (3) retain accurate data for the recent past.
*
* All in all, one instance uses about 8KB space.
*
* @author Kohsuke Kawaguchi
*/
@ExportedBean
public class MultiStageTimeSeries {
/**
* Name of this data series.
*/
public final Localizable title;
/**
* Used to render a line in the trend chart.
*/
public final Color color;
/**
* Updated every 10 seconds. Keep data up to 1 hour.
*/
@Exported
public final TimeSeries sec10;
/**
* Updated every 1 min. Keep data up to 1 day.
*/
@Exported
public final TimeSeries min;
/**
* Updated every 1 hour. Keep data up to 4 weeks.
*/
@Exported
public final TimeSeries hour;
private int counter;
public MultiStageTimeSeries(Localizable title, Color color, float initialValue, float decay) {
this.title = title;
this.color = color;
this.sec10 = new TimeSeries(initialValue, decay, 6 * 60);
this.min = new TimeSeries(initialValue, decay, 60 * 24);
this.hour = new TimeSeries(initialValue, decay, 28 * 24);
}
/**
* @deprecated since 2009-04-05. Use
* {@link #MultiStageTimeSeries(Localizable, Color, float, float)}
*/
public MultiStageTimeSeries(float initialValue, float decay) {
this(Messages._MultiStageTimeSeries_EMPTY_STRING(), Color.WHITE, initialValue, decay);
}
/**
* Call this method every 10 sec and supply a new data point.
*/
public void update(float f) {
counter = (counter + 1) % 360; // 1hour/10sec = 60mins/10sec=3600secs/10sec = 360
sec10.update(f);
if (counter % 6 == 0) {
min.update(f);
}
if (counter == 0) {
hour.update(f);
}
}
/**
* Selects a {@link TimeSeries}.
*/
public TimeSeries pick(TimeScale timeScale) {
switch (timeScale) {
case HOUR:
return hour;
case MIN:
return min;
case SEC10:
return sec10;
default:
throw new AssertionError();
}
}
/**
* Gets the most up-to-date data point value.
*/
public float getLatest(TimeScale timeScale) {
return pick(timeScale).getLatest();
}
public Api getApi() {
return new Api(this);
}
/**
* Choose which datapoint to use.
*/
public enum TimeScale {
SEC10(TimeUnit2.SECONDS.toMillis(10)),
MIN(TimeUnit2.MINUTES.toMillis(1)),
HOUR(TimeUnit2.HOURS.toMillis(1));
/**
* Number of milliseconds (10 secs, 1 min, and 1 hour) that this
* constant represents.
*/
public final long tick;
TimeScale(long tick) {
this.tick = tick;
}
/**
* Creates a new {@link DateFormat} suitable for processing this
* {@link TimeScale}.
*/
public DateFormat createDateFormat() {
switch (this) {
case HOUR:
return new SimpleDateFormat("MMM/dd HH");
case MIN:
return new SimpleDateFormat("HH:mm");
case SEC10:
return new SimpleDateFormat("HH:mm:ss");
default:
throw new AssertionError();
}
}
/**
* Parses the {@link TimeScale} from the query parameter.
*/
public static TimeScale parse(String type) {
if (type == null) {
return TimeScale.MIN;
}
return Enum.valueOf(TimeScale.class, type.toUpperCase(Locale.ENGLISH));
}
}
/**
* Represents the trend chart that consists of several
* {@link MultiStageTimeSeries}.
*
* <p> This object is renderable as HTTP response.
*/
public static final class TrendChart implements HttpResponse {
public final TimeScale timeScale;
public final List<MultiStageTimeSeries> series;
public final DataSet dataset;
public TrendChart(TimeScale timeScale, MultiStageTimeSeries... series) {
this.timeScale = timeScale;
this.series = new ArrayList<MultiStageTimeSeries>(Arrays.asList(series));
this.dataset = createDataset();
}
/**
* Creates a {@link DefaultCategoryDataset} for rendering a graph from a
* set of {@link MultiStageTimeSeries}.
*/
private DataSet createDataset() {
DataSet<String, String> ds = new DataSet<String, String>();
DateFormat format = timeScale.createDateFormat();
GraphSeries<String> xSeries = new GraphSeries<String>("Time");
ds.setXSeries(xSeries);
float[] data = series.get(0).pick(timeScale).getHistory();
Date date = new Date(System.currentTimeMillis() - timeScale.tick * data.length);
for (int j = data.length - 1; j >= 0; j--) {
date = new Date(date.getTime() + timeScale.tick);
String timeStr = format.format(date);
xSeries.add(timeStr);
}
for (MultiStageTimeSeries mstSeries : series) {
GraphSeries<Number> ySeries = new GraphSeries<Number>(GraphSeries.TYPE_LINE, mstSeries.title.toString(), mstSeries.color, false, false);
ySeries.setStacked(false);
ds.addYSeries(ySeries);
data = mstSeries.pick(timeScale).getHistory();
for (int j = data.length - 1; j >= 0; j--) {
ySeries.add(data[j]);
}
}
// For backward compatibility with JFreechart
float[][] dataPoints = new float[series.size()][];
for (int i = 0; i < series.size(); i++) {
dataPoints[i] = series.get(i).pick(timeScale).getHistory();
}
int dataLength = dataPoints[0].length;
for (float[] dataPoint : dataPoints) {
assert dataLength == dataPoint.length;
}
for (int i = dataLength - 1; i >= 0; i--) {
date = new Date(date.getTime() + timeScale.tick);
String timeStr = format.format(date);
for (int j = 0; j < dataPoints.length; j++) {
ds.add((double) dataPoints[j][i], series.get(j).title.toString(), timeStr);
}
}
return ds;
}
public Graph createGraph() {
Graph graph = new Graph(-1, 500, 400);
graph.setYAxisLabel("");
graph.setData(createDataset());
graph.setChartType(Graph.TYPE_LINE);
graph.setMultiStageTimeSeries(series);
return graph;
}
/**
* Renders this object as an image.
*/
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
createGraph().doPng(req, rsp);
}
}
public static TrendChart createTrendChart(TimeScale scale, MultiStageTimeSeries... data) {
return new TrendChart(scale, data);
}
}