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