This change speeds up the ClientBundle and CssResource generators.

- Use the compiler's ResourceOracle instead of the ClassLoader to find resources
- Add a caching mechanism to ClientBundle's ResourceContext to allow ResourceGenerators to cache data between invocations.
- Update CssResourceGenerator to use the cache for obfuscated type names.

The cache in ResourceContext will be discarded whenever the TypeOracle changes or its reload count increases.

Patch by: bobv
Review by: rice

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5566 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/resources/ext/ResourceContext.java b/user/src/com/google/gwt/resources/ext/ResourceContext.java
index 409dc31..31cf03f 100644
--- a/user/src/com/google/gwt/resources/ext/ResourceContext.java
+++ b/user/src/com/google/gwt/resources/ext/ResourceContext.java
@@ -72,6 +72,19 @@
       throws UnableToCompleteException;
 
   /**
+   * Retrieve data from the ResourceContext.
+   * 
+   * @param <T> the type of data to retrieve
+   * @param key the key value passed to {@link #getCachedData}
+   * @param clazz the type to which the cached value must be assignable
+   * @return the value previously passed to {@link #putCachedData} or
+   *         <code>null</code> if the data was not found
+   * @throws ClassCastException if the cached data is not assignable to the
+   *           specified type
+   */
+  <T> T getCachedData(String key, Class<T> clazz);
+
+  /**
    * Return the interface type of the resource bundle being generated.
    */
   JClassType getClientBundleType();
