I’m seeing the performance counter “# Induced GC” (which should stay at zero in a perfect app) increasing rapidly when processing small files (<= 32×32) via WriteableBitmap.
While this isn’t a significant bottleneck inside a small app, it becomes a very huge problem (app freezing at 99.75% “% Time in GC” for several seconds at each step) when there exist some thousand objects in memory (ex: EntityFramework context loaded with many entities and relationships).
Synthetic test:
var objectCountPressure = (
from x in Enumerable.Range(65, 26)
let root = new DirectoryInfo((char)x + ":\\")
let subs =
from y in Enumerable.Range(0, 100 * IntPtr.Size)
let sub =new {DI = new DirectoryInfo(Path.Combine(root.FullName, "sub" + y)), Parent = root}
let files = from z in Enumerable.Range(0, 400) select new {FI = new FileInfo(Path.Combine(sub.DI.FullName, "file" + z)), Parent = sub}
select new {sub, files = files.ToList()}
select new {root, subs = subs.ToList()}
).ToList();
const int Size = 32;
Action<int> handler = threadnr => {
Console.WriteLine(threadnr + " => " + Thread.CurrentThread.ManagedThreadId);
for (int i = 0; i < 10000; i++) {
var wb = new WriteableBitmap(Size, Size, 96, 96, PixelFormats.Bgra32, null);
wb.Lock();
var stride = wb.BackBufferStride;
var blocks = stride / sizeof(int);
unsafe {
var row = (byte*)wb.BackBuffer;
for (int y = 0; y < wb.PixelHeight; y++, row += stride)
{
var start = (int*)row;
for (int x = 0; x < blocks; x++, start++)
*start = i;
}
}
wb.Unlock();
wb.Freeze(); }
};
var sw = Stopwatch.StartNew();
Console.WriteLine("start: {0:n3} ms", sw.Elapsed.TotalMilliseconds);
Parallel.For(0, Environment.ProcessorCount, new ParallelOptions{MaxDegreeOfParallelism = Environment.ProcessorCount}, handler);
Console.WriteLine("stop : {0:n2} s", sw.Elapsed.TotalSeconds);
GC.KeepAlive(objectCountPressure);
I can run this test using “const int Size = 48” a dozen times: It always returns in ~1.5s and “# Induced GC” sometimes increases by 1 or 2.
When I change “const int Size = 48” into “const int Size = 32” then something very very bad is happening: “# Induced GC” increases by 10 per second and the overall runtime now is more than a minute: ~80s !
[Tested on Win7x64 Core-i7-2600 with 8GB RAM // .NET 4.0.30319.237 ]
WTF!?
Either the Framework has a very bad bug or I’m doing something entirely wrong.
BTW:
I came around this problem not by doing image processing but by just using a Tooltip containing an Image against some database entities via a DataTemplate:
This worked fine (fast) while there didn’t exist very much objects in RAM — but when there existed some million other objects (totally unrelated) then showing the Tooltip always delayed for several seconds, while everything else just was working fine.
Under all of the
SafeMILHandleMemoryPressureandSafeMILHandlenonsense is a call to a method onMS.Internal.MemoryPressure, which uses the static field “_totalMemory” to keep track of how much memory WPF thinks is allocated. When it hits a (rather small) limit, the induced GCs start and never end.You can stop WPF from behaving this way entirely using a little reflection magic; just set
_totalMemoryto something appropriately negative so the limit is never reached and the induced GCs never happen: