diff --git a/src/App.xaml.cs b/src/App.xaml.cs
index 8a26a165..1d23d17e 100644
--- a/src/App.xaml.cs
+++ b/src/App.xaml.cs
@@ -598,26 +598,43 @@ private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
///
public static void ReleaseMemory()
{
- // Garbage Collector
- try
- {
- GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
- GC.WaitForPendingFinalizers();
- GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
- }
- catch
- {
- // ignored
- }
+ ReleaseMemory
+ (
+ ReleaseMemoryMode.Combined,
+ () =>
+ {
+ GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
+ GC.WaitForPendingFinalizers();
+ GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
+ },
+ () => NativeMethods.EmptyWorkingSet(Process.GetCurrentProcess().Handle)
+ );
+ }
- // Optimize App Working Set
- try
+ internal static void ReleaseMemory(ReleaseMemoryMode mode, Action collectGarbage, Action trimWorkingSet)
+ {
+ if (mode == ReleaseMemoryMode.GCOnly || mode == ReleaseMemoryMode.Combined)
{
- NativeMethods.EmptyWorkingSet(Process.GetCurrentProcess().Handle);
+ try
+ {
+ collectGarbage();
+ }
+ catch
+ {
+ // ignored
+ }
}
- catch (Exception)
+
+ if (mode == ReleaseMemoryMode.TrimOnly || mode == ReleaseMemoryMode.Combined)
{
- // ignored
+ try
+ {
+ trimWorkingSet();
+ }
+ catch
+ {
+ // ignored
+ }
}
}
diff --git a/src/Core/OptimizationTiming.cs b/src/Core/OptimizationTiming.cs
new file mode 100644
index 00000000..47c9d4fa
--- /dev/null
+++ b/src/Core/OptimizationTiming.cs
@@ -0,0 +1,68 @@
+using System;
+
+namespace WinMemoryCleaner
+{
+ ///
+ /// Provides a deterministic seam for measuring optimization completion.
+ ///
+ internal sealed class OptimizationTiming
+ {
+ private readonly Func _clock;
+ private readonly TimeSpan _startedAt;
+
+ internal OptimizationTiming(Func clock)
+ {
+ if (clock == null)
+ throw new ArgumentNullException("clock");
+
+ _clock = clock;
+ _startedAt = _clock();
+ }
+
+ internal TimeSpan FinalAppReleaseDuration { get; private set; }
+
+ internal TimeSpan Elapsed
+ {
+ get { return _clock().Subtract(_startedAt); }
+ }
+
+ internal TimeSpan Measure(Action stage)
+ {
+ if (stage == null)
+ throw new ArgumentNullException("stage");
+
+ var startedAt = _clock();
+
+ stage();
+
+ return _clock().Subtract(startedAt);
+ }
+
+ internal TimeSpan Complete(Action finalAppRelease)
+ {
+ if (finalAppRelease == null)
+ throw new ArgumentNullException("finalAppRelease");
+
+ var finalAppReleaseStartedAt = _clock();
+
+ try
+ {
+ finalAppRelease();
+ }
+ finally
+ {
+ FinalAppReleaseDuration = _clock().Subtract(finalAppReleaseStartedAt);
+ }
+
+ return Elapsed;
+ }
+ }
+
+ internal enum ReleaseMemoryMode
+ {
+ None,
+ GCOnly,
+ TrimOnly,
+ Combined
+ }
+}
diff --git a/src/Service/ComputerService.cs b/src/Service/ComputerService.cs
index a4ca5319..ee2f6b5e 100644
--- a/src/Service/ComputerService.cs
+++ b/src/Service/ComputerService.cs
@@ -149,10 +149,10 @@ public void Optimize(Enums.Memory.Optimization.Reason reason, Enums.Memory.Areas
if (areas == Enums.Memory.Areas.None)
return;
- var errorRuntime = new TimeSpan();
- var infoRuntime = new TimeSpan();
var optimizationReason = reason.GetString();
var stopwatch = new Stopwatch();
+ var totalStopwatch = Stopwatch.StartNew();
+ var optimizationTiming = new OptimizationTiming(() => totalStopwatch.Elapsed);
var value = (byte)0;
var error = new LogOptimizationData { Reason = optimizationReason };
@@ -178,8 +178,6 @@ public void Optimize(Enums.Memory.Optimization.Reason reason, Enums.Memory.Areas
Name = Localizer.String.WorkingSet,
Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", stopwatch.Elapsed.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture))
});
-
- infoRuntime = infoRuntime.Add(stopwatch.Elapsed);
}
catch (Exception e)
{
@@ -189,8 +187,6 @@ public void Optimize(Enums.Memory.Optimization.Reason reason, Enums.Memory.Areas
Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", stopwatch.Elapsed.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture)),
Error = e.GetMessage()
});
-
- errorRuntime = errorRuntime.Add(stopwatch.Elapsed);
}
}
@@ -214,8 +210,6 @@ public void Optimize(Enums.Memory.Optimization.Reason reason, Enums.Memory.Areas
Name = Localizer.String.SystemFileCache,
Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", stopwatch.Elapsed.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture))
});
-
- infoRuntime = infoRuntime.Add(stopwatch.Elapsed);
}
catch (Exception e)
{
@@ -225,8 +219,6 @@ public void Optimize(Enums.Memory.Optimization.Reason reason, Enums.Memory.Areas
Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", stopwatch.Elapsed.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture)),
Error = e.GetMessage()
});
-
- errorRuntime = errorRuntime.Add(stopwatch.Elapsed);
}
}
@@ -250,8 +242,6 @@ public void Optimize(Enums.Memory.Optimization.Reason reason, Enums.Memory.Areas
Name = Localizer.String.ModifiedPageList,
Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", stopwatch.Elapsed.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture))
});
-
- infoRuntime = infoRuntime.Add(stopwatch.Elapsed);
}
catch (Exception e)
{
@@ -261,8 +251,6 @@ public void Optimize(Enums.Memory.Optimization.Reason reason, Enums.Memory.Areas
Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", stopwatch.Elapsed.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture)),
Error = e.GetMessage()
});
-
- errorRuntime = errorRuntime.Add(stopwatch.Elapsed);
}
}
@@ -289,8 +277,6 @@ public void Optimize(Enums.Memory.Optimization.Reason reason, Enums.Memory.Areas
Name = standbyList,
Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", stopwatch.Elapsed.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture))
});
-
- infoRuntime = infoRuntime.Add(stopwatch.Elapsed);
}
catch (Exception e)
{
@@ -300,8 +286,6 @@ public void Optimize(Enums.Memory.Optimization.Reason reason, Enums.Memory.Areas
Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", stopwatch.Elapsed.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture)),
Error = e.GetMessage()
});
-
- errorRuntime = errorRuntime.Add(stopwatch.Elapsed);
}
}
@@ -325,8 +309,6 @@ public void Optimize(Enums.Memory.Optimization.Reason reason, Enums.Memory.Areas
Name = Localizer.String.CombinedPageList,
Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", stopwatch.Elapsed.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture))
});
-
- infoRuntime = infoRuntime.Add(stopwatch.Elapsed);
}
catch (Exception e)
{
@@ -336,8 +318,6 @@ public void Optimize(Enums.Memory.Optimization.Reason reason, Enums.Memory.Areas
Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", stopwatch.Elapsed.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture)),
Error = e.GetMessage()
});
-
- errorRuntime = errorRuntime.Add(stopwatch.Elapsed);
}
}
@@ -361,8 +341,6 @@ public void Optimize(Enums.Memory.Optimization.Reason reason, Enums.Memory.Areas
Name = Localizer.String.RegistryCache,
Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", stopwatch.Elapsed.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture))
});
-
- infoRuntime = infoRuntime.Add(stopwatch.Elapsed);
}
catch (Exception e)
{
@@ -372,8 +350,6 @@ public void Optimize(Enums.Memory.Optimization.Reason reason, Enums.Memory.Areas
Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", stopwatch.Elapsed.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture)),
Error = e.GetMessage()
});
-
- errorRuntime = errorRuntime.Add(stopwatch.Elapsed);
}
}
@@ -397,8 +373,6 @@ public void Optimize(Enums.Memory.Optimization.Reason reason, Enums.Memory.Areas
Name = Localizer.String.ModifiedFileCache,
Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", stopwatch.Elapsed.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture))
});
-
- infoRuntime = infoRuntime.Add(stopwatch.Elapsed);
}
catch (Exception e)
{
@@ -408,8 +382,6 @@ public void Optimize(Enums.Memory.Optimization.Reason reason, Enums.Memory.Areas
Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", stopwatch.Elapsed.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture)),
Error = e.GetMessage()
});
-
- errorRuntime = errorRuntime.Add(stopwatch.Elapsed);
}
}
@@ -422,27 +394,42 @@ public void Optimize(Enums.Memory.Optimization.Reason reason, Enums.Memory.Areas
OnOptimizeProgressUpdate(value, Localizer.String.GarbageCollector);
}
- App.ReleaseMemory();
+ optimizationTiming.Complete(App.ReleaseMemory);
+
+ var appReleaseTiming = new LogOptimizationDataMemoryArea
+ {
+ Name = Localizer.String.GarbageCollector,
+ Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", optimizationTiming.FinalAppReleaseDuration.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture))
+ };
+
+ // Include final-stage timing without turning failed native work into a success notification.
+ if (info.MemoryAreas.Any())
+ info.MemoryAreas.Add(appReleaseTiming);
+
+ if (error.MemoryAreas.Any())
+ error.MemoryAreas.Add(appReleaseTiming);
}
catch
{
// ignored
}
+ var totalRuntime = optimizationTiming.Elapsed;
+
// Log
try
{
// Info
if (info.MemoryAreas.Any())
{
- info.Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", infoRuntime.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture));
+ info.Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", totalRuntime.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture));
Logger.Log(new Log(Enums.Log.Levels.Information, Localizer.String.MemoryOptimized, info));
}
// Error
if (error.MemoryAreas.Any())
{
- error.Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", errorRuntime.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture));
+ error.Duration = string.Format(Localizer.Culture, "{0:0.0} {1}", totalRuntime.TotalSeconds, Localizer.String.Seconds.ToLower(Localizer.Culture));
Logger.Log(new Log(Enums.Log.Levels.Error, Localizer.String.Invalid, error));
}
diff --git a/src/Test/ReleaseMemoryTests.cs b/src/Test/ReleaseMemoryTests.cs
new file mode 100644
index 00000000..67b3eb63
--- /dev/null
+++ b/src/Test/ReleaseMemoryTests.cs
@@ -0,0 +1,194 @@
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+
+namespace WinMemoryCleaner.MemoryReleaseTests
+{
+ [TestFixture]
+ public sealed class ReleaseMemoryTests
+ {
+ [Test]
+ public void ReleaseMemory_WhenModeIsNone_DoesNotRunCleanupStages()
+ {
+ var calls = new List();
+
+ App.ReleaseMemory(
+ ReleaseMemoryMode.None,
+ () => calls.Add("GC"),
+ () => calls.Add("Trim"));
+
+ Assert.AreEqual(0, calls.Count);
+ }
+
+ [Test]
+ public void ReleaseMemory_WhenModeIsGCOnly_RunsOnlyGarbageCollection()
+ {
+ var calls = new List();
+
+ App.ReleaseMemory(
+ ReleaseMemoryMode.GCOnly,
+ () => calls.Add("GC"),
+ () => calls.Add("Trim"));
+
+ CollectionAssert.AreEqual(new[] { "GC" }, calls);
+ }
+
+ [Test]
+ public void ReleaseMemory_WhenModeIsTrimOnly_RunsOnlyWorkingSetTrim()
+ {
+ var calls = new List();
+
+ App.ReleaseMemory(
+ ReleaseMemoryMode.TrimOnly,
+ () => calls.Add("GC"),
+ () => calls.Add("Trim"));
+
+ CollectionAssert.AreEqual(new[] { "Trim" }, calls);
+ }
+
+ [Test]
+ public void ReleaseMemory_WhenModeIsCombined_RunsBothCleanupStages()
+ {
+ var calls = new List();
+
+ App.ReleaseMemory(
+ ReleaseMemoryMode.Combined,
+ () => calls.Add("GC"),
+ () => calls.Add("Trim"));
+
+ CollectionAssert.AreEqual(new[] { "GC", "Trim" }, calls);
+ }
+
+ [Test]
+ public void ReleaseMemory_WhenCombined_RunsGarbageCollectionBeforeWorkingSetTrim()
+ {
+ var calls = new List();
+
+ App.ReleaseMemory(
+ ReleaseMemoryMode.Combined,
+ () => calls.Add("GC"),
+ () => calls.Add("Trim"));
+
+ Assert.Less(calls.IndexOf("GC"), calls.IndexOf("Trim"));
+ }
+
+ [Test]
+ public void ReleaseMemory_WhenGarbageCollectionThrows_StillRunsWorkingSetTrim()
+ {
+ var calls = new List();
+
+ App.ReleaseMemory(
+ ReleaseMemoryMode.Combined,
+ () =>
+ {
+ calls.Add("GC");
+ throw new InvalidOperationException();
+ },
+ () => calls.Add("Trim"));
+
+ CollectionAssert.AreEqual(new[] { "GC", "Trim" }, calls);
+ }
+
+ [Test]
+ public void ReleaseMemory_WhenWorkingSetTrimThrows_DoesNotPreventGarbageCollection()
+ {
+ var calls = new List();
+
+ App.ReleaseMemory(
+ ReleaseMemoryMode.Combined,
+ () => calls.Add("GC"),
+ () =>
+ {
+ calls.Add("Trim");
+ throw new InvalidOperationException();
+ });
+
+ CollectionAssert.AreEqual(new[] { "GC", "Trim" }, calls);
+ }
+
+ [Test]
+ public void OptimizeTiming_WhenFinalAppReleaseRuns_IncludesItInTotalDuration()
+ {
+ var calls = new List();
+ var clock = new FakeClock();
+ var timing = new OptimizationTiming(clock.Now);
+
+ timing.Measure(() =>
+ {
+ calls.Add("Native");
+ clock.Advance(TimeSpan.FromSeconds(2));
+ });
+
+ var totalRuntime = timing.Complete(() =>
+ {
+ calls.Add("AppRelease");
+ clock.Advance(TimeSpan.FromSeconds(3));
+ });
+
+ CollectionAssert.AreEqual(new[] { "Native", "AppRelease" }, calls);
+ Assert.AreEqual(TimeSpan.FromSeconds(3), timing.FinalAppReleaseDuration);
+ Assert.AreEqual(TimeSpan.FromSeconds(5), totalRuntime);
+ }
+
+ [Test]
+ public void OptimizeTiming_WhenNativeStageFailsAndOverheadRuns_IncludesAllElapsedTime()
+ {
+ var clock = new FakeClock(TimeSpan.FromSeconds(100));
+ var timing = new OptimizationTiming(clock.Now);
+
+ Assert.Throws(() => timing.Measure(() =>
+ {
+ clock.Advance(TimeSpan.FromSeconds(2));
+ throw new InvalidOperationException();
+ }));
+
+ clock.Advance(TimeSpan.FromSeconds(1));
+
+ var totalRuntime = timing.Complete(() => clock.Advance(TimeSpan.FromSeconds(3)));
+
+ Assert.AreEqual(TimeSpan.FromSeconds(6), totalRuntime);
+ }
+
+ [Test]
+ public void OptimizeTiming_WhenFinalAppReleaseThrows_CapturesItsDuration()
+ {
+ var clock = new FakeClock();
+ var timing = new OptimizationTiming(clock.Now);
+
+ clock.Advance(TimeSpan.FromSeconds(1));
+
+ Assert.Throws(() => timing.Complete(() =>
+ {
+ clock.Advance(TimeSpan.FromSeconds(2));
+ throw new InvalidOperationException();
+ }));
+
+ Assert.AreEqual(TimeSpan.FromSeconds(2), timing.FinalAppReleaseDuration);
+ Assert.AreEqual(TimeSpan.FromSeconds(3), timing.Elapsed);
+ }
+
+ private sealed class FakeClock
+ {
+ private TimeSpan _now;
+
+ public FakeClock(TimeSpan? initialTime = null)
+ {
+ _now = initialTime ?? TimeSpan.Zero;
+ }
+
+ public void Advance(TimeSpan duration)
+ {
+ _now = _now.Add(duration);
+ }
+
+ public TimeSpan Now()
+ {
+ return _now;
+ }
+ }
+ }
+}
+
+#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
diff --git a/src/WinMemoryCleaner.csproj b/src/WinMemoryCleaner.csproj
index 6bd06c24..2e2b415c 100644
--- a/src/WinMemoryCleaner.csproj
+++ b/src/WinMemoryCleaner.csproj
@@ -128,6 +128,7 @@
+
@@ -163,6 +164,7 @@
+