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