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