-2

I'm dealing with a memory leak in my code. I'm using C# and WPF

Let me explain the code a bit. Firstly, I'm reading a long text file (several thousands of lines) into a string array. After that, im going through each line in the array, and I'm filtering out the lines I don't need. After a bunch of filtering, around 3000 lines are remaining. Then, I create a new object of the type ItemEntry for each line that remained. An ItemEntry object has a canvas, that contains several textboxes, textblocks and buttons. When that is done, all the 3000 objects are listed in a StackPanel that is inside a scrollviewer.

When reading a file, it always goes through this process. Before reading a new file, it clears the StackPanel. One process takes up a lot of memory already, but unfortunately, upon cleaning the StackPanel and reading a new file of the same length but with different content, it does not seem to free up the memory used by the content of the StackPanel before, even though I cleared it. The usage is only rising.

I'd appreciate help finding/fixing the memory leak, as well as ideas for improving the memory consumption in the first place. Thank you very much!

Here is the code that reads the file and displays it:

public async void LoadLootTable(string path)
{

    //Get list of content in file, remove all non-item lines so only items remain
    string[] loadedItems = File.ReadAllLines(currentLootTable);
    List<string> items = new List<string>();

    items.Clear();
    foreach (string item in loadedItems)
    {
        if (item.Contains("\"tag\""))
        {
            string itemFiltered;
            itemFiltered = item.Replace(" ", "");
            itemFiltered = itemFiltered.Replace("\"tag\":", "");
            itemFiltered = itemFiltered.Substring(1, itemFiltered.Length - 2);
            items.Add(itemFiltered);
        }
        else if (!item.Contains("\"tag\"") && !item.Contains("{") && !item.Contains("}") && !item.Contains("[") && !item.Contains("]") && !item.Contains("\"rolls\"") && !item.Contains("\"type\"") && !item.Contains("\"function\"") && item.Contains("\"") && !item.Contains("\"weight\"") && !item.Contains("\"count\"") && !item.Contains("\"min\": 1") && !item.Contains("\"max\": 64") && !item.Contains("\"out\"") && !item.Contains("\"score\""))
        {
            string itemFiltered;
            itemFiltered = item.Replace("\"", "");
            itemFiltered = itemFiltered.Replace("name:", "");
            itemFiltered = itemFiltered.Replace(" ", "");
            itemFiltered = itemFiltered.Replace("tag:", "");
            itemFiltered = itemFiltered.Replace(",", "");
            items.Add(itemFiltered);
        }
    }

    List<string> finalItemList = new List<string>();
    //Check for each item if it has NBT and add it to the string
    for (int i = 0; i < items.Count; i++)
    {
        if (!items[i].Contains("{"))
        {
            if (i < items.Count - 1)
            {

                if (items[i + 1].Contains("{"))
                {
                    finalItemList.Add(string.Format("{0};{1}", items[i], items[i + 1]));
                }

                if (!items[i + 1].Contains("{"))
                {
                    finalItemList.Add(string.Format("{0};none", items[i]));
                }
            }
            else
            {
                {
                    finalItemList.Add(string.Format("{0};none", items[i]));
                }
            }
        }
    }

    itemList.Clear();
    int index = 0;
    //Add an entry for all items
    foreach (string item in finalItemList)
    {
        string[] splitItem = item.Split(';');
        itemList.Add(new itemEntry(splitItem[0], splitItem[1], index));
        index++;
    }

    //Show 'loading' message
    stpWorkspace.Children.Clear();
    tblLoadingItems.Margin = new Thickness(svWorkspace.ActualWidth / 2 - 150, svWorkspace.ActualHeight / 2 - 75, 0, 0);
    tblLoadingItems.Text = "Loading items, please wait...\nThis may take a few seconds!";
    stpWorkspace.Children.Add(tblLoadingItems);
    await Task.Delay(5); //Allows the UI to update and show the textblock

    //Add all item entrys to workspace
    stpWorkspace.Children.Clear();
    tblLootTableStats.Text = string.Format("Current Loot table: {0} - Total amount of items: {1}", currentLootTable.Replace(currentDatapack, "").Replace("/data/randomitemgiver/loot_tables/", ""), itemList.Count);
    stpWorkspace.Children.Add(cvsLootTableStats);
    foreach (itemEntry entry in itemList)
    {
        stpWorkspace.Children.Add(entry.bdrItem);
    }
}

And here's the ItemEntry class: 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows;

namespace Random_Item_Giver_Updater
{
    public class itemEntry
    {
        //Controls
        public Border bdrItem = new Border();
        private Canvas cvsItem = new Canvas();
        private TextBlock tblItemName = new TextBlock();
        private TextBlock tblItemNBT = new TextBlock();
        public Button btnDelete = new Button();
        private TextBox tbItemName = new TextBox();
        private Button btnSaveItemName = new Button();
        private TextBox tbItemNBT = new TextBox();
        private Button btnSaveItemNBT = new Button();
        public TextBlock tblEntryIndex = new TextBlock();
        private TextBlock tblIndicator = new TextBlock();

