Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: lib/task.js

Issue 9084006: task.js: cooperative multitasking for long-running tasks (Closed)
Patch Set: Created Dec. 26, 2012, 5:06 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « lib/counter_task.js ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 };
OLDNEW
« no previous file with comments | « lib/counter_task.js ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld