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