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;