2

I have a simple program that just reading XPS file, i've read the following post and it did solve part of the issue.
Opening XPS document in .Net causes a memory leak

class Program
{
    static int intCounter = 0;
    static object _intLock = new object();

    static int getInt()
    {
        lock (_intLock)
        {
            return intCounter++;
        }
    }

    static void Main(string[] args)
    {

        Console.ReadLine();

        for (int i = 0; i < 100; i++)
        {

            Thread t = new Thread(() =>
            {
                var ogXps = File.ReadAllBytes(@"C:\Users\Nathan\Desktop\Objective.xps");
                readXps(ogXps);
                Console.WriteLine(getInt().ToString());
            });
            t.SetApartmentState(ApartmentState.STA);
            t.Start();
            Thread.Sleep(50);
        }
        Console.ReadLine();
    }


    static void readXps(byte[] originalXPS)
    {
        try
        {

            MemoryStream inputStream = new MemoryStream(originalXPS);
            string memoryStreamUri = "memorystream://" + Path.GetFileName(Guid.NewGuid().ToString() + ".xps");
            Uri packageUri = new Uri(memoryStreamUri);
            Package oldPackage = Package.Open(inputStream);
            PackageStore.AddPackage(packageUri, oldPackage);
            XpsDocument xpsOld = new XpsDocument(oldPackage, CompressionOption.Normal, memoryStreamUri);
            FixedDocumentSequence seqOld = xpsOld.GetFixedDocumentSequence();


            //The following did solve some of the memory issue
            //-----------------------------------------------
            var docPager = seqOld.DocumentPaginator;
            docPager.ComputePageCount();
            for (int i = 0; i < docPager.PageCount; i++)
            {
                FixedPage fp = docPager.GetPage(i).Visual as FixedPage;
                fp.UpdateLayout();
            }
            seqOld = null;
            //-----------------------------------------------


            xpsOld.Close();
            oldPackage.Close();
            oldPackage = null;
            inputStream.Close();
            inputStream.Dispose();
            inputStream = null;
            PackageStore.RemovePackage(packageUri);
        }
        catch (Exception e)
        {

        }
    }
}

^ The program will read a XPS file for hundred times Before apply the fix ^Before apply the fix

After the fix ^After apply the fix

So the fix in the post suggested did eliminate some objects, however i found that there are still objects like Dispatcher , ContextLayoutManager and MediaContext still exists in memory and their number are exactly 100, is this a normal behavior or a memory leak? How do i fix this? Thanks.

25/7/2018 Update
Adding the line Dispatcher.CurrentDispatcher.InvokeShutdown(); did get rid of the Dispatcher , ContextLayoutManager and MediaContext object, don't know if this is an ideal way to fix.

nathan1658
  • 175
  • 1
  • 14

1 Answers1

1

It looks like those classes you're left with are from the XPSDocument, that implements IDisposable but you don't call those. And there are a few more classes that implement that same interface and if they do, as a rule of thumb, either wrap them in a using statement so it is guaranteed their Dispose method gets called or call their Dispose method your self.

An improved version of your readXps method will look like this:

static void readXps(byte[] originalXPS)
{
    try
    {
        using (MemoryStream inputStream = new MemoryStream(originalXPS))
        {
            string memoryStreamUri = "memorystream://" + Path.GetFileName(Guid.NewGuid().ToString() + ".xps");
            Uri packageUri = new Uri(memoryStreamUri);
            using(Package oldPackage = Package.Open(inputStream))
            {
                PackageStore.AddPackage(packageUri, oldPackage);
                using(XpsDocument xpsOld = new XpsDocument(oldPackage, CompressionOption.Normal, memoryStreamUri))
                {
                    FixedDocumentSequence seqOld = xpsOld.GetFixedDocumentSequence();

                    //The following did solve some of the memory issue
                    //-----------------------------------------------
                    var docPager = seqOld.DocumentPaginator;
                    docPager.ComputePageCount();
                    for (int i = 0; i < docPager.PageCount; i++)
                    {
                        FixedPage fp = docPager.GetPage(i).Visual as FixedPage;
                        fp.UpdateLayout();
                    }
                    seqOld = null;
                    //-----------------------------------------------           
                } // disposes XpsDocument
            } // dispose Package
            PackageStore.RemovePackage(packageUri);
         } // dispose MemoryStream
    }
    catch (Exception e)
    {
        // really do something here, at least:
        Debug.WriteLine(e);
    }
}

This should at least clean-up most of the objects. I'm not sure if you're going to see the effects in your profiling as that depends on if the objects are actually collected during your analysis. Profiling a debug build might give unanticipated results.

As the remainder of those object instances seem to be bound to the System.Windows.Threading.Dispatcher I suggest you could try to keep a reference to your Threads (but at this point you might consider looking into Tasks) ansd once all threads are done, call the static ExitAllFrames on the Dispatcher.

Your main method will then look like this:

Console.ReadLine();

Thread[] all = new Thread[100];
for (int i = 0; i < all.Length; i++)
{
    var t = new Thread(() =>
    {
        var ogXps = File.ReadAllBytes(@"C:\Users\Nathan\Desktop\Objective.xps");
        readXps(ogXps);
        Console.WriteLine(getInt().ToString());
    });
    t.SetApartmentState(ApartmentState.STA);
    t.Start();
    all[i] = t; // keep reference
    Thread.Sleep(50);
}
foreach(var t in all) t.Join(); // https://stackoverflow.com/questions/263116/c-waiting-for-all-threads-to-complete
all = null; // meh
Dispatcher.ExitAllFrames(); // https://stackoverflow.com/a/41953265/578411
Console.ReadLine();
rene
  • 41,474
  • 78
  • 114
  • 152
  • Thanks for the detailed tips! I'm profiling with Release build but those objects are still existing :\ – nathan1658 Jul 22 '18 at 09:41
  • Then what remains are the Threads that are kept alive. Instead of having those threads scoped to the inner for loop, keep them in an array, at the end of your loop Join them all on the current thread, and call Dispatcher.ExitAllFrames(); but I've not tried this before if this can be done without side-effects. So use with care and read-up on it to prevent surprises – rene Jul 22 '18 at 10:08
  • I see your point, but aren't the thread will be "killed" once it reach is end of block? – nathan1658 Jul 22 '18 at 11:59
  • @nathan1658 well, yes. But as those classes seem tied to the WPF infrastructure I can imagine they "keep" the thread around until the appdomain unloads as spinning up new threads would be expensive. But I admit it is more a thing worth trying then that I have been digging through the reference source to confirm my hunch. – rene Jul 22 '18 at 12:04
  • I think I have fixed the issue, though i don't know if it's the most ideal solution. Seems calling the XpsDocument will bring the WPF dispatcher to the STA thread, that i have to shut it down manually else it will retain in the memory. Anyway thanks for your help! – nathan1658 Jul 25 '18 at 15:05