From 6a9d8fb77cdc32b4a90e4edb026b2e85ba8b2214 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Wed, 5 Oct 2011 10:12:37 -0700 Subject: [PATCH] Migrated some utility bits from Narya. --- .../java/com/threerings/util/DefaultMap.java | 113 +++++ .../com/threerings/util/MessageBundle.java | 397 ++++++++++++++++++ .../com/threerings/util/MessageManager.java | 228 ++++++++++ .../com/threerings/util/MethodProfiler.java | 328 +++++++++++++++ .../com/threerings/util/RunningStats.java | 78 ++++ .../java/com/threerings/util/TimeUtil.java | 170 ++++++++ 6 files changed, 1314 insertions(+) create mode 100644 src/main/java/com/threerings/util/DefaultMap.java create mode 100644 src/main/java/com/threerings/util/MessageBundle.java create mode 100644 src/main/java/com/threerings/util/MessageManager.java create mode 100644 src/main/java/com/threerings/util/MethodProfiler.java create mode 100644 src/main/java/com/threerings/util/RunningStats.java create mode 100644 src/main/java/com/threerings/util/TimeUtil.java diff --git a/src/main/java/com/threerings/util/DefaultMap.java b/src/main/java/com/threerings/util/DefaultMap.java new file mode 100644 index 0000000..4edce2b --- /dev/null +++ b/src/main/java/com/threerings/util/DefaultMap.java @@ -0,0 +1,113 @@ +// +// ooo-util - a place for OOO utilities +// Copyright (C) 2011 Three Rings Design, Inc., All Rights Reserved +// http://github.com/threerings/ooo-util/blob/master/LICENSE + +package com.threerings.util; + +import java.lang.reflect.Constructor; + +import java.util.HashMap; +import java.util.Map; + +import com.google.common.base.Function; +import com.google.common.collect.ForwardingMap; +import com.google.common.collect.Maps; + +/** + * Provides a map implementation that automatically creates, and inserts into the map, a default + * value for any value retrieved via the {@link #fetch} method which has no entry for that key. + */ +public class DefaultMap extends ForwardingMap +{ + /** + * Creates a default map backed by a {@link HashMap} that creates instances of the supplied + * class (using its no-args constructor) as defaults. + */ + public static DefaultMap newInstanceHashMap (Class clazz) + { + return newInstanceMap(Maps.newHashMap(), clazz); + } + + /** + * Creates a default map backed by the supplied map that creates instances of the supplied + * class (using its no-args constructor) as defaults. + */ + public static DefaultMap newInstanceMap (Map delegate, Class clazz) + { + Function creator = newInstanceCreator(clazz); + return newMap(delegate, creator); + } + + /** + * Returns a Function that makes instances of the supplied class (using its no-args + * constructor) as default values. + */ + public static Function newInstanceCreator (Class clazz) { + + final Constructor ctor; + try { + ctor = clazz.getConstructor(); + } catch (NoSuchMethodException nsme) { + throw new IllegalArgumentException(clazz + " must have a no-args constructor."); + } + return new Function() { + public V apply (K key) { + try { + return ctor.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + } + + /** + * Creates a default map backed by a {@link HashMap} using the supplied default creator. + */ + public static DefaultMap newHashMap (Function creator) + { + return newMap(Maps.newHashMap(), creator); + } + + /** + * Creates a default map backed by the supplied map using the supplied default creator. + */ + public static DefaultMap newMap (Map delegate, Function creator) + { + return new DefaultMap(delegate, creator); + } + + /** + * Looks up the supplied key in the map, returning the value to which it is mapped. If the key + * has no mapping, a default value will be obtained for the requested key and placed into the + * map. The current and subsequent lookups will return that value. + */ + public V fetch (K key) + { + V value = get(key); + // null is a valid value, so we check containsKey() before creating a default + if (value == null && !containsKey(key)) { + put(key, value = _creator.apply(key)); + } + return value; + } + + /** + * Creates a default map backed by the supplied map using the supplied default creator. + */ + protected DefaultMap (Map delegate, Function creator) + { + _delegate = delegate; + _creator = creator; + } + + @Override // from ForwardingMap + protected Map delegate() + { + return _delegate; + } + + protected final Map _delegate; + protected final Function _creator; +} diff --git a/src/main/java/com/threerings/util/MessageBundle.java b/src/main/java/com/threerings/util/MessageBundle.java new file mode 100644 index 0000000..4ab70c5 --- /dev/null +++ b/src/main/java/com/threerings/util/MessageBundle.java @@ -0,0 +1,397 @@ +// +// ooo-util - a place for OOO utilities +// Copyright (C) 2011 Three Rings Design, Inc., All Rights Reserved +// http://github.com/threerings/ooo-util/blob/master/LICENSE + +package com.threerings.util; + +import java.text.MessageFormat; +import java.util.Collection; +import java.util.Enumeration; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +import com.samskivert.text.MessageUtil; +import com.samskivert.util.StringUtil; + +import static com.threerings.Log.log; + +/** + * A message bundle provides an easy mechanism by which to obtain translated message strings from + * a resource bundle. It uses the {@link MessageFormat} class to substitute arguments into the + * translation strings. Message bundles would generally be obtained via the {@link MessageManager}, + * but could be constructed individually if so desired. + */ +public class MessageBundle +{ + /** + * Call this to "taint" any string that has been entered by an entity outside the application + * so that the translation code knows not to attempt to translate this string when doing + * recursive translations (see {@link #xlate}). + */ + public static String taint (Object text) + { + return MessageUtil.taint(text); + } + + /** + * Composes a message key with a single argument. The message can subsequently be translated + * in a single call using {@link #xlate}. + */ + public static String compose (String key, Object arg) + { + return MessageUtil.compose(key, new Object[] { arg }); + } + + /** + * Composes a message key with an array of arguments. The message can subsequently be + * translated in a single call using {@link #xlate}. + */ + public static String compose (String key, Object... args) + { + return MessageUtil.compose(key, args); + } + + /** + * Composes a message key with an array of arguments. The message can subsequently be + * translated in a single call using {@link #xlate}. + */ + public static String compose (String key, String... args) + { + return MessageUtil.compose(key, args); + } + + /** + * A convenience method for calling {@link #compose(String,Object[])} with an array of + * arguments that will be automatically tainted (see {@link #taint}). + */ + public static String tcompose (String key, Object... args) + { + return MessageUtil.tcompose(key, args); + } + + /** + * Required for backwards compatibility. Alas. + */ + public static String tcompose (String key, Object arg) + { + return MessageUtil.tcompose(key, new Object[] { arg }); + } + + /** + * Required for backwards compatibility. Alas. + */ + public static String tcompose (String key, Object arg1, Object arg2) + { + return MessageUtil.tcompose(key, new Object[] { arg1, arg2 }); + } + + /** + * A convenience method for calling {@link #compose(String,String[])} with an array of + * arguments that will be automatically tainted (see {@link #taint}). + */ + public static String tcompose (String key, String... args) + { + return MessageUtil.tcompose(key, args); + } + + /** + * Returns a fully qualified message key which, when translated by some other bundle, will + * know to resolve and utilize the supplied bundle to translate this particular key. + */ + public static String qualify (String bundle, String key) + { + return MessageUtil.qualify(bundle, key); + } + + /** + * Returns the bundle name from a fully qualified message key. + * + * @see #qualify + */ + public static String getBundle (String qualifiedKey) + { + return MessageUtil.getBundle(qualifiedKey); + } + + /** + * Returns the unqualified portion of the key from a fully qualified message key. + * + * @see #qualify + */ + public static String getUnqualifiedKey (String qualifiedKey) + { + return MessageUtil.getUnqualifiedKey(qualifiedKey); + } + + /** + * Initializes the message bundle which will obtain localized messages from the supplied + * resource bundle. The path is provided purely for reporting purposes. + */ + public void init (MessageManager msgmgr, String path, + ResourceBundle bundle, MessageBundle parent) + { + _msgmgr = msgmgr; + _path = path; + _bundle = bundle; + _parent = parent; + } + + /** + * Obtains the translation for the specified message key. No arguments are substituted into + * the translated string. If a translation message does not exist for the specified key, an + * error is logged and the key itself is returned so that the caller need not worry about + * handling a null response. + */ + public String get (String key) + { + // if this string is tainted, we don't translate it, instead we + // simply remove the taint character and return it to the caller + if (MessageUtil.isTainted(key)) { + return MessageUtil.untaint(key); + } + + String msg = getResourceString(key); + return (msg != null) ? msg : key; + } + + /** + * Adds all messages whose key starts with the specified prefix to the supplied collection. + * + * @param includeParent if true, messages from our parent bundle (and its parent bundle, all + * the way up the chain will be included). + */ + public void getAll (String prefix, Collection messages, boolean includeParent) + { + Enumeration iter = _bundle.getKeys(); + while (iter.hasMoreElements()) { + String key = iter.nextElement(); + if (key.startsWith(prefix)) { + messages.add(get(key)); + } + } + if (includeParent && _parent != null) { + _parent.getAll(prefix, messages, includeParent); + } + } + + /** + * Adds all keys for messages whose key starts with the specified prefix to the supplied + * collection. + * + * @param includeParent if true, messages from our parent bundle (and its parent bundle, all + * the way up the chain will be included). + */ + public void getAllKeys (String prefix, Collection keys, boolean includeParent) + { + Enumeration iter = _bundle.getKeys(); + while (iter.hasMoreElements()) { + String key = iter.nextElement(); + if (key.startsWith(prefix)) { + keys.add(key); + } + } + if (includeParent && _parent != null) { + _parent.getAllKeys(prefix, keys, includeParent); + } + } + + /** + * Returns true if we have a translation mapping for the supplied key, false if not. + */ + public boolean exists (String key) + { + return getResourceString(key, false) != null; + } + + /** + * Get a String from the resource bundle, or null if there was an error. + */ + public String getResourceString (String key) + { + return getResourceString(key, true); + } + + /** + * Get a String from the resource bundle, or null if there was an error. + * + * @param key the resource key. + * @param reportMissing whether or not the method should log an error if the resource didn't + * exist. + */ + public String getResourceString (String key, boolean reportMissing) + { + try { + if (_bundle != null) { + return _bundle.getString(key); + } + } catch (MissingResourceException mre) { + // fall through and try the parent + } + + // if we have a parent, try getting the string from them + if (_parent != null) { + String value = _parent.getResourceString(key, false); + if (value != null) { + return value; + } + // if we didn't find it in our parent, we want to fall + // through and report missing appropriately + } + + if (reportMissing) { + log.warning("Missing translation message", "bundle", _path, "key", key, new Exception()); + } + + return null; + } + + /** + * Obtains the translation for the specified message key. The specified arguments are + * substituted into the translated string. + * + *

