diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 4c415b63..a5163284 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -60,7 +60,8 @@ export default defineConfig({ { text: "Interop Instances", link: "/guide/interop-instances" }, { text: "Emit Preferences", link: "/guide/emit-prefs" }, { text: "Build Configuration", link: "/guide/build-config" }, - { text: "Sideloading Binaries", link: "/guide/sideloading" } + { text: "Sideloading Binaries", link: "/guide/sideloading" }, + { text: "NativeAOT-LLVM", link: "/guide/llvm" } ] }, { diff --git a/docs/guide/build-config.md b/docs/guide/build-config.md index 28edc312..a73cf5db 100644 --- a/docs/guide/build-config.md +++ b/docs/guide/build-config.md @@ -7,6 +7,7 @@ Build and publish related options are configured in `.csproj` file via MSBuild p | BootsharpName | bootsharp | Name of the generated JavaScript module. | | BootsharpEmbedBinaries | true | Whether to embed binaries to the JavaScript module file. | | BootsharpAggressiveTrimming | false | Whether to disable some .NET features to reduce binary size. | +| BootsharpLLVM | false | Enable experimental [NativeAOT-LLVM](/guide/llvm) backend. | | BootsharpBundleCommand | npx rollup | The command to bundle generated JavaScrip solution. | | BootsharpPublishDirectory | /bin | Directory to publish generated JavaScript module. | | BootsharpTypesDirectory | /types | Directory to publish type declarations. | diff --git a/docs/guide/llvm.md b/docs/guide/llvm.md new file mode 100644 index 00000000..0ecc1145 --- /dev/null +++ b/docs/guide/llvm.md @@ -0,0 +1,61 @@ +# NativeAOT-LLVM + +Starting with v0.6.0 Bootsharp supports .NET's experimental [NativeAOT-LLVM](https://github.com/dotnet/runtimelab/tree/feature/NativeAOT-LLVM) backend. + +By default, when targeting `browser-wasm`, .NET is using the Mono runtime, even when compiled in AOT mode. Compared to the modern NativeAOT (previously CoreRT) runtime, Mono's performance is lacking in speed, binary size and compilation times. NativeAOT-LLVM backend not only uses the modern runtime instead of Mono, but also optimizes it with the [LLVM](https://llvm.org) toolchain, further improving the performance. + +Below is a benchmark comparing interop and compute performance of various languages and .NET versions compiled to WASM to give you a rough idea on the differences: + +| | Rust | .NET LLVM | .NET AOT | Go | +|-------------|-------|-----------|-----------|---------| +| Echo Number | `1.0` | `11.9` | `21.1` | `718.7` | +| Echo Struct | `1.0` | `1.6` | `4.3` | `20.8` | +| Fibonacci | `1.0` | `1.1` | `1.5` | `3.8` | + +— the results are relative to the Rust baseline (lower is better). Sources of the benchmark are here: https://github.com/elringus/bootsharp/tree/main/samples/bench. + +## Setup + +Use following `.csproj` as a reference for enabling NativeAOT-LLVM with Bootsharp: + +```xml + + + + + net9.0-browser + browser-wasm + + true + + + + + + + + + + + true + true + none + $(EmccFlags) -O3 + false + + https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json; + + + + + + + + + + + + + + +``` diff --git a/samples/bench/bench.mjs b/samples/bench/bench.mjs index ca6619e5..3a4f8581 100644 --- a/samples/bench/bench.mjs +++ b/samples/bench/bench.mjs @@ -19,7 +19,7 @@ if (!lang || lang.toLowerCase() === "rust") if (!lang || lang.toLowerCase() === "llvm") await run(".NET LLVM", await initDotNetLLVM()); if (!lang || lang.toLowerCase() === "net") - await run(".NET", await initDotNet()); + await run(".NET AOT", await initDotNet()); if (!lang || lang.toLowerCase() === "boot") await run("Bootsharp", await initBootsharp()); if (!lang || lang.toLowerCase() === "go") diff --git a/samples/bench/bootsharp/Boot.csproj b/samples/bench/bootsharp/Boot.csproj index a82b5027..91e80bff 100644 --- a/samples/bench/bootsharp/Boot.csproj +++ b/samples/bench/bootsharp/Boot.csproj @@ -1,12 +1,8 @@ - net9.0 - Release + net9.0-browser browser-wasm - true - Speed - true @@ -15,4 +11,28 @@ + + + + true + true + true + none + $(EmccFlags) -O3 + false + + https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json; + + + + + + + + + + + + + diff --git a/samples/bench/dotnet-llvm/Program.cs b/samples/bench/dotnet-llvm/Program.cs index 538a1b26..dfa6363c 100644 --- a/samples/bench/dotnet-llvm/Program.cs +++ b/samples/bench/dotnet-llvm/Program.cs @@ -10,7 +10,6 @@ public struct Data public string[] Messages; } - [JsonSerializable(typeof(Data))] internal partial class SourceGenerationContext : JsonSerializerContext; diff --git a/samples/bench/readme.md b/samples/bench/readme.md index 6d440e13..b480890d 100644 --- a/samples/bench/readme.md +++ b/samples/bench/readme.md @@ -14,8 +14,8 @@ All results are relative to the Rust baseline (lower is better). ## 2024 (.NET 9) -| | Rust | .NET LLVM | .NET | Bootsharp | Go | -|-------------|-------|-----------|--------|-----------|---------| -| Echo Number | `1.0` | `14.4` | `21.1` | `25.7` | `718.7` | -| Echo Struct | `1.0` | `1.5` | `4.3` | `4.9` | `20.8` | -| Fibonacci | `1.0` | `1.1` | `1.5` | `1.6` | `3.8` | +| | Rust | .NET LLVM | Bootsharp | .NET AOT | Go | +|-------------|-------|-----------|-----------|-----------|---------| +| Echo Number | `1.0` | `11.9` | `11.9` | `21.1` | `718.7` | +| Echo Struct | `1.0` | `1.6` | `1.6` | `4.3` | `20.8` | +| Fibonacci | `1.0` | `1.1` | `1.5` | `1.5` | `3.8` | diff --git a/samples/trimming/README.md b/samples/trimming/README.md index b39009e9..b60d13da 100644 --- a/samples/trimming/README.md +++ b/samples/trimming/README.md @@ -6,7 +6,7 @@ To test and measure build size: ### Measurements (KB) -| .NET | Raw | Brotli | -|-------|-------|--------| -| 8.0.1 | 2,298 | 739 | -| 9.0.1 | 2,369 | 761 | +| | Raw | Brotli | +|-------------|-------|--------| +| .NET 8 | 2,298 | 739 | +| .NET 9 LLVM | 1,749 | 520 | diff --git a/samples/trimming/cs/Program.cs b/samples/trimming/cs/Program.cs index c7b6b5bf..ce33b79a 100644 --- a/samples/trimming/cs/Program.cs +++ b/samples/trimming/cs/Program.cs @@ -1,9 +1,12 @@ using Bootsharp; -Log("Hello from .NET!"); - public static partial class Program { + public static void Main () + { + Log("Hello from .NET!"); + } + [JSFunction] public static partial void Log (string message); } diff --git a/samples/trimming/cs/Trimming.csproj b/samples/trimming/cs/Trimming.csproj index 6858f874..2a317976 100644 --- a/samples/trimming/cs/Trimming.csproj +++ b/samples/trimming/cs/Trimming.csproj @@ -1,7 +1,7 @@ - net9.0 + net9.0-browser browser-wasm false @@ -19,4 +19,28 @@ WorkingDirectory="$(BootsharpPublishDirectory)"/> + + + + true + true + true + none + $(EmccFlags) -Oz + false + + https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json; + + + + + + + + + + + + + diff --git a/src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs index 1188419b..5985d7eb 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs @@ -45,6 +45,7 @@ protected override void AddAssembly (string assemblyName, params MockSource[] so BuildEngine = Engine, TrimmingEnabled = false, EmbedBinaries = false, - Threading = false + Threading = false, + LLVM = false }; } diff --git a/src/cs/Bootsharp.Publish.Test/Pack/SolutionInspectionTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/SolutionInspectionTest.cs index 627ebc1a..9f880464 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/SolutionInspectionTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/SolutionInspectionTest.cs @@ -25,4 +25,52 @@ public void WhenAssemblyInspectionFailsWarningIsLogged () Execute(); Assert.Contains(Engine.Warnings, w => w.Contains("Failed to inspect 'foo.dll' assembly")); } + + [Fact] + public void IgnoresAssembliesNotPresentInBuildDirectory () + { + var buildDir = $"{Project.Root}/build"; + Task.BuildDirectory = buildDir; + Directory.CreateDirectory(buildDir); + File.WriteAllText($"{buildDir}/foo.wasm", ""); + + foreach (var file in Directory.EnumerateFiles(Project.Root)) + File.WriteAllText($"{buildDir}/{Path.GetFileName(file)}", File.ReadAllText(file)); + + AddAssembly("foo.dll", + WithClass("[JSInvokable] public static void InvFoo () {}") + ); + AddAssembly("bar.dll", + WithClass("[JSInvokable] public static void InvBar () {}") + ); + Execute(); + + Assert.Contains(Engine.Messages, w => w.Contains("foo")); + Assert.DoesNotContain(Engine.Messages, w => w.Contains("bar")); + } + + [Fact] + public void DoesntIgnoreAssembliesWhenLLVM () + { + Task.LLVM = true; + + var buildDir = $"{Project.Root}/build"; + Task.BuildDirectory = buildDir; + Directory.CreateDirectory(buildDir); + File.WriteAllText($"{buildDir}/foo.wasm", ""); + + foreach (var file in Directory.EnumerateFiles(Project.Root)) + File.WriteAllText($"{buildDir}/{Path.GetFileName(file)}", File.ReadAllText(file)); + + AddAssembly("foo.dll", + WithClass("[JSInvokable] public static void InvFoo () {}") + ); + AddAssembly("bar.dll", + WithClass("[JSInvokable] public static void InvBar () {}") + ); + Execute(); + + Assert.Contains(Engine.Messages, w => w.Contains("foo")); + Assert.Contains(Engine.Messages, w => w.Contains("bar")); + } } diff --git a/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs b/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs index 9c46ab37..abb0baa7 100644 --- a/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs +++ b/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs @@ -11,6 +11,7 @@ public sealed class BootsharpPack : Microsoft.Build.Utilities.Task public required bool TrimmingEnabled { get; set; } public required bool EmbedBinaries { get; set; } public required bool Threading { get; set; } + public required bool LLVM { get; set; } public override bool Execute () { @@ -32,13 +33,18 @@ private Preferences ResolvePreferences () private SolutionInspection InspectSolution (Preferences prefs) { var inspector = new SolutionInspector(prefs, EntryAssemblyName); - // Assemblies in publish dir are trimmed and don't contain some data (eg, method arg names). - // While the inspected dir contains extra assemblies we don't need in build. Hence the filtering. - var included = Directory.GetFiles(BuildDirectory, "*.wasm").Select(Path.GetFileNameWithoutExtension).ToHashSet(); - var inspected = Directory.GetFiles(InspectedDirectory, "*.dll").Where(p => included.Contains(Path.GetFileNameWithoutExtension(p))); - var inspection = inspector.Inspect(InspectedDirectory, inspected); + var inspection = inspector.Inspect(InspectedDirectory, GetFiles()); new InspectionReporter(Log).Report(inspection); return inspection; + + IEnumerable GetFiles () + { + if (LLVM) return Directory.GetFiles(InspectedDirectory, "*.dll"); + // Assemblies in publish dir are trimmed and don't contain some data (eg, method arg names). + // While the inspected dir contains extra assemblies we don't need in build. Hence the filtering. + var included = Directory.GetFiles(BuildDirectory, "*.wasm").Select(Path.GetFileNameWithoutExtension).ToHashSet(); + return Directory.GetFiles(InspectedDirectory, "*.dll").Where(p => included.Contains(Path.GetFileNameWithoutExtension(p))); + } } private void GenerateBindings (Preferences prefs, SolutionInspection inspection) diff --git a/src/cs/Bootsharp/Bootsharp.csproj b/src/cs/Bootsharp/Bootsharp.csproj index bb3ee012..35ba3097 100644 --- a/src/cs/Bootsharp/Bootsharp.csproj +++ b/src/cs/Bootsharp/Bootsharp.csproj @@ -2,11 +2,9 @@ net9.0 - enable - enable Bootsharp Bootsharp - Compile C# solution into single-file ES module with auto-generated JavaScript bindings and type definitions. + Use C# in web apps with comfort. NU5100 @@ -15,15 +13,12 @@ - - <_Parameter1>browser - - + diff --git a/src/cs/Bootsharp/Build/Bootsharp.props b/src/cs/Bootsharp/Build/Bootsharp.props index 55fb6dd1..6f6876ed 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.props +++ b/src/cs/Bootsharp/Build/Bootsharp.props @@ -10,6 +10,7 @@ true true false + true bootsharp @@ -17,6 +18,8 @@ true false + + false diff --git a/src/cs/Bootsharp/Build/Bootsharp.targets b/src/cs/Bootsharp/Build/Bootsharp.targets index b9869418..de8c0eb7 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.targets +++ b/src/cs/Bootsharp/Build/Bootsharp.targets @@ -10,6 +10,8 @@ $(BootsharpIntermediateDirectory)/Serializer.g.cs $(BootsharpIntermediateDirectory)/Interop.g.cs $(AssemblyName).dll + CopyNativeBinary + WasmNestedPublishApp @@ -39,6 +41,7 @@ false false false + false false true false @@ -91,11 +94,12 @@ - + - $(WasmAppDir)/$(WasmRuntimeAssetsLocation) + $(PublishDir) + $(WasmAppDir)/$(WasmRuntimeAssetsLocation) $(BaseOutputPath.Replace('\', '/')) $(BootSharpBaseOutputPath)$(BootsharpName) $(BootsharpPublishDirectory)/types @@ -119,7 +123,8 @@ EntryAssemblyName="$(BootsharpEntryAssemblyName)" TrimmingEnabled="$(BootsharpAggressiveTrimming)" EmbedBinaries="$(BootsharpEmbedBinaries)" - Threading="$(BootsharpThreading)"/> + Threading="$(BootsharpThreading)" + LLVM="$(BootsharpLLVM)"/> + - 0.5.0 + 0.6.0 Elringus javascript typescript ts js wasm node deno bun interop codegen https://sharp.elringus.com @@ -17,4 +18,5 @@ + diff --git a/src/js/test/cs/Test/Test.csproj b/src/js/test/cs/Test/Test.csproj index 7b2a6c52..1730c2fa 100644 --- a/src/js/test/cs/Test/Test.csproj +++ b/src/js/test/cs/Test/Test.csproj @@ -15,7 +15,7 @@ - +