| /* |
| * 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; |
| } |
| } |