        //Item attributes
        public string itemName;     
        public string itemNBT;
        public string newName;
        public string newNBT;
        private int itemIndex;
        public bool isModified = false;
        public bool isDeleted = false;

        //Reference to main window
        MainWindow wndMain = (MainWindow)Application.Current.MainWindow;

        //-- Constructor --//

        public itemEntry(string name, string nbt, int index)
        {

            //Initialize variables
            itemName = name.TrimEnd('\r', '\n');
            itemNBT = nbt.TrimEnd('\r', '\n'); ;
            itemIndex = index;
            newName = itemName;
            newNBT = itemNBT;

            //Set backcolor
            if (index % 2 == 0)
            {
                cvsItem.Background = new SolidColorBrush(Color.FromArgb(100, 70, 70, 70));
            }
            else
            {
                cvsItem.Background = new SolidColorBrush(Color.FromArgb(100, 90, 90, 90));
            }
            cvsItem.Height = 50;

            //Create itemborder
            bdrItem.Margin = new Thickness(0, 0, 0, 0);
            bdrItem.Child = cvsItem;
            bdrItem.HorizontalAlignment = HorizontalAlignment.Stretch;
            bdrItem.VerticalAlignment = VerticalAlignment.Top;

            //Create item name text
            tblItemName.Margin = new Thickness(10, 10, 0, 0);
            tblItemName.Text = itemName;
            tblItemName.FontSize = 20;
            tblItemName.Foreground = new SolidColorBrush(Colors.White);
            cvsItem.Children.Add(tblItemName);
            tblItemName.MouseDown += new MouseButtonEventHandler(tblItemName_MouseDown);

            //Create delete button
            btnDelete.Height = 25;
            btnDelete.Width = 100;
            btnDelete.Content = "Delete";
            btnDelete.Margin = new Thickness(wndMain.ActualWidth - 460, 10, 0, 0);
            btnDelete.Click += new RoutedEventHandler(btnDelete_Click);
            cvsItem.Children.Add(btnDelete);

            //Create item name save button
            btnSaveItemName.Height = 25;
            btnSaveItemName.Width = 100;
            btnSaveItemName.Content = "Confirm";
            btnSaveItemName.Margin = new Thickness(370, 10, 0, 0);
            btnSaveItemName.Click += new RoutedEventHandler(btnSaveItemName_Click);
            cvsItem.Children.Add(btnSaveItemName);
            btnSaveItemName.Visibility = Visibility.Hidden;

            //Create item name textbox
            tbItemName.Width = 350;
            tbItemName.Height = 25;
            tbItemName.FontSize = 18;
            tbItemName.Margin = new Thickness(10, 10, 0, 0);
            tbItemName.Visibility = Visibility.Hidden;
            cvsItem.Children.Add(tbItemName);

            //Create item NBT save button
            btnSaveItemNBT.Height = 25;
            btnSaveItemNBT.Width = 100;
            btnSaveItemNBT.Content = "Confirm";
            btnSaveItemNBT.Margin = new Thickness(859, 10, 0, 0);
            btnSaveItemNBT.Click += new RoutedEventHandler(btnSaveItemNBT_Click);
            cvsItem.Children.Add(btnSaveItemNBT);
            btnSaveItemNBT.Visibility = Visibility.Hidden;

            //Create item NBT textbox
            tbItemNBT.Width = 350;
            tbItemNBT.Height = 25;
            tbItemNBT.FontSize = 18;
            tbItemNBT.Margin = new Thickness(500, 10, 0, 0);
            tbItemNBT.Visibility = Visibility.Hidden;
            cvsItem.Children.Add(tbItemNBT);

            //Create item NBT text
            if (itemNBT != "none")
            {
                tblItemNBT.Margin = new Thickness(500, 10, 0, 0);
                tblItemNBT.Text = string.Format("NBT: {0}", itemNBT);
                tblItemNBT.FontSize = 20;
                tblItemNBT.Foreground = new SolidColorBrush(Colors.White);
                cvsItem.Children.Add(tblItemNBT);
                tblItemNBT.MouseDown += new MouseButtonEventHandler(tblItemNBT_MouseDown);
            }

            //Create entry index
            tblEntryIndex.Margin = new Thickness(wndMain.ActualWidth - 345, 10, 0, 0);
            tblEntryIndex.Text = (itemIndex + 1).ToString();
            tblEntryIndex.TextAlignment = TextAlignment.Center;
            tblEntryIndex.FontSize = 20;
            tblEntryIndex.FontWeight = FontWeights.DemiBold;
            tblEntryIndex.Foreground = new SolidColorBrush(Colors.White);
            cvsItem.Children.Add(tblEntryIndex);

            //Create Indicator
            tblIndicator.Margin = new Thickness(wndMain.ActualWidth - 490, 5, 0, 0);
            tblIndicator.FontWeight = FontWeights.Bold;
            tblIndicator.FontSize = 24;
            cvsItem.Children.Add(tblIndicator);
        }

        //-- Event Handlers --//

