Revision: 6402 Author: [email protected] Date: Fri Oct 16 15:20:56 2009 Log: Initial add of Scheduler API.
Patch by: bobv Review by: bruce, jgw http://gwt-code-reviews.appspot.com/77820 http://code.google.com/p/google-web-toolkit/source/detail?r=6402 Added: /trunk/user/src/com/google/gwt/core/client/Scheduler.java /trunk/user/src/com/google/gwt/core/client/impl/SchedulerImpl.java Modified: /trunk/dev/core/src/com/google/gwt/dev/shell/ModuleSpace.java /trunk/user/src/com/google/gwt/core/client/impl/Impl.java ======================================= --- /dev/null +++ /trunk/user/src/com/google/gwt/core/client/Scheduler.java Fri Oct 16 15:20:56 2009 @@ -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); +} ======================================= --- /dev/null +++ /trunk/user/src/com/google/gwt/core/client/impl/SchedulerImpl.java Fri Oct 16 15:20:56 2009 @@ -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); + } +} ======================================= --- /trunk/dev/core/src/com/google/gwt/dev/shell/ModuleSpace.java Fri Oct 16 14:04:54 2009 +++ /trunk/dev/core/src/com/google/gwt/dev/shell/ModuleSpace.java Fri Oct 16 15:20:56 2009 @@ -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; - } - } 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); + 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 + } + 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( ======================================= --- /trunk/user/src/com/google/gwt/core/client/impl/Impl.java Fri Oct 16 14:04:54 2009 +++ /trunk/user/src/com/google/gwt/core/client/impl/Impl.java Fri Oct 16 15:20:56 2009 @@ -153,6 +153,16 @@ return _; } }-*/; + + /** + * 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)}. @@ -160,10 +170,7 @@ @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; } } --~--~---------~--~----~------------~-------~--~----~ http://groups.google.com/group/Google-Web-Toolkit-Contributors -~----------~----~----~----~------~----~------~--~---