If the first argument in the array is an {@link Integer} object, a translation will be + * selected accounting for plurality in the following manner. Assume a message key of + * m.widgets, the following translations should be defined:

 m.widgets.0 =
+     * no widgets. m.widgets.1 = {0} widget. m.widgets.n = {0} widgets. 
+ * + * The specified argument is substituted into the translated string as appropriate. Consider + * using: + * + *
 m.widgets.n = {0,number,integer} widgets. 
+ * + * to obtain proper insertion of commas and dots as appropriate for the locale. + * + *

See {@link MessageFormat} for more information on how the substitution is performed. If + * a translation message does not exist for the specified key, an error is logged and the key + * itself (plus the arguments) is returned so that the caller need not worry about handling a + * null response. + */ + public String get (String key, Object... args) + { + // if this is a qualified key, we need to pass the buck to the + // appropriate message bundle + if (key.startsWith(MessageUtil.QUAL_PREFIX)) { + MessageBundle qbundle = _msgmgr.getBundle(getBundle(key)); + return qbundle.get(getUnqualifiedKey(key), args); + } + + // Select the proper suffix if our first argument can be coaxed into an integer + String suffix = getSuffix(args); + String msg = getResourceString(key + suffix, false); + + if (msg == null) { + // Playing with fire: This only works because it's the same "" reference we return + // from getSuffix() + // Don't try this at home. Keep out of reach of children. If swallowed, consult + // StringUtil.isBlank() + if (suffix != "") { + // Try the original key + msg = getResourceString(key, false); + } + + if (msg == null) { + log.warning("Missing translation message", "bundle", _path, "key", key, + new Exception()); + + // return something bogus + return (key + StringUtil.toString(args)); + } + } + + return MessageFormat.format(MessageUtil.escape(msg), args); + } + + /** + * Obtains the translation for the specified message key. The specified arguments are + * substituted into the translated string. + */ + public String get (String key, String... args) + { + return get(key, (Object[]) args); + } + + /** + * A helper function for {@link #get(String,Object[])} that allows us to automatically perform + * plurality processing if our first argument can be coaxed to an {@link Integer}. + */ + public String getSuffix (Object[] args) + { + if (args.length > 0 && args[0] != null) { + try { + int count = (args[0] instanceof Integer) ? (Integer)args[0] : + Integer.parseInt(args[0].toString()); + switch (count) { + case 0: return ".0"; + case 1: return ".1"; + default: return ".n"; + } + } catch (NumberFormatException e) { + // Fall out + } + } + return ""; + } + + /** + * Obtains the translation for the specified compound message key. A compound key contains the + * message key followed by a tab separated list of message arguments which will be substituted + * into the translation string. + * + *

See {@link MessageFormat} for more information on how the substitution is performed. If + * a translation message does not exist for the specified key, an error is logged and the key + * itself (plus the arguments) is returned so that the caller need not worry about handling a + * null response. + */ + public String xlate (String compoundKey) + { + // if this is a qualified key, we need to pass the buck to the appropriate message bundle; + // we have to do it here because we want the compound arguments of this key to be + // translated in the context of the containing message bundle qualification + if (compoundKey.startsWith(MessageUtil.QUAL_PREFIX)) { + MessageBundle qbundle = _msgmgr.getBundle(getBundle(compoundKey)); + return qbundle.xlate(getUnqualifiedKey(compoundKey)); + } + + // to be more efficient about creating unnecessary objects, we + // do some checking before splitting + int tidx = compoundKey.indexOf('|'); + if (tidx == -1) { + return get(compoundKey); + + } else { + String key = compoundKey.substring(0, tidx); + String argstr = compoundKey.substring(tidx+1); + String[] args = StringUtil.split(argstr, "|"); + // unescape and translate the arguments + for (int ii = 0; ii < args.length; ii++) { + // if the argument is tainted, do no further translation + // (it might contain |s or other fun stuff) + if (MessageUtil.isTainted(args[ii])) { + args[ii] = MessageUtil.unescape(MessageUtil.untaint(args[ii])); + } else { + args[ii] = xlate(MessageUtil.unescape(args[ii])); + } + } + return get(key, (Object[]) args); + } + } + + @Override + public String toString () + { + return "[bundle=" + _bundle + ", path=" + _path + "]"; + } + + /** The message manager via whom we'll resolve fully qualified translation strings. */ + protected MessageManager _msgmgr; + + /** The path that identifies the resource bundle we are using to obtain our messages. */ + protected String _path; + + /** The resource bundle from which we obtain our messages. */ + protected ResourceBundle _bundle; + + /** Our parent bundle if we're not the global bundle. */ + protected MessageBundle _parent; +} diff --git a/src/main/java/com/threerings/util/MessageManager.java b/src/main/java/com/threerings/util/MessageManager.java new file mode 100644 index 0000000..6433bc0 --- /dev/null +++ b/src/main/java/com/threerings/util/MessageManager.java @@ -0,0 +1,228 @@ +// +// ooo-util - a place for OOO utilities +// Copyright (C) 2011 Three Rings Design, Inc., All Rights Reserved +// http://github.com/threerings/ooo-util/blob/master/LICENSE + +package com.threerings.util; + +import java.util.HashMap; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +import com.google.common.collect.Maps; + +import com.samskivert.util.StringUtil; + +import static com.threerings.Log.log; + +/** + * The message manager provides a thin wrapper around Java's built-in localization support, + * supporting a policy of dividing up localization resources into logical units, all of the + * translations for which are contained in a single messages file. + * + *

The message manager assumes that the locale remains constant for the duration of its + * operation. If the locale were to change during the operation of the client, a call to + * {@link #setLocale} should be made to inform the message manager of the new locale (which will + * clear the message bundle cache). + */ +public class MessageManager +{ + /** + * The name of the global resource bundle (which other bundles revert to if they can't locate + * a message within themselves). It must be named global.properties and live at + * the top of the bundle hierarchy. + */ + public static final String GLOBAL_BUNDLE = "global"; + + /** + * Constructs a message manager with the supplied resource prefix and the default locale. The + * prefix will be prepended to the path of all resource bundles prior to their resolution. For + * example, if a prefix of rsrc.messages was provided and a message bundle with + * the name game.chess was later requested, the message manager would attempt to + * load a resource bundle with the path rsrc.messages.game.chess and would + * eventually search for a file in the classpath with the path + * rsrc/messages/game/chess.properties. + * + *

See the documentation for {@link ResourceBundle#getBundle(String,Locale,ClassLoader)} + * for a more detailed explanation of how resource bundle paths are resolved. + */ + public MessageManager (String resourcePrefix) + { + // keep the prefix + _prefix = resourcePrefix; + + // use the default locale + _locale = Locale.getDefault(); + log.debug("Using locale: " + _locale + "."); + + // make sure the prefix ends with a dot + if (!_prefix.endsWith(".")) { + _prefix += "."; + } + + // load up the global bundle + _global = getBundle(GLOBAL_BUNDLE); + } + + /** + * Get the locale that is being used to translate messages. This may be useful if using + * standard translations, for example new SimpleDateFormat("EEEE", getLocale()) to get the + * name of a weekday that matches the language being used for all other client translations. + */ + public Locale getLocale () + { + return _locale; + } + + /** + * Sets the locale to the specified locale. Subsequent message bundles fetched via the message + * manager will use the new locale. The message bundle cache will also be cleared. + */ + public void setLocale (Locale locale) + { + setLocale(locale, false); + } + + /** + * Sets the locale to the specified locale. Subsequent message bundles fetched via the message + * manager will use the new locale. The message bundle cache will also be cleared. + * + * @param updateGlobal set to true if you want the global bundle reloaded in the new locale + */ + public void setLocale (Locale locale, boolean updateGlobal) + { + _locale = locale; + _cache.clear(); + + if (updateGlobal) { + _global = getBundle(GLOBAL_BUNDLE); + } + } + + /** + * Sets the appropriate resource prefix for where to find subsequent message bundles. + */ + public void setPrefix (String resourcePrefix) + { + _prefix = resourcePrefix; + + // Need to reget the global bundle at the new prefix location. + _global = getBundle(GLOBAL_BUNDLE); + } + + /** + * Allows a custom classloader to be configured for locating translation resources. + */ + public void setClassLoader (ClassLoader loader) + { + _loader = loader; + } + + /** + * Fetches the message bundle for the specified path. If no bundle can be located with the + * specified path, a special bundle is returned that returns the untranslated message + * identifiers instead of an associated translation. This is done so that error code to handle + * a failed bundle load need not be replicated wherever bundles are used. Instead an error + * will be logged and the requesting service can continue to function in an impaired state. + */ + public MessageBundle getBundle (String path) + { + // first look in the cache + MessageBundle bundle = _cache.get(path); + if (bundle != null) { + return bundle; + } + + // if it's not cached, we'll need to resolve it + ResourceBundle rbundle = loadBundle(_prefix + path); + + // if the resource bundle contains a special resource, we'll interpret that as a derivation + // of MessageBundle to instantiate for handling that class + MessageBundle customBundle = null; + if (rbundle != null) { + String mbclass = null; + try { + mbclass = rbundle.getString(MBUNDLE_CLASS_KEY).trim(); + if (!StringUtil.isBlank(mbclass)) { + customBundle = (MessageBundle)Class.forName(mbclass).newInstance(); + } + + } catch (MissingResourceException mre) { + // nothing to worry about + + } catch (Throwable t) { + log.warning("Failure instantiating custom message bundle", "mbclass", mbclass, + "error", t); + } + } + + // initialize our message bundle, cache it and return it (if we couldn't resolve the + // bundle, the message bundle will cope with its null resource bundle) + bundle = createBundle(path, rbundle, customBundle); + _cache.put(path, bundle); + return bundle; + } + + /** + * Returns the bundle to use for the given path and resource bundle. If customBundle is + * non-null, it's an instance of the bundle class specified by the bundle itself and should be + * used as part of the created bundle. + */ + protected MessageBundle createBundle (String path, ResourceBundle rbundle, + MessageBundle customBundle) + { + // if there was no custom class, or we failed to instantiate the custom class, use a + // standard message bundle + if (customBundle == null) { + customBundle = new MessageBundle(); + } + initBundle(customBundle, path, rbundle); + return customBundle; + } + + /** + * Initializes the given bundle with this manager and the given path and resource bundle. + */ + protected void initBundle (MessageBundle bundle, String path, ResourceBundle rbundle) + { + bundle.init(this, path, rbundle, _global); + } + + /** + * Loads a bundle from the given path, or returns null if it can't be found. + */ + protected ResourceBundle loadBundle (String path) + { + try { + if (_loader != null) { + return ResourceBundle.getBundle(path, _locale, _loader); + } + return ResourceBundle.getBundle(path, _locale); + } catch (MissingResourceException mre) { + log.warning("Unable to resolve resource bundle", "path", path, "locale", _locale, + mre); + return null; + } + } + + /** The prefix we prepend to resource paths prior to loading. */ + protected String _prefix; + + /** The locale for which we're obtaining message bundles. */ + protected Locale _locale; + + /** A custom class loader that we use to load resource bundles. */ + protected ClassLoader _loader; + + /** A cache of instantiated message bundles. */ + protected HashMap _cache = Maps.newHashMap(); + + /** Our top-level message bundle, from which others obtain messages if + * they can't find them within themselves. */ + protected MessageBundle _global; + + /** A key that can contain the classname of a custom message bundle + * class to be used to handle messages for a particular bundle. */ + protected static final String MBUNDLE_CLASS_KEY = "msgbundle_class"; +} diff --git a/src/main/java/com/threerings/util/MethodProfiler.java b/src/main/java/com/threerings/util/MethodProfiler.java new file mode 100644 index 0000000..54e2965 --- /dev/null +++ b/src/main/java/com/threerings/util/MethodProfiler.java @@ -0,0 +1,328 @@ +// +// ooo-util - a place for OOO utilities +// Copyright (C) 2011 Three Rings Design, Inc., All Rights Reserved +// http://github.com/threerings/ooo-util/blob/master/LICENSE + +package com.threerings.util; + +import java.util.Map; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.collect.Maps; + +import com.samskivert.util.StringUtil; + +import static com.threerings.NaryaLog.log; + +/** + * Records the times that it takes to call methods. Allows one simultaneous method call per thread + * (no nested calls). Uses java's nano second timer. Results are logged by method name. + */ +public class MethodProfiler +{ + /** + * The results of sampling for a single method. + */ + public static class Result + { + /** + * Creates a new result with the given values. + */ + Result (int numSamples, double average, double stdDev) + { + this.numSamples = numSamples; + this.averageTime = average; + this.standardDeviation = stdDev; + } + + /** Number of method calls sampled. */ + public final int numSamples; + + /** Average time spent in the method. */ + public final double averageTime; + + /** Standard deviation from the average. */ + public final double standardDeviation; + + // from Object + @Override + public String toString () + { + return StringUtil.fieldsToString(this); + } + } + + /** + * Runs some very basic tests of the method profiler. + */ + public static void main (String args[]) + throws InterruptedException + { + int testNum = 0; + if (args.length > 0) { + testNum = Integer.parseInt(args[0]); + } + switch (testNum) { + case 0: // rum some rpc threads + MethodProfiler test = new MethodProfiler(); + Thread t1 = test.new TestThread("testm1", 100, 50); + Thread t2 = test.new TestThread("testm2", 100, 50); + t1.start(); + t2.start(); + t1.join(); + t2.join(); + for (Map.Entry method : test.getResults().entrySet()) { + log.info(method.getKey(), "result", method.getValue()); + } + break; + case 1: + simpleSampleTest("Single", 100); + break; + case 2: + simpleSampleTest("Triple", 100, 0, 200); + break; + case 3: + simpleSampleTest("Multi", 0, 25, 50, 100, 125, 150, 175, 200, 112.5, 112.5, 112.5); + break; + case 4: + MethodProfiler test4 = new MethodProfiler(); + test4.enter("L1a"); + test4.enter("L2a"); + test4.swap("L2b"); + test4.enter("L3a"); + test4.exit(null); + test4.exit(null); + test4.swap("L1b"); + test4.swap("L1c"); + test4.exit(null); + for (Map.Entry result : test4.getResults().entrySet()) { + log.info("Results", "name", result.getKey(), "value", result.getValue()); + } + break; + } + } + + /** + * Gets all method results so far. + */ + public Map getResults () + { + Map cmap = _profiles.asMap(); + Map results = Maps.newHashMapWithExpectedSize(cmap.size()); + for (Map.Entry entry : cmap.entrySet()) { + synchronized (entry.getValue()) { + results.put(entry.getKey(), toResult(entry.getValue())); + } + } + return results; + } + + /** + * Notes that the calling thread has entered the given method and records the time stamp. + */ + public void enter (String methodName) + { + Method method = _stack.get().push(); + method.name = methodName; + method.entryTime = System.nanoTime(); + } + + /** + * Notes that the calling thread has exited the given method and records the time delta since + * entry. The method parameter is not strictly necessary but allows some error checking. If not + * null, it must match the most recent value given to {@link #enter} for the calling thread. + */ + public void exit (String methodName) + { + long nanos = System.nanoTime(); + Method method = _stack.get().pop(); + if (method == null || (methodName != null && !methodName.equals(method.name))) { + // TODO: warn, but only once + return; + } + + long elapsed = nanos - method.entryTime; + recordTime(method.fullName(), (double)elapsed / 1000000); + method.name = null; + // Clear the ThreadLocal after we've profiled our whole stack to prevent our class loader + // from hanging around + if (_stack.get().size() == 0) { + _stack.remove(); + } + } + + /** + * Transition to a new method or segment. Exits the current one and enters the given one. + */ + public void swap (String methodName) + { + exit(null); + enter(methodName); + } + + /** + * Clears out the profile for the current thread, and invokes the exit of the top-level method. + * This allows callers to use one try... finally block in their top-level method without skewing + * the results for nested methods that may have thrown exceptions and/or not called + * {@link #exit}. + */ + public void exitAndClear (String methodName) + { + Stack stack = _stack.get(); + while (stack.size() > 1) { + stack.pop(); + } + if (stack.size() > 0) { + exit(methodName); + } + } + + /** + * Clears all recorded methods and times. + */ + public void reset () + { + _profiles.invalidateAll(); + } + + /** + * Adds a sample to our profile of the given method. + */ + protected void recordTime (String method, double elapsedMs) + { + RunningStats stats = _profiles.getUnchecked(method); + synchronized (stats) { + stats.addSample(elapsedMs); + } + } + + /** + * For testing, just calls the {@link #enter} and {@link #exit} methods at a fixed interval + * for a given number of times. + */ + protected class TestThread extends Thread + { + public TestThread (String method, int methodCount, long sleep) + { + _method = method; + _methodCount = methodCount; + _sleep = sleep; + } + + // from Runnable + @Override public void run () + { + try { + for (int ii = 0; ii < _methodCount; ++ii) { + MethodProfiler.this.enter(_method); + Thread.sleep(_sleep); + MethodProfiler.this.exit(_method); + } + } catch (InterruptedException ie) { + } + } + + protected int _methodCount; + protected String _method; + protected long _sleep; + } + + /** + * Describes what we know about an in-progress method call. + */ + protected static class Method + { + /** The name of the entered method. */ + public String name; + + /** The time the method was entered. */ + public long entryTime; + + /** The parent of the method. */ + public Method caller; + + /** + * Gets this method's name, prefixed with all parent method names separated by dots. + */ + public String fullName () + { + if (caller != null) { + return caller.fullName() + "." + name; + } + return name; + } + } + + /** + * Describes what we know about a nested set of in progress method calls. This is a fake stack + * that avoid reallocating entries, i.e. pop() == push() and push() == pop(). + */ + protected static class Stack + { + public Method push () + { + if (_size == _methods.length) { + Method []realloc = new Method[_size + 1]; + System.arraycopy(_methods, 0, realloc, 0, _size); + _methods = realloc; + _methods[_size] = new Method(); + } + _methods[_size].name = null; + _methods[_size].caller = _size > 0 ? _methods[_size - 1] : null; + return _methods[_size++]; + } + + public Method pop () + { + if (_size == 0) { + return null; + } + return _methods[--_size]; + } + + public int size () + { + return _size; + } + + protected int _size; + protected Method[] _methods = {new Method()}; + } + + /** + * Runs the method profile stat collection with the given samples and logs the result. + */ + protected static void simpleSampleTest (String name, double... samples) + { + RunningStats stats = new RunningStats(); + for (double sample : samples) { + stats.addSample(sample); + } + log.info(name, "results", toResult(stats)); + } + + /** + * Calculates the results of the the profile. + */ + protected static Result toResult (RunningStats stats) + { + return new Result(stats.getNumSamples(), stats.getMean(), stats.getStandardDeviation()); + } + + /** Set of active methods in the current thread. */ + protected ThreadLocal _stack = new ThreadLocal() { + @Override protected Stack initialValue () { + return new Stack(); + } + }; + + /** Stats by method name. */ + protected final Cache _profiles = CacheBuilder.newBuilder() + .build(new CacheLoader() { + public RunningStats load (String key) { + return new RunningStats(); + } + }); +} diff --git a/src/main/java/com/threerings/util/RunningStats.java b/src/main/java/com/threerings/util/RunningStats.java new file mode 100644 index 0000000..67ae958 --- /dev/null +++ b/src/main/java/com/threerings/util/RunningStats.java @@ -0,0 +1,78 @@ +// +// ooo-util - a place for OOO utilities +// Copyright (C) 2011 Three Rings Design, Inc., All Rights Reserved +// http://github.com/threerings/ooo-util/blob/master/LICENSE + +package com.threerings.util; + +/** + * Calculates live values for the mean, variance and standard deviation of a set of samples. + * Not thread safe! + */ +public class RunningStats +{ + /** + * Adds a new sample. + */ + public void addSample (double sample) + { + // From http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#On-line_algorithm + _numSamples++; + double deltaToOld = sample - _mean; + _mean += deltaToOld / _numSamples; + double deltaToNew = sample - _mean; + _varianceSum += deltaToOld * deltaToNew; + if (sample < _min) { + _min = sample; + } + if (sample > _max) { + _max = sample; + } + } + + /** + * Returns the minimum sample added or {@link Double#POSITIVE_INFINITY} if no samples have + * been added. + */ + public double getMin () + { + return _min; + } + + /** + * Returns the maximum sample added or {@link Double#NEGATIVE_INFINITY} if no samples have + * been added. + */ + public double getMax () + { + return _max; + } + + public double getVariance () + { + if (getNumSamples() == 0) { + return 0; + } + return _varianceSum / getNumSamples(); + } + + public int getNumSamples () + { + return _numSamples; + } + + public double getMean () + { + return _mean; + } + + public double getStandardDeviation () + { + return Math.sqrt(getVariance()); + } + + protected int _numSamples; + protected double _mean; + protected double _varianceSum; + protected double _max = Double.NEGATIVE_INFINITY, _min = Double.POSITIVE_INFINITY; +} \ No newline at end of file diff --git a/src/main/java/com/threerings/util/TimeUtil.java b/src/main/java/com/threerings/util/TimeUtil.java new file mode 100644 index 0000000..97f3bec --- /dev/null +++ b/src/main/java/com/threerings/util/TimeUtil.java @@ -0,0 +1,170 @@ +// +// ooo-util - a place for OOO utilities +// Copyright (C) 2011 Three Rings Design, Inc., All Rights Reserved +// http://github.com/threerings/ooo-util/blob/master/LICENSE + +package com.threerings.util; + +import java.util.ArrayList; + +import com.google.common.collect.Lists; + +/** + * Utility for times. + */ +public class TimeUtil +{ + /** Time unit constant. */ + public static final byte MILLISECOND = 0; + + /** Time unit constant. */ + public static final byte SECOND = 1; + + /** Time unit constant. */ + public static final byte MINUTE = 2; + + /** Time unit constant. */ + public static final byte HOUR = 3; + + /** Time unit constant. */ + public static final byte DAY = 4; + + // TODO: Weeks?, months? + protected static final byte MAX_UNIT = DAY; + + /** + * Returns (in seconds) the time elapsed between the supplied start and end timestamps (which + * must be in milliseconds). Partial seconds are truncated, not rounded. + */ + public static int elapsedSeconds (long startStamp, long endStamp) + { + if (endStamp < startStamp) { + throw new IllegalArgumentException("End time must be after start time " + + "[start=" + startStamp + ", end=" + endStamp + "]"); + } + return (int)((endStamp - startStamp)/1000L); + } + + /** + * Get a translatable string specifying the magnitude of the specified duration. Results will + * be between "1 second" and "X hours", with all times rounded to the nearest unit. "0 units" + * will never be displayed, the minimum is 1. + */ + public static String getTimeOrderString (long duration, byte minUnit) + { + return getTimeOrderString(duration, minUnit, MAX_UNIT); + } + + /** + * Get a translatable string specifying the magnitude of the specified duration, with the units + * of time bounded between the minimum and maximum specified. "0 units" will never be returned, + * the minimum is 1. + */ + public static String getTimeOrderString (long duration, byte minUnit, byte maxUnit) + { + // enforce sanity + minUnit = (byte) Math.min(minUnit, maxUnit); + maxUnit = (byte) Math.min(maxUnit, MAX_UNIT); + + for (byte uu = MILLISECOND; uu <= MAX_UNIT; uu++) { + int quantity = getQuantityPerUnit(uu); + if ((minUnit <= uu) && (duration < quantity || maxUnit == uu)) { + duration = Math.max(1, duration); + return MessageBundle.tcompose(getTransKey(uu), String.valueOf(duration)); + } + duration = Math.round(duration / quantity); + } + + // will not happen, because eventually gg will be MAX_UNIT + Thread.dumpStack(); + return null; + } + + /** + * Get a translatable string specifying the duration, down to the minimum granularity. + */ + public static String getTimeString (long duration, byte minUnit) + { + return getTimeString(duration, minUnit, false); + } + + /** + * Get a translatable string specifying the duration, down to the minimum granularity. + * + * Normally rounds down to the nearest minUnit, but optionally rounds up. + */ + public static String getTimeString (long duration, byte minUnit, boolean roundUp) + { + // sanity + minUnit = (byte) Math.min(minUnit, MAX_UNIT); + duration = Math.abs(duration); + + if (roundUp) { + long quantity = 1; + for (byte uu = MILLISECOND; uu < MAX_UNIT - 1; uu++) { + quantity *= getQuantityPerUnit(uu); + } + + if (duration % quantity > 0) { + duration += quantity; + } + } + + ArrayList list = Lists.newArrayList(); + int parts = 0; // how many parts are in the translation string? + for (byte uu = MILLISECOND; uu <= MAX_UNIT; uu++) { + int quantity = getQuantityPerUnit(uu); + if (minUnit <= uu) { + long amt = duration % quantity; + if (amt != 0) { + list.add(MessageBundle.tcompose(getTransKey(uu), String.valueOf(amt))); + parts++; + } + } + duration /= quantity; + if (duration <= 0 && parts > 0) { + break; + } + } + + if (parts == 0) { + // Wow, we didn't get ANYTHING? Okay, I guess that means it's zero of our minUnit + return MessageBundle.tcompose(getTransKey(minUnit), 0); + + } else if (parts == 1) { + return list.get(0); + + } else { + return MessageBundle.compose("m.times_" + parts, list.toArray()); + } + } + + /** + * Internal method to get the quantity for the specified unit. (Not very OO) + */ + protected static int getQuantityPerUnit (byte unit) + { + switch (unit) { + case MILLISECOND: return 1000; + case SECOND: case MINUTE: return 60; + case HOUR: return 24; + case DAY: return Integer.MAX_VALUE; + default: return -1; + } + } + + /** + * Internal method to get the translation key for the specified unit. (Not very OO) + */ + protected static String getTransKey (byte unit) + { + switch (unit) { + case MILLISECOND: return "m.millisecond"; + case SECOND: return "m.second"; + case MINUTE: return "m.minute"; + case HOUR: return "m.hour"; + case DAY: return "m.day"; + default: return null; + } + } +}