1 /** 2 * Management of timed events. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Simon Arlott 12 * Vladimir Panteleev <ae@cy.md> 13 */ 14 15 module ae.sys.timing; 16 17 public import core.time; 18 19 import std.exception; 20 21 /// Prototype for core.time.MonoTime (TickDuration replacement). 22 /// See https://github.com/D-Programming-Language/druntime/pull/711 23 static if (!is(core.time.MonoTime)) 24 { 25 deprecated struct MonoTime 26 { 27 enum max = MonoTime(ulong.max); 28 29 static MonoTime currTime() 30 { 31 return MonoTime(TickDuration.currSystemTick().hnsecs); 32 } 33 34 MonoTime opBinary(string op)(Duration d) const 35 if (op == "+") 36 { 37 return MonoTime(hnsecs + d.total!"hnsecs"); 38 } 39 40 Duration opBinary(string op)(MonoTime o) const 41 if (op == "-") 42 { 43 return dur!"hnsecs"(cast(long)(hnsecs - o.hnsecs)); 44 } 45 46 int opCmp(MonoTime o) const { return hnsecs == o.hnsecs ? 0 : hnsecs > o.hnsecs ? 1 : -1; } 47 48 private: 49 ulong hnsecs; 50 } 51 } 52 53 unittest 54 { 55 assert(MonoTime.init < MonoTime.max); 56 } 57 58 // TODO: allow customization of timing mechanism (alternatives to TickDuration)? 59 60 debug(TIMER) import std.stdio : stderr; 61 debug(TIMER_TRACK) import std.stdio : stderr; 62 debug(TIMER_TRACK) import ae.utils.exception; 63 64 static this() 65 { 66 // Bug 6631 67 //enforce(TickDuration.ticksPerSec != 0, "TickDuration not available on this system"); 68 } 69 70 /// Manages and schedules a list of timer tasks. 71 final class Timer 72 { 73 private: 74 TimerTask head; 75 TimerTask tail; 76 size_t count; 77 78 void add(TimerTask task, TimerTask start) 79 { 80 debug(TIMER_VERBOSE) stderr.writefln("Adding task %s which waits for %s.", cast(void*)task, task.delay); 81 debug(TIMER_TRACK) task.additionStackTrace = getStackTrace(); 82 83 auto now = MonoTime.currTime(); 84 85 if (start !is null) 86 assert(start.owner is this); 87 88 task.owner = this; 89 task.prev = null; 90 task.next = null; 91 task.when = now + task.delay; 92 93 TimerTask tmp = start is null ? head : start; 94 95 while (tmp !is null) 96 { 97 if (task.when < tmp.when) 98 { 99 task.next = tmp; 100 task.prev = tmp.prev; 101 if (tmp.prev) 102 tmp.prev.next = task; 103 tmp.prev = task; 104 break; 105 } 106 tmp = tmp.next; 107 } 108 109 if (tmp is null) 110 { 111 if (head !is null) 112 { 113 tail.next = task; 114 task.prev = tail; 115 tail = task; 116 } 117 else 118 { 119 head = task; 120 tail = task; 121 } 122 } 123 else 124 if (tmp is head) 125 head = task; 126 127 assert(head is null || head.prev is null); 128 assert(tail is null || tail.next is null); 129 count++; 130 } 131 132 /// Unschedule a task. 133 void remove(TimerTask task) 134 { 135 debug (TIMER_VERBOSE) stderr.writefln("Removing task %s which waits for %s.", cast(void*)task, task.delay); 136 assert(task.owner is this); 137 if (task is head) 138 { 139 if (head.next) 140 { 141 head = head.next; 142 head.prev = null; 143 debug (TIMER_VERBOSE) stderr.writefln("Removed current task, next task is waiting for %s (next at %s).", head.delay, head.when); 144 } 145 else 146 { 147 debug (TIMER_VERBOSE) stderr.writefln("Removed last task."); 148 assert(tail is task); 149 head = tail = null; 150 } 151 } 152 else 153 if (task is tail) 154 { 155 tail = task.prev; 156 if (tail) 157 tail.next = null; 158 } 159 else 160 { 161 TimerTask tmp = task.prev; 162 if (task.prev) 163 task.prev.next = task.next; 164 if (task.next) 165 { 166 task.next.prev = task.prev; 167 task.next = tmp; 168 } 169 } 170 task.owner = null; 171 task.next = task.prev = null; 172 count--; 173 } 174 175 void restart(TimerTask task) 176 { 177 TimerTask tmp; 178 179 assert(task.owner !is null, "This TimerTask is not active"); 180 assert(task.owner is this, "This TimerTask is not owned by this Timer"); 181 debug (TIMER_VERBOSE) stderr.writefln("Restarting task %s which waits for %s.", cast(void*)task, task.delay); 182 183 // Store current position, as the new position must be after it 184 tmp = task.next !is null ? task.next : task.prev; 185 186 remove(task); 187 assert(task.owner is null); 188 189 add(task, tmp); 190 assert(task.owner is this); 191 } 192 193 public: 194 /// Pretend there are no tasks scheduled. 195 bool disabled; 196 197 /// Run scheduled tasks. 198 /// Returns true if any tasks ran. 199 bool prod() 200 { 201 if (disabled) return false; 202 203 auto now = MonoTime.currTime(); 204 205 bool ran; 206 207 if (head !is null) 208 { 209 while (head !is null && head.when <= now) 210 { 211 TimerTask task = head; 212 remove(head); 213 debug (TIMER) stderr.writefln("%s: Firing task %s that waited for %s of %s.", now, cast(void*)task, task.delay + (now - task.when), task.delay); 214 if (task.handleTask) 215 task.handleTask(this, task); 216 ran = true; 217 } 218 219 debug (TIMER_VERBOSE) if (head !is null) stderr.writefln("Current task is waiting for %s, %s remaining.", head.delay, head.when - now); 220 } 221 222 return ran; 223 } 224 225 /// Add a new task to the timer. 226 void add(TimerTask task) 227 { 228 debug (TIMER_VERBOSE) stderr.writefln("Adding task %s which waits for %s.", cast(void*)task, task.delay); 229 assert(task.owner is null, "This TimerTask is already active"); 230 add(task, null); 231 assert(task.owner is this); 232 assert(head !is null); 233 } 234 235 /// Return true if there are pending tasks scheduled. 236 bool isWaiting() 237 { 238 return !disabled && head !is null; 239 } 240 241 /// Return the MonoTime of the next scheduled task, or MonoTime.max if no tasks are scheduled. 242 MonoTime getNextEvent() 243 { 244 return disabled || head is null ? MonoTime.max : head.when; 245 } 246 247 /// Return the time until the first scheduled task, or Duration.max if no tasks are scheduled. 248 Duration getRemainingTime() 249 { 250 if (disabled || head is null) 251 return Duration.max; 252 253 auto now = MonoTime.currTime(); 254 255 debug(TIMER) stderr.writefln("First task is %s, due to fire in %s", cast(void*)head, head.when - now); 256 debug(TIMER_TRACK) stderr.writefln("\tCreated:\n\t\t%-(%s\n\t\t%)\n\tAdded:\n\t\t%-(%s\n\t\t%)", 257 head.creationStackTrace, head.additionStackTrace); 258 259 if (now < head.when) // "when" is in the future 260 return head.when - now; 261 else 262 return Duration.zero; 263 } 264 265 debug invariant() 266 { 267 if (head is null) 268 { 269 assert(tail is null); 270 assert(count == 0); 271 } 272 else 273 { 274 TimerTask t = cast(TimerTask)head; 275 assert(t.prev is null); 276 int n=1; 277 while (t.next) 278 { 279 assert(t.owner is this); 280 auto next = t.next; 281 assert(t is next.prev); 282 assert(t.when <= next.when); 283 t = next; 284 n++; 285 } 286 assert(t.owner is this); 287 assert(t is tail); 288 assert(count == n); 289 } 290 } 291 } 292 293 /// Represents a task that needs to run at some point in the future. 294 final class TimerTask 295 { 296 private: 297 Timer owner; 298 TimerTask prev; 299 TimerTask next; 300 301 MonoTime when; 302 Duration _delay; 303 304 debug(TIMER_TRACK) string[] creationStackTrace, additionStackTrace; 305 306 public: 307 this(Duration delay, Handler handler = null) 308 { 309 assert(delay >= Duration.zero, "Creating TimerTask with a negative Duration"); 310 _delay = delay; 311 handleTask = handler; 312 debug(TIMER_TRACK) creationStackTrace = getStackTrace(); 313 } /// 314 315 /// Return whether the task is scheduled to run on a Timer. 316 bool isWaiting() 317 { 318 return owner !is null; 319 } 320 321 /// Remove this task from the scheduler. 322 void cancel() 323 { 324 assert(isWaiting(), "This TimerTask is not active"); 325 owner.remove(this); 326 assert(!isWaiting()); 327 } 328 329 /// Reschedule the task to run with the same delay from now. 330 void restart() 331 { 332 assert(isWaiting(), "This TimerTask is not active"); 333 owner.restart(this); 334 assert(isWaiting()); 335 } 336 337 /// The duration that this task is scheduled to run after. 338 /// Changing the delay is only allowed for inactive tasks. 339 @property Duration delay() 340 { 341 return _delay; 342 } 343 344 /// ditto 345 @property void delay(Duration delay) 346 { 347 assert(delay >= Duration.zero, "Setting TimerTask delay to a negative Duration"); 348 assert(owner is null, "Changing duration of an active TimerTask"); 349 _delay = delay; 350 } 351 352 /// Called when this timer task fires. 353 alias Handler = void delegate(Timer timer, TimerTask task); 354 Handler handleTask; /// ditto 355 } 356 357 /// The default timer 358 Timer mainTimer; 359 360 static this() 361 { 362 mainTimer = new Timer(); 363 } 364 365 // ******************************************************************************************************************** 366 367 /// Convenience function to schedule and return a `TimerTask` that runs `handler` after `delay` once. 368 TimerTask setTimeout(Args...)(void delegate(Args) handler, Duration delay, Args args) 369 { 370 auto task = new TimerTask(delay, (Timer timer, TimerTask task) { handler(args); }); 371 mainTimer.add(task); 372 return task; 373 } 374 375 /// Convenience function to schedule and return a `TimerTask` that runs `handler` after `delay` repeatedly. 376 TimerTask setInterval(Args...)(void delegate(Args) handler, Duration delay, Args args) 377 { 378 auto task = new TimerTask(delay, (Timer timer, TimerTask task) { mainTimer.add(task); handler(args); }); 379 mainTimer.add(task); 380 return task; 381 } 382 383 /// Calls `task.cancel`. 384 void clearTimeout(TimerTask task) 385 { 386 task.cancel(); 387 } 388 389 // ******************************************************************************************************************** 390 391 /// Used to throttle actions to happen no more often than a certain period. 392 /// If last was less that span ago, return false. 393 /// Otherwise, update last to the current time and return true. 394 bool throttle(ref MonoTime last, Duration span) 395 { 396 MonoTime now = MonoTime.currTime(); 397 auto elapsed = now - last; 398 if (elapsed < span) 399 return false; 400 else 401 { 402 last = now; 403 return true; 404 } 405 } 406 407 // https://issues.dlang.org/show_bug.cgi?id=7016 408 version(unittest) static import ae.utils.array; 409 410 unittest 411 { 412 import ae.utils.array; 413 import core.thread; 414 415 MonoTime[string] lastNag; 416 assert( lastNag.getOrAdd("cheese").throttle(10.msecs)); 417 assert(!lastNag.getOrAdd("cheese").throttle(10.msecs)); 418 Thread.sleep(20.msecs); 419 assert( lastNag.getOrAdd("cheese").throttle(10.msecs)); 420 assert(!lastNag.getOrAdd("cheese").throttle(10.msecs)); 421 }