Page 1 of 1

Seeming GDI Objects leak in v28

Posted: 26 Dec 2025 21:38
by rseiler
I'm currently on 28.10.0200, and I think I've been on each of the v28 versions since they've come out (even a few from the late beta period back then). I mention this because I was also on v27 (x86) in its day and I never saw the issue I'm about to describe with that version on the same Win11 system. This seems to be something about v28, and I've seen it happen with numerous versions in this line (some versions were replaced so quickly that the problem never had a chance to occur)

I normally keep the Win11 25H2 session going for at least a week at a time without rebooting. In that time, XY is never closed. This allows GDI Objects (visible in Task Manager, Details; or Process Explorer) to grow over time. It starts for XY at about 1289, which is unusual in itself (no other program is even close, and I have a lot of them), but if it stayed there or at least stayed under 10K (the hard limit, unless you edit the Registry), there wouldn't be a problem. v28 does not, however, at least for me. GDI grows for XY at a pace of about 500 every 4 hours (rate of increase may vary by usage--average use yields about this figure for me).

When it gets to 10K, you can't use it anymore. It's very hard to describe, but when you click it to use it (from its minimized state), you see a kind of fractured XY window, which makes sense given what GDI Objects are. It's not crashed, since you can close it, but this is classically what happens (with any program) in a GDI leak hitting the ceiling.

Since GDI Objects are the very definition of obscure, I have a Powershell script that shows the top processes for not only GDI but also User Obj, Threads, and Handles. This is the first thing I run when something is unexplained. To be clear, XY has NO problem with the other three, but I like to include them since they're known troublemakers in general (especially Handles).

Code: Select all

