Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Auto-scrolling text box uses more memory than expected

I have an application that logs messages to the screen using a TextBox. The update function uses some Win32 functions to ensure that the box automatically scrolls to the end unless the user is viewing another line. Here is the update function:

private bool logToScreen = true;

// Constants for extern calls to various scrollbar functions
private const int SB_HORZ = 0x0;
private const int SB_VERT = 0x1;
private const int WM_HSCROLL = 0x114;
private const int WM_VSCROLL = 0x115;
private const int SB_THUMBPOSITION = 4;
private const int SB_BOTTOM = 7;
private const int SB_OFFSET = 13;

[DllImport("user32.dll")]
static extern int SetScrollPos(IntPtr hWnd, int nBar, int nPos, bool bRedraw);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int GetScrollPos(IntPtr hWnd, int nBar);
[DllImport("user32.dll")]
private static extern bool PostMessageA(IntPtr hWnd, int nBar, int wParam, int lParam);
[DllImport("user32.dll")]
static extern bool GetScrollRange(IntPtr hWnd, int nBar, out int lpMinPos, out int lpMaxPos);

private void LogMessages(string text)
{
    if (this.logToScreen)
    {
        bool bottomFlag = false;
        int VSmin;
        int VSmax;
        int sbOffset;
        int savedVpos;
        // Make sure this is done in the UI thread
        if (this.txtBoxLogging.InvokeRequired)
        {
            this.txtBoxLogging.Invoke(new TextBoxLoggerDelegate(LogMessages), new object[] { text });
        }
        else
        {
            // Win32 magic to keep the textbox scrolling to the newest append to the textbox unless
            // the user has moved the scrollbox up
            sbOffset = (int)((this.txtBoxLogging.ClientSize.Height - SystemInformation.HorizontalScrollBarHeight) / (this.txtBoxLogging.Font.Height));
            savedVpos = GetScrollPos(this.txtBoxLogging.Handle, SB_VERT);
            GetScrollRange(this.txtBoxLogging.Handle, SB_VERT, out VSmin, out VSmax);
            if (savedVpos >= (VSmax - sbOffset - 1))
                bottomFlag = true;
            this.txtBoxLogging.AppendText(text + Environment.NewLine);
            if (bottomFlag)
            {
                GetScrollRange(this.txtBoxLogging.Handle, SB_VERT, out VSmin, out VSmax);
                savedVpos = VSmax - sbOffset;
                bottomFlag = false;
            }
            SetScrollPos(this.txtBoxLogging.Handle, SB_VERT, savedVpos, true);
            PostMessageA(this.txtBoxLogging.Handle, WM_VSCROLL, SB_THUMBPOSITION + 0x10000 * savedVpos, 0);
        }
    }
}

Now the strange thing is that the text box consumes at least double the memory that I would expect it to. For example, when there are ~1MB of messages in the TextBox, the application can consume up to 6MB of memory (in addition to what it uses when the logToScreen is set to false). The increase is always at least double what I expect, and (as in my example) sometimes more.

What is more strange is that using:

this.txtBoxLogging.Clear();
for (int i = 0; i < 3; i++)
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

Does not free the memory (in fact, it increases slightly).

Any idea where the memory is going as I'm logging these messages? I don't believe it has anything to do with the Win32 calls, but I've included it to be thorough.

EDIT:

The first couple of responses I got were related to how to track a memory leak, so I thought I should share my methodology. I used a combination of WinDbg and perfmon to track the memory use over time (from a couple hours to days). The total number of bytes on all CLR heaps does not increase by more than I expect, but the total number of private bytes steadily increases as more messages are logged. This makes WinDbg less useful, as it's tools (sos) and commands (dumpheap, gcroot, etc.) are based on .NET's managed memory.

This is likely why GC.Collect() can not help me, as it is only looking for free memory on the CLR heap. My leak appears to be in un-managed memory.

like image 484
cgyDeveloper Avatar asked Nov 16 '09 16:11

cgyDeveloper


2 Answers

How are you determining the memory usage? You'd have to watch the CLR memory usage for your application, not the memory used by the system for the whole application (you can use Perfmon for that). Perhaps you are already using the correct method of monitoring.

It seems to me that you are using StringBuilder internally. If so, that would explain the doubling of the memory, because that's the way StringBuilder works internally.

The GC.Collect() may not do anything if the references to your objects are still in scope, or if any of your code uses static variables.


EDIT:
I'll leave the above, because it may still be true, but I looked up AppendText's internals. It does not append (i.e., to a StringBuilder), instead, it sets the SelectedText property, which does not set a string but sends a Win32 message (string is cached on retrieval).

Because strings are immutable, this means, for every string, there will be three copies: one in the calling application, one in the "cache" of the base Control and one in the actual Win32 textbox control. Each character is two bytes wide. This means that any 1MB of text, will consume 6MB of memory (I know, this is a bit simplistic, but that's basically what seems happening).

EDIT 2: not sure if it'll make any change, but you can consider calling SendMessage yourself. But it sure starts to look like you'll need your own scrolling algorithm and your own owner-drawn textbox to get the memory down.

like image 131
Abel Avatar answered Oct 03 '22 17:10

Abel


Tracking the amount of memory used by an application, especially a garbage collected language, is a tricky undertaking. People often used the overall memory count for an application to determine the objects still in use (for instance via task manager). This is questionably effective for native application but will give very misleading results for managed applications.

In order to correctly determine the amount of memory used by your CLR objects, you need to use a tool that is specifically geared to measuring that. For instance, I find the best way is to use a combination of WinDbg and sos.dll to measure the current rooted objects. This will both tell you the size of your managed objects and point out what objects are actually taking up the extra memory.

Here's a nice article on the subject

  • http://blogs.msdn.com/delay/archive/2009/03/11/where-s-your-leak-at-using-windbg-sos-and-gcroot-to-diagnose-a-net-memory-leak.aspx
like image 26
JaredPar Avatar answered Oct 03 '22 19:10

JaredPar