        private void btnDelete_Click(object sender, RoutedEventArgs e)
        {
            if (isDeleted == false) //Set state to deleted
            {
                isModified = true;
                isDeleted = true;
                btnDelete.Content = "Undo deletion";
                SetIndicatorState();
            }
            else if (isDeleted == true) //If the item has been deleted, set state to undeleted
            {
                //Check if the item has been modified in some other way before setting the modified state to false
                if (itemName == newName)
                {
                    isModified = false;
                }
                isDeleted = false;
                btnDelete.Content = "Delete";
                SetIndicatorState();
            }

        }

        private void btnSaveItemName_Click(object sender, RoutedEventArgs e)
        {
            //Hide the controls for editing and show the item name
            tbItemName.Visibility = Visibility.Hidden;
            btnSaveItemName.Visibility = Visibility.Hidden;
            tblItemName.Visibility = Visibility.Visible;
            newName = tbItemName.Text;
            tblItemName.Text = newName;

            //Check if the item name has been changed and change modified state
            if (newName != itemName)
            {
                isModified = true;
                SetIndicatorState();
            }
            else
            {
                if (isDeleted == false && itemNBT == newNBT)
                {
                    isModified = false;
                }
                SetIndicatorState();
            }
        }

        private void btnSaveItemNBT_Click(object sender, RoutedEventArgs e)
        {
            //Hide the controls for editing and show the item NBT
            tbItemNBT.Visibility = Visibility.Hidden;
            btnSaveItemNBT.Visibility = Visibility.Hidden;
            tblItemNBT.Visibility = Visibility.Visible;
            newNBT = tbItemNBT.Text;
            tblItemNBT.Text = string.Format("NBT: {0}", newNBT);

            //Check if the item NBT has been changed and change modified state
            if (newNBT != itemNBT)
            {
                isModified = true;
                SetIndicatorState();
            }
            else
            {
                if (isDeleted == false && itemName == newName)
                {
                    isModified = false;
                }
                SetIndicatorState();
            }
        }

        private void tblItemName_MouseDown(object sender, MouseEventArgs e)
        {
            //Show the controls for editing and hide the original name
            tbItemName.Visibility = Visibility.Visible;
            btnSaveItemName.Visibility = Visibility.Visible;
            tblItemName.Visibility = Visibility.Hidden;
            tbItemName.Text = tblItemName.Text;
        }

        private void tblItemNBT_MouseDown(object sender, MouseEventArgs e)
        {
            //Show the controls for editing and hide the original NBT
            tbItemNBT.Visibility = Visibility.Visible;
            btnSaveItemNBT.Visibility = Visibility.Visible;
            tblItemNBT.Visibility = Visibility.Hidden;
            tbItemNBT.Text = tblItemNBT.Text.Replace("NBT: ", "");
        }

        //-- Custom Methods --//

        private void SetIndicatorState() { 
        
            if(isModified == true && isDeleted == true)
            {
                //Item deleted, show indicator
                tblIndicator.Visibility = Visibility.Visible;
                tblIndicator.Text = "X";
                tblIndicator.Foreground = new SolidColorBrush(Colors.Red);
            }
            else if (isModified == true && isDeleted == false)
            {
                //Item modified, show indicator
                tblIndicator.Visibility = Visibility.Visible;
                tblIndicator.Text = "#";
                tblIndicator.Foreground = new SolidColorBrush(Colors.LightBlue);
            }
            else if(isModified == false && isDeleted == false)
            {
                //No changes, hide indicator
                tblIndicator.Visibility = Visibility.Hidden;
            }
            else
            {
                //Invalid state provided, hide indicator
                tblIndicator.Visibility = Visibility.Hidden;
            }    
        }
    }
}
Seeloewen
  • 31
  • 7
  • 1
    Instead of loading _all_ the lines into an array and then filtering them, why not read lines one by one using `File.ReadLines` and filter them as you go? You'd probably save a bunch of memory that way. The garbage collector doesn't necessarily free memory as soon as it's unreachable, so you can't just assume that an increase in memory usage is a memory leak. You should use a memory profiler. – Etienne de Martel Aug 15 '23 at 04:14
  • 3
    As for another improvement, you should use WPF styles and templates to actually control the look of your items, not build them in code. Then you can simply bind the list of items to something like a ListBox and use data virtualisation to get a dramatic change in performance. Your entire `itemEntry` class should be XAML, not C#. – Etienne de Martel Aug 15 '23 at 04:17
  • See [Data Templating Overview](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/data/data-templating-overview?view=netframeworkdesktop-4.8). – Clemens Aug 15 '23 at 04:32
  • 4
    Do not say "memory leak" before you have done memory profiling. A rising memory usage can simply mean the GC has not felt the need for a gen 2 collection. – JonasH Aug 15 '23 at 06:38
  • All the comments so far have been good. Some things you should worry about: Check for any _Disposable_ objects (instances of types that implement `IDisposable`) and make sure you Dispose them. Check to make sure that you aren't keeping references to objects you don't need any more. Measure memory use using _Performance Monitor_ (aka _PerfMon_) and the _.NET Memory Counters_. They accurately measure managed memory use and let you see the effects of the GC – Flydog57 Aug 15 '23 at 18:33

0 Answers0