GDI - AHK v2 mem usage question (or maybe memory leak on my part) Topic is solved

Get help with using AutoHotkey (v2 or newer) and its commands and hotkeys
User avatar
TheArkive
Posts: 1027
Joined: 05 Aug 2016, 08:06
Location: The Construct
Contact:

GDI - AHK v2 mem usage question (or maybe memory leak on my part)

07 Apr 2021, 08:19

I'm trying to make a GDI class lib, and I'm not sure if the following example is a memory leak, or is expected overhead.

I tried to make this cross-section as small as possible

Method #1 below simply creates / destroys 1 DC and 1 brush using DllCall().

Method #2 uses some of my class wrapper bits to catalog created objects in gdipp.ObjList (an array) for easy cleanup.
Even though the objects (ptrs) are freed with appropriate DllCall(), and the ObjList is decremented with the .Destroy() method, the mem usage seems larger than I would expect.

I'm just not sure if this is expected mem usage, or likely a memory leak?

I've done lots of checking to ensure that i am able to catch a failed delete/release of a pointer, but no such errors are thrown.

Any ideas?

Code: Select all

; AHK v2

msgbox "starting loop"
Loop 4000 {
    ; ==================================================================
    ; Method #1
    ; no wrapping - uses much less memory, but still increases mem size
    ; - from 1,912K to 2,112K
    ; Only creates and destroys 1 DC and 1 brush per iteration.
    ; Still some extra memory usage, but not as bad as below.
    ; ==================================================================
    ; hDC := DllCall("CreateCompatibleDC", "UPtr", 0)
    ; brush := DllCall("CreateSolidBrush", "UInt", 0xFF0000)
    ; old := DllCall("SelectObject", "UPtr", hDC, "UPtr", brush)
    
    ; DllCall("DeleteDC", "UPtr", hDC)
    ; DllCall("DeleteDC", "UPtr", brush)
    ; DllCall("DeleteDC", "UPtr", old)
    
    ; ==================================================================
    ; Method #2
    ; wrapping - uses over 3x as much memory -> overhead? or leak?
    ; - from 1,936K to 6,432K
    ; This catalogs every object in gdipp.ObjList (an array).
    ; Stored objects are released with proper DllCall and the array size
    ; is reduced afterwards, but memory doesn't seem to be freed.
    ; ==================================================================
    hDC2 := gdipp.CompatDC()
    brush := hDC2.CreateBrush(0xFF0000)
    
    hDC2.Destroy()
    brush.Destroy()
}


msgbox "ObjList: " gdipp.ObjList.Length "`r`nCheck memory."

; ==================================================================
; gdipp class
; ==================================================================
class gdipp {
    Static ObjList := [] ; trying to do automatic tracking of objects for easy cleanup
    
    Static __New() {
        this.LoadConstants()
    }
    Static BGR_RGB(_c) { ; this conversion works both ways, it ignores 0xFF000000
        return (_c & 0xFF)<<16 | (_c & 0xFF00) | (_c & 0xFF0000)>>16 | (_c & 0xFF000000)
    }
    Static CompatDC(in_hDC:="") {
        If (in_hDC="")
            hDC := DllCall("CreateCompatibleDC", "UPtr", 0, "UPtr")             ; mem DC
        Else If IsInteger(in_hDC)
            hDC := DllCall("CreateCompatibleDC", "UPtr", in_hDC, "UPtr")        ; DC based on input DC ptr
        Else If (IsObject(in_hDC) And in_hDC.cType = "DC")
            hDC := DllCall("CreateCompatibleDC", "UPtr", in_hDC.ptr, "UPtr")    ; DC based on input DC obj
        Else
            hDC := ""                                                           ; unsupported, throw an error
        
        If !hDC
            throw Exception("Compatible DC creation failed.`r`n`r`nA_LastError:  " A_LastError)
        
        return gdipp.DC([hDC,1])
    }
    Static ObjLookup(in_ptr) {
        For i, obj in this.ObjList
            If (in_ptr = obj.ptr)
                return obj
        return "" ; if no match
    }
    
