OLD | NEW |
(Empty) | |
| 1 /** |
| 2 * The default value for runaway_limit, used only in the constructor. |
| 3 * @type {Number} |
| 4 */ |
| 5 const default_runaway_limit = 1000; |
| 6 |
| 7 /** |
| 8 * Class for long-running tasks. Such tasks divide work into discrete units of e
ffort, which allows them to be |
| 9 * interrupted and to post progress. |
| 10 * <p/> |
| 11 * Note that are no callback or events in this class. Any state feedback should
be implemented as needed within the |
| 12 * task generator. task generator can distinguish cancellation by examining the
rvalue of its yield statements. The |
| 13 * generator cannot detect being paused, but the control object that started the
task can. |
| 14 * <p/> |
| 15 * PLANNED: There's no 'pause()' in this version. This is very similar to 'cance
l()' except that it's restartable. The |
| 16 * code isn't particularly difficult, but it would require reworking the main 'r
un()' loop. |
| 17 * <p/> |
| 18 * PLANNED: The current version will handle AJAX calls by polling the task gener
ator every millisecond to see if there |
| 19 * have been pending calls that have completed since the last poll. It would mak
e for better performance for the task |
| 20 * to signal that it should pause execution and then continue only after a pendi
ng call completes. This would eliminate |
| 21 * run-time polling overhead. |
| 22 * <p/> |
| 23 * There would also need to be an interface presented to the task for pause and
resume. Resuming is the harder one, |
| 24 * because the task needs an object to call when 'Long_Task.prototype.run' is no
t in the call stack. Currently, the |
| 25 * generator is instantiated before the constructor for this class, which means
either passing such an object with |
| 26 * 'send()' if that constructor is to be called. The better way is likely for th
is class to instantiate the generator |
| 27 * with a pause/resume object as argument, but this only allows parametric gener
ators (pretty much a requirement) if |
| 28 * if 'Function.protoype.bind()' works on generator-functions (untried) or if an
equivalent can be hacked up. |
| 29 * |
| 30 * @param {Generator} task_generator |
| 31 * The task generator is the task to be run, implemented as a generator. Ea
ch call to the generator performs an |
| 32 * increment of computation, whose size is determined by the task. This is
part of a cooperative multitasking |
| 33 * system. |
| 34 * <p/> |
| 35 * Note that this argument is a generator, not the function that returns a
generator when called. Instantiating |
| 36 * the generator with a function call is the responsibility of the code tha
t instantiates this class. |
| 37 * <p/> |
| 38 * Note 'task_generator.next()' is always called at least once, because 'ru
n()' calls that method before it detects |
| 39 * cancellation. This is required by the interface to a generator, since it
's valid to call 'send()', which is how |
| 40 * the runner signals cancellation to the task, only after the first call t
o 'next'. If, for whatever reason, it's |
| 41 * necessary to detect cancellation before the work begins, the generator s
hould have an extra do-nothing 'yield' |
| 42 * statement at the beginning. |
| 43 * |
| 44 * @param {Number} runaway_limit |
| 45 * Optional. The maximum number of iterations before 'run()' will throw an
exception. Disable runaway detection by |
| 46 * setting this value to zero. |
| 47 * |
| 48 */ |
| 49 var Long_Task = exports.Long_Task = function ( task_generator, runaway_limit ) |
| 50 { |
| 51 /** |
| 52 * The runner is a generator that performs a single increment of computation
with each call to next(). It returns |
| 53 * 'false' when it is not yet completed and 'true' once it has. Calling the
generator with 'send( true )' notifies |
| 54 * the generator that it has been cancelled; thereafter it must return 'true
' always. |
| 55 * @type {Function} |
| 56 */ |
| 57 this.task_generator = task_generator; |
| 58 |
| 59 /** |
| 60 * Cancellation flag. Set in the cancel() method. Tested each iteration in r
un(). |
| 61 * @type {Boolean} |
| 62 */ |
| 63 this.cancelled = false; |
| 64 |
| 65 /** |
| 66 * Runnable flag. This is essentially the state variable of a two-state mach
ine, which starts at "runnable" and |
| 67 * goes to "completed". |
| 68 * @type {Boolean} |
| 69 */ |
| 70 this.runnable = true; |
| 71 |
| 72 /** |
| 73 * The maximum number of iterations before 'run()' will throw an exception.
Disable runaway detection by setting |
| 74 * this value to zero. |
| 75 * @type {Number} |
| 76 */ |
| 77 this.runaway_limit = (arguments.length < 2) ? default_runaway_limit : runawa
y_limit; |
| 78 |
| 79 /** |
| 80 * Iteration counter. This is only incremented when a runaway limit is in ef
fect. |
| 81 * @type {Number} |
| 82 */ |
| 83 this.count = 0; |
| 84 |
| 85 /** |
| 86 * Internal timer. This is an nsITimer rather than the window time to allow
background operation. |
| 87 * @type {nsITimer} |
| 88 */ |
| 89 this.timer = Cc["@mozilla.org/timer;1"].createInstance( Ci.nsITimer ); |
| 90 }; |
| 91 |
| 92 /** |
| 93 * Cancel command. Calling this function cancels the pending task as soon as pos
sible, which is nowhere near |
| 94 * immediate with JavaScript. |
| 95 * <p/> |
| 96 * WARNING: The current way that cancellation is implemented, there will be one
additional call to the task |
| 97 * generator before cancellation. If that's a problem, it's time to fix the algo
rithm, which means making |
| 98 * an initial call to 'next()' before setting up the standing loop, and swapping
the order of iterating and checking |
| 99 * for cancellation. |
| 100 */ |
| 101 Long_Task.prototype.cancel = function () |
| 102 { |
| 103 this.cancelled = true; |
| 104 }; |
| 105 |
| 106 /** |
| 107 * Run command |
| 108 */ |
| 109 Long_Task.prototype.run = function () |
| 110 { |
| 111 Long_Task.log( "run begin. runaway_limit = " + this.runaway_limit ); |
| 112 |
| 113 if ( !this.runnable ) |
| 114 { |
| 115 throw "Long_Task no longer runnable"; |
| 116 } |
| 117 |
| 118 this._run_once(); |
| 119 }; |
| 120 |
| 121 /** |
| 122 * The main body of the runner. |
| 123 */ |
| 124 Long_Task.prototype._run_once = function () |
| 125 { |
| 126 /* |
| 127 * The bulk of this function is structure as 'do ... while( false )' in orde
r to use 'break' for flow control, |
| 128 * instead of splitting off a second function and using 'return'. |
| 129 */ |
| 130 do |
| 131 { |
| 132 /* |
| 133 * Main iteration call. The call to run() goes into a try-block to ensur
e we stop gracefully if the generator |
| 134 * throws, since that doesn't always signal an error |
| 135 */ |
| 136 try |
| 137 { |
| 138 if ( this.task_generator.next() ) |
| 139 { |
| 140 /* |
| 141 * The task generator returned true, which means that it's finis
hed. |
| 142 */ |
| 143 break; |
| 144 } |
| 145 } |
| 146 catch ( ex ) |
| 147 { |
| 148 if ( ex === StopIteration ) |
| 149 { |
| 150 Long_Task.log( "run iteration stopped" ); |
| 151 /* |
| 152 * StopIteration is not an error but just signals the end of dat
a for an ordinary iterator. Since the |
| 153 * generator has signalled us, we don't send any signal by calli
ng 'send()'. |
| 154 */ |
| 155 break; |
| 156 } |
| 157 else |
| 158 { |
| 159 Long_Task.log( "run iteration exception: " + ex.toString() ); |
| 160 throw ex; |
| 161 } |
| 162 } |
| 163 |
| 164 /* |
| 165 * Runaway detection. |
| 166 */ |
| 167 if ( this.runaway_limit > 0 ) |
| 168 { |
| 169 ++this.count; |
| 170 Long_Task.log( "run iteration " + this.count, true ); |
| 171 if ( this.count >= this.runaway_limit ) |
| 172 { |
| 173 this.cancelled = true; |
| 174 /* |
| 175 * FUTURE: This should really throw an exception after cancellin
g the generator. |
| 176 */ |
| 177 } |
| 178 } |
| 179 |
| 180 /* |
| 181 * Cancellation detection. |
| 182 */ |
| 183 if ( this.cancelled ) |
| 184 { |
| 185 Long_Task.log( "run cancellation begin" ); |
| 186 try |
| 187 { |
| 188 /* |
| 189 * We've received a command to cancel from elsewhere. Notify the
generator that we're shutting down and |
| 190 * exit the loop. We're doing this within a try-block because th
e generator will typically throw |
| 191 * StopIteration at this point, which isn't an error. |
| 192 */ |
| 193 this.task_generator.send( true ); |
| 194 } |
| 195 catch ( ex ) |
| 196 { |
| 197 /* |
| 198 * StopIteration is not an error as a result of cancellation, bu
t any other exception is. |
| 199 */ |
| 200 if ( ex !== StopIteration ) |
| 201 { |
| 202 Long_Task.log( "run cancellation exception: " + ex.toString(
) ); |
| 203 throw ex; |
| 204 } |
| 205 } |
| 206 Long_Task.log( "run cancellation end" ); |
| 207 break; |
| 208 } |
| 209 |
| 210 /* |
| 211 * Infinite loop behavior happens here, where we schedule ourselves for
another run as soon as possible |
| 212 * after we complete. In practice, this means "at the next millisecond t
imer tick, or after all event handlers |
| 213 * have run, whichever comes last". If the unit of computation is less t
han 50 ms, the duration overhead will be |
| 214 * over 1%, but it's just fine for scheduling asynchronous events where
this internal timer is not the limiting |
| 215 * factor in total duration, such as AJAX calls. |
| 216 */ |
| 217 this.timer.initWithCallback( this._run_once.bind( this ), 0, Ci.nsITimer
.TYPE_ONE_SHOT ); |
| 218 return; |
| 219 } while ( false ) |
| 220 this.runnable = false; |
| 221 }; |
| 222 |
| 223 Long_Task.log = function ( msg, allow ) |
| 224 { |
| 225 if ( arguments.length < 2 || allow ) |
| 226 Cu.reportError( "Long_Task: " + msg ); |
| 227 }; |
OLD | NEW |