wx on MSW: leaked GDI region objects

Doug Anderson doug at vitamindinc.com
Wed Oct 24 10:05:41 PDT 2007


Hello,

I'm working on a wxPython application that can potentially have  
_lots_ of objects on the screen.  So far, wxPython has been pretty  
good at handling it.  However, we're now running into a problem on  
Windows where wxWidgets uses up an amazing number of GDI objects.  I  
have identified one big problem area in the wxWidgets source code.   
Once I get this resolved, I will see if the GDI object count is still  
out of hand.


--

Summary:
The problem appears to be that wxWidgets is "leaking" GDI objects.   
Specifically, window objects appear to be holding onto region GDI  
objects in an improper fashion.

--

System Info (for completeness--I believe it will repro on all Windows  
builds of wx):
* wxPython 2.8.6
* python 2.4.4
* Windows XP

--

Observed behavior:
* For each 'wxWindow' that is created, the GDI count is increased by  
one on its first paint.  If the window is never painted, the GDI  
count is never increased.
* The GDI count is never decremented as long as the widget is still  
valid.
* Using some tools (the GDILeaks.exe program from here: <http:// 
msdn.microsoft.com/msdnmag/issues/03/01/GDILeaks/default.aspx>), I  
identified that the new objects were regions.
* Repainting causes these regions to be freed, but new ones to be  
allocated, so the count remains the same.  Again, the GDILeaks  
program was used.

--

Steps to reproduce:
0. Install the "win32" extensions for python, since my sample code  
relies on it.
1. Run the attached code.
2. On my machine, it will come up with a bunch of messages ending  
with "MyFrame OnIdle: 26".  This means there are 26 GDI objects.
3. Click on the frame 3 times.  This will show the three "MyWin"  
objects.  Notice that we're now at 29 GDI objects.
4. Keep clicking.  This will hide and then reshow the objects.  GDI  
objects stays constant at 29 (actually, the hide increments it to 30,  
but then it stays constant).

Running with GDILeaks (mentioned above): Start GDILeaks and use the  
command line "Python C:\wherever\GDITest.py" in the GUI.

--

wxWidgets source code analysis (based on 2.8.4 source code, even  
though I'm testing on 2.8.6):

I believe that the problem is in: "bool wxWindowMSW::HandlePaint()".   
Specifically, it creates a wxRegion and then stores it in a member  
variable on this line: "m_updateRegion = wxRegion((WXHRGN)  
hRegion);".  As far as I can tell (I'm not an expert at all in the  
wxWidgets source base), this member variable is never cleared (so the  
region refcount is never decremented) until the next time the paint  
comes around.  When the next paint comes around, the old variable no  
longer has a reference, but we've created a new one to take its place.

This analysis matches the observed behavior to a tee, so I'm pretty  
confident about it.

I am not enough of an expert with wxWigets to suggest a fix (that's  
what I'm hoping one of you can do, though maybe I should be posting  
to a different forum?).  There must be a reason that the region is  
being stored.  It does appear to be used in "DoIsExposed" in wincmn.cpp.

--

Possible hacky workaround:

Putting this in my paint event seems to fix the problem.  I'm not  
suggesting it's a good idea since I don't know all of the  
consequences, but it does plug up the leak:

         updateRegion = self.GetUpdateRegion()
         updateRegion.Destroy()

--

Example program (GDITest.py):

import wx

from win32api     import GetCurrentProcess
from win32con     import GR_GDIOBJECTS
from win32process import GetGuiResources

def GdiReport(desc):
     numGdi = GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS)
     print "%s: %d" % (desc, numGdi)

class MyWin(wx.Window):
     def __init__(self, parent, pos=wx.DefaultPosition,  
size=wx.DefaultSize):
         GdiReport("MyWin init 0")
         wx.Window.__init__(self, parent, -1, pos=pos, size=size)
         GdiReport("MyWin init 1")

         self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnErase)
         self.Bind(wx.EVT_PAINT, self.OnPaint)
         self.Bind(wx.EVT_SIZE, self.OnSize)
         GdiReport("MyWin init 2")

         handle = self.GetHandle()
         GdiReport("MyWin init 3 - %d" % handle)

     def OnErase(self, event):
         GdiReport("MyWin erase 0")
         dc = event.GetDC()
         GdiReport("MyWin erase 1")
         dc.Clear()
         GdiReport("MyWin erase 2")

     def OnPaint(self, event):
         GdiReport("MyWin paint 0")
         dc = wx.PaintDC(self)
         GdiReport("MyWin paint 1")
         dc.Clear()
         GdiReport("MyWin paint 2")
         del dc
         GdiReport("MyWin paint 3")

         handle = self.GetHandle()
         GdiReport("MyWin paint 4 - %d" % handle)

         #updateRegion = self.GetUpdateRegion()
         #GdiReport("MyWin paint 5 - %d" % handle)
         #updateRegion.Destroy()
         #GdiReport("MyWin paint 6 - %d" % handle)

     def OnSize(self, event):
         GdiReport("MyWin size 0")
         event.Skip()

