Adds a test for AsyncFragmentLoader.

Review by: bobv


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5749 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/CodeSplitter.java b/dev/core/src/com/google/gwt/dev/jjs/impl/CodeSplitter.java
index 3512b15..22bd0ce 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/CodeSplitter.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/CodeSplitter.java
@@ -24,11 +24,13 @@
 import com.google.gwt.dev.jjs.SourceInfo;
 import com.google.gwt.dev.jjs.ast.Context;
 import com.google.gwt.dev.jjs.ast.HasEnclosingType;
+import com.google.gwt.dev.jjs.ast.JArrayType;
 import com.google.gwt.dev.jjs.ast.JClassLiteral;
 import com.google.gwt.dev.jjs.ast.JDeclaredType;
 import com.google.gwt.dev.jjs.ast.JExpression;
 import com.google.gwt.dev.jjs.ast.JField;
 import com.google.gwt.dev.jjs.ast.JMethod;
+import com.google.gwt.dev.jjs.ast.JMethodCall;
 import com.google.gwt.dev.jjs.ast.JNewArray;
 import com.google.gwt.dev.jjs.ast.JNode;
 import com.google.gwt.dev.jjs.ast.JPrimitiveType;
@@ -468,9 +470,24 @@
     return (value == null) ? 0 : value;
   }
 
