Update Scheduler API. - Add EntryCommands - Add repeating FinallyCommand - Always empty the FinallyCommand queue before returning to the browser Add state-getting method to Impl and add a fix for the finally block potentially not firing on IE. Patch by: bobv Review by: jat git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7315 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/core/client/Scheduler.java b/user/src/com/google/gwt/core/client/Scheduler.java index e7d71de..ef6a090 100644 --- a/user/src/com/google/gwt/core/client/Scheduler.java +++ b/user/src/com/google/gwt/core/client/Scheduler.java
@@ -57,11 +57,48 @@ public abstract void scheduleDeferred(ScheduledCommand cmd); /** + * An "entry" command will be executed before GWT-generated code is invoked by + * the browser's event loop. The {@link RepeatingCommand} will be called once + * per entry from the event loop until <code>false</code> is returned. This + * type of command is appropriate for instrumentation or code that needs to + * know when "something happens." + * <p> + * If an entry command schedules another entry command, the second command + * will be executed before control flow continues to the GWT-generated code. + */ + public abstract void scheduleEntry(RepeatingCommand cmd); + + /** + * An "entry" command will be executed before GWT-generated code is invoked by + * the browser's event loop. This type of command is appropriate for code that + * needs to know when "something happens." + * <p> + * If an entry command schedules another entry command, the second command + * will be executed before control flow continues to the GWT-generated code. + */ + public abstract void scheduleEntry(ScheduledCommand cmd); + + /** + * A "finally" command will be executed before GWT-generated code returns + * control to the browser's event loop. The {@link RepeatingCommand#execute()} + * method will be called once per exit to the event loop until + * <code>false</code> is returned. This type of command is appropriate for + * instrumentation or cleanup code. + * <p> + * If a finally command schedules another finally command, the second command + * will be executed before control flow returns to the browser. + */ + public abstract void scheduleFinally(RepeatingCommand 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> + * If a finally command schedules another finally command, the second command + * will be executed before control flow returns to the browser. + * <p> * Consider the following: * * <pre>
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 4fd7774..3dd0f85 100644 --- a/user/src/com/google/gwt/core/client/impl/Impl.java +++ b/user/src/com/google/gwt/core/client/impl/Impl.java
@@ -53,7 +53,15 @@ */ public static native JavaScriptObject entry(JavaScriptObject jsFunction) /*-{ return function() { - return @com.google.gwt.core.client.impl.Impl::entry0(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)(jsFunction, this, arguments); + try { + return @com.google.gwt.core.client.impl.Impl::entry0(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)(jsFunction, this, arguments); + } catch (e) { + // This catch block is here to ensure that the finally block in entry0 + // will be executed correctly on IE6/7. We can't put a catch Throwable + // in entry0 because this would always cause the unhandled exception to + // be wrapped in a JavaScriptException type. + throw e; + } }; }-*/; @@ -128,6 +136,20 @@ }-*/; /** + * Indicates if <code>$entry</code> has been called. + */ + public static boolean isEntryOnStack() { + return entryDepth > 0; + } + + /** + * Indicates if <code>$entry</code> is present on the stack more than once. + */ + public static boolean isNestedEntry() { + return entryDepth > 1; + } + + /** * Implicitly called by JavaToJavaScriptCompiler.findEntryPoints(). */ public static native JavaScriptObject registerEntry() /*-{ @@ -161,7 +183,11 @@ assert entryDepth >= 0 : "Negative entryDepth value at entry " + entryDepth; // We want to disable some actions in the reentrant case - return entryDepth++ == 0; + if (entryDepth++ == 0) { + SchedulerImpl.INSTANCE.flushEntryCommands(); + return true; + } + return false; } /** @@ -194,6 +220,11 @@ // Can't handle any exceptions, let them percolate normally return apply(jsFunction, thisObj, arguments); } + + /* + * DO NOT ADD catch(Throwable t) here, it would always wrap the thrown + * value. Instead, entry() has a general catch-all block. + */ } finally { exit(initialEntry); }
diff --git a/user/src/com/google/gwt/core/client/impl/SchedulerImpl.java b/user/src/com/google/gwt/core/client/impl/SchedulerImpl.java index eccabe7..ea8b598 100644 --- a/user/src/com/google/gwt/core/client/impl/SchedulerImpl.java +++ b/user/src/com/google/gwt/core/client/impl/SchedulerImpl.java
@@ -70,6 +70,39 @@ } /** + * Calls {@link SchedulerImpl#flushPostEventPumpCommands()}. + */ + private final class Flusher implements 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(); + } + } + + /** + * Keeps {@link Flusher} running. + */ + private final class Rescuer implements 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. + */ + scheduleFixedDelay(flusher, FLUSHER_DELAY); + } + return shouldBeRunning; + } + } + + /** * Use a GWT.create() here to make it simple to hijack the default * implementation. */ @@ -92,6 +125,13 @@ private static final double TIME_SLICE = 100; /** + * Extract boilerplate code. + */ + private static JsArray<Task> createQueue() { + return JavaScriptObject.createArray().cast(); + } + + /** * Called from scheduledFixedInterval to give $entry a static function. */ @SuppressWarnings("unused") @@ -100,12 +140,25 @@ } /** + * Provides lazy-init pattern for the task queues. + */ + private static JsArray<Task> push(JsArray<Task> queue, Task task) { + if (queue == null) { + queue = createQueue(); + } + queue.push(task); + return queue; + } + + /** * Execute a list of Tasks that hold RepeatingCommands. * * @return A replacement array that is possibly a shorter copy of * <code>tasks</code> */ private static JsArray<Task> runRepeatingTasks(JsArray<Task> tasks) { + assert tasks != null : "tasks"; + boolean canceledSomeTasks = false; int length = tasks.length(); double start = Duration.currentTimeMillis(); @@ -129,16 +182,15 @@ } if (canceledSomeTasks) { - JsArray<Task> newTasks = JavaScriptObject.createArray().cast(); + JsArray<Task> newTasks = createQueue(); // Remove tombstones for (int i = 0; i < length; i++) { - if (tasks.get(i) == null) { - continue; + if (tasks.get(i) != null) { + newTasks.push(tasks.get(i)); } - newTasks.push(tasks.get(i)); } assert newTasks.length() < length; - return newTasks; + return newTasks.length() == 0 ? null : newTasks; } else { return tasks; } @@ -150,9 +202,14 @@ * that want to repeat will be pushed onto the <code>rescheduled</code> queue. * The contents of <code>tasks</code> may not be altered while this method is * executing. + * + * @return <code>rescheduled</code> or a newly-allocated array if + * <code>rescheduled</code> is null. */ - private static void runScheduledTasks(JsArray<Task> tasks, + private static JsArray<Task> runScheduledTasks(JsArray<Task> tasks, JsArray<Task> rescheduled) { + assert tasks != null : "tasks"; + for (int i = 0, j = tasks.length(); i < j; i++) { assert tasks.length() == j : "Working array length changed " + tasks.length() + " != " + j; @@ -162,7 +219,7 @@ // Move repeating commands to incremental commands queue if (t.isRepeating()) { if (t.executeRepeating()) { - rescheduled.push(t); + rescheduled = push(rescheduled, t); } } else { t.executeScheduled(); @@ -173,6 +230,7 @@ } } } + return rescheduled; } private static native void scheduleFixedDelayImpl(RepeatingCommand cmd, @@ -211,36 +269,13 @@ * A RepeatingCommand that calls flushPostEventPumpCommands(). It repeats if * there are any outstanding deferred or incremental commands. */ - 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(); - } - }; + Flusher flusher; /** * This provides some backup for the main flusher task in case it gets shut * down by a slow-script warning. */ - 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. - */ - scheduleFixedDelay(flusher, FLUSHER_DELAY); - } - return shouldBeRunning; - } - }; + Rescuer rescue; /* * Work queues. Timers store their state on the function, so we don't need to @@ -248,35 +283,74 @@ * Processing the values in the queues is a one-shot, and then the array is * discarded. */ - JsArray<Task> deferredCommands = JavaScriptObject.createArray().cast(); - JsArray<Task> incrementalCommands = JavaScriptObject.createArray().cast(); - JsArray<Task> finallyCommands = JavaScriptObject.createArray().cast(); + JsArray<Task> deferredCommands; + JsArray<Task> entryCommands; + JsArray<Task> finallyCommands; + JsArray<Task> incrementalCommands; /* * These two flags are used to control the state of the flusher and rescuer * commands. */ - private boolean shouldBeRunning = false; private boolean flushRunning = false; + private boolean shouldBeRunning = false; + + /** + * Called by {@link Impl#entry(JavaScriptObject)}. + */ + public void flushEntryCommands() { + if (entryCommands != null) { + JsArray<Task> rescheduled = null; + // This do-while loop handles commands scheduling commands + do { + JsArray<Task> oldQueue = entryCommands; + entryCommands = null; + rescheduled = runScheduledTasks(oldQueue, rescheduled); + } while (entryCommands != null); + entryCommands = rescheduled; + } + } /** * Called by {@link Impl#entry(JavaScriptObject)}. */ public void flushFinallyCommands() { - JsArray<Task> oldFinally = finallyCommands; - finallyCommands = JavaScriptObject.createArray().cast(); - runScheduledTasks(oldFinally, finallyCommands); + if (finallyCommands != null) { + JsArray<Task> rescheduled = null; + // This do-while loop handles commands scheduling commands + do { + JsArray<Task> oldQueue = finallyCommands; + finallyCommands = null; + rescheduled = runScheduledTasks(oldQueue, rescheduled); + } while (finallyCommands != null); + finallyCommands = rescheduled; + } } @Override public void scheduleDeferred(ScheduledCommand cmd) { - deferredCommands.push(Task.create(cmd)); + deferredCommands = push(deferredCommands, Task.create(cmd)); maybeSchedulePostEventPumpCommands(); } @Override + public void scheduleEntry(RepeatingCommand cmd) { + entryCommands = push(entryCommands, Task.create(cmd)); + } + + @Override + public void scheduleEntry(ScheduledCommand cmd) { + entryCommands = push(entryCommands, Task.create(cmd)); + } + + @Override + public void scheduleFinally(RepeatingCommand cmd) { + finallyCommands = push(finallyCommands, Task.create(cmd)); + } + + @Override public void scheduleFinally(ScheduledCommand cmd) { - finallyCommands.push(Task.create(cmd)); + finallyCommands = push(finallyCommands, Task.create(cmd)); } @Override @@ -292,7 +366,7 @@ @Override public void scheduleIncremental(RepeatingCommand cmd) { // Push repeating commands onto the same initial queue for relative order - deferredCommands.push(Task.create(cmd)); + deferredCommands = push(deferredCommands, Task.create(cmd)); maybeSchedulePostEventPumpCommands(); } @@ -300,21 +374,38 @@ * Called by Flusher. */ void flushPostEventPumpCommands() { - JsArray<Task> oldDeferred = deferredCommands; - deferredCommands = JavaScriptObject.createArray().cast(); + if (deferredCommands != null) { + JsArray<Task> oldDeferred = deferredCommands; + deferredCommands = null; - runScheduledTasks(oldDeferred, incrementalCommands); - incrementalCommands = runRepeatingTasks(incrementalCommands); + /* We might not have any incremental commands queued. */ + if (incrementalCommands == null) { + incrementalCommands = createQueue(); + } + runScheduledTasks(oldDeferred, incrementalCommands); + } + + if (incrementalCommands != null) { + incrementalCommands = runRepeatingTasks(incrementalCommands); + } } boolean isWorkQueued() { - return deferredCommands.length() > 0 || incrementalCommands.length() > 0; + return deferredCommands != null || incrementalCommands != null; } private void maybeSchedulePostEventPumpCommands() { if (!shouldBeRunning) { shouldBeRunning = true; + + if (flusher == null) { + flusher = new Flusher(); + } scheduleFixedDelayImpl(flusher, FLUSHER_DELAY); + + if (rescue == null) { + rescue = new Rescuer(); + } scheduleFixedDelayImpl(rescue, RESCUE_DELAY); } }
diff --git a/user/test/com/google/gwt/core/CoreSuite.java b/user/test/com/google/gwt/core/CoreSuite.java index df1cedf..8e37156 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.SchedulerTest; import com.google.gwt.core.client.impl.AsyncFragmentLoaderTest; import com.google.gwt.core.client.impl.EmulatedStackTraceTest; import com.google.gwt.core.client.impl.SchedulerImplTest; @@ -36,15 +37,16 @@ GWTTestSuite suite = new GWTTestSuite("All core tests"); // $JUnit-BEGIN$ + suite.addTestSuite(AsyncFragmentLoaderTest.class); + suite.addTestSuite(EmulatedStackTraceTest.class); + suite.addTestSuite(GWTTest.class); suite.addTestSuite(HttpThrowableReporterTest.class); suite.addTestSuite(JavaScriptExceptionTest.class); suite.addTestSuite(JsArrayTest.class); - suite.addTestSuite(GWTTest.class); - suite.addTestSuite(StackTraceCreatorTest.class); - suite.addTestSuite(EmulatedStackTraceTest.class); - suite.addTestSuite(AsyncFragmentLoaderTest.class); - suite.addTestSuite(XhrLoadingStrategyTest.class); suite.addTestSuite(SchedulerImplTest.class); + suite.addTestSuite(SchedulerTest.class); + suite.addTestSuite(StackTraceCreatorTest.class); + suite.addTestSuite(XhrLoadingStrategyTest.class); // $JUnit-END$ return suite;
diff --git a/user/test/com/google/gwt/core/client/SchedulerTest.java b/user/test/com/google/gwt/core/client/SchedulerTest.java new file mode 100644 index 0000000..78ac9dd --- /dev/null +++ b/user/test/com/google/gwt/core/client/SchedulerTest.java
@@ -0,0 +1,66 @@ +/* + * 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.Scheduler.ScheduledCommand; +import com.google.gwt.junit.DoNotRunWith; +import com.google.gwt.junit.Platform; +import com.google.gwt.junit.client.GWTTestCase; + +/** + * This is a black-box test of the Scheduler API. + */ +@DoNotRunWith(Platform.HtmlUnit) +// TODO(amitmanjhi) This tickles a devmode HtmlUnit deadlock +public class SchedulerTest extends GWTTestCase { + + private static final int TEST_DELAY = 500000; + + @Override + public String getModuleName() { + return "com.google.gwt.core.Core"; + } + + /** + * Tests that an entry command can schedule a finally command where the whole + * thing is kicked off by a deferred command. + */ + public void testEndToEnd() { + final boolean[] ranEntry = {false}; + + final ScheduledCommand finallyCommand = new ScheduledCommand() { + public void execute() { + assertTrue(ranEntry[0]); + finishTest(); + } + }; + + Scheduler.get().scheduleEntry(new ScheduledCommand() { + public void execute() { + ranEntry[0] = true; + Scheduler.get().scheduleFinally(finallyCommand); + } + }); + + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + public void execute() { + assertTrue(ranEntry[0]); + } + }); + + delayTestFinish(TEST_DELAY); + } +}
diff --git a/user/test/com/google/gwt/core/client/impl/SchedulerImplTest.java b/user/test/com/google/gwt/core/client/impl/SchedulerImplTest.java index 46b019f..00fb773 100644 --- a/user/test/com/google/gwt/core/client/impl/SchedulerImplTest.java +++ b/user/test/com/google/gwt/core/client/impl/SchedulerImplTest.java
@@ -15,12 +15,16 @@ */ package com.google.gwt.core.client.impl; +import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.Scheduler.RepeatingCommand; import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.core.client.impl.SchedulerImpl.Task; +import com.google.gwt.junit.DoNotRunWith; +import com.google.gwt.junit.Platform; import com.google.gwt.junit.client.GWTTestCase; /** - * Tests that poke at the internal state of SchedulerImpl. + * This is a white-box test of the Scheduler API implementation. */ public class SchedulerImplTest extends GWTTestCase { @@ -59,6 +63,20 @@ } } + /** + * The EntryCommand and FinallyCommand queues should have the same behavior, + * so we use this interface to reuse the same test logic. + */ + interface QueueTester { + void flush(); + + JsArray<Task> queue(); + + void schedule(RepeatingCommand cmd); + + void schedule(ScheduledCommand cmd); + } + private static final int TEST_DELAY = 5000; @Override @@ -66,6 +84,8 @@ return "com.google.gwt.core.Core"; } + @DoNotRunWith(Platform.HtmlUnit) + // TODO(amitmanjhi) This tickles a devmode HtmlUnit deadlock public void testDeferredCommands() { final SchedulerImpl impl = new SchedulerImpl(); @@ -82,7 +102,7 @@ impl.scheduleDeferred(new ScheduledCommand() { public void execute() { assertTrue(values[0]); - assertEquals(0, impl.deferredCommands.length()); + assertNull(impl.deferredCommands); finishTest(); } }); @@ -90,25 +110,51 @@ delayTestFinish(TEST_DELAY); } - public void testFinallyCommands() { - SchedulerImpl impl = new SchedulerImpl(); + public void testEntryCommands() { + final SchedulerImpl impl = new SchedulerImpl(); - final boolean[] values = {false}; - impl.scheduleFinally(new ArraySetterCommand(values)); + testQueue(new QueueTester() { + public void flush() { + impl.flushEntryCommands(); + } - assertEquals(1, impl.finallyCommands.length()); + public JsArray<Task> queue() { + return impl.entryCommands; + } - ScheduledCommand nullCommand = new NullCommand(); - impl.scheduleFinally(nullCommand); - assertEquals(2, impl.finallyCommands.length()); - assertSame(nullCommand, impl.finallyCommands.get(1).getScheduled()); + public void schedule(RepeatingCommand cmd) { + impl.scheduleEntry(cmd); + } - impl.flushFinallyCommands(); - - assertTrue(values[0]); - assertEquals(0, impl.finallyCommands.length()); + public void schedule(ScheduledCommand cmd) { + impl.scheduleEntry(cmd); + } + }); } + public void testFinallyCommands() { + final SchedulerImpl impl = new SchedulerImpl(); + + testQueue(new QueueTester() { + public void flush() { + impl.flushFinallyCommands(); + } + + public JsArray<Task> queue() { + return impl.finallyCommands; + } + + public void schedule(RepeatingCommand cmd) { + impl.scheduleFinally(cmd); + } + + public void schedule(ScheduledCommand cmd) { + impl.scheduleFinally(cmd); + } + }); + } + + @DoNotRunWith(Platform.HtmlUnit) public void testFixedDelayCommands() { final SchedulerImpl impl = new SchedulerImpl(); final int[] values = {0, 4}; @@ -131,6 +177,7 @@ delayTestFinish(TEST_DELAY); } + @DoNotRunWith(Platform.HtmlUnit) public void testFixedPeriodCommands() { final SchedulerImpl impl = new SchedulerImpl(); final int[] values = {0, 4}; @@ -153,6 +200,7 @@ delayTestFinish(TEST_DELAY); } + @DoNotRunWith(Platform.HtmlUnit) public void testIncrementalCommands() { final SchedulerImpl impl = new SchedulerImpl(); @@ -166,13 +214,16 @@ impl.scheduleDeferred(new ScheduledCommand() { public void execute() { // After the incremental command has fired, it's moved to a new queue - assertEquals(0, impl.deferredCommands.length()); + assertNull(impl.deferredCommands); assertTrue(String.valueOf(values[0]), values[0] <= values[1]); if (values[0] == values[1]) { + // Haven't yet cleared the queue, still in flushPostEventPumpCommands + assertNotNull(impl.incrementalCommands); assertEquals(0, impl.incrementalCommands.length()); finishTest(); } else { + assertNotNull(impl.incrementalCommands); assertEquals(1, impl.incrementalCommands.length()); assertSame(counter, impl.incrementalCommands.get(0).getRepeating()); impl.scheduleDeferred(this); @@ -184,4 +235,41 @@ delayTestFinish(TEST_DELAY); } + + private void testQueue(final QueueTester impl) { + boolean[] oneShotValues = {false}; + final boolean[] chainedValues = {false}; + int[] counterValues = {0, 2}; + + impl.schedule(new ArraySetterCommand(oneShotValues)); + impl.schedule(new CountingCommand(counterValues)); + impl.schedule(new ScheduledCommand() { + public void execute() { + // Schedule another entry + impl.schedule(new ArraySetterCommand(chainedValues)); + } + }); + + assertEquals(3, impl.queue().length()); + + ScheduledCommand nullCommand = new NullCommand(); + impl.schedule(nullCommand); + assertEquals(4, impl.queue().length()); + assertSame(nullCommand, impl.queue().get(3).getScheduled()); + + impl.flush(); + + // Ensure the command-schedules-command case has been executed + assertTrue(chainedValues[0]); + + // Test that the RepeatingCommand is still scheduled + assertEquals(1, counterValues[0]); + assertEquals(1, impl.queue().length()); + impl.flush(); + + // Everything should be finished now + assertEquals(2, counterValues[0]); + assertTrue(oneShotValues[0]); + assertNull(impl.queue()); + } }