1 /++
2  + Authors: Stephan Dilly, lastname dot firstname at gmail dot com
3  + Copyright: MIT
4  +/
5 module unecht.core.fibers;
6 
7 public import core.thread : Fiber;
8 import core.thread : Thread;
9 
10 import derelict.util.system;
11 
12 import std.datetime : Duration;
13 
14 /// function type that can be used as a fiber
15 alias UEFiberFunc = void function();
16 /// delegate type that can be used as a fiber
17 alias UEFiberDelegate = void delegate();
18 
19 /++
20  + acts like a std.thread.Fiber - adds child Fiber member to enable yield on child fibers (=wait for child fiber to finish)
21  +
22  + See_Also:
23  +	`UEFibers`
24  +/
25 final class UEFiber : Fiber
26 {
27 	/// pointer to child Fiber
28 	private Fiber child;
29 
30 	//TODO: make nothrow once we loose the dmd<2067 compat
31 	/// c'tor
32 	public this(T)(T fn) if (is(T == UEFiberFunc) || is(T == UEFiberDelegate))
33 	{
34 		super(fn);
35 
36 		initParent();
37 	}
38 
39 	/// resets the Fiber to be reused with another method
40 	public void reset(T)(T fn) if (is(T == UEFiberFunc) || is(T == UEFiberDelegate))
41 	{
42 		super.reset(fn);
43 
44 		initParent();
45 	}
46 
47 	/// usage of `reset`
48 	@system unittest
49 	{
50 		bool res;
51 		auto fiber = new UEFiber(cast(UEFiberDelegate) { res = false; });
52 		fiber.reset(cast(UEFiberDelegate) { res = true; });
53 		fiber.call();
54 		assert(res);
55 	}
56 
57 	/// initializes parents child property to point to this
58 	private void initParent()
59 	{
60 		UEFiber parent = cast(UEFiber) Fiber.getThis();
61 		if (parent)
62 		{
63 			assert(parent.child is null);
64 			parent.child = this;
65 		}
66 	}
67 
68 	/// will call the Fiber in a nothrow way, so it will return any thrown exception inside without automatic rethrow
69 	public void safeCall()
70 	{
71 		static if (__VERSION__ >= 2067)
72 			auto e = call(Fiber.Rethrow.no);
73 		else
74 			auto e = call(false);
75 
76 		if (e)
77 		{
78 			static if (__VERSION__ < 2067)
79 			{
80 				import std.stdio : writefln;
81 
82 				writefln("error: %s", e);
83 			}
84 			else
85 			{
86 				import std.experimental.logger : errorf;
87 
88 				errorf("error: %s", e.toString());
89 			}
90 		}
91 	}
92 }
93 
94 ///
95 @system unittest
96 {
97 	int i = 0;
98 	auto fiber = new UEFiber(cast(UEFiberDelegate) { i++; Fiber.yield; i++; });
99 
100 	fiber.call(); // executes until yield
101 	assert(i == 1);
102 
103 	fiber.call(); // finishes
104 	assert(i == 2);
105 }
106 
107 /++
108  + returns a function object that can be used to wait in a `UEFiber` for a certain amount of time
109  +
110  + Params:
111  +	d =	a string that will be mixed in and has to evaluate to a `DateTime`
112  +
113  + Returns: A function object
114  +
115  + See_Also:
116  +	`UEFiber`, `UEFiberFunc`
117  +
118  + Examples:
119  + ---
120  + UEFibers.yield(waitFiber!"2.seconds");
121  + ---
122  +/
123 UEFiberFunc waitFiber(string d)()
124 {
125 	import std.datetime : Clock, minutes, msecs, nsecs, seconds;
126 
127 	return {
128 		auto targetTime = Clock.currTime + mixin(d);
129 		while (Clock.currTime < targetTime)
130 			Fiber.yield;
131 	};
132 }
133 
134 /++
135  + UEFibers acts as a container for fibers.
136  + It manages reusing `UEFiber` objects after they are finished.
137  + `startFiber` first tries to find a finished `UEFiber` and only otherwise allocates
138  +
139  + See_Also:
140  +	`UEFiber`
141  +/
142 struct UEFibers
143 {
144 	private static UEFiber[] fibers;
145 
146 	/// start a function as a fiber
147 	public static void startFiber(UEFiberFunc func)
148 	{
149 		import std.functional : toDelegate;
150 
151 		startFiber(func.toDelegate());
152 	}
153 
154 	/// ditto
155 	public static void startFiber(UEFiberDelegate func)
156 	{
157 		UEFiber newFiber = findFreeFiber();
158 
159 		if (newFiber)
160 		{
161 			newFiber.reset(func);
162 		}
163 		else
164 		{
165 			newFiber = new UEFiber(func);
166 			fibers ~= newFiber;
167 		}
168 
169 		newFiber.safeCall();
170 	}
171 
172 	//TODO: when runFibers moves TERM'd fibers to end then iterate over array in reverse
173 	private static UEFiber findFreeFiber()
174 	{
175 		foreach (f; fibers)
176 		{
177 			if (f.state == Fiber.State.TERM)
178 				return f;
179 		}
180 
181 		return null;
182 	}
183 
184 	/// yield the current fiber until func is finished running
185 	public static yield(UEFiberDelegate func)
186 	{
187 		assert(Fiber.getThis());
188 
189 		startFiber(func);
190 
191 		Fiber.yield();
192 	}
193 
194 	/// ditto
195 	public static yield(UEFiberFunc func)
196 	{
197 		import std.functional : toDelegate;
198 
199 		yield(func.toDelegate());
200 	}
201 
202 	//TODO: move Fibers in TERM state to the end of array
203 	/++
204 	 + Calls all running fibers that do not wait for a child to finish
205 	 +
206 	 + Note: Do not count on the order of execution of fibers, they could be reordered
207 	 +/
208 	public static void runFibers()
209 	{
210 		foreach (f; fibers)
211 		{
212 			if (f.state != Fiber.State.TERM)
213 			{
214 				if (!f.child)
215 				{
216 					f.safeCall();
217 				}
218 				else
219 				{
220 					if (f.child.state == Fiber.State.TERM)
221 					{
222 						f.child = null;
223 						f.safeCall();
224 					}
225 				}
226 			}
227 		}
228 	}
229 }
230 
231 /// basic yield
232 @system unittest
233 {
234 	int i = 0;
235 	// reset
236 	UEFibers.fibers.length = 0;
237 
238 	UEFibers.startFiber({ i++; Fiber.yield(); i++; });
239 
240 	assert(i == 1);
241 	UEFibers.runFibers();
242 	assert(i == 2);
243 }
244 
245 /// fiber object reuse
246 @system unittest
247 {
248 	int i = 0;
249 	// reset
250 	UEFibers.fibers.length = 0;
251 
252 	UEFibers.startFiber({ i++; });
253 	UEFibers.startFiber({ i++; });
254 
255 	assert(UEFibers.fibers.length == 1);
256 	assert(i == 2);
257 }
258 
259 @system unittest
260 {
261 	// test yield on other fibers
262 
263 	string log;
264 
265 	UEFibers.fibers.length = 0;
266 
267 	auto f1 = { log ~= 'b'; Fiber.yield(); log ~= 'c'; };
268 
269 	UEFibers.startFiber({ log ~= 'a'; UEFibers.yield(f1); log ~= 'd'; });
270 
271 	assert(UEFibers.fibers.length == 2);
272 	assert(log == "ab", log);
273 
274 	UEFibers.runFibers();
275 
276 	assert(log == "abc");
277 
278 	UEFibers.runFibers();
279 
280 	assert(log == "abcd");
281 
282 	UEFibers.startFiber({ log ~= 'e'; });
283 
284 	assert(log == "abcde");
285 
286 	assert(UEFibers.fibers.length == 2);
287 }
288 
289 @system unittest
290 {
291 	// test reusing fibers
292 
293 	int i;
294 
295 	UEFibers.fibers.length = 0;
296 
297 	auto f1 = { Fiber.yield(); i++; };
298 
299 	foreach (j; 0 .. 5)
300 		UEFibers.startFiber(f1);
301 
302 	assert(UEFibers.fibers.length == 5);
303 	assert(i == 0);
304 
305 	UEFibers.runFibers();
306 
307 	assert(i == 5);
308 
309 	foreach (j; 0 .. 5)
310 		UEFibers.startFiber(f1);
311 
312 	assert(UEFibers.fibers.length == 5);
313 	assert(i == 5);
314 
315 	UEFibers.runFibers();
316 
317 	assert(i == 10);
318 
319 	assert(UEFibers.fibers.length == 5);
320 }
321 
322 @system unittest
323 {
324 	// test wait fiber
325 
326 	import std.datetime : Clock, msecs;
327 
328 	UEFibers.fibers.length = 0;
329 	bool run = false;
330 
331 	immutable now = Clock.currTime;
332 	UEFibers.startFiber({ UEFibers.yield(waitFiber!"1.msecs"); run = true; });
333 
334 	int cycles = 0;
335 	while (!run)
336 	{
337 		UEFibers.runFibers();
338 		cycles++;
339 	}
340 
341 	assert(Clock.currTime - now >= 1.msecs);
342 	assert(cycles > 10, "this should at least take 10 cylces");
343 }
344 
345 @system unittest
346 {
347 	// test bug about resetting existing fibers
348 
349 	import std.datetime : Clock, msecs;
350 
351 	UEFibers.fibers.length = 0;
352 	auto run = 0;
353 
354 	immutable now = Clock.currTime;
355 	UEFibers.startFiber({
356 		foreach (i; 0 .. 5)
357 		{
358 			UEFibers.yield(waitFiber!"1.msecs");
359 
360 			run++;
361 		}
362 	});
363 
364 	while (run < 5)
365 	{
366 		UEFibers.runFibers();
367 	}
368 
369 	assert(UEFibers.fibers.length == 2);
370 	assert(Clock.currTime - now >= 5.msecs);
371 }