Remove JSNI paths from RPC deserialization. AKA: Make RPC screaming fast in DevMode.

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


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10419 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java b/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
index 71440ca..213b4dd 100644
--- a/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
+++ b/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
@@ -357,6 +357,19 @@
     }
 
     @Override
+    public Class<?> loadClass(String name) throws ClassNotFoundException {
+      try {
+        return getParent().loadClass(name);
+      } catch (Throwable t) {
+        // Make a second attempt not only on ClassNotFoundExceptions, but also errors like 
+        // ClassCircularityError
+        Class c = findClass(name);
+        resolveClass(c);
+        return c;
+      }
+    }
+    
+    @Override
     protected synchronized Class<?> findClass(String name)
         throws ClassNotFoundException {
       String resourceName = name.replace('.', '/') + ".class";
diff --git a/user/src/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamReader.java b/user/src/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamReader.java
index 6a9b797..ca4c183 100644
--- a/user/src/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamReader.java
+++ b/user/src/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamReader.java
@@ -15,41 +15,229 @@
  */
 package com.google.gwt.user.client.rpc.impl;
 
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.UnsafeNativeLong;
+import com.google.gwt.dev.jjs.SourceOrigin;
+import com.google.gwt.dev.js.JsParser;
+import com.google.gwt.dev.js.ast.JsArrayLiteral;
+import com.google.gwt.dev.js.ast.JsBooleanLiteral;
+import com.google.gwt.dev.js.ast.JsContext;
+import com.google.gwt.dev.js.ast.JsExpression;
+import com.google.gwt.dev.js.ast.JsInvocation;
+import com.google.gwt.dev.js.ast.JsModVisitor;
+import com.google.gwt.dev.js.ast.JsNameRef;
+import com.google.gwt.dev.js.ast.JsNumberLiteral;
+import com.google.gwt.dev.js.ast.JsPostfixOperation;
+import com.google.gwt.dev.js.ast.JsPrefixOperation;
+import com.google.gwt.dev.js.ast.JsRootScope;
+import com.google.gwt.dev.js.ast.JsStatement;
+import com.google.gwt.dev.js.ast.JsStringLiteral;
+import com.google.gwt.dev.js.ast.JsUnaryOperator;
+import com.google.gwt.dev.js.ast.JsValueLiteral;
+import com.google.gwt.dev.js.ast.JsVisitor;
+import com.google.gwt.lang.LongLib;
 import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
 import com.google.gwt.user.client.rpc.SerializationException;
 
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+
 /**
- * For internal use only. Used for server call serialization.
+ * A version of ClientSerializationStreamReader which is optimized for performance in
+ * devmode.
+ * 
+ * There is a super-src version of this class which is optimized for webmode performance.
  */
 public final class ClientSerializationStreamReader extends
     AbstractSerializationStreamReader {
 
-  private static native JavaScriptObject eval(String encoded) /*-{
-    return eval(encoded);
-  }-*/;
+  /**
+   * Decodes an RPC payload from a JS string. There is currently no design document that describes
+   * the payload, instead you must infer it from reading ServerSerializationStreamWriter.
+   * I'll briefly describe the payload here:
+   * <p> 
+   * The server sends down a string of JavaScript which is eval'ed by the webmode version of
+   * ClientSerializationStreamReader into an array. The array contains primitive values,
+   * followed by a nested array of strings, followed by a couple of header primitive values.  
+   * For example,
+   * <pre>
+   * [ 1, 0, 3, -7, 13, [ "string one", "string two", "string three" ], 7, 0 ] 
+   * </pre>
+   * Long primitives are encoded as strings in the outer array, and strings in the string table
+   * are referenced by index values in the outer array. 
+   * <p> 
+   * The payload is almost a JSON literal except for some nuances, like unicode and array concats.
+   * We have a specialized devmode version to decode this payload, because the webmode version
+   * requires multiple round-trips between devmode and the JSVM for every single element in the
+   * payload. This can require several seconds to decode a single RPC payload. The RpcDecoder
+   * operates by doing a limited JS parse on the payload within the devmode VM using Rhino,
+   * avoiding the cross-process RPCs.  
+   * <p> 
+   */
+  private static class RpcDecoder extends JsVisitor {
 
-  private static native int getLength(JavaScriptObject array) /*-{
-    return array.length;
-  }-*/;
+    private static final String JS_INFINITY_LITERAL = "Infinity";
+    private static final String JS_NAN_LITERAL = "NaN";
 
-  int index;
+    enum State {
+      EXPECTING_PAYLOAD_BEGIN,
+      EXPECTING_STRING_TABLE,
+      IN_STRING_TABLE,
+      EXPECTING_END
+    }
+    
+    State state = State.EXPECTING_PAYLOAD_BEGIN;
 
-  JavaScriptObject results;
+    List<String> stringTable = new ArrayList<String>();
+    List<JsValueLiteral> values = new ArrayList<JsValueLiteral>();
+    
+    boolean negative;
 
-  JavaScriptObject stringTable;
+    @Override
+    public void endVisit(JsArrayLiteral x, JsContext ctx) {
+      if (state == State.IN_STRING_TABLE) {
+        state = State.EXPECTING_END;
+      }
+    }
 
+    public List<String> getStringTable() {
+      return stringTable;
+    }
+
+    public List<JsValueLiteral> getValues() {
+      return values;
+    }
+    
+    @Override
+    public boolean visit(JsArrayLiteral x, JsContext ctx) {
+      switch (state) {
+        case EXPECTING_PAYLOAD_BEGIN:
+          state = State.EXPECTING_STRING_TABLE;
+        return true;
+        case EXPECTING_STRING_TABLE:
+          state = State.IN_STRING_TABLE;
+        return true;
+        default:
+        throw new RuntimeException("Unexpected array in RPC payload. The string table has " 
+              + "already started.");                  
+      }
+    }
+    
+    @Override
+    public boolean visit(JsBooleanLiteral x, JsContext ctx) {
+      values.add(x);
+      return true;
+    }
+
+    @Override
+    public boolean visit(JsNameRef x, JsContext ctx) {
+      String ident = x.getIdent();
+      
+      if (ident.equals(JS_NAN_LITERAL)) {
+        values.add(new JsNumberLiteral(SourceOrigin.UNKNOWN, Double.NaN));
+      } else if (ident.equals(JS_INFINITY_LITERAL)) {
+        double val = negative ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
+        negative = false;
+        values.add(new JsNumberLiteral(SourceOrigin.UNKNOWN, val));
+      } else {
+        throw new RuntimeException("Unexpected identifier: " + ident);
+      }
+
+      return true;
+    }
+
+    @Override
+    public boolean visit(JsNumberLiteral x, JsContext ctx) {
+      if (negative) {
+        x = new JsNumberLiteral(x.getSourceInfo(), -x.getValue());
+        negative = false;
+      }
+      values.add(x);
+      return true;
+    }
+
+    @Override
+    public boolean visit(JsPrefixOperation x, JsContext ctx) {
+      if (x.getOperator().equals(JsUnaryOperator.NEG)) {
+        negative = !negative;
+        return true;
+      }
+      
+      // Lots of prefix operators, but we only see negatives for literals
+      throw new RuntimeException("Unexpected prefix operator: " + x.toSource()); 
+    }
+
+    @Override
+    public boolean visit(JsPostfixOperation x, JsContext ctx) {
+      throw new RuntimeException("Unexpected postfix operator: " + x.toSource());
+    }
+
+    @Override
+    public boolean visit(JsStringLiteral x, JsContext ctx) {
+      if (state == State.IN_STRING_TABLE) {
+        stringTable.add(x.getValue());
+      } else {
+        values.add(x);
+      }
+      return true;
+    }
+  }
+
+  private RpcDecoder decoder;
+  private int index;
   private Serializer serializer;
 
+  /**
+   * The server breaks up large arrays in the RPC payload into smaller arrays using concat()
+   * expressions. For example, [1, 2, 3, 4, 5, 6, 7] can be broken up into
+   * [1, 2].concat([3, 4], [5, 6], [7,8])
+   * <p> 
+   * This visitor reverses that transform by reducing all concat invocations into a single array 
+   * literal. 
+   */
+  private static class ConcatEvaler extends JsModVisitor {
+
+    @Override
+    public boolean visit(JsInvocation invoke, JsContext ctx) {
+      JsExpression expr = invoke.getQualifier();
+      if (!(expr instanceof JsNameRef)) {
+        return super.visit(invoke, ctx);
+      }
+      
+      JsNameRef name = (JsNameRef) expr;
+      if (!name.getIdent().equals("concat")) {
+        return super.visit(invoke, ctx);
+      }
+
+      JsArrayLiteral headElements = (JsArrayLiteral) name.getQualifier();
+
+      for (JsExpression ex : invoke.getArguments()) {
+        JsArrayLiteral arg = (JsArrayLiteral) ex;
+        headElements.getExpressions().addAll(arg.getExpressions());
+      }
+      
+      ctx.replaceMe(headElements);
+      return true;
+    }
+  }
+  
   public ClientSerializationStreamReader(Serializer serializer) {
     this.serializer = serializer;
   }
 
   @Override
   public void prepareToRead(String encoded) throws SerializationException {
-    results = eval(encoded);
-    index = getLength(results);
+    try {
+      List<JsStatement> stmts = JsParser.parse(SourceOrigin.UNKNOWN, JsRootScope.INSTANCE,
+          new StringReader(encoded));
+      ConcatEvaler concatEvaler = new ConcatEvaler();
+      concatEvaler.acceptList(stmts);
+      decoder = new RpcDecoder();
+      decoder.acceptList(stmts);
+    } catch (Exception e) {
+      throw new SerializationException("Failed to parse RPC payload", e);
+    }
+
+    index = decoder.getValues().size();
     super.prepareToRead(encoded);
 
     if (getVersion() != SERIALIZATION_STREAM_VERSION) {
@@ -62,44 +250,56 @@
       throw new IncompatibleRemoteServiceException("Got an unknown flag from "
           + "server: " + getFlags());
     }
-
-    stringTable = readJavaScriptObject();
   }
 
-  public native boolean readBoolean() /*-{
-    return !!this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
-  }-*/;
+  @Override
+  public boolean readBoolean() {
+    JsValueLiteral literal = decoder.getValues().get(--index);
+    return literal.isBooleanTrue();
+  }
 
-  public native byte readByte() /*-{
-    return this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
-  }-*/;
-
-  public native char readChar() /*-{
-    return this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
-  }-*/;
-
-  public native double readDouble() /*-{
-    return this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
-  }-*/;
-
-  public native float readFloat() /*-{
-    return this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
-  }-*/;
-
-  public native int readInt() /*-{
-    return this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
-  }-*/;
+  @Override
+  public byte readByte() {    
+    JsNumberLiteral literal = (JsNumberLiteral) decoder.getValues().get(--index);
+    return (byte) literal.getValue();
+  }
   
-  @UnsafeNativeLong
-  public native long readLong() /*-{
-    var s = this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
-    return @com.google.gwt.lang.LongLib::longFromBase64(Ljava/lang/String;)(s);
-  }-*/;
-
-  public native short readShort() /*-{
-    return this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
-  }-*/;
-
+  @Override
+  public char readChar() {    
+    JsNumberLiteral literal = (JsNumberLiteral) decoder.getValues().get(--index);
+    return (char) literal.getValue();
+  }
+  
+  @Override
+  public double readDouble() {    
+    JsNumberLiteral literal = (JsNumberLiteral) decoder.getValues().get(--index);
+    return literal.getValue();
+  }
+  
+  @Override
+  public float readFloat() {    
+    JsNumberLiteral literal = (JsNumberLiteral) decoder.getValues().get(--index);
+    return (float) literal.getValue();
+  }
+  
+  @Override
+  public int readInt() {    
+    JsNumberLiteral literal = (JsNumberLiteral) decoder.getValues().get(--index);
+    return (int) literal.getValue();
+  }
+  
+  @Override
+  public long readLong() {    
+    return LongLib.longFromBase64(((JsStringLiteral)decoder.getValues().get(--index)).getValue());
+  }
+  
+  @Override
+  public short readShort() {    
+    JsNumberLiteral literal = (JsNumberLiteral) decoder.getValues().get(--index);
+    return (short) literal.getValue();
+  }
+  
+  @Override
   public String readString() {
     return getString(readInt());
   }
@@ -115,13 +315,8 @@
   }
 
   @Override
-  protected native String getString(int index) /*-{
+  protected String getString(int index) {
     // index is 1-based
-    return index > 0 ? this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::stringTable[index - 1] : null;
-  }-*/;
-
-  private native JavaScriptObject readJavaScriptObject() /*-{
-    return this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
-  }-*/;
-
+    return index > 0 ? decoder.getStringTable().get(index - 1) : null;
+  }  
 }
