Browse Source

Try to combine AvaloniaResources with the same system path (#15302)

* Try to combine AvaloniaResources with the same system path

* Fix test assert
Max Katz 1 year ago
parent
commit
b5db6bb0f6

+ 46 - 9
src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.Linq;
 using System.Text;
 
 namespace Avalonia.Utilities
@@ -66,20 +67,45 @@ namespace Avalonia.Utilities
             }
         }
 
+        [Obsolete]
         public static void WriteResources(Stream output, List<(string Path, int Size, Func<Stream> Open)> resources)
         {
-            var entries = new List<AvaloniaResourcesIndexEntry>(resources.Count);
+            WriteResources(output,
+                resources.Select(r => new AvaloniaResourcesEntry { Path = r.Path, Open = r.Open, Size = r.Size })
+                    .ToList());
+        }
+
+        public static void WriteResources(Stream output, IReadOnlyList<AvaloniaResourcesEntry> resources)
+        {
+            var entries = new List<AvaloniaResourcesIndexEntry>();
+            var index = new Dictionary<string, (AvaloniaResourcesIndexEntry entry, Func<Stream> open)>();
             var offset = 0;
 
             foreach (var resource in resources)
             {
-                entries.Add(new AvaloniaResourcesIndexEntry
+                // Try to combine resources with the same system path, if present.
+                if (!string.IsNullOrEmpty(resource.SystemPath)
+                    && index.TryGetValue(resource.SystemPath, out var existingResource))
                 {
-                    Path = resource.Path,
-                    Offset = offset,
-                    Size = resource.Size
-                });
-                offset += resource.Size;
+                    entries.Add(new AvaloniaResourcesIndexEntry
+                    {
+                        Path = resource.Path,
+                        Offset = existingResource.entry.Offset,
+                        Size = existingResource.entry.Size
+                    });
+                }
+                else
+                {
+                    var entry = new AvaloniaResourcesIndexEntry
+                    {
+                        Path = resource.Path,
+                        Offset = offset,
+                        Size = resource.Size
+                    };
+                    index[resource.SystemPath ?? offset.ToString()] = (entry, resource.Open!);
+                    entries.Add(entry);
+                    offset += resource.Size;
+                }
             }
 
             using var writer = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true);
@@ -94,9 +120,9 @@ namespace Avalonia.Utilities
             writer.Write(indexSize);
             output.Position = posAfterEntries;
 
-            foreach (var resource in resources)
+            foreach (var pair in index)
             {
-                using var resourceStream = resource.Open();
+                using var resourceStream = pair.Value.open();
                 resourceStream.CopyTo(output);
             }
         }
@@ -113,4 +139,15 @@ namespace Avalonia.Utilities
 
         public int Size { get; set; }
     }
+
+#if !BUILDTASK
+    public
+#endif
+    class AvaloniaResourcesEntry
+    {
+        public string? Path { get; init; }
+        public Func<Stream>? Open { get; init; }
+        public int Size { get; init; }
+        public string? SystemPath { get; init; }
+    }
 }

+ 7 - 1
src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs

@@ -80,7 +80,13 @@ namespace Avalonia.Build.Tasks
         {
             AvaloniaResourcesIndexReaderWriter.WriteResources(
                 output,
-                sources.Select(source => (source.Path, source.Size, (Func<Stream>) source.Open)).ToList());
+                sources.Select(source => new AvaloniaResourcesEntry
+                {
+                    Path = source.Path,
+                    Size = source.Size,
+                    SystemPath = source.SystemPath,
+                    Open = source.Open
+                }).ToList());
         }
 
         private bool PreProcessXamlFiles(List<Source> sources)

+ 8 - 5
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using System.Runtime.CompilerServices;
 using Avalonia.Platform.Internal;
 using Avalonia.Utilities;
 using Mono.Cecil;
