/* * The MIT License * * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.model; import hudson.util.TimeUnit2; import hudson.util.NoOverlapCategoryAxis; import hudson.util.ChartUtil; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Arrays; import java.util.ArrayList; import java.util.List; import java.io.IOException; import java.awt.*; import java.util.Locale; import org.jfree.data.category.DefaultCategoryDataset; import org.jfree.chart.JFreeChart; import org.jfree.chart.ChartFactory; import org.jfree.chart.axis.CategoryAxis; import org.jfree.chart.axis.CategoryLabelPositions; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.renderer.category.LineAndShapeRenderer; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.CategoryPlot; import org.jfree.ui.RectangleInsets; 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; import javax.servlet.ServletException; /** * 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. */ //TODO: review and check whether we can do it private public final Localizable title; /** * Used to render a line in the trend chart. */ //TODO: review and check whether we can do it private public final Color color; /** * Updated every 10 seconds. Keep data up to 1 hour. */ //TODO: review and check whether we can do it private @Exported public final TimeSeries sec10; /** * Updated every 1 min. Keep data up to 1 day. */ //TODO: review and check whether we can do it private @Exported public final TimeSeries min; /** * Updated every 1 hour. Keep data up to 4 weeks. */ //TODO: review and check whether we can do it private @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); } public Localizable getTitle() { return title; } public Color getColor() { return color; } public TimeSeries getSec10() { return sec10; } public TimeSeries getMin() { return min; } public TimeSeries getHour() { return hour; } /** * @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 DefaultCategoryDataset dataset; public TimeScale getTimeScale() { return timeScale; } public List<MultiStageTimeSeries> getSeries() { return series; } public DefaultCategoryDataset getDataset() { return 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 DefaultCategoryDataset createDataset() { 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; DefaultCategoryDataset ds = new DefaultCategoryDataset(); DateFormat format = timeScale.createDateFormat(); Date dt = new Date(System.currentTimeMillis()-timeScale.tick*dataLength); for (int i = dataLength-1; i>=0; i--) { dt = new Date(dt.getTime()+timeScale.tick); String l = format.format(dt); for(int j=0; j<dataPoints.length; j++) ds.addValue(dataPoints[j][i],series.get(j).title.toString(),l); } return ds; } /** * Draws a chart into {@link JFreeChart}. */ public JFreeChart createChart() { final JFreeChart chart = ChartFactory.createLineChart(null, // chart title null, // unused null, // range axis label dataset, // data PlotOrientation.VERTICAL, // orientation true, // include legend true, // tooltips false // urls ); chart.setBackgroundPaint(Color.white); final CategoryPlot plot = chart.getCategoryPlot(); plot.setBackgroundPaint(Color.WHITE); plot.setOutlinePaint(null); plot.setRangeGridlinesVisible(true); plot.setRangeGridlinePaint(Color.black); final LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer(); renderer.setBaseStroke(new BasicStroke(3)); for (int i = 0; i < series.size(); i++) renderer.setSeriesPaint(i, series.get(i).color); final CategoryAxis domainAxis = new NoOverlapCategoryAxis(null); plot.setDomainAxis(domainAxis); domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_90); domainAxis.setLowerMargin(0.0); domainAxis.setUpperMargin(0.0); domainAxis.setCategoryMargin(0.0); final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis(); rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); // crop extra space around the graph plot.setInsets(new RectangleInsets(0, 0, 0, 5.0)); return chart; } /** * Renders this object as an image. */ public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException { ChartUtil.generateGraph(req, rsp, createChart(), 500, 400); } } public static TrendChart createTrendChart(TimeScale scale, MultiStageTimeSeries... data) { return new TrendChart(scale,data); } }