diff --git a/user/super/com/google/gwt/user/translatable/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamReader.java b/user/super/com/google/gwt/user/translatable/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamReader.java
new file mode 100644
index 0000000..f85f42e
--- /dev/null
+++ b/user/super/com/google/gwt/user/translatable/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamReader.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.client.rpc.impl;
+
+import com.google.gwt.core.client.GwtScriptOnly;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.UnsafeNativeLong;
+import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
+import com.google.gwt.user.client.rpc.SerializationException;
+
+/**
+ * The web-mode only version of ClientSerializationStreamReader. Any changes to this class to deal
+ * with protocol changes must also be applied to the devmode version of 
+ * ClientSerializationStreamReader. 
+ */
+@GwtScriptOnly
+public final class ClientSerializationStreamReader extends
+    AbstractSerializationStreamReader {
+
+  private static native JavaScriptObject eval(String encoded) /*-{
+    return eval(encoded);
+  }-*/;
+
+  private static native int getLength(JavaScriptObject array) /*-{
+    return array.length;
+  }-*/;
+
+  int index;
+
+  JavaScriptObject results;
+
+  JavaScriptObject stringTable;
+
+  private Serializer serializer;
+
+  public ClientSerializationStreamReader(Serializer serializer) {
+    this.serializer = serializer;
+  }
+
+  @Override
+  public void prepareToRead(String encoded) throws SerializationException {
+    results = eval(encoded);
+    index = getLength(results);
+    super.prepareToRead(encoded);
+
+    if (getVersion() != SERIALIZATION_STREAM_VERSION) {
+      throw new IncompatibleRemoteServiceException("Expecting version "
+          + SERIALIZATION_STREAM_VERSION + " from server, got " + getVersion()
+          + ".");
+    }
+    
+    if (!areFlagsValid()) {
+      throw new IncompatibleRemoteServiceException("Got an unknown flag from "
+          + "server: " + getFlags());
+    }
+
+    stringTable = readJavaScriptObject();
+  }
+
+  public native boolean readBoolean() /*-{
+    return !!this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
+  }-*/;
+
+  public native byte readByte() /*-{
+    return this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
+  }-*/;
+
+  public native char readChar() /*-{
+    return this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
+  }-*/;
+
+  public native double readDouble() /*-{
+    return this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
+  }-*/;
+
+  public native float readFloat() /*-{
+    return this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
+  }-*/;
+
+  public native int readInt() /*-{
+    return this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
+  }-*/;
+  
+  @UnsafeNativeLong
+  public native long readLong() /*-{
+    var s = this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
+    return @com.google.gwt.lang.LongLib::longFromBase64(Ljava/lang/String;)(s);
+  }-*/;
+
+  public native short readShort() /*-{
+    return this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
+  }-*/;
+
+  public String readString() {
+    return getString(readInt());
+  }
+
+  @Override
+  protected Object deserialize(String typeSignature)
+      throws SerializationException {
+    int id = reserveDecodedObjectIndex();
+    Object instance = serializer.instantiate(this, typeSignature);
+    rememberDecodedObject(id, instance);
+    serializer.deserialize(this, instance, typeSignature);
+    return instance;
+  }
+
+  @Override
+  protected native String getString(int index) /*-{
+    // index is 1-based
+    return index > 0 ? this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::stringTable[index - 1] : null;
+  }-*/;
+
+  private native JavaScriptObject readJavaScriptObject() /*-{
+    return this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::results[--this.@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::index];
+  }-*/;
+
+}