@@ -67,11 +68,13 @@ namespace Avalonia.Build.Tasks
 
                 AvaloniaResourcesIndexReaderWriter.WriteResources(
                     output,
-                    _resources.Select(x => (
-                        Path: x.Key,
-                        Size: x.Value.FileContents.Length,
-                        Open: (Func<Stream>) (() => new MemoryStream(x.Value.FileContents))
-                    )).ToList());
+                    _resources.Select(x => new AvaloniaResourcesEntry
+                    {
+                        Path = x.Key,
+                        Size = x.Value.FileContents.Length,
+                        SystemPath = x.Value.FilePath,
+                        Open = () => new MemoryStream(x.Value.FileContents)
+                    }).ToList());
 
                 output.Position = 0L;
                 _embedded = new EmbeddedResource(Constants.AvaloniaResourceName, ManifestResourceAttributes.Public, output);

+ 91 - 0
tests/Avalonia.Base.UnitTests/Utilities/AvaloniaResourcesIndexTests.cs

@@ -0,0 +1,91 @@
+using System;
+using System.IO;
+using System.Text;
+using Avalonia.Utilities;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests;
+
+public class AvaloniaResourcesIndexTests
+{
+    [Fact]
+    public void Should_Write_And_Read_The_Same_Resources()
+    {
+        using var memoryStream = new MemoryStream();
+
+        var fooBytes = Encoding.UTF8.GetBytes("foo");
+        var booBytes = Encoding.UTF8.GetBytes("boo");
+        AvaloniaResourcesIndexReaderWriter.WriteResources(memoryStream,
+            new[]
+            {
+                new AvaloniaResourcesEntry
+                {
+                    Path = "foo.xaml", Size = fooBytes.Length, Open = () => new MemoryStream(fooBytes)
+                },
+                new AvaloniaResourcesEntry
+                {
+                    Path = "boo.xaml", Size = booBytes.Length, Open = () => new MemoryStream(booBytes)
+                }
+            });
+
+        memoryStream.Seek(4, SeekOrigin.Begin); // skip 4 bytes for "index size" field.
+
+        var index = AvaloniaResourcesIndexReaderWriter.ReadIndex(memoryStream);
+        var resourcesBasePosition = memoryStream.Position;
+
+        Span<byte> buffer = stackalloc byte[index[0].Size];
+
+        Assert.Equal("foo.xaml", index[0].Path);
+        Assert.Equal(0, index[0].Offset);
+        Assert.Equal(fooBytes.Length, index[0].Size);
+
+        memoryStream.Seek(resourcesBasePosition + index[0].Offset, SeekOrigin.Begin);
+        memoryStream.ReadExactly(buffer);
+        Assert.Equal(fooBytes, buffer.ToArray());
+
+        Assert.Equal("boo.xaml", index[1].Path);
+        Assert.Equal(fooBytes.Length, index[1].Offset);
+        Assert.Equal(booBytes.Length, index[1].Size);
+
+        memoryStream.Seek(resourcesBasePosition + index[1].Offset, SeekOrigin.Begin);
+        memoryStream.ReadExactly(buffer);
+        Assert.Equal(booBytes, buffer.ToArray());
+    }
+
+    [Fact]
+    public void Should_Combined_Same_Physical_Path_Resources()
+    {
+        using var memoryStream = new MemoryStream();
+
+        var resourceBytes = Encoding.UTF8.GetBytes("resource-data");
+        AvaloniaResourcesIndexReaderWriter.WriteResources(memoryStream, new[]
+        {
+            new AvaloniaResourcesEntry
+            {
+                Path = "app.xaml",
+                SystemPath = "app.ico",
+                Size = resourceBytes.Length,
+                Open = () => new MemoryStream(resourceBytes)
+            },
+            new AvaloniaResourcesEntry
+            {
+                Path = "!__AvaloniaDefaultWindowIcon",
+                SystemPath = "app.ico",
+                Size = resourceBytes.Length,
+                Open = () => new MemoryStream(resourceBytes)
+            }
+        });
+
+        memoryStream.Seek(4, SeekOrigin.Begin); // skip 4 bytes for "index size" field.
+
+        var index = AvaloniaResourcesIndexReaderWriter.ReadIndex(memoryStream);
+
+        Assert.Equal("app.xaml", index[0].Path);
+        Assert.Equal(0, index[0].Offset);
+        Assert.Equal(resourceBytes.Length, index[0].Size);
+
+        Assert.Equal("!__AvaloniaDefaultWindowIcon", index[1].Path);
+        Assert.Equal(0, index[1].Offset);
+        Assert.Equal(resourceBytes.Length, index[1].Size);
+    }
+}