@@ -97,6 +110,20 @@
   String getImplementationSimpleSourceName() throws IllegalStateException;
 
   /**
+   * Store data in the ResourceContext. ResourceGenerators may reduce the amount
+   * of recomputation performed by caching data the ResourceContext. This cache
+   * will be invalidated when the compiler's TypeOracle is refreshed or
+   * replaced. Each ResourceGenerator has an isolated view of the cache.
+   * 
+   * @param <T> the type of data being stored
+   * @param key a string key to locate the data
+   * @param value the value to store
+   * @return <code>true</code> if the cache did not previously contain the
+   *         key-value pair
+   */
+  <T> boolean putCachedData(String key, T value);
+
+  /**
    * Indicates if the runtime context supports data: urls. When data URLs are
    * supported by the context, aggregation of resource data into larger payloads
    * is discouraged, as it offers reduced benefit to the application at runtime.
diff --git a/user/src/com/google/gwt/resources/ext/ResourceGeneratorUtil.java b/user/src/com/google/gwt/resources/ext/ResourceGeneratorUtil.java
index 57a51ab..a55c2d8 100644
--- a/user/src/com/google/gwt/resources/ext/ResourceGeneratorUtil.java
+++ b/user/src/com/google/gwt/resources/ext/ResourceGeneratorUtil.java
@@ -22,6 +22,8 @@
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.typeinfo.JMethod;
 import com.google.gwt.core.ext.typeinfo.JPackage;
+import com.google.gwt.dev.resource.Resource;
+import com.google.gwt.dev.resource.ResourceOracle;
 import com.google.gwt.resources.client.ClientBundle.Source;
 
 import java.lang.annotation.Annotation;
@@ -29,12 +31,45 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Utility methods for building ResourceGenerators.
  */
 public final class ResourceGeneratorUtil {
 
+  private static class ClassLoaderLocator implements Locator {
+    private final ClassLoader classLoader;
+
+    public ClassLoaderLocator(ClassLoader classLoader) {
+      this.classLoader = classLoader;
+    }
+
+    public URL locate(String resourceName) {
+      return classLoader.getResource(resourceName);
+    }
+  }
+
+  /**
+   * Wrapper interface around different strategies for loading resource data.
+   */
+  private interface Locator {
+    URL locate(String resourceName);
+  }
+
+  private static class ResourceOracleLocator implements Locator {
+    private final Map<String, Resource> resourceMap;
+
+    public ResourceOracleLocator(ResourceOracle oracle) {
+      resourceMap = oracle.getResourceMap();
+    }
+
+    public URL locate(String resourceName) {
+      Resource r = resourceMap.get(resourceName);
+      return r == null ? null : r.getURL();
+    }
+  }
+
   /**
    * These are type names from previous APIs or from APIs with similar
    * functionality that might be confusing.
@@ -110,74 +145,16 @@
    * @throws UnableToCompleteException if ore or more of the sources could not
    *           be found. The error will be reported via the <code>logger</code>
    *           provided to this method
+   * @deprecated Loading through a ClassLoader with this method is much slower
+   *             than the other <code>findResources</code> methods which make
+   *             use of the compiler's ResourceOracle.
    */
+  @Deprecated
   public static URL[] findResources(TreeLogger logger, ClassLoader classLoader,
       ResourceContext context, JMethod method, String[] defaultSuffixes)
       throws UnableToCompleteException {
-    logger = logger.branch(TreeLogger.DEBUG, "Finding resources");
-
-    String locale;
-    try {
-      PropertyOracle oracle = context.getGeneratorContext().getPropertyOracle();
-      SelectionProperty prop = oracle.getSelectionProperty(logger, "locale");
-      locale = prop.getCurrentValue();
-    } catch (BadPropertyValueException e) {
-      locale = null;
-    }
-
-    checkForDeprecatedAnnotations(logger, method);
-
-    Source resourceAnnotation = method.getAnnotation(Source.class);
-    if (resourceAnnotation == null) {
-      if (defaultSuffixes != null) {
-        for (String extension : defaultSuffixes) {
-          logger.log(TreeLogger.SPAM, "Trying default extension " + extension);
-          URL resourceUrl = tryFindResource(classLoader,
-              getPathRelativeToPackage(method.getEnclosingType().getPackage(),
-                  method.getName() + extension), locale);
-
-          if (resourceUrl != null) {
-            return new URL[] {resourceUrl};
-          }
-        }
-      }
-      logger.log(TreeLogger.SPAM,
-          "No annotation and no hits with default extensions");
-      return new URL[0];
-    }
-
-    String[] resources = resourceAnnotation.value();
-
-    URL[] toReturn = new URL[resources.length];
-
-    boolean error = false;
-    int tagIndex = 0;
-    for (String resource : resources) {
-      // Try to find the resource relative to the package.
-      URL resourceURL = tryFindResource(classLoader, getPathRelativeToPackage(
-          method.getEnclosingType().getPackage(), resource), locale);
-
-      // If we didn't find the resource relative to the package, assume it is
-      // absolute.
-      if (resourceURL == null) {
-        resourceURL = tryFindResource(classLoader, resource, locale);
-      }
-
-      if (resourceURL == null) {
-        logger.log(TreeLogger.ERROR, "Resource " + resource
-            + " not found on classpath. Is the name specified as "
-            + "Class.getResource() would expect?");
-        error = true;
-      }
-
-      toReturn[tagIndex++] = resourceURL;
-    }
-
-    if (error) {
-      throw new UnableToCompleteException();
-    }
-
-    return toReturn;
+    return findResources(logger, new ClassLoaderLocator(classLoader), context,
+        method, defaultSuffixes);
   }
 
   /**
@@ -189,9 +166,9 @@
    * property and will attempt to use a best-match lookup by removing locale
    * components.
    * <p>
-   * The current Thread's context ClassLoader will be used to resolve resource
-   * locations. If it is necessary to alter the manner in which resources are
-   * resolved, use the overload that accepts an arbitrary ClassLoader.
+   * The compiler's ResourceOracle will be used to resolve resource locations.
+   * If it is necessary to alter the manner in which resources are resolved, use
+   * the overload that accepts an arbitrary ClassLoader.
    * 
    * @param logger a TreeLogger that will be used to report errors or warnings
    * @param context the ResourceContext in which the ResourceGenerator is
@@ -217,9 +194,9 @@
    * property and will attempt to use a best-match lookup by removing locale
    * components.
    * <p>
-   * The current Thread's context ClassLoader will be used to resolve resource
-   * locations. If it is necessary to alter the manner in which resources are
-   * resolved, use the overload that accepts an arbitrary ClassLoader.
+   * The compiler's ResourceOracle will be used to resolve resource locations.
+   * If it is necessary to alter the manner in which resources are resolved, use
+   * the overload that accepts an arbitrary ClassLoader.
    * 
    * @param logger a TreeLogger that will be used to report errors or warnings
    * @param context the ResourceContext in which the ResourceGenerator is
@@ -238,8 +215,8 @@
   public static URL[] findResources(TreeLogger logger, ResourceContext context,
       JMethod method, String[] defaultSuffixes)
       throws UnableToCompleteException {
-    return findResources(logger,
-        Thread.currentThread().getContextClassLoader(), context, method,
+    return findResources(logger, new ResourceOracleLocator(
+        context.getGeneratorContext().getResourcesOracle()), context, method,
         defaultSuffixes);
   }
 
@@ -260,6 +237,75 @@
     }
   }
 
+  private static URL[] findResources(TreeLogger logger, Locator locator,
+      ResourceContext context, JMethod method, String[] defaultSuffixes)
+      throws UnableToCompleteException {
+    logger = logger.branch(TreeLogger.DEBUG, "Finding resources");
+
+    String locale;
+    try {
+      PropertyOracle oracle = context.getGeneratorContext().getPropertyOracle();
+      SelectionProperty prop = oracle.getSelectionProperty(logger, "locale");
+      locale = prop.getCurrentValue();
+    } catch (BadPropertyValueException e) {
+      locale = null;
+    }
+
+    checkForDeprecatedAnnotations(logger, method);
+
+    Source resourceAnnotation = method.getAnnotation(Source.class);
+    if (resourceAnnotation == null) {
+      if (defaultSuffixes != null) {
+        for (String extension : defaultSuffixes) {
+          logger.log(TreeLogger.SPAM, "Trying default extension " + extension);
+          URL resourceUrl = tryFindResource(locator, getPathRelativeToPackage(
+              method.getEnclosingType().getPackage(), method.getName()
+                  + extension), locale);
+
+          if (resourceUrl != null) {
+            return new URL[] {resourceUrl};
+          }
+        }
+      }
+      logger.log(TreeLogger.SPAM,
+          "No annotation and no hits with default extensions");
+      return new URL[0];
+    }
+
+    String[] resources = resourceAnnotation.value();
+
+    URL[] toReturn = new URL[resources.length];
+
+    boolean error = false;
+    int tagIndex = 0;
+    for (String resource : resources) {
+      // Try to find the resource relative to the package.
+      URL resourceURL = tryFindResource(locator, getPathRelativeToPackage(
+          method.getEnclosingType().getPackage(), resource), locale);
+
+      // If we didn't find the resource relative to the package, assume it is
+      // absolute.
+      if (resourceURL == null) {
+        resourceURL = tryFindResource(locator, resource, locale);
+      }
+
+      if (resourceURL == null) {
+        logger.log(TreeLogger.ERROR, "Resource " + resource
+            + " not found on classpath. Is the name specified as "
+            + "Class.getResource() would expect?");
+        error = true;
+      }
+
+      toReturn[tagIndex++] = resourceURL;
+    }
+
+    if (error) {
+      throw new UnableToCompleteException();
+    }
+
+    return toReturn;
+  }
+
   /**
    * Converts a package relative path into an absolute path.
    * 
@@ -274,14 +320,14 @@
   /**
    * This performs the locale lookup function for a given resource name.
    * 
-   * @param classLoader the ClassLoader to use to load the resources
+   * @param locator the Locator to use to load the resources
    * @param resourceName the string name of the desired resource
    * @param locale the locale of the current rebind permutation
    * @return a URL by which the resource can be loaded, <code>null</code> if one
    *         cannot be found
    */
-  private static URL tryFindResource(ClassLoader classLoader,
-      String resourceName, String locale) {
+  private static URL tryFindResource(Locator locator, String resourceName,
+      String locale) {
     URL toReturn = null;
 
     // Look for locale-specific variants of individual resources
@@ -299,13 +345,13 @@
           localeInsert += "_" + localeSegments[j];
         }
 
-        toReturn = classLoader.getResource(prefix + localeInsert + extension);
+        toReturn = locator.locate(prefix + localeInsert + extension);
         if (toReturn != null) {
           break;
         }
       }
     } else {
-      toReturn = classLoader.getResource(resourceName);
+      toReturn = locator.locate(resourceName);
     }
 
     return toReturn;
