From 5a16dc70083eae491d171a90b2b27d02620c4e4c Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 23 Jul 2026 16:05:09 -0500 Subject: [PATCH 1/3] Fix CoreCLR startup-aware locking Mark CoreCLR startup complete after managed runtime initialization so lazy assembly and DSO operations use their synchronization primitives. Add a concurrent cold-start assembly decompression stress test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7bf1a651-5488-4ef9-92e6-863f27e23da5 --- src/native/clr/host/host.cc | 4 + .../Tests/InstallAndRunTests.cs | 87 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/src/native/clr/host/host.cc b/src/native/clr/host/host.cc index 70585533641..477db0e350e 100644 --- a/src/native/clr/host/host.cc +++ b/src/native/clr/host/host.cc @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -483,6 +484,9 @@ void Host::Java_mono_android_Runtime_initInternal ( internal_timing.end_event (); // native to managed internal_timing.end_event (); // total init time } + + // Managed initialization can load assemblies and DSOs, so enable locking only after it returns. + MonodroidState::mark_startup_done (); } void Host::Java_mono_android_Runtime_register (JNIEnv *env, jstring managedType, jclass nativeClass, jstring methods) noexcept diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index f854e6f70c6..e80ad008f16 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -659,6 +659,93 @@ public void DeployToDevice ([Values] bool isRelease, [Values (AndroidRuntime.Cor Assert.IsTrue (didLaunch, "Activity should have started."); } + [Test] + public void ConcurrentAssemblyStoreLoadsUseStartupLock () + { + const int assemblyCount = 8; + const string expectedLogcatOutput = "STARTUP_LOCK_STRESS_DONE"; + + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: true)) { + return; + } + + var path = Path.Combine ("temp", TestName); + var app = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (AndroidRuntime.CoreCLR, "startuplock")) { + IsRelease = true, + AndroidLinkModeRelease = AndroidLinkMode.SdkOnly, + }; + app.SetRuntime (AndroidRuntime.CoreCLR); + app.SetRuntimeIdentifiers (new [] { DeviceAbi }); + app.SetProperty ("AndroidEnablePreloadAssemblies", "false"); + + var taskBodies = new StringBuilder (); + for (int i = 0; i < assemblyCount; i++) { + string projectName = $"StartupLockLibrary{i}"; + var library = new XamarinAndroidLibraryProject { + IsRelease = true, + ProjectName = projectName, + }; + library.Sources.Add (new BuildItem.Source ("Marker.cs") { + TextContent = () => $""" + namespace {projectName}; + + public static class Marker + {{ + public static int Value => {i}; + }} + """, + }); + app.References.Add (new BuildItem.ProjectReference ($"..\\{projectName}\\{projectName}.csproj", projectName, library.ProjectGuid)); + + using var libraryBuilder = CreateDllBuilder (Path.Combine (path, projectName)); + Assert.IsTrue (libraryBuilder.Build (library), $"{projectName} build should have succeeded."); + + taskBodies.AppendLine ($""" + global::System.Threading.Tasks.Task.Run (() => {{ + ready.Signal (); + gate.Wait (); + var assembly = global::System.Reflection.Assembly.Load ("{projectName}"); + var type = assembly.GetType ("{projectName}.Marker", throwOnError: true); + var property = type.GetProperty ("Value") ?? throw new global::System.MissingMemberException (type.FullName, "Value"); + if (!global::System.Object.Equals (property.GetValue (null), {i})) {{ + throw new global::System.InvalidOperationException ("Failed to load {projectName}."); + }} + }}), + """); + } + + app.MainActivity = app.DefaultMainActivity.Replace ("//${AFTER_ONCREATE}", $$""" + using var ready = new global::System.Threading.CountdownEvent ({{assemblyCount}}); + using var gate = new global::System.Threading.ManualResetEventSlim (); + global::System.Threading.Tasks.Task[] tasks = [ + {{taskBodies}} + ]; + if (!ready.Wait (global::System.TimeSpan.FromSeconds (30))) { + throw new global::System.TimeoutException ("Timed out starting concurrent assembly loads."); + } + gate.Set (); + global::System.Threading.Tasks.Task.WaitAll (tasks); + global::Android.Util.Log.Info ("StartupLockTest", "{{expectedLogcatOutput}}"); + """); + + using var appBuilder = CreateApkBuilder (Path.Combine (path, app.ProjectName)); + Assert.IsTrue (appBuilder.Install (app), "Install should have succeeded."); + + for (int attempt = 0; attempt < 10; attempt++) { + ClearAdbLogcat (); + AdbStartActivity ($"{app.PackageName}/{app.JavaPackageName}.MainActivity"); + Assert.IsTrue ( + MonitorAdbLogcat ( + line => line.Contains (expectedLogcatOutput), + Path.Combine (Root, appBuilder.ProjectDirectory, $"startup-lock-{attempt}.log"), + ActivityStartTimeoutInSeconds + ), + $"Concurrent assembly load attempt {attempt} did not complete." + ); + RunAdbCommand ($"shell am force-stop --user all {app.PackageName}"); + } + } + [Test] public void AssemblyStoreDecompressionCacheMapsPersistedAssemblies () { From a5e5bd971e0797d2b10e3fe25d1ff6ba75490f66 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 23 Jul 2026 16:07:38 -0500 Subject: [PATCH 2/3] Remove speculative startup lock stress test Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7bf1a651-5488-4ef9-92e6-863f27e23da5 --- .../Tests/InstallAndRunTests.cs | 87 ------------------- 1 file changed, 87 deletions(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index e80ad008f16..f854e6f70c6 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -659,93 +659,6 @@ public void DeployToDevice ([Values] bool isRelease, [Values (AndroidRuntime.Cor Assert.IsTrue (didLaunch, "Activity should have started."); } - [Test] - public void ConcurrentAssemblyStoreLoadsUseStartupLock () - { - const int assemblyCount = 8; - const string expectedLogcatOutput = "STARTUP_LOCK_STRESS_DONE"; - - if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: true)) { - return; - } - - var path = Path.Combine ("temp", TestName); - var app = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (AndroidRuntime.CoreCLR, "startuplock")) { - IsRelease = true, - AndroidLinkModeRelease = AndroidLinkMode.SdkOnly, - }; - app.SetRuntime (AndroidRuntime.CoreCLR); - app.SetRuntimeIdentifiers (new [] { DeviceAbi }); - app.SetProperty ("AndroidEnablePreloadAssemblies", "false"); - - var taskBodies = new StringBuilder (); - for (int i = 0; i < assemblyCount; i++) { - string projectName = $"StartupLockLibrary{i}"; - var library = new XamarinAndroidLibraryProject { - IsRelease = true, - ProjectName = projectName, - }; - library.Sources.Add (new BuildItem.Source ("Marker.cs") { - TextContent = () => $""" - namespace {projectName}; - - public static class Marker - {{ - public static int Value => {i}; - }} - """, - }); - app.References.Add (new BuildItem.ProjectReference ($"..\\{projectName}\\{projectName}.csproj", projectName, library.ProjectGuid)); - - using var libraryBuilder = CreateDllBuilder (Path.Combine (path, projectName)); - Assert.IsTrue (libraryBuilder.Build (library), $"{projectName} build should have succeeded."); - - taskBodies.AppendLine ($""" - global::System.Threading.Tasks.Task.Run (() => {{ - ready.Signal (); - gate.Wait (); - var assembly = global::System.Reflection.Assembly.Load ("{projectName}"); - var type = assembly.GetType ("{projectName}.Marker", throwOnError: true); - var property = type.GetProperty ("Value") ?? throw new global::System.MissingMemberException (type.FullName, "Value"); - if (!global::System.Object.Equals (property.GetValue (null), {i})) {{ - throw new global::System.InvalidOperationException ("Failed to load {projectName}."); - }} - }}), - """); - } - - app.MainActivity = app.DefaultMainActivity.Replace ("//${AFTER_ONCREATE}", $$""" - using var ready = new global::System.Threading.CountdownEvent ({{assemblyCount}}); - using var gate = new global::System.Threading.ManualResetEventSlim (); - global::System.Threading.Tasks.Task[] tasks = [ - {{taskBodies}} - ]; - if (!ready.Wait (global::System.TimeSpan.FromSeconds (30))) { - throw new global::System.TimeoutException ("Timed out starting concurrent assembly loads."); - } - gate.Set (); - global::System.Threading.Tasks.Task.WaitAll (tasks); - global::Android.Util.Log.Info ("StartupLockTest", "{{expectedLogcatOutput}}"); - """); - - using var appBuilder = CreateApkBuilder (Path.Combine (path, app.ProjectName)); - Assert.IsTrue (appBuilder.Install (app), "Install should have succeeded."); - - for (int attempt = 0; attempt < 10; attempt++) { - ClearAdbLogcat (); - AdbStartActivity ($"{app.PackageName}/{app.JavaPackageName}.MainActivity"); - Assert.IsTrue ( - MonitorAdbLogcat ( - line => line.Contains (expectedLogcatOutput), - Path.Combine (Root, appBuilder.ProjectDirectory, $"startup-lock-{attempt}.log"), - ActivityStartTimeoutInSeconds - ), - $"Concurrent assembly load attempt {attempt} did not complete." - ); - RunAdbCommand ($"shell am force-stop --user all {app.PackageName}"); - } - } - [Test] public void AssemblyStoreDecompressionCacheMapsPersistedAssemblies () { From 4c0fd19eccba1e67b650ab526f7929a79858c6ab Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 23 Jul 2026 16:09:38 -0500 Subject: [PATCH 3/3] Match Mono startup transition placement Remove the CoreCLR-only comment so the startup completion call mirrors the existing Mono implementation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7bf1a651-5488-4ef9-92e6-863f27e23da5 --- src/native/clr/host/host.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/src/native/clr/host/host.cc b/src/native/clr/host/host.cc index 477db0e350e..f1c3fa1e8e4 100644 --- a/src/native/clr/host/host.cc +++ b/src/native/clr/host/host.cc @@ -485,7 +485,6 @@ void Host::Java_mono_android_Runtime_initInternal ( internal_timing.end_event (); // total init time } - // Managed initialization can load assemblies and DSOs, so enable locking only after it returns. MonodroidState::mark_startup_done (); }