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 <vladimir@thecybershadow.net> 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 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 final class Timer 71 { 72 private: 73 TimerTask head; 74 TimerTask tail; 75 size_t count; 76 77 void add(TimerTask task, TimerTask start) 78 { 79 debug(TIMER_VERBOSE) stderr.writefln("Adding task %s which waits for %s.", cast(void*)task, task.delay); 80 debug(TIMER_TRACK) task.additionStackTrace = getStackTrace(); 81 82 auto now = MonoTime.currTime(); 83 84 if (start !is null) 85 assert(start.owner is this); 86 87 task.owner = this; 88 task.prev = null; 89 task.next = null; 90 task.when = now + task.delay; 91 92 TimerTask tmp = start is null ? head : start; 93 94 while (tmp !is null) 95 { 96 if (task.when < tmp.when) 97 { 98 task.next = tmp; 99 task.prev = tmp.prev; 100 if (tmp.prev) 101 tmp.prev.next = task; 102 tmp.prev = task; 103 break; 104 } 105 tmp = tmp.next; 106 } 107 108 if (tmp is null) 109 { 110 if (head !is null) 111 { 112 tail.next = task; 113 task.prev = tail; 114 tail = task; 115 } 116 else 117 { 118 head = task; 119 tail = task; 120 } 121 } 122 else 123 if (tmp is head) 124 head = task; 125 126 assert(head is null || head.prev is null); 127 assert(tail is null || tail.next is null); 128 count++; 129 } 130 131 /// Unschedule a task. 132 void remove(TimerTask task) 133 { 134 debug (TIMER_VERBOSE) stderr.writefln("Removing task %s which waits for %s.", cast(void*)task, task.delay); 135 assert(task.owner is this); 136 if (task is head) 137 { 138 if (head.next) 139 { 140 head = head.next; 141 head.prev = null; 142 debug (TIMER_VERBOSE) stderr.writefln("Removed current task, next task is waiting for %s (next at %s).", head.delay, head.when); 143 } 144 else 145 { 146 debug (TIMER_VERBOSE) stderr.writefln("Removed last task."); 147 assert(tail is task); 148 head = tail = null; 149 } 150 } 151 else 152 if (task is tail) 153 { 154 tail = task.prev; 155 if (tail) 156 tail.next = null; 157 } 158 else 159 { 160 TimerTask tmp = task.prev; 161 if (task.prev) 162 task.prev.next = task.next; 163 if (task.next) 164 { 165 task.next.prev = task.prev; 166 task.next = tmp; 167 } 168 } 169 task.owner = null; 170 task.next = task.prev = null; 171 count--; 172 } 173 174 void restart(TimerTask task) 175 { 176 TimerTask tmp; 177 178 assert(task.owner !is null, "This TimerTask is not active"); 179 assert(task.owner is this, "This TimerTask is not owned by this Timer"); 180 debug (TIMER_VERBOSE) stderr.writefln("Restarting task %s which waits for %s.", cast(void*)task, task.delay); 181 182 // Store current position, as the new position must be after it 183 tmp = task.next !is null ? task.next : task.prev; 184 185 remove(task); 186 assert(task.owner is null); 187 188 add(task, tmp); 189 assert(task.owner is this); 190 } 191 192 public: 193 /// Pretend there are no tasks scheduled. 194 bool disabled; 195 196 /// Run scheduled tasks. 197 /// Returns true if any tasks ran. 198 bool prod() 199 { 200 if (disabled) return false; 201 202 auto now = MonoTime.currTime(); 203 204 bool ran; 205 206 if (head !is null) 207 { 208 while (head !is null && head.when <= now) 209 { 210 TimerTask task = head; 211 remove(head); 212 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); 213 if (task.handleTask) 214 task.handleTask(this, task); 215 ran = true; 216 } 217 218 debug (TIMER_VERBOSE) if (head !is null) stderr.writefln("Current task is waiting for %s, %s remaining.", head.delay, head.when - now); 219 } 220 221 return ran; 222 } 223 224 /// Add a new task to the timer. 225 void add(TimerTask task) 226 { 227 debug (TIMER_VERBOSE) stderr.writefln("Adding task %s which waits for %s.", cast(void*)task, task.delay); 228 assert(task.owner is null, "This TimerTask is already active"); 229 add(task, null); 230 assert(task.owner is this); 231 assert(head !is null); 232 } 233 234 /// Return true if there are pending tasks scheduled. 235 bool isWaiting() 236 { 237 return !disabled && head !is null; 238 } 239 240 /// Return the MonoTime of the next scheduled task, or MonoTime.max if no tasks are scheduled. 241 MonoTime getNextEvent() 242 { 243 return disabled || head is null ? MonoTime.max : head.when; 244 } 245 246 /// Return the time until the first scheduled task, or Duration.max if no tasks are scheduled. 247 Duration getRemainingTime() 248 { 249 if (disabled || head is null) 250 return Duration.max; 251 252 auto now = MonoTime.currTime(); 253 254 debug(TIMER) stderr.writefln("First task is %s, due to fire in %s", cast(void*)head, head.when - now); 255 debug(TIMER_TRACK) stderr.writefln("\tCreated:\n\t\t%-(%s\n\t\t%)\n\tAdded:\n\t\t%-(%s\n\t\t%)", 256 head.creationStackTrace, head.additionStackTrace); 257 258 if (now < head.when) // "when" is in the future 259 return head.when - now; 260 else 261 return Duration.zero; 262 } 263 264 debug invariant() 265 { 266 if (head is null) 267 { 268 assert(tail is null); 269 assert(count == 0); 270 } 271 else 272 { 273 TimerTask t = cast(TimerTask)head; 274 assert(t.prev is null); 275 int n=1; 276 while (t.next) 277 { 278 assert(t.owner is this); 279 auto next = t.next; 280 assert(t is next.prev); 281 assert(t.when <= next.when); 282 t = next; 283 n++; 284 } 285 assert(t.owner is this); 286 assert(t is tail); 287 assert(count == n); 288 } 289 } 290 } 291 292 final class TimerTask 293 { 294 private: 295 Timer owner; 296 TimerTask prev; 297 TimerTask next; 298 299 MonoTime when; 300 Duration _delay; 301 302 debug(TIMER_TRACK) string[] creationStackTrace, additionStackTrace; 303 304 alias void delegate(Timer timer, TimerTask task) Handler; 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 void cancel() 322 { 323 assert(isWaiting(), "This TimerTask is not active"); 324 owner.remove(this); 325 assert(!isWaiting()); 326 } 327 328 /// Reschedule the task to run with the same delay from now. 329 void restart() 330 { 331 assert(isWaiting(), "This TimerTask is not active"); 332 owner.restart(this); 333 assert(isWaiting()); 334 } 335 336 @property Duration delay() 337 { 338 return _delay; 339 } 340 341 @property void delay(Duration delay) 342 { 343 assert(delay >= Duration.zero, "Setting TimerTask delay to a negative Duration"); 344 assert(owner is null, "Changing duration of an active TimerTask"); 345 _delay = delay; 346 } 347 348 Handler handleTask; 349 } 350 351 /// The default timer 352 Timer mainTimer; 353 354 static this() 355 { 356 mainTimer = new Timer(); 357 } 358 359 // ******************************************************************************************************************** 360 361 TimerTask setTimeout(Args...)(void delegate(Args) handler, Duration delay, Args args) 362 { 363 auto task = new TimerTask(delay, (Timer timer, TimerTask task) { handler(args); }); 364 mainTimer.add(task); 365 return task; 366 } 367 368 TimerTask setInterval(Args...)(void delegate(Args) handler, Duration delay, Args args) 369 { 370 auto task = new TimerTask(delay, (Timer timer, TimerTask task) { mainTimer.add(task); handler(args); }); 371 mainTimer.add(task); 372 return task; 373 } 374 375 void clearTimeout(TimerTask task) 376 { 377 task.cancel(); 378 } 379 380 // ******************************************************************************************************************** 381 382 /// Used to throttle actions to happen no more often than a certain period. 383 /// If last was less that span ago, return false. 384 /// Otherwise, update last to the current time and return true. 385 bool throttle(ref MonoTime last, Duration span) 386 { 387 MonoTime now = MonoTime.currTime(); 388 auto elapsed = now - last; 389 if (elapsed < span) 390 return false; 391 else 392 { 393 last = now; 394 return true; 395 } 396 } 397 398 // http://d.puremagic.com/issues/show_bug.cgi?id=7016 399 version(unittest) static import ae.utils.array; 400 401 unittest 402 { 403 import ae.utils.array; 404 import core.thread; 405 406 MonoTime[string] lastNag; 407 assert( lastNag.getOrAdd("cheese").throttle(10.msecs)); 408 assert(!lastNag.getOrAdd("cheese").throttle(10.msecs)); 409 Thread.sleep(20.msecs); 410 assert( lastNag.getOrAdd("cheese").throttle(10.msecs)); 411 assert(!lastNag.getOrAdd("cheese").throttle(10.msecs)); 412 }