Jump to content
aehimself

GDI object leak and overflow when TImageList is on a frame

Recommended Posts

Hello,

 

Our legacy application mainly consist of frames which are created and displayed runtime when needed. Several parts of these frames are the same, so these functionalities were created on a smaller frame which was then embedded to the main frame. So basically we have a form with a panel, we create a frame in that panel and the constructor creates an other frame, which is on the frame we are creating. The subfame has an imagelist on it.

Recently our automatic test started to fail after creating and destroying ~600 frames because of "invalid parameter" when the DFM streaming was creating the bitmap to add it to the imagelist.

When executing the same test from the Delphi 10.4.2 IDE, actually 2 error messages appear, one after the other, but at the same place: parameter incorrect and out of system resources.

 

I managed to find out that the errors start appearing when the GDI object count reaches 9999 and a new frame is about to be created.

 

If I clear all bitmaps from the imagelist I get an "Invalid imagelist" error when GDI object count reaches the limit.

If I remove the ImageList from the frame, the test finishes successfully, without GDI object count raising above 2500.

 

No matter how hard I try I can not reproduce the issue in a fresh appication even if I model the imagelist-on-a-frame-on-a-frame-in-a-panel-on-a-form layout. GDI object count simply stays at 42.

 

Now, this is the first time I heard that there is such a thing as GDI objects and that there is a limit on it, so my experience is converging towards zero.


We have DeLeaker purchased and is showing no memory leaks. There are tons and tons of GDI objects during runtime, all from the same source:

 

image.thumb.png.1d7fa571ae174a4146b6b75ca28ecad8.png

 

Do you guys have any tips on how to find out what is causing this leak?

Edited by aehimself

Share this post


Link to post

I don't think that the ImageList should be on the frame.

Anyway, is your test-project also creates the sub-frames from the constructor?

You should check where and what is calling GetDC/ReleaseDC in the VCL and if they are called in pair.

Edited by Attila Kovacs

Share this post


Link to post
2 minutes ago, Uwe Raabe said:

What is your registry value for HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\GDIProcessHandleQuota?

Could it be that it is 10000 decimal instead of 10000 hex?

It is the installation default, which is 10000 decimal. It seems irrelevant though, as if even an abandoned ImageList is on the innermost frame (all code commented out, not used at all, no images in it) will cause the application to have 7500+ GDI objects in about 20 minutes. If I completely remove the ImageList, GDI object count stays top 2500.

Share this post


Link to post
2 hours ago, Attila Kovacs said:

I don't think that the ImageList should be on the frame.

There is a combobox, which is normally pulling those images from the ImageList. I have a hunch that if I get rid of the imagelist and push those bitmaps in resources the issue is solved... it just bugs me that I don't know what is causing the mayhem now.

2 hours ago, Attila Kovacs said:

Anyway, is your test-project also creates the sub-frames from the constructor?

Of course. I tried to replicate everything as close to the real thing as possible. Btw, creating the frames from constructor means inherited... by embedded I meant to add the innermost frame to an outer one from the designer.

2 hours ago, Attila Kovacs said:

You should check where and what is calling GetDC/ReleaseDC in the VCL and if they are called in pair.

I suspect the VCL is working correctly, this is why creating and destroying my test frame 100k + times makes no difference. I'm suspecting something funky going on here... like the Z-order TTreeNode leak with styles active or some 3rd party component interference.

My knowledge is severely limited in this area though, that's why I need starting points.

I'll do a quick search to see if GetDC is called in our custom components / frame code somewhere, but I doubt. The complexity the ancient code is written is pretty basic... data storage is a TStringList 78% of the times 🙂

Share this post


Link to post
4 minutes ago, aehimself said:

It is the installation default, which is 10000 decimal.

Yes, is was MS itself making that error.

 

BTW, Windows 10 requires a reboot for the changes to get applied - and there is a similar entry under the WOW6432Node (both should match).

 

Anyway, there were several things done to fix a GDI leak and significantly reduce the overall GDI handle usage in Delphi 11.

  • Like 1

Share this post


Link to post
1 minute ago, Uwe Raabe said:

Yes, is was MS itself making that error.

 

BTW, Windows 10 requires a reboot for the changes to get applied - and there is a similar entry under the WOW6432Node (both should match).

 

Anyway, there were several things done to fix a GDI leak and significantly reduce the overall GDI handle usage in Delphi 11.

I already introduced my boss to D11 when it was released and I personally was the one who said I don't see a reason why we would need to upgrade.

Good thing is that I asked him to doublecheck our licensing and we should still be eligible. Will be a funny talk, though 🙂

 

My personal OCD still hasn't calmed down yet. How come a completely unused TImageList can cause a GDI object leak?

Or I'm just looking at the completely wrong direction. That is unfortunately a completely valid option, too.

Share this post


Link to post

I'm fine with 10000 decimal.

 

Quote

We have DeLeaker purchased and is showing no memory leaks.

Is that on the screenshot?

It's showing a bunch of HBITMAP leaks in the comctl32.dll.

TImageList is a wrapper for some windows imagelist so I'm suspecting that it's not freed correctly or created twice under the same reference etc...

I'd look for that.

 

Share this post


Link to post
Just now, Attila Kovacs said:

