Browse Source

Disposable document, ChunkyImage finalizer

Equbuxu 3 years ago
parent
commit
ef27f7800d

+ 10 - 0
src/ChunkyImageLib/Chunk.cs

@@ -5,6 +5,14 @@ namespace ChunkyImageLib
 {
     public class Chunk : IDisposable
     {
+        private static volatile int chunkCounter = 0;
+        /// <summary>
+        /// The number of chunks that haven't yet been returned (includes garbage collected chunks).
+        /// Used in tests to make sure that all chunks are disposed.
+        /// </summary>
+        public static int ChunkCounter => chunkCounter;
+
+
         private bool returned = false;
         public Surface Surface { get; }
         public Vector2i PixelSize { get; }
@@ -22,6 +30,7 @@ namespace ChunkyImageLib
         {
             var chunk = ChunkPool.Instance.Get(resolution) ?? new Chunk(resolution);
             chunk.returned = false;
+            Interlocked.Increment(ref chunkCounter);
             return chunk;
         }
 
@@ -35,6 +44,7 @@ namespace ChunkyImageLib
             if (returned)
                 return;
             returned = true;
+            Interlocked.Decrement(ref chunkCounter);
             ChunkPool.Instance.Push(this);
         }
     }

+ 26 - 9
src/ChunkyImageLib/ChunkyImage.cs

@@ -1,7 +1,7 @@
-using ChunkyImageLib.DataHolders;
+using System.Runtime.CompilerServices;
+using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.Operations;
 using SkiaSharp;
-using System.Runtime.CompilerServices;
 
 [assembly: InternalsVisibleTo("ChunkyImageLibTest")]
 namespace ChunkyImageLib
@@ -627,17 +627,34 @@ namespace ChunkyImageLib
                 if (disposed)
                     return;
                 CancelChanges();
-                foreach (var (_, chunk) in tempRasterClipChunks)
+                DisposeAll();
+                GC.SuppressFinalize(this);
+            }
+        }
+
+        private void DisposeAll()
+        {
+            foreach (var (_, chunk) in tempRasterClipChunks)
+                chunk.Dispose();
+            foreach (var (_, chunks) in committedChunks)
+            {
+                foreach (var (_, chunk) in chunks)
+                {
                     chunk.Dispose();
-                foreach (var (_, chunks) in committedChunks)
+                }
+            }
+            foreach (var (_, chunks) in latestChunks)
+            {
+                foreach (var (_, chunk) in chunks)
                 {
-                    foreach (var (_, chunk) in chunks)
-                    {
-                        chunk.Dispose();
-                    }
+                    chunk.Dispose();
                 }
-                disposed = true;
             }
+            disposed = true;
+        }
+        ~ChunkyImage()
+        {
+            DisposeAll();
         }
     }
 }

+ 28 - 0
src/ChunkyImageLibTest/ChunkyImageTests.cs

@@ -0,0 +1,28 @@
+using ChunkyImageLib;
+using SkiaSharp;
+using Xunit;
+
+namespace ChunkyImageLibTest;
+public class ChunkyImageTests
+{
+    [Fact]
+    public void ChunkyImage_Dispose_ReturnsAllChunks()
+    {
+        ChunkyImage image = new ChunkyImage(new(ChunkyImage.ChunkSize, ChunkyImage.ChunkSize));
+        image.DrawRectangle(new(new(5, 5), new(80, 80), 2, SKColors.AliceBlue, SKColors.Snow));
+        using (Chunk target = Chunk.Create())
+        {
+            image.DrawLatestChunkOn(new(0, 0), ChunkyImageLib.DataHolders.ChunkResolution.Full, target.Surface.SkiaSurface, new(0, 0));
+            image.CancelChanges();
+            image.Resize(new(ChunkyImage.ChunkSize * 4, ChunkyImage.ChunkSize * 4));
+            image.CommitChanges();
+            image.DrawRectangle(new(new(0, 0), image.CommittedSize, 2, SKColors.AliceBlue, SKColors.Snow));
+            image.CommitChanges();
+            image.DrawRectangle(new(new(0, 0), image.CommittedSize, 2, SKColors.AliceBlue, SKColors.Snow));
+            image.CancelChanges();
+        }
+        image.Dispose();
+
+        Assert.Equal(0, Chunk.ChunkCounter);
+    }
+}

