package net.i2p.stat; import java.io.IOException; import java.util.Properties; import net.i2p.data.DataHelper; /** * Simple rate calculator for periodically sampled data points - determining an * average value over a period, the number of events in that period, the maximum number * of events (using the interval between events), and lifetime data. * * If value is always a constant, you should be using Frequency instead. */ public class Rate { //private final static Log _log = new Log(Rate.class); private float _currentTotalValue; // was long, save space private int _currentEventCount; private int _currentTotalEventTime; private float _lastTotalValue; // was long, save space private int _lastEventCount; private int _lastTotalEventTime; private float _extremeTotalValue; // was long, save space private int _extremeEventCount; private int _extremeTotalEventTime; private float _lifetimeTotalValue; private long _lifetimeEventCount; private long _lifetimeTotalEventTime; private RateSummaryListener _summaryListener; private RateStat _stat; private long _lastCoalesceDate; private long _creationDate; // was long, save space private int _period; /** locked during coalesce and addData */ // private final Object _lock = new Object(); /** in the current (partial) period, what is the total value acrued through all events? */ public synchronized double getCurrentTotalValue() { return _currentTotalValue; } /** in the current (partial) period, how many events have occurred? */ public synchronized long getCurrentEventCount() { return _currentEventCount; } /** in the current (partial) period, how much of the time has been spent doing the events? */ public synchronized long getCurrentTotalEventTime() { return _currentTotalEventTime; } /** in the last full period, what was the total value acrued through all events? */ public synchronized double getLastTotalValue() { return _lastTotalValue; } /** in the last full period, how many events occurred? */ public synchronized long getLastEventCount() { return _lastEventCount; } /** in the last full period, how much of the time was spent doing the events? */ public synchronized long getLastTotalEventTime() { return _lastTotalEventTime; } /** what was the max total value acrued in any period? */ public synchronized double getExtremeTotalValue() { return _extremeTotalValue; } /** * when the max(totalValue) was achieved, how many events occurred in that period? * Note that this is not necesarily the highest event count; that isn't tracked. */ public synchronized long getExtremeEventCount() { return _extremeEventCount; } /** when the max(totalValue) was achieved, how much of the time was spent doing the events? */ public synchronized long getExtremeTotalEventTime() { return _extremeTotalEventTime; } /** since rate creation, what was the total value acrued through all events? */ public synchronized double getLifetimeTotalValue() { return _lifetimeTotalValue; } /** since rate creation, how many events have occurred? */ public synchronized long getLifetimeEventCount() { return _lifetimeEventCount; } /** since rate creation, how much of the time was spent doing the events? */ public synchronized long getLifetimeTotalEventTime() { return _lifetimeTotalEventTime; } /** when was the rate last coalesced? */ public synchronized long getLastCoalesceDate() { return _lastCoalesceDate; } /** when was this rate created? */ public synchronized long getCreationDate() { return _creationDate; } /** how large should this rate's cycle be? */ public synchronized long getPeriod() { return _period; } public RateStat getRateStat() { return _stat; } public void setRateStat(RateStat rs) { _stat = rs; } /** * A rate with period shorter than Router.COALESCE_TIME = 50*1000 has to * be manually coalesced before values are fetched from it. * @param period number of milliseconds in the period this rate deals with, min 1, max Integer.MAX_VALUE * @throws IllegalArgumentException if the period is invalid */ public Rate(long period) throws IllegalArgumentException { if (period <= 0 || period > Integer.MAX_VALUE) throw new IllegalArgumentException(); _creationDate = now(); _lastCoalesceDate = _creationDate; _period = (int) period; } /** * Create a new rate and load its state from the properties, taking data * from the data points underneath the given prefix. <p> * (e.g. prefix = "profile.dbIntroduction.60m", this will load the associated data points such * as "profile.dbIntroduction.60m.lifetimeEventCount"). The data can be exported * through store(outputStream, "profile.dbIntroduction.60m"). * * @param prefix prefix to the property entries (should NOT end with a period) * @param treatAsCurrent if true, we'll treat the loaded data as if no time has * elapsed since it was written out, but if it is false, we'll * treat the data with as much freshness (or staleness) as appropriate. * @throws IllegalArgumentException if the data was formatted incorrectly */ public Rate(Properties props, String prefix, boolean treatAsCurrent) throws IllegalArgumentException { this(1); load(props, prefix, treatAsCurrent); } /** * Accrue the data in the current period as an instantaneous event. * If value is always a constant, you should be using Frequency instead. * If you always use this call, eventDuration is always zero, * and the various get*Saturation*() and get*EventTime() methods will return zero. */ public synchronized void addData(long value) { _currentTotalValue += value; _currentEventCount++; _lifetimeTotalValue += value; _lifetimeEventCount++; } /** * Accrue the data in the current period as if the event took the specified amount of time * If value is always a constant, you should be using Frequency instead. * If eventDuration is nonzero, then the various get*Saturation*() and get*EventTime() * methods will also return nonzero. * * <pre> * There are at least 4 possible strategies for eventDuration: * * 1) eventDuration is always zero. * The various get*Saturation*() and get*EventTime() methods will return zero. * * 2) Each eventDuration is relatively small, and reflects processing time. * This is probably the original meaning of "saturation", as it allows you * to track how much time is spent gathering the stats. * get*EventTime() will be close to 0. * get*EventSaturation() will return values close to 0, * get*SaturationLimit() will return adjusted values for the totals. * * 3) The total of the eventDurations are approximately equal to total elapsed time. * get*EventTime() will be close to the period. * get*EventSaturation() will return values close to 1, * get*SaturationLimit() will return adjusted values for the totals. * * 4) Each eventDuration is not a duration at all, but someother independent data. * get*EventTime() may be used to retrieve the data. * get*EventSaturation() are probably useless. * get*SaturationLimit() are probably useless. * </pre> * * @param value value to accrue in the current period * @param eventDuration how long it took to accrue this data (set to 0 if it was instantaneous) */ public synchronized void addData(long value, long eventDuration) { _currentTotalValue += value; _currentEventCount++; _currentTotalEventTime += eventDuration; _lifetimeTotalValue += value; _lifetimeEventCount++; _lifetimeTotalEventTime += eventDuration; } /** 2s is plenty of slack to deal with slow coalescing (across many stats) */ private static final int SLACK = 2000; public void coalesce() { long now = now(); double correctedTotalValue; // for summaryListener which divides by rounded EventCount synchronized (this) { long measuredPeriod = now - _lastCoalesceDate; if (measuredPeriod < _period - SLACK) { // no need to coalesce (assuming we only try to do so once per minute) //if (_log.shouldLog(Log.DEBUG)) // _log.debug("not coalescing, measuredPeriod = " + measuredPeriod + " period = " + _period); return; } // ok ok, lets coalesce // how much were we off by? (so that we can sample down the measured values) float periodFactor = measuredPeriod / (float)_period; _lastTotalValue = _currentTotalValue / periodFactor; _lastEventCount = (int) (0.499999 + (_currentEventCount / periodFactor)); _lastTotalEventTime = (int) (_currentTotalEventTime / periodFactor); _lastCoalesceDate = now; if (_currentEventCount == 0) correctedTotalValue = 0; else correctedTotalValue = _currentTotalValue * (_lastEventCount / (double) _currentEventCount); if (_lastTotalValue >= _extremeTotalValue) { // get the most recent if identical _extremeTotalValue = _lastTotalValue; _extremeEventCount = _lastEventCount; _extremeTotalEventTime = _lastTotalEventTime; } _currentTotalValue = 0.0f; _currentEventCount = 0; _currentTotalEventTime = 0; } if (_summaryListener != null) _summaryListener.add(correctedTotalValue, _lastEventCount, _lastTotalEventTime, _period); } public void setSummaryListener(RateSummaryListener listener) { _summaryListener = listener; } public RateSummaryListener getSummaryListener() { return _summaryListener; } /** * What was the average value across the events in the last period? */ public synchronized double getAverageValue() { int lec = _lastEventCount; // avoid race NPE if ((_lastTotalValue != 0) && (lec > 0)) return _lastTotalValue / lec; return 0.0D; } /** * During the extreme period (i.e. the period with the highest total value), * what was the average value? */ public synchronized double getExtremeAverageValue() { if ((_extremeTotalValue != 0) && (_extremeEventCount > 0)) return _extremeTotalValue / _extremeEventCount; return 0.0D; } /** * What was the average value across the events since the stat was created? */ public synchronized double getLifetimeAverageValue() { if ((_lifetimeTotalValue != 0) && (_lifetimeEventCount > 0)) return _lifetimeTotalValue / _lifetimeEventCount; return 0.0D; } /** * @return the average or lifetime average depending on last event count * @since 0.9.4 */ public synchronized double getAvgOrLifetimeAvg() { if (getLastEventCount() > 0) return getAverageValue(); return getLifetimeAverageValue(); } /** * During the last period, how much of the time was spent actually processing events in proportion * to how many events could have occurred if there were no intervals? * * @return ratio, or 0 if event times aren't used */ public synchronized double getLastEventSaturation() { if ((_lastEventCount > 0) && (_lastTotalEventTime > 0)) { /*double eventTime = (double) _lastTotalEventTime / (double) _lastEventCount; double maxEvents = _period / eventTime; double saturation = _lastEventCount / maxEvents; return saturation; */ return ((double)_lastTotalEventTime) / (double)_period; } return 0.0D; } /** * During the extreme period (i.e. the period with the highest total value), * how much of the time was spent actually processing events * in proportion to how many events could have occurred if there were no intervals? * * @return ratio, or 0 if the statistic doesn't use event times */ public synchronized double getExtremeEventSaturation() { if ((_extremeEventCount > 0) && (_extremeTotalEventTime > 0)) { double eventTime = (double) _extremeTotalEventTime / (double) _extremeEventCount; double maxEvents = _period / eventTime; return _extremeEventCount / maxEvents; } return 0.0D; } /** * During the lifetime of this stat, how much of the time was spent actually processing events in proportion * to how many events could have occurred if there were no intervals? * * @return ratio, or 0 if event times aren't used */ public synchronized double getLifetimeEventSaturation() { if ((_lastEventCount > 0) && (_lifetimeTotalEventTime > 0)) { double eventTime = (double) _lifetimeTotalEventTime / (double) _lifetimeEventCount; double maxEvents = _period / eventTime; double numPeriods = getLifetimePeriods(); double avgEventsPerPeriod = _lifetimeEventCount / numPeriods; return avgEventsPerPeriod / maxEvents; } return 0.0D; } /** how many periods have we already completed? */ public synchronized long getLifetimePeriods() { long lifetime = now() - _creationDate; double periods = lifetime / (double) _period; return (long) Math.floor(periods); } /** * using the last period's rate, what is the total value that could have been sent * if events were constant? * * @return max total value, or 0 if event times aren't used */ public synchronized double getLastSaturationLimit() { if ((_lastTotalValue != 0) && (_lastEventCount > 0) && (_lastTotalEventTime > 0)) { double saturation = getLastEventSaturation(); if (saturation != 0.0D) return _lastTotalValue / saturation; return 0.0D; } return 0.0D; } /** * During the extreme period (i.e. the period with the highest total value), * what is the total value that could have been * sent if events were constant? * * @return event total at saturation, or 0 if no event times are measured */ public synchronized double getExtremeSaturationLimit() { if ((_extremeTotalValue != 0) && (_extremeEventCount > 0) && (_extremeTotalEventTime > 0)) { double saturation = getExtremeEventSaturation(); if (saturation != 0.0d) return _extremeTotalValue / saturation; return 0.0D; } return 0.0D; } /** * What was the total value, compared to the total value in * the extreme period (i.e. the period with the highest total value), * Warning- returns ratio, not percentage (i.e. it is not multiplied by 100 here) */ public synchronized double getPercentageOfExtremeValue() { if ((_lastTotalValue != 0) && (_extremeTotalValue != 0)) return _lastTotalValue / _extremeTotalValue; return 0.0D; } /** * How large was the last period's value as compared to the lifetime average value? * Warning- returns ratio, not percentage (i.e. it is not multiplied by 100 here) */ public synchronized double getPercentageOfLifetimeValue() { if ((_lastTotalValue != 0) && (_lifetimeTotalValue != 0)) { double lifetimePeriodValue = _period * (_lifetimeTotalValue / (now() - _creationDate)); return _lastTotalValue / lifetimePeriodValue; } return 0.0D; } /** * @return a thread-local temp object containing computed averages. * @since 0.9.4 */ public RateAverages computeAverages() { return computeAverages(RateAverages.getTemp(),false); } /** * @param out where to store the computed averages. * @param useLifetime whether the lifetime average should be used if * there are no events. * @return the same RateAverages object for chaining * @since 0.9.4 */ public synchronized RateAverages computeAverages(RateAverages out, boolean useLifetime) { out.reset(); final long total = _currentEventCount + _lastEventCount; out.setTotalEventCount(total); if (total <= 0) { final double avg = useLifetime ? getLifetimeAverageValue() : getAverageValue(); out.setAverage(avg); } else { if (_currentEventCount > 0) out.setCurrent( getCurrentTotalValue() / _currentEventCount ); if (_lastEventCount > 0) out.setLast( getLastTotalValue() / _lastEventCount ); out.setTotalValues(getCurrentTotalValue() + getLastTotalValue()); out.setAverage( out.getTotalValues() / total ); } return out; } public synchronized void store(String prefix, StringBuilder buf) throws IOException { PersistenceHelper.addTime(buf, prefix, ".period", "Length of the period:", _period); PersistenceHelper.addDate(buf, prefix, ".creationDate", "When was this rate created?", _creationDate); PersistenceHelper.addDate(buf, prefix, ".lastCoalesceDate", "When did we last coalesce this rate?", _lastCoalesceDate); PersistenceHelper.addDate(buf, prefix, ".currentDate", "When was this data written?", now()); PersistenceHelper.add(buf, prefix, ".currentTotalValue", "Total value of data points in the current (uncoalesced) period", _currentTotalValue); PersistenceHelper.add(buf, prefix, ".currentEventCount", "How many events have occurred in the current (uncoalesced) period?", _currentEventCount); PersistenceHelper.addTime(buf, prefix, ".currentTotalEventTime", "How much time have the events in the current (uncoalesced) period consumed?", _currentTotalEventTime); PersistenceHelper.add(buf, prefix, ".lastTotalValue", "Total value of data points in the most recent (coalesced) period", _lastTotalValue); PersistenceHelper.add(buf, prefix, ".lastEventCount", "How many events have occurred in the most recent (coalesced) period?", _lastEventCount); PersistenceHelper.addTime(buf, prefix, ".lastTotalEventTime", "How much time have the events in the most recent (coalesced) period consumed?", _lastTotalEventTime); PersistenceHelper.add(buf, prefix, ".extremeTotalValue", "Total value of data points in the most extreme period", _extremeTotalValue); PersistenceHelper.add(buf, prefix, ".extremeEventCount", "How many events have occurred in the most extreme period?", _extremeEventCount); PersistenceHelper.addTime(buf, prefix, ".extremeTotalEventTime", "How much time have the events in the most extreme period consumed?", _extremeTotalEventTime); PersistenceHelper.add(buf, prefix, ".lifetimeTotalValue", "Total value of data points since this stat was created", _lifetimeTotalValue); PersistenceHelper.add(buf, prefix, ".lifetimeEventCount", "How many events have occurred since this stat was created?", _lifetimeEventCount); PersistenceHelper.addTime(buf, prefix, ".lifetimeTotalEventTime", "How much total time was consumed by the events since this stat was created?", _lifetimeTotalEventTime); } /** * Load this rate from the properties, taking data from the data points * underneath the given prefix. * * @param prefix prefix to the property entries (should NOT end with a period) * @param treatAsCurrent if true, we'll treat the loaded data as if no time has * elapsed since it was written out, but if it is false, we'll * treat the data with as much freshness (or staleness) as appropriate. * @throws IllegalArgumentException if the data was formatted incorrectly */ public synchronized void load(Properties props, String prefix, boolean treatAsCurrent) throws IllegalArgumentException { _period = PersistenceHelper.getInt(props, prefix, ".period"); _creationDate = PersistenceHelper.getLong(props, prefix, ".creationDate"); _lastCoalesceDate = PersistenceHelper.getLong(props, prefix, ".lastCoalesceDate"); _currentTotalValue = (float)PersistenceHelper.getDouble(props, prefix, ".currentTotalValue"); _currentEventCount = PersistenceHelper.getInt(props, prefix, ".currentEventCount"); _currentTotalEventTime = (int)PersistenceHelper.getLong(props, prefix, ".currentTotalEventTime"); _lastTotalValue = (float)PersistenceHelper.getDouble(props, prefix, ".lastTotalValue"); _lastEventCount = PersistenceHelper.getInt(props, prefix, ".lastEventCount"); _lastTotalEventTime = (int)PersistenceHelper.getLong(props, prefix, ".lastTotalEventTime"); _extremeTotalValue = (float)PersistenceHelper.getDouble(props, prefix, ".extremeTotalValue"); _extremeEventCount = PersistenceHelper.getInt(props, prefix, ".extremeEventCount"); _extremeTotalEventTime = (int)PersistenceHelper.getLong(props, prefix, ".extremeTotalEventTime"); _lifetimeTotalValue = (float)PersistenceHelper.getDouble(props, prefix, ".lifetimeTotalValue"); _lifetimeEventCount = PersistenceHelper.getLong(props, prefix, ".lifetimeEventCount"); _lifetimeTotalEventTime = PersistenceHelper.getLong(props, prefix, ".lifetimeTotalEventTime"); if (treatAsCurrent) _lastCoalesceDate = now(); if (_period <= 0) throw new IllegalArgumentException("Period for " + prefix + " is invalid"); coalesce(); } /** * This is used in StatSummarizer and SummaryListener. * We base it on the stat we are tracking, not the stored data. */ @Override public synchronized boolean equals(Object obj) { if ((obj == null) || !(obj instanceof Rate)) return false; if (obj == this) return true; Rate r = (Rate) obj; if (_period != r.getPeriod() || _creationDate != r.getCreationDate()) return false; if (_stat == null && r._stat == null) return true; if (_stat != null && r._stat != null) return _stat.nameGroupDescEquals(r._stat); return false; } /** * It doesn't appear that Rates are ever stored in a Set or Map * (RateStat stores in an array) so let's make this easy. */ @Override public synchronized int hashCode() { return DataHelper.hashCode(_stat) ^ _period ^ ((int) _creationDate); } @Override public synchronized String toString() { StringBuilder buf = new StringBuilder(2048); buf.append("\n\t total value: ").append(getLastTotalValue()); buf.append("\n\t highest total value: ").append(getExtremeTotalValue()); buf.append("\n\t lifetime total value: ").append(getLifetimeTotalValue()); buf.append("\n\t # periods: ").append(getLifetimePeriods()); buf.append("\n\t average value: ").append(getAverageValue()); buf.append("\n\t highest average value: ").append(getExtremeAverageValue()); buf.append("\n\t lifetime average value: ").append(getLifetimeAverageValue()); buf.append("\n\t % of lifetime rate: ").append(100.0d * getPercentageOfLifetimeValue()); buf.append("\n\t % of highest rate: ").append(100.0d * getPercentageOfExtremeValue()); buf.append("\n\t # events: ").append(getLastEventCount()); buf.append("\n\t lifetime events: ").append(getLifetimeEventCount()); if (getLifetimeTotalEventTime() > 0) { // we have some actual event durations buf.append("\n\t % of time spent processing events: ").append(100.0d * getLastEventSaturation()); buf.append("\n\t total value if we were always processing events: ").append(getLastSaturationLimit()); buf.append("\n\t max % of time spent processing events: ").append(100.0d * getExtremeEventSaturation()); buf.append("\n\t max total value if we were always processing events: ") .append(getExtremeSaturationLimit()); } return buf.toString(); } private final static long now() { // "event time" is in the stat log (and uses Clock). // we just want sequential and stable time here, so use the OS time, since it doesn't // skew periodically return System.currentTimeMillis(); //Clock.getInstance().now(); } }