[wasm] Build static components; include hot_reload in runtime (#54568)
authorAleksey Kliger (λgeek) <alklig@microsoft.com>
Thu, 24 Jun 2021 00:13:37 +0000 (20:13 -0400)
committerGitHub <noreply@github.com>
Thu, 24 Jun 2021 00:13:37 +0000 (20:13 -0400)
* [wasm] Build static components; include hot_reload in runtime

   Workaround until https://github.com/dotnet/runtime/issues/54565 is fixed
   Build the runtime always with support for hot_reload, and without diagnostics_tracing

* Update wasm.proj

* Add a browser functional test for hot reload

   Just check that the capabilities are non-empty which is a good proxy for hot reload being enabled in the runtime.

* Turn off trimming for hot reload functional test

* Disable test on browser AOT

* fix whitespace

Co-authored-by: Thays Grazia <thaystg@gmail.com>
14 files changed:
src/libraries/System.Runtime.Loader/tests/ApplyUpdate/System.Reflection.Metadata.ApplyUpdate.Test.MethodBody1/System.Reflection.Metadata.ApplyUpdate.Test.MethodBody1.csproj
src/libraries/System.Runtime.Loader/tests/ApplyUpdateTest.cs
src/mono/mono.proj
src/mono/wasm/Makefile
src/mono/wasm/wasm.proj
src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/ApplyUpdateReferencedAssembly.csproj [new file with mode: 0644]
src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/MethodBody1.cs [new file with mode: 0644]
src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/MethodBody1_v1.cs [new file with mode: 0644]
src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/MethodBody1_v2.cs [new file with mode: 0644]
src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/deltascript.json [new file with mode: 0644]
src/tests/FunctionalTests/WebAssembly/Browser/HotReload/Program.cs [new file with mode: 0644]
src/tests/FunctionalTests/WebAssembly/Browser/HotReload/WebAssembly.Browser.HotReload.Test.csproj [new file with mode: 0644]
src/tests/FunctionalTests/WebAssembly/Browser/HotReload/index.html [new file with mode: 0644]
src/tests/FunctionalTests/WebAssembly/Browser/HotReload/runtime.js [new file with mode: 0644]

index e533377..2a953d1 100644 (file)
@@ -18,6 +18,7 @@ namespace System.Reflection.Metadata
     public class ApplyUpdateTest
     {
         [Fact]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/54617", typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser), nameof(PlatformDetection.IsMonoAOT))]
         void StaticMethodBodyUpdate()
         {
             ApplyUpdateUtil.TestCase(static () =>
index e5d4bb2..e459f87 100644 (file)
       <_MonoCMakeArgs Include="-DENABLE_INTERP_LIB=1"/>
       <_MonoCMakeArgs Include="-DDISABLE_ICALL_TABLES=1"/>
       <_MonoCMakeArgs Include="-DDISABLE_CRASH_REPORTING=1"/>
-      <_MonoCMakeArgs Include="-DDISABLE_COMPONENTS=1"/>
       <_MonoCMakeArgs Include="-DENABLE_ICALL_EXPORT=1"/>
       <_MonoCMakeArgs Include="-DENABLE_LAZY_GC_THREAD_CREATION=1"/>
       <_MonoCMakeArgs Include="-DENABLE_LLVM_RUNTIME=1"/>
index 9f6d055..86288cd 100644 (file)
@@ -48,12 +48,20 @@ provision-wasm: .stamp-wasm-install-and-select-$(EMSCRIPTEN_VERSION)
        @echo "----------------------------------------------------------"
        @echo "Installed emsdk into EMSDK_PATH=$(TOP)/src/mono/wasm/emsdk"
 
+# FIXME: When https://github.com/dotnet/runtime/issues/54565 is fixed, and the WasmApp targets are updated to use mono runtime components, remove this
+MONO_COMPONENT_LIBS= \
+       $(MONO_BIN_DIR)/libmono-component-hot_reload-static.a \
+       $(MONO_BIN_DIR)/libmono-component-diagnostics_tracing-stub-static.a
+
 MONO_OBJ_DIR=$(OBJDIR)/mono/Browser.wasm.$(CONFIG)
 MONO_INCLUDE_DIR=$(MONO_BIN_DIR)/include/mono-2.0
 BUILDS_OBJ_DIR=$(MONO_OBJ_DIR)/wasm
+# libmonosgen-2.0 is in MONO_LIBS twice because the components and the runtime are depend on each other
 MONO_LIBS = \
        $(MONO_BIN_DIR)/libmono-ee-interp.a \
        $(MONO_BIN_DIR)/libmonosgen-2.0.a \
+       $(MONO_COMPONENT_LIBS) \
+       $(MONO_BIN_DIR)/libmonosgen-2.0.a \
        $(MONO_BIN_DIR)/libmono-ilgen.a \
        $(MONO_BIN_DIR)/libmono-icall-table.a \
        $(MONO_BIN_DIR)/libmono-profiler-aot.a \
index 489d882..ffc2fb9 100644 (file)
     <ItemGroup>
       <ICULibNativeFiles Include="$(ICULibDir)/libicuuc.a;
                                   $(ICULibDir)/libicui18n.a" />
+      <MonoComponentLibs Include="$(MonoArtifactsPath)libmono-component-hot_reload-static.a;
+                                  $(MonoArtifactsPath)libmono-component-diagnostics_tracing-stub-static.a" />
       <MonoLibFiles Include="$(MonoArtifactsPath)libmono-ee-interp.a;
                              $(MonoArtifactsPath)libmonosgen-2.0.a;
                              $(MonoArtifactsPath)libmono-ilgen.a;
                              $(NativeBinDir)libSystem.Native.a;
                              $(NativeBinDir)libSystem.IO.Compression.Native.a" />
       <MonoLibFiles Include="@(ICULibNativeFiles)" />
+      <MonoLibFiles Include="@(MonoComponentLibs)" />
       <PInvokeTableFile Include="$(WasmObjDir)\pinvoke-table.h" />
       <ICULibFiles Include="$(ICULibDir)/*.dat" />
     </ItemGroup>
diff --git a/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/ApplyUpdateReferencedAssembly.csproj b/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/ApplyUpdateReferencedAssembly.csproj
new file mode 100644 (file)
index 0000000..46e260f
--- /dev/null
@@ -0,0 +1,30 @@
+<Project Sdk="Microsoft.NET.Sdk" TreatAsLocalProperty="EnableAggressiveTrimming;PublishTrimmed">
+  <PropertyGroup>
+    <TestRuntime>true</TestRuntime>
+    <DeltaScript>deltascript.json</DeltaScript>
+    <OutputType>library</OutputType>
+    <IsTestProject>false</IsTestProject>
+    <IsTestSupportProject>true</IsTestSupportProject>
+    <!-- to call AsssemblyExtensions.ApplyUpdate we need Optimize=false, EmitDebugInformation=true in all configurations -->
+    <Optimize>false</Optimize>
+    <EmitDebugInformation>true</EmitDebugInformation>
+    <!-- hot reload is not compatible with trimming -->
+    <EnableAggressiveTrimming>false</EnableAggressiveTrimming>
+    <PublishTrimmed>false</PublishTrimmed>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Compile Include="MethodBody1.cs" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <!-- This package from https://github.com/dotnet/hotreload-utils provides
+         targets that read the json delta script and generates deltas based on the baseline assembly and the modified sources.
+
+         Projects must define the DeltaScript property that specifies the (relative) path to the json script.
+         Deltas will be emitted next to the output assembly.  Deltas will be copied when the current
+         project is referenced from other other projects.
+    -->
+    <PackageReference Include="Microsoft.DotNet.HotReload.Utils.Generator.BuildTool" Version="$(MicrosoftDotNetHotReloadUtilsGeneratorBuildToolVersion)" />
+  </ItemGroup>
+</Project>
diff --git a/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/MethodBody1.cs b/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/MethodBody1.cs
new file mode 100644 (file)
index 0000000..9e98604
--- /dev/null
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace ApplyUpdateReferencedAssembly
+{
+    public class MethodBody1 {
+        public static string StaticMethod1 () {
+            return "OLD STRING";
+        }
+    }
+}
diff --git a/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/MethodBody1_v1.cs b/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/MethodBody1_v1.cs
new file mode 100644 (file)
index 0000000..4aab1e8
--- /dev/null
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace ApplyUpdateReferencedAssembly
+{
+    public class MethodBody1 {
+        public static string StaticMethod1 () {
+            return "NEW STRING";
+        }
+    }
+}
diff --git a/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/MethodBody1_v2.cs b/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/MethodBody1_v2.cs
new file mode 100644 (file)
index 0000000..83f0142
--- /dev/null
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace ApplyUpdateReferencedAssembly
+{
+    public class MethodBody1 {
+        public static string StaticMethod1 () {
+            return "NEWEST STRING";
+        }
+    }
+}
diff --git a/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/deltascript.json b/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/ApplyUpdateReferencedAssembly/deltascript.json
new file mode 100644 (file)
index 0000000..8e73836
--- /dev/null
@@ -0,0 +1,7 @@
+{
+    "changes": [
+        {"document": "MethodBody1.cs", "update": "MethodBody1_v1.cs"},
+        {"document": "MethodBody1.cs", "update": "MethodBody1_v2.cs"},
+    ]
+}
+
diff --git a/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/Program.cs b/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/Program.cs
new file mode 100644 (file)
index 0000000..cb005b0
--- /dev/null
@@ -0,0 +1,81 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+namespace Sample
+{
+    public class Test
+    {
+        public static void Main(string[] args)
+        {
+            Console.WriteLine ("Hello, World!");
+        }
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        public static int TestMeaning()
+        {
+            const int success = 42;
+            const int failure = 1;
+
+            var ty = typeof(System.Reflection.Metadata.AssemblyExtensions);
+            var mi = ty.GetMethod("GetApplyUpdateCapabilities", BindingFlags.NonPublic | BindingFlags.Static, Array.Empty<Type>());
+
+            if (mi == null)
+                return failure;
+
+            var caps = mi.Invoke(null, null) as string;
+
+            if (String.IsNullOrEmpty(caps))
+                return failure;
+
+            var assm = typeof (ApplyUpdateReferencedAssembly.MethodBody1).Assembly;
+
+            var r = ApplyUpdateReferencedAssembly.MethodBody1.StaticMethod1();
+            if ("OLD STRING" != r)
+                return failure;
+
+            ApplyUpdate(assm);
+
+            r = ApplyUpdateReferencedAssembly.MethodBody1.StaticMethod1();
+            if ("NEW STRING" != r)
+                return failure;
+
+            ApplyUpdate(assm);
+
+            r = ApplyUpdateReferencedAssembly.MethodBody1.StaticMethod1();
+            if ("NEWEST STRING" != r)
+                return failure;
+
+            return success;
+        }
+
+        private static System.Collections.Generic.Dictionary<Assembly, int> assembly_count = new();
+
+        internal static void ApplyUpdate (System.Reflection.Assembly assm)
+        {
+            int count;
+            if (!assembly_count.TryGetValue(assm, out count))
+                count = 1;
+            else
+                count++;
+            assembly_count [assm] = count;
+
+            /* FIXME WASM: Location is empty on wasm. Make up a name based on Name */
+            string basename = assm.Location;
+            if (basename == "")
+                basename = assm.GetName().Name + ".dll";
+            Console.Error.WriteLine($"Apply Delta Update for {basename}, revision {count}");
+
+            string dmeta_name = $"{basename}.{count}.dmeta";
+            string dil_name = $"{basename}.{count}.dil";
+            byte[] dmeta_data = System.IO.File.ReadAllBytes(dmeta_name);
+            byte[] dil_data = System.IO.File.ReadAllBytes(dil_name);
+            byte[] dpdb_data = null; // TODO also use the dpdb data
+
+            System.Reflection.Metadata.AssemblyExtensions.ApplyUpdate(assm, dmeta_data, dil_data, dpdb_data);
+        }
+    }
+}
diff --git a/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/WebAssembly.Browser.HotReload.Test.csproj b/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/WebAssembly.Browser.HotReload.Test.csproj
new file mode 100644 (file)
index 0000000..2ba0552
--- /dev/null
@@ -0,0 +1,52 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <MonoForceInterpreter>true</MonoForceInterpreter>
+    <RunAOTCompilation>false</RunAOTCompilation>
+    <PublishTrimmed>false</PublishTrimmed>
+    <TestRuntime>true</TestRuntime>
+    <Scenario>WasmTestOnBrowser</Scenario>
+    <ExpectedExitCode>42</ExpectedExitCode>
+    <WasmMainJSPath>runtime.js</WasmMainJSPath>
+    <EnableDefaultItems>false</EnableDefaultItems>
+    <!-- setting WasmXHarnessMonoArgs doesn't work here, but see runtime.js -->
+    <!-- <WasmXHarnessMonoArgs>- -setenv=DOTNET_MODIFIABLE_ASSEMBLIES=debug</WasmXHarnessMonoArgs> -->
+  </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="Program.cs" />
+    <Content Include="index.html">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </Content>
+    <ProjectReference Include="ApplyUpdateReferencedAssembly\ApplyUpdateReferencedAssembly.csproj" />
+  </ItemGroup>
+
+  <Target Name="AfterWasmBuildApp" AfterTargets="WasmBuildApp">
+    <Copy SourceFiles="$(OutDir)\index.html" DestinationFolder="$(WasmAppDir)" />
+  </Target>
+
+  <Target Name="PreserveEnCAssembliesFromLinking"
+          Condition="'$(TargetOS)' == 'Browser' and '$(EnableAggressiveTrimming)' == 'true'"
+          BeforeTargets="ConfigureTrimming">
+    <ItemGroup>
+      <!-- Don't modify EnC test assemblies -->
+      <TrimmerRootAssembly
+          Condition="$([System.String]::Copy('%(ResolvedFileToPublish.FileName)%(ResolvedFileToPublish.Extension)').EndsWith('ApplyUpdateReferencedAssembly.dll'))"
+          Include="%(ResolvedFileToPublish.FullPath)" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="IncludeDeltasInWasmBundle"
+          BeforeTargets="PrepareForWasmBuildApp"
+          Condition="'$(TargetOS)' == 'Browser'">
+    <ItemGroup>
+      <!-- FIXME: this belongs in eng/testing/tests.wasm.targets -->
+      <!-- FIXME: Can we do something on the Content items in the referenced projects themselves to get this for free? -->
+      <WasmFilesToIncludeInFileSystem Include="@(PublishItemsOutputGroupOutputs)"
+                                      Condition="$([System.String]::new('%(PublishItemsOutputGroupOutputs.Identity)').EndsWith('.dmeta'))" />
+      <WasmFilesToIncludeInFileSystem Include="@(PublishItemsOutputGroupOutputs)"
+                                      Condition="$([System.String]::new('%(PublishItemsOutputGroupOutputs.Identity)').EndsWith('.dil'))" />
+      <WasmFilesToIncludeInFileSystem Include="@(PublishItemsOutputGroupOutputs)"
+                                      Condition="$([System.String]::new('%(PublishItemsOutputGroupOutputs.Identity)').EndsWith('.dpdb'))" />
+    </ItemGroup>
+  </Target>
+  
+</Project>
diff --git a/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/index.html b/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/index.html
new file mode 100644 (file)
index 0000000..ad7cc21
--- /dev/null
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<!--  Licensed to the .NET Foundation under one or more agreements. -->
+<!-- The .NET Foundation licenses this file to you under the MIT license. -->
+<html>
+  <head>
+    <title>TESTS</title>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  </head>
+  <body onload="onLoad()">
+    <h3 id="header">Wasm Browser Sample</h3>
+    Result from Sample.Test.TestMeaning: <span id="out"></span>
+    <script type='text/javascript'>
+      var is_testing = false;
+      var onLoad = function() {
+        var url = new URL(decodeURI(window.location));
+        let args = url.searchParams.getAll('arg');
+        is_testing = args !== undefined && (args.find(arg => arg == '--testing') !== undefined);
+      };
+
+      var test_exit = function(exit_code)
+      {
+        if (!is_testing) {
+          console.log(`test_exit: ${exit_code}`);
+          return;
+        }
+
+        /* Set result in a tests_done element, to be read by xharness */
+        var tests_done_elem = document.createElement("label");
+        tests_done_elem.id = "tests_done";
+        tests_done_elem.innerHTML = exit_code.toString();
+        document.body.appendChild(tests_done_elem);
+
+        console.log(`WASM EXIT ${exit_code}`);
+      };
+
+      var App = {
+        init: function () {
+          var exit_code = BINDING.call_static_method("[WebAssembly.Browser.HotReload.Test] Sample.Test:TestMeaning", []);
+          document.getElementById("out").innerHTML = exit_code;
+
+          if (is_testing)
+          {
+            console.debug(`exit_code: ${exit_code}`);
+            test_exit(exit_code);
+          }
+        },
+      };
+    </script>
+    <script type="text/javascript" src="runtime.js"></script>
+
+    <script defer src="dotnet.js"></script>
+
+  </body>
+</html>
diff --git a/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/runtime.js b/src/tests/FunctionalTests/WebAssembly/Browser/HotReload/runtime.js
new file mode 100644 (file)
index 0000000..4859991
--- /dev/null
@@ -0,0 +1,47 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+var Module = { 
+
+    config: null,
+
+    preInit: async function() {
+        Module.config = await MONO.mono_wasm_load_config("./mono-config.json");
+    },
+
+    onRuntimeInitialized: function () {
+        if (!Module.config || Module.config.error) {
+            console.log("No config found");
+            test_exit(1);
+            throw(Module.config.error);
+        }
+        
+        Module.config.loaded_cb = function () {
+            try {
+                App.init ();
+            } catch (error) {
+                test_exit(1);
+                throw (error);
+            }
+        };
+        Module.config.fetch_file_cb = function (asset) {
+            return fetch (asset, { credentials: 'same-origin' });
+        }
+
+        if (Module.config.environment_variables !== undefined) {
+            console.log ("expected environment variables to be undefined, but they're: ", Module.config.environment_variables);
+            test_exit(1);
+        }
+        Module.config.environment_variables = {
+            "DOTNET_MODIFIABLE_ASSEMBLIES": "debug"
+        };
+
+        try
+        {
+            MONO.mono_load_runtime_and_bcl_args (Module.config);
+        } catch (error) {
+            test_exit(1);
+            throw(error);
+        }
+    },
+};