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 {Boolean} [may_pause=false] |
| 43 * The |
| 44 * |
| 45 * @param {Number} [runaway_limit] |
| 46 * The maximum number of iterations before 'run()' will throw an exception.
Disable runaway detection by |
| 47 * setting this value to zero. |
| 48 * |
| 49 */ |
| 50 var Long_Task = exports.Long_Task = function( task_instance, may_pause, runaway_
limit ) |
| 51 { |
| 52 /** |
| 53 * The core of a long task is a generator that runs on unit of computation w
ith each call to next(). This object |
| 54 * will give us such a generator by calling its generator() member. |
| 55 * @type {*} |
| 56 */ |
| 57 this.task_instance = task_instance; |
| 58 |
| 59 /** |
| 60 * The task generator for the task. It's initialized to null here, and set t
o the actual generator at the beginning |
| 61 * of the run() method. |
| 62 * <p/> |
| 63 * It returns 'false' when it is not yet completed and 'true' once it has. C
alling the generator with 'send( true )' |
| 64 * notifies the generator that it has been cancelled; thereafter it must ret
urn 'true' always. |
| 65 * @type {Boolean} |
| 66 */ |
| 67 this.task_generator = null; |
| 68 |
| 69 /** |
| 70 * Cancellation flag. Set in the cancel() method. Tested each iteration in r
un(). |
| 71 * @type {Boolean} |
| 72 */ |
| 73 this.cancelled = false; |
| 74 |
| 75 /** |
| 76 * Pause state flag. |
| 77 * @type {Boolean} |
| 78 */ |
| 79 this.paused = false; |
| 80 |
| 81 /** |
| 82 * Runnable flag. This is essentially the state variable of a two-state mach
ine, which starts at "runnable" and |
| 83 * goes to "completed". |
| 84 * @type {Boolean} |
| 85 */ |
| 86 this.runnable = true; |
| 87 |
| 88 /** |
| 89 * The maximum number of iterations before 'run()' will throw an exception.
Disable runaway detection by setting |
| 90 * this value to zero. |
| 91 * @type {Number} |
| 92 */ |
| 93 this.runaway_limit = (arguments.length < 2) ? default_runaway_limit : runawa
y_limit; |
| 94 |
| 95 /** |
| 96 * Iteration counter. This is only incremented when a runaway limit is in ef
fect. |
| 97 * @type {Number} |
| 98 */ |
| 99 this.count = 0; |
| 100 |
| 101 /** |
| 102 * XPCOM thread manager. Used to implement dispatch(). |
| 103 * @type {nsIThreadManager} |
| 104 */ |
| 105 this.thread_manager = Cc["@mozilla.org/thread-manager;1"].createInstance( Ci
.nsIThreadManager ); |
| 106 |
| 107 /** |
| 108 * Logging service. |
| 109 * @type {Logger} |
| 110 */ |
| 111 this.logger = new Logger( "Long_Task" ); |
| 112 }; |
| 113 |
| 114 /** |
| 115 * Close the task out completely. |
| 116 */ |
| 117 Long_Task.prototype.close = function() |
| 118 { |
| 119 this.cancel(); |
| 120 |
| 121 // DEFECT: We need to close the iterator, as well. |
| 122 /* |
| 123 * This is not trivial to implement correctly. If the task is paused, it mea
ns there's a pending operation that we |
| 124 * cannot prevent from executing, but will at some point will call resume().
It's also possible that there's already |
| 125 * another iteration of the main loop already dispatched. |
| 126 */ |
| 127 }; |
| 128 |
| 129 /** |
| 130 * Cancel command. Calling this function cancels the pending task as soon as pos
sible, which is nowhere near |
| 131 * immediate with JavaScript. |
| 132 * <p/> |
| 133 * WARNING: The current way that cancellation is implemented, there will be one
additional call to the task |
| 134 * generator before cancellation. If that's a problem, it's time to fix the algo
rithm, which means making |
| 135 * an initial call to 'next()' before setting up the standing loop, and swapping
the order of iterating and checking |
| 136 * for cancellation. |
| 137 */ |
| 138 Long_Task.prototype.cancel = function() |
| 139 { |
| 140 this.cancelled = true; |
| 141 }; |
| 142 |
| 143 /** |
| 144 * Run command |
| 145 */ |
| 146 Long_Task.prototype.run = function() |
| 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 "Long_Task no longer runnable"; |
| 154 } |
| 155 this.task_generator = this.task_instance.generator( this.pause.bind( this ),
this.resume.bind( this ) ); |
| 156 this._run_once(); |
| 157 }; |
| 158 |
| 159 /** |
| 160 * The main body of the runner. |
| 161 */ |
| 162 Long_Task.prototype._run_once = function() |
| 163 { |
| 164 var log = this.logger.make_log( "_run_once" ); |
| 165 |
| 166 /* |
| 167 * The bulk of this function is structure as 'do ... while( false )' in orde
r to use 'break' for flow control, |
| 168 * instead of splitting off a second function and using 'return'. |
| 169 */ |
| 170 //noinspection LoopStatementThatDoesntLoopJS |
| 171 do |
| 172 { |
| 173 /* |
| 174 * If we must pause, we simply don't do anything else now. The landing c
ode of the asynchronous object must |
| 175 * call resume(), which will dispatch the present function again and sta
rt the iteration cycle up again. |
| 176 */ |
| 177 if ( this.paused ) |
| 178 { |
| 179 return; |
| 180 /* |
| 181 * FUTURE: start a watchdog timer here that will cancel the object i
f the task times out. |
| 182 */ |
| 183 } |
| 184 |
| 185 /* |
| 186 * Main iteration call. The call to run() goes into a try-block to ensur
e we stop gracefully if the generator |
| 187 * throws, since that doesn't always signal an error |
| 188 */ |
| 189 try |
| 190 { |
| 191 if ( this.task_generator.next() ) |
| 192 { |
| 193 /* |
| 194 * The task generator returned true, which means that it's finis
hed. |
| 195 */ |
| 196 break; |
| 197 } |
| 198 } |
| 199 catch ( ex ) |
| 200 { |
| 201 if ( ex === StopIteration ) |
| 202 { |
| 203 log( "End. Task iterator stopped" ); |
| 204 /* |
| 205 * StopIteration is not an error but just signals the end of dat
a for an ordinary iterator. Since the |
| 206 * generator has signalled us, we don't send any signal by calli
ng 'send()'. |
| 207 */ |
| 208 break; |
| 209 } |
| 210 else |
| 211 { |
| 212 log( "Iteration exception " + ex.toString() ); |
| 213 throw ex; |
| 214 } |
| 215 } |
| 216 |
| 217 /* |
| 218 * Runaway detection. |
| 219 */ |
| 220 if ( this.runaway_limit > 0 ) |
| 221 { |
| 222 ++this.count; |
| 223 log( "Iteration " + this.count, false ); |
| 224 if ( this.count >= this.runaway_limit ) |
| 225 { |
| 226 this.cancelled = true; |
| 227 /* |
| 228 * FUTURE: This should really throw an exception after cancellin
g the generator. |
| 229 */ |
| 230 } |
| 231 } |
| 232 |
| 233 /* |
| 234 * Cancellation detection. |
| 235 */ |
| 236 if ( this.cancelled ) |
| 237 { |
| 238 log( "Cancellation begin" ); |
| 239 try |
| 240 { |
| 241 /* |
| 242 * We've received a command to cancel from elsewhere. Notify the
generator that we're shutting down and |
| 243 * exit the loop. We're doing this within a try-block because th
e generator will typically throw |
| 244 * StopIteration at this point, which isn't an error. |
| 245 */ |
| 246 this.task_generator.send( true ); |
| 247 } |
| 248 catch ( ex ) |
| 249 { |
| 250 /* |
| 251 * StopIteration is not an error as a result of cancellation, bu
t any other exception is. |
| 252 */ |
| 253 if ( ex !== StopIteration ) |
| 254 { |
| 255 log( "Cancellation exception: " + ex.toString() ); |
| 256 throw ex; |
| 257 } |
| 258 } |
| 259 log( "Cancellation end" ); |
| 260 break; |
| 261 } |
| 262 |
| 263 /* |
| 264 * Infinite loop behavior happens here, where we schedule ourselves for
another run as soon as possible |
| 265 * after we complete. This uses the container's thread manager, so it ex
ecutes more-or-less immediately. |
| 266 * If there are long-duration asynchronous actions in the task, such as
loading web pages or AJAX calls, |
| 267 * this routine runs too fast to be effective as a poll. Such tasks shou
ld pause when such operations are |
| 268 * pending. |
| 269 */ |
| 270 this.dispatch(); |
| 271 return; |
| 272 } while ( false ); |
| 273 this.runnable = false; |
| 274 }; |
| 275 |
| 276 /** |
| 277 * Pause instruction. Since JavaScript is not multi-threaded, the pause instruct
ion does not block. Instead, it takes |
| 278 * effect at the next 'yield' statement. |
| 279 */ |
| 280 Long_Task.prototype.pause = function() |
| 281 { |
| 282 this.paused = true; |
| 283 }; |
| 284 |
| 285 /** |
| 286 * |
| 287 */ |
| 288 Long_Task.prototype.resume = function() |
| 289 { |
| 290 this.paused = false; |
| 291 this.dispatch(); |
| 292 }; |
| 293 |
| 294 /** |
| 295 * Dispatch another iteration. This is used ordinarily at the end of _run_once()
and also by resume() to restart |
| 296 * the iteration. |
| 297 */ |
| 298 Long_Task.prototype.dispatch = function() |
| 299 { |
| 300 this.thread_manager.currentThread.dispatch( |
| 301 {run: this._run_once.bind( this )}, |
| 302 Ci.nsIEventTarget.DISPATCH_NORMAL |
| 303 ); |
| 304 }; |
| 305 |
OLD | NEW |