/* * The MIT License * * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Seiji Sogabe * * 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.Extension; import jenkins.util.SystemProperties; import hudson.model.MultiStageTimeSeries.TimeScale; import hudson.model.MultiStageTimeSeries.TrendChart; import hudson.model.queue.SubTask; import hudson.model.queue.Tasks; import hudson.util.ColorPalette; import hudson.util.NoOverlapCategoryAxis; import jenkins.model.Jenkins; import org.jenkinsci.Symbol; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.CategoryAxis; import org.jfree.chart.axis.CategoryLabelPositions; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.plot.CategoryPlot; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.renderer.category.LineAndShapeRenderer; import org.jfree.data.category.CategoryDataset; import org.jfree.ui.RectangleInsets; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.export.Exported; import java.awt.*; import java.io.IOException; import java.io.Serializable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.List; import javax.annotation.CheckForNull; /** * Utilization statistics for a node or a set of nodes. * * <h2>Implementation Note</h2> * <p> * Instances of this class is not capable of updating the statistics itself * — instead, it's done by the {@link LoadStatisticsUpdater} timer. * This is more efficient (as it allows us a single pass to update all stats), * but it's not clear to me if the loss of autonomy is worth it. * * @author Kohsuke Kawaguchi * @see Label#loadStatistics * @see Jenkins#overallLoad * @see Jenkins#unlabeledLoad */ @ExportedBean public abstract class LoadStatistics { /** * {@code true} if and only if the new way of building statistics has been implemented by this class. * @since 1.607 */ private final boolean modern; /** * Number of executors defined for Jenkins and how it changes over time. * @since 1.607 */ @Exported public final MultiStageTimeSeries definedExecutors; /** * Number of executors on-line and how it changes over time. Replaces {@link #totalExecutors} * @since 1.607 */ @Exported public final MultiStageTimeSeries onlineExecutors; /** * Number of executors in the process of coming on-line and how it changes over time. * @since 1.607 */ @Exported public final MultiStageTimeSeries connectingExecutors; /** * Number of busy executors and how it changes over time. */ @Exported public final MultiStageTimeSeries busyExecutors; /** * Number of executors not executing and how it changes over time. Note the these executors may not be able to * take work, see {@link #availableExecutors}. * @since 1.607 */ @Exported public final MultiStageTimeSeries idleExecutors; /** * Number of executors not executing and available to take work and how it changes over time. * @since 1.607 */ @Exported public final MultiStageTimeSeries availableExecutors; /** * Number of total executors and how it changes over time. * @deprecated use {@link #onlineExecutors}. Note {@code totalExecutors==onlineExecutors} for backward * compatibility support. */ @Exported @Deprecated public final MultiStageTimeSeries totalExecutors; /** * Number of {@link hudson.model.Queue.BuildableItem}s that can run on any node in this node set but blocked. */ @Exported public final MultiStageTimeSeries queueLength; protected LoadStatistics(int initialOnlineExecutors, int initialBusyExecutors) { this.definedExecutors = new MultiStageTimeSeries(Messages._LoadStatistics_Legends_DefinedExecutors(), ColorPalette.YELLOW, initialOnlineExecutors, DECAY); this.onlineExecutors = new MultiStageTimeSeries( Messages._LoadStatistics_Legends_OnlineExecutors(), ColorPalette.BLUE, initialOnlineExecutors,DECAY); this.connectingExecutors = new MultiStageTimeSeries(Messages._LoadStatistics_Legends_ConnectingExecutors(), ColorPalette.YELLOW, 0, DECAY); this.busyExecutors = new MultiStageTimeSeries( Messages._LoadStatistics_Legends_BusyExecutors(), ColorPalette.RED, initialBusyExecutors,DECAY); this.idleExecutors = new MultiStageTimeSeries(Messages._LoadStatistics_Legends_IdleExecutors(), ColorPalette.YELLOW, initialOnlineExecutors - initialBusyExecutors, DECAY); this.availableExecutors = new MultiStageTimeSeries(Messages._LoadStatistics_Legends_AvailableExecutors(), ColorPalette.YELLOW, initialOnlineExecutors - initialBusyExecutors, DECAY); this.queueLength = new MultiStageTimeSeries( Messages._LoadStatistics_Legends_QueueLength(),ColorPalette.GREY, 0, DECAY); this.totalExecutors = onlineExecutors; modern = isModern(getClass()); } /*package*/ static boolean isModern(Class<? extends LoadStatistics> clazz) { // cannot use Util.isOverridden as these are protected methods. boolean hasGetNodes = false; boolean hasMatches = false; while (clazz != LoadStatistics.class && clazz != null && !(hasGetNodes && hasMatches)) { if (!hasGetNodes) { try { final Method getNodes = clazz.getDeclaredMethod("getNodes"); hasGetNodes = !Modifier.isAbstract(getNodes.getModifiers()); } catch (NoSuchMethodException e) { // ignore } } if (!hasMatches) { try { final Method getNodes = clazz.getDeclaredMethod("matches", Queue.Item.class, SubTask.class); hasMatches = !Modifier.isAbstract(getNodes.getModifiers()); } catch (NoSuchMethodException e) { // ignore } } if (!(hasGetNodes && hasMatches) && LoadStatistics.class.isAssignableFrom(clazz.getSuperclass())) { clazz = (Class<? extends LoadStatistics>) clazz.getSuperclass(); } } return hasGetNodes && hasMatches; } /** * @deprecated use {@link #idleExecutors} directly. */ @Deprecated public float getLatestIdleExecutors(TimeScale timeScale) { return idleExecutors.pick(timeScale).getLatest(); } /** * Computes the # of idle executors right now and obtains the snapshot value. * @deprecated use {@link #computeSnapshot()} and then {@link LoadStatisticsSnapshot#getIdleExecutors()} */ @Deprecated public abstract int computeIdleExecutors(); /** * Computes the # of total executors right now and obtains the snapshot value. * @deprecated use {@link #computeSnapshot()} and then {@link LoadStatisticsSnapshot#getOnlineExecutors()} */ @Deprecated public abstract int computeTotalExecutors(); /** * Computes the # of queue length right now and obtains the snapshot value. * @deprecated use {@link #computeSnapshot()} and then {@link LoadStatisticsSnapshot#getQueueLength()} */ @Deprecated public abstract int computeQueueLength(); /** * Creates a trend chart. */ public JFreeChart createChart(CategoryDataset ds) { final JFreeChart chart = ChartFactory.createLineChart(null, // chart title null, // unused null, // range axis label ds, // 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)); configureRenderer(renderer); 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; } protected void configureRenderer(LineAndShapeRenderer renderer) { renderer.setSeriesPaint(0, ColorPalette.BLUE); // online renderer.setSeriesPaint(1, ColorPalette.RED); // busy renderer.setSeriesPaint(2, ColorPalette.GREY); // queue renderer.setSeriesPaint(3, ColorPalette.YELLOW);// available } /** * Creates {@link CategoryDataset} which then becomes the basis * of the load statistics graph. */ public TrendChart createTrendChart(TimeScale timeScale) { return MultiStageTimeSeries.createTrendChart(timeScale,onlineExecutors,busyExecutors,queueLength,availableExecutors); } /** * Generates the load statistics graph. */ public TrendChart doGraph(@QueryParameter String type) throws IOException { return createTrendChart(TimeScale.parse(type)); } public Api getApi() { return new Api(this); } /** * @deprecated use {@link #updateCounts(LoadStatisticsSnapshot)} */ @Deprecated protected void updateExecutorCounts() { updateCounts(computeSnapshot()); } /** * Updates all the series from the current snapshot. * @param current the current snapshot. * @since 1.607 */ protected void updateCounts(LoadStatisticsSnapshot current) { definedExecutors.update(current.getDefinedExecutors()); onlineExecutors.update(current.getOnlineExecutors()); connectingExecutors.update(current.getConnectingExecutors()); busyExecutors.update(current.getBusyExecutors()); idleExecutors.update(current.getIdleExecutors()); availableExecutors.update(current.getAvailableExecutors()); queueLength.update(current.getQueueLength()); } /** * Returns the {@link Node} instances that this statistic counts. * @return the {@link Node} * @since 1.607 */ protected abstract Iterable<Node> getNodes(); /** * Returns {@code true} is the specified {@link SubTask} from the {@link Queue} should be counted. * @param item the {@link Queue.Item} that the {@link SubTask belongs to} * @param subTask the {@link SubTask} * @return {@code true} IFF the specified {@link SubTask} from the {@link Queue} should be counted. * @since 1.607 */ protected abstract boolean matches(Queue.Item item, SubTask subTask); /** * Computes a self-consistent snapshot of the load statistics. * * Note: The original method of computing load statistics would compute the total and idle counts independently * which could lead to counting errors while jobs started in between the different state counting operations. * By returning a {@link LoadStatisticsSnapshot} we get a single consistent view of the counts which was valid * for at least one point in time during the execution of this method. * * @return a self-consistent snapshot of the load statistics. * @since 1.607 */ public LoadStatisticsSnapshot computeSnapshot() { if (modern) { return computeSnapshot(Jenkins.getInstance().getQueue().getBuildableItems()); } else { int t = computeTotalExecutors(); int i = computeIdleExecutors(); return new LoadStatisticsSnapshot(t, t, Math.max(i-t,0), Math.max(t-i,0), i, i, computeQueueLength()); } } /** * Computes the self-consistent snapshot with the specified queue items. * @param queue the queue items. * @return a self-consistent snapshot of the load statistics. * @since 1.607 */ protected LoadStatisticsSnapshot computeSnapshot(Iterable<Queue.BuildableItem> queue) { final LoadStatisticsSnapshot.Builder builder = LoadStatisticsSnapshot.builder(); final Iterable<Node> nodes = getNodes(); if (nodes != null) { for (Node node : nodes) { builder.with(node); } } int q = 0; if (queue != null) { for (Queue.BuildableItem item : queue) { for (SubTask st : item.task.getSubTasks()) { if (matches(item, st)) q++; } } } return builder.withQueueLength(q).build(); } /** * With 0.90 decay ratio for every 10sec, half reduction is about 1 min. * * Put differently, the half reduction time is {@code CLOCK*log(0.5)/log(DECAY)} */ public static final float DECAY = Float.parseFloat(SystemProperties.getString(LoadStatistics.class.getName()+".decay","0.9")); /** * Load statistics clock cycle in milliseconds. Specify a small value for quickly debugging this feature and node provisioning through cloud. */ public static int CLOCK = SystemProperties.getInteger(LoadStatistics.class.getName() + ".clock", 10 * 1000); /** * Periodically update the load statistics average. */ @Extension @Symbol("loadStatistics") public static class LoadStatisticsUpdater extends PeriodicWork { public long getRecurrencePeriod() { return CLOCK; } protected void doRun() { Jenkins j = Jenkins.getInstance(); List<Queue.BuildableItem> bis = j.getQueue().getBuildableItems(); // update statistics on agents for( Label l : j.getLabels() ) { l.loadStatistics.updateCounts(l.loadStatistics.computeSnapshot(bis)); } // update statistics of the entire system j.unlabeledLoad.updateCounts(j.unlabeledLoad.computeSnapshot(bis)); j.overallLoad.updateCounts(j.overallLoad.computeSnapshot(bis)); } private int count(List<Queue.BuildableItem> bis, Label l) { int q=0; for (Queue.BuildableItem bi : bis) { for (SubTask st : Tasks.getSubTasksOf(bi.task)) if (bi.getAssignedLabelFor(st)==l) q++; } return q; } } /** * Holds a snapshot of the current statistics. * @since 1.607 */ @ExportedBean public static class LoadStatisticsSnapshot implements Serializable { private static final long serialVersionUID = 1L; /** * The total number of executors that Jenkins currently knows, this includes all off-line agents. */ private final int definedExecutors; /** * The total number of executors that are currently on-line. */ private final int onlineExecutors; /** * The total number of executors that are currently in the process of connecting to Jenkins. */ private final int connectingExecutors; /** * The total number of executors that are currently busy running jobs. */ private final int busyExecutors; /** * The total number of executors that are currently on-line and idle. This includes executors that are * not accepting tasks. */ private final int idleExecutors; /** * The total number of executors that are currently on-line, idle and accepting tasks. */ private final int availableExecutors; /** * The number of items in the queue. */ private final int queueLength; private LoadStatisticsSnapshot( int definedExecutors, int onlineExecutors, int connectingExecutors, int busyExecutors, int idleExecutors, int availableExecutors, int queueLength) { this.definedExecutors = definedExecutors; this.onlineExecutors = onlineExecutors; this.connectingExecutors = connectingExecutors; // assert definedExecutors == onlineExecutors + connectingExecutors; this.busyExecutors = busyExecutors; this.idleExecutors = idleExecutors; // assert onlineExecutors == busyExecutors + idleExecutors; this.availableExecutors = availableExecutors; // assert availableExecutors <= idleExecutors; this.queueLength = queueLength; } /** * The total number of executors that Jenkins currently knows, this includes all off-line agents. */ @Exported public int getDefinedExecutors() { return definedExecutors; } /** * The total number of executors that are currently on-line. */ @Exported public int getOnlineExecutors() { return onlineExecutors; } /** * The total number of executors that are currently in the process of connecting to Jenkins. */ @Exported public int getConnectingExecutors() { return connectingExecutors; } /** * The total number of executors that are currently busy running jobs. */ @Exported public int getBusyExecutors() { return busyExecutors; } /** * The total number of executors that are currently on-line and idle. This includes executors that are * not accepting tasks. */ @Exported public int getIdleExecutors() { return idleExecutors; } /** * The total number of executors that are currently on-line, idle and accepting tasks. */ @Exported public int getAvailableExecutors() { return availableExecutors; } /** * The number of items in the queue. */ @Exported public int getQueueLength() { return queueLength; } /** {@inheritDoc} */ @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } LoadStatisticsSnapshot that = (LoadStatisticsSnapshot) o; if (availableExecutors != that.availableExecutors) { return false; } if (busyExecutors != that.busyExecutors) { return false; } if (connectingExecutors != that.connectingExecutors) { return false; } if (definedExecutors != that.definedExecutors) { return false; } if (idleExecutors != that.idleExecutors) { return false; } if (onlineExecutors != that.onlineExecutors) { return false; } if (queueLength != that.queueLength) { return false; } return true; } /** {@inheritDoc} */ @Override public int hashCode() { int result = definedExecutors; result = 31 * result + onlineExecutors; result = 31 * result + connectingExecutors; result = 31 * result + busyExecutors; result = 31 * result + idleExecutors; result = 31 * result + availableExecutors; result = 31 * result + queueLength; return result; } /** {@inheritDoc} */ @Override public String toString() { final StringBuilder sb = new StringBuilder("LoadStatisticsSnapshot{"); sb.append("definedExecutors=").append(definedExecutors); sb.append(", onlineExecutors=").append(onlineExecutors); sb.append(", connectingExecutors=").append(connectingExecutors); sb.append(", busyExecutors=").append(busyExecutors); sb.append(", idleExecutors=").append(idleExecutors); sb.append(", availableExecutors=").append(availableExecutors); sb.append(", queueLength=").append(queueLength); sb.append('}'); return sb.toString(); } /** * Use a builder so we can add more stats if needed. * Not thread safe * @since 1.607 */ public static class Builder { private int definedExecutors; private int onlineExecutors; private int connectingExecutors; private int busyExecutors; private int idleExecutors; private int availableExecutors; private int queueLength; public LoadStatisticsSnapshot build() { return new LoadStatisticsSnapshot( definedExecutors, onlineExecutors, connectingExecutors, busyExecutors, idleExecutors, availableExecutors, queueLength ); } public Builder withQueueLength(int queueLength) { this.queueLength = queueLength; return this; } public Builder with(@CheckForNull Node node) { if (node != null) { return with(node.toComputer()); } return this; } public Builder with(@CheckForNull Computer computer) { if (computer == null) { return this; } if (computer.isOnline()) { final List<Executor> executors = computer.getExecutors(); final boolean acceptingTasks = computer.isAcceptingTasks(); for (Executor e : executors) { definedExecutors++; onlineExecutors++; if (e.getCurrentWorkUnit() != null) { busyExecutors++; } else { idleExecutors++; if (acceptingTasks) availableExecutors++; } } } else { final int numExecutors = computer.getNumExecutors(); definedExecutors += numExecutors; if (computer.isConnecting()) { connectingExecutors += numExecutors; } } return this; } } public static Builder builder() { return new Builder(); } } }