/**
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Arne Kepp, The OpenGeo, Copyright 2009
*/
package org.geowebcache.stats;
import java.time.Clock;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geowebcache.conveyor.Conveyor.CacheResult;
import org.geowebcache.util.ServletUtils;
public class RuntimeStats {
private static Log log = LogFactory.getLog(RuntimeStats.class);
final int pollInterval;
final long startTime;
// These must be multiples of POLL_INTERVAL
final int[] intervals;
final String[] intervalDescs;
int curBytes = 0;
int curRequests = 0;
long peakBytesTime = 0;
int peakBytes = 0;
long peakRequestsTime = 0;
int peakRequests = 0;
long totalBytes = 0;
long totalRequests = 0;
long totalHits;
long totalMisses;
long totalWMS;
final int[] bytes;
final int[] requests;
int ringPos = 0;
RuntimeStatsThread statsThread;
final private Clock clock;
/**
*
* @param pollInterval seconds between recording aggregate values
* @param intervals the intervals for which to report, in seconds, ascending. Each interval
* must be a multiple of the pollInterval
* @param intervalDescs the description for each of the previously defined intervals
*/
public RuntimeStats(int pollInterval, List<Integer> intervals, List<String> intervalDescs) {
this(pollInterval, intervals, intervalDescs, Clock.systemDefaultZone());
}
/**
*
* @param pollInterval seconds between recording aggregate values
* @param intervals the intervals for which to report, in seconds, ascending. Each interval
* must be a multiple of the pollInterval
* @param intervalDescs the description for each of the previously defined intervals
* @param clock the clock to use to keep track of the time
*/
public RuntimeStats(int pollInterval, List<Integer> intervals, List<String> intervalDescs, Clock clock) {
this.clock = clock;
this.startTime = this.clock.millis();
this.pollInterval = pollInterval;
if(intervals.size() != intervalDescs.size()) {
log.fatal("The interval and interval description lists must be of the same size!");
}
if(pollInterval < 1) {
log.error("poll interval cannot be less than 1 second");
}
this.intervals = new int[intervals.size()];
for(int i=0; i< intervals.size(); i++) {
int curVal = intervals.get(i);
if(curVal % pollInterval != 0) {
log.error("The interval ("+curVal+") must be a multiple of the poll interval " + pollInterval);
curVal = curVal - (curVal % pollInterval);
}
this.intervals[i] = curVal;
}
this.intervalDescs = new String[intervalDescs.size()];
for(int i=0; i< intervalDescs.size(); i++) {
this.intervalDescs[i] = intervalDescs.get(i);
}
bytes = new int[this.intervals[this.intervals.length - 1] / pollInterval];
requests = new int[this.intervals[this.intervals.length - 1] / pollInterval];
}
public void start() {
statsThread = new RuntimeStatsThread(this);
statsThread.start();
}
public void destroy() {
if(this.statsThread != null) {
statsThread.run = false;
statsThread.interrupt();
Thread.yield();
}
}
public void log(int size, CacheResult cacheResult) {
if(this.statsThread != null) {
synchronized(bytes) {
curBytes += size;
curRequests += 1;
if(cacheResult == CacheResult.HIT) {
totalHits++;
} else if(cacheResult == CacheResult.MISS) {
totalMisses++;
} else if(cacheResult == CacheResult.WMS) {
totalWMS++;
}
}
}
}
protected int[] popIntervalData() {
synchronized(bytes) {
int[] ret = {curBytes, curRequests};
curBytes = 0;
curRequests = 0;
return ret;
}
}
public String getHTMLStats() {
long runningTime = (clock.millis() - startTime) / 1000;
StringBuilder str = new StringBuilder();
str.append("<table border=\"0\" cellspacing=\"5\" class=\"stats\">");
synchronized(bytes) {
// Starting time
if(runningTime > 0) {
str.append("<tbody>");
str.append("<tr><th colspan=\"2\" scope=\"row\">Started:</th><td colspan=\"3\">");
str.append(ServletUtils.formatTimestamp(this.startTime)+ " (" + formatTimeDiff(runningTime) + ") ");
str.append("</td></tr>\n");
str.append("<tr><th colspan=\"2\" scope=\"row\">Total number of requests:</th><td colspan=\"3\">"+totalRequests);
str.append(" (" + totalRequests / (runningTime) +"/s ) ");
str.append("</td></tr>\n");
str.append("<tr><th colspan=\"2\" scope=\"row\">Total number of untiled WMS requests:</th><td colspan=\"3\">"+totalWMS);
str.append(" (" + totalWMS / (runningTime) +"/s ) ");
str.append("</td></tr>\n");
str.append("<tr><th colspan=\"2\" scope=\"row\">Total number of bytes:</th><td colspan=\"3\">"+totalBytes);
str.append(" ("+formatBits((totalBytes*8.0)/(runningTime))+") ");
str.append("</td></tr>\n");
str.append("</tbody>");
str.append("<tbody>");
} else {
str.append("<tbody>");
str.append("<tr><th colspan=\"5\">Runtime stats not yet available, try again in a few seconds.</th></tr>");
str.append("<tbody>");
}
str.append("<tr><th colspan=\"2\" scope=\"row\">Cache hit ratio:</th><td colspan=\"3\">");
if(totalHits + totalMisses > 0) {
double hitPercentage = (totalHits * 100.0) / (totalHits + totalMisses);
int rounded = (int) Math.round(hitPercentage * 100.0);
int percents = rounded / 100;
int decimals = rounded - percents * 100;
str.append( percents + "." + decimals +"% of requests");
} else {
str.append("No data");
}
str.append("</td></tr>\n");
str.append("<tr><th colspan=\"2\" scope=\"row\">Blank/KML/HTML:</th><td colspan=\"3\">");
if(totalRequests > 0) {
if(totalHits + totalMisses == 0) {
str.append("100.0% of requests");
} else {
int rounded = (int) Math.round(((totalRequests - totalHits - totalMisses - totalWMS) * 100.0) / totalRequests);
int percents = rounded / 100;
int decimals = rounded - percents * 100;
str.append( percents + "." + decimals +"% of requests");
}
} else {
str.append("No data");
}
str.append("</td></tr>\n");
str.append("</tbody>");
str.append("<tbody>");
str.append("<tr><th colspan=\"2\" scope=\"row\">Peak request rate:</th><td colspan=\"3\">");
if(totalRequests > 0) {
str.append(formatRequests( (peakRequests * 1.0) / pollInterval));
str.append(" ("+ServletUtils.formatTimestamp(peakRequestsTime)+") ");
} else {
str.append("No data");
}
str.append("</td></tr>\n");
str.append("<tr><th colspan=\"2\" scope=\"row\">Peak bandwidth:</th><td colspan=\"3\">");
if(totalRequests > 0) {
str.append(formatBits((peakBytes * 8.0) / pollInterval));
str.append(" ("+ServletUtils.formatTimestamp(peakRequestsTime)+") ");
} else {
str.append("No data");
}
str.append("</td></tr>\n");
str.append("</tbody>");
str.append("<tbody>");
str.append("<tr><th scope=\"col\">Interval</th><th scope=\"col\">Requests</th><th scope=\"col\">Rate</th><th scope=\"col\">Bytes</th><th scope=\"col\">Bandwidth</th></tr>\n");
for(int i=0; i<intervals.length; i++) {
if(runningTime < intervals[i]) {
continue;
}
String[] requests = calculateRequests(intervals[i]);
String[] bits = calculateBits(intervals[i]);
str.append("<tr><td>"
+intervalDescs[i]+"</td><td>"
+requests[0]+"</td><td>"
+requests[1]+"</td><td>"
+bits[0]+"</td><td>"
+bits[1]+"</td><td>"
+"</tr>\n");
}
str.append("</tbody>");
str.append("<tbody>");
str.append("<tr><td colspan=\"5\">All figures are "+pollInterval+" second(s) delayed and do not include HTTP overhead</td></tr>");
str.append("<tr><td colspan=\"5\">The cache hit ratio does not account for metatiling</td></tr>");
str.append("</tbody>");
}
return str.toString();
}
private String[] calculateRequests(int interval) {
int nodeCount = interval / pollInterval;
int accu = 0;
synchronized(bytes) {
int pos = ((ringPos - 1) + bytes.length) % bytes.length;
for(int i=0; i<nodeCount; i++) {
accu += requests[pos];
pos = ((pos - 1) + bytes.length) % bytes.length;
}
}
String avg = formatRequests((accu * 1.0) / interval);
String[] ret = {accu + "", avg};
return ret;
}
private String formatRequests(double requestsps) {
return Math.round(requestsps * 10.0) / 10.0 + " /s";
}
private String[] calculateBits(int interval) {
int nodeCount = interval / pollInterval;
int accu = 0;
int pos = ((ringPos - 1) + bytes.length) % bytes.length;
synchronized(bytes) {
for(int i=0; i<nodeCount; i++) {
accu += bytes[pos];
pos = ((pos - 1) + bytes.length) % bytes.length;
}
}
String avg = formatBits((accu * 8.0) / interval);
String[] ret = {accu + "", avg};
return ret;
}
private String formatBits(double bitsps) {
String avg;
if(bitsps > 1000000) {
avg = (Math.round(bitsps / 100000.0) / 10.0) + " mbps";
} else if(bitsps > 1000) {
avg = (Math.round(bitsps / 100.0) / 10.0) + " kbps";
} else {
avg = (Math.round(bitsps * 10.0) / 10.0) + " bps";
}
return avg;
}
private String formatTimeDiff(long seconds) {
if(seconds < 3600) {
return (seconds / 60) + " minutes";
} else if(seconds < 3600*48) {
return (seconds / 3600) + " hours";
} else {
return (seconds / (3600*24)) + " days";
}
}
private class RuntimeStatsThread extends Thread {
final RuntimeStats stats;
boolean run = true;
private RuntimeStatsThread(RuntimeStats runtimeStats) {
this.stats = runtimeStats;
}
public void run() {
while(run) {
try {
Thread.sleep(stats.pollInterval * 1000);
} catch (InterruptedException e) {
// /Nothing
}
updateLists();
}
}
private void updateLists() {
synchronized(bytes) {
int[] bytesRequests = stats.popIntervalData();
stats.totalBytes += bytesRequests[0];
stats.totalRequests += bytesRequests[1];
if(bytesRequests[0] > peakBytes) {
peakBytes = bytesRequests[0];
peakBytesTime = clock.millis();
}
if(bytesRequests[1] > peakRequests) {
peakRequests = bytesRequests[1];
peakRequestsTime = clock.millis();
}
bytes[ringPos] = bytesRequests[0];
requests[ringPos] = bytesRequests[1];
ringPos = (ringPos + 1) % bytes.length;
}
}
}
}