OLD | NEW |
---|---|
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 // Timer heap implemented as a array-based binary heap[0]. | 5 // Timer heap implemented as a array-based binary heap[0]. |
6 // This allows for O(1) `first`, O(log(n)) `remove`/`removeFirst` and O(log(n)) | 6 // This allows for O(1) `first`, O(log(n)) `remove`/`removeFirst` and O(log(n)) |
7 // `add`. | 7 // `add`. |
8 // | 8 // |
9 // To ensure the timers are ordered by insertion time, the _Timer class has a | 9 // To ensure the timers are ordered by insertion time, the _Timer class has a |
10 // `_id` field set when added to the heap. | 10 // `_id` field set when added to the heap. |
(...skipping 99 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
110 | 110 |
111 static int _parentIndex(int index) => (index - 1) ~/ 2; | 111 static int _parentIndex(int index) => (index - 1) ~/ 2; |
112 static int _leftChildIndex(int index) => 2 * index + 1; | 112 static int _leftChildIndex(int index) => 2 * index + 1; |
113 static int _rightChildIndex(int index) => 2 * index + 2; | 113 static int _rightChildIndex(int index) => 2 * index + 2; |
114 } | 114 } |
115 | 115 |
116 class _Timer implements Timer { | 116 class _Timer implements Timer { |
117 // Cancels the timer in the event handler. | 117 // Cancels the timer in the event handler. |
118 static const int _NO_TIMER = -1; | 118 static const int _NO_TIMER = -1; |
119 | 119 |
120 // Timers are ordered by wakeup time. | 120 // We distinguish what kind of message arrived based on the value being sent. |
121 static const _ZERO_EVENT = 1; | |
122 static const _TIMEOUT_EVENT = null; | |
Søren Gjesse
2015/02/27 14:12:56
Additional question: Why are you using 1 and null,
Ivan Posva
2015/03/03 04:51:53
The external event handler is sending nulls for ti
| |
123 | |
124 // Timers are ordered by wakeup time. Timers with a timeout value of > 0 do | |
125 // end up on the TimerHeap. Timers with a timeout of 0 are queued in a list. | |
121 static _TimerHeap _heap = new _TimerHeap(); | 126 static _TimerHeap _heap = new _TimerHeap(); |
122 static _Timer _firstZeroTimer; | 127 static _Timer _firstZeroTimer; |
123 static _Timer _lastZeroTimer; | 128 static _Timer _lastZeroTimer; |
124 | 129 |
125 // We use an id to be able to sort timers with the same expiration time. | 130 // We use an id to be able to sort timers with the same expiration time. |
126 // ids are recycled after ID_MASK enqueues or when the timer queue is empty. | 131 // ids are recycled after ID_MASK enqueues or when the timer queue is empty. |
127 static int _ID_MASK = 0x1fffffff; | 132 static int _ID_MASK = 0x1fffffff; |
128 static int _idCount = 0; | 133 static int _idCount = 0; |
129 | 134 |
130 static RawReceivePort _receivePort; | 135 static RawReceivePort _receivePort; |
131 static SendPort _sendPort; | 136 static SendPort _sendPort; |
132 static int _scheduledWakeupTime; | 137 static int _scheduledWakeupTime; |
133 // Keep track whether at least one message is pending in the event loop. This | |
134 // way we do not have to notify for every pending _firstZeroTimer. | |
135 static var _messagePending = false; | |
136 | 138 |
137 static bool _handlingCallbacks = false; | 139 static bool _handlingCallbacks = false; |
138 | 140 |
139 Function _callback; // Closure to call when timer fires. null if canceled. | 141 Function _callback; // Closure to call when timer fires. null if canceled. |
140 int _wakeupTime; // Expiration time. | 142 int _wakeupTime; // Expiration time. |
141 int _milliSeconds; // Duration specified at creation. | 143 int _milliSeconds; // Duration specified at creation. |
142 bool _repeating; // Indicates periodic timers. | 144 bool _repeating; // Indicates periodic timers. |
143 var _indexOrNext; // Index if part of the TimerHeap, link otherwise. | 145 var _indexOrNext; // Index if part of the TimerHeap, link otherwise. |
144 int _id; // Incrementing id to enable sorting of timers with same expiry. | 146 int _id; // Incrementing id to enable sorting of timers with same expiry. |
145 | 147 |
(...skipping 21 matching lines...) Expand all Loading... | |
167 // to nearest millisecond, not up, so that time + duration is before | 169 // to nearest millisecond, not up, so that time + duration is before |
168 // duration milliseconds from now. Using microsecond timers like | 170 // duration milliseconds from now. Using microsecond timers like |
169 // Stopwatch allows detecting that the timer fires early. | 171 // Stopwatch allows detecting that the timer fires early. |
170 int now = new DateTime.now().millisecondsSinceEpoch; | 172 int now = new DateTime.now().millisecondsSinceEpoch; |
171 int wakeupTime = (milliSeconds == 0) ? now : (now + 1 + milliSeconds); | 173 int wakeupTime = (milliSeconds == 0) ? now : (now + 1 + milliSeconds); |
172 | 174 |
173 _Timer timer = new _Timer._internal(callback, | 175 _Timer timer = new _Timer._internal(callback, |
174 wakeupTime, | 176 wakeupTime, |
175 milliSeconds, | 177 milliSeconds, |
176 repeating); | 178 repeating); |
177 | 179 // Enqueue this newly created timer in the appropriate structure and |
178 if (timer._addTimerToHeap()) { | 180 // notify if necessary. |
179 // The new timer is the first in queue. Update event handler. | 181 timer._enqueue(); |
180 _notifyEventHandler(); | |
181 } | |
182 return timer; | 182 return timer; |
183 } | 183 } |
184 | 184 |
185 factory _Timer(int milliSeconds, void callback(Timer timer)) { | 185 factory _Timer(int milliSeconds, void callback(Timer timer)) { |
186 return _createTimer(callback, milliSeconds, false); | 186 return _createTimer(callback, milliSeconds, false); |
187 } | 187 } |
188 | 188 |
189 factory _Timer.periodic(int milliSeconds, void callback(Timer timer)) { | 189 factory _Timer.periodic(int milliSeconds, void callback(Timer timer)) { |
190 return _createTimer(callback, milliSeconds, true); | 190 return _createTimer(callback, milliSeconds, true); |
191 } | 191 } |
192 | 192 |
193 bool get _isInHeap => _indexOrNext is int; | 193 bool get _isInHeap => _indexOrNext is int; |
194 | 194 |
195 void _clear() { | |
196 _callback = null; | |
197 } | |
198 | |
199 int _compareTo(_Timer other) { | 195 int _compareTo(_Timer other) { |
200 int c = _wakeupTime - other._wakeupTime; | 196 int c = _wakeupTime - other._wakeupTime; |
201 if (c != 0) return c; | 197 if (c != 0) return c; |
202 return _id - other._id; | 198 return _id - other._id; |
203 } | 199 } |
204 | 200 |
205 bool get isActive => _callback != null; | 201 bool get isActive => _callback != null; |
206 | 202 |
207 // Cancels a set timer. The timer is removed from the timer list and if | 203 // Cancels a set timer. The timer is removed from the timer heap if it is a |
208 // the given timer is the earliest timer the event handler is notified. | 204 // non-zero timer. Zero timers are kept in the list as they need to consume |
205 // the corresponding pending message. | |
209 void cancel() { | 206 void cancel() { |
210 _clear(); | 207 _callback = null; |
208 // Only heap timers are really removed. Zero timers need to consume their | |
209 // corresponding wakeup message so they are left in the queue. | |
211 if (!_isInHeap) return; | 210 if (!_isInHeap) return; |
212 // Only heap timers are really removed. Others are just dropped on | 211 bool update = _heap.isFirst(this); |
213 // notification. | |
214 bool update = (_firstZeroTimer == null) && _heap.isFirst(this); | |
215 _heap.remove(this); | 212 _heap.remove(this); |
216 if (update) { | 213 if (update) { |
217 _notifyEventHandler(); | 214 _notifyEventHandler(); |
218 } | 215 } |
219 } | 216 } |
220 | 217 |
221 void _advanceWakeupTime() { | 218 void _advanceWakeupTime() { |
222 // Recalculate the next wakeup time. For repeating timers with a 0 timeout | 219 // Recalculate the next wakeup time. For repeating timers with a 0 timeout |
223 // the next wakeup time is now. | 220 // the next wakeup time is now. |
224 _id = _nextId(); | 221 _id = _nextId(); |
225 if (_milliSeconds > 0) { | 222 if (_milliSeconds > 0) { |
226 _wakeupTime += _milliSeconds; | 223 _wakeupTime += _milliSeconds; |
227 } else { | 224 } else { |
228 _wakeupTime = new DateTime.now().millisecondsSinceEpoch; | 225 _wakeupTime = new DateTime.now().millisecondsSinceEpoch; |
229 } | 226 } |
230 } | 227 } |
231 | 228 |
232 // Adds a timer to the heap or timer list. Timers with the same wakeup time | 229 // Adds a timer to the heap or timer list. Timers with the same wakeup time |
233 // are enqueued in order and notified in FIFO order. | 230 // are enqueued in order and notified in FIFO order. |
234 bool _addTimerToHeap() { | 231 void _enqueue() { |
235 if (_milliSeconds == 0) { | 232 if (_milliSeconds == 0) { |
236 if (_firstZeroTimer == null) { | 233 if (_firstZeroTimer == null) { |
237 _lastZeroTimer = this; | 234 _lastZeroTimer = this; |
238 _firstZeroTimer = this; | 235 _firstZeroTimer = this; |
239 return true; | |
240 } else { | 236 } else { |
241 _lastZeroTimer._indexOrNext = this; | 237 _lastZeroTimer._indexOrNext = this; |
242 _lastZeroTimer = this; | 238 _lastZeroTimer = this; |
243 return false; | |
244 } | 239 } |
240 // Every zero timer gets its own event. | |
241 _notifyZeroHandler(); | |
245 } else { | 242 } else { |
246 _heap.add(this); | 243 _heap.add(this); |
247 return _firstZeroTimer == null && _heap.isFirst(this); | 244 if (_heap.isFirst(this)) { |
245 _notifyEventHandler(); | |
246 } | |
248 } | 247 } |
249 } | 248 } |
250 | 249 |
251 | 250 |
251 // Enqeue one message for each zero timer. To be able to distinguish from | |
252 // EventHandler messages we send a _ZERO_EVENT instead of a _TIMEOUT_EVENT. | |
253 static void _notifyZeroHandler() { | |
254 if (_sendPort == null) { | |
255 _createTimerHandler(); | |
256 } | |
257 _sendPort.send(_ZERO_EVENT); | |
258 } | |
259 | |
260 | |
261 // Handle the notification of a zero timer. Make sure to also execute non-zero | |
262 // timers with a lower expiration time. | |
263 static List _queueFromZeroEvent() { | |
264 var pendingTimers = new List(); | |
265 assert(_firstZeroTimer != null); | |
266 // Collect pending timers from the timer heap that have an expiration prior | |
267 // to the currently notified zero timer. | |
268 var timer; | |
269 while (!_heap.isEmpty && (_heap.first._compareTo(_firstZeroTimer) < 0)) { | |
270 timer = _heap.removeFirst(); | |
271 pendingTimers.add(timer); | |
272 } | |
273 // Append the first zero timer to the pending timers. | |
274 timer = _firstZeroTimer; | |
275 _firstZeroTimer = timer._indexOrNext; | |
276 timer._indexOrNext = null; | |
277 pendingTimers.add(timer); | |
278 return pendingTimers; | |
279 } | |
280 | |
281 | |
252 static void _notifyEventHandler() { | 282 static void _notifyEventHandler() { |
253 if (_handlingCallbacks) { | 283 if (_handlingCallbacks) { |
254 // While we are already handling callbacks we will not notify the event | 284 // While we are already handling callbacks we will not notify the event |
255 // handler. _handleTimeout will call _notifyEventHandler once all pending | 285 // handler. _handleTimeout will call _notifyEventHandler once all pending |
256 // timers are processed. | 286 // timers are processed. |
257 return; | 287 return; |
258 } | 288 } |
259 | 289 |
290 // If there are no pending timers. Close down the receive port. | |
260 if ((_firstZeroTimer == null) && _heap.isEmpty) { | 291 if ((_firstZeroTimer == null) && _heap.isEmpty) { |
261 // No pending timers: Close the receive port and let the event handler | 292 // No pending timers: Close the receive port and let the event handler |
262 // know. | 293 // know. |
263 if (_receivePort != null) { | 294 if (_sendPort != null) { |
264 VMLibraryHooks.eventHandlerSendData(null, _sendPort, _NO_TIMER); | 295 VMLibraryHooks.eventHandlerSendData(null, _sendPort, _NO_TIMER); |
265 _shutdownTimerHandler(); | 296 _shutdownTimerHandler(); |
266 } | 297 } |
267 } else { | 298 return; |
268 if (_receivePort == null) { | 299 } else if (_heap.isEmpty) { |
300 // Only zero timers are left. Cancel any scheduled wakeups. | |
301 VMLibraryHooks.eventHandlerSendData(null, _sendPort, _NO_TIMER); | |
302 return; | |
303 } | |
304 | |
305 // Only send a message if the requested wakeup time differs from the | |
306 // already scheduled wakeup time. | |
307 var wakeupTime = _heap.first._wakeupTime; | |
308 if ((_scheduledWakeupTime == null) || | |
309 (wakeupTime != _scheduledWakeupTime)) { | |
310 if (_sendPort == null) { | |
269 // Create a receive port and register a message handler for the timer | 311 // Create a receive port and register a message handler for the timer |
270 // events. | 312 // events. |
271 _createTimerHandler(); | 313 _createTimerHandler(); |
272 } | 314 } |
273 if (_firstZeroTimer != null) { | 315 VMLibraryHooks.eventHandlerSendData(null, _sendPort, wakeupTime); |
274 if (!_messagePending) { | 316 _scheduledWakeupTime = wakeupTime; |
275 _sendPort.send(null); | |
276 _messagePending = true; // Reset when the port receives a message. | |
277 } | |
278 } else { | |
279 var wakeupTime = _heap.first._wakeupTime; | |
280 if ((_scheduledWakeupTime == null) || | |
281 (wakeupTime != _scheduledWakeupTime)) { | |
282 VMLibraryHooks.eventHandlerSendData(null, _sendPort, wakeupTime); | |
283 _scheduledWakeupTime = wakeupTime; | |
284 } | |
285 } | |
286 } | 317 } |
287 } | 318 } |
288 | 319 |
289 static void _handleTimeout() { | 320 static List _queueFromTimeoutEvent() { |
290 // Fast exit if no timers have been scheduled. | 321 var pendingTimers = new List(); |
322 if (_firstZeroTimer != null) { | |
323 // Collect pending timers from the timer heap that have an expiration | |
324 // prior to the next zero timer. | |
325 // By definition the first zero timer has been scheduled before the | |
326 // current time, meaning all timers which are "less than" the first zero | |
327 // timer are expired. The first zero timer will be dispatched when its | |
328 // corresponding message is delivered. | |
329 var timer; | |
330 while (!_heap.isEmpty && (_heap.first._compareTo(_firstZeroTimer) < 0)) { | |
331 timer = _heap.removeFirst(); | |
332 pendingTimers.add(timer); | |
333 } | |
334 } else { | |
335 // Collect pending timers from the timer heap which have expired at this | |
336 // time. | |
337 var currentTime = new DateTime.now().millisecondsSinceEpoch; | |
338 var timer; | |
339 while (!_heap.isEmpty && (_heap.first._wakeupTime <= currentTime)) { | |
340 timer = _heap.removeFirst(); | |
341 pendingTimers.add(timer); | |
342 } | |
343 } | |
344 return pendingTimers; | |
345 } | |
346 | |
347 static void _runTimers(List pendingTimers) { | |
348 // If there are no pending timers currently reset the id space before we | |
349 // have a chance to enqueue new timers. | |
291 if (_heap.isEmpty && (_firstZeroTimer == null)) { | 350 if (_heap.isEmpty && (_firstZeroTimer == null)) { |
292 assert(_receivePort == null); | 351 _idCount = 0; |
293 return; | |
294 } | 352 } |
295 | 353 |
296 // Collect all pending timers. | 354 // Fast exit if no pending timers. |
297 var head = null; | 355 if (pendingTimers.length == 0) { |
298 var tail = null; | |
299 if (_heap.isEmpty) { | |
300 // Only immediate timers are scheduled. Take over the whole list as is. | |
301 assert(_firstZeroTimer != null); | |
302 assert(_lastZeroTimer != null); | |
303 head = _firstZeroTimer; | |
304 tail = _lastZeroTimer; | |
305 _firstZeroTimer = null; | |
306 _lastZeroTimer = null; | |
307 } else { | |
308 assert(!_heap.isEmpty); | |
309 // Keep track of the lowest wakeup times for both the list and heap. If | |
310 // the respective queue is empty move its time beyond the current time. | |
311 var currentTime = new DateTime.now().millisecondsSinceEpoch; | |
312 var heapTime = _heap.first._wakeupTime; | |
313 var listTime = (_firstZeroTimer == null) ? | |
314 (currentTime + 1) : _firstZeroTimer._wakeupTime; | |
315 | |
316 while ((heapTime <= currentTime) || (listTime <= currentTime)) { | |
317 var timer; | |
318 // Consume the timers in order by removing from heap or list based on | |
319 // their wakeup time and update the queue's time. | |
320 assert((heapTime != listTime) || | |
321 ((_heap.first != null) && (_firstZeroTimer != null))); | |
322 if ((heapTime < listTime) || | |
323 ((heapTime == listTime) && | |
324 (_heap.first._id < _firstZeroTimer._id))) { | |
325 timer = _heap.removeFirst(); | |
326 heapTime = _heap.isEmpty ? | |
327 (currentTime + 1) : _heap.first._wakeupTime; | |
328 } else { | |
329 timer = _firstZeroTimer; | |
330 assert(timer._milliSeconds == 0); | |
331 _firstZeroTimer = timer._indexOrNext; | |
332 if (_firstZeroTimer == null) { | |
333 _lastZeroTimer = null; | |
334 listTime = currentTime + 1; | |
335 } else { | |
336 // We want to drain all entries from the list as they should have | |
337 // been pending for 0 ms. To prevent issues with current time moving | |
338 // we ensure that the listTime does not go beyond current, unless | |
339 // the list is empty. | |
340 listTime = _firstZeroTimer._wakeupTime; | |
341 if (listTime > currentTime) { | |
342 listTime = currentTime; | |
343 } | |
344 } | |
345 } | |
346 | |
347 // Append this timer to the pending timer list. | |
348 timer._indexOrNext = null; | |
349 if (head == null) { | |
350 assert(tail == null); | |
351 head = timer; | |
352 tail = timer; | |
353 } else { | |
354 tail._indexOrNext = timer; | |
355 tail = timer; | |
356 } | |
357 } | |
358 } | |
359 | |
360 // No timers queued: Early exit. | |
361 if (head == null) { | |
362 return; | 356 return; |
363 } | 357 } |
364 | 358 |
365 // If there are no pending timers currently reset the id space before we | |
366 // have a chance to enqueue new timers. | |
367 assert(_firstZeroTimer == null); | |
368 if (_heap.isEmpty) { | |
369 _idCount = 0; | |
370 } | |
371 | |
372 // Trigger all of the pending timers. New timers added as part of the | 359 // Trigger all of the pending timers. New timers added as part of the |
373 // callbacks will be enqueued now and notified in the next spin at the | 360 // callbacks will be enqueued now and notified in the next spin at the |
374 // earliest. | 361 // earliest. |
375 _handlingCallbacks = true; | 362 _handlingCallbacks = true; |
376 try { | 363 try { |
377 while (head != null) { | 364 for (var i = 0; i < pendingTimers.length; i++) { |
378 // Dequeue the first candidate timer. | 365 // Next pending timer. |
379 var timer = head; | 366 var timer = pendingTimers[i]; |
380 head = timer._indexOrNext; | |
381 timer._indexOrNext = null; | 367 timer._indexOrNext = null; |
382 | 368 |
383 // One of the timers in the pending_timers list can cancel | 369 // One of the timers in the pending_timers list can cancel |
384 // one of the later timers which will set the callback to | 370 // one of the later timers which will set the callback to |
385 // null. | 371 // null. |
Søren Gjesse
2015/02/27 12:38:46
Extend this comment to say that all zero timers st
Ivan Posva
2015/03/03 04:51:53
Done.
| |
386 if (timer._callback != null) { | 372 if (timer._callback != null) { |
387 var callback = timer._callback; | 373 var callback = timer._callback; |
388 if (!timer._repeating) { | 374 if (!timer._repeating) { |
389 // Mark timer as inactive. | 375 // Mark timer as inactive. |
390 timer._callback = null; | 376 timer._callback = null; |
391 } | 377 } |
392 callback(timer); | 378 callback(timer); |
393 // Re-insert repeating timer if not canceled. | 379 // Re-insert repeating timer if not canceled. |
394 if (timer._repeating && (timer._callback != null)) { | 380 if (timer._repeating && (timer._callback != null)) { |
395 timer._advanceWakeupTime(); | 381 timer._advanceWakeupTime(); |
396 timer._addTimerToHeap(); | 382 timer._enqueue(); |
397 } | 383 } |
398 // Execute pending micro tasks. | 384 // Execute pending micro tasks. |
399 _runPendingImmediateCallback(); | 385 _runPendingImmediateCallback(); |
400 } | 386 } |
401 } | 387 } |
402 } finally { | 388 } finally { |
403 _handlingCallbacks = false; | 389 _handlingCallbacks = false; |
390 // Notify the event handler or shutdown the port if no more pending | |
391 // timers are present. | |
404 _notifyEventHandler(); | 392 _notifyEventHandler(); |
405 } | 393 } |
406 } | 394 } |
407 | 395 |
408 // Creates a receive port and registers an empty handler on that port. Just | 396 // Creates a receive port and registers an empty handler on that port. Just |
409 // the triggering of the event loop will ensure that timers are executed. | 397 // the triggering of the event loop will ensure that timers are executed. |
410 static _ignoreMessage(_) { | 398 static void _handleMessage(msg) { |
411 _messagePending = false; | 399 var pendingTimers; |
400 if (msg == _ZERO_EVENT) { | |
401 pendingTimers = _queueFromZeroEvent(); | |
402 assert(pendingTimers.length > 0); | |
403 } else { | |
404 assert(msg == _TIMEOUT_EVENT); | |
405 pendingTimers = _queueFromTimeoutEvent(); | |
406 } | |
407 _runTimers(pendingTimers); | |
412 } | 408 } |
413 | 409 |
414 static void _createTimerHandler() { | 410 static void _createTimerHandler() { |
415 assert(_receivePort == null); | 411 assert(_receivePort == null); |
416 _receivePort = new RawReceivePort(_ignoreMessage); | 412 assert(_sendPort == null); |
413 _receivePort = new RawReceivePort(_handleMessage); | |
417 _sendPort = _receivePort.sendPort; | 414 _sendPort = _receivePort.sendPort; |
418 _scheduledWakeupTime = null; | 415 _scheduledWakeupTime = null; |
419 _messagePending = false; | |
420 } | 416 } |
421 | 417 |
422 static void _shutdownTimerHandler() { | 418 static void _shutdownTimerHandler() { |
423 _receivePort.close(); | 419 _receivePort.close(); |
424 _receivePort = null; | 420 _receivePort = null; |
425 _sendPort = null; | 421 _sendPort = null; |
426 _scheduledWakeupTime = null; | 422 _scheduledWakeupTime = null; |
427 _messagePending = false; | |
428 } | 423 } |
429 | 424 |
430 // The Timer factory registered with the dart:async library by the embedder. | 425 // The Timer factory registered with the dart:async library by the embedder. |
431 static Timer _factory(int milliSeconds, | 426 static Timer _factory(int milliSeconds, |
432 void callback(Timer timer), | 427 void callback(Timer timer), |
433 bool repeating) { | 428 bool repeating) { |
434 if (repeating) { | 429 if (repeating) { |
435 return new _Timer.periodic(milliSeconds, callback); | 430 return new _Timer.periodic(milliSeconds, callback); |
436 } | 431 } |
437 return new _Timer(milliSeconds, callback); | 432 return new _Timer(milliSeconds, callback); |
438 } | 433 } |
439 } | 434 } |
440 | 435 |
441 _setupHooks() { | 436 _setupHooks() { |
442 VMLibraryHooks.timerFactory = _Timer._factory; | 437 VMLibraryHooks.timerFactory = _Timer._factory; |
443 } | 438 } |
OLD | NEW |