if(-not ('Win32' -as [type])){Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public static class Win32 { [DllImport("user32.dll")] public static extern int GetGuiResources(IntPtr hProcess,int uiFlags); }'}; $esc=[char]27; $red=if(Test-Path variable:PSStyle){$PSStyle.Foreground.Red}else{"$esc[31m"}; $grn=if(Test-Path variable:PSStyle){$PSStyle.Foreground.Green}else{"$esc[32m"}; $dim=if(Test-Path variable:PSStyle){$PSStyle.Foreground.BrightBlack}else{"$esc[90m"}; $rst=if(Test-Path variable:PSStyle){$PSStyle.Reset}else{"$esc[0m"}; $b=if(Test-Path variable:PSStyle){$PSStyle.Bold}else{"$esc[1m"}; $boff=if(Test-Path variable:PSStyle){$PSStyle.BoldOff}else{"$esc[22m"}; $fmt={param([double]$x) if($x -ge 1GB){"{0:N1}GB" -f ($x/1GB)} elseif($x -ge 1MB){"{0:N0}MB" -f ($x/1MB)} elseif($x -ge 1KB){"{0:N0}KB" -f ($x/1KB)} else{"{0:N0}B" -f $x}}; $p=Get-Process|ForEach-Object{ $g=$null;$u=$null;$n=$null; if($_.Id -eq 4){$n='System (ntoskrnl.exe)'} elseif($_.Id -eq 0){$n='System Idle Process'} else { try{$n=$_.MainModule.ModuleName}catch{}; if(-not $n){$n="$($_.ProcessName).exe"} }; try{$g=[Win32]::GetGuiResources($_.Handle,0)}catch{}; try{$u=[Win32]::GetGuiResources($_.Handle,1)}catch{}; [pscustomobject]@{Process=$n;Id=$_.Id;Handles=$_.HandleCount;Threads=$_.Threads.Count;GDI=$g;USER=$u} }; $rows=@($p|Sort Handles -Desc|Select -First 3|%{[pscustomobject]@{Order=1;Metric='Handles';Process=$_.Process;Count=[int]$_.Handles;Id=[int]$_.Id}})+@($p|Sort Threads -Desc|Select -First 3|%{[pscustomobject]@{Order=2;Metric='Threads';Process=$_.Process;Count=[int]$_.Threads;Id=[int]$_.Id}})+@($p|?{$_.GDI -ne $null}|Sort GDI -Desc|Select -First 3|%{[pscustomobject]@{Order=3;Metric='GDI Obj';Process=$_.Process;Count=[int]$_.GDI;Id=[int]$_.Id}})+@($p|?{$_.USER -ne $null}|Sort USER -Desc|Select -First 3|%{[pscustomobject]@{Order=4;Metric='User Obj';Process=$_.Process;Count=[int]$_.USER;Id=[int]$_.Id}}); $mw=(@('Metric')+($rows.Metric|%{$_.ToString()})|Measure-Object Length -Maximum).Maximum; $pw=(@('Process')+($rows.Process|%{$_.ToString()})|Measure-Object Length -Maximum).Maximum; $cw=(@('Count')+($rows.Count|%{$_.ToString()})|Measure-Object Length -Maximum).Maximum; $iw=(@('Id')+($rows.Id|%{$_.ToString()})|Measure-Object Length -Maximum).Maximum; $hdr=("Metric".PadRight($mw)+"  "+"Process".PadRight($pw)+"  "+"Count".PadLeft($cw)+"   "+"Id".PadLeft($iw)); $sep='-'*$hdr.Length; $out=New-Object System.Collections.Generic.List[string]; $out.Add($hdr); $out.Add($sep); $prev=$null; foreach($r in ($rows|Sort Order,@{Expression='Count';Descending=$true})){ if($prev -and $r.Order -ne $prev){$out.Add($sep)}; $prev=$r.Order; $thr=1000; switch($r.Metric){'Handles'{$thr=10000}'Threads'{$thr=500}'GDI Obj'{$thr=5000}'User Obj'{$thr=5000}}; $cnt=$r.Count.ToString().PadLeft($cw); $cntColor=$grn; if($r.Count -ge $thr){$cntColor=$red}; $cntc=$cntColor+$cnt+$rst; $idc=$dim+($r.Id.ToString().PadLeft($iw))+$rst; $out.Add($r.Metric.PadRight($mw)+"  "+$r.Process.PadRight($pw)+"  "+$cntc+"   "+$idc) }; $svchostPids=@($rows | Where-Object{$_.Process -ieq 'svchost.exe'} | Select-Object -ExpandProperty Id -Unique); if($svchostPids.Count -gt 0){ $out.Add(''); $out.Add('svchost.exe contents (top list only):'); foreach($spid in $svchostPids){ $svcs=Get-CimInstance Win32_Service -Filter ("ProcessId={0}" -f $spid) -ErrorAction SilentlyContinue | Sort-Object Name | Select-Object Name,DisplayName; if($svcs){ $out.Add(("  PID {0}: {1}" -f $spid,(($svcs|ForEach-Object{ "{0} ({1})" -f $_.Name,$_.DisplayName }) -join ', '))) } else { $out.Add(("  PID {0}: <none found>" -f $spid)) } } }; $cs=(Get-Counter '\Memory\Available Bytes','\Memory\Committed Bytes','\Memory\Commit Limit','\Memory\Cache Bytes','\Memory\Pool Paged Bytes','\Memory\Pool Nonpaged Bytes').CounterSamples; $cv={param([string]$suffix) [double]($cs|Where-Object{$_.Path -like "*$suffix"}|Select-Object -First 1 -ExpandProperty CookedValue)}; $tp=[double](Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory; $avail=& $cv '\Memory\Available Bytes'; $inuse=$tp-$avail; $comm=& $cv '\Memory\Committed Bytes'; $clim=& $cv '\Memory\Commit Limit'; $cache=& $cv '\Memory\Cache Bytes'; $pp=& $cv '\Memory\Pool Paged Bytes'; $np=& $cv '\Memory\Pool Nonpaged Bytes'; $out.Add(''); $out.Add(("Memory: "+$b+"InUse"+$boff+" $(& $fmt $inuse)  "+$b+"Avail"+$boff+" $(& $fmt $avail)")); $out.Add(("        "+$b+"Commit"+$boff+" $(& $fmt $comm)/$(& $fmt $clim)  "+$b+"Cache"+$boff+" $(& $fmt $cache)")); $out.Add(("        "+$b+"PagedPool"+$boff+" $(& $fmt $pp)  "+$b+"NonPagedPool"+$boff+" $(& $fmt $np)")); ([Environment]::NewLine + ($out -join [Environment]::NewLine) + [Environment]::NewLine)

Re: Seeming GDI Objects leak in v28

Posted: 27 Dec 2025 09:57
by admin
Confirmed! This is clearly a tB bug, probably a couple of them (the language XY64 is written in is so new, it's still in beta). I'll watch it, isolate it, report it, and wait for it. It will take time but eventually it will be fixed. Thanks for the report! :tup:

Re: Seeming GDI Objects leak in v28

Posted: 27 Dec 2025 23:27
by JohnM
Interesting...

Since I started watching it, my GDI Object count has moved from about 2500 to 3037.

Is it possible that this GDI Object leak is cause of my hangs? (see: viewtopic.php?t=29106)

Re: Seeming GDI Objects leak in v28

Posted: 27 Dec 2025 23:45
by rseiler
@admin. That's good to hear. As a test, I tried some of the things I usually do, this time while watching GDI live (in Task Manager, off to the side), but aside from opening Preferences, which takes a whopping 1K GDI (crucially, these are decremented once closing Preferences), I only saw small amounts. I don't do anything fancy at all: copies, moves, moving around directories a lot, hovering over files to get the popup info, accessing a few toolbar items like Mini Tree, hovering over the file icon to get picture or video previews. I thought those last two were bound to be it, but I don't think so.

But doing one of those, or some combination, starts the ball rolling, and at that point it becomes inevitable given enough time. The ascent can accelerate unexpectedly somehow (otherwise, I would have had to stay up all night clicking away in XY to get to 6.8K in the last 24 hours).

@JohnM: It doesn't sound like it given that you also mentioned the x86 version. Beyond that, I've never seen it hang, just become unusable because it no longer looks like itself--it can still be closed by right-clicking the taskbar icon.

Re: Seeming GDI Objects leak in v28

Posted: 27 Dec 2025 23:57
by JohnM
@JohnM: It doesn't sound like it given that you also mentioned the x86 version.
Sorry for the misunderstanding, for me it is only x64. The OP mentioned both x86 and x64. I just hijacked the topic. :whistle:

Re: Seeming GDI Objects leak in v28

Posted: 28 Dec 2025 00:10
by rseiler
Oh, I didn't even notice that you weren't the top post.

It's certainly possible then. Maybe this manifests a little differently. You'll know for sure by checking GDI the next time it happens. If it's not sitting on 9999, it's a different issue.

Speaking of which, what is the "tB issue" mentioned at the end? tB=toolbar?

Re: Seeming GDI Objects leak in v28

Posted: 28 Dec 2025 00:22
by JohnM
I will check my GDI Count next time. Thanks.

"tB" is TwinBASIC (see: https://twinbasic.com/), which is the language x64 XY is built in. For some discussion see: viewtopic.php?t=28273