OLD | NEW |
(Empty) | |
| 1 let {Logger} = require( "logger" ); |
| 2 /** |
| 3 * The default value for runaway_limit, used only in the constructor. |
| 4 * @type {Number} |
| 5 */ |
| 6 const default_runaway_limit = 1000; |
| 7 |
| 8 /** |
| 9 * Class for long-running tasks. Such tasks divide work into discrete units of e
ffort, which allows them to be |
| 10 * interrupted and to post progress. |
| 11 * <p/> |
| 12 * Note that are no callback or events in this class. Any state feedback should
be implemented as needed within the |
| 13 * task generator. task generator can distinguish cancellation by examining the
rvalue of its yield statements. The |
| 14 * generator cannot detect being paused, but the control object that started the
task can. |
| 15 * <p/> |
| 16 * PLANNED: The current version will handle AJAX calls by polling the task gener
ator every millisecond to see if there |
| 17 * have been pending calls that have completed since the last poll. It would mak
e for better performance for the task |
| 18 * to signal that it should pause execution and then continue only after a pendi
ng call completes. This would eliminate |
| 19 * run-time polling overhead. |
| 20 * <p/> |
| 21 * There would also need to be an interface presented to the task for pause and
resume. Resuming is the harder one, |
| 22 * because the task needs an object to call when 'Long_Task.prototype.run' is no
t in the call stack. Currently, the |
| 23 * generator is instantiated before the constructor for this class, which means
either passing such an object with |
| 24 * 'send()' if that constructor is to be called. The better way is likely for th
is class to instantiate the generator |
| 25 * with a pause/resume object as argument, but this only allows parametric gener
ators (pretty much a requirement) if |
| 26 * if 'Function.protoype.bind()' works on generator-functions (untried) or if an
equivalent can be hacked up. |
| 27 * |
| 28 * @param {Generator} task_generator |
| 29 * The task generator is the task to be run, implemented as a generator. Ea
ch call to the generator performs an |
| 30 * increment of computation, whose size is determined by the task. This is
part of a cooperative multitasking |
| 31 * system. |
| 32 * <p/> |
| 33 * Note that this argument is a generator, not the function that returns a
generator when called. Instantiating |
| 34 * the generator with a function call is the responsibility of the code tha
t instantiates this class. |
| 35 * <p/> |
| 36 * Note 'task_generator.next()' is always called at least once, because 'ru
n()' calls that method before it detects |
| 37 * cancellation. This is required by the interface to a generator, since it
's valid to call 'send()', which is how |
| 38 * the runner signals cancellation to the task, only after the first call t
o 'next'. If, for whatever reason, it's |
| 39 * necessary to detect cancellation before the work begins, the generator s
hould have an extra do-nothing 'yield' |
| 40 * statement at the beginning. |
| 41 * |
| 42 * @param {Number} [runaway_limit] |
| 43 * The maximum number of iterations before 'run()' will throw an exception.
Disable runaway detection by |
| 44 * setting this value to zero. |
| 45 * |
| 46 */ |
| 47 var Long_Task = exports.Long_Task = function( task_instance, runaway_limit ) |
| 48 { |
| 49 /** |
| 50 * The core of a long task is a generator that runs on unit of computation wit
h each call to next(). This object |
| 51 * will give us such a generator by calling its generator() member. |
| 52 * @type {*} |
| 53 */ |
| 54 this.task_instance = task_instance; |
| 55 |
| 56 /** |
| 57 * The task generator for the task. It's initialized to null here, and set to
the actual generator at the beginning |
| 58 * of the run() method. |
| 59 * <p/> |
| 60 * It returns 'false' when it is not yet completed and 'true' once it has. Cal
ling the generator with 'send( true )' |
| 61 * notifies the generator that it has been cancelled; thereafter it must retur
n 'true' always. |
| 62 * @type {Boolean} |
| 63 */ |
| 64 this.task_generator = null; |
| 65 |
| 66 /** |
| 67 * Cancellation flag. Set in the cancel() method. Tested each iteration in run
(). |
| 68 * @type {Boolean} |
| 69 */ |
| 70 this.cancelled = false; |
| 71 |
| 72 /** |
| 73 * Pause state flag. |
| 74 * @type {Boolean} |
| 75 */ |
| 76 this.paused = false; |
| 77 |
| 78 /** |
| 79 * Runnable flag. This is essentially the state variable of a two-state machin
e, which starts at "runnable" and |
| 80 * goes to "completed". |
| 81 * @type {Boolean} |
| 82 */ |
| 83 this.runnable = true; |
| 84 |
| 85 /** |
| 86 * The maximum number of iterations before 'run()' will throw an exception. Di
sable runaway detection by setting |
| 87 * this value to zero. |
| 88 * @type {Number} |
| 89 */ |
| 90 this.runaway_limit = (arguments.length < 2) ? default_runaway_limit : runaway_
limit; |
| 91 |
| 92 /** |
| 93 * Iteration counter. This is only incremented when a runaway limit is in effe
ct. |
| 94 * @type {Number} |
| 95 */ |
| 96 this.count = 0; |
| 97 |
| 98 /** |
| 99 * XPCOM thread manager. Used to implement dispatch(). |
| 100 * @type {nsIThreadManager} |
| 101 */ |
| 102 this.thread_manager = Cc["@mozilla.org/thread-manager;1"].createInstance( Ci.n
sIThreadManager ); |
| 103 |
| 104 /** |
| 105 * Logging service. |
| 106 * @type {Logger} |
| 107 */ |
| 108 this.logger = new Logger( "Long_Task" ); |
| 109 }; |
| 110 |
| 111 /** |
| 112 * Close the task out completely. |
| 113 */ |
| 114 Long_Task.prototype.close = function() |
| 115 { |
| 116 this.cancel(); |
| 117 |
| 118 // DEFECT: We need to close the iterator, as well. |
| 119 /* |
| 120 * This is not trivial to implement correctly. If the task is paused, it means
there's a pending operation that we |
| 121 * cannot prevent from executing, but will at some point will call resume(). I
t's also possible that there's already |
| 122 * another iteration of the main loop already dispatched. |
| 123 */ |
| 124 }; |
| 125 |
| 126 /** |
| 127 * Cancel command. Calling this function cancels the pending task as soon as pos
sible, which is nowhere near |
| 128 * immediate with JavaScript. |
| 129 * <p/> |
| 130 * WARNING: The current way that cancellation is implemented, there will be one
additional call to the task |
| 131 * generator before cancellation. If that's a problem, it's time to fix the algo
rithm, which means making |
| 132 * an initial call to 'next()' before setting up the standing loop, and swapping
the order of iterating and checking |
| 133 * for cancellation. |
| 134 */ |
| 135 Long_Task.prototype.cancel = function() |
| 136 { |
| 137 this.cancelled = true; |
| 138 }; |
| 139 |
| 140 /** |
| 141 * Run command |
| 142 * |
| 143 * @param {Function} finisher |
| 144 * @param {Function} [catcher] |
| 145 */ |
| 146 Long_Task.prototype.run = function( finisher, catcher ) |
| 147 { |
| 148 var log = this.logger.make_log( "run" ); |
| 149 log( "Begin, runaway_limit = " + this.runaway_limit ); |
| 150 |
| 151 if ( !this.runnable ) |
| 152 { |
| 153 throw new Error( "Long_Task no longer runnable" ); |
| 154 } |
| 155 /* |
| 156 * We don't want to start up another after the first one has started. Therefor
e, we can only call run() once and |
| 157 * get any useful behavior. |
| 158 */ |
| 159 this.runnable = false; |
| 160 |
| 161 /** |
| 162 * Function to transfer control to when the run is completed. This happens reg
ardless of whether or not there's an |
| 163 * exception thrown. |
| 164 * @type {Function} |
| 165 */ |
| 166 this.finisher = finisher; |
| 167 /** |
| 168 * Function to call if there's an exception thrown during the run. |
| 169 * @type {Function} |
| 170 */ |
| 171 this.catcher = catcher; |
| 172 |
| 173 try |
| 174 { |
| 175 /** |
| 176 * The generator that acts as the body of the Long_Task. It's instantiated h
ere so that the 'pause' and 'resume' |
| 177 * functions can be passed to the generator. |
| 178 * @type {Generator} |
| 179 */ |
| 180 this.task_generator = this.task_instance.generator( this.pause.bind( this ),
this.resume.bind( this ) ); |
| 181 } |
| 182 catch ( e ) |
| 183 { |
| 184 throw e; |
| 185 //this._run_catch( e ); |
| 186 //return; |
| 187 } |
| 188 this._run_once(); |
| 189 }; |
| 190 |
| 191 /** |
| 192 * The main body of the runner. |
| 193 * |
| 194 * The return points of this function fall into two categories. The internal ret
urn points keep the Long_Task active as |
| 195 * a control structure. The external return points pass control back to the call
ing code. |
| 196 */ |
| 197 Long_Task.prototype._run_once = function() |
| 198 { |
| 199 var log = this.logger.make_log( "_run_once" ); |
| 200 |
| 201 /* |
| 202 * If we must pause, we simply don't do anything else now. The landing code of
the asynchronous object must |
| 203 * call resume(), which will dispatch the present function again and start the
iteration cycle up again. This |
| 204 * can be a source of failure if pause-resume coordination isn't done correctl
y in the task generator. |
| 205 */ |
| 206 if ( this.paused ) |
| 207 { |
| 208 /* |
| 209 * Internal return. Nothing scheduled. |
| 210 */ |
| 211 return; |
| 212 /* |
| 213 * FUTURE: start a watchdog timer here that will cancel the object if the ta
sk times out. It might be the case |
| 214 * that this is better done by the task generator only if necessary. A gener
ator-transformer that sets up such |
| 215 * a timer could be the replacement of setting up a timer here. |
| 216 */ |
| 217 } |
| 218 |
| 219 /* |
| 220 * Main iteration call. The call to run() goes into a try-block to ensure we s
top gracefully if the generator |
| 221 * throws, since that doesn't always signal an error |
| 222 */ |
| 223 try |
| 224 { |
| 225 if ( this.task_generator.next() ) |
| 226 { |
| 227 /* |
| 228 * The task generator returned true, which means that it's finished. |
| 229 * |
| 230 * External return. Ordinary. |
| 231 */ |
| 232 this._run_finally(); |
| 233 return; |
| 234 } |
| 235 } |
| 236 catch ( ex ) |
| 237 { |
| 238 if ( ex === StopIteration ) |
| 239 { |
| 240 log( "End. Task iterator stopped" ); |
| 241 /* |
| 242 * StopIteration is not an error but just signals the end of data for an o
rdinary iterator. Since the |
| 243 * generator has signalled us, we don't send any signal by calling 'send()
'. |
| 244 * |
| 245 * External return. Ordinary. |
| 246 */ |
| 247 this._run_finally(); |
| 248 } |
| 249 else |
| 250 { |
| 251 log( "Iteration exception " + ex.toString() ); |
| 252 /* |
| 253 * External return. Exceptional. |
| 254 */ |
| 255 this._run_catch( ex ); |
| 256 } |
| 257 return; |
| 258 } |
| 259 // Assert 'this.task_generator' returned false, which means that it isn't fini
shed. |
| 260 |
| 261 /* |
| 262 * Runaway detection. |
| 263 */ |
| 264 if ( this.runaway_limit > 0 ) |
| 265 { |
| 266 ++this.count; |
| 267 log( "Iteration " + this.count, false ); |
| 268 if ( this.count >= this.runaway_limit ) |
| 269 { |
| 270 this.cancelled = true; |
| 271 /* |
| 272 * External return. Exceptional. |
| 273 */ |
| 274 this._run_catch( new Error( "Long_Task: runaway iteration. count=" + this.
count ) ); |
| 275 return; |
| 276 } |
| 277 } |
| 278 |
| 279 /* |
| 280 * Cancellation detection. |
| 281 */ |
| 282 if ( this.cancelled ) |
| 283 { |
| 284 log( "Cancellation begin" ); |
| 285 try |
| 286 { |
| 287 /* |
| 288 * We've received a command to cancel from elsewhere. Notify the generator
that we're shutting down and |
| 289 * exit the loop. We're doing this within a try-block because the generato
r will typically throw |
| 290 * StopIteration at this point, which isn't an error. |
| 291 */ |
| 292 this.task_generator.send( true ); |
| 293 } |
| 294 catch ( ex ) |
| 295 { |
| 296 /* |
| 297 * StopIteration is not an error as a result of cancellation, but any othe
r exception is. |
| 298 */ |
| 299 if ( ex !== StopIteration ) |
| 300 { |
| 301 log( "Cancellation exception: " + ex.toString() ); |
| 302 /* |
| 303 * External return. Exceptional. |
| 304 */ |
| 305 this._run_catch( ex ); |
| 306 } |
| 307 } |
| 308 log( "Cancellation end" ); |
| 309 /* |
| 310 * External return. Ordinary. |
| 311 */ |
| 312 this._run_finally(); |
| 313 return; |
| 314 } |
| 315 |
| 316 /* |
| 317 * Infinite loop behavior happens here, where we schedule ourselves for anothe
r run as soon as possible |
| 318 * after we complete. This uses the container's thread manager, so it executes
more-or-less immediately. |
| 319 * If there are long-duration asynchronous actions in the task, such as loadin
g web pages or AJAX calls, |
| 320 * this routine runs too fast to be effective as a poll. Such tasks should pau
se when such operations are |
| 321 * pending. |
| 322 */ |
| 323 this._dispatch( this._run_once.bind( this ) ); |
| 324 /* |
| 325 * Internal return. Next iteration is scheduled. |
| 326 */ |
| 327 }; |
| 328 |
| 329 /** |
| 330 * Execute the finally-function. |
| 331 */ |
| 332 Long_Task.prototype._run_finally = function() |
| 333 { |
| 334 /* |
| 335 * We dispatch the actual catch-function so that it's not running in this cont
ext. This alleviates some odd timing |
| 336 * behavior for the user if there are pending events. |
| 337 */ |
| 338 if ( this.finisher ) |
| 339 { |
| 340 this._dispatch( this.finisher ); |
| 341 } |
| 342 }; |
| 343 |
| 344 /** |
| 345 * Execute the catch-function and then the finally-function, mimicking the behav
ior of a 'try' statement. |
| 346 * |
| 347 * @param {*} e |
| 348 * The value thrown as an exception. It's treated as an opaque type. |
| 349 */ |
| 350 Long_Task.prototype._run_catch = function( e ) |
| 351 { |
| 352 if ( this.catcher ) |
| 353 { |
| 354 this._dispatch( |
| 355 function() |
| 356 { |
| 357 this.catcher( e ); |
| 358 this._run_finally(); |
| 359 }.bind( this ) |
| 360 ); |
| 361 } |
| 362 }; |
| 363 |
| 364 /** |
| 365 * Pause instruction. Since JavaScript is not multi-threaded, the pause instruct
ion does not block. Instead, it takes |
| 366 * effect at the next 'yield' statement. |
| 367 */ |
| 368 Long_Task.prototype.pause = function() |
| 369 { |
| 370 this.paused = true; |
| 371 }; |
| 372 |
| 373 /** |
| 374 * |
| 375 */ |
| 376 Long_Task.prototype.resume = function() |
| 377 { |
| 378 this.paused = false; |
| 379 this._dispatch( this._run_once.bind( this ) ); |
| 380 }; |
| 381 |
| 382 /** |
| 383 * Dispatch another iteration. This is used ordinarily at the end of _run_once()
and also by resume() to restart |
| 384 * the iteration. |
| 385 */ |
| 386 Long_Task.prototype._dispatch = function( f ) |
| 387 { |
| 388 this.thread_manager.currentThread.dispatch( |
| 389 {run: f}, |
| 390 Ci.nsIEventTarget.DISPATCH_NORMAL |
| 391 ); |
| 392 }; |
| 393 |
OLD | NEW |