1 /** 2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved. 3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This module contain timer utilities. 7 */ 8 module my.timer; 9 10 import logger = std.experimental.logger; 11 import core.time : Duration, dur; 12 import std.datetime : SysTime, Clock; 13 import std.container.rbtree; 14 import std.typecons : Nullable; 15 16 @safe: 17 18 /// A collection of timers. 19 struct Timers { 20 private { 21 RedBlackTree!Timer timers; 22 Nullable!Timer front_; 23 } 24 25 void put(Timer.Action action, Duration d) { 26 timers.stableInsert(Timer(Clock.currTime + d, action)); 27 } 28 29 void put(Timer.Action action, SysTime t) { 30 timers.stableInsert(Timer(t, action)); 31 } 32 33 /// Get how long until the next timer expire. The minimum is minSleep. 34 Duration expireAt(Duration minSleep) nothrow { 35 import std.algorithm : max; 36 37 if (empty) { 38 return minSleep; 39 } 40 return max(minSleep, timers.front.expire - Clock.currTime); 41 } 42 43 /// Sleep until the next action triggers. 44 void sleep(Duration minSleep) @trusted { 45 import core.thread : Thread; 46 47 Thread.sleep(expireAt(minSleep)); 48 } 49 50 /// Sleep until the next action triggers and execute it, if there are any. 51 void tick(Duration minSleep) { 52 sleep(minSleep); 53 if (!empty) { 54 front.action(this); 55 popFront; 56 } 57 } 58 59 Timer front() pure nothrow { 60 assert(!empty, "Can't get front of an empty range"); 61 if (front_.isNull && !timers.empty) 62 front_ = timers.front; 63 return front_.get; 64 } 65 66 void popFront() { 67 assert(!empty, "Can't pop front of an empty range"); 68 if (!front_.isNull) { 69 timers.removeKey(front_.get); 70 front_.nullify; 71 } else { 72 timers.removeFront; 73 } 74 } 75 76 bool empty() pure nothrow @nogc { 77 return timers.empty && front_.isNull; 78 } 79 } 80 81 auto makeTimers() { 82 return Timers(new RedBlackTree!Timer); 83 } 84 85 /// An individual timer. 86 struct Timer { 87 private { 88 alias Action = void delegate(ref Timers); 89 SysTime expire; 90 size_t id; 91 } 92 93 Action action; 94 95 this(SysTime expire, Action action) { 96 this.expire = expire; 97 this.action = action; 98 this.id = () @trusted { return cast(size_t)&action; }(); 99 } 100 101 bool opEquals()(auto ref const typeof(this) s) const { 102 return expire == s.expire && id == s.id; 103 } 104 105 int opCmp(ref const typeof(this) rhs) const { 106 // return -1 if "this" is less than rhs, 1 if bigger and zero equal 107 if (expire < rhs.expire) 108 return -1; 109 if (expire > rhs.expire) 110 return 1; 111 if (id < rhs.id) 112 return -1; 113 if (id > rhs.id) 114 return 1; 115 return 0; 116 } 117 } 118 119 @("shall pop the first timer;") 120 unittest { 121 int timerPopped; 122 auto timers = makeTimers; 123 124 timers.put((ref Timers) { timerPopped = 42; }, 1000.dur!"msecs"); 125 timers.put((ref Timers) { timerPopped = 2; }, 2.dur!"msecs"); 126 127 timers.sleep(1.dur!"msecs"); 128 timers.front.action(timers); 129 assert(timerPopped == 2); 130 } 131 132 /// A negative duration mean it will be removed. 133 alias IntervalAction = Duration delegate(); 134 135 /// Timers that fire each interval. The intervals is adjusted by `action` and 136 /// removed if the interval is < 0. 137 auto makeInterval(ref Timers ts, IntervalAction action, Duration interval) { 138 void repeatFn(ref Timers ts) @safe { 139 const res = action(); 140 if (res >= Duration.zero) { 141 ts.put(&repeatFn, res); 142 } 143 } 144 145 ts.put(&repeatFn, interval); 146 } 147 148 @("shall remove the interval timer when it return false") 149 unittest { 150 int ticks; 151 auto timers = makeTimers; 152 153 makeInterval(timers, () { 154 ticks++; 155 if (ticks < 3) 156 return 2.dur!"msecs"; 157 return -1.dur!"seconds"; 158 }, 2.dur!"msecs"); 159 while (!timers.empty) { 160 timers.tick(Duration.zero); 161 } 162 163 assert(ticks == 3); 164 }