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