Enhancement to JsDuplicateFunctionRemover to remove duplicate virtual methods as well. That is, given

_.method1 = function(a) { body }
_.method2 = function(b) { body }

Hoist the duplicate virtual methods and replace with a named global function.

function c(a) { body }
_.method1 = c;
_.method2 = c;

Produces a 1%-3% improvement in code size when stack stripping is enabled.

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

Review by: skybrian@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@11038 8db76d5a-ed1c-0410-87a9-c151d255dfc7
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 5902127..d6f4bd1 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -395,6 +395,8 @@
               }
               if (changed) {
                 JsUnusedFunctionRemover.exec(jsProgram);
+                // run again
+                JsObfuscateNamer.exec(jsProgram);
               }
             }
           }
diff --git a/dev/core/src/com/google/gwt/dev/js/JsDuplicateFunctionRemover.java b/dev/core/src/com/google/gwt/dev/js/JsDuplicateFunctionRemover.java
index 845ba4f..1bddd9a 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsDuplicateFunctionRemover.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsDuplicateFunctionRemover.java
@@ -46,10 +46,17 @@
 
     private final Map<JsName, JsName> duplicateOriginalMap = new IdentityHashMap<JsName, JsName>();
 
+    private final Map<JsFunction, JsFunction> duplicateMethodOriginalMap = new IdentityHashMap<JsFunction, JsFunction>();
+
+
     private final Stack<JsNameRef> invocationQualifiers = new Stack<JsNameRef>();
 
+    // static / global methods
     private final Map<String, JsName> uniqueBodies = new HashMap<String, JsName>();
 
+    // vtable methods
+    private final Map<String, JsFunction> uniqueMethodBodies = new HashMap<String, JsFunction>();
+
     public DuplicateFunctionBodyRecorder() {
       // Add sentinel to stop Stack.peek() from throwing exception.
       invocationQualifiers.add(null);
@@ -84,20 +91,31 @@
       return duplicateOriginalMap;
     }
 
+    public Map<JsFunction, JsFunction> getDuplicateMethodMap() {
+      return duplicateMethodOriginalMap;
+    }
+
     @Override
     public boolean visit(JsFunction x, JsContext ctx) {
+      String fnSource = x.toSource();
+      String body = fnSource.substring(fnSource.indexOf("("));
       /*
-       * Don't process anonymous functions.
+       * Static function processed separate from virtual functions
        */
       if (x.getName() != null) {
-        String fnSource = x.toSource();
-        String body = fnSource.substring(fnSource.indexOf("("));
         JsName original = uniqueBodies.get(body);
         if (original != null) {
           duplicateOriginalMap.put(x.getName(), original);
         } else {
           uniqueBodies.put(body, x.getName());
         }
+      } else if (x.isFromJava()) {
+         JsFunction original = uniqueMethodBodies.get(body);
+         if (original != null) {
+           duplicateMethodOriginalMap.put(x, original);
+         } else {
+           uniqueMethodBodies.put(body, x);
+         }
       }
       return true;
     }
@@ -114,13 +132,27 @@
   private class ReplaceDuplicateInvocationNameRefs extends JsModVisitor {
 
     private final Set<JsName> blacklist;
+    private final Map<JsFunction, JsFunction> dupMethodMap;
+    private final Map<JsFunction, JsName> hoistMap;
 
     private final Map<JsName, JsName> duplicateMap;
 
     public ReplaceDuplicateInvocationNameRefs(Map<JsName, JsName> duplicateMap,
-        Set<JsName> blacklist) {
+        Set<JsName> blacklist, Map<JsFunction, JsFunction> dupMethodMap,
+        Map<JsFunction, JsName> hoistMap) {
       this.duplicateMap = duplicateMap;
       this.blacklist = blacklist;
+      this.dupMethodMap = dupMethodMap;
+      this.hoistMap = hoistMap;
+    }
+
+    @Override
+    public void endVisit(JsFunction x, JsContext ctx) {
+      if (dupMethodMap.containsKey(x)) {
+        ctx.replaceMe(hoistMap.get(dupMethodMap.get(x)).makeRef(x.getSourceInfo()));
+      } else if (hoistMap.containsKey(x)) {
+        ctx.replaceMe(hoistMap.get(x).makeRef(x.getSourceInfo()));
+      }
     }
 
     @Override
@@ -152,8 +184,28 @@
   private boolean execImpl(JsBlock fragment) {
     DuplicateFunctionBodyRecorder dfbr = new DuplicateFunctionBodyRecorder();
     dfbr.accept(fragment);
+    int count = 0;
+    Map<JsFunction, JsName> hoistMap = new HashMap<JsFunction, JsName>();
+    // Hoist all anonymous versions
+    Map<JsFunction, JsFunction> dupMethodMap = dfbr.getDuplicateMethodMap();
+    for (JsFunction x : dupMethodMap.values()) {
+      if (!hoistMap.containsKey(x)) {
+        // move function to top scope and re-declaring it with a unique name
+        JsName newName = program.getScope().declareName("_DUP" + count++);
+        JsFunction newFunc = new JsFunction(x.getSourceInfo(),
+            program.getScope(), newName, x.isFromJava());
+        // we're not using the old function anymore, we can use reuse the body instead of cloning it
+        newFunc.setBody(x.getBody());
+        // also copy the parameters from the old function
+        newFunc.getParameters().addAll(x.getParameters());
+        // add the new function to the top level list of statements
+        fragment.getStatements().add(newFunc.makeStmt());
+        hoistMap.put(x, newName);
+      }
+    }
+
     ReplaceDuplicateInvocationNameRefs rdup = new ReplaceDuplicateInvocationNameRefs(
-        dfbr.getDuplicateMap(), dfbr.getBlacklist());
+        dfbr.getDuplicateMap(), dfbr.getBlacklist(), dupMethodMap, hoistMap);
     rdup.accept(fragment);
     return rdup.didChange();
   }
