| /* |
| * 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.core.client.impl; |
| |
| import com.google.gwt.core.client.GWT; |
| import com.google.gwt.core.client.JavaScriptObject; |
| import com.google.gwt.core.client.RunAsyncCallback; |
| |
| /** |
| * <p> |
| * Low-level support to download an extra fragment of code. This should not be |
| * invoked directly by user code. |
| * </p> |
| * |
| * <p> |
| * The fragments are numbered as follows, assuming there are <em>m</em> split |
| * points: |
| * |
| * <ul> |
| * <li>0 -- the <em>base</em> fragment, which is initially downloaded |
| * <li>1..m -- fragments for each split point |
| * <li>m+1 -- the <em>leftovers</em> fragment of code that goes nowhere else |
| * </ul> |
| * |
| * <p> |
| * Since the precise way to load code depends on the linker, linkers should |
| * specify a rebind of {@link LoadingStrategy}. |
| */ |
| public class AsyncFragmentLoader { |
| /** |
| * A strategy for loading code fragments. |
| */ |
| public interface LoadingStrategy { |
| void startLoadingFragment(int fragment, LoadTerminatedHandler loadTerminatedHandler); |
| } |
| |
| /** |
| * An interface for handlers of load completion. On a failed download, this |
| * callback should be invoked or else the requested download will hang |
| * indefinitely. On a successful download, it's optional to call this method. |
| * If it is called at all, it must be called after the downloaded code has |
| * been installed, so that {@link AsyncFragmentLoader} can distinguish |
| * successful from unsuccessful downloads. |
| */ |
| public interface LoadTerminatedHandler { |
| void loadTerminated(Throwable reason); |
| } |
| |
| /** |
| * 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, int fragment, int size); |
| } |
| |
| /** |
| * Labels used for runAsync lightweight metrics. |
| */ |
| public static class LwmLabels { |
| public static final String BEGIN = "begin"; |
| |
| public static final String END = "end"; |
| |
| private static final String LEFTOVERS_DOWNLOAD = "leftoversDownload"; |
| |
| private static String downloadGroupForExclusive(int splitPoint) { |
| return "download" + splitPoint; |
| } |
| } |
| |
| /** |
| * The standard logger used in a web browser. It uses the lightweight metrics |
| * system. |
| */ |
| public static class StandardLogger implements Logger { |
| /** |
| * Always use this as {@link #isStatsAvailable()} && |
| * {@link #stats(JavaScriptObject)}. |
| */ |
| private static native boolean stats(JavaScriptObject data) /*-{ |
| return $stats(data); |
| }-*/; |
| |
| @Override |
| public void logEventProgress(String eventGroup, String type, int fragment, int size) { |
| @SuppressWarnings("unused") |
| boolean toss = |
| isStatsAvailable() && stats(createStatsEvent(eventGroup, type, fragment, size)); |
| } |
| |
| private native JavaScriptObject createStatsEvent(String eventGroup, |
| String type, int fragment, int size) /*-{ |
| var evt = { |
| moduleName: @com.google.gwt.core.client.GWT::getModuleName()(), |
| sessionId: $sessionId, |
| subSystem: 'runAsync', |
| evtGroup: eventGroup, |
| millis: (new Date()).getTime(), |
| type: type |
| }; |
| if (fragment >= 0) { |
| evt.fragment = fragment; |
| } |
| if (size >= 0) { |
| evt.size = size; |
| } |
| return evt; |
| }-*/; |
| |
| private native boolean isStatsAvailable() /*-{ |
| return !!$stats; |
| }-*/; |
| } |
| |
| /** |
| * An exception indicating than at HTTP download failed. |
| */ |
| static class HttpDownloadFailure extends RuntimeException { |
| private final int statusCode; |
| |
| public HttpDownloadFailure(String url, int statusCode, String statusText) { |
| super("Download of " + url + " failed with status " + statusCode + "(" + statusText + ")"); |
| this.statusCode = statusCode; |
| } |
| |
| public int getStatusCode() { |
| return statusCode; |
| } |
| } |
| |
| /** |
| * An exception indicating than at HTTP download succeeded, but installing its |
| * body failed. |
| */ |
| static class HttpInstallFailure extends RuntimeException { |
| public HttpInstallFailure(String url, String text, Throwable rootCause) { |
| super("Install of " + url + " failed with text " + text, rootCause); |
| } |
| } |
| |
| /** |
| * A trivial queue of int's that should compile much better than a |
| * LinkedList<Integer>. It assumes that it has a bound on the number of |
| * items added to the queue. Removing items does not free up more space, but |
| * calling <code>clear()</code> does. |
| */ |
| 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; |
| } |
| |
| /** |
| * Removes all elements, and also makes all space in the queue available |
| * again. |
| */ |
| public void clear() { |
| read = 0; |
| write = 0; |
| } |
| |
| public int peek() { |
| assert read < write; |
| return array[read]; |
| } |
| |
| public int remove() { |
| assert read < write; |
| return array[read++]; |
| } |
| |
| public int size() { |
| return write - read; |
| } |
| } |
| |
| /** |
| * Internal load error handler. This calls all user-provided error handlers |
| * and cancels all pending downloads. |
| */ |
| private class ResetAfterDownloadFailure implements LoadTerminatedHandler { |
| private final int fragment; |
| |
| public ResetAfterDownloadFailure(int myFragment) { |
| this.fragment = myFragment; |
| } |
| |
| @Override |
| public void loadTerminated(Throwable reason) { |
| if (fragmentLoading != fragment) { |
| // fragment already loaded successfully |
| return; |
| } |
| |
| // Cancel all pending downloads. |
| |
| /* |
| * Make a local list of the handlers to run, in case one of them calls |
| * another runAsync |
| */ |
| LoadTerminatedHandler[] handlersToRun = pendingDownloadErrorHandlers; |
| pendingDownloadErrorHandlers = new LoadTerminatedHandler[numEntries + 1]; |
| |
| /* |
| * Call clear() here so that requestedExclusives makes all of its space |
| * available for later requests. |
| */ |
| requestedExclusives.clear(); |
| |
| fragmentLoading = -1; |
| |
| /* |
| * Run the handlers. If an exception is thrown while canceling any of |
| * them, remember and throw the last one. |
| */ |
| RuntimeException lastException = null; |
| |
| for (LoadTerminatedHandler handler : handlersToRun) { |
| if (handler != null) { |
| try { |
| handler.loadTerminated(reason); |
| } catch (RuntimeException e) { |
| lastException = e; |
| } |
| } |
| } |
| |
| if (lastException != null) { |
| throw lastException; |
| } |
| } |
| } |
| |
| /** |
| * The standard instance of AsyncFragmentLoader used in a web browser. Outside |
| * of GWT generated JavaScript (i.e our vanilla JUnit tests, or if referenced |
| * in a server context), this field is {@code null}. When compiled to |
| * JavaScript, the parameters to this call are rewritten by |
| * {@link com.google.gwt.dev.jjs.impl.codesplitter.ReplaceRunAsyncs}. So this must be a |
| * method call of exactly two arguments to succeed when invoked in web mode. |
| */ |
| public static AsyncFragmentLoader BROWSER_LOADER = makeBrowserLoader(1, new int[]{}); |
| |
| /** |
| * Called by compiler-generated code when a fragment is loaded. |
| * |
| * @param fragment the fragment number |
| */ |
| public static void onLoad(int fragment) { |
| BROWSER_LOADER.onLoadImpl(fragment); |
| } |
| |
| /** |
| * Called by the compiler to implement {@link GWT#runAsync}. |
| * |
| * @param fragment the fragment number |
| * @param callback the callback to run |
| */ |
| public static void runAsync(int fragment, RunAsyncCallback callback) { |
| BROWSER_LOADER.runAsyncImpl(fragment, callback); |
| } |
| |
| /** |
| * Creates the loader stored as {@link #BROWSER_LOADER}. |
| * |
| * @returns {@code null} if not in GWT client code, where |
| * {@link GWT#create(Class)} cannot be used, or a fragment loader for |
| * the user's application otherwise. |
| */ |
| private static AsyncFragmentLoader makeBrowserLoader(int numFragments, int initialLoad[]) { |
| if (GWT.isClient()) { |
| return new AsyncFragmentLoader(numFragments, initialLoad, |
| (LoadingStrategy) GWT.create(LoadingStrategy.class), (Logger) GWT.create(Logger.class), |
| (OnSuccessExecutor) GWT.create(OnSuccessExecutor.class)); |
| } else { |
| return null; |
| } |
| } |
| |
| private final OnSuccessExecutor onSuccessExecutor; |
| |
| /** |
| * Callbacks indexed by fragment number. |
| */ |
| private final Object[][] allCallbacks; |
| |
| /** |
| * The fragment currently loading, or -1 if there aren't any. |
| */ |
| private int fragmentLoading = -1; |
| |
| /** |
| * 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.CodeSplitter} modifying the initializer |
| * to {@link #BROWSER_LOADER}. The list does <em>not</em> include the |
| * leftovers fragment, which must be loaded once all of these are finished. |
| */ |
| private final int[] initialLoadSequence; |
| |
| /** |
| * This array indicates which fragments have been successfully loaded. |
| */ |
| private final boolean[] isLoaded; |
| |
| private final LoadingStrategy loadingStrategy; |
| |
| private final Logger logger; |
| |
| /** |
| * 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 final int numEntries; |
| |
| /** |
| * Externally provided handlers for all outstanding and queued download |
| * requests. |
| */ |
| private LoadTerminatedHandler[] pendingDownloadErrorHandlers; |
| |
| /** |
| * Whether prefetching is currently enabled. |
| */ |
| private boolean prefetching = false; |
| |
| /** |
| * This queue has fragments that have been requested to be prefetched. If it's |
| * <code>null</code>, that indicates no prefetch requests, which should cause |
| * all of this class's prefetching code to drop out of the compiled output. |
| */ |
| private BoundedIntQueue prefetchQueue = null; |
| |
| /** |
| * Base fragments that remain to be downloaded. It is lazily initialized in |
| * the first call to {@link #startLoadingNextFragment()}. It does include the |
| * leftovers fragment. |
| */ |
| private BoundedIntQueue remainingInitialFragments = null; |
| |
| /** |
| * Exclusive fragments that have been requested but that are not yet |
| * downloading. |
| */ |
| private final BoundedIntQueue requestedExclusives; |
| |
| public AsyncFragmentLoader(int numEntries, int[] initialLoadSequence, |
| LoadingStrategy loadingStrategy, Logger logger, OnSuccessExecutor executor) { |
| this.numEntries = numEntries; |
| this.initialLoadSequence = initialLoadSequence; |
| this.loadingStrategy = loadingStrategy; |
| this.logger = logger; |
| this.onSuccessExecutor = executor; |
| int numEntriesPlusOne = numEntries + 1; |
| this.allCallbacks = new Object[numEntriesPlusOne][]; |
| this.requestedExclusives = new BoundedIntQueue(numEntriesPlusOne); |
| this.isLoaded = new boolean[numEntriesPlusOne]; |
| this.pendingDownloadErrorHandlers = new LoadTerminatedHandler[numEntriesPlusOne]; |
| } |
| |
| public boolean isAlreadyLoaded(int splitPoint) { |
| return isLoaded[splitPoint]; |
| } |
| |
| /** |
| * Request that a sequence of split points be prefetched. Code for the split |
| * points in <code>splitPoints</code> will be downloaded and installed |
| * whenever there is nothing else to download. Each call to this method |
| * overwrites the entire prefetch queue with the newly specified one. |
| */ |
| public void setPrefetchQueue(int... runAsyncSplitPoints) { |
| if (prefetchQueue == null) { |
| prefetchQueue = new BoundedIntQueue(numEntries); |
| } |
| prefetchQueue.clear(); |
| for (int sp : runAsyncSplitPoints) { |
| prefetchQueue.add(sp); |
| } |
| startLoadingNextFragment(); |
| } |
| |
| public void startPrefetching() { |
| prefetching = true; |
| startLoadingNextFragment(); |
| } |
| |
| public void stopPrefetching() { |
| prefetching = false; |
| } |
| |
| /** |
| * Inform the loader that a fragment has now finished loading. |
| */ |
| void fragmentHasLoaded(int fragment) { |
| logFragmentLoaded(fragment); |
| if (fragment < pendingDownloadErrorHandlers.length) { |
| pendingDownloadErrorHandlers[fragment] = null; |
| } |
| |
| /** |
| * It is possible for a fragment to be preloaded by the linker or server before runAsync() has |
| * requested it, in this case the leftovers fragment will be have it's onLoad() called before |
| * remainingInitialFragments has been initialized. |
| */ |
| if (isInitial(fragment) && remainingInitialFragments != null) { |
| assert (fragment == remainingInitialFragments.peek()); |
| remainingInitialFragments.remove(); |
| } |
| |
| assert (fragment == fragmentLoading); |
| fragmentLoading = -1; |
| |
| assert !isLoaded[fragment]; |
| isLoaded[fragment] = true; |
| |
| startLoadingNextFragment(); |
| } |
| |
| /** |
| * Requests a load of the code for the specified split point. If the load |
| * fails, <code>loadErrorHandler</code> will be invoked. If it succeeds, then |
| * the code will be installed, and the code is expected to invoke its own |
| * on-success hooks, including a call to either |
| * {@link #leftoversFragmentHasLoaded()} or {@link #fragmentHasLoaded(int)}. |
| * |
| * @param splitPoint the split point whose code needs to be loaded |
| */ |
| void inject(int splitPoint, LoadTerminatedHandler loadErrorHandler) { |
| pendingDownloadErrorHandlers[splitPoint] = loadErrorHandler; |
| if (!isInitial(splitPoint)) { |
| requestedExclusives.add(splitPoint); |
| } |
| startLoadingNextFragment(); |
| } |
| |
| void leftoversFragmentHasLoaded() { |
| onLoadImpl(leftoversFragment()); |
| } |
| |
| private boolean anyPrefetchesRequested() { |
| return prefetching && prefetchQueue != null && prefetchQueue.size() > 0; |
| } |
| |
| /** |
| * Clear out any inject and prefetch requests that are already loaded. Only |
| * remove items from the head of each queue; any stale entries later in the |
| * queue will be removed later. |
| */ |
| private void clearRequestsAlreadyLoaded() { |
| while (requestedExclusives.size() > 0 && isLoaded[requestedExclusives.peek()]) { |
| int offset = requestedExclusives.remove(); |
| if (offset < pendingDownloadErrorHandlers.length) { |
| pendingDownloadErrorHandlers[offset] = null; |
| } |
| } |
| |
| if (prefetchQueue != null) { |
| while (prefetchQueue.size() > 0 && isLoaded[prefetchQueue.peek()]) { |
| prefetchQueue.remove(); |
| } |
| } |
| } |
| |
| private String downloadGroup(int fragment) { |
| return (fragment == leftoversFragment()) ? LwmLabels.LEFTOVERS_DOWNLOAD : LwmLabels |
| .downloadGroupForExclusive(fragment); |
| } |
| |
| /** |
| * Return whether all initial fragments have completed loading. |
| */ |
| private boolean haveInitialFragmentsLoaded() { |
| return remainingInitialFragments != null && remainingInitialFragments.size() == 0; |
| } |
| |
| /** |
| * Initialize {@link #remainingInitialFragments} if it isn't already. |
| */ |
| private void initializeRemainingInitialFragments() { |
| if (remainingInitialFragments == null) { |
| remainingInitialFragments = new BoundedIntQueue(initialLoadSequence.length + 1); |
| for (int sp : initialLoadSequence) { |
| remainingInitialFragments.add(sp); |
| } |
| remainingInitialFragments.add(leftoversFragment()); |
| } |
| } |
| |
| /** |
| * Returns <code>true</code> if array contains only <code>null</code> |
| * elements. |
| */ |
| private boolean isEmpty(Object[] array) { |
| for (int i = 0; i < array.length; i++) { |
| if (array[i] != null) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private boolean isInitial(int splitPoint) { |
| if (splitPoint == leftoversFragment()) { |
| return true; |
| } |
| for (int sp : initialLoadSequence) { |
| if (sp == splitPoint) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean isLoading(int splitPoint) { |
| return pendingDownloadErrorHandlers[splitPoint] != null; |
| } |
| |
| private int leftoversFragment() { |
| return numEntries; |
| } |
| |
| private void logDownloadStart(int fragment) { |
| logEventProgress(downloadGroup(fragment), LwmLabels.BEGIN, fragment, -1); |
| } |
| |
| /** |
| * Log an event with the {@Logger} this instance was provided. |
| */ |
| private void logEventProgress(String eventGroup, String type) { |
| logEventProgress(eventGroup, type, -1, -1); |
| } |
| |
| /** |
| * 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 void logEventProgress(String eventGroup, String type, int fragment, int size) { |
| logger.logEventProgress(eventGroup, type, fragment, size); |
| } |
| |
| private void logFragmentLoaded(int fragment) { |
| String logGroup = downloadGroup(fragment); |
| logEventProgress(logGroup, LwmLabels.END, fragment, -1); |
| } |
| |
| private void onLoadImpl(int fragment) { |
| fragmentHasLoaded(fragment); |
| Object[] callbacks = allCallbacks[fragment]; |
| if (callbacks != null) { |
| logEventProgress("runCallbacks" + fragment, "begin"); |
| allCallbacks[fragment] = null; |
| for (Object callback : callbacks) { |
| try { |
| ((RunAsyncCallback) callback).onSuccess(); |
| } catch (Throwable t) { |
| GWT.reportUncaughtException(t); |
| } |
| } |
| logEventProgress("runCallbacks" + fragment, "end"); |
| } |
| } |
| |
| private void runAsyncImpl(final int fragment, RunAsyncCallback callback) { |
| if (isLoaded[fragment]) { |
| assert allCallbacks[fragment] == null; |
| this.onSuccessExecutor.execute(this, callback); |
| return; |
| } |
| |
| Object[] callbacks = allCallbacks[fragment]; |
| if (callbacks == null) { |
| callbacks = allCallbacks[fragment] = new RunAsyncCallback[0]; |
| } |
| // Take advantage of no range checking in web mode. |
| assert GWT.isScript(); |
| callbacks[callbacks.length] = callback; |
| |
| if (!isLoading(fragment)) { |
| inject(fragment, new AsyncFragmentLoader.LoadTerminatedHandler() { |
| @Override |
| public void loadTerminated(Throwable reason) { |
| Object[] callbacks = allCallbacks[fragment]; |
| if (callbacks != null) { |
| allCallbacks[fragment] = null; |
| for (Object callback : callbacks) { |
| ((RunAsyncCallback) callback).onFailure(reason); |
| } |
| } |
| } |
| }); |
| } |
| } |
| |
| void executeOnSuccess0(RunAsyncCallback callback) { |
| /* |
| * Calls on {@link RunAsyncCallback#onSuccess} from {@link AsyncFragmentLoader} is special |
| * treated (See RescueVisitor in ControlFlowAnalyzer) so that code splitter will not follow them |
| * on fragment analysis. That is, if don't call onSuccess from here and instead call it directly |
| * from the scheduled command, then it will make the code splitter put the split point code in |
| * the initial fragment. |
| */ |
| callback.onSuccess(); |
| } |
| |
| private void startLoadingFragment(int fragment) { |
| assert (fragmentLoading < 0); |
| fragmentLoading = fragment; |
| logDownloadStart(fragment); |
| loadingStrategy.startLoadingFragment(fragment, new ResetAfterDownloadFailure(fragment)); |
| } |
| |
| /** |
| * Start downloading the next fragment queued up, if there are any. |
| */ |
| private void startLoadingNextFragment() { |
| if (fragmentLoading >= 0) { |
| // Already loading something |
| return; |
| } |
| |
| initializeRemainingInitialFragments(); |
| clearRequestsAlreadyLoaded(); |
| |
| if (isEmpty(pendingDownloadErrorHandlers) && !anyPrefetchesRequested()) { |
| /* |
| * Don't load anything if there aren't any requests outstanding. |
| */ |
| return; |
| } |
| |
| // Check if an initial needs downloading |
| if (remainingInitialFragments.size() > 0) { |
| startLoadingFragment(remainingInitialFragments.peek()); |
| return; |
| } |
| |
| assert (haveInitialFragmentsLoaded()); |
| |
| // Check if an exclusive is pending |
| if (requestedExclusives.size() > 0) { |
| startLoadingFragment(requestedExclusives.remove()); |
| return; |
| } |
| |
| // Check the prefetch queue |
| if (anyPrefetchesRequested()) { |
| startLoadingFragment(prefetchQueue.remove()); |
| return; |
| } |
| |
| // Nothing needed downloading after all?! |
| assert false; |
| } |
| } |