/* * Part of the CCNx Java Library. * * Copyright (C) 2011 Palo Alto Research Center, Inc. * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 2.1 * as published by the Free Software Foundation. * This library 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 * Lesser General Public License for more details. You should have received * a copy of the GNU Lesser General Public License along with this library; * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, * Fifth Floor, Boston, MA 02110-1301 USA. */ package org.ccnx.ccn.impl; import java.util.concurrent.atomic.AtomicLong; import org.ccnx.ccn.impl.CCNStats.CCNEnumStats.IStatsEnum; /** * This is the base class for performance counters. It is the public API that a user * would access to check or display performance counters. A class that counts statistics * should implement CCNStatistics. * * A typical usage is illustrated in ExampleClassWithStatistics at the end of this * module. By using the helper class CCNEnumStats, a class with statistics only * needs to define an enum of the stats it will use, and create a CCNEnumStats * member with reference to that Enum. * * Usage example is also shown in org.ccnx.ccn.test.impl.CCNStatsTest.java * * The high-level abstractions are CCNStats and CCNStatistics. A typical user would * only deal with those two things. * * The mid-level abstractions are CCNEnumStats and IStatsEnum. These are for the * developer of a class that uses CCNStats. The developer could, of course, do any * implementation that implements CCNStatistics. The CCNEnumStats abstraction is * to simplify the process so the developer only needs to define an Enum of the * counters they want to use, and pass that to CCNEnumStats. The Enum must * implement IStatsEnum. After that, the usage is very simple and automagically * gives the CCNStats object needed by the CCNStatistics interface. * * The interface CCNCategoriezedStatistics is for modules that keep statistics * in several cateogries (e.g. for each ContentName you express an interest for). * * Added support for an "averaging" counter. This will track the sum, sum^2, and * count so one can get an average and standard deviation. Basically, there's * a simple "long" counter and an "averaging counter" for each Enum, so you could * use the normal "increment(item)" or the new "addSample(item, value)" calls on any * of the Enums. If you call addSample(item, value), then the item "item" will be * tagged as an averaging stat and the toString() method will format it as such. * * Might want to add an EWMA type counter too. I think we'll want to expand the * IStatsEnum to make it take a counter type argument. */ public abstract class CCNStats { public interface CCNStatistics { public CCNStats getStats(); } public interface CCNCategorizedStatistics { /** * Get a list of the category keys. The toString() method * of the key should be meaningful. */ public Object [] getCategoryNames(); /** * Get the statistics associated with the key * @param name * @return May be null if #category is not found * @throws ClassCastException if #cateogry is not appropriate */ public CCNStats getStatsByName(Object category) throws ClassCastException; } // =========================================================== // This is the public API for a user /** * If enabled, gather stats, otherwise do not * @param enabled */ public abstract void setEnabled(boolean enabled); /** * Return a list of statistic counter names, in the preferred display order * @return */ public abstract String [] getCounterNames(); /** * Is the counter an averaging counter? This will only function * correctly after the system is run for a while and we see if * it is called with increment or addsample. */ public abstract boolean isAveragingCounter(String name) throws IllegalArgumentException; /** * Return the value of a counter * @param name * @return * @throws IllegalArgumentException if name unrecognized */ public abstract long getCounter(String name) throws IllegalArgumentException; /** * Return the average and standard deviation of a counter. You * need to have been accumulating samples with the addSample(item, value) * method. * * @param name * @return [avg, stdev] array. May be NaN. * @throws IllegalArgumentException if name unrecognized */ public abstract double[] getAverageAndStdev(String name) throws IllegalArgumentException; /** * Return a text description of the units of the counter (e.g. packets, packets per second) * @param name * @return * @throws IllegalArgumentException if name unrecognized */ public abstract String getCounterUnits(String name) throws IllegalArgumentException; /** * Reset all counters to zero */ public abstract void clearCounters(); /** * Dump the counters in the preferred format to a String for display */ public abstract String toString(); // ======================================================================================= // Everything below here is helpers for a developer creating a class that // implements CCNStatistics /** * This is a helper class for implementing statistics */ public static class CCNEnumStats<K extends Enum<K>> extends CCNStats { /** * The statistics Enum used by "K extends Enum<K>" must implement this * interface. See the example in ExampleClassWithStatistics below for * how to do this. */ public interface IStatsEnum { /** * Given the enum string, return its index value */ public int getIndex(String name) throws IllegalArgumentException; /** * Given an index value, return the name */ public String getName(int index) throws ArrayIndexOutOfBoundsException; /** * Return the units of the count */ public String getUnits(int index) throws ArrayIndexOutOfBoundsException; /** * Return a short description of the counter */ public String getDescription(int index) throws ArrayIndexOutOfBoundsException; /** * Return all counter names */ public String [] getNames(); } public CCNEnumStats(IStatsEnum stats) { _resolver = stats; int size = _resolver.getNames().length; _counters = new AtomicLong[size]; _avgcounters = new AveragingCounter[size]; for(int i = 0; i < size; i++) { _counters[i] = new AtomicLong(0); _avgcounters[i] = new AveragingCounter(); } } @Override public void clearCounters() { for(AtomicLong al : _counters) al.set(0); for(AveragingCounter ac : _avgcounters) ac.clear(); } @Override public boolean isAveragingCounter(String name) throws IllegalArgumentException { int index = _resolver.getIndex(name); return _avgcounters[index].getCount() > 0; } @Override public long getCounter(String name) throws IllegalArgumentException { int index = _resolver.getIndex(name); return _counters[index].get(); } @Override public double[] getAverageAndStdev(String name) throws IllegalArgumentException { int index = _resolver.getIndex(name); return _avgcounters[index].getAverageAndDeviation(); } @Override public String [] getCounterNames() { return _resolver.getNames(); } @Override public String getCounterUnits(String name) throws IllegalArgumentException { int index = _resolver.getIndex(name); return _resolver.getUnits(index); } @Override public void setEnabled(boolean enabled) { _enabled = enabled; } @Override public String toString() { // figure out a width int width = 1; for(int i = 0; i < _counters.length; i++) { String key = _resolver.getName(i); if( key.length() > width ) width = key.length(); } String format = String.format("%%-%ds", width); // we should cache this and use a dirty flag StringBuilder sb = new StringBuilder(); for(int i = 0; i < _counters.length; i++) { String key = _resolver.getName(i); String units = _resolver.getUnits(i); String description = _resolver.getDescription(i); sb.append(String.format(format, key)); sb.append(": "); // if we have been accumulating an avg/std, then return it // as that, otherwise return it as a counter. if( _avgcounters[i].getCount() > 0 ) { sb.append(_avgcounters[i].toString()); } else { sb.append(_counters[i].get()); } sb.append(" ("); sb.append(units); sb.append(") "); sb.append(description); sb.append('\n'); } return sb.toString(); } public void increment(K key) { increment(key, 1); } public void increment(K key, int value) { if(_enabled) { _counters[key.ordinal()].addAndGet(value); } } /** * Add a sample to the averaging counter for the key. This * make the key an averaging counter as reported by toString(). */ public void addSample(K key, long value) { if(_enabled) { _avgcounters[key.ordinal()].addSample(value); } } // ======================= protected final AtomicLong [] _counters; protected final IStatsEnum _resolver; protected boolean _enabled = true; protected final AveragingCounter [] _avgcounters; /** * This is used to track an averaging counter. * This is a thread-safe class, so will work like * the AtomicLong. * * Returns the mean (sum/count) and sample standard * deviation. The sample standard deviation is: * * 1/(N-1) * Sum(x_i - mean)^2 = N/(N-1) * ( 1/N * sum^2 - mean^2) */ private static class AveragingCounter { public AveragingCounter() { clear(); } public synchronized void addSample(long sample) { _sum += sample; _sum2+= sample * sample; _count++; _dirty = true; } public synchronized void clear() { _sum = 0; _sum2= 0; _count=0; _dirty = true; } /** * returns the [average, stdev] pair. Both may be NaN if there * are not enough samples (need 1 for avg, 2 for stdev). */ public synchronized double[] getAverageAndDeviation() { if(_dirty) { calculate(); } // return a copy so values inside the array cannot be modified double[] out = new double[2]; out[0] = _avg; out[1] = _std; return out; } public synchronized long getCount() { return _count; } public synchronized String toString() { if(_dirty) { calculate(); } return _string; } // ============================ private boolean _dirty = false; private long _sum; private long _sum2; private long _count; private double _avg, _std; private String _string; // must be called in a synchronized method private void calculate() { _avg = Double.NaN; _std = Double.NaN; if( _count > 0 ) { _avg = (double) _sum / _count; if( _count > 1 ) { double inner = (double) _sum2 / _count - (_avg * _avg); double var = _count/(_count-1) * inner ; _std = Math.sqrt(var); } } _string = String.format("avg %.3g stdev %.3g", _avg, _std); _dirty = false; } } } public static class ExampleClassWithStatistics implements CCNStatistics { public enum MyStats implements IStatsEnum { // ============================================= // The properties are of the format: // EnumProperty ("units", "short description") // To use in a different class, just copy-and-paste, then change this section SendRequests ("packets", "The number of packets sent"), RecvMessages ("packets", "The number of packets received"), SendRate ("packets per second", "The average of packet/sec transmits"), BytesPerPacket ("bytes per packet", "The average of bytes per packet transmits"); // ============================================ // Everything below here is the internal implementation for the // IStatsEnum interface. You should not need to change anything protected final String _units; protected final String _description; protected final static String [] _names; static { _names = new String[MyStats.values().length]; for(MyStats stat : MyStats.values() ) _names[stat.ordinal()] = stat.toString(); } MyStats(String units, String description) { _units = units; _description = description; } public String getDescription(int index) { return MyStats.values()[index]._description; } public int getIndex(String name) { MyStats x = MyStats.valueOf(name); return x.ordinal(); } public String getName(int index) { return MyStats.values()[index].toString(); } public String getUnits(int index) { return MyStats.values()[index]._units; } public String [] getNames() { return _names; } } /** * Instantiate our stats counter. Note that we need to pass a Java generic type * for our Enum and then pass an instance of the Enum to the constructor. It does * not matter which enum value we pass to the constructor, it just needs a concrete * object it can reference. */ CCNEnumStats<MyStats> _stats = new CCNEnumStats<MyStats>(MyStats.SendRequests); // These are example methods showing the typical calling conventions // track the last time we sent a packet to get the packets/sec counter. private long _lastsend = System.currentTimeMillis() / 1000; private long _pktcount = 0; public void send(Object o, int len) { _stats.increment(MyStats.SendRequests); // some example proxy for byte size _stats.addSample(MyStats.BytesPerPacket, len); long now = System.currentTimeMillis() / 1000; // how many seconds has it been? long secs = now - _lastsend; _lastsend = now; _pktcount ++; if( secs > 0 ) { // integer pps. long value = _pktcount / secs; _stats.addSample(MyStats.SendRate, value); _pktcount = 0; } } public void recv(Object o) { _stats.increment(MyStats.RecvMessages); } /** * Implement the IStatsEnum interface */ public CCNStats getStats() { return _stats; } } }