diff --git a/dev/core/test/com/google/gwt/dev/js/JsDuplicateFunctionRemoverTest.java b/dev/core/test/com/google/gwt/dev/js/JsDuplicateFunctionRemoverTest.java
index 9ce5659..f28cc80 100644
--- a/dev/core/test/com/google/gwt/dev/js/JsDuplicateFunctionRemoverTest.java
+++ b/dev/core/test/com/google/gwt/dev/js/JsDuplicateFunctionRemoverTest.java
@@ -15,6 +15,22 @@
  */
 package com.google.gwt.dev.js;
 
+import com.google.gwt.dev.jjs.SourceOrigin;
+
+import com.google.gwt.dev.js.ast.JsContext;
+import com.google.gwt.dev.js.ast.JsFunction;
+import com.google.gwt.dev.js.ast.JsModVisitor;
+import com.google.gwt.dev.js.ast.JsProgram;
+import com.google.gwt.dev.js.ast.JsStatement;
+import com.google.gwt.dev.js.ast.JsVisitor;
+import com.google.gwt.dev.util.TextOutput;
+import com.google.gwt.dev.util.DefaultTextOutput;
+
+import java.lang.reflect.Method;
+import java.io.StringReader;
+import java.util.List;
+
+
 /**
  * Tests the JsStaticEval optimizer.
  */
@@ -35,8 +51,48 @@
         optimize("function a(){};function b(){} a(); b();"));
   }
 
+  public void testVirtualRemoveDuplicates() throws Exception {
+    JsProgram program = new JsProgram();
+    String js = "_.method1=function(){};_.method2=function(){};_.method1();_.method2();";
+    List<JsStatement> expected = JsParser.parse(SourceOrigin.UNKNOWN,
+      program.getScope(), new StringReader(js));
+    program.getGlobalBlock().getStatements().addAll(expected);
+    new JsModVisitor() {
+	public void endVisit(JsFunction func, JsContext ctx) {
+          func.setFromJava(true);
+        }
+    }.accept(program);
+
+    assertEquals("_.method1=_DUP0;_.method2=_DUP0;_.method1();_.method2();function _DUP0(){}\n",
+		 optimize(program, JsSymbolResolver.class, JsDuplicateFunctionRemover.class,
+                         JsUnusedFunctionRemover.class));		 
+  }
+
   private String optimize(String js) throws Exception {
     return optimize(js, JsSymbolResolver.class,
         JsDuplicateFunctionRemover.class, JsUnusedFunctionRemover.class);
   }
-}
\ No newline at end of file
+
+
+  /**
+   * Optimize a JS program.
+   * 
+   * @param js the source program
+   * @param toExec a list of classes that implement
+   *          <code>static void exec(JsProgram)</code>
+   * @return optimized JS
+   */
+  protected String optimize(JsProgram program, Class<?>... toExec) throws Exception {
+ 
+    for (Class<?> clazz : toExec) {
+      Method m = clazz.getMethod("exec", JsProgram.class);
+      m.invoke(null, program);
+    }
+
+    TextOutput text = new DefaultTextOutput(true);
+    JsVisitor generator = new JsSourceGenerationVisitor(text);
+
+    generator.accept(program);
+    return text.toString();
+  }
+}