none
Memory Allocation Issues (possible leak) in WPF application, using Pages and TabContol RRS feed

  • Question

  • Disclaimer:

    I'm having huge issue with memory managment, within my application. They have poped up first in May, 2018, but thanks to various articles on MSDN and here, SO, I was able to reduce the problem, but not really fix it. After I did some reading, testing and coding, I'm comming here, because I need help.

    Application background:

    My app is a sell-management app, for small client. It runs on remote desktops, with quite limited memory, so I need to keep memory usage low. MainWindow, consists list menu, to the right (old-schoolish), after clicking on menu item, new Page is being opened as a RadTabItem (ib4 you hit me with "Ask teleriks for that", it doesn't matter, I have the same issue with TabItems) content, here is a code sample:

     private void KlientDzwoniTab(object sender, MouseButtonEventArgs e)
        {
            App.StronaGlowna.Cursor = Cursors.Wait;
    
            try
            {
                if (CheckIfTabIsOpened("Klient dzwoni") == false)
                {
                    _nazwakarty = "Klient dzwoni";
                    var strona = new KlientDzwoni();
                    AddItem(strona, _nazwakarty);
    
                    App.StronaGlowna.Cursor = Cursors.Arrow;
                }
                else
                {
                    SelectTab("Klient dzwoni");
                    App.StronaGlowna.Cursor = Cursors.Arrow;
                }
            }
            catch (Exception exception)
            ... catch block and stuff

    and essential thing, which is AddItem() function:

    public void AddItem(Page strona, string header)
        {
    
            try
            {
                #region Close button region
                var grid = new Grid();
    
    
                var c1 = new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) };
    
                var c2 = new ColumnDefinition { Width = new GridLength(21) };
    
    
                grid.ColumnDefinitions.Add(c1);
                grid.ColumnDefinitions.Add(c2);
    
    
    
                var button = new RadButton
                {
                    Padding = new Thickness(0, 0, 0, 0),
                    Margin = new Thickness(5, 0, 0, 0),
                    Width = 16,
                    Height = 16,
                    HorizontalAlignment = HorizontalAlignment.Stretch,
                    VerticalAlignment = VerticalAlignment.Top,
                    VerticalContentAlignment = VerticalAlignment.Top,
                    Content = "x",
                    FontWeight = FontWeights.Bold,
                    Background = Brushes.Transparent,
                    BorderThickness = new Thickness(0, 0, 0, 0),
                    Tag = header
    
    
                };
    
                button.Click += Zamknijtab;
    
                var stringHeader = header.Replace(" ", "");
                stringHeader = stringHeader.Replace(":", "");
                stringHeader = stringHeader.Replace("/", "");
                stringHeader = stringHeader.Replace("-", "");
                stringHeader = stringHeader.Replace(".", "");
                stringHeader = stringHeader.Replace("_", "");
    
                var textBlock = new TextBlock
                {
                    Text = header
                };
    
                // panel.Name = header;
    
    
    
                grid.Children.Add(textBlock);
                Grid.SetColumn(textBlock, 0);
    
                grid.Children.Add(button);
                Grid.SetColumn(button, 1);
    
              #endregion
    
                var content = strona.Content;
    
                var itemToAdd = new RadTabItem
                {
                    Header = grid,
                    Content = content
    
    
                };
    
    
    
                var klientDzwoni = strona as KlientDzwoni;
                if (klientDzwoni != null)
                {
    
                    itemToAdd.KeyDown += klientDzwoni.F3KeyDown;
                }
    
                itemToAdd.SetValue(NameProperty, stringHeader);
    
    
                tabControl.Items.Add(itemToAdd);
    
                tabControl.SelectedItem = itemToAdd;
    
    
    
            }
            catch (Exception e)
            {
               ...catch stuff
            }
    
    
        }

    Now, user can, obviously, close those tabs, hitting Zamknijtab event:

    public void Zamknijtab(object sender, RoutedEventArgs e)
        {
    
            try
            {
                var listaTabow = tabControl.Items;
    
                var button = sender as RadButton;
    
                if (button != null)
                {
                    if (button.Tag != null)
                    {
                        var stringHeader = button.Tag as string;
                        stringHeader = stringHeader.Replace(" ", "");
                        stringHeader = stringHeader.Replace(":", "");
                        stringHeader = stringHeader.Replace("/", "");
                        stringHeader = stringHeader.Replace("-", "");
                        stringHeader = stringHeader.Replace(".", "");
                        stringHeader = stringHeader.Replace("_", "");
    
                        var tabs = listaTabow.Cast<RadTabItem>();
    
    
    
                        tab = tabs.Reverse().FirstOrDefault(f => f.Name == stringHeader);
                        if (tab == null)
                        {
                            tab = tabs.Reverse().FirstOrDefault(f => f.Name == stringHeader);
                        }
    
                        if (tab != null)
                        {
                            if (button.Tag.ToString().Contains("Zam:") &&
                                !button.Tag.ToString().Contains("podsumowanie"))
                            {
    
                                NumerZamowienia = stringHeader.Replace("Zam", "");
                                if (App.ZamowieniaCommitGet(NumerZamowienia))
                                {
    
                                    var textBox = new Label
                                    {
                                        Content =
                                            "Na zamówieniu są wprowadzone pozycje, czy chcesz usunąć zamówienie?!",
                                        FontWeight = FontWeights.Bold,
                                        Foreground = Brushes.Red
                                    };
    
    
    
    
                                    RadWindow.Confirm(new DialogParameters
                                    {
    
                                        Header = "Potwierdź zamknięcie okna",
                                        Content = textBox,
                                        Closed = OnConfirmClosed,
                                        Owner = App.StronaGlowna,
                                        OkButtonContent = "Tak",
                                        CancelButtonContent = "Nie"
    
                                    });
    
    
                                }
                                else
                                {
                                    if (NumerZamowienia != null)
                                    {
    
                                        var zamowienie = Db.dst_Orders.FirstOrDefault(f => f.Numer == NumerZamowienia);
                                        if (zamowienie != null)
                                        {
                                            ZamowienieId = zamowienie.Id_Order;
                                        }
    
                                        var pozycje = Db.dst_OrderLines.Where(f =>
                                            f.Order_Id == ZamowienieId && f.Ilosc != null && f.Ilosc > 0);
                                        if (!pozycje.Any())
                                        {
                                            var id = App.GetUserId();
                                            Db.No_Order(ZamowienieId, null, id);
                                        }
                                        tabControl.Items.Remove(tab);
    
    
                                    }
                                }
                            }
                            else
                            {
                                tabControl.Items.Remove(tab);
                            }
    
                        }
    
                    }
                    else
                    {
                        var typ = tabControl.SelectedItem as RadTabItem;
                        var tabs = listaTabow.Cast<RadTabItem>();
                        if (typ != null)
                        {
                            tab = tabs.Reverse().FirstOrDefault(f => f.Name == typ.Name);
    
                            if (tab != null)
                            {
                                tabControl.Items.Remove(tab);
    
                            }
                        }
                    }               
                }
                else
                {
                    var typ = tabControl.SelectedItem as RadTabItem;
                    var tabs = listaTabow.Cast<RadTabItem>();
                    if (typ != null)
                    {
                        tab = tabs.Reverse().FirstOrDefault(f => f.Name == typ.Name);
    
                        if (tab != null)
                        {
                            tabControl.Items.Remove(tab);
    
                        }
                    }
                }
    
    
    
            }
            catch (Exception exception)
            {
               ... Another catch

    This one, could've been a little refactored, but well, it is, as it is.

    The Problem:

    Closing tabs, doesn't release allocated memory to them (like never).

    All my items, opened in RadTabItems are a custom Pages (tried it with UserControl, same issue) and problem exist to all of them. KlientDzwoni() class is a little one, raising memory usage, by, like ~4 MB, but I have much bigger pages, that raise it by 100-200 MB (reporting tools).

    Speaking of memory and MB, you might ask, how did I analyze those, so I started with, ofc, Task Manager and it shows, that memory usage raises, every time I open new tab, and it never goes down. After reading this wonderful post, I've downloaded JetBrains dotMemory, and it helped me to detect memory leaks in my application, BUT it doesn't actually cosider not releasing memory as a leak.

    In my opinion, those pages (like KlientDzwoni) are being held somewhere in memory, for some reason and are never released AND not considered as not needed (thus, not being detected as a leak). Here are things I've tried, to fix the problem.

    My Miserable Attempts:

    • Referring to article, mentioned before, I've tried to use GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); . It actually helps a little (reduces leak, by ~10-20%), but doesn't fully fix the problem. Also, since it's not recommended to force GC to work, I'm kinda scared.
    • BindingOperations.ClearBinding - I thought that this idea is kinda cool. I mean, I don't unbind events and property in my pages, firstly, because I don't have correct event for that (well, there is no Closing event, there is Unloaded on the other hand), and secondly, it's C#, so I expect Dispatcher and GC to do the job for me. Maybe I'm completly wrong about this, and I should unsubscribe all the events and nullify all collections and variables in some event, but, when TabItems, is removed, it's content (Page.Content) is removed, thus, there shouldn't be any event, to unsubscribe, right?
    • Nullifying variables and properties, like TabItem.Content, variables, holding page objects, etc. 0 results.
    • Using UserControl, I mentioned it above, it doesn't help at all.
    • Giving up on my beloved framework, which is Telerik (it is not), and using TabItems, instead of RadTabItems, 0 results.

    What was actually unhelpful:

    There are many posts, articles, thesis, about Memory Leaks (I'm still unsure if it is a leak) and TabControl probles, but, I believe, they are constructed poorly, so answers are inacurate for few reasons:

    • Many answers focuses on "Memory Leak", with ideas how to check if it is a memory leak, or it's not, or it's, I don't know, Apricot. The core of it problem is that, memory is being allocated and is never released. I don't care how do we call that issue.
    • Other answers links questions/posts/articles/answers with the same, recurring issues. This link is being used the most often.

    I wasn't able to find actual solution to this issue. It's been posted on SO, on MSDN and in the other places, many times, in past few years, but I've never found OP answer like "thanks guys, the issue was xxx, the fix was to yyy". But there is some kind of pattern - Someone comes with TabItem memory issues, and never leaves with correct answer.

    So, can someone, finally, answer question "How to release allocated memory" and end this endless problem?

    EDIT:

    Uploaded sample project here: https://1drv.ms/u/s!Ali8Cn1kITEDhEwn-WAEEl04talS 

    It consists telerik components, but can be replaced with vanilla WPF, to present the same behaviour.



    • Edited by Vanghern Wednesday, February 20, 2019 8:09 AM link change
    Tuesday, February 19, 2019 2:11 PM

Answers

  • Hi  Vanghern,

    As far as I know, when call GC.Collect does remove the container from generators cache but if you have events or handlers remaining between the item and container the container will not get collected by GC.

    Therefore release all the handlers and wpf will take care of removing container from memory, actually GC will remove it.  

    So simply release all your custom logic you have define.

    The following code I test on my side. 

    public void Zamknijtab(object sender, RoutedEventArgs e)
            {
                var listaTabow = tabControl.Items;
                var button = sender as RadButton;
                if (button != null)
                {
                    button.Click -= Zamknijtab;
                    var typ = tabControl.SelectedItem as RadTabItem;
                    var tabs = listaTabow.Cast<RadTabItem>();
                    if (typ != null)
                    {
                        tab = tabs.Reverse().FirstOrDefault(f => f.Name == typ.Name);
                        if (tab != null)
                        {
                            tabControl.Items.Remove(tab);
                            tab = null;
                        }
                    }
                    GC.Collect();
    
                }
                else
                {
                    var typ = tabControl.SelectedItem as RadTabItem;
                    var tabs = listaTabow.Cast<RadTabItem>();
                    if (typ != null)
                    {
                        tab = tabs.Reverse().FirstOrDefault(f => f.Name == typ.Name);
    
                        if (tab != null)
                        {
                            tabControl.Items.Remove(tab);
                        }
                    }
                }
                App.main.Cursor = Cursors.Arrow;
            }
    
    
    

    Best Regards,

    Yong Lu


    MSDN Community Support
    Please remember to click "Mark as Answer" the responses that resolved your issue, and to click "Unmark as Answer" if not. This can be beneficial to other community members reading this thread. If you have any compliments or complaints to MSDN Support, feel free to contact MSDNFSF@microsoft.com.

    • Marked as answer by Vanghern Thursday, February 21, 2019 10:10 AM
    Thursday, February 21, 2019 5:18 AM
    Moderator

All replies

  • Hi  Vanghern,

    Unfortunately, we cannot download the demo form the link you provided.



    I suggest you can upload your minimum demo to OneDrive(Including your test material/steps and remove all private information). We can download it and debugging. 
    Share OneDrive files and folders:
    https://support.office.com/en-us/article/Share-OneDrive-files-and-folders-9fcc2f7d-de0c-4cec-93b0-a82024800c07


    Best Regards,

    Yong Lu


    MSDN Community Support
    Please remember to click "Mark as Answer" the responses that resolved your issue, and to click "Unmark as Answer" if not. This can be beneficial to other community members reading this thread. If you have any compliments or complaints to MSDN Support, feel free to contact MSDNFSF@microsoft.com.

    Wednesday, February 20, 2019 7:31 AM
    Moderator
  • Done, here is the file: https://1drv.ms/u/s!Ali8Cn1kITEDhEwn-WAEEl04talS
    Wednesday, February 20, 2019 8:09 AM
  • Hi  Vanghern,

    As far as I know, when call GC.Collect does remove the container from generators cache but if you have events or handlers remaining between the item and container the container will not get collected by GC.

    Therefore release all the handlers and wpf will take care of removing container from memory, actually GC will remove it.  

    So simply release all your custom logic you have define.

    The following code I test on my side. 

    public void Zamknijtab(object sender, RoutedEventArgs e)
            {
                var listaTabow = tabControl.Items;
                var button = sender as RadButton;
                if (button != null)
                {
                    button.Click -= Zamknijtab;
                    var typ = tabControl.SelectedItem as RadTabItem;
                    var tabs = listaTabow.Cast<RadTabItem>();
                    if (typ != null)
                    {
                        tab = tabs.Reverse().FirstOrDefault(f => f.Name == typ.Name);
                        if (tab != null)
                        {
                            tabControl.Items.Remove(tab);
                            tab = null;
                        }
                    }
                    GC.Collect();
    
                }
                else
                {
                    var typ = tabControl.SelectedItem as RadTabItem;
                    var tabs = listaTabow.Cast<RadTabItem>();
                    if (typ != null)
                    {
                        tab = tabs.Reverse().FirstOrDefault(f => f.Name == typ.Name);
    
                        if (tab != null)
                        {
                            tabControl.Items.Remove(tab);
                        }
                    }
                }
                App.main.Cursor = Cursors.Arrow;
            }
    
    
    

    Best Regards,

    Yong Lu


    MSDN Community Support
    Please remember to click "Mark as Answer" the responses that resolved your issue, and to click "Unmark as Answer" if not. This can be beneficial to other community members reading this thread. If you have any compliments or complaints to MSDN Support, feel free to contact MSDNFSF@microsoft.com.

    • Marked as answer by Vanghern Thursday, February 21, 2019 10:10 AM
    Thursday, February 21, 2019 5:18 AM
    Moderator
  • Thanks, this actually helped. What is strange, for larger pages in my main app, they still remain in memory and GC needs to be called like 3 times, before memory is released. But still, it fixed issues (or at least improved performance).
    Thursday, February 21, 2019 10:12 AM