package com.ausregistry.jtoolkit2.session; import com.ausregistry.jtoolkit2.Timer; import com.ausregistry.jtoolkit2.se.CommandType; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.HashMap; import java.util.ArrayList; /** * Keep track of how many commands of each type have been processed recently. * Instances of this class provide no synchronization. If multiple threads * access an instance concurrently, they must provide their own * synchronization. Specifically, concurrent invocations of * <code>increment</code> may exhibit unexpected behaviour. * * @author anthony (anthony@ausregistry.com.au) */ public class CommandCounter { /// exp number of command types in a normal session. private static final int INITIAL_TYPE_MAP_SIZE = 8; /// 95% CI value for number of commands per reset interval. private static final int INITIAL_TIME_LIST_SIZE = 3; /// Default time interval for retaining records of command processing. private static final long DEFAULT_RESET_INTERVAL = 1000; private Map<String, List<Long>> recentMap; private long resetInterval; // milliseconds private int recentTotal; private Map<String, Long> totalMap; private long total; { recentMap = new HashMap<String, List<Long>>(INITIAL_TYPE_MAP_SIZE); totalMap = new HashMap<String, Long>(INITIAL_TYPE_MAP_SIZE); recentTotal = 0; total = 0L; } /** * Construct a command counter using the default reset interval. */ public CommandCounter() { this(DEFAULT_RESET_INTERVAL); } /** * Construct a command counter which ages command processing records out * based on the given reset interval (in milliseconds). * * @param resetInterval the interval of time over which counts are taken. */ public CommandCounter(long resetInterval) { this.resetInterval = resetInterval; } /** * Increment the count of commands of the given type processed recently. * * @param type the type of command being processed. */ public void increment(CommandType type) { String name = type.getCommandName(); // list of times in milliseconds List<Long> timeList; if (totalMap.containsKey(name)) { long val = totalMap.get(name) + 1; totalMap.put(name, val); } else { totalMap.put(name, 1L); } if (recentMap.containsKey(name)) { timeList = recentMap.get(name); cleanAndCheckEmpty(timeList); } else { timeList = new ArrayList<Long>(INITIAL_TIME_LIST_SIZE); recentMap.put(name, timeList); } timeList.add(Timer.now()); recentTotal++; total++; } /** * Get the number of commands of the given type processed recently (within * the reset interval from now). */ public int getRecentCount(CommandType type) { String name = type.getCommandName(); if (!recentMap.containsKey(name)) { return 0; } else { List<Long> timeList = recentMap.get(name); boolean allRemoved = cleanAndCheckEmpty(timeList); if (allRemoved) { reset(name); return 0; } else { return timeList.size(); } } } /** * Get the total number of commands of the given type recorded by this * counter. */ public long getCount(CommandType type) { String name = type.getCommandName(); if (!totalMap.containsKey(name)) { return 0L; } else { return totalMap.get(name); } } /** * Get the total number of commands of all types recorded by this counter. */ public long getTotal() { return total; } /** * Get an approximation of the total number of commands processed * recently. This is actually an upper bound on the actual number of * commands processed recently, as the underlying per-command type lists * are not necessarily up-to-date. For the exact number, but at * significantly greater cost in execution time, use * <code>getExactTotal</code>. */ public int getRecentTotal() { return recentTotal; } /** * Get the total number of commands processed recently. This first cleans * up the underlying per-command type lists to guarantee that only recent * commands are counted in the total. */ public int getExactRecentTotal() { for (Map.Entry<String, List<Long>> entry : recentMap.entrySet()) { clean(entry.getValue(), entry.getKey()); } return recentTotal; } private void reset(String type) { recentMap.remove(type); } /** * Iterate through the list, removing old items, until an item is found * that is more recent than <code>resetInterval</code> milliseconds ago. * Since the list is naturally ordered by time, no later items need be * checked. Also, it is expected that the list will be short enough that * linear search will yield acceptable performance. * * @return Indicate whether the list is left empty upon return. That is, * whether <code>timeList.isEmpty()</code> would return true. */ private synchronized boolean cleanAndCheckEmpty(List<Long> timeList) { ListIterator<Long> listIter = timeList.listIterator(); while (listIter.hasNext()) { if (Timer.msDiff(listIter.next()) > resetInterval) { listIter.remove(); recentTotal--; } else { return false; } } return true; } /** * Perform cleanAndCheckEmpty, then <code>reset</code> the appropriate * entry in <code>recentMap</code> if the cleanup removes all records from * the list. */ private void clean(List<Long> timeList, String type) { cleanAndCheckEmpty(timeList); } }