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