+ 7 - 1
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -3,7 +3,7 @@ using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
 namespace PixiEditor.ChangeableDocument.Changeables
 {
-    internal class Document : IChangeable, IReadOnlyDocument
+    internal class Document : IChangeable, IReadOnlyDocument, IDisposable
     {
         public IReadOnlyFolder ReadOnlyStructureRoot => StructureRoot;
         public IReadOnlySelection ReadOnlySelection => Selection;
@@ -17,6 +17,12 @@ namespace PixiEditor.ChangeableDocument.Changeables
         internal Selection Selection { get; } = new();
         public Vector2i Size { get; set; } = DefaultSize;
 
+        public void Dispose()
+        {
+            StructureRoot.Dispose();
+            Selection.Dispose();
+        }
+
         public StructureMember FindMemberOrThrow(Guid guid) => FindMember(guid) ?? throw new ArgumentException("Could not find member with guid " + guid.ToString());
         public StructureMember? FindMember(Guid guid)
         {

+ 6 - 1
src/PixiEditor.ChangeableDocument/Changeables/Selection.cs

@@ -4,7 +4,7 @@ using SkiaSharp;
 
 namespace PixiEditor.ChangeableDocument.Changeables
 {
-    internal class Selection : IReadOnlySelection
+    internal class Selection : IReadOnlySelection, IDisposable
     {
         public static SKColor SelectionColor { get; } = SKColors.CornflowerBlue;
         public bool IsEmptyAndInactive { get; set; } = true;
@@ -12,5 +12,10 @@ namespace PixiEditor.ChangeableDocument.Changeables
 
         public IReadOnlyChunkyImage ReadOnlySelectionImage => SelectionImage;
         public bool ReadOnlyIsEmptyAndInactive => IsEmptyAndInactive;
+
+        public void Dispose()
+        {
+            SelectionImage.Dispose();
+        }
     }
 }

+ 44 - 3
src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs

@@ -7,9 +7,11 @@ using PixiEditor.ChangeableDocument.Changes;
 
 namespace ChangeableDocument
 {
-    public class DocumentChangeTracker
+    public class DocumentChangeTracker : IDisposable
     {
         private Document document;
+        private bool disposed = false;
+        private bool running = false;
         public IReadOnlyDocument Document => document;
 
         private UpdateableChange? activeChange = null;
@@ -18,6 +20,31 @@ namespace ChangeableDocument
         private Stack<List<Change>> undoStack = new();
         private Stack<List<Change>> redoStack = new();
 
+        public void Dispose()
+        {
+            if (running)
+                throw new InvalidOperationException("Something is currently being processed");
+            if (disposed)
+                return;
+            disposed = true;
+
+            document.Dispose();
+
+            activeChange?.Dispose();
+
+            if (activePacket != null)
+                foreach (var change in activePacket)
+                    change.Dispose();
+
+            foreach (var list in undoStack)
+                foreach (var change in list)
+                    change.Dispose();
+
+            foreach (var list in redoStack)
+                foreach (var change in list)
+                    change.Dispose();
+        }
+
         public DocumentChangeTracker()
         {
             document = new Document();
@@ -192,12 +219,26 @@ namespace ChangeableDocument
 
         public async Task<List<IChangeInfo?>> ProcessActions(List<IAction> actions)
         {
-            return await Task.Run(() => ProcessActionList(actions)).ConfigureAwait(true);
+            if (disposed)
+                throw new ObjectDisposedException(nameof(DocumentChangeTracker));
+            if (running)
+                throw new InvalidOperationException("Already currently processing");
+            running = true;
+            var result = await Task.Run(() => ProcessActionList(actions)).ConfigureAwait(true);
+            running = false;
+            return result;
         }
 
         public List<IChangeInfo?> ProcessActionsSync(List<IAction> actions)
         {
-            return ProcessActionList(actions);
+            if (disposed)
+                throw new ObjectDisposedException(nameof(DocumentChangeTracker));
+            if (running)
+                throw new InvalidOperationException("Already currently processing");
+            running = true;
+            var result = ProcessActionList(actions);
+            running = false;
+            return result;
         }
     }
 }

+ 4 - 4
src/PixiEditorPrototype/ViewModels/ViewModelMain.cs

@@ -1,11 +1,11 @@
-using ChunkyImageLib.DataHolders;
+using System.ComponentModel;
+using System.Windows.Input;
+using System.Windows.Media;
+using ChunkyImageLib.DataHolders;
 using PixiEditor.Zoombox;
 using PixiEditorPrototype.Models;
 using PixiEditorPrototype.Views;
 using SkiaSharp;
-using System.ComponentModel;
-using System.Windows.Input;
-using System.Windows.Media;
 
 namespace PixiEditorPrototype.ViewModels
 {