class MyFrame(wx.Frame):
     def __init__(self):
         GdiReport("MyFrame init 0")
         wx.Frame.__init__(self, None, -1, "Hello")

         GdiReport("MyFrame init 1")
         self.Bind(wx.EVT_PAINT, self.OnPaint)
         GdiReport("MyFrame init 2")

         wins = []
         wins.append(MyWin(self, (1,1), (2,2)))
         wins.append(MyWin(self, (11,11), (2,2)))
         wins.append(MyWin(self, (21,21), (2,2)))
         for win in wins:
             win.Hide()
         self.wins = wins

         self.Bind(wx.EVT_IDLE, self.OnIdle)
         self.Bind(wx.EVT_LEFT_UP, self.OnClick)

     def OnClick(self, event):
         for win in self.wins:
             if not win.IsShown():
                 win.Enable()
                 win.Show()
                 break
         else:
             for win in self.wins:
                 win.Hide()
         event.Skip()

     def OnIdle(self, event):
         GdiReport("MyFrame OnIdle")
         event.Skip()

     def OnPaint(self, event):
         GdiReport("MyFrame paint 0")
         dc = wx.PaintDC(self)
         GdiReport("MyFrame paint 1")
         dc.Clear()
         GdiReport("MyFrame paint 2")
         del dc
         GdiReport("MyFrame paint 3")

######################################################################## 
######
def _simpleTest():
     """Create a simple test app to see how things worked!
     """
     GdiReport("Before PySimpleApp")
     app = wx.PySimpleApp()
     GdiReport("After  PySimpleApp")

     GdiReport("Before MyFrame")
     frame = MyFrame()
     GdiReport("After  MyFrame")
     frame.Show(1)
     GdiReport("After  showing MyFrame")

     app.MainLoop()

if __name__ == '__main__':
     _simpleTest()

--

Output of example (I clicked the frame 7 times to get this).

Before PySimpleApp: 5
After  PySimpleApp: 21
Before MyFrame: 21
MyFrame init 0: 21
MyFrame init 1: 21
MyFrame init 2: 21
MyWin init 0: 21
MyWin init 1: 21
MyWin init 2: 21
MyWin init 3 - 656202: 21
MyWin init 0: 21
MyWin init 1: 21
MyWin init 2: 21
MyWin init 3 - 656234: 21
MyWin init 0: 21
MyWin init 1: 21
MyWin init 2: 21
MyWin init 3 - 590664: 21
After  MyFrame: 21
After  showing MyFrame: 22
MyFrame paint 0: 25
MyFrame paint 1: 26
MyFrame paint 2: 27
MyFrame paint 3: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyFrame OnIdle: 26
MyWin erase 0: 27
MyWin erase 1: 27
MyWin erase 2: 27
MyFrame paint 0: 27
MyFrame paint 1: 28
MyFrame paint 2: 28
MyFrame paint 3: 27
MyWin paint 0: 27
MyWin paint 1: 28
MyWin paint 2: 28
MyWin paint 3: 27
MyWin paint 4 - 656202: 27
MyFrame OnIdle: 27
MyFrame OnIdle: 27
MyWin erase 0: 28
MyWin erase 1: 28
MyWin erase 2: 28
MyFrame paint 0: 28
MyFrame paint 1: 29
MyFrame paint 2: 29
MyFrame paint 3: 28
MyWin paint 0: 28
MyWin paint 1: 29
MyWin paint 2: 29
MyWin paint 3: 28
MyWin paint 4 - 656234: 28
MyFrame OnIdle: 28
MyFrame OnIdle: 28
MyWin erase 0: 29
MyWin erase 1: 29
MyWin erase 2: 29
MyFrame paint 0: 29
MyFrame paint 1: 30
MyFrame paint 2: 30
MyFrame paint 3: 29
MyWin paint 0: 29
MyWin paint 1: 30
MyWin paint 2: 30
MyWin paint 3: 29
MyWin paint 4 - 590664: 29
MyFrame OnIdle: 29
MyFrame OnIdle: 29
MyFrame OnIdle: 29
MyFrame OnIdle: 29
MyFrame OnIdle: 29
MyFrame OnIdle: 29
MyFrame OnIdle: 29
MyFrame OnIdle: 29
MyFrame OnIdle: 29
MyFrame paint 0: 30
MyFrame paint 1: 31
MyFrame paint 2: 31
MyFrame paint 3: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyWin erase 0: 31
MyWin erase 1: 31
MyWin erase 2: 31
MyFrame paint 0: 30
MyFrame paint 1: 31
MyFrame paint 2: 31
MyFrame paint 3: 30
MyWin paint 0: 30
MyWin paint 1: 31
MyWin paint 2: 31
MyWin paint 3: 30
MyWin paint 4 - 656202: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyWin erase 0: 31
MyWin erase 1: 31
MyWin erase 2: 31
MyFrame paint 0: 30
MyFrame paint 1: 31
MyFrame paint 2: 31
MyFrame paint 3: 30
MyWin paint 0: 30
MyWin paint 1: 31
MyWin paint 2: 31
MyWin paint 3: 30
MyWin paint 4 - 656234: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyWin erase 0: 31
MyWin erase 1: 31
MyWin erase 2: 31
MyFrame paint 0: 30
MyFrame paint 1: 31
MyFrame paint 2: 31
MyFrame paint 3: 30
MyWin paint 0: 30
MyWin paint 1: 31
MyWin paint 2: 31
MyWin paint 3: 30
MyWin paint 4 - 590664: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30
MyFrame OnIdle: 30

--

My requests:

1. I'd love it if someone could fix the wxWigets source base.  That  
would probably be the most ideal for us.

2. If someone can validate that my hacky workaround (above) is not  
_too_ bad (or what the side effects are), it gives us an option to  
use it until a fix is in wxWidgets.


I'm happy to file this report in SourceForge as well.

--

If you got this far, I appreciate it.  Thanks for reading!

-Doug

-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.wxwidgets.org/pipermail/wxpython-users/attachments/20071024/b297c82d/attachment.htm


More information about the wxpython-users mailing list