    ; ===================================================================
    ; Base Obj
    ; ===================================================================
    class base_obj {
        __New(p*) { ; p1 = _gdipp // p2 = ptr
            this.ptr := p[2][1] ; obj pointer
            this.temp := false  ; is obj temp or no?
            
            If (this.cType = "DC") { ; GetDC = 0 // CreateCompatibleDC = 1
                (p[2][2]=0) ? (this.release := "ReleaseDC") : (p[2][2]=1) ? (this.release := "DeleteDC") : ""
                this.StretchBltMode := 3 ; 3 = ColorOnColor / 4 = Halftone - 3 is a good default for speed
            } Else If (this.cType != "DC")
                this.CurDC := 0
            
            gdipp.ObjList.Push(this)
        }
        __Delete() {
            this.Destroy()          ; both ways seem to not save as much memory as hoped
            ; this.Clean_Up()
        }
        Clean_Up(destroy:=true) {
            For i, obj in gdipp.ObjList {
                If (obj.ptr = this.ptr) {
                    (destroy) ? obj.Destroy() : ""
                    gdipp.ObjList[i] := ""
                    gdipp.ObjList.RemoveAt(i)
                    Break
                }
            }
        }
        Destroy(r1 := "") {
            If !this.ptr
                return
            
            If (this.cType = "DC") {
                r1 := DllCall(this.release, "UPtr", this.ptr)   ; ReleaseDC / DeleteDC
            } Else
                r1 := DllCall("DeleteObject", "UPtr", this.ptr)
            
            this.Clean_Up(false) ; remove obj list entry, but don't .Destroy() (circular reference)
            
            If r1<0 ; doesn't fire... all deletions success?
                msgbox "weird r1: " r1 " / type: " this.cType
            
            If (!r1)
                throw Exception("Error on obj release, or object not yet supported."
                              ,,"Obj Type: " this.cType "`r`nObj Ptr: " this.ptr)
        }
        
        cType[] {
            get => StrReplace(this.__Class,"gdipp.","")
        }
    }
    
    ; ===================================================================
    ; Brush - mostly LOGBRUSH
    ; ===================================================================
    ; Brush & Hatch Styles: https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-logbrush
    ;   - Brush: DibPattern, DibPattern8x8, DibPatternPT, Hatched, Hollow, Pattern, Pattern8x8, Solid (default)
    ;   - Hatch: BDiagonal, Cross, DiagCross, FDiagonal, Horizontal (default), Vertical
    ;   NOTE: For corresponding integer values, see constants below in .LoadConstants() method.
    class Brush extends gdipp.base_obj {
        
    }
    
    ; ===================================================================
    ; Device Context obj
    ; ===================================================================
    class DC extends gdipp.base_obj { ; maybe have param to specify BltStretchMode()
        delayMethod := "" ; for delayed drawing
        delayParams := ""
        
        CreateBrush(_ColorRef:=0x000000, _type:="solid", _hatch:="Horizontal") {
            LOGBRUSH := BufferAlloc((A_PtrSize=8)?16:12,0)
            
            BR_TYPE  := IsInteger(_type) ? _type : gdipp.BrushTypes[_type]
            COLORREF := gdipp.BGR_RGB(_ColorRef) ; reverse input RGB to BGR, leave alpha
            HS_TYPE  := IsInteger(_hatch) ? _hatch : gdipp.HatchTypes[_hatch]
            
            NumPut("UInt", BR_TYPE, "UInt", COLORREF, "UPtr", HS_TYPE, LOGBRUSH) ; HS_TYPE can also be a ptr to a packed DIB
            
            pLogbrush := DllCall("gdi32\CreateBrushIndirect", "UPtr", LOGBRUSH.ptr)
            return gdipp.Brush([pLogbrush,0])
        }
        ; SelectObject(p*) {
            ; If (p.Length = 1)
                ; DC := this, CurObj := p[1]
            ; Else If (p.Length = 2)
                ; DC := p[1], CurObj := p[2]
            ; Else If (!p.Length Or p.Length > 2)
                ; throw Exception("Invalid number of parameters.")
            
            ; If IsInteger(CurObj) {          ; stock object
                ; old_ptr := DllCall("gdi32\SelectObject", "UPtr", DC.ptr, "UPtr", in_ptr := CurObj)
            ; } Else {                        ; wrapped object
                ; CurObj.CurDC := DC          ; set obj.CurDC in the obj
                ; old_ptr := DllCall("gdi32\SelectObject", "UPtr", DC.ptr, "UPtr", in_ptr := CurObj.ptr)
            ; }
            
            ; return old_ptr
        ; }
        
        ; ====================================================================
        ; DC Properties
        ; ====================================================================
        StretchBltMode[] { ; 4 = Halftone / 3 = ColorOnColor
            set => DllCall("gdi32\SetStretchBltMode", "UPtr", this.ptr, "Int", value)
            get => DllCall("gdi32\GetStretchBltMode", "UPtr", this.ptr)
        }
    }
    