+  /**
+   * Installs the initial load sequence into the
+   * AsyncFragmentLoader.BROWSER_LOADER. The initializer looks like this:
+   * 
+   * <pre>
+       public static AsyncFragmentLoader BROWSER_LOADER = new AsyncFragmentLoader(1,
+         new int[] {}, new StandardLoadingStrategy(), new StandardLogger());
+     </pre>
+   * 
+   * The second argument (<code>new int[]</code>) gets replaced by an array
+   * corresponding to <code>initialLoadSequence</code>.
+   */
   private static void installInitialLoadSequenceField(JProgram program,
       LinkedHashSet<Integer> initialLoadSequence) {
-    JField initLoadSeqField = program.getIndexedField("AsyncFragmentLoader.initialLoadSequence");
+    JMethodCall constructorCall = ReplaceRunAsyncs.getBrowserLoaderConstructor(program);
+    assert constructorCall.getArgs().get(1).getType() instanceof JArrayType;
+    assert ((JArrayType) constructorCall.getArgs().get(1).getType()).getElementType() == JPrimitiveType.INT;
+
     SourceInfo info = program.createSourceInfoSynthetic(ReplaceRunAsyncs.class,
         "array with initial load sequence");
     List<JExpression> intExprs = new ArrayList<JExpression>();
@@ -481,8 +498,8 @@
      * Note: the following field is known to have a manually installed
      * initializer, of new int[0].
      */
-    initLoadSeqField.getDeclarationStatement().initializer = JNewArray.createInitializers(
-        program, info, program.getTypeArray(JPrimitiveType.INT, 1), intExprs);
+    constructorCall.setArg(1, JNewArray.createInitializers(program, info,
+        program.getTypeArray(JPrimitiveType.INT, 1), intExprs));
   }
 
   private static <T> T last(T[] array) {
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/ControlFlowAnalyzer.java b/dev/core/src/com/google/gwt/dev/jjs/impl/ControlFlowAnalyzer.java
index 95830eb..6322083 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/ControlFlowAnalyzer.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/ControlFlowAnalyzer.java
@@ -817,7 +817,7 @@
 
   public void traverseFromLeftoversFragmentHasLoaded() {
     if (program.entryMethods.size() > 1) {
-      traverseFrom(program.getIndexedMethod("AsyncFragmentLoader.leftoversFragmentHasLoaded"));
+      traverseFrom(program.getIndexedMethod("AsyncFragmentLoader.browserLoaderLeftoversFragmentHasLoaded"));
     }
   }
 
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentExtractor.java b/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentExtractor.java
index e76ae03..0bfedd0 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentExtractor.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentExtractor.java
@@ -207,10 +207,11 @@
    * {@link com.google.gwt.core.client.impl.AsyncFragmentLoader#leftoversFragmentHasLoaded()}.
    */
   public List<JsStatement> createCallToLeftoversFragmentHasLoaded() {
-    JMethod loadedMethod = jprogram.getIndexedMethod("AsyncFragmentLoader.leftoversFragmentHasLoaded");
+    JMethod loadedMethod = jprogram.getIndexedMethod("AsyncFragmentLoader.browserLoaderLeftoversFragmentHasLoaded");
     JsName loadedMethodName = map.nameForMethod(loadedMethod);
     SourceInfo sourceInfo = jsprogram.getSourceInfo().makeChild(
-        FragmentExtractor.class, "call to leftoversFragmentHasLoaded ");
+        FragmentExtractor.class,
+        "call to browserLoaderLeftoversFragmentHasLoaded ");
     JsInvocation call = new JsInvocation(sourceInfo);
     call.setQualifier(loadedMethodName.makeRef(sourceInfo));
     List<JsStatement> newStats = Collections.<JsStatement> singletonList(call.makeStmt());
@@ -220,9 +221,9 @@
   /**
    * Assume that all code described by <code>alreadyLoadedPredicate</code> has
    * been downloaded. Extract enough JavaScript statements that the code
-   * described by <code>livenessPredicate</code> can also run. The caller
-   * should ensure that <code>livenessPredicate</code> includes strictly more
-   * live code than <code>alreadyLoadedPredicate</code>.
+   * described by <code>livenessPredicate</code> can also run. The caller should
+   * ensure that <code>livenessPredicate</code> includes strictly more live code
+   * than <code>alreadyLoadedPredicate</code>.
    */
   public List<JsStatement> extractStatements(
       LivenessPredicate livenessPredicate,
@@ -301,7 +302,7 @@
       entryMethodNames.add(name);
     }
 
-    JMethod leftoverFragmentLoaded = jprogram.getIndexedMethod("AsyncFragmentLoader.leftoversFragmentHasLoaded");
+    JMethod leftoverFragmentLoaded = jprogram.getIndexedMethod("AsyncFragmentLoader.browserLoaderLeftoversFragmentHasLoaded");
     if (leftoverFragmentLoaded != null) {
       JsName name = map.nameForMethod(leftoverFragmentLoaded);
       assert name != null;
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java b/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java
index 85b74c1..a71c4e5 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java
@@ -122,14 +122,14 @@
     srcWriter.println("public static void onLoad() {");
     srcWriter.println("loaded = true;");
     srcWriter.println("instance = new " + getLoaderSimpleName() + "();");
-    srcWriter.println(ASYNC_FRAGMENT_LOADER + ".fragmentHasLoaded("
+    srcWriter.println(ASYNC_FRAGMENT_LOADER + ".BROWSER_LOADER.fragmentHasLoaded("
         + entryNumber + ");");
 
     srcWriter.println(ASYNC_FRAGMENT_LOADER
-        + ".logEventProgress(\"runCallbacks" + entryNumber + "\", \"begin\");");
+        + ".BROWSER_LOADER.logEventProgress(\"runCallbacks" + entryNumber + "\", \"begin\");");
     srcWriter.println("instance.runCallbacks();");
     srcWriter.println(ASYNC_FRAGMENT_LOADER
-        + ".logEventProgress(\"runCallbacks" + entryNumber + "\", \"end\");");
+        + ".BROWSER_LOADER.logEventProgress(\"runCallbacks" + entryNumber + "\", \"end\");");
 
     srcWriter.println("}");
   }
@@ -160,7 +160,7 @@
     srcWriter.println("}");
     srcWriter.println("if (!loading) {");
     srcWriter.println("loading = true;");
-    srcWriter.println("AsyncFragmentLoader.inject(" + entryNumber + ",");
+    srcWriter.println("AsyncFragmentLoader.BROWSER_LOADER.inject(" + entryNumber + ",");
     srcWriter.println("  new AsyncFragmentLoader.LoadErrorHandler() {");
     srcWriter.println("    public void loadFailed(Throwable reason) {");
     srcWriter.println("      loading = false;");
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
index c1a5ffb..b8f03dc 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
@@ -1147,7 +1147,7 @@
       List<JsFunction> nonInitialEntries = Arrays.asList(entryFunctions).subList(
           x.getEntryCount(0), entryFunctions.length);
       if (!nonInitialEntries.isEmpty()) {
-        JMethod loadedMethod = program.getIndexedMethod("AsyncFragmentLoader.leftoversFragmentHasLoaded");
+        JMethod loadedMethod = program.getIndexedMethod("AsyncFragmentLoader.browserLoaderLeftoversFragmentHasLoaded");
         JsName loadedMethodName = names.get(loadedMethod);
         SourceInfo sourceInfo = jsProgram.getSourceInfo().makeChild(
             GenerateJavaScriptAST.class, "call to leftoversFragmentHasLoaded ");
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JavaToJavaScriptMap.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JavaToJavaScriptMap.java
index 7b21d28..996fce0 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/JavaToJavaScriptMap.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JavaToJavaScriptMap.java
@@ -26,17 +26,18 @@
  */
 public interface JavaToJavaScriptMap {
   /**
-   * Return the JavaScript name corresponding to a Java type.
-   */
-  JsName nameForType(JReferenceType type);
-  
-  /**
    * Return the JavaScript name corresponding to a Java method.
    */
   JsName nameForMethod(JMethod method);
 
   /**
-   * If <code>name</code> is the name of a <code>var<code> that corresponds to a Java
+   * Return the JavaScript name corresponding to a Java type.
+   */
+  JsName nameForType(JReferenceType type);
+
+  /**
+   * If <code>name</code> is the name of a
+   * <code>var<code> that corresponds to a Java
    * static field, then return that field. Otherwise, return null.
    */
   JField nameToField(JsName name);
@@ -52,16 +53,16 @@
    * string literal, then return the string it interns. Otherwise, return null.
    */
   String stringLiteralForName(JsName var);
-  
+
   /**
-   * If <code>stat</code> is used to set up the definition of some class,
-   * return that class. Otherwise, return null.
+   * If <code>stat</code> is used to set up the definition of some class, return
+   * that class. Otherwise, return null.
    */
   JReferenceType typeForStatement(JsStatement stat);
-  
+
   /**
-   * If <code>stat</code> is used to set up a vtable entry for a method,
-   * then return that method.  Otherwise return null.
+   * If <code>stat</code> is used to set up a vtable entry for a method, then
+   * return that method. Otherwise return null.
    */
   JMethod vtableInitToMethod(JsStatement stat);
 }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/ReplaceRunAsyncs.java b/dev/core/src/com/google/gwt/dev/jjs/impl/ReplaceRunAsyncs.java
index 8fe7b6a..8c341da 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/ReplaceRunAsyncs.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/ReplaceRunAsyncs.java
@@ -23,6 +23,7 @@
 import com.google.gwt.dev.jjs.ast.JMethod;
 import com.google.gwt.dev.jjs.ast.JMethodCall;
 import com.google.gwt.dev.jjs.ast.JModVisitor;
+import com.google.gwt.dev.jjs.ast.JPrimitiveType;
 import com.google.gwt.dev.jjs.ast.JProgram;
 import com.google.gwt.dev.jjs.ast.JType;
 
@@ -120,6 +121,17 @@
     new ReplaceRunAsyncs(program).execImpl();
   }
 
+  /**
+   * Extract the initializer of AsyncFragmentLoader.BROWSER_LOADER. A couple of
+   * parts of the compiler modify this constructor call.
+   */
+  static JMethodCall getBrowserLoaderConstructor(JProgram program) {
+    JField field = program.getIndexedField("AsyncFragmentLoader.BROWSER_LOADER");
+    JMethodCall constructorCall = (JMethodCall) field.getDeclarationStatement().getInitializer();
+    assert constructorCall.getArgs().size() == 4;
+    return constructorCall;
+  }
+
   private JProgram program;
   private Map<Integer, RunAsyncReplacement> runAsyncReplacements = new HashMap<Integer, RunAsyncReplacement>();
 
@@ -173,7 +185,8 @@
   }
 
   private void setNumEntriesInAsyncFragmentLoader(int entryCount) {
-    JField field = program.getIndexedField("AsyncFragmentLoader.numEntries");
-    field.getDeclarationStatement().initializer = program.getLiteralInt(entryCount);
+    JMethodCall constructorCall = getBrowserLoaderConstructor(program);
+    assert constructorCall.getArgs().get(0).getType() == JPrimitiveType.INT;
+    constructorCall.setArg(0, program.getLiteralInt(entryCount));
   }
 }
