From 411f7e6ed352b0e87c3a8a67dbcbed48aa6c7f7e Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Thu, 14 Mar 2024 16:43:56 +0100 Subject: [PATCH] [browser][MT] dedicated io thread (#99422) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marek FiĊĦera --- eng/testing/WasmRunnerTemplate.cmd | 3 + eng/testing/WasmRunnerTemplate.sh | 4 + eng/testing/tests.browser.targets | 1 - .../BrowserHttpHandler/BrowserHttpInterop.cs | 2 +- .../TC_SchemaSet_AllowXmlAttributes.cs | 2 +- .../TC_SchemaSet_EnableUpaCheck.cs | 2 +- .../XmlSchemaSet/TC_SchemaSet_Misc.cs | 14 +- .../XmlSchemaSet/TC_SchemaSet_NmTokens.cs | 2 +- .../XmlSchemaSet/TC_SchemaSet_ProhibitDTD.cs | 28 ++-- .../XmlSchemaSet/TC_SchemaSet_Reprocess.cs | 12 +- .../XmlSchemaValidatorApi/PropertiesTests.cs | 2 +- .../XmlSchemaValidatorApi/ValidateMisc.cs | 4 +- .../JavaScript/Interop/JavaScriptExports.cs | 31 ++++- .../JavaScript/JSHostImplementation.Types.cs | 4 + .../JavaScript/JSHostImplementation.cs | 2 +- .../JavaScript/JSProxyContext.cs | 2 +- .../Marshaling/JSMarshalerArgument.Task.cs | 38 ++++-- ...ces.JavaScript.BackgroundExec.Tests.csproj | 7 - .../JavaScript/JSExportTest.cs | 2 +- .../JavaScript/WebWorkerTest.cs | 120 ++++++++++++++++-- .../JavaScript/WebWorkerTestBase.cs | 21 ++- .../JavaScript/WebWorkerTestHelper.cs | 91 +++++++------ .../JavaScript/WebWorkerTestHelper.mjs | 10 ++ .../CancellationTokenTests.cs | 1 + .../MethodCoverage.cs | 2 + .../System.Threading.Tasks.Tests.csproj | 2 + ...syncEnumerableToBlockingEnumerableTests.cs | 3 + .../Task/TaskContinueWithTests.cs | 2 + .../tests/SemaphoreSlimTests.cs | 1 + .../tests/System.Threading.Tests.csproj | 3 + src/libraries/tests.proj | 4 - src/mono/browser/runtime/cwraps.ts | 8 ++ src/mono/browser/runtime/driver.c | 22 +++- src/mono/browser/runtime/exports-binding.ts | 3 +- src/mono/browser/runtime/exports-internal.ts | 3 +- src/mono/browser/runtime/globals.ts | 1 + src/mono/browser/runtime/invoke-cs.ts | 8 +- src/mono/browser/runtime/loader/config.ts | 10 +- src/mono/browser/runtime/managed-exports.ts | 23 ++-- src/mono/browser/runtime/multi-threading.md | 7 + src/mono/browser/runtime/pthreads/index.ts | 1 + .../browser/runtime/pthreads/io-thread.ts | 40 ++++++ src/mono/browser/runtime/pthreads/shared.ts | 17 +-- .../browser/runtime/pthreads/ui-thread.ts | 3 + src/mono/browser/runtime/startup.ts | 20 ++- src/mono/browser/runtime/types/internal.ts | 8 ++ src/mono/mono/utils/mono-threads-wasm.c | 49 ++++++- src/mono/mono/utils/mono-threads-wasm.h | 12 ++ .../Blazor/BlazorWasmTestBase.cs | 1 + 49 files changed, 487 insertions(+), 171 deletions(-) delete mode 100644 src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/BackgroundExec/System.Runtime.InteropServices.JavaScript.BackgroundExec.Tests.csproj create mode 100644 src/mono/browser/runtime/pthreads/io-thread.ts diff --git a/eng/testing/WasmRunnerTemplate.cmd b/eng/testing/WasmRunnerTemplate.cmd index 83aeb53cad03a..f92cee17cc9df 100644 --- a/eng/testing/WasmRunnerTemplate.cmd +++ b/eng/testing/WasmRunnerTemplate.cmd @@ -59,6 +59,9 @@ if /I [%XHARNESS_COMMAND%] == [test] ( if [%BROWSER_PATH%] == [] if not [%HELIX_CORRELATION_PAYLOAD%] == [] ( set "BROWSER_PATH=--browser-path^=%HELIX_CORRELATION_PAYLOAD%\chrome-win\chrome.exe" ) + if [%JS_ENGINE_ARGS%] == [] ( + set "JS_ENGINE_ARGS=--browser-arg^=--js-flags^=--stack-trace-limit^=1000" + ) ) if [%XHARNESS_ARGS%] == [] ( diff --git a/eng/testing/WasmRunnerTemplate.sh b/eng/testing/WasmRunnerTemplate.sh index 71347666cde80..4f5856546fc56 100644 --- a/eng/testing/WasmRunnerTemplate.sh +++ b/eng/testing/WasmRunnerTemplate.sh @@ -58,6 +58,10 @@ if [[ "$XHARNESS_COMMAND" == "test" ]]; then fi fi fi +else + if [[ -z "$JS_ENGINE_ARGS" ]]; then + JS_ENGINE_ARGS="--browser-arg=--js-flags=--stack-trace-limit=1000" + fi fi if [[ -z "$XHARNESS_ARGS" ]]; then diff --git a/eng/testing/tests.browser.targets b/eng/testing/tests.browser.targets index 06f07c898eabc..bce044984e937 100644 --- a/eng/testing/tests.browser.targets +++ b/eng/testing/tests.browser.targets @@ -87,7 +87,6 @@ <_AppArgs Condition="'$(IsFunctionalTest)' != 'true' and '$(WasmMainAssemblyFileName)' != ''">--run $(WasmMainAssemblyFileName) <_AppArgs Condition="'$(IsFunctionalTest)' == 'true'">--run $(AssemblyName).dll - <_XUnitBackgroundExec Condition="'$(_XUnitBackgroundExec)' == '' and '$(WasmEnableThreads)' == 'true'">true $(WasmTestAppArgs) -backgroundExec $(WasmXHarnessMonoArgs) --setenv=IsWasmBackgroundExec=true <_AppArgs Condition="'$(WasmTestAppArgs)' != ''">$(_AppArgs) $(WasmTestAppArgs) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs index eee79765c246f..c003cb7615e28 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs @@ -147,7 +147,7 @@ public static async Task CancellationHelper(Task promise, CancellationToken canc } }, (promise, jsController))) { - await promise.ConfigureAwait(true); + await promise.ConfigureAwait(false); } } catch (OperationCanceledException oce) when (cancellationToken.IsCancellationRequested) diff --git a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_AllowXmlAttributes.cs b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_AllowXmlAttributes.cs index 2bed5cd37c19b..2d5ffe75de7f5 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_AllowXmlAttributes.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_AllowXmlAttributes.cs @@ -273,7 +273,7 @@ public void v1(string xmlFile, string xsdFile, bool allowXmlAttributes, int expe if (xsdFile != null) xss.Add(null, Path.Combine(testData, xsdFile)); - XmlReader vr = CreateReader(Path.Combine(testData, xmlFile), xss, allowXmlAttributes); + using XmlReader vr = CreateReader(Path.Combine(testData, xmlFile), xss, allowXmlAttributes); while (vr.Read()) ; Assert.Equal(warningCount, expectedWarningCount); diff --git a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_EnableUpaCheck.cs b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_EnableUpaCheck.cs index 5de2beac167de..9576efe837844 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_EnableUpaCheck.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_EnableUpaCheck.cs @@ -146,7 +146,7 @@ public void v1(object param0, object param1, object param2, int[] expectedErrorL xss.ValidationEventHandler += new ValidationEventHandler(ValidationCallback); xss.Add(null, Path.Combine(testData, xsdFile)); - XmlReader vr = CreateReader(Path.Combine(testData, xmlFile), xss, false); + using XmlReader vr = CreateReader(Path.Combine(testData, xmlFile), xss, false); while (vr.Read()) ; CError.Compare(errorCount, expectedErrorCount, "Error Count mismatch"); diff --git a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_Misc.cs b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_Misc.cs index 0cec7918b3787..cd8de8da28359 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_Misc.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_Misc.cs @@ -89,7 +89,7 @@ public void v2() XmlSchemaValidationFlags.ProcessInlineSchema; settings.ValidationEventHandler += new ValidationEventHandler(ValidationCallback); settings.Schemas.Add(ss); - XmlReader vr = XmlReader.Create(Path.Combine(TestData._Root, "bug115049.xml"), settings); + using XmlReader vr = XmlReader.Create(Path.Combine(TestData._Root, "bug115049.xml"), settings); while (vr.Read()) ; CError.Compare(errorCount, 1, "Error Count mismatch!"); return; @@ -108,7 +108,7 @@ public void v4() XmlSchemaValidationFlags.ProcessSchemaLocation | XmlSchemaValidationFlags.ProcessInlineSchema; settings.ValidationEventHandler += new ValidationEventHandler(ValidationCallback); - XmlReader vr = XmlReader.Create(new StringReader(xml), settings, (string)null); + using XmlReader vr = XmlReader.Create(new StringReader(xml), settings, (string)null); while (vr.Read()) ; CError.Compare(errorCount, 0, "Error Count mismatch!"); CError.Compare(warningCount, 1, "Warning Count mismatch!"); @@ -531,7 +531,7 @@ public void v106() #pragma warning disable 0618 settings.ProhibitDtd = false; #pragma warning restore 0618 - XmlReader r = XmlReader.Create(Path.Combine(TestData._Root, "XMLSchema.xsd"), settings); + using XmlReader r = XmlReader.Create(Path.Combine(TestData._Root, "XMLSchema.xsd"), settings); ss1.Add(null, r); ss1.Compile(); @@ -568,7 +568,7 @@ public void v107() settings.Schemas.Add(schemaSet); settings.ValidationEventHandler += new ValidationEventHandler(ValidationCallback); settings.ValidationType = ValidationType.Schema; - XmlReader vr = XmlReader.Create(new StringReader(strXml), settings); + using XmlReader vr = XmlReader.Create(new StringReader(strXml), settings); while (vr.Read()) ; @@ -742,7 +742,7 @@ public void v112() XmlSchema mainSchema = set.Add(null, Path.Combine(TestData._Root, "bug382035a.xsd")); set.Compile(); - XmlReader r = XmlReader.Create(Path.Combine(TestData._Root, "bug382035a1.xsd")); + using XmlReader r = XmlReader.Create(Path.Combine(TestData._Root, "bug382035a1.xsd")); XmlSchema reParsedInclude = XmlSchema.Read(r, new ValidationEventHandler(ValidationCallback)); ((XmlSchemaExternal)mainSchema.Includes[0]).Schema = reParsedInclude; @@ -766,7 +766,7 @@ public void v113() settings.ValidationFlags |= XmlSchemaValidationFlags.ReportValidationWarnings | XmlSchemaValidationFlags.ProcessSchemaLocation; settings.ValidationEventHandler += new ValidationEventHandler(ValidationCallback); settings.ValidationType = ValidationType.Schema; - XmlReader vr = XmlReader.Create(new StringReader(strXml), settings); + using XmlReader vr = XmlReader.Create(new StringReader(strXml), settings); while (vr.Read()) ; @@ -1056,7 +1056,7 @@ public void Dev10_40509() string xsd = Path.Combine(TestData._Root, "bug511217.xsd"); XmlSchemaSet s = new XmlSchemaSet(); s.XmlResolver = new XmlUrlResolver(); - XmlReader r = XmlReader.Create(xsd); + using XmlReader r = XmlReader.Create(xsd); s.Add(null, r); s.Compile(); XmlReaderSettings rs = new XmlReaderSettings(); diff --git a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_NmTokens.cs b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_NmTokens.cs index c9844ef693063..86d8c82d1e330 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_NmTokens.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_NmTokens.cs @@ -30,7 +30,7 @@ public void TestSchemaCompile(string fileName, bool negative) Assert.True(negative, args.Message); numevents++; }; - XmlReader r = XmlReader.Create(xsd); + using XmlReader r = XmlReader.Create(xsd); s.Add(null, r); s.Compile(); Assert.False(negative && numevents != 1); diff --git a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_ProhibitDTD.cs b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_ProhibitDTD.cs index c1737a3395d07..497253ceb83cd 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_ProhibitDTD.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_ProhibitDTD.cs @@ -149,7 +149,7 @@ public void v2() Initialize(); XmlSchemaSet xss = new XmlSchemaSet(); xss.ValidationEventHandler += ValidationCallback; - XmlReader r = CreateReader(Path.Combine(TestData._Root, "bug356711_a.xsd")); + using XmlReader r = CreateReader(Path.Combine(TestData._Root, "bug356711_a.xsd")); try { xss.Add(null, r); @@ -190,7 +190,7 @@ public void v4() XmlSchemaSet xss = new XmlSchemaSet(); xss.XmlResolver = new XmlUrlResolver(); xss.ValidationEventHandler += ValidationCallback; - XmlReader r = CreateReader(Path.Combine(TestData._Root, "bug356711.xsd")); + using XmlReader r = CreateReader(Path.Combine(TestData._Root, "bug356711.xsd")); try { xss.Add(null, r); @@ -314,7 +314,7 @@ public void v10(object param0) xss.XmlResolver = new XmlUrlResolver(); xss.ValidationEventHandler += ValidationCallback; - XmlReader r = CreateReader(Path.Combine(TestData._Root, param0.ToString()), false); + using XmlReader r = CreateReader(Path.Combine(TestData._Root, param0.ToString()), false); try { xss.Add(null, r); @@ -363,8 +363,8 @@ public void v12(object param0) XmlSchemaSet xss = new XmlSchemaSet(); xss.ValidationEventHandler += ValidationCallback; - XmlReader r = CreateReader(Path.Combine(TestData._Root, param0.ToString()), false); - XmlReader r2 = CreateReader(r, true); + using XmlReader r = CreateReader(Path.Combine(TestData._Root, param0.ToString()), false); + using XmlReader r2 = CreateReader(r, true); try { xss.Add(null, r2); @@ -387,8 +387,8 @@ public void v13(object param0) xss.XmlResolver = new XmlUrlResolver(); xss.ValidationEventHandler += ValidationCallback; - XmlReader r = CreateReader(Path.Combine(TestData._Root, param0.ToString()), false); - XmlReader r2 = CreateReader(r, true); + using XmlReader r = CreateReader(Path.Combine(TestData._Root, param0.ToString()), false); + using XmlReader r2 = CreateReader(r, true); try { @@ -413,7 +413,7 @@ public void v14() xss.XmlResolver = new XmlUrlResolver(); xss.ValidationEventHandler += ValidationCallback; - XmlReader r = CreateReader(Path.Combine(TestData._Root, "bug356711.xsd"), false); + using XmlReader r = CreateReader(Path.Combine(TestData._Root, "bug356711.xsd"), false); try { @@ -437,8 +437,8 @@ public void v15() XmlSchemaSet xss = new XmlSchemaSet(); xss.ValidationEventHandler += ValidationCallback; - XmlReader r1 = CreateReader(Path.Combine(TestData._Root, "bug356711_a.xsd")); - XmlReader r2 = CreateReader(Path.Combine(TestData._Root, "bug356711_b.xsd"), false); + using XmlReader r1 = CreateReader(Path.Combine(TestData._Root, "bug356711_a.xsd")); + using XmlReader r2 = CreateReader(Path.Combine(TestData._Root, "bug356711_b.xsd"), false); try { @@ -482,7 +482,7 @@ public void v20(object param0) try { - XmlReader reader = CreateReader(Path.Combine(TestData._Root, param0.ToString()), xss, true); + using XmlReader reader = CreateReader(Path.Combine(TestData._Root, param0.ToString()), xss, true); while (reader.Read()) ; } catch (XmlException) @@ -539,7 +539,7 @@ public void v22(object param0) try { - XmlReader reader = CreateReader(Path.Combine(TestData._Root, param0.ToString()), xss, false); + using XmlReader reader = CreateReader(Path.Combine(TestData._Root, param0.ToString()), xss, false); while (reader.Read()) ; } catch (XmlException) @@ -561,8 +561,8 @@ public void v23() try { - XmlReader r1 = CreateReader(Path.Combine(TestData._Root, "bug356711_1.xml"), true); - XmlReader r2 = CreateReader(r1, xss, false); + using XmlReader r1 = CreateReader(Path.Combine(TestData._Root, "bug356711_1.xml"), true); + using XmlReader r2 = CreateReader(r1, xss, false); while (r2.Read()) ; } catch (XmlException) diff --git a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_Reprocess.cs b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_Reprocess.cs index 9aa0e60422379..894569f9e3fd5 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_Reprocess.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_Reprocess.cs @@ -560,7 +560,7 @@ public void v51(object param0, object param1, object param2, object param3) settings.ValidationEventHandler += new ValidationEventHandler(ValidationCallback); settings.ValidationType = ValidationType.Schema; settings.Schemas = set; - XmlReader reader = XmlReader.Create(xmlFile, settings); + using XmlReader reader = XmlReader.Create(xmlFile, settings); while (reader.Read()) { } CError.Compare(bWarningCallback, false, "Warning count mismatch"); @@ -581,8 +581,8 @@ public void v51(object param0, object param1, object param2, object param3) bErrorCallback = false; _output.WriteLine("Second validation ***************"); settings.Schemas = set; - reader = XmlReader.Create(xmlFile, settings); - while (reader.Read()) { } + using XmlReader reader2 = XmlReader.Create(xmlFile, settings); + while (reader2.Read()) { } CError.Compare(bWarningCallback, false, "Warning count mismatch"); CError.Compare(bErrorCallback, false, "Error count mismatch"); @@ -606,8 +606,8 @@ public void v51(object param0, object param1, object param2, object param3) _output.WriteLine("Third validation, Expecting errors ***************"); settings.Schemas = set; - reader = XmlReader.Create(xmlFile, settings); - while (reader.Read()) { } + using XmlReader reader3 = XmlReader.Create(xmlFile, settings); + while (reader3.Read()) { } CError.Compare(bWarningCallback, false, "Warning count mismatch"); CError.Compare(bErrorCallback, true, "Error count mismatch"); @@ -623,7 +623,7 @@ public XmlSchema LoadSchema(string path, string baseuri) _output.WriteLine("Correct uri: " + correctUri); using (Stream s = new FileStream(Path.GetFullPath(path), FileMode.Open, FileAccess.Read, FileShare.Read, 1)) { - XmlReader r = XmlReader.Create(s, new XmlReaderSettings(), includeUri); + using XmlReader r = XmlReader.Create(s, new XmlReaderSettings(), includeUri); _output.WriteLine("Reader uri: " + r.BaseURI); using (r) { diff --git a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaValidatorApi/PropertiesTests.cs b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaValidatorApi/PropertiesTests.cs index 70fc7f882ce0b..0675045b014ee 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaValidatorApi/PropertiesTests.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaValidatorApi/PropertiesTests.cs @@ -306,7 +306,7 @@ public void XmlReaderAsALineInfoProvider() XmlSchemaInfo info = new XmlSchemaInfo(); XmlSchemaValidator val = CreateValidator(CreateSchemaSetFromXml(xmlSrc)); - XmlReader r = XmlReader.Create(new StringReader(xmlSrc)); + using XmlReader r = XmlReader.Create(new StringReader(xmlSrc)); val.LineInfoProvider = (r as IXmlLineInfo); diff --git a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaValidatorApi/ValidateMisc.cs b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaValidatorApi/ValidateMisc.cs index 1e9fb3a25599a..25cb496ed38a4 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaValidatorApi/ValidateMisc.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaValidatorApi/ValidateMisc.cs @@ -901,7 +901,7 @@ public void XSDValidationGeneratesInvalidError_1() // TempDirectory path must end with a DirectorySeratorChar, otherwise it will throw in the Xml validation. settings.Schemas.Add("mainschema", XmlReader.Create(new StringReader(xsd), null, EnsureTrailingSlash(tempDirectory.Path))); settings.ValidationType = ValidationType.Schema; - XmlReader reader = XmlReader.Create(new StringReader(xml), settings); + using XmlReader reader = XmlReader.Create(new StringReader(xml), settings); XmlDocument doc = new XmlDocument(); doc.Load(reader); @@ -926,7 +926,7 @@ public void XSDValidationGeneratesInvalidError_2() // TempDirectory path must end with a DirectorySeratorChar, otherwise it will throw in the Xml validation. settings.Schemas.Add("mainschema", XmlReader.Create(new StringReader(xsd), null, EnsureTrailingSlash(tempDirectory.Path))); settings.ValidationType = ValidationType.Schema; - XmlReader reader = XmlReader.Create(new StringReader(xml), settings); + using XmlReader reader = XmlReader.Create(new StringReader(xml), settings); XmlDocument doc = new XmlDocument(); doc.Load(reader); diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs index 3585ad1ae5ddd..6faf786f3bd53 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs @@ -104,8 +104,8 @@ public static void ReleaseJSOwnedObjectByGCHandle(JSMarshalerArgument* arguments try { - // when we arrive here, we are on the thread which owns the proxies - var ctx = arg_exc.AssertCurrentThreadContext(); + // when we arrive here, we are on the thread which owns the proxies or on IO thread + var ctx = arg_exc.ToManagedContext; ctx.ReleaseJSOwnedObjectByGCHandle(arg_1.slot.GCHandle); } catch (Exception ex) @@ -131,6 +131,11 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer) // we may need to consider how to solve blocking of the synchronous call // see also https://github.com/dotnet/runtime/issues/76958#issuecomment-1921418290 arg_exc.AssertCurrentThreadContext(); + + if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode) + { + Thread.ThrowOnBlockingWaitOnJSInteropThread = true; + } #endif GCHandle callback_gc_handle = (GCHandle)arg_1.slot.GCHandle; @@ -148,8 +153,16 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer) { arg_exc.ToJS(ex); } +#if FEATURE_WASM_MANAGED_THREADS + finally + { + if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode) + { + Thread.ThrowOnBlockingWaitOnJSInteropThread = false; + } + } +#endif } - // the marshaled signature is: void CompleteTask(GCHandle holder, Exception? exceptionResult, T? result) public static void CompleteTask(JSMarshalerArgument* arguments_buffer) { @@ -161,8 +174,8 @@ public static void CompleteTask(JSMarshalerArgument* arguments_buffer) try { - // when we arrive here, we are on the thread which owns the proxies - var ctx = arg_exc.AssertCurrentThreadContext(); + // when we arrive here, we are on the thread which owns the proxies or on IO thread + var ctx = arg_exc.ToManagedContext; var holder = ctx.GetPromiseHolder(arg_1.slot.GCHandle); JSHostImplementation.ToManagedCallback callback; @@ -270,6 +283,10 @@ public static void BeforeSyncJSExport(JSMarshalerArgument* arguments_buffer) { var ctx = arg_exc.AssertCurrentThreadContext(); ctx.IsPendingSynchronousCall = true; + if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode) + { + Thread.ThrowOnBlockingWaitOnJSInteropThread = true; + } } catch (Exception ex) { @@ -288,6 +305,10 @@ public static void AfterSyncJSExport(JSMarshalerArgument* arguments_buffer) { var ctx = arg_exc.AssertCurrentThreadContext(); ctx.IsPendingSynchronousCall = false; + if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode) + { + Thread.ThrowOnBlockingWaitOnJSInteropThread = false; + } } catch (Exception ex) { diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Types.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Types.cs index 18d4a52cd808b..1e1e74b711090 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Types.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Types.cs @@ -72,6 +72,8 @@ public enum MainThreadingMode : int UIThread = 0, // Running the managed main thread on dedicated WebWorker. Marshaling all JavaScript calls to and from the main thread. DeputyThread = 1, + // TODO comments + DeputyAndIOThreads = 2, } // keep in sync with types\internal.ts @@ -80,6 +82,8 @@ public enum JSThreadBlockingMode : int // throw PlatformNotSupportedException if blocking .Wait is called on threads with JS interop, like JSWebWorker and Main thread. // Avoids deadlocks (typically with pending JS promises on the same thread) by throwing exceptions. NoBlockingWait = 0, + // TODO comments + AllowBlockingWaitInAsyncCode = 1, // allow .Wait on all threads. // Could cause deadlocks with blocking .Wait on a pending JS Task/Promise on the same thread or similar Task/Promise chain. AllowBlockingWait = 100, diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs index 232613b132841..abadf7f1d0191 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs @@ -101,7 +101,7 @@ public static async Task CancellationHelper(Task jsTask, Can CancelablePromise.CancelPromise((Task)s!); }, jsTask)) { - return await jsTask.ConfigureAwait(true); + return await jsTask.ConfigureAwait(false); } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index cede7971db757..02618e778bc82 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -43,7 +43,7 @@ private JSProxyContext() public JSAsyncTaskScheduler? AsyncTaskScheduler; public static MainThreadingMode MainThreadingMode = MainThreadingMode.DeputyThread; - public static JSThreadBlockingMode ThreadBlockingMode = JSThreadBlockingMode.NoBlockingWait; + public static JSThreadBlockingMode ThreadBlockingMode = JSThreadBlockingMode.AllowBlockingWaitInAsyncCode; public static JSThreadInteropMode ThreadInteropMode = JSThreadInteropMode.SimpleSynchronousJSInterop; public bool IsPendingSynchronousCall; diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs index cb1e43a1a0096..cf636727ce623 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs @@ -50,14 +50,15 @@ public unsafe void ToManaged(out Task? value) lock (ctx) { PromiseHolder holder = ctx.GetPromiseHolder(slot.GCHandle); - // we want to run the continuations on the original thread which called the JSImport, so RunContinuationsAsynchronously, rather than ExecuteSynchronously - // TODO TaskCreationOptions.RunContinuationsAsynchronously - TaskCompletionSource tcs = new TaskCompletionSource(holder); + TaskCompletionSource tcs = new TaskCompletionSource(holder, TaskCreationOptions.RunContinuationsAsynchronously); ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => { if (arguments_buffer == null) { - tcs.TrySetException(new TaskCanceledException("WebWorker which is origin of the Promise is being terminated.")); + if (!tcs.TrySetException(new TaskCanceledException("WebWorker which is origin of the Promise is being terminated."))) + { + Environment.FailFast("Failed to set exception to TaskCompletionSource (arguments buffer is null)"); + } return; } ref JSMarshalerArgument arg_2 = ref arguments_buffer[3]; // set by caller when this is SetException call @@ -65,11 +66,17 @@ public unsafe void ToManaged(out Task? value) if (arg_2.slot.Type != MarshalerType.None) { arg_2.ToManaged(out Exception? fail); - tcs.TrySetException(fail!); + if (!tcs.TrySetException(fail!)) + { + Environment.FailFast("Failed to set exception to TaskCompletionSource (exception raised)"); + } } else { - tcs.TrySetResult(); + if (!tcs.TrySetResult()) + { + Environment.FailFast("Failed to set result to TaskCompletionSource (marshaler type is none)"); + } } // eventual exception is handled by caller }; @@ -101,14 +108,15 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback lock (ctx) { var holder = ctx.GetPromiseHolder(slot.GCHandle); - // we want to run the continuations on the original thread which called the JSImport, so RunContinuationsAsynchronously, rather than ExecuteSynchronously - // TODO TaskCreationOptions.RunContinuationsAsynchronously - TaskCompletionSource tcs = new TaskCompletionSource(holder); + TaskCompletionSource tcs = new TaskCompletionSource(holder, TaskCreationOptions.RunContinuationsAsynchronously); ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => { if (arguments_buffer == null) { - tcs.TrySetException(new TaskCanceledException("WebWorker which is origin of the Promise is being terminated.")); + if (!tcs.TrySetException(new TaskCanceledException("WebWorker which is origin of the Promise is being terminated."))) + { + Environment.FailFast("Failed to set exception to TaskCompletionSource (arguments buffer is null)"); + } return; } @@ -118,12 +126,18 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback { arg_2.ToManaged(out Exception? fail); if (fail == null) throw new InvalidOperationException(SR.FailedToMarshalException); - tcs.TrySetException(fail); + if (!tcs.TrySetException(fail)) + { + Environment.FailFast("Failed to set exception to TaskCompletionSource (exception raised)"); + } } else { marshaler(ref arg_3, out T result); - tcs.TrySetResult(result); + if(!tcs.TrySetResult(result)) + { + Environment.FailFast("Failed to set result to TaskCompletionSource (marshaler type is none)"); + } } // eventual exception is handled by caller }; diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/BackgroundExec/System.Runtime.InteropServices.JavaScript.BackgroundExec.Tests.csproj b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/BackgroundExec/System.Runtime.InteropServices.JavaScript.BackgroundExec.Tests.csproj deleted file mode 100644 index 506fcc01f008a..0000000000000 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/BackgroundExec/System.Runtime.InteropServices.JavaScript.BackgroundExec.Tests.csproj +++ /dev/null @@ -1,7 +0,0 @@ - - - <_XUnitBackgroundExec Condition="'$(_XUnitBackgroundExec)' == ''">true - System.Runtime.InteropServices.JavaScript.Tests - - - diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSExportTest.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSExportTest.cs index 2d7b7cbf6429c..170f393deb7c6 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSExportTest.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSExportTest.cs @@ -37,7 +37,7 @@ public async Task JsExportInt32DiscardNoWait(int value) { JavaScriptTestHelper.optimizedReached=0; JavaScriptTestHelper.invoke1O(value); - await JavaScriptTestHelper.Delay(0); + await JavaScriptTestHelper.Delay(50); Assert.Equal(value, JavaScriptTestHelper.optimizedReached); } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs index 28c2c5c0f06c1..c67bac997294b 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs @@ -68,6 +68,34 @@ public async Task JSDelay_Cancellation(Executor executor) await Assert.ThrowsAnyAsync(() => canceledTask); } + [Theory, MemberData(nameof(GetBlockingFriendlyTargetThreads))] + public async Task JSDelay_Blocking_Wait(Executor executor) + { + var cts = new CancellationTokenSource(); + var blockedTask = executor.Execute(async () => + { + await executor.StickyAwait(WebWorkerTestHelper.CreateDelay(), cts.Token); + var promise = WebWorkerTestHelper.JSDelay(100); + promise.Wait(); + }, cts.Token); + + await blockedTask; + } + + [Theory, MemberData(nameof(GetBlockingFriendlyTargetThreads))] + public async Task JSDelay_Blocking_GetResult(Executor executor) + { + var cts = new CancellationTokenSource(); + var blockedTask = executor.Execute(async () => + { + await executor.StickyAwait(WebWorkerTestHelper.CreateDelay(), cts.Token); + var promise = WebWorkerTestHelper.JSDelay(100); + promise.GetAwaiter().GetResult(); + }, cts.Token); + + await blockedTask; + } + [Fact] public async Task JSSynchronizationContext_Send_Post_Items_Cancellation() { @@ -382,7 +410,7 @@ await executor.Execute(async () => } [Theory, MemberData(nameof(GetTargetThreads))] - public async Task JSDelay_ContinueWith(Executor executor) + public async Task JSDelay_ContinueWith_Async(Executor executor) { using var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => @@ -391,9 +419,23 @@ await executor.Execute(async () => await WebWorkerTestHelper.JSDelay(10).ContinueWith(_ => { - // continue on the context of the target JS interop - executor.AssertInteropThread(); - }, TaskContinuationOptions.ExecuteSynchronously); + Assert.True(Thread.CurrentThread.IsThreadPoolThread); + }, TaskContinuationOptions.RunContinuationsAsynchronously); + }, cts.Token); + } + + [Theory, MemberData(nameof(GetTargetThreads))] + public async Task JSDelay_ContinueWith_Sync(Executor executor) + { + using var cts = CreateTestCaseTimeoutSource(); + await executor.Execute(async () => + { + await executor.StickyAwait(WebWorkerTestHelper.CreateDelay(), cts.Token); + + await WebWorkerTestHelper.JSDelay(10).ContinueWith(_ => + { + Assert.True(Thread.CurrentThread.IsThreadPoolThread); + }, TaskContinuationOptions.ExecuteSynchronously); // ExecuteSynchronously is ignored }, cts.Token); } @@ -411,6 +453,21 @@ await executor.Execute(async () => }, cts.Token); } + [Theory, MemberData(nameof(GetTargetThreads))] + public async Task JSDelay_ConfigureAwait_False(Executor executor) + { + using var cts = CreateTestCaseTimeoutSource(); + await executor.Execute(async () => + { + await executor.StickyAwait(WebWorkerTestHelper.CreateDelay(), cts.Token); + + await WebWorkerTestHelper.JSDelay(10).ConfigureAwait(false); + + // resolve/reject on I/O thread -> thread pool + Assert.True(Thread.CurrentThread.IsThreadPoolThread); + }, cts.Token); + } + [Theory, MemberData(nameof(GetTargetThreads))] public async Task ManagedDelay_ContinueWith(Executor executor) { @@ -439,11 +496,13 @@ await executor.Execute(async () => } [Theory, MemberData(nameof(GetTargetThreadsAndBlockingCalls))] - public async Task WaitAssertsOnJSInteropThreads(Executor executor, NamedCall method) + public async Task WaitDoesNotAssertInAsyncCode(Executor executor, NamedCall method) { using var cts = CreateTestCaseTimeoutSource(); - await executor.Execute(Task () => + await executor.Execute(async () => { + await executor.StickyAwait(WebWorkerTestHelper.InitializeAsync(), cts.Token); + Exception? exception = null; try { @@ -453,10 +512,55 @@ await executor.Execute(Task () => { exception = ex; } + + Assert.Null(exception); + }, cts.Token); + } + + [Theory, MemberData(nameof(GetTargetThreadsAndBlockingCalls))] + public async Task WaitAssertsOnSyncCallback(Executor executor, NamedCall method) + { + using var cts = CreateTestCaseTimeoutSource(); + await executor.Execute(async () => + { + await executor.StickyAwait(WebWorkerTestHelper.InitializeAsync(), cts.Token); + + Exception? exception = null; + // the callback will hit Main or JSWebWorker, not the original executor thread + await WebWorkerTestHelper.CallMeBackSync(() => { + // when we are inside of synchronous callback, all blocking .Wait is forbidden + try + { + method.Call(cts.Token); + } + catch (Exception ex) + { + exception = ex; + } + }); + Console.WriteLine("WaitAssertsOnJSInteropThreads: ExecuterType: " + executor.Type + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); - executor.AssertBlockingWait(exception); + Assert.NotNull(exception); + Assert.IsType(exception); + }, cts.Token); + } - return Task.CompletedTask; + [Theory, MemberData(nameof(GetTargetThreadsAndBlockingCalls))] + public async Task WaitAssertsOnSyncJSExport(Executor executor, NamedCall method) + { + using var cts = CreateTestCaseTimeoutSource(); + await executor.Execute(async () => + { + await executor.StickyAwait(WebWorkerTestHelper.InitializeAsync(), cts.Token); + + WebWorkerTestHelper.CurrentCallback = method; + WebWorkerTestHelper.CurrentCancellationToken = cts.Token; + // the callback will hit Main or JSWebWorker, not the original executor thread + await WebWorkerTestHelper.CallExportBackSync(nameof(WebWorkerTestHelper.CallCurrentCallback)); + + Console.WriteLine("WaitAssertsOnJSInteropThreads: ExecuterType: " + executor.Type + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); + Assert.NotNull(WebWorkerTestHelper.LastException); + Assert.IsType(WebWorkerTestHelper.LastException); }, cts.Token); } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs index 87f88745377b0..3fb53d484def0 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs @@ -45,9 +45,17 @@ public static IEnumerable GetTargetThreads() return Enum.GetValues().Select(type => new object[] { new Executor(type) }); } - public static IEnumerable GetSpecificTargetThreads() + public static IEnumerable GetBlockingFriendlyTargetThreads() { - yield return new object[] { new Executor(ExecutorType.JSWebWorker), new Executor(ExecutorType.Main) }; + yield return new object[] { new Executor(ExecutorType.Main) }; + yield return new object[] { new Executor(ExecutorType.NewThread) }; + yield return new object[] { new Executor(ExecutorType.ThreadPool) }; + // JSWebWorker is missing here because JS can't resolve promises while blocked + } + + public static IEnumerable GetSpecificTargetThreads2x() + { + yield return new object[] { new Executor(ExecutorType.Main), new Executor(ExecutorType.Main) }; yield break; } @@ -137,15 +145,6 @@ async Task ActionsInDifferentThreads2() } } - public class NamedCall - { - public string Name { get; set; } - public delegate void Method(CancellationToken ct); - public Method Call { get; set; } - - override public string ToString() => Name; - } - public static IEnumerable BlockingCalls = new List { new NamedCall { Name = "Task.Wait", Call = delegate (CancellationToken ct) { Task.Delay(10, ct).Wait(ct); }}, diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.cs index 35cdd8ff1858b..1fc94b83adba9 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.cs @@ -18,6 +18,7 @@ public partial class WebWorkerTestHelper public static readonly string LocalWsEcho = "ws://" + Environment.GetEnvironmentVariable("DOTNET_TEST_WEBSOCKETHOST") + "/WebSocket/EchoWebSocket.ashx"; [JSImport("globalThis.console.log")] + [return: JSMarshalAs] public static partial void Log(string message); [JSImport("delay", "InlineTestHelper")] @@ -38,6 +39,30 @@ public partial class WebWorkerTestHelper [JSImport("promiseValidateState", "WebWorkerTestHelper")] public static partial Task PromiseValidateState(JSObject state); + [JSImport("callMeBackSync", "WebWorkerTestHelper")] + public static partial Task CallMeBackSync([JSMarshalAs] Action syncCallback); + + [JSImport("callExportBackSync", "WebWorkerTestHelper")] + public static partial Task CallExportBackSync(string syncExportName); + + public static NamedCall CurrentCallback; + public static CancellationToken CurrentCancellationToken = CancellationToken.None; + public static Exception? LastException = null; + + [JSExport] + public static void CallCurrentCallback() + { + LastException = null; + try + { + CurrentCallback.Call(CurrentCancellationToken); + } + catch (Exception ex) + { + LastException = ex; + } + } + public static string GetOriginUrl() { using var globalThis = JSHost.GlobalThis; @@ -121,7 +146,6 @@ public enum ExecutorType public class Executor { public int ExecutorTID; - public SynchronizationContext ExecutorSynchronizationContext; private static SynchronizationContext _mainSynchronizationContext; public static SynchronizationContext MainSynchronizationContext { @@ -156,7 +180,6 @@ public Task Execute(Func job, CancellationToken cancellationToken) Task wrapExecute() { ExecutorTID = Environment.CurrentManagedThreadId; - ExecutorSynchronizationContext = SynchronizationContext.Current ?? MainSynchronizationContext; AssertTargetThread(); return job(); } @@ -194,6 +217,15 @@ public void AssertTargetThread() { Assert.False(Thread.CurrentThread.IsThreadPoolThread, "IsThreadPoolThread:" + Thread.CurrentThread.IsThreadPoolThread + " Type " + Type); } + if (Type == ExecutorType.Main || Type == ExecutorType.JSWebWorker) + { + Assert.NotNull(SynchronizationContext.Current); + Assert.Equal("System.Runtime.InteropServices.JavaScript.JSSynchronizationContext", SynchronizationContext.Current.GetType().FullName); + } + else + { + Assert.Null(SynchronizationContext.Current); + } } public void AssertAwaitCapturedContext() @@ -230,51 +262,6 @@ public void AssertAwaitCapturedContext() } } - public void AssertBlockingWait(Exception? exception) - { - switch (Type) - { - case ExecutorType.Main: - case ExecutorType.JSWebWorker: - Assert.NotNull(exception); - Assert.IsType(exception); - break; - case ExecutorType.NewThread: - case ExecutorType.ThreadPool: - Assert.Null(exception); - break; - } - } - - public void AssertInteropThread() - { - switch (Type) - { - case ExecutorType.Main: - Assert.Equal(1, Environment.CurrentManagedThreadId); - Assert.Equal(ExecutorTID, Environment.CurrentManagedThreadId); - Assert.False(Thread.CurrentThread.IsThreadPoolThread); - break; - case ExecutorType.JSWebWorker: - Assert.NotEqual(1, Environment.CurrentManagedThreadId); - Assert.Equal(ExecutorTID, Environment.CurrentManagedThreadId); - Assert.False(Thread.CurrentThread.IsThreadPoolThread); - break; - case ExecutorType.NewThread: - // it will synchronously continue on the UI thread - Assert.Equal(1, Environment.CurrentManagedThreadId); - Assert.NotEqual(ExecutorTID, Environment.CurrentManagedThreadId); - Assert.False(Thread.CurrentThread.IsThreadPoolThread); - break; - case ExecutorType.ThreadPool: - // it will synchronously continue on the UI thread - Assert.Equal(1, Environment.CurrentManagedThreadId); - Assert.NotEqual(ExecutorTID, Environment.CurrentManagedThreadId); - Assert.False(Thread.CurrentThread.IsThreadPoolThread); - break; - } - } - public override string ToString() => Type.ToString(); // make sure we stay on the executor @@ -394,4 +381,14 @@ public static Task RunOnTargetAsync(SynchronizationContext ctx, Func job, } #endregion + + public class NamedCall + { + public string Name { get; set; } + public delegate void Method(CancellationToken ct); + public Method Call { get; set; } + + override public string ToString() => Name; + } + } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.mjs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.mjs index 558fb181b47d6..e2a8cfadfaea7 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.mjs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.mjs @@ -73,3 +73,13 @@ export function delay(ms) { export function getRndInteger(min, max) { return Math.floor(Math.random() * (max - min)) + min; } + +export async function callMeBackSync(syncCallback) { + syncCallback(); +} + +export async function callExportBackSync(syncExportName) { + const WebWorkerTestHelper = dllExports.System.Runtime.InteropServices.JavaScript.Tests.WebWorkerTestHelper; + const method = WebWorkerTestHelper[syncExportName] + method(); +} \ No newline at end of file diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/CancellationTokenTests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/CancellationTokenTests.cs index 72df26fa4c988..ba75f617bf0b5 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/CancellationTokenTests.cs +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/CancellationTokenTests.cs @@ -874,6 +874,7 @@ static void FinalizeHelper(DisposeTracker disposeTracker) // Several tests for deriving custom user types from CancellationTokenSource [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/99519", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmThreadingSupported))] public static void DerivedCancellationTokenSource() { // Verify that a derived CTS is functional diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/MethodCoverage.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/MethodCoverage.cs index 3509d10843bcb..eca5c0c92e203 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/MethodCoverage.cs +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/MethodCoverage.cs @@ -279,6 +279,7 @@ public static async Task Task_WhenAny_TwoTasks_WakesOnFirstCompletion() } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/99500", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmThreadingSupported))] public static void CancellationTokenRegitration() { ManualResetEvent mre = new ManualResetEvent(false); @@ -296,6 +297,7 @@ public static void CancellationTokenRegitration() /// verify that the taskawaiter.UnsafeOnCompleted is invoked /// [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/99519", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmThreadingSupported))] public static void TaskAwaiter() { ManualResetEvent mre = new ManualResetEvent(false); diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj index 19521db08c333..8f7a8cb6b51cf 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj @@ -3,6 +3,8 @@ true true $(NetCoreAppCurrent) + + true diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/Task/AsyncEnumerableToBlockingEnumerableTests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/Task/AsyncEnumerableToBlockingEnumerableTests.cs index 0692aedb514f9..786734ad8391e 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/Task/AsyncEnumerableToBlockingEnumerableTests.cs +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/Task/AsyncEnumerableToBlockingEnumerableTests.cs @@ -70,6 +70,7 @@ static async IAsyncEnumerable CreateSourceEnumerable() } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/99519", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmThreadingSupported))] public static void AsyncEnumerableWithDelays() { var source = new InstrumentedAsyncEnumerable(CreateSourceEnumerable()); @@ -104,6 +105,7 @@ static async IAsyncEnumerable CreateSourceEnumerable() } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/99519", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmThreadingSupported))] public static void AsyncEnumerableWithException() { var source = new InstrumentedAsyncEnumerable(CreateSourceEnumerable()); @@ -132,6 +134,7 @@ static async IAsyncEnumerable CreateSourceEnumerable() } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/99519", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmThreadingSupported))] public static void AsyncEnumerableWithCancellation() { var source = new InstrumentedAsyncEnumerable(CreateSourceEnumerable()); diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/Task/TaskContinueWithTests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/Task/TaskContinueWithTests.cs index 2800a5e9e24c4..76803afb0195a 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/Task/TaskContinueWithTests.cs +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/Task/TaskContinueWithTests.cs @@ -1076,6 +1076,7 @@ public static void RunContinuationCancelTest_State() } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/99519", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmThreadingSupported))] public static void TestNoDeadlockOnContinueWith() { Debug.WriteLine("TestNoDeadlockOnContinueWith: shouldn't deadlock if it passes."); @@ -1255,6 +1256,7 @@ public static void LongContinuationChain_Unwrap_DoesNotStackOverflow() } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/99519", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmThreadingSupported))] public static void LongContinuationChain_Await_DoesNotStackOverflow() { const int DiveDepth = 12_000; diff --git a/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs b/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs index 7aabd01c39f1e..dfc75fc2a89e6 100644 --- a/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs +++ b/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs @@ -90,6 +90,7 @@ public static void RunSemaphoreSlimTest1_WaitAsync() } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/99501", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmThreadingSupported))] public static void RunSemaphoreSlimTest1_WaitAsync_NegativeCases() { // Invalid timeout diff --git a/src/libraries/System.Threading/tests/System.Threading.Tests.csproj b/src/libraries/System.Threading/tests/System.Threading.Tests.csproj index e938db9863da3..54261d3a1fe66 100644 --- a/src/libraries/System.Threading/tests/System.Threading.Tests.csproj +++ b/src/libraries/System.Threading/tests/System.Threading.Tests.csproj @@ -7,6 +7,9 @@ true + + true + diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index c0a7b0443b463..2059501a33952 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -400,10 +400,6 @@ - - - - diff --git a/src/mono/browser/runtime/cwraps.ts b/src/mono/browser/runtime/cwraps.ts index 23c0330d6eced..11168751ebb3b 100644 --- a/src/mono/browser/runtime/cwraps.ts +++ b/src/mono/browser/runtime/cwraps.ts @@ -27,8 +27,12 @@ const threading_cwraps: SigLine[] = WasmEnableThreads ? [ [false, "mono_wasm_init_finalizer_thread", null, []], [false, "mono_wasm_invoke_jsexport_async_post", "void", ["number", "number", "number"]], [false, "mono_wasm_invoke_jsexport_sync_send", "void", ["number", "number", "number"]], + [false, "mono_wasm_invoke_jsexport_sync", "void", ["number", "number"]], [true, "mono_wasm_create_deputy_thread", "number", []], + [true, "mono_wasm_create_io_thread", "number", []], [true, "mono_wasm_register_ui_thread", "void", []], + [true, "mono_wasm_register_io_thread", "void", []], + [true, "mono_wasm_print_thread_dump", "void", []], ] : []; // when the method is assigned/cached at usage, instead of being invoked directly from cwraps, it can't be marked lazy, because it would be re-bound on each call @@ -146,8 +150,12 @@ export interface t_ThreadingCwraps { mono_wasm_init_finalizer_thread(): void; mono_wasm_invoke_jsexport_async_post(targetTID: PThreadPtr, method: MonoMethod, args: VoidPtr): void; mono_wasm_invoke_jsexport_sync_send(targetTID: PThreadPtr, method: MonoMethod, args: VoidPtr): void; + mono_wasm_invoke_jsexport_sync(method: MonoMethod, args: VoidPtr): void; mono_wasm_create_deputy_thread(): PThreadPtr; + mono_wasm_create_io_thread(): PThreadPtr; mono_wasm_register_ui_thread(): void; + mono_wasm_register_io_thread(): void; + mono_wasm_print_thread_dump(): void; } export interface t_ProfilerCwraps { diff --git a/src/mono/browser/runtime/driver.c b/src/mono/browser/runtime/driver.c index d8a3da3f100ac..366fec9d60b70 100644 --- a/src/mono/browser/runtime/driver.c +++ b/src/mono/browser/runtime/driver.c @@ -257,15 +257,25 @@ mono_wasm_invoke_jsexport (MonoMethod *method, void* args) extern void mono_threads_wasm_async_run_in_target_thread_vii (void* target_thread, void (*func) (gpointer, gpointer), gpointer user_data1, gpointer user_data2); extern void mono_threads_wasm_sync_run_in_target_thread_vii (void* target_thread, void (*func) (gpointer, gpointer), gpointer user_data1, gpointer user_data2); +extern void mono_print_thread_dump (void *sigctx); + +EMSCRIPTEN_KEEPALIVE void +mono_wasm_print_thread_dump (void) +{ + mono_print_thread_dump (NULL); +} // this is running on the target thread static void mono_wasm_invoke_jsexport_async_post_cb (MonoMethod *method, void* args) { mono_wasm_invoke_jsexport (method, args); - // TODO assert receiver_should_free ? - if (args) - free (args); + if (args) { + MonoBoolean *is_receiver_should_free = (MonoBoolean *)(args + 20/*JSMarshalerArgumentOffsets.ReceiverShouldFree*/); + if(*is_receiver_should_free != 0){ + free (args); + } + } } // async @@ -281,8 +291,8 @@ extern js_interop_event before_sync_js_import; extern js_interop_event after_sync_js_import; // this is running on the target thread -static void -mono_wasm_invoke_jsexport_sync_send_cb (MonoMethod *method, void* args) +EMSCRIPTEN_KEEPALIVE void +mono_wasm_invoke_jsexport_sync (MonoMethod *method, void* args) { before_sync_js_import (args); mono_wasm_invoke_jsexport (method, args); @@ -293,7 +303,7 @@ mono_wasm_invoke_jsexport_sync_send_cb (MonoMethod *method, void* args) EMSCRIPTEN_KEEPALIVE void mono_wasm_invoke_jsexport_sync_send (void* target_thread, MonoMethod *method, void* args /*JSMarshalerArguments*/) { - mono_threads_wasm_sync_run_in_target_thread_vii(target_thread, (void (*)(gpointer, gpointer))mono_wasm_invoke_jsexport_sync_send_cb, method, args); + mono_threads_wasm_sync_run_in_target_thread_vii(target_thread, (void (*)(gpointer, gpointer))mono_wasm_invoke_jsexport_sync, method, args); } #endif /* DISABLE_THREADS */ diff --git a/src/mono/browser/runtime/exports-binding.ts b/src/mono/browser/runtime/exports-binding.ts index 10d71579ce99d..d2ff55bc088a7 100644 --- a/src/mono/browser/runtime/exports-binding.ts +++ b/src/mono/browser/runtime/exports-binding.ts @@ -29,7 +29,7 @@ import { mono_wasm_cancel_promise } from "./cancelable-promise"; import { mono_wasm_start_deputy_thread_async, mono_wasm_pthread_on_pthread_attached, mono_wasm_pthread_on_pthread_unregistered, - mono_wasm_pthread_on_pthread_registered, mono_wasm_pthread_set_name, mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop + mono_wasm_pthread_on_pthread_registered, mono_wasm_pthread_set_name, mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop, mono_wasm_start_io_thread_async } from "./pthreads"; import { mono_wasm_dump_threads } from "./pthreads/ui-thread"; @@ -43,6 +43,7 @@ export const mono_wasm_threads_imports = !WasmEnableThreads ? [] : [ mono_wasm_pthread_on_pthread_unregistered, mono_wasm_pthread_set_name, mono_wasm_start_deputy_thread_async, + mono_wasm_start_io_thread_async, // mono-threads.c mono_wasm_dump_threads, diff --git a/src/mono/browser/runtime/exports-internal.ts b/src/mono/browser/runtime/exports-internal.ts index fb49b71a92f15..3158b553713e4 100644 --- a/src/mono/browser/runtime/exports-internal.ts +++ b/src/mono/browser/runtime/exports-internal.ts @@ -4,7 +4,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import { MonoObjectNull, type MonoObject } from "./types/internal"; -import cwraps, { profiler_c_functions } from "./cwraps"; +import cwraps, { profiler_c_functions, threads_c_functions as twraps } from "./cwraps"; import { mono_wasm_send_dbg_command_with_parms, mono_wasm_send_dbg_command, mono_wasm_get_dbg_command_info, mono_wasm_get_details, mono_wasm_release_object, mono_wasm_call_function_on, mono_wasm_debugger_resume, mono_wasm_detach_debugger, mono_wasm_raise_debug_event, mono_wasm_change_debugger_log_level, mono_wasm_debugger_attached } from "./debug"; import { http_wasm_supports_streaming_request, http_wasm_supports_streaming_response, http_wasm_create_controller, http_wasm_abort_request, http_wasm_abort_response, http_wasm_transform_stream_write, http_wasm_transform_stream_close, http_wasm_fetch, http_wasm_fetch_stream, http_wasm_fetch_bytes, http_wasm_get_response_header_names, http_wasm_get_response_header_values, http_wasm_get_response_bytes, http_wasm_get_response_length, http_wasm_get_streamed_response_bytes, http_wasm_get_response_type, http_wasm_get_response_status } from "./http"; import { exportedRuntimeAPI, Module, runtimeHelpers } from "./globals"; @@ -120,6 +120,7 @@ export function cwraps_internal(internal: any): void { mono_wasm_profiler_init_aot: profiler_c_functions.mono_wasm_profiler_init_aot, mono_wasm_profiler_init_browser: profiler_c_functions.mono_wasm_profiler_init_browser, mono_wasm_exec_regression: cwraps.mono_wasm_exec_regression, + mono_wasm_print_thread_dump: WasmEnableThreads ? twraps.mono_wasm_print_thread_dump : undefined, }); } diff --git a/src/mono/browser/runtime/globals.ts b/src/mono/browser/runtime/globals.ts index 15ea06b82c6d0..9513254cc55aa 100644 --- a/src/mono/browser/runtime/globals.ts +++ b/src/mono/browser/runtime/globals.ts @@ -65,6 +65,7 @@ export function setRuntimeGlobals(globalObjects: GlobalObjects) { afterPreRun: createPromiseController(), beforeOnRuntimeInitialized: createPromiseController(), afterMonoStarted: createPromiseController(), + afterIOStarted: createPromiseController(), afterOnRuntimeInitialized: createPromiseController(), afterPostRun: createPromiseController(), nativeAbort: (reason: any) => { throw reason || new Error("abort"); }, diff --git a/src/mono/browser/runtime/invoke-cs.ts b/src/mono/browser/runtime/invoke-cs.ts index f8aa906ce2df5..893245041d27a 100644 --- a/src/mono/browser/runtime/invoke-cs.ts +++ b/src/mono/browser/runtime/invoke-cs.ts @@ -208,7 +208,7 @@ function bind_fn_1RA(closure: BindingClosure) { let promise = res_converter(args); // call C# side - invoke_async_jsexport(method, args, size); + invoke_async_jsexport(runtimeHelpers.managedThreadTID, method, args, size); // in case the C# side returned synchronously promise = end_marshal_task_to_js(args, undefined, promise); @@ -273,7 +273,7 @@ function bind_fn_2RA(closure: BindingClosure) { let promise = res_converter(args); // call C# side - invoke_async_jsexport(method, args, size); + invoke_async_jsexport(runtimeHelpers.managedThreadTID, method, args, size); // in case the C# side returned synchronously promise = end_marshal_task_to_js(args, undefined, promise); @@ -318,13 +318,13 @@ function bind_fn(closure: BindingClosure) { // call C# side if (is_async) { - invoke_async_jsexport(method, args, size); + invoke_async_jsexport(runtimeHelpers.managedThreadTID, method, args, size); // in case the C# side returned synchronously js_result = end_marshal_task_to_js(args, undefined, js_result); } else if (is_discard_no_wait) { // call C# side, fire and forget - invoke_async_jsexport(method, args, size); + invoke_async_jsexport(runtimeHelpers.managedThreadTID, method, args, size); } else { invoke_sync_jsexport(method, args); diff --git a/src/mono/browser/runtime/loader/config.ts b/src/mono/browser/runtime/loader/config.ts index f4f41750a061f..07b7750a2a2c6 100644 --- a/src/mono/browser/runtime/loader/config.ts +++ b/src/mono/browser/runtime/loader/config.ts @@ -199,10 +199,10 @@ export function normalizeConfig() { config.finalizerThreadStartDelayMs = 200; } if (config.mainThreadingMode == undefined) { - config.mainThreadingMode = MainThreadingMode.DeputyThread; + config.mainThreadingMode = MainThreadingMode.DeputyAndIOThreads; } if (config.jsThreadBlockingMode == undefined) { - config.jsThreadBlockingMode = JSThreadBlockingMode.NoBlockingWait; + config.jsThreadBlockingMode = JSThreadBlockingMode.AllowBlockingWaitInAsyncCode; } if (config.jsThreadInteropMode == undefined) { config.jsThreadInteropMode = JSThreadInteropMode.SimpleSynchronousJSInterop; @@ -214,6 +214,12 @@ export function normalizeConfig() { ) { validModes = true; } + else if (config.mainThreadingMode == MainThreadingMode.DeputyAndIOThreads + && config.jsThreadBlockingMode == JSThreadBlockingMode.AllowBlockingWaitInAsyncCode + && config.jsThreadInteropMode == JSThreadInteropMode.SimpleSynchronousJSInterop + ) { + validModes = true; + } else if (config.mainThreadingMode == MainThreadingMode.DeputyThread && config.jsThreadBlockingMode == JSThreadBlockingMode.AllowBlockingWait && config.jsThreadInteropMode == JSThreadInteropMode.SimpleSynchronousJSInterop diff --git a/src/mono/browser/runtime/managed-exports.ts b/src/mono/browser/runtime/managed-exports.ts index 5b0bdfe62a04e..df99745d6af37 100644 --- a/src/mono/browser/runtime/managed-exports.ts +++ b/src/mono/browser/runtime/managed-exports.ts @@ -3,7 +3,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; -import { GCHandle, GCHandleNull, JSMarshalerArguments, JSThreadInteropMode, MarshalerToCs, MarshalerToJs, MarshalerType, MonoMethod } from "./types/internal"; +import { GCHandle, GCHandleNull, JSMarshalerArguments, JSThreadInteropMode, MarshalerToCs, MarshalerToJs, MarshalerType, MonoMethod, PThreadPtr } from "./types/internal"; import cwraps, { threads_c_functions as twraps } from "./cwraps"; import { runtimeHelpers, Module, loaderHelpers, mono_assert } from "./globals"; import { JavaScriptMarshalerArgSize, alloc_stack_frame, get_arg, get_arg_gc_handle, is_args_exception, set_arg_i32, set_arg_intptr, set_arg_type, set_gc_handle, set_receiver_should_free } from "./marshal"; @@ -11,7 +11,7 @@ import { marshal_array_to_cs, marshal_array_to_cs_impl, marshal_bool_to_cs, mars import { marshal_int32_to_js, end_marshal_task_to_js, marshal_string_to_js, begin_marshal_task_to_js, marshal_exception_to_js } from "./marshal-to-js"; import { do_not_force_dispose, is_gcv_handle } from "./gc-handles"; import { assert_c_interop, assert_js_interop } from "./invoke-js"; -import { mono_wasm_main_thread_ptr } from "./pthreads"; +import { monoThreadInfo, mono_wasm_main_thread_ptr } from "./pthreads"; import { _zero_region, copyBytes } from "./memory"; import { stringToUTF8Ptr } from "./strings"; import { mono_log_debug } from "./logging"; @@ -62,7 +62,7 @@ export function call_entry_point(main_assembly_name: string, program_args: strin // because this is async, we could pre-allocate the promise let promise = begin_marshal_task_to_js(res, MarshalerType.TaskPreCreated, marshal_int32_to_js); - invoke_async_jsexport(managedExports.CallEntrypoint, args, size); + invoke_async_jsexport(runtimeHelpers.managedThreadTID, managedExports.CallEntrypoint, args, size); // in case the C# side returned synchronously promise = end_marshal_task_to_js(args, marshal_int32_to_js, promise); @@ -124,11 +124,12 @@ export function release_js_owned_object_by_gc_handle(gc_handle: GCHandle) { const arg1 = get_arg(args, 2); set_arg_type(arg1, MarshalerType.Object); set_gc_handle(arg1, gc_handle); - if (is_gcv_handle(gc_handle)) { - // this must stay synchronous for free_gcv_handle sake + if (!WasmEnableThreads || is_gcv_handle(gc_handle) || !monoThreadInfo.isUI) { + // this must stay synchronous for free_gcv_handle sake, to not use-after-free + // also on JSWebWorker, because the message could arrive after the worker is terminated and the GCHandle of JSProxyContext is already freed invoke_sync_jsexport(managedExports.ReleaseJSOwnedObjectByGCHandle, args); } else { - invoke_async_jsexport(managedExports.ReleaseJSOwnedObjectByGCHandle, args, size); + invoke_async_jsexport(runtimeHelpers.ioThreadTID, managedExports.ReleaseJSOwnedObjectByGCHandle, args, size); } } finally { Module.stackRestore(sp); @@ -154,7 +155,7 @@ export function complete_task(holder_gc_handle: GCHandle, error?: any, data?: an mono_assert(res_converter, "res_converter missing"); res_converter(arg3, data); } - invoke_async_jsexport(managedExports.CompleteTask, args, size); + invoke_async_jsexport(runtimeHelpers.ioThreadTID, managedExports.CompleteTask, args, size); } finally { Module.stackRestore(sp); } @@ -259,7 +260,7 @@ export function install_main_synchronization_context(jsThreadBlockingMode: numbe } } -export function invoke_async_jsexport(method: MonoMethod, args: JSMarshalerArguments, size: number): void { +export function invoke_async_jsexport(managedTID: PThreadPtr, method: MonoMethod, args: JSMarshalerArguments, size: number): void { assert_js_interop(); if (!WasmEnableThreads || runtimeHelpers.isManagedRunningOnCurrentThread) { cwraps.mono_wasm_invoke_jsexport(method, args as any); @@ -272,7 +273,7 @@ export function invoke_async_jsexport(method: MonoMethod, args: JSMarshalerArgum const bytes = JavaScriptMarshalerArgSize * size; const cpy = Module._malloc(bytes) as any; copyBytes(args as any, cpy, bytes); - twraps.mono_wasm_invoke_jsexport_async_post(runtimeHelpers.managedThreadTID, method, cpy); + twraps.mono_wasm_invoke_jsexport_async_post(managedTID, method, cpy); } } @@ -288,7 +289,7 @@ export function invoke_sync_jsexport(method: MonoMethod, args: JSMarshalerArgume throw new Error("Cannot call synchronous C# method from inside a synchronous call to a JS method."); } if (runtimeHelpers.isManagedRunningOnCurrentThread) { - cwraps.mono_wasm_invoke_jsexport(method, args as any); + twraps.mono_wasm_invoke_jsexport_sync(method, args as any); } else { // this is blocking too twraps.mono_wasm_invoke_jsexport_sync_send(runtimeHelpers.managedThreadTID, method, args as any); @@ -315,7 +316,7 @@ export function bind_assembly_exports(assemblyName: string): Promise { // because this is async, we could pre-allocate the promise let promise = begin_marshal_task_to_js(res, MarshalerType.TaskPreCreated); - invoke_async_jsexport(managedExports.BindAssemblyExports, args, size); + invoke_async_jsexport(runtimeHelpers.managedThreadTID, managedExports.BindAssemblyExports, args, size); // in case the C# side returned synchronously promise = end_marshal_task_to_js(args, marshal_int32_to_js, promise); diff --git a/src/mono/browser/runtime/multi-threading.md b/src/mono/browser/runtime/multi-threading.md index 4e308852e5034..e4b3985923d50 100644 --- a/src/mono/browser/runtime/multi-threading.md +++ b/src/mono/browser/runtime/multi-threading.md @@ -12,6 +12,13 @@ - DOM events like `onClick` need to be asynchronous, if the handler needs use synchronous `[JSImport]` - synchronous calls to `[JSImport]`/`[JSExport]` can't synchronously call back + * `MainThreadingMode.DeputyAndIOThreads` + `JSThreadBlockingMode.AllowBlockingWaitInAsyncCode` + `JSThreadInteropMode.SimpleSynchronousJSInterop` + + **default threading**, safe, tested, supported + + blocking `.Wait` is allowed on thread pool and new threads + - blocking `.Wait` throws `PlatformNotSupportedException` on `JSWebWorker` and main thread only when they are called from JS via synchronous `JSExport` + - DOM events like `onClick` need to be asynchronous, if the handler needs use synchronous `[JSImport]` + - synchronous calls to `[JSImport]`/`[JSExport]` can't synchronously call back + * `MainThreadingMode.DeputyThread` + `JSThreadBlockingMode.AllowBlockingWait` + `JSThreadInteropMode.SimpleSynchronousJSInterop` + pragmatic for legacy codebase, which contains blocking code and can't be fully executed on thread pool or new threads - ** could cause deadlocks !!!** diff --git a/src/mono/browser/runtime/pthreads/index.ts b/src/mono/browser/runtime/pthreads/index.ts index e8a8d4abf4898..0a5911605282d 100644 --- a/src/mono/browser/runtime/pthreads/index.ts +++ b/src/mono/browser/runtime/pthreads/index.ts @@ -17,3 +17,4 @@ export { } from "./worker-thread"; export { mono_wasm_start_deputy_thread_async } from "./deputy-thread"; +export { mono_wasm_start_io_thread_async } from "./io-thread"; diff --git a/src/mono/browser/runtime/pthreads/io-thread.ts b/src/mono/browser/runtime/pthreads/io-thread.ts new file mode 100644 index 0000000000000..ebbd2b82a9b79 --- /dev/null +++ b/src/mono/browser/runtime/pthreads/io-thread.ts @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import WasmEnableThreads from "consts:wasmEnableThreads"; +import BuildConfiguration from "consts:configuration"; + +import { mono_log_error, mono_log_info } from "../logging"; +import { monoThreadInfo, postMessageToMain, update_thread_info } from "./shared"; +import { Module, loaderHelpers } from "../globals"; +import { WorkerToMainMessageType } from "../types/internal"; +import { threads_c_functions as tcwraps } from "../cwraps"; + +export function mono_wasm_start_io_thread_async() { + if (!WasmEnableThreads) return; + + + if (BuildConfiguration === "Debug" && globalThis.setInterval) globalThis.setInterval(() => { + mono_log_info("I/O thread is alive!"); + }, 3000); + + try { + monoThreadInfo.isIo = true; + monoThreadInfo.threadName = "JS I/O Thread"; + update_thread_info(); + tcwraps.mono_wasm_register_io_thread(); + postMessageToMain({ + monoCmd: WorkerToMainMessageType.ioStarted, + info: monoThreadInfo, + }); + Module.runtimeKeepalivePush(); + } + catch (err) { + mono_log_error("mono_wasm_start_io_thread_async() failed", err); + loaderHelpers.mono_exit(1, err); + throw err; + } + + // same as emscripten_exit_with_live_runtime() + throw "unwind"; +} \ No newline at end of file diff --git a/src/mono/browser/runtime/pthreads/shared.ts b/src/mono/browser/runtime/pthreads/shared.ts index 5eb76b7f8fc37..c83eb3b967e03 100644 --- a/src/mono/browser/runtime/pthreads/shared.ts +++ b/src/mono/browser/runtime/pthreads/shared.ts @@ -71,14 +71,15 @@ export function update_thread_info(): void { const threadType = !monoThreadInfo.isRegistered ? "emsc" : monoThreadInfo.isUI ? "-UI-" : monoThreadInfo.isDeputy ? "dpty" - : monoThreadInfo.isTimer ? "timr" - : monoThreadInfo.isLongRunning ? "long" - : monoThreadInfo.isThreadPoolGate ? "gate" - : monoThreadInfo.isDebugger ? "dbgr" - : monoThreadInfo.isThreadPoolWorker ? "pool" - : monoThreadInfo.isExternalEventLoop ? "jsww" - : monoThreadInfo.isBackground ? "back" - : "norm"; + : monoThreadInfo.isIo ? "-IO-" + : monoThreadInfo.isTimer ? "timr" + : monoThreadInfo.isLongRunning ? "long" + : monoThreadInfo.isThreadPoolGate ? "gate" + : monoThreadInfo.isDebugger ? "dbgr" + : monoThreadInfo.isThreadPoolWorker ? "pool" + : monoThreadInfo.isExternalEventLoop ? "jsww" + : monoThreadInfo.isBackground ? "back" + : "norm"; const hexPtr = (monoThreadInfo.pthreadId as any).toString(16).padStart(8, "0"); const hexPrefix = monoThreadInfo.isRegistered ? "0x" : "--"; monoThreadInfo.threadPrefix = `${hexPrefix}${hexPtr}-${threadType}`; diff --git a/src/mono/browser/runtime/pthreads/ui-thread.ts b/src/mono/browser/runtime/pthreads/ui-thread.ts index baa72e7a3b975..9838615cd2163 100644 --- a/src/mono/browser/runtime/pthreads/ui-thread.ts +++ b/src/mono/browser/runtime/pthreads/ui-thread.ts @@ -100,6 +100,9 @@ function monoWorkerMessageHandler(worker: PThreadWorker, ev: MessageEvent): case WorkerToMainMessageType.deputyStarted: runtimeHelpers.afterMonoStarted.promise_control.resolve(message.deputyProxyGCHandle); break; + case WorkerToMainMessageType.ioStarted: + runtimeHelpers.afterIOStarted.promise_control.resolve(); + break; case WorkerToMainMessageType.deputyFailed: runtimeHelpers.afterMonoStarted.promise_control.reject(new Error(message.error)); break; diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index 41ce07e37d3c4..1ae17e43edf19 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -2,9 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. import WasmEnableThreads from "consts:wasmEnableThreads"; +import BuildConfiguration from "consts:configuration"; import { DotnetModuleInternal, CharPtrNull, MainThreadingMode } from "./types/internal"; -import { ENVIRONMENT_IS_NODE, exportedRuntimeAPI, INTERNAL, loaderHelpers, Module, runtimeHelpers, createPromiseController, mono_assert, ENVIRONMENT_IS_WORKER } from "./globals"; +import { exportedRuntimeAPI, INTERNAL, loaderHelpers, Module, runtimeHelpers, createPromiseController, mono_assert } from "./globals"; import cwraps, { init_c_exports, threads_c_functions as tcwraps } from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; import { toBase64StringImpl } from "./base64"; @@ -20,7 +21,7 @@ import { wait_for_all_assets } from "./assets"; import { replace_linker_placeholders } from "./exports-binding"; import { endMeasure, MeasuredBlock, startMeasure } from "./profiler"; import { interp_pgo_load_data, interp_pgo_save_data } from "./interp-pgo"; -import { mono_log_debug, mono_log_error, mono_log_warn } from "./logging"; +import { mono_log_debug, mono_log_error, mono_log_info, mono_log_warn } from "./logging"; // threads import { populateEmscriptenPool, mono_wasm_init_threads, init_finalizer_thread } from "./pthreads"; @@ -269,8 +270,13 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { Module.runtimeKeepalivePush(); + if (WasmEnableThreads && BuildConfiguration === "Debug" && globalThis.setInterval) globalThis.setInterval(() => { + mono_log_info("UI thread is alive!"); + }, 3000); - if (WasmEnableThreads && runtimeHelpers.config.mainThreadingMode == MainThreadingMode.DeputyThread) { + if (WasmEnableThreads && + (runtimeHelpers.config.mainThreadingMode == MainThreadingMode.DeputyThread + || runtimeHelpers.config.mainThreadingMode == MainThreadingMode.DeputyAndIOThreads)) { // this will create thread and call start_runtime() on it runtimeHelpers.monoThreadInfo = monoThreadInfo; runtimeHelpers.isManagedRunningOnCurrentThread = false; @@ -278,6 +284,10 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { runtimeHelpers.managedThreadTID = tcwraps.mono_wasm_create_deputy_thread(); runtimeHelpers.proxyGCHandle = await runtimeHelpers.afterMonoStarted.promise; + if (WasmEnableThreads && runtimeHelpers.config.mainThreadingMode == MainThreadingMode.DeputyAndIOThreads) { + runtimeHelpers.ioThreadTID = tcwraps.mono_wasm_create_io_thread(); + } + // TODO make UI thread not managed tcwraps.mono_wasm_register_ui_thread(); monoThreadInfo.isAttached = true; @@ -291,8 +301,8 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { await start_runtime(); } - if (ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER) { - Module.runtimeKeepalivePush(); + if (WasmEnableThreads && runtimeHelpers.config.mainThreadingMode == MainThreadingMode.DeputyAndIOThreads) { + await runtimeHelpers.afterIOStarted.promise; } runtimeList.registerRuntime(exportedRuntimeAPI); diff --git a/src/mono/browser/runtime/types/internal.ts b/src/mono/browser/runtime/types/internal.ts index 5b32ec09f9b4f..16967d26b9d29 100644 --- a/src/mono/browser/runtime/types/internal.ts +++ b/src/mono/browser/runtime/types/internal.ts @@ -209,6 +209,7 @@ export type RuntimeHelpers = { monoThreadInfo: PThreadInfo, proxyGCHandle: GCHandle | undefined, managedThreadTID: PThreadPtr, + ioThreadTID: PThreadPtr, currentThreadTID: PThreadPtr, isManagedRunningOnCurrentThread: boolean, isPendingSynchronousCall: boolean, // true when we are in the middle of a synchronous call from managed code from same thread @@ -222,6 +223,7 @@ export type RuntimeHelpers = { afterPreRun: PromiseAndController, beforeOnRuntimeInitialized: PromiseAndController, afterMonoStarted: PromiseAndController, + afterIOStarted: PromiseAndController, afterOnRuntimeInitialized: PromiseAndController, afterPostRun: PromiseAndController, @@ -495,6 +497,7 @@ export const enum WorkerToMainMessageType { deputyCreated = "createdDeputy", deputyFailed = "deputyFailed", deputyStarted = "monoStarted", + ioStarted = "ioStarted", preload = "preload", } @@ -525,6 +528,7 @@ export interface PThreadInfo { isRunning?: boolean, isAttached?: boolean, isDeputy?: boolean, + isIo?: boolean, isExternalEventLoop?: boolean, isUI?: boolean; isBackground?: boolean, @@ -573,6 +577,8 @@ export const enum MainThreadingMode { UIThread = 0, // Running the managed main thread on dedicated WebWorker. Marshaling all JavaScript calls to and from the main thread. DeputyThread = 1, + // TODO comment + DeputyAndIOThreads = 2, } // keep in sync with JSHostImplementation.Types.cs @@ -580,6 +586,8 @@ export const enum JSThreadBlockingMode { // throw PlatformNotSupportedException if blocking .Wait is called on threads with JS interop, like JSWebWorker and Main thread. // Avoids deadlocks (typically with pending JS promises on the same thread) by throwing exceptions. NoBlockingWait = 0, + // TODO comment + AllowBlockingWaitInAsyncCode = 1, // allow .Wait on all threads. // Could cause deadlocks with blocking .Wait on a pending JS Task/Promise on the same thread or similar Task/Promise chain. AllowBlockingWait = 100, diff --git a/src/mono/mono/utils/mono-threads-wasm.c b/src/mono/mono/utils/mono-threads-wasm.c index ca11139ce43b3..cfd411c712c38 100644 --- a/src/mono/mono/utils/mono-threads-wasm.c +++ b/src/mono/mono/utils/mono-threads-wasm.c @@ -496,7 +496,7 @@ mono_threads_wasm_on_thread_attached (pthread_t tid, const char* thread_name, gb #else if (mono_threads_wasm_is_ui_thread ()) { // FIXME: we should not be attaching UI thread with deputy design - // but right now we do, because mono_wasm_load_runtime is running in UI thread + // but right now we do // g_assert(!mono_threads_wasm_is_ui_thread ()); return; } @@ -542,7 +542,9 @@ mono_threads_wasm_on_thread_registered (void) #ifndef DISABLE_THREADS static pthread_t deputy_thread_tid; +static pthread_t io_thread_tid; extern void mono_wasm_start_deputy_thread_async (void); +extern void mono_wasm_start_io_thread_async (void); extern void mono_wasm_trace_logger (const char *log_domain, const char *log_level, const char *message, mono_bool fatal, void *user_data); extern void mono_wasm_dump_threads (void); @@ -582,6 +584,37 @@ mono_wasm_create_deputy_thread (void) return deputy_thread_tid; } +gboolean +mono_threads_wasm_is_io_thread (void) +{ + return pthread_self () == io_thread_tid; +} + +MonoNativeThreadId +mono_threads_wasm_io_thread_tid (void) +{ + return (MonoNativeThreadId) io_thread_tid; +} + +// this is running in io thread +static gsize +io_thread_fn (void* unused_arg G_GNUC_UNUSED) +{ + io_thread_tid = pthread_self (); + + // this will throw JS "unwind" + mono_wasm_start_io_thread_async(); + + return 0;// never reached +} + +EMSCRIPTEN_KEEPALIVE MonoNativeThreadId +mono_wasm_create_io_thread (void) +{ + pthread_create (&io_thread_tid, NULL, (void *(*)(void *)) io_thread_fn, NULL); + return io_thread_tid; +} + // TODO ideally we should not need to have UI thread registered as managed EMSCRIPTEN_KEEPALIVE void mono_wasm_register_ui_thread (void) @@ -596,6 +629,20 @@ mono_wasm_register_ui_thread (void) MONO_ENTER_GC_SAFE_UNBALANCED; } +// TODO ideally we should not need to have UI thread registered as managed +EMSCRIPTEN_KEEPALIVE void +mono_wasm_register_io_thread (void) +{ + MonoThread *thread = mono_thread_internal_attach (mono_get_root_domain ()); + mono_thread_set_state (thread, ThreadState_Background); + mono_thread_info_set_flags (MONO_THREAD_INFO_FLAGS_NONE); + + MonoThreadInfo *info = mono_thread_info_current_unchecked (); + g_assert (info); + info->runtime_thread = TRUE; + MONO_ENTER_GC_SAFE_UNBALANCED; +} + void mono_threads_wasm_async_run_in_target_thread (pthread_t target_thread, void (*func) (void)) { diff --git a/src/mono/mono/utils/mono-threads-wasm.h b/src/mono/mono/utils/mono-threads-wasm.h index 1aa68ee7d329c..ad9dc40b32db0 100644 --- a/src/mono/mono/utils/mono-threads-wasm.h +++ b/src/mono/mono/utils/mono-threads-wasm.h @@ -34,15 +34,27 @@ mono_wasm_dump_threads_async (void); gboolean mono_threads_wasm_is_deputy_thread (void); +gboolean +mono_threads_wasm_is_io_thread (void); + MonoNativeThreadId mono_threads_wasm_deputy_thread_tid (void); +MonoNativeThreadId +mono_threads_wasm_io_thread_tid (void); + MonoNativeThreadId mono_wasm_create_deputy_thread (void); +MonoNativeThreadId +mono_wasm_create_io_thread (void); + void mono_wasm_register_ui_thread (void); +void +mono_wasm_register_io_thread (void); + void mono_threads_wasm_async_run_in_target_thread (pthread_t target_thread, void (*func) (void)); diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorWasmTestBase.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorWasmTestBase.cs index 64bbe81cc7d4d..d66fa1d04a0fb 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorWasmTestBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorWasmTestBase.cs @@ -218,6 +218,7 @@ public async Task BlazorRunTest(string runArgs, Assert.Equal("Current count: 0", txt); await page.Locator("text=\"Click me\"").ClickAsync(); + await Task.Delay(300); txt = await page.Locator("p[role='status']").InnerHTMLAsync(); Assert.Equal("Current count: 1", txt); }