diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System.Private.Runtime.InteropServices.JavaScript.Tests.csproj b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System.Private.Runtime.InteropServices.JavaScript.Tests.csproj index fe9348f4678320..8c552fccec58c0 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System.Private.Runtime.InteropServices.JavaScript.Tests.csproj +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System.Private.Runtime.InteropServices.JavaScript.Tests.csproj @@ -17,6 +17,14 @@ + + + + + + + + diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/Simple/SimpleTest.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/Simple/SimpleTest.cs new file mode 100644 index 00000000000000..18318330ffcf67 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/Simple/SimpleTest.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace System.Runtime.InteropServices.JavaScript.Tests +{ + public static class SimpleTest + { + public static async Task Test() + { + var tests = new List>(); + tests.Add(TimerTests.T0_NoTimer); + tests.Add(TimerTests.T1_OneTimer); + tests.Add(TimerTests.T2_SecondTimerEarlier); + tests.Add(TimerTests.T3_SecondTimerLater); + tests.Add(TimerTests.T5_FiveTimers); + + try + { + Console.WriteLine("SimpleMain start test!"); + var failures = 0; + var failureNames = new List(); + foreach (var test in tests) + { + var failed = await RunTest(test); + if (failed != null) + { + failureNames.Add(failed); + failures++; + } + } + + foreach (var failure in failureNames) + { + Console.WriteLine(failure); + } + Console.WriteLine($"{Environment.NewLine}=== TEST EXECUTION SUMMARY ==={Environment.NewLine}Total: {tests.Count}, Failed: {failures}"); + return failures; + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + return -1; + } + } + + private static async Task RunTest(Func action) + { + try + { + Console.WriteLine("[STRT] " + action.Method.Name); + await action(); + Console.WriteLine("[DONE] " + action.Method.Name); + return null; + } + catch (Exception ex) + { + var message="[FAIL] "+action.Method.Name + " " + ex.Message; + Console.WriteLine(message); + return message; + } + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/Simple/TimerTests.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/Simple/TimerTests.cs new file mode 100644 index 00000000000000..3f7f7ac8add77c --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/Simple/TimerTests.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.Runtime.InteropServices.JavaScript.Tests +{ + public static class TimerTests + { + static JSObject _timersHelper = (JSObject)Runtime.GetGlobalObject("timersHelper"); + static Function _installWrapper = (Function)_timersHelper.GetObjectProperty("install"); + static Function _getRegisterCount = (Function)_timersHelper.GetObjectProperty("getRegisterCount"); + static Function _getHitCount = (Function)_timersHelper.GetObjectProperty("getHitCount"); + static Function _cleanupWrapper = (Function)_timersHelper.GetObjectProperty("cleanup"); + + static public async Task T0_NoTimer() + { + try + { + _installWrapper.Call(); + + var setCounter = (int)_getRegisterCount.Call(); + Assert.Equal(0, setCounter); + } + finally + { + await WaitForCleanup(); + } + } + + static public async Task T1_OneTimer() + { + int wasCalled = 0; + Timer? timer = null; + try + { + _installWrapper.Call(); + + timer = new Timer((_) => + { + Console.WriteLine("In timer"); + wasCalled++; + }, null, 10, 0); + + var setCounter = (int)_getRegisterCount.Call(); + Assert.True(0 == wasCalled, $"wasCalled: {wasCalled}"); + Assert.True(1 == setCounter, $"setCounter: {setCounter}"); + } + finally + { + await WaitForCleanup(); + Assert.True(1 == wasCalled, $"wasCalled: {wasCalled}"); + timer?.Dispose(); + } + } + + static public async Task T2_SecondTimerEarlier() + { + int wasCalled = 0; + Timer? timer1 = null; + Timer? timer2 = null; + try + { + _installWrapper.Call(); + + timer1 = new Timer((_) => + { + Console.WriteLine("In timer1"); + wasCalled++; + }, null, 10, 0); + timer2 = new Timer((_) => + { + Console.WriteLine("In timer2"); + wasCalled++; + }, null, 5, 0); + + var setCounter = (int)_getRegisterCount.Call(); + Assert.True(2 == setCounter, $"setCounter: {setCounter}"); + Assert.True(0 == wasCalled, $"wasCalled: {wasCalled}"); + + } + finally + { + await WaitForCleanup(); + Assert.True(2 == wasCalled, $"wasCalled: {wasCalled}"); + timer1?.Dispose(); + timer2?.Dispose(); + } + } + + static public async Task T3_SecondTimerLater() + { + int wasCalled = 0; + Timer? timer1 = null; + Timer? timer2 = null; + try + { + _installWrapper.Call(); + + timer1 = new Timer((_) => + { + Console.WriteLine("In timer1"); + wasCalled++; + }, null, 10, 0); + timer2 = new Timer((_) => + { + Console.WriteLine("In timer2"); + wasCalled++; + }, null, 20, 0); + + var setCounter = (int)_getRegisterCount.Call(); + Assert.True(0 == wasCalled, $"wasCalled: {wasCalled}"); + Assert.True(1 == setCounter, $"setCounter: {setCounter}"); + } + finally + { + await WaitForCleanup(); + Assert.True(2 == wasCalled, $"wasCalled: {wasCalled}"); + timer1?.Dispose(); + timer2?.Dispose(); + } + } + + static public async Task T5_FiveTimers() + { + int wasCalled = 0; + Timer? timer1 = null; + Timer? timer2 = null; + Timer? timer3 = null; + Timer? timer4 = null; + Timer? timer5 = null; + try + { + _installWrapper.Call(); + + timer1 = new Timer((_) => + { + Console.WriteLine("In timer1"); + wasCalled++; + }, null, 800, 0); + timer2 = new Timer((_) => + { + Console.WriteLine("In timer2"); + wasCalled++; + }, null, 600, 0); + timer3 = new Timer((_) => + { + Console.WriteLine("In timer3"); + wasCalled++; + }, null, 400, 0); + timer4 = new Timer((_) => + { + Console.WriteLine("In timer4"); + wasCalled++; + }, null, 200, 0); + timer5 = new Timer((_) => + { + Console.WriteLine("In timer5"); + wasCalled++; + }, null, 000, 0); + + var setCounter = (int)_getRegisterCount.Call(); + Assert.True(0 == wasCalled, $"wasCalled: {wasCalled}"); + Assert.True(5 == setCounter, $"setCounter: {setCounter}"); + } + finally + { + await WaitForCleanup(); + var hitCounter = (int)_getHitCount.Call(); + var setCounter = (int)_getRegisterCount.Call(); + Assert.True(5 == wasCalled, $"wasCalled: {wasCalled}"); + Assert.True(8 == hitCounter, $"hitCounter: {hitCounter}"); + Assert.True(12 == setCounter, $"setCounter: {setCounter}"); + timer1?.Dispose(); + timer2?.Dispose(); + timer3?.Dispose(); + timer4?.Dispose(); + timer5?.Dispose(); + } + } + + static private async Task WaitForCleanup() + { + Console.WriteLine("wait for cleanup begin"); + await Task.Delay(1000); + _cleanupWrapper.Call(); + Console.WriteLine("wait for cleanup end"); + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/simple.html b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/simple.html new file mode 100644 index 00000000000000..59acbcfcd1cd1e --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/simple.html @@ -0,0 +1,14 @@ + + + + + + TESTS + + + + + + + + \ No newline at end of file diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/simple.js b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/simple.js new file mode 100644 index 00000000000000..bd9f900aafebf6 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/simple.js @@ -0,0 +1,79 @@ +class TimersHelper { + install() { + const measuredCallbackName = "mono_wasm_set_timeout_exec"; + globalThis.registerCounter = 0; + globalThis.hitCounter = 0; + console.log("install") + if (!globalThis.originalSetTimeout) { + globalThis.originalSetTimeout = globalThis.setTimeout; + } + globalThis.setTimeout = (cb, time) => { + var start = Date.now().valueOf(); + if (cb.name === measuredCallbackName) { + globalThis.registerCounter++; + console.log(`registerCounter: ${globalThis.registerCounter} now:${start} delay:${time}`) + } + return globalThis.originalSetTimeout(() => { + if (cb.name === measuredCallbackName) { + var hit = Date.now().valueOf(); + globalThis.hitCounter++; + var delta = hit - start; + console.log(`hitCounter: ${globalThis.hitCounter} now:${hit} delay:${time} delta:${delta}`) + } + cb(); + }, time); + }; + } + + getRegisterCount() { + console.log(`registerCounter: ${globalThis.registerCounter} `) + return globalThis.registerCounter; + } + + getHitCount() { + console.log(`hitCounter: ${globalThis.hitCounter} `) + return globalThis.hitCounter; + } + + cleanup() { + console.log(`cleanup registerCounter: ${globalThis.registerCounter} hitCounter: ${globalThis.hitCounter} `) + globalThis.setTimeout = globalThis.originalSetTimeout; + } +} + +globalThis.timersHelper = new TimersHelper(); + +var Module = { + + config: null, + + preInit: async function() { + await MONO.mono_wasm_load_config("./mono-config.json"); // sets Module.config implicitly + }, + + // Called when the runtime is initialized and wasm is ready + onRuntimeInitialized: function () { + if (!Module.config || Module.config.error) { + console.log("No config found"); + return; + } + + Module.config.loaded_cb = function () { + try { + BINDING.call_static_method("[System.Private.Runtime.InteropServices.JavaScript.Tests] System.Runtime.InteropServices.JavaScript.Tests.SimpleTest:Test", []); + } catch (error) { + throw (error); + } + }; + Module.config.fetch_file_cb = function (asset) { + return fetch (asset, { credentials: 'same-origin' }); + } + + try + { + MONO.mono_load_runtime_and_bcl_args (Module.config); + } catch (error) { + throw(error); + } + }, +}; diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.Mono.cs index 5ad4ea2e23cead..4187cfcb829089 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.Mono.cs @@ -17,7 +17,9 @@ internal partial class TimerQueue { private static List? s_scheduledTimers; private static List? s_scheduledTimersToFire; + private static long s_shortestDueTimeMs = long.MaxValue; + // this means that it's in the s_scheduledTimers collection, not that it's the one which would run on the next TimeoutCallback private bool _isScheduled; private long _scheduledDueTimeMs; @@ -27,24 +29,25 @@ private TimerQueue(int id) [DynamicDependency("TimeoutCallback")] // The id argument is unused in netcore + // This replaces the current pending setTimeout with shorter one [MethodImplAttribute(MethodImplOptions.InternalCall)] private static extern void SetTimeout(int timeout, int id); // Called by mini-wasm.c:mono_set_timeout_exec private static void TimeoutCallback() { - int shortestWaitDurationMs = PumpTimerQueue(); + // always only have one scheduled at a time + s_shortestDueTimeMs = long.MaxValue; - if (shortestWaitDurationMs != int.MaxValue) - { - SetTimeout((int)shortestWaitDurationMs, 0); - } + long currentTimeMs = TickCount64; + ReplaceNextSetTimeout(PumpTimerQueue(currentTimeMs), currentTimeMs); } + // this is called with shortest of timers scheduled on the particular TimerQueue private bool SetTimer(uint actualDuration) { Debug.Assert((int)actualDuration >= 0); - long dueTimeMs = TickCount64 + (int)actualDuration; + long currentTimeMs = TickCount64; if (!_isScheduled) { s_scheduledTimers ??= new List(Instances.Length); @@ -52,24 +55,65 @@ private bool SetTimer(uint actualDuration) s_scheduledTimers.Add(this); _isScheduled = true; } - _scheduledDueTimeMs = dueTimeMs; - SetTimeout((int)actualDuration, 0); + + _scheduledDueTimeMs = currentTimeMs + (int)actualDuration; + + ReplaceNextSetTimeout(ShortestDueTime(), currentTimeMs); return true; } - private static int PumpTimerQueue() + // shortest time of all TimerQueues + private static void ReplaceNextSetTimeout(long shortestDueTimeMs, long currentTimeMs) { - if (s_scheduledTimersToFire == null) + if (shortestDueTimeMs == int.MaxValue) + { + return; + } + + // this also covers s_shortestDueTimeMs = long.MaxValue when none is scheduled + if (s_shortestDueTimeMs > shortestDueTimeMs) + { + s_shortestDueTimeMs = shortestDueTimeMs; + int shortestWait = Math.Max((int)(shortestDueTimeMs - currentTimeMs), 0); + // this would cancel the previous schedule and create shorter one + // it is expensive call + SetTimeout(shortestWait, 0); + } + } + + private static long ShortestDueTime() + { + if (s_scheduledTimers == null) { return int.MaxValue; } + long shortestDueTimeMs = long.MaxValue; + var timers = s_scheduledTimers!; + for (int i = timers.Count - 1; i >= 0; --i) + { + TimerQueue timer = timers[i]; + if (timer._scheduledDueTimeMs < shortestDueTimeMs) + { + shortestDueTimeMs = timer._scheduledDueTimeMs; + } + } + + return shortestDueTimeMs; + } + + private static long PumpTimerQueue(long currentTimeMs) + { + if (s_scheduledTimersToFire == null) + { + return ShortestDueTime(); + } + List timersToFire = s_scheduledTimersToFire!; List timers; timers = s_scheduledTimers!; - long currentTimeMs = TickCount64; - int shortestWaitDurationMs = int.MaxValue; + long shortestDueTimeMs = int.MaxValue; for (int i = timers.Count - 1; i >= 0; --i) { TimerQueue timer = timers[i]; @@ -88,9 +132,9 @@ private static int PumpTimerQueue() continue; } - if (waitDurationMs < shortestWaitDurationMs) + if (timer._scheduledDueTimeMs < shortestDueTimeMs) { - shortestWaitDurationMs = (int)waitDurationMs; + shortestDueTimeMs = timer._scheduledDueTimeMs; } } @@ -103,7 +147,7 @@ private static int PumpTimerQueue() timersToFire.Clear(); } - return shortestWaitDurationMs; + return shortestDueTimeMs; } } } diff --git a/src/mono/wasm/runtime/library_mono.js b/src/mono/wasm/runtime/library_mono.js index d40e893135c1e5..710b613ade12f5 100644 --- a/src/mono/wasm/runtime/library_mono.js +++ b/src/mono/wasm/runtime/library_mono.js @@ -1503,8 +1503,12 @@ var MonoSupportLib = { mono_set_timeout: function (timeout, id) { if (typeof globalThis.setTimeout === 'function') { - globalThis.setTimeout (function () { - MONO.mono_wasm_set_timeout_exec (id); + if (MONO.lastScheduleTimeoutId) { + globalThis.clearTimeout(MONO.lastScheduleTimeoutId); + MONO.lastScheduleTimeoutId = undefined; + } + MONO.lastScheduleTimeoutId = globalThis.setTimeout(function mono_wasm_set_timeout_exec () { + MONO.mono_wasm_set_timeout_exec(id); }, timeout); } else { ++MONO.pump_count;