Only intern strings on non-IE browsers when they occur 2 or more times.

Review at http://gwt-code-reviews.appspot.com/1336802


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9694 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java
index ab9e5c4..18d9bd1 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardLinkerContext.java
@@ -92,7 +92,7 @@
 
     @Override
     public boolean visit(JsFunction x, JsContext ctx) {
-      didChange |= JsStringInterner.exec(program, x.getBody(), x.getScope());
+      didChange |= JsStringInterner.exec(program, x.getBody(), x.getScope(), true);
       return false;
     }
   }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
index 13fce81..854899b 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -355,11 +355,28 @@
         }
       }
 
+      // detect if browser is ie6 or not known
+      boolean isIE6orUnknown = false;
+      for (PropertyOracle oracle : propertyOracles) {
+        try {
+          SelectionProperty userAgentProperty = oracle.getSelectionProperty(
+              logger, "user.agent");
+          if ("ie6".equals(userAgentProperty.getCurrentValue())) {
+            isIE6orUnknown = true;
+            break;
+          }
+        } catch (BadPropertyValueException e) {
+          // user agent unknown; play it safe
+          isIE6orUnknown = true;
+          break;
+        }
+      }
+
       // (10.5) Obfuscate
       Map<JsName, String> obfuscateMap = Maps.create();
       switch (options.getOutput()) {
         case OBFUSCATED:
-          obfuscateMap = JsStringInterner.exec(jprogram, jsProgram);
+          obfuscateMap = JsStringInterner.exec(jprogram, jsProgram, isIE6orUnknown);
           JsObfuscateNamer.exec(jsProgram);
           if (options.isAggressivelyOptimize()) {
             if (JsStackEmulator.getStackMode(propertyOracles) == JsStackEmulator.StackMode.STRIP) {
@@ -380,7 +397,7 @@
           JsPrettyNamer.exec(jsProgram);
           break;
         case DETAILED:
-          obfuscateMap = JsStringInterner.exec(jprogram, jsProgram);
+          obfuscateMap = JsStringInterner.exec(jprogram, jsProgram, isIE6orUnknown);
           JsVerboseNamer.exec(jsProgram);
           break;
         default:
@@ -398,21 +415,7 @@
       // http://code.google.com/p/google-web-toolkit/issues/detail?id=1440
       // note, JsIEBlockTextTransformer now handles restructuring top level
       // blocks, this class now handles non-top level blocks only.
-      boolean splitBlocks = false;
-      for (PropertyOracle oracle : propertyOracles) {
-        try {
-          SelectionProperty userAgentProperty = oracle.getSelectionProperty(
-              logger, "user.agent");
-          if ("ie6".equals(userAgentProperty.getCurrentValue())) {
-            splitBlocks = true;
-            break;
-          }
-        } catch (BadPropertyValueException e) {
-          // user agent unknown; play it safe
-          splitBlocks = true;
-          break;
-        }
-      }
+      boolean splitBlocks = isIE6orUnknown;
 
       if (splitBlocks) {
         JsIEBlockSizeVisitor.exec(jsProgram);
diff --git a/dev/core/src/com/google/gwt/dev/js/JsStringInterner.java b/dev/core/src/com/google/gwt/dev/js/JsStringInterner.java
index f85bf7e..7961f36 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsStringInterner.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsStringInterner.java
@@ -22,6 +22,7 @@
 import com.google.gwt.dev.js.ast.JsContext;
 import com.google.gwt.dev.js.ast.JsModVisitor;
 import com.google.gwt.dev.js.ast.JsName;
+import com.google.gwt.dev.js.ast.JsNode;
 import com.google.gwt.dev.js.ast.JsPostfixOperation;
 import com.google.gwt.dev.js.ast.JsPrefixOperation;
 import com.google.gwt.dev.js.ast.JsProgram;
@@ -31,6 +32,7 @@
 import com.google.gwt.dev.js.ast.JsStringLiteral;
 import com.google.gwt.dev.js.ast.JsVars;
 import com.google.gwt.dev.js.ast.JsVars.JsVar;
+import com.google.gwt.dev.js.ast.JsVisitor;
 
 import java.util.Collection;
 import java.util.Comparator;
@@ -44,13 +46,86 @@
 import java.util.TreeSet;
 
 /**
- * Interns all String literals in a JsProgram. Each unique String will be
- * assigned to a variable in an appropriate program fragment and the
- * JsStringLiteral will be replaced with a JsNameRef. This optimization is
- * complete in a single pass, although it may be performed multiple times
- * without duplicating the intern pool.
+ * Interns conditionally either all String literals in a JsProgram, or Strings
+ * which exceed a certain usage count. Each unique String will be assigned to a
+ * variable in an appropriate program fragment and the JsStringLiteral will be
+ * replaced with a JsNameRef. This optimization is complete in a single pass,
+ * although it may be performed multiple times without duplicating the intern
+ * pool.
  */
 public class JsStringInterner {
+
+  /**
+   * Counts occurences of each potentially internable String literal.
+   */
+  private static class OccurenceCounter extends JsVisitor {
+
+    private Map<String, Integer> occurenceMap = new HashMap<String, Integer>();
+
+    public Map<String, Integer> buildOccurenceMap() {
+      return occurenceMap;
+    }
+
+    /**
+     * Prevents 'fixing' an otherwise illegal operation.
+     */
+    @Override
+    public boolean visit(JsBinaryOperation x, JsContext ctx) {
+      return !x.getOperator().isAssignment()
+          || !(x.getArg1() instanceof JsStringLiteral);
+    }
+
+    /**
+     * Prevents 'fixing' an otherwise illegal operation.
+     */
+    @Override
+    public boolean visit(JsPostfixOperation x, JsContext ctx) {
+      return !(x.getArg() instanceof JsStringLiteral);
+    }
+
+    /**
+     * Prevents 'fixing' an otherwise illegal operation.
+     */
+    @Override
+    public boolean visit(JsPrefixOperation x, JsContext ctx) {
+      return !(x.getArg() instanceof JsStringLiteral);
+    }
+
+    /**
+     * We ignore property initializer labels in object literals, but do process
+     * the expression. This is because the LHS is always treated as a string,
+     * and never evaluated as an expression.
+     */
+    @Override
+    public boolean visit(JsPropertyInitializer x, JsContext ctx) {
+      accept(x.getValueExpr());
+      return false;
+    }
+
+    /**
+     * Count occurences of String literal.
+     */
+    @Override
+    public boolean visit(JsStringLiteral x, JsContext ctx) {
+      String literal = x.getValue();
+      Integer occurs = occurenceMap.get(literal);
+      if (occurs == null) {
+        occurs = 0;
+      }
+      occurenceMap.put(literal, ++occurs);
+      return false;
+    }
+
+    /**
+     * This prevents duplicating the intern pool by not traversing JsVar
+     * declarations that look like they were created by the interner.
+     */
+    @Override
+    public boolean visit(JsVar x, JsContext ctx) {
+      return !(x.getName().getIdent().startsWith(PREFIX));
+    }
+  }
+
   /**
    * Replaces JsStringLiterals with JsNameRefs, creating new JsName allocations
    * on the fly.
@@ -69,6 +144,12 @@
         LITERAL_COMPARATOR);
 
     /**
+     * Minimum number of times a string must occur to be interned.
+     */
+    private static final Integer INTERN_THRESHOLD = Integer.parseInt(
+        System.getProperty("gwt.jjs.stringInternerThreshold", "2"));
+
+    /**
      * A counter used for assigning ids to Strings. Even though it's unlikely
      * that someone would actually have two billion strings in their
      * application, it doesn't hurt to think ahead.
@@ -76,6 +157,12 @@
     private long lastId = 0;
 
     /**
+     * Count of # of occurences of each String literal, or null if
+     * count-sensitive interning is off
+     */
+    private Map<String, Integer> occurenceMap;
+
+    /**
      * Only used to get fragment load order so strings used in multiple
      * fragments need only be downloaded once.
      */
@@ -97,11 +184,13 @@
      * Constructor.
      * 
      * @param scope specifies the scope in which the interned strings should be
-     *          created.
+     * @param occurenceMap
      */
-    public StringVisitor(JProgram program, JsScope scope) {
+    public StringVisitor(JProgram program, JsScope scope,
+        Map<String, Integer> occurenceMap) {
       this.program = program;
       this.scope = scope;
+      this.occurenceMap = occurenceMap;
     }
 
     @Override
@@ -150,6 +239,13 @@
      */
     @Override
     public boolean visit(JsStringLiteral x, JsContext ctx) {
+      if (occurenceMap != null) {
+        Integer occurences = occurenceMap.get(x.getValue());
+        assert occurences != null;
+        if (occurences < INTERN_THRESHOLD) {
+          return false;
+        }
+      }
       JsName name = toCreate.get(x);
       if (name == null) {
         String ident = PREFIX + lastId++;
@@ -208,10 +304,13 @@
    * @param jprogram the JProgram that has fragment dependency data for
    *          <code>program</code>
    * @param program the JsProgram
+   * @param alwaysIntern true for browsers like IE which must always intern literals
    * @return a map describing the interning that occurred
    */
-  public static Map<JsName, String> exec(JProgram jprogram, JsProgram program) {
-    StringVisitor v = new StringVisitor(jprogram, program.getScope());
+  public static Map<JsName, String> exec(JProgram jprogram, JsProgram program,
+      boolean alwaysIntern) {
+    StringVisitor v = new StringVisitor(jprogram, program.getScope(), alwaysIntern ? null :
+      getOccurenceMap(program));
     v.accept(program);
 
     Map<Integer, SortedSet<JsStringLiteral>> bins = new HashMap<Integer, SortedSet<JsStringLiteral>>();
@@ -238,10 +337,13 @@
    * 
    * @param block the block to visit
    * @param scope the JsScope in which to reserve the new identifiers
+   * @param alwaysIntern true for browsers like IE which must always intern literals
    * @return <code>true</code> if any changes were made to the block
    */
-  public static boolean exec(JsProgram program, JsBlock block, JsScope scope) {
-    StringVisitor v = new StringVisitor(null, scope);
+  public static boolean exec(JsProgram program, JsBlock block, JsScope scope,
+      boolean alwaysIntern) {
+    StringVisitor v = new StringVisitor(null, scope, alwaysIntern ? null :
+        getOccurenceMap(block));
     v.accept(block);
 
     createVars(program, block, v.toCreate.keySet(), v.toCreate);
@@ -270,6 +372,12 @@
     }
   }
 
+  private static Map<String, Integer> getOccurenceMap(JsNode node) {
+    OccurenceCounter oc = new OccurenceCounter();
+    oc.accept(node);
+    return oc.buildOccurenceMap();
+  }
+
   private static Map<JsName, String> reverse(
       SortedMap<JsStringLiteral, JsName> toCreate) {
     Map<JsName, String> reversed = new LinkedHashMap<JsName, String>(