diff --git a/user/src/com/google/gwt/core/client/impl/AsyncFragmentLoader.java b/user/src/com/google/gwt/core/client/impl/AsyncFragmentLoader.java
index 490a502..6545297 100644
--- a/user/src/com/google/gwt/core/client/impl/AsyncFragmentLoader.java
+++ b/user/src/com/google/gwt/core/client/impl/AsyncFragmentLoader.java
@@ -16,7 +16,6 @@
 package com.google.gwt.core.client.impl;
 
 import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArrayInteger;
 import com.google.gwt.xhr.client.ReadyStateChangeHandler;
 import com.google.gwt.xhr.client.XMLHttpRequest;
 
@@ -73,6 +72,26 @@
   }
 
   /**
+   * A strategy for loading code fragments.
+   */
+  public interface LoadingStrategy {
+    void startLoadingFragment(int fragment, LoadErrorHandler loadErrorHandler);
+  }
+
+  /**
+   * A strategy for logging progress.
+   */
+  public interface Logger {
+    /**
+     * Log an event. The <code>fragment</code> and <code>size</code> are boxed
+     * so that they can be optional. A value of <code>null</code> for either one
+     * means that they are not specified.
+     */
+    void logEventProgress(String eventGroup, String type, Integer fragment,
+        Integer size);
+  }
+
+  /**
    * Labels used for runAsync lightweight metrics.
    */
   public static class LwmLabels {
@@ -88,6 +107,40 @@
   }
 
   /**
+   * A trivial queue of int's that should compile much better than a
+   * LinkedList&lt;Integer&gt;. It assumes that there will be a maximum number
+   * of items passed through the queue for its entire life.
+   */
+  private static class BoundedIntQueue {
+    private final int[] array;
+    private int read = 0;
+    private int write = 0;
+
+    public BoundedIntQueue(int maxPuts) {
+      array = new int[maxPuts];
+    }
+    
+    public void add(int x) {
+      assert (write < array.length);
+      array[write++] = x;
+    }
+    
+    public int peek() {
+      assert read < write;
+      return array[read];
+    }
+    
+    public int remove() {
+      assert read < write;
+      return array[read++];
+    }
+    
+    public int size() {
+      return write - read;
+    }
+  }
+
+  /**
    * An exception indicating than at HTTP download failed.
    */
   private static class HttpDownloadFailure extends RuntimeException {
@@ -106,8 +159,7 @@
   /**
    * Handles a failure to download a fragment in the initial sequence.
    */
-  private static class InitialFragmentDownloadFailed implements
-      LoadErrorHandler {
+  private class InitialFragmentDownloadFailed implements LoadErrorHandler {
     public void loadFailed(Throwable reason) {
       initialFragmentsLoading = false;
 
@@ -120,10 +172,10 @@
       List<LoadErrorHandler> handlersToRun = new ArrayList<LoadErrorHandler>();
 
       // add handlers that are waiting pending the initials download
-      assert waitingForInitialFragments.length() == waitingForInitialFragmentsErrorHandlers.size();
-      while (waitingForInitialFragments.length() > 0) {
+      assert waitingForInitialFragments.size() == waitingForInitialFragmentsErrorHandlers.size();
+      while (waitingForInitialFragments.size() > 0) {
         handlersToRun.add(waitingForInitialFragmentsErrorHandlers.remove());
-        waitingForInitialFragments.shift();
+        waitingForInitialFragments.remove();
       }
 
       // add handlers for pending initial fragment downloads
@@ -150,6 +202,122 @@
     }
   }
 
+  /**
+   * The standard logger used in a web browser. It uses the lightweight metrics
+   * system.
+   */
+  private static class StandardLogger implements Logger {
+    /**
+     * Always use this as {@link isStatsAvailable} &amp;&amp;
+     * {@link #stats(JavaScriptObject)}.
+     */
+    private static native boolean stats(JavaScriptObject data) /*-{
+      return $stats(data);
+    }-*/;
+
+    public void logEventProgress(String eventGroup, String type,
+        Integer fragment, Integer size) {
+      @SuppressWarnings("unused")
+      boolean toss = isStatsAvailable()
+          && stats(createStatsEvent(eventGroup, type, fragment, size));
+    }
+
+    private native JavaScriptObject createStatsEvent(String eventGroup,
+        String type, Integer fragment, Integer size) /*-{
+      var evt = {
+       moduleName: @com.google.gwt.core.client.GWT::getModuleName()(), 
+        subSystem: 'runAsync',
+        evtGroup: eventGroup,
+        millis: (new Date()).getTime(),
+        type: type
+      };
+      if (fragment != null) {
+        evt.fragment = fragment.@java.lang.Integer::intValue()();
+      }
+      if (size != null) {
+        evt.size = size.@java.lang.Integer::intValue()();
+      }
+      return evt;
+    }-*/;
+
+    private native boolean isStatsAvailable() /*-{
+      return !!$stats;
+    }-*/;
+  }
+
+  /**
+   * The standard loading strategy used in a web browser.
+   */
+  private static class XhrLoadingStrategy implements LoadingStrategy {
+    public void startLoadingFragment(int fragment,
+        final LoadErrorHandler loadErrorHandler) {
+      String fragmentUrl = gwtStartLoadingFragment(fragment, loadErrorHandler);
+
+      if (fragmentUrl == null) {
+        // The download has already started; nothing more to do
+        return;
+      }
+
+      // use XHR to download it
+
+      final XMLHttpRequest xhr = XMLHttpRequest.create();
+
+      xhr.open(HTTP_GET, fragmentUrl);
+
+      xhr.setOnReadyStateChange(new ReadyStateChangeHandler() {
+        public void onReadyStateChange(XMLHttpRequest ignored) {
+          if (xhr.getReadyState() == XMLHttpRequest.DONE) {
+            xhr.clearOnReadyStateChange();
+            if ((xhr.getStatus() == HTTP_STATUS_OK || xhr.getStatus() == HTTP_STATUS_NON_HTTP)
+                && xhr.getResponseText() != null
+                && xhr.getResponseText().length() != 0) {
+              try {
+                gwtInstallCode(xhr.getResponseText());
+              } catch (RuntimeException e) {
+                loadErrorHandler.loadFailed(e);
+              }
+            } else {
+              loadErrorHandler.loadFailed(new HttpDownloadFailure(
+                  xhr.getStatus()));
+            }
+          }
+        }
+      });
+
+      xhr.send();
+    }
+
+    /**
+     * Call the linker-supplied <code>__gwtInstallCode</code> method. See the
+     * {@link AsyncFragmentLoader class comment} for more details.
+     */
+    private native void gwtInstallCode(String text) /*-{
+      __gwtInstallCode(text);
+    }-*/;
+ 
+    /**
+     * Call the linker-supplied __gwtStartLoadingFragment function. It should
+     * either start the download and return null or undefined, or it should return
+     * a URL that should be downloaded to get the code. If it starts the download
+     * itself, it can synchronously load it, e.g. from cache, if that makes sense.
+     */
+    private native String gwtStartLoadingFragment(int fragment,
+        LoadErrorHandler loadErrorHandler) /*-{
+      function loadFailed(e) {
+        loadErrorHandler.@com.google.gwt.core.client.impl.AsyncFragmentLoader$LoadErrorHandler::loadFailed(Ljava/lang/Throwable;)(e);
+      }
+      return __gwtStartLoadingFragment(fragment, loadFailed);
+    }-*/;
+  }
+
+  /**
+   * The standard instance of AsyncFragmentLoader used in a web browser. The
+   * parameters to this call are filled in by
+   * {@link com.google.gwt.dev.jjs.impl.ReplaceRunAsyncs}.
+   */
+  public static AsyncFragmentLoader BROWSER_LOADER = new AsyncFragmentLoader(1,
+      new int[] {}, new XhrLoadingStrategy(), new StandardLogger());
+
   private static final String HTTP_GET = "GET";
 
   /**
@@ -162,63 +330,87 @@
   private static final int HTTP_STATUS_OK = 200;
 
   /**
+   * A helper static method that invokes
+   * BROWSER_LOADER.leftoversFragmentHasLoaded(). Such a call is generated by
+   * the compiler, as it is much simpler if there is a static method to wrap up
+   * the call.
+   */
+  public static void browserLoaderLeftoversFragmentHasLoaded() {
+    BROWSER_LOADER.leftoversFragmentHasLoaded();
+  }
+
+  /**
    * Error handlers for failure to download an initial fragment.
    * 
    * TODO(spoon) make it a lightweight integer map
    */
-  private static Map<Integer, LoadErrorHandler> initialFragmentErrorHandlers = new HashMap<Integer, LoadErrorHandler>();
+  private Map<Integer, LoadErrorHandler> initialFragmentErrorHandlers = new HashMap<Integer, LoadErrorHandler>();
 
   /**
    * Indicates that the next fragment in {@link #remainingInitialFragments} is
    * currently downloading.
    */
-  private static boolean initialFragmentsLoading = false;
+  private boolean initialFragmentsLoading = false;
 
   /**
    * The sequence of fragments to load initially, before anything else can be
    * loaded. This array will hold the initial sequence of bases followed by the
    * leftovers fragment. It is filled in by
-   * {@link com.google.gwt.dev.jjs.impl.CodeSplitter}.  It does *not* include
-   * the leftovers fragment, which must be loaded once all of these are finished.
+   * {@link com.google.gwt.dev.jjs.impl.CodeSplitter} modifying the initializer
+   * to {@link #INSTANCE}. The list does <em>not</em> include the leftovers
+   * fragment, which must be loaded once all of these are finished.
    */
-  private static int[] initialLoadSequence = new int[] { };
+  private final int[] initialLoadSequence;
+
+  private LoadingStrategy loadingStrategy = new XhrLoadingStrategy();
+
+  private final Logger logger;
 
   /**
-   * The total number of split points in the program, counting the initial entry
-   * as an honorary split point. This is changed to the correct value by
-   * {@link com.google.gwt.dev.jjs.impl.ReplaceRunAsyncs}.
+   * The total number of entry points in the program, which is the number of
+   * split points plus one for the main entry point of the program.
    */
-  private static int numEntries = 1;
+  private final int numEntries;
 
   /**
    * Base fragments that remain to be downloaded. It is lazily initialized in
-   * the first call to {@link #startLoadingNextInitial()}.  It does include
-   * the leftovers fragment.
+   * the first call to {@link #startLoadingNextInitial()}. It does include the
+   * leftovers fragment.
    */
-  private static JsArrayInteger remainingInitialFragments = null;
+  private BoundedIntQueue remainingInitialFragments = null;
 
   /**
    * Split points that have been reached, but that cannot be downloaded until
-   * the initial fragments finish downloading.
+   * the initial fragments finish downloading. TODO(spoon) use something lighter
+   * than a LinkedList
    */
-  private static JsArrayInteger waitingForInitialFragments = createJsArrayInteger();
+  private final BoundedIntQueue waitingForInitialFragments;
 
   /**
    * Error handlers for the above queue.
    * 
    * TODO(spoon) change this to a lightweight JS collection
    */
-  private static Queue<LoadErrorHandler> waitingForInitialFragmentsErrorHandlers = new LinkedList<LoadErrorHandler>();
+  private Queue<LoadErrorHandler> waitingForInitialFragmentsErrorHandlers = new LinkedList<LoadErrorHandler>();
+
+  public AsyncFragmentLoader(int numEntries, int[] initialLoadSequence,
+      LoadingStrategy loadingStrategy, Logger logger) {
+    this.numEntries = numEntries;
+    this.initialLoadSequence = initialLoadSequence;
+    this.loadingStrategy = loadingStrategy;
+    this.logger = logger;
+    waitingForInitialFragments = new BoundedIntQueue(numEntries + 1);
+  }
 
   /**
    * Inform the loader that a fragment has now finished loading.
    */
-  public static void fragmentHasLoaded(int fragment) {
+  public void fragmentHasLoaded(int fragment) {
     logFragmentLoaded(fragment);
 
     if (isInitial(fragment)) {
-      assert (fragment == remainingInitialFragments.get(0));
-      remainingInitialFragments.shift();
+      assert (fragment == remainingInitialFragments.peek());
+      remainingInitialFragments.remove();
       initialFragmentErrorHandlers.remove(fragment);
 
       startLoadingNextInitial();
@@ -230,7 +422,8 @@
    * 
    * @param splitPoint the split point whose code needs to be loaded
    */
-  public static void inject(int splitPoint, LoadErrorHandler loadErrorHandler) {
+  public void inject(int splitPoint, LoadErrorHandler loadErrorHandler) {
+
     if (haveInitialFragmentsLoaded()) {
       /*
        * The initial fragments has loaded. Immediately start loading the
@@ -255,8 +448,8 @@
        * initial fragments have all been loaded.
        */
 
-      assert (waitingForInitialFragments.length() == waitingForInitialFragmentsErrorHandlers.size());
-      waitingForInitialFragments.push(splitPoint);
+      assert (waitingForInitialFragments.size() == waitingForInitialFragmentsErrorHandlers.size());
+      waitingForInitialFragments.add(splitPoint);
       waitingForInitialFragmentsErrorHandlers.add(loadErrorHandler);
     }
 
@@ -266,70 +459,28 @@
     if (!initialFragmentsLoading) {
       startLoadingNextInitial();
     }
-
-    return;
   }
-  
-  public static void leftoversFragmentHasLoaded() {
+
+  public void leftoversFragmentHasLoaded() {
     fragmentHasLoaded(leftoversFragment());
   }
 
   /**
-   * Log an event with the lightweight metrics framework.
+   * Log an event with the {@Logger} this instance was provided.
    */
-  public static void logEventProgress(String eventGroup, String type) {
+  public void logEventProgress(String eventGroup, String type) {
     logEventProgress(eventGroup, type, null, null);
   }
 
-  private static native JsArrayInteger createJsArrayInteger() /*-{
-    return [];
-  }-*/;
-
-  private static native JavaScriptObject createStatsEvent(String eventGroup,
-      String type, Integer fragment, Integer size) /*-{
-    var evt = {
-     moduleName: @com.google.gwt.core.client.GWT::getModuleName()(), 
-      subSystem: 'runAsync',
-      evtGroup: eventGroup,
-      millis: (new Date()).getTime(),
-      type: type
-    };
-    if (fragment != null) {
-      evt.fragment = fragment.@java.lang.Integer::intValue()();
-    }
-    if (size != null) {
-      evt.size = size.@java.lang.Integer::intValue()();
-    }
-    return evt;
-  }-*/;
-
-  private static native void gwtInstallCode(String text) /*-{
-    __gwtInstallCode(text);
-  }-*/;
-
-  /**
-   * Call the linker-supplied __gwtStartLoadingFragment function. It should
-   * either start the download and return null or undefined, or it should return
-   * a URL that should be downloaded to get the code. If it starts the download
-   * itself, it can synchronously load it, e.g. from cache, if that makes sense.
-   */
-  private static native String gwtStartLoadingFragment(int fragment,
-      LoadErrorHandler loadErrorHandler) /*-{
-    function loadFailed(e) {
-      loadErrorHandler.@com.google.gwt.core.client.impl.AsyncFragmentLoader$LoadErrorHandler::loadFailed(Ljava/lang/Throwable;)(e);
-    }
-    return __gwtStartLoadingFragment(fragment, loadFailed);
-  }-*/;
-
   /**
    * Return whether all initial fragments have completed loading.
    */
-  private static boolean haveInitialFragmentsLoaded() {
+  private boolean haveInitialFragmentsLoaded() {
     return remainingInitialFragments != null
-        && remainingInitialFragments.length() == 0;
+        && remainingInitialFragments.size() == 0;
   }
 
-  private static boolean isInitial(int splitPoint) {
+  private boolean isInitial(int splitPoint) {
     if (splitPoint == leftoversFragment()) {
       return true;
     }
@@ -341,83 +492,49 @@
     return false;
   }
 
-  private static native boolean isStatsAvailable() /*-{
-    return !!$stats;
-  }-*/;
-
-  private static int leftoversFragment() {
+  private int leftoversFragment() {
     return numEntries;
   }
 
   /**
-   * Log an event with the lightweight metrics framework. The
+   * Log event progress via the {@link Logger} this instance was provided. The
    * <code>fragment</code> and <code>size</code> objects are allowed to be
    * <code>null</code>.
    */
-  private static void logEventProgress(String eventGroup, String type,
+  private void logEventProgress(String eventGroup, String type,
       Integer fragment, Integer size) {
-    @SuppressWarnings("unused")
-    boolean toss = isStatsAvailable()
-        && stats(createStatsEvent(eventGroup, type, fragment, size));
+    logger.logEventProgress(eventGroup, type, fragment, size);
   }
 
-  private static void logFragmentLoaded(int fragment) {
+  private void logFragmentLoaded(int fragment) {
     String logGroup = (fragment == leftoversFragment())
         ? LwmLabels.LEFTOVERS_DOWNLOAD : LwmLabels.downloadGroup(fragment);
     logEventProgress(logGroup, LwmLabels.END, fragment, null);
   }
 
-  private static void startLoadingFragment(int fragment,
+  private void startLoadingFragment(int fragment,
       final LoadErrorHandler loadErrorHandler) {
-    String fragmentUrl = gwtStartLoadingFragment(fragment, loadErrorHandler);
-
-    if (fragmentUrl != null) {
-      // use XHR
-      final XMLHttpRequest xhr = XMLHttpRequest.create();
-
-      xhr.open(HTTP_GET, fragmentUrl);
-
-      xhr.setOnReadyStateChange(new ReadyStateChangeHandler() {
-        public void onReadyStateChange(XMLHttpRequest xhr) {
-          if (xhr.getReadyState() == XMLHttpRequest.DONE) {
-            xhr.clearOnReadyStateChange();
-            if ((xhr.getStatus() == HTTP_STATUS_OK || xhr.getStatus() == HTTP_STATUS_NON_HTTP)
-                && xhr.getResponseText() != null
-                && xhr.getResponseText().length() != 0) {
-              try {
-                gwtInstallCode(xhr.getResponseText());
-              } catch (RuntimeException e) {
-                loadErrorHandler.loadFailed(e);
-              }
-            } else {
-              loadErrorHandler.loadFailed(new HttpDownloadFailure(
-                  xhr.getStatus()));
-            }
-          }
-        }
-      });
-
-      xhr.send();
-    }
+    loadingStrategy.startLoadingFragment(fragment, loadErrorHandler);
   }
 
   /**
    * Start downloading the next fragment in the initial sequence, if there are
    * any left.
    */
-  private static void startLoadingNextInitial() {
+  private void startLoadingNextInitial() {
     if (remainingInitialFragments == null) {
       // first call, so initialize remainingInitialFragments
-      remainingInitialFragments = createJsArrayInteger();
+      remainingInitialFragments = new BoundedIntQueue(
+          initialLoadSequence.length + 1);
       for (int sp : initialLoadSequence) {
-        remainingInitialFragments.push(sp);
+        remainingInitialFragments.add(sp);
       }
-      remainingInitialFragments.push(leftoversFragment());
+      remainingInitialFragments.add(leftoversFragment());
     }
-    
+
     if (initialFragmentErrorHandlers.isEmpty()
         && waitingForInitialFragmentsErrorHandlers.isEmpty()
-        && remainingInitialFragments.length() > 1) {
+        && remainingInitialFragments.size() > 1) {
       /*
        * No further requests are pending, and more than the leftovers fragment
        * is left outstanding. Stop loading stuff for now.
@@ -426,12 +543,12 @@
       return;
     }
 
-    if (remainingInitialFragments.length() > 0) {
+    if (remainingInitialFragments.size() > 0) {
       // start loading the next initial fragment
       initialFragmentsLoading = true;
-      int nextSplitPoint = remainingInitialFragments.get(0);
-      logEventProgress(LwmLabels.downloadGroup(nextSplitPoint), LwmLabels.BEGIN,
-          nextSplitPoint, null);
+      int nextSplitPoint = remainingInitialFragments.peek();
+      logEventProgress(LwmLabels.downloadGroup(nextSplitPoint),
+          LwmLabels.BEGIN, nextSplitPoint, null);
       startLoadingFragment(nextSplitPoint, new InitialFragmentDownloadFailed());
       return;
     }
@@ -439,20 +556,12 @@
     // all initials are finished
     initialFragmentsLoading = false;
     assert (haveInitialFragmentsLoaded());
-    
+
     // start loading any pending fragments
-    assert (waitingForInitialFragments.length() == waitingForInitialFragmentsErrorHandlers.size());
-    while (waitingForInitialFragments.length() > 0) {
-      startLoadingFragment(waitingForInitialFragments.shift(),
+    assert (waitingForInitialFragments.size() == waitingForInitialFragmentsErrorHandlers.size());
+    while (waitingForInitialFragments.size() > 0) {
+      startLoadingFragment(waitingForInitialFragments.remove(),
           waitingForInitialFragmentsErrorHandlers.remove());
     }
   }
-
-  /**
-   * Always use this as {@link isStatsAvailable} &amp;&amp;
-   * {@link #stats(JavaScriptObject)}.
-   */
-  private static native boolean stats(JavaScriptObject data) /*-{
-    return $stats(data);
-  }-*/;
 }
diff --git a/user/test/com/google/gwt/core/CoreSuite.java b/user/test/com/google/gwt/core/CoreSuite.java
index 18e67ad..659642f 100644
--- a/user/test/com/google/gwt/core/CoreSuite.java
+++ b/user/test/com/google/gwt/core/CoreSuite.java
@@ -19,6 +19,7 @@
 import com.google.gwt.core.client.HttpThrowableReporterTest;
 import com.google.gwt.core.client.JavaScriptExceptionTest;
 import com.google.gwt.core.client.JsArrayTest;
+import com.google.gwt.core.client.impl.AsyncFragmentLoaderTest;
 import com.google.gwt.core.client.impl.StackTraceCreatorTest;
 import com.google.gwt.junit.tools.GWTTestSuite;
 
@@ -37,6 +38,7 @@
     suite.addTestSuite(JsArrayTest.class);
     suite.addTestSuite(GWTTest.class);
     suite.addTestSuite(StackTraceCreatorTest.class);
+    suite.addTestSuite(AsyncFragmentLoaderTest.class);
     // $JUnit-END$
 
     return suite;
diff --git a/user/test/com/google/gwt/core/client/impl/AsyncFragmentLoaderTest.java b/user/test/com/google/gwt/core/client/impl/AsyncFragmentLoaderTest.java
new file mode 100644
index 0000000..89a158a
--- /dev/null
+++ b/user/test/com/google/gwt/core/client/impl/AsyncFragmentLoaderTest.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2009 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.core.client.impl;
+
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadErrorHandler;
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy;
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.Logger;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public class AsyncFragmentLoaderTest extends TestCase {
+  private static class MockErrorHandler implements LoadErrorHandler {
+    private boolean wasCalled = false;
+
+    public boolean getWasCalled() {
+      return wasCalled;
+    }
+
+    public void loadFailed(Throwable reason) {
+      wasCalled = true;
+    }
+  }
+
+  private static class MockLoadStrategy implements LoadingStrategy {
+    public final Map<Integer, LoadErrorHandler> errorHandlers = new HashMap<Integer, LoadErrorHandler>();
+    private List<Integer> loadRequests = new LinkedList<Integer>();
+
+    public void assertFragmentsRequested(int... expectedAry) {
+      List<Integer> actual = new ArrayList<Integer>(loadRequests);
+      loadRequests.clear();
+      List<Integer> expected = toList(expectedAry);
+
+      if (!sameContents(actual, expected)) {
+        fail("Expected= " + commaSeparated(expected) + "; actual="
+            + commaSeparated(actual));
+      }
+    }
+
+    public void startLoadingFragment(int fragment,
+        LoadErrorHandler loadErrorHandler) {
+      errorHandlers.put(fragment, loadErrorHandler);
+      loadRequests.add(fragment);
+    }
+
+    private String commaSeparated(List<Integer> ary) {
+      StringBuilder sb = new StringBuilder();
+      boolean first = true;
+      for (Integer x : ary) {
+        if (first) {
+          first = false;
+        } else {
+          sb.append(",");
+        }
+        sb.append(x);
+      }
+      return sb.toString();
+    }
+
+    private boolean sameContents(List<Integer> actual, List<Integer> expected) {
+      if (actual.size() != expected.size()) {
+        return false;
+      }
+      for (int i = 0; i < actual.size(); i++) {
+        if (!actual.get(i).equals(expected.get(i))) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    private List<Integer> toList(int[] ary) {
+      List<Integer> list = new ArrayList<Integer>();
+      for (int i = 0; i < ary.length; i++) {
+        list.add(ary[i]);
+      }
+      return list;
+    }
+  }
+
+  private static final LoadErrorHandler NULL_ERROR_HANDLER = new LoadErrorHandler() {
+    public void loadFailed(Throwable reason) {
+    }
+  };
+
+  private static final Logger NULL_LOGGER = new Logger() {
+    public void logEventProgress(String eventGroup, String type,
+        Integer fragment, Integer size) {
+    }
+  };
+
+  private static Throwable makeLoadFailedException() {
+    return new RuntimeException("Load Failed");
+  }
+
+  public void testBasics() {
+    MockLoadStrategy reqs = new MockLoadStrategy();
+    int numEntries = 5;
+    AsyncFragmentLoader loader = new AsyncFragmentLoader(numEntries,
+        new int[] {}, reqs, NULL_LOGGER);
+
+    loader.inject(1, NULL_ERROR_HANDLER);
+    reqs.assertFragmentsRequested(numEntries);
+    loader.leftoversFragmentHasLoaded();
+    reqs.assertFragmentsRequested(1);
+    loader.fragmentHasLoaded(1);
+
+    loader.inject(2, NULL_ERROR_HANDLER);
+    reqs.assertFragmentsRequested(2);
+    loader.fragmentHasLoaded(2);
+  }
+
+  /**
+   * Check the behavior when there are download failures.
+   */
+  public void testDownloadFailures() {
+    MockLoadStrategy reqs = new MockLoadStrategy();
+    int numEntries = 6;
+    AsyncFragmentLoader loader = new AsyncFragmentLoader(numEntries, new int[] {
+        1, 2, 3}, reqs, NULL_LOGGER);
+
+    // request fragment 1
+    MockErrorHandler error1try1 = new MockErrorHandler();
+    loader.inject(1, error1try1);
+    reqs.assertFragmentsRequested(1);
+
+    // fragment 1 fails
+    loadFailed(reqs, 1);
+    assertTrue(error1try1.getWasCalled());
+
+    // try again on fragment 1
+    MockErrorHandler error1try2 = new MockErrorHandler();
+    loader.inject(1, error1try2);
+    reqs.assertFragmentsRequested(1);
+
+    // this time fragment 1 succeeds
+    loader.fragmentHasLoaded(1);
+    reqs.assertFragmentsRequested();
+    assertFalse(error1try2.getWasCalled());
+
+    // request a later initial fragment (3), and see what happens if an
+    // intermediate download fails
+    MockErrorHandler error3try1 = new MockErrorHandler();
+    loader.inject(3, error3try1);
+    reqs.assertFragmentsRequested(2);
+
+    loadFailed(reqs, 2);
+    assertTrue(error3try1.wasCalled);
+
+    // request both 3 and 5, an see what happens if
+    // the leftovers download fails
+    MockErrorHandler error3try2 = new MockErrorHandler();
+    MockErrorHandler error5try1 = new MockErrorHandler();
+    loader.inject(3, error3try2);
+    loader.inject(5, error5try1);
+    reqs.assertFragmentsRequested(2);
+
+    loader.fragmentHasLoaded(2);
+    reqs.assertFragmentsRequested(3);
+
+    loader.fragmentHasLoaded(3);
+    reqs.assertFragmentsRequested(numEntries);
+
+    loadFailed(reqs, numEntries); // leftovers fails!
+    assertFalse(error3try2.getWasCalled()); // 3 should have succeeded
+    assertTrue(error5try1.getWasCalled()); // 5 failed
+    reqs.errorHandlers.get(numEntries);
+
+    // now try 5 again, and have everything succeed
+    MockErrorHandler error5try2 = new MockErrorHandler();
+    loader.inject(5, error5try2);
+    reqs.assertFragmentsRequested(numEntries);
+
+    loader.leftoversFragmentHasLoaded();
+    reqs.assertFragmentsRequested(5);
+
+    loader.fragmentHasLoaded(5);
+    reqs.assertFragmentsRequested();
+    assertFalse(error5try2.getWasCalled());
+  }
+
+  /**
+   * If only the first part of the initial load sequence is requested, then
+   * don't request more.
+   */
+  public void testLoadingPartOfInitialSequence() {
+    MockLoadStrategy reqs = new MockLoadStrategy();
+    int numEntries = 6;
+    AsyncFragmentLoader loader = new AsyncFragmentLoader(numEntries, new int[] {
+        1, 2, 3}, reqs, NULL_LOGGER);
+
+    loader.inject(1, NULL_ERROR_HANDLER);
+    reqs.assertFragmentsRequested(1);
+
+    loader.fragmentHasLoaded(1);
+    reqs.assertFragmentsRequested(); // should stop
+
+    loader.inject(2, NULL_ERROR_HANDLER);
+    reqs.assertFragmentsRequested(2);
+
+    loader.fragmentHasLoaded(2);
+    reqs.assertFragmentsRequested(); // again, should stop
+
+    loader.inject(3, NULL_ERROR_HANDLER);
+    reqs.assertFragmentsRequested(3);
+
+    loader.fragmentHasLoaded(3);
+    reqs.assertFragmentsRequested(numEntries); // last initial, so it should
+    // request the leftovers
+
+    loader.fragmentHasLoaded(numEntries);
+    reqs.assertFragmentsRequested();
+
+    // check that exclusives now load
+    loader.inject(5, NULL_ERROR_HANDLER);
+    reqs.assertFragmentsRequested(5);
+
+    loader.fragmentHasLoaded(5);
+    reqs.assertFragmentsRequested();
+  }
+
+  /**
+   * A thorough exercise of loading with an initial load sequence specified.
+   */
+  public void testWithInitialLoadSequence() {
+    MockLoadStrategy reqs = new MockLoadStrategy();
+    int numEntries = 6;
+    AsyncFragmentLoader loader = new AsyncFragmentLoader(numEntries, new int[] {
+        1, 2, 3}, reqs, NULL_LOGGER);
+
+    loader.inject(1, NULL_ERROR_HANDLER);
+    reqs.assertFragmentsRequested(1);
+
+    loader.inject(3, NULL_ERROR_HANDLER);
+    reqs.assertFragmentsRequested(); // still waiting on fragment 1
+
+    loader.inject(5, NULL_ERROR_HANDLER);
+    reqs.assertFragmentsRequested(); // still waiting on fragment 1
+
+    // say that 1 loads, which should trigger a chain of backlogged downloads
+    loader.fragmentHasLoaded(1);
+    reqs.assertFragmentsRequested(2); // next initial
+
+    loader.fragmentHasLoaded(2);
+    reqs.assertFragmentsRequested(3); // next initial
+
+    loader.fragmentHasLoaded(3);
+    reqs.assertFragmentsRequested(numEntries); // leftovers
+
+    loader.leftoversFragmentHasLoaded();
+    reqs.assertFragmentsRequested(5);
+
+    loader.fragmentHasLoaded(5);
+    reqs.assertFragmentsRequested(); // quiescent
+
+    // check that new exclusive fragment requests work
+    loader.inject(4, NULL_ERROR_HANDLER);
+    reqs.assertFragmentsRequested(4);
+    loader.fragmentHasLoaded(4);
+    reqs.assertFragmentsRequested();
+  }
+
+  private void loadFailed(MockLoadStrategy reqs, int fragment) {
+    reqs.errorHandlers.get(fragment).loadFailed(makeLoadFailedException());
+  }
+}