diff --git a/user/src/com/google/gwt/resources/rebind/context/AbstractClientBundleGenerator.java b/user/src/com/google/gwt/resources/rebind/context/AbstractClientBundleGenerator.java
index 7ff745d..6d93493 100644
--- a/user/src/com/google/gwt/resources/rebind/context/AbstractClientBundleGenerator.java
+++ b/user/src/com/google/gwt/resources/rebind/context/AbstractClientBundleGenerator.java
@@ -265,12 +265,14 @@
    * Create fields and assignments for a single ResourceGenerator.
    */
   private boolean createFieldsAndAssignments(TreeLogger logger,
-      ResourceContext resourceContext, ResourceGenerator rg,
+      AbstractResourceContext resourceContext, ResourceGenerator rg,
       List<JMethod> generatorMethods, SourceWriter sw, ClientBundleFields fields) {
 
     // Defer failure until this phase has ended
     boolean fail = false;
 
+    resourceContext.setCurrentResourceGenerator(rg);
+
     // Write all field values
     try {
       rg.createFields(logger.branch(TreeLogger.DEBUG, "Creating fields"),
@@ -322,7 +324,7 @@
    */
   private void createFieldsAndAssignments(TreeLogger logger, SourceWriter sw,
       Map<ResourceGenerator, List<JMethod>> generators,
-      ResourceContext resourceContext, ClientBundleFields fields)
+      AbstractResourceContext resourceContext, ClientBundleFields fields)
       throws UnableToCompleteException {
     // Try to provide as many errors as possible before failing.
     boolean success = true;
@@ -455,12 +457,13 @@
   /**
    * Call finish() on several ResourceGenerators.
    */
-  private void finish(TreeLogger logger, ResourceContext context,
+  private void finish(TreeLogger logger, AbstractResourceContext context,
       Collection<ResourceGenerator> generators)
       throws UnableToCompleteException {
     boolean fail = false;
     // Finalize the ResourceGenerator
     for (ResourceGenerator rg : generators) {
+      context.setCurrentResourceGenerator(rg);
       try {
         rg.finish(
             logger.branch(TreeLogger.DEBUG, "Finishing ResourceGenerator"),
@@ -505,8 +508,8 @@
   private Map<ResourceGenerator, List<JMethod>> initAndPrepare(
       TreeLogger logger,
       Map<Class<? extends ResourceGenerator>, List<JMethod>> taskList,
-      ResourceContext resourceContext, ClientBundleRequirements requirements)
-      throws UnableToCompleteException {
+      AbstractResourceContext resourceContext,
+      ClientBundleRequirements requirements) throws UnableToCompleteException {
     // Try to provide as many errors as possible before failing.
     boolean success = true;
     Map<ResourceGenerator, List<JMethod>> toReturn = new IdentityHashMap<ResourceGenerator, List<JMethod>>();
@@ -530,9 +533,10 @@
   }
 
   private boolean initAndPrepare(TreeLogger logger,
-      ResourceContext resourceContext, ResourceGenerator rg,
+      AbstractResourceContext resourceContext, ResourceGenerator rg,
       List<JMethod> generatorMethods, ClientBundleRequirements requirements) {
     try {
+      resourceContext.setCurrentResourceGenerator(rg);
       rg.init(
           logger.branch(TreeLogger.DEBUG, "Initializing ResourceGenerator"),
           resourceContext);
diff --git a/user/src/com/google/gwt/resources/rebind/context/AbstractResourceContext.java b/user/src/com/google/gwt/resources/rebind/context/AbstractResourceContext.java
index 458ee59..100d757 100644
--- a/user/src/com/google/gwt/resources/rebind/context/AbstractResourceContext.java
+++ b/user/src/com/google/gwt/resources/rebind/context/AbstractResourceContext.java
@@ -19,12 +19,17 @@
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.dev.util.Util;
 import com.google.gwt.resources.ext.ResourceContext;
+import com.google.gwt.resources.ext.ResourceGenerator;
 import com.google.gwt.resources.ext.ResourceGeneratorUtil;
 
 import java.io.IOException;
 import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.WeakHashMap;
 
 /**
  * Defines base methods for ResourceContext implementations.
@@ -36,6 +41,17 @@
    */
   protected static final int MAX_INLINE_SIZE = 2 << 15;
 
+  /**
+   * Maps ResourceContext caches to their associated TypeOracles. This is a weak
+   * map and will not prevent TypeOracles from being gc'ed.
+   */
+  private static final Map<TypeOracle, Map<String, Object>> CACHES = new WeakHashMap<TypeOracle, Map<String, Object>>();
+
+  /**
+   * The key we use to store the expected TypeOracle reload count.
+   */
+  private static final String TYPE_ORACLE_RELOAD_COUNT_KEY = ":ReloadCount";
+
   protected static String toBase64(byte[] data) {
     // This is bad, but I am lazy and don't want to write _another_ encoder
     sun.misc.BASE64Encoder enc = new sun.misc.BASE64Encoder();
@@ -43,7 +59,24 @@
     return base64Contents;
   }
 
+  private static Map<String, Object> getCache(TypeOracle oracle) {
+    Map<String, Object> toReturn = CACHES.get(oracle);
+    if (toReturn != null) {
+      long expectedCount = (Long) toReturn.get(TYPE_ORACLE_RELOAD_COUNT_KEY);
+      if (oracle.getReloadCount() == expectedCount) {
+        return toReturn;
+      }
+    }
+
+    toReturn = new HashMap<String, Object>();
+    toReturn.put(TYPE_ORACLE_RELOAD_COUNT_KEY, (Object) oracle.getReloadCount());
+    CACHES.put(oracle, toReturn);
+    return toReturn;
+  }
+
   private final TreeLogger logger;
+  private final Map<String, Object> cache;
+  private String currentResourceGeneratorType;
   private final GeneratorContext context;
   private final JClassType resourceBundleType;
   private String simpleSourceName;
@@ -53,6 +86,7 @@
     this.logger = logger;
     this.context = context;
     this.resourceBundleType = resourceBundleType;
+    this.cache = getCache(context.getTypeOracle());
   }
 
   public String deploy(URL resource, boolean xhrCompatible)
@@ -69,6 +103,10 @@
     }
   }
 
+  public <T> T getCachedData(String key, Class<T> clazz) {
+    return clazz.cast(cache.get(currentResourceGeneratorType + ":" + key));
+  }
+
   public JClassType getClientBundleType() {
     return resourceBundleType;
   }
@@ -85,6 +123,11 @@
     return simpleSourceName;
   }
 
+  public <T> boolean putCachedData(String key, T value) {
+    key = currentResourceGeneratorType + ":" + key;
+    return value != cache.put(key, value);
+  }
+
   protected GeneratorContext getContext() {
     return context;
   }
@@ -93,6 +136,10 @@
     return logger;
   }
 
+  void setCurrentResourceGenerator(ResourceGenerator rg) {
+    currentResourceGeneratorType = rg.getClass().getName();
+  }
+
   void setSimpleSourceName(String name) {
     simpleSourceName = name;
   }
diff --git a/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java b/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
index 26b5c6c..2a4a7e5 100644
--- a/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
+++ b/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
@@ -968,6 +968,13 @@
 
   private static final String[] DEFAULT_EXTENSIONS = new String[] {".css"};
 
+  /**
+   * These constants are used to cache obfuscated class names.
+   */
+  private static final String KEY_BY_CLASS_AND_METHOD = "classAndMethod";
+  private static final String KEY_HAS_CACHED_DATA = "hasCachedData";
+  private static final String KEY_SHARED_METHODS = "sharedMethods";
+
   public static void main(String[] args) {
     for (int i = 0; i < 1000; i++) {
       System.out.println(makeIdent(i));
@@ -1318,11 +1325,9 @@
     stringType = typeOracle.findType(String.class.getName());
     assert stringType != null;
 
-    replacementsByClassAndMethod = new IdentityHashMap<JClassType, Map<JMethod, String>>();
-    replacementsForSharedMethods = new IdentityHashMap<JMethod, String>();
     stylesheetMap = new IdentityHashMap<JMethod, CssStylesheet>();
 
-    computeObfuscatedNames(logger);
+    initReplacements(logger, context);
   }
 
   @Override
@@ -1506,6 +1511,33 @@
     return false;
   }
 
+  /**
+   * This method will initialize the maps that contain the obfuscated class
+   * names.
+   */
+  @SuppressWarnings("unchecked")
+  private void initReplacements(TreeLogger logger, ResourceContext context) {
+    if (context.getCachedData(KEY_HAS_CACHED_DATA, Boolean.class) == Boolean.TRUE) {
+      replacementsByClassAndMethod = context.getCachedData(
+          KEY_BY_CLASS_AND_METHOD, Map.class);
+      replacementsForSharedMethods = context.getCachedData(KEY_SHARED_METHODS,
+          Map.class);
+      logger.log(TreeLogger.DEBUG, "Using cached replacements maps");
+
+    } else {
+      context.putCachedData(KEY_HAS_CACHED_DATA, Boolean.TRUE);
+
+      replacementsByClassAndMethod = new IdentityHashMap<JClassType, Map<JMethod, String>>();
+      context.putCachedData(KEY_BY_CLASS_AND_METHOD,
+          replacementsByClassAndMethod);
+
+      replacementsForSharedMethods = new IdentityHashMap<JMethod, String>();
+      context.putCachedData(KEY_SHARED_METHODS, replacementsForSharedMethods);
+
+      computeObfuscatedNames(logger);
+    }
+  }
+
   private boolean isStrict(TreeLogger logger, ResourceContext context,
       JMethod method) {
     Strict strictAnnotation = method.getAnnotation(Strict.class);
@@ -1634,7 +1666,8 @@
       Map<JMethod, String> classReplacements) {
 
     String replacement = classReplacements.get(toImplement);
-    assert replacement != null;
+    assert replacement != null : "Missing replacement for "
+        + toImplement.getName();
 
     sw.println(toImplement.getReadableDeclaration(false, true, true, true, true)
         + "{");