Initial add of Scheduler API.
Patch by: bobv
Review by: bruce, jgw
http://gwt-code-reviews.appspot.com/77820
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6402 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/shell/ModuleSpace.java b/dev/core/src/com/google/gwt/dev/shell/ModuleSpace.java
index 47f8f0e..73255a0 100644
--- a/dev/core/src/com/google/gwt/dev/shell/ModuleSpace.java
+++ b/dev/core/src/com/google/gwt/dev/shell/ModuleSpace.java
@@ -342,36 +342,47 @@
String entryPointTypeName = null;
try {
// Set up GWT-entry code
- Class<?> clazz = loadClassFromSourceName("com.google.gwt.core.client.impl.Impl");
- Method registerEntry = clazz.getMethod("registerEntry");
+ Class<?> implClass = loadClassFromSourceName("com.google.gwt.core.client.impl.Impl");
+ Method registerEntry = implClass.getDeclaredMethod("registerEntry");
+ registerEntry.setAccessible(true);
registerEntry.invoke(null);
+ Method enter = implClass.getDeclaredMethod("enter");
+ enter.setAccessible(true);
+ enter.invoke(null);
+
String[] entryPoints = host.getEntryPointTypeNames();
if (entryPoints.length > 0) {
- for (int i = 0; i < entryPoints.length; i++) {
- entryPointTypeName = entryPoints[i];
- clazz = loadClassFromSourceName(entryPointTypeName);
- Method onModuleLoad = null;
- try {
- onModuleLoad = clazz.getMethod("onModuleLoad");
- if (!Modifier.isStatic(onModuleLoad.getModifiers())) {
- // it's non-static, so we need to rebind the class
- onModuleLoad = null;
+ try {
+ for (int i = 0; i < entryPoints.length; i++) {
+ entryPointTypeName = entryPoints[i];
+ Class<?> clazz = loadClassFromSourceName(entryPointTypeName);
+ Method onModuleLoad = null;
+ try {
+ onModuleLoad = clazz.getMethod("onModuleLoad");
+ if (!Modifier.isStatic(onModuleLoad.getModifiers())) {
+ // it's non-static, so we need to rebind the class
+ onModuleLoad = null;
+ }
+ } catch (NoSuchMethodException e) {
+ // okay, try rebinding it; maybe the rebind result will have one
}
- } catch (NoSuchMethodException e) {
- // okay, try rebinding it; maybe the rebind result will have one
+ Object module = null;
+ if (onModuleLoad == null) {
+ module = rebindAndCreate(entryPointTypeName);
+ onModuleLoad = module.getClass().getMethod("onModuleLoad");
+ // Record the rebound name of the class for stats (below).
+ entryPointTypeName = module.getClass().getName().replace('$', '.');
+ }
+ onModuleLoad.setAccessible(true);
+ invokeNativeVoid("fireOnModuleLoadStart", null,
+ new Class[] {String.class}, new Object[] {entryPointTypeName});
+ onModuleLoad.invoke(module);
}
- Object module = null;
- if (onModuleLoad == null) {
- module = rebindAndCreate(entryPointTypeName);
- onModuleLoad = module.getClass().getMethod("onModuleLoad");
- // Record the rebound name of the class for stats (below).
- entryPointTypeName = module.getClass().getName().replace('$', '.');
- }
- onModuleLoad.setAccessible(true);
- invokeNativeVoid("fireOnModuleLoadStart", null,
- new Class[] {String.class}, new Object[] {entryPointTypeName});
- onModuleLoad.invoke(module);
+ } finally {
+ Method exit = implClass.getDeclaredMethod("exit", boolean.class);
+ exit.setAccessible(true);
+ exit.invoke(null, true);
}
} else {
logger.log(
diff --git a/user/src/com/google/gwt/core/client/Scheduler.java b/user/src/com/google/gwt/core/client/Scheduler.java
new file mode 100644
index 0000000..51d616f
--- /dev/null
+++ b/user/src/com/google/gwt/core/client/Scheduler.java
@@ -0,0 +1,121 @@
+/*
+ * 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;
+
+import com.google.gwt.core.client.impl.SchedulerImpl;
+
+/**
+ * This class provides low-level task scheduling primitives. Any exceptions
+ * thrown by the command objects executed by the scheduler will be passed to the
+ * {@link GWT.UncaughtExceptionHandler} if one is installed.
+ */
+public abstract class Scheduler {
+
+ /**
+ * General-purpose Command interface for tasks that repeat.
+ */
+ public interface RepeatingCommand {
+ /**
+ * Returns true if the RepeatingCommand should be invoked again.
+ */
+ boolean execute();
+ }
+
+ /**
+ * General-purpose Command interface.
+ */
+ public interface ScheduledCommand {
+ /**
+ * Invokes the command.
+ */
+ void execute();
+ }
+
+ /**
+ * Use a GWT.create() here to make it simple to hijack the default
+ * implementation.
+ */
+ private static final Scheduler IMPL = GWT.create(SchedulerImpl.class);
+
+ /**
+ * Returns the default implementation of the Scheduler API.
+ */
+ public static Scheduler get() {
+ return IMPL;
+ }
+
+ /**
+ * A deferred command is executed after the browser event loop returns.
+ */
+ public abstract void scheduleDeferred(ScheduledCommand cmd);
+
+ /**
+ * A "finally" command will be executed before GWT-generated code returns
+ * control to the browser's event loop. This type of command is used to
+ * aggregate small amounts of work before performing a non-recurring,
+ * heavyweight operation.
+ * <p>
+ * Consider the following:
+ *
+ * <pre>
+ * try {
+ * nativeEventCallback(); // Calls scheduleFinally one or more times
+ * } finally {
+ * executeFinallyCommands();
+ * }
+ * </pre>
+ *
+ * @see com.google.gwt.dom.client.StyleInjector
+ */
+ public abstract void scheduleFinally(ScheduledCommand cmd);
+
+ /**
+ * Schedules a repeating command that is scheduled with a constant delay. That
+ * is, the next invocation of the command will be scheduled for
+ * <code>delayMs</code> milliseconds after the last invocation completes.
+ * <p>
+ * For example, assume that a command takes 30ms to run and a 100ms delay is
+ * provided. The second invocation of the command will occur at 130ms after
+ * the first invocation starts.
+ *
+ * @param cmd the command to execute
+ * @param delayMs the amount of time to wait after one invocation ends before
+ * the next invocation
+ */
+ public abstract void scheduleFixedDelay(RepeatingCommand cmd, int delayMs);
+
+ /**
+ * Schedules a repeating command that is scheduled with a constant
+ * periodicity. That is, the command will be invoked every
+ * <code>delayMs</code> milliseconds, regardless of how long the previous
+ * invocation took to complete.
+ *
+ * @param cmd the command to execute
+ * @param delayMs the period with which the command is executed
+ */
+ public abstract void scheduleFixedPeriod(RepeatingCommand cmd, int delayMs);
+
+ /**
+ * Schedules a repeating command that performs incremental work. This type of
+ * command is encouraged for long-running processes that perform computation
+ * or that manipulate the DOM. The commands in this queue are invoked many
+ * times in rapid succession and are then deferred to allow the browser to
+ * process its event queue.
+ *
+ * @param cmd the command to execute
+ */
+ public abstract void scheduleIncremental(RepeatingCommand cmd);
+}
diff --git a/user/src/com/google/gwt/core/client/impl/Impl.java b/user/src/com/google/gwt/core/client/impl/Impl.java
index 458c7dd..408552d 100644
--- a/user/src/com/google/gwt/core/client/impl/Impl.java
+++ b/user/src/com/google/gwt/core/client/impl/Impl.java
@@ -155,15 +155,22 @@
}-*/;
/**
+ * Called by ModuleSpace in hosted mode when running onModuleLoads.
+ */
+ private static boolean enter() {
+ assert entryDepth >= 0 : "Negative entryDepth value at entry " + entryDepth;
+
+ // We want to disable some actions in the reentrant case
+ return entryDepth++ == 0;
+ }
+
+ /**
* Implements {@link #entry(JavaScriptObject)}.
*/
@SuppressWarnings("unused")
private static Object entry0(Object jsFunction, Object thisObj,
Object arguments) throws Throwable {
- assert entryDepth >= 0 : "Negative entryDepth value at entry " + entryDepth;
-
- // We want to disable some actions in the reentrant case
- boolean initialEntry = entryDepth++ == 0;
+ boolean initialEntry = enter();
try {
/*
@@ -188,12 +195,23 @@
return apply(jsFunction, thisObj, arguments);
}
} finally {
- if (initialEntry) {
- // TODO(bobv) FinallyCommand.flush() goes here
- }
- entryDepth--;
- assert entryDepth >= 0 : "Negative entryDepth value at exit "
- + entryDepth;
+ exit(initialEntry);
+ }
+ }
+
+ /**
+ * Called by ModuleSpace in hosted mode when running onModuleLoads.
+ */
+ private static void exit(boolean initialEntry) {
+ if (initialEntry) {
+ SchedulerImpl.flushFinallyCommands();
+ }
+
+ // Decrement after we call flush
+ entryDepth--;
+ assert entryDepth >= 0 : "Negative entryDepth value at exit " + entryDepth;
+ if (initialEntry) {
+ assert entryDepth == 0 : "Depth not 0" + entryDepth;
}
}
diff --git a/user/src/com/google/gwt/core/client/impl/SchedulerImpl.java b/user/src/com/google/gwt/core/client/impl/SchedulerImpl.java
new file mode 100644
index 0000000..0ff623d
--- /dev/null
+++ b/user/src/com/google/gwt/core/client/impl/SchedulerImpl.java
@@ -0,0 +1,308 @@
+/*
+ * 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.Duration;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.Scheduler;
+
+/**
+ * This is used by Scheduler to collaborate with Impl in order to have
+ * FinallyCommands executed.
+ */
+public class SchedulerImpl extends Scheduler {
+
+ /**
+ * Metadata bag for command objects. It's a JSO so that a lightweight JsArray
+ * can be used instead of a Collections type.
+ */
+ static final class Task extends JavaScriptObject {
+ public static native Task create(RepeatingCommand cmd) /*-{
+ return [cmd, true];
+ }-*/;
+
+ public static native Task create(ScheduledCommand cmd) /*-{
+ return [cmd, false];
+ }-*/;
+
+ protected Task() {
+ }
+
+ public boolean executeRepeating() {
+ return getRepeating().execute();
+ }
+
+ public void executeScheduled() {
+ getScheduled().execute();
+ }
+
+ /**
+ * Has implicit cast.
+ */
+ private native RepeatingCommand getRepeating() /*-{
+ return this[0];
+ }-*/;
+
+ /**
+ * Has implicit cast.
+ */
+ private native ScheduledCommand getScheduled() /*-{
+ return this[0];
+ }-*/;
+
+ private native boolean isRepeating() /*-{
+ return this[1];
+ }-*/;
+ }
+
+ /**
+ * A RepeatingCommand that calls flushPostEventPumpCommands(). It repeats if
+ * there are any outstanding deferred or incremental commands.
+ */
+ static final RepeatingCommand FLUSHER = new RepeatingCommand() {
+ public boolean execute() {
+ flushRunning = true;
+ flushPostEventPumpCommands();
+ /*
+ * No finally here, we want this to be clear only on a normal exit. An
+ * abnormal exit would indicate that an exception isn't being caught
+ * correctly or that a slow script warning canceled the timer.
+ */
+ flushRunning = false;
+ return shouldBeRunning = isWorkQueued();
+ }
+ };
+
+ /**
+ * This provides some backup for the main flusher task in case it gets shut
+ * down by a slow-script warning.
+ */
+ static final RepeatingCommand RESCUE = new RepeatingCommand() {
+ public boolean execute() {
+ if (flushRunning) {
+ /*
+ * Since JS is single-threaded, if we're here, then than means that
+ * FLUSHER.execute() started, but did not finish. Reschedule FLUSHER.
+ */
+ scheduleFixedDelayImpl(FLUSHER, FLUSHER_DELAY);
+ }
+ return shouldBeRunning;
+ }
+ };
+
+ /**
+ * Indicates the location of a previously-live command that has been removed
+ * from the queue.
+ */
+ static final Task TOMBSTONE = JavaScriptObject.createObject().cast();
+
+ /*
+ * Work queues. Timers store their state on the function, so we don't need to
+ * track them.
+ */
+ static final JsArray<Task> DEFERRED_COMMANDS = JavaScriptObject.createArray().cast();
+
+ static final JsArray<Task> INCREMENTAL_COMMANDS = JavaScriptObject.createArray().cast();
+
+ static final JsArray<Task> FINALLY_COMMANDS = JavaScriptObject.createArray().cast();
+
+ /*
+ * These two flags are used to control the state of the flusher and rescuer
+ * commands.
+ */
+ private static boolean shouldBeRunning = false;
+
+ private static boolean flushRunning = false;
+
+ /**
+ * The delay between flushing the task queues.
+ */
+ private static final int FLUSHER_DELAY = 1;
+ /**
+ * The delay between checking up on SSW problems.
+ */
+ private static final int RESCUE_DELAY = 50;
+ /**
+ * The amount of time that we're willing to spend executing
+ * IncrementalCommands.
+ */
+ private static final double TIME_SLICE = 100;
+
+ public static void scheduleDeferredImpl(ScheduledCommand cmd) {
+ DEFERRED_COMMANDS.push(Task.create(cmd));
+ maybeSchedulePostEventPumpCommands();
+ }
+
+ public static void scheduleFinallyImpl(ScheduledCommand cmd) {
+ FINALLY_COMMANDS.push(Task.create(cmd));
+ }
+
+ public static native void scheduleFixedDelayImpl(RepeatingCommand cmd,
+ int delayMs) /*-{
+ $wnd.setTimeout(function() {
+ // $entry takes care of uncaught exception handling
+ var ret = $entry(@com.google.gwt.core.client.impl.SchedulerImpl::execute(Lcom/google/gwt/core/client/Scheduler$RepeatingCommand;))(cmd);
+ if (ret) {
+ $wnd.setTimeout(arguments.callee, delayMs);
+ }
+ }, delayMs);
+ }-*/;
+
+ public static native void scheduleFixedPeriodImpl(RepeatingCommand cmd,
+ int delayMs) /*-{
+ var fn = function() {
+ // $entry takes care of uncaught exception handling
+ var ret = $entry(@com.google.gwt.core.client.impl.SchedulerImpl::execute(Lcom/google/gwt/core/client/Scheduler$RepeatingCommand;))(cmd);
+ if (!ret) {
+ // Either canceled or threw an exception
+ $wnd.clearInterval(arguments.callee.token);
+ }
+ };
+ fn.token = $wnd.setInterval(fn, delayMs);
+ }-*/;
+
+ public static void scheduleIncrementalImpl(RepeatingCommand cmd) {
+ INCREMENTAL_COMMANDS.push(Task.create(cmd));
+ maybeSchedulePostEventPumpCommands();
+ }
+
+ /**
+ * Called by {@link Impl#entry(JavaScriptObject)}.
+ */
+ static void flushFinallyCommands() {
+ runScheduledTasks(FINALLY_COMMANDS, FINALLY_COMMANDS);
+ }
+
+ /**
+ * Called by Flusher.
+ */
+ static void flushPostEventPumpCommands() {
+ runScheduledTasks(DEFERRED_COMMANDS, INCREMENTAL_COMMANDS);
+ runRepeatingTasks(INCREMENTAL_COMMANDS);
+ }
+
+ static boolean isWorkQueued() {
+ return DEFERRED_COMMANDS.length() > 0 || INCREMENTAL_COMMANDS.length() > 0;
+ }
+
+ /**
+ * Called from scheduledFixedInterval to give $entry a static function.
+ */
+ @SuppressWarnings("unused")
+ private static boolean execute(RepeatingCommand cmd) {
+ return cmd.execute();
+ }
+
+ private static void maybeSchedulePostEventPumpCommands() {
+ if (!shouldBeRunning) {
+ shouldBeRunning = true;
+ scheduleFixedDelayImpl(FLUSHER, FLUSHER_DELAY);
+ scheduleFixedDelayImpl(RESCUE, RESCUE_DELAY);
+ }
+ }
+
+ /**
+ * Execute a list of Tasks that hold RepeatingCommands.
+ */
+ private static void runRepeatingTasks(JsArray<Task> tasks) {
+ boolean canceledSomeTasks = false;
+ int length = tasks.length();
+ double start = Duration.currentTimeMillis();
+
+ while (Duration.currentTimeMillis() - start < TIME_SLICE) {
+ for (int i = 0; i < length; i++) {
+ Task t = tasks.get(i);
+ if (t == TOMBSTONE) {
+ continue;
+ }
+
+ assert t.isRepeating() : "Found a non-repeating Task";
+
+ if (!t.executeRepeating()) {
+ tasks.set(i, TOMBSTONE);
+ canceledSomeTasks = true;
+ }
+ }
+ }
+
+ if (canceledSomeTasks) {
+ // Remove tombstones
+ int last = 0;
+ for (int i = 0; i < length; i++) {
+ if (tasks.get(i) == TOMBSTONE) {
+ continue;
+ }
+ tasks.set(last++, tasks.get(i));
+ }
+ tasks.setLength(last + 1);
+ }
+ }
+
+ /**
+ * Execute a list of Tasks that hold both ScheduledCommands and
+ * RepeatingCommands. Any RepeatingCommands in the <code>tasks</code> queue
+ * that want to repeat will be pushed onto the <code>rescheduled</code> queue.
+ */
+ private static void runScheduledTasks(JsArray<Task> tasks,
+ JsArray<Task> rescheduled) {
+ // Use the while-shift pattern in case additional commands are enqueued
+ while (tasks.length() > 0) {
+ Task t = tasks.shift();
+
+ try {
+ // Move repeating commands to incremental commands queue
+ if (t.isRepeating()) {
+ if (t.executeRepeating()) {
+ rescheduled.push(t);
+ }
+ } else {
+ t.executeScheduled();
+ }
+ } catch (RuntimeException e) {
+ if (GWT.getUncaughtExceptionHandler() != null) {
+ GWT.getUncaughtExceptionHandler().onUncaughtException(e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void scheduleDeferred(ScheduledCommand cmd) {
+ scheduleDeferredImpl(cmd);
+ }
+
+ @Override
+ public void scheduleFinally(ScheduledCommand cmd) {
+ scheduleFinallyImpl(cmd);
+ }
+
+ @Override
+ public void scheduleFixedDelay(RepeatingCommand cmd, int delayMs) {
+ scheduleFixedDelayImpl(cmd, delayMs);
+ }
+
+ @Override
+ public void scheduleFixedPeriod(RepeatingCommand cmd, int delayMs) {
+ scheduleFixedPeriodImpl(cmd, delayMs);
+ }
+
+ @Override
+ public void scheduleIncremental(RepeatingCommand cmd) {
+ scheduleIncrementalImpl(cmd);
+ }
+}