Delayed Logic & Scheduler
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against GWT 2.13.1 (2026-06-19), gwt-maven-plugin 1.3.0 and the Jakarta Servlet 6 stack of the
modular-webapp archetype, checked on 2026-10-08. Every timing and every output block below was produced with JDK 21,
-sourceLevel 17 and Chrome 155 (headless) on the Bookshelf scenario of this section; the test outputs come from
HtmlUnit and from a plain JVM. It is client knowledge: Timer and Scheduler are browser APIs compiled to
JavaScript, so none of the code below can run on the server, and the shared module cannot use them (the build and
JVM-test parts at the end are the exception). A browser runs your code on one thread, and everything on this page is about
choosing when the next piece of your code gets that thread: later, repeatedly, after the current event, before the
browser paints, or in small slices so the page stays responsive.
Choosing the right tool
GWT gives you two families of tools. com.google.gwt.user.client.Timer is a thin wrapper over the browser’s
setTimeout and setInterval. com.google.gwt.core.client.Scheduler is a set of "low-level task scheduling
primitives" (its own description) that cover everything else: running a command after the current event, around the
GWT entry points, in time slices, and as fixed-delay or fixed-period loops.
| You want to | Use | Notes |
|---|---|---|
Run something once, N milliseconds from now, and be able to cancel it |
|
One object per pending job; |
Repeat something at a fixed interval and be able to stop it from outside |
|
Keeps firing even if |
Repeat until the command itself says stop, with a constant gap after each run |
|
Return |
Repeat until the command says stop, with a constant period |
|
Runs can overlap in time if the work is asynchronous |
Run once after the current event handler has finished |
|
Not a microtask: runs after native |
Run once, right before control returns to the browser |
|
Batching small changes into one heavy operation |
Run something every time the browser calls into GWT code |
|
Instrumentation |
Do a long computation or DOM update without freezing the page |
|
About 16 ms of work per slice, then the browser gets control back |
Animate the user interface |
|
Both |
The event loop in one minute
A browser executes JavaScript in tasks. The page loads, a click arrives, a timer fires, an XMLHttpRequest finishes: each
is a task, and while one task runs nothing else happens on the page, including painting and further input. When the task
returns, the browser runs the pending microtasks (native Promise callbacks), may paint, and then picks the next task.
Two consequences drive the whole page.
-
Code that does not return control to the browser freezes the page. The Developer Guide says a script that runs for more than about 10 seconds risks the browser’s unresponsive-script dialog.
-
"Later" always means "in a later task". Even
schedule(0)cannot run until the current task has returned.
GWT-compiled code wraps every call that comes from outside (event handlers, timer callbacks, XHR callbacks, the module
load) in an internal entry function, the $entry that you also meet in JSNI & Overlay Types. That wrapper is where the
Scheduler hooks in and where uncaught exceptions are routed to GWT.UncaughtExceptionHandler.
Timer
Timer is an abstract class: subclass it, override run(), and call schedule or scheduleRepeating. The Javadoc
says it serves the same purpose as java.util.Timer, simplified for a single-threaded environment.
| Member | Behaviour (checked in the 2.13.1 source and by running it) |
|---|---|
|
Called when the timer fires. It runs inside the GWT entry wrapper, so an uncaught exception reaches the uncaught-exception handler |
|
One-shot. Throws |
|
Repeats until cancelled. Throws |
|
Stops the timer. A no-op if the timer is not running, so calling it twice is fine |
|
|
Schedule, reschedule, repeat and cancel
Timer t = new Timer() {
@Override
public void run() {
log("t.run() fired, isRunning=" + isRunning());
}
};
t.schedule(100);
t.schedule(200); // cancels the first schedule: only one run happens
try { t.schedule(-1); } catch (IllegalArgumentException e) { log(e.getMessage()); }
try { t.scheduleRepeating(0); } catch (IllegalArgumentException e) { log(e.getMessage()); }
Timer repeating = new Timer() {
int n;
@Override
public void run() {
n++;
log("repeating run #" + n);
if (n == 3) {
cancel(); // a timer may cancel itself from run()
}
}
};
repeating.scheduleRepeating(100);
Output (milliseconds since the module loaded; the rescheduled one-shot fires once, 200 ms after the second call, and the two failed calls leave the pending run untouched):
1 ms new Timer: isRunning=false
1 ms after schedule(100): isRunning=true
1 ms schedule(-1) -> IllegalArgumentException: must be non-negative
1 ms scheduleRepeating(0) -> IllegalArgumentException: must be positive
1 ms scheduleRepeating(-5) -> IllegalArgumentException: must be positive
1 ms after the failed calls: isRunning=true (the earlier schedule(200) is still pending)
1 ms cancelled twice without error; isRunning=false
201 ms t.run() fired, isRunning=false
506 ms repeating run #1
604 ms repeating run #2
704 ms repeating run #3
704 ms cancel() inside run(): isRunning=false
A timer that was scheduled with schedule(700) and then with scheduleRepeating(900) fired only the repeating
variant, first after 900 ms, then every 900 ms: the second call replaced the first and changed the mode.
A timeout around a remote call
The Developer Guide’s classic use of Timer is a timeout around a long operation. Its rules of thumb still hold: keep
the timer in a field, never start a second one while one is pending, cancel it when the work succeeds, and clear the
field when the work ends or the timer expires. Here is the pattern on the Bookshelf catalogue service (the
asynchronous interface from GWT-RPC), with a 200 ms limit:
private Timer timeoutTimer;
private boolean aborted;
private void load(CatalogServiceAsync service, int limitMs) {
if (timeoutTimer != null) {
log("already running");
return;
}
aborted = false;
timeoutTimer = new Timer() {
@Override
public void run() {
timeoutTimer = null;
aborted = true;
log("timeout expired -> abort flag set");
}
};
timeoutTimer.schedule(limitMs);
service.getBooks(0, 25, new AsyncCallback<BookPage>() {
@Override
public void onSuccess(BookPage page) {
cancelTimeout();
if (aborted) {
log("late answer discarded");
return;
}
show(page);
}
@Override
public void onFailure(Throwable caught) {
cancelTimeout();
}
});
}
private void cancelTimeout() {
if (timeoutTimer != null) {
timeoutTimer.cancel();
timeoutTimer = null;
}
}
Against a stub service whose latency was 500 ms, then 100 ms (a second load call while the first is pending is
refused):
0 ms RPC timeout pattern with a 200 ms limit; service latency 500 ms
0 ms request sent (in flight: 1)
0 ms already running
201 ms timeout expired -> abort flag set
501 ms late answer discarded
902 ms --- second run: latency 100 ms (answers before the timeout)
902 ms request sent (in flight: 1)
1002 ms answer in time: total=25
The timer does not cancel the request: the browser still receives the answer at 501 ms, which is why the callback checks the flag. A timeout like this only stops the user interface from waiting. If the server work must also stop, the server needs its own deadline.
Timers belong to the widget that starts them
A repeating Timer keeps a reference to your object and fires until somebody cancels it, including after the widget
that created it has left the page. The reliable place to start and stop it is the widget lifecycle: start in onLoad()
(called when the widget is attached) and cancel in onUnload() (called when it is detached). In an Activity, the
equivalent is start(...) and onStop()/onCancel() (see MVP, Activities & Places).
class LastUpdatedLabel extends Label {
private int ticks;
private final Timer ticker = new Timer() {
@Override
public void run() {
setText("tick " + (++ticks));
}
};
@Override
protected void onLoad() {
super.onLoad();
ticker.scheduleRepeating(100);
}
@Override
protected void onUnload() {
ticker.cancel();
super.onUnload();
}
}
Added to the RootPanel and removed after 450 ms the label showed tick 4, the timer reported isRunning=true just
before onUnload() cancelled it, and 500 ms later the text was still tick 4.
How exact are timers?
They are not. A timer fires no earlier than requested and, if the page is busy, later. The MDN reference for
setTimeout lists the reasons: nested timers get a minimum delay of about 4 ms once they have been chained several
times, and browsers throttle timers in inactive tabs (Firefox desktop to one second, Chrome with several throttling
levels). The throttling showed up while preparing this page: with the page in a hidden browser pane,
scheduleDeferred and Timer.schedule(300) ran about 720 ms after they were requested. The same code in a visible
page ran on time. Do not use a timer to measure time; compare Duration.currentTimeMillis() (or
System.currentTimeMillis()) values instead, as the Developer Guide’s elapsed-time example does.
Animations are not a job for Timer
Both Javadocs carry the same note: if you are using a timer to schedule a user interface animation, use
com.google.gwt.animation.client.AnimationScheduler instead, because the browser can then optimise the animation
(it wraps requestAnimationFrame). Use Timer and Scheduler for logic, not for frames.
Source: Javadoc — AnimationScheduler
Scheduler
Scheduler.get() returns the one default implementation. Scheduler has two command interfaces and eight scheduling
methods (six names, two of them overloaded). Any exception thrown by a command is passed to the installed
GWT.UncaughtExceptionHandler, if there is one.
| Interface | Method | Purpose |
|---|---|---|
|
|
A command that runs once. |
|
|
Return |
| Method | Runs | Notes from the Javadoc and the source |
|---|---|---|
|
Once, "after the browser event loop returns" |
Queued, then flushed by an internal timer of 1 ms |
|
Before GWT-generated code is invoked by the browser’s event loop |
The repeating form is called once per entry until it returns |
|
Before GWT-generated code returns control to the event loop |
The repeating form is called once per exit until it returns |
|
After |
Constant delay between the end of one run and the start of the next |
|
Every |
Constant period, regardless of how long the previous run took |
|
Many times in rapid succession, then yields |
For long-running computation or DOM manipulation; the browser processes its queue between bursts |
Source: Javadoc — Scheduler, GWT 2.13.1 source — Scheduler.java, Javadoc — Scheduler.RepeatingCommand
Nothing in this API returns a handle. To cancel a Scheduler loop from outside, keep a flag that the command reads and
return false when it is set; to cancel by handle, use Timer.
scheduleDeferred: after the current event
The Developer Guide’s use case is initialisation order: the code that must wait for the rest of the current event to finish. A deferred command runs only after the handler that scheduled it has returned, so objects created later in the same method already exist:
private TextBox dataEntry;
private void build() {
// Set the focus on the widget after the setup below has completed.
Scheduler.get().scheduleDeferred(() -> dataEntry.setFocus(true));
dataEntry = new TextBox();
RootPanel.get().add(dataEntry);
}
The book uses the same idea after a window resize: it defers the repositioning of a slider until the browser has finished resizing (see Outdated material in older books and posts).
The measured order
The documentation says in words what runs "before" and "after". The following program schedules one of each kind
twice: from onModuleLoad and from a click handler that a timer triggers 300 ms later. A native
Promise.resolve().then(...) wrapped in $entry is included for comparison.
private void schedule(String p) {
Scheduler.get().scheduleDeferred(() -> log(p + "scheduleDeferred"));
Scheduler.get().scheduleEntry(() -> log(p + "scheduleEntry"));
Scheduler.get().scheduleFinally(() -> {
log(p + "scheduleFinally");
Scheduler.get().scheduleFinally(() -> log(p + " nested scheduleFinally"));
});
new Timer() {
@Override
public void run() { log(p + "Timer.schedule(0)"); }
}.schedule(0);
Scheduler.get().scheduleFixedDelay(() -> { log(p + "scheduleFixedDelay(0)"); return false; }, 0);
Scheduler.get().scheduleFixedPeriod(() -> { log(p + "scheduleFixedPeriod(0)"); return false; }, 0);
Scheduler.get().scheduleIncremental(() -> { log(p + "scheduleIncremental"); return false; });
microtask(() -> log(p + "Promise microtask (native, for comparison)"));
}
The same order came out of three separate runs, from the click handler and from onModuleLoad:
0 ms onModuleLoad: start
0 ms onModuleLoad: end
0 ms [load] scheduleFinally
0 ms [load] nested scheduleFinally (scheduled from a finally command)
0 ms [load] scheduleEntry
0 ms [load] Promise microtask (native, for comparison)
0 ms [load] Timer.schedule(0)
0 ms [load] scheduleFixedDelay(0)
0 ms [load] scheduleFixedPeriod(0)
2 ms [load] scheduleDeferred
2 ms [load] scheduleIncremental
303 ms click handler: start
303 ms click handler: end
303 ms [click] scheduleFinally
303 ms [click] nested scheduleFinally (scheduled from a finally command)
303 ms [click] scheduleEntry
303 ms [click] Promise microtask (native, for comparison)
306 ms [click] Timer.schedule(0)
306 ms [click] scheduleFixedDelay(0)
306 ms [click] scheduleFixedPeriod(0)
306 ms [click] scheduleDeferred
306 ms [click] scheduleIncremental
Reading the log:
-
scheduleFinallyran as soon as the handler returned, and the finally command it scheduled itself ran before control returned to the browser. -
scheduleEntrydid not run at the end of the handler. It ran the next time the browser entered GWT code, which here was thePromisecallback (wrapped in$entry). An entry command is a hook for "something is about to happen", not a way to run code soon. -
The native
Promisecallback ran before any timer-based command. A deferred command is not a microtask: it is flushed by an internal 1 ms timer, so it runs afterTimer.schedule(0),scheduleFixedDelay(0)andscheduleFixedPeriod(0)that were requested in the same task. -
scheduleIncrementalshares the deferred queue, so its first run came right after the deferred command.
Do not build logic on the relative order of timer-based commands scheduled in the same millisecond. Rely only on what the Javadoc promises: finally and entry commands bracket the GWT code, deferred commands wait for the event loop.
scheduleFixedDelay and scheduleFixedPeriod
Both take a RepeatingCommand and a number of milliseconds. They differ in what the time is measured from. A command
that takes 30 ms with a 100 ms parameter, five runs each:
Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
int n;
@Override
public boolean execute() {
log("fixedDelay(100) start #" + n);
busy(30); // a 30 ms busy loop stands in for real work
return ++n < 5;
}
}, 100);
// the same body with Scheduler.get().scheduleFixedPeriod(..., 100)
100 ms fixedDelay(100) start #0
233 ms fixedDelay(100) start #1 (+133 since previous start)
365 ms fixedDelay(100) start #2 (+132 since previous start)
497 ms fixedDelay(100) start #3 (+132 since previous start)
629 ms fixedDelay(100) start #4 (+132 since previous start)
1104 ms fixedPeriod(100) start #0
1206 ms fixedPeriod(100) start #1 (+102 since previous start)
1305 ms fixedPeriod(100) start #2 (+99 since previous start)
1405 ms fixedPeriod(100) start #3 (+100 since previous start)
1505 ms fixedPeriod(100) start #4 (+100 since previous start)
Fixed delay measures from the end of the previous run (30 ms of work plus the 100 ms delay, about 132 ms between
starts), which is what you want when work must never pile up. Fixed period measures from the start (100 ms between
starts), which suits a heartbeat but, as the polling section shows, lets asynchronous work overlap. The first run of
scheduleFixedDelay also waits one full delay.
scheduleIncremental: work in slices
scheduleIncremental accepts a RepeatingCommand that does a small amount of work per execute() and returns true
while more remains. The 2.13.1 implementation keeps incremental commands in one queue and runs them round-robin for
up to 16 ms (the source says this "allows control to be returned to the browser 60 times a second"), then yields with
a 1 ms timer so the browser can process input and paint. Another timer of 50 ms acts as a safety net if the flusher is
killed, for example by a slow-script warning.
A 600 x 1 ms computation, first as one synchronous loop and then as 600 incremental calls, while a repeating Timer
of 10 ms measured how long the page was unresponsive:
202 ms synchronous loop of 600 x 1 ms starts
802 ms synchronous loop ends
903 ms heartbeat (10 ms) max gap during the loop: 601 ms
903 ms scheduleIncremental: 600 x 1 ms units, one unit per execute()
1676 ms scheduleIncremental finished: 600 execute() calls
1676 ms calls per slice for the first slices: [65 calls, 16 calls, 16 calls, 16 calls, 16 calls, 16 calls]
1676 ms heartbeat (10 ms) max gap during the run: 21 ms
The synchronous loop made the timer miss 600 ms; the incremental version never made it wait more than 21 ms, at the
price of a longer total time (773 ms against 600 ms). The first slice held 65 calls in this run, and the later slices
settled at 16 calls of 1 ms each. A call must therefore be small: a single execute() that itself runs for a second
cannot be sliced by the scheduler.
What happens when a command throws
The Javadoc says exceptions go to the uncaught-exception handler. What each API does afterwards differs, and it
matters for polling loops. The program installed GWT.setUncaughtExceptionHandler(...) and made each kind of command
throw IllegalStateException on its second run:
| Work | Observed after the second run threw | Consequence |
|---|---|---|
|
The handler was called; the next deferred command still ran |
One failure does not poison the queue |
|
The handler was called |
One-shot, nothing to continue |
|
The handler was called and the timer kept firing (runs 3 and 4 followed) |
A repeating timer never stops by itself |
|
The handler was called and there was no third run |
The loop ends silently apart from the handler |
|
The handler was called and there was no third run |
Same |
|
The handler was called; the command stayed queued and ran again about 50 ms later (runs 3 to 5) |
A command that always throws is retried over and over |
Catch exceptions inside the command and return the value you mean, rather than relying on these side effects. In a polling loop, a failure should be a deliberate decision to stop, retry or back off.
Breaking up long-running work
The Developer Guide motivates scheduleIncremental with the browser’s unresponsive-script dialog. The practical rule is
simpler: any task that keeps the thread for a noticeable fraction of a second makes the page feel stuck, and a few
seconds can bring up the browser’s warning. Typical Bookshelf culprits are building a search index for the whole catalogue, rendering a
very long list into the DOM, or parsing a large JSON document.
The recipe has four parts: keep the state in fields, do a bounded chunk per execute(), report progress, and let the
caller cancel.
class BatchedIndexer {
interface Listener {
void onProgress(int done, int total);
void onDone(int distinctTitles);
}
private final List<Book> books;
private final Listener listener;
private final int chunk;
private final Set<String> titles = new HashSet<>();
private int next;
private boolean cancelled;
BatchedIndexer(List<Book> books, int chunk, Listener listener) {
this.books = books;
this.chunk = chunk;
this.listener = listener;
}
void start() {
Scheduler.get().scheduleIncremental(() -> {
if (cancelled) {
return false;
}
int end = Math.min(next + chunk, books.size());
for (; next < end; next++) {
titles.add(slug(books.get(next).getTitle() + " " + books.get(next).getAuthor()));
}
listener.onProgress(next, books.size());
if (next < books.size()) {
return true; // more to do: the scheduler calls us again
}
listener.onDone(titles.size());
return false; // finished: leave the queue
});
}
void cancel() {
cancelled = true;
}
static String slug(String s) {
return s.toLowerCase().replaceAll("[^a-z0-9]+", "-").replaceAll("-+$", "");
}
}
Run over 200 000 synthetic Book objects with chunks of 500, next to a single loop over the same data (a 10 ms
heartbeat Timer measures responsiveness; the regular expressions make each item costly enough to matter):
270 ms one synchronous loop over 200000 books: 58 ms, distinct=15000
371 ms heartbeat max gap: 59 ms
391 ms progress 50000/200000
404 ms progress 100000/200000
418 ms progress 150000/200000
432 ms progress 200000/200000
432 ms BatchedIndexer(chunk=500) done: 400 chunks, 61 ms, distinct=15000
432 ms heartbeat max gap: 18 ms
Total time is practically the same; what changes is that the page was blocked for 59 ms in one case and at most 18 ms in the other. Notes on the design:
-
Pick the chunk so that one call is a fraction of a millisecond to a few milliseconds. The scheduler itself decides how many calls fit into a slice, so a smaller chunk only costs a little more overhead.
-
Do not touch the DOM in every chunk if you can avoid it: collect, then update once (or in pages) so the browser lays out less often.
scheduleFinallyis the tool for "collect during this event, apply once at the end". -
If the work is truly heavy and independent of the DOM, a Web Worker is the next step, reached through JsInterop (see JsInterop and DOM & Elemental2). Slicing keeps one thread responsive; it does not add CPU.
Polling the server
Polling is a Timer or Scheduler loop that calls the server. It looks trivial, and it is where the differences
between the pacing modes show. The two requirements are that requests must not overlap and that failures must back off.
First, the naive version: scheduleFixedPeriod(..., 100) sending a request each time, against a service that takes
250 ms to answer:
0 ms A) fixed period 100 ms, service latency 250 ms (requests overlap)
101 ms request sent (in flight: 1)
201 ms request sent (in flight: 2)
301 ms request sent (in flight: 3)
401 ms request sent (in flight: 3)
502 ms request sent (in flight: 3)
602 ms request sent (in flight: 3)
1501 ms A) max requests in flight: 3
Requests pile up as soon as the server is slower than the period, which is exactly when it can least afford them. The robust shape schedules the next poll from the callback of the previous one:
class CatalogPoller {
interface Listener {
void onTotal(int total);
void onError(Throwable t);
}
private final CatalogServiceAsync service;
private final Listener listener;
private final int intervalMs;
private final Timer timer = new Timer() {
@Override
public void run() {
poll();
}
};
private boolean running;
CatalogPoller(CatalogServiceAsync service, int intervalMs, Listener listener) {
this.service = service;
this.intervalMs = intervalMs;
this.listener = listener;
}
void start() {
if (!running) {
running = true;
poll();
}
}
void stop() {
running = false;
timer.cancel();
}
private void poll() {
service.getBooks(0, 1, new AsyncCallback<BookPage>() {
@Override
public void onSuccess(BookPage page) {
if (!running) {
return; // stopped while the request was in flight
}
listener.onTotal(page.getTotal());
timer.schedule(intervalMs);
}
@Override
public void onFailure(Throwable t) {
if (!running) {
return;
}
listener.onError(t);
timer.schedule(intervalMs * 4); // back off after a failure
}
});
}
}
Same 250 ms service, 100 ms interval:
1501 ms B) CatalogPoller: next poll scheduled from the callback, 100 ms after the answer, latency 250 ms
1501 ms request sent (in flight: 1)
1753 ms total=25
1855 ms request sent (in flight: 1)
2105 ms total=25
2206 ms request sent (in flight: 1)
2458 ms total=25
2559 ms request sent (in flight: 1)
2703 ms B) stopped; max requests in flight: 1
Things the poller gets right, and things to add in a real application:
-
It only schedules the next poll in the callback, so the effective cycle is "latency + interval" and the server load adapts to its own speed.
-
stop()also covers the answer that arrives after stopping (runningis checked), the equivalent of the late answer in the timeout example. -
Tie
start()andstop()to a lifecycle:onLoad/onUnloadof a widget, orstart/onStopof an Activity, as in the widget example above. A poller nobody stops keeps loading the server for a page nobody is looking at. -
A real service call needs an XSRF token and is subject to the same-origin rules, so use the services of GWT-RPC and XSRF, RPC & Server Security unchanged; polling does not exempt a call from them. Authorisation stays on the server.
-
Choose the interval from the data’s freshness requirement, add some random jitter if thousands of browsers start at once, and stop polling when the tab is hidden if staleness is acceptable (the standard Page Visibility API, reached through DOM & Elemental2).
-
When updates must arrive within seconds, polling is wasteful. Server push through WebSockets or Server-Sent Events (reached with Elemental2, see DOM & Elemental2; REST & JSON Libraries lists the alternatives) is the next step, and RequestBuilder, JSON & XML covers the plain HTTP route.
Testing delayed code
Delayed code is hard to test because the test method returns before the code runs. Two techniques cover it. The general test setup is in Testing.
In a GWT test (HtmlUnit). GWTTestCase runs the test method, and the test ends when the method returns, unless it
calls delayTestFinish(timeoutMillis). The test then lasts until finishTest() is called or the timeout expires.
public class DelayedLogicTest extends GWTTestCase {
@Override
public String getModuleName() {
return "com.example.bookshelf.TimingTest";
}
public void testDeferredRunsAfterTheTestMethodReturns() {
final List<String> order = new ArrayList<>();
delayTestFinish(2000);
Scheduler.get().scheduleDeferred(() -> {
order.add("deferred");
assertEquals("[sync, deferred]", order.toString());
finishTest();
});
order.add("sync");
}
public void testTimerFires() {
delayTestFinish(2000);
new Timer() {
@Override
public void run() {
finishTest();
}
}.schedule(100);
}
public void testIncrementalRunsUntilFalse() {
final int[] n = {0};
delayTestFinish(2000);
Scheduler.get().scheduleIncremental(() -> {
if (++n[0] < 50) {
return true;
}
assertEquals(50, n[0]);
finishTest();
return false;
});
}
}
All three passed. A fourth test that called delayTestFinish(300) and never reached finishTest() failed with:
com.google.gwt.junit.client.TimeoutException: A timeout expired after 300ms elapsed.
In a plain JVM test. GWT test cases are slow, so the logic around the scheduler is better tested on the JVM. That
requires not calling Scheduler.get() from the class: inject the Scheduler through the constructor and use
com.google.gwt.core.client.testing.StubScheduler in the test. It records commands and runs them when you ask.
public class CatalogRefresher {
private final Scheduler scheduler;
private final Runnable refresh;
private int remaining;
public CatalogRefresher(Scheduler scheduler, Runnable refresh) {
this.scheduler = scheduler;
this.refresh = refresh;
}
public void start(int times, int delayMs) {
remaining = times;
scheduler.scheduleFixedDelay(() -> {
refresh.run();
return --remaining > 0;
}, delayMs);
}
}
@Test
public void refreshesThreeTimesThenStops() {
StubScheduler scheduler = new StubScheduler();
int[] calls = {0};
new CatalogRefresher(scheduler, () -> calls[0]++).start(3, 30_000);
assertEquals(1, scheduler.getRepeatingCommands().size());
assertTrue(scheduler.executeRepeatingCommands()); // refresh 1, still scheduled
assertTrue(scheduler.executeRepeatingCommands()); // refresh 2
assertFalse(scheduler.executeRepeatingCommands()); // refresh 3: the command returned false
assertEquals(3, calls[0]);
assertEquals(0, scheduler.getRepeatingCommands().size());
}
Source: Javadoc — StubScheduler
The test ran in 10 ms on the JVM. StubScheduler also has executeCommands() and executeScheduledCommands() for
the one-shot commands. Timer has no such stub: it is native code, so keep Timer in a thin class (or wrap it behind
an interface) and put the logic in classes you can test with StubScheduler. Calling Scheduler.get() directly on the
JVM fails in the class initialiser:
java.lang.ExceptionInInitializerError caused by java.lang.UnsupportedOperationException: ERROR: GWT.create() is only usable in client code! It cannot be called, for example, from server code. If you are running a unit test, check that your test case extends GWTTestCase and that GWT.create() is not called from within an initializer or constructor.
DeferredCommand and IncrementalCommand (deprecated)
Before Scheduler existed, GWT offered two static helpers, and both are still in gwt-user 2.13.1, marked
@Deprecated. Code from older tutorials and from books compiles, and you will meet it in legacy projects.
| Deprecated | Replacement | Why (from the Javadoc) |
|---|---|---|
|
|
"the static nature of this API prevents effective mocking for JRE-only tests" |
|
|
Already deprecated "as of release 1.4" in favour of |
|
|
Same |
|
None |
"with no replacement because the presence of this method causes arbitrary scheduling decisions" |
|
|
|
The old class is not just a different spelling of the new one. This program mixes both in one handler:
1 ms start
1 ms end
2 ms Scheduler.scheduleDeferred #1
2 ms Scheduler.scheduleDeferred #2
2 ms IncrementalCommand passed to Scheduler.scheduleIncremental #1
2 ms IncrementalCommand passed to Scheduler.scheduleIncremental #2
2 ms IncrementalCommand passed to Scheduler.scheduleIncremental #3
2 ms DeferredCommand.addCommand(Command)
6 ms DeferredCommand after addPause()
6 ms DeferredCommand.addCommand(IncrementalCommand) #1
6 ms DeferredCommand.addCommand(IncrementalCommand) #2
6 ms DeferredCommand.addCommand(IncrementalCommand) #3
DeferredCommand keeps its own queue, so its commands did not interleave with the Scheduler ones even though
DeferredCommand.addCommand(Command) was called between two scheduleDeferred calls; and addPause() pushed the
following commands to a later turn of the event loop. Migrating therefore means moving all the related deferred code
to Scheduler together; mixing the two APIs gives no ordering guarantee. Because Command extends ScheduledCommand,
the migration of a call is mechanical:
// before
DeferredCommand.addCommand(new Command() {
@Override
public void execute() {
redimensionalise();
}
});
// after
Scheduler.get().scheduleDeferred(this::redimensionalise);
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
|
A negative delay, often a computed value ( |
Clamp with |
|
A period of 0 or less |
Use at least 1 ms; for "as fast as possible" use |
A second |
One |
Use one |
A timer keeps running after its widget or Activity is gone |
Nobody called |
Start in |
A repeating |
|
Catch inside |
An incremental command throws forever |
After an exception it stays in the queue and is retried about 50 ms later |
Catch exceptions inside the command and return |
Polling requests pile up, the server gets slower |
|
Schedule the next poll from the callback, as in |
The page freezes in one event handler |
The handler runs a long loop on the only thread |
Slice it with |
|
A single |
Smaller chunks per call |
|
A deferred command is a timer-based task, not a microtask |
Do not mix the two when order matters; use one mechanism |
Timers run late in a background tab |
Browsers throttle inactive tabs (measured here: about 720 ms of extra delay in a hidden pane) |
Do not rely on timer precision; pause polling in hidden tabs |
|
|
Concatenate strings, or use |
|
The class under test calls |
Inject the |
|
|
Call |
A test passes although its asynchronous assertion never ran |
No |
Always pair asynchronous work with |
Old code that mixes |
Two separate queues |
Migrate related code together to |
Outdated material in older books and posts
The two chapters of GWT in Action (written for GWT 1.3 and 1.4) that touch this topic predate Scheduler: the file
Scheduler.java in the 2.13.1 sources carries a 2009 copyright header, two years after the book. The book therefore
uses Timer (for example to switch a button’s style back after a click, and for the update timer of its Server Status
component) and DeferredCommand (to resize a slider after the browser has finished a window resize). Read them for the
reasoning, and translate the code:
-
DeferredCommand.addCommand(new Command() {...})becomesScheduler.get().scheduleDeferred(...), andIncrementalCommandbecomesRepeatingCommand/scheduleIncremental. -
ClickListener,WindowResizeListenerandComposite-based "updateable" components are the pre-handler event model; use handlers and a widget withonLoad/onUnloadfor the timer, as shown above. -
The book’s polling chapter also describes emulating server push by blocking server threads. With today’s servlet containers that holds a thread per client; prefer WebSockets or Server-Sent Events, and treat polling as the fallback.
-
The Developer Guide page itself is partly stale: its table of contents still announces
DeferredCommandandIncrementalCommand, while the examples further down useScheduler, and theIncrementalCommandexample in the page is only an outline (it uses aresultvariable that it never declares). The Javadoc and the source are the reference.
Where a statement on this page says "measured", it is from the run described in the first paragraph, not from the Developer Guide.
What changed recently
The release notes are short on these classes. They mention a performance fix in RepeatingCommand (2.5.1 RC1, issue
7307), a Timer.cancel() fix for old Internet Explorer versions (2.6.0 RC1, issue 8101; the cancelCounter workaround is
still in the 2.13.1 Timer source), and the webApiUsage property (2.5.1 RC1), whose stable setting makes the
animation library use a timer instead of requestAnimationFrame. Nothing newer concerns Timer or Scheduler.
DeferredCommand and IncrementalCommand are deprecated but not removed in 2.13.1. The exact numbers on this page
(the 16 ms slice, the 1 ms and 50 ms internal timers) are implementation details of SchedulerImpl in 2.13.1, not
documented guarantees.
Source: GWT — Release notes