/* This code is part of Freenet. It is distributed under the GNU General * Public License, version 2 (or at your option any later version). See * http://www.gnu.org/ for further details of the GPL. */ package freenet.support.math; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import freenet.node.TimeSkewDetectorCallback; import freenet.support.Logger; import freenet.support.SimpleFieldSet; import freenet.support.Logger.LogLevel; /** * Time decaying running average. * * Decay factor = 0.5 ^ (interval / halflife). * * So if the interval is exactly the half-life then reporting 0 will halve the value. * * Note that the older version has a half life on the influence of any given report without taking * into account the fact that reports persist and accumulate. :) * */ public final class TimeDecayingRunningAverage implements RunningAverage, Cloneable { private static final long serialVersionUID = -1; static final int MAGIC = 0x5ff4ac94; @Override public final TimeDecayingRunningAverage clone() { // Override clone to synchronize, as per comments in RunningAverage. // Implement Cloneable to shut up findbugs. synchronized(this) { return new TimeDecayingRunningAverage(this); } } double curValue; final double halfLife; long lastReportTime; long createdTime; long totalReports; boolean started; double defaultValue; double minReport; double maxReport; boolean logDEBUG; private final TimeSkewDetectorCallback timeSkewCallback; @Override public String toString() { long now = System.currentTimeMillis(); synchronized(this) { return super.toString() + ": currentValue="+curValue+", halfLife="+halfLife+ ", lastReportTime="+(now - lastReportTime)+ "ms ago, createdTime="+(now - createdTime)+ "ms ago, totalReports="+totalReports+", started="+started+ ", defaultValue="+defaultValue+", min="+minReport+", max="+maxReport; } } /** * * @param defaultValue * @param halfLife * @param min * @param max * @param callback */ public TimeDecayingRunningAverage(double defaultValue, long halfLife, double min, double max, TimeSkewDetectorCallback callback) { curValue = defaultValue; this.defaultValue = defaultValue; started = false; this.halfLife = halfLife; createdTime = lastReportTime = System.currentTimeMillis(); this.minReport = min; this.maxReport = max; totalReports = 0; logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this); if(logDEBUG) Logger.debug(this, "Created "+this, new Exception("debug")); this.timeSkewCallback = callback; } /** * * @param defaultValue * @param halfLife * @param min * @param max * @param fs * @param callback */ public TimeDecayingRunningAverage(double defaultValue, long halfLife, double min, double max, SimpleFieldSet fs, TimeSkewDetectorCallback callback) { curValue = defaultValue; this.defaultValue = defaultValue; started = false; this.halfLife = halfLife; createdTime = System.currentTimeMillis(); this.lastReportTime = -1; // long warm-up may skew results, so lets wait for the first report this.minReport = min; this.maxReport = max; totalReports = 0; logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this); if(logDEBUG) Logger.debug(this, "Created "+this, new Exception("debug")); if(fs != null) { started = fs.getBoolean("Started", false); if(started) { curValue = fs.getDouble("CurrentValue", curValue); if(curValue > maxReport || curValue < minReport || Double.isNaN(curValue)) { curValue = defaultValue; totalReports = 0; createdTime = System.currentTimeMillis(); } else { totalReports = fs.getLong("TotalReports", 0); long uptime = fs.getLong("Uptime", 0); createdTime = System.currentTimeMillis() - uptime; } } } this.timeSkewCallback = callback; } /** * * @param defaultValue * @param halfLife * @param min * @param max * @param dis * @param callback * @throws IOException */ public TimeDecayingRunningAverage(double defaultValue, double halfLife, double min, double max, DataInputStream dis, TimeSkewDetectorCallback callback) throws IOException { int m = dis.readInt(); if(m != MAGIC) throw new IOException("Invalid magic "+m); int v = dis.readInt(); if(v != 1) throw new IOException("Invalid version "+v); curValue = dis.readDouble(); if(Double.isInfinite(curValue) || Double.isNaN(curValue)) throw new IOException("Invalid weightedTotal: "+curValue); if((curValue < min) || (curValue > max)) throw new IOException("Out of range: curValue = "+curValue); started = dis.readBoolean(); long priorExperienceTime = dis.readLong(); this.halfLife = halfLife; this.minReport = min; this.maxReport = max; this.defaultValue = defaultValue; logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this); lastReportTime = -1; createdTime = System.currentTimeMillis() - priorExperienceTime; totalReports = dis.readLong(); this.timeSkewCallback = callback; } /** * * @param a */ public TimeDecayingRunningAverage(TimeDecayingRunningAverage a) { this.createdTime = a.createdTime; this.defaultValue = a.defaultValue; this.halfLife = a.halfLife; this.lastReportTime = a.lastReportTime; this.maxReport = a.maxReport; this.minReport = a.minReport; this.started = a.started; this.totalReports = a.totalReports; this.curValue = a.curValue; this.timeSkewCallback = a.timeSkewCallback; } /** * * @return */ @Override public synchronized double currentValue() { return curValue; } /** * * @param d */ @Override public void report(double d) { synchronized(this) { // Must synchronize first to achieve serialization. long now = System.currentTimeMillis(); if(d < minReport) { Logger.error(this, "Impossible: "+d+" on "+this, new Exception("error")); return; } if(d > maxReport) { Logger.error(this, "Impossible: "+d+" on "+this, new Exception("error")); return; } if(Double.isInfinite(d) || Double.isNaN(d)) { Logger.error(this, "Reported infinity or NaN to "+this+" : "+d, new Exception("error")); return; } totalReports++; if(!started) { curValue = d; started = true; if(logDEBUG) Logger.debug(this, "Reported "+d+" on "+this+" when just started"); } else if(lastReportTime != -1) { // might be just serialized in long thisInterval = now - lastReportTime; long uptime = now - createdTime; if(thisInterval < 0) { Logger.error(this, "Clock (reporting) went back in time, ignoring report: "+now+" was "+lastReportTime+" (back "+(-thisInterval)+"ms)"); lastReportTime = now; if(timeSkewCallback != null) timeSkewCallback.setTimeSkewDetectedUserAlert(); return; } double thisHalfLife = halfLife; if(uptime < 0) { Logger.error(this, "Clock (uptime) went back in time, ignoring report: "+now+" was "+createdTime+" (back "+(-uptime)+"ms)"); if(timeSkewCallback != null) timeSkewCallback.setTimeSkewDetectedUserAlert(); return; // Disable sensitivity hack. // Excessive sensitivity at start isn't necessarily a good thing. // In particular it makes the average inconsistent - 20 reports of 0 at 1s intervals have a *different* effect to 10 reports of 0 at 2s intervals! // Also it increases the impact of startup spikes, which then take a long time to recover from. //} else { //double oneFourthOfUptime = uptime / 4D; //if(oneFourthOfUptime < thisHalfLife) thisHalfLife = oneFourthOfUptime; } if(thisHalfLife == 0) thisHalfLife = 1; double changeFactor = Math.pow(0.5, (thisInterval) / thisHalfLife); double oldCurValue = curValue; curValue = curValue * changeFactor /* close to 1.0 if short interval, close to 0.0 if long interval */ + (1.0 - changeFactor) * d; // FIXME remove when stop getting reports of wierd output values if(curValue < minReport || curValue > maxReport) { Logger.error(this, "curValue="+curValue+" was "+oldCurValue+" - out of range"); curValue = oldCurValue; } if(logDEBUG) Logger.debug(this, "Reported "+d+" on "+this+": thisInterval="+thisInterval+ ", halfLife="+halfLife+", uptime="+uptime+", thisHalfLife="+thisHalfLife+ ", changeFactor="+changeFactor+", oldCurValue="+oldCurValue+ ", currentValue="+currentValue()+ ", thisInterval="+thisInterval+", thisHalfLife="+thisHalfLife+ ", uptime="+uptime+", changeFactor="+changeFactor); } lastReportTime = now; } } /** * * @param d */ @Override public void report(long d) { report((double)d); } @Override public double valueIfReported(double r) { throw new UnsupportedOperationException(); } /** * * @param out * @throws IOException */ public void writeDataTo(DataOutputStream out) throws IOException { long now = System.currentTimeMillis(); synchronized(this) { out.writeInt(MAGIC); out.writeInt(1); out.writeDouble(curValue); out.writeBoolean(started); out.writeLong(totalReports); out.writeLong(now - createdTime); } } /** * * @return */ public int getDataLength() { return 4 + 4 + 8 + 8 + 1 + 8 + 8; } @Override public synchronized long countReports() { return totalReports; } /** * * @return */ public synchronized long lastReportTime() { return lastReportTime; } /** * * @param shortLived * @return */ public synchronized SimpleFieldSet exportFieldSet(boolean shortLived) { SimpleFieldSet fs = new SimpleFieldSet(shortLived); fs.putSingle("Type", "TimeDecayingRunningAverage"); fs.put("CurrentValue", curValue); fs.put("Started", started); fs.put("TotalReports", totalReports); fs.put("Uptime", System.currentTimeMillis() - createdTime); return fs; } }