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);
+  }
+}