فهرست منبع

[WASM] Implement timers. (#7193)

* [wasm] Add server.py for those that want to experiment with streaming compilation.

* [wasm] Implement System.Threading.Timer.

Timer implementation is done in a somewhat convoluted way.

First, we change Timer to not depend on a scheduler thread.
What we do is call for the runtime to schedule a timed callback to be executed.

Next step, the runtime. We add enough plumbing to get this call to JS.

Finally, in JS we either call setTimeout or schedule it to be run on the next tick.

Oh, wait, timeouts are ignored on non-web environments? Yes, right now its only use case
is offline testing, so ain't no time to wait for all this timeouting.
Rodrigo Kumpera 7 سال پیش
والد
کامیت
0a8126c209

+ 139 - 70
mcs/class/corlib/System.Threading/Timer.cs

@@ -33,8 +33,35 @@ using System.Collections.Generic;
 using System.Collections;
 using System.Runtime.CompilerServices;
 
+
 namespace System.Threading
 {
+#if WASM
+	internal static class WasmRuntime {
+		static Dictionary<int, Action> callbacks;
+		static int next_id;
+
+		[MethodImplAttribute(MethodImplOptions.InternalCall)]
+		static extern void SetTimeout (int timeout, int id);
+
+		internal static void ScheduleTimeout (int timeout, Action action) {
+			if (callbacks == null)
+				callbacks = new Dictionary<int, Action> ();
+			int id = ++next_id;
+			callbacks [id] = action;
+			SetTimeout (timeout, id);
+		}
+
+		//XXX Keep this in sync with mini-wasm.c:mono_set_timeout_exec
+		static void TimeoutCallback (int id) {
+			var cb = callbacks [id];
+			callbacks.Remove (id);
+			cb ();
+		}
+	}
+#endif
+
+
 	[ComVisible (true)]
 	public sealed class Timer
 		: MarshalByRefObject, IDisposable
@@ -198,8 +225,62 @@ namespace System.Threading
 		sealed class Scheduler {
 			static Scheduler instance;
 			SortedList list;
+
+#if WASM
+			List<Timer> cached_new_time;
+			bool scheduled_zero;
+
+			void InitScheduler () {
+				cached_new_time = new List<Timer> (512);
+			}
+
+			void WakeupScheduler () {
+				if (!scheduled_zero) {
+					WasmRuntime.ScheduleTimeout (0, this.RunScheduler);
+					scheduled_zero = true;
+				}
+			}
+
+			void RunScheduler() {
+				scheduled_zero = false;
+				int ms_wait = RunSchedulerLoop (cached_new_time);
+				if (ms_wait >= 0) {
+					WasmRuntime.ScheduleTimeout (ms_wait, this.RunScheduler);
+					if (ms_wait == 0)
+						scheduled_zero = true;
+				}
+			}
+#else
 			ManualResetEvent changed;
 
+			void InitScheduler () {
+				changed = new ManualResetEvent (false);
+				Thread thread = new Thread (SchedulerThread);
+				thread.IsBackground = true;
+				thread.Start ();
+			}
+
+			void WakeupScheduler () {
+				changed.Set ();
+			}
+
+			void SchedulerThread ()
+			{
+				Thread.CurrentThread.Name = "Timer-Scheduler";
+				var new_time = new List<Timer> (512);
+				while (true) {
+					int ms_wait = -1;
+					lock (this) {
+						changed.Reset ();
+						ms_wait = RunSchedulerLoop (new_time);
+					}
+					// Wait until due time or a timer is changed and moves from/to the first place in the list.
+					changed.WaitOne (ms_wait);
+				}
+			}
+
+#endif
+
 			static Scheduler ()
 			{
 				instance = new Scheduler ();
@@ -211,11 +292,8 @@ namespace System.Threading
 
 			private Scheduler ()
 			{
-				changed = new ManualResetEvent (false);
 				list = new SortedList (new TimerComparer (), 1024);
-				Thread thread = new Thread (SchedulerThread);
-				thread.IsBackground = true;
-				thread.Start ();
+				InitScheduler ();
 			}
 
 			public void Remove (Timer timer)
@@ -250,7 +328,7 @@ namespace System.Threading
 					}
 				}
 				if (wake)
-					changed.Set ();
+					WakeupScheduler();
 			}
 
 			// lock held by caller
@@ -327,73 +405,64 @@ namespace System.Threading
 				timer.callback (timer.state);
 			}
 
