I was going to share this anyway after getting it to work but hit a road-block here somewhere and couldn't figure out why it never triggered an event as intended. I promise 5 stars to anyone that can get it to work. It should post offending process to the output window as well as log the event. HINT: Triggering should occur at line 242 (I think).
Full disclosure... This is the product of 2 days working with ChatGPT to develop the following code:
Code: Select all
; ==================================================
; Memory Leak Detector Settings Window (PureBasic)
; ==================================================
; --- Globals for detector configuration ---
Global SnapshotInterval = 3000 ; ms
Global MinGrowthMB = 3 ; MB
Global NumSnapshotsToCheck = 3 ; consecutive snapshots to check
Global GrowthCountThreshold = 3 ; hits before logging
Global StartupGraceSnapshots = 3 ; ignore first N snapshots
Global NoGrowthResetSamples = 3 ; reset hits if no growth over this many snapshots
;--- Global Declarations ---
Global NewList CurrentProcs.s() ; List of current process snapshots
Global NewMap ProcHistory.s() ; Map: process name → snapshot history
Global NewMap PreviousMemory.q() ; process → last observed memory in bytes
Global NewMap GrowthHits.i() ; process → hit counter
Global NewMap NoGrowthCount.i() ; process → no-growth counter
; ; --- Gadget IDs ---
#GID_Text_SnapshotInterval = 11
#GID_Text_MinGrowthMB = 12
#GID_Text_GrowthCountThreshold = 13
#GID_Text_StartupGraceSnapshots = 14
#GID_Text_NoGrowthResetSamples = 15
#GID_Text_NumSnapshotsToCheck = 16
#GID_Spin_SnapshotInterval = 101
#GID_Spin_MinGrowthMB = 102
#GID_Spin_GrowthCountThreshold = 103
#GID_Spin_StartupGraceSnapshots = 104
#GID_Spin_NoGrowthResetSamples = 105
#GID_Spin_NumSnapshotsToCheck = 110
#GID_ApplyButton = 201
#GID_CancelButton = 202
#GID_HelpButton = 203
; --- Procedure: Capture current processes ---
Procedure CaptureProcesses()
Program$ = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
param$ = "-NoProfile -Command " + Chr(34) + "Get-Process | ForEach-Object { $_.Name + ' ' + [math]::Round($_.WS/1MB,1) }" + Chr(34)
ClearList(CurrentProcs.s())
EXE = RunProgram(Program$, param$, "", #PB_Program_Open | #PB_Program_Read | #PB_Program_Hide)
If EXE
While ProgramRunning(EXE)
If AvailableProgramOutput(EXE)
OutputLine$ = Trim(ReadProgramString(EXE))
If OutputLine$ <> ""
AddElement(CurrentProcs.s())
CurrentProcs.s() = OutputLine$
; Debug OutputLine$
EndIf
EndIf
Wend
CloseProgram(EXE)
EndIf
EndProcedure
; --- Open Settings Window ---
If OpenWindow(0, 100, 100, 320, 250, "Leak Detector Settings", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
; Labels
TextGadget(#GID_Text_SnapshotInterval, 40, 10, 180, 20, "Snapshot Interval (MilliSeconds):")
TextGadget(#GID_Text_MinGrowthMB, 10, 40, 210, 20, "Minimum Growth (MegaBytes) / sample:")
TextGadget(#GID_Text_StartupGraceSnapshots, 10, 70, 180, 20, "Startup Grace Snapshots:")
TextGadget(#GID_Text_NoGrowthResetSamples, 10, 100, 180, 20, "No-Growth Reset Samples:")
TextGadget(#GID_Text_NumSnapshotsToCheck, 10, 130, 200, 20, "Samples to check before loggiong:")
TextGadget(#GID_Text_GrowthCountThreshold, 10, 160, 180, 20, "Growth Count Threshold:")
; SpinGadgets with defaults (all values inside min/max)
SpinGadget(#GID_Spin_SnapshotInterval, 200, 10, 100, 20, 1000, 60000,#PB_Spin_Numeric)
SpinGadget(#GID_Spin_MinGrowthMB, 200, 40, 100, 20, 1, 100,#PB_Spin_Numeric)
SpinGadget(#GID_Spin_StartupGraceSnapshots, 200, 70, 100, 20, 0, 10,#PB_Spin_Numeric)
SpinGadget(#GID_Spin_NoGrowthResetSamples, 200, 100, 100, 20, 0, 20,#PB_Spin_Numeric)
SpinGadget(#GID_Spin_NumSnapshotsToCheck, 200, 130, 100, 20, 1, 20,#PB_Spin_Numeric)
SpinGadget(#GID_Spin_GrowthCountThreshold, 200, 160, 100, 20, 1, 20,#PB_Spin_Numeric)
; Buttons
ButtonGadget(#GID_ApplyButton, 50, 220, 80, 20, "Apply")
ButtonGadget(#GID_CancelButton, 150, 220, 80, 20, "Cancel/Close")
ButtonGadget(#GID_HelpButton, 100, 180, 80, 20, "Help")
SetGadgetText(#GID_Spin_SnapshotInterval,Str(SnapshotInterval))
SetGadgetText(#GID_Spin_MinGrowthMB,Str(MinGrowthMB))
SetGadgetText(#GID_Spin_GrowthCountThreshold,Str(GrowthCountThreshold))
SetGadgetText(#GID_Spin_StartupGraceSnapshots,Str(StartupGraceSnapshots))
SetGadgetText(#GID_Spin_NoGrowthResetSamples,Str(NoGrowthResetSamples))
SetGadgetState(#GID_Spin_NumSnapshotsToCheck, 3)
EndIf
Procedure ShowHelp()
helpText$ = "Memory Leak Detector Help" + #CRLF$
helpText$ + "-------------------------" + #CRLF$
helpText$ + "Snapshot Interval: Time between memory snapshots." + #CRLF$
helpText$ + "MinGrowth_MB: Minimum growth per snapshot (MB) to consider for a leak." + #CRLF$
helpText$ + "NumSnapshotsToCheck: Number of consecutive snapshots a process must grow to log as leak." + #CRLF$
helpText$ + "GrowthCountThreshold: How many times the growth must hit MinGrowthMB before logging." + #CRLF$
helpText$ + "StartupGraceSnapshots: Ignore the first N snapshots of a process after it appears." + #CRLF$
helpText$ + "NoGrowthResetSamples: Reset hit count if no growth occurs for this many snapshots." + #CRLF$
helpText$ + "NumSnapshotsToCheck - How many snapshots to examine in a row" + #CRLF$
helpText$ + "GrowthCountThreshold -How many of those snapshots need a growth ≥ MinGrowthMB To envoke logging" + #CRLF$
helpText$ + #CRLF$ + "Example:" + #CRLF$
helpText$ + "A process with memory 100 → 105 → 112 → 120 MB with MinGrowthMB=5 and NumSnapshotsToCheck=3 will be logged as leaking."
MessageRequester("Memory Leak Detector Help", helpText$, #PB_MessageRequester_Ok)
EndProcedure
CaptureProcesses()
; ==================================================
; Event Loop (your canonical style)
; ==================================================
Repeat
Event = WaitWindowEvent()
Select Event
Case #PB_Event_Gadget
Select EventGadget()
Case #GID_ApplyButton
; Read values from gadgets and update globals
SnapshotInterval = GetGadgetState(#GID_Spin_SnapshotInterval)
MinGrowthMB = GetGadgetState(#GID_Spin_MinGrowthMB)
GrowthCountThreshold = GetGadgetState(#GID_Spin_GrowthCountThreshold)
StartupGraceSnapshots = GetGadgetState(#GID_Spin_StartupGraceSnapshots)
NoGrowthResetSamples = GetGadgetState(#GID_Spin_NoGrowthResetSamples)
NumSnapshotsToCheck = GetGadgetState(#GID_Spin_NumSnapshotsToCheck)
; Feedback
Debug "Settings Applied:"
Debug "SnapshotInterval=" + Str(SnapshotInterval)
Debug "MinGrowthMB=" + Str(MinGrowthMB)
Debug "GrowthCountThreshold=" + Str(GrowthCountThreshold)
Debug "StartupGraceSnapshots=" + Str(StartupGraceSnapshots)
Debug "NoGrowthResetSamples=" + Str(NoGrowthResetSamples)
Debug "GID_Spin_NumSnapshotsToCheck=" + Str(NumSnapshotsToCheck)
Break
Case #GID_CancelButton
End
Case #GID_HelpButton
ShowHelp()
EndSelect
EndSelect
Until event = #PB_Event_CloseWindow
CloseWindow(0)
;==================================================
;PureBasic Memory Leak Detector (GUI + Logging)
;==================================================
; --- Procedure: Update ProcHistory Map ---
Procedure UpdateHistory()
ForEach CurrentProcs.s()
Name$ = StringField(CurrentProcs.s(), 1, " ")
WS_MB$ = StringField(CurrentProcs.s(), 2, " ")
If ProcHistory(Name$) = ""
ProcHistory(Name$) = WS_MB$
Else
ProcHistory(Name$) + "_" + WS_MB$ ; append new snapshot
EndIf
Next
EndProcedure
; --- Procedure: Continuous logging of growing processes ---
Procedure LogLeak(Name$, Snapshots$)
logFile$ = "MemoryLeakLog.txt"
If OpenFile(1, logFile$, #PB_File_Append)
currentTime$ = FormatDate("%yyyy-%mm-%dd %hh:%ii:%ss", Date())
WriteStringN(1, currentTime$ + " | Growing leak | " + Name$ + " | History: " + Snapshots$)
CloseFile(1)
Else
Debug "Failed to write to log file!"
EndIf
EndProcedure
Debug GetCurrentDirectory()
; --- Procedure: Detect memory leaks ---
Procedure DetectLeaks()
AddGadgetItem(0, -1, "----- Memory Leak Check -----")
ForEach ProcHistory()
If MapKey(ProcHistory()) = "LeakyApp"
Debug MapKey(ProcHistory())+ " "+ProcHistory()
Debug "Prev=" + StrD(PreviousVal.d) + " Curr=" + StrD(CurrentVal.d) + " Diff=" + StrD(CurrentVal.d - PreviousVal.d)
EndIf
Name$ = MapKey(ProcHistory())
Snapshots$ = ProcHistory()
NumFields = CountString(Snapshots$, "_") + 1
; --- Startup grace + minimum history ---
If NumFields <= StartupGraceSnapshots
Continue
EndIf
If NumFields < NumSnapshotsToCheck
Continue
EndIf
; --- Examine last NumSnapshotsToCheck samples ---
StartIndex = NumFields - NumSnapshotsToCheck + 1
PreviousVal.d = ValD(StringField(Snapshots$, StartIndex, "_"))
HitsThisPass = 0
GrowthSeen = #False
i = StartIndex + 1
While i <= NumFields
CurrentVal.d = ValD(StringField(Snapshots$, i, "_"))
If (CurrentVal.d - PreviousVal.d) >= MinGrowthMB
HitsThisPass + 1
GrowthSeen = #True
EndIf
PreviousVal.d = CurrentVal.d
i + 1
Wend
; --- Update counters ---
If GrowthSeen
; GrowthHits(Name$) + HitsThisPass
GrowthHits(Name$) + 1
NoGrowthCount(Name$) = 0
Else
NoGrowthCount(Name$) + 1
EndIf
; --- Reset if no growth for too long ---
If NoGrowthCount(Name$) >= NoGrowthResetSamples
GrowthHits(Name$) = 0
NoGrowthCount(Name$) = 0
EndIf
; --- Log only after sustained growth ---
If GrowthHits(Name$) >= GrowthCountThreshold
Debug "***** TRIGGER REACHED *****"
line$ = "Memory leak detected: " + Name$ + " | History: " + Snapshots$
AddGadgetItem(0, -1, line$)
Debug line$
LogLeak(Name$, Snapshots$)
; keep logging if it continues
GrowthHits(Name$) = 0
EndIf
Next
EndProcedure
; --- Open GUI ---
If OpenWindow(0, 0, 0, 400, 400, "Memory Leak Detector", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
EditorGadget(0, 8, 8, 384, 384, #PB_Editor_ReadOnly | #PB_Editor_WordWrap)
AddWindowTimer(0, 123, SnapshotInterval)
AddGadgetItem(0, -1, "Memory Leak Detector Started.")
EndIf
; --- Main Event Loop ---
Repeat
event = WaitWindowEvent()
Select event
Case #PB_Event_CloseWindow
End
Case #PB_Event_Timer
If EventTimer() = 123
CaptureProcesses()
UpdateHistory()
DetectLeaks()
EndIf
EndSelect
ForEver
Code: Select all
MessageRequester("Leaking Pending", "Press Enter to begin leaking.", #MB_OK | #MB_SYSTEMMODAL)
Procedure LeakyFunction()
Static fails
Protected ptr
ptr = AllocateMemory(3 * 1024 * 1024) ; exactly MinGrowthMB
If ptr = 0
fails + 1
If fails = 3
MessageRequester("Allocate Failed", "failed to allocate mem 3 times.", #MB_OK | #MB_SYSTEMMODAL)
EndIf
ProcedureReturn
EndIf
; Force OS-visible commit
FillMemory(ptr, 3 * 1024 * 1024, 1)
EndProcedure
; Run longer than minimum to be safe
For i = 1 To 10
LeakyFunction()
Delay(3000) ; match SnapshotInterval
Next
Delay(12000)
MessageRequester("Leaking Ceased", "leaking has concluded.", #MB_OK | #MB_SYSTEMMODAL)
End
And remember ... 5 stars to the one who gets it posting and logging offenders. THANKS!!!
[EDIT 02/10/26] Sergey found the Problem so WE fixed it by adding line 8 which I have done above, AND 5 stars go to Sergey!!


