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 a task which waits for %s.", 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 a task which waits for %s.", 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 a task which waits for %s.", 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 a task that waited for %s of %s.", now, 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 a task which waits for %s.", 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_TRACK) stderr.writefln("First timer due to fire in %s:\n\tCreated:\n\t\t%-(%s\n\t\t%)\n\tAdded:\n\t\t%-(%s\n\t\t%)", 255 head.when - now, head.creationStackTrace, head.additionStackTrace); 256 257 if (now < head.when) // "when" is in the future 258 return head.when - now; 259 else 260 return Duration.zero; 261 } 262 263 debug invariant() 264 { 265 if (head is null) 266 { 267 assert(tail is null); 268 assert(count == 0); 269 } 270 else 271 { 272 TimerTask t = cast(TimerTask)head; 273 assert(t.prev is null); 274 int n=1; 275 while (t.next) 276 { 277 assert(t.owner is this); 278 auto next = t.next; 279 assert(t is next.prev); 280 assert(t.when <= next.when); 281 t = next; 282 n++; 283 } 284 assert(t.owner is this); 285 assert(t is tail); 286 assert(count == n); 287 } 288 } 289 } 290 291 final class TimerTask 292 { 293 private: 294 Timer owner; 295 TimerTask prev; 296 TimerTask next; 297 298 MonoTime when; 299 Duration _delay; 300 301 debug(TIMER_TRACK) string[] creationStackTrace, additionStackTrace; 302 303 alias void delegate(Timer timer, TimerTask task) Handler; 304 305 public: 306 this(Duration delay, Handler handler = null) 307 { 308 assert(delay >= Duration.zero, "Creating TimerTask with a negative Duration"); 309 _delay = delay; 310 handleTask = handler; 311 debug(TIMER_TRACK) creationStackTrace = getStackTrace(); 312 } 313 314 /// Return whether the task is scheduled to run on a Timer. 315 bool isWaiting() 316 { 317 return owner !is null; 318 } 319 320 void cancel() 321 { 322 assert(isWaiting(), "This TimerTask is not active"); 323 owner.remove(this); 324 assert(!isWaiting()); 325 } 326 327 /// Reschedule the task to run with the same delay from now. 328 void restart() 329 { 330 assert(isWaiting(), "This TimerTask is not active"); 331 owner.restart(this); 332 assert(isWaiting()); 333 } 334 335 @property Duration delay() 336 { 337 return _delay; 338 } 339 340 @property void delay(Duration delay) 341 { 342 assert(delay >= Duration.zero, "Setting TimerTask delay to a negative Duration"); 343 assert(owner is null, "Changing duration of an active TimerTask"); 344 _delay = delay; 345 } 346 347 Handler handleTask; 348 } 349 350 /// The default timer 351 Timer mainTimer; 352 353 static this() 354 { 355 mainTimer = new Timer(); 356 } 357 358 // ******************************************************************************************************************** 359 360 TimerTask setTimeout(Args...)(void delegate(Args) handler, Duration delay, Args args) 361 { 362 auto task = new TimerTask(delay, (Timer timer, TimerTask task) { handler(args); }); 363 mainTimer.add(task); 364 return task; 365 } 366 367 TimerTask setInterval(Args...)(void delegate(Args) handler, Duration delay, Args args) 368 { 369 auto task = new TimerTask(delay, (Timer timer, TimerTask task) { mainTimer.add(task); handler(args); }); 370 mainTimer.add(task); 371 return task; 372 } 373 374 void clearTimeout(TimerTask task) 375 { 376 task.cancel(); 377 } 378 379 // ******************************************************************************************************************** 380 381 /// Used to throttle actions to happen no more often than a certain period. 382 /// If last was less that span ago, return false. 383 /// Otherwise, update last to the current time and return true. 384 bool throttle(ref MonoTime last, Duration span) 385 { 386 MonoTime now = MonoTime.currTime(); 387 auto elapsed = now - last; 388 if (elapsed < span) 389 return false; 390 else 391 { 392 last = now; 393 return true; 394 } 395 } 396 397 // http://d.puremagic.com/issues/show_bug.cgi?id=7016 398 version(unittest) static import ae.utils.array; 399 400 unittest 401 { 402 import ae.utils.array; 403 import core.thread; 404 405 MonoTime[string] lastNag; 406 assert( lastNag.getOrAdd("cheese").throttle(10.msecs)); 407 assert(!lastNag.getOrAdd("cheese").throttle(10.msecs)); 408 Thread.sleep(20.msecs); 409 assert( lastNag.getOrAdd("cheese").throttle(10.msecs)); 410 assert(!lastNag.getOrAdd("cheese").throttle(10.msecs)); 411 }