From de5c4b35c8fc5ce1c114ae842c1b2e59fda271e5 Mon Sep 17 00:00:00 2001 From: Damian Hickey Date: Sun, 26 Jul 2026 13:21:30 +0200 Subject: [PATCH 1/3] perf: enable trimming, cut install size ~56% Self-contained WinUI publish shipped the full untrimmed BCL and Windows SDK projection (220 MB installed). PublishTrimmed drops it to ~98 MB. JSON (de)serialization moved to source-generated contexts so config and rule-hit log survive trimming; XAML already uses x:Bind throughout, so no other reflection hazards. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- .../Configuration/AppConfigJsonContext.cs | 12 ++++++++++++ .../Configuration/ConfigStore.cs | 11 ++--------- .../Logging/RuleHitLogJsonContext.cs | 8 ++++++++ src/BrowserWrangler.Core/Logging/RuleHitLogStore.cs | 9 ++------- src/BrowserWrangler/BrowserWrangler.csproj | 3 +++ 6 files changed, 28 insertions(+), 17 deletions(-) create mode 100644 src/BrowserWrangler.Core/Configuration/AppConfigJsonContext.cs create mode 100644 src/BrowserWrangler.Core/Logging/RuleHitLogJsonContext.cs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 718b8e0..c4f01fa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: if: matrix.arch == 'x64' run: dotnet test tests\BrowserWrangler.Core.Tests -c Release - - name: Publish (self-contained, ReadyToRun) + - name: Publish (self-contained, trimmed, ReadyToRun) run: > dotnet publish src\BrowserWrangler -c Release -r win-${{ matrix.arch }} diff --git a/src/BrowserWrangler.Core/Configuration/AppConfigJsonContext.cs b/src/BrowserWrangler.Core/Configuration/AppConfigJsonContext.cs new file mode 100644 index 0000000..70ade66 --- /dev/null +++ b/src/BrowserWrangler.Core/Configuration/AppConfigJsonContext.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace BrowserWrangler.Core.Configuration; + +/// Source-generated JSON contract for so config +/// (de)serialization keeps working under assembly trimming. +[JsonSourceGenerationOptions( + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, + UseStringEnumConverter = true)] +[JsonSerializable(typeof(AppConfig))] +internal sealed partial class AppConfigJsonContext : JsonSerializerContext; diff --git a/src/BrowserWrangler.Core/Configuration/ConfigStore.cs b/src/BrowserWrangler.Core/Configuration/ConfigStore.cs index acafb19..4aad536 100644 --- a/src/BrowserWrangler.Core/Configuration/ConfigStore.cs +++ b/src/BrowserWrangler.Core/Configuration/ConfigStore.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using System.Text.Json.Serialization; using BrowserWrangler.Core.Models; namespace BrowserWrangler.Core.Configuration; @@ -10,12 +9,6 @@ namespace BrowserWrangler.Core.Configuration; /// public sealed class ConfigStore { - private static readonly JsonSerializerOptions Options = new() - { - WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, - Converters = { new JsonStringEnumConverter() }, - }; private static readonly string DefaultManagedUpdatesDirectoryPath = Path.GetFullPath(Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "BrowserWrangler", @@ -50,7 +43,7 @@ public AppConfig Load(Version? runningVersion = null) try { using FileStream stream = File.OpenRead(ConfigFilePath); - config = JsonSerializer.Deserialize(stream, Options) ?? new AppConfig(); + config = JsonSerializer.Deserialize(stream, AppConfigJsonContext.Default.AppConfig) ?? new AppConfig(); } catch (JsonException) { @@ -75,7 +68,7 @@ public void Save(AppConfig config) string tmp = ConfigFilePath + ".tmp"; using (FileStream stream = File.Create(tmp)) { - JsonSerializer.Serialize(stream, config, Options); + JsonSerializer.Serialize(stream, config, AppConfigJsonContext.Default.AppConfig); } File.Move(tmp, ConfigFilePath, overwrite: true); diff --git a/src/BrowserWrangler.Core/Logging/RuleHitLogJsonContext.cs b/src/BrowserWrangler.Core/Logging/RuleHitLogJsonContext.cs new file mode 100644 index 0000000..a21664e --- /dev/null +++ b/src/BrowserWrangler.Core/Logging/RuleHitLogJsonContext.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace BrowserWrangler.Core.Logging; + +/// Source-generated JSON contract for so the +/// rule-hit log keeps working under assembly trimming. +[JsonSerializable(typeof(RuleHitLogEntry))] +internal sealed partial class RuleHitLogJsonContext : JsonSerializerContext; diff --git a/src/BrowserWrangler.Core/Logging/RuleHitLogStore.cs b/src/BrowserWrangler.Core/Logging/RuleHitLogStore.cs index 12fc366..4d31d2c 100644 --- a/src/BrowserWrangler.Core/Logging/RuleHitLogStore.cs +++ b/src/BrowserWrangler.Core/Logging/RuleHitLogStore.cs @@ -10,11 +10,6 @@ public sealed class RuleHitLogStore { public const int MaxRetainedEntries = 500; - private static readonly JsonSerializerOptions Options = new() - { - WriteIndented = false, - }; - public RuleHitLogStore() : this(Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @@ -35,7 +30,7 @@ public void Append(RuleHitLogEntry entry) Directory.CreateDirectory(dir); } - string line = JsonSerializer.Serialize(entry, Options); + string line = JsonSerializer.Serialize(entry, RuleHitLogJsonContext.Default.RuleHitLogEntry); byte[] payload = Encoding.UTF8.GetBytes(line + Environment.NewLine); using (var stream = new FileStream(LogFilePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)) { @@ -64,7 +59,7 @@ public IReadOnlyList ReadLatest(int maxEntries = 200) try { - RuleHitLogEntry? entry = JsonSerializer.Deserialize(line, Options); + RuleHitLogEntry? entry = JsonSerializer.Deserialize(line, RuleHitLogJsonContext.Default.RuleHitLogEntry); if (entry is not null) { latestWindow.Enqueue(entry); diff --git a/src/BrowserWrangler/BrowserWrangler.csproj b/src/BrowserWrangler/BrowserWrangler.csproj index ce2eaaf..8fd3d60 100644 --- a/src/BrowserWrangler/BrowserWrangler.csproj +++ b/src/BrowserWrangler/BrowserWrangler.csproj @@ -11,6 +11,9 @@ true None true + + true $(DefineConstants);DISABLE_XAML_GENERATED_MAIN Assets\app.ico enable From 5bc5999cef2d4836f622af0e1f255298502ab046 Mon Sep 17 00:00:00 2001 From: Damian Hickey Date: Sun, 26 Jul 2026 13:31:29 +0200 Subject: [PATCH 2/3] build: use partial trim mode for publish Only assemblies that opt in via IsTrimmable get trimmed; non-compatible dependencies ship intact. The WASDK/WinRT assemblies self-declare trimmable, so output size is unchanged vs full mode. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/BrowserWrangler/BrowserWrangler.csproj | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/BrowserWrangler/BrowserWrangler.csproj b/src/BrowserWrangler/BrowserWrangler.csproj index 8fd3d60..462bfa0 100644 --- a/src/BrowserWrangler/BrowserWrangler.csproj +++ b/src/BrowserWrangler/BrowserWrangler.csproj @@ -12,8 +12,11 @@ None true + contexts and XAML uses x:Bind, so reflection-based trimming hazards are avoided. + Partial mode processes only assemblies that explicitly opt in to trimming; + dependencies that are not trim-compatible are copied intact. --> true + partial $(DefineConstants);DISABLE_XAML_GENERATED_MAIN Assets\app.ico enable From ff29fcdfe742ec39af2015a7f52e998461eabb1b Mon Sep 17 00:00:00 2001 From: Damian Hickey Date: Sun, 26 Jul 2026 14:02:10 +0200 Subject: [PATCH 3/3] docs: correct trim-safety rationale in csproj comment No XAML bindings exist; views are populated in code-behind. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/BrowserWrangler/BrowserWrangler.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BrowserWrangler/BrowserWrangler.csproj b/src/BrowserWrangler/BrowserWrangler.csproj index 462bfa0..76ae44f 100644 --- a/src/BrowserWrangler/BrowserWrangler.csproj +++ b/src/BrowserWrangler/BrowserWrangler.csproj @@ -12,7 +12,7 @@ None true true