001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.user.client;
017:
018: import com.google.gwt.core.client.GWT;
019: import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
020:
021: import java.util.ArrayList;
022: import java.util.Iterator;
023: import java.util.List;
024:
025: /**
026: * Class which executes {@link Command}s and {@link IncrementalCommand}s after
027: * all currently pending event handlers have completed. This class attempts to
028: * protect against slow script warnings by running commands in small time
029: * increments.
030: *
031: * <p>
032: * It is still possible that a poorly written command could cause a slow script
033: * warning which a user may choose to cancel. In that event, a
034: * {@link CommandCanceledException} or an
035: * {@link IncrementalCommandCanceledException} is reported through the current
036: * {@link UncaughtExceptionHandler} depending on the type of command which
037: * caused the warning. All other commands will continue to be executed.
038: * </p>
039: *
040: * TODO(mmendez): Can an SSW be detected without using a timer? Currently, if a
041: * {@link Command} or an {@link IncrementalCommand} calls either
042: * {@link Window#alert(String)} or the JavaScript <code>alert(String)</code>
043: * methods directly or indirectly then the {@link #cancellationTimer} can fire,
044: * resulting in a false SSW cancellation detection.
045: */
046: class CommandExecutor {
047:
048: /**
049: * A circular iterator used by this class. This iterator will wrap back to
050: * zero when it hits the end of the commands.
051: */
052: private class CircularIterator implements Iterator<Object> {
053: /**
054: * Index of the element where this iterator should wrap back to the
055: * beginning of the collection.
056: */
057: private int end;
058:
059: /**
060: * Index of the last item returned by {@link #next()}.
061: */
062: private int last = -1;
063:
064: /**
065: * Index of the next command to execute.
066: */
067: private int next = 0;
068:
069: /**
070: * Returns <code>true</code> if there are more commands in the queue.
071: *
072: * @return <code>true</code> if there are more commands in the queue.
073: */
074: public boolean hasNext() {
075: return next < end;
076: }
077:
078: /**
079: * Returns the next command from the queue. When the end of the dispatch
080: * region is reached it will wrap back to the start.
081: *
082: * @return next command from the queue.
083: */
084: public Object next() {
085: last = next;
086: Object command = commands.get(next++);
087: if (next >= end) {
088: next = 0;
089: }
090:
091: return command;
092: }
093:
094: /**
095: * Removes the command which was previously returned by {@link #next()}.
096: *
097: * @return the command which was previously returned by {@link #next()}.
098: */
099: public void remove() {
100: assert (last >= 0);
101:
102: commands.remove(last);
103: --end;
104:
105: if (last <= next) {
106: if (--next < 0) {
107: next = 0;
108: }
109: }
110:
111: last = -1;
112: }
113:
114: /**
115: * Returns the last element returned by {@link #next()}.
116: *
117: * @return last element returned by {@link #next()}
118: */
119: private Object getLast() {
120: assert (last >= 0);
121: return commands.get(last);
122: }
123:
124: private void setEnd(int end) {
125: assert (end >= next);
126:
127: this .end = end;
128: }
129:
130: private void setLast(int last) {
131: this .last = last;
132: }
133:
134: private boolean wasRemoved() {
135: return last == -1;
136: }
137: }
138:
139: /**
140: * Default amount of time to wait before assuming that a script cancellation
141: * has taken place. This should be a platform dependent value, ultimately we
142: * may need to acquire this value based on a rebind decision. For now, we
143: * chose the smallest value known to cause an SSW.
144: */
145: private static final long DEFAULT_CANCELLATION_TIMEOUT_MILLIS = 10000;
146:
147: /**
148: * Default amount of time to spend dispatching commands before we yield to the
149: * system.
150: */
151: private static final long DEFAULT_TIME_SLICE_MILLIS = 100;
152:
153: /**
154: * Returns true the end time has been reached or exceeded.
155: *
156: * @param currentTimeMillis current time in milliseconds
157: * @param startTimeMillis end time in milliseconds
158: * @return true if the end time has been reached
159: */
160: private static boolean hasTimeSliceExpired(long currentTimeMillis,
161: long startTimeMillis) {
162: return Math.abs(currentTimeMillis - startTimeMillis) >= DEFAULT_TIME_SLICE_MILLIS;
163: }
164:
165: /**
166: * Timer used to recover from script cancellations arising from slow script
167: * warnings.
168: */
169: private final Timer cancellationTimer = new Timer() {
170: @Override
171: public void run() {
172: if (!isExecuting()) {
173: /*
174: * If we are not executing, then the cancellation timer expired right
175: * about the time that the command dispatcher finished -- we are okay so
176: * we just exit.
177: */
178: return;
179: }
180:
181: doCommandCanceled();
182: }
183: };
184:
185: /**
186: * Commands that need to be executed.
187: */
188: private final List<Object> commands = new ArrayList<Object>();
189:
190: /**
191: * Set to <code>true</code> when we are actively dispatching commands.
192: */
193: private boolean executing = false;
194:
195: /**
196: * Timer used to drive the dispatching of commands in the background.
197: */
198: private final Timer executionTimer = new Timer() {
199: @Override
200: public void run() {
201: assert (!isExecuting());
202:
203: setExecutionTimerPending(false);
204:
205: doExecuteCommands(System.currentTimeMillis());
206: }
207: };
208:
209: /**
210: * Set to <code>true</code> when we are waiting for a dispatch timer event
211: * to fire.
212: */
213: private boolean executionTimerPending = false;
214:
215: /**
216: * The single circular iterator instance that we use to iterate over the
217: * collection of commands.
218: */
219: private final CircularIterator iterator = new CircularIterator();
220:
221: /**
222: * Submits a {@link Command} for execution.
223: *
224: * @param command command to submit
225: */
226: public void submit(Command command) {
227: commands.add(command);
228:
229: maybeStartExecutionTimer();
230: }
231:
232: /**
233: * Submits an {@link IncrementalCommand} for execution.
234: *
235: * @param command command to submit
236: */
237: public void submit(IncrementalCommand command) {
238: commands.add(command);
239:
240: maybeStartExecutionTimer();
241: }
242:
243: /**
244: * Reports either a {@link CommandCanceledException} or an
245: * {@link IncrementalCommandCanceledException} back through the
246: * {@link UncaughtExceptionHandler} if one is set.
247: */
248: protected void doCommandCanceled() {
249: Object cmd = iterator.getLast();
250: iterator.remove();
251: assert (cmd != null);
252:
253: RuntimeException ex = null;
254: if (cmd instanceof Command) {
255: ex = new CommandCanceledException((Command) cmd);
256: } else if (cmd instanceof IncrementalCommand) {
257: ex = new IncrementalCommandCanceledException(
258: (IncrementalCommand) cmd);
259: }
260:
261: if (ex != null) {
262: UncaughtExceptionHandler ueh = GWT
263: .getUncaughtExceptionHandler();
264: if (ueh != null) {
265: ueh.onUncaughtException(ex);
266: }
267: }
268:
269: setExecuting(false);
270:
271: maybeStartExecutionTimer();
272: }
273:
274: /**
275: * This method will dispatch commands from the command queue. It will dispatch
276: * commands until one of the following conditions is <code>true</code>:
277: * <ul>
278: * <li>It consumed its dispatching time slice {@value #DEFAULT_TIME_SLICE_MILLIS}</li>
279: * <li>It encounters a <code>null</code> in the command queue</li>
280: * <li>All commands which were present at the start of the dispatching have
281: * been removed from the command queue</li>
282: * <li>The command that it was processing was canceled due to a false
283: * cancellation -- in this case we exit without updating any state</li>
284: * </ul>
285: *
286: * @param startTimeMillis the time when this method started
287: */
288: protected void doExecuteCommands(long startTimeMillis) {
289: assert (!isExecutionTimerPending());
290:
291: boolean wasCanceled = false;
292: try {
293: setExecuting(true);
294:
295: iterator.setEnd(commands.size());
296:
297: cancellationTimer
298: .schedule((int) DEFAULT_CANCELLATION_TIMEOUT_MILLIS);
299:
300: while (iterator.hasNext()) {
301: Object element = iterator.next();
302:
303: boolean removeCommand = true;
304: try {
305: if (element == null) {
306: // null forces a yield or pause in execution
307: return;
308: }
309:
310: if (element instanceof Command) {
311: Command command = (Command) element;
312: command.execute();
313: } else if (element instanceof IncrementalCommand) {
314: IncrementalCommand incrementalCommand = (IncrementalCommand) element;
315: removeCommand = !incrementalCommand.execute();
316: }
317:
318: } finally {
319: wasCanceled = iterator.wasRemoved();
320: if (wasCanceled) {
321: /*
322: * The iterator may have already had its remove method called, if
323: * it has, then we need to exit without updating any state
324: */
325: return;
326: }
327:
328: if (removeCommand) {
329: iterator.remove();
330: }
331: }
332:
333: if (hasTimeSliceExpired(System.currentTimeMillis(),
334: startTimeMillis)) {
335: // the time slice has expired
336: return;
337: }
338: }
339: } finally {
340: if (!wasCanceled) {
341: cancellationTimer.cancel();
342:
343: setExecuting(false);
344:
345: maybeStartExecutionTimer();
346: }
347: }
348: }
349:
350: /**
351: * Starts the dispatch timer if there are commands to dispatch and we are not
352: * waiting for a dispatch timer and we are not actively dispatching.
353: */
354: protected void maybeStartExecutionTimer() {
355: if (!commands.isEmpty() && !isExecutionTimerPending()
356: && !isExecuting()) {
357: setExecutionTimerPending(true);
358: executionTimer.schedule(1);
359: }
360: }
361:
362: /**
363: * This method is for testing only.
364: */
365: List<Object> getPendingCommands() {
366: return commands;
367: }
368:
369: /**
370: * This method is for testing only.
371: */
372: void setExecuting(boolean executing) {
373: this .executing = executing;
374: }
375:
376: /**
377: * This method is for testing only.
378: */
379: void setLast(int last) {
380: iterator.setLast(last);
381: }
382:
383: /**
384: * Returns <code>true</code> if this instance is currently dispatching
385: * commands.
386: *
387: * @return <code>true</code> if this instance is currently dispatching
388: * commands
389: */
390: private boolean isExecuting() {
391: return executing;
392: }
393:
394: /**
395: * Returns <code>true</code> if a the dispatch timer was scheduled but it
396: * still has not fired.
397: *
398: * @return <code>true</code> if a the dispatch timer was scheduled but it
399: * still has not fired
400: */
401: private boolean isExecutionTimerPending() {
402: return executionTimerPending;
403: }
404:
405: private void setExecutionTimerPending(boolean pending) {
406: executionTimerPending = pending;
407: }
408: }
|