-			void SchedulerThread ()
-			{
-				Thread.CurrentThread.Name = "Timer-Scheduler";
-				var new_time = new List<Timer> (512);
-				while (true) {
-					int ms_wait = -1;
-					long ticks = GetTimeMonotonic ();
-					lock (this) {
-						changed.Reset ();
-						//PrintList ();
-						int i;
-						int count = list.Count;
-						for (i = 0; i < count; i++) {
-							Timer timer = (Timer) list.GetByIndex (i);
-							if (timer.next_run > ticks)
-								break;
-
-							list.RemoveAt (i);
-							count--;
-							i--;
-							ThreadPool.UnsafeQueueUserWorkItem (TimerCB, timer);
-							long period = timer.period_ms;
-							long due_time = timer.due_time_ms;
-							bool no_more = (period == -1 || ((period == 0 || period == Timeout.Infinite) && due_time != Timeout.Infinite));
-							if (no_more) {
-								timer.next_run = Int64.MaxValue;
-							} else {
-								timer.next_run = GetTimeMonotonic () + TimeSpan.TicksPerMillisecond * timer.period_ms;
-								new_time.Add (timer);
-							}
-						}
-
-						// Reschedule timers with a new due time
-						count = new_time.Count;
-						for (i = 0; i < count; i++) {
-							Timer timer = new_time [i];
-							Add (timer);
-						}
-						new_time.Clear ();
-						ShrinkIfNeeded (new_time, 512);
-
-						// Shrink the list
-						int capacity = list.Capacity;
-						count = list.Count;
-						if (capacity > 1024 && count > 0 && (capacity / count) > 3)
-							list.Capacity = count * 2;
-
-						long min_next_run = Int64.MaxValue;
-						if (list.Count > 0)
-							min_next_run = ((Timer) list.GetByIndex (0)).next_run;
-
-						//PrintList ();
-						ms_wait = -1;
-						if (min_next_run != Int64.MaxValue) {
-							long diff = (min_next_run - GetTimeMonotonic ())  / TimeSpan.TicksPerMillisecond;
-							if (diff > Int32.MaxValue)
-								ms_wait = Int32.MaxValue - 1;
-							else {
-								ms_wait = (int)(diff);
-								if (ms_wait < 0)
-									ms_wait = 0;
-							}
-						}
+			int RunSchedulerLoop (List<Timer> new_time) {
+				int ms_wait = -1;
+				int i;
+				int count = list.Count;
+				long ticks = GetTimeMonotonic ();
+
+				for (i = 0; i < count; i++) {
+					Timer timer = (Timer) list.GetByIndex (i);
+					if (timer.next_run > ticks)
+						break;
+
+					list.RemoveAt (i);
+					count--;
+					i--;
+					ThreadPool.UnsafeQueueUserWorkItem (TimerCB, timer);
+					long period = timer.period_ms;
+					long due_time = timer.due_time_ms;
+					bool no_more = (period == -1 || ((period == 0 || period == Timeout.Infinite) && due_time != Timeout.Infinite));
+					if (no_more) {
+						timer.next_run = Int64.MaxValue;
+					} else {
+						timer.next_run = GetTimeMonotonic () + TimeSpan.TicksPerMillisecond * timer.period_ms;
+						new_time.Add (timer);
+					}
+				}
+
+				// Reschedule timers with a new due time
+				count = new_time.Count;
+				for (i = 0; i < count; i++) {
+					Timer timer = new_time [i];
+					Add (timer);
+				}
+				new_time.Clear ();
+				ShrinkIfNeeded (new_time, 512);
+
+				// Shrink the list
+				int capacity = list.Capacity;
+				count = list.Count;
+				if (capacity > 1024 && count > 0 && (capacity / count) > 3)
+					list.Capacity = count * 2;
+
+				long min_next_run = Int64.MaxValue;
+				if (list.Count > 0)
+					min_next_run = ((Timer) list.GetByIndex (0)).next_run;
+
+				//PrintList ();
+				ms_wait = -1;
+				if (min_next_run != Int64.MaxValue) {
+					long diff = (min_next_run - GetTimeMonotonic ())  / TimeSpan.TicksPerMillisecond;
+					if (diff > Int32.MaxValue)
+						ms_wait = Int32.MaxValue - 1;
+					else {
+						ms_wait = (int)(diff);
+						if (ms_wait < 0)
+							ms_wait = 0;
 					}
-					// Wait until due time or a timer is changed and moves from/to the first place in the list.
-					changed.WaitOne (ms_wait);
 				}
+				return ms_wait;
 			}
 
 			void ShrinkIfNeeded (List<Timer> list, int initial)

+ 5 - 0
mono/mini/mini-runtime.c

@@ -4347,6 +4347,11 @@ register_icalls (void)
 	register_icall_no_wrapper (mono_tls_set_domain, "mono_tls_set_domain", "void ptr");
 	register_icall_no_wrapper (mono_tls_set_sgen_thread_info, "mono_tls_set_sgen_thread_info", "void ptr");
 	register_icall_no_wrapper (mono_tls_set_lmf_addr, "mono_tls_set_lmf_addr", "void ptr");
+
+
+#ifdef MONO_ARCH_HAS_REGISTER_ICALL
+	mono_arch_register_icall ();
+#endif
 }
 
 MonoJitStats mono_jit_stats = {0};

+ 45 - 0
mono/mini/mini-wasm.c

@@ -1,5 +1,7 @@
 #include "mini.h"
 
+#include <emscripten.h>
+
 
 gpointer
 mono_arch_get_this_arg_from_call (mgreg_t *regs, guint8 *code)
@@ -154,6 +156,49 @@ mono_thread_state_init_from_handle (MonoThreadUnwindState *tctx, MonoThreadInfo
 }
 
 
+EMSCRIPTEN_KEEPALIVE void
+mono_set_timeout_exec (int id)
+{
+	ERROR_DECL (error);
+	MonoClass *klass = mono_class_load_from_name (mono_defaults.corlib, "System.Threading", "WasmRuntime");
+	g_assert (klass);
+
+	MonoMethod *method = mono_class_get_method_from_name (klass, "TimeoutCallback", -1);
+	g_assert (method);
+
+	gpointer params[1] = { &id };
+	MonoObject *exc = NULL;
+
+	mono_runtime_try_invoke (method, NULL, params, &exc, error);
+
+	//YES we swallow exceptions cuz there's nothing much we can do from here.
+	//FIXME Maybe call the unhandled exception function?
+	if (!is_ok (error)) {
+		printf ("timeout callback failed due to %s\n", mono_error_get_message (error));
+		mono_error_cleanup (error);
+	}
+
+	if (exc) {
+		char *type_name = mono_type_get_full_name (mono_object_get_class (exc));
+		printf ("timeout callback threw a %s\n", type_name);
+		g_free (type_name);
+	}
+}
+
+extern void mono_set_timeout (int t, int d);
+
+void
+mono_wasm_set_timeout (int timeout, int id)
+{
+	mono_set_timeout (timeout, id);
+}
+
+void
+mono_arch_register_icall (void)
+{
+	mono_add_internal_call ("System.Threading.WasmRuntime::SetTimeout", mono_wasm_set_timeout);
+}
+
 /*
 The following functions don't belong here, but are due to laziness.
 */

+ 3 - 0
mono/mini/mini-wasm.h

@@ -49,5 +49,8 @@ typedef struct {
 #define MONO_ARCH_FRAME_ALIGNMENT 16
 
 #define MONO_ARCH_INTERPRETER_SUPPORTED 1
+#define MONO_ARCH_HAS_REGISTER_ICALL 1
+
+
 
 #endif /* __MONO_MINI_WASM_H__ */  

+ 4 - 0
mono/mini/mini.h

@@ -2185,6 +2185,10 @@ gboolean  mono_arch_opcode_supported            (int opcode);
 void     mono_arch_setup_resume_sighandler_ctx  (MonoContext *ctx, gpointer func);
 gboolean  mono_arch_have_fast_tls               (void);
 
+#ifdef MONO_ARCH_HAS_REGISTER_ICALL
+void      mono_arch_register_icall              (void);
+#endif
+
 #ifdef MONO_ARCH_SOFT_FLOAT_FALLBACK
 gboolean  mono_arch_is_soft_float               (void);
 #else

+ 1 - 0
sdks/wasm/Makefile

@@ -126,6 +126,7 @@ package: build
 	cp sample.html tmp/
 	cp sample.cs tmp/
 	cp README.md tmp/
+	cp server.py tmp/
 	(cd tmp;  zip -r9 ../mono-wasm-$(shell git rev-parse --short HEAD).zip .)
 	rm -rf tmp
 

+ 3 - 0
sdks/wasm/README.md

@@ -29,6 +29,9 @@ Once that's done, Start a web server from the SDK directory (where sample.html i
 python -m SimpleHTTPServer
 ```
 
+Unfortunately, the above http server doesn't give  wasm binaries the right mime type, which disables WebAssembly stream compilation.
+The included server.py script solves this and can be used instead.
+
 Go to `locahost:8000/sample.html` and it should work.
 
 [1] https://github.com/kripken/emscripten

+ 20 - 0
sdks/wasm/library_mono.js

@@ -3,7 +3,12 @@ var MonoSupportLib = {
 	$MONO__postset: 'Module["pump_message"] = MONO.pump_message',
 	$MONO: {
 		pump_count: 0,
+		timeout_queue: [],
 		pump_message: function () {
+			while (MONO.timeout_queue.length > 0) {
+				--MONO.pump_count;
+				MONO.timeout_queue.shift()();
+			}
 			while (MONO.pump_count > 0) {
 				--MONO.pump_count;
 				Module.ccall ("mono_background_exec");
@@ -17,6 +22,21 @@ var MonoSupportLib = {
 			window.setTimeout (MONO.pump_message, 0);
 		}
 	},
+
+
+	mono_set_timeout: function (timeout, id) {
+		if (ENVIRONMENT_IS_WEB) {
+			window.setTimeout (function () {
+				Module.ccall ("mono_set_timeout_exec", 'void', [ 'number' ], [ id ]);
+			}, timeout);
+		} else {
+			++MONO.pump_count;
+			MONO.timeout_queue.push(function() {
+				Module.ccall ("mono_set_timeout_exec", 'void', [ 'number' ], [ id ]);
+			})
+		}
+		
+	}
 };
 
 autoAddDeps(MonoSupportLib, '$MONO')

+ 26 - 0
sdks/wasm/main.cs

@@ -105,6 +105,26 @@ public class Driver {
 		return fin_count < 100;
 	}
 
+	static bool timer_called;
+	static int pump_count;
+
+	static void TimerStart () {
+		Timer t = new Timer ((_) => {
+			timer_called = true;
+		});
+		t.Change (10, Timeout.Infinite);
+		latest_test_result = "EITA";
+	}
+
+	static bool TimerPump () {
+		++pump_count;
+		if (pump_count > 5 || timer_called) {
+			latest_test_result = timer_called ? "PASS" : "FAIL";
+			return false;
+		}
+
+		return true;
+	}
 
 	static int run_count;
 	public static string Send (string key, string val) {
@@ -144,6 +164,8 @@ public class Driver {
 			return DelePump ();
 		if (name == "gc")
 			return GcPump ();
+		if (name == "timer")
+			return TimerPump ();
 
 		if (testRunner == null)
 			return false;
@@ -178,6 +200,10 @@ public class Driver {
 			GcStart ();
 			return;
 		}
+		if (name == "timer") {
+			TimerStart ();
+			return;
+		}
 
 		string extra_disable = "";
 		latest_test_result = "IN-PROGRESS";

+ 14 - 0
sdks/wasm/server.py

@@ -0,0 +1,14 @@
+import SimpleHTTPServer
+import SocketServer
+
+PORT = 8000
+
+class Handler(SimpleHTTPServer.SimpleHTTPRequestHandler):
+    pass
+
+Handler.extensions_map['.wasm'] = 'application/wasm'
+
+httpd = SocketServer.TCPServer(("", PORT), Handler)
+
+print "serving at port", PORT
+httpd.serve_forever()