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 }