Is that on the screenshot?

It's showing a bunch of HBITMAP leaks in the comctl32.dll.

TImageList is a wrapper for some windows imagelist so I'm suspecting that it's not freed correctly or created twice under the same reference etc...

I'd look for that.

Yes, it is. My first idea was that the skinning of TcxPageControl caused this (we recently applied some DevExpress skins based on user input) as it was the most recent change with comctrls involvement.

My issue is that the ImageList is simply dropped on the innermost frame design-time. It should be created and freed up automatically. If there would be a bug in the Delphi wrapper, it would be visible in my test case, too.

 

The real code contains a cximageList (DevExpress version) but symptoms are exactly the same if I replace it with a simply TImageList. This is why I didn't mention in until now. I doubt it will make any difference, though.

Share this post


Link to post
7 minutes ago, aehimself said:

If there would be a bug in the Delphi wrapper, it would be visible in my test case, too.

Well, I'm not sure on that. It's harder to create the exact same environment as one would think.

You see what is leaking, you could start placing breakpoints to the constructor/destructor of the imagelists first and count them, and check who the caller is.

Share this post


Link to post
1 minute ago, Attila Kovacs said:

You see what is leaking, you could start placing breakpoints to the constructor/destructor of the imagelists first and count them, and check who the caller is.

The issue is, about 600 frames are opened and closed before the issue detectibly appears. My sanity will give up first than to count each and every TImageList and track their lifetime.

Also, any Delphi object leak would be reported by DeLeaker upon application closure. I spent more than a year to get that list clean, so no more unfreed imagelists / TBitmaps 🙂

Share this post


Link to post

The issue is always there, even at the first run. But I can't make you place breakpoints and run the app, so I can't really help further 😉

 

Share this post


Link to post
On 10/30/2021 at 1:50 AM, aehimself said:

any Delphi object leak would be reported by DeLeaker upon application closure

Something could leak without a detectable leakage.

for i in 1..MaxInt do TButton.Create(MainForm)

these buttons will be freed on form close but they obviously leak

Edited by Fr0sT.Brutal
  • Like 1

Share this post


Link to post

Not that it has any bearing on this problem, but I keep my shared image lists in a data module

  • Like 2

Share this post


Link to post
49 minutes ago, Fr0sT.Brutal said:

Something could leak without a detectable leakage.

for i in 1..MaxInt do TButton.Create(MainForm)

these buttons will be freed on form close but they obviously leak

That is true, and definitely could apply here (no memory / GDI leak upon application closure) but we are talking about auto-created components here. The imagelist is on the innermost frame, and that frame is embedded design time on the parent frame.

I’ll check the whole chain tomorrow though - if just one is created runtime it can cause this.

 

Thanks!

Share this post


Link to post
7 minutes ago, Lars Fosdal said:

Not that it has any bearing on this problem, but I keep my shared image lists in a data module

Yep, in my own projects me too. Unfortunately this application was written some 20+ years ago and contains several thousand frames and hundreds of custom components… it will take a while to check and correct all these small design mistakes :)

Share this post


Link to post
15 minutes ago, Lars Fosdal said:

Not that it has any bearing on this problem, but I keep my shared image lists in a data module

That will also save memory when multiple instances of the frame are created.

  • Like 2

Share this post


Link to post
2 hours ago, aehimself said:

That is true, and definitely could apply here (no memory / GDI leak upon application closure) but we are talking about auto-created components here.

BTW, FastMM is able to dump the whole heap and, IIRC, even produce diffs between two dumps. This could help to find lost objects

Share this post


Link to post
Quote

The issue is, about 600 frames are opened and closed before the issue detectibly appears. My sanity will give up first than to count each and every TImageList and track their lifetime.

I believe breakpoints have a "pass count", right click on the red dot to set it. It might save your sanity.

Share this post


Link to post

There was a small trick I was unaware of. The innermost frame (which contained the ImageList) was not created once, but at least 10-20 times. Then, based on some criteria only one of them was shown (it's a really bad practice in my opinion, but it is how it is).

Since the ImageList contains 10-11 images, basically what I wanted to say is... 1 main frame easily created 100-200 GDI objects (or more) just for this one ImageList. To add salt to the wound, some of these main frames are not actually closed when they should be to improve loading time upon the next time they should be shown.

 

Moving the ImageList to a separate location kept the amount of GDI objects below 2500 at all times and the test finished without any issues. At the end of the day, @Fr0sT.Brutal was right - there was no leak, there were just way too many created which would have been freed up normally once the application (frame) closes.

 

Legacy code is the best. You can learn so much just from the design issues your predecessors made 🙂 Actually, without irony this time. I had no idea that "GDI object" as a thing exists and can make your program to crash!

Share this post


Link to post

So they were leaks from the perspective of the initial test. This means that your repro's were also wrong, didn't they?

Share this post


Link to post
Just now, Attila Kovacs said:

So they were leaks from the perspective of the initial test. This means that your repro's were also wrong, didn't they?

Well, yes and no. My repro was correct - in a way as it should have been done. In real world it could be like

Repeat
 CreateInnerMostFrameWithImageList;
Until False;

and just wait until I run out of GDI objects.

 

There were no leaks, just too many created at once due to a design issue.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×