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<Integer>. 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} &&
+ * {@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} &&
- * {@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());
+ }
+}