    ; =============================================================================================
    ; method for loading constants - so we can turn CaseSense off in Map()
    ; =============================================================================================
    Static LoadConstants() {
        ; Brush Types (0-3, 5-8) ; https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-logbrush
        bt := Map(), bt.CaseSense := false
          bt["Solid"]         := 0
        , bt["Hollow"]        := 1
        , bt["Hatched"]       := 2
        , bt["Pattern"]       := 3
        , bt["DibPattern"]    := 5
        , bt["DibPatternPT"]  := 6
        , bt["Pattern8x8"]    := 7 
        , bt["DibPattern8x8"] := 8
        this.BrushTypes := bt
        
        ; Hatch Styles (0-5) ; https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-logbrush
        ht := Map(), ht.CaseSense := false
          ht["Horizontal"] := 0
        , ht["Vertical"]   := 1
        , ht["FDiagonal"]  := 2
        , ht["BDiagonal"]  := 3
        , ht["Cross"]      := 4
        , ht["DiagCross"]  := 5
        this.HatchTypes := ht
        
        ; Stock Object types (1-14) ; https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-getstockobject
        so := Map(), so.CaseSense := false
          so["WhiteBrush"]        := 0
        , so["LtGrayBrush"]       := 1
        , so["GrayBrush"]         := 2
        , so["DkGrayBrush"]       := 3
        , so["BlackBrush"]        := 4
        , so["HollowBrush"]       := 5 ; a.k.a. NULL_BRUSH
        , so["WhitePen"]          := 6
        , so["BlackPen"]          := 7
        , so["NullPen"]           := 8
        , so["_Unknown_9"]        := 9 ; always returns 0 (so far)
        , so["OemFixedFont"]      := 10
        , so["AnsiFixedFont"]     := 11
        , so["AnsiVarFont"]       := 12
        , so["SystemFont"]        := 13
        , so["DefaultDeviceFont"] := 14
        , so["DefaultPallette"]   := 15
        , so["SystemFixedFont"]   := 16
        , so["DefaultGuiFont"]    := 17
        , so["DcBrush"]           := 18
        , so["DcPen"]             := 19
        , so["DcColorSpace"]      := 20 ; not documented
        , so["DcBitmap"]          := 21 ; not documented
        this.StockObjectTypes := so
        
        sop := Map(), sop.CaseSense := false ; StockObjectPtrs
        For name, val in so
            If (name != "_Unknown_9")
                sop[name] := DllCall("gdi32\GetStockObject", "Int", val)
        this.StockObjectPtrs := sop
    }
    Static GetFlag(iInput,member) { ; reverse lookup for Map() constants
        For prop, value in gdipp.%member%
            If (iInput = value)
                return prop
        return "" ; if no match
        ; throw Exception("GetFlag() method:  Value not listed.`r`n`r`nValue:  " iInput)
    }
}
swagfag
Posts: 6222
Joined: 11 Jan 2017, 17:59

Re: GDI - AHK v2 mem usage question (or maybe memory leak on my part)

07 Apr 2021, 12:29

the for loop(or __Enum() or something else related to it) internally creates an additional reference or fails to release an existing one. the bug has been introduced in a128 but i havent been keeping up with the source changes so i cant tell precisely where

Code: Select all

#Requires AutoHotkey v2.0-a127+

class MyClass
{
	__New() => MsgBox(A_ThisFunc)
	__Delete() => MsgBox(A_ThisFunc)
}

; mc := MyClass.New() ; a127
mc := MyClass() ; a128+
; mc's refcount is 1 after instantiation. this is expected

Arr := [mc] ; mc's refcount increased to 2. this is expected
mc := '' ; mc's refcount decreased to 1. this is expected

for Obj in Arr
{
	; mc's refcount increased to 2, in loop body. this is expected
}
; mc's refcount should have dropped back down to 1 after the loop. it remains pegged at 2, this is unexpected

; lose the reference to the array, causing it to destroy itself.
; mc's refcount should have then dropped to 0, causing its destructor to run.
; instead, mc's bugged refcount(2) drops to 1, without any client code holding a reference to it, thus leaking it. this is unexpected
Arr := ''

MsgBox "mc's destructor should have run by now. if it hasnt, uve observed a bug"
User avatar
TheArkive
Posts: 1027
Joined: 05 Aug 2016, 08:06
Location: The Construct
Contact:

Re: GDI - AHK v2 mem usage question (or maybe memory leak on my part)

07 Apr 2021, 14:36

Thanks @swagfag. That makes sense.

Edit: I'll play around with this and see if i can figure any kind of work around.
User avatar
TheArkive
Posts: 1027
Joined: 05 Aug 2016, 08:06
Location: The Construct
Contact:

Re: GDI - AHK v2 mem usage question (or maybe memory leak on my part)

08 Apr 2021, 09:36

Using a Loop instead of For certainly works, as long as the array is linear of course.

The only way I can think of to avoid this leak with a Map is to store the key names in a separate linear array.

I tried Array.__Enum() as well, and got the same result.
swagfag
Posts: 6222
Joined: 11 Jan 2017, 17:59

Re: GDI - AHK v2 mem usage question (or maybe memory leak on my part)

09 Apr 2021, 01:18

for code u own, u can decrement the refcount urself ObjRelease(ObjPtr(theObject))
although u should probably just wait until its fixed

Return to “Ask for Help (v2)”

Who is online

Users browsing this forum: Smile_ and 68 guests