blob: a787730e629d02c00bbe2eb10866a276aec5daa8 [file] [log] [blame]
/*
* Copyright 2007 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.user.client;
import com.google.gwt.core.client.Duration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Class which executes {@link Command}s and {@link IncrementalCommand}s after
* all currently pending event handlers have completed. This class attempts to
* protect against slow script warnings by running commands in small time
* increments.
*
* <p>
* It is still possible that a poorly written command could cause a slow script
* warning which a user may choose to cancel. In that event, a
* {@link CommandCanceledException} or an
* {@link IncrementalCommandCanceledException} is reported through
* {@link GWT#reportUncaughtException} depending on the type of command which
* caused the warning. All other commands will continue to be executed.
* </p>
*
* TODO(mmendez): Can an SSW be detected without using a timer? Currently, if a
* {@link Command} or an {@link IncrementalCommand} calls either
* {@link Window#alert(String)} or the JavaScript <code>alert(String)</code>
* methods directly or indirectly then the cancellation timer can fire,
* resulting in a false SSW cancellation detection.
*/
class CommandExecutor {
/**
* A circular iterator used by this class. This iterator will wrap back to
* zero when it hits the end of the commands.
*/
private class CircularIterator implements Iterator<Object> {
/**
* Index of the element where this iterator should wrap back to the
* beginning of the collection.
*/
private int end;
/**
* Index of the last item returned by {@link #next()}.
*/
private int last = -1;
/**
* Index of the next command to execute.
*/
private int next = 0;
/**
* Returns <code>true</code> if there are more commands in the queue.
*
* @return <code>true</code> if there are more commands in the queue.
*/
public boolean hasNext() {
return next < end;
}
/**
* Returns the next command from the queue. When the end of the dispatch
* region is reached it will wrap back to the start.
*
* @return next command from the queue.
*/
public Object next() {
last = next;
Object command = commands.get(next++);
if (next >= end) {
next = 0;
}
return command;
}
/**
* Removes the command which was previously returned by {@link #next()}.
*
*/
public void remove() {
assert (last >= 0);
commands.remove(last);
--end;
if (last <= next) {
if (--next < 0) {
next = 0;
}
}
last = -1;
}
/**
* Returns the last element returned by {@link #next()}.
*
* @return last element returned by {@link #next()}
*/
private Object getLast() {
assert (last >= 0);
return commands.get(last);
}
private void setEnd(int end) {
assert (end >= next);
this.end = end;
}
private void setLast(int last) {
this.last = last;
}
private boolean wasRemoved() {
return last == -1;
}
}
/**
* Default amount of time to wait before assuming that a script cancellation
* has taken place. This should be a platform dependent value, ultimately we
* may need to acquire this value based on a rebind decision. For now, we
* chose the smallest value known to cause an SSW.
*/
private static final int DEFAULT_CANCELLATION_TIMEOUT_MILLIS = 10000;
/**
* Default amount of time to spend dispatching commands before we yield to the
* system.
*/
private static final int DEFAULT_TIME_SLICE_MILLIS = 100;
/**
* Returns true the end time has been reached or exceeded.
*
* @param currentTimeMillis current time in milliseconds
* @param startTimeMillis end time in milliseconds
* @return true if the end time has been reached
*/
private static boolean hasTimeSliceExpired(double currentTimeMillis,
double startTimeMillis) {
return currentTimeMillis - startTimeMillis >= DEFAULT_TIME_SLICE_MILLIS;
}
/**
* Timer used to recover from script cancellations arising from slow script
* warnings.
*/
private final Timer cancellationTimer = new Timer() {
@Override
public void run() {
if (!isExecuting()) {
/*
* If we are not executing, then the cancellation timer expired right
* about the time that the command dispatcher finished -- we are okay so
* we just exit.
*/
return;
}
doCommandCanceled();
}
};
/**
* Commands that need to be executed.
*/
private final List<Object> commands = new ArrayList<Object>();
/**
* Set to <code>true</code> when we are actively dispatching commands.
*/
private boolean executing = false;
/**
* Timer used to drive the dispatching of commands in the background.
*/
private final Timer executionTimer = new Timer() {
@Override
public void run() {
assert (!isExecuting());
setExecutionTimerPending(false);
doExecuteCommands(Duration.currentTimeMillis());
}
};
/**
* Set to <code>true</code> when we are waiting for a dispatch timer event
* to fire.
*/
private boolean executionTimerPending = false;
/**
* The single circular iterator instance that we use to iterate over the
* collection of commands.
*/
private final CircularIterator iterator = new CircularIterator();
/**
* Submits a {@link Command} for execution.
*
* @param command command to submit
*/
public void submit(Command command) {
commands.add(command);
maybeStartExecutionTimer();
}
/**
* Submits an {@link IncrementalCommand} for execution.
*
* @param command command to submit
*/
public void submit(IncrementalCommand command) {
commands.add(command);
maybeStartExecutionTimer();
}
/**
* Removes the command from the queue and throws either a
* {@link CommandCanceledException} or an
* {@link IncrementalCommandCanceledException} depending on type of the
* command.
*/
protected void doCommandCanceled() {
Object cmd = iterator.getLast();
iterator.remove();
assert (cmd != null);
if (cmd instanceof Command) {
throw new CommandCanceledException((Command) cmd);
} else if (cmd instanceof IncrementalCommand) {
throw new IncrementalCommandCanceledException((IncrementalCommand) cmd);
}
setExecuting(false);
maybeStartExecutionTimer();
}
/**
* This method will dispatch commands from the command queue. It will dispatch
* commands until one of the following conditions is <code>true</code>:
* <ul>
* <li>It consumed its dispatching time slice
* {@value #DEFAULT_TIME_SLICE_MILLIS}</li>
* <li>It encounters a <code>null</code> in the command queue</li>
* <li>All commands which were present at the start of the dispatching have
* been removed from the command queue</li>
* <li>The command that it was processing was canceled due to a false
* cancellation -- in this case we exit without updating any state</li>
* </ul>
*
* @param startTimeMillis the time when this method started
*/
protected void doExecuteCommands(double startTimeMillis) {
assert (!isExecutionTimerPending());
boolean wasCanceled = false;
try {
setExecuting(true);
iterator.setEnd(commands.size());
cancellationTimer.schedule(DEFAULT_CANCELLATION_TIMEOUT_MILLIS);
while (iterator.hasNext()) {
Object element = iterator.next();
boolean removeCommand = true;
try {
if (element == null) {
// null forces a yield or pause in execution
return;
}
if (element instanceof Command) {
Command command = (Command) element;
command.execute();
} else if (element instanceof IncrementalCommand) {
IncrementalCommand incrementalCommand = (IncrementalCommand) element;
removeCommand = !incrementalCommand.execute();
}
} finally {
wasCanceled = iterator.wasRemoved();
if (!wasCanceled) {
if (removeCommand) {
iterator.remove();
}
}
}
if (hasTimeSliceExpired(Duration.currentTimeMillis(), startTimeMillis)) {
// the time slice has expired
return;
}
}
} finally {
if (!wasCanceled) {
cancellationTimer.cancel();
setExecuting(false);
maybeStartExecutionTimer();
}
}
}
/**
* Starts the dispatch timer if there are commands to dispatch and we are not
* waiting for a dispatch timer and we are not actively dispatching.
*/
protected void maybeStartExecutionTimer() {
if (!commands.isEmpty() && !isExecutionTimerPending() && !isExecuting()) {
setExecutionTimerPending(true);
executionTimer.schedule(1);
}
}
/**
* This method is for testing only.
*/
List<Object> getPendingCommands() {
return commands;
}
/**
* This method is for testing only.
*/
void setExecuting(boolean executing) {
this.executing = executing;
}
/**
* This method is for testing only.
*/
void setLast(int last) {
iterator.setLast(last);
}
/**
* Returns <code>true</code> if this instance is currently dispatching
* commands.
*
* @return <code>true</code> if this instance is currently dispatching
* commands
*/
private boolean isExecuting() {
return executing;
}
/**
* Returns <code>true</code> if a the dispatch timer was scheduled but it
* still has not fired.
*
* @return <code>true</code> if a the dispatch timer was scheduled but it
* still has not fired
*/
private boolean isExecutionTimerPending() {
return executionTimerPending;
}
private void setExecutionTimerPending(boolean pending) {
